bevyengine / bevyengine/bevy

Web (HiDPI): exiting fullscreen double-counts the scale factor, doubling the surface each frame until it exceeds the WebGL2 texture limit and crashes

Open
#24,945 1 comment 0 reactions 0 assignees View on GitHub
A-Windowing C-Bug I-Crash O-Web S-Ready-For-Implementation
Dominant language
Rust
Stars
48.2k
Forks
4.8k
Avg merge
3d 16h
Merged PRs (30d)
171

Description

## Bevy version and features

- main @ 327fa7c0326b82492559c8ffa4b3a9d7fd2c31e5

## Relevant system information

- ANGLE / WebGL 2.0, backend Gl — ANGLE (Apple, ANGLE Metal Renderer: Apple M3 Max)
- Chromium, devicePixelRatio = 2 (HiDPI / Retina)
- Not reproduced on: WebGPU, devicePixelRatio = 1, Firefox, or Safari.

## What you did

On a wasm build with a HiDPI display (devicePixelRatio = 2), toggle a window's WindowMode from Fullscreen/BorderlessFullscreen back to Windowed at runtime.
```rs
use bevy::prelude::*;
use bevy::window::{MonitorSelection, VideoModeSelection, WindowMode};

fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
mode: WindowMode::Fullscreen(
MonitorSelection::Current, VideoModeSelection::Current),
..default()
}),
..default()
}))
.add_systems(Startup, |mut c: Commands| { c.spawn(Camera2d); })
.add_systems(Update, |input: Res>, mut q: Query<&mut Window>| {
if input.just_pressed(KeyCode::KeyF) {
for mut w in &mut q {
w.mode = match w.mode {
WindowMode::Windowed => WindowMode::Fullscreen(
MonitorSelection::Current, VideoModeSelection::Current),
_ => WindowMode::Windowed,
};
}
}
})
.run();
}
```

Build for WebGL2 and serve (the crash is WebGL2-specific — the webgl2 feature is on by default; don't enable webgpu):

```bash
# one-time setup
rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cli basic-http-server

# build + generate JS bindings (replace with your crate name)
cargo build --release --target wasm32-unknown-unknown
wasm-bindgen --out-name wasm_example --out-dir web --target web \
target/wasm32-unknown-unknown/release/.wasm

# an index.html that loads web/wasm_example.js — e.g. Bevy's
# https://github.com/bevyengine/bevy/blob/main/examples/wasm/index.html
# then serve:
basic-http-server web
```

## What went wrong

The physical size doubles every frame and the surface is reconfigured each time, until it exceeds WebGL2's max_texture_size (16384) and the app quits. Deterministic across runs (condensed from debug logs):

``` bash
Window size changed from 2400x1614 to 4800x3228
Window size changed from 4800x3228 to 9600x6456
Window size changed from 9600x6456 to 19200x12912
ERROR In Surface::configure: `Surface` width and height must be within the maximum supported texture size. Requested was (19200, 12912), maximum extent is 16384.
ERROR Quitting the application due to Validation RenderError
```

## Additional information

Root cause: scale factor double-counted on web

The physical size is multiplied by devicePixelRatio twice — once by the browser (canvas backing buffer = CSS size × DPR), then again by Bevy when it feeds the surface size back to the canvas — forming a feedback loop:

1. winit reports `WindowEvent::Resized(size)`, where size is the canvas's measured device-pixel content box.
2. Bevy stores it verbatim: `react_to_resize` → `set_physical_resolution(size.width, size.height)` (`crates/bevy_winit/src/state.rs:915`).
3. 3. The renderer reads `window.resolution.physical_width()/height()` and configures the wgpu surface at that size
(`crates/bevy_render/src/view/window/mod.rs:137`), which sets the canvas backing buffer (canvas.width/height).
4. The browser re-measures a larger device-pixel content box → winit reports a size doubled again → back to step 2. Runaway ×2 per frame.

On devicePixelRatio = 1 the double-count is ×1 (no visible growth), which is why this only manifests on HiDPI.

### Not a winit bug (isolated + confirmed against winit source)
-
- A winit-only repro (no Bevy/wgpu) does not loop: exiting fullscreen resizes cleanly (2400 → 600). Adding one line that writes the reported physical size back to canvas.width/height — exactly what `Surface::configure` does — reproduces the identical cascade.
- winit 0.30.13's web backend: `set_canvas_size writes `only the CSS (logical) size, never the backing buffer; Resized is a measurement of `devicePixelContentBoxSize`, not size × dpr. So winit expects the renderer to size the backing buffer to `logical_CSS` × `devicePixelRatio` once; Bevy instead re-scales an already-physical value.

Instrumented probes confirm each Resized payload is exactly 2× the previous, with no Bevy-requested resize (request_inner_size never fired) and no scale-factor change (ScaleFactorChanged never fired; scale factor constant at 2).

### Suggested fixes

1. Root fix — compute the web surface size from logical size × scale factor exactly once, instead of writing winit's already-device-pixel Resized value back to the surface. Touches crates/bevy_render/src/view/window/mod.rs:137 and bevy_winit's react_to_resize / set_physical_resolution.
2. Defensive clamp — clamp surface width/height to `RenderAdapter::limits().max_texture_dimension_2d` before `Surface::configure` and log a warning, turning the crash into a degraded-but-alive state. (Stops the crash but not the loop.)
3. Resize-stability guard — warn/debug_assert if physical size changes on N consecutive frames with no user/OS event, to catch regressions of this functionality.

### Notes

- Discovered while verifying the unrelated web monitor-crash fix (#22792 / #23649).
- Only tested on Chromium + WebGL2 + DPR 2. Worth confirming on WebGPU, DPR = 1, Firefox, and Safari (winit's Safari resize path differs — content_rect().to_physical(scale) rather than devicePixelContentBoxSize).

Contributor guide

Open the contributing guide

Research direction

Reproduce the wasm32 WebGL2 case with Chromium at devicePixelRatio 2 using the fullscreen toggle shown in the issue. Read crates/bevy_winit/src/state.rs around react_to_resize/set_physical_resolution and crates/bevy_render/src/view/window/mod.rs around surface configuration. Done means exiting fullscreen no longer causes repeated physical-size doubling or exceeds the WebGL2 texture limit; confirm behavior across the listed browser and renderer cases.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust, wasm
Domain
computer-graphics, web-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
54/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.