From f15671ecf3af804331ed55a2ee2e9715e874639e Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Tue, 13 Oct 2026 14:56:25 +1300 Subject: [PATCH] fix(agc): copy the depth plane again after a storage write A sampled copy of a depth plane is kept while the surface's write serial is unchanged. Storing a written storage view back into the depth image did not bump the serial, so a later draw or dispatch that sampled the plane as an array or cube kept the values from before the store. The store-back bumps it now. The dispatch test samples the plane through a copy, stores to it in a dispatch and samples it again. --- .../Graphics/src/DepthSurface.cpp | 1 + .../tests/execution/DepthStorageDispatch.cpp | 32 +++++++++++++++++-- docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 3ae3081a6..d8968b069 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -192,6 +192,7 @@ public: auto& plane = stencil ? stencilPlane : depthPlane; if (plane.storage.get() == &candidate) { copyPlane(plane, stencil, false); + written = ++depthWrites; return true; } if (std::none_of(plane.replaced.begin(), plane.replaced.end(), [&](const auto& old) { return old.lock().get() == &candidate; })) continue; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp index 6ac51ec00..8888be01f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp @@ -44,6 +44,8 @@ constexpr std::size_t StencilOffset = 1u << 16u; constexpr std::size_t OutputOffset = 2u << 16u; constexpr std::size_t MultisampledOffset = 4u << 16u; constexpr std::size_t MultisampledColorOffset = 5u << 16u; +constexpr std::size_t SampledOffset = 8u << 16u; +constexpr std::size_t LaterDepthOffset = 10u << 16u; alignas(256) constexpr std::array ScaleDepthCode{ 0x7e3c0300u, 0x7e3e0280u, 0x7e140280u, 0xf0001108u, 0x00010a1eu, 0xbf8c3f70u, 0x34060082u, @@ -67,6 +69,10 @@ alignas(256) constexpr std::array StoreTwoViewsCode{ 0x7e280d00u, 0x102828ffu, 0x3c800000u, 0x7e3c0300u, 0x7e3e0280u, 0xf0201108u, 0x0001141eu, 0xf0201108u, 0x0003141eu, 0xbf810000u, }; +alignas(256) constexpr std::array LoadLayerCode{ + 0x7e020280u, 0x7e04020cu, 0xf0001128u, 0x00010300u, 0xbf8c3f70u, 0x34080082u, 0xe0701000u, 0x80000304u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; + class GuestBlock { public: GuestBlock() { @@ -103,14 +109,14 @@ std::array BufferDescriptor(const void* data, std::uint32_t by return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; } -std::array TextureDescriptor(const void* data, std::uint32_t format, std::uint32_t type = Type2D, std::uint32_t tile = TileLinear) { +std::array TextureDescriptor(const void* data, std::uint32_t format, std::uint32_t type = Type2D, std::uint32_t tile = TileLinear, std::uint32_t lastArray = 0) { const auto address = static_cast(reinterpret_cast(data)); return { static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u), ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), 0xfacu | (tile << 20u) | (type << 28u), - 0u, 0u, 0u, 0u, + lastArray, 0u, 0u, 0u, }; } @@ -255,6 +261,27 @@ void MultisampledCase(AgcDriver::VulkanDevice& device, const Shaders& compiled, ExpectRefusal([&] { device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(StoreCode.data())); }, "storage image access to a multisampled depth surface", "a storage view of a multisampled depth plane"); } +void SampledCopyCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, std::uint8_t* block) { + auto* depth = block + SampledOffset; + const std::span output(reinterpret_cast(block + OutputOffset), Threads); + const Surface surface{reinterpret_cast(depth), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + const Surface later{reinterpret_cast(block + LaterDepthOffset), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + Require(Draw(device, shaders, surface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the sampled depth producer did not cover the surface"); + Require(Draw(device, shaders, later, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the later depth producer did not cover its surface"); + + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(output.data(), static_cast(output.size_bytes())); + const auto layers = TextureDescriptor(depth, Format32Float, Type2DArray, TileLinear, 1u); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(layers.begin(), layers.end(), userData.begin() + 4); + const auto load = Compile(device, LoadLayerCode, userData); + const auto raise = PlaneProgram(device, ScaleDepthCode, output, depth, Format32Float, 1.5f); + RunPlane(device, load, LoadLayerCode, output, std::bit_cast(0.5f), "first layer of the depth plane copy"); + RunPlane(device, load, LoadLayerCode, output, std::bit_cast(0.5f), "first layer of the unchanged depth plane copy"); + RunPlane(device, raise, ScaleDepthCode, output, std::bit_cast(0.5f), "depth dispatch between two plane copies"); + RunPlane(device, load, LoadLayerCode, output, std::bit_cast(0.75f), "first layer of the depth plane copy after the dispatch stored to the plane"); +} + void TwoViewsCase(AgcDriver::VulkanDevice& device, std::uint8_t* block) { std::vector userData(20, 0u); const auto first = TextureDescriptor(block, Format32Float, Type2D, TileLinear); @@ -277,6 +304,7 @@ int main() { ArrayViewCase(*device, block.Data()); MultisampledCase(*device, shaders, block.Data()); TwoViewsCase(*device, block.Data()); + SampledCopyCase(*device, shaders, block.Data()); std::puts("depth storage dispatch tests passed"); return 0; } catch (const std::exception& error) { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index ca871a5d2..bc4083bff 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -27,7 +27,7 @@ ### Functional - [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 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, 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 or address) 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. -- [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 and when a pending fast clear is applied), 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). +- [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. - [Per-face depth bias](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): Vulkan has one depth bias per draw, so a draw that rasterizes both faces with different front and back bias (PA_SU_SC_MODE_CNTL, PA_SU_POLY_OFFSET_FRONT/BACK_*) is recorded twice, front faces with the back faces culled and then back faces with the front faces culled. The vertex stages run twice, so a vertex shader that writes memory writes twice, and all front faces land before all back faces instead of in primitive order, which differs only for blending or stencil updates that depend on the order. - [Partial rect-list primitives](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): a rect-list draw whose vertex or index count is not a multiple of 3 draws its complete rectangles and leaves out the 1 or 2 vertices after them, because rect lists are drawn as Vulkan patch lists of 3 control points, which generate [⌊vertexCount/3⌋ patches](https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html#drawing-patch-lists). What the console draws for the incomplete rectangle is not measured; Palworld (PPSA23126) draws 4 vertices × 9 instances as a rect list when its character creation opens.