From 53212b18ac1c7562bddb73c352f1663fc348a5c0 Mon Sep 17 00:00:00 2001 From: Mouad-Scriptz-0 Date: Mon, 12 Oct 2026 12:28:08 +0000 Subject: [PATCH 01/14] feat(agc): remap a viewport depth range the device cannot express into the vertex shader Without VK_EXT_depth_range_unrestricted a viewport depth range outside [0, 1] threw. Minecraft (PPSA17221) draws its world with scale 1 and offset 0 in negative-one-to-one clip space, so its depth is the raw z/w clamped by PA_SC_VPORT_ZMIN/ZMAX. On such a device the vertex shader now scales clip z so z/w is the console's depth, the pipeline clamps depth at the console's bounds, and two clip distances restore the near and far planes that depth clamping removes. Mode, scale and offset travel as pipeline specialization constants from the draw's vertex stage info, in both the per-draw and the registration-module path; mesh and tessellation draws with such a range still throw. The console's depth clamp bounds are clipped to [0, 1]: exact for a normalized depth target or none, and reported once for a tested or written float depth target, where a depth below 0 is stored as 0 (Minecraft clamps its float depth target to [-1, 1]). The shader validator accepts the remap's two extra clip distances next to the one a program may already export. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 14 +++ .../Execution/include/Driver/Driver.hpp | 4 +- .../Execution/include/VulkanDevice.hpp | 8 +- .../Execution/src/Driver/Draw/Draw.cpp | 10 +- .../Execution/src/Driver/Draw/DrawDecode.cpp | 7 +- .../src/Driver/Draw/DrawRegisterKey.cpp | 2 + .../Execution/src/VulkanDevice.cpp | 1 + .../Graphics/include/State.hpp | 11 ++ .../libSceAgcDriver/Graphics/src/State.cpp | 27 +++++ .../libSceAgcDriver/tests/DepthRangeRemap.cpp | 110 ++++++++++++++++++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 51 ++++++++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 8 +- core/shader/recompiler/CacheKey.hpp | 4 + .../ControlFlow/src/RequestSerializer.cpp | 19 ++- .../Optimization/ShaderStageInputInfo.hpp | 7 ++ .../recompiler/PipelineSpecialization.hpp | 2 + core/shader/recompiler/Recompiler.cpp | 12 +- core/shader/recompiler/Recompiler.hpp | 4 + .../include/SpirvBackend/SpirvEmitter.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 2 + .../SpirvBackend/src/SpirvEmitter.cpp | 1 + .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 6 + .../SpirvBackend/src/SpirvModuleEmitter.cpp | 50 ++++++++ .../src/ShaderInputInfoBuilder.cpp | 1 + .../tests/RequestSerializerTests.cpp | 16 +-- docs/dev/debt/libSceAgcDriver.md | 1 + 26 files changed, 356 insertions(+), 23 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index fdf3f37b5c..671b222736 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2536,6 +2536,20 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_depth_range_remap_tests EXCLUDE_FROM_ALL tests/DepthRangeRemap.cpp) +target_link_libraries(agc_depth_range_remap_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_depth_range_remap_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_depth_range_remap_tests) +if(BUILD_TESTING) + set_target_properties(agc_depth_range_remap_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_depth_range_remap COMMAND agc_depth_range_remap_tests) + set_tests_properties(agc_depth_range_remap PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_depth_range_remap PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_position_invariance_tests EXCLUDE_FROM_ALL tests/PositionInvariance.cpp) target_link_libraries(agc_position_invariance_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_position_invariance_tests PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 84c2a17592..efb14f5fb9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -157,8 +157,8 @@ private: void reportDrawRelocation(); static bool sameVertexInfo(const ShaderRecompiler::ShaderVertexStageInfo& a, const ShaderRecompiler::ShaderVertexStageInfo& b); static bool sameDecode(const DrawDecode& a, const DrawDecode& b); - std::shared_ptr decodeDraw(const QueueState& queue, const Submission& submission); - void resolveDrawDecode(const QueueState& queue, const Submission& submission, std::shared_ptr& decode, bool registerKey, std::uint64_t drawKey, bool profile); + std::shared_ptr decodeDraw(const QueueState& queue, const Submission& submission, const Graphics::Context& context); + void resolveDrawDecode(const QueueState& queue, const Submission& submission, const Graphics::Context& context, std::shared_ptr& decode, bool registerKey, std::uint64_t drawKey, bool profile); void lookupDraw(const Submission& submission, const std::shared_ptr& localDevice, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector& programs, const std::vector& roles, const std::vector>& vertexInfos, bool useDrawEntries, bool registerKey, bool profile, std::uint64_t& drawKey, std::shared_ptr& entry, std::vector>& matched, std::vector>& matchedRegions, bool& drawHit, bool& verifyHit, DrawPhaseTiming& phaseTiming, std::array& phaseMs, const std::shared_ptr& dataEntry, DrawDataCandidates& dataCandidates, std::vector& stageData); std::shared_ptr materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector>& vertexInfos, std::vector& memory, const std::vector& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr& localDevice, ShaderMemory& shaderMemory, std::vector& stageCaptures, std::vector& recompiled, bool drawHit, const std::vector>& matched, const std::vector>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array& phaseMs, std::string& rejected); static void recordStageDataPositions(DispatchVariant& variant, const ShaderRecompiler::ResourceCapture& capture, const std::vector& decodeReads); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index cc6d4a0e28..8fcbd334dd 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -331,9 +331,7 @@ public: void EnqueueDraw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots = {}); bool PrebuildGraphicsLibraries(const Graphics::ShaderStages& stages, std::span shaders); -private: - VulkanDevice(const PresentationWindow* window, bool compileOnly); - friend struct VulkanDeviceTestAccess; +public: // The device's Graphics::Context: a copy of the one built at setup (its instance functions // resolved then, its function table filled then), or with APS5_NO_CONTEXT_CACHE=1 built anew. Graphics::Context graphicsContext() const; @@ -341,6 +339,10 @@ private: std::uint32_t computeSubgroupSize(const ShaderRecompiler::RecompileResult& shader) const; VkPipelineShaderStageCreateFlags computeStageFlags(const ShaderRecompiler::RecompileResult& shader) const; std::shared_ptr createComputePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress); + +private: + VulkanDevice(const PresentationWindow* window, bool compileOnly); + friend struct VulkanDeviceTestAccess; ShaderRecompiler::SpirvTarget buildTarget() const; Graphics::Context buildContext() const; // Body of Dispatch and DispatchIndirect: `arguments` 0 dispatches x, y, z groups. 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 71066bbe9a..048c9322d5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -8,6 +8,7 @@ #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "Optimization/ResourceProgram.hpp" #include +#include #include #include #include @@ -162,7 +163,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe if (program.merged) InitializeMerged(queue, program, program.mergedPointer, program.mergedPointerRequired, false); } if (verifyDrawDataHits()) { - const auto full = decodeDraw(queue, submission); + const auto full = decodeDraw(queue, submission, localDevice->graphicsContext()); if (!sameDecode(*refreshed, *full)) { std::size_t differing = 0; while (differing < full->programs.size() && differing < refreshed->programs.size() && full->programs[differing].userData == refreshed->programs[differing].userData) ++differing; @@ -173,7 +174,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe } decode = std::move(refreshed); } - resolveDrawDecode(queue, submission, decode, registerKey, drawKey, profile); + resolveDrawDecode(queue, submission, localDevice->graphicsContext(), decode, registerKey, drawKey, profile); const auto& graphics = decode->state; auto pixel = decode->pixel; std::vector programs = decode->programs; @@ -241,6 +242,11 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe 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), program.tailContinuation.get()) | graphics.layerExports; + if (vertexInfos[i]) { + vertexInfos[i]->depthRemapMode = graphics.depthRemap.mode; + vertexInfos[i]->depthRemapScale = std::bit_cast(graphics.depthRemap.scale); + vertexInfos[i]->depthRemapOffset = std::bit_cast(graphics.depthRemap.offset); + } }; 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/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index 598176196b..87fc676af8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -121,9 +121,10 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const } } -std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Submission& submission) { +std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Submission& submission, const Graphics::Context& context) { auto product = std::make_shared(); product->state = Graphics::DecodeState(queue, VertexWorkgroupRequired(queue, *submission.shaders)); + Graphics::ApplyDepthRemap(context, product->state); DecodeGraphicsPrograms(*product, queue, *submission.shaders, false, true); product->pixel = Graphics::DecodePixelStageInfo(queue.context, Graphics::ExportMappings(product->state), Graphics::PixelProgramSkipped(queue)); product->pixel.targetExportPacking = Graphics::ExportPackings(product->state); @@ -143,11 +144,11 @@ std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Su return product; } -void Driver::resolveDrawDecode(const QueueState& queue, const Submission& submission, std::shared_ptr& decode, bool registerKey, std::uint64_t drawKey, bool profile) { +void Driver::resolveDrawDecode(const QueueState& queue, const Submission& submission, const Graphics::Context& context, std::shared_ptr& decode, bool registerKey, std::uint64_t drawKey, bool profile) { if (decode == nullptr || verifyDrawRecipe()) { std::vector readLog; const Graphics::RegisterReadLogScope logScope(verifyDrawRecipe() && registerKey ? &readLog : nullptr); - auto fresh = decodeDraw(queue, submission); + auto fresh = decodeDraw(queue, submission, context); if (verifyDrawRecipe() && registerKey) { std::uint64_t facadeMismatches = 0; for (const auto read : readLog) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index 56e4801c73..c809c50e51 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -214,6 +214,8 @@ void Driver::traceDrawKeyLookup(const std::array& components, bool Driver::sameVertexInfo(const ShaderRecompiler::ShaderVertexStageInfo& a, const ShaderRecompiler::ShaderVertexStageInfo& b) { if (a.resourcesNum != b.resourcesNum || a.fetchAttribReg != b.fetchAttribReg || a.fetchBufferReg != b.fetchBufferReg || a.fetchEmbedded != b.fetchEmbedded || a.paClVsOutCntl != b.paClVsOutCntl) return false; + if (a.resourcesNum != b.resourcesNum || a.fetchAttribReg != b.fetchAttribReg || a.fetchBufferReg != b.fetchBufferReg || a.fetchEmbedded != b.fetchEmbedded) return false; + if (a.depthRemapMode != b.depthRemapMode || a.depthRemapScale != b.depthRemapScale || a.depthRemapOffset != b.depthRemapOffset) return false; for (std::uint32_t i = 0; i < a.resourcesNum && i < a.resources.size(); ++i) { if (a.resources[i].fields != b.resources[i].fields) return false; const auto& x = a.resourcesDst[i]; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index e1e3b027f6..61870f0241 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3495,6 +3495,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.float64DenormPreserve = state->float64DenormPreserve; target.float16DenormPreserve = state->float16DenormPreserve; target.spannedWaves = state->spannedWaves; + target.depthRangeRemap = !state->depthRangeUnrestricted; target.fixedPushSlots = state->graphicsPipelineLibrary; target.registrationModules = state->graphicsPipelineLibrary; target.robustBufferAccess2 = state->robustBufferAccess2; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index c2a2c0510b..10902afd8a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -125,6 +125,16 @@ struct State { VkViewport viewport; bool negativeOneToOne; bool depthClamp = false; + float depthScale = 1; + float depthOffset = 0; + float depthClampMin = 0; + float depthClampMax = 1; + struct DepthRemap { + std::uint32_t mode = 0; + float scale = 1; + float offset = 0; + bool operator==(const DepthRemap&) const = default; + } depthRemap; bool alphaToCoverage = false; bool ditheredAlphaToCoverage = false; bool perSampleShading = false; @@ -150,6 +160,7 @@ inline constexpr std::uint32_t PositionExportLayoutBits = 0xffffu | (1u << 21u) ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = false); State DecodeState(const QueueState& queue, bool vertexWorkgroup = false); +void ApplyDepthRemap(const Context& context, State& state); std::array SampleLocations(const State& state); VkExtent2D SampleLocationGrid(const State& state); std::array ExportMappings(const State& state); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 77b8c2595b..7728f536b4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1019,6 +1019,29 @@ ReversedBlend TranslateReversedBlend(std::uint32_t blendControl, std::uint8_t co return {blend, routing->code}; } +void ApplyDepthRemap(const Context& context, State& state) { + const auto& viewport = state.viewport; + const bool inRange = viewport.minDepth >= 0 && viewport.minDepth <= 1 && viewport.maxDepth >= 0 && viewport.maxDepth <= 1; + if (context.depthRangeUnrestricted || inRange) return; + Require(state.stages.path == ShaderPath::Vertex && !state.stages.mesh && !state.stages.tessellation, "viewport depth outside [0, 1] is remapped on the vertex path only"); + Require(context.depthClamp, "viewport depth outside [0, 1] needs depth clamping to stand in for the console's clamp"); + const float clampMin = std::clamp(state.depthClampMin, 0.0f, 1.0f); + const float clampMax = std::clamp(state.depthClampMax, 0.0f, 1.0f); + const bool floatDepth = state.depth.has_value() && (state.depth->format == VK_FORMAT_D32_SFLOAT || state.depth->format == VK_FORMAT_D32_SFLOAT_S8_UINT); + if ((clampMin != state.depthClampMin || clampMax != state.depthClampMax) && floatDepth && (state.depthWrite || state.depthTest)) { + static bool reported = false; + if (!reported) { + reported = true; + std::fprintf(stderr, "[gpu] viewport depth clamp bounds [%g, %g] on a float depth target are clipped to [0, 1]: the device has no VK_EXT_depth_range_unrestricted\n", state.depthClampMin, state.depthClampMax); + } + } + state.depthRemap = {state.negativeOneToOne ? 2u : 1u, state.depthScale, state.depthOffset}; + state.viewport.minDepth = clampMin; + state.viewport.maxDepth = clampMax; + state.negativeOneToOne = false; + state.depthClamp = true; +} + State DecodeState(const QueueState& queue, bool vertexWorkgroup) { if (auto reason = DepthMaintenanceRejection(queue); !reason.empty()) throw std::runtime_error(reason); const auto& cx = queue.context; @@ -1232,6 +1255,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { throw std::runtime_error(message.str()); } Require(readFloat(cx, 0xb4) <= readFloat(cx, 0xb5), "inverted viewport depth clamp bounds"); + result.depthScale = zs; + result.depthOffset = zo; + result.depthClampMin = readFloat(cx, 0xb4); + result.depthClampMax = readFloat(cx, 0xb5); result.viewport = collapsed ? VkViewport{xo, yo, 1, 1, minDepth, maxDepth} : VkViewport{xo - xs, yo - ys, 2 * xs, 2 * ys, minDepth, maxDepth}; if (const auto renderControl = find(cx, 0x000); result.depth && renderControl != cx.end() && (renderControl->second & 1u) != 0) { result.viewport.minDepth = result.depth->clearDepth; diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp new file mode 100644 index 0000000000..9f1312d663 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp @@ -0,0 +1,110 @@ +#include "Recompiler.hpp" +#include "PipelineSpecialization.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct ModuleFacts { + std::map specializationDefaults; + std::uint32_t clipDistanceLength = 0; + bool clipDistanceBuiltIn = false; + std::size_t multiplies = 0; +}; + +ModuleFacts inspect(const ShaderRecompiler::RecompileResult& result) { + ModuleFacts facts; + const auto& words = result.spirv.Words(); + std::map specIds; + std::map specValues; + std::map constants; + std::map arrayLengths; + std::map pointerTargets; + std::map variableTypes; + std::set clipVariables; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + const auto count = words[at] >> 16u; + if (op == spv::OpDecorate && count == 4 && words[at + 2] == spv::DecorationSpecId) specIds[words[at + 1]] = words[at + 3]; + if (op == spv::OpDecorate && count == 4 && words[at + 2] == spv::DecorationBuiltIn && words[at + 3] == spv::BuiltInClipDistance) clipVariables.insert(words[at + 1]); + if (op == spv::OpSpecConstant && count == 4) specValues[words[at + 2]] = words[at + 3]; + if (op == spv::OpConstant && count == 4) constants[words[at + 2]] = words[at + 3]; + if (op == spv::OpTypeArray && count == 4) arrayLengths[words[at + 1]] = words[at + 3]; + if (op == spv::OpTypePointer && count == 4) pointerTargets[words[at + 1]] = words[at + 3]; + if (op == spv::OpVariable && count >= 4) variableTypes[words[at + 2]] = words[at + 1]; + if (op == spv::OpFMul) ++facts.multiplies; + } + for (const auto& [id, specId] : specIds) { + if (const auto value = specValues.find(id); value != specValues.end()) facts.specializationDefaults[specId] = value->second; + } + for (const auto variable : clipVariables) { + facts.clipDistanceBuiltIn = true; + const auto pointer = variableTypes.find(variable); + if (pointer == variableTypes.end()) continue; + const auto array = pointerTargets.find(pointer->second); + if (array == pointerTargets.end()) continue; + const auto length = arrayLengths.find(array->second); + if (length == arrayLengths.end()) continue; + const auto constant = constants.find(length->second); + if (constant != constants.end()) facts.clipDistanceLength = constant->second; + } + return facts; +} + +constexpr std::array PositionExport{0xf80008cfu, 0u, 0xbf810000u}; + +ShaderRecompiler::RecompileRequest baseRequest(bool remap) { + using namespace ShaderRecompiler; + RecompileRequest request{}; + request.shader = {ShaderStage::Vertex, 0x10000u, PositionExport, 0, {}}; + request.context.waveSize = 64; + ShaderVertexStageInfo vertex{}; + vertex.depthRemapMode = 2u; + vertex.depthRemapScale = std::bit_cast(1.0f); + vertex.depthRemapOffset = std::bit_cast(0.0f); + request.context.vertex = vertex; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 64; + static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityClipDistance}; + request.target.supportedCapabilities = capabilities; + request.target.depthRangeRemap = remap; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return request; +} + +void require(bool condition, const char* what) { + if (!condition) throw std::runtime_error(what); +} + +} + +int main() { + using namespace ShaderRecompiler; + try { + const auto remapped = inspect(Recompile(baseRequest(true))); + const auto plain = inspect(Recompile(baseRequest(false))); + require(remapped.clipDistanceBuiltIn && remapped.clipDistanceLength == plain.clipDistanceLength + 2u, "the remap did not add its two clip distances"); + require(remapped.multiplies >= plain.multiplies + 2u, "the remap does not scale the clip z and offset it by w"); + require(!plain.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase) && !remapped.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 3u), "unexpected specialization constants"); + std::cout << "Depth range remap tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << "Depth range remap test failed: " << error.what() << '\n'; + for (const bool remap : {true, false}) { + const auto facts = inspect(Recompile(baseRequest(remap))); + std::cerr << " remap=" << remap << ": spec ids"; + for (const auto& [id, value] : facts.specializationDefaults) std::cerr << ' ' << id << '=' << value; + std::cerr << "; clip builtin " << facts.clipDistanceBuiltIn << " length " << facts.clipDistanceLength << "; fmul " << facts.multiplies << '\n'; + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index c4d38792ee..af6cb71bcb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3533,6 +3533,56 @@ void quadPixelMaskTests() { expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, varying, storing); }, "differs between the pixels of a quad"); } +void DepthRemapTests() { + auto queue = makeState(); + queue.context[0x204] = 0; + queue.context[0x113] = std::bit_cast(1.0f); + queue.context[0x114] = 0; + AgcDriver::Graphics::Context remapping{}; + remapping.depthClamp = true; + auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.negativeOneToOne && state.viewport.minDepth == -1 && state.viewport.maxDepth == 1 && state.depthRemap.mode == 0, "a negative-one-to-one depth transform decoded with a remap"); + AgcDriver::Graphics::ApplyDepthRemap(remapping, state); + Require(state.depthRemap.mode == 2 && state.depthRemap.scale == 1.0f && state.depthRemap.offset == 0.0f, "the remap did not take the viewport's depth scale and offset"); + Require(state.viewport.minDepth == 0 && state.viewport.maxDepth == 1 && !state.negativeOneToOne && state.depthClamp, "the remapped viewport does not clamp at the console's bounds in zero-to-one clip space"); + AgcDriver::Graphics::Context unrestricted{}; + unrestricted.depthRangeUnrestricted = true; + auto kept = AgcDriver::Graphics::DecodeState(queue); + AgcDriver::Graphics::ApplyDepthRemap(unrestricted, kept); + Require(kept.depthRemap.mode == 0 && kept.viewport.minDepth == -1 && kept.negativeOneToOne && !kept.depthClamp, "an unrestricted device remapped the depth range"); + AgcDriver::Graphics::Context noClamp{}; + auto refused = AgcDriver::Graphics::DecodeState(queue); + expectFailure([&] { AgcDriver::Graphics::ApplyDepthRemap(noClamp, refused); }, "depth clamping"); + queue.context[0x113] = std::bit_cast(0.5f); + queue.context[0x114] = std::bit_cast(0.5f); + auto inRange = AgcDriver::Graphics::DecodeState(queue); + AgcDriver::Graphics::ApplyDepthRemap(remapping, inRange); + Require(inRange.depthRemap.mode == 0 && inRange.negativeOneToOne && !inRange.depthClamp, "an in-range depth transform was remapped"); + queue.context[0x204] = 0x80000u; + queue.context[0x113] = std::bit_cast(2.0f); + queue.context[0x114] = std::bit_cast(-0.5f); + queue.context[0xb4] = std::bit_cast(0.25f); + queue.context[0xb5] = std::bit_cast(0.75f); + auto bounded = AgcDriver::Graphics::DecodeState(queue); + AgcDriver::Graphics::ApplyDepthRemap(remapping, bounded); + Require(bounded.depthRemap.mode == 1 && bounded.depthRemap.scale == 2.0f && bounded.depthRemap.offset == -0.5f && bounded.viewport.minDepth == 0.25f && bounded.viewport.maxDepth == 0.75f, "a zero-to-one remap did not take the clamp bounds"); + queue.context[0xb4] = std::bit_cast(-1.0f); + queue.context[0xb5] = std::bit_cast(2.0f); + auto clipped = AgcDriver::Graphics::DecodeState(queue); + AgcDriver::Graphics::ApplyDepthRemap(remapping, clipped); + Require(clipped.depthRemap.mode == 1 && clipped.viewport.minDepth == 0 && clipped.viewport.maxDepth == 1, "clamp bounds beyond [0, 1] were not clipped to the representable depth range"); + auto unorm = AgcDriver::Graphics::DecodeState(queue); + unorm.depth = AgcDriver::Graphics::DepthTarget{0x10000, 0, {64, 64}, VK_FORMAT_D16_UNORM, 0.0f, 0}; + unorm.depthTest = unorm.depthWrite = true; + AgcDriver::Graphics::ApplyDepthRemap(remapping, unorm); + Require(unorm.viewport.minDepth == 0 && unorm.viewport.maxDepth == 1, "a normalized depth target refused clipped clamp bounds"); + auto floating = AgcDriver::Graphics::DecodeState(queue); + floating.depth = AgcDriver::Graphics::DepthTarget{0x10000, 0, {64, 64}, VK_FORMAT_D32_SFLOAT, 0.0f, 0}; + floating.depthWrite = true; + AgcDriver::Graphics::ApplyDepthRemap(remapping, floating); + Require(floating.depthRemap.mode == 1 && floating.viewport.minDepth == 0 && floating.viewport.maxDepth == 1 && floating.depthClamp, "a written float depth target refused clipped clamp bounds"); +} + void DepthClipTests() { auto queue = makeState(); const auto direct = AgcDriver::Graphics::DecodeState(queue); @@ -8612,6 +8662,7 @@ int main(int argc, char** argv) { hardwareScreenOffsetTests(); srgb8TargetTests(); DepthClipTests(); + DepthRemapTests(); DepthStencilTests(); OneDimensionalColorTests(); htileDepthClearTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 0ef746978e..df0db17c16 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3216,7 +3216,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQSAAAAA=", "new requests did not use serialization version 32"); + require(requestPrefix(encoded, 8u) == "NVNQQSEAAAA=", "new requests did not use serialization version 33"); const std::array fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -3334,7 +3334,7 @@ void verifyPixelRequestSerialization() { for (std::size_t bytes = 0; bytes < 45u; ++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, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSEAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSIAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -3483,24 +3483,28 @@ void verifyVertexInfoVersion19() { require(std::equal(maskedBytes.begin(), maskedBytes.begin() + static_cast(maskOffset + 1u), sampledBytes.begin()) && sampledBytes[maskOffset + 1u] == 0x1u && sampledBytes[maskOffset + 5u] == 0x2u && sampledBytes[maskOffset + 9u] == 0x4u && sampledBytes[maskOffset + 13u] == 0x8u, "the quad sample masks are not the four words after the quad pixel mask"); auto version29 = sampledBytes; version29.pop_back(); + version29.pop_back(); version29.erase(version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u)); version29[4] = 29u; const auto replay29 = serializer.Deserialize(encodeBase64(version29)); require(replay29.request.context.pixel.has_value() && replay29.request.context.pixel->quadSampleMask == std::array{0x1u, 0x2u, 0x4u, 0x8u} && replay29.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay29.request)) == sampledBytes, "a version 29 request read a reversed-order blend code or misread the fields after the quad sample masks"); auto version28 = sampledBytes; version28.pop_back(); + version28.pop_back(); version28.erase(version28.begin() + static_cast(maskOffset + 1u), version28.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u)); version28[4] = 28u; const auto replay28 = serializer.Deserialize(encodeBase64(version28)); require(replay28.request.context.pixel.has_value() && replay28.request.context.pixel->quadPixelMask == 0x2u && replay28.request.context.pixel->quadSampleMask == std::array{0xffffu, 0xffffu, 0xffffu, 0xffffu} && replay28.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay28.request)) == maskedBytes, "a version 28 request read quad sample masks or a reversed-order blend code, or misread the fields after them"); auto version27 = maskedBytes; version27.pop_back(); + version27.pop_back(); version27.erase(version27.begin() + static_cast(maskOffset), version27.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u)); version27[4] = 27u; const auto replay27 = serializer.Deserialize(encodeBase64(version27)); require(replay27.request.context.pixel.has_value() && replay27.request.context.pixel->quadPixelMask == 0xfu && replay27.request.context.pixel->reversedBlend == 0u && serializer.Serialize(replay27.request) == encodedPixel, "a version 27 request read a quad pixel mask or reversed-order blend code, or misread the fields after it"); auto legacyPixel = plainPixel; legacyPixel.pop_back(); + legacyPixel.pop_back(); legacyPixel.erase(legacyPixel.begin() + static_cast(allBitsOffset), legacyPixel.begin() + static_cast(allBitsOffset + 2u + sampleMaskBytes + 4u)); legacyPixel[4] = 26u; const auto replay26 = serializer.Deserialize(encodeBase64(legacyPixel)); diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index f501252286..14d760b320 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -208,6 +208,9 @@ private: append(key, value.fetchBufferReg); append(key, value.fetchEmbedded); append(key, value.paClVsOutCntl); + append(key, value.depthRemapMode); + append(key, value.depthRemapScale); + append(key, value.depthRemapOffset); 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) { @@ -257,6 +260,7 @@ private: append(key, value.srgbDecodeFormats); append(key, value.narrowSubgroupClock); append(key, value.float64DenormPreserve); + append(key, value.depthRangeRemap); append(key, value.registrationModules); append(key, value.registrationWordLimit); append(key, value.vertexSubgroups); diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index bd80f9bbd6..cb4728ae18 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -778,7 +778,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(32u); + writer.WriteU32(33u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -804,6 +804,12 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const writer.WriteBool(request.target.float16DenormPreserve); writer.WriteU32Span(request.context.linkedCallFallbacks); writer.WriteBool(request.target.spannedWaves); + writer.WriteBool(request.target.depthRangeRemap); + if (request.context.vertex.has_value()) { + writer.WriteU32(request.context.vertex->depthRemapMode); + writer.WriteU32(request.context.vertex->depthRemapScale); + writer.WriteU32(request.context.vertex->depthRemapOffset); + } return base64Encode(buffer); } @@ -812,7 +818,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 > 32u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 33u) 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); @@ -849,6 +855,15 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const result.request.context.linkedCallFallbacks = result.linkedCallFallbacks; } if (version >= 32u) result.request.target.spannedWaves = reader.ReadBool(); + if (version >= 33u) { + result.request.target.depthRangeRemap = reader.ReadBool(); + if (result.vertex.has_value()) { + result.vertex->depthRemapMode = reader.ReadU32(); + result.vertex->depthRemapScale = reader.ReadU32(); + result.vertex->depthRemapOffset = reader.ReadU32(); + result.request.context.vertex = result.vertex; + } + } if (version >= 21u && reader.Remaining() != 0u) throw std::runtime_error("RequestSerializer: unexpected trailing data"); return result; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 2ba98418bd..c75cfc8363 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -69,6 +69,12 @@ struct ShaderStageRuntime { } }; +struct ShaderDepthRemap { + std::uint32_t mode = 0; + std::uint32_t scaleBits = 0; + std::uint32_t offsetBits = 0; +}; + struct ShaderClipSpaceTransform { float scale[2] = {}; float offset[2] = {}; @@ -137,6 +143,7 @@ struct ShaderVertexInputInfo { std::uint32_t hostSubgroupSize = 64; std::uint32_t paClVsOutCntl = 0; ShaderClipSpaceTransform clipSpace; + ShaderDepthRemap depthRemap; ShaderMeshInputInfo mesh; ShaderTessellationInputInfo tess; bool fetchExternal = false; diff --git a/core/shader/recompiler/PipelineSpecialization.hpp b/core/shader/recompiler/PipelineSpecialization.hpp index e47d9879be..08c321a9da 100644 --- a/core/shader/recompiler/PipelineSpecialization.hpp +++ b/core/shader/recompiler/PipelineSpecialization.hpp @@ -30,6 +30,8 @@ inline constexpr std::uint32_t RasterPrimitive = 8233u; inline constexpr std::uint32_t LayerExported = 8234u; inline constexpr std::uint32_t StencilReferenceOffset = 8235u; inline constexpr std::uint32_t ReversedBlend = 8236u; +inline constexpr std::uint32_t DepthRemapBase = 8240u; +inline constexpr std::uint32_t DepthRemapWords = 3u; inline constexpr std::uint32_t HeapCountBase = 8256u; inline constexpr std::uint32_t MipCountBase = 8320u; inline constexpr std::uint32_t DescriptorIndexBase = 16384u; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index d08bc8a148..f5e497486c 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -527,6 +527,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) targetOptions.nonConstantImageOffsets = request.target.nonConstantImageOffsets; targetOptions.narrowSubgroupClock = request.target.narrowSubgroupClock; targetOptions.float64DenormPreserve = request.target.float64DenormPreserve; + targetOptions.depthRangeRemap = request.target.depthRangeRemap; targetOptions.registrationModules = registration; targetOptions.fragmentDescriptorSet = request.target.registrationModules; targetOptions.float16DenormPreserve = request.target.float16DenormPreserve; @@ -905,6 +906,12 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil if (variant.info.info.rasterPrimitive) moduleKey.push_back(static_cast(request.context.pixel->rasterPrimitive)); if (variant.info.info.layerExported) moduleKey.push_back(request.context.pixel->layerExported ? 1u : 0u); } + const bool depthRemap = request.target.depthRangeRemap && request.shader.stage == ShaderStage::Vertex && request.context.vertex.has_value(); + if (depthRemap) { + moduleKey.push_back(request.context.vertex->depthRemapMode); + moduleKey.push_back(request.context.vertex->depthRemapScale); + moduleKey.push_back(request.context.vertex->depthRemapOffset); + } result.vertexAttributes.reserve(result.vertexInputs.size()); std::array vertexClasses{}; std::array runtimeSelectors{}; @@ -933,7 +940,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil if (artifact.registrationModule) runtimeSelectors.at(input.location / RuntimeAbi::VertexSelectorsPerDword) |= (selectors & 0xfffu) << (input.location % RuntimeAbi::VertexSelectorsPerDword * 16u); } } - if (!plan->bindings.specialization.empty() || variant.bindings.layout.UsesPushData() || !artifact.vertexInputPatches.empty() || request.context.pixel) { + if (!plan->bindings.specialization.empty() || variant.bindings.layout.UsesPushData() || !artifact.vertexInputPatches.empty() || request.context.pixel || depthRemap) { { std::shared_lock lock(plan->mutex); if (const auto found = plan->modules.find(moduleKey); found != plan->modules.end()) entry = found->second; @@ -955,6 +962,9 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil if (variant.info.info.rasterPrimitive) constants.push_back({PipelineSpecialization::RasterPrimitive, moduleKey[index++]}); if (variant.info.info.layerExported) constants.push_back({PipelineSpecialization::LayerExported, moduleKey[index++]}); } + if (depthRemap) { + for (std::uint32_t word = 0; word < PipelineSpecialization::DepthRemapWords; ++word) constants.push_back({PipelineSpecialization::DepthRemapBase + word, moduleKey[index++]}); + } if (!artifact.vertexInputPatches.empty()) { for (const auto& input : result.vertexInputs) { const auto selectors = moduleKey[index++]; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 38de551273..2faf18545e 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -196,6 +196,9 @@ struct ShaderVertexStageInfo { std::uint32_t fetchBufferReg; bool fetchEmbedded; std::uint32_t paClVsOutCntl = 0; + std::uint32_t depthRemapMode = 0; + std::uint32_t depthRemapScale = 0; + std::uint32_t depthRemapOffset = 0; }; struct ShaderFloatMode { @@ -270,6 +273,7 @@ struct SpirvTarget { bool float16DenormPreserve = false; bool spannedWaves = false; bool robustBufferAccess2 = false; + bool depthRangeRemap = false; }; struct BindingLayout { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index 7f7ccbdec5..385638dd21 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -22,6 +22,7 @@ struct SpirvTargetOptions { bool nonConstantImageOffsets = false; bool narrowSubgroupClock = false; bool float64DenormPreserve = false; + bool depthRangeRemap = false; bool registrationModules = false; bool fragmentDescriptorSet = false; bool float16DenormPreserve = false; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 4c54b41678..3cdf176ef9 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -104,6 +104,8 @@ struct SpirvEmitterState { std::uint32_t spannedExchangeVariable = 0; bool narrowSubgroupClock = false; bool float64DenormPreserve = false; + bool depthRangeRemap = false; + std::uint32_t depthRemapClipIndex = 0; bool registrationModules = false; bool fragmentDescriptorSet = false; bool float16DenormPreserve = false; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index f30656653d..fd078cfb76 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -257,6 +257,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.nonConstantImageOffsets = target.nonConstantImageOffsets; state.narrowSubgroupClock = target.narrowSubgroupClock; state.float64DenormPreserve = target.float64DenormPreserve; + state.depthRangeRemap = target.depthRangeRemap; state.registrationModules = target.registrationModules; state.fragmentDescriptorSet = target.fragmentDescriptorSet; state.float16DenormPreserve = target.float16DenormPreserve; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 2f44307dc3..5119368611 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -327,6 +327,11 @@ void DefineOutputs(SpirvEmitterState& state) { DefineMeshOutputs(state); return; } + const bool depthRemap = state.depthRangeRemap && state.program.Resources().stage == IrShaderStage::Vertex; + if (depthRemap) { + state.depthRemapClipIndex = clipDistanceCount; + clipDistanceCount += 2u; + } const auto stage = state.program.Resources().stage; const bool exportsPosition = std::ranges::any_of(state.outputs, [](const SpirvOutputBinding& output) { return output.kind == StageOutputKind::Position; }); if (exportsPosition && (stage == IrShaderStage::Vertex || stage == IrShaderStage::TessellationEvaluation)) { @@ -346,6 +351,7 @@ void DefineOutputs(SpirvEmitterState& state) { } return variable; }; + if (depthRemap) BuiltIn(state.clipDistanceVariable, F32ArrayType(state, clipDistanceCount), "gl_ClipDistance", spv::BuiltInClipDistance); for (auto& binding : state.outputs) { switch (binding.kind) { case StageOutputKind::Position: diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index accf51eb13..4f76d0a5c1 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -515,6 +515,53 @@ std::uint32_t ConvertClipCoordinate(SpirvEmitterState& state, std::uint32_t coor return ndc; } +std::uint32_t RemapPositionDepth(SpirvEmitterState& state, std::uint32_t position) { + const auto& remap = state.inputInfo.vertex->depthRemap; + const auto f32 = TypeF32(state); + const auto u32 = TypeU32(state); + const auto boolean = TypeBool(state); + std::array components{}; + for (std::uint32_t i = 0; i < 4u; i++) { + components.at(i) = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, f32, components.at(i), position, i); + } + const auto mode = state.module.SpecializationConstant(u32, PipelineSpecialization::DepthRemapBase, remap.mode); + const auto scaleBits = state.module.SpecializationConstant(u32, PipelineSpecialization::DepthRemapBase + 1u, remap.scaleBits); + const auto offsetBits = state.module.SpecializationConstant(u32, PipelineSpecialization::DepthRemapBase + 2u, remap.offsetBits); + const auto scale = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitcast, f32, scale, scaleBits); + const auto offset = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitcast, f32, offset, offsetBits); + const auto scaled = state.module.AllocateId(); + state.module.AddFunction(spv::OpFMul, f32, scaled, components[2], scale); + const auto shifted = state.module.AllocateId(); + state.module.AddFunction(spv::OpFMul, f32, shifted, components[3], offset); + const auto remapped = state.module.AllocateId(); + state.module.AddFunction(spv::OpFAdd, f32, remapped, scaled, shifted); + const auto active = state.module.AllocateId(); + state.module.AddFunction(spv::OpINotEqual, boolean, active, mode, ConstantU32(state, 0u)); + const auto z = Select(state, f32, active, remapped, components[2]); + const auto symmetric = state.module.AllocateId(); + state.module.AddFunction(spv::OpIEqual, boolean, symmetric, mode, ConstantU32(state, 2u)); + const auto nearSymmetric = state.module.AllocateId(); + state.module.AddFunction(spv::OpFAdd, f32, nearSymmetric, components[3], components[2]); + const auto near = Select(state, f32, symmetric, nearSymmetric, components[2]); + const auto far = state.module.AllocateId(); + state.module.AddFunction(spv::OpFSub, f32, far, components[3], components[2]); + const auto inside = ConstantF32Value(state, 1.0f); + const auto storeDistance = [&](std::uint32_t index, std::uint32_t distance) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, f32), pointer, state.clipDistanceVariable, ConstantU32(state, index)); + state.module.AddFunction(spv::OpStore, pointer, Select(state, f32, active, distance, inside)); + }; + if (state.clipDistanceVariable == 0u) throw std::runtime_error("depth remap clip distances were not declared"); + storeDistance(state.depthRemapClipIndex, near); + storeDistance(state.depthRemapClipIndex + 1u, far); + const auto converted = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 4u), converted, components[0], components[1], z, components[3]); + return converted; +} + std::uint32_t ConvertPositionToClipSpace(SpirvEmitterState& state, std::uint32_t position) { if (state.inputInfo.vertex == nullptr) { throw std::runtime_error("vertex input info is missing for clip-space conversion"); @@ -1535,6 +1582,9 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { if (state.inputInfo.vertex->clipSpace.enabled) { value = ConvertPositionToClipSpace(state, value); } + if (state.depthRangeRemap && state.program.Resources().stage == IrShaderStage::Vertex) { + value = RemapPositionDepth(state, value); + } } if (exp.kind == ExportTargetKind::Position && state.zeroPositionClipIndex) { StoreZeroPositionClipDistance(state, value); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 992dcf3388..1728ff1a4f 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -171,6 +171,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues vertexStorage.hostSubgroupSize = hostSubgroupSize; vertexStorage.fetchEmbedded = vertex.fetchEmbedded; vertexStorage.paClVsOutCntl = vertex.paClVsOutCntl; + vertexStorage.depthRemap = {vertex.depthRemapMode, vertex.depthRemapScale, vertex.depthRemapOffset}; vertexStorage.fetchExternal = false; vertexStorage.fetchAttribReg = static_cast(vertex.fetchAttribReg); vertexStorage.fetchBufferReg = static_cast(vertex.fetchBufferReg); diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index f30aae1501..dc716d625f 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -206,11 +206,11 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 32u; + upgraded[4] = 33u; if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 5u, 0u); - upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u, 0u); + upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 13u, 0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); auto truncated = bytes; truncated.pop_back(); @@ -245,11 +245,11 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 32u; + layered[4] = 33u; layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); if (fixture.tessellation) layered.insert(layered.end() - 35u, 5u, 0u); - layered.insert(layered.end(), sizeof(std::uint64_t) + 1u, 0u); + layered.insert(layered.end(), sizeof(std::uint64_t) + 1u + 13u, 0u); Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); } } @@ -273,7 +273,7 @@ int main() { bytes[4] = 32u; bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); - bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u, 0u); + bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u + 13u, 0u); Require(serializer.Serialize(replay.request) == EncodeBase64(bytes)); auto truncated23 = DecodeBase64(encoded23); @@ -397,7 +397,7 @@ int main() { upgraded[4] = 32u; if (fixture.pixel) upgraded.insert(upgraded.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); - upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u, 0u); + upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 13u, 0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); for (const bool dither : {false, true}) { if (!fixture.pixel && dither) continue; @@ -426,9 +426,9 @@ int main() { const auto replay = serializer.Deserialize(encoded); Require(replay.request.target.float16DenormPreserve == preserve); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 32u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 33u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); auto future = bytes; - future[4] = 33u; + future[4] = 34u; bool futureRejected = false; try { static_cast(serializer.Deserialize(EncodeBase64(future))); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 86c2eba6e7..daac3bed96 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -174,3 +174,4 @@ - [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted when blending is disabled (ENABLE clear or BLEND_BYPASS set). Vulkan converts unblended shader values and clamps to the normalized range automatically; blending into an unclamped normalized target remains unmodeled because Vulkan always clamps inputs prior to blending. - [Thick SW_4KB_S_X volumes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) use AMD addrlib's GFX10 non-RB+ 16-pipe `GFX10_SW_4K_S3_X_PATINFO` equations for 1, 2, 4, 8 and 16-byte elements, including packed mip tails. The layout follows [AMD PAL c5e80007](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp); it has not been measured on console hardware. Other thick swizzle modes outside the supported families still throw. Mipmapped 3D color targets remain refused by the color-register decoder. +- [Viewport depth range remap](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - on a device without `VK_EXT_depth_range_unrestricted` a viewport depth range outside [0, 1] (Minecraft PPSA17221 draws its world with `PA_CL_VPORT_ZSCALE` 1, `ZOFFSET` 0 in negative-one-to-one clip space) is folded into the vertex shader: `RemapPositionDepth` scales clip z so z/w is the console's depth, the pipeline's depth clamp applies the console's `PA_SC_VPORT_ZMIN/ZMAX` clipped to [0, 1] (exact for a normalized depth target or none; Minecraft clamps its float depth target to [-1, 1], so a depth below 0 is stored as 0 there, which only a later test or read at exactly 0 can tell, and the first such draw is reported once), and two extra `gl_ClipDistance` elements restore the near and far planes depth clamping removes. Mode, scale and offset are pipeline specialization constants (`PipelineSpecialization::DepthRemapBase`), so each distinct depth transform is one vertex module variant. The remap serves the vertex path only; mesh and tessellation draws with such a range still throw. Whether the console clamps before or after its depth bias and how it treats a clamp range narrower than the transform's are not measured. From b28e6c83dedb096936aaa04fb57a90409b27d0d9 Mon Sep 17 00:00:00 2001 From: Niko Date: Wed, 14 Oct 2026 17:54:03 +0200 Subject: [PATCH 02/14] fix(agc): keep viewport depth scale and offset out of the shader cache key Only the remap mode changes what the vertex shader compiles to. Scale and offset are specialization constants that each materialization takes from its own request, so they no longer split the compiled variant per depth transform. The result memo hashes them with the snapshot instead, so one prepared vertex shader does not hand the module specialized for one transform to a draw with another. sameVertexInfo repeated its first comparison without paClVsOutCntl; the repeat is dropped. A test pins the version 27 layout (the remap fields are the last bytes of a vertex request), a version 25 replay, truncation and trailing-data refusal, and that scale and offset leave the cache key alone while the mode does not. agc_depth_range_remap materializes one prepared vertex shader with two transforms and checks that each gets its own module. Co-Authored-By: Claude Opus 5.5 --- .../src/Driver/Draw/DrawRegisterKey.cpp | 1 - .../libSceAgcDriver/tests/DepthRangeRemap.cpp | 21 +++++++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 56 +++++++++++++++++++ core/shader/recompiler/CacheKey.hpp | 2 - core/shader/recompiler/Recompiler.cpp | 2 + 5 files changed, 79 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index c809c50e51..4f08f7d974 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -214,7 +214,6 @@ void Driver::traceDrawKeyLookup(const std::array& components, bool Driver::sameVertexInfo(const ShaderRecompiler::ShaderVertexStageInfo& a, const ShaderRecompiler::ShaderVertexStageInfo& b) { if (a.resourcesNum != b.resourcesNum || a.fetchAttribReg != b.fetchAttribReg || a.fetchBufferReg != b.fetchBufferReg || a.fetchEmbedded != b.fetchEmbedded || a.paClVsOutCntl != b.paClVsOutCntl) return false; - if (a.resourcesNum != b.resourcesNum || a.fetchAttribReg != b.fetchAttribReg || a.fetchBufferReg != b.fetchBufferReg || a.fetchEmbedded != b.fetchEmbedded) return false; if (a.depthRemapMode != b.depthRemapMode || a.depthRemapScale != b.depthRemapScale || a.depthRemapOffset != b.depthRemapOffset) return false; for (std::uint32_t i = 0; i < a.resourcesNum && i < a.resources.size(); ++i) { if (a.resources[i].fields != b.resources[i].fields) return false; diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp index 9f1312d663..05a7ca231f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp @@ -1,5 +1,6 @@ #include "Recompiler.hpp" #include "PipelineSpecialization.hpp" +#include "Optimization/ResourceProgram.hpp" #include #include #include @@ -85,6 +86,25 @@ void require(bool condition, const char* what) { if (!condition) throw std::runtime_error(what); } +void preparedTransformTests() { + using namespace ShaderRecompiler; + auto request = baseRequest(true); + request.useCache = true; + const auto handle = PrepareShader(request); + const auto materialize = [&](float scale, float offset) { + auto moved = request; + moved.context.vertex->depthRemapScale = std::bit_cast(scale); + moved.context.vertex->depthRemapOffset = std::bit_cast(offset); + const auto invocation = PreparedShaderInvocation::TryCreate(moved, handle); + require(invocation.has_value(), "another depth transform did not match the prepared vertex shader"); + return invocation->Materialize(*invocation->Capture(SrtRuntime{})); + }; + const auto first = materialize(1.0f, 0.0f); + const auto scaled = materialize(4.0f, -1.5f); + require(scaled->PipelineVariantId() != first->PipelineVariantId(), "a prepared vertex shader reused the result materialized for another depth transform"); + require(materialize(1.0f, 0.0f)->PipelineVariantId() == first->PipelineVariantId(), "the same depth transform did not reuse its materialized vertex shader"); +} + } int main() { @@ -95,6 +115,7 @@ int main() { require(remapped.clipDistanceBuiltIn && remapped.clipDistanceLength == plain.clipDistanceLength + 2u, "the remap did not add its two clip distances"); require(remapped.multiplies >= plain.multiplies + 2u, "the remap does not scale the clip z and offset it by w"); require(!plain.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase) && !remapped.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 3u), "unexpected specialization constants"); + preparedTransformTests(); std::cout << "Depth range remap tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index df0db17c16..285203fb9b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3535,6 +3535,61 @@ void verifyVertexInfoVersion19() { } } +void verifyDepthRemapRequests() { + 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{}; + vertex.context.vertex->paClVsOutCntl = 0x89abcdefu; + const auto plain = serializer.Serialize(vertex); + vertex.target.depthRangeRemap = true; + vertex.context.vertex->depthRemapMode = 2u; + vertex.context.vertex->depthRemapScale = 0x40000000u; + vertex.context.vertex->depthRemapOffset = 0xbf000000u; + const auto remapped = serializer.Serialize(vertex); + const auto replay = serializer.Deserialize(remapped); + require(replay.request.target.depthRangeRemap && replay.request.context.vertex.has_value() && replay.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "the depth remap target flag or the vertex info did not round-trip"); + require(replay.request.context.vertex->depthRemapMode == 2u && replay.request.context.vertex->depthRemapScale == 0x40000000u && replay.request.context.vertex->depthRemapOffset == 0xbf000000u, "the depth remap mode, scale or offset did not round-trip"); + require(serializer.Serialize(replay.request) == remapped, "a version 27 depth remap request changed on re-serialization"); + const auto plainBytes = decodeBase64(plain); + const auto remappedBytes = decodeBase64(remapped); + require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 13u && std::equal(plainBytes.begin(), plainBytes.end() - 13, remappedBytes.begin()), "the depth remap fields are not the last 13 bytes of a vertex request"); + auto version25 = remappedBytes; + version25[4] = 25u; + version25.resize(version25.size() - 13u); + const auto legacy = serializer.Deserialize(encodeBase64(version25)); + require(!legacy.request.target.depthRangeRemap && legacy.request.context.vertex.has_value() && legacy.request.context.vertex->depthRemapMode == 0u && legacy.request.context.vertex->depthRemapScale == 0u && legacy.request.context.vertex->depthRemapOffset == 0u, "a version 25 vertex request read depth remap fields"); + require(serializer.Serialize(legacy.request) == plain, "a version 25 vertex request misread the fields before the depth remap"); + auto truncated = remappedBytes; + truncated.pop_back(); + expectFailure([&] { static_cast(serializer.Deserialize(encodeBase64(truncated))); }, "truncated data", "a truncated depth remap offset was accepted"); + auto trailing = remappedBytes; + trailing.push_back(0u); + expectFailure([&] { static_cast(serializer.Deserialize(encodeBase64(trailing))); }, "unexpected trailing data", "a byte after the depth remap fields was accepted"); + RecompileRequest pixel{}; + pixel.shader = {ShaderStage::Fragment, 0x20000u, code, 0, {}}; + pixel.context.waveSize = 64; + pixel.context.pixel = ShaderPixelStageInfo{}; + pixel.target.depthRangeRemap = true; + const auto pixelReplay = serializer.Deserialize(serializer.Serialize(pixel)); + require(pixelReplay.request.target.depthRangeRemap && !pixelReplay.request.context.vertex.has_value(), "a pixel request lost the depth remap target flag or gained vertex info"); + std::vector key; + std::vector other; + RecompileCacheKey::Build(vertex, key); + auto moved = vertex; + moved.context.vertex->depthRemapScale = 0x3f000000u; + moved.context.vertex->depthRemapOffset = 0x3e800000u; + RecompileCacheKey::Build(moved, other); + require(key == other, "the viewport depth scale or offset entered the shader cache key"); + auto zeroToOne = vertex; + zeroToOne.context.vertex->depthRemapMode = 1u; + RecompileCacheKey::Build(zeroToOne, other); + require(key != other, "the depth remap mode did not change the shader cache key"); +} + void verifyLegacyPixelRequests() { using namespace ShaderRecompiler; static constexpr std::array legacyPixelRequests{ @@ -5391,6 +5446,7 @@ int main(int argc, char** argv) { verifyCanonicalVersion20And21Requests(); verifyPixelRequestSerialization(); verifyVertexInfoVersion19(); + verifyDepthRemapRequests(); verifyPositionExportComponents(); verifyLegacyPixelRequests(); verifyPixelExportReplay(); diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 14d760b320..36cfd8f28d 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -209,8 +209,6 @@ private: append(key, value.fetchEmbedded); append(key, value.paClVsOutCntl); append(key, value.depthRemapMode); - append(key, value.depthRemapScale); - append(key, value.depthRemapOffset); 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/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index f5e497486c..3b092fddd7 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -1246,6 +1246,8 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh for (const auto field : vertex.resources[i].fields) mix(field); mix(vertex.resourcesDst[i].fetchIndex); } + mix(vertex.depthRemapScale); + mix(vertex.depthRemapOffset); } else { mix(1ull << 32u); } From a3067a04d4a0d217ce8193d1bba2ae20871b9578 Mon Sep 17 00:00:00 2001 From: Niko Date: Wed, 14 Oct 2026 18:02:45 +0200 Subject: [PATCH 03/14] fix(agc): clamp a remapped depth at narrower bounds instead of rescaling it ApplyDepthRemap gives the Vulkan viewport the console's clamp bounds clipped to [0, 1]. A zero-to-one viewport maps the shader's z/w linearly onto [minDepth, maxDepth], so with bounds narrower than [0, 1] the remap rescaled the depth into them instead of clamping it there; only bounds of [0, 1] or wider (Minecraft's [-1, 1]) came out right. Scale and offset are now divided by the clamp range and the offset moved by its minimum, so the viewport and the depth clamp together give scale * z/w + offset clamped to the bounds. agc_driver_depth_range_remap_draw draws through the remap path and, when the device has VK_EXT_depth_range_unrestricted, through the unrestricted path, then checks the stored depth with probe draws and that the two clip distances cut at the near and far planes. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Graphics/src/State.cpp | 5 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 3 +- .../tests/execution/DepthRangeRemapDraw.cpp | 192 ++++++++++++++++++ 3 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DepthRangeRemapDraw.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 7728f536b4..4a0da9af77 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1035,7 +1035,10 @@ void ApplyDepthRemap(const Context& context, State& state) { std::fprintf(stderr, "[gpu] viewport depth clamp bounds [%g, %g] on a float depth target are clipped to [0, 1]: the device has no VK_EXT_depth_range_unrestricted\n", state.depthClampMin, state.depthClampMax); } } - state.depthRemap = {state.negativeOneToOne ? 2u : 1u, state.depthScale, state.depthOffset}; + const float range = clampMax - clampMin; + const float scale = range > 0.0f ? state.depthScale / range : state.depthScale; + const float offset = range > 0.0f ? (state.depthOffset - clampMin) / range : state.depthOffset; + state.depthRemap = {state.negativeOneToOne ? 2u : 1u, scale, offset}; state.viewport.minDepth = clampMin; state.viewport.maxDepth = clampMax; state.negativeOneToOne = false; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index af6cb71bcb..f112b15f0b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3565,12 +3565,13 @@ void DepthRemapTests() { queue.context[0xb5] = std::bit_cast(0.75f); auto bounded = AgcDriver::Graphics::DecodeState(queue); AgcDriver::Graphics::ApplyDepthRemap(remapping, bounded); - Require(bounded.depthRemap.mode == 1 && bounded.depthRemap.scale == 2.0f && bounded.depthRemap.offset == -0.5f && bounded.viewport.minDepth == 0.25f && bounded.viewport.maxDepth == 0.75f, "a zero-to-one remap did not take the clamp bounds"); + Require(bounded.depthRemap.mode == 1 && bounded.depthRemap.scale == 4.0f && bounded.depthRemap.offset == -1.5f && bounded.viewport.minDepth == 0.25f && bounded.viewport.maxDepth == 0.75f, "a zero-to-one remap did not divide its transform by the clamp range it gives the viewport"); queue.context[0xb4] = std::bit_cast(-1.0f); queue.context[0xb5] = std::bit_cast(2.0f); auto clipped = AgcDriver::Graphics::DecodeState(queue); AgcDriver::Graphics::ApplyDepthRemap(remapping, clipped); Require(clipped.depthRemap.mode == 1 && clipped.viewport.minDepth == 0 && clipped.viewport.maxDepth == 1, "clamp bounds beyond [0, 1] were not clipped to the representable depth range"); + Require(clipped.depthRemap.scale == 2.0f && clipped.depthRemap.offset == -0.5f, "clamp bounds clipped to [0, 1] changed the depth transform"); auto unorm = AgcDriver::Graphics::DecodeState(queue); unorm.depth = AgcDriver::Graphics::DepthTarget{0x10000, 0, {64, 64}, VK_FORMAT_D16_UNORM, 0.0f, 0}; unorm.depthTest = unorm.depthWrite = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthRangeRemapDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthRangeRemapDraw.cpp new file mode 100644 index 0000000000..1a4a56ebf8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthRangeRemapDraw.cpp @@ -0,0 +1,192 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "Recompiler.hpp" +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +namespace Harness = DepthFastClearHarness; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +struct Transform { + bool negativeOneToOne; + float scale; + float offset; + float clampMin; + float clampMax; +}; + +struct Case { + Transform transform; + float z; + bool drawn; + float probe; + bool covered; + bool remapOnly; + const char* what; +}; + +constexpr Transform Plain{false, 1.0f, 0.0f, 0.0f, 1.0f}; + +alignas(4096) std::array, 32> Depths{}; +alignas(256) std::array, 3>, 128> VertexSets{}; +std::size_t vertexSets = 0; + +std::uint32_t Draw(AgcDriver::VulkanDevice& device, std::uint64_t depth, const Transform& transform, bool remap, float z, bool write) { + Require(vertexSets < VertexSets.size(), "the test ran out of vertex sets"); + auto& vertices = VertexSets[vertexSets++]; + vertices = {{{-1.0f, -1.0f, z, 1.0f}, {3.0f, -1.0f, z, 1.0f}, {-1.0f, 3.0f, z, 1.0f}}}; + Harness::Pixels.fill(std::byte{0}); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Harness::Pixels.data()), {Harness::Width, Harness::Height}, VK_FORMAT_R8G8B8A8_UNORM, Harness::Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.depth = AgcDriver::Graphics::DepthTarget{depth, 0, {Harness::Width, Harness::Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + state.depthTest = true; + state.depthWrite = write; + state.depthCompare = write ? VK_COMPARE_OP_ALWAYS : VK_COMPARE_OP_LESS; + state.renderExtent = {Harness::Width, Harness::Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + const float minDepth = transform.negativeOneToOne ? transform.offset - transform.scale : transform.offset; + state.viewport = {0, static_cast(Harness::Height), static_cast(Harness::Width), -static_cast(Harness::Height), minDepth, transform.offset + transform.scale}; + state.negativeOneToOne = transform.negativeOneToOne; + state.depthScale = transform.scale; + state.depthOffset = transform.offset; + state.depthClampMin = transform.clampMin; + state.depthClampMax = transform.clampMax; + state.scissor = {{0, 0}, {Harness::Width, Harness::Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + auto context = device.graphicsContext(); + context.depthRangeUnrestricted = !remap; + AgcDriver::Graphics::ApplyDepthRemap(context, state); + const bool inRange = minDepth >= 0.0f && transform.offset + transform.scale <= 1.0f; + Require((state.depthRemap.mode != 0u) == (remap && !inRange), "the draw did not take the depth path the test asked for"); + + auto target = device.Target(); + target.depthRangeRemap = state.depthRemap.mode != 0u; + ShaderRecompiler::ShaderVertexStageInfo vertexInfo{}; + vertexInfo.depthRemapMode = state.depthRemap.mode; + vertexInfo.depthRemapScale = std::bit_cast(state.depthRemap.scale); + vertexInfo.depthRemapOffset = std::bit_cast(state.depthRemap.offset); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = Harness::BufferDescriptor(vertices.data(), 16u, static_cast(vertices.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(Harness::VertexCode.data()), std::as_bytes(std::span(Harness::VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(Harness::VertexCode.data()), Harness::VertexCode, 0, {}}, + {32, 0, vertexUserData, std::nullopt, std::nullopt, vertexInfo, vertexMemory}, + 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 = true; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + std::vector pixelUserData(4, 0u); + const std::array pixelMemory{{{reinterpret_cast(Harness::PixelCode.data()), std::as_bytes(std::span(Harness::PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(Harness::PixelCode.data()), Harness::PixelCode, 0, {}}, + {32, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(vertices.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); + std::uint32_t covered = 0; + for (std::size_t index = 0; index < Harness::Pixels.size() / 4u; ++index) { + if (std::to_integer(Harness::Pixels[index * 4u]) == 255u) ++covered; + } + return covered; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto context = device->graphicsContext(); + if (!context.depthClamp) { + std::puts("skipped, the device has no depth clamping"); + return VulkanTestSkipped; + } + constexpr float Step = 1.0f / 64.0f; + constexpr Transform minecraft{true, 1.0f, 0.0f, -1.0f, 1.0f}; + constexpr Transform widened{false, 2.0f, -0.5f, 0.0f, 1.0f}; + constexpr Transform bounded{false, 2.0f, -0.5f, 0.25f, 0.75f}; + constexpr std::array cases{{ + {minecraft, 0.5f - Step, true, 0.5f, false, false, "negative-one-to-one depth just below 0.5"}, + {minecraft, 0.5f + Step, true, 0.5f, true, false, "negative-one-to-one depth just above 0.5"}, + {minecraft, -1.5f, false, 0.5f, true, false, "negative-one-to-one clip z in front of the near plane"}, + {minecraft, 1.5f, false, 0.5f, true, false, "negative-one-to-one clip z behind the far plane"}, + {widened, 0.5f - Step / 2.0f, true, 0.5f, false, false, "scaled zero-to-one depth just below 0.5"}, + {widened, 0.5f + Step / 2.0f, true, 0.5f, true, false, "scaled zero-to-one depth just above 0.5"}, + {widened, -0.125f, false, 0.5f, true, false, "zero-to-one clip z in front of the near plane"}, + {widened, 1.125f, false, 0.5f, true, false, "zero-to-one clip z behind the far plane"}, + {bounded, 0.4f, true, 0.35f, false, false, "depth 0.3 inside clamp bounds [0.25, 0.75], probed at 0.35"}, + {bounded, 0.4f, true, 0.25f, true, false, "depth 0.3 inside clamp bounds [0.25, 0.75], probed at 0.25"}, + {bounded, 0.6f, true, 0.65f, true, false, "depth 0.7 inside clamp bounds [0.25, 0.75], probed at 0.65"}, + {bounded, 0.2f, true, 0.2f, true, true, "depth -0.1 clamped to 0.25, probed at 0.2"}, + {bounded, 0.8f, true, 0.8f, false, true, "depth 1.1 clamped to 0.75, probed at 0.8"}, + }}; + std::size_t surfaces = 0; + std::size_t ran = 0; + for (const bool remap : {true, false}) { + if (!remap && !context.depthRangeUnrestricted) continue; + for (const auto& test : cases) { + if (!remap && (test.remapOnly || (test.transform.negativeOneToOne && !context.depthClipControl))) continue; + Require(surfaces < Depths.size(), "the test ran out of depth surfaces"); + const auto depth = static_cast(reinterpret_cast(Depths[surfaces++].data())); + const auto drawn = Draw(*device, depth, test.transform, remap, test.z, true); + const auto passed = Draw(*device, depth, Plain, false, test.probe, false); + const auto path = remap ? "remapped" : "unrestricted"; + if (drawn != (test.drawn ? Harness::Covered : 0u) || passed != (test.covered ? Harness::Covered : 0u)) { + char detail[256]; + std::snprintf(detail, sizeof(detail), "%s (%s path): the draw covered %u of %u pixels (expected %s) and the probe passed %u (expected %s)", test.what, path, drawn, Harness::Covered, test.drawn ? "all" : "none", passed, test.covered ? "all" : "none"); + throw std::runtime_error(detail); + } + ++ran; + } + } + std::printf("depth range remap draw tests passed (%zu cases, unrestricted path %s)\n", ran, context.depthRangeUnrestricted ? "compared" : "absent"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 2af047862e50522f1f1883672764b3b213257cb8 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 18 Oct 2026 14:59:16 +1300 Subject: [PATCH 04/14] fix(agc): journal the viewport depth remap of a pipeline The depth remap mode, scale and offset change the pipeline a draw creates, so the pipeline journal's recipe carries them. The file format is bumped and the State layout guard follows the new size. --- .../libSceAgcDriver/Graphics/src/PipelineJournal.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 77d4566132..17ba1fec25 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -35,7 +35,7 @@ static_assert(sizeof(VkStencilOpState) == 28, "VkStencilOpState changed: update static_assert(sizeof(VkVertexInputBindingDescription) == 12, "VkVertexInputBindingDescription changed: update the recipe codec"); static_assert(sizeof(VkVertexInputAttributeDescription) == 16, "VkVertexInputAttributeDescription changed: update the recipe codec"); #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(State) == 752, "State changed: update CanonicalPipelineState and the recipe codec"); +static_assert(sizeof(State) == 784, "State changed: update CanonicalPipelineState and the recipe codec"); static_assert(sizeof(ShaderStages) == 112, "ShaderStages changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 40, "MeshConfiguration changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "TessellationConfiguration changed: update the recipe codec"); @@ -44,7 +44,7 @@ static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "Tessel constexpr std::uint32_t FileMagic = 0x4c4a5041u; // Bumped when the recipe codec, CanonicalPipelineState or the store key change in a way the // layout guards above do not catch (a field renumbered or reinterpreted at the same size). -constexpr std::uint32_t FileFormat = 3u; +constexpr std::uint32_t FileFormat = 4u; constexpr std::uint32_t ModuleRecord = 1u; constexpr std::uint32_t GraphicsRecord = 2u; constexpr std::uint32_t ComputeRecord = 3u; @@ -216,6 +216,9 @@ void encodeState(Writer& writer, const State& state) { writer.Bool(state.sampleLocationGrid); writer.Bool(state.perSampleShading); for (const auto mask : state.quadSampleMask) writer.Raw(mask); + writer.U32(state.depthRemap.mode); + writer.Raw(state.depthRemap.scale); + writer.Raw(state.depthRemap.offset); } void decodeState(Reader& reader, State& state) { @@ -274,6 +277,9 @@ void decodeState(Reader& reader, State& state) { state.sampleLocationGrid = reader.Bool(); state.perSampleShading = reader.Bool(); for (auto& mask : state.quadSampleMask) mask = reader.Raw(); + state.depthRemap.mode = reader.U32(); + state.depthRemap.scale = reader.Raw(); + state.depthRemap.offset = reader.Raw(); } void encodeRecipe(Writer& writer, const GraphicsPipelineRecipe& recipe) { @@ -1037,6 +1043,7 @@ State CanonicalPipelineState(const State& state) { canonical.sampleLocationGrid = state.sampleLocationGrid; canonical.perSampleShading = state.perSampleShading; canonical.quadSampleMask = state.quadSampleMask; + canonical.depthRemap = state.depthRemap; return canonical; } From 2ec16fb1c2adf4911186a8f7886337b6dc1ea483 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 18 Oct 2026 16:38:30 +1300 Subject: [PATCH 05/14] test(agc): follow request format 33 in the serializer replay tests The depth remap fields moved to request format 33 when this branch was rebased onto main's format 32. The replay tests that rebuild older streams now drop and append the remap tail on top of the linked call fallbacks and the spanned waves flag, and the depth remap test downgrades to format 32. --- .../prx/libSceAgcDriver/tests/ShaderMemory.cpp | 14 +++++++------- .../recompiler/tests/RequestSerializerTests.cpp | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 285203fb9b..ac8f914be8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3553,16 +3553,16 @@ void verifyDepthRemapRequests() { const auto replay = serializer.Deserialize(remapped); require(replay.request.target.depthRangeRemap && replay.request.context.vertex.has_value() && replay.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "the depth remap target flag or the vertex info did not round-trip"); require(replay.request.context.vertex->depthRemapMode == 2u && replay.request.context.vertex->depthRemapScale == 0x40000000u && replay.request.context.vertex->depthRemapOffset == 0xbf000000u, "the depth remap mode, scale or offset did not round-trip"); - require(serializer.Serialize(replay.request) == remapped, "a version 27 depth remap request changed on re-serialization"); + require(serializer.Serialize(replay.request) == remapped, "a version 33 depth remap request changed on re-serialization"); const auto plainBytes = decodeBase64(plain); const auto remappedBytes = decodeBase64(remapped); require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 13u && std::equal(plainBytes.begin(), plainBytes.end() - 13, remappedBytes.begin()), "the depth remap fields are not the last 13 bytes of a vertex request"); - auto version25 = remappedBytes; - version25[4] = 25u; - version25.resize(version25.size() - 13u); - const auto legacy = serializer.Deserialize(encodeBase64(version25)); - require(!legacy.request.target.depthRangeRemap && legacy.request.context.vertex.has_value() && legacy.request.context.vertex->depthRemapMode == 0u && legacy.request.context.vertex->depthRemapScale == 0u && legacy.request.context.vertex->depthRemapOffset == 0u, "a version 25 vertex request read depth remap fields"); - require(serializer.Serialize(legacy.request) == plain, "a version 25 vertex request misread the fields before the depth remap"); + auto version32 = remappedBytes; + version32[4] = 32u; + version32.resize(version32.size() - 13u); + const auto legacy = serializer.Deserialize(encodeBase64(version32)); + require(!legacy.request.target.depthRangeRemap && legacy.request.context.vertex.has_value() && legacy.request.context.vertex->depthRemapMode == 0u && legacy.request.context.vertex->depthRemapScale == 0u && legacy.request.context.vertex->depthRemapOffset == 0u, "a version 32 vertex request read depth remap fields"); + require(serializer.Serialize(legacy.request) == plain, "a version 32 vertex request misread the fields before the depth remap"); auto truncated = remappedBytes; truncated.pop_back(); expectFailure([&] { static_cast(serializer.Deserialize(encodeBase64(truncated))); }, "truncated data", "a truncated depth remap offset was accepted"); diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index dc716d625f..5a58051e53 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -270,7 +270,7 @@ int main() { Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); Require(replay.request.target.float16DenormPreserve == preserve); - bytes[4] = 32u; + bytes[4] = 33u; bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u + 13u, 0u); @@ -394,7 +394,7 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 32u; + upgraded[4] = 33u; if (fixture.pixel) upgraded.insert(upgraded.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 13u, 0u); @@ -407,12 +407,12 @@ int main() { const auto replay25 = serializer.Deserialize(EncodeBase64(main25)); Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u)); Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u)); - main25[4] = 32u; + main25[4] = 33u; if (fixture.pixel) { main25.insert(main25.begin() + 260u, 0u); InsertPixelDefaultsAfterFrontFace(main25); } - main25.insert(main25.end(), sizeof(std::uint64_t) + 1u, 0u); + main25.insert(main25.end(), sizeof(std::uint64_t) + 1u + 13u, 0u); Require(serializer.Serialize(replay25.request) == EncodeBase64(main25)); } } From 5db206aba0d0c90f3ca0ebfa19fc6350dc8c60cf Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 18 Oct 2026 19:01:06 +1300 Subject: [PATCH 06/14] fix(agc): read the depth remap scale and offset from push constants at run time The remap mode stays a specialization constant and cache key input. Scale and offset are two push words at byte 104, patched per draw, so draws that differ only in depth range share one pipeline. The recipe, journal and serializer carry the mode only. --- .../Execution/src/Driver/Draw/Draw.cpp | 2 - .../Execution/src/Driver/Draw/DrawCapture.cpp | 2 +- .../src/Driver/Draw/DrawRegisterKey.cpp | 2 +- .../src/Driver/Shaders/ShaderRegistry.cpp | 2 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 11 +++++ .../Graphics/src/PipelineJournal.cpp | 6 +-- .../libSceAgcDriver/tests/DepthRangeRemap.cpp | 49 ++++++++++++++----- .../libSceAgcDriver/tests/ShaderMemory.cpp | 17 ++----- .../tests/execution/DepthRangeRemapDraw.cpp | 23 ++++++++- .../ControlFlow/src/RequestSerializer.cpp | 4 -- .../Optimization/ShaderStageInputInfo.hpp | 2 - .../recompiler/PipelineSpecialization.hpp | 2 +- core/shader/recompiler/Recompiler.cpp | 7 ++- core/shader/recompiler/Recompiler.hpp | 4 +- .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 2 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 18 ++++--- .../src/ShaderInputInfoBuilder.cpp | 2 +- .../tests/RequestSerializerTests.cpp | 10 ++-- docs/dev/debt/libSceAgcDriver.md | 2 +- 19 files changed, 105 insertions(+), 62 deletions(-) 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 048c9322d5..cb71dec602 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -244,8 +244,6 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe if (vertexInfos[i]) vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207), program.tailContinuation.get()) | graphics.layerExports; if (vertexInfos[i]) { vertexInfos[i]->depthRemapMode = graphics.depthRemap.mode; - vertexInfos[i]->depthRemapScale = std::bit_cast(graphics.depthRemap.scale); - vertexInfos[i]->depthRemapOffset = std::bit_cast(graphics.depthRemap.offset); } }; if (!registerKey) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp index 616d63c690..f08ea4c1d7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -23,7 +23,7 @@ std::shared_ptr Driver::materializeDraw program.binary, {waveSize, program.firstUserSgpr, program.userData, std::nullopt, program.binary.stage == Stage::Fragment ? std::optional(pixel) : std::nullopt, vertexInfos[i], memory, RegisteredFloatMode(*program.snapshot)}, localDevice->Target(), - {0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : graphics.stages.tessellation && pushOffset < Graphics::PipelinePushSlotBytes ? ShaderRecompiler::TessellationLevelPushOffsetBytes : Graphics::PipelinePushSlotBytes) - pushOffset % Graphics::PipelinePushSlotBytes}, + {0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : graphics.stages.tessellation && pushOffset < Graphics::PipelinePushSlotBytes ? ShaderRecompiler::TessellationLevelPushOffsetBytes : graphics.depthRemap.mode != 0u ? ShaderRecompiler::DepthRemapPushOffsetBytes : Graphics::PipelinePushSlotBytes) - pushOffset % Graphics::PipelinePushSlotBytes}, ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, graphics.stages.mesh, graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}} }; const auto waitedBefore = traceCapSync() || profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index 4f08f7d974..6b45803a45 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -214,7 +214,7 @@ void Driver::traceDrawKeyLookup(const std::array& components, bool Driver::sameVertexInfo(const ShaderRecompiler::ShaderVertexStageInfo& a, const ShaderRecompiler::ShaderVertexStageInfo& b) { if (a.resourcesNum != b.resourcesNum || a.fetchAttribReg != b.fetchAttribReg || a.fetchBufferReg != b.fetchBufferReg || a.fetchEmbedded != b.fetchEmbedded || a.paClVsOutCntl != b.paClVsOutCntl) return false; - if (a.depthRemapMode != b.depthRemapMode || a.depthRemapScale != b.depthRemapScale || a.depthRemapOffset != b.depthRemapOffset) return false; + if (a.depthRemapMode != b.depthRemapMode) return false; for (std::uint32_t i = 0; i < a.resourcesNum && i < a.resources.size(); ++i) { if (a.resources[i].fields != b.resources[i].fields) return false; const auto& x = a.resourcesDst[i]; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 2a654e0e53..db733cfd13 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1202,7 +1202,7 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod std::vector prepared; std::uint32_t pushOffset = 0; const bool fixedSlots = target.fixedPushSlots; - const auto capacity = decoded.state.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : decoded.state.stages.tessellation ? ShaderRecompiler::TessellationLevelPushOffsetBytes : Graphics::PipelinePushSlotBytes; + const auto capacity = decoded.state.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : decoded.state.stages.tessellation ? ShaderRecompiler::TessellationLevelPushOffsetBytes : decoded.state.depthRemap.mode != 0u ? ShaderRecompiler::DepthRemapPushOffsetBytes : Graphics::PipelinePushSlotBytes; for (std::size_t index = 0; index < decoded.programs.size(); ++index) { if (decoded.roles[index] == ShaderRecompiler::ProgramRole::GeometryBack) continue; const auto& program = decoded.programs[index]; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index d23793371f..b77f6501db 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1603,6 +1603,15 @@ struct RecordedDraw { std::vector>> stencilReplay; }; +// A remapped viewport depth range reads its scale and offset from the draw's push block (the vertex shader's +// RemapPositionDepth), so draws that differ only in them share one pipeline; the draw's own state supplies them. +void patchDepthRemapPush(std::array& block, const State& state) { + if (state.depthRemap.mode == 0u) return; + static_assert(ShaderRecompiler::DepthRemapPushOffsetBytes + ShaderRecompiler::DepthRemapPushBytes <= PipelinePushSlotBytes); + const std::array words{state.depthRemap.scale, state.depthRemap.offset}; + std::memcpy(block.data() + ShaderRecompiler::DepthRemapPushOffsetBytes, words.data(), sizeof(words)); +} + void recordStencilReplay(const Context& context, VkCommandBuffer commands, const State& state, const Pm4::DrawParameters& draw, const DrawInputs& inputs, std::span>> replay, std::span shaders, const ShaderResources& resources, const std::array* bytes = nullptr, VkShaderStageFlags stages = 0) { State selected = state; auto block = bytes != nullptr ? *bytes : AssemblePushConstants(shaders); @@ -1610,6 +1619,7 @@ void recordStencilReplay(const Context& context, VkCommandBuffer commands, const resources.PatchPushConstants(block); stages = PushConstantStages(shaders); } + patchDepthRemapPush(block, state); const auto fragment = std::ranges::find_if(shaders, [](const CompiledShader& shader) { return shader.stage == ShaderRecompiler::ShaderStage::Fragment && !shader.program->stencilReferences.empty(); }); Require(fragment != shaders.end(), "stencil replay fragment stage is missing"); const auto referenceOffset = fragment->pushConstantOffset + fragment->program->stencilReferenceDword * sizeof(std::uint32_t); @@ -1647,6 +1657,7 @@ void pushDrawConstants(const Pipeline& pipeline, VkCommandBuffer commands, const resources.PatchPushConstants(block); stages = PushConstantStages(shaders); } + patchDepthRemapPush(block, state); if (state.stages.tessellation) { for (const auto& shader : shaders) Require(shader.program->pushConstants.empty() || shader.pushConstantOffset >= PipelinePushSlotBytes || shader.pushConstantOffset + shader.program->pushConstants.size() <= ShaderRecompiler::TessellationLevelPushOffsetBytes, "a stage's push constants overlap the tessellation levels"); const std::array levels{state.stages.minTessellationLevel, state.stages.maxTessellationLevel}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 17ba1fec25..5657d3d0bb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -217,8 +217,6 @@ void encodeState(Writer& writer, const State& state) { writer.Bool(state.perSampleShading); for (const auto mask : state.quadSampleMask) writer.Raw(mask); writer.U32(state.depthRemap.mode); - writer.Raw(state.depthRemap.scale); - writer.Raw(state.depthRemap.offset); } void decodeState(Reader& reader, State& state) { @@ -278,8 +276,6 @@ void decodeState(Reader& reader, State& state) { state.perSampleShading = reader.Bool(); for (auto& mask : state.quadSampleMask) mask = reader.Raw(); state.depthRemap.mode = reader.U32(); - state.depthRemap.scale = reader.Raw(); - state.depthRemap.offset = reader.Raw(); } void encodeRecipe(Writer& writer, const GraphicsPipelineRecipe& recipe) { @@ -1043,7 +1039,7 @@ State CanonicalPipelineState(const State& state) { canonical.sampleLocationGrid = state.sampleLocationGrid; canonical.perSampleShading = state.perSampleShading; canonical.quadSampleMask = state.quadSampleMask; - canonical.depthRemap = state.depthRemap; + canonical.depthRemap.mode = state.depthRemap.mode; return canonical; } diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp index 05a7ca231f..d2bce12ec3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace { @@ -18,6 +19,8 @@ struct ModuleFacts { std::uint32_t clipDistanceLength = 0; bool clipDistanceBuiltIn = false; std::size_t multiplies = 0; + bool pushConstantVariable = false; + std::set pushDwordLoads; }; ModuleFacts inspect(const ShaderRecompiler::RecompileResult& result) { @@ -30,6 +33,8 @@ ModuleFacts inspect(const ShaderRecompiler::RecompileResult& result) { std::map pointerTargets; std::map variableTypes; std::set clipVariables; + std::set pushVariables; + std::vector pushIndexIds; for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { const auto op = static_cast(words[at] & 0xffffu); const auto count = words[at] >> 16u; @@ -40,11 +45,17 @@ ModuleFacts inspect(const ShaderRecompiler::RecompileResult& result) { if (op == spv::OpTypeArray && count == 4) arrayLengths[words[at + 1]] = words[at + 3]; if (op == spv::OpTypePointer && count == 4) pointerTargets[words[at + 1]] = words[at + 3]; if (op == spv::OpVariable && count >= 4) variableTypes[words[at + 2]] = words[at + 1]; + if (op == spv::OpVariable && count >= 4 && words[at + 3] == spv::StorageClassPushConstant) pushVariables.insert(words[at + 2]); + if (op == spv::OpAccessChain && count == 6 && pushVariables.contains(words[at + 3])) pushIndexIds.push_back(words[at + 5]); if (op == spv::OpFMul) ++facts.multiplies; } for (const auto& [id, specId] : specIds) { if (const auto value = specValues.find(id); value != specValues.end()) facts.specializationDefaults[specId] = value->second; } + facts.pushConstantVariable = !pushVariables.empty(); + for (const auto id : pushIndexIds) { + if (const auto value = constants.find(id); value != constants.end()) facts.pushDwordLoads.insert(value->second); + } for (const auto variable : clipVariables) { facts.clipDistanceBuiltIn = true; const auto pointer = variableTypes.find(variable); @@ -68,8 +79,6 @@ ShaderRecompiler::RecompileRequest baseRequest(bool remap) { request.context.waveSize = 64; ShaderVertexStageInfo vertex{}; vertex.depthRemapMode = 2u; - vertex.depthRemapScale = std::bit_cast(1.0f); - vertex.depthRemapOffset = std::bit_cast(0.0f); request.context.vertex = vertex; request.target.vulkanVersion = 0x00401000u; request.target.spirvVersion = 0x00010300u; @@ -77,7 +86,7 @@ ShaderRecompiler::RecompileRequest baseRequest(bool remap) { static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityClipDistance}; request.target.supportedCapabilities = capabilities; request.target.depthRangeRemap = remap; - request.layout.pushConstantSizeBytes = 128; + request.layout.pushConstantSizeBytes = ShaderRecompiler::DepthRemapPushOffsetBytes; request.useCache = false; return request; } @@ -91,20 +100,37 @@ void preparedTransformTests() { auto request = baseRequest(true); request.useCache = true; const auto handle = PrepareShader(request); - const auto materialize = [&](float scale, float offset) { + const auto materialize = [&](std::uint32_t mode) { auto moved = request; - moved.context.vertex->depthRemapScale = std::bit_cast(scale); - moved.context.vertex->depthRemapOffset = std::bit_cast(offset); + moved.context.vertex->depthRemapMode = mode; const auto invocation = PreparedShaderInvocation::TryCreate(moved, handle); - require(invocation.has_value(), "another depth transform did not match the prepared vertex shader"); + require(invocation.has_value(), "the prepared vertex shader did not match a request of its own mode"); return invocation->Materialize(*invocation->Capture(SrtRuntime{})); }; - const auto first = materialize(1.0f, 0.0f); - const auto scaled = materialize(4.0f, -1.5f); - require(scaled->PipelineVariantId() != first->PipelineVariantId(), "a prepared vertex shader reused the result materialized for another depth transform"); - require(materialize(1.0f, 0.0f)->PipelineVariantId() == first->PipelineVariantId(), "the same depth transform did not reuse its materialized vertex shader"); + const auto first = materialize(2u); + require(materialize(2u)->PipelineVariantId() == first->PipelineVariantId(), "the same depth remap mode did not reuse its materialized vertex shader"); + auto other = request; + other.context.vertex->depthRemapMode = 1u; + require(!PreparedShaderInvocation::TryCreate(other, handle).has_value(), "a prepared vertex shader matched a request of another depth remap mode"); } +void pushedTransformTests() { + using namespace ShaderRecompiler; + const auto remapped = Recompile(baseRequest(true)); + const auto plain = Recompile(baseRequest(false)); + const auto facts = inspect(remapped); + require(facts.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase) && !facts.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 1u) && !facts.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 2u), "only the depth remap mode may be a specialization constant"); + require(facts.pushConstantVariable && facts.pushDwordLoads.contains(DepthRemapPushOffsetBytes / 4u) && facts.pushDwordLoads.contains(DepthRemapPushOffsetBytes / 4u + 1u), "the remapped vertex shader does not read the scale and offset from the push block"); + require(!inspect(plain).pushConstantVariable, "a vertex shader without a depth remap gained a push constant block"); + auto crowded = baseRequest(true); + crowded.layout.pushConstantSizeBytes = DepthRemapPushOffsetBytes + 4u; + bool refused = false; + try { + static_cast(Recompile(crowded)); + } catch (const std::runtime_error&) { + refused = true; + } + require(refused, "a remapped vertex shader with push data over the depth remap words was accepted"); } int main() { @@ -116,6 +142,7 @@ int main() { require(remapped.multiplies >= plain.multiplies + 2u, "the remap does not scale the clip z and offset it by w"); require(!plain.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase) && !remapped.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 3u), "unexpected specialization constants"); preparedTransformTests(); + pushedTransformTests(); std::cout << "Depth range remap tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index ac8f914be8..498ead6c7f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3547,25 +3547,23 @@ void verifyDepthRemapRequests() { const auto plain = serializer.Serialize(vertex); vertex.target.depthRangeRemap = true; vertex.context.vertex->depthRemapMode = 2u; - vertex.context.vertex->depthRemapScale = 0x40000000u; - vertex.context.vertex->depthRemapOffset = 0xbf000000u; const auto remapped = serializer.Serialize(vertex); const auto replay = serializer.Deserialize(remapped); require(replay.request.target.depthRangeRemap && replay.request.context.vertex.has_value() && replay.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "the depth remap target flag or the vertex info did not round-trip"); - require(replay.request.context.vertex->depthRemapMode == 2u && replay.request.context.vertex->depthRemapScale == 0x40000000u && replay.request.context.vertex->depthRemapOffset == 0xbf000000u, "the depth remap mode, scale or offset did not round-trip"); + require(replay.request.context.vertex->depthRemapMode == 2u, "the depth remap mode did not round-trip"); require(serializer.Serialize(replay.request) == remapped, "a version 33 depth remap request changed on re-serialization"); const auto plainBytes = decodeBase64(plain); const auto remappedBytes = decodeBase64(remapped); - require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 13u && std::equal(plainBytes.begin(), plainBytes.end() - 13, remappedBytes.begin()), "the depth remap fields are not the last 13 bytes of a vertex request"); + require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 5u && std::equal(plainBytes.begin(), plainBytes.end() - 5, remappedBytes.begin()), "the depth remap flag and mode are not the last 5 bytes of a vertex request"); auto version32 = remappedBytes; version32[4] = 32u; - version32.resize(version32.size() - 13u); + version32.resize(version32.size() - 5u); const auto legacy = serializer.Deserialize(encodeBase64(version32)); - require(!legacy.request.target.depthRangeRemap && legacy.request.context.vertex.has_value() && legacy.request.context.vertex->depthRemapMode == 0u && legacy.request.context.vertex->depthRemapScale == 0u && legacy.request.context.vertex->depthRemapOffset == 0u, "a version 32 vertex request read depth remap fields"); + require(!legacy.request.target.depthRangeRemap && legacy.request.context.vertex.has_value() && legacy.request.context.vertex->depthRemapMode == 0u, "a version 32 vertex request read depth remap fields"); require(serializer.Serialize(legacy.request) == plain, "a version 32 vertex request misread the fields before the depth remap"); auto truncated = remappedBytes; truncated.pop_back(); - expectFailure([&] { static_cast(serializer.Deserialize(encodeBase64(truncated))); }, "truncated data", "a truncated depth remap offset was accepted"); + expectFailure([&] { static_cast(serializer.Deserialize(encodeBase64(truncated))); }, "truncated data", "a truncated depth remap mode was accepted"); auto trailing = remappedBytes; trailing.push_back(0u); expectFailure([&] { static_cast(serializer.Deserialize(encodeBase64(trailing))); }, "unexpected trailing data", "a byte after the depth remap fields was accepted"); @@ -3579,11 +3577,6 @@ void verifyDepthRemapRequests() { std::vector key; std::vector other; RecompileCacheKey::Build(vertex, key); - auto moved = vertex; - moved.context.vertex->depthRemapScale = 0x3f000000u; - moved.context.vertex->depthRemapOffset = 0x3e800000u; - RecompileCacheKey::Build(moved, other); - require(key == other, "the viewport depth scale or offset entered the shader cache key"); auto zeroToOne = vertex; zeroToOne.context.vertex->depthRemapMode = 1u; RecompileCacheKey::Build(zeroToOne, other); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthRangeRemapDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthRangeRemapDraw.cpp index 1a4a56ebf8..b0d533d202 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthRangeRemapDraw.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthRangeRemapDraw.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include "Recompiler.hpp" #include "DepthFastClearHarness.hpp" @@ -87,8 +88,6 @@ std::uint32_t Draw(AgcDriver::VulkanDevice& device, std::uint64_t depth, const T target.depthRangeRemap = state.depthRemap.mode != 0u; ShaderRecompiler::ShaderVertexStageInfo vertexInfo{}; vertexInfo.depthRemapMode = state.depthRemap.mode; - vertexInfo.depthRemapScale = std::bit_cast(state.depthRemap.scale); - vertexInfo.depthRemapOffset = std::bit_cast(state.depthRemap.offset); std::vector vertexUserData(4, 0u); const auto vertexBuffer = Harness::BufferDescriptor(vertices.data(), 16u, static_cast(vertices.size())); std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); @@ -183,6 +182,26 @@ int main() { ++ran; } } + // Scale and offset reach the vertex shader through the draw's push block, so two draws whose depth remap + // differs only in them build one pipeline between them: the second finds it in the store, and both + // store the depth they should. + { + constexpr Transform widerScale{false, 4.0f, -1.5f, 0.0f, 1.0f}; + Require(surfaces + 2u <= Depths.size(), "the test ran out of depth surfaces"); + const auto depthA = static_cast(reinterpret_cast(Depths[surfaces++].data())); + const auto depthB = static_cast(reinterpret_cast(Depths[surfaces++].data())); + const auto first = Draw(*device, depthA, widened, true, 0.5f + Step / 2.0f, true); + const auto before = AgcDriver::Graphics::PipelineStoreTotals(); + const auto second = Draw(*device, depthB, widerScale, true, 0.5f + Step / 4.0f, true); + const auto after = AgcDriver::Graphics::PipelineStoreTotals(); + Require(first == Harness::Covered && second == Harness::Covered, "a draw with a remapped depth did not cover the target"); + Require(after.misses == before.misses && after.hits > before.hits, "a draw that differs only in the depth scale and offset built another pipeline"); + for (const auto depth : {depthA, depthB}) { + Require(Draw(*device, depth, Plain, false, 0.5f, false) == Harness::Covered, "a depth remapped by a shared pipeline was stored below 0.5 + 1/64"); + Require(Draw(*device, depth, Plain, false, 0.5f + 2.0f * Step, false) == 0u, "a depth remapped by a shared pipeline was stored above 0.5 + 1/64"); + } + ++ran; + } std::printf("depth range remap draw tests passed (%zu cases, unrestricted path %s)\n", ran, context.depthRangeUnrestricted ? "compared" : "absent"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index cb4728ae18..f6a9ca13e1 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -807,8 +807,6 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const writer.WriteBool(request.target.depthRangeRemap); if (request.context.vertex.has_value()) { writer.WriteU32(request.context.vertex->depthRemapMode); - writer.WriteU32(request.context.vertex->depthRemapScale); - writer.WriteU32(request.context.vertex->depthRemapOffset); } return base64Encode(buffer); } @@ -859,8 +857,6 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const result.request.target.depthRangeRemap = reader.ReadBool(); if (result.vertex.has_value()) { result.vertex->depthRemapMode = reader.ReadU32(); - result.vertex->depthRemapScale = reader.ReadU32(); - result.vertex->depthRemapOffset = reader.ReadU32(); result.request.context.vertex = result.vertex; } } diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index c75cfc8363..46af4ad396 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -71,8 +71,6 @@ struct ShaderStageRuntime { struct ShaderDepthRemap { std::uint32_t mode = 0; - std::uint32_t scaleBits = 0; - std::uint32_t offsetBits = 0; }; struct ShaderClipSpaceTransform { diff --git a/core/shader/recompiler/PipelineSpecialization.hpp b/core/shader/recompiler/PipelineSpecialization.hpp index 08c321a9da..35059904f2 100644 --- a/core/shader/recompiler/PipelineSpecialization.hpp +++ b/core/shader/recompiler/PipelineSpecialization.hpp @@ -31,7 +31,7 @@ inline constexpr std::uint32_t LayerExported = 8234u; inline constexpr std::uint32_t StencilReferenceOffset = 8235u; inline constexpr std::uint32_t ReversedBlend = 8236u; inline constexpr std::uint32_t DepthRemapBase = 8240u; -inline constexpr std::uint32_t DepthRemapWords = 3u; +inline constexpr std::uint32_t DepthRemapWords = 1u; inline constexpr std::uint32_t HeapCountBase = 8256u; inline constexpr std::uint32_t MipCountBase = 8320u; inline constexpr std::uint32_t DescriptorIndexBase = 16384u; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 3b092fddd7..ed4cc2b844 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -908,9 +908,10 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil } const bool depthRemap = request.target.depthRangeRemap && request.shader.stage == ShaderStage::Vertex && request.context.vertex.has_value(); if (depthRemap) { + if (request.context.vertex->depthRemapMode != 0u && request.layout.pushConstantOffsetBytes % (RuntimeAbi::PushConstantDwords * 4u) + request.layout.pushConstantSizeBytes > DepthRemapPushOffsetBytes) { + throw std::runtime_error("a remapped vertex shader's push data must end before the depth remap words"); + } moduleKey.push_back(request.context.vertex->depthRemapMode); - moduleKey.push_back(request.context.vertex->depthRemapScale); - moduleKey.push_back(request.context.vertex->depthRemapOffset); } result.vertexAttributes.reserve(result.vertexInputs.size()); std::array vertexClasses{}; @@ -1246,8 +1247,6 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh for (const auto field : vertex.resources[i].fields) mix(field); mix(vertex.resourcesDst[i].fetchIndex); } - mix(vertex.depthRemapScale); - mix(vertex.depthRemapOffset); } else { mix(1ull << 32u); } diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 2faf18545e..2b3761cf75 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -197,8 +197,6 @@ struct ShaderVertexStageInfo { bool fetchEmbedded; std::uint32_t paClVsOutCntl = 0; std::uint32_t depthRemapMode = 0; - std::uint32_t depthRemapScale = 0; - std::uint32_t depthRemapOffset = 0; }; struct ShaderFloatMode { @@ -330,6 +328,8 @@ inline constexpr std::uint32_t MeshDrawPushOffsetBytes = 104; inline constexpr std::uint32_t MeshDrawPushBytes = 24; inline constexpr std::uint32_t TessellationLevelPushOffsetBytes = 120; inline constexpr std::uint32_t TessellationLevelPushBytes = 8; +inline constexpr std::uint32_t DepthRemapPushOffsetBytes = 104; +inline constexpr std::uint32_t DepthRemapPushBytes = 8; inline constexpr std::uint32_t MeshArgumentAddressDword = 4; inline constexpr std::uint32_t MeshArgumentIndexCountDword = 3; inline constexpr std::uint32_t MeshArgumentFirstIndexDword = 4; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 5119368611..6c81c60221 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -422,7 +422,7 @@ void DefineOutputs(SpirvEmitterState& state) { void DefineDescriptors(SpirvEmitterState& state) { const IrBindingLayout& layout = state.program.Metadata().bindings; const IrShaderStage stage = state.program.Resources().stage; - if (layout.UsesPushData() || state.stencilReplay || stage == IrShaderStage::Mesh || stage == IrShaderStage::TessellationControl) { + if (layout.UsesPushData() || state.stencilReplay || stage == IrShaderStage::Mesh || stage == IrShaderStage::TessellationControl || (state.depthRangeRemap && stage == IrShaderStage::Vertex)) { const auto type = PushConstantBlockType(state); state.pushConstantVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPushConstant, type), spv::StorageClassPushConstant); state.module.AddName(type, "BufferResource"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 4f76d0a5c1..6ade936360 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -526,12 +526,18 @@ std::uint32_t RemapPositionDepth(SpirvEmitterState& state, std::uint32_t positio state.module.AddFunction(spv::OpCompositeExtract, f32, components.at(i), position, i); } const auto mode = state.module.SpecializationConstant(u32, PipelineSpecialization::DepthRemapBase, remap.mode); - const auto scaleBits = state.module.SpecializationConstant(u32, PipelineSpecialization::DepthRemapBase + 1u, remap.scaleBits); - const auto offsetBits = state.module.SpecializationConstant(u32, PipelineSpecialization::DepthRemapBase + 2u, remap.offsetBits); - const auto scale = state.module.AllocateId(); - state.module.AddFunction(spv::OpBitcast, f32, scale, scaleBits); - const auto offset = state.module.AllocateId(); - state.module.AddFunction(spv::OpBitcast, f32, offset, offsetBits); + if (state.pushConstantVariable == 0u) throw std::runtime_error("depth remap push constants were not declared"); + const auto pushFloat = [&](std::uint32_t dword) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePushConstantElementPointer(state), pointer, state.pushConstantVariable, ConstantU32(state, 0u), ConstantU32(state, DepthRemapPushOffsetBytes / 4u + dword)); + const auto bits = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, u32, bits, pointer); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitcast, f32, value, bits); + return value; + }; + const auto scale = pushFloat(0u); + const auto offset = pushFloat(1u); const auto scaled = state.module.AllocateId(); state.module.AddFunction(spv::OpFMul, f32, scaled, components[2], scale); const auto shifted = state.module.AllocateId(); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 1728ff1a4f..025e1f1c0a 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -171,7 +171,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues vertexStorage.hostSubgroupSize = hostSubgroupSize; vertexStorage.fetchEmbedded = vertex.fetchEmbedded; vertexStorage.paClVsOutCntl = vertex.paClVsOutCntl; - vertexStorage.depthRemap = {vertex.depthRemapMode, vertex.depthRemapScale, vertex.depthRemapOffset}; + vertexStorage.depthRemap = {vertex.depthRemapMode}; vertexStorage.fetchExternal = false; vertexStorage.fetchAttribReg = static_cast(vertex.fetchAttribReg); vertexStorage.fetchBufferReg = static_cast(vertex.fetchBufferReg); diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 5a58051e53..a11fd53fd9 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -210,7 +210,7 @@ int main() { if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 5u, 0u); - upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 13u, 0u); + upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u, 0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); auto truncated = bytes; truncated.pop_back(); @@ -249,7 +249,7 @@ int main() { layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); if (fixture.tessellation) layered.insert(layered.end() - 35u, 5u, 0u); - layered.insert(layered.end(), sizeof(std::uint64_t) + 1u + 13u, 0u); + layered.insert(layered.end(), sizeof(std::uint64_t) + 1u + 5u, 0u); Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); } } @@ -273,7 +273,7 @@ int main() { bytes[4] = 33u; bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); - bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u + 13u, 0u); + bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u + 5u, 0u); Require(serializer.Serialize(replay.request) == EncodeBase64(bytes)); auto truncated23 = DecodeBase64(encoded23); @@ -397,7 +397,7 @@ int main() { upgraded[4] = 33u; if (fixture.pixel) upgraded.insert(upgraded.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); - upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 13u, 0u); + upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u, 0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); for (const bool dither : {false, true}) { if (!fixture.pixel && dither) continue; @@ -412,7 +412,7 @@ int main() { main25.insert(main25.begin() + 260u, 0u); InsertPixelDefaultsAfterFrontFace(main25); } - main25.insert(main25.end(), sizeof(std::uint64_t) + 1u + 13u, 0u); + main25.insert(main25.end(), sizeof(std::uint64_t) + 1u + 5u, 0u); Require(serializer.Serialize(replay25.request) == EncodeBase64(main25)); } } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index daac3bed96..4c02450d9a 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -174,4 +174,4 @@ - [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted when blending is disabled (ENABLE clear or BLEND_BYPASS set). Vulkan converts unblended shader values and clamps to the normalized range automatically; blending into an unclamped normalized target remains unmodeled because Vulkan always clamps inputs prior to blending. - [Thick SW_4KB_S_X volumes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) use AMD addrlib's GFX10 non-RB+ 16-pipe `GFX10_SW_4K_S3_X_PATINFO` equations for 1, 2, 4, 8 and 16-byte elements, including packed mip tails. The layout follows [AMD PAL c5e80007](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp); it has not been measured on console hardware. Other thick swizzle modes outside the supported families still throw. Mipmapped 3D color targets remain refused by the color-register decoder. -- [Viewport depth range remap](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - on a device without `VK_EXT_depth_range_unrestricted` a viewport depth range outside [0, 1] (Minecraft PPSA17221 draws its world with `PA_CL_VPORT_ZSCALE` 1, `ZOFFSET` 0 in negative-one-to-one clip space) is folded into the vertex shader: `RemapPositionDepth` scales clip z so z/w is the console's depth, the pipeline's depth clamp applies the console's `PA_SC_VPORT_ZMIN/ZMAX` clipped to [0, 1] (exact for a normalized depth target or none; Minecraft clamps its float depth target to [-1, 1], so a depth below 0 is stored as 0 there, which only a later test or read at exactly 0 can tell, and the first such draw is reported once), and two extra `gl_ClipDistance` elements restore the near and far planes depth clamping removes. Mode, scale and offset are pipeline specialization constants (`PipelineSpecialization::DepthRemapBase`), so each distinct depth transform is one vertex module variant. The remap serves the vertex path only; mesh and tessellation draws with such a range still throw. Whether the console clamps before or after its depth bias and how it treats a clamp range narrower than the transform's are not measured. +- [Viewport depth range remap](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - on a device without `VK_EXT_depth_range_unrestricted` a viewport depth range outside [0, 1] (Minecraft PPSA17221 draws its world with `PA_CL_VPORT_ZSCALE` 1, `ZOFFSET` 0 in negative-one-to-one clip space) is folded into the vertex shader: `RemapPositionDepth` scales clip z so z/w is the console's depth, the pipeline's depth clamp applies the console's `PA_SC_VPORT_ZMIN/ZMAX` clipped to [0, 1] (exact for a normalized depth target or none; Minecraft clamps its float depth target to [-1, 1], so a depth below 0 is stored as 0 there, which only a later test or read at exactly 0 can tell, and the first such draw is reported once), and two extra `gl_ClipDistance` elements restore the near and far planes depth clamping removes. The mode is a pipeline specialization constant (`PipelineSpecialization::DepthRemapBase`); scale and offset are read from the draw's push block (two words at byte 104, `DepthRemapPushOffsetBytes`, so a remapped draw's stage push data ends before it), so draws that differ only in them share one vertex module and one pipeline. The remap serves the vertex path only; mesh and tessellation draws with such a range still throw. Whether the console clamps before or after its depth bias and how it treats a clamp range narrower than the transform's are not measured. From d6f03d5f17c090ca2b21d6cfe2f882b2d77d660c Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 18 Oct 2026 21:06:17 +1300 Subject: [PATCH 07/14] fix(agc): close the anonymous namespace in the depth range remap test --- core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp index d2bce12ec3..b3738e9067 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp @@ -133,6 +133,8 @@ void pushedTransformTests() { require(refused, "a remapped vertex shader with push data over the depth remap words was accepted"); } +} + int main() { using namespace ShaderRecompiler; try { From 69c7efa9d34cd1a388cc16611a5c7738c016b1b9 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 18 Oct 2026 21:38:46 +1300 Subject: [PATCH 08/14] test(agc): do not expect the remap mode as a specialization constant in a direct build --- core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp index b3738e9067..0a5758f819 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp @@ -119,7 +119,7 @@ void pushedTransformTests() { const auto remapped = Recompile(baseRequest(true)); const auto plain = Recompile(baseRequest(false)); const auto facts = inspect(remapped); - require(facts.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase) && !facts.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 1u) && !facts.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 2u), "only the depth remap mode may be a specialization constant"); + require(!facts.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 1u) && !facts.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 2u), "the depth remap scale or offset is a specialization constant"); require(facts.pushConstantVariable && facts.pushDwordLoads.contains(DepthRemapPushOffsetBytes / 4u) && facts.pushDwordLoads.contains(DepthRemapPushOffsetBytes / 4u + 1u), "the remapped vertex shader does not read the scale and offset from the push block"); require(!inspect(plain).pushConstantVariable, "a vertex shader without a depth remap gained a push constant block"); auto crowded = baseRequest(true); From bfe5a3fee33b8bc496230b3b9e87ffc4191fb2dc Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 18 Oct 2026 21:40:24 +1300 Subject: [PATCH 09/14] test(agc): check the prepared module carries only the depth remap mode as a specialization constant --- .../libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp index 0a5758f819..bb417dfdbf 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -23,9 +24,8 @@ struct ModuleFacts { std::set pushDwordLoads; }; -ModuleFacts inspect(const ShaderRecompiler::RecompileResult& result) { +ModuleFacts inspect(std::span words) { ModuleFacts facts; - const auto& words = result.spirv.Words(); std::map specIds; std::map specValues; std::map constants; @@ -70,6 +70,10 @@ ModuleFacts inspect(const ShaderRecompiler::RecompileResult& result) { return facts; } +ModuleFacts inspect(const ShaderRecompiler::RecompileResult& result) { + return inspect(result.spirv.Words()); +} + constexpr std::array PositionExport{0xf80008cfu, 0u, 0xbf810000u}; ShaderRecompiler::RecompileRequest baseRequest(bool remap) { @@ -107,6 +111,8 @@ void preparedTransformTests() { require(invocation.has_value(), "the prepared vertex shader did not match a request of its own mode"); return invocation->Materialize(*invocation->Capture(SrtRuntime{})); }; + const auto prepared = inspect(GetPreparedCode(*handle)); + require(prepared.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase) && !prepared.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 1u) && !prepared.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 2u), "only the depth remap mode may be a specialization constant of the prepared module"); const auto first = materialize(2u); require(materialize(2u)->PipelineVariantId() == first->PipelineVariantId(), "the same depth remap mode did not reuse its materialized vertex shader"); auto other = request; From 5a5f2b42fded2fcd9cc0f5083f626811e345c819 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 18 Oct 2026 22:14:53 +1300 Subject: [PATCH 10/14] test(agc): read the prepared module's specialization constants from the artifact's SPIR-V --- core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp index bb417dfdbf..8452a2ae1a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp @@ -111,7 +111,7 @@ void preparedTransformTests() { require(invocation.has_value(), "the prepared vertex shader did not match a request of its own mode"); return invocation->Materialize(*invocation->Capture(SrtRuntime{})); }; - const auto prepared = inspect(GetPreparedCode(*handle)); + const auto prepared = inspect(GetPreparedArtifact(*handle).spirv.Words()); require(prepared.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase) && !prepared.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 1u) && !prepared.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 2u), "only the depth remap mode may be a specialization constant of the prepared module"); const auto first = materialize(2u); require(materialize(2u)->PipelineVariantId() == first->PipelineVariantId(), "the same depth remap mode did not reuse its materialized vertex shader"); From 441ff8718858507a8b8df6e6ccd4a0951c110655 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 18 Oct 2026 22:51:26 +1300 Subject: [PATCH 11/14] fix(agc): give a depth-remapped vertex shader its push constant stage A remapped vertex shader reads the scale and offset from the push block, but the pipeline's push constant stages only counted programs with push data of their own, so a shader with none got no stage in a non-GPL layout and its scale and offset never arrived. The result now says it reads them, and the push stages include the vertex stage for it. The draw test gains a fetchless remapped vertex shader with no push data. --- .../Graphics/include/Shaders.hpp | 2 +- .../tests/execution/DepthRangeRemapDraw.cpp | 35 ++++++++++++++++--- core/shader/recompiler/Recompiler.cpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Shaders.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Shaders.hpp index 76e24b0e23..2ddac45e80 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Shaders.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Shaders.hpp @@ -73,7 +73,7 @@ inline VkShaderStageFlags PushConstantStages(std::span sha VkShaderStageFlags result = 0; for (const auto& shader : shaders) { Require(shader.program != nullptr, "missing compiled shader"); - if (!shader.program->pushConstants.empty() || shader.stage == ShaderRecompiler::ShaderStage::Mesh || shader.stage == ShaderRecompiler::ShaderStage::TessellationControl) result |= VulkanStage(shader.stage); + if (!shader.program->pushConstants.empty() || shader.program->readsDepthRemapPush || shader.stage == ShaderRecompiler::ShaderStage::Mesh || shader.stage == ShaderRecompiler::ShaderStage::TessellationControl) result |= VulkanStage(shader.stage); } return result; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthRangeRemapDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthRangeRemapDraw.cpp index b0d533d202..c6b18d12c8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthRangeRemapDraw.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthRangeRemapDraw.cpp @@ -48,7 +48,14 @@ alignas(4096) std::array, 32 alignas(256) std::array, 3>, 128> VertexSets{}; std::size_t vertexSets = 0; -std::uint32_t Draw(AgcDriver::VulkanDevice& device, std::uint64_t depth, const Transform& transform, bool remap, float z, bool write) { +// A vertex shader that reads no user data: the fullscreen triangle comes from the vertex index (v5), and the +// clip z is the literal at index 9. Its program carries no push data of its own. +alignas(256) std::array FetchlessCode{ + 0x36020a81u, 0x34020282u, 0x4a0202c1u, 0x7e020b01u, 0x2c040a81u, 0x34040482u, 0x4a0404c1u, 0x7e040b02u, + 0x7e0602ffu, 0u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, +}; + +std::uint32_t Draw(AgcDriver::VulkanDevice& device, std::uint64_t depth, const Transform& transform, bool remap, float z, bool write, bool fetchless = false) { Require(vertexSets < VertexSets.size(), "the test ran out of vertex sets"); auto& vertices = VertexSets[vertexSets++]; vertices = {{{-1.0f, -1.0f, z, 1.0f}, {3.0f, -1.0f, z, 1.0f}, {-1.0f, 3.0f, z, 1.0f}}}; @@ -89,11 +96,16 @@ std::uint32_t Draw(AgcDriver::VulkanDevice& device, std::uint64_t depth, const T ShaderRecompiler::ShaderVertexStageInfo vertexInfo{}; vertexInfo.depthRemapMode = state.depthRemap.mode; std::vector vertexUserData(4, 0u); - const auto vertexBuffer = Harness::BufferDescriptor(vertices.data(), 16u, static_cast(vertices.size())); - std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); - const std::array vertexMemory{{{reinterpret_cast(Harness::VertexCode.data()), std::as_bytes(std::span(Harness::VertexCode))}}}; + if (fetchless) { + FetchlessCode[9] = std::bit_cast(z); + } else { + const auto vertexBuffer = Harness::BufferDescriptor(vertices.data(), 16u, static_cast(vertices.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + } + const std::span vertexCode = fetchless ? std::span(FetchlessCode) : std::span(Harness::VertexCode); + const std::array vertexMemory{{{reinterpret_cast(vertexCode.data()), std::as_bytes(vertexCode)}}}; ShaderRecompiler::RecompileRequest vertex{ - {ShaderStage::Vertex, reinterpret_cast(Harness::VertexCode.data()), Harness::VertexCode, 0, {}}, + {ShaderStage::Vertex, reinterpret_cast(vertexCode.data()), vertexCode, 0, {}}, {32, 0, vertexUserData, std::nullopt, std::nullopt, vertexInfo, vertexMemory}, target, {0, 0, 0, 64} @@ -101,6 +113,7 @@ std::uint32_t Draw(AgcDriver::VulkanDevice& device, std::uint64_t depth, const T vertex.useCache = false; const auto vertexResult = ShaderRecompiler::Recompile(vertex); const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + if (fetchless) Require(vertexPush == 0u, "the fetchless vertex shader carries push data of its own"); ShaderRecompiler::ShaderPixelStageInfo pixel{}; pixel.wave32 = true; @@ -202,6 +215,18 @@ int main() { } ++ran; } + // A remapped vertex shader with no push data of its own, and a fragment shader with none: the pipeline's + // push stages must still include the vertex stage (a non-GPL layout names them), or the scale and offset + // reach no one. + { + Require(surfaces < Depths.size(), "the test ran out of depth surfaces"); + const auto depth = static_cast(reinterpret_cast(Depths[surfaces++].data())); + const auto drawn = Draw(*device, depth, widened, true, 0.5f + Step / 2.0f, true, true); + Require(drawn == Harness::Covered, "a fetchless remapped vertex shader did not cover the target"); + Require(Draw(*device, depth, Plain, false, 0.5f, false) == Harness::Covered, "a fetchless remapped vertex shader stored a depth below 0.5 + 1/64, so its scale and offset did not arrive"); + Require(Draw(*device, depth, Plain, false, 0.5f + 2.0f * Step, false) == 0u, "a fetchless remapped vertex shader stored a depth above 0.5 + 1/64"); + ++ran; + } std::printf("depth range remap draw tests passed (%zu cases, unrestricted path %s)\n", ran, context.depthRangeUnrestricted ? "compared" : "absent"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index ed4cc2b844..b0c232f4d6 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -1003,6 +1003,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil result.workgroupMemoryDwords = WorkgroupMemoryStrideDwords(variant.info.info); result.bindings = std::move(bindings.bindings); result.pushConstants = std::move(bindings.pushConstants); + result.readsDepthRemapPush = depthRemap; if (!artifact.stencilReferences.empty()) { if (artifact.stencilReferenceDword * sizeof(std::uint32_t) != result.pushConstants.size() || result.pushConstants.size() + sizeof(std::uint32_t) > request.layout.pushConstantSizeBytes) throw std::runtime_error("stencil replay push constant lies outside the stage range"); result.pushConstants.resize(result.pushConstants.size() + sizeof(std::uint32_t)); diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 2b3761cf75..ee164e38e5 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -626,6 +626,7 @@ struct ShaderInvocation { std::uint64_t specializationId = 0; std::vector bindings; std::vector pushConstants; + bool readsDepthRemapPush = false; std::vector vertexAttributes; std::uint32_t poisonedSrtReads = 0; std::vector imageTablePoison; From b876d43fb115e8bbfb659bcba50070255db425a8 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 18 Oct 2026 23:28:51 +1300 Subject: [PATCH 12/14] fix(agc): journal whether a vertex shader reads the depth remap push words The flag that adds the vertex stage to the push constant stages was not part of the encoded result, so a journalled remapped vertex shader replayed without it. Encode and decode it with the invocation and cover it in the result round trip and the push stage test. --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 11 +++++++++++ .../prx/libSceAgcDriver/tests/ShaderDiskCache.cpp | 2 ++ core/shader/recompiler/ShaderDiskCache.cpp | 6 ++++-- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index f112b15f0b..8a1c5c7dbe 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -10,6 +10,7 @@ #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "ShaderDiskCache.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" @@ -4551,6 +4552,16 @@ void pushConstantTests() { fragment.pushConstants.clear(); shaders[1].pushConstantOffset = 999; Require(AgcDriver::Graphics::PushConstantStages(shaders) == VK_SHADER_STAGE_VERTEX_BIT, "empty push constants contributed a stage"); + { + ShaderRecompiler::RecompileResult reading; + reading.readsDepthRemapPush = true; + std::vector encoded; + ShaderRecompiler::ShaderDiskCache::EncodeResult(reading, encoded); + ShaderRecompiler::RecompileResult replayed; + Require(ShaderRecompiler::ShaderDiskCache::DecodeResult(encoded, replayed), "a result that reads the depth remap push words does not decode"); + const std::array remapped{{{ShaderRecompiler::ShaderStage::Vertex, &replayed, 0}}}; + Require(replayed.pushConstants.empty() && AgcDriver::Graphics::PushConstantStages(remapped) == VK_SHADER_STAGE_VERTEX_BIT, "a journalled vertex shader that reads the depth remap push words lost its push stage"); + } Require(AgcDriver::Graphics::AssemblePushConstants(shaders)[8] == std::byte{0}, "empty push constants were copied"); shaders[1].program = nullptr; expectFailure([&] { AgcDriver::Graphics::AssemblePushConstants(shaders); }, "missing compiled shader"); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index ccf689340d..b8545f9b7b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -125,6 +125,7 @@ void requireSameResult(const RecompileResult& left, const RecompileResult& right const std::string prefix = std::string(what) + ": "; require(sameBindings(left.bindings, right.bindings), prefix + "bindings differ"); require(left.pushConstants == right.pushConstants, prefix + "push constants differ"); + require(left.readsDepthRemapPush == right.readsDepthRemapPush, prefix + "depth remap push reads differ"); require(left.specialization == right.specialization, prefix + "specialization constants differ"); require(left.workgroupMemoryDwords == right.workgroupMemoryDwords, prefix + "workgroup memory stride differs"); require(left.poisonedSrtReads == right.poisonedSrtReads, prefix + "poisoned SRT read counts differ"); @@ -194,6 +195,7 @@ RecompileResult sampleResult() { result.bindings[1].role = DescriptorRole::GuestBuffers; result.bindings[1].imageShape.reset(); result.pushConstants = {std::byte{1}, std::byte{0xff}, std::byte{0}, std::byte{0x80}, std::byte{7}}; + result.readsDepthRemapPush = true; result.bdaAbiVersion = 3; result.memoryOffsetDword = 7; result.hostSubgroupSize = 32; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index e61aed2b6e..738e2f71db 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -85,8 +85,8 @@ namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) static_assert(sizeof(CompiledShaderArtifact) == 240, "CompiledShaderArtifact changed: update the artifact encoder"); -static_assert(sizeof(ShaderInvocation) == 200, "ShaderInvocation changed: update the invocation encoder"); -static_assert(sizeof(RecompileResult) == 472, "RecompileResult changed: update EncodeResult and DecodeResult"); +static_assert(sizeof(ShaderInvocation) == 208, "ShaderInvocation changed: update the invocation encoder"); +static_assert(sizeof(RecompileResult) == 480, "RecompileResult changed: update EncodeResult and DecodeResult"); static_assert(sizeof(DescriptorBinding) == 592, "DescriptorBinding changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 32, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(VertexInput) == 16, "VertexInput changed: update the vertex input encoder"); @@ -561,6 +561,7 @@ void encodeInvocation(Writer& writer, const ShaderInvocation& invocation) { writer.List(invocation.bindings, encodeBinding); writer.Value(invocation.pushConstants.size()); for (const auto byte : invocation.pushConstants) writer.Value(static_cast(byte)); + writer.Value(invocation.readsDepthRemapPush); writer.List(invocation.vertexAttributes, [](Writer& out, const VertexAttribute& attribute) { out.Value(attribute.location); out.Value(attribute.components); @@ -600,6 +601,7 @@ void decodeInvocation(Reader& reader, ShaderInvocation& invocation) { reader.Values(pushConstants); invocation.pushConstants.resize(pushConstants.size()); if (!pushConstants.empty()) std::memcpy(invocation.pushConstants.data(), pushConstants.data(), pushConstants.size()); + reader.Value(invocation.readsDepthRemapPush); reader.List(invocation.vertexAttributes, 32, [](Reader& in, VertexAttribute& attribute) { in.Value(attribute.location); in.Value(attribute.components); From 2ca390366bf8d08f2483a538ac8959bae16ab34b Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 18 Oct 2026 23:53:58 +1300 Subject: [PATCH 13/14] fix(agc): accept the depth remap push block in the shader interface check The SPIR-V interface check only allowed a push constant block for a program with push data of its own, so a remapped vertex shader without any was rejected as having an invalid push constant interface. --- .../prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 39ddfd7f61..f3b5ade950 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -540,7 +540,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } } } else if (variable.storage == spv::StorageClassPushConstant) { - Require(!push && (!shader.pushConstants.empty() || mesh || (control && !rectListControl)) && (type[0] & 0xffffu) == spv::OpTypeStruct, "invalid push constant interface"); + Require(!push && (!shader.pushConstants.empty() || shader.readsDepthRemapPush || mesh || (control && !rectListControl)) && (type[0] & 0xffffu) == spv::OpTypeStruct, "invalid push constant interface"); push = true; Require(type.size() == 3 && module.decorations[typeId].block, "push constant variable must be a Block struct with exactly one member"); const auto offset = module.offsets.find({typeId, 0}); @@ -612,7 +612,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(shader.vertexAttributes.empty(), "vertex attribute metadata is invalid for this stage"); } Require(descriptors.size() == shader.bindings.size(), "recompiler binding metadata contains undeclared resources"); - Require(push == (!shader.pushConstants.empty() || mesh || (control && !rectListControl && !barycentricPatches)), "recompiler push constant metadata disagrees with SPIR-V"); + Require(push == (!shader.pushConstants.empty() || shader.readsDepthRemapPush || mesh || (control && !rectListControl && !barycentricPatches)), "recompiler push constant metadata disagrees with SPIR-V"); for (const auto id : module.interface) Require(module.variables.contains(id), "entry point interface contains an unknown variable"); if (mesh) Require(module.primitiveIndices == state.stages.mesh->outputPrimitiveSize, "mesh shader does not export primitive indices"); if (stage == Stage::Vertex || mesh || evaluation || geometry) Require(module.position, "vertex shader does not export position"); From 5392c27efb1c58d69663e261b6bc79be8ee53bb8 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 19 Oct 2026 00:59:33 +1300 Subject: [PATCH 14/14] test(agc): draw a fetchless mode 0 vertex shader on a remapping target On a device without VK_EXT_depth_range_unrestricted the target remaps every vertex shader, so an in-range draw carries the remap's push block too. Draw it from a direct and from a prepared build; both must validate and store the right depth. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 ++ .../tests/execution/DepthRangeRemapDraw.cpp | 29 ++++++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 671b222736..fc526f1fe3 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1605,6 +1605,8 @@ target_include_directories(agc_driver_color1555_target_tests PRIVATE $) +target_include_directories(agc_driver_depth_range_remap_draw_tests PRIVATE $) + foreach(stage IN ITEMS vert frag) set(agcProvokingShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/ProvokingVertex.${stage}) set(agcProvokingShaderSpv ${agcTestShaderGeneratedRoot}/ProvokingVertex_${stage}.spv) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthRangeRemapDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthRangeRemapDraw.cpp index c6b18d12c8..831ec0b751 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthRangeRemapDraw.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthRangeRemapDraw.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" #include "DepthFastClearHarness.hpp" #include "VulkanTestDevice.hpp" #include @@ -55,7 +56,7 @@ alignas(256) std::array FetchlessCode{ 0x7e0602ffu, 0u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, }; -std::uint32_t Draw(AgcDriver::VulkanDevice& device, std::uint64_t depth, const Transform& transform, bool remap, float z, bool write, bool fetchless = false) { +std::uint32_t Draw(AgcDriver::VulkanDevice& device, std::uint64_t depth, const Transform& transform, bool remap, float z, bool write, bool fetchless = false, bool targetRemap = false, bool prepared = false) { Require(vertexSets < VertexSets.size(), "the test ran out of vertex sets"); auto& vertices = VertexSets[vertexSets++]; vertices = {{{-1.0f, -1.0f, z, 1.0f}, {3.0f, -1.0f, z, 1.0f}, {-1.0f, 3.0f, z, 1.0f}}}; @@ -92,7 +93,7 @@ std::uint32_t Draw(AgcDriver::VulkanDevice& device, std::uint64_t depth, const T Require((state.depthRemap.mode != 0u) == (remap && !inRange), "the draw did not take the depth path the test asked for"); auto target = device.Target(); - target.depthRangeRemap = state.depthRemap.mode != 0u; + target.depthRangeRemap = state.depthRemap.mode != 0u || targetRemap; ShaderRecompiler::ShaderVertexStageInfo vertexInfo{}; vertexInfo.depthRemapMode = state.depthRemap.mode; std::vector vertexUserData(4, 0u); @@ -110,8 +111,17 @@ std::uint32_t Draw(AgcDriver::VulkanDevice& device, std::uint64_t depth, const T target, {0, 0, 0, 64} }; - vertex.useCache = false; - const auto vertexResult = ShaderRecompiler::Recompile(vertex); + vertex.useCache = prepared; + std::shared_ptr vertexHolder; + if (prepared) { + const auto handle = ShaderRecompiler::PrepareShader(vertex); + const auto invocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(vertex, handle); + Require(invocation.has_value(), "the prepared vertex shader did not match its own request"); + vertexHolder = invocation->Materialize(*invocation->Capture(ShaderRecompiler::SrtRuntime{std::span(vertexUserData)})); + } else { + vertexHolder = std::make_shared(ShaderRecompiler::Recompile(vertex)); + } + const auto& vertexResult = *vertexHolder; const auto vertexPush = static_cast(vertexResult.pushConstants.size()); if (fetchless) Require(vertexPush == 0u, "the fetchless vertex shader carries push data of its own"); @@ -227,6 +237,17 @@ int main() { Require(Draw(*device, depth, Plain, false, 0.5f + 2.0f * Step, false) == 0u, "a fetchless remapped vertex shader stored a depth above 0.5 + 1/64"); ++ran; } + // On a device without VK_EXT_depth_range_unrestricted the target remaps every vertex shader, so an ordinary + // in-range draw (mode 0) with no push data of its own still carries the remap's push block: it must validate + // and draw, from a direct build and from a prepared one. + for (const bool prepared : {false, true}) { + Require(surfaces < Depths.size(), "the test ran out of depth surfaces"); + const auto depth = static_cast(reinterpret_cast(Depths[surfaces++].data())); + const auto drawn = Draw(*device, depth, Plain, false, 0.5f + Step / 2.0f, true, true, true, prepared); + Require(drawn == Harness::Covered, prepared ? "a fetchless mode 0 vertex shader on a remapping target, prepared, did not cover the target" : "a fetchless mode 0 vertex shader on a remapping target did not cover the target"); + Require(Draw(*device, depth, Plain, false, 0.5f, false) == Harness::Covered && Draw(*device, depth, Plain, false, 0.5f + 2.0f * Step, false) == 0u, "a fetchless mode 0 vertex shader on a remapping target stored the wrong depth"); + ++ran; + } std::printf("depth range remap draw tests passed (%zu cases, unrestricted path %s)\n", ran, context.depthRangeUnrestricted ? "compared" : "absent"); return 0; } catch (const std::exception& error) {