From 6ee445f0c20c3c7701281ce69e285021ba0a8728 Mon Sep 17 00:00:00 2001
From: TarkusTK <90155059+TarkusR@users.noreply.github.com>
Date: Mon, 20 Jul 2026 14:27:46 +0200
Subject: [PATCH] [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.
---
src/SharpEmu.Libs/Agc/AgcExports.cs | 94 ++++++++++++++++--
src/SharpEmu.Libs/Agc/GnmTiling.cs | 149 ++++++++++++++++++++++++++++
2 files changed, 234 insertions(+), 9 deletions(-)
diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs
index 07c06b5b..4995d79d 100644
--- a/src/SharpEmu.Libs/Agc/AgcExports.cs
+++ b/src/SharpEmu.Libs/Agc/AgcExports.cs
@@ -7782,7 +7782,10 @@ public static partial class AgcExports
TextureDescriptor descriptor,
uint sourceWidth,
int logicalByteCount,
- byte[] source)
+ byte[] source,
+ bool baseMipInTail = false,
+ int tailElementX = 0,
+ int tailElementY = 0)
{
if (!GnmTiling.NeedsDetile(descriptor.TileMode) ||
!TryGetTextureElementLayout(
@@ -7795,6 +7798,48 @@ public static partial class AgcExports
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];
return GnmTiling.TryDetile(
source,
@@ -7869,13 +7914,17 @@ public static partial class AgcExports
}
var physicalSourceByteCount = sourceByteCount;
- if (GnmTiling.NeedsDetile(descriptor.TileMode) &&
+ var elementsWide = 0;
+ var elementsHigh = 0;
+ var bytesPerElement = 0;
+ var hasElementLayout = GnmTiling.NeedsDetile(descriptor.TileMode) &&
TryGetTextureElementLayout(
descriptor,
sourceWidth,
- out var elementsWide,
- out var elementsHigh,
- out var bytesPerElement) &&
+ out elementsWide,
+ out elementsHigh,
+ out bytesPerElement);
+ if (hasElementLayout &&
GnmTiling.TryGetTiledByteCount(
descriptor.TileMode,
elementsWide,
@@ -7893,6 +7942,27 @@ public static partial class AgcExports
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
// generation-stale when the guest CPU rewrites a CPU-backed image
// (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
// sampled textures before seeding the Vulkan image.
var storageSource = new byte[(int)physicalSourceByteCount];
- if (ctx.Memory.TryRead(descriptor.Address, storageSource))
+ if (ctx.Memory.TryRead(descriptor.Address + baseMipByteOffset, storageSource))
{
readSucceeded = true;
var linearStorage = TryDetileTextureSource(
descriptor,
sourceWidth,
checked((int)sourceByteCount),
- storageSource) ?? storageSource
+ storageSource,
+ baseMipInTail,
+ mipTailElementX,
+ mipTailElementY) ?? storageSource
.AsSpan(0, checked((int)sourceByteCount))
.ToArray();
if (linearStorage.AsSpan().IndexOfAnyExcept((byte)0) >= 0)
@@ -8055,7 +8128,7 @@ public static partial class AgcExports
}
var source = new byte[(int)physicalSourceByteCount];
- if (!ctx.Memory.TryRead(descriptor.Address, source))
+ if (!ctx.Memory.TryRead(descriptor.Address + baseMipByteOffset, source))
{
TraceTextureFallback(
descriptor,
@@ -8092,7 +8165,10 @@ public static partial class AgcExports
descriptor,
sourceWidth,
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);
texture = new GuestDrawTexture(
descriptor.Address,
diff --git a/src/SharpEmu.Libs/Agc/GnmTiling.cs b/src/SharpEmu.Libs/Agc/GnmTiling.cs
index 2fe0c00b..c3683733 100644
--- a/src/SharpEmu.Libs/Agc/GnmTiling.cs
+++ b/src/SharpEmu.Libs/Agc/GnmTiling.cs
@@ -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;
+ }
+
+ ///
+ /// Locates mip 0 in a GFX10 mip chain, which AddrLib stores smallest-first
+ /// (Gfx10Lib::ComputeSurfaceInfoMacroTiled/MicroTiled).
+ ///
+ 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;
+ }
+
///
/// Deswizzles into linear row-major order.
/// Elements are pixels for uncompressed formats and 4x4 blocks for