Web (HiDPI): exiting fullscreen double-counts the scale factor, doubling the surface each frame until it exceeds the WebGL2 texture limit and crashes
- 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
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