Emscripten GLUT browser resizing fix
PR has been merged!
- Build with debug info:
emcc emscripten/test/hello_world_gles.c -g -O0 -s WASM=1 -o hello_world_gles.html
-
Install C/C++ DevTools (DWARF) extension: https://developer.chrome.com/docs/devtools/wasm
-
Test CSS scaling with emrun CSS_*.html:
emrun CSS_full_scaling.html
- Run an emscripten test:
cd <emscripten dir>
python test/runner.py -v browser.test_glut_resize --browser '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'
emrun out/test/test.html
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.
Three issues were found:
-
On window resize, glutReshapeFunc is never called.
-
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.
-
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:
-
Register a new resize callback
GLUT.reshapeHandler
usingwindow.addEventListener
, replacingBrowser.resizeListeners.push
. Previous work in this area (see below) utilizedresizeListeners
, however this fix takes a different route that is self-contained and I think simpler:- Using
window.addEventListener
keeps the fix entirely withinlibglut.js
, avoiding anylibbrowser.js
changes as in previous attempts. As well,updateResizeListeners
doesn't pass CSS-scaled canvas dimensions, so changingupdateResizeListeners
implementation might be necessary and this could impact other non-GLUT clients, going beyond this GLUT-only fix. - Since
glutInit
already utilizeswindow.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.
- Using
-
Create a new resize callback function,
GLUT.reshapeHandler
, which does the following:- Updates
canvas
dimensions (viaBrowser.setCanvasSize
) tocanvas.clientWidth
andclientHeight
, so that CSS scaling is accounted for. If no CSS scaling is present,clientWidth
andclientHeight
matchcanvas.width
andheight
, so these values are safe to use in all cases, scaling or not. - After updating the canvas size, pass
clientWidth
andclientHeight
toglutReshapeFunc
. This is needed so that GLUT reshape callbacks can properly update their viewport transform by callingglViewport
with the actual canvas dimensions.
- Updates
-
At GLUT startup in
glutMainLoop
, callGLUT.reshapeHandler
instead ofglutReshapeWindow
.- 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).
- As mentioned above,
Manual testing:
- Window resizing with no CSS, CSS scaling, CSS pixel dimensions, and a mix of these for canvas width and height.
- Entering and exiting fullscreen, and loading a page while already in fullscreen.
- 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:
- 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).
- 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.
- Also added tests to assert canvas size consistency after glutMainLoop and glutReshapeWindow API calls.
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 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 (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 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 which was helpful in writing the automated tests and also to confirm that no DPI ratio work is addressed by this fix.