feat(agc): storage image access to depth planes through 2D array views

A 2D array storage view of a depth plane from slice 0 threw. It now gets
a storage image over all its slices: each slice with a depth surface at
the plane's address plus a whole depth slice per layer is copied from
that surface before every use and back after the work; the other slices
hold guest memory like any storage image. Cubes, stencil arrays, later
base slices and layer pitches other than the depth slice size still
throw.

GTA V (PPSA04264) stops at compute shader 0x242d4a200 on a 5-slice R32
Z_64KB_X array over its shadow depth surfaces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1W4jdNSKTV7xwuS6xQPJV
This commit is contained in:
thibaut 2026-10-13 09:36:55 +02:00
parent 665ad7da74
commit 696d1660c1
4 changed files with 227 additions and 18 deletions

View File

@ -153,19 +153,17 @@ public:
float clearDepth = 0.0f;
std::uint8_t clearStencil = 0;
std::shared_ptr<StorageTexture> Storage(std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip) {
if (resource.width != target.extent.width || resource.height != target.extent.height) return nullptr;
bool StorageViewMatches(std::span<const std::uint32_t> words, const GuestTextureResource& resource) const {
if (resource.width != target.extent.width || resource.height != target.extent.height) return false;
const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress;
const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT;
const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT;
const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u);
if (ResolveTextureFormat(resource.format) != expected && !depthBits) return nullptr;
const bool oneLayer = resource.dimension == TextureDimension::k2DArray && resource.baseArray == 0 && resource.depthOrLastArray == 0;
if (resource.dimension != TextureDimension::k2D && !oneLayer) {
char text[256];
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented (storage=%ux%u guest format=%u mip=%u dimension=%d tile=%d)", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, resource.format, mip, static_cast<int>(resource.dimension), static_cast<int>(resource.tileMode));
throw std::runtime_error(text);
}
return ResolveTextureFormat(resource.format) == expected || depthBits;
}
void RequireStorable(const GuestTextureResource& resource, std::uint32_t mip) const {
const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress;
Require(target.samples == 1, "storage image access to a multisampled depth surface is not implemented");
if (!stencil && hostFormat != target.format) {
char text[224];
@ -177,6 +175,18 @@ public:
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to level %u of the %s plane of depth surface 0x%llx is not implemented", mip, stencil ? "stencil" : "depth", static_cast<unsigned long long>(target.address));
throw std::runtime_error(text);
}
}
std::shared_ptr<StorageTexture> Storage(std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip) {
if (!StorageViewMatches(words, resource)) return nullptr;
const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress;
const bool oneLayer = resource.dimension == TextureDimension::k2DArray && resource.baseArray == 0 && resource.depthOrLastArray == 0;
if (resource.dimension != TextureDimension::k2D && !oneLayer) {
char text[256];
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented (storage=%ux%u guest format=%u mip=%u dimension=%d tile=%d)", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, resource.format, mip, static_cast<int>(resource.dimension), static_cast<int>(resource.tileMode));
throw std::runtime_error(text);
}
RequireStorable(resource, mip);
ApplyFastClear();
auto& plane = stencil ? stencilPlane : depthPlane;
if (plane.storage == nullptr || plane.storage->Descriptor().format != resource.format || plane.storage->Descriptor().tileMode != resource.tileMode || plane.storage->Descriptor().baseAddress != resource.baseAddress || plane.storage->Descriptor().dimension != resource.dimension) {
@ -439,6 +449,82 @@ std::map<std::tuple<VkDevice, std::uint64_t, std::uint32_t, std::uint32_t, VkFor
return *copies;
}
struct LayeredSlice {
DepthSurface* surface;
std::uint32_t layer;
};
class LayeredDepthStorage {
public:
LayeredDepthStorage(const Context& context, const GuestTextureResource& resource, const DepthTarget& base, std::uint32_t bytesPerTexel, std::uint32_t layers) : context(context), base(base), extent(base.extent), bytesPerTexel(bytesPerTexel), layers(layers) {
const auto bytes = DescribeSurface(resource).guestBytes;
StorageTexture::FlushPending(resource.baseAddress, static_cast<std::size_t>(bytes), nullptr, "storage image creation", PublishScope::None);
storage = StorageTexture::Create(context, *context.detiler, resource, 0);
Require(storage->ImageLayers() == layers, "a layered depth storage view's image does not hold its layers");
staging = std::make_unique<DeviceBuffer>(context, static_cast<std::size_t>(sliceBytes()) * layers, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
}
void Copy(std::span<const LayeredSlice> slices, bool toStorage) {
if (slices.empty()) return;
auto* recorder = Recorder::Active();
Require(recorder != nullptr, "depth surface storage copies need an active recorder");
const auto commands = recorder->Commands();
recorder->Keep(storage);
const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy);
std::vector<VkBufferImageCopy> storageRegions;
const auto region = [&](std::uint32_t layer, VkImageAspectFlags aspect, std::uint32_t imageLayer) {
VkBufferImageCopy copy{};
copy.bufferOffset = sliceBytes() * layer;
copy.imageSubresource = {aspect, 0, imageLayer, 1};
copy.imageExtent = {extent.width, extent.height, 1};
return copy;
};
for (const auto& slice : slices) storageRegions.push_back(region(slice.layer, VK_IMAGE_ASPECT_COLOR_BIT, slice.layer));
const auto copyToBuffer = context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer");
const auto copyToImage = context.Function<PFN_vkCmdCopyBufferToImage>("vkCmdCopyBufferToImage");
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
if (toStorage) {
for (const auto& slice : slices) {
const auto copy = region(slice.layer, VK_IMAGE_ASPECT_DEPTH_BIT, 0);
copyToBuffer(commands, slice.surface->image, VK_IMAGE_LAYOUT_GENERAL, staging->Handle(), 1, &copy);
}
} else {
copyToBuffer(commands, storage->Image(), VK_IMAGE_LAYOUT_GENERAL, staging->Handle(), static_cast<std::uint32_t>(storageRegions.size()), storageRegions.data());
}
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
if (toStorage) {
copyToImage(commands, staging->Handle(), storage->Image(), VK_IMAGE_LAYOUT_GENERAL, static_cast<std::uint32_t>(storageRegions.size()), storageRegions.data());
} else {
for (const auto& slice : slices) {
const auto copy = region(slice.layer, VK_IMAGE_ASPECT_DEPTH_BIT, 0);
copyToImage(commands, staging->Handle(), slice.surface->image, VK_IMAGE_LAYOUT_GENERAL, 1, &copy);
}
}
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT);
Recorder::CountBarriers(Recorder::CommandClass::Copy, 3);
recorder->EndGpuTiming(timing, sliceBytes() * slices.size());
}
const Context context;
const DepthTarget base;
const VkExtent2D extent;
const std::uint32_t bytesPerTexel;
const std::uint32_t layers;
std::shared_ptr<StorageTexture> storage;
private:
std::unique_ptr<DeviceBuffer> staging;
VkDeviceSize sliceBytes() const {
return static_cast<VkDeviceSize>(extent.width) * extent.height * bytesPerTexel;
}
};
std::map<std::tuple<VkDevice, std::uint64_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, int, int>, std::unique_ptr<LayeredDepthStorage>>& layeredStorages() {
static auto* storages = new std::map<std::tuple<VkDevice, std::uint64_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, int, int>, std::unique_ptr<LayeredDepthStorage>>();
return *storages;
}
bool sameSurface(const DepthTarget& a, const DepthTarget& b) {
return a.address == b.address && a.stencilAddress == b.stencilAddress && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.format == b.format && a.samples == b.samples;
}
@ -575,6 +661,7 @@ void ClearDepthSurfaces(VkDevice device) {
drawnDepth = nullptr;
std::erase_if(surfaces(), [&](const auto& surface) { return surface->context.device == device; });
std::erase_if(planeCopies(), [&](const auto& entry) { return std::get<0>(entry.first) == device; });
std::erase_if(layeredStorages(), [&](const auto& entry) { return std::get<0>(entry.first) == device; });
}
std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components) {
@ -682,17 +769,71 @@ bool DepthSurfaceAt(const Context& context, std::span<const std::uint32_t> words
return ResolveTextureFormat(resource.format) == expected || depthBits;
}
namespace {
std::vector<LayeredSlice> layeredSlices(const Context& context, const LayeredDepthStorage& storage) {
const auto& base = storage.base;
std::vector<LayeredSlice> slices;
const auto& list = surfaces();
for (std::uint32_t layer = 0; layer < storage.layers; ++layer) {
const auto address = base.address + static_cast<std::uint64_t>(layer) * DepthSliceBytes(base.extent, storage.bytesPerTexel);
const auto slice = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == context.device && surface->target.address == address && surface->target.extent.width == base.extent.width && surface->target.extent.height == base.extent.height && surface->target.format == base.format && surface->target.samples == 1; });
if (slice != list.rend()) slices.push_back({slice->get(), layer});
}
return slices;
}
std::shared_ptr<StorageTexture> layeredDepthStorage(const Context& context, DepthSurface& surface, const GuestTextureResource& resource, std::uint32_t mip) {
const auto& target = surface.target;
const auto layers = resource.depthOrLastArray + 1u;
const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT;
const auto bytesPerTexel = d16 ? 2u : 4u;
if (resource.baseArray != 0) {
char text[256];
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to the depth planes of depth surface 0x%llx through a 2D array view of slices %u-%u is not implemented", static_cast<unsigned long long>(target.address), resource.baseArray, resource.depthOrLastArray);
throw std::runtime_error(text);
}
surface.RequireStorable(resource, mip);
const auto geometry = DescribeSurface(resource);
if (layers > 1 && geometry.GuestLayerOffset(1) != DepthSliceBytes(target.extent, bytesPerTexel)) {
char text[256];
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to the depth planes of depth surface 0x%llx with a layer pitch of 0x%llx bytes, not the depth slice size 0x%llx, is not implemented", static_cast<unsigned long long>(target.address), static_cast<unsigned long long>(geometry.GuestLayerOffset(1)), static_cast<unsigned long long>(DepthSliceBytes(target.extent, bytesPerTexel)));
throw std::runtime_error(text);
}
auto& entry = layeredStorages()[{context.device, resource.baseAddress, resource.width, resource.height, layers, resource.format, static_cast<int>(resource.tileMode), static_cast<int>(resource.dimension)}];
if (entry == nullptr) entry = std::make_unique<LayeredDepthStorage>(context, resource, target, bytesPerTexel, layers);
else static_cast<void>(entry->storage->Refresh());
const auto slices = layeredSlices(context, *entry);
for (const auto& slice : slices) slice.surface->ApplyFastClear();
entry->Copy(slices, true);
return entry->storage;
}
}
std::shared_ptr<StorageTexture> DepthSurfaceStorage(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip) {
std::lock_guard lock(surfacesMutex());
const auto& list = surfaces();
const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) {
return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress));
});
return found == list.rend() ? nullptr : (*found)->Storage(words, resource, mip);
if (found == list.rend()) return nullptr;
auto& surface = **found;
const bool stencil = surface.target.stencilAddress != 0 && surface.target.stencilAddress == resource.baseAddress;
if (stencil || resource.dimension != TextureDimension::k2DArray || (resource.baseArray == 0 && resource.depthOrLastArray == 0)) return surface.Storage(words, resource, mip);
if (!surface.StorageViewMatches(words, resource)) return nullptr;
return layeredDepthStorage(context, surface, resource, mip);
}
bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage) {
std::lock_guard lock(surfacesMutex());
for (auto& [key, entry] : layeredStorages()) {
if (std::get<0>(key) != context.device || entry->storage.get() != &storage) continue;
const auto slices = layeredSlices(context, *entry);
entry->Copy(slices, false);
for (const auto& slice : slices) slice.surface->written = ++depthWrites;
return true;
}
for (const auto& surface : surfaces()) {
if (surface->context.device == context.device && surface->StoreBack(storage)) return true;
}

View File

@ -243,10 +243,10 @@ void Run(Context context, std::uint8_t* block) {
layeredStencil.depthOrLastArray = 1;
ExpectFailure([&] { static_cast<void>(DepthSurfaceStorage(context, {}, layeredStencil, 0)); }, "storage image access to depth/stencil surface", "a two-layer 2D array view of the stencil plane was accepted");
layeredDepth.depthOrLastArray = 1;
ExpectFailure([&] { static_cast<void>(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "storage image access to depth/stencil surface", "a two-layer 2D array view of the depth plane was accepted");
ExpectFailure([&] { static_cast<void>(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "with a layer pitch of", "a two-layer 2D array view of the depth plane with a layer pitch other than the depth slice size was accepted");
layeredDepth.depthOrLastArray = 0;
layeredDepth.baseArray = 1;
ExpectFailure([&] { static_cast<void>(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "storage image access to depth/stencil surface", "a 2D array view of layer 1 of the depth plane was accepted");
ExpectFailure([&] { static_cast<void>(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "through a 2D array view of slices 1-0", "a 2D array view of layer 1 of the depth plane was accepted");
}
}

View File

@ -39,7 +39,8 @@ constexpr std::uint32_t Type2D = 9;
constexpr std::uint32_t Type2DArray = 13;
constexpr std::uint32_t TileLinear = 0;
constexpr std::uint32_t TileStandard256B = 1;
constexpr std::size_t BlockBytes = 1u << 20u;
constexpr std::uint32_t TileZ64KBX = 0x18;
constexpr std::size_t BlockBytes = 2u << 20u;
constexpr std::size_t StencilOffset = 1u << 16u;
constexpr std::size_t OutputOffset = 2u << 16u;
constexpr std::size_t MultisampledOffset = 4u << 16u;
@ -48,6 +49,8 @@ constexpr std::size_t SampledOffset = 8u << 16u;
constexpr std::size_t LaterDepthOffset = 10u << 16u;
constexpr std::size_t ArrayDepthOffset = 12u << 16u;
constexpr std::size_t ArrayStencilOffset = 13u << 16u;
constexpr std::size_t ArrayOffset = 16u << 16u;
constexpr std::uint32_t ArrayLayers = 3;
alignas(256) constexpr std::array<std::uint32_t, 14> ScaleDepthCode{
0x7e3c0300u, 0x7e3e0280u, 0x7e140280u, 0xf0001108u, 0x00010a1eu, 0xbf8c3f70u, 0x34060082u,
@ -247,13 +250,77 @@ void ArrayViewCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, std:
RequireRow(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}), "depth test against the one-layer 2D array view's depth");
RunPlane(device, layer(AddStencilLayerCode, stencil, Format8Uint, 0.0f), AddStencilLayerCode, output, 5u, "stencil dispatch through a one-layer 2D array view");
RequireRow(Draw(device, shaders, surface, {.depthTest = false, .stencilTest = true, .stencil = StencilEqual(9u)}), "stencil test against the one-layer 2D array view's stencil");
}
ShaderRecompiler::RecompileResult LayerProgram(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, std::span<std::uint32_t> output, const std::uint8_t* layers, std::uint32_t layer) {
std::vector<std::uint32_t> userData(16, 0u);
const auto texture = TextureDescriptor(depth, Format32Float, Type2DArray, TileLinear, 1u);
const auto buffer = BufferDescriptor(output.data(), static_cast<std::uint32_t>(output.size_bytes()));
const auto texture = TextureDescriptor(layers, Format32Float, Type2DArray, TileZ64KBX, ArrayLayers - 1u);
std::copy(buffer.begin(), buffer.end(), userData.begin());
std::copy(texture.begin(), texture.end(), userData.begin() + 4);
userData[14] = std::bit_cast<std::uint32_t>(1.0f / 64.0f);
const auto program = Compile(device, StoreLayerCode, userData);
ExpectRefusal([&] { device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(StoreLayerCode.data())); }, "storage image access to depth/stencil surface", "a two-layer 2D array storage view of the depth plane");
userData[12] = layer;
userData[14] = std::bit_cast<std::uint32_t>(1.0f / 128.0f);
return Compile(device, code, userData);
}
void StoreLayer(AgcDriver::VulkanDevice& device, std::span<std::uint8_t> layers, std::span<std::uint32_t> output, std::uint32_t layer) {
const auto store = LayerProgram(device, StoreLayerCode, output, layers.data(), layer);
device.Dispatch(store, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(StoreLayerCode.data()));
device.WaitIdle();
}
void RequireGuestLayer(std::span<const std::uint8_t> layer, bool stored, float rest, float initial, const std::string& what) {
std::vector<std::uint32_t> seen(Threads, 0u);
std::uint32_t kept = 0;
for (std::size_t offset = 0; offset < layer.size(); offset += sizeof(float)) {
float texel = 0.0f;
std::memcpy(&texel, layer.data() + offset, sizeof(texel));
const auto lane = static_cast<std::uint32_t>(texel * 128.0f);
if (stored && texel < 0.25f && texel != initial && static_cast<float>(lane) / 128.0f == texel) {
++seen[lane];
continue;
}
if (texel == rest) {
++kept;
continue;
}
Require(texel == initial, what + ": guest texel at offset " + std::to_string(offset) + " is " + std::to_string(texel) + ", expected " + std::to_string(rest) + " or the untouched " + std::to_string(initial));
}
const auto texels = static_cast<std::uint32_t>(layer.size() / sizeof(float));
const auto expected = (rest == initial ? texels : Width * Height) - (stored ? Threads : 0u);
Require(kept == expected, what + ": " + std::to_string(kept) + " guest texels hold " + std::to_string(rest) + ", expected " + std::to_string(expected));
if (!stored) return;
for (std::uint32_t lane = 0; lane < Threads; ++lane) Require(seen[lane] == 1u, what + ": the value stored by lane " + std::to_string(lane) + " is in guest memory " + std::to_string(seen[lane]) + " times");
}
void LayeredArrayViewCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, std::uint8_t* block) {
const auto sliceBytes = static_cast<std::size_t>(AgcDriver::Graphics::DepthSliceBytes({Width, Height}, 4u));
Require(sliceBytes == 1u << 16u, "a depth slice of the test extent is not one 64 KiB block");
const std::span<std::uint8_t> layers(block + ArrayOffset, sliceBytes * ArrayLayers);
const std::span<std::uint32_t> output(reinterpret_cast<std::uint32_t*>(block + OutputOffset), Threads);
const auto guestLayer = [&](std::uint32_t layer) { return layers.subspan(sliceBytes * layer, sliceBytes); };
const float untouched = 0.875f;
const float guestDepth = 0.75f;
for (std::uint32_t layer = 0; layer < ArrayLayers; ++layer) {
const float value = layer == 2 ? guestDepth : untouched;
for (std::size_t offset = 0; offset < sliceBytes; offset += sizeof(float)) std::memcpy(guestLayer(layer).data() + offset, &value, sizeof(float));
}
const Surface first{reinterpret_cast<std::uintptr_t>(guestLayer(0).data()), 0, 0, false, VK_FORMAT_D32_SFLOAT};
const Surface second{reinterpret_cast<std::uintptr_t>(guestLayer(1).data()), 0, 0, false, VK_FORMAT_D32_SFLOAT};
Require(Draw(device, shaders, first, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the first layer's depth producer did not cover its surface");
Require(Draw(device, shaders, second, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the second layer's depth producer did not cover its surface");
StoreLayer(device, layers, output, 1);
RequireRow(Draw(device, shaders, second, {.depthCompare = VK_COMPARE_OP_GREATER}), "depth test against the values a 2D array storage view stored to the second layer's surface");
Require(Draw(device, shaders, first, {.depthCompare = VK_COMPARE_OP_GREATER}) == 0, "a store to the second layer changed the first layer's surface");
StoreLayer(device, layers, output, 2);
RequireRow(Draw(device, shaders, second, {.depthCompare = VK_COMPARE_OP_GREATER}), "depth test against the second layer's surface after a store to the third layer");
AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast<std::uintptr_t>(layers.data()), layers.size(), nullptr, "test");
device.WaitIdle();
RequireGuestLayer(guestLayer(0), false, 0.5f, untouched, "the first layer in guest memory after the flush");
RequireGuestLayer(guestLayer(1), true, 0.5f, untouched, "the second layer in guest memory after the flush");
RequireGuestLayer(guestLayer(2), true, guestDepth, guestDepth, "the third layer in guest memory after the flush");
}
void MultisampledCase(AgcDriver::VulkanDevice& device, const Shaders& compiled, std::uint8_t* block) {
@ -334,6 +401,7 @@ int main() {
const auto shaders = DepthFastClearHarness::Compile(*device);
StoreBackCase(*device, shaders, block.Data());
ArrayViewCase(*device, shaders, block.Data());
LayeredArrayViewCase(*device, shaders, block.Data());
MultisampledCase(*device, shaders, block.Data());
TwoViewsCase(*device, block.Data());
SampledCopyCase(*device, shaders, block.Data());

View File

@ -29,7 +29,7 @@
- [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. Retired Vulkan images remain allocated until device teardown to preserve outstanding references. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported.
- [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A matching view still throws when it has another dimension (2D array of more than one layer or from another layer, cube) or level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console.
- [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane from slice 0 is a storage image over all its slices (`LayeredDepthStorage`): a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, or a sampled copy) is a separate image, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console.
- [Collapsed color array views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) preserve the selected slice in the XOR address equation when the view is represented as a shifted 2D surface. Native Vulkan coverage checks R_X draws, array reads, publication and re-upload for slices 1-3; Lost Soul Aside PPSA26358 validates the RGBA16F cube mip chain on Windows/NVIDIA. Other devices and console comparisons remain unverified. Full-array views retain their layer addressing.
- [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. Planes with mips, a base slice other than 0, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below).
- [SW_64KB_D_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the existing texture block/mip geometry and equations from [AMD addrlib GFX10 non-RB+](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) for 16 pipes and 256-byte interleave. The address-reference tests use legal pipe/bank XOR bits for that configuration. Raw column bits outside the source configuration's pipe/bank mask 0xcf00 throw; their console meaning is unresolved. Console address equations and render results have not been measured.