From 33f9109999a9b29b92c9dafe4ffcd16fb9cd8616 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Tue, 29 Sep 2026 13:16:53 +1300 Subject: [PATCH] feat(shader): translate NGG geometry programs to mesh shaders The geometry path's merged ES/GS programs (VGT_SHADER_STAGES_EN.GS_EN) were compiled as mesh-stage programs whose input metadata was never filled in, so every such draw failed with "mesh shader translation configuration is not supported". Recompiler: - BuildShaderStageInputInfo takes the draw's MeshConfiguration; the mesh input helpers (primitive size, step and counts) are implemented. - The prologue emulates one NGG subgroup per workgroup: s2 GS_TG_INFO, s3 the merged wave info per wave, the GS vertex offsets (ES thread index times VGT_ESGS_RING_ITEMSIZE, odd strip triangles swapped), the primitive id, and the ES vertex and instance ids. An indexed draw's vertex id is read from the index buffer through a raw V# the driver puts in hidden user words 4..7. - s_sendmsg GS_ALLOC_REQ becomes MeshAllocate. The epilogue zeroes the allocation first, clamps it to the output limits, culls primitives no thread exported and decodes the 9-bit vertex indices. - The draw parameters live in the last six push dwords. A BDA fault in a mesh program no longer stops the invocation (as for other programs with barriers, the fault is recorded and the read returns zero), so every invocation reaches the subgroup barriers and the epilogue. - The cache keys cover the mesh configuration; requests serialize its new esgsItemSize field (version 4). Driver: - DecodeShaderStages reads VGT_ESGS_RING_ITEMSIZE (now in the draw key) and sizes the subgroup for its ES vertices, GS primitives and outputs. - Stages compile below the mesh draw parameters, which every mesh draw pushes; the mesh stage always takes part in the push range. - The index-buffer V# is set per draw (per record on the CPU-resolved indirect path); the index-buffer alias check spares read-only elements. - The workgroup and LocalSize checks count two guest lanes per invocation on 32-wide subgroups; mesh draws may carry a first vertex or instance. agc_driver_mesh_tests (EXCLUDE_FROM_ALL, needs VK_EXT_mesh_shader) recompiles a hand-assembled merged ES/GS program in the PS5 compiler's layout and draws indexed and non-indexed triangle lists in one- and two-wave subgroups and a triangle strip across odd-starting subgroups, reading the pixels back. agc_shader_replay prints a request's draw and mesh configuration. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 20 +- .../libSceAgcDriver/Execution/src/Driver.cpp | 19 +- .../libSceAgcDriver/Graphics/include/Draw.hpp | 2 + .../Graphics/include/Shaders.hpp | 2 +- .../Graphics/include/State.hpp | 5 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 40 ++- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 3 +- .../Graphics/src/ShaderResources.cpp | 4 +- .../Graphics/src/ShaderValidation.cpp | 4 +- .../libSceAgcDriver/Graphics/src/State.cpp | 5 +- .../prx/libSceAgcDriver/PostCompile.cmake | 6 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 5 + core/libs/prx/libSceAgcDriver/tests/Mesh.cpp | 233 ++++++++++++++++++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 36 +++ .../libSceAgcDriver/tools/ShaderReplay.cpp | 8 + core/shader/recompiler/CacheKey.hpp | 10 + .../ControlFlow/src/RequestSerializer.cpp | 14 +- core/shader/recompiler/Deserialize.cpp | 2 +- .../Optimization/ShaderStageInputInfo.hpp | 9 +- core/shader/recompiler/Recompiler.cpp | 19 +- core/shader/recompiler/Recompiler.hpp | 5 + .../SpirvBackend/src/SpirvBdaRead.cpp | 2 +- .../SpirvBackend/src/SpirvEmitter.cpp | 2 +- .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 2 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 30 ++- .../Translation/ShaderInputInfoBuilder.hpp | 2 +- .../src/ControlFlowInstructions.cpp | 7 +- .../Translation/src/InstructionTranslator.cpp | 55 +++-- .../src/ShaderInputInfoBuilder.cpp | 20 +- 29 files changed, 494 insertions(+), 77 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/Mesh.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 09793e98c..3989edc63 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -46,7 +46,7 @@ add_custom_command( VERBATIM ) -add_library(libSceAgcDriver SHARED EXCLUDE_FROM_ALL +set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/DisplayBuffer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver.cpp @@ -95,6 +95,10 @@ add_library(libSceAgcDriver SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Submit/src/Dcb.cpp ) +add_library(libSceAgcDriver SHARED EXCLUDE_FROM_ALL + ${agcDriverSources} +) + target_include_directories(libSceAgcDriver PRIVATE ${LIBS_INCLUDE_DIR} ${agcTextureDetileGeneratedRoot} @@ -303,6 +307,20 @@ target_link_libraries(agc_driver_bda_device_tests PRIVATE SDL2-static libc shade set_target_properties(agc_driver_bda_device_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_bda_device_tests) +add_executable(agc_driver_mesh_tests EXCLUDE_FROM_ALL + tests/Mesh.cpp + ${agcDriverSources} +) +target_include_directories(agc_driver_mesh_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${agcTextureDetileGeneratedRoot} + ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include + ${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Headers/include +) +target_link_libraries(agc_driver_mesh_tests PRIVATE shader_recompiler_agc_driver SDL2-static libc libkernel) +set_target_properties(agc_driver_mesh_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_driver_mesh_tests) + if(BUILD_TESTING) foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_dpp_write_tests agc_shader_disk_cache_tests agc_driver_flip_tests agc_driver_pm4_tests) string(REGEX REPLACE "_tests$" "" agcTestName "${agcTest}") diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp index e1c0f4904..65f93ed4d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp @@ -1426,7 +1426,7 @@ private: const auto sameMesh = [](const std::optional& x, const std::optional& y) { if (x.has_value() != y.has_value()) return false; if (!x) return true; - return x->inputPrimitive == y->inputPrimitive && x->primitivesPerGroup == y->primitivesPerGroup && x->verticesPerGroup == y->verticesPerGroup && x->maxVertices == y->maxVertices && x->maxPrimitives == y->maxPrimitives && x->threadsPerGroup == y->threadsPerGroup && x->ldsSizeDwords == y->ldsSizeDwords && x->provokingVertex == y->provokingVertex; + return x->inputPrimitive == y->inputPrimitive && x->primitivesPerGroup == y->primitivesPerGroup && x->verticesPerGroup == y->verticesPerGroup && x->maxVertices == y->maxVertices && x->maxPrimitives == y->maxPrimitives && x->threadsPerGroup == y->threadsPerGroup && x->ldsSizeDwords == y->ldsSizeDwords && x->provokingVertex == y->provokingVertex && x->esgsItemSize == y->esgsItemSize; }; const auto sameTess = [](const std::optional& x, const std::optional& y) { if (x.has_value() != y.has_value()) return false; @@ -4203,6 +4203,15 @@ private: const auto& graphics = decode->state; const auto& pixel = decode->pixel; std::vector programs = decode->programs; + const auto setMeshIndexBuffer = [&](const Pm4::DrawParameters& parameters) { + if (!graphics.stages.mesh) return; + auto& words = programs.front().userData; + require(programs.front().firstUserSgpr == 0 && words.size() >= ShaderRecompiler::MeshIndexBufferUserWord + 4, "mesh program lacks the hidden user words"); + const auto descriptor = Graphics::MeshIndexBufferDescriptor(parameters, programs.front().binary.codeAddress); + std::copy(descriptor.begin(), descriptor.end(), words.begin() + ShaderRecompiler::MeshIndexBufferUserWord); + }; + if (!drawParameters.indirect) setMeshIndexBuffer(drawParameters); + else if (graphics.stages.mesh) setMeshIndexBuffer(Pm4::DrawParameters{drawParameters.indexAddress, std::max(drawParameters.indexCount, 1u), drawParameters.indexSize, 1, 0, drawParameters.indexed}); const std::vector& roles = decode->roles; phase(DrawRowDecode); // The user word an SH register named by an indirect draw packet lands in: the program whose @@ -4297,7 +4306,7 @@ private: mix(graphics.stages.mesh.has_value()); if (graphics.stages.mesh) { const auto& mesh = *graphics.stages.mesh; - for (const auto value : {mesh.inputPrimitive, mesh.primitivesPerGroup, mesh.verticesPerGroup, mesh.maxVertices, mesh.maxPrimitives, mesh.threadsPerGroup, mesh.ldsSizeDwords, mesh.provokingVertex}) mix(value); + for (const auto value : {mesh.inputPrimitive, mesh.primitivesPerGroup, mesh.verticesPerGroup, mesh.maxVertices, mesh.maxPrimitives, mesh.threadsPerGroup, mesh.ldsSizeDwords, mesh.provokingVertex, mesh.esgsItemSize}) mix(value); } mix(graphics.stages.tessellation.has_value()); if (graphics.stages.tessellation) { @@ -4483,7 +4492,7 @@ private: program.binary, {waveSize, program.firstUserSgpr, program.userData, std::nullopt, program.binary.stage == Stage::Fragment ? std::optional(pixel) : std::nullopt, vertexInfos[i], memory}, localDevice->Target(), - {0, 0, pushOffset, Graphics::PipelinePushConstantBytes - pushOffset}, + {0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : Graphics::PipelinePushConstantBytes) - pushOffset}, ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, graphics.stages.mesh, graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}} }; const auto waitedBefore = TraceCapSync() || profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0; @@ -4810,6 +4819,10 @@ private: } else { direct.firstVertex = indirect.indxOffset; } + if (graphics.stages.mesh) { + setMeshIndexBuffer(direct); + patched.insert(0); + } for (const auto programIndex : patched) { auto& result = results[resultIndex[programIndex]]; const auto pushBytes = result.pushConstants.size(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp index 63724bec1..531d44eee 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp @@ -14,6 +14,8 @@ namespace AgcDriver::Graphics { // APS5_NO_DRAW_RECIPE=1). void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots = {}, std::shared_ptr* recipe = nullptr); +std::array MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw, std::uint64_t unreadAddress); + // 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/Shaders.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Shaders.hpp index 00d19fc87..307dbde3c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Shaders.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Shaders.hpp @@ -49,7 +49,7 @@ inline VkShaderStageFlags PushConstantStages(std::span sha VkShaderStageFlags result = 0; for (const auto& shader : shaders) { Require(shader.program != nullptr, "missing compiled shader"); - if (!shader.program->pushConstants.empty()) result |= VulkanStage(shader.stage); + if (!shader.program->pushConstants.empty() || shader.stage == ShaderRecompiler::ShaderStage::Mesh) result |= VulkanStage(shader.stage); } return result; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index dffd822f2..5c17647f1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -122,16 +122,15 @@ struct DrawKeyRange { std::uint32_t first; std::uint32_t count; }; -inline constexpr std::array DrawKeyRegisters{{ +inline constexpr std::array DrawKeyRegisters{{ {RegisterBank::Context, 0x000, 1}, {RegisterBank::Context, 0x002, 1}, {RegisterBank::Context, 0x007, 1}, {RegisterBank::Context, 0x00a, 4}, {RegisterBank::Context, 0x010, 6}, {RegisterBank::Context, 0x01a, 4}, {RegisterBank::Context, 0x080, 4}, {RegisterBank::Context, 0x08c, 4}, {RegisterBank::Context, 0x090, 2}, {RegisterBank::Context, 0x094, 2}, {RegisterBank::Context, 0x0b4, 2}, {RegisterBank::Context, 0x105, 4}, {RegisterBank::Context, 0x10b, 3}, {RegisterBank::Context, 0x10f, 6}, // SPI_PS_INPUT_CNTL_0..31, SPI_PS_INPUT_ENA/ADDR, SPI_PS_IN_CONTROL, SPI_SHADER_POS/Z/COL_FORMAT, // CB_BLEND0..7_CONTROL, GE_MAX_OUTPUT_PER_SUBGROUP. {RegisterBank::Context, 0x191, 32}, {RegisterBank::Context, 0x1b3, 2}, {RegisterBank::Context, 0x1b6, 1}, {RegisterBank::Context, 0x1c3, 3}, {RegisterBank::Context, 0x1e0, 8}, {RegisterBank::Context, 0x1ff, 1}, // DB_DEPTH_CONTROL .. PA_CL_VS_OUT_CNTL, PA_SC_MODE_CNTL_0/1, VGT_GS_MODE, VGT_GS_VERT_ITEMSIZE, - // VGT_SHADER_STAGES_EN/GS_ONCHIP, VGT_TF_PARAM/DB_ALPHA_TO_MASK, PA_SC_AA_CONFIG and // PA_SU_VTX_CNTL, the sample masks, PA_SC_CONSERVATIVE_RASTERIZATION_CNTL. - {RegisterBank::Context, 0x200, 8}, {RegisterBank::Context, 0x292, 2}, {RegisterBank::Context, 0x29b, 1}, {RegisterBank::Context, 0x2ce, 1}, {RegisterBank::Context, 0x2d5, 2}, {RegisterBank::Context, 0x2db, 2}, {RegisterBank::Context, 0x2f8, 2}, {RegisterBank::Context, 0x30e, 2}, {RegisterBank::Context, 0x313, 1}, + {RegisterBank::Context, 0x200, 8}, {RegisterBank::Context, 0x292, 2}, {RegisterBank::Context, 0x29b, 1}, {RegisterBank::Context, 0x2ab, 1}, {RegisterBank::Context, 0x2ce, 1}, {RegisterBank::Context, 0x2d5, 2}, {RegisterBank::Context, 0x2db, 2}, {RegisterBank::Context, 0x2f8, 2}, {RegisterBank::Context, 0x30e, 2}, {RegisterBank::Context, 0x313, 1}, // CB_COLOR0..7_BASE .. DCC_BASE (15 words a slot), CB_COLOR0..7_BASE_EXT, DCC_BASE_EXT, ATTRIB2, ATTRIB3. {RegisterBank::Context, 0x318, 0x78}, {RegisterBank::Context, 0x390, 8}, {RegisterBank::Context, 0x3a8, 0x18}, // The pixel program address, RSRC2 and user words; the geometry-back user pointer and program diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 8b46b0812..88b37d143 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -503,8 +503,10 @@ void CheckBufferAliases(std::span shaders, const ColorTarg const auto address = descriptor.Base48(); const auto size = descriptor.GetSize(); if (size == 0 || address == 0) continue; + const auto element = offset / 4; + const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; Require(!overlap(address, size, target.address, target.bytes), "shader buffer aliases the render target"); - Require(!overlap(address, size, indexAddress, indexBytes), "writable shader buffer aliases the index buffer"); + Require(!written || !overlap(address, size, indexAddress, indexBytes), "writable shader buffer aliases the index buffer"); } } } @@ -512,6 +514,14 @@ void CheckBufferAliases(std::span shaders, const ColorTarg } +std::array MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw, std::uint64_t unreadAddress) { + const auto address = draw.indexed ? draw.indexAddress : unreadAddress; + const auto bytes = draw.indexed ? (static_cast(draw.indexCount) * draw.indexSize + 3u) & ~std::uint64_t{3} : 4u; + Require(address != 0 && bytes != 0 && bytes <= 0xffffffffu && (address >> 48u) == 0, "invalid mesh index buffer range"); + constexpr std::uint32_t RawWord3 = 0x31016facu; + return {static_cast(address), static_cast(address >> 32u) & 0xffffu, static_cast(bytes), RawWord3}; +} + 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>>()); @@ -655,7 +665,6 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P APS5_LOG_CHARS_OUT_DEBUG("ValidateShaders OK"); APS5_LOG_OUT_DEBUG("PipelineStages=0x%x", static_cast(inputs.shaderStages)); if (state.stages.mesh) { - Require(draw.firstVertex == 0 && draw.firstInstance == 0, "mesh draw offsets are unsupported"); APS5_LOG_CHARS_OUT_DEBUG("Mesh path"); Require(context.meshShader, "device does not support mesh shaders"); const auto& mesh = *state.stages.mesh; @@ -995,6 +1004,22 @@ 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) { + auto block = bytes != nullptr ? *bytes : AssemblePushConstants(shaders); + if (bytes == nullptr) { + resources.PatchPushConstants(block); + stages = PushConstantStages(shaders); + } + if (!state.stages.mesh) { + pipeline.PushConstants(commands, stages, block); + return; + } + const std::array words{draw.indexCount, draw.firstVertex, draw.firstInstance, draw.indexed ? draw.indexSize : 0u, 0u, 0u}; + 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); +} + // The record of a draw whose targets are all resident (`lean`: recorded into the device's open // batch, rendering in the general layout): one memory barrier before the pass (earlier writes to // the attachments and the draw's inputs visible to it) and one after it, recorded by the recorder @@ -1156,12 +1181,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"); - if (record.pushBytes != nullptr) record.pipeline->PushConstants(commands, record.pushStages, *record.pushBytes); - else { - auto bytes = AssemblePushConstants(shaders); - resources.PatchPushConstants(bytes); - record.pipeline->PushConstants(commands, PushConstantStages(shaders), bytes); - } + pushDrawConstants(*record.pipeline, commands, state, draw, shaders, resources, record.pushBytes, record.pushStages); APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded"); recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset); if (args != nullptr) CountIndirectDraw(record.indirect->path, record.indirect->readMs, rewritten); @@ -1580,9 +1600,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"); - auto pushBytes = AssemblePushConstants(shaders); - resources->PatchPushConstants(pushBytes); - pipeline->PushConstants(commands, PushConstantStages(shaders), pushBytes); + pushDrawConstants(*pipeline, commands, state, draw, shaders, *resources, nullptr, 0); APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded"); recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset); if (args != nullptr) CountIndirectDraw(indirect.path, indirect.readMs, rewritten); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 8481a4e0e..36772cf21 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -57,7 +57,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput if (state.stages.mesh) { Require(context.meshShader, "device does not support VK_EXT_mesh_shader"); const auto& mesh = *state.stages.mesh; - Require(mesh.threadsPerGroup <= context.meshLimits.maxMeshWorkGroupInvocations && mesh.threadsPerGroup <= context.meshLimits.maxMeshWorkGroupSize[0], "mesh workgroup exceeds device limits"); + const auto invocations = state.stages.vertexWaveSize == 64u && context.subgroup.subgroupSize == 32u ? mesh.threadsPerGroup / 2u : mesh.threadsPerGroup; + Require(invocations <= context.meshLimits.maxMeshWorkGroupInvocations && invocations <= context.meshLimits.maxMeshWorkGroupSize[0], "mesh workgroup exceeds device limits"); Require(mesh.maxVertices <= context.meshLimits.maxMeshOutputVertices && mesh.maxPrimitives <= context.meshLimits.maxMeshOutputPrimitives && static_cast(mesh.ldsSizeDwords) * 4 <= context.meshLimits.maxMeshSharedMemorySize, "mesh output or LDS exceeds device limits"); } const auto pushStages = PushConstantStages(shaders); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 95e471b7a..2fd5ee157 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2034,10 +2034,8 @@ std::size_t ShaderResources::addGuestBuffer(std::span words Require(target == nullptr || !overlap(address, size, target->address, target->bytes), "shader buffer aliases the render target"); // APS5_ALL_BUFFERS_WRITTEN=1: every element is noted as written, as before bufferWritten existed. static const bool allWritten = std::getenv("APS5_ALL_BUFFERS_WRITTEN") != nullptr; + Require(!written || !overlap(address, size, indexAddress, indexBytes), "writable shader buffer aliases the index buffer"); written = written || allWritten; - // Unconditional (also for read-only elements): Draw.cpp's CheckBufferAliases repeats this check - // on every resource-cache hit, and a draw must fail its hit exactly when it fails its build. - Require(!overlap(address, size, indexAddress, indexBytes), "writable shader buffer aliases the index buffer"); if (written) guestMemory.AddWritable(address, size, atomic); else { guestMemory.AddReadable(address, size); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 502b00981..76b3f374d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -349,7 +349,9 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi if (mesh) { Require(state.stages.mesh.has_value(), "mesh configuration is missing"); const auto& config = *state.stages.mesh; - mode(spv::ExecutionModeLocalSize, {config.threadsPerGroup, 1, 1}); + const auto local = module.modes.find(spv::ExecutionModeLocalSize); + const bool paired = state.stages.vertexWaveSize == 64u && subgroup.subgroupSize == 32u && local != module.modes.end() && local->second == std::vector{config.threadsPerGroup / 2u, 1, 1}; + mode(spv::ExecutionModeLocalSize, {paired ? config.threadsPerGroup / 2u : config.threadsPerGroup, 1, 1}); mode(spv::ExecutionModeOutputVertices, {config.maxVertices}); mode(spv::ExecutionModeOutputPrimitivesEXT, {config.maxPrimitives}); mode(spv::ExecutionModeOutputTrianglesEXT, {}); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 0e7e3e92f..cf340973a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -363,7 +363,10 @@ ShaderStages DecodeShaderStages(const QueueState& queue) { validate(groupPrimitives != 0, "geometry subgroup contains no primitives"); const auto resources = read(queue.shader, 0x8b, RegisterBank::Shader); validate(((read(queue.shader, 0x8a, RegisterBank::Shader) >> 29u) & 3u) == 3 && ((resources >> 16u) & 3u) == 3, "unsupported geometry VGPR allocation"); - result.mesh = ShaderRecompiler::MeshConfiguration{primitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, primitives * (verticesPerPrimitive - 2u), ((maxVertices + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, 0}; + const auto esgsItemSize = read(queue.context, 0x2ab); + validate(esgsItemSize != 0 && esgsItemSize * vertices <= 0xffffu, "invalid VGT_ESGS_RING_ITEMSIZE"); + const auto threads = std::max({(groupPrimitives - 1u) * inputStep + inputSize, primitives, maxVertices, primitives * (verticesPerPrimitive - 2u)}); + result.mesh = ShaderRecompiler::MeshConfiguration{primitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, primitives * (verticesPerPrimitive - 2u), ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, 0, esgsItemSize}; } return result; } diff --git a/core/libs/prx/libSceAgcDriver/PostCompile.cmake b/core/libs/prx/libSceAgcDriver/PostCompile.cmake index b48b71293..3cabf5ccd 100644 --- a/core/libs/prx/libSceAgcDriver/PostCompile.cmake +++ b/core/libs/prx/libSceAgcDriver/PostCompile.cmake @@ -10,19 +10,19 @@ add_custom_command( VERBATIM ) -foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_bda_device_tests) +foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests) if(TARGET ${agcTarget}) target_sources(${agcTarget} PRIVATE Graphics/src/BufferPool.cpp) endif() endforeach() -foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests) +foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_mesh_tests) if(TARGET ${agcTarget}) target_sources(${agcTarget} PRIVATE Graphics/src/TextureDetilerDescriptors.cpp Graphics/src/TextureCache.cpp) endif() endforeach() -foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_bda_device_tests) +foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests) if(TARGET ${agcTarget}) target_sources(${agcTarget} PRIVATE Graphics/src/GpuColorTransfer.cpp ${agcColorTransferHeader}) endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 77359edc7..30d18bddb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -188,10 +188,15 @@ void ShaderStageTests() { queue.context[0x1ff] = 64; queue.context[0x2ce] = 3; queue.context[0x29b] = 2; + queue.context[0x2ab] = 4; queue.shader[0x8a] = 3u << 29u; queue.shader[0x8b] = 3u << 16u; auto stages = AgcDriver::Graphics::DecodeState(queue).stages; Require(stages.path == AgcDriver::Graphics::ShaderPath::Geometry && stages.mesh && stages.mesh->primitivesPerGroup == 21 && stages.mesh->verticesPerGroup == 63, "geometry assembly changed"); + Require(stages.mesh->maxVertices == 64 && stages.mesh->maxPrimitives == 21 && stages.mesh->threadsPerGroup == 64 && stages.mesh->esgsItemSize == 4, "geometry subgroup outputs changed"); + queue.context[0x2ab] = 0; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "invalid VGT_ESGS_RING_ITEMSIZE"); + queue.context[0x2ab] = 4; queue.userConfig[0x25b] = 0; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "invalid geometry subgroup"); queue = makeState(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp new file mode 100644 index 000000000..129df2015 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp @@ -0,0 +1,233 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 192; +constexpr std::uint32_t Height = 128; +constexpr std::array Background{16, 24, 40, 255}; +alignas(256) std::array Pixels{}; + +alignas(256) constexpr std::array GeometryCode{ + 0x8f6a9003, 0x94fe6ac1, 0xbf88000b, 0xd7650006, 0x000100c1, 0xd7660006, 0x00020cc1, 0x93ebff03, + 0x00040018, 0xd7460006, 0x04190c6b, 0x340c0c82, 0xd8340000, 0x00000506, 0xbf8cc07f, 0xbefe04c1, + 0xbf8a0000, 0x938dff02, 0x00090016, 0x9382ff03, 0x00040018, 0x938cff03, 0x00080008, 0xd7650009, + 0x000100c1, 0xd7660009, 0x000212c1, 0xd746000a, 0x04250c02, 0x7da8120c, 0xbf88002e, 0x361600ff, + 0x0000ffff, 0x2c180090, 0x361a02ff, 0x0000ffff, 0xd8d80000, 0x0b00000b, 0xd8d80000, 0x0c00000c, + 0xd8d80000, 0x0d00000d, 0xbf8cc07f, 0xe0382000, 0x8002100b, 0xe0382010, 0x8002140b, 0xe0382000, + 0x8002180c, 0xe0382010, 0x80021c0c, 0xe0382000, 0x8002200d, 0xe0382010, 0x8002240d, 0x161c1483, + 0x161e14ff, 0x00000060, 0xbf8c3f70, 0xdb7c0400, 0x0000100f, 0xdb7c0410, 0x0000140f, 0xdb7c0420, + 0x0000180f, 0xdb7c0430, 0x00001c0f, 0xdb7c0440, 0x0000200f, 0xdb7c0450, 0x0000240f, 0x4a501c81, + 0x4a521c82, 0x3450508a, 0x34525294, 0xd772002a, 0x04a6510e, 0xbf8cc07f, 0xbefe04c1, 0xbf8a0000, + 0x930e830d, 0xbf078002, 0xbf850003, 0x8f0f8c0d, 0x887c0f0e, 0xbf900009, 0x7da8140d, 0xbf880002, + 0xf8000941, 0x0000002a, 0xbefe04c1, 0x7da8140e, 0xbf88000a, 0x34561485, 0xdbfc0400, 0x2c00002b, + 0xdbfc0410, 0x3000002b, 0xbf8cc07f, 0xf80008cf, 0x2f2e2d2c, 0xf800020f, 0x33323130, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0xc8020002, 0xc8060102, 0xc80a0202, 0xc80e0302, 0xf800180f, 0x03020100, 0xbf810000, +}; + +struct Vertex { + std::array position; + std::array color; +}; + +constexpr std::uint32_t Columns = 8; +constexpr std::uint32_t Rows = 5; +constexpr std::uint32_t Triangles = Columns * Rows; + +std::array TriangleColor(std::uint32_t triangle) { + return {static_cast(40u + 29u * triangle), static_cast(200u + 53u * triangle), static_cast(triangle % 2u == 0u ? 60u : 200u), 255}; +} + +std::array TriangleVertices(std::uint32_t triangle) { + const float cellWidth = 2.0f / Columns; + const float cellHeight = 2.0f / Rows; + const float x = -1.0f + cellWidth * static_cast(triangle % Columns); + const float y = -1.0f + cellHeight * static_cast(triangle / Columns); + const auto color = TriangleColor(triangle); + const std::array rgba{color[0] / 255.0f, color[1] / 255.0f, color[2] / 255.0f, 1.0f}; + return {{ + {{x + 0.1f * cellWidth, y + 0.1f * cellHeight, 0.5f, 1.0f}, rgba}, + {{x + 0.9f * cellWidth, y + 0.1f * cellHeight, 0.5f, 1.0f}, rgba}, + {{x + 0.5f * cellWidth, y + 0.9f * cellHeight, 0.5f, 1.0f}, rgba}, + }}; +} + +std::size_t CellPixel(std::uint32_t triangle) { + const auto column = triangle % Columns; + const auto row = triangle / Columns; + const auto x = (column * Width + Width / 2u) / Columns; + const auto y = Height - 1u - (row * Height + (Height * 4u) / 10u) / Rows; + return (static_cast(y) * Width + x) * 4u; +} + +alignas(256) std::array Scrambled{}; +alignas(256) std::array Indices{}; +alignas(256) std::array Ordered{}; +alignas(256) std::array Strip{}; + +std::array VertexBufferDescriptor(const void* vertices, std::uint32_t count) { + const auto address = reinterpret_cast(vertices); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (32u << 16u), count, 0x01016facu}; +} + +void ClearPixels() { + for (std::size_t i = 0; i < Pixels.size(); i += 4) { + for (std::size_t c = 0; c < 4; ++c) Pixels[i + c] = std::byte{Background[c]}; + } +} + +bool PixelIs(std::size_t offset, const std::array& color) { + for (std::size_t c = 0; c < 4; ++c) { + if (std::to_integer(Pixels[offset + c]) != color[c]) return false; + } + return true; +} + +std::string PixelText(std::size_t offset) { + char text[64]; + std::snprintf(text, sizeof(text), "(%u, %u, %u, %u)", std::to_integer(Pixels[offset]), std::to_integer(Pixels[offset + 1]), std::to_integer(Pixels[offset + 2]), std::to_integer(Pixels[offset + 3])); + return text; +} + +struct MeshDraw { + ShaderRecompiler::MeshConfiguration mesh; + AgcDriver::Pm4::DrawParameters draw; + std::array vertexBuffer; +}; + +void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { + const auto target = device.Target(); + std::vector userData(12, 0u); + const auto index = AgcDriver::Graphics::MeshIndexBufferDescriptor(setup.draw, reinterpret_cast(GeometryCode.data())); + std::copy(index.begin(), index.end(), userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord); + std::copy(setup.vertexBuffer.begin(), setup.vertexBuffer.end(), userData.begin() + 8); + const std::array geometryMemory{{{reinterpret_cast(GeometryCode.data()), std::as_bytes(std::span(GeometryCode))}}}; + ShaderRecompiler::RecompileRequest geometry{ + {ShaderStage::Mesh, reinterpret_cast(GeometryCode.data()), GeometryCode, 0, {}}, + {64, 0, userData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, geometryMemory}, + target, + {0, 0, 0, ShaderRecompiler::MeshDrawPushOffsetBytes}, + ShaderRecompiler::GraphicsCompileContext{0, {}, setup.mesh, std::nullopt, {setup.draw.indexAddress, setup.draw.indexCount, setup.draw.indexSize, setup.draw.instanceCount}} + }; + const auto meshResult = ShaderRecompiler::Recompile(geometry); + const auto meshPush = static_cast(meshResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.interpolatorCount = 1; + pixel.interpolatorSettings[0] = 0x400u; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, + {64, 0, {}, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + {0, 0, meshPush, ShaderRecompiler::MeshDrawPushOffsetBytes - meshPush}, + std::nullopt + }; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Mesh, &meshResult, 0}, + {ShaderStage::Fragment, &pixelResult, meshPush} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, 0x20u, 64, 64, setup.mesh, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = setup.mesh.inputPrimitive == 6u ? VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP : VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + 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.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + device.Draw(state, setup.draw, shaders); + device.WaitIdle(); +} + +constexpr ShaderRecompiler::MeshConfiguration SmallSubgroup{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}; +constexpr ShaderRecompiler::MeshConfiguration WideSubgroup{4u, 32u, 96u, 96u, 32u, 128u, 2048u, 0u, 4u}; + +void CheckTriangles(const char* what) { + for (std::uint32_t triangle = 0; triangle < Triangles; ++triangle) { + const auto offset = CellPixel(triangle); + Require(PixelIs(offset, TriangleColor(triangle)), std::string(what) + ": triangle " + std::to_string(triangle) + " was not rendered in its color, pixel " + PixelText(offset)); + } + Require(PixelIs(0, Background) && PixelIs(Pixels.size() - 4u, Background), std::string(what) + ": the corners changed"); +} + +} + +int main() { + try { + setenv("APS5_NO_SHADER_DISK_CACHE", "1", 1); + for (std::uint32_t triangle = 0; triangle < Triangles; ++triangle) { + const auto vertices = TriangleVertices(triangle); + for (std::uint32_t k = 0; k < 3; ++k) { + Ordered[3 * triangle + k] = vertices[k]; + const auto slot = 3 * (Triangles - 1 - triangle) + (k + 1) % 3; + Scrambled[slot] = vertices[k]; + Indices[3 * triangle + k] = static_cast(slot); + } + } + const std::array stripColor{128.0f / 255.0f, 1.0f, 64.0f / 255.0f, 1.0f}; + for (std::uint32_t i = 0; i < Strip.size(); ++i) { + const float x = -1.0f + 2.0f * static_cast(i / 2u) / Columns; + const float y = i % 2u == 0u ? -1.0f : 0.0f; + Strip[i] = {{x, y, 0.5f, 1.0f}, stripColor}; + } + + AgcDriver::VulkanDevice device; + const auto target = device.Target(); + if (!target.mesh.has_value()) { + std::puts("Mesh tests skipped: the device has no VK_EXT_mesh_shader"); + return 0; + } + + for (const auto& [name, subgroup] : {std::pair{"one-wave subgroups", SmallSubgroup}, std::pair{"two-wave subgroups", WideSubgroup}}) { + ClearPixels(); + DrawMesh(device, {subgroup, {reinterpret_cast(Indices.data()), static_cast(Indices.size()), 2, 1, 0, true}, VertexBufferDescriptor(Scrambled.data(), static_cast(Scrambled.size()))}); + CheckTriangles((std::string("indexed triangle list, ") + name).c_str()); + + ClearPixels(); + DrawMesh(device, {subgroup, {0, static_cast(Ordered.size()), 0, 1, 0, false}, VertexBufferDescriptor(Ordered.data(), static_cast(Ordered.size()))}); + CheckTriangles((std::string("non-indexed triangle list, ") + name).c_str()); + } + + ClearPixels(); + const ShaderRecompiler::MeshConfiguration strip{6u, 3u, 5u, 9u, 3u, 64u, 1024u, 0u, 4u}; + DrawMesh(device, {strip, {0, static_cast(Strip.size()), 0, 1, 0, false}, VertexBufferDescriptor(Strip.data(), static_cast(Strip.size()))}); + const std::array stripPixel{128, 255, 64, 255}; + for (std::uint32_t y = Height / 2u + 2u; y < Height - 2u; y += 5u) { + for (std::uint32_t x = 2u; x < Width - 2u; x += 5u) { + const auto offset = (static_cast(y) * Width + x) * 4u; + Require(PixelIs(offset, stripPixel), "triangle strip: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is " + PixelText(offset)); + } + } + Require(PixelIs((static_cast(Height / 4u) * Width + Width / 2u) * 4u, Background), "triangle strip: the upper half changed"); + + std::puts("Mesh tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 5067f59d9..720621c6a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3,10 +3,12 @@ #include "Optimization/RequestMemoryView.hpp" #include "Optimization/ResourceMaterializer.hpp" #include "Optimization/ResourceProgram.hpp" +#include "Optimization/ShaderStageInputInfo.hpp" #include "Optimization/SrtWalker/SrtFlatSlotClasses.hpp" #if ANYPS5_ENABLE_SPIRV_TOOLS #include "SpirvBackend/SpirvOptimizer.hpp" #endif +#include "CacheKey.hpp" #include #include #include @@ -405,6 +407,39 @@ void verifyProgramCounterRelativeData() { require(moved.cacheHit, "program counter data: relocating the shader recompiled it"); require(dataBase(moved) == codeAddress + 0x1000u + 56u, "program counter data: the relocated shader bound the old address"); } +void verifyMeshConfiguration() { + using namespace ShaderRecompiler; + ShaderMeshInputInfo list; + list.inputPrimitive = 4u; + require(list.InputPrimitiveSize() == 3u && list.InputPrimitiveStep() == 3u && list.InputVertexCount(21u) == 63u && list.InputPrimitiveCount(63u) == 21u && list.InputPrimitiveCount(2u) == 0u && list.InputVertexCount(0u) == 0u, "triangle list subgroup sizes changed"); + ShaderMeshInputInfo strip; + strip.inputPrimitive = 6u; + require(strip.InputPrimitiveSize() == 3u && strip.InputPrimitiveStep() == 1u && strip.InputVertexCount(21u) == 23u && strip.InputPrimitiveCount(23u) == 21u, "triangle strip subgroup sizes changed"); + ShaderMeshInputInfo lines; + lines.inputPrimitive = 2u; + ShaderMeshInputInfo points; + points.inputPrimitive = 1u; + require(lines.InputPrimitiveSize() == 2u && lines.InputPrimitiveStep() == 2u && points.InputPrimitiveSize() == 1u && points.InputVertexCount(5u) == 5u, "line or point subgroup sizes changed"); + + static constexpr std::array code{0xbf810000u}; + RecompileRequest request{}; + request.shader = {ShaderStage::Mesh, 0x10000u, code, 0, {}}; + request.context.waveSize = 64; + const MeshConfiguration mesh{4u, 21u, 63u, 64u, 21u, 64u, 256u, 0u, 12u}; + request.graphics = GraphicsCompileContext{0u, {}, mesh, std::nullopt, {}}; + const auto replay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(request)); + require(replay.request.graphics.has_value() && replay.request.graphics->mesh.has_value() && replay.request.graphics->mesh->esgsItemSize == 12u && replay.request.graphics->mesh->primitivesPerGroup == 21u, "mesh configuration was lost in serialization"); + std::vector key; + RecompileCacheKey::Build(request, key); + const auto first = key; + auto other = request; + auto otherMesh = mesh; + otherMesh.esgsItemSize = 16u; + other.graphics = GraphicsCompileContext{0u, {}, otherMesh, std::nullopt, {}}; + RecompileCacheKey::Build(other, key); + require(key != first && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(other), "the cache keys ignore the mesh configuration"); +} + } int main() { @@ -414,6 +449,7 @@ int main() { verifyPureFlatSlots(); verifyBindlessTable(); verifyProgramCounterRelativeData(); + verifyMeshConfiguration(); #if ANYPS5_ENABLE_SPIRV_TOOLS const std::vector minimalSpirv{ 0x07230203u, 0x00010000u, 0u, 5u, 0u, diff --git a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp index dd73918e3..de5b97b7a 100644 --- a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp @@ -48,6 +48,14 @@ bool Replay(const char* path) { std::printf(" compute: threads %ux%ux%u, lds %u dwords, group ids %d%d%d, tg size %d, thread id components %u\n", compute.numThreads[0], compute.numThreads[1], compute.numThreads[2], compute.ldsSizeDwords, compute.groupIdEnable[0], compute.groupIdEnable[1], compute.groupIdEnable[2], compute.tgSizeEnable, compute.threadIdComponentCount); if (compute.PartialGroups()) std::printf(" partial groups: dispatch of %ux%ux%u threads\n", compute.partialThreads[0], compute.partialThreads[1], compute.partialThreads[2]); } + if (request.request.graphics.has_value()) { + const auto& graphics = *request.request.graphics; + std::printf(" graphics: %zu linked programs, draw %u indices of %u bytes at 0x%llx, %u instances\n", graphics.linkedPrograms.size(), graphics.draw.indexCount, graphics.draw.indexElementBytes, static_cast(graphics.draw.indexAddress), graphics.draw.instanceCount); + if (graphics.mesh.has_value()) { + const auto& mesh = *graphics.mesh; + std::printf(" mesh: input primitive %u, %u primitives / %u vertices per group, max %u vertices / %u primitives, %u threads, lds %u dwords, provoking %u, ESGS item %u\n", mesh.inputPrimitive, mesh.primitivesPerGroup, mesh.verticesPerGroup, mesh.maxVertices, mesh.maxPrimitives, mesh.threadsPerGroup, mesh.ldsSizeDwords, mesh.provokingVertex, mesh.esgsItemSize); + } + } if (g_memory) { // --mem: the captured inputs. User data words are printed; each memory region is written to // mem__.bin next to the request for inspection with other tools. diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index bf86cb817..fc574d67c 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -30,6 +30,7 @@ public: append(key, request.context.compute); append(key, request.context.pixel); append(key, request.context.vertex); + appendMesh(key, request); append(key, request.target); append(key, DebugProbeActive()); } @@ -48,6 +49,7 @@ public: append(key, request.context.compute); append(key, request.context.pixel); append(key, request.context.vertex); + appendMesh(key, request); std::uint64_t hash = 0xcbf29ce484222325ull; for (const auto value : key) { hash ^= value; @@ -70,6 +72,14 @@ public: } private: + static void appendMesh(std::vector& key, const RecompileRequest& request) { + if (request.shader.stage != ShaderStage::Mesh) return; + const auto* mesh = request.graphics && request.graphics->mesh ? &*request.graphics->mesh : nullptr; + append(key, mesh != nullptr); + if (mesh == nullptr) return; + for (const auto value : {mesh->inputPrimitive, mesh->primitivesPerGroup, mesh->verticesPerGroup, mesh->maxVertices, mesh->maxPrimitives, mesh->threadsPerGroup, mesh->ldsSizeDwords, mesh->provokingVertex, mesh->esgsItemSize}) append(key, value); + } + template static void append(std::vector& key, TValue value) requires (std::is_integral_v || std::is_enum_v) { key.push_back(static_cast(value)); diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 0588b5da7..2a4ee6295 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -426,9 +426,10 @@ void writeMeshConfiguration(Writer& writer, const MeshConfiguration& configurati writer.WriteU32(configuration.threadsPerGroup); writer.WriteU32(configuration.ldsSizeDwords); writer.WriteU32(configuration.provokingVertex); + writer.WriteU32(configuration.esgsItemSize); } -MeshConfiguration readMeshConfiguration(Reader& reader) { +MeshConfiguration readMeshConfiguration(Reader& reader, std::uint32_t version) { MeshConfiguration configuration{}; configuration.inputPrimitive = reader.ReadU32(); configuration.primitivesPerGroup = reader.ReadU32(); @@ -438,6 +439,7 @@ MeshConfiguration readMeshConfiguration(Reader& reader) { configuration.threadsPerGroup = reader.ReadU32(); configuration.ldsSizeDwords = reader.ReadU32(); configuration.provokingVertex = reader.ReadU32(); + configuration.esgsItemSize = version >= 4u ? reader.ReadU32() : 4u; return configuration; } @@ -615,7 +617,7 @@ void writeGraphicsCompileContext(Writer& writer, const GraphicsCompileContext& g writer.WriteU32(graphics.draw.instanceCount); } -GraphicsCompileContext readGraphicsCompileContext(Reader& reader, DeserializedGraphicsCompileContext& storage) { +GraphicsCompileContext readGraphicsCompileContext(Reader& reader, DeserializedGraphicsCompileContext& storage, std::uint32_t version) { GraphicsCompileContext graphics{}; graphics.firstUserSgpr = reader.ReadU32(); const auto programCount = reader.ReadU64(); @@ -639,7 +641,7 @@ GraphicsCompileContext readGraphicsCompileContext(Reader& reader, DeserializedGr } graphics.linkedPrograms = storage.linkedPrograms; if (reader.ReadBool()) { - storage.mesh = readMeshConfiguration(reader); + storage.mesh = readMeshConfiguration(reader, version); graphics.mesh = storage.mesh; } if (reader.ReadBool()) { @@ -660,7 +662,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(3u); + writer.WriteU32(4u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -683,7 +685,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 > 3u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 4u) 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); @@ -691,7 +693,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const result.request.layout = readBindingLayout(reader); if (reader.ReadBool()) { result.graphicsStorage = std::make_unique(); - result.request.graphics = readGraphicsCompileContext(reader, *result.graphicsStorage); + result.request.graphics = readGraphicsCompileContext(reader, *result.graphicsStorage, version); } if (version >= 2u) result.request.useCache = reader.ReadBool(); if (version >= 3u && result.request.context.compute.has_value()) { diff --git a/core/shader/recompiler/Deserialize.cpp b/core/shader/recompiler/Deserialize.cpp index 377bebb3a..4e2574a14 100644 --- a/core/shader/recompiler/Deserialize.cpp +++ b/core/shader/recompiler/Deserialize.cpp @@ -152,7 +152,7 @@ int main(int argc, char** argv) { std::cout << "Disassembly written to disasm.txt (" << disasm.size() << " bytes)\n"; const ShaderStageKind stageKind = ToShaderStageKind(request.shader.stage); - const auto inputInfo = BuildShaderStageInputInfo(stageKind, request.context, request.target.subgroupSize); + const auto inputInfo = BuildShaderStageInputInfo(stageKind, request.context, request.target.subgroupSize, request.graphics && request.graphics->mesh ? &*request.graphics->mesh : nullptr); GraphBuilder graphBuilder; auto cfg = graphBuilder.Build(decoded); diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 44757fdc8..e8915fadd 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -100,18 +100,19 @@ struct ShaderMeshInputInfo: ShaderWorkgroupInputInfo { std::uint32_t maxVertices = 0; std::uint32_t maxPrimitives = 0; std::uint32_t provokingVertex = 0; + std::uint32_t esgsItemSize = 0; [[nodiscard]] std::uint32_t InputPrimitiveSize() const { - throw std::runtime_error("shader input helper not implemented"); + return inputPrimitive == 1u ? 1u : inputPrimitive == 2u ? 2u : 3u; } [[nodiscard]] std::uint32_t InputPrimitiveStep() const { - throw std::runtime_error("shader input helper not implemented"); + return inputPrimitive == 6u ? 1u : InputPrimitiveSize(); } [[nodiscard]] std::uint32_t InputPrimitiveCount(std::uint32_t vertices) const { - throw std::runtime_error("shader input helper not implemented"); + return vertices < InputPrimitiveSize() ? 0u : (vertices - InputPrimitiveSize()) / InputPrimitiveStep() + 1u; } [[nodiscard]] std::uint32_t InputVertexCount(std::uint32_t primitives) const { - throw std::runtime_error("shader input helper not implemented"); + return primitives == 0u ? 0u : (primitives - 1u) * InputPrimitiveStep() + InputPrimitiveSize(); } }; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 85f4dccdb..3ee626b4f 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -82,11 +82,16 @@ std::uint32_t HostSubgroupSize(const RecompileRequest& request) { return request.target.subgroupSize; } +ShaderStageInputInfo RequestInputInfo(const RecompileRequest& request) { + const auto* mesh = request.graphics && request.graphics->mesh ? &*request.graphics->mesh : nullptr; + return BuildShaderStageInputInfo(toShaderStageKind(request.shader.stage), request.context, HostSubgroupSize(request), mesh); +} + } IrProgram PrepareResourceProgram(const RecompileRequest& request) { const auto stageKind = toShaderStageKind(request.shader.stage); - const auto inputInfo = BuildShaderStageInputInfo(stageKind, request.context, HostSubgroupSize(request)); + const auto inputInfo = RequestInputInfo(request); constexpr RdnaInstructionDecoder decoder; const auto decoded = decoder.Decode(request.shader.code); @@ -278,7 +283,7 @@ std::uint64_t nextVariantId() { } CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, const ResourceSnapshot& resourceSnapshot, const ResourceSpecialization& resourceSpecialization) { - const auto inputInfo = BuildShaderStageInputInfo(toShaderStageKind(request.shader.stage), request.context, HostSubgroupSize(request)); + const auto inputInfo = RequestInputInfo(request); constexpr DeadCodeEliminator deadCodeEliminator; constexpr ResourceMaterializer resourceMaterializer; resourceMaterializer.Apply(program, resourceSpecialization); @@ -405,7 +410,7 @@ RecompileResult materializeVariant(SourceEntry& source, const RecompileRequest& } RecompileResult RecompileImpl(const RecompileRequest& request) { - static_cast(BuildShaderStageInputInfo(toShaderStageKind(request.shader.stage), request.context, HostSubgroupSize(request))); + static_cast(RequestInputInfo(request)); RequestMemoryView memory(request.context.memory); const auto runtime = memory.MakeRuntime(request.context.userData, request.shader.codeAddress); ResourceSnapshot snapshot; @@ -587,7 +592,7 @@ auto recompileReporting(const RecompileRequest& request, Impl&& impl) -> decltyp } std::shared_ptr GetResourcePlan(const RecompileRequest& request) { - static_cast(BuildShaderStageInputInfo(toShaderStageKind(request.shader.stage), request.context, HostSubgroupSize(request))); + static_cast(RequestInputInfo(request)); if (request.useCache) return getSource(request)->plan; return makeResourcePlan(request); } @@ -616,7 +621,7 @@ std::shared_ptr CaptureResources(const RecompileRequest& static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto started = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; // Validates the stage inputs once per request, as GetResourcePlan and Recompile(request) do. - static_cast(BuildShaderStageInputInfo(toShaderStageKind(request.shader.stage), request.context, HostSubgroupSize(request))); + static_cast(RequestInputInfo(request)); auto capture = std::make_shared(); if (request.useCache) { capture->source = getSource(request); @@ -631,7 +636,7 @@ std::shared_ptr CaptureResources(const RecompileRequest& std::shared_ptr ResolveSource(const RecompileRequest& request) { if (!request.useCache) return nullptr; - static_cast(BuildShaderStageInputInfo(toShaderStageKind(request.shader.stage), request.context, HostSubgroupSize(request))); + static_cast(RequestInputInfo(request)); return std::make_shared(SourceHandle{getSource(request)}); } @@ -639,7 +644,7 @@ std::shared_ptr CaptureResources(const RecompileRequest& static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto started = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; // The whole vertex family (Vertex, Local, TC, TE, Mesh) validates V# fields the memo key does not cover. - if (request.shader.stage != ShaderStage::Compute && request.shader.stage != ShaderStage::Fragment) static_cast(BuildShaderStageInputInfo(toShaderStageKind(request.shader.stage), request.context, HostSubgroupSize(request))); + if (request.shader.stage != ShaderStage::Compute && request.shader.stage != ShaderStage::Fragment) static_cast(RequestInputInfo(request)); auto capture = std::make_shared(); capture->source = handle.source; capture->plan = handle.source->plan; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index e24ea1bcd..588bae6fd 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -174,6 +174,7 @@ struct MeshConfiguration { std::uint32_t threadsPerGroup; std::uint32_t ldsSizeDwords; std::uint32_t provokingVertex; + std::uint32_t esgsItemSize = 0; }; struct TessellationConfiguration { @@ -184,6 +185,10 @@ struct TessellationConfiguration { std::uint32_t outputTopology; }; +inline constexpr std::uint32_t MeshDrawPushOffsetBytes = 104; +inline constexpr std::uint32_t MeshDrawPushBytes = 24; +inline constexpr std::uint32_t MeshIndexBufferUserWord = 4; + struct GraphicsDrawParameters { std::uint64_t indexAddress; std::uint32_t indexCount; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index c42a895e1..3787acf5c 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -117,7 +117,7 @@ void ValidateBdaTarget(const IrProgram& program, const SpirvTargetOptions& targe for (const auto extension : {"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}) { if (std::find(target.supportedExtensions.begin(), target.supportedExtensions.end(), extension) == target.supportedExtensions.end()) throw std::runtime_error(std::string("BDA requires unsupported extension ") + extension); } - if (program.Resources().stage == IrShaderStage::Mesh || program.Resources().stage == IrShaderStage::TessellationControl) throw std::runtime_error("BDA fault termination requires a barrier-safe mesh or tessellation-control execution protocol"); + if (program.Resources().stage == IrShaderStage::TessellationControl) throw std::runtime_error("BDA fault termination requires a barrier-safe tessellation-control execution protocol"); } bool BdaInvocationsMayStop(const IrProgram& program) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index e9d65296e..a5b44f316 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -259,7 +259,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.waveLdsScope = WaveLdsScope(program, workgroup, state.laneCount); if (const char* guard = std::getenv("APS5_LOOP_GUARD")) state.loopGuardLimit = static_cast(std::strtoul(guard, nullptr, 0)); // Stopped invocations would leave the wave LDS barriers incomplete. - state.bdaStopsInvocations = state.waveLdsScope == 0 && BdaInvocationsMayStop(program); + state.bdaStopsInvocations = state.waveLdsScope == 0 && BdaInvocationsMayStop(program) && program.Resources().stage != IrShaderStage::Mesh; EmitModuleHeader(state, bindings); EmitProgram(state); state.module.EmitEntryPoint(ExecutionModelForStage(state.program.Resources().stage), state.mainFunc, "main", state.interfaceVariables); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 110176243..377a084ee 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -331,7 +331,7 @@ void DefineOutputs(SpirvEmitterState& state) { void DefineDescriptors(SpirvEmitterState& state) { const IrBindingLayout& layout = state.program.Metadata().bindings; const IrShaderStage stage = state.program.Resources().stage; - if (layout.UsesPushData()) { + if (layout.UsesPushData() || stage == IrShaderStage::Mesh) { const auto type = PushConstantBlockType(state); state.pushConstantVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPushConstant, type), spv::StorageClassPushConstant); state.module.AddName(type, "BufferResource"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index f82cfba1a..d80eec77c 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -532,11 +532,31 @@ void DefineMeshOutputs(SpirvEmitterState& state) { void EmitMeshEntryPoint(SpirvEmitterState& state) { state.module.AddFunction(spv::OpFunction, TypeVoid(state), state.mainFunc, spv::FunctionControlMaskNone, TypeFunction(state)); EmitLabel(state, state.module.AllocateId()); + for (std::uint32_t half = 0; half < state.laneCount; half++) { + state.module.AddFunction(spv::OpStore, MeshElement(state, state.meshPrimitiveData, spv::StorageClassPrivate, TypeU32(state), ConstantU32(state, half)), ConstantU32(state, 0x80000000u)); + } + const auto firstInvocation = state.module.AllocateId(); + state.laneHalf = 0; + state.module.AddFunction(spv::OpIEqual, TypeBool(state), firstInvocation, EmitLocalInvocationIndex(state), ConstantU32(state, 0u)); + EmitIfCondition(state, firstInvocation, [&]() { + for (std::uint32_t field = 0; field < 2u; field++) { + state.module.AddFunction(spv::OpStore, MeshElement(state, state.meshAllocation, spv::StorageClassWorkgroup, TypeU32(state), ConstantU32(state, field)), ConstantU32(state, 0u)); + } + }); state.module.AddFunction(spv::OpFunctionCall, TypeVoid(state), state.module.AllocateId(), state.meshGuestFunc); state.module.AddFunction(spv::OpControlBarrier, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | spv::MemorySemanticsWorkgroupMemoryMask)); - const auto vertices = MeshLoad(state, state.meshAllocation, spv::StorageClassWorkgroup, TypeU32(state), ConstantU32(state, 0u)); - const auto primitives = MeshLoad(state, state.meshAllocation, spv::StorageClassWorkgroup, TypeU32(state), ConstantU32(state, 1u)); + const auto& mesh = state.inputInfo.vertex->mesh; + const auto clamp = [&](std::uint32_t value, std::uint32_t limit) { + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), result, GlslStd450(state), GLSLstd450UMin, value, ConstantU32(state, limit)); + return result; + }; + const auto vertices = clamp(MeshLoad(state, state.meshAllocation, spv::StorageClassWorkgroup, TypeU32(state), ConstantU32(state, 0u)), mesh.maxVertices); + const auto primitives = clamp(MeshLoad(state, state.meshAllocation, spv::StorageClassWorkgroup, TypeU32(state), ConstantU32(state, 1u)), mesh.maxPrimitives); state.module.AddFunction(spv::OpSetMeshOutputsEXT, vertices, primitives); + const auto anyVertex = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), anyVertex, GlslStd450(state), GLSLstd450UMax, vertices, ConstantU32(state, 1u)); + const auto lastVertex = EmitBinaryU32(state, spv::OpISub, anyVertex, ConstantU32(state, 1u)); for (std::uint32_t half = 0; half < state.laneCount; half++) { state.laneHalf = half; const auto index = EmitLocalInvocationIndex(state); @@ -559,8 +579,10 @@ void EmitMeshEntryPoint(SpirvEmitterState& state) { const auto packed = MeshLoad(state, state.meshPrimitiveData, spv::StorageClassPrivate, TypeU32(state), ConstantU32(state, half)); std::array vertex {}; for (std::uint32_t component = 0; component < 3u; component++) { + const auto field = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitFieldUExtract, TypeU32(state), field, packed, ConstantU32(state, component * 10u), ConstantU32(state, 9u)); vertex.at(component) = state.module.AllocateId(); - state.module.AddFunction(spv::OpBitFieldUExtract, TypeU32(state), vertex.at(component), packed, ConstantU32(state, component * 10u), ConstantU32(state, 10u)); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), vertex.at(component), GlslStd450(state), GLSLstd450UMin, field, lastVertex); } const auto triangle = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 3u), triangle, vertex[0], vertex[1], vertex[2]); @@ -937,7 +959,7 @@ std::uint32_t EmitMeshDrawParameter(SpirvValueEmitContext& ctx, const IrValue& i ctx.Fail(inst, "invalid mesh draw parameter"); } const auto pointer = state.module.AllocateId(); - state.module.AddFunction(spv::OpAccessChain, TypePushConstantElementPointer(state), pointer, state.pushConstantVariable, ConstantU32(state, 0u), ConstantU32(state, index)); + state.module.AddFunction(spv::OpAccessChain, TypePushConstantElementPointer(state), pointer, state.pushConstantVariable, ConstantU32(state, 0u), ConstantU32(state, MeshDrawPushOffsetBytes / 4u + index)); const auto result = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeU32(state), result, pointer); return result; diff --git a/core/shader/recompiler/Translation/include/Translation/ShaderInputInfoBuilder.hpp b/core/shader/recompiler/Translation/include/Translation/ShaderInputInfoBuilder.hpp index 1096eb2e7..1f923d34d 100644 --- a/core/shader/recompiler/Translation/include/Translation/ShaderInputInfoBuilder.hpp +++ b/core/shader/recompiler/Translation/include/Translation/ShaderInputInfoBuilder.hpp @@ -9,7 +9,7 @@ namespace ShaderRecompiler { // hostSubgroupSize is the device subgroup width; wave64 workgroups on 32-wide subgroups run each // invocation as two guest lanes so a guest wave stays one host subgroup. -ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const GuestContext& context, std::uint32_t hostSubgroupSize); +ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const GuestContext& context, std::uint32_t hostSubgroupSize, const MeshConfiguration* mesh = nullptr); } diff --git a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp index c7a76848b..4159f2b23 100644 --- a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp @@ -174,7 +174,12 @@ void TranslationContext::sBarrier() { (void)ir.Emit(IrOpcode::Barrier, IrType::Void, {}); } -void TranslationContext::sSendmsg(const RdnaInstruction&) { +void TranslationContext::sSendmsg(const RdnaInstruction& inst) { + constexpr std::uint32_t GsAllocReq = 9u; + if (program.Resources().stage == IrShaderStage::Mesh && (inst.rawWords[0] & 0xfu) == GsAllocReq) { + (void)ir.Emit(IrOpcode::MeshAllocate, IrType::Void, {&ir.GetM0()}); + return; + } (void)ir.Emit(IrOpcode::Sendmsg, IrType::Void, {}); } diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 73813b189..3f88ce180 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -233,8 +233,10 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO } } else if (options.stage == ShaderStageKind::Mesh) { const auto& mesh = options.inputInfo.vertex->mesh; - if (options.waveSize != 64u || mesh.primitivesPerGroup == 0u || mesh.verticesPerGroup > 64u || totalThreads > 15u * 64u) { - throw std::runtime_error("mesh shader translation configuration is not supported"); + const std::uint32_t size = mesh.InputPrimitiveSize(); + const std::uint32_t stepCount = mesh.InputPrimitiveStep(); + if (options.waveSize != 64u || mesh.primitivesPerGroup == 0u || mesh.verticesPerGroup != mesh.InputVertexCount(mesh.primitivesPerGroup) || mesh.verticesPerGroup > totalThreads || mesh.primitivesPerGroup > totalThreads || totalThreads % 64u != 0u || totalThreads > 15u * 64u || mesh.esgsItemSize == 0u || mesh.esgsItemSize * mesh.verticesPerGroup > 0xffffu) { + throw std::runtime_error("mesh shader translation configuration is not supported (wave " + std::to_string(options.waveSize) + ", primitives per group " + std::to_string(mesh.primitivesPerGroup) + ", vertices per group " + std::to_string(mesh.verticesPerGroup) + ", threads " + std::to_string(totalThreads) + ", ESGS item size " + std::to_string(mesh.esgsItemSize) + ")"); } constexpr std::uint32_t kTriStripPrimitiveType = 6u; const auto u32 = [&entryIr](std::uint32_t value) -> IrValue& { @@ -250,36 +252,47 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO return entryIr.ISub(lhs, minimum(lhs, rhs)); }; IrValue& local = builtin(StageInputKind::LocalInvocationIndex); - IrValue& primitiveChunk = entryIr.IMul(builtin(StageInputKind::WorkgroupId, 0u), u32(mesh.primitivesPerGroup)); - IrValue& step = u32(mesh.InputPrimitiveStep()); - IrValue& size = u32(mesh.InputPrimitiveSize()); - IrValue& chunk = entryIr.IMul(primitiveChunk, step); - IrValue& vertices = minimum(subtractSaturate(draw(0u), chunk), u32(mesh.verticesPerGroup)); - IrValue& primitives = entryIr.Select(entryIr.ULessThan(vertices, size), u32(0u), entryIr.IAdd(entryIr.Emit(IrOpcode::UDiv32, IrOpcodeType(IrOpcode::UDiv32), {&subtractSaturate(vertices, size), &step}), u32(1u))); + 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& 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)); IrValue& waveBase = entryIr.BitwiseAnd(local, u32(~63u)); IrValue& vertexCount = minimum(subtractSaturate(vertices, waveBase), u32(64u)); IrValue& primitiveCount = minimum(subtractSaturate(primitives, waveBase), u32(64u)); - IrValue& waveInfo = entryIr.BitwiseOr(entryIr.ShiftLeftLogical(wave, u32(24u)), u32(((totalThreads + 63u) / 64u) << 28u)); + IrValue& waveInfo = entryIr.BitwiseOr(entryIr.ShiftLeftLogical(wave, u32(24u)), u32((totalThreads / 64u) << 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(primitiveChunk, local), u32(1u)) : u32(0u); + IrValue& parity = mesh.inputPrimitive == kTriStripPrimitiveType ? entryIr.BitwiseAnd(entryIr.IAdd(firstPrimitive, local), u32(1u)) : u32(0u); IrValue& vertex = entryIr.IMul(local, step); - IrValue& first = entryIr.IAdd(vertex, parity); - IrValue& second = mesh.InputPrimitiveSize() >= 2u ? entryIr.ISub(entryIr.IAdd(vertex, u32(1u)), parity) : u32(0u); - IrValue& third = mesh.InputPrimitiveSize() == 3u ? entryIr.IAdd(vertex, u32(2u)) : u32(0u); - entryIr.SetVectorReg(static_cast(0), entryIr.BitwiseOr(entryIr.ShiftLeftLogical(first, u32(2u)), entryIr.ShiftLeftLogical(second, u32(18u)))); - entryIr.SetVectorReg(static_cast(1), entryIr.ShiftLeftLogical(third, u32(2u))); - IrValue& inputVertex = entryIr.IAdd(chunk, local); + IrValue& item = u32(mesh.esgsItemSize); + IrValue& first = entryIr.IMul(entryIr.IAdd(vertex, parity), item); + IrValue& second = size >= 2u ? entryIr.IMul(entryIr.ISub(entryIr.IAdd(vertex, u32(1u)), parity), item) : u32(0u); + IrValue& third = size == 3u ? 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)); + entryIr.SetVectorReg(static_cast(3), u32(0u)); + entryIr.SetVectorReg(static_cast(4), u32(0u)); + if (options.userDataBaseRegister != 0u || options.userDataCount < 8u) { + throw std::runtime_error("mesh shader translation requires the merged program's eight hidden user words"); + } + IrValue& inputVertex = entryIr.IAdd(firstVertex, local); IrValue& indexBytes = draw(3u); IrValue& indexed = entryIr.INotEqual(indexBytes, u32(0u)); - IrValue& indexLow = draw(4u); - IrValue& byteOffset = entryIr.IAdd(entryIr.BitwiseAnd(indexLow, u32(3u)), entryIr.IMul(inputVertex, indexBytes)); - IrValue& indexResource = entryIr.Emit(IrOpcode::GetAddressResource, IrOpcodeType(IrOpcode::GetAddressResource), {&entryIr.BitwiseAnd(indexLow, u32(~3u)), &draw(5u)}); + IrValue& byteOffset = entryIr.IMul(inputVertex, 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::Global}); - IrValue& packedIndex = entryIr.Emit(IrOpcode::LoadAddressU32, IrOpcodeType(IrOpcode::LoadAddressU32), {&indexResource, &entryIr.BitwiseAnd(byteOffset, u32(~3u)), &u32(0u), &entryIr.LogicalAnd(indexed, entryIr.ULessThan(local, vertices))}, MemoryFlags{.index = memoryIndex}); + program.Resources().memoryInfo.push_back(MemoryInfo{.kind = ResourceKind::Buffer, .resource = 1u, .offen = true}); + IrValue& packedIndex = entryIr.Emit(IrOpcode::LoadBufferU32, IrOpcodeType(IrOpcode::LoadBufferU32), {&indexResource, &u32(0u), &entryIr.BitwiseAnd(byteOffset, u32(~3u)), &u32(0u), &entryIr.LogicalAnd(indexed, entryIr.ULessThan(local, vertices))}, MemoryFlags{.index = memoryIndex}); IrValue& index = entryIr.Emit(IrOpcode::BitFieldUExtract, IrOpcodeType(IrOpcode::BitFieldUExtract), {&packedIndex, &entryIr.IMul(entryIr.BitwiseAnd(byteOffset, u32(3u)), u32(8u)), &entryIr.IMul(indexBytes, u32(8u))}); + 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))); + 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))); } else if (options.stage == ShaderStageKind::Local) { entryIr.SetScalarReg(static_cast(3), entryIr.Constant(64u)); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index d29849d3a..2525f99cd 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -63,7 +63,7 @@ void _detectVertexBuffers(ShaderVertexInputInfo& info) { } -ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const GuestContext& context, std::uint32_t hostSubgroupSize) { +ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const GuestContext& context, std::uint32_t hostSubgroupSize, const MeshConfiguration* mesh) { switch (stage) { case ShaderStageKind::Compute: { if (!context.compute.has_value()) { @@ -154,6 +154,24 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues vertexStorage.resourcesDst[i].fetchIndex = vertex.resourcesDst[i].fetchIndex; } _detectVertexBuffers(vertexStorage); + if (stage == ShaderStageKind::Mesh) { + if (mesh == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: a mesh-stage program has no mesh configuration"); + auto& target = vertexStorage.mesh; + target.threadsNum[0] = mesh->threadsPerGroup; + target.threadsNum[1] = 1u; + target.threadsNum[2] = 1u; + target.ldsSizeDwords = mesh->ldsSizeDwords; + target.waveSize = context.waveSize; + target.hostSubgroupSize = hostSubgroupSize; + target.inputPrimitive = mesh->inputPrimitive; + target.primitivesPerGroup = mesh->primitivesPerGroup; + target.verticesPerGroup = mesh->verticesPerGroup; + target.maxVertices = mesh->maxVertices; + target.maxPrimitives = mesh->maxPrimitives; + target.provokingVertex = mesh->provokingVertex; + target.esgsItemSize = mesh->esgsItemSize; + if (target.threadsNum[0] == 0u || target.maxVertices == 0u || target.maxPrimitives == 0u || target.provokingVertex > 2u) throw std::runtime_error("ShaderInputInfoBuilder: invalid mesh configuration"); + } ShaderStageInputInfo result; result.vertex = &vertexStorage; return result;