Skip to content

Fix window resize/fullscreen rendering, add ultrawide fill mode - #254

Open
KohlsAdrian wants to merge 1 commit into
sonicnext-dev:mainfrom
KohlsAdrian:fix/window-resize-present-scaling
Open

KohlsAdrian wants to merge 1 commit into
sonicnext-dev:mainfrom
KohlsAdrian:fix/window-resize-present-scaling

Conversation

@KohlsAdrian

@KohlsAdrian KohlsAdrian commented Sep 16, 2026

Copy link
Copy Markdown

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:

  • resizing or maximising the window,
  • toggling fullscreen (ALT+ENTER),
  • moving the window to another monitor.

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" that WindowSize, Monitor, AspectRatio, ResolutionScale and Fullscreen are all disabled behind in the options menu.

On a later size change:

  1. ComputeViewportDimensions() grew s_viewportWidth/Height to the new window size,
  2. the intermediary back buffer was recreated at that larger size,
  3. the guest still drew a launch-sized image at the origin of that larger target,
  4. and the gamma correction blit copied it 1:1 (texture.Load, black outside g_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 in app.cpp right 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.
  • Scaling blitgamma_correction_ps.metal and gamma_correction_ps.hlsl now scale the source into the destination rectangle using manual bilinear filtering, so no additional sampler binding was needed.
  • Authoritative output size on macOSGameWindow tracks SDL_GetWindowSizeInPixels on the main thread (s_pixelWidth/s_pixelHeight, updated on RESIZED, SIZE_CHANGED, DISPLAY_CHANGED and per frame). CheckSwapChain() detects output-size changes itself rather than relying on needsResize().
  • ImGui mouse mapping — input is mapped back through the scaled destination rectangle so menus stay clickable.

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 exactly 1.0 — identical 1:1 centring to before. Only the post-lock path changes.

Nothing under thirdparty/ was modified.

Feature: ultrawide fill mode

Config::AspectRatio now 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:

  • Auto (default) — fills the entire window. No letterboxing or pillarboxing, so the HUD reaches the window corners. Intended for ultrawide displays, alongside the existing UIAlignmentMode::Edge and CutsceneAspectRatio::Unlocked, which already extend the HUD to the edges and unlock in-game cutscenes.
  • Original — preserves the game's aspect ratio and letterboxes instead.

The existing option was reused rather than adding a new one, since Auto vs Original already carries this meaning, and a new setting would need localisation across all supported languages.

Limitations

  • The guest still renders at the aspect ratio it was launched with. Launching at the target resolution (for example starting in ultrawide fullscreen) renders natively and is pixel-correct; changing to a different aspect ratio at runtime is scaled, which stretches geometry in Auto mode. Original avoids the stretch at the cost of bars. Since Config::Fullscreen is persisted, toggling fullscreen and restarting yields the native path.
  • Pre-rendered videos remain letterboxed, as that is baked into the source media. In-game cutscenes are covered by CutsceneAspectRatio::Unlocked.
  • This does not implement real buffer resize; the remaining // TODO: implement buffer resize options stay disabled. Proper runtime resolution changes would still require the guest to re-create its render targets.

Testing

  • Full clean build on macOS (arm64, Metal), including regeneration of both the SPIR-V and Metal shader binaries.
  • Verified in-game that resizing, maximising, fullscreen toggling and moving between monitors all render across the whole window with a correctly centred camera.
  • Ultrawide fill mode confirmed to remove letterboxing with the HUD reaching the window edges.

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:
Screenshot 2026-09-16 at 16 55 02

Settings:
Screenshot 2026-09-16 at 16 55 42
Screenshot 2026-09-16 at 16 55 49

4K Monitor:
Screenshot 2026-09-16 at 16 56 07


This was done using Claude Opus 5 with Kiro.

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.
@KohlsAdrian

Copy link
Copy Markdown
Author

Please support this PR with comments and changes, it's a must have fix for MacOS gaming with this recomp

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant