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