[Gpu] Sample 2D array textures with real layers (#471)

Texture arrays were uploaded and viewed as plain 2D images, so every
layer index in a shader resolved to slice 0. The guest-texture gate
also rejected array, 3D and cube descriptor types outright, sending
those resources to the 1x1 black fallback.

UI atlases hit this constantly, since they pack several sheets as array
slices and pick one per vertex. In Demon's Souls the settings menu
bottom bar stretched a mid-atlas crop across itself, and slider thumbs
and button prompts drew the wrong sheet.

The MIMG decoder already had Dimension and IsArray, so
IsArrayedImageBinding makes one rule out of them for the SPIR-V
translator and the Vulkan backend to share. Both have to agree or the
declared image type and the bound view type mismatch. Sample and gather
bindings with an array address now declare an arrayed image and pass
(u, v, slice). AgcExports reads every slice at the per-slice mip-chain
stride and passes the layers packed in one buffer, which uploads as a
2D array image in a single copy region.

Load and store bindings are unchanged. Arrayed bindings that resolve to
a fallback or to a single-layer guest image get a one-layer 2D array
view so the descriptor still matches the shader.

Tested on Demon's Souls (PPSA01342): the bottom bar, slider thumbs and
button prompts draw their correct sheets. 470 tests pass.
This commit is contained in:
TarkusTK
2026-07-20 14:44:06 +02:00
committed by GitHub
parent dce7c87c4d
commit 25d741b35b
6 changed files with 233 additions and 48 deletions
+110 -18
View File
@@ -147,6 +147,10 @@ public static partial class AgcExports
private const uint Gen5TextureFormatR16G16B16A16Float = 12;
private const uint Gen5TextureType1D = 8;
private const uint Gen5TextureType2D = 9;
private const uint Gen5TextureType3D = 10;
private const uint Gen5TextureTypeCube = 11;
private const uint Gen5TextureType1DArray = 12;
private const uint Gen5TextureType2DArray = 13;
private const ulong MaxPresentedTextureBytes = 128UL * 1024UL * 1024UL;
private const ulong VideoOutPixelFormatA8R8G8B8Srgb = 0x80000000;
private const ulong VideoOutPixelFormatA8B8G8R8Srgb = 0x80002200;
@@ -438,7 +442,8 @@ public static partial class AgcExports
TextureDescriptor Descriptor,
bool IsStorage,
uint MipLevel,
IReadOnlyList<uint> SamplerDescriptor);
IReadOnlyList<uint> SamplerDescriptor,
bool IsArrayed = false);
private readonly record struct RenderTargetWriter(
ulong Sequence,
@@ -5959,7 +5964,8 @@ public static partial class AgcExports
binding,
exportEvaluation.ImageBindings),
binding.MipLevel ?? 0,
binding.SamplerDescriptor));
binding.SamplerDescriptor,
Gen5ShaderTranslator.IsArrayedImageBinding(binding)));
}
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
@@ -6416,7 +6422,8 @@ public static partial class AgcExports
texture,
isStorage,
binding.MipLevel ?? 0,
binding.SamplerDescriptor));
binding.SamplerDescriptor,
Gen5ShaderTranslator.IsArrayedImageBinding(binding)));
}
error = string.Empty;
@@ -7387,6 +7394,7 @@ public static partial class AgcExports
binding.IsStorage,
binding.MipLevel,
binding.SamplerDescriptor,
binding.IsArrayed,
out var texture))
{
textures.Add(texture);
@@ -7877,18 +7885,23 @@ public static partial class AgcExports
bool isStorage,
uint mipLevel,
IReadOnlyList<uint> samplerDescriptor,
bool isArrayed,
out GuestDrawTexture texture)
{
texture = default!;
if ((descriptor.Type != Gen5TextureType1D &&
descriptor.Type != Gen5TextureType2D) ||
descriptor.Type != Gen5TextureType2D &&
descriptor.Type != Gen5TextureType3D &&
descriptor.Type != Gen5TextureTypeCube &&
descriptor.Type != Gen5TextureType1DArray &&
descriptor.Type != Gen5TextureType2DArray) ||
descriptor.Width == 0 ||
descriptor.Height == 0 ||
descriptor.Width > 8192 ||
descriptor.Height > 8192)
{
TraceTextureFallback(descriptor, "invalid-descriptor");
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType, isArrayed);
return true;
}
@@ -7909,7 +7922,7 @@ public static partial class AgcExports
TraceTextureFallback(
descriptor,
$"invalid-byte-count:{sourceByteCount}");
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType, isArrayed);
return true;
}
@@ -7938,7 +7951,7 @@ public static partial class AgcExports
if (physicalSourceByteCount > MaxPresentedTextureBytes ||
physicalSourceByteCount > int.MaxValue)
{
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType, isArrayed);
return true;
}
@@ -7949,8 +7962,8 @@ public static partial class AgcExports
var baseMipInTail = false;
var mipTailElementX = 0;
var mipTailElementY = 0;
if (hasElementLayout && resourceMipLevels > 1)
{
var chainSliceBytes = physicalSourceByteCount;
if (hasElementLayout && resourceMipLevels > 1 &&
GnmTiling.TryGetBaseMipPlacement(
descriptor.TileMode,
elementsWide,
@@ -7960,14 +7973,26 @@ public static partial class AgcExports
out baseMipByteOffset,
out baseMipInTail,
out mipTailElementX,
out mipTailElementY);
out mipTailElementY,
out var placedChainSliceBytes))
{
chainSliceBytes = placedChainSliceBytes;
}
var wantsArrayUpload = isArrayed &&
!isStorage &&
descriptor.Address != 0 &&
(descriptor.Type == Gen5TextureType2DArray ||
descriptor.Type == Gen5TextureType1DArray) &&
descriptor.Depth > 1;
var arrayUploadLayers = wantsArrayUpload ? descriptor.Depth : 1u;
// 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
// through the texel copy below so the refresh path re-uploads.
if (!isStorage &&
!wantsArrayUpload &&
descriptor.Address != 0 &&
GuestGpu.Current.IsGuestImageUploadKnown(
descriptor.Address,
@@ -7990,7 +8015,8 @@ public static partial class AgcExports
Pitch: sourceWidth,
TileMode: descriptor.TileMode,
DstSelect: descriptor.DstSelect,
Sampler: ToGuestSampler(samplerDescriptor));
Sampler: ToGuestSampler(samplerDescriptor),
ArrayedView: isArrayed);
return true;
}
@@ -8105,7 +8131,9 @@ public static partial class AgcExports
descriptor.DstSelect,
descriptor.TileMode,
sourceWidth,
sampler)))
sampler,
isArrayed,
arrayUploadLayers)))
{
texture = new GuestDrawTexture(
descriptor.Address,
@@ -8123,17 +8151,77 @@ public static partial class AgcExports
Pitch: sourceWidth,
TileMode: descriptor.TileMode,
DstSelect: descriptor.DstSelect,
Sampler: sampler);
Sampler: sampler,
ArrayedView: isArrayed,
ArrayLayers: arrayUploadLayers);
return true;
}
if (wantsArrayUpload)
{
var arrayLayers = arrayUploadLayers;
var layerBytes = checked((int)sourceByteCount);
var totalBytes = (long)layerBytes * arrayLayers;
if (totalBytes <= int.MaxValue)
{
var layered = new byte[totalBytes];
var uploadedLayers = 0u;
for (var layer = 0u; layer < arrayLayers; layer++)
{
var sliceSource = new byte[(int)physicalSourceByteCount];
if (!ctx.Memory.TryRead(
descriptor.Address + layer * chainSliceBytes + baseMipByteOffset,
sliceSource))
{
break;
}
var sliceLinear = TryDetileTextureSource(
descriptor,
sourceWidth,
layerBytes,
sliceSource,
baseMipInTail,
mipTailElementX,
mipTailElementY) ?? sliceSource.AsSpan(0, layerBytes).ToArray();
sliceLinear.AsSpan(0, layerBytes)
.CopyTo(layered.AsSpan(checked((int)(layer * layerBytes))));
uploadedLayers++;
}
if (uploadedLayers == arrayLayers)
{
texture = new GuestDrawTexture(
descriptor.Address,
descriptor.Width,
descriptor.Height,
descriptor.Format,
descriptor.NumberType,
layered,
IsFallback: false,
IsStorage: false,
MipLevels: descriptor.MipLevels,
MipLevel: mipLevel,
BaseMipLevel: descriptor.ViewBaseLevel,
ResourceMipLevels: descriptor.ResourceMipLevels,
Pitch: sourceWidth,
TileMode: descriptor.TileMode,
DstSelect: descriptor.DstSelect,
Sampler: sampler,
ArrayedView: true,
ArrayLayers: arrayLayers);
return true;
}
}
}
var source = new byte[(int)physicalSourceByteCount];
if (!ctx.Memory.TryRead(descriptor.Address + baseMipByteOffset, source))
{
TraceTextureFallback(
descriptor,
$"guest-read-failed:{sourceByteCount}");
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType, isArrayed);
return true;
}
@@ -8187,7 +8275,8 @@ public static partial class AgcExports
TileMode: descriptor.TileMode,
DstSelect: descriptor.DstSelect,
Sampler: ToGuestSampler(samplerDescriptor),
WriteGeneration: hasWriteGeneration ? writeGeneration : -1);
WriteGeneration: hasWriteGeneration ? writeGeneration : -1,
ArrayedView: isArrayed);
return true;
}
@@ -8466,7 +8555,8 @@ public static partial class AgcExports
private static GuestDrawTexture CreateFallbackGuestDrawTexture(
bool isStorage,
uint format,
uint numberType)
uint numberType,
bool isArrayed = false)
{
var fallbackFormat = format == 0 ? 10u : format;
var fallbackNumberType = numberType;
@@ -8480,7 +8570,8 @@ public static partial class AgcExports
IsFallback: true,
IsStorage: isStorage,
MipLevels: 1,
MipLevel: 0);
MipLevel: 0,
ArrayedView: isArrayed);
}
private static GuestSampler ToGuestSampler(IReadOnlyList<uint> descriptor) =>
@@ -8856,7 +8947,8 @@ public static partial class AgcExports
texture,
isStorage,
binding.MipLevel ?? 0,
binding.SamplerDescriptor));
binding.SamplerDescriptor,
Gen5ShaderTranslator.IsArrayedImageBinding(binding)));
hasStorageBinding |= isStorage;
var descriptorState = descriptorValid ? string.Empty : "/invalid-desc";