From ba4571a253576a9b428f3f1428bb9b10b82fddfb Mon Sep 17 00:00:00 2001 From: mitul72 Date: Tue, 13 Oct 2026 18:18:42 -0600 Subject: [PATCH] fix(agc): name in-place draw inputs and multisampled targets in pass hazards A recorded draw's pass access listed its shader-resource reads, indirect arguments, storage images and resident color targets, but not the index and vertex ranges it binds in place from host imports (DrawInputs::inPlaceReads) nor its multisampled color targets (null in RecordedDraw::targets). A later draw writing those ranges through a shader buffer, or a pass sharing the multisampled target under another pass key, found no hazard: the break between the passes recorded no barrier, or the later draw continued the pass, so its writes could land before the earlier draw fetched its inputs, and the two passes' attachment writes and store operations on the multisampled image were unordered. The in-place input ranges join the pass reads, and MultisampleTargetView reports the target's image, which joins the pass attachments as written. --- .../Graphics/include/MultisampleTarget.hpp | 2 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 7 +- .../Graphics/src/MultisampleTarget.cpp | 6 +- .../tests/execution/MultisampledPassOrder.cpp | 149 +++++++++++ .../tests/execution/PassInputHazard.cpp | 241 ++++++++++++++++++ 5 files changed, 401 insertions(+), 4 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/MultisampledPassOrder.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PassInputHazard.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index 1d9cdb4590..5e872f1dec 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -54,8 +54,8 @@ std::size_t MultisampledDccBytes(const ColorTarget& target); VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span dcc, std::span cmask); bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear); bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear); -VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target); std::shared_ptr MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D); +VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, VkImage* image = nullptr); void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget& target); VkImageView MultisampleTargetStorageView(const Context& context, const GuestTextureResource& resource); bool MultisampleTargetStorable(const Context& context, const ColorTarget& target); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 9aaa239642..ab2410ea97 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1598,6 +1598,7 @@ struct RecordedDraw { std::vector> targets; std::vector targetSlices; std::vector> proxies; + std::vector multisampleImages; const IndirectRecord* indirect = nullptr; std::span moved; bool listed = false; @@ -1794,12 +1795,14 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam passReads.emplace_back(args->arguments, args->arguments + args->RangeBytes()); if (args->countIndirect) passReads.emplace_back(args->countAddress, args->countAddress + 4); } + passReads.insert(passReads.end(), inputs.inPlaceReads.begin(), inputs.inPlaceReads.end()); const auto passImages = resources.StorageImages(); std::vector> passAttachments; passAttachments.reserve(record.targets.size()); for (const auto& target : record.targets) { if (target != nullptr) passAttachments.emplace_back(target->Image(), true); } + for (const auto image : record.multisampleImages) passAttachments.emplace_back(image, true); const bool unknownMemory = addressBased || resources.UsesAddressTable() || resources.HasCopiedWrites(); static const bool addressReadPasses = std::getenv("APS5_NO_ADDRESS_READ_PASSES") == nullptr; const bool readOnlyBda = addressReadPasses && resources.BdaReadOnly(); @@ -2022,6 +2025,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& TileMipLayout mip{}; std::unique_ptr target; bool multisampled = false; + VkImage multisampleImage = VK_NULL_HANDLE; // APS5_DUMP_TARGETS: the rendered linear pixels, read back for target_
_.raw. std::unique_ptr dump; }; @@ -2038,7 +2042,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (color.samples > 1) { binding.multisampled = true; timer.phase(PhaseSetup); - targetViews.push_back(MultisampleTargetView(context, color)); + targetViews.push_back(MultisampleTargetView(context, color, &binding.multisampleImage)); timer.phase(PhaseReadTarget); continue; } @@ -2319,6 +2323,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& for (const auto& binding : targets) record.targetSlices.push_back(binding.color.arraySlice); for (const auto& binding : targets) { if (binding.proxied) record.proxies.push_back(binding.resident); + if (binding.multisampled) record.multisampleImages.push_back(binding.multisampleImage); } record.indirect = args != nullptr ? &indirect : nullptr; record.listed = listed; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index fcee92002b..98b455d7b6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -672,9 +672,11 @@ bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colo return true; } -VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target) { +VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, VkImage* image) { std::lock_guard lock(targetsMutex()); - return acquire(context, target).view; + const auto& acquired = acquire(context, target); + if (image != nullptr) *image = acquired.image; + return acquired.view; } namespace { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampledPassOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampledPassOrder.cpp new file mode 100644 index 0000000000..2075a2e2c8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampledPassOrder.cpp @@ -0,0 +1,149 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using namespace AgcDriver::Graphics; +using DepthFastClearHarness::Compile; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::Shaders; +using DepthFastClearHarness::Triangles; +using DepthFastClearHarness::Width; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Samples = 4; + +alignas(4096) std::array Multisampled{}; +alignas(4096) std::array DepthA{}; +alignas(4096) std::array DepthB{}; + +struct Barrier { + VkPipelineStageFlags source; + VkPipelineStageFlags destination; + std::vector memory; + std::vector images; +}; + +class Capture { +public: + static Context Install(const Context& base) { + Require(resolver == nullptr, "nested barrier capture"); + resolver = base.deviceProc; + device = base.device; + Context captured = base; + captured.bufferPool.reset(); + captured.deviceProc = resolve; + captured.functions = nullptr; + return captured; + } + static std::vector Take() { return std::exchange(barriers, {}); } + +private: + static PFN_vkVoidFunction VKAPI_CALL resolve(VkDevice handle, const char* name) { + if (std::string_view(name) == "vkCmdPipelineBarrier") return reinterpret_cast(pipelineBarrier); + return resolver(handle, name); + } + static VKAPI_ATTR void VKAPI_CALL pipelineBarrier(VkCommandBuffer commands, VkPipelineStageFlags source, VkPipelineStageFlags destination, VkDependencyFlags dependencies, std::uint32_t memoryCount, const VkMemoryBarrier* memory, std::uint32_t bufferCount, const VkBufferMemoryBarrier* buffers, std::uint32_t imageCount, const VkImageMemoryBarrier* images) { + barriers.push_back({source, destination, std::vector(memory, memory + memoryCount), std::vector(images, images + imageCount)}); + reinterpret_cast(resolver(device, "vkCmdPipelineBarrier"))(commands, source, destination, dependencies, memoryCount, memory, bufferCount, buffers, imageCount, images); + } + + static inline PFN_vkGetDeviceProcAddr resolver = nullptr; + static inline VkDevice device = VK_NULL_HANDLE; + static inline std::vector barriers; +}; + +bool OrdersColorWrites(const std::vector& barriers) { + return std::any_of(barriers.begin(), barriers.end(), [](const Barrier& barrier) { + return barrier.source == VK_PIPELINE_STAGE_ALL_COMMANDS_BIT && barrier.destination == VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT && barrier.images.empty() && barrier.memory.size() == 1 && (barrier.memory[0].srcAccessMask & VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT) != 0 && (barrier.memory[0].dstAccessMask & VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT) != 0; + }); +} + +void Draw(const Context& context, AgcDriver::VulkanDevice& device, const Shaders& compiled, std::uint64_t depth) { + const std::array shaders{{ + {ShaderStage::Vertex, &compiled.vertex, 0}, + {ShaderStage::Fragment, &compiled.pixel, PixelPushOffset(compiled.vertexPush, device.Target())} + }}; + State state{}; + state.stages = {ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Multisampled.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Multisampled.size(), 0xe4u}; + state.color.samples = Samples; + state.colors = {state.color}; + state.hasColorTarget = true; + state.depth = DepthTarget{depth, 0, {Width, Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0, false, Samples}; + state.depthTest = true; + state.depthWrite = true; + state.depthCompare = VK_COMPARE_OP_LESS; + state.samples = Samples; + 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(Triangles().size()), 0, 1, 0, false}; + AgcDriver::Graphics::Draw(context, state, draw, shaders); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto base = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if ((base.limits.framebufferColorSampleCounts & Samples) == 0 || (base.limits.framebufferDepthSampleCounts & Samples) == 0) { + std::puts("skipped, the device cannot render four-sample color and depth"); + return VulkanTestSkipped; + } + if (!Recorder::MergeBarriers()) { + std::puts("skipped, barrier elision is off"); + return VulkanTestSkipped; + } + const auto shaders = Compile(*device); + device->WaitIdle(); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto captured = Capture::Install(base); + Recorder recorder(captured); + recorder.Activate(); + const auto depthA = reinterpret_cast(DepthA.data()); + const auto depthB = reinterpret_cast(DepthB.data()); + Draw(captured, *device, shaders, depthA); + Draw(captured, *device, shaders, depthB); + Capture::Take(); + Draw(captured, *device, shaders, depthA); + const auto between = Capture::Take(); + Require(OrdersColorWrites(between), "multisampled pass order: two draws sharing a multisampled color target while switching depth targets recorded no barrier ordering the first draw's color writes before the second"); + recorder.Submit(); + recorder.Sync(); + std::puts("multisampled pass order tests passed (ordering asserted on the recorded barriers; the multisampled image is not resolved to guest memory)"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PassInputHazard.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PassInputHazard.cpp new file mode 100644 index 0000000000..66e6aff3eb --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PassInputHazard.cpp @@ -0,0 +1,241 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using DepthFastClearHarness::BufferDescriptor; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::Require; +using DepthFastClearHarness::Triangles; +using DepthFastClearHarness::VertexCode; +using DepthFastClearHarness::Width; +using ShaderRecompiler::ShaderStage; + +constexpr std::size_t BlockBytes = 65536; +const ColorTargetLayout TargetLayout(Width, Height, ColorTileMode::RenderTarget, 4); + +alignas(4096) std::array FirstDepth{}; +alignas(4096) std::array SecondDepth{}; + +alignas(256) constexpr std::array CoverCode{0x7e0002f2, 0xf800180f, 0x00000000, 0xbf810000}; +alignas(256) constexpr std::array StoreCode{0x7e0a0280, 0x7e0c0280, 0xe0702000, 0x80010506, 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000}; + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "pass input hazard: cannot map a block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "pass input hazard: cannot allocate a block"); + std::memset(data, 0, bytes); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + + std::uint64_t Address() const { return reinterpret_cast(data); } + + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +struct Program { + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult pixel; + std::uint32_t vertexPush = 0; +}; + +Program Compile(AgcDriver::VulkanDevice& device, const void* vertices, std::span pixelCode, const void* stored) { + const auto target = device.Target(); + Program program; + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(vertices, 16u, static_cast(Triangles().size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {32, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + program.vertex = ShaderRecompiler::Recompile(vertex); + program.vertexPush = static_cast(program.vertex.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = true; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + std::vector pixelUserData(8, 0u); + if (stored != nullptr) { + const auto storeBuffer = BufferDescriptor(stored, 4u, 1u); + std::copy(storeBuffer.begin(), storeBuffer.end(), pixelUserData.begin() + 4); + } + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {32, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(program.vertexPush, target) + }; + fragment.useCache = false; + program.pixel = ShaderRecompiler::Recompile(fragment); + return program; +} + +void Draw(AgcDriver::VulkanDevice& device, const Program& program, const Block& target, const float* depth, const std::uint32_t* indices) { + const std::array shaders{{ + {ShaderStage::Vertex, &program.vertex, 0}, + {ShaderStage::Fragment, &program.pixel, PixelPushOffset(program.vertexPush, device.Target())} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {target.Address(), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, TargetLayout.Bytes(), 0xe4u, ColorTileMode::RenderTarget}; + state.colors = {state.color}; + state.hasColorTarget = true; + if (depth != nullptr) { + state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(depth), 0, {Width, Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + state.depthTest = true; + state.depthWrite = true; + state.depthCompare = VK_COMPARE_OP_ALWAYS; + } + 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{reinterpret_cast(indices), static_cast(Triangles().size()), indices != nullptr ? 4u : 0u, 1, 0, indices != nullptr}; + device.Draw(state, draw, shaders); +} + +bool WriterFenced(std::uint64_t address, std::size_t bytes) { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr, "pass input hazard: no active recorder"); + const std::array, 1> writes{{{address, address + bytes}}}; + return recorder->StartDrawPass(0x5eed, false, false, AgcDriver::Graphics::PassAccess{{}, writes, {}, {}, false, false}).barrier; +} + +std::size_t Covered(AgcDriver::VulkanDevice& device, const Block& target) { + AgcDriver::Graphics::StorageTexture::FlushPending(target.Address(), target.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(TargetLayout.Bytes()); + AgcDriver::GuestMemory::Read(target.Address(), std::as_writable_bytes(std::span(stored)), 1); + return static_cast(std::count(stored.begin(), stored.end(), std::uint8_t{0xff})); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const Block inputs(BlockBytes); + auto* indices = reinterpret_cast(inputs.data); + constexpr std::array triangle{0u, 1u, 2u}; + const auto reader = Compile(*device, Triangles().data(), CoverCode, nullptr); + const auto writer = Compile(*device, Triangles().data(), StoreCode, indices + 1); + constexpr std::size_t all = std::size_t{Width} * Height * 4u; + const std::array, 3> depths{{{nullptr, nullptr}, {FirstDepth.data(), nullptr}, {FirstDepth.data(), SecondDepth.data()}}}; + for (const auto& [readDepth, writeDepth] : depths) { + const Block read(TargetLayout.Bytes()); + const Block written(TargetLayout.Bytes()); + std::memcpy(indices, triangle.data(), sizeof(triangle)); + const auto variant = std::string(readDepth == nullptr ? "without depth" : writeDepth == nullptr ? "with depth on the reader" : "with a depth target each"); + Draw(*device, reader, read, readDepth, indices); + Require(WriterFenced(inputs.Address(), sizeof(triangle)), "pass input hazard " + variant + ": a pass writing the indices an open pass reads in place took no barrier"); + Draw(*device, reader, read, readDepth, indices); + Draw(*device, writer, written, writeDepth, nullptr); + Require(Covered(*device, read) == all, "pass input hazard " + variant + ": the draw reading its indices in place did not cover its target, a later pass overwrote them first"); + Require(Covered(*device, written) == all, "pass input hazard " + variant + ": the storing draw did not cover its target"); + std::array stored{}; + AgcDriver::GuestMemory::Read(inputs.Address(), std::as_writable_bytes(std::span(stored)), 4); + Require(stored == std::array{0u, 0u, 2u}, "pass input hazard " + variant + ": the storing draw's write did not reach the indices"); + } + std::puts("pass input hazard tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +}