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

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>
This commit is contained in:
NKR0ne
2026-07-25 08:35:30 -04:00
parent b8f2cf4b43
commit 62503e6dd7

View File

@@ -1312,6 +1312,15 @@ LatteTexture::LatteTexture(Latte::E_DIM dim, MPTR physAddress, MPTR physMipAddre
{
this->enableReadback = true;
}
else if (this->tileMode == Latte::E_HWTILEMODE::TM_1D_TILED_THIN1 && (width * height) <= 64 * 64)
{
// Some titles reduce a frame down to a tiny surface on the GPU and then read the result
// back on the CPU - Disney Infinity does this for auto exposure, ending in a 1x8 R32_FLOAT
// holding average scene luminance. Those surfaces are 1D tiled rather than linear, so they
// would never be mirrored back and the title reads stale memory, computes an exposure of
// zero and renders the whole scene black. They are small enough that mirroring them is cheap.
this->enableReadback = true;
}
// calculate number of potential mip levels (from effective size)
sint32 effectiveWidth = width;