~/adam.log

Hands on Rust - A1 - Fullscreen and Windows Deployment

Published 2026-08-18

Chasing a Segfault: Fullscreen Resizing and Shipping Rusty Roguelike on Windows

While getting ready to package and ship my Hands on Rust project (Rusty Roguelike, bracket-lib ~0.8.1), I ran into a crash that took a fair amount of digging to properly root-cause: maximizing the game window would silently kill the process, no panic message, nothing. Here’s the full trail — the wrong turns included — because I think the debugging process is as useful to record as the fix.

The symptom

Maximizing the game window in WSL (Ubuntu, via WSLg) caused the process to exit instantly with no output. Running with RUST_BACKTRACE=1 produced nothing either, which was the first clue this wasn’t an ordinary Rust panic.

Checking the exit code confirmed it:

$ echo $?
11

Exit code 11 under a POSIX shell means the process was killed by SIGSEGV — a segmentation fault. That rules out a Rust-level panic (which would show a backtrace) and points at something happening below the Rust code, most likely in the native graphics/windowing layer.

First (wrong) theory: console resize mode

My original BTermBuilder setup had no resize handling at all:

let context = BTermBuilder::new()
    .with_title("Rusty Roguelike")
    .with_fps_cap(FRAME_DURATION)
    .with_dimensions(CONSOLE_WIDTH, CONSOLE_HEIGHT)
    .with_tile_dimensions(TILE_SIZE, TILE_SIZE)
    .with_resource_path("resources/")
    .with_font("dungeonfont.png", 32, 32)
    .with_font("terminal8x8.png", 8, 8)
    .with_simple_console(DISPLAY_WIDTH, DISPLAY_HEIGHT, "dungeonfont.png")
    .with_simple_console_no_bg(DISPLAY_WIDTH, DISPLAY_HEIGHT, "dungeonfont.png")
    .with_simple_console_no_bg(DISPLAY_WIDTH * 2, DISPLAY_HEIGHT * 2, "terminal8x8.png")
    .build()?;

bracket-lib exposes a builder method, with_automatic_console_resize(bool), that looked relevant. The name is a little misleading, though — checking the actual source (bracket-terminal‘s initializer.rs) and doc comments confirmed what it really does:

“Enable resize changing console size, rather than scaling.”

In other words, true makes a resize event change the number of tiles in the grid to fit the new window size. It does not keep the same tile count and scale each tile larger — which is what I actually wanted (same grid, bigger pixels). That’s the default behavior when the method isn’t called at all.

Enabling with_automatic_console_resize(true) did stop the segfault — once — but introduced visibly off-center rendering, which makes sense: the console’s tile dimensions were changing size, but the game’s map/camera code was still built around a fixed DISPLAY_WIDTH/DISPLAY_HEIGHT grid.

Ruling out bracket-lib’s resize logic

To isolate the variable, I removed the resize call entirely and re-tested:

// .with_automatic_console_resize(true)  // removed
.build()?;

Result: still segfaulted, exit code 11, identical to before. So the resize mode wasn’t the deciding factor either way.

Next I tried disabling vsync, since vsync-related timing bugs are a common culprit in windowing/graphics crashes:

.with_vsync(false)

This run exited cleanly (echo $?0) after I maximized, played for a while, and closed the window myself. It looked fixed. It wasn’t — a subsequent run with the identical code and identical action (maximize) produced a different result again: the window closed on its own without me touching it, exit code 0 that time, and a third run produced exit code 104.

The real signal: non-determinism

Three different exit codes (11, 0, 104) from the exact same trigger, on the exact same code, is itself the important diagnostic. Deterministic logic bugs in application code produce the same failure every time. Getting different outcomes on repeated identical runs is the signature of memory corruption at a layer below the application — in this case, most likely the graphics driver / windowing translation layer, not bracket-lib and not my game logic.

Root cause: WSLg’s OpenGL translation layer

WSLg (the GUI subsystem for WSL2) doesn’t talk to the GPU directly — OpenGL calls get translated through a Direct3D12 compatibility layer (libd3d12core.so/libd3d12.so) to reach the Windows host’s GPU driver. Searching turned up numerous, entirely unrelated projects hitting the same failure pattern specifically on window resize under WSLg: Qt’s Vulkan examples, glxgears, the Manim animation library, and others all segfault on resize in WSL2/WSLg in ways that don’t reproduce on native Linux or native Windows. Some reports found workarounds (forcing software rendering via LIBGL_ALWAYS_SOFTWARE=1), but in my case that didn’t resolve it either — reinforcing that this is a lower-level, somewhat unpredictable compatibility bug rather than something addressable from application code.

bracket-lib’s public builder API (BTermBuilder / InitHints) doesn’t expose a way to disable window resizing outright either — InitHints only covers vsync, fullscreen, and fitscreen — so there was no clean way to sidestep the bug from within the game’s own configuration while still targeting WSL.

Resolution: it’s not a WSL problem for players

Since WSLg is a development-environment quirk and not something end users running the shipped game will ever touch, the practical path was to stop trying to fix a bug that lives outside the codebase and instead verify behavior on the actual target platform: native Windows.

Cross-compiling for Windows

From WSL, cross-compiling to a native Windows .exe needs a Windows-targeting linker toolchain (mingw-w64) plus the corresponding Rust target:

sudo apt update
sudo apt install mingw-w64
rustup target add x86_64-pc-windows-gnu

Then, from the project directory:

cargo build --release --target x86_64-pc-windows-gnu

This produces a native .exe under target/x86_64-pc-windows-gnu/release/. Since Rust statically links dependencies, the resulting executable doesn’t need any extra runtime — it just needs to sit alongside the resources/ folder (fonts, RON files, etc.) the same way the Linux build does.

The resize question resolves itself

Running the cross-compiled .exe natively on Windows: the maximize button is greyed out, and there are no resize handles on the window edges at all. bracket-lib’s native Windows window is fixed-size by default. There’s no user action available that could trigger the resize/context-recreation code path that was segfaulting under WSLg in the first place — so the crash simply isn’t reachable on the platform players will actually use.

Takeaways

  • An exit code of 11 (SIGSEGV) with no panic output points below the Rust/application layer — worth checking early rather than assuming it’s your own logic.
  • with_automatic_console_resize changes grid size on resize, not tile pixel size — the opposite of what its name might suggest. Default (unset) behavior is the one that scales tiles while keeping the grid fixed.
  • Inconsistent crash behavior (different exit codes from an identical trigger) is itself diagnostic — it points to memory corruption or a driver-level bug rather than deterministic application logic.
  • WSLg’s OpenGL-to-D3D12 translation layer has known, broad instability around window resize events across many unrelated GL applications — not unique to bracket-lib or Rust.
  • Testing on the actual deployment target (native Windows, via x86_64-pc-windows-gnu cross-compilation) resolved the question definitively: the crash doesn’t exist there, because the window isn’t resizable on that platform at all.