From 53212b18ac1c7562bddb73c352f1663fc348a5c0 Mon Sep 17 00:00:00 2001 From: Mouad-Scriptz-0 Date: Mon, 12 Oct 2026 12:28:08 +0000 Subject: [PATCH] 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.