From 1f3963c54307035aa8dc1e128cc3a8b6ddfcd8b2 Mon Sep 17 00:00:00 2001 From: Slick Daddy <129640104+slick-daddy@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:57:39 +0300 Subject: [PATCH] [Gpu] Factor the exact-XOR swizzle equation in the texture detiler (#483) TryDetile's exact-XOR fast path (PS5 swizzle modes 5/9/24/27) ran the full AddrLib address equation per element: a 16-bit interleave with 32 PopCount calls for every pixel of textures that are millions of elements. Each output bit is parity(x & XMask) XOR parity(y & YMask), and parity distributes over XOR, so the offset factors into independent xTerm(x) ^ yTerm(y) fields. Precompute the per-column X term once and hoist the Y term per row, collapsing the inner loop to one array load and one XOR. Add GnmTilingDetileTests, which lays out a tiled buffer from an independent re-derivation of the mode-27 equation and asserts TryDetile reconstructs it byte-for-byte. Co-authored-by: slick-daddy --- src/SharpEmu.Libs/Agc/GnmTiling.cs | 30 +++++-- .../Agc/GnmTilingDetileTests.cs | 84 +++++++++++++++++++ 2 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 tests/SharpEmu.Libs.Tests/Agc/GnmTilingDetileTests.cs diff --git a/src/SharpEmu.Libs/Agc/GnmTiling.cs b/src/SharpEmu.Libs/Agc/GnmTiling.cs index 565f6139..6f93bb2e 100644 --- a/src/SharpEmu.Libs/Agc/GnmTiling.cs +++ b/src/SharpEmu.Libs/Agc/GnmTiling.cs @@ -422,6 +422,18 @@ internal static class GnmTiling } } + // The XOR equation offset factors cleanly into independent X and Y + // fields — each output bit is parity(x & XMask) XOR parity(y & YMask), + // and parity distributes over XOR, so offset(x, y) == xTerm(x) ^ yTerm(y). + // Precompute the per-column X term once (reused across every row) so the + // inner loop drops from a 16-bit interleave with 32 PopCounts per element + // to a single array load and one XOR. + var xTermByColumn = hasExactXorPattern ? new int[elementsWide] : []; + for (var x = 0; hasExactXorPattern && x < elementsWide; x++) + { + xTermByColumn[x] = (int)PatternAxisTerm((uint)x, xorPattern, useX: true); + } + for (var y = 0; y < elementsHigh; y++) { var blockY = y / blockHeight; @@ -429,6 +441,8 @@ internal static class GnmTiling var rowBlockBase = (long)blockY * blocksPerRow; var tableRowBase = inBlockY * blockWidth; var destRowBase = (long)y * elementsWide * bytesPerElement; + // Y term is constant across the row; hoist it out of the inner loop. + var yTerm = hasExactXorPattern ? (int)PatternAxisTerm((uint)y, xorPattern, useX: false) : 0; for (var x = 0; x < elementsWide; x++) { var blockX = x / blockWidth; @@ -436,7 +450,7 @@ internal static class GnmTiling var blockIndex = rowBlockBase + blockX; var sourceByte = hasExactXorPattern - ? blockIndex * blockBytes + ComputePatternOffset((uint)x, (uint)y, xorPattern) + ? blockIndex * blockBytes + (xTermByColumn[x] ^ yTerm) : (blockIndex * blockElements + blockTable[tableRowBase + inBlockX]) * (long)bytesPerElement; var destByte = destRowBase + (long)x * bytesPerElement; @@ -493,14 +507,20 @@ internal static class GnmTiling return pattern.Length != 0; } - private static long ComputePatternOffset(uint x, uint y, AddressBit[] pattern) + // The AddrLib within-block byte offset is a per-bit XOR equation: + // offset = OR over bits of ( parity(x & XMask) XOR parity(y & YMask) ) << bit + // Because parity distributes over XOR, that whole offset factors into two + // independent axis terms: PatternAxisTerm(x, useX: true) ^ + // PatternAxisTerm(y, useX: false). Splitting the axes lets TryDetile cache + // the X term per column and hoist the Y term per row instead of recomputing + // the full 16-bit interleave (32 PopCounts) for every element. + private static uint PatternAxisTerm(uint coordinate, AddressBit[] pattern, bool useX) { uint offset = 0; for (var bit = 0; bit < pattern.Length; bit++) { - var equation = pattern[bit]; - var parity = (System.Numerics.BitOperations.PopCount(x & equation.XMask) + - System.Numerics.BitOperations.PopCount(y & equation.YMask)) & 1; + var mask = useX ? pattern[bit].XMask : pattern[bit].YMask; + var parity = System.Numerics.BitOperations.PopCount(coordinate & mask) & 1; offset |= (uint)parity << bit; } diff --git a/tests/SharpEmu.Libs.Tests/Agc/GnmTilingDetileTests.cs b/tests/SharpEmu.Libs.Tests/Agc/GnmTilingDetileTests.cs new file mode 100644 index 00000000..0e694fd1 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Agc/GnmTilingDetileTests.cs @@ -0,0 +1,84 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.Libs.Agc; +using Xunit; + +namespace SharpEmu.Libs.Tests.Agc; + +// TryDetile's exact-XOR fast path (PS5 swizzle modes 5/9/24/27) factors the +// AddrLib bit-interleave into independent per-column X and per-row Y terms so +// the inner loop is one array load and one XOR instead of a 16-bit interleave. +// These tests pin that the factored output stays byte-identical to the direct +// AddrLib address equation. +public sealed class GnmTilingDetileTests +{ + // Independent re-derivation of the 64 KiB RB+ R_X equation (swizzle mode 27, + // 2 bytes/element) straight from the address-bit table, so the tiled source + // layout does not depend on TryDetile's own internal factoring. + private static readonly (uint XMask, uint YMask)[] RbPlus64KRenderX2Bpp = + [ + (0, 0), (1u << 0, 0), (1u << 1, 0), (1u << 2, 0), + (0, 1u << 0), (0, 1u << 1), (0, 1u << 2), (1u << 3, 0), + (1u << 7, (1u << 4) | (1u << 7)), (1u << 4, 1u << 4), (1u << 6, 1u << 5), (1u << 5, 1u << 6), + (0, 1u << 3), (1u << 6, 0), (1u << 7, 1u << 7), (1u << 8, 1u << 6), + ]; + + private static uint ReferenceOffset(uint x, uint y, (uint XMask, uint YMask)[] pattern) + { + uint offset = 0; + for (var bit = 0; bit < pattern.Length; bit++) + { + var parity = (System.Numerics.BitOperations.PopCount(x & pattern[bit].XMask) + + System.Numerics.BitOperations.PopCount(y & pattern[bit].YMask)) & 1; + offset |= (uint)parity << bit; + } + + return offset; + } + + [Fact] + public void TryDetile_ExactXorMode27_MatchesReferenceAddressEquation() + { + const uint swizzleMode = 27; // 64 KiB RB+ R_X + const int bytesPerElement = 2; + const int blockBytes = 65536; + // SquareBlockDimensions(32768 elements): 15 bits split 8/7, x favored. + const int blockWidth = 256; + const int blockHeight = 128; + const int elementsWide = 384; // spans two block columns and a partial third + const int elementsHigh = 200; + var blocksPerRow = (elementsWide + blockWidth - 1) / blockWidth; + var blocksPerColumn = (elementsHigh + blockHeight - 1) / blockHeight; + + // Lay out a tiled source where each element stores its own linear index, + // placed at the byte address the AddrLib equation dictates. The tiled + // buffer is sized by padded whole blocks (block addressing overshoots the + // linear extent). A correct detile must recover ascending linear indices. + var tiled = new byte[blocksPerRow * blocksPerColumn * blockBytes]; + for (var y = 0; y < elementsHigh; y++) + { + for (var x = 0; x < elementsWide; x++) + { + var blockIndex = (long)(y / blockHeight) * blocksPerRow + (x / blockWidth); + // The equation yields a byte offset within the block (bit 0 is + // Zero at 2bpp, keeping element writes 2-byte aligned). + var sourceByte = (int)(blockIndex * blockBytes + + ReferenceOffset((uint)x, (uint)y, RbPlus64KRenderX2Bpp)); + var linearIndex = (ushort)(y * elementsWide + x); + tiled[sourceByte] = (byte)linearIndex; + tiled[sourceByte + 1] = (byte)(linearIndex >> 8); + } + } + + var linear = new byte[elementsWide * elementsHigh * bytesPerElement]; + var ok = GnmTiling.TryDetile(tiled, linear, swizzleMode, elementsWide, elementsHigh, bytesPerElement); + + Assert.True(ok); + for (var i = 0; i < elementsWide * elementsHigh; i++) + { + var value = (ushort)(linear[i * 2] | (linear[i * 2 + 1] << 8)); + Assert.Equal((ushort)i, value); + } + } +}