Skip to content

Fullscreen canvas sizing uses screen.* leading to letterboxing on macOS #26096

@kfastov

Description

@kfastov

Summary

When entering real fullscreen via emscripten_request_fullscreen_strategy, the fullscreen CSS size is derived from screen.width/height. On macOS with a notch, screen.* includes the reserved area, while the actual fullscreen viewport (innerWidth/innerHeight or visualViewport) is smaller. The canvas ends up oversized and the browser downscales it, causing letterboxing/blurriness and click coordinate mismatch.

Expected

Canvas CSS size should match the fullscreen viewport (prefer visualViewport, fallback to innerWidth/innerHeight) so aspect-fit occurs against the visible area without additional browser downscale.

Actual

Canvas CSS uses screen.*. Example from repro below: screen=1512x982 while inner=1200x905, so CSS ends up at 1309.33x982 even though the viewport is 1200x905.

Repro

  1. Build:
emcc main.c -O0 -g -sEXPORTED_FUNCTIONS=_main,_enter_fullscreen --post-js post.js -o index.html
python3 -m http.server 8000
  1. Open http://localhost:8000 in Chrome on macOS with a notch.
  2. Click the Fullscreen button.

main.c

#include <emscripten.h>
#include <emscripten/html5.h>
#include <stdio.h>
#include <string.h>

static void dump_metrics(const char *tag) {
  EM_ASM({
    var tag = UTF8ToString($0);
    var canvas = Module['canvas'];
    var rect = canvas.getBoundingClientRect();
    var vv = window.visualViewport;
    var payload = {};
    payload.screen = Array(screen.width, screen.height);
    payload.avail = Array(screen.availWidth, screen.availHeight);
    payload.inner = Array(innerWidth, innerHeight);
    payload.outer = Array(outerWidth, outerHeight);
    payload.visualViewport = vv ? Array(vv.width, vv.height, vv.offsetLeft, vv.offsetTop, vv.scale) : null;
    payload.canvasCss = Array(canvas.clientWidth, canvas.clientHeight);
    payload.canvasStyle = Array(canvas.style.width || String(), canvas.style.height || String());
    payload.canvasRect = Array(rect.width, rect.height, rect.left, rect.top);
    payload.canvasPx = Array(canvas.width, canvas.height);
    payload.dpr = devicePixelRatio;
    payload.fullscreen = !!document.fullscreenElement;
    payload.client = Array(document.documentElement.clientWidth, document.documentElement.clientHeight);
    console.log('[metrics]', tag, JSON.stringify(payload));
  }, tag);
}

static EM_BOOL fullscreen_change(int eventType, const EmscriptenFullscreenChangeEvent *e, void *userData) {
  (void)eventType;
  (void)userData;
  printf("[fschange] isFullscreen=%d element=%dx%d screen=%dx%d\n",
         e->isFullscreen, e->elementWidth, e->elementHeight, e->screenWidth, e->screenHeight);
  dump_metrics("fullscreenchange");
  return 0;
}

static void delayed_dump(void *arg) {
  (void)arg;
  dump_metrics("after request");
}

EMSCRIPTEN_KEEPALIVE void enter_fullscreen(void) {
  EmscriptenFullscreenStrategy strategy;
  memset(&strategy, 0, sizeof(strategy));
  strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_ASPECT;
  strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_NONE;
  strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT;

  dump_metrics("before request");
  EMSCRIPTEN_RESULT res = emscripten_request_fullscreen_strategy("#canvas", true, &strategy);
  printf("[request] res=%d\n", res);
  emscripten_async_call(delayed_dump, NULL, 200);
}

int main() {
  emscripten_set_canvas_element_size("#canvas", 800, 600);
  dump_metrics("startup");
  emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, NULL, true, fullscreen_change);
  puts("Click fullscreen button to enter fullscreen.");
  return 0;
}

post.js

if (typeof document !== 'undefined') {
  const btn = document.createElement('button');
  btn.id = 'fullscreen';
  btn.textContent = 'Fullscreen';
  btn.style.cssText = 'position:fixed;top:10px;left:10px;z-index:1000;';
  btn.addEventListener('click', () => {
    if (Module && Module._enter_fullscreen) {
      Module._enter_fullscreen();
    } else {
      console.warn('enter_fullscreen not exported');
    }
  });
  document.body.appendChild(btn);
}

Logs (excerpt)

[metrics] before request {"screen":[1512,982],"avail":[1512,949],"inner":[1200,818],"outer":[1200,905],"visualViewport":[1200,818,0,0,1],"canvasCss":[800,600],"canvasStyle":["",""],"canvasRect":[800,600,200,79.7265625],"canvasPx":[800,600],"dpr":2,"fullscreen":false,"client":[1200,818]}
[fschange] isFullscreen=1 element=1200x905 screen=1512x982
[metrics] fullscreenchange {"screen":[1512,982],"avail":[1512,949],"inner":[1200,905],"outer":[1200,905],"visualViewport":[1200,905,0,0,1],"canvasCss":[1200,905],"canvasStyle":["1309.33px","982px"],"canvasRect":[1200,905,0,0],"canvasPx":[800,600],"dpr":2,"fullscreen":true,"client":[1200,905]}
[metrics] after request {"screen":[1512,982],"avail":[1512,949],"inner":[1200,905],"outer":[1200,905],"visualViewport":[1200,905,0,0,1],"canvasCss":[1200,905],"canvasStyle":["1200px","900px"],"canvasRect":[1200,905,0,0],"canvasPx":[800,600],"dpr":2,"fullscreen":true,"client":[1200,905]}

Environment

  • macOS 26.2 (build 25C56), MacBook Pro (14-inch, 2021)
  • Chrome 143.0.7499.193
  • Emscripten 4.0.24-git

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions