Skip to content

Latte: mirror small 1D-tiled render targets back to guest memory#2007

Open
NKR0ne wants to merge 1 commit into
cemu-project:mainfrom
NKR0ne:latte-readback-small-tiled-targets
Open

Latte: mirror small 1D-tiled render targets back to guest memory#2007
NKR0ne wants to merge 1 commit into
cemu-project:mainfrom
NKR0ne:latte-readback-small-tiled-targets

Conversation

@NKR0ne

@NKR0ne NKR0ne commented Jul 25, 2026

Copy link
Copy Markdown

Problem

Disney Infinity renders the entire scene black. Audio, menus, HUD and input all work normally, and
the black scene reproduces identically on both the Vulkan and OpenGL backends.

Cause

The game implements auto-exposure on the GPU. It reduces the frame through a chain of render
targets — 1152x600 → 284x150 → 96x48 → 17x16 → 4x8 → a final 1x8 R32_FLOAT holding average
scene luminance — and then reads that value back on the CPU to compute an exposure scalar, which it
uploads as a shader uniform.

LatteTexture only sets enableReadback when tileMode == TM_LINEAR_ALIGNED. That chain is
TM_1D_TILED_THIN1, so the result was never mirrored back to guest memory. The game read stale
zeros, computed an exposure of 0, and uploaded it. Cemu then passed that zero through perfectly
correctly — the lighting shader ends in

R5i.x = floatBitsToInt(mul_nonIEEE(intBitsToFloat(R123i.y), intBitsToFloat(uf_remappedPS[13].x)));
passPixelColor0 = vec4(intBitsToFloat(R5i.x), intBitsToFloat(R5i.y), intBitsToFloat(R5i.z), intBitsToFloat(R5i.w));

so every lit pixel became black. UI is drawn after that multiply, which is why it stayed visible
and made this look like a render-target or presentation bug rather than a readback one.

How it was diagnosed

Captured with RenderDoc:

  • Pixel History on the HDR scene target shows fragments reaching it and passing every test
    (passed=1) while writing shaderOut=(0.0000, 0.0000, 0.0000, 0.0000) — so geometry rasterizes
    correctly and the pixel shader itself produces black.
  • Dumping the generated GLSL showed the single scalar multiply above.
  • Dumping the uniform block showed uf_remappedPS[13] = (0, 0, 0, 0) while every other entry was
    populated, and while the same frame's G-buffer pass had fully populated uniforms.

For completeness, these were ruled out along the way: depth-EQUAL rejection (disproved by forcing
EQUALALWAYS), texture-cache format aliasing, scissor/viewport, colour write masks, swapchain
acquisition, and shader position invariance.

Fix

Also enable readback for TM_1D_TILED_THIN1 targets of at most 64x64. Full-size render targets are
untouched, so the common case is unaffected.

The size limit is a heuristic — it's meant to cover the "GPU reduces something to a tiny surface
that the CPU then reads" pattern while avoiding readback cost on real render targets. Happy to
change the bound, key it on something other than dimensions, or narrow the condition further if
you'd prefer something more targeted.

Testing

  • Disney Infinity 1.0 (US, 0005000010132900), Vulkan, NVIDIA RTX 3080 — scene renders correctly
    with the fix, black without it. Verified on a clean build with all debug instrumentation removed.
  • Not yet tested: other titles, and the framerate impact has not been measured. Readback
    introduces a GPU→CPU sync, so I'd appreciate a second opinion on whether the 64x64 bound is
    conservative enough, and I'm glad to run any specific title you'd like checked before merge.

Titles that reduce a frame down to a tiny surface on the GPU and then read
the result back on the CPU never received that data, because enableReadback
was only set for TM_LINEAR_ALIGNED surfaces.

Disney Infinity does this for auto exposure. It reduces the frame through
1152x600 -> 284x150 -> 96x48 -> 17x16 -> 4x8 -> a final 1x8 R32_FLOAT holding
average scene luminance, then reads that value on the CPU to derive an
exposure scalar which it uploads as a shader uniform. The chain is
TM_1D_TILED_THIN1, so it was never mirrored back, the game read stale zeros
and uploaded an exposure of 0, and the lighting shader (which ends in
colour * uf_remappedPS[13].x) rendered the entire scene black. UI is drawn
after that multiply and stayed visible, so the symptom was a black scene with
working audio, menus and HUD on both the Vulkan and OpenGL backends.

Also enable readback for TM_1D_TILED_THIN1 surfaces of at most 64x64. Full
size render targets are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Exzap

Exzap commented Jul 25, 2026

Copy link
Copy Markdown
Member

Appreciate the pull requests, but they fail to meet our AI contribution guidelines

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.

2 participants