Fix window resize/fullscreen rendering, add ultrawide fill mode - #254
KohlsAdrian wants to merge 3 commits into
Conversation
The guest is given its render resolution once, when it constructs its renderer, and cannot re-create its render targets at a different size afterwards (the unimplemented "buffer resize" that Fullscreen, Monitor, WindowSize, AspectRatio and ResolutionScale are all disabled behind). Growing the viewport after that point made the guest draw a launch-sized image into the corner of a larger render target, which the present blit then copied 1:1, leaving the rest of the window black and the camera visibly off-centre. This happened on any post-startup size change: resizing the window, toggling fullscreen, or moving to another monitor. Pin the viewport to the guest's resolution once it has been handed over, and scale that image to the window when presenting instead: - Add Video::LockGuestResolution(), called where the guest receives its render config, so ComputeViewportDimensions() stops resizing the viewport afterwards while still tracking the real output size. - Add Video::ComputePresentRect() and make the gamma correction blit scale the guest's image into it, with manual bilinear filtering so no additional sampler binding is required. - Track the output size from SDL's main-thread pixel size on macOS. The Metal swap chain resolves its size from a cached window query on the render thread, so needsResize() can miss resizes, fullscreen toggles and monitor changes entirely; CheckSwapChain() now detects the change itself. - Map ImGui mouse input through the scaled destination rectangle. Config::AspectRatio now selects how the image is fit to the window and is enabled in the options menu, since it no longer needs a buffer resize: Auto fills the whole window so the HUD reaches the corners (best for ultrawide, alongside UIAlignmentMode::Edge and CutsceneAspectRatio:: Unlocked), Original preserves the aspect ratio and letterboxes. Note that the guest still renders at the aspect ratio it was launched with, so launching at the target resolution renders natively while a later change to a different aspect is scaled.
Present-time scaling can only stretch or letterbox a fixed-resolution guest image, so ComputePresentRect now always preserves the guest's aspect ratio (contain fit) instead of stretching in Auto. This removes the geometry distortion that happened when the window aspect differed from the launch aspect. Because the guest builds its render targets once at launch and cannot re-create them at a new size at runtime (the unimplemented buffer resize), the only way to fill a differently-shaped window natively is to relaunch so the targets are rebuilt at the new size. In Auto mode GameWindow::MaybeRestartForAspectChange() now relaunches via App::Restart when the window aspect ratio differs from the guest's launch aspect (Config::WindowSize/Fullscreen persist across the restart). It is suppressed while initialising, loading, saving or changing display so a relaunch never interrupts that state. Original keeps a fixed 16:9 and is unaffected.
Update: no distortion in Auto + automatic re-render at the new window aspectFollow-up to feedback that What changed1.
2. So Constraints / behaviour to be aware of
Testing
|
…reen) Replace the guest-vs-output aspect comparison with a loop-safe trigger based on the render OUTPUT aspect ratio changing and settling. Comparing against the last observed output aspect (rather than the guest's fixed launch aspect) prevents the restart loop that occurred when the guest could not lock exactly to the live drawable aspect: after a relaunch the output aspect is stable, so the baseline matches and it does not fire again. A short settle delay avoids relaunching on every intermediate size during a resize drag or fullscreen transition. This also covers entering fullscreen via the macOS green title-bar button, which enters a native fullscreen Space that sets no SDL fullscreen flag but does change the drawable size: - IsFullscreen() now masks SDL_WINDOW_FULLSCREEN (covers both the borderless desktop fullscreen used by ALT+ENTER and native fullscreen). - When deciding what to persist for the relaunch, treat "the window covers the display" (width >= 98%, height >= 90% of the display bounds, allowing for the menu bar/notch inset) as fullscreen, so the relaunched instance comes back as borderless desktop fullscreen at the new aspect instead of windowed. Only applies in EAspectRatio::Auto; suppressed while initialising, loading or saving.
Update 2: loop-safe auto-restart + macOS fullscreen handlingRefines the What changed
Constraints / behaviour to be aware of
|
|
That's a lot of words you didn't write to say "I broke arbitrary aspect ratio support". This is not the resize behaviour that we are aiming to implement. There is work already being done to allow for buffer resize, which can properly provide that context to all of the pre-existing patches for aspect ratio correction. This is a problem that requires reverse-engineering the game code to fix, it's not something you can solve by throwing an LLM at it. |
The issue
Any change to the window size after startup broke rendering: the game kept drawing at its launch size in the top-left corner of the window, the rest of the window stayed black, and the 3D camera appeared off-centre relative to the window.
It reproduced on macOS/Metal by:
Launching directly at a given size was always correct — only later changes were affected.
Root cause
The guest is told its render resolution exactly once, in
Sonicteam::AppMarathon::AppMarathon(app.cpp), and it builds its render targets from that. It has no way to re-create them at a different size at runtime. This is the same unimplemented "buffer resize" thatWindowSize,Monitor,AspectRatio,ResolutionScaleandFullscreenare all disabled behind in the options menu.On a later size change:
ComputeViewportDimensions()grews_viewportWidth/Heightto the new window size,texture.Load, black outsideg_ViewportSize).Hence content in the corner, black elsewhere, and an apparently off-centre camera. ALT+ENTER and window-manager resizing bypass the disabled options, which is how the state is reached.
A second, macOS-specific problem compounded it: the Metal swap chain resolves its size from a cached window query performed on the render thread, so
needsResize()could miss the change entirely and report a stale size.What was done
Since the guest genuinely cannot re-render at a new size, its resolution is pinned and the result is scaled at present time.
Video::LockGuestResolution()— called inapp.cppright where the guest receives its render config. After this,ComputeViewportDimensions()no longer resizes the viewport, but still tracks the real output size and recomputes the aspect ratio offsets.Video::ComputePresentRect()— computes the destination rectangle for the guest's image inside the render output.gamma_correction_ps.metalandgamma_correction_ps.hlslnow scale the source into the destination rectangle using manual bilinear filtering, so no additional sampler binding was needed.GameWindowtracksSDL_GetWindowSizeInPixelson the main thread (s_pixelWidth/s_pixelHeight, updated onRESIZED,SIZE_CHANGED,DISPLAY_CHANGEDand per frame).CheckSwapChain()detects output-size changes itself rather than relying onneedsResize().Behaviour before the guest locks its resolution (installer, boot) is unchanged: in both
ComputeViewportDimensions()branches the viewport matches the output on one axis, so the fit scale is exactly1.0— identical 1:1 centring to before. Only the post-lock path changes.Nothing under
thirdparty/was modified.Feature: ultrawide fill mode
Config::AspectRationow selects how the image is fit to the window, and is enabled in the options menu because it no longer requires a buffer resize — it is applied when presenting and takes effect live:UIAlignmentMode::EdgeandCutsceneAspectRatio::Unlocked, which already extend the HUD to the edges and unlock in-game cutscenes.The existing option was reused rather than adding a new one, since
AutovsOriginalalready carries this meaning, and a new setting would need localisation across all supported languages.Limitations
Automode.Originalavoids the stretch at the cost of bars. SinceConfig::Fullscreenis persisted, toggling fullscreen and restarting yields the native path.CutsceneAspectRatio::Unlocked.// TODO: implement buffer resizeoptions stay disabled. Proper runtime resolution changes would still require the guest to re-create its render targets.Testing
Only D3D12/Vulkan were not exercised at runtime; the shared code path and the equivalent HLSL change mirror the Metal one, and the pre-lock behaviour is unchanged by construction.
Preview
Ultrawide Monitor:

Settings:


4K Monitor:

This was done using Claude Opus 5 with Kiro.