From 8c06af0ad9502d2dd85ef7fa61f59f1a75c01260 Mon Sep 17 00:00:00 2001 From: Scott Schuster Date: Fri, 9 Oct 2026 23:11:00 -0500 Subject: [PATCH] fix(agc): honour primitive restart in mesh-path triangle strips A geometry (NGG) strip draw on the mesh path assigned every primitive the next index positions without looking for the restart index: a triangle spanning a restart fetched vertex 0xFFFF and was drawn to wherever that read landed, and the strip parity never reset, so after an odd-length strip every triangle's winding flipped and face culling dropped it. For an indexed strip draw with restart enabled and the restart index present, the draw now records each position's strip start (one past the last restart at or before it) and passes the table after the mesh arguments, flagged in draw word 3. The mesh prologue reads the start of its primitive's last vertex: a primitive whose start lies past its first vertex spans a restart and is made degenerate, and the others take their parity from their strip's start (or the draw's first index). The restart vertex itself fetches vertex 0. The table is built from the index bytes the draw already reads (its copy, or the guest memory an in-place import reads), next to #1968's restart-aware highest-index scan. The new Mesh.cpp test draws quads as four-index strips each followed by a restart (16- and 32-bit indices, and an indirect draw starting mid-buffer) with front faces culled: every quad must render whole and the gaps between them stay clear. Without the table the first check fails ("a triangle crossed the restart after quad 0"). AI-assisted (Claude Code). Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/VertexInput.hpp | 25 +++++++++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 44 ++++++++++++--- core/libs/prx/libSceAgcDriver/tests/Mesh.cpp | 55 ++++++++++++++++++- .../IntermediateRepresentation/IrOpcode.hpp | 1 + .../src/IrOpcode.cpp | 1 + core/shader/recompiler/Recompiler.hpp | 3 + .../SpirvBackend/SpirvEmitterInstructions.hpp | 1 + .../SpirvBackend/src/SpirvFlowEmitter.cpp | 1 + .../SpirvBackend/src/SpirvModuleEmitter.cpp | 46 +++++++++++++--- .../Translation/src/InstructionTranslator.cpp | 22 ++++++-- 10 files changed, 177 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp index d3b182c7a..0bc1a7c21 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp @@ -246,6 +246,31 @@ inline std::optional HighestDrawIndex(std::span return highest; } +inline std::vector MeshRestartTable(std::span indices, std::uint32_t indexSize) { + Require(indexSize == 2 || indexSize == 4, "unsupported index size"); + const auto restartIndex = indexSize == 2 ? 0xffffu : 0xffffffffu; + std::vector starts(indices.size() / indexSize); + std::uint32_t stripStart = 0; + bool restarted = false; + for (std::size_t position = 0; position < starts.size(); ++position) { + std::uint32_t index = 0; + if (indexSize == 2) { + std::uint16_t value = 0; + std::memcpy(&value, indices.data() + position * 2u, sizeof(value)); + index = value; + } else { + std::memcpy(&index, indices.data() + position * 4u, sizeof(index)); + } + if (index == restartIndex) { + restarted = true; + stripStart = static_cast(position) + 1u; + } + starts[position] = stripStart; + } + if (!restarted) starts.clear(); + return starts; +} + inline std::vector SoloZeroPaddedFetchIndices(const std::vector& fetches, const std::vector& fetchValid) { Require(fetchValid.size() == fetches.size(), "fetch validity does not match the fetch count"); std::vector solo; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 4d238de0c..edca5809a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -961,6 +961,7 @@ struct DrawInputs { std::set fragmentOutputs; VkPipelineStageFlags shaderStages = 0; std::uint32_t meshGroups = 0; + std::vector restartTable; }; std::shared_ptr zeroVertexBuffer(const Context& context, const ShaderRecompiler::VertexAttribute& attribute) { @@ -1062,6 +1063,10 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P Require(*highest <= context.limits.maxDrawIndexedIndexValue, "index exceeds the device's indexed draw limit"); Require(!fanGeometry || !state.primitiveRestart || *highest != (draw.indexSize == 2 ? 0xffffu : 0xffffffffu), "primitive restart in a triangle fan geometry draw is unsupported"); inputs.maxIndex = *highest; + if (skipRestart && state.stages.mesh && state.stages.mesh->inputPrimitive == 6) { + const auto indexSpan = import != nullptr ? std::span(reinterpret_cast(draw.indexAddress), static_cast(indexBytes)) : copy.buffer->Bytes().first(static_cast(indexBytes)); + inputs.restartTable = MeshRestartTable(indexSpan, draw.indexSize); + } if (import != nullptr) { inputs.indexHandle = import->buffer; inputs.indexOffset = draw.indexAddress - import->base; @@ -1363,10 +1368,30 @@ bool recordIndirectArguments(const Context& context, VkCommandBuffer commands, R return true; } -std::shared_ptr recordMeshArguments(const Context& context, VkCommandBuffer commands, Recorder* recorder, bool recorded, const State& state, const Pm4::DrawParameters& draw, const IndirectRecord& indirect, const std::function& countBarrier) { +std::shared_ptr meshArgumentBuffer(const Context& context, std::span restartTable) { + const auto tableBytes = restartTable.empty() ? 0u : sizeof(std::uint32_t) * (restartTable.size() + 1u); + auto arguments = std::make_shared(context, ShaderRecompiler::MeshArgumentBytes + tableBytes, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + if (!restartTable.empty()) { + Require(restartTable.size() <= std::numeric_limits::max(), "mesh restart table is too large"); + const auto length = static_cast(restartTable.size()); + auto* bytes = arguments->Bytes().data() + ShaderRecompiler::MeshArgumentBytes; + std::memcpy(bytes, &length, sizeof(length)); + std::memcpy(bytes + sizeof(length), restartTable.data(), restartTable.size_bytes()); + } + return arguments; +} + +std::shared_ptr directMeshArguments(const Context& context, const Pm4::DrawParameters& draw, std::span restartTable) { + auto arguments = meshArgumentBuffer(context, restartTable); + const MeshArguments resolved{0u, 0u, 0u, draw.indexCount, 0u}; + std::memcpy(arguments->Bytes().data(), &resolved, sizeof(resolved)); + return arguments; +} + +std::shared_ptr recordMeshArguments(const Context& context, VkCommandBuffer commands, Recorder* recorder, bool recorded, const State& state, const Pm4::DrawParameters& draw, const IndirectRecord& indirect, const std::function& countBarrier, std::span restartTable) { const auto* args = indirect.args; const auto rules = MeshArgumentRulesFor(context, *state.stages.mesh, draw.indexCount); - auto arguments = std::make_shared(context, ShaderRecompiler::MeshArgumentBytes, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + auto arguments = meshArgumentBuffer(context, restartTable); if (indirect.path == IndirectDrawPath::Gpu && recorder != nullptr && indirect.argumentImport->address != 0) { const std::array words{rules.indexCount, rules.inputSize, rules.step, rules.primitivesPerGroup, rules.maxGroups, rules.maxInstances, rules.maxTotal}; if (recorder->RecordMeshArguments(commands, indirect.argumentImport->address + (args->arguments - indirect.argumentImport->base), arguments->DeviceAddress(), words)) { @@ -1481,7 +1506,7 @@ struct RecordedDraw { VkShaderStageFlags pushStages = 0; }; -void pushDrawConstants(const Pipeline& pipeline, VkCommandBuffer commands, const State& state, const Pm4::DrawParameters& draw, std::span shaders, const ShaderResources& resources, const std::array* bytes, VkShaderStageFlags stages, VkDeviceAddress meshArguments = 0) { +void pushDrawConstants(const Pipeline& pipeline, VkCommandBuffer commands, const State& state, const Pm4::DrawParameters& draw, std::span shaders, const ShaderResources& resources, const std::array* bytes, VkShaderStageFlags stages, VkDeviceAddress meshArguments = 0, bool restartTable = false) { auto block = bytes != nullptr ? *bytes : AssemblePushConstants(shaders); if (bytes == nullptr) { resources.PatchPushConstants(block); @@ -1493,7 +1518,7 @@ void pushDrawConstants(const Pipeline& pipeline, VkCommandBuffer commands, const } const auto firstVertex = draw.indirect ? draw.indirect->vertexConstant : draw.firstVertex; const auto firstInstance = draw.indirect ? draw.indirect->instanceConstant : draw.firstInstance; - const std::array words{draw.indexCount, firstVertex, firstInstance, draw.indexed ? draw.indexSize : 0u, static_cast(meshArguments), static_cast(meshArguments >> 32u)}; + const std::array words{draw.indexCount, firstVertex, firstInstance, draw.indexed ? draw.indexSize | (restartTable ? ShaderRecompiler::MeshIndexRestartTable : 0u) : 0u, static_cast(meshArguments), static_cast(meshArguments >> 32u)}; static_assert(ShaderRecompiler::MeshDrawPushOffsetBytes + ShaderRecompiler::MeshDrawPushBytes == PipelinePushSlotBytes); std::memcpy(block.data() + ShaderRecompiler::MeshDrawPushOffsetBytes, words.data(), sizeof(words)); pipeline.PushConstants(commands, stages | VK_SHADER_STAGE_MESH_BIT_EXT, block); @@ -1651,6 +1676,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam VkBuffer argumentBuffer = VK_NULL_HANDLE; VkDeviceSize argumentOffset = 0; bool rewritten = false; + if (!meshIndirect && !inputs.restartTable.empty()) meshArguments = directMeshArguments(context, draw, inputs.restartTable); if (continued) { record.pipeline->Continue(commands, state); } else { @@ -1665,7 +1691,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam } APS5_LOG_CHARS_OUT_DEBUG("Upload barrier recorded"); if (meshIndirect) { - meshArguments = recordMeshArguments(context, commands, recorder, true, state, draw, *record.indirect, countBarrier); + meshArguments = recordMeshArguments(context, commands, recorder, true, state, draw, *record.indirect, countBarrier, inputs.restartTable); argumentBuffer = meshArguments->Handle(); } else if (gpuIndirect) { rewritten = recordIndirectArguments(context, commands, recorder, true, *record.indirect, scratch, argumentBuffer, argumentOffset, countBarrier); @@ -1683,7 +1709,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam resources.Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout()); } APS5_LOG_CHARS_OUT_DEBUG("Resources bound"); - pushDrawConstants(*record.pipeline, commands, state, draw, shaders, resources, record.pushBytes, record.pushStages, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0); + pushDrawConstants(*record.pipeline, commands, state, draw, shaders, resources, record.pushBytes, record.pushStages, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0, !inputs.restartTable.empty()); APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded"); recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset); if (record.pipeline->SplitsFaces()) { @@ -2117,8 +2143,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& APS5_LOG_CHARS_OUT_DEBUG("Upload barrier recorded"); std::shared_ptr meshArguments; if (state.stages.mesh && args != nullptr) { - meshArguments = recordMeshArguments(context, commands, recorder, recorded, state, draw, indirect, countBarrier); + meshArguments = recordMeshArguments(context, commands, recorder, recorded, state, draw, indirect, countBarrier, inputs.restartTable); argumentBuffer = meshArguments->Handle(); + } else if (!inputs.restartTable.empty()) { + meshArguments = directMeshArguments(context, draw, inputs.restartTable); } else if (gpuIndirect) { rewritten = recordIndirectArguments(context, commands, recorder, recorded, indirect, scratch, argumentBuffer, argumentOffset, countBarrier); } @@ -2153,7 +2181,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& APS5_LOG_CHARS_OUT_DEBUG("Pipeline Begin OK"); resources->Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline->Layout()); APS5_LOG_CHARS_OUT_DEBUG("Resources bound"); - pushDrawConstants(*pipeline, commands, state, draw, shaders, *resources, nullptr, 0, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0); + pushDrawConstants(*pipeline, commands, state, draw, shaders, *resources, nullptr, 0, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0, !inputs.restartTable.empty()); APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded"); recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset); if (pipeline->SplitsFaces()) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp index 9cc2ceaa9..c92bd7fad 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp @@ -95,6 +95,9 @@ alignas(256) std::array Fan{}; alignas(256) std::array FanScrambled{}; alignas(256) std::array FanIndices{}; alignas(256) std::array FanRestart{}; +alignas(256) std::array Quads{}; +alignas(256) std::array QuadStrips{}; +alignas(256) std::array QuadStrips32{}; std::array VertexBufferDescriptor(const void* vertices, std::uint32_t count) { const auto address = reinterpret_cast(vertices); @@ -127,6 +130,7 @@ struct MeshDraw { std::uint32_t geometryPushBytes = ShaderRecompiler::MeshDrawPushOffsetBytes; bool restart = false; std::uint32_t waveSize = 64u; + VkCullModeFlags cull = VK_CULL_MODE_NONE; }; void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { @@ -178,7 +182,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; state.negativeOneToOne = false; state.scissor = {{0, 0}, {Width, Height}}; - state.cullMode = VK_CULL_MODE_NONE; + state.cullMode = setup.cull; state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; state.blend.colorWriteMask = 15; state.blends = {state.blend}; @@ -205,6 +209,24 @@ void CheckFan(const char* what) { Require(PixelIs(0, Background) && PixelIs(Pixels.size() - 4u, Background), std::string(what) + ": the corners changed"); } +std::size_t QuadPixel(std::uint32_t quad, float across) { + const auto x = static_cast((static_cast(quad) + across * 0.8f) * Width / Columns); + return (static_cast(Height * 3u / 4u) * Width + x) * 4u; +} + +void CheckQuads(const char* what, std::uint32_t first, std::uint32_t count) { + for (std::uint32_t quad = 0; quad < Columns; ++quad) { + const bool drawn = quad >= first && quad < first + count; + for (const float across : {0.25f, 0.75f}) { + const auto offset = QuadPixel(quad, across); + Require(PixelIs(offset, drawn ? TriangleColor(quad) : Background), std::string(what) + ": quad " + std::to_string(quad) + (across < 0.5f ? " left" : " right") + " half pixel " + PixelText(offset)); + } + const auto gap = QuadPixel(quad, 1.12f); + Require(PixelIs(gap, Background), std::string(what) + ": a triangle crossed the restart after quad " + std::to_string(quad) + ", pixel " + PixelText(gap)); + } + Require(PixelIs((static_cast(Height / 4u) * Width + Width / 2u) * 4u, Background), std::string(what) + ": the upper half changed"); +} + void CheckTriangles(const char* what) { for (std::uint32_t triangle = 0; triangle < Triangles; ++triangle) { const auto offset = CellPixel(triangle); @@ -249,6 +271,22 @@ int main() { FanRestart[k] = FanIndices[k]; } FanRestart[FanRim] = 0xffffu; + for (std::uint32_t quad = 0; quad < Columns; ++quad) { + const auto color = TriangleColor(quad); + const std::array rgba{color[0] / 255.0f, color[1] / 255.0f, color[2] / 255.0f, 1.0f}; + const float left = -1.0f + 2.0f * static_cast(quad) / Columns; + const float right = left + 1.6f / Columns; + Quads[4 * quad] = {{left, -1.0f, 0.5f, 1.0f}, rgba}; + Quads[4 * quad + 1] = {{left, 0.0f, 0.5f, 1.0f}, rgba}; + Quads[4 * quad + 2] = {{right, -1.0f, 0.5f, 1.0f}, rgba}; + Quads[4 * quad + 3] = {{right, 0.0f, 0.5f, 1.0f}, rgba}; + for (std::uint32_t k = 0; k < 4; ++k) { + QuadStrips[5 * quad + k] = static_cast(4 * quad + k); + QuadStrips32[5 * quad + k] = 4 * quad + k; + } + QuadStrips[5 * quad + 4] = 0xffffu; + QuadStrips32[5 * quad + 4] = 0xffffffffu; + } const auto testDevice = OpenVulkanTestDevice(); if (!testDevice) return VulkanTestSkipped; @@ -332,6 +370,21 @@ int main() { } Require(PixelIs((static_cast(Height / 4u) * Width + Width / 2u) * 4u, Background), "triangle strip: the upper half changed"); + const auto quadBuffer = VertexBufferDescriptor(Quads.data(), static_cast(Quads.size())); + ClearPixels(); + DrawMesh(device, {strip, {reinterpret_cast(QuadStrips.data()), static_cast(QuadStrips.size()), 2, 1, 0, true}, quadBuffer, ShaderRecompiler::MeshDrawPushOffsetBytes, true, 64u, VK_CULL_MODE_FRONT_BIT}); + CheckQuads("16-bit triangle strips with restarts", 0, Columns); + ClearPixels(); + DrawMesh(device, {strip, {reinterpret_cast(QuadStrips32.data()), static_cast(QuadStrips32.size()), 4, 1, 0, true}, quadBuffer, ShaderRecompiler::MeshDrawPushOffsetBytes, true, 64u, VK_CULL_MODE_FRONT_BIT}); + CheckQuads("32-bit triangle strips with restarts", 0, Columns); + const std::array quadWords{15u, 1u, 5u, 0u, 0u}; + std::copy(quadWords.begin(), quadWords.end(), record); + AgcDriver::Pm4::DrawParameters quadDraw{reinterpret_cast(QuadStrips.data()), static_cast(QuadStrips.size()), 2, 1, 0, true}; + quadDraw.indirect = AgcDriver::Pm4::DrawParameters::IndirectDraw{reinterpret_cast(record), 0x25u, 20u, 20u, 1u, false, 0u, 0x280u, 0x280u, 0x280u, false, 0u}; + ClearPixels(); + DrawMesh(device, {strip, quadDraw, quadBuffer, ShaderRecompiler::MeshDrawPushOffsetBytes, true, 64u, VK_CULL_MODE_FRONT_BIT}); + CheckQuads("indirect triangle strips with restarts", 1, 3); + const ShaderRecompiler::MeshConfiguration fan{5u, 3u, 5u, 9u, 3u, 64u, 1024u, 0u, 4u}; ClearPixels(); DrawMesh(device, {fan, {0, static_cast(Fan.size()), 0, 1, 0, false}, VertexBufferDescriptor(Fan.data(), static_cast(Fan.size()))}); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp index f08b2da82..e580b1118 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp @@ -79,6 +79,7 @@ enum class IrOpcode : std::uint16_t { RealtimeClock, MeshDrawParameter, MeshArgument, + MeshRestartStart, MeshAllocate, TessellationBase, GetTessellationAttribute, diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp index ec6ddd79e..7abf9208a 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp @@ -120,6 +120,7 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("RealtimeClock", U64), makeMeta("MeshDrawParameter", U32, U32), makeMeta("MeshArgument", U32, U32), + makeMeta("MeshRestartStart", U32, U32, U1), makeMeta("MeshAllocate", Void, U32), makeMeta("TessellationBase", U32, U32), makeMeta("GetTessellationAttribute", U32, U32, U32, U1), diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index e22a50162..ab6def974 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -278,6 +278,9 @@ inline constexpr std::uint32_t MeshArgumentAddressDword = 4; inline constexpr std::uint32_t MeshArgumentIndexCountDword = 3; inline constexpr std::uint32_t MeshArgumentFirstIndexDword = 4; inline constexpr std::uint32_t MeshArgumentBytes = 20; +inline constexpr std::uint32_t MeshIndexRestartTable = 0x100; +inline constexpr std::uint32_t MeshRestartLengthDword = MeshArgumentBytes / 4; +inline constexpr std::uint32_t MeshRestartTableDword = MeshRestartLengthDword + 1; inline constexpr std::uint32_t MeshIndexBufferUserWord = 4; inline constexpr std::uint32_t WorkgroupMemoryDescriptorSet = 1; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp index 8e275b607..3513c1a8f 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp @@ -62,6 +62,7 @@ void EmitVoid(SpirvValueEmitContext& context); void EmitBarrier(SpirvEmitterState& state); std::uint32_t EmitMeshDrawParameter(SpirvValueEmitContext& ctx, const IrValue& inst); std::uint32_t EmitMeshArgument(SpirvValueEmitContext& ctx, const IrValue& inst); +std::uint32_t EmitMeshRestartStart(SpirvValueEmitContext& ctx, const IrValue& inst); std::uint32_t EmitGetTessellationAttribute(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitSetTessellationAttribute(SpirvValueEmitContext& ctx, const IrValue& inst); std::uint32_t EmitGetUserData(SpirvEmitterState& state, ScalarReg reg); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index d4e8eaa77..e851f59c3 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -294,6 +294,7 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::RealtimeClock: return Invoke(EmitRealtimeClock, ctx, inst); case IrOpcode::MeshDrawParameter: return Invoke(EmitMeshDrawParameter, ctx, inst); case IrOpcode::MeshArgument: return Invoke(EmitMeshArgument, ctx, inst); + case IrOpcode::MeshRestartStart: return Invoke(EmitMeshRestartStart, ctx, inst); case IrOpcode::MeshAllocate: return Invoke(EmitMeshAllocate, ctx, inst); case IrOpcode::TessellationBase: return Invoke(EmitTessellationBase, ctx, inst); case IrOpcode::GetTessellationAttribute: return Invoke(EmitGetTessellationAttribute, ctx, inst); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index baf755e18..07f3ca948 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1053,12 +1053,9 @@ std::uint32_t EmitMeshDrawParameter(SpirvValueEmitContext& ctx, const IrValue& i return result; } -std::uint32_t EmitMeshArgument(SpirvValueEmitContext& ctx, const IrValue& inst) { - auto& state = ctx.state; - const auto index = inst.Argument(0)->ImmediateU32(); - if (state.program.Resources().stage != IrShaderStage::Mesh || index >= MeshArgumentBytes / 4u) { - ctx.Fail(inst, "invalid mesh argument"); - } +namespace { + +std::uint32_t MeshArgumentAddress(SpirvEmitterState& state) { const auto push = [&](std::uint32_t dword) { const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpAccessChain, TypePushConstantElementPointer(state), pointer, state.pushConstantVariable, ConstantU32(state, 0u), ConstantU32(state, MeshDrawPushOffsetBytes / 4u + dword)); @@ -1068,7 +1065,42 @@ std::uint32_t EmitMeshArgument(SpirvValueEmitContext& ctx, const IrValue& inst) }; const auto low = push(MeshArgumentAddressDword); const auto high = push(MeshArgumentAddressDword + 1u); - const auto address = Binary(state, spv::OpBitwiseOr, TypeScalarU64(state), low, Binary(state, spv::OpShiftLeftLogical, TypeScalarU64(state), high, ConstantU32(state, 32u))); + return Binary(state, spv::OpBitwiseOr, TypeScalarU64(state), low, Binary(state, spv::OpShiftLeftLogical, TypeScalarU64(state), high, ConstantU32(state, 32u))); +} + +std::uint32_t LoadMeshArgumentWord(SpirvEmitterState& state, std::uint32_t address, std::uint32_t dword) { + const auto element = Binary(state, spv::OpIAdd, TypeScalarU64(state), address, Binary(state, spv::OpShiftLeftLogical, TypeScalarU64(state), Unary(state, spv::OpUConvert, TypeScalarU64(state), dword), ConstantU32(state, 2u))); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpConvertUToPtr, TypePointer(state, spv::StorageClassPhysicalStorageBuffer, TypeU32(state)), pointer, element); + const auto loaded = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), loaded, pointer, spv::MemoryAccessAlignedMask, 4u); + return loaded; +} + +} + +std::uint32_t EmitMeshRestartStart(SpirvValueEmitContext& ctx, const IrValue& inst) { + auto& state = ctx.state; + if (state.program.Resources().stage != IrShaderStage::Mesh) { + ctx.Fail(inst, "mesh restart table read outside a mesh shader"); + } + return EmitValueOrZeroIfCondition(state, ctx.Arg(inst, 1), [&]() { + const auto address = MeshArgumentAddress(state); + const auto length = LoadMeshArgumentWord(state, address, ConstantU32(state, MeshRestartLengthDword)); + const auto last = Binary(state, spv::OpISub, TypeU32(state), length, ConstantU32(state, 1u)); + const auto position = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), position, GlslStd450(state), GLSLstd450UMin, ctx.Arg(inst, 0), last); + return LoadMeshArgumentWord(state, address, Binary(state, spv::OpIAdd, TypeU32(state), position, ConstantU32(state, MeshRestartTableDword))); + }); +} + +std::uint32_t EmitMeshArgument(SpirvValueEmitContext& ctx, const IrValue& inst) { + auto& state = ctx.state; + const auto index = inst.Argument(0)->ImmediateU32(); + if (state.program.Resources().stage != IrShaderStage::Mesh || index >= MeshArgumentBytes / 4u) { + ctx.Fail(inst, "invalid mesh argument"); + } + const auto address = MeshArgumentAddress(state); const auto present = Binary(state, spv::OpINotEqual, TypeBool(state), address, BdaConstant(state, 0u)); const auto before = state.currentLabel; const auto loadLabel = state.module.AllocateId(); diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index b764b46af..f6b44497a 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -262,12 +262,20 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO IrValue& primitiveCount = minimum(subtractSaturate(primitives, waveBase), u32(options.waveSize)); IrValue& waveInfo = entryIr.BitwiseOr(entryIr.ShiftLeftLogical(wave, u32(24u)), u32((totalThreads / options.waveSize) << 28u)); entryIr.SetScalarReg(static_cast(3), entryIr.BitwiseOr(waveInfo, entryIr.BitwiseOr(entryIr.ShiftLeftLogical(primitiveCount, u32(8u)), vertexCount))); - IrValue& parity = mesh.inputPrimitive == kTriStripPrimitiveType ? entryIr.BitwiseAnd(entryIr.IAdd(firstPrimitive, local), u32(1u)) : u32(0u); + const bool strip = mesh.inputPrimitive == kTriStripPrimitiveType; + IrValue& indexWord = draw(3u); + IrValue& stripPosition = entryIr.IAdd(entryIr.IAdd(firstPrimitive, local), firstIndex); + IrValue& restartTable = entryIr.INotEqual(entryIr.BitwiseAnd(indexWord, u32(MeshIndexRestartTable)), u32(0u)); + IrValue& stripStart = strip ? entryIr.Emit(IrOpcode::MeshRestartStart, IrOpcodeType(IrOpcode::MeshRestartStart), {&entryIr.IAdd(stripPosition, u32(2u)), &restartTable}) : u32(0u); + IrValue& parity = strip ? entryIr.BitwiseAnd(entryIr.ISub(stripPosition, entryIr.Emit(IrOpcode::UMax32, IrOpcodeType(IrOpcode::UMax32), {&stripStart, &firstIndex})), u32(1u)) : u32(0u); IrValue& vertex = entryIr.IMul(local, step); IrValue& item = u32(mesh.esgsItemSize); - IrValue& first = fan ? entryIr.IMul(entryIr.IAdd(vertex, u32(1u)), item) : entryIr.IMul(entryIr.IAdd(vertex, parity), item); - IrValue& second = fan ? entryIr.IMul(entryIr.IAdd(vertex, u32(2u)), item) : size >= 2u ? entryIr.IMul(entryIr.ISub(entryIr.IAdd(vertex, u32(1u)), parity), item) : u32(0u); - IrValue& third = fan ? u32(0u) : size == 3u ? entryIr.IMul(entryIr.IAdd(vertex, u32(2u)), item) : u32(0u); + const auto unlessRestarted = [&](IrValue& offset) -> IrValue& { + return strip ? entryIr.Select(entryIr.ULessThan(stripPosition, stripStart), entryIr.IMul(vertex, item), offset) : offset; + }; + IrValue& first = fan ? entryIr.IMul(entryIr.IAdd(vertex, u32(1u)), item) : unlessRestarted(entryIr.IMul(entryIr.IAdd(vertex, parity), item)); + IrValue& second = fan ? entryIr.IMul(entryIr.IAdd(vertex, u32(2u)), item) : size >= 2u ? unlessRestarted(entryIr.IMul(entryIr.ISub(entryIr.IAdd(vertex, u32(1u)), parity), item)) : u32(0u); + IrValue& third = fan ? u32(0u) : size == 3u ? unlessRestarted(entryIr.IMul(entryIr.IAdd(vertex, u32(2u)), item)) : u32(0u); entryIr.SetVectorReg(static_cast(0), entryIr.BitwiseOr(entryIr.BitwiseAnd(first, u32(0xffffu)), entryIr.ShiftLeftLogical(second, u32(16u)))); entryIr.SetVectorReg(static_cast(1), entryIr.BitwiseAnd(third, u32(0xffffu))); entryIr.SetVectorReg(static_cast(2), entryIr.IAdd(firstPrimitive, local)); @@ -277,7 +285,7 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO throw std::runtime_error("mesh shader translation requires the merged program's eight hidden user words"); } IrValue& inputVertex = fan ? entryIr.Select(entryIr.IEqual(local, u32(0u)), u32(0u), entryIr.IAdd(firstVertex, local)) : entryIr.IAdd(firstVertex, local); - IrValue& indexBytes = draw(3u); + IrValue& indexBytes = entryIr.BitwiseAnd(indexWord, u32(0xffu)); IrValue& indexed = entryIr.INotEqual(indexBytes, u32(0u)); IrValue& byteOffset = entryIr.IMul(entryIr.IAdd(inputVertex, firstIndex), indexBytes); IrValue& indexResource = entryIr.Emit(IrOpcode::GetBufferResource, IrOpcodeType(IrOpcode::GetBufferResource), {&entryIr.GetUserData(static_cast(4)), &entryIr.GetUserData(static_cast(5)), &entryIr.GetUserData(static_cast(6)), &entryIr.GetUserData(static_cast(7))}); @@ -288,7 +296,9 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO for (std::uint32_t reg = 4u; reg < 8u; reg++) { entryIr.SetScalarReg(static_cast(reg), u32(0u)); } - entryIr.SetVectorReg(static_cast(5), entryIr.IAdd(draw(1u), entryIr.Select(indexed, index, inputVertex))); + IrValue& restartIndex = entryIr.Select(entryIr.IEqual(indexBytes, u32(2u)), u32(0xffffu), u32(0xffffffffu)); + IrValue& fetchedIndex = entryIr.Select(entryIr.LogicalAnd(restartTable, entryIr.IEqual(index, restartIndex)), u32(0u), index); + entryIr.SetVectorReg(static_cast(5), entryIr.IAdd(draw(1u), entryIr.Select(indexed, fetchedIndex, inputVertex))); entryIr.SetVectorReg(static_cast(6), u32(0u)); entryIr.SetVectorReg(static_cast(7), u32(0u)); entryIr.SetVectorReg(static_cast(8), entryIr.IAdd(draw(2u), builtin(StageInputKind::WorkgroupId, 1u)));