From 04138f50ade7f37e9035b795bc200196134037dd Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Mon, 12 Oct 2026 18:47:58 +0200 Subject: [PATCH 1/2] feat(agc): vertex clip and cull distances PA_CL_VS_OUT_CNTL's clip and cull enables and its misc and distance vector bits reach the recompiler's vertex info (request version 20), the draw key, the vertex-info comparison and the recompile cache key. Registration and draw-time preparation set them the same way the draw does; a program without pos1..pos3 exports gets none, so its artifact does not depend on the register. Guest clip distances share main's gl_ClipDistance array with the all-zero position distance; cull distances need shaderCullDistance. Up to eight clip and cull distances are accepted. Co-Authored-By: john.maia Co-Authored-By: Claude Opus 5.5 (1M context) --- .../include/Driver/Shaders/ShaderRegistry.hpp | 5 ++++ .../Execution/src/Driver/Draw/Draw.cpp | 1 + .../Execution/src/Driver/Draw/DrawLookup.cpp | 1 + .../src/Driver/Draw/DrawRegisterKey.cpp | 2 +- .../src/Driver/Shaders/ShaderRegistry.cpp | 24 ++++++++++++++++- .../Execution/src/VulkanDevice.cpp | 5 ++++ .../Graphics/include/Context.hpp | 1 + .../Graphics/include/Pipeline.hpp | 2 +- .../Graphics/include/State.hpp | 5 ++++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 5 +++- .../Graphics/src/ShaderValidation.cpp | 27 ++++++++++++++----- .../libSceAgcDriver/Graphics/src/State.cpp | 8 ++++-- core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 10 ++++--- core/shader/recompiler/Recompiler.hpp | 1 + .../src/ShaderInputInfoBuilder.cpp | 1 + docs/dev/debt/shader.md | 1 + 17 files changed, 83 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index ad90c2db7..d8b6055f0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Execution/include/QueueState.hpp" #include #include +#include #include #include #include @@ -84,6 +85,7 @@ struct ShaderSnapshot { std::shared_ptr prepared = std::make_shared(); std::shared_ptr registeredState; std::shared_ptr> workgroupUse = std::make_shared>(0u); + std::shared_ptr> positionVectorUse = std::make_shared>(0u); }; @@ -92,6 +94,9 @@ std::shared_ptr ProgramSnapshot(const ShaderRegistry& shad void NoteRawGraphicsDraw(); bool RawGraphicsDrawn(); bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset); +// The PA_CL_VS_OUT_CNTL position export bits a vertex program is compiled with: none for a program +// without pos1..pos3 exports, so its artifact does not depend on them. +std::uint32_t PositionExportLayout(const ShaderSnapshot* snapshot, std::size_t codeOffset, std::span code, std::uint32_t paClVsOutCntl); bool VertexWorkgroupRequired(const QueueState& queue, const ShaderRegistry& registry); std::shared_ptr PrepareShaderWithDiagnostics(const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::ShaderPreparationContext* preparation = nullptr); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index e8ecb282d..dafff997f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -145,6 +145,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe if (program.binary.stage == Stage::Fragment || roles[i] == Role::GeometryBack) return; decodeReads[i].clear(); vertexInfos[i] = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &decodeReads[i], false, 8u - program.firstUserSgpr); + if (vertexInfos[i]) vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207)); }; if (!registerKey) { for (std::size_t i = 0; i < programs.size(); ++i) decodeVertexInfo(i); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index e028edc5d..3df0685a7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -48,6 +48,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr code, std::uint32_t paClVsOutCntl) { + constexpr std::uint8_t Unknown = 0, Unused = 1, Used = 2; + const auto bits = paClVsOutCntl & Graphics::PositionExportLayoutBits; + if (bits == 0) return 0; + const bool cached = snapshot != nullptr && codeOffset == 0; + if (cached) { + const auto use = snapshot->positionVectorUse->load(std::memory_order_acquire); + if (use != Unknown) return use == Used ? bits : 0; + } + const auto decoded = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(code); + const bool used = std::any_of(decoded.instructions.begin(), decoded.instructions.end(), [](const auto& instruction) { + return instruction.family == ShaderRecompiler::RdnaInstructionFamily::EXP && instruction.exportTarget >= 0x0du && instruction.exportTarget <= 0x0fu && instruction.exportEnableMask != 0; + }); + if (cached) snapshot->positionVectorUse->store(used ? Used : Unused, std::memory_order_release); + return used ? bits : 0; +} + bool VertexWorkgroupRequired(const QueueState& queue, const ShaderRegistry& registry) { Graphics::NoteRegisterRead(Graphics::RegisterBank::Context, 0x2d5); @@ -676,7 +693,11 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap if (firstUser == 0) userCount += 8; } plan->userData.resize(userCount); - if (stage != Stage::Compute && stage != Stage::Fragment && snapshot.type != 6) vertex = Graphics::DecodeVertexStageInfo(snapshot.header, snapshot.headerAddress, plan->userData, nullptr, true, 8u - firstUser); + if (stage != Stage::Compute && stage != Stage::Fragment && snapshot.type != 6) { + vertex = Graphics::DecodeVertexStageInfo(snapshot.header, snapshot.headerAddress, plan->userData, nullptr, true, 8u - firstUser); + const auto outputs = state.context.find(0x207u); + if (vertex && outputs != state.context.end()) vertex->paClVsOutCntl = PositionExportLayout(&snapshot, codeOffset, std::span(snapshot.code).subspan(codeOffset), outputs->second); + } plan->swappc = {vertex.has_value(), firstUser, userCount}; if (deferred != nullptr) { if (const auto counter = ShaderRecompiler::UnresolvableSwappcTarget(plan->decoded, &plan->swappc)) { @@ -910,6 +931,7 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod const auto wave = fragment ? decoded.state.stages.fragmentWaveSize : decoded.state.stages.vertexWaveSize; std::optional vertex; if (!fragment) vertex = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true, 8u - program.firstUserSgpr); + if (vertex) vertex->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, decoded.state.paClVsOutCntl); pushOffset = Graphics::StagePushOffset(pushOffset, program.binary.stage, fixedSlots); const auto offset = pushOffset; const auto room = (fixedSlots && fragment ? Graphics::PipelinePushSlotBytes : capacity) - pushOffset % Graphics::PipelinePushSlotBytes; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 24584da02..acf0d2b6c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -269,6 +269,7 @@ struct VulkanDevice::State { bool geometryShader = false; bool sampleRateShading = false; bool shaderClipDistance = false; + bool shaderCullDistance = false; bool shaderClock = false; bool narrowSubgroupClock = false; bool float64DenormPreserve = false; @@ -1398,8 +1399,11 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->sampleRateShading = enabled.sampleRateShading == VK_TRUE; if (enabled.geometryShader) state->capabilities.push_back(spv::CapabilityGeometry); enabled.shaderClipDistance = available.shaderClipDistance; + enabled.shaderCullDistance = available.shaderCullDistance; state->shaderClipDistance = enabled.shaderClipDistance == VK_TRUE; + state->shaderCullDistance = enabled.shaderCullDistance == VK_TRUE; if (enabled.shaderClipDistance) state->capabilities.push_back(spv::CapabilityClipDistance); + if (enabled.shaderCullDistance) state->capabilities.push_back(spv::CapabilityCullDistance); if (enabled.shaderStorageImageWriteWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageWriteWithoutFormat); if (enabled.shaderStorageImageReadWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageReadWithoutFormat); state->storageImageWithoutFormat = enabled.shaderStorageImageWriteWithoutFormat == VK_TRUE && enabled.shaderStorageImageReadWithoutFormat == VK_TRUE; @@ -3128,6 +3132,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.geometryShader = state->geometryShader; context.sampleRateShading = state->sampleRateShading; context.shaderClipDistance = state->shaderClipDistance; + context.shaderCullDistance = state->shaderCullDistance; context.nullDescriptors = state->shaderProfile != nullptr && state->shaderProfile->NullDescriptors(); context.bufferInt64Atomics = state->bufferInt64Atomics; context.placeholderImages = state->placeholderImages.get(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 6ca5f6c43..8138cae99 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -161,6 +161,7 @@ struct Context { bool geometryShader = false; bool sampleRateShading = false; bool shaderClipDistance = false; + bool shaderCullDistance = false; bool nullDescriptors = false; bool bufferInt64Atomics = false; const PlaceholderImages* placeholderImages = nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 6e6ca7fdf..04264fefb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -123,7 +123,7 @@ bool ValidateSampleLocations(const Context& context, const State& state); void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment); bool ShadersWriteMemory(std::span shaders); // Returns the color attachment locations the pixel shader writes. -std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false, bool shaderClipDistance = false); +std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false, bool shaderClipDistance = false, bool shaderCullDistance = false); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index dd27811b7..bb6a54665 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -118,8 +118,13 @@ struct State { std::uint32_t samples = 1; std::array sampleLocations{}; bool customSampleLocations = false; + std::uint32_t paClVsOutCntl = 0; }; +// PA_CL_VS_OUT_CNTL bits that place position exports (clip and cull distances, the misc and the two +// distance vectors); a vertex program that exports pos1..pos3 depends on them. +inline constexpr std::uint32_t PositionExportLayoutBits = 0xffffu | (1u << 21u) | (1u << 22u) | (1u << 23u); + ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = false); State DecodeState(const QueueState& queue, bool vertexWorkgroup = false); std::array SampleLocations(const State& state); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 428976353..becd716ea 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -686,6 +686,9 @@ bool ValidationKey(const Context& context, std::span shade add(context.subgroup.supportedStages); add(context.subgroup.supportedOperations); add(context.fragmentShaderBarycentric); + add(context.shaderClipDistance); + add(context.shaderCullDistance); + add(state.paClVsOutCntl); add(state.stages.mesh.has_value()); if (state.stages.mesh) { const auto& mesh = *state.stages.mesh; @@ -737,7 +740,7 @@ std::set CachedFragmentOutputs(const Context& context, std::span< } std::set outputs; try { - outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics, context.shaderClipDistance); + outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics, context.shaderClipDistance, context.shaderCullDistance); } catch (const std::exception& error) { if (keyed) { std::lock_guard lock(validationMutex()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 685e1567b..3be732e88 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include #include +#include #include #include #include @@ -44,7 +45,8 @@ struct Module { std::map outputs; std::optional secondSource; bool position = false; - bool clipDistance = false; + std::uint32_t clipDistances = 0; + std::uint32_t cullDistances = 0; bool primitiveIndices = false; bool fragmentBarycentric = false; std::map> modes; @@ -111,9 +113,13 @@ struct Module { if (!input) position = true; return; } - if (value == spv::BuiltInClipDistance && !input && (vertex || stage == Stage::TessellationEvaluation)) { - Require(!clipDistance && Signature(type) == "f32[1]", "invalid or duplicate clip distance output"); - clipDistance = true; + // One gl_ClipDistance array holds the guest clip distances and the recompiler's all-zero position distance. + if (!input && ((value == spv::BuiltInClipDistance && (vertex || stage == Stage::TessellationEvaluation)) || (value == spv::BuiltInCullDistance && vertex))) { + const auto signature = Signature(type); + auto& count = value == spv::BuiltInClipDistance ? clipDistances : cullDistances; + Require(count == 0 && signature.size() == 6 && signature.starts_with("f32[") && signature[4] >= '1' && signature[4] <= '8' && signature[5] == ']', "invalid or duplicate clip or cull distance output"); + count = static_cast(signature[4] - '0'); + Require(clipDistances + cullDistances <= 8, "more than eight clip and cull distances in one shader"); return; } if (stage == Stage::TessellationControl || stage == Stage::TessellationEvaluation || stage == Stage::Mesh) { @@ -143,7 +149,7 @@ struct Module { } }; -Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance) { +Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance) { using Stage = ShaderRecompiler::ShaderStage; Require(compiled.program != nullptr, "missing compiled shader"); const auto& shader = *compiled.program; @@ -237,6 +243,12 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi fragment && capability == spv::CapabilityStencilExportEXT; + if ((vertex || evaluation) && (capability == spv::CapabilityClipDistance || capability == spv::CapabilityCullDistance)) { + char word[16]; + std::snprintf(word, sizeof(word), "0x%08x", state.paClVsOutCntl); + Require(capability == spv::CapabilityClipDistance ? shaderClipDistance : vertex && shaderCullDistance, std::string("vertex clip or cull distance needs shaderClipDistance or shaderCullDistance, which the device lacks (PA_CL_VS_OUT_CNTL=") + word + ")"); + } + // Enabled unconditionally or by the device setup in VulkanDevice. const bool isFeatureCapability = capability == spv::CapabilitySampled1D || @@ -248,6 +260,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi (fragment && sampleRateShading && capability == spv::CapabilitySampleRateShading) || capability == spv::CapabilityImageMSArray || capability == spv::CapabilityStorageImageMultisample || + (vertex && shaderCullDistance && capability == spv::CapabilityCullDistance) || capability == spv::CapabilityStorageImageWriteWithoutFormat || capability == spv::CapabilityStorageImageReadWithoutFormat || capability == spv::CapabilityInt64 || @@ -570,7 +583,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } -std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance) { +std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance) { using Stage = ShaderRecompiler::ShaderStage; const bool tessellation = state.stages.path == ShaderPath::Tessellation; const bool mesh = state.stages.path == ShaderPath::Geometry; @@ -591,7 +604,7 @@ std::set ValidateShaders(std::span shaders, Require(shaders[i].program != nullptr, "missing compiled shader"); Require(shaders[i].stage == expected, "graphics stage order disagrees"); for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shaders[i].stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "graphics resource uses a descriptor set other than its stage's"); - const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance); + const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance); if (i != 0) { for (const auto& [location, signature] : current.inputs) { const auto output = previous.outputs.find(location); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 1d72646e0..24ec02f0a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -100,6 +100,7 @@ VkConservativeRasterizationModeEXT decodeConservativeRasterization(const QueueSt // Render target index, viewport index and the misc export vector that carries them are accepted but // not routed: color targets are single-layer, so layered draws land in layer 0. constexpr std::uint32_t LayerExports = (1u << 18u) | (1u << 19u) | (1u << 21u) | (1u << 24u); +constexpr std::uint32_t ClipCullExports = 0xffffu | (1u << 22u) | (1u << 23u); constexpr std::uint32_t DepthControlMask = ~0x007007f0u; // EXEC_ON_HIER_FAIL / EXEC_ON_NOOP / EXEC_IF_OVERLAPPED (bits 9, 10, 17) only force the pixel shader // to run, which it always does here. @@ -698,7 +699,9 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { std::fprintf(stderr, "[gpu] layer/viewport index vertex exports are ignored (PA_CL_VS_OUT_CNTL=0x%08x)\n", read(cx, 0x207)); } } - zero(cx, 0x207, ~LayerExports, "clip distances, layer, viewport or auxiliary vertex exports"); + zero(cx, 0x207, ~(LayerExports | ClipCullExports), "layer, viewport or auxiliary vertex exports"); + Require(std::popcount(read(cx, 0x207) & 0xffffu) <= 8, "more than eight clip and cull distances are unsupported"); + result.paClVsOutCntl = read(cx, 0x207); result.samples = decodeSamples(cx); decodeSampleLocations(cx, result); { @@ -1184,7 +1187,8 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (value(queue.userConfig, 0x242, primitive) && (primitive & 0x3fu) != 1 && (primitive & 0x3fu) != 2 && (primitive & 0x3fu) != 3 && (primitive & 0x3fu) != 4 && (primitive & 0x3fu) != 5 && (primitive & 0x3fu) != 6) return "AGC graphics: primitive restart is only supported for point, line and triangle topologies"; if (value(cx, 0x103, resetIndex) && (resetIndex & 0xffffu) != 0xffffu) return "AGC graphics: primitive restart index other than all ones is unsupported"; } - if (auto reason = nonzero(cx, 0x207, ~LayerExports, "clip distances, layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason; + if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports), "layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason; + if (value(cx, 0x207, word) && std::popcount(word & 0xffffu) > 8) return require(false, "more than eight clip and cull distances are unsupported"); if (value(cx, 0x200, word)) { const auto renderControl = find(cx, 0x000); const bool depthClear = renderControl != cx.end() && (renderControl->second & 1u) != 0; diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 08f5d79c5..19c4d46bb 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -192,6 +192,7 @@ private: append(key, value.fetchAttribReg); append(key, value.fetchBufferReg); append(key, value.fetchEmbedded); + append(key, value.paClVsOutCntl); if (value.resourcesNum > value.resources.size()) throw std::runtime_error("Shader cache: invalid vertex resource count"); append(key, value.resourcesNum); for (std::uint32_t i = 0; i < value.resourcesNum; ++i) { diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 5202b9d20..46cc45c52 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -412,9 +412,10 @@ void writeVertexInfo(Writer& writer, const ShaderVertexStageInfo& info) { writer.WriteU32(info.fetchAttribReg); writer.WriteU32(info.fetchBufferReg); writer.WriteBool(info.fetchEmbedded); + writer.WriteU32(info.paClVsOutCntl); } -ShaderVertexStageInfo readVertexInfo(Reader& reader) { +ShaderVertexStageInfo readVertexInfo(Reader& reader, std::uint32_t version) { ShaderVertexStageInfo info{}; for (auto& resource : info.resources) { for (std::uint32_t& value : resource.fields) { @@ -431,6 +432,7 @@ ShaderVertexStageInfo readVertexInfo(Reader& reader) { info.fetchAttribReg = reader.ReadU32(); info.fetchBufferReg = reader.ReadU32(); info.fetchEmbedded = reader.ReadBool(); + if (version >= 20u) info.paClVsOutCntl = reader.ReadU32(); return info; } @@ -575,7 +577,7 @@ GuestContext readGuestContext(Reader& reader, DeserializedRequest& result, std:: context.pixel = result.pixel; } if (reader.ReadBool()) { - result.vertex = readVertexInfo(reader); + result.vertex = readVertexInfo(reader, version); context.vertex = result.vertex; } const auto regionCount = reader.ReadU64(); @@ -746,7 +748,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(19u); + writer.WriteU32(20u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -777,7 +779,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 19u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 20u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 79eafe04e..4ec0fc73f 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -171,6 +171,7 @@ struct ShaderVertexStageInfo { std::uint32_t fetchAttribReg; std::uint32_t fetchBufferReg; bool fetchEmbedded; + std::uint32_t paClVsOutCntl = 0; }; struct ShaderFloatMode { diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index ce067c491..3e80174f9 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -163,6 +163,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues vertexStorage.logicalStage = _toIrShaderStage(stage); vertexStorage.hostSubgroupSize = hostSubgroupSize; vertexStorage.fetchEmbedded = vertex.fetchEmbedded; + vertexStorage.paClVsOutCntl = vertex.paClVsOutCntl; vertexStorage.fetchExternal = false; vertexStorage.fetchAttribReg = static_cast(vertex.fetchAttribReg); vertexStorage.fetchBufferReg = static_cast(vertex.fetchBufferReg); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 5e05dc25c..9289c0983 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -13,6 +13,7 @@ - [s_subvector_loop_begin and s_subvector_loop_end](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge, which SPIR-V requires exactly once per loop header (SPIRV-Tools rejects the module and the NVIDIA driver 615.78.08 crashes compiling it); a loop whose back-edge block becomes unreachable in another way is not handled - [Vertex entry merged wave info](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) - `s3` of every vertex-path shader is set to `waveSize | waveSize << 8` (vertex and primitive count of a full wave, wave index 0), or to `32 | 32 << 8` for a wave64 program on a 32-lane host subgroup, which runs it at one lane per invocation and holds no lanes 32-63. On a device whose `subgroupSupportedStages` lacks the vertex stage (lavapipe, MoltenVK), vertex and local programs run as one-lane waves: `s3` is `1 | 1 << 8`, the lane id is 0, a ballot is the invocation's own bit, a shuffle or broadcast returns the invocation's value, an elect is true and a subgroup reduction is the value itself, so no `GroupNonUniform` capability is declared. Layout taken from the prologue Mesa's ACO emits for NGG vertex shaders (`s_bfe_u64 exec` from `s3`), not measured on hardware. The driver gives every graphics program user data from `s8`, so the condition does not tell NGG from legacy vertex shaders: they just never read `s3`. A draw whose last wave is partial still reports a full wave, and so does a host wave with idle invocations (an RTX 5070 ran every vertex warp of a non-indexed triangle list with 30 of its 32 invocations). - [s_barrier in vertex-path programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another; the `GS_ALLOC_REQ` such a barrier orders on NGG passthrough programs is already dropped on the vertex path. The Vertex execution model has no workgroup barrier. Programs with LDS, GDS or memory writes throw. Not measured on hardware +- [Vertex clip and cull distances](../../../core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp) (libSceAgcDriver) - `PA_CL_VS_OUT_CNTL` enable bit `s` of `CLIP_DIST_ENA`/`CULL_DIST_ENA` is taken to select component `s` of the `pos1`/`pos2` exports, with the Vulkan array index counted by the enabled bits below it, and `VS_OUT_CCDIST0_VEC_ENA` as bit 22; this follows the recompiler's existing position-export mapping, not hardware or documentation. The guest clip distances share one `gl_ClipDistance` array with the all-zero clip position distance, which takes the index after them, so a shader with eight guest clip and cull distances exceeds the eight the device holds and fails to compile. A vertex program whose own code has no `pos1`..`pos3` export is compiled and prepared without these bits (`PositionExportLayout`), so registration and draws agree on its artifact; an export in a subroutine it calls is not seen by that scan ### Functional From a72dcb93aa5c6def2cf6aed2cf9ea6f6f8db0616 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Mon, 12 Oct 2026 18:47:58 +0200 Subject: [PATCH 2/2] test(agc): clip and cull distances drawn through the device and the driver Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 8 + .../prx/libSceAgcDriver/tests/Graphics.cpp | 54 ++++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 119 ++++++++- .../execution/VertexClipCullDistances.cpp | 176 +++++++++++++ .../tests/execution/VertexClipCullDraw.cpp | 239 ++++++++++++++++++ 5 files changed, 594 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDraw.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 4b1fc3d98..371ccc8c2 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1277,6 +1277,14 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + # The driver draw test again with the draw key off: the draw cache then keys on the decoded vertex + # stage, which must include PA_CL_VS_OUT_CNTL. + add_test(NAME agc_driver_vertex_clip_cull_draw_no_key COMMAND agc_driver_vertex_clip_cull_draw_tests no-key) + set_tests_properties(agc_driver_vertex_clip_cull_draw_no_key PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_DRAW_KEY=1" SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_driver_vertex_clip_cull_draw_no_key PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_pm4_failure COMMAND agc_driver_pm4_tests failure) set_tests_properties(agc_driver_pm4_failure PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77) add_test(NAME agc_descriptor_pool_reset_failure COMMAND agc_driver_graphics_tests --descriptor-pool-reset-failure) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 7bfbc5a0d..95fff1142 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2248,6 +2248,18 @@ void multisampleTests() { Require(TakeMultisampledClear(clearedCmask, pending, halfFloat, clear) && clear.float32[0] == 1.0f && !TakeMultisampledClear(clearedCmask, pending, halfFloat, clear), "a pending CMASK clear and a whole fill did not clear once to the CMASK clear color"); } +void clipDistanceTests() { + auto queue = makeState(); + queue.context[0x207] = 0x0040000f; + Require(AgcDriver::Graphics::DecodeState(queue).paClVsOutCntl == 0x0040000fu, "DecodeState dropped the PA_CL_VS_OUT_CNTL value the validator quotes"); + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "four clip distances were rejected by the precheck"); + queue.context[0x207] = 0x004001ff; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "more than eight clip and cull distances"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("more than eight") != std::string::npos, "nine clip distances passed the precheck"); +} + void DepthClipTests() { auto queue = makeState(); const auto direct = AgcDriver::Graphics::DecodeState(queue); @@ -3356,6 +3368,8 @@ struct ModuleShape { bool fragDepth = false; std::uint32_t sampleMaskLength = 0; std::uint32_t floatControlsWidth = 0; + std::uint32_t clipDistanceLength = 0; + std::uint32_t cullDistanceLength = 0; }; void emit(std::vector& out, spv::Op op, std::initializer_list operands) { @@ -3455,6 +3469,30 @@ std::vector makeModule(const ModuleShape& shape) { emit(annotations, spv::OpDecorate, {variable, spv::DecorationBuiltIn, spv::BuiltInSampleMask}); extraInterface.push_back(variable); } + if (shape.clipDistanceLength != 0) { + const auto length = id(); + const auto array = id(); + const auto pointer = id(); + const auto variable = id(); + emit(declarations, spv::OpConstant, {uintType, length, shape.clipDistanceLength}); + emit(declarations, spv::OpTypeArray, {array, floatType, length}); + emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassOutput, array}); + emit(declarations, spv::OpVariable, {pointer, variable, spv::StorageClassOutput}); + emit(annotations, spv::OpDecorate, {variable, spv::DecorationBuiltIn, spv::BuiltInClipDistance}); + extraInterface.push_back(variable); + } + if (shape.cullDistanceLength != 0) { + const auto length = id(); + const auto array = id(); + const auto pointer = id(); + const auto variable = id(); + emit(declarations, spv::OpConstant, {uintType, length, shape.cullDistanceLength}); + emit(declarations, spv::OpTypeArray, {array, floatType, length}); + emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassOutput, array}); + emit(declarations, spv::OpVariable, {pointer, variable, spv::StorageClassOutput}); + emit(annotations, spv::OpDecorate, {variable, spv::DecorationBuiltIn, spv::BuiltInCullDistance}); + extraInterface.push_back(variable); + } if (shape.perVertex) { const auto length = id(); const auto array = id(); @@ -3524,6 +3562,8 @@ std::vector makeModule(const ModuleShape& shape) { emit(words, spv::OpCapability, {spv::CapabilityShader}); if (shape.sampleId) emit(words, spv::OpCapability, {spv::CapabilitySampleRateShading}); if (shape.layer) emit(words, spv::OpCapability, {spv::CapabilityGeometry}); + if (shape.clipDistanceLength != 0) emit(words, spv::OpCapability, {spv::CapabilityClipDistance}); + if (shape.cullDistanceLength != 0) emit(words, spv::OpCapability, {spv::CapabilityCullDistance}); if (shape.barycentric) { emit(words, spv::OpCapability, {spv::CapabilityFragmentBarycentricKHR}); const std::string extension = "SPV_KHR_fragment_shader_barycentric"; @@ -4814,6 +4854,19 @@ void validationTests() { expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, true, false); }, "unsupported device capability 35"); pixel.spirv = makeModule({.fragment = true, .sampleMaskLength = 2}); expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported fragment built-in"); + pixel.spirv = makeModule({.fragment = true}); + vertex.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 4, .cullDistanceLength = 4}); + AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, true, true); + state.paClVsOutCntl = 0x0040000fu; + vertex.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 4}); + expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, true); }, "PA_CL_VS_OUT_CNTL=0x0040000f"); + AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, true, false); + vertex.spirv = makeModule({.parameterOutput = true, .cullDistanceLength = 4}); + expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, true, false); }, "PA_CL_VS_OUT_CNTL=0x0040000f"); + AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, true); + state.paClVsOutCntl = 0; + vertex.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 9}); + expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, true, true); }, "clip or cull distance"); vertex.spirv = makeModule({.parameterOutput = true, .sampleId = true}); expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported device capability 35"); vertex.spirv = makeModule({.parameterOutput = true}); @@ -5392,6 +5445,7 @@ int main(int argc, char** argv) { uint8x4TargetTests(); multisampleTests(); MsaaEnableSingleSampleTests(); + clipDistanceTests(); ShaderStageTests(); NggVertexWorkgroupTests(); TuningFieldTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index e032bead1..46a4f373c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -12,6 +12,7 @@ #include "Optimization/SrtWalker/SrtFlatSlotClasses.hpp" #include "Optimization/SrtWalker.hpp" #include "IntermediateRepresentation/IrBuilder.hpp" +#include "IntermediateRepresentation/IrProgram.hpp" #include "SpirvBackend/SpirvAnalysis.hpp" #if ANYPS5_ENABLE_SPIRV_TOOLS #include "SpirvBackend/SpirvOptimizer.hpp" @@ -1660,6 +1661,22 @@ void verifyMeshConfiguration() { request.graphics = GraphicsCompileContext{0u, {}, mesh, std::nullopt, {}}; const auto replay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(request)); require(replay.request.graphics.has_value() && replay.request.graphics->mesh.has_value() && replay.request.graphics->mesh->esgsItemSize == 12u && replay.request.graphics->mesh->primitivesPerGroup == 21u, "mesh configuration was lost in serialization"); + { + RecompileRequest vertex{}; + vertex.shader = {ShaderStage::Vertex, 0x10000u, code, 0, {}}; + vertex.context.waveSize = 64; + ShaderVertexStageInfo info{}; + info.paClVsOutCntl = 0x0040000fu; + vertex.context.vertex = info; + const auto back = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(vertex)); + require(back.request.context.vertex.has_value() && back.request.context.vertex->paClVsOutCntl == 0x0040000fu, "PA_CL_VS_OUT_CNTL was lost in serialization"); + std::vector clipped; + RecompileCacheKey::Build(vertex, clipped); + vertex.context.vertex->paClVsOutCntl = 0; + std::vector unclipped; + RecompileCacheKey::Build(vertex, unclipped); + require(clipped != unclipped, "PA_CL_VS_OUT_CNTL is not part of the recompile cache key"); + } std::vector key; RecompileCacheKey::Build(request, key); const auto first = key; @@ -1809,13 +1826,13 @@ void verifyPixelRequestSerialization() { minimal.context.pixel = ShaderPixelStageInfo{}; minimal.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQRMAAAA=", "new requests did not use serialization version 19"); + require(requestPrefix(encoded, 8u) == "NVNQQRQAAAA=", "new requests did not use serialization version 20"); require(serializer.Deserialize(encoded).request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle, "the pixel primitive class did not round-trip"); constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; for (std::size_t bytes = 0; bytes < 20u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag or primitive class was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRQAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRUAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -1829,6 +1846,102 @@ void verifyPixelRequestSerialization() { require(version18.request.target.registrationModules && version18.request.target.registrationWordLimit == 1234u && version18.request.target.vertexSubgroups, "a version 18 request trailer was misread"); } +void verifyPositionExportComponents() { + constexpr std::uint32_t none = 0xffffffffu; + struct Case { + std::uint32_t control; + std::array clip; + std::array cull; + }; + const std::array cases{{ + {0x00400000u, {none, none, none, none}, {none, none, none, none}}, + {0x00400001u, {0u, none, none, none}, {none, none, none, none}}, + {0x00400002u, {none, 0u, none, none}, {none, none, none, none}}, + {0x00400005u, {0u, none, 1u, none}, {none, none, none, none}}, + {0x00400100u, {none, none, none, none}, {0u, none, none, none}}, + {0x00400400u, {none, none, none, none}, {none, none, 0u, none}}, + }}; + for (const auto& test : cases) { + for (std::uint32_t component = 0; component < 4; ++component) { + const auto output = ShaderRecompiler::DecodePositionExportComponent(test.control, 1u, component); + require(output.clipDistance == test.clip[component] && output.cullDistance == test.cull[component], "PA_CL_VS_OUT_CNTL decoded a position component to the wrong clip or cull distance"); + } + } +} + +std::vector decodeBase64(std::string_view text) { + constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::vector bytes; + std::uint32_t chunk = 0; + std::size_t bits = 0; + for (const char symbol : text) { + if (symbol == '=') break; + const auto value = alphabet.find(symbol); + require(value != std::string_view::npos, "invalid base64 symbol"); + chunk = ((chunk << 6u) | static_cast(value)) & 0xffffffu; + bits += 6u; + if (bits >= 8u) { + bits -= 8u; + bytes.push_back(static_cast((chunk >> bits) & 0xffu)); + } + } + return bytes; +} + +std::string encodeBase64(const std::vector& bytes) { + constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string text; + for (std::size_t index = 0; index < bytes.size(); index += 3u) { + const std::size_t count = std::min(3u, bytes.size() - index); + std::uint32_t chunk = static_cast(bytes[index]) << 16u; + if (count > 1u) chunk |= static_cast(bytes[index + 1u]) << 8u; + if (count > 2u) chunk |= bytes[index + 2u]; + for (std::size_t symbol = 0; symbol < 4u; ++symbol) { + text.push_back(symbol <= count ? alphabet[(chunk >> (18u - 6u * symbol)) & 0x3fu] : '='); + } + } + return text; +} + +void verifyVertexInfoVersion19() { + using namespace ShaderRecompiler; + const std::array code{0xbf810000u}; + const RequestSerializer serializer; + RecompileRequest vertex{}; + vertex.shader = {ShaderStage::Vertex, 0x10000u, code, 0, {}}; + vertex.context.waveSize = 64; + vertex.context.vertex = ShaderVertexStageInfo{}; + const auto unmarked = serializer.Serialize(vertex); + vertex.context.vertex->paClVsOutCntl = 0x89abcdefu; + const auto marked = serializer.Serialize(vertex); + const auto plain = decodeBase64(unmarked); + const auto flagged = decodeBase64(marked); + require(plain.size() == flagged.size(), "PA_CL_VS_OUT_CNTL changed the request size"); + std::size_t offset = 0; + while (offset < plain.size() && plain[offset] == flagged[offset]) ++offset; + require(offset + 4u <= plain.size() && flagged[offset] == 0xefu && flagged[offset + 1u] == 0xcdu && flagged[offset + 2u] == 0xabu && flagged[offset + 3u] == 0x89u, "PA_CL_VS_OUT_CNTL is not the four bytes the version 20 vertex info adds"); + require(std::equal(plain.begin() + offset + 4u, plain.end(), flagged.begin() + offset + 4u), "PA_CL_VS_OUT_CNTL moved the fields after it"); + auto legacy = plain; + legacy.erase(legacy.begin() + offset, legacy.begin() + offset + 4u); + for (const auto version : {16u, 17u, 18u, 19u}) { + legacy[4] = static_cast(version); + const auto replay = serializer.Deserialize(encodeBase64(legacy)); + require(replay.request.context.vertex.has_value() && replay.request.context.vertex->paClVsOutCntl == 0u, "a legacy vertex info read a PA_CL_VS_OUT_CNTL word"); + require(serializer.Serialize(replay.request) == unmarked, "a legacy vertex info misread the fields after it"); + } + RecompileRequest pixel{}; + pixel.shader = {ShaderStage::Fragment, 0x20000u, code, 0, {}}; + pixel.context.waveSize = 64; + pixel.context.pixel = ShaderPixelStageInfo{}; + pixel.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle; + const auto encodedPixel = serializer.Serialize(pixel); + auto legacyPixel = decodeBase64(encodedPixel); + legacyPixel[4] = 19u; + const auto replayPixel = serializer.Deserialize(encodeBase64(legacyPixel)); + require(replayPixel.request.context.pixel.has_value() && replayPixel.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle, "a main version 19 request lost its pixel primitive class"); + require(serializer.Serialize(replayPixel.request) == encodedPixel, "a main version 19 request misread the fields after its pixel primitive class"); +} + void verifyLegacyPixelRequests() { using namespace ShaderRecompiler; static constexpr std::array legacyPixelRequests{ @@ -3361,6 +3474,8 @@ int main(int argc, char** argv) { verifyMeshConfiguration(); verifyPixelInputs(); verifyPixelRequestSerialization(); + verifyVertexInfoVersion19(); + verifyPositionExportComponents(); verifyLegacyPixelRequests(); verifyPixelExportReplay(); verifyPixelParameterSlots(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp new file mode 100644 index 000000000..fe7d3b448 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp @@ -0,0 +1,176 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 192; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t DistanceVector = 0x00400000u; +alignas(256) std::array Pixels{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xe0382010, 0x80000405, 0xbf8c3f70, + 0xf80000cf, 0x03020100, 0xf80008df, 0x07060504, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0002f2, 0x7e0202f2, 0x7e0402f2, 0x7e0602f2, 0xf800080f, 0x03020100, 0xbf810000, +}; + +struct Vertex { + std::array position; + std::array distances; +}; + +constexpr std::array PositionX{-3.0f, 1.0f, 1.0f}; +constexpr std::array PositionY{-1.0f, -1.0f, 3.0f}; +constexpr std::array Negative{-1.0f, -1.0f, -1.0f}; +constexpr std::array Mixed{-1.0f, 1.0f, -1.0f}; + +enum class Visible { All, None, X, Y, XAndY }; + +struct Row { + const char* name; + std::uint32_t control; + Visible visible; + std::array component0; + std::array component1; + std::array component2; +}; + +const std::array Rows{{ + {"no enable bits", DistanceVector, Visible::All, Negative, Negative, Negative}, + {"clip plane 0 from component 0", DistanceVector | 0x1u, Visible::X, PositionX, Negative, Negative}, + {"clip plane 1 alone, from component 1", DistanceVector | 0x2u, Visible::Y, Negative, PositionY, Negative}, + {"clip planes 0x5, components 0 and 2", DistanceVector | 0x5u, Visible::XAndY, PositionX, Negative, PositionY}, + {"cull plane 0, every vertex negative", DistanceVector | 0x100u, Visible::None, Negative, Negative, Negative}, + {"cull plane 0, one vertex not negative", DistanceVector | 0x100u, Visible::All, Mixed, Negative, Negative}, + {"cull plane 0 from component 2, sparse 0x4", DistanceVector | 0x400u, Visible::None, Mixed, Negative, Negative}, +}}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +bool Expected(Visible visible, std::uint32_t pixel) { + const double x = -1.0 + 2.0 * ((pixel % Width) + 0.5) / Width; + const double y = 1.0 - 2.0 * ((pixel / Width) + 0.5) / Height; + switch (visible) { + case Visible::All: return true; + case Visible::None: return false; + case Visible::X: return x >= 0.0; + case Visible::Y: return y >= 0.0; + case Visible::XAndY: return x >= 0.0 && y >= 0.0; + } + return false; +} + +void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const Row& row) { + Pixels.fill(std::byte{0}); + std::array vertices{}; + for (std::size_t i = 0; i < vertices.size(); ++i) { + vertices[i] = {{PositionX[i], PositionY[i], 0.5f, 1.0f}, {row.component0[i], row.component1[i], row.component2[i], 0.0f}}; + } + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(vertices.data(), sizeof(Vertex), static_cast(vertices.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::ShaderVertexStageInfo vertexInfo{}; + vertexInfo.paClVsOutCntl = row.control; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, vertexInfo, vertexMemory}, + device.Target(), + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = waveSize == 32u; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, + {waveSize, 0, std::vector(4, 0u), std::nullopt, pixel, std::nullopt, pixelMemory}, + device.Target(), + PixelPushLayout(vertexPush, device.Target()) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, device.Target())} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.paClVsOutCntl = row.control; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + 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(vertices.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(const Row& row, std::uint32_t waveSize) { + const auto what = std::string("vertex clip and cull distances, ") + row.name + ", wave" + std::to_string(waveSize); + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const auto value = std::to_integer(Pixels[pixel * 4u]); + Require(value == 0u || value == 255u, what + ": pixel " + std::to_string(pixel) + " stored " + std::to_string(value)); + const bool written = value == 255u; + const bool expected = Expected(row.visible, pixel); + Require(written == expected, what + ": pixel (" + std::to_string(pixel % Width) + ", " + std::to_string(pixel / Width) + ") is " + (written ? "written" : "clear") + ", expected " + (expected ? "written" : "clear")); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (device->Target().subgroupSize < 32u) { + std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + for (const auto waveSize : {64u, 32u}) { + for (const auto& row : Rows) { + Draw(*device, waveSize, row); + Check(row, waveSize); + } + } + std::puts("vertex clip and cull distance tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDraw.cpp new file mode 100644 index 000000000..0eb45fd95 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDraw.cpp @@ -0,0 +1,239 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Width = 192; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t DistanceVector = 0x00400000u; +constexpr std::uint32_t SetContextReg = 0x69u; +constexpr std::uint32_t SetShReg = 0x76u; +constexpr std::uint32_t SetUconfigReg = 0x79u; +constexpr std::uint32_t DrawIndexAuto = 0x2du; + +constexpr std::size_t Block = 65536; +constexpr std::size_t ColorBytes = Width * Height * 4; +std::byte* ColorMemory = nullptr; +alignas(64) volatile std::uint32_t EndOfPipe = 0; + +std::byte* AllocateWatched(std::size_t bytes) { +#ifdef _WIN32 + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (raw == MAP_FAILED) throw std::runtime_error("cannot map the watched block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast(aligned + bytes), begin + Block - aligned); + void* block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); +#endif + return static_cast(block); +} + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80020005, 0xe0382010, 0x80020405, 0xbf8c3f70, + 0xf80000cf, 0x03020100, 0xf80008df, 0x07060504, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0002f2, 0x7e0202f2, 0x7e0402f2, 0x7e0602f2, 0xf800080f, 0x03020100, 0xbf810000, +}; + +struct Vertex { + std::array position; + std::array distances; +}; + +constexpr std::array PositionX{-3.0f, 1.0f, 1.0f}; +constexpr std::array PositionY{-1.0f, -1.0f, 3.0f}; +constexpr std::array Negative{-1.0f, -1.0f, -1.0f}; +constexpr std::array Mixed{-1.0f, 1.0f, -1.0f}; + +enum class Visible { All, None, X, Y }; + +struct Step { + const char* name; + std::uint32_t control; + Visible visible; + std::array component0; + std::array component1; + std::array component2; +}; + +const std::array Steps{{ + {"no enable bits", DistanceVector, Visible::All, Negative, Negative, Negative}, + {"clip plane 0 from component 0", DistanceVector | 0x1u, Visible::X, PositionX, Negative, Negative}, + {"cull plane 0 from component 2, not every vertex negative", DistanceVector | 0x400u, Visible::All, Negative, Negative, Mixed}, + {"clip plane 1 alone, from component 1", DistanceVector | 0x2u, Visible::Y, Negative, PositionY, Negative}, +}}; + +struct RegisteredShader { + Shader shader; + ShaderUserData userData; +}; +RegisteredShader VertexShader{}; +RegisteredShader PixelShader{}; + +const std::vector> ContextBaseline{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, + {0x81, 0x80000000}, {0x90, 0x80000000}, {0x94, 0x80000000}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, {0x114, 0}, {0x113, std::bit_cast(1.0f)}, +}; + +void Register(RegisteredShader& registered, const void* code, std::uint32_t bytes, std::uint8_t type) { + registered.shader.file_header = 0x34333231u; + registered.shader.version = 0x18u; + registered.shader.user_data = ®istered.userData; + registered.shader.code = code; + registered.shader.header_size = sizeof(RegisteredShader); + registered.shader.shader_size = bytes; + registered.shader.type = type; + AgcDriverRegisterShader_nid_postfix(®istered.shader); +} + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void SetRegister(std::vector& words, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) { + words.push_back(0xc0010000u | (opcode << 8u)); + words.push_back(offset); + words.push_back(value); +} + +std::vector Frame(const Step& step, const std::array& vertices) { + const auto colorAddress = reinterpret_cast(ColorMemory); + const auto vertexAddress = reinterpret_cast(VertexCode.data()); + const auto pixelAddress = reinterpret_cast(PixelCode.data()); + std::vector words; + for (const auto& [offset, value] : ContextBaseline) SetRegister(words, SetContextReg, offset, value); + SetRegister(words, SetContextReg, 0x207, step.control); + SetRegister(words, SetContextReg, 0x318, static_cast(colorAddress >> 8u)); + SetRegister(words, SetContextReg, 0x390, static_cast(colorAddress >> 40u)); + SetRegister(words, SetContextReg, 0x3b0, ((Width - 1u) << 14u) | (Height - 1u)); + SetRegister(words, SetContextReg, 0x10f, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x110, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x111, std::bit_cast(-(Height / 2.0f))); + SetRegister(words, SetContextReg, 0x112, std::bit_cast(Height / 2.0f)); + const auto extent = (Height << 16u) | Width; + SetRegister(words, SetContextReg, 0xd, extent); + SetRegister(words, SetContextReg, 0x82, extent); + SetRegister(words, SetContextReg, 0x91, extent); + SetRegister(words, SetContextReg, 0x95, extent); + SetRegister(words, SetUconfigReg, 0x242, 4u); + SetRegister(words, SetShReg, 0x008, static_cast(pixelAddress >> 8u)); + SetRegister(words, SetShReg, 0x009, static_cast(pixelAddress >> 40u)); + SetRegister(words, SetShReg, 0x00b, 0u); + SetRegister(words, SetShReg, 0x0c8, static_cast(vertexAddress >> 8u)); + SetRegister(words, SetShReg, 0x0c9, static_cast(vertexAddress >> 40u)); + SetRegister(words, SetShReg, 0x08b, 4u << 1u); + const auto descriptor = BufferDescriptor(vertices.data(), sizeof(Vertex), static_cast(vertices.size())); + for (std::uint32_t i = 0; i < descriptor.size(); ++i) SetRegister(words, SetShReg, 0x08c + i, descriptor[i]); + words.insert(words.end(), {0xc0000000u | (1u << 16u) | (DrawIndexAuto << 8u), static_cast(vertices.size()), 2u}); + const auto labelAddress = reinterpret_cast(&EndOfPipe); + words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast(labelAddress), static_cast(static_cast(labelAddress) >> 32u), 1u, 0u, 0u}); + return words; +} + +void Check(const Step& step, const std::vector& colors) { + const auto what = std::string("vertex clip and cull distances through the driver, ") + step.name; + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const auto value = std::to_integer(colors[pixel * 4u]); + Require(value == 0u || value == 255u, what + ": pixel " + std::to_string(pixel) + " stored " + std::to_string(value)); + const double x = -1.0 + 2.0 * ((pixel % Width) + 0.5) / Width; + const double y = 1.0 - 2.0 * ((pixel / Width) + 0.5) / Height; + const bool expected = step.visible == Visible::All || (step.visible == Visible::X && x >= 0.0) || (step.visible == Visible::Y && y >= 0.0); + const bool written = value == 255u; + Require(written == expected, what + ": pixel (" + std::to_string(pixel % Width) + ", " + std::to_string(pixel / Width) + ") is " + (written ? "written" : "clear") + ", expected " + (expected ? "written" : "clear")); + } +} + + +// A program that exports only pos0 is compiled without the PA_CL_VS_OUT_CNTL position export bits, at +// registration and at draw alike; one that exports pos1 keeps them, without the bits outside the layout. +void PositionExportLayoutTests() { + using AgcDriver::DriverDetail::PositionExportLayout; + constexpr std::array PositionOnly{0xf80008cfu, 0x03020100u, 0xbf810000u}; + Require(PositionExportLayout(nullptr, 0, VertexCode, 0x0040000fu) == 0x0040000fu, "a program exporting pos1 lost its position export bits"); + Require(PositionExportLayout(nullptr, 0, VertexCode, 0x0144000fu) == 0x0040000fu, "bits outside the position export layout reached the vertex info"); + Require(PositionExportLayout(nullptr, 0, PositionOnly, 0x0040000fu) == 0u, "a program exporting only pos0 kept the position export bits"); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{0x10000u, 0u, 1u, {PositionOnly.begin(), PositionOnly.end()}, {}}; + for (int use = 0; use < 2; ++use) Require(PositionExportLayout(&snapshot, 0, snapshot.code, 0x0040000fu) == 0u, "a cached pos0-only program kept the position export bits"); + snapshot.code.assign(VertexCode.begin(), VertexCode.end()); + Require(PositionExportLayout(&snapshot, 2, std::span(snapshot.code).subspan(2), 0x0040000fu) == 0x0040000fu, "a program at an offset was answered from the cache of its snapshot"); +} + +} + +int main(int argc, char** argv) { + try { + const bool noKey = argc > 1 && std::string(argv[1]) == "no-key"; + if (noKey) Require(std::getenv("APS5_NO_DRAW_KEY") != nullptr, "the no-key run must start with APS5_NO_DRAW_KEY set"); + PositionExportLayoutTests(); + ColorMemory = AllocateWatched(2 * Block); + Register(VertexShader, VertexCode.data(), sizeof(VertexCode), 2); + Register(PixelShader, PixelCode.data(), sizeof(PixelCode), 1); + for (const auto& step : Steps) { + std::array vertices{}; + for (std::size_t i = 0; i < vertices.size(); ++i) { + vertices[i] = {{PositionX[i], PositionY[i], 0.5f, 1.0f}, {step.component0[i], step.component1[i], step.component2[i], 0.0f}}; + } + std::fill_n(ColorMemory, ColorBytes, std::byte{0}); + EndOfPipe = 0; + auto words = Frame(step, vertices); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, std::string("DCB submission failed for ") + step.name); + AgcDriverWaitIdle_nid_postfix(); + Require(EndOfPipe == 1u, std::string("the end-of-pipe label was not written for ") + step.name); + std::vector colors(ColorBytes); + AgcDriver::GuestMemory::Read(static_cast(reinterpret_cast(ColorMemory)), colors); + Check(step, colors); + } + AgcDriverShutdown_nid_postfix(); + std::puts(noKey ? "vertex clip and cull distance draws passed without the draw key" : "vertex clip and cull distance draws passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +}