From 2912da02fe8525a81862efb040602b9052826518 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Tue, 20 Jan 2026 14:51:56 +0900 Subject: [PATCH] [D3D12] Fix gamma_unorm16 render target transfer shaders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gamma stored as R16G16B16A16_UNORM is 64bpp in host memory but represents a single 32bpp pixel, unlike true 64bpp formats which pack two 32bpp pixels together. Separate dest_is_gamma_unorm16 from dest_is_64bpp to avoid incorrect coordinate transforms. Add output paths for gamma_unorm16 destinations: - gamma→gamma: direct passthrough (already linear in R16G16B16A16) - k_8_8_8_8→gamma: convert gamma-encoded source to linear - Cross-format packed EDRAM: midpoint encoding to survive UNORM16 quantization round-trip through PreSaturatedLinearToPWLGamma Fixes red bleeding in Halo Reach, image halving in Just Cause 2, Portal not rendering, and black rock/plant textures in Halo Reach. --- .../gpu/d3d12/d3d12_render_target_cache.cc | 90 ++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/src/xenia/gpu/d3d12/d3d12_render_target_cache.cc b/src/xenia/gpu/d3d12/d3d12_render_target_cache.cc index 74616f810..3cc4c4bbe 100644 --- a/src/xenia/gpu/d3d12/d3d12_render_target_cache.cc +++ b/src/xenia/gpu/d3d12/d3d12_render_target_cache.cc @@ -2064,6 +2064,12 @@ D3D12RenderTargetCache::GetOrCreateTransferPipelines(TransferShaderKey key) { xenos::DepthRenderTargetFormat(key.dest_resource_format); bool dest_is_64bpp = dest_is_color && xenos::IsColorRenderTargetFormat64bpp(dest_color_format); + // Whether dest is gamma stored as R16G16B16A16_UNORM (64bpp host storage but + // 32bpp coordinate addressing). + bool dest_is_gamma_unorm16 = + dest_is_color && + dest_color_format == xenos::ColorRenderTargetFormat::k_8_8_8_8_GAMMA && + gamma_render_target_as_unorm16_; xenos::ColorRenderTargetFormat source_color_format = xenos::ColorRenderTargetFormat(key.source_resource_format); @@ -3315,7 +3321,7 @@ D3D12RenderTargetCache::GetOrCreateTransferPipelines(TransferShaderKey key) { a.OpMov(dxbc::Dest::OStencilRef(), dxbc::Src::R(1, dxbc::Src::kXXXX)); } - if (dest_is_64bpp) { + if (dest_is_64bpp || dest_is_gamma_unorm16) { // Handle construction of 64bpp color, either from two 32-bit samples in r0 // and r1, or from one 64bpp sample in r1. Using r2.xy as temporary when // needed. @@ -3327,6 +3333,10 @@ D3D12RenderTargetCache::GetOrCreateTransferPipelines(TransferShaderKey key) { case xenos::ColorRenderTargetFormat::k_8_8_8_8_GAMMA: { // 8_8_8_8_GAMMA is represented by linear stored in // R16G16B16A16_UNORM. + if (dest_is_gamma_unorm16) { + a.OpMov(dxbc::Dest::O(0), dxbc::Src::R(1)); + break; + } for (uint32_t i = 0; i < 2; ++i) { for (uint32_t j = 0; j < 3; ++j) { DxbcShaderTranslator::PreSaturatedLinearToPWLGamma(a, i, j, i, j, @@ -3336,6 +3346,15 @@ D3D12RenderTargetCache::GetOrCreateTransferPipelines(TransferShaderKey key) { } [[fallthrough]]; case xenos::ColorRenderTargetFormat::k_8_8_8_8: { + if (dest_is_gamma_unorm16) { + // Convert gamma-encoded source to linear for gamma_unorm16 dest. + for (uint32_t j = 0; j < 3; ++j) { + DxbcShaderTranslator::PWLGammaToLinear(a, 1, j, 1, j, true, 2, 0, + 2, 1); + } + a.OpMov(dxbc::Dest::O(0), dxbc::Src::R(1)); + break; + } color_packed_in_r0x_and_r1x = true; for (uint32_t i = 0; i < 2; ++i) { a.OpMAd(dxbc::Dest::R(i), dxbc::Src::R(i), dxbc::Src::LF(255.0f), @@ -3468,6 +3487,75 @@ D3D12RenderTargetCache::GetOrCreateTransferPipelines(TransferShaderKey key) { if (dest_color_format == xenos::ColorRenderTargetFormat::k_32_32_FLOAT) { a.OpMov(dxbc::Dest::O(0, 0b0001), dxbc::Src::R(0, dxbc::Src::kXXXX)); a.OpMov(dxbc::Dest::O(0, 0b0010), dxbc::Src::R(1, dxbc::Src::kXXXX)); + } else if (dest_is_gamma_unorm16) { + // For gamma_unorm16, only r1.x has a valid packed 32-bit EDRAM + // dword (one pixel, since gamma_unorm16 is 32bpp Xenos / 64bpp + // host). Reinterpret as k_8_8_8_8_GAMMA (4 gamma-encoded bytes) + // and convert to linear float for R16G16B16A16_UNORM output. + // + // Use midpoint encoding to survive the UNORM16 quantization + // round-trip through PreSaturatedLinearToPWLGamma (which uses + // trunc()). Storing the exact PWLGammaToLinear result puts values + // at the lower boundary of the valid range, where UNORM16 + // quantization can push them below threshold causing +/-1 byte + // errors that corrupt cross-format EDRAM reinterpretation. + // + // For gamma byte B, the midpoint linear value is: + // Piece 0 (B < 64): F = (B + 0.5) / 1023.0 + // Piece 1 (64<=B<96): F = (B - 31.5) / 511.5 + // Piece 2 (96<=B<192): F = (B - 63.5) / 255.75 + // Piece 3 (B >= 192): F = (B - 127.5) / 127.875 + // Using MAd form: F = B * recip + offset. + + // Extract 4 bytes: r1.xyzw = [R, G, B, A] as uint. + a.OpUBFE(dxbc::Dest::R(1), dxbc::Src::LU(8, 8, 8, 8), + dxbc::Src::LU(0, 8, 16, 24), + dxbc::Src::R(1, dxbc::Src::kXXXX)); + + // Alpha: o0.w = float(A) / 255.0 (no gamma conversion). + a.OpUToF(dxbc::Dest::R(0, 0b1000), dxbc::Src::R(1, dxbc::Src::kWWWW)); + a.OpMul(dxbc::Dest::O(0, 0b1000), dxbc::Src::R(0, dxbc::Src::kWWWW), + dxbc::Src::LF(1.0f / 255.0f)); + + // RGB: per-channel midpoint encoding using r0.xy as (recip, + // offset) and r2.x as comparison temp. + for (uint32_t j = 0; j < 3; ++j) { + // Default to piece 0. + a.OpMov(dxbc::Dest::R(0, 0b0001), dxbc::Src::LF(1.0f / 1023.0f)); + a.OpMov(dxbc::Dest::R(0, 0b0010), dxbc::Src::LF(0.5f / 1023.0f)); + // Piece 1: byte >= 64. + a.OpUGE(dxbc::Dest::R(2, 0b0001), dxbc::Src::R(1).Select(j), + dxbc::Src::LU(64)); + a.OpMovC(dxbc::Dest::R(0, 0b0001), dxbc::Src::R(2, dxbc::Src::kXXXX), + dxbc::Src::LF(1.0f / 511.5f), + dxbc::Src::R(0, dxbc::Src::kXXXX)); + a.OpMovC(dxbc::Dest::R(0, 0b0010), dxbc::Src::R(2, dxbc::Src::kXXXX), + dxbc::Src::LF(-31.5f / 511.5f), + dxbc::Src::R(0, dxbc::Src::kYYYY)); + // Piece 2: byte >= 96. + a.OpUGE(dxbc::Dest::R(2, 0b0001), dxbc::Src::R(1).Select(j), + dxbc::Src::LU(96)); + a.OpMovC(dxbc::Dest::R(0, 0b0001), dxbc::Src::R(2, dxbc::Src::kXXXX), + dxbc::Src::LF(1.0f / 255.75f), + dxbc::Src::R(0, dxbc::Src::kXXXX)); + a.OpMovC(dxbc::Dest::R(0, 0b0010), dxbc::Src::R(2, dxbc::Src::kXXXX), + dxbc::Src::LF(-63.5f / 255.75f), + dxbc::Src::R(0, dxbc::Src::kYYYY)); + // Piece 3: byte >= 192. + a.OpUGE(dxbc::Dest::R(2, 0b0001), dxbc::Src::R(1).Select(j), + dxbc::Src::LU(192)); + a.OpMovC(dxbc::Dest::R(0, 0b0001), dxbc::Src::R(2, dxbc::Src::kXXXX), + dxbc::Src::LF(1.0f / 127.875f), + dxbc::Src::R(0, dxbc::Src::kXXXX)); + a.OpMovC(dxbc::Dest::R(0, 0b0010), dxbc::Src::R(2, dxbc::Src::kXXXX), + dxbc::Src::LF(-127.5f / 127.875f), + dxbc::Src::R(0, dxbc::Src::kYYYY)); + // F = float(byte) * recip + offset. + a.OpUToF(dxbc::Dest::R(2, 0b0001), dxbc::Src::R(1).Select(j)); + a.OpMAd(dxbc::Dest::O(0, 1 << j), dxbc::Src::R(2, dxbc::Src::kXXXX), + dxbc::Src::R(0, dxbc::Src::kXXXX), + dxbc::Src::R(0, dxbc::Src::kYYYY)); + } } else { for (uint32_t i = 0; i < 2; ++i) { a.OpUBFE(dxbc::Dest::O(0, 0b11 << (i * 2)), dxbc::Src::LU(16),