diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 26e7a8a267..49a6952558 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2503,6 +2503,16 @@ target_include_directories(agc_driver_color565_target_tests PRIVATE $) +if(BUILD_TESTING) + add_test(NAME agc_driver_tessellation_depth_range_remap COMMAND agc_driver_tessellation_tests --depth-remap) + set_tests_properties(agc_driver_tessellation_depth_range_remap PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_tessellation_depth_range_remap PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + target_include_directories(agc_driver_depth_bounds_emulation_draw_tests PRIVATE $) foreach(stage IN ITEMS vert frag) 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 30b3692d90..6697f87de7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -73,6 +73,9 @@ std::shared_ptr Driver::materializeDraw {0, 0, pushOffset, Graphics::StagePushLimit(graphics.stages, pushOffset, graphics.depthRemap.mode != 0u) - pushOffset % Graphics::PipelinePushSlotBytes}, ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, Graphics::StageMeshConfiguration(graphics.stages, program.binary.stage), graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}, program.binary.stage == Stage::Mesh ? meshBarycentrics : std::nullopt} }; + if (graphics.depthRemap.mode != 0u && pushOffset < Graphics::PipelinePushSlotBytes) { + request.layout.pushConstantSizeBytes = std::min(request.layout.pushConstantSizeBytes, ShaderRecompiler::DepthRemapPushOffsetBytes - pushOffset); + } const auto waitedBefore = traceCapSync() || profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0; auto invocation = [&] { try { 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 3a69eaa097..069a5e418f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1802,7 +1802,10 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod const auto wave = Graphics::StageWaveSize(decoded.state.stages, program.binary.stage); pushOffset = Graphics::StagePushOffset(pushOffset, program.binary.stage, fixedSlots); const auto offset = pushOffset; - const auto room = (fixedSlots && fragment ? Graphics::PipelinePushSlotBytes : capacity) - pushOffset % Graphics::PipelinePushSlotBytes; + auto room = (fixedSlots && fragment ? Graphics::PipelinePushSlotBytes : capacity) - pushOffset % Graphics::PipelinePushSlotBytes; + if (decoded.state.depthRemap.mode != 0u && offset < Graphics::PipelinePushSlotBytes) { + room = std::min(room, ShaderRecompiler::DepthRemapPushOffsetBytes - offset); + } if (deferUnprepared && DeferFailedRegistration(*program.snapshot)) { prepared.push_back({program.snapshot, {program.codeOffset, nullptr}, program.binary.stage, offset, nullptr}); continue; @@ -1818,6 +1821,7 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod } if (vertex) { vertex->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, decoded.state.paClVsOutCntl, program.tailContinuation.get()) | decoded.state.layerExports; + vertex->depthRemapMode = decoded.state.depthRemap.mode; Graphics::ApplyPointSize(decoded.state, *vertex); } ShaderRecompiler::RecompileRequest request{program.binary, {wave, program.firstUserSgpr, program.userData, {}, fragment ? std::optional(decoded.pixel) : std::nullopt, vertex, memory, RegisteredFloatMode(*program.snapshot)}, target, {0, 0, offset, room}, ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, Graphics::StageMeshConfiguration(decoded.state.stages, program.binary.stage), decoded.state.stages.tessellation, {}}}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 5aee142f85..c89611918b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1431,7 +1431,9 @@ 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"); + const bool vertex = state.stages.path == ShaderPath::Vertex && !state.stages.tessellation; + const bool tessellation = state.stages.path == ShaderPath::Tessellation && state.stages.tessellation.has_value(); + Require((vertex || tessellation) && !state.stages.mesh, "viewport depth outside [0, 1] is remapped on the vertex and tessellation paths 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); diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp index 8452a2ae1a..257d5b4987 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp @@ -75,11 +75,13 @@ ModuleFacts inspect(const ShaderRecompiler::RecompileResult& result) { } constexpr std::array PositionExport{0xf80008cfu, 0u, 0xbf810000u}; +constexpr std::array DomainPositionExport{0xf8000941u, 0u, 0xf80008cfu, 0x06050605u, 0xbf810000u}; -ShaderRecompiler::RecompileRequest baseRequest(bool remap) { +ShaderRecompiler::RecompileRequest baseRequest(bool remap, ShaderRecompiler::ShaderStage stage = ShaderRecompiler::ShaderStage::Vertex) { using namespace ShaderRecompiler; RecompileRequest request{}; - request.shader = {ShaderStage::Vertex, 0x10000u, PositionExport, 0, {}}; + const auto code = stage == ShaderStage::TessellationEvaluation ? std::span(DomainPositionExport) : std::span(PositionExport); + request.shader = {stage, 0x10000u, code, 0, {}}; request.context.waveSize = 64; ShaderVertexStageInfo vertex{}; vertex.depthRemapMode = 2u; @@ -87,9 +89,13 @@ ShaderRecompiler::RecompileRequest baseRequest(bool remap) { request.target.vulkanVersion = 0x00401000u; request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 64; - static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityClipDistance}; + static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityClipDistance, spv::CapabilityTessellation}; request.target.supportedCapabilities = capabilities; request.target.depthRangeRemap = remap; + if (stage == ShaderStage::TessellationEvaluation) { + request.target.tessellation = TessellationTargetLimits{32u, 128u, 128u, 128u, 4096u, 128u, 128u}; + request.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{3u, 3u, 1u, 2u, 2u}, {0u, 3u, 0u, 1u}}; + } request.layout.pushConstantSizeBytes = ShaderRecompiler::DepthRemapPushOffsetBytes; request.useCache = false; return request; @@ -99,9 +105,9 @@ void require(bool condition, const char* what) { if (!condition) throw std::runtime_error(what); } -void preparedTransformTests() { +void preparedTransformTests(ShaderRecompiler::ShaderStage stage) { using namespace ShaderRecompiler; - auto request = baseRequest(true); + auto request = baseRequest(true, stage); request.useCache = true; const auto handle = PrepareShader(request); const auto materialize = [&](std::uint32_t mode) { @@ -120,15 +126,15 @@ void preparedTransformTests() { require(!PreparedShaderInvocation::TryCreate(other, handle).has_value(), "a prepared vertex shader matched a request of another depth remap mode"); } -void pushedTransformTests() { +void pushedTransformTests(ShaderRecompiler::ShaderStage stage) { using namespace ShaderRecompiler; - const auto remapped = Recompile(baseRequest(true)); - const auto plain = Recompile(baseRequest(false)); + const auto remapped = Recompile(baseRequest(true, stage)); + const auto plain = Recompile(baseRequest(false, stage)); const auto facts = inspect(remapped); 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); + auto crowded = baseRequest(true, stage); crowded.layout.pushConstantSizeBytes = DepthRemapPushOffsetBytes + 4u; bool refused = false; try { @@ -139,18 +145,41 @@ void pushedTransformTests() { require(refused, "a remapped vertex shader with push data over the depth remap words was accepted"); } +void clipDistanceCapacityTests(ShaderRecompiler::ShaderStage stage) { + using namespace ShaderRecompiler; + static constexpr std::array vertex{0xf80008cfu, 0u, 0xf80008dfu, 0u, 0xf80008efu, 0u, 0xbf810000u}; + static constexpr std::array domain{0xf8000941u, 0u, 0xf80008cfu, 0x05050505u, 0xf80008dfu, 0x05050505u, 0xf80008efu, 0x05050505u, 0xbf810000u}; + auto request = baseRequest(true, stage); + request.shader.code = stage == ShaderStage::TessellationEvaluation ? std::span(domain) : std::span(vertex); + request.context.vertex->paClVsOutCntl = 0xc0003fu; + require(inspect(Recompile(request)).clipDistanceLength == 8u, "the remap did not preserve six guest clip distances beside its two planes"); + for (const auto control : {0xc0007fu, 0xc0403fu, 0xc000ffu}) { + request.context.vertex->paClVsOutCntl = control; + bool refused = false; + try { + static_cast(Recompile(request)); + } catch (const std::runtime_error& error) { + refused = std::string(error.what()).find("two free clip-distance components") != std::string::npos; + } + require(refused, "a remap that exceeds the combined clip and cull distance capacity was accepted"); + } +} + } 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"); - preparedTransformTests(); - pushedTransformTests(); + for (const auto stage : {ShaderStage::Vertex, ShaderStage::TessellationEvaluation}) { + const auto remapped = inspect(Recompile(baseRequest(true, stage))); + const auto plain = inspect(Recompile(baseRequest(false, stage))); + 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(stage); + pushedTransformTests(stage); + clipDistanceCapacityTests(stage); + } std::cout << "Depth range remap tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index d9bb76882b..06bd5eeb12 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -4727,6 +4727,17 @@ void DepthRemapTests() { 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"); + auto tessellated = AgcDriver::Graphics::DecodeState(queue); + tessellated.stages.path = AgcDriver::Graphics::ShaderPath::Tessellation; + tessellated.stages.tessellation = ShaderRecompiler::TessellationConfiguration{3u, 3u, 1u, 2u, 2u}; + auto nativeTessellation = tessellated; + AgcDriver::Graphics::ApplyDepthRemap(unrestricted, nativeTessellation); + Require(nativeTessellation.depthRemap.mode == 0 && nativeTessellation.viewport.minDepth == -0.5f && nativeTessellation.viewport.maxDepth == 1.5f, "an unrestricted tessellation draw was remapped"); + AgcDriver::Graphics::ApplyDepthRemap(remapping, tessellated); + Require(tessellated.depthRemap.mode == 1 && tessellated.depthRemap.scale == 2.0f && tessellated.depthRemap.offset == -0.5f && tessellated.depthClamp, "a tessellation draw did not remap its depth range"); + auto combined = nativeTessellation; + combined.stages.mesh = ShaderRecompiler::MeshConfiguration{}; + expectFailure([&] { AgcDriver::Graphics::ApplyDepthRemap(remapping, combined); }, "vertex and tessellation paths only"); } void DepthBoundsEmulationTests() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index a861172480..ac929f9daf 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -565,7 +566,7 @@ struct Stages { ShaderRecompiler::RecompileResult fragment; }; -ShaderRecompiler::RecompileResult Compile(const ShaderRecompiler::SpirvTarget& target, ShaderStage stage, std::span code, std::uint32_t& pushCursor, const PatchDraw& patches = Triangles) { +ShaderRecompiler::RecompileResult Compile(const ShaderRecompiler::SpirvTarget& target, ShaderStage stage, std::span code, std::uint32_t& pushCursor, const PatchDraw& patches = Triangles, std::uint32_t depthRemapMode = 0u) { static const std::array domainUserData{}; const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; const bool hull = stage == ShaderStage::TessellationControl; @@ -582,10 +583,12 @@ ShaderRecompiler::RecompileResult Compile(const ShaderRecompiler::SpirvTarget& t pixel = info; } else { vertex = ShaderRecompiler::ShaderVertexStageInfo{}; + vertex->depthRemapMode = depthRemapMode; graphics = ShaderRecompiler::GraphicsCompileContext{hull ? 0u : 8u, {}, std::nullopt, patches.configuration, {reinterpret_cast(patches.indices.data()), static_cast(patches.indices.size()), 2, patches.instances}}; } auto layout = stage == ShaderStage::Fragment ? PixelPushLayout(pushCursor, target) : ShaderRecompiler::BindingLayout{0, 0, pushCursor, AgcDriver::Graphics::PipelinePushSlotBytes - pushCursor}; if (layout.pushConstantOffsetBytes < AgcDriver::Graphics::PipelinePushSlotBytes) layout.pushConstantSizeBytes = ShaderRecompiler::TessellationLevelPushOffsetBytes - layout.pushConstantOffsetBytes; + if (depthRemapMode != 0u && layout.pushConstantOffsetBytes < AgcDriver::Graphics::PipelinePushSlotBytes) layout.pushConstantSizeBytes = ShaderRecompiler::DepthRemapPushOffsetBytes - layout.pushConstantOffsetBytes; const ShaderRecompiler::RecompileRequest request{ {stage, reinterpret_cast(code.data()), code, 0, {}}, {64, hull ? 0u : 8u, userData, std::nullopt, pixel, vertex, memory}, @@ -634,7 +637,17 @@ std::uint32_t Row(float y) { return static_cast((1.0f - y) * 0.5f * Height); } -void Draw(AgcDriver::VulkanDevice& device, const Stages& stages, VkCullModeFlags cull, VkFrontFace front, const PatchDraw& patches = Triangles) { +struct DepthDraw { + std::uint64_t address; + bool write; + bool negativeOneToOne = false; + float scale = 1.0f; + float offset = 0.0f; + float clampMin = 0.0f; + float clampMax = 1.0f; +}; + +void Draw(AgcDriver::VulkanDevice& device, const Stages& stages, VkCullModeFlags cull, VkFrontFace front, const PatchDraw& patches = Triangles, const DepthDraw* depth = nullptr) { for (std::size_t i = 0; i < Pixels.size(); i += 4) { for (std::size_t c = 0; c < 4; ++c) Pixels[i + c] = std::byte{Background[c]}; } @@ -665,10 +678,95 @@ void Draw(AgcDriver::VulkanDevice& device, const Stages& stages, VkCullModeFlags state.blend.colorWriteMask = 15; state.blends = {state.blend}; state.blendConstants = {}; + if (depth != nullptr) { + state.depth = AgcDriver::Graphics::DepthTarget{depth->address, 0u, {Width, Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0u}; + state.depthTest = true; + state.depthWrite = depth->write; + state.depthCompare = depth->write ? VK_COMPARE_OP_ALWAYS : VK_COMPARE_OP_LESS; + state.depthScale = depth->scale; + state.depthOffset = depth->offset; + state.depthClampMin = depth->clampMin; + state.depthClampMax = depth->clampMax; + state.negativeOneToOne = depth->negativeOneToOne; + state.viewport.minDepth = depth->negativeOneToOne ? depth->offset - depth->scale : depth->offset; + state.viewport.maxDepth = depth->offset + depth->scale; + auto context = device.graphicsContext(); + context.depthRangeUnrestricted = false; + AgcDriver::Graphics::ApplyDepthRemap(context, state); + } device.Draw(state, {reinterpret_cast(patches.indices.data()), static_cast(patches.indices.size()), 2, patches.instances, 0, true, 0, patches.firstInstance}, shaders); device.WaitIdle(); } +void RequireDepthRangeRemap(AgcDriver::VulkanDevice& device) { + auto target = device.Target(); + Require(device.graphicsContext().depthClamp, "tessellation depth remap needs depth clamping"); + target.depthRangeRemap = true; + const auto compile = [&](float z, std::uint32_t mode) { + std::vector domain(DomainCode.begin(), DomainCode.end()); + const auto literal = std::ranges::find(domain, 0x7e260280u); + Require(literal != domain.end(), "the domain fixture has no constant depth"); + *literal = 0x7e2602ffu; + domain.insert(literal + 1, std::bit_cast(z)); + std::uint32_t cursor = 0u; + return Stages{ + Compile(target, ShaderStage::Local, LocalCode, cursor), + Compile(target, ShaderStage::TessellationControl, HullCode, cursor), + Compile(target, ShaderStage::TessellationEvaluation, domain, cursor, Triangles, mode), + Compile(target, ShaderStage::Fragment, PixelCode, cursor), + }; + }; + const auto covered = [] { + std::uint32_t count = 0u; + for (std::size_t offset = 0; offset < Pixels.size(); offset += 4u) { + if (Pixels[offset] == std::byte{255}) ++count; + } + return count; + }; + Draw(device, compile(0.5f, 0u), VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE); + const auto expected = covered(); + Require(expected != 0u, "the depth fixture drew no tessellated patches"); + struct Case { + bool symmetric; + float scale; + float offset; + float clampMin; + float clampMax; + float z; + bool drawn; + float probe; + bool passes; + }; + constexpr std::array cases{{ + {true, 1.0f, 0.0f, 0.0f, 1.0f, 0.75f, true, 0.5f, true}, + {true, 1.0f, 0.0f, 0.0f, 1.0f, -0.5f, true, 0.25f, false}, + {true, 1.0f, 0.0f, 0.0f, 1.0f, -1.25f, false, 0.5f, true}, + {true, 1.0f, 0.0f, 0.0f, 1.0f, 1.25f, false, 0.5f, true}, + {false, 2.0f, -0.5f, 0.0f, 1.0f, 0.25f, true, 0.125f, false}, + {false, 2.0f, -0.5f, 0.0f, 1.0f, 0.75f, true, 0.875f, true}, + {false, 2.0f, -0.5f, 0.0f, 1.0f, -0.125f, false, 0.5f, true}, + {false, 2.0f, -0.5f, 0.0f, 1.0f, 1.125f, false, 0.5f, true}, + {false, 2.0f, -0.5f, 0.25f, 0.75f, 0.4f, true, 0.35f, false}, + {false, 2.0f, -0.5f, 0.25f, 0.75f, 0.6f, true, 0.65f, true}, + {false, 2.0f, -0.5f, 0.25f, 0.75f, 0.2f, true, 0.2f, true}, + {false, 2.0f, -0.5f, 0.25f, 0.75f, 0.8f, true, 0.8f, false}, + {false, -2.0f, 1.5f, 0.0f, 1.0f, 0.4f, true, 0.65f, true}, + {false, -2.0f, 1.5f, 0.0f, 1.0f, 0.6f, true, 0.35f, false}, + }}; + alignas(4096) static std::array, cases.size()> depths{}; + for (std::size_t index = 0; index < cases.size(); ++index) { + const auto& test = cases[index]; + const auto address = static_cast(reinterpret_cast(depths[index].data())); + const DepthDraw write{address, true, test.symmetric, test.scale, test.offset, test.clampMin, test.clampMax}; + Draw(device, compile(test.z, test.symmetric ? 2u : 1u), VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, Triangles, &write); + Require(covered() == (test.drawn ? expected : 0u), "tessellation remap clipping differs in case " + std::to_string(index)); + const DepthDraw probe{address, false}; + Draw(device, compile(test.probe, 0u), VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, Triangles, &probe); + Require(covered() == (test.passes ? expected : 0u), "tessellation remap stored depth differs in case " + std::to_string(index)); + } + std::puts("Tessellation depth remap: 14 clipping and stored-depth cases passed"); +} + std::uint32_t CoveredPixels(std::uint32_t patch, bool interior) { std::uint32_t count = 0; for (std::uint32_t y = Row(0.5f); y <= Row(-0.5f) && y < Height; ++y) { @@ -1095,7 +1193,7 @@ void RequireRegisteredMergedStages() { } -int main() { +int main(int argc, char** argv) { try { auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; @@ -1104,6 +1202,10 @@ int main() { std::puts("tessellation: skipped, the device has no tessellation shaders"); return VulkanTestSkipped; } + if (argc == 2 && std::string_view(argv[1]) == "--depth-remap") { + RequireDepthRangeRemap(*device); + return 0; + } RequireSingleControlPointHulls(target); ExpectRefusal(target, ShaderStage::TessellationControl, HullUnguardedFactors, "not written by control point 0 alone"); ExpectRefusal(target, ShaderStage::TessellationControl, HullSquaredAddress, "not a whole number of control points"); diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 091ebecbd2..abc5ff780c 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -1152,10 +1152,10 @@ 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(); + const bool depthRemap = request.target.depthRangeRemap && (request.shader.stage == ShaderStage::Vertex || request.shader.stage == ShaderStage::TessellationEvaluation) && 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"); + throw std::runtime_error("a remapped raster shader's push data must end before the depth remap words"); } moduleKey.push_back(request.context.vertex->depthRemapMode); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index f71d15cbca..0fbd448816 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -368,8 +368,11 @@ void DefineOutputs(SpirvEmitterState& state) { DefineMeshOutputs(state); return; } - const bool depthRemap = state.depthRangeRemap && state.program.Resources().stage == IrShaderStage::Vertex; + const bool depthRemap = state.depthRangeRemap && (state.program.Resources().stage == IrShaderStage::Vertex || state.program.Resources().stage == IrShaderStage::TessellationEvaluation); if (depthRemap) { + if (clipDistanceCount + cullDistanceCount > MaxClipDistances - 2u) { + FailEmit("depth remap needs two free clip-distance components"); + } state.depthRemapClipIndex = clipDistanceCount; clipDistanceCount += 2u; } @@ -460,7 +463,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 || (state.depthRangeRemap && stage == IrShaderStage::Vertex)) { + if (layout.UsesPushData() || state.stencilReplay || stage == IrShaderStage::Mesh || stage == IrShaderStage::TessellationControl || (state.depthRangeRemap && (stage == IrShaderStage::Vertex || stage == IrShaderStage::TessellationEvaluation))) { 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 fb3901c1a5..eb38969f9a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -2075,7 +2075,7 @@ 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) { + if (state.depthRangeRemap && (state.program.Resources().stage == IrShaderStage::Vertex || state.program.Resources().stage == IrShaderStage::TessellationEvaluation)) { value = RemapPositionDepth(state, value); } } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 2212f86f54..3e81e57073 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -273,7 +273,7 @@ - [Multisample image readback](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp) fetches unsigned R8/R16/R32/R32G32/R32G32B32A32 or D32 depth samples through a 2D array view in GENERAL, in layer/y/x/fragment order. The caller owns producer completion, image transitions and resource lifetimes. It implements Vulkan per-sample fetches, not guest-memory publication, metadata expansion or PS5 layout equivalence; D32 exceptional-value preservation has not been measured. Other formats, sample counts and invalid ranges throw. - [Multisample image upload](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleUpload.cpp) does not model guest layouts, native ownership or CMASK/FMASK/DCC publication; console equivalence and large-title initialization costs remain unmeasured. It stores 1/2/4/8/16-byte elements from a storage buffer into an unsigned 2D multisample array view in layer/y/x/fragment order. It requires enabled multisample storage images and formatless image writes, 2/4/8 fragments and valid aligned buffer and dispatch ranges. The caller supplies the matching storage view in GENERAL and owns barriers, completion and resource lifetimes. - [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. 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. +- [Viewport depth range remap](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - mesh draws with a viewport depth range outside [0, 1] still throw on devices without `VK_EXT_depth_range_unrestricted`, including combined tessellation and geometry draws. The vertex and tessellation fallback clips `PA_SC_VPORT_ZMIN/ZMAX` to [0, 1], so a negative depth on a float target is stored as 0: Minecraft PPSA17221 requests [-1, 1], which a later test or read at 0 can distinguish. The fallback needs depth clamping and two free components out of the eight combined clip/cull distances. Console clamp/bias ordering and clamp ranges narrower than the viewport transform remain unmeasured. - [Benchmark rewrite failure test off Windows](../../../core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp) (libSceAgcDriver) - `ReplaceGzipFile` checks that a write failing at `.tmp` keeps the previous `aps5-bench.json.gz` only on non-Windows builds: the `std::ios_base::failure` thrown inside the MinGW ofstream did not unwind to the test's catch, so Windows CI covers only the failed move. - [Sampled array windows](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) (libSceAgcDriver) - a sampled 2D array, 1D array or cube view past slice 0 is built from its viewed slices only (`ViewedArrayWindow`). The window needs a thin surface without DCC metadata, or with keys that resolve over the whole surface to Uncompressed (the window then drops the metadata; keys are rescanned at each lookup). For a 64 KiB XOR swizzle it starts at the last slice at or before the base whose swizzle slice bits are zero (R_X 1-byte and Z_X 4-byte: a multiple of 16); when that is slice 0 the view builds slices 0 to its last slice. A window and a whole-array view of the same surface are separate images over overlapping memory, kept coherent by the existing overlap rules, at the cost of both. PPSA26344 samples a cube array at slices 4299..7232 at 1467 MiB instead of about 3.8 GB; PPSA01341 samples slice 636 of a DCC 2D array at 52 MiB instead of 2.5 GiB.