[AGC] Read mip 0 from its GFX10 mip-chain offset (#470)

GFX10 stores a mip chain smallest-first: the mip tail packs into the
first swizzle block, the remaining mips follow in decreasing size, and
mip 0 ends up at the end of the allocation. We read the base level
straight from the descriptor address, so every mipped sampled texture
decoded as a collage of its own smaller mips - in Demon's Souls that
showed up as scrambled menu text and repeated controller icons.

GnmTiling.TryGetBaseMipPlacement ports the AddrLib chain-offset math
from Gfx10Lib::ComputeSurfaceInfoMacroTiled/MicroTiled. It returns a
byte offset to mip 0, or, when the whole chain fits inside the tail
block, the element coordinates of mip 0 within that block.
TryCreateGuestDrawTexture applies the offset to the sampled and storage
guest reads, and TryDetileTextureSource lifts a tail-resident mip 0 out
of the detiled block as a sub-rectangle.

MAX_MIP is only decoded from extended descriptors, so resources without
one, and single-level resources, keep the current behaviour.

Tested on Demon's Souls (PPSA01342): menu text and icons decode
correctly instead of showing shrunken copies of themselves. Verified
offline by dumping the raw tiled bytes and the detiled output for a
4096x4096 UI atlas and checking the art lands at the sampled
coordinates.
This commit is contained in:
TarkusTK
2026-07-20 14:27:46 +02:00
committed by GitHub
parent 9d187dec55
commit 6ee445f0c2
2 changed files with 234 additions and 9 deletions
+85 -9
View File
@@ -7782,7 +7782,10 @@ public static partial class AgcExports
TextureDescriptor descriptor, TextureDescriptor descriptor,
uint sourceWidth, uint sourceWidth,
int logicalByteCount, int logicalByteCount,
byte[] source) byte[] source,
bool baseMipInTail = false,
int tailElementX = 0,
int tailElementY = 0)
{ {
if (!GnmTiling.NeedsDetile(descriptor.TileMode) || if (!GnmTiling.NeedsDetile(descriptor.TileMode) ||
!TryGetTextureElementLayout( !TryGetTextureElementLayout(
@@ -7795,6 +7798,48 @@ public static partial class AgcExports
return null; return null;
} }
if (baseMipInTail)
{
if (!GnmTiling.TryGetBlockElementDimensions(
descriptor.TileMode,
bytesPerElement,
out var blockWidth,
out var blockHeight))
{
return null;
}
var blockByteCount = (long)blockWidth * blockHeight * bytesPerElement;
if (source.Length < blockByteCount ||
(long)elementsWide * elementsHigh * bytesPerElement > logicalByteCount)
{
return null;
}
var blockLinear = new byte[blockByteCount];
if (!GnmTiling.TryDetile(
source,
blockLinear,
descriptor.TileMode,
blockWidth,
blockHeight,
bytesPerElement))
{
return null;
}
var tailLinear = new byte[logicalByteCount];
var rowBytes = elementsWide * bytesPerElement;
for (var y = 0; y < elementsHigh; y++)
{
var sourceOffset = (((long)tailElementY + y) * blockWidth + tailElementX) * bytesPerElement;
blockLinear.AsSpan((int)sourceOffset, rowBytes)
.CopyTo(tailLinear.AsSpan(y * rowBytes, rowBytes));
}
return tailLinear;
}
var linear = new byte[logicalByteCount]; var linear = new byte[logicalByteCount];
return GnmTiling.TryDetile( return GnmTiling.TryDetile(
source, source,
@@ -7869,13 +7914,17 @@ public static partial class AgcExports
} }
var physicalSourceByteCount = sourceByteCount; var physicalSourceByteCount = sourceByteCount;
if (GnmTiling.NeedsDetile(descriptor.TileMode) && var elementsWide = 0;
var elementsHigh = 0;
var bytesPerElement = 0;
var hasElementLayout = GnmTiling.NeedsDetile(descriptor.TileMode) &&
TryGetTextureElementLayout( TryGetTextureElementLayout(
descriptor, descriptor,
sourceWidth, sourceWidth,
out var elementsWide, out elementsWide,
out var elementsHigh, out elementsHigh,
out var bytesPerElement) && out bytesPerElement);
if (hasElementLayout &&
GnmTiling.TryGetTiledByteCount( GnmTiling.TryGetTiledByteCount(
descriptor.TileMode, descriptor.TileMode,
elementsWide, elementsWide,
@@ -7893,6 +7942,27 @@ public static partial class AgcExports
return true; return true;
} }
var resourceMipLevels = descriptor.HasExtendedDescriptor
? descriptor.ResourceMipLevels
: 1u;
var baseMipByteOffset = 0UL;
var baseMipInTail = false;
var mipTailElementX = 0;
var mipTailElementY = 0;
if (hasElementLayout && resourceMipLevels > 1)
{
GnmTiling.TryGetBaseMipPlacement(
descriptor.TileMode,
elementsWide,
elementsHigh,
bytesPerElement,
resourceMipLevels,
out baseMipByteOffset,
out baseMipInTail,
out mipTailElementX,
out mipTailElementY);
}
// Upload-known (not plain availability): the presenter's answer goes // Upload-known (not plain availability): the presenter's answer goes
// generation-stale when the guest CPU rewrites a CPU-backed image // generation-stale when the guest CPU rewrites a CPU-backed image
// (video planes, streamed font atlases), which routes this draw back // (video planes, streamed font atlases), which routes this draw back
@@ -7943,14 +8013,17 @@ public static partial class AgcExports
// and run the same AddrLib-derived detile path used below for // and run the same AddrLib-derived detile path used below for
// sampled textures before seeding the Vulkan image. // sampled textures before seeding the Vulkan image.
var storageSource = new byte[(int)physicalSourceByteCount]; var storageSource = new byte[(int)physicalSourceByteCount];
if (ctx.Memory.TryRead(descriptor.Address, storageSource)) if (ctx.Memory.TryRead(descriptor.Address + baseMipByteOffset, storageSource))
{ {
readSucceeded = true; readSucceeded = true;
var linearStorage = TryDetileTextureSource( var linearStorage = TryDetileTextureSource(
descriptor, descriptor,
sourceWidth, sourceWidth,
checked((int)sourceByteCount), checked((int)sourceByteCount),
storageSource) ?? storageSource storageSource,
baseMipInTail,
mipTailElementX,
mipTailElementY) ?? storageSource
.AsSpan(0, checked((int)sourceByteCount)) .AsSpan(0, checked((int)sourceByteCount))
.ToArray(); .ToArray();
if (linearStorage.AsSpan().IndexOfAnyExcept((byte)0) >= 0) if (linearStorage.AsSpan().IndexOfAnyExcept((byte)0) >= 0)
@@ -8055,7 +8128,7 @@ public static partial class AgcExports
} }
var source = new byte[(int)physicalSourceByteCount]; var source = new byte[(int)physicalSourceByteCount];
if (!ctx.Memory.TryRead(descriptor.Address, source)) if (!ctx.Memory.TryRead(descriptor.Address + baseMipByteOffset, source))
{ {
TraceTextureFallback( TraceTextureFallback(
descriptor, descriptor,
@@ -8092,7 +8165,10 @@ public static partial class AgcExports
descriptor, descriptor,
sourceWidth, sourceWidth,
checked((int)sourceByteCount), checked((int)sourceByteCount),
source) ?? source.AsSpan(0, checked((int)sourceByteCount)).ToArray(); source,
baseMipInTail,
mipTailElementX,
mipTailElementY) ?? source.AsSpan(0, checked((int)sourceByteCount)).ToArray();
DumpLinearTextureIfRequested(descriptor, sourceWidth, rgba); DumpLinearTextureIfRequested(descriptor, sourceWidth, rgba);
texture = new GuestDrawTexture( texture = new GuestDrawTexture(
descriptor.Address, descriptor.Address,
+149
View File
@@ -194,6 +194,155 @@ internal static class GnmTiling
} }
} }
public static bool TryGetBlockElementDimensions(
uint swizzleMode,
int bytesPerElement,
out int blockWidth,
out int blockHeight)
{
blockWidth = 0;
blockHeight = 0;
if (bytesPerElement <= 0 ||
!TryGetSwizzleKind(swizzleMode, out _, out var blockBytes))
{
return false;
}
var bppLog2 = BitLog2((uint)bytesPerElement);
if (bppLog2 < 0)
{
return false;
}
(blockWidth, blockHeight) = SquareBlockDimensions(blockBytes >> bppLog2);
return blockWidth != 0 && blockHeight != 0;
}
/// <summary>
/// Locates mip 0 in a GFX10 mip chain, which AddrLib stores smallest-first
/// (Gfx10Lib::ComputeSurfaceInfoMacroTiled/MicroTiled).
/// </summary>
public static bool TryGetBaseMipPlacement(
uint swizzleMode,
int elementsWide,
int elementsHigh,
int bytesPerElement,
uint resourceMipLevels,
out ulong byteOffset,
out bool inMipTail,
out int tailElementX,
out int tailElementY)
{
byteOffset = 0;
inMipTail = false;
tailElementX = 0;
tailElementY = 0;
if (resourceMipLevels <= 1 ||
!ShouldDetile(swizzleMode) ||
elementsWide <= 0 ||
elementsHigh <= 0 ||
bytesPerElement <= 0 ||
!TryGetSwizzleKind(swizzleMode, out _, out var blockBytes))
{
return false;
}
var bppLog2 = BitLog2((uint)bytesPerElement);
if (bppLog2 < 0)
{
return false;
}
var (blockWidth, blockHeight) = SquareBlockDimensions(blockBytes >> bppLog2);
var blockSizeLog2 = BitLog2((uint)blockBytes);
if (blockWidth == 0 || blockHeight == 0 || blockSizeLog2 < 8)
{
return false;
}
var mipLevels = (int)Math.Min(resourceMipLevels, 16u);
var maxMipsInTail = blockSizeLog2 <= 8 ? 0
: blockSizeLog2 <= 11
? 1 + (1 << (blockSizeLog2 - 9))
: blockSizeLog2 - 4;
var tailWidth = (blockSizeLog2 & 1) != 0 ? blockWidth >> 1 : blockWidth;
var tailHeight = (blockSizeLog2 & 1) != 0 ? blockHeight : blockHeight >> 1;
var firstMipInTail = mipLevels;
var mipSizes = new ulong[mipLevels];
for (var i = 0; i < mipLevels; i++)
{
var mipWidth = Math.Max(elementsWide >> i, 1);
var mipHeight = Math.Max(elementsHigh >> i, 1);
if (maxMipsInTail > 0 &&
mipWidth <= tailWidth &&
mipHeight <= tailHeight &&
mipLevels - i <= maxMipsInTail)
{
firstMipInTail = i;
break;
}
var alignedWidth = (ulong)(mipWidth + blockWidth - 1) / (ulong)blockWidth * (ulong)blockWidth;
var alignedHeight = (ulong)(mipHeight + blockHeight - 1) / (ulong)blockHeight * (ulong)blockHeight;
mipSizes[i] = alignedWidth * alignedHeight * (ulong)bytesPerElement;
}
if (firstMipInTail == 0)
{
var m = maxMipsInTail - 1;
var mipOffset = m > 6 ? 16 << m : m << 8;
var mipX = ((mipOffset >> 9) & 1) |
((mipOffset >> 10) & 2) |
((mipOffset >> 11) & 4) |
((mipOffset >> 12) & 8) |
((mipOffset >> 13) & 16) |
((mipOffset >> 14) & 32);
var mipY = ((mipOffset >> 8) & 1) |
((mipOffset >> 9) & 2) |
((mipOffset >> 10) & 4) |
((mipOffset >> 11) & 8) |
((mipOffset >> 12) & 16) |
((mipOffset >> 13) & 32);
if ((blockSizeLog2 & 1) != 0)
{
(mipX, mipY) = (mipY, mipX);
if ((bppLog2 & 1) != 0)
{
mipY = (mipY << 1) | (mipX & 1);
mipX >>= 1;
}
}
var (microWidth, microHeight) = SquareBlockDimensions(256 >> bppLog2);
if (microWidth == 0 || microHeight == 0)
{
return false;
}
tailElementX = mipX * microWidth;
tailElementY = mipY * microHeight;
if (tailElementX + elementsWide > blockWidth ||
tailElementY + elementsHigh > blockHeight)
{
tailElementX = 0;
tailElementY = 0;
return false;
}
inMipTail = true;
return true;
}
byteOffset = firstMipInTail < mipLevels ? (ulong)blockBytes : 0;
for (var i = firstMipInTail - 1; i >= 1; i--)
{
byteOffset += mipSizes[i];
}
return true;
}
/// <summary> /// <summary>
/// Deswizzles <paramref name="tiled"/> into linear row-major order. /// Deswizzles <paramref name="tiled"/> into linear row-major order.
/// Elements are pixels for uncompressed formats and 4x4 blocks for /// Elements are pixels for uncompressed formats and 4x4 blocks for