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] 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