Skip to content

MSAA fixes by Claude - #22472

Merged
hrydgard merged 5 commits into
masterfrom
msaa-fixes
Oct 9, 2026
Merged

hrydgard merged 5 commits into
masterfrom
msaa-fixes

Conversation

@hrydgard

@hrydgard hrydgard commented Oct 9, 2026

Copy link
Copy Markdown
Owner

Had it sweep the full set of framedumps and fix any MSAA issues it found.

This is all Vulkan-only, because currently only Vulkan supports MSAA.

Claude says

This PR fixes MSAA rendering bugs on Vulkan, found by rendering all the frame dumps with 4x MSAA and comparing against no MSAA. One of the fixes also helps without MSAA. MSAA is Vulkan-only, so the other backends are unaffected, apart from depth copies (see below).

Changes

  • --msaa=N for headless (0 = off, 1 = 2x, 2 = 4x, 3 = 8x), through CommandLineOptions.

  • Copy depth instead of drawing it. BlitFramebuffer moved depth with a raster draw.

    • With MSAA, that draw reads the single-sampled depth image, which is never resolved. Silent Hill: Origins resizes its light buffer mid-frame, and its depth-EQUAL light passes then drew nothing, so the flashlight was missing with MSAA.
    • The raster copy isn't exact without MSAA either. Burnout Dominator's recursive rendering (Burnout dominator have lighting issue (recursive rendering in framebuffer border) #11100) improves a lot with an image copy (numbers below).
    • Depth is now image-copied wherever the backend can copy part of a depth buffer (framebufferSeparateDepthCopySupported, so Vulkan).
  • Centroid interpolation for texture coordinates and smooth-shaded colors on Vulkan. Without it, pixels a primitive only partly covers extrapolate their UVs and sample outside the primitive's texture region. Without MSAA it makes no difference.

  • CLUT lookups of multisampled framebuffers read one sample. A framebuffer used as CLUT indices was read resolved, so along edges the indices were averages and looked up the wrong colors. Dragon Ball Z: Tenkaichi Tag Team draws its outlines from a CLUT32 lookup of the frame, and they came out noisy and miscolored. Now each pixel's first sample is copied into a temporary buffer with texelFetch, and the lookup reads that. This adds:

    • BindFramebufferSamplesAsTexture and a framebufferSampleReadSupported capability in thin3d;
    • SamplerFlags::MULTISAMPLE;
    • SAMPLED usage on multisampled color images, and a transition back to attachment layout before they're rendered to again;
    • a DRAW2D_COPY_COLOR_SAMPLE0 Draw2D pipeline.

    A color-grade CLUT loses its edge anti-aliasing this way, which is why a few dumps get slightly worse. Exempting smooth ramps was worse overall, because the ramp detector misses some of them.

Frame dumps vs PSP

972 dumps with PSP references, Vulkan, total pixels differing from the PSP:

master this PR
No MSAA 8,123,201 7,934,288
4x MSAA 8,216,199 8,039,425

Without MSAA only the depth copy changes anything: 7 dumps improve and none get worse.

Dump master this PR
Burnout Dominator recursive rendering (#11100) 112,157 6,052
Burnout Dominator recursive rendering (#11100, 2nd dump) 116,766 34,467

With 4x MSAA:

  • The Silent Hill: Origins flashlight dump now matches its no-MSAA render apart from edge pixels: 473 px differ, down from 18,718.
  • The Dragon Ball Z outline mask matches the no-MSAA mask.

All 999 dumps render with MSAA without errors.

What's left with MSAA is expected anti-aliasing: geometry edges, and full-screen quads that stop a fraction of a pixel short of the screen edge (Metal Slug XX).

Unit tests and test.py -g --graphics=software pass.

🤖 Generated with Claude Code

@hrydgard hrydgard added this to the v1.21 milestone Oct 9, 2026
@hrydgard hrydgard added Vulkan GE emulation Backend-independent GPU issues labels Oct 9, 2026
hrydgard and others added 5 commits October 9, 2026 15:49
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BlitFramebuffer drew depth with a raster copy. With MSAA that reads the
single-sampled depth, which is never resolved: Silent Hill: Origins resizes
its light buffer mid-frame, and with garbage depth its depth-EQUAL light
passes drew nothing, so the flashlight was missing. The raster copy isn't
exact without MSAA either: Burnout Dominator's recursive rendering (#11100)
goes from 112k to 6k pixels off the PSP with a copy. Now depth is copied
wherever the backend can copy just part of it (Vulkan).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With MSAA, a pixel that a primitive only partly covers otherwise
extrapolates them to the pixel center, outside the primitive, and samples
textures outside its UV range. Without MSAA nothing changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st sample

With MSAA, a framebuffer textured as CLUT indices was read resolved, so the
indices along edges were averages of the samples and looked up wrong colors.
Dragon Ball Z: Tenkaichi Tag Team draws its outlines from a CLUT32 lookup of
its framebuffer, and they came out noisy and miscolored. Copy each pixel's
first sample with texelFetch into a temporary buffer and look up that.

This needs the multisampled color images to be sampled, and transitioned
back to attachment layout before rendering to them again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ShiftDepthForOffsetRendering copied through a single-sampled temp buffer, so
with MSAA CopyDepthRect fell back to a depth blit, which Vulkan can't do with
multisampled images (PerformBlit asserts). Give the temp buffer the same
samples and copy; multisampled depth is never blitted or drawn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hrydgard
hrydgard merged commit 7ec4d02 into master Oct 9, 2026
27 checks passed
@hrydgard
hrydgard deleted the msaa-fixes branch October 9, 2026 14:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

GE emulation Backend-independent GPU issues Vulkan

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant