From 2eda9f893c614bacd214de56f34cb3a45b73886b Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Mon, 12 Oct 2026 03:01:41 +0200 Subject: [PATCH] feat(agc): rebuild the hull's relative patch index when it is used as data Hull programs whose relative patch index (v1 bits 7:0) reached anything but a tessellation ring or LDS address threw, because the lowering runs every Vulkan patch as relative patch 0. The quad terrain hull 0x80002fc700 in Ghost of Yotei is one, and so is the domain 0x8068fbd900 (v7). Such hulls and domains now take (PrimitiveID mod GE_CNTL.PRIM_GRP_SIZE) mod VGT_LS_HS_CONFIG.NUM_PATCHES, as if hull threadgroups took NUM_PATCHES patches at a time out of each primitive group; both stages see the same PrimitiveID for a patch. This holds only when every instance starts a new threadgroup, so it needs GE_CNTL.BREAK_WAVE_AT_EOI (bit 18); with the bit clear the instances share threadgroups and such programs still throw. PRIM_GRP_SIZE and BREAK_WAVE_AT_EOI join the tessellation configuration (draw keys, hull and domain cache keys, request version 24). A missing GE_CNTL throws, and so do local programs, whose relative vertex index depends on vertex reuse that Vulkan does not expose. --- .../Execution/src/Driver/Draw/DrawLookup.cpp | 2 +- .../src/Driver/Draw/DrawRegisterKey.cpp | 2 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 2 + .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 2 + .../libSceAgcDriver/Graphics/src/State.cpp | 3 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 12 +++- .../libSceAgcDriver/tests/ShaderMemory.cpp | 28 ++++++++-- .../tests/execution/Tessellation.cpp | 56 +++++++++++++++++++ core/shader/recompiler/CacheKey.hpp | 2 + .../ControlFlow/src/RequestSerializer.cpp | 10 +++- .../Optimization/ShaderStageInputInfo.hpp | 2 + .../Optimization/src/TessellationLowering.cpp | 31 ++++++++-- core/shader/recompiler/Recompiler.hpp | 2 + .../src/ShaderInputInfoBuilder.cpp | 2 + docs/dev/debt/shader.md | 1 + 15 files changed, 142 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index 069e72aeb1..a456ce969f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -26,7 +26,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(tess.breakAtInstance)}) mix(value); } mix(graphics.rectList); mix(programs.size()); 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 8369224ac7..10297ffef8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -118,7 +118,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { const auto sameTess = [](const std::optional& x, const std::optional& y) { if (x.has_value() != y.has_value()) return false; if (!x) return true; - return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology && x->patchesPerGroup == y->patchesPerGroup; + return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology && x->patchesPerGroup == y->patchesPerGroup && x->primitiveGroupSize == y->primitiveGroupSize && x->breakAtInstance == y->breakAtInstance; }; if (s.stages.path != t.stages.path || s.stages.registerValue != t.stages.registerValue || s.stages.vertexWaveSize != t.stages.vertexWaveSize || s.stages.fragmentWaveSize != t.stages.fragmentWaveSize || !sameMesh(s.stages.mesh, t.stages.mesh) || !sameTess(s.stages.tessellation, t.stages.tessellation)) return false; if (!sameColor(s.color, t.color) || s.colors.size() != t.colors.size() || s.blends.size() != t.blends.size()) return false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index adc88aee57..6733d42519 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -666,6 +666,8 @@ bool ValidationKey(const Context& context, std::span shade add(tessellation.partitioning); add(tessellation.outputTopology); add(tessellation.patchesPerGroup); + add(tessellation.primitiveGroupSize); + add(tessellation.breakAtInstance ? 1u : 0u); } } return keyed; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 2a7abf526c..e010e98c22 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -833,6 +833,8 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, tessellation.partitioning); append(key, tessellation.outputTopology); append(key, tessellation.patchesPerGroup); + append(key, tessellation.primitiveGroupSize); + append(key, tessellation.breakAtInstance); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 2a7172224d..c7e4a0b17f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -646,7 +646,8 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { validate((value & ~0x0207ed0du) == 0 && (value & 0x02002000u) == 0x02002000u && (value & 3u) == 1u && ((value >> 3u) & 3u) == 1u, "unsupported tessellation routing"); const auto config = read(queue.context, 0x2d6); const auto parameters = read(queue.context, 0x2db); - ShaderRecompiler::TessellationConfiguration tess{(config >> 8u) & 0x3fu, (config >> 14u) & 0x3fu, parameters & 3u, (parameters >> 2u) & 7u, (parameters >> 5u) & 7u, config & 0xffu}; + const auto primitiveGroup = find(queue.userConfig, 0x25b, RegisterBank::UserConfig); + ShaderRecompiler::TessellationConfiguration tess{(config >> 8u) & 0x3fu, (config >> 14u) & 0x3fu, parameters & 3u, (parameters >> 2u) & 7u, (parameters >> 5u) & 7u, config & 0xffu, primitiveGroup == queue.userConfig.end() ? 0u : primitiveGroup->second & 0x1ffu, primitiveGroup != queue.userConfig.end() && (primitiveGroup->second & 0x40000u) != 0u}; validate(tess.inputControlPoints != 0 && tess.inputControlPoints <= 32 && tess.outputControlPoints != 0 && tess.outputControlPoints <= 32, "invalid tessellation control-point counts"); validate(tess.patchesPerGroup != 0, "VGT_LS_HS_CONFIG.NUM_PATCHES is zero"); if ((parameters & ~0x000600ffu) != 0) Require(false, zeroMessage(0x2db, parameters, "VGT_TF_PARAM donut, factor detection, memory type or reserved state")); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index c31f7f4000..7bd45dff22 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -748,6 +748,7 @@ void ShaderStageTests() { queue.context[0x2d5] = 0x200d; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "Patch topology and HS_EN disagree"); queue.userConfig[0x242] = 9; + queue.userConfig[0x25b] = 0x1fe55; queue.context[0x2d6] = 0xc355; queue.context[0x2db] = 0x40049; queue.context[0x286] = 0x42800000; @@ -765,7 +766,7 @@ void ShaderStageTests() { } Require(stages.path == AgcDriver::Graphics::ShaderPath::Tessellation && stages.tessellation, "tessellation routing changed"); const auto& tessellation = *stages.tessellation; - Require(tessellation.inputControlPoints == 3 && tessellation.outputControlPoints == 3 && tessellation.domain == 1 && tessellation.partitioning == 2 && tessellation.outputTopology == 2 && stages.minTessellationLevel == 1.0f && stages.maxTessellationLevel == 64.0f && tessellation.patchesPerGroup == 85, "the passthrough triangle-patch configuration decoded differently"); + Require(tessellation.inputControlPoints == 3 && tessellation.outputControlPoints == 3 && tessellation.domain == 1 && tessellation.partitioning == 2 && tessellation.outputTopology == 2 && stages.minTessellationLevel == 1.0f && stages.maxTessellationLevel == 64.0f && tessellation.patchesPerGroup == 85 && tessellation.primitiveGroupSize == 85 && !tessellation.breakAtInstance, "the passthrough triangle-patch configuration decoded differently"); for (const auto [parameters, reason] : {std::pair{0x44049u, "VGT_TF_PARAM"}, std::pair{0x140049u, "VGT_TF_PARAM"}, std::pair{0x2040049u, "VGT_TF_PARAM"}, std::pair{0x40059u, "fractional-odd partitioning"}, std::pair{0x4004du, "fractional-odd partitioning"}, std::pair{0x40045u, "fractional-odd partitioning"}, std::pair{0x40048u, "fractional-odd partitioning"}, std::pair{0x40029u, "fractional-odd partitioning"}, std::pair{0x4004bu, "fractional-odd partitioning"}}) { queue.context[0x2db] = parameters; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason); @@ -775,9 +776,16 @@ void ShaderStageTests() { queue.context[0x2d6] = 0x1043f; queue.context[0x2db] = 0x4006a; queue.context[0x286] = 0x41100000; + queue.userConfig[0x25b] = 0x5fe3f; const auto quadStages = AgcDriver::Graphics::DecodeState(queue).stages; const auto& quads = quadStages.tessellation; - Require(quads && quads->inputControlPoints == 4 && quads->outputControlPoints == 4 && quads->domain == 2 && quads->partitioning == 2 && quads->outputTopology == 3 && quadStages.minTessellationLevel == 1.0f && quadStages.maxTessellationLevel == 9.0f && quads->patchesPerGroup == 63, "counter-clockwise quad patches with HOS levels [1, 9] and 63 patches per group decoded differently"); + Require(quads && quads->inputControlPoints == 4 && quads->outputControlPoints == 4 && quads->domain == 2 && quads->partitioning == 2 && quads->outputTopology == 3 && quadStages.minTessellationLevel == 1.0f && quadStages.maxTessellationLevel == 9.0f && quads->patchesPerGroup == 63 && quads->primitiveGroupSize == 63 && quads->breakAtInstance, "counter-clockwise quad patches with HOS levels [1, 9], 63 patches per group and BREAK_WAVE_AT_EOI decoded differently"); + queue.userConfig[0x25b] = 0x8040; + const auto shared = AgcDriver::Graphics::DecodeState(queue).stages.tessellation; + Require(shared && shared->primitiveGroupSize == 64 && !shared->breakAtInstance, "GE_CNTL 0x8040 did not decode as 64 primitives per group with BREAK_WAVE_AT_EOI clear"); + queue.userConfig.erase(0x25b); + const auto missing = AgcDriver::Graphics::DecodeState(queue).stages.tessellation; + Require(missing->primitiveGroupSize == 0 && !missing->breakAtInstance, "a draw without GE_CNTL decoded a primitive group size or BREAK_WAVE_AT_EOI"); queue.context[0x2db] = 0x40069; Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->outputTopology == 3, "counter-clockwise triangle patches decoded differently"); for (const auto [maximum, minimum, reason] : {std::tuple{0x3f800000u, 0x40000000u, "tessellation level clamps"}, std::tuple{0x41100000u, 0xbf800000u, "tessellation level clamps"}, std::tuple{0x3f000000u, 0x00000000u, "tessellation level clamps"}, std::tuple{0x42800000u, 0x42800000u, "tessellation level clamps"}, std::tuple{0x7fc00000u, 0x3f800000u, "non-finite register"}, std::tuple{0x7f800000u, 0x3f800000u, "non-finite register"}}) { diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index a52274cfa9..3cab2f43d6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1709,10 +1709,10 @@ void verifyMeshConfiguration() { RecompileRequest hull{}; hull.shader = {ShaderStage::TessellationControl, 0x10000u, code, 0, {}}; hull.context.waveSize = 64; - hull.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{4u, 4u, 2u, 2u, 3u, 63u}, {}}; + hull.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{4u, 4u, 2u, 2u, 3u, 63u, 63u, true}, {}}; const auto hullReplay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(hull)); const auto* replayed = hullReplay.request.graphics && hullReplay.request.graphics->tessellation ? &*hullReplay.request.graphics->tessellation : nullptr; - require(replayed != nullptr && replayed->domain == 2u && replayed->outputTopology == 3u && replayed->patchesPerGroup == 63u, "the quad tessellation configuration or its patches per group were lost in serialization"); + require(replayed != nullptr && replayed->domain == 2u && replayed->outputTopology == 3u && replayed->patchesPerGroup == 63u && replayed->primitiveGroupSize == 63u && replayed->breakAtInstance, "the quad tessellation configuration, its patches per group, its primitive group size or BREAK_WAVE_AT_EOI were lost in serialization"); auto grouped = hull; grouped.graphics->tessellation->patchesPerGroup = 62u; for (const auto stage : {ShaderStage::Local, ShaderStage::TessellationControl, ShaderStage::TessellationEvaluation}) { @@ -1723,6 +1723,26 @@ void verifyMeshConfiguration() { RecompileCacheKey::Build(grouped, key); require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on the patches per group, which bound their relative patch index"); } + auto primitiveGrouped = hull; + primitiveGrouped.graphics->tessellation->primitiveGroupSize = 126u; + for (const auto stage : {ShaderStage::Local, ShaderStage::TessellationControl, ShaderStage::TessellationEvaluation}) { + hull.shader.stage = stage; + primitiveGrouped.shader.stage = stage; + RecompileCacheKey::Build(hull, key); + const auto patchKey = key; + RecompileCacheKey::Build(primitiveGrouped, key); + require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on the primitive group size, which rebuilds their relative patch index"); + } + auto shared = hull; + shared.graphics->tessellation->breakAtInstance = false; + for (const auto stage : {ShaderStage::Local, ShaderStage::TessellationControl, ShaderStage::TessellationEvaluation}) { + hull.shader.stage = stage; + shared.shader.stage = stage; + RecompileCacheKey::Build(hull, key); + const auto patchKey = key; + RecompileCacheKey::Build(shared, key); + require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on BREAK_WAVE_AT_EOI, which decides whether their relative patch index can be rebuilt"); + } const std::array users{}; request.context.waveSize = 32u; request.context.userData = users; @@ -1864,14 +1884,14 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle; minimal.context.pixel->layerExported = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQRcAAAA=", "new requests did not use serialization version 23"); + require(requestPrefix(encoded, 8u) == "NVNQQRgAAAA=", "new requests did not use serialization version 24"); require(serializer.Deserialize(encoded).request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle, "the pixel primitive class did not round-trip"); require(serializer.Deserialize(encoded).request.context.pixel->layerExported, "the pixel layer export did not round-trip"); constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; for (std::size_t bytes = 0; bytes < 22u; ++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 or layer export flag was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRgAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRkAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index f264bdbecc..e0e687fbc6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include namespace { @@ -69,6 +70,14 @@ alignas(256) constexpr std::array DomainCode{ 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, 0x14131214, 0xbf810000, }; +alignas(256) constexpr std::array DomainRelativeData{ + 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0x4a2a0e81, 0x7e2a0d15, 0x102a2aff, 0x3e000000, + 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, 0xe0341020, 0x04030e01, 0xbf8c3f70, 0x08040af2, + 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, 0x10221705, 0x3e221b06, 0x3e221f02, 0xd5510012, + 0x040a0d05, 0x102424ff, 0x40400000, 0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, + 0x14151214, 0xbf810000, +}; + alignas(256) constexpr std::array DomainCompareBranch{ 0xf8000941, 0x00000000, 0xd4040014, 0x00020a80, 0xbeea0414, 0xbf870001, 0xbf800000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, 0xe0341020, 0x04030e01, 0xbf8c3f70, @@ -104,6 +113,16 @@ alignas(256) constexpr std::array HullPatchConstant{ 0x0202070b, 0xe0701064, 0x0202060b, 0xbf810000, }; +alignas(256) constexpr std::array HullRelativeData{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0x7da82881, 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc1008, + 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, + 0xe0781000, 0x0403180a, 0x4a162481, 0x161616ff, 0x000000d0, 0x4c1616ff, 0x00008000, 0xe0741000, + 0x0202070b, 0x4a182481, 0x7e180d0c, 0xe0701064, 0x02020c0b, 0xbf810000, +}; + alignas(256) constexpr std::array HullUnguardedFactors{ 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, @@ -251,6 +270,17 @@ alignas(256) constexpr std::array Indices{ Corner(2, 0, 0), Corner(2, 2, 0), Corner(2, 1, 2), }; +alignas(256) constexpr std::array LatePatchIndices{ + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(2, 0, 0), Corner(2, 2, 0), Corner(2, 1, 2), + Corner(0, 0, 0), Corner(0, 2, 0), Corner(0, 1, 2), +}; + alignas(256) constexpr std::array QuadIndices{ Corner(0, 0, 0), Corner(0, 2, 0), Corner(0, 2, 2), Corner(0, 0, 2), Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 2, 2), Corner(1, 0, 2), @@ -497,6 +527,27 @@ void RequireIntegerPartitioning(AgcDriver::VulkanDevice& device, const ShaderRec Require(CoveredPixels(2, false) > 100 && PixelAt(Column(2, 0.25f), center)[1] > 32, "integer partitioning: the level-5 patch has no interior domain points at its centre, " + PixelText(PixelAt(Column(2, 0.25f), center))); } +void RequireRelativePatches(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + const auto center = Row(-0.5f + 1.0f / 3.0f); + for (const auto& [hull, domain, name] : {std::tuple{std::span(HullRelativeData), std::span(DomainPatchRead), "hull"}, std::tuple{std::span(HullCode), std::span(DomainRelativeData), "domain"}}) { + for (const auto& [patches, blue] : {std::pair{PatchDraw{{3u, 3u, 1u, 2u, 2u, 3u, 3u, true}, Indices}, 96}, std::pair{PatchDraw{{3u, 3u, 1u, 2u, 2u, 2u, 4u, true}, Indices}, 32}, std::pair{PatchDraw{{3u, 3u, 1u, 2u, 2u, 3u, 4u, true}, LatePatchIndices}, 96}, std::pair{PatchDraw{{3u, 3u, 1u, 2u, 2u, 3u, 2u, true}, Indices}, 32}}) { + std::uint32_t cursor = 0; + const Stages stages{ + Compile(target, ShaderStage::Local, LocalCode, cursor, patches), + Compile(target, ShaderStage::TessellationControl, hull, cursor, patches), + Compile(target, ShaderStage::TessellationEvaluation, domain, cursor, patches), + Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), + }; + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches); + for (const auto& [patch, expected] : {std::pair{0u, 32}, std::pair{2u, blue}}) { + const auto pixel = PixelAt(Column(patch, 0.25f), center); + Require(pixel[0] == 255 && std::abs(pixel[2] - expected) <= 1, "the patch in column " + std::to_string(patch) + " with " + std::to_string(patches.configuration.patchesPerGroup) + " patches per group and " + std::to_string(patches.configuration.primitiveGroupSize) + " primitives per group did not take its blue channel from the " + name + " program's relative patch index, " + PixelText(pixel)); + } + Require(PixelAt(Column(1, 0.25f), center) == Background, "the patch with level 0 was not culled"); + } + } +} + void RequireQuads(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { const auto center = Row(0.0f); std::array frontFacing{}; @@ -589,6 +640,10 @@ int main() { ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainNeighbourPoint, "neither its patch's control points nor a patch-constant record", RecordPatches); ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainCulledPrimitive, "passthrough primitive"); ExpectRefusal(target, ShaderStage::Local, LocalIndexAsData, "outside a tessellation ring or LDS address"); + ExpectRefusal(target, ShaderStage::TessellationControl, HullRelativeData, "GE_CNTL.PRIM_GRP_SIZE is 0", RecordPatches); + ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainRelativeData, "GE_CNTL.PRIM_GRP_SIZE is 0", RecordPatches); + ExpectRefusal(target, ShaderStage::TessellationControl, HullRelativeData, "GE_CNTL.BREAK_WAVE_AT_EOI is clear", {{3u, 3u, 1u, 2u, 2u, 3u, 3u}, Indices}); + ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainRelativeData, "GE_CNTL.BREAK_WAVE_AT_EOI is clear", {{3u, 3u, 1u, 2u, 2u, 3u, 3u}, Indices}); ExpectRefusal(target, ShaderStage::TessellationControl, HullCode, "outside its patch's quad factors", {{3u, 3u, 2u, 2u, 2u}, Indices}); ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "not affine"); ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "not affine", {{3u, 3u, 1u, 2u, 2u, 158u}, Indices}); @@ -605,6 +660,7 @@ int main() { RequireIntegerPartitioning(*device, target); RequireQuads(*device, target); RequirePatchConstants(*device, target); + RequireRelativePatches(*device, target); RequireCompareBranches(target); device.reset(); RequireRegisteredStages(); diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 6f51a855af..137ca7b8fd 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -108,6 +108,8 @@ private: for (const auto value : {tessellation->inputControlPoints, tessellation->outputControlPoints, tessellation->domain, tessellation->partitioning, tessellation->outputTopology}) append(key, value); if (request.shader.stage == ShaderStage::Local) return; append(key, tessellation->patchesPerGroup); + append(key, tessellation->primitiveGroupSize); + append(key, tessellation->breakAtInstance); } template diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 217d54eee7..f7ecdc5598 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -523,6 +523,8 @@ void writeTessellationConfiguration(Writer& writer, const TessellationConfigurat writer.WriteU32(configuration.partitioning); writer.WriteU32(configuration.outputTopology); writer.WriteU32(configuration.patchesPerGroup); + writer.WriteU32(configuration.primitiveGroupSize); + writer.WriteBool(configuration.breakAtInstance); } TessellationConfiguration readTessellationConfiguration(Reader& reader, std::uint32_t version) { @@ -533,6 +535,10 @@ TessellationConfiguration readTessellationConfiguration(Reader& reader, std::uin configuration.partitioning = reader.ReadU32(); configuration.outputTopology = reader.ReadU32(); if (version >= 22u) configuration.patchesPerGroup = reader.ReadU32(); + if (version >= 24u) { + configuration.primitiveGroupSize = reader.ReadU32(); + configuration.breakAtInstance = reader.ReadBool(); + } return configuration; } @@ -752,7 +758,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(23u); + writer.WriteU32(24u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -784,7 +790,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 > 23u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 24u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index f5029b374d..9c521c43ea 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -113,6 +113,8 @@ struct ShaderTessellationInputInfo { std::uint32_t partitioning = 0; std::uint32_t outputTopology = 0; std::uint32_t patchesPerGroup = 256; + std::uint32_t primitiveGroupSize = 0; + bool breakAtInstance = false; }; struct ShaderVertexInputInfo { diff --git a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp index d75c10c039..8d9e91c426 100644 --- a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp +++ b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp @@ -572,8 +572,9 @@ public: Fail("a tessellation ring base is used outside a lowered ring access", 0u); } } - requireRelativeIndependence(relative); - for (IrValue* base : relative) base->ReplaceAllUsesWith(&constant(0u)); + const bool data = usedAsData(relative); + if (data) requireGroupedPatches(); + for (IrValue* base : relative) base->ReplaceAllUsesWith(data ? &groupedPatch(*base) : &constant(0u)); deadCodeEliminator.Eliminate(program); program.Metadata().tessellationLocalStride = localStride; program.Metadata().tessellationControlStride = controlStride; @@ -732,7 +733,7 @@ private: return address && address->dynamic && address->relative == static_cast(tessellation.inputControlPoints) * localStride; } - void requireRelativeIndependence(const std::vector& bases) { + bool usedAsData(const std::vector& bases) { std::vector pending(bases.begin(), bases.end()); std::unordered_set visited(bases.begin(), bases.end()); while (!pending.empty()) { @@ -758,9 +759,31 @@ private: } if (dynamicInputAddress(user, value)) continue; const auto address = evaluator.Evaluate(value, nullptr); - if (!address || address->relative != 0) Fail("the relative patch or vertex index is used outside a tessellation ring or LDS address", 0u); + if (!address || address->relative != 0) return true; } } + return false; + } + + void requireGroupedPatches() const { + if (stage == IrShaderStage::Local) Fail("the relative patch or vertex index is used outside a tessellation ring or LDS address", 0u); + if (tessellation.primitiveGroupSize == 0u) Fail(stage == IrShaderStage::TessellationControl ? "the hull program uses its relative patch index as data, but GE_CNTL.PRIM_GRP_SIZE is 0 or not programmed" : "the domain program uses its relative patch index as data, but GE_CNTL.PRIM_GRP_SIZE is 0 or not programmed", 0u); + if (!tessellation.breakAtInstance) Fail(stage == IrShaderStage::TessellationControl ? "the hull program uses its relative patch index as data, but GE_CNTL.BREAK_WAVE_AT_EOI is clear, so the instances of a draw can share hull threadgroups" : "the domain program uses its relative patch index as data, but GE_CNTL.BREAK_WAVE_AT_EOI is clear, so the instances of a draw can share hull threadgroups", 0u); + } + + IrValue& groupedPatch(IrValue& base) { + const auto emit = [&](IrOpcode opcode, std::initializer_list arguments) -> IrValue& { + IrValue& value = program.CreateValue(opcode, IrType::U32); + for (IrValue* argument : arguments) value.AddArgument(argument); + base.Parent()->InsertInstructionBefore(&base, &value); + return value; + }; + const auto remainder = [&](IrValue& value, std::uint32_t divisor) -> IrValue& { + IrValue& count = constant(divisor); + return emit(IrOpcode::ISub32, {&value, &emit(IrOpcode::IMul32, {&emit(IrOpcode::UDiv32, {&value, &count}), &count})}); + }; + IrValue& primitive = emit(IrOpcode::GetBuiltin, {&constant(static_cast(StageInputKind::PrimitiveId)), &constant(0u)}); + return remainder(remainder(primitive, tessellation.primitiveGroupSize), tessellation.patchesPerGroup); } void removeUnobservedCycles() { diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 9a22de5259..d10b07f18b 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -289,6 +289,8 @@ struct TessellationConfiguration { std::uint32_t partitioning; std::uint32_t outputTopology; std::uint32_t patchesPerGroup = 256; + std::uint32_t primitiveGroupSize = 0; + bool breakAtInstance = false; }; inline constexpr std::uint32_t MeshDrawPushOffsetBytes = 104; diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 4bce08497c..7fbb203fa3 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -189,6 +189,8 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues target.partitioning = tessellation->partitioning; target.outputTopology = tessellation->outputTopology; target.patchesPerGroup = tessellation->patchesPerGroup; + target.primitiveGroupSize = tessellation->primitiveGroupSize; + target.breakAtInstance = tessellation->breakAtInstance; } if (stage == ShaderStageKind::Mesh) { if (mesh == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: a mesh-stage program has no mesh configuration"); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 03725ac06a..d4eafc72ff 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -89,6 +89,7 @@ - [VGT_HOS_MIN/MAX_TESS_LEVEL](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) clamp positive tessellation factors (inner ones to at least 1); outer factors that are zero, negative or NaN still cull the patch and such inner factors become max(HOS_MIN, 1). The console's clamp and cull order is not measured. - [Patch constants](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull programs write and domain programs read are placed by their off-chip address modulo the record stride, as the two compile apart; a domain read of a field the hull never writes, or of only a neighbouring patch's record, is not detected. - [Integer partitioning](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (`VGT_TF_PARAM.PARTITIONING` 0) is drawn with Vulkan's equal spacing, which [radeonsi programs as `PART_INTEGER`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L539-540), with factors bounded at 64 instead of fractional-odd's 63; how the console rounds a clamped factor up to a whole number of segments is not measured. Power-of-two (1) and fractional-even (3) partitioning throw. +- The [relative patch index](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull and domain programs use as data is (PrimitiveID mod PRIM_GRP_SIZE) mod NUM_PATCHES, as if each primitive group and each instance started new threadgroups; it throws when GE_CNTL.BREAK_WAVE_AT_EOI is clear. Unmeasured; PPSA26344 programs 64 primitives for 63 patches. - The [output modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the shader's float mode as measured on RDNA2 (gfx1035): the hardware applies it only with IEEE mode off and output denormals of the result's precision flushed (bit 1 of the `FLOAT_MODE` denormal field clear), and ignores it otherwise. On f32 results it is applied as the hardware does with f32 denormals flushed; a shader without a known float mode is translated with IEEE mode off and f32 denormals flushed, the mode titles use. On f16 and f64 results it is ignored, and it throws in the modes where the hardware applies it. - The [clamp modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows `DX10_CLAMP`, measured on RDNA2 (gfx1035) for f32, f16, packed f16 and f64 results at IEEE=0 and IEEE=1: with `DX10_CLAMP`=1 (and in shaders without a known float mode) a NaN result clamps to +0, including the quieted NaN that `v_pk_min_f16` and `v_pk_max_f16` return for a signaling NaN operand with IEEE=1 (measured on RDNA2 gfx1036 at both `IEEE_MODE` and both `DX10_CLAMP` settings); with `DX10_CLAMP`=0 a NaN result is written unchanged and every other value clamps to [0, 1] as before. f64 results follow the same rule on hardware, but a clamped f64 result with `DX10_CLAMP`=0 throws until it has an execution test. With `DX10_CLAMP`=0 the NaN's bits are those the translation already produces for the instruction without clamp, so where those are left to the driver (f32 and scalar f16 arithmetic) the payload is the driver's, not the hardware's input propagation. `v_dot2_f32_f16` and the integer clamps are unchanged. - [FP16_OVFL](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows the shader's float mode as measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f16 denormals kept and flushed: with `FP16_OVFL`=1 an f16 result that rounds to ±infinity is written as ±0x7bff unless a source operand is infinite, so `v_rcp_f16`, `v_rsq_f16` and `v_log_f16` of ±0 and `v_exp_f16` of 16 or more are clamped while an infinite input passes through, and `v_div_fixup_f16` keeps the infinity it writes for a zero denominator or an infinite numerator and clamps any other infinite quotient, a NaN one included. `v_cvt_pkrtz_f16_f32` rounds toward zero and never overflows. The sources are checked as the f32 values the translation computes with; the rule is applied to `v_fma_mix*_f16` with f32 sources and to `v_interp_p2_f16` without a measurement of infinite f32 sources or parameters. Round modes other than nearest even are not modelled, so the overflow threshold is the nearest-even one (65520).