Skip to content

Commit e77e985

Browse files
erik-larsensbc100
andauthored
Fix GLUT window resizing when CSS scaling (#24699)
When using GLUT and the window is resized with the canvas dependent upon it due to CSS scaling, the result is a stretched canvas with blocky pixel scaling. Here's a CSS scaling example: <style> canvas { position: fixed; width: 75%; height: 75%; } </style> <canvas id="canvas"></canvas> While position fixed isn't strictly necessary, it more readily shows the problem as it makes the canvas size directly dependent upon the browser window. For comparison, SDL behaves properly in this same scenario. ## Fix Three issues were found: 1. On window resize, glutReshapeFunc is never called. 2. Even with glutReshapeFunc working, the dimensions passed to it do not include CSS scaling. Specifically, the canvas width and height are never updated with the canvas clientWidth and clientHeight, which does include scaling. 3. On GLUT program startup, glutMainLoop calls glutReshapeWindow, which is slightly problematic for the case of loading the page while already in fullscreen. This is a problem because, while an initial resize is needed on startup, glutReshapeWindow also forces an exit from fullscreen mode. Here are the proposed fixes: 1. Register a new resize callback `GLUT.reshapeHandler` using `window.addEventListener`, replacing `Browser.resizeListeners.push`. Previous work in this area (see below) utilized `resizeListeners`, however this fix takes a different route that is self-contained and I think simpler: - Using `window.addEventListener` keeps the fix entirely within `libglut.js`, avoiding any `libbrowser.js` changes as in previous attempts. As well, `updateResizeListeners` doesn't pass CSS-scaled canvas dimensions, so changing `updateResizeListeners` implementation might be necessary and this could impact other non-GLUT clients, going beyond this GLUT-only fix. - Since `glutInit` already utilizes `window.addEventListener` for all other event handling, doing the same for the resize event seems consistent and simpler, as it avoids mixing event handling methods for GLUT. 2. Create a new resize callback function, `GLUT.reshapeHandler`, which does the following: - Updates `canvas` dimensions (via `Browser.setCanvasSize`) to `canvas.clientWidth` and `clientHeight`, so that CSS scaling is accounted for. If no CSS scaling is present, `clientWidth` and `clientHeight` match `canvas.width` and `height`, so these values are safe to use in all cases, scaling or not. - After updating the canvas size, pass `clientWidth` and `clientHeight` to `glutReshapeFunc`. This is needed so that GLUT reshape callbacks can properly update their viewport transform by calling `glViewport` with the actual canvas dimensions. 3. At GLUT startup in `glutMainLoop`, call `GLUT.reshapeHandler` instead of `glutReshapeWindow`. - As mentioned above, `glutReshapeWindow` has an unwanted side effect of always forcing an exit from fullscreen (and this is by design, according to the [GLUT API](https://www.opengl.org/resources/libraries/glut/spec3/node23.html)). ## Testing Manual testing: 1. Window resizing with no CSS, CSS scaling, CSS pixel dimensions, and a mix of these for canvas width and height. 2. Entering and exiting fullscreen, and loading a page while already in fullscreen. 3. No DPI testing done (window.devicePixelRatio != 1), as GLUT is not currently DPI aware and this fix does not address it. I did confirm on Retina Mac that this fix doesn't make this issue any better or worse. Automated tests: 1. Added test/browser/test_glut_resize.c, with tests to assert canvas size matches target size under various scenarios (no CSS, CSS scaling, CSS pixel dimensions, and a mix of these), as well as canvas size always matching canvas client size (clientWidth and clientHeight). 2. Since programmatic browser window resizing is not allowed for security reasons, these tests dispatch a resize event after each CSS style change as a workaround. 3. Also added tests to assert canvas size consistency after glutMainLoop and glutReshapeWindow API calls. ## Related work All the previous work in this area worked toward enabling GLUT resize callbacks via Emscripten’s built-in event handling (specifically `Browser.resizeListeners` and `updateResizeListeners`). As mentioned above, this fix takes a different approach that is entirely self-contained within `libglut.js`. This 2013 [commit](6d6490e) added `GLUT.reshapeFunc` to `Browser.resizeListeners`, presumably to handle window resizing. However there is no test code with that commit, and as of current Emscripten, `updateResizeListeners()` is never called on window resizing with GLUT programs, so this code is currently a no-op. [Issue 7133](#7133) (that I logged in 2018, hi again!) got part of the way on a fix, but used `glutReshapeWindow` which has the previously mentioned side effect of exiting fullscreen. This was closed unresolved. [PR 9835](#9835) proposed a fix for 7133. Also closed unresolved, this fix involved modifying `libbrowser.js` in order to get resize callbacks to GLUT via `resizeListeners`. While this got resize callbacks working, in my testing it didn’t pass CSS-scaled canvas size in the callback (the all-important clientWidth and clientHeight). I also looked at how SDL handles resizing, which uses `resizeEventListeners`, but decided the more straightforward fix was to use `addEventListener`. Last, I looked at [GLFW CSS scaling test](https://github.com/emscripten-core/emscripten/blob/main/test/browser/test_glfw3_css_scaling.c) which was helpful in writing the automated tests and also to confirm that no DPI ratio work is addressed by this fix. Fixes: #7133 --------- Co-authored-by: Sam Clegg <[email protected]>
1 parent 94d1e77 commit e77e985

File tree

4 files changed

+229
-5
lines changed

4 files changed

+229
-5
lines changed

src/lib/libglut.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,13 @@ var LibraryGLUT = {
298298
{{{ makeDynCall('vii', 'GLUT.reshapeFunc') }}}(width, height);
299299
}
300300
_glutPostRedisplay();
301+
},
302+
303+
// Resize callback stage 1: update canvas by setCanvasSize, which notifies resizeListeners including GLUT.reshapeFunc
304+
onResize: () => {
305+
// Update canvas size to clientWidth and clientHeight, which include CSS scaling
306+
var canvas = Browser.getCanvas();
307+
Browser.setCanvasSize(canvas.clientWidth, canvas.clientHeight, /*noUpdates*/false);
301308
}
302309
},
303310

@@ -336,6 +343,10 @@ var LibraryGLUT = {
336343
// Firefox
337344
window.addEventListener('DOMMouseScroll', GLUT.onMouseWheel, true);
338345

346+
// Resize callback stage 1: update canvas which notifies resizeListeners
347+
window.addEventListener('resize', GLUT.onResize, true);
348+
349+
// Resize callback stage 2: updateResizeListeners notifies reshapeFunc
339350
Browser.resizeListeners.push((width, height) => {
340351
if (GLUT.reshapeFunc) {
341352
{{{ makeDynCall('vii', 'GLUT.reshapeFunc') }}}(width, height);
@@ -359,6 +370,8 @@ var LibraryGLUT = {
359370
// Firefox
360371
window.removeEventListener('DOMMouseScroll', GLUT.onMouseWheel, true);
361372

373+
window.removeEventListener('resize', GLUT.onResize, true);
374+
362375
var canvas = Browser.getCanvas();
363376
canvas.width = canvas.height = 1;
364377
});
@@ -633,10 +646,10 @@ var LibraryGLUT = {
633646
},
634647

635648
glutMainLoop__proxy: 'sync',
636-
glutMainLoop__deps: ['$GLUT', 'glutReshapeWindow', 'glutPostRedisplay'],
649+
glutMainLoop__deps: ['$GLUT', 'glutPostRedisplay'],
637650
glutMainLoop: () => {
638-
var canvas = Browser.getCanvas();
639-
_glutReshapeWindow(canvas.width, canvas.height);
651+
// Do an initial resize, since there's no window resize event on startup
652+
GLUT.onResize();
640653
_glutPostRedisplay();
641654
throw 'unwind';
642655
},

test/browser/test_glut_resize.c

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/*
2+
* Copyright 2025 The Emscripten Authors. All rights reserved.
3+
* Emscripten is available under two separate licenses, the MIT license and the
4+
* University of Illinois/NCSA Open Source License. Both these licenses can be
5+
* found in the LICENSE file.
6+
*/
7+
8+
#include <stdio.h>
9+
#include <assert.h>
10+
#include <stdint.h>
11+
#include <emscripten/emscripten.h>
12+
#include <GL/glut.h>
13+
14+
typedef struct {
15+
int32_t width;
16+
int32_t height;
17+
} rect_size_t;
18+
19+
static rect_size_t browser_window_size = { 0, 0 };
20+
static rect_size_t glut_init_size = { 0, 0 };
21+
static rect_size_t glut_reshape_size = { 0, 0 };
22+
static rect_size_t target_size = { 0, 0 };
23+
24+
/*
25+
* Set run_async_verification to 0 for sync test cases, and 1 for async tests.
26+
*
27+
* Callback sequence for test case 1 & 2 (synchronous):
28+
* glutMainLoop -> GLUT.onSize -> Browser.setCanvasSize -> updateResizeListeners -> GLUT.reshapeFunc
29+
* glutResizeWindow -> Browser.setCanvasSize -> updateResizeListeners -> GLUT.reshapeFunc
30+
*
31+
* Callback sequence for test cases 3-5 (async):
32+
* window resize -> async update -> GLUT.onSize -> Browser.setCanvasSize -> updateResizeListeners -> GLUT.reshapeFunc
33+
*
34+
* Because window resize does not immediately call GLUT.onSize, we wait to run verification of a test until we get
35+
* confirmation in GLUT.reshapeFunc. And after verification is done, we move on to the next test.
36+
*
37+
*/
38+
static int run_async_verification = 0;
39+
40+
void print_size_test(int test_num, const char* name, rect_size_t rect_size) {
41+
printf("Test %d: %s = %d x %d\n", test_num, name, rect_size.width, rect_size.height);
42+
}
43+
44+
int equal_size(rect_size_t rect_1, rect_size_t rect_2) {
45+
return (rect_1.width == rect_2.width && rect_1.height == rect_2.height);
46+
}
47+
48+
/**
49+
* Obtain various dimensions
50+
*/
51+
EM_JS(void, get_browser_window_size, (int32_t* width, int32_t* height), {
52+
setValue(Number(width), window.innerWidth, 'i32');
53+
setValue(Number(height), window.innerHeight, 'i32');
54+
});
55+
56+
EM_JS(void, get_canvas_client_size, (int32_t* width, int32_t* height), {
57+
const canvas = Module.canvas;
58+
setValue(Number(width), canvas.clientWidth, 'i32');
59+
setValue(Number(height), canvas.clientHeight, 'i32');
60+
});
61+
62+
EM_JS(void, get_canvas_size, (int32_t* width, int32_t* height), {
63+
const canvas = Module.canvas;
64+
setValue(Number(width), canvas.width, 'i32');
65+
setValue(Number(height), canvas.height, 'i32');
66+
});
67+
68+
/**
69+
* Update canvas style with given width and height, then invoke window resize event
70+
*/
71+
EM_JS(void, test_resize_with_CSS, (const char* position, const char* width, const char* height), {
72+
const canvas = Module.canvas;
73+
canvas.style.position = UTF8ToString(Number(position));
74+
canvas.style.width = UTF8ToString(Number(width));
75+
canvas.style.height = UTF8ToString(Number(height));
76+
77+
window.dispatchEvent(new UIEvent('resize'));
78+
});
79+
80+
/**
81+
* Verify canvas and reshape callback match target size, and also that
82+
* canvas width, height matches canvas clientWidth, clientHeight
83+
*/
84+
void assert_canvas_and_target_sizes_equal() {
85+
/* verify target size match */
86+
rect_size_t canvas_size;
87+
get_canvas_size(&canvas_size.width, &canvas_size.height);
88+
assert(equal_size(canvas_size, target_size));
89+
assert(equal_size(glut_reshape_size, target_size));
90+
91+
/* verify canvas client size match */
92+
rect_size_t canvas_client_size;
93+
get_canvas_client_size(&canvas_client_size.width, &canvas_client_size.height);
94+
assert(equal_size(canvas_size, canvas_client_size));
95+
}
96+
97+
/**
98+
* Verify the result of the previous test and then run the next one
99+
*/
100+
void verify_test_and_run_next() {
101+
void run_next_test();
102+
103+
assert_canvas_and_target_sizes_equal();
104+
run_next_test();
105+
}
106+
107+
/**
108+
* Resizing tests
109+
*/
110+
void run_next_test() {
111+
static int test_num = 0;
112+
++test_num;
113+
114+
switch(test_num) {
115+
case 1: {
116+
/* startup */
117+
target_size = glut_init_size;
118+
print_size_test(test_num, "startup, no CSS: canvas == glutReshapeFunc == glutInitWindow size", target_size);
119+
verify_test_and_run_next();
120+
break;
121+
}
122+
case 2: {
123+
/* glutReshapeWindow */
124+
target_size.width = glut_init_size.width + 40;
125+
target_size.height = glut_init_size.height + 20;
126+
print_size_test(test_num, "glut reshape, no CSS: canvas == glutReshapeFunc == glutReshapeWindow size", target_size);
127+
glutReshapeWindow(target_size.width, target_size.height);
128+
verify_test_and_run_next();
129+
break;
130+
}
131+
case 3: {
132+
/* 100% scale CSS */
133+
target_size = browser_window_size;
134+
print_size_test(test_num, "100% window scale CSS: canvas == glutReshapeFunc == browser window size", target_size);
135+
run_async_verification = 1;
136+
test_resize_with_CSS("fixed", "100%", "100%"); /* fixed, so canvas is driven by window size */
137+
break;
138+
}
139+
case 4: {
140+
/* specific pixel size CSS */
141+
target_size.width = glut_init_size.width - 20;
142+
target_size.height = glut_init_size.height + 40;
143+
print_size_test(test_num, "specific pixel size CSS: canvas == glutReshapeFunc == CSS specific size", target_size);
144+
char css_width[16], css_height[16];
145+
snprintf (css_width, 16, "%dpx", target_size.width);
146+
snprintf (css_height, 16, "%dpx", target_size.height);
147+
run_async_verification = 1;
148+
test_resize_with_CSS("static", css_width, css_height); /* static, canvas is driven by CSS size */
149+
break;
150+
}
151+
case 5: {
152+
/* mix of CSS scale and pixel size */
153+
target_size.width = browser_window_size.width;
154+
target_size.height = 100;
155+
print_size_test(test_num, "100% width, 100px height CSS: canvas == glutReshapeFunc == CSS mixed size", target_size);
156+
run_async_verification = 1;
157+
test_resize_with_CSS("fixed", "100%", "100px"); /* fixed, canvas width is driven by window size */
158+
break;
159+
}
160+
default: {
161+
/* all tests complete */
162+
emscripten_force_exit(0);
163+
break;
164+
}
165+
}
166+
}
167+
168+
/**
169+
* Idle callback - start tests
170+
*/
171+
void start_tests() {
172+
glutIdleFunc(NULL);
173+
run_next_test();
174+
}
175+
176+
/**
177+
* Reshape callback - record latest size, verify and run next test if async
178+
*/
179+
void reshape(int w, int h) {
180+
glut_reshape_size.width = (int32_t)w;
181+
glut_reshape_size.height = (int32_t)h;
182+
183+
if (run_async_verification) {
184+
run_async_verification = 0; /* Only one verification per test */
185+
verify_test_and_run_next();
186+
}
187+
}
188+
189+
int main(int argc, char *argv[]) {
190+
get_browser_window_size(&browser_window_size.width, &browser_window_size.height);
191+
192+
/* Make glut initial canvas size be 1/2 of browser window */
193+
glut_init_size.width = browser_window_size.width / 2;
194+
glut_init_size.height = browser_window_size.height / 2;
195+
196+
glutInit(&argc, argv);
197+
glutInitWindowSize(glut_init_size.width, glut_init_size.height);
198+
glutInitDisplayMode(GLUT_RGB);
199+
glutCreateWindow("test_glut_resize.c");
200+
201+
/* Set up glut callback functions */
202+
glutIdleFunc(start_tests);
203+
glutReshapeFunc(reshape);
204+
glutDisplayFunc(NULL);
205+
206+
glutMainLoop();
207+
return 0;
208+
}

test/code_size/test_codesize_hello_dylink_all.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"a.out.js": 246735,
2+
"a.out.js": 246847,
33
"a.out.nodebug.wasm": 597852,
4-
"total": 844587,
4+
"total": 844699,
55
"sent": [
66
"IMG_Init",
77
"IMG_Load",

test/test_browser.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,6 +1135,9 @@ def test_glut_glutget(self):
11351135
self.btest_exit('glut_glutget.c', cflags=['-lglut', '-lGL'])
11361136
self.btest_exit('glut_glutget.c', cflags=['-lglut', '-lGL', '-DAA_ACTIVATED', '-DDEPTH_ACTIVATED', '-DSTENCIL_ACTIVATED', '-DALPHA_ACTIVATED'])
11371137

1138+
def test_glut_resize(self):
1139+
self.btest_exit('test_glut_resize.c')
1140+
11381141
def test_sdl_joystick_1(self):
11391142
# Generates events corresponding to the Working Draft of the HTML5 Gamepad API.
11401143
# http://www.w3.org/TR/2012/WD-gamepad-20120529/#gamepad-interface

0 commit comments

Comments
 (0)