[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 <slick-daddy@users.noreply.github.com>
This commit is contained in:
Slick Daddy
2026-07-21 12:57:39 +03:00
committed by GitHub
parent 4bb1af93d7
commit 1f3963c543
2 changed files with 109 additions and 5 deletions
+25 -5
View File
@@ -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;
}
@@ -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);
}
}
}