From 738ae9df067886846cf665c42e3f2a07abdbe21f Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Tue, 13 Oct 2026 12:34:25 +1300 Subject: [PATCH] test(agc): cover depth storage access through dispatches Draw depth and stencil, read and rewrite each plane in compute dispatches, and check the stored values with depth-tested and stencil-tested draws, a following dispatch and the flushed guest bytes. The array view, multisampled and two-view refusals are checked through dispatches as well. --- .../tests/execution/DepthStorageDispatch.cpp | 286 ++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp new file mode 100644 index 000000000..6ac51ec00 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp @@ -0,0 +1,286 @@ +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using DepthFastClearHarness::Covered; +using DepthFastClearHarness::Draw; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::Pixels; +using DepthFastClearHarness::Shaders; +using DepthFastClearHarness::Surface; +using DepthFastClearHarness::Width; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Format8Uint = 5; +constexpr std::uint32_t Format32Float = 22; +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::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; + +alignas(256) constexpr std::array ScaleDepthCode{ + 0x7e3c0300u, 0x7e3e0280u, 0x7e140280u, 0xf0001108u, 0x00010a1eu, 0xbf8c3f70u, 0x34060082u, + 0xe0701000u, 0x80000a03u, 0x1014140cu, 0xf0201108u, 0x00010a1eu, 0xbf810000u, 0xbf810000u, +}; + +alignas(256) constexpr std::array AddStencilCode{ + 0x7e3c0300u, 0x7e3e0280u, 0x7e140280u, 0xf0001108u, 0x00010a1eu, 0xbf8c3f70u, 0x34060082u, + 0xe0701000u, 0x80000a03u, 0x4a141484u, 0xf0201108u, 0x00010a1eu, 0xbf810000u, 0xbf810000u, +}; + +alignas(256) constexpr std::array StoreCode{ + 0x7e280d00u, 0x102828ffu, 0x3c800000u, 0x7e3c0300u, 0x7e3e0280u, 0xf0201108u, 0x0001141eu, 0xbf810000u, 0xbf810000u, +}; + +alignas(256) constexpr std::array StoreLayerCode{ + 0x7e280d00u, 0x1028280eu, 0x7e3c0300u, 0x7e3e0280u, 0x7e40020cu, 0x7e42020du, 0xf0241128u, 0x0001141eu, 0xbf810000u, 0xbf810000u, +}; + +alignas(256) constexpr std::array StoreTwoViewsCode{ + 0x7e280d00u, 0x102828ffu, 0x3c800000u, 0x7e3c0300u, 0x7e3e0280u, 0xf0201108u, 0x0001141eu, 0xf0201108u, 0x0003141eu, 0xbf810000u, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(65536, BlockBytes)); +#endif + Require(block != nullptr, "depth storage dispatch: cannot allocate the guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + 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) { + 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, + }; +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, std::span userData) { + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +ShaderRecompiler::RecompileResult PlaneProgram(AgcDriver::VulkanDevice& device, std::span code, std::span output, const std::uint8_t* plane, std::uint32_t format, float factor) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(output.data(), static_cast(output.size_bytes())); + const auto texture = TextureDescriptor(plane, format); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + userData[12] = std::bit_cast(factor); + return Compile(device, code, userData); +} + +void RunPlane(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& program, std::span code, std::span output, std::uint32_t expected, const std::string& what) { + std::fill(output.begin(), output.end(), 0xdeadbeefu); + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(output[lane] == expected, what + ": the dispatch read " + std::to_string(output[lane]) + " at texel " + std::to_string(lane) + ", expected " + std::to_string(expected)); + } +} + +void RequireRow(std::uint32_t covered, const std::string& what) { + Require(covered == Threads, what + ": " + std::to_string(covered) + " pixels passed, expected the " + std::to_string(Threads) + " the dispatch stored"); + for (std::uint32_t x = 0; x < Threads; ++x) { + Require(std::to_integer(Pixels[x * 4u]) == 255u, what + ": pixel " + std::to_string(x) + " of the first row did not pass"); + } +} + +template +void ExpectRefusal(TAction action, const char* expected, const std::string& what) { + try { + action(); + } catch (const std::exception& error) { + Require(std::string(error.what()).find(expected) != std::string::npos, what + " threw another error: " + error.what()); + return; + } + throw std::runtime_error(what + " was accepted"); +} + +VkStencilOpState StencilEqual(std::uint32_t reference) { + return {VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, 0xffu, 0u, reference}; +} + +void StoreBackCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, std::uint8_t* block) { + auto* depth = block; + auto* stencil = block + StencilOffset; + const std::span output(reinterpret_cast(block + OutputOffset), Threads); + const Surface surface{reinterpret_cast(depth), reinterpret_cast(stencil), 0, false, VK_FORMAT_D32_SFLOAT_S8_UINT}; + const VkStencilOpState writeFive{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 5u}; + const auto bits = [](float value) { return std::bit_cast(value); }; + Require(Draw(device, shaders, surface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS, .stencilTest = true, .stencil = writeFive}) == Covered, "the depth and stencil producer did not cover the surface"); + + const auto raise = PlaneProgram(device, ScaleDepthCode, output, depth, Format32Float, 1.5f); + const auto lower = PlaneProgram(device, ScaleDepthCode, output, depth, Format32Float, 0.5f); + RunPlane(device, raise, ScaleDepthCode, output, bits(0.5f), "first depth dispatch"); + RequireRow(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}), "depth test against the first dispatch's depth"); + RunPlane(device, lower, ScaleDepthCode, output, bits(0.75f), "second depth dispatch"); + RequireRow(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_GREATER}), "depth test against the second dispatch's depth"); + Require(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}) == 0, "the first dispatch's depth outlived the second dispatch"); + RunPlane(device, raise, ScaleDepthCode, output, bits(0.375f), "third depth dispatch, its program's second"); + RequireRow(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}), "depth test against the third dispatch's depth"); + + const auto add = PlaneProgram(device, AddStencilCode, output, stencil, Format8Uint, 0.0f); + RunPlane(device, add, AddStencilCode, output, 5u, "first stencil dispatch"); + RequireRow(Draw(device, shaders, surface, {.depthTest = false, .stencilTest = true, .stencil = StencilEqual(9u)}), "stencil test against the first dispatch's stencil"); + RunPlane(device, add, AddStencilCode, output, 9u, "second stencil dispatch"); + RequireRow(Draw(device, shaders, surface, {.depthTest = false, .stencilTest = true, .stencil = StencilEqual(13u)}), "stencil test against the second dispatch's stencil"); + Require(Draw(device, shaders, surface, {.depthTest = false, .stencilTest = true, .stencil = StencilEqual(5u)}) == Covered - Threads, "a stencil dispatch changed stencil values it did not store"); + RequireRow(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}), "depth test after the stencil dispatches"); + + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(depth), Width * Height * sizeof(float), nullptr, "test"); + device.WaitIdle(); + for (std::uint32_t x = 0; x < Width; ++x) { + float texel = 0.0f; + std::memcpy(&texel, depth + x * sizeof(float), sizeof(texel)); + Require(texel == (x < Threads ? 0.5625f : 0.5f), "guest depth texel " + std::to_string(x) + " is " + std::to_string(texel) + " after the flush"); + } + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(stencil), Width * Height, nullptr, "test"); + device.WaitIdle(); + for (std::uint32_t x = 0; x < Width; ++x) { + Require(stencil[x] == (x < Threads ? 13u : 5u), "guest stencil texel " + std::to_string(x) + " is " + std::to_string(stencil[x]) + " after the flush"); + } +} + +void ArrayViewCase(AgcDriver::VulkanDevice& device, std::uint8_t* block) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(block, Format32Float, Type2DArray); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + userData[14] = std::bit_cast(1.0f / 64.0f); + const auto program = Compile(device, StoreLayerCode, userData); + ExpectRefusal([&] { device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(StoreLayerCode.data())); }, "storage image access to depth/stencil surface", "a one-layer 2D array storage view of the depth plane"); +} + +void MultisampledCase(AgcDriver::VulkanDevice& device, const Shaders& compiled, std::uint8_t* block) { + auto* depth = block + MultisampledOffset; + const std::array shaders{{ + {ShaderRecompiler::ShaderStage::Vertex, &compiled.vertex, 0}, + {ShaderRecompiler::ShaderStage::Fragment, &compiled.pixel, PixelPushOffset(compiled.vertexPush, device.Target())} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(block + MultisampledColorOffset), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Width * Height * 16u, 0xe4u}; + state.color.samples = 4; + state.colors = {state.color}; + state.hasColorTarget = true; + state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(depth), 0, {Width, Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0, false, 4}; + state.depthTest = true; + state.depthWrite = true; + state.depthCompare = VK_COMPARE_OP_ALWAYS; + state.samples = 4; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(DepthFastClearHarness::Triangles().size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(depth, Format32Float); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const auto program = Compile(device, StoreCode, userData); + 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 TwoViewsCase(AgcDriver::VulkanDevice& device, std::uint8_t* block) { + std::vector userData(20, 0u); + const auto first = TextureDescriptor(block, Format32Float, Type2D, TileLinear); + const auto second = TextureDescriptor(block, Format32Float, Type2D, TileStandard256B); + std::copy(first.begin(), first.end(), userData.begin() + 4); + std::copy(second.begin(), second.end(), userData.begin() + 12); + const auto program = Compile(device, StoreTwoViewsCode, userData); + ExpectRefusal([&] { device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(StoreTwoViewsCode.data())); }, "through a view that another view of the plane", "a dispatch storing to two views of one depth plane"); +} + +} + +int main() { + try { + GuestBlock block; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto shaders = DepthFastClearHarness::Compile(*device); + StoreBackCase(*device, shaders, block.Data()); + ArrayViewCase(*device, block.Data()); + MultisampledCase(*device, shaders, block.Data()); + TwoViewsCase(*device, block.Data()); + std::puts("depth storage dispatch tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +}