From fb55cb2c0ef74e059f6a57419eea623e0f783b6c Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 4 Oct 2026 03:02:27 +1300 Subject: [PATCH] perf(agc): draw mesh-stage indirect draws on the GPU A mesh program took its draw's index count from push constants, so an indirect mesh-path (NGG) draw needed the CPU to read its record first, waiting for the GPU to finish everything recorded before. In PPSA21564 that was about 170 waits per 10 s at the title, menu and level (about 1 s), and one per frame in the intro (2.2 s per 10 s). The two free mesh push dwords now carry the device address of an argument record (a VkDrawMeshTasksIndirectCommandEXT followed by the index count and first index). When the address is set, the prologue takes the index count and the first index from the record (the new MeshArgument IR value, a load through a physical storage buffer pointer) and offsets its index reads by the first index; a direct draw leaves the address 0 and draws as before. Mesh modules declare the physical storage buffer capability for it, and the driver's SPIR-V validation accepts the extension in mesh modules. An indexed mesh draw with one record that patches no user SGPR now stays on the GPU. A one-thread compute pass (Graphics/shaders/MeshArguments.comp) turns the record, read in place from its host import, into the draw's argument record: the groups and instances of vkCmdDrawMeshTasksIndirectEXT and the index count and first index the mesh program reads, with the CPU path's rules (count clamped to the index buffer, nothing for an empty record or past the device's group limits). The index buffer descriptor covers the whole buffer and the program offsets by the first index. When the records cannot be read in place, the CPU reads them and fills the same argument record (ResolveMeshArguments). --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 15 ++++ .../src/Driver/Draw/IndirectDraw.cpp | 5 +- .../libSceAgcDriver/Graphics/include/Draw.hpp | 20 +++++ .../Graphics/include/Recorder.hpp | 4 + .../Graphics/shaders/MeshArguments.comp | 34 ++++++++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 85 ++++++++++++++++--- .../libSceAgcDriver/Graphics/src/Recorder.cpp | 48 +++++++++++ .../Graphics/src/ShaderValidation.cpp | 2 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 34 ++++++++ core/libs/prx/libSceAgcDriver/tests/Mesh.cpp | 41 +++++++++ .../IntermediateRepresentation/IrOpcode.hpp | 1 + .../src/IrOpcode.cpp | 1 + core/shader/recompiler/Recompiler.hpp | 4 + .../SpirvBackend/SpirvEmitterInstructions.hpp | 1 + .../SpirvBackend/src/SpirvAnalysis.cpp | 1 + .../SpirvBackend/src/SpirvFlowEmitter.cpp | 1 + .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 9 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 36 ++++++++ .../Translation/src/InstructionTranslator.cpp | 10 ++- 19 files changed, 332 insertions(+), 20 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/shaders/MeshArguments.comp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 0ae946114..7ee765a74 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -46,6 +46,19 @@ add_custom_command( VERBATIM ) +set(agcMeshArgumentsShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/MeshArguments.comp) +set(agcMeshArgumentsShaderSpv ${agcTextureDetileGeneratedDir}/MeshArguments.spv) +set(agcMeshArgumentsShaderHeader ${agcTextureDetileGeneratedDir}/MeshArguments_spv.h) +add_custom_command( + OUTPUT "${agcMeshArgumentsShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcMeshArgumentsShaderSpv}" "${agcMeshArgumentsShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcMeshArgumentsShaderSpv} -DOUTPUT=${agcMeshArgumentsShaderHeader} + -DSYMBOL=MESH_ARGUMENTS_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcMeshArgumentsShaderSource}" glslang-standalone + VERBATIM +) + set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/DisplayBuffer.cpp @@ -130,6 +143,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/UnitShadow.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Sampler.cpp ${agcTextureDetileShaderHeader} + ${agcMeshArgumentsShaderHeader} ${CMAKE_CURRENT_SOURCE_DIR}/Eq/src/Event.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Eq/src/Query.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Resource/src/Lifecycle.cpp @@ -388,6 +402,7 @@ add_test_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL Graphics/src/Recorder.cpp Graphics/src/ShaderInputState.cpp ${agcTextureDetileShaderHeader} + ${agcMeshArgumentsShaderHeader} Execution/src/GuestMemory.cpp Execution/src/Pm4.cpp ) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp index f4198dc96..a9ebfa025 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp @@ -19,7 +19,10 @@ std::optional Driver::classifyIndirectDraw(const Sha using Path = Graphics::IndirectDrawPath; const auto support = localDevice->DrawIndirectSupport(); const auto fail = [&](Path reason) { if (!indirectCpu) indirectCpu = reason; }; - if (graphics.stages.path != Graphics::ShaderPath::Vertex || graphics.rectList) fail(Path::NonVertexPath); + constexpr std::uint32_t NoLocation = 0x280u; + const bool meshPath = graphics.stages.mesh.has_value() && !graphics.stages.tessellation && !graphics.rectList; + const bool meshGpuSide = meshPath && drawParameters.indexed && indirect.count == 1 && !indirect.countIndirect && indirect.baseVertexLocation == NoLocation && indirect.startInstanceLocation == NoLocation && (!indirect.drawIndexEnabled || indirect.drawIndexLocation == NoLocation); + if ((graphics.stages.path != Graphics::ShaderPath::Vertex || graphics.rectList) && !meshGpuSide) fail(Path::NonVertexPath); if (indirect.drawIndexEnabled && indirect.drawIndexSgpr >= 0) fail(Path::DrawIndex); if (indirect.countIndirect && !support.count) fail(Path::FeatureGap); if (indirect.countIndirect && indirect.count > 1 && !support.multi) fail(Path::FeatureGap); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp index 13bbe2103..e8fc9a7c2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp @@ -30,6 +30,26 @@ void KeepDrawInput(Recorder* recorder, std::uint64_t address, const DrawInputCop std::array MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw, std::uint64_t unreadAddress); +struct MeshArguments { + std::uint32_t groups; + std::uint32_t instances; + std::uint32_t layers; + std::uint32_t indexCount; + std::uint32_t firstIndex; +}; +static_assert(sizeof(MeshArguments) == ShaderRecompiler::MeshArgumentBytes); +struct MeshArgumentRules { + std::uint32_t indexCount; + std::uint32_t inputSize; + std::uint32_t step; + std::uint32_t primitivesPerGroup; + std::uint32_t maxGroups; + std::uint32_t maxInstances; + std::uint32_t maxTotal; +}; +MeshArgumentRules MeshArgumentRulesFor(const Context& context, const ShaderRecompiler::MeshConfiguration& mesh, std::uint32_t indexCount); +MeshArguments ResolveMeshArguments(const Pm4::DrawArguments& record, const MeshArgumentRules& rules); + // Why DrawWithRecipe did not record from the recipe (the caller then runs Draw): the draw is not // recordable (no recorder, APS5_SYNC_DRAWS, APS5_DUMP_TARGETS), a resident target is gone from the // storage cache, the template is gone, the pipeline is gone from the pipeline store, or the diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 697ce09f8..36d1b8529 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -179,6 +179,7 @@ public: void Sync(); void CountSamples(); static std::uint64_t SamplesPassed(); + bool RecordMeshArguments(VkCommandBuffer commands, VkDeviceAddress record, VkDeviceAddress arguments, std::span rules); // Waits only for the batches up to the newest one that writes the range (submitting the open // batch when it is that one); later batches stay in flight. Fences of one queue signal in // submission order, so completions still run in order. Debug aid: APS5_NO_SYNC_THROUGH=1 syncs all. @@ -603,6 +604,9 @@ private: void beginSamples(Batch& batch); void readSamples(Batch& batch); bool countingSamples = false; + int meshArgumentState = 0; + VkPipelineLayout meshArgumentLayout = VK_NULL_HANDLE; + VkPipeline meshArgumentPipeline = VK_NULL_HANDLE; // BeginGpuTiming on the open batch without Commands() (RecordStore times its own run, which // Commands() would close). std::uint32_t beginTiming(std::uint64_t key); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/MeshArguments.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/MeshArguments.comp new file mode 100644 index 000000000..887790522 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/MeshArguments.comp @@ -0,0 +1,34 @@ +#version 450 +#extension GL_EXT_buffer_reference : require + +layout(local_size_x = 1) in; +layout(buffer_reference, std430, buffer_reference_align = 4) buffer Words { uint words[]; }; +layout(push_constant) uniform Parameters { + Words record; + Words arguments; + uint indexCount; + uint inputSize; + uint step; + uint primitivesPerGroup; + uint maxGroups; + uint maxInstances; + uint maxTotal; + uint unused; +} parameters; + +void main() { + uint count = parameters.record.words[0]; + uint instances = parameters.record.words[1]; + uint first = parameters.record.words[2]; + uint effective = first < parameters.indexCount ? min(count, parameters.indexCount - first) : 0u; + uint groups = 0u; + if (effective >= parameters.inputSize && instances != 0u) { + groups = (effective - parameters.inputSize) / parameters.step / parameters.primitivesPerGroup + 1u; + if (groups > parameters.maxGroups || instances > parameters.maxInstances || groups > parameters.maxTotal / instances) groups = 0u; + } + parameters.arguments.words[0] = groups; + parameters.arguments.words[1] = groups != 0u ? instances : 0u; + parameters.arguments.words[2] = groups != 0u ? 1u : 0u; + parameters.arguments.words[3] = effective; + parameters.arguments.words[4] = first; +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index e0a797962..a2f9d6047 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -594,6 +594,24 @@ std::array MeshIndexBufferDescriptor(const Pm4::DrawParameters return {static_cast(address), static_cast(address >> 32u) & 0xffffu, static_cast(bytes), RawWord3}; } +MeshArgumentRules MeshArgumentRulesFor(const Context& context, const ShaderRecompiler::MeshConfiguration& mesh, std::uint32_t indexCount) { + const auto inputSize = mesh.inputPrimitive == 1 ? 1u : mesh.inputPrimitive == 2 ? 2u : 3u; + const auto step = mesh.inputPrimitive == 5 || mesh.inputPrimitive == 6 ? 1u : inputSize; + Require(mesh.primitivesPerGroup != 0, "mesh draw contains no complete primitive"); + return {indexCount, inputSize, step, mesh.primitivesPerGroup, context.meshLimits.maxMeshWorkGroupCount[0], context.meshLimits.maxMeshWorkGroupCount[1], context.meshLimits.maxMeshWorkGroupTotalCount}; +} + +MeshArguments ResolveMeshArguments(const Pm4::DrawArguments& record, const MeshArgumentRules& rules) { + const auto first = record.firstVertexOrIndex; + const auto effective = first < rules.indexCount ? std::min(record.count, rules.indexCount - first) : 0u; + std::uint32_t groups = 0; + if (effective >= rules.inputSize && record.instances != 0) { + groups = (effective - rules.inputSize) / rules.step / rules.primitivesPerGroup + 1u; + if (groups > rules.maxGroups || record.instances > rules.maxInstances || groups > rules.maxTotal / record.instances) groups = 0; + } + return {groups, groups != 0 ? record.instances : 0u, groups != 0 ? 1u : 0u, effective, first}; +} + std::shared_ptr>> DrawCopiedWriters() { // Never destroyed: a completion action of a batch still in flight at static teardown may erase from it. static auto* const writers = new std::shared_ptr>>(std::make_shared>>()); @@ -744,7 +762,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P } } if (args == nullptr) Require(draw.indexCount != 0 && draw.instanceCount != 0, "zero-count indexed draws are unsupported"); - else Require(!state.stages.mesh && !state.stages.tessellation && !state.rectList, "indirect draw on a non-vertex path must be resolved by the driver"); + else Require((!state.stages.mesh || draw.indexed) && !state.stages.tessellation && !state.rectList, "indirect draw on a non-vertex path must be resolved by the driver"); inputs.indexBytes = static_cast(draw.indexCount) * draw.indexSize; const auto indexBytes = inputs.indexBytes; APS5_LOG_OUT_DEBUG("Index buffer bytes=%llu", static_cast(indexBytes)); @@ -769,11 +787,13 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P const auto& mesh = *state.stages.mesh; const auto inputSize = mesh.inputPrimitive == 1 ? 1u : mesh.inputPrimitive == 2 ? 2u : 3u; Require(draw.indexCount >= inputSize && mesh.primitivesPerGroup != 0, "mesh draw contains no complete primitive"); - const auto step = mesh.inputPrimitive == 5 || mesh.inputPrimitive == 6 ? 1u : inputSize; - const auto primitives = (draw.indexCount - inputSize) / step + 1u; - inputs.meshGroups = (primitives - 1u) / mesh.primitivesPerGroup + 1u; - APS5_LOG_OUT_DEBUG("Mesh primitives=%u groups=%u", primitives, inputs.meshGroups); - Require(inputs.meshGroups <= context.meshLimits.maxMeshWorkGroupCount[0] && draw.instanceCount <= context.meshLimits.maxMeshWorkGroupCount[1] && static_cast(inputs.meshGroups) * draw.instanceCount <= context.meshLimits.maxMeshWorkGroupTotalCount, "mesh draw exceeds workgroup count limits"); + if (args == nullptr) { + const auto step = mesh.inputPrimitive == 5 || mesh.inputPrimitive == 6 ? 1u : inputSize; + const auto primitives = (draw.indexCount - inputSize) / step + 1u; + inputs.meshGroups = (primitives - 1u) / mesh.primitivesPerGroup + 1u; + APS5_LOG_OUT_DEBUG("Mesh primitives=%u groups=%u", primitives, inputs.meshGroups); + Require(inputs.meshGroups <= context.meshLimits.maxMeshWorkGroupCount[0] && draw.instanceCount <= context.meshLimits.maxMeshWorkGroupCount[1] && static_cast(inputs.meshGroups) * draw.instanceCount <= context.meshLimits.maxMeshWorkGroupTotalCount, "mesh draw exceeds workgroup count limits"); + } } if (state.stages.tessellation) Require(draw.indexCount % state.stages.tessellation->inputControlPoints == 0, "incomplete tessellation patch"); // Viewport and scissor are dynamic pipeline state, so their limits are checked here per draw. @@ -943,6 +963,10 @@ struct IndirectRecord { // GPU-side indirect draw from `argumentBuffer` or the CPU-read records with the driver's rules. void recordDrawCommands(const Context& context, VkCommandBuffer commands, const State& state, const Pm4::DrawParameters& draw, const DrawInputs& inputs, const IndirectRecord* indirect, VkBuffer argumentBuffer, VkDeviceSize argumentOffset) { const auto* args = indirect != nullptr ? indirect->args : nullptr; + if (state.stages.mesh && args != nullptr) { + context.Function("vkCmdDrawMeshTasksIndirectEXT")(commands, argumentBuffer, argumentOffset, 1, sizeof(VkDrawMeshTasksIndirectCommandEXT)); + return; + } if (state.stages.mesh) { APS5_LOG_OUT_DEBUG("vkCmdDrawMeshTasksEXT groups=%u instances=%u", inputs.meshGroups, draw.instanceCount); context.Function("vkCmdDrawMeshTasksEXT")(commands, inputs.meshGroups, draw.instanceCount, 1); @@ -1025,6 +1049,24 @@ 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) { + 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); + 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)) { + countBarrier(2); + if (recorded) recorder->NotePendingRead(args->arguments, static_cast(args->RangeBytes()), Recorder::ReadKind::Indirect); + return arguments; + } + } + const auto record = !indirect.records.empty() ? indirect.records.front() : Pm4::ReadDrawArguments(*args, 0); + const auto resolved = ResolveMeshArguments(record, rules); + std::memcpy(arguments->Bytes().data(), &resolved, sizeof(resolved)); + return arguments; +} + // Debug aid: APS5_CHECK_INDIRECT_ARGS=1 reads the records of a GPU-side draw back on the CPU once // its batch completed (the GPU has finished writing them by then) and prints the first 16, counting // records whose Constant dimension holds a value the hardware would have ignored. Empty otherwise. @@ -1123,7 +1165,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) { +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) { auto block = bytes != nullptr ? *bytes : AssemblePushConstants(shaders); if (bytes == nullptr) { resources.PatchPushConstants(block); @@ -1133,7 +1175,9 @@ void pushDrawConstants(const Pipeline& pipeline, VkCommandBuffer commands, const pipeline.PushConstants(commands, stages, block); return; } - const std::array words{draw.indexCount, draw.firstVertex, draw.firstInstance, draw.indexed ? draw.indexSize : 0u, 0u, 0u}; + 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)}; static_assert(ShaderRecompiler::MeshDrawPushOffsetBytes + ShaderRecompiler::MeshDrawPushBytes == PipelinePushConstantBytes); std::memcpy(block.data() + ShaderRecompiler::MeshDrawPushOffsetBytes, words.data(), sizeof(words)); pipeline.PushConstants(commands, stages | VK_SHADER_STAGE_MESH_BIT_EXT, block); @@ -1257,7 +1301,8 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam if (recorder->HasQueuedStores() && (resources.HoldsLease() || recorder->AnyQueuedStore(touches))) recorder->FlushStores(); const auto drawBindings = resources.PrepareDrawBindings(*recorder); const bool capture = CaptureInputsEnabled(); - const bool continued = !capture && !readsTarget && !gpuIndirect && recorder->ContinuesRenderPass(passKey); + const bool meshIndirect = state.stages.mesh && args != nullptr; + const bool continued = !capture && !readsTarget && !gpuIndirect && !meshIndirect && recorder->ContinuesRenderPass(passKey); outcome.passContinued = continued; outcome.passBegun = !continued; const auto commands = continued ? recorder->CommandsInRenderPass() : recorder->Commands(); @@ -1276,6 +1321,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam recorder->NoteAccess(CommandClass::Draw, Recorder::Access{reads, resources.GpuWrites(), images, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, resources.HoldsLease()}); } std::unique_ptr scratch; + std::shared_ptr meshArguments; VkBuffer argumentBuffer = VK_NULL_HANDLE; VkDeviceSize argumentOffset = 0; bool rewritten = false; @@ -1286,7 +1332,12 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, 0, 1, &before, 0, nullptr, 0, nullptr); countBarrier(1); APS5_LOG_CHARS_OUT_DEBUG("Upload barrier recorded"); - if (gpuIndirect) rewritten = recordIndirectArguments(context, commands, recorder, true, *record.indirect, scratch, argumentBuffer, argumentOffset, countBarrier); + if (meshIndirect) { + meshArguments = recordMeshArguments(context, commands, recorder, true, state, draw, *record.indirect, countBarrier); + argumentBuffer = meshArguments->Handle(); + } else if (gpuIndirect) { + rewritten = recordIndirectArguments(context, commands, recorder, true, *record.indirect, scratch, argumentBuffer, argumentOffset, countBarrier); + } // Earlier recorded work (dispatches, the previous draw) wrote the images in the general // layout, which a lean draw renders in: no transitions. APS5_LOG_OUT_DEBUG("Beginning pipeline renderExtent=%ux%u", state.renderExtent.width, state.renderExtent.height); @@ -1300,9 +1351,10 @@ 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); + pushDrawConstants(*record.pipeline, commands, state, draw, shaders, resources, record.pushBytes, record.pushStages, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0); APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded"); recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset); + if (meshArguments != nullptr) recorder->Keep(meshArguments); if (args != nullptr) CountIndirectDraw(record.indirect->path, record.indirect->readMs, rewritten); auto checkRecords = indirectRecordCheck(record.indirect); APS5_LOG_CHARS_OUT_DEBUG("Draw recorded"); @@ -1704,7 +1756,13 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& countBarrier(); } APS5_LOG_CHARS_OUT_DEBUG("Upload barrier recorded"); - if (gpuIndirect) rewritten = recordIndirectArguments(context, commands, recorder, recorded, indirect, scratch, argumentBuffer, argumentOffset, countBarrier); + std::shared_ptr meshArguments; + if (state.stages.mesh && args != nullptr) { + meshArguments = recordMeshArguments(context, commands, recorder, recorded, state, draw, indirect, countBarrier); + argumentBuffer = meshArguments->Handle(); + } else if (gpuIndirect) { + rewritten = recordIndirectArguments(context, commands, recorder, recorded, indirect, scratch, argumentBuffer, argumentOffset, countBarrier); + } for (auto& binding : targets) { if (binding.resident != nullptr) { imageBarrier(context, commands, binding.resident->Image(), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); @@ -1731,9 +1789,10 @@ 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); + pushDrawConstants(*pipeline, commands, state, draw, shaders, *resources, nullptr, 0, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0); APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded"); recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset); + if (meshArguments != nullptr && recorded) recorder->Keep(meshArguments); if (args != nullptr) CountIndirectDraw(indirect.path, indirect.readMs, rewritten); auto checkRecords = indirectRecordCheck(args != nullptr ? &indirect : nullptr); APS5_LOG_CHARS_OUT_DEBUG("Draw recorded"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 979c007f6..d5754cbe3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/shaders/MeshArguments_spv.h" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp" @@ -1094,6 +1095,8 @@ Recorder::~Recorder() { std::lock_guard tableLock(labelTableMutex); if (labelTableOwner == this) labelTableOwner = nullptr; } + if (meshArgumentPipeline != VK_NULL_HANDLE) context.Function("vkDestroyPipeline")(context.device, meshArgumentPipeline, nullptr); + if (meshArgumentLayout != VK_NULL_HANDLE) context.Function("vkDestroyPipelineLayout")(context.device, meshArgumentLayout, nullptr); for (auto& [commands, fence] : spare) { context.Function("vkFreeCommandBuffers")(context.device, context.pool, 1, &commands); context.Function("vkDestroyFence")(context.device, fence, nullptr); @@ -1817,6 +1820,51 @@ std::uint64_t Recorder::SamplesPassed() { return samplesPassed.load(std::memory_order_acquire); } +bool Recorder::RecordMeshArguments(VkCommandBuffer commands, VkDeviceAddress record, VkDeviceAddress arguments, std::span rules) { + constexpr std::uint32_t PushBytes = 48; + if (meshArgumentState == 0) { + meshArgumentState = -1; + VkShaderModule module = VK_NULL_HANDLE; + try { + const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, PushBytes}; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.pushConstantRangeCount = 1; + layoutInfo.pPushConstantRanges = &push; + Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &meshArgumentLayout), "vkCreatePipelineLayout mesh arguments"); + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = sizeof(MESH_ARGUMENTS_SPV); + moduleInfo.pCode = MESH_ARGUMENTS_SPV; + Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule mesh arguments"); + VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; + pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + pipelineInfo.stage.module = module; + pipelineInfo.stage.pName = "main"; + pipelineInfo.layout = meshArgumentLayout; + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &meshArgumentPipeline), "vkCreateComputePipelines mesh arguments"); + context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + meshArgumentState = 1; + } catch (const std::exception& error) { + if (module != VK_NULL_HANDLE) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + std::fprintf(stderr, "[gpu] mesh indirect draws read their records on the CPU: %s\n", error.what()); + } + } + if (meshArgumentState < 0) return false; + recordBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, meshArgumentPipeline); + struct { + VkDeviceAddress record; + VkDeviceAddress arguments; + std::array rules; + } parameters{record, arguments, {rules[0], rules[1], rules[2], rules[3], rules[4], rules[5], rules[6], 0u}}; + static_assert(sizeof(parameters) == PushBytes); + context.Function("vkCmdPushConstants")(commands, meshArgumentLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, PushBytes, ¶meters); + context.Function("vkCmdDispatch")(commands, 1, 1, 1); + recordBarrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT | VK_PIPELINE_STAGE_MESH_SHADER_BIT_EXT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_SHADER_READ_BIT); + CountBarriers(CommandClass::Draw, 2); + return true; +} + void Recorder::beginSamples(Batch& batch) { VkQueryPoolCreateInfo info{VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO}; info.queryType = VK_QUERY_TYPE_OCCLUSION; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index e966e456e..2b671faec 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -251,7 +251,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(fragment && fragmentShaderBarycentric, "SPV_KHR_fragment_shader_barycentric requires enabled fragmentShaderBarycentric in a fragment shader"); break; } - Require(extension == "SPV_KHR_float_controls" || (mesh && extension == "SPV_EXT_mesh_shader") || (descriptorIndexing && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension"); + Require(extension == "SPV_KHR_float_controls" || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || (descriptorIndexing && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension"); break; } case spv::OpDecorateId: diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index c5e2ff5f2..e537a57c1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1660,6 +1660,39 @@ bool recompilesDebugBranch(std::uint32_t opcode) { return !ShaderRecompiler::Recompile(request).spirv.Words().empty(); } +void meshArgumentTests() { + using AgcDriver::Graphics::MeshArguments; + using AgcDriver::Graphics::ResolveMeshArguments; + AgcDriver::Graphics::Context context{}; + context.meshLimits.maxMeshWorkGroupCount[0] = 1000; + context.meshLimits.maxMeshWorkGroupCount[1] = 600; + context.meshLimits.maxMeshWorkGroupTotalCount = 4000; + const ShaderRecompiler::MeshConfiguration points{1u, 1u, 1u, 1u, 1u, 64u, 1024u, 0u, 4u}; + const ShaderRecompiler::MeshConfiguration triangles{4u, 32u, 96u, 96u, 32u, 128u, 2048u, 0u, 4u}; + const ShaderRecompiler::MeshConfiguration strip{6u, 8u, 10u, 10u, 8u, 64u, 1024u, 0u, 4u}; + const ShaderRecompiler::MeshConfiguration fan{5u, 8u, 10u, 10u, 8u, 64u, 1024u, 0u, 4u}; + const auto same = [](const MeshArguments& a, const MeshArguments& b) { return a.groups == b.groups && a.instances == b.instances && a.layers == b.layers && a.indexCount == b.indexCount && a.firstIndex == b.firstIndex; }; + const auto rules = [&](const ShaderRecompiler::MeshConfiguration& mesh, std::uint32_t indexCount) { return AgcDriver::Graphics::MeshArgumentRulesFor(context, mesh, indexCount); }; + const auto record = [](std::uint32_t count, std::uint32_t instances, std::uint32_t first) { return AgcDriver::Pm4::DrawArguments{count, instances, first, 0, 0}; }; + Require(same(ResolveMeshArguments(record(1, 512, 0), rules(points, 1)), {1, 512, 1, 1, 0}), "one point, 512 instances"); + Require(same(ResolveMeshArguments(record(1, 0, 0), rules(points, 1)), {0, 0, 0, 1, 0}), "no instances draws nothing"); + Require(same(ResolveMeshArguments(record(0, 4, 0), rules(points, 1)), {0, 0, 0, 0, 0}), "no indices draws nothing"); + Require(same(ResolveMeshArguments(record(96, 2, 0), rules(triangles, 300)), {1, 2, 1, 96, 0}), "one full group"); + Require(same(ResolveMeshArguments(record(99, 2, 0), rules(triangles, 300)), {2, 2, 1, 99, 0}), "a partial second group"); + Require(same(ResolveMeshArguments(record(200, 3, 150), rules(triangles, 300)), {2, 3, 1, 150, 150}), "count clamped to the index buffer"); + Require(same(ResolveMeshArguments(record(9, 1, 300), rules(triangles, 300)), {0, 0, 0, 0, 300}), "first index past the index buffer draws nothing"); + Require(same(ResolveMeshArguments(record(2, 1, 0), rules(triangles, 300)), {0, 0, 0, 2, 0}), "no complete triangle draws nothing"); + Require(same(ResolveMeshArguments(record(10, 1, 0), rules(strip, 64)), {1, 1, 1, 10, 0}), "eight strip triangles are one group"); + Require(same(ResolveMeshArguments(record(11, 1, 0), rules(strip, 64)), {2, 1, 1, 11, 0}), "a ninth strip triangle starts a group"); + Require(same(ResolveMeshArguments(record(10, 1, 0), rules(fan, 64)), {1, 1, 1, 10, 0}), "eight fan triangles are one group"); + Require(same(ResolveMeshArguments(record(11, 1, 0), rules(fan, 64)), {2, 1, 1, 11, 0}), "a ninth fan triangle starts a group"); + Require(same(ResolveMeshArguments(record(1, 601, 0), rules(points, 1)), {0, 0, 0, 1, 0}), "instances over the device limit draw nothing"); + Require(same(ResolveMeshArguments(record(96 * 7, 600, 0), rules(triangles, 96 * 7)), {0, 0, 0, 96 * 7, 0}), "groups times instances over the device limit draw nothing"); + Require(same(ResolveMeshArguments(record(96 * 6, 600, 0), rules(triangles, 96 * 6)), {6, 600, 1, 96 * 6, 0}), "groups times instances at the device limit"); + Require(same(ResolveMeshArguments(record(0xffffffffu, 1, 0xfffffff0u), rules(points, 0xffffffffu)), {15, 1, 1, 15, 0xfffffff0u}), "first index near the end of a huge index buffer"); + Require(same(ResolveMeshArguments(record(0xffffffffu, 1, 0), rules(points, 0xffffffffu)), {0, 0, 0, 0xffffffffu, 0}), "groups over the device limit draw nothing"); +} + void debugBranchTests() { for (const auto opcode : {0x17u, 0x18u, 0x19u, 0x1au}) Require(recompilesDebugBranch(opcode), "a conditional debug branch did not recompile"); } @@ -1741,6 +1774,7 @@ int main() { resourceTests(); misalignedShaderDataTests(); debugBranchTests(); + meshArgumentTests(); validationTests(); vertexCopyTests(); pixelParameterSlotTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp index 9ea3246a0..025817a38 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "Recompiler.hpp" +#include "prx/libc/include/GuestAllocations.hpp" #include #include #include @@ -251,6 +252,39 @@ int main() { CheckTriangles((std::string("non-indexed triangle list, ") + name).c_str()); } + constexpr std::size_t recordBlockBytes = 65536; + auto* record = static_cast(std::aligned_alloc(65536, recordBlockBytes)); + Require(record != nullptr, "cannot allocate the indirect record block"); + { + GuestAllocations::Mutation mutation; + mutation.Add(record, recordBlockBytes, true, true); + } + const auto drawIndirect = [&](std::uint32_t count, std::uint32_t instances, std::uint32_t first) { + const std::array words{count, instances, first, 0u, 0u}; + std::copy(words.begin(), words.end(), record); + AgcDriver::Pm4::DrawParameters draw{reinterpret_cast(Indices.data()), static_cast(Indices.size()), 2, 1, 0, true}; + draw.indirect = AgcDriver::Pm4::DrawParameters::IndirectDraw{reinterpret_cast(record), 0x25u, 20u, 20u, 1u, false, 0u, 0x280u, 0x280u, 0x280u, false, 0u}; + ClearPixels(); + DrawMesh(device, {WideSubgroup, draw, VertexBufferDescriptor(Scrambled.data(), static_cast(Scrambled.size()))}); + }; + drawIndirect(static_cast(Indices.size()), 1, 0); + CheckTriangles("indirect indexed triangle list"); + drawIndirect(3u * 10u, 1, 3u * 5u); + for (std::uint32_t triangle = 0; triangle < Triangles; ++triangle) { + const auto offset = CellPixel(triangle); + const bool drawn = triangle >= 5u && triangle < 15u; + Require(drawn ? PixelIs(offset, TriangleColor(triangle)) : PixelIs(offset, Background), "indirect index range: triangle " + std::to_string(triangle) + " pixel " + PixelText(offset)); + } + drawIndirect(1000u, 1, 3u * (Triangles - 2u)); + for (std::uint32_t triangle = 0; triangle < Triangles; ++triangle) { + const auto offset = CellPixel(triangle); + Require(triangle >= Triangles - 2u ? PixelIs(offset, TriangleColor(triangle)) : PixelIs(offset, Background), "indirect index count past the buffer: triangle " + std::to_string(triangle) + " pixel " + PixelText(offset)); + } + for (const auto& [count, instances, first] : {std::array{static_cast(Indices.size()), 0u, 0u}, std::array{0u, 1u, 0u}, std::array{3u, 1u, static_cast(Indices.size())}}) { + drawIndirect(count, instances, first); + for (std::uint32_t triangle = 0; triangle < Triangles; ++triangle) Require(PixelIs(CellPixel(triangle), Background), "an empty indirect record drew triangle " + std::to_string(triangle)); + } + ClearPixels(); DrawMesh(device, {SmallSubgroup, {0, static_cast(Ordered.size()), 0, 1, 0, false}, VertexBufferDescriptor(Ordered.data(), static_cast(Ordered.size())), 0}); CheckTriangles("mesh program without push data"); @@ -285,6 +319,13 @@ int main() { if (!refused) throw; } Require(refused, "a triangle fan with a restart index was drawn"); + const std::array fanWords{static_cast(FanIndices.size()), 1u, 0u, 0u, 0u}; + std::copy(fanWords.begin(), fanWords.end(), record); + AgcDriver::Pm4::DrawParameters fanDraw{reinterpret_cast(FanIndices.data()), static_cast(FanIndices.size()), 2, 1, 0, true}; + fanDraw.indirect = AgcDriver::Pm4::DrawParameters::IndirectDraw{reinterpret_cast(record), 0x25u, 20u, 20u, 1u, false, 0u, 0x280u, 0x280u, 0x280u, false, 0u}; + ClearPixels(); + DrawMesh(device, {fan, fanDraw, VertexBufferDescriptor(FanScrambled.data(), static_cast(FanScrambled.size()))}); + CheckFan("indirect indexed triangle fan"); std::puts("Mesh tests passed"); return 0; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp index 3346eceb6..5c9d1bc1d 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp @@ -78,6 +78,7 @@ enum class IrOpcode : std::uint16_t { ShaderClock, RealtimeClock, MeshDrawParameter, + MeshArgument, MeshAllocate, TessellationBase, GetTessellationAttribute, diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp index a2ae78222..7edb0ef10 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp @@ -119,6 +119,7 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("ShaderClock", U64), makeMeta("RealtimeClock", U64), makeMeta("MeshDrawParameter", U32, U32), + makeMeta("MeshArgument", U32, U32), 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 7041b0254..13d733e50 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -238,6 +238,10 @@ struct TessellationConfiguration { inline constexpr std::uint32_t MeshDrawPushOffsetBytes = 104; inline constexpr std::uint32_t MeshDrawPushBytes = 24; +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 MeshIndexBufferUserWord = 4; struct GraphicsDrawParameters { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp index e29e8c59a..ad70a7343 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp @@ -62,6 +62,7 @@ std::uint32_t EmitIdentity(SpirvValueEmitContext& ctx, std::uint32_t value); 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 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/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index c712ffca8..67b56daf9 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -16,6 +16,7 @@ bool UniformSource(const IrValue& value) { case IrOpcode::ReadConst: case IrOpcode::ReadConstBuffer: case IrOpcode::MeshDrawParameter: + case IrOpcode::MeshArgument: case IrOpcode::GetSrtResource: case IrOpcode::GetBufferResource: case IrOpcode::GetAddressResource: diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index 0dfa41ab3..5bb992ec5 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -252,6 +252,7 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::ShaderClock: return Invoke(EmitShaderClock, ctx, 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::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/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 9f6a14fa8..c7b0c1c01 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -161,14 +161,17 @@ void CheckBindings(const IrProgram& program, const BindingAllocationResult& bind void EmitBaseHeader(SpirvModule& module, const IrProgram& program) { module.EmitCapability(spv::CapabilityShader); - if (program.Info().usesDma) { + const bool physical = program.Info().usesDma || program.Resources().stage == IrShaderStage::Mesh; + if (physical) { module.EmitCapability(spv::CapabilityInt64); module.EmitCapability(spv::CapabilityPhysicalStorageBufferAddresses); - module.EmitCapability(spv::CapabilityStorageBuffer8BitAccess); module.EmitExtension("SPV_KHR_physical_storage_buffer"); + } + if (program.Info().usesDma) { + module.EmitCapability(spv::CapabilityStorageBuffer8BitAccess); module.EmitExtension("SPV_KHR_8bit_storage"); } - module.AddMemoryModel(program.Info().usesDma ? spv::AddressingModelPhysicalStorageBuffer64 : spv::AddressingModelLogical, spv::MemoryModelGLSL450); + module.AddMemoryModel(physical ? spv::AddressingModelPhysicalStorageBuffer64 : spv::AddressingModelLogical, spv::MemoryModelGLSL450); } void DefineInputs(SpirvEmitterState& state) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 5f97e4551..b85d7ee98 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -2,6 +2,7 @@ #include "SpirvBackend/SpirvEmitterHelpers.hpp" #include "SpirvBackend/SpirvEmitterInstructions.hpp" #include "SpirvBackend/SpirvFlowEmitter.hpp" +#include "SpirvBackend/SpirvBda.hpp" #include #include #include @@ -986,6 +987,41 @@ 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"); + } + 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)); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); + return Unary(state, spv::OpUConvert, TypeScalarU64(state), value); + }; + 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))); + const auto present = Binary(state, spv::OpINotEqual, TypeBool(state), address, BdaConstant(state, 0u)); + const auto before = state.currentLabel; + const auto loadLabel = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, present, loadLabel, merge); + EmitLabel(state, loadLabel); + const auto element = Binary(state, spv::OpIAdd, TypeScalarU64(state), address, BdaConstant(state, 4ull * index)); + 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); + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, merge); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpPhi, TypeU32(state), value, loaded, loadLabel, ConstantU32(state, 0u), before); + return value; +} + std::uint32_t EmitGetTessellationAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { return EmitValueOrZeroIfCondition(ctx.state, ctx.Arg(inst, 2), [&]() { const auto value = ctx.state.module.AllocateId(); diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 6cdf3f74a..b02afbaa4 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -252,6 +252,9 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO const auto draw = [&entryIr](std::uint32_t index) -> IrValue& { return entryIr.Emit(IrOpcode::MeshDrawParameter, IrOpcodeType(IrOpcode::MeshDrawParameter), {&entryIr.Constant(index)}); }; + const auto argument = [&entryIr](std::uint32_t index) -> IrValue& { + return entryIr.Emit(IrOpcode::MeshArgument, IrOpcodeType(IrOpcode::MeshArgument), {&entryIr.Constant(index)}); + }; const auto minimum = [&entryIr](IrValue& lhs, IrValue& rhs) -> IrValue& { return entryIr.Emit(IrOpcode::UMin32, IrOpcodeType(IrOpcode::UMin32), {&lhs, &rhs}); }; @@ -262,7 +265,10 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO IrValue& firstPrimitive = entryIr.IMul(builtin(StageInputKind::WorkgroupId, 0u), u32(mesh.primitivesPerGroup)); IrValue& step = u32(stepCount); IrValue& firstVertex = entryIr.IMul(firstPrimitive, step); - IrValue& vertices = minimum(subtractSaturate(draw(0u), firstVertex), u32(mesh.verticesPerGroup)); + IrValue& indirect = entryIr.INotEqual(entryIr.BitwiseOr(draw(MeshArgumentAddressDword), draw(MeshArgumentAddressDword + 1u)), u32(0u)); + IrValue& indexCount = entryIr.Select(indirect, argument(MeshArgumentIndexCountDword), draw(0u)); + IrValue& firstIndex = argument(MeshArgumentFirstIndexDword); + IrValue& vertices = minimum(subtractSaturate(indexCount, firstVertex), u32(mesh.verticesPerGroup)); IrValue& primitives = entryIr.Select(entryIr.ULessThan(vertices, u32(size)), u32(0u), entryIr.IAdd(entryIr.Emit(IrOpcode::UDiv32, IrOpcodeType(IrOpcode::UDiv32), {&subtractSaturate(vertices, u32(size)), &step}), u32(1u))); entryIr.SetScalarReg(static_cast(2), entryIr.BitwiseOr(entryIr.ShiftLeftLogical(vertices, u32(12u)), entryIr.ShiftLeftLogical(primitives, u32(22u)))); IrValue& wave = entryIr.ShiftRightLogical(local, u32(6u)); @@ -288,7 +294,7 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO 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& indexed = entryIr.INotEqual(indexBytes, u32(0u)); - IrValue& byteOffset = entryIr.IMul(inputVertex, indexBytes); + 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))}); const std::uint32_t memoryIndex = static_cast(program.Resources().memoryInfo.size()); program.Resources().memoryInfo.push_back(MemoryInfo{.kind = ResourceKind::Buffer, .resource = 1u, .offen = true});