From a5b3673a3913d98a39044cc3173185068b44c207 Mon Sep 17 00:00:00 2001 From: nmzik Date: Tue, 27 Oct 2026 09:31:50 +0100 Subject: [PATCH] graphics: draw patched-offset indirect draws from GPU-read records --- src/graphics/guest_gpu/graphicsRun.cpp | 33 +++++++++++++++---- .../renderer/pipeline/pipelineCache.cpp | 3 ++ .../renderer/pipeline/pipelineCache.h | 2 ++ src/graphics/host_gpu/renderer/render.h | 7 ++-- src/graphics/host_gpu/renderer/renderDraw.cpp | 12 ++++--- .../presentation/window/vulkanWindow.cpp | 3 ++ .../backend/spirv/spirvEmitterFlow.cpp | 3 +- .../backend/spirv/spirvEmitterModule.cpp | 10 +++++- .../frontend/translate/Translate.cpp | 21 ++++++++++-- src/graphics/shader/recompiler/ir/ShaderIR.h | 2 ++ .../ir/passes/ShaderInfoCollection.cpp | 4 +++ src/graphics/shader/shader.cpp | 1 + src/graphics/shader/shader.h | 13 ++++++++ tests/ShaderRecompilerComputeTests.cpp | 25 ++++++++++++-- 14 files changed, 118 insertions(+), 21 deletions(-) diff --git a/src/graphics/guest_gpu/graphicsRun.cpp b/src/graphics/guest_gpu/graphicsRun.cpp index 9157595f0..ccda178cf 100644 --- a/src/graphics/guest_gpu/graphicsRun.cpp +++ b/src/graphics/guest_gpu/graphicsRun.cpp @@ -814,6 +814,27 @@ static void PatchIndirectDrawOffsets(CommandProcessor& cp, IndirectDrawRegisters write(registers.draw_index, draw_index); } +// Writes the GS user-data slots the packet patches; false when the shaders cannot read the +// offsets from GPU records: a first-index or subdraw-index destination, or another stage's +// user data. +static bool IndirectDrawOffsets(IndirectDrawRegisters registers, ShaderIndirectOffsets& offsets) { + if (registers.index_offset != Pm4::SH_NOP || registers.draw_index != Pm4::SH_NOP) { + return false; + } + for (const auto [location, slot]: {std::pair {registers.vertex_offset, &offsets.vertex}, + std::pair {registers.instance_offset, &offsets.instance}}) { + if (location == Pm4::SH_NOP) { + continue; + } + const uint32_t index = location - Pm4::SPI_SHADER_USER_DATA_GS_0; + if (index >= HW::UserSgprInfo::SGPRS_MAX) { + return false; + } + *slot = static_cast(index); + } + return true; +} + void CommandProcessor::DrawIndirect(uint32_t data_offset, IndirectDrawRegisters registers, uint32_t draw_initiator, bool indexed) { const auto args_size = @@ -845,13 +866,11 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun // NUM_INSTANCES keeps the last draw's instance count. m_num_instances_addr = args_addr + static_cast(draw_count - 1) * stride_in_bytes + offsetof(vk::DrawIndirectCommand, instanceCount); - // With no SGPRs to patch, the GPU reads the records if the shaders allow. Shaders apply - // the records' offsets only through those SGPRs; Vulkan applies them, which matches - // while they are zero. - const DrawIndirectArgs records {args_addr, draw_count, stride_in_bytes}; - if (registers.vertex_offset == Pm4::SH_NOP && registers.instance_offset == Pm4::SH_NOP && - registers.index_offset == Pm4::SH_NOP && registers.draw_index == Pm4::SH_NOP && - (!indexed || m_index_buffer_size != 0) && + // The GPU reads the records if the shaders allow; the VS prolog loads the patched SGPRs from + // Vulkan's bases. With no SGPR patched, Vulkan applies the offsets, which matches while they + // are zero. + DrawIndirectArgs records {args_addr, draw_count, stride_in_bytes}; + if (IndirectDrawOffsets(registers, records.offsets) && (!indexed || m_index_buffer_size != 0) && (indexed ? executor.DrawIndex(m_submit_id, CurrentBuffer(), {.index_count = m_index_buffer_size, .index_addr = IndexAddress(0), diff --git a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp index 419d9580c..bde029a80 100644 --- a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp @@ -687,6 +687,9 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender vertex_params[0] = PrepareProgram(vertex_regs, context, user_config, vertex_info[0]); } const bool mesh_active = vertex_info[0].logical_stage == ShaderType::Mesh; + if (!tess_active && !mesh_active) { + vertex_info[0].indirect_offsets = state.indirect_offsets; + } if (object_primitive_id) { // CxObjectIdMode: register source plus a 32-bit pipelined primitive-ID addition. EXIT_NOT_IMPLEMENTED(sh.m_paClObjprimIdCntl != 2u || mesh_active || tess_active || diff --git a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.h b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.h index 46fce0f11..b088b67cf 100644 --- a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.h +++ b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.h @@ -137,6 +137,8 @@ struct DrawRenderState { bool ps_active = false; std::array vertex_info; ShaderPixelInputInfo ps_input_info; + // Input: the user-data slots a GPU-read indirect draw patches. + ShaderIndirectOffsets indirect_offsets; [[nodiscard]] std::span VertexStages() const { return {vertex_info.data(), vertex_info[1].stage ? 3u : 1u}; diff --git a/src/graphics/host_gpu/renderer/render.h b/src/graphics/host_gpu/renderer/render.h index 08b556474..49462a9c4 100644 --- a/src/graphics/host_gpu/renderer/render.h +++ b/src/graphics/host_gpu/renderer/render.h @@ -45,9 +45,10 @@ enum class DrawOffsetSource : uint8_t { // Nonzero address: the GPU reads count Vulkan indirect draw records, stride bytes apart. struct DrawIndirectArgs { - uint64_t address = 0; - uint32_t count = 0; - uint32_t stride = 0; + uint64_t address = 0; + uint32_t count = 0; + uint32_t stride = 0; + ShaderIndirectOffsets offsets; }; struct DrawIndexArgs { diff --git a/src/graphics/host_gpu/renderer/renderDraw.cpp b/src/graphics/host_gpu/renderer/renderDraw.cpp index dab458944..1ea60852d 100644 --- a/src/graphics/host_gpu/renderer/renderDraw.cpp +++ b/src/graphics/host_gpu/renderer/renderDraw.cpp @@ -1250,8 +1250,9 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff } } -// Vulkan offsets for a prepared draw, or nullopt when its indirect records need CPU counts, as -// mesh and quad-list draws do. Indirect records carry their own offsets. +// Vulkan offsets for a prepared draw, or nullopt when its indirect records need the CPU: mesh +// and quad-list draws for counts, tessellation draws for patched SGPRs. Indirect records carry +// their own offsets. static std::optional ResolveDrawEmit(const HW::UserConfig& ucfg, const ShaderVertexInputInfo& vs, DrawOffsetSource source, @@ -1260,7 +1261,8 @@ static std::optional ResolveDrawEmit(const HW::UserConfig& const DrawIndirectArgs& indirect) { if (indirect.address != 0) { if (vs.stage.module->info.stage == ShaderType::Mesh || - ucfg.GetPrimType() == Prospero::PrimitiveType::kQuadListLegacy) { + ucfg.GetPrimType() == Prospero::PrimitiveType::kQuadListLegacy || + vs.indirect_offsets != indirect.offsets) { return std::nullopt; } return DrawEmitInfo {.indirect = indirect}; @@ -1324,7 +1326,7 @@ bool RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer, hw_check(buffer); - DrawRenderState state {}; + DrawRenderState state {.indirect_offsets = args.indirect.offsets}; if (!GetDrawTopology(ucfg, state.topology)) { return true; } @@ -1429,7 +1431,7 @@ bool RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D const DrawCallInfo draw {CommandBufferDebugOp::DrawIndexAuto, args.vertex_count, args.instance_count, args.first_instance}; - DrawRenderState state {}; + DrawRenderState state {.indirect_offsets = args.indirect.offsets}; if (!GetDrawTopology(ucfg, state.topology)) { ResetBindings(); return true; diff --git a/src/graphics/presentation/window/vulkanWindow.cpp b/src/graphics/presentation/window/vulkanWindow.cpp index 5fcd9f21d..73bfebc27 100644 --- a/src/graphics/presentation/window/vulkanWindow.cpp +++ b/src/graphics/presentation/window/vulkanWindow.cpp @@ -51,6 +51,7 @@ struct VulkanExtensions { vk::PhysicalDeviceVulkan11Features WindowContext::RequiredVulkan11Features() noexcept { vk::PhysicalDeviceVulkan11Features features {}; features.storageBuffer16BitAccess = VK_TRUE; + features.shaderDrawParameters = VK_TRUE; return features; } @@ -278,6 +279,8 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa required_features12.samplerMirrorClampToEdge); check_feature(features11.storageBuffer16BitAccess, "storageBuffer16BitAccess", WindowContext::RequiredVulkan11Features().storageBuffer16BitAccess); + check_feature(features11.shaderDrawParameters, "shaderDrawParameters", + WindowContext::RequiredVulkan11Features().shaderDrawParameters); check_feature(features12.storageBuffer8BitAccess, "storageBuffer8BitAccess", required_features12.storageBuffer8BitAccess); check_feature(features12.timelineSemaphore, "timelineSemaphore", diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp index 9dc78a8e8..8c89a2c3d 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp @@ -63,7 +63,8 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c } if (kind == IR::StageInputKind::VertexIndex || kind == IR::StageInputKind::InstanceIndex || kind == IR::StageInputKind::InvocationId || kind == IR::StageInputKind::PrimitiveId || - kind == IR::StageInputKind::Layer || kind == IR::StageInputKind::SampleId) { + kind == IR::StageInputKind::Layer || kind == IR::StageInputKind::SampleId || + kind == IR::StageInputKind::BaseVertex || kind == IR::StageInputKind::BaseInstance) { const auto value = state.builder.AllocateId(); const auto bits = state.builder.AllocateId(); state.builder.AddFunction(spv::OpLoad, TypeI32(state), value, variable); diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterModule.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterModule.cpp index 709db2f2d..063ef8233 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterModule.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterModule.cpp @@ -476,6 +476,8 @@ uint32_t BuiltInForInput(IR::StageInputKind kind) { case IR::StageInputKind::LocalInvocationId: return spv::BuiltInLocalInvocationId; case IR::StageInputKind::LocalInvocationIndex: return spv::BuiltInLocalInvocationIndex; case IR::StageInputKind::GlobalInvocationId: return spv::BuiltInGlobalInvocationId; + case IR::StageInputKind::BaseVertex: return spv::BuiltInBaseVertex; + case IR::StageInputKind::BaseInstance: return spv::BuiltInBaseInstance; default: return UINT32_MAX; } } @@ -526,7 +528,9 @@ void DefineInputs(EmitterState& state) { case IR::StageInputKind::PrimitiveId: case IR::StageInputKind::InstanceIndex: case IR::StageInputKind::Layer: - case IR::StageInputKind::SampleId: type = TypeI32(state); break; + case IR::StageInputKind::SampleId: + case IR::StageInputKind::BaseVertex: + case IR::StageInputKind::BaseInstance: type = TypeI32(state); break; case IR::StageInputKind::WorkgroupId: case IR::StageInputKind::NumWorkgroups: case IR::StageInputKind::LocalInvocationId: @@ -802,6 +806,10 @@ void DefineModule(EmitterState& state) { if (InputVariableForKind(state, IR::StageInputKind::SampleId) != 0) { state.builder.RequireCapability(spv::CapabilitySampleRateShading); } + if (InputVariableForKind(state, IR::StageInputKind::BaseVertex) != 0 || + InputVariableForKind(state, IR::StageInputKind::BaseInstance) != 0) { + state.builder.RequireCapability(spv::CapabilityDrawParameters); + } if (state.program.info.image_gather_extended) { state.builder.RequireCapability(spv::CapabilityImageGatherExtended); } diff --git a/src/graphics/shader/recompiler/frontend/translate/Translate.cpp b/src/graphics/shader/recompiler/frontend/translate/Translate.cpp index 06bc315f5..3e9b6dfa3 100644 --- a/src/graphics/shader/recompiler/frontend/translate/Translate.cpp +++ b/src/graphics/shader/recompiler/frontend/translate/Translate.cpp @@ -1389,10 +1389,27 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph& IR::U32(IR::Value(options.wave_size << 12u))); entry_ir.SetScalarReg(static_cast(3), entry_ir.BitwiseOr(vertex_count, IR::U32(IR::Value(1u << 28u)))); + // Vulkan adds an indirect record's bases to the indices; the hardware passes them + // only in the user SGPRs the packet patches. + const auto& offsets = options.input_info.vertex->indirect_offsets; + const auto index = [&](uint8_t slot, IR::StageInputKind kind, + IR::StageInputKind base_kind) { + if (offsets == ShaderIndirectOffsets {}) { + return builtin(kind); + } + const auto base = builtin(base_kind); + if (slot < options.user_data_count) { + entry_ir.SetScalarReg(static_cast(options.user_data_base + slot), + base); + } + return entry_ir.ISub(builtin(kind), base); + }; entry_ir.SetVectorReg(static_cast(5), - builtin(IR::StageInputKind::VertexIndex)); + index(offsets.vertex, IR::StageInputKind::VertexIndex, + IR::StageInputKind::BaseVertex)); entry_ir.SetVectorReg(static_cast(8), - builtin(IR::StageInputKind::InstanceIndex)); + index(offsets.instance, IR::StageInputKind::InstanceIndex, + IR::StageInputKind::BaseInstance)); } } const bool flush_f32_inputs = options.stage == ShaderType::Compute && diff --git a/src/graphics/shader/recompiler/ir/ShaderIR.h b/src/graphics/shader/recompiler/ir/ShaderIR.h index fe6b21346..6a5a06f3c 100644 --- a/src/graphics/shader/recompiler/ir/ShaderIR.h +++ b/src/graphics/shader/recompiler/ir/ShaderIR.h @@ -251,6 +251,8 @@ enum class StageInputKind { LocalInvocationId, LocalInvocationIndex, GlobalInvocationId, + BaseVertex, + BaseInstance, Parameter, }; diff --git a/src/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp b/src/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp index a566360fe..54da499d7 100644 --- a/src/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp @@ -175,6 +175,8 @@ void AddBuiltinInput(ShaderInfo& info, StageInputKind kind) { case StageInputKind::GlobalInvocationId: AddInput(info, kind, 0, 3, "gl_GlobalInvocationID"); break; + case StageInputKind::BaseVertex: AddInput(info, kind, 0, 1, "gl_BaseVertex"); break; + case StageInputKind::BaseInstance: AddInput(info, kind, 0, 1, "gl_BaseInstance"); break; case StageInputKind::PackedAncillary: case StageInputKind::Parameter: break; } @@ -444,6 +446,8 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs case StageInputKind::InstanceIndex: case StageInputKind::FrontFacing: case StageInputKind::LocalInvocationIndex: + case StageInputKind::BaseVertex: + case StageInputKind::BaseInstance: if (component != 0u) { return Fail("typed scalar builtin component is out of range"); } diff --git a/src/graphics/shader/shader.cpp b/src/graphics/shader/shader.cpp index c4bb97749..e3d8ca362 100644 --- a/src/graphics/shader/shader.cpp +++ b/src/graphics/shader/shader.cpp @@ -422,6 +422,7 @@ void BuildStageStaticKey(const ShaderVertexInputInfo& info, std::vector(info.clip_space.enabled)); if (info.clip_space.enabled) { for (const float value: info.clip_space.scale) { diff --git a/src/graphics/shader/shader.h b/src/graphics/shader/shader.h index 9db2c6299..91e19a2bc 100644 --- a/src/graphics/shader/shader.h +++ b/src/graphics/shader/shader.h @@ -124,6 +124,18 @@ struct ShaderTessellationInputInfo { uint32_t output_topology = 0; }; +// GS user-data slots an indirect draw packet writes its record's base vertex and first instance +// to. Like the hardware index VGPRs, the vertex shader's indices exclude both bases once either +// slot is set. +struct ShaderIndirectOffsets { + static constexpr uint8_t None = UINT8_MAX; + + uint8_t vertex = None; + uint8_t instance = None; + + bool operator==(const ShaderIndirectOffsets&) const = default; +}; + struct ShaderVertexInputInfo { static constexpr int RES_MAX = 32; @@ -146,6 +158,7 @@ struct ShaderVertexInputInfo { ShaderClipSpaceTransform clip_space; ShaderMeshInputInfo mesh; ShaderTessellationInputInfo tess; + ShaderIndirectOffsets indirect_offsets; bool fetch_external = false; bool fetch_embedded = false; }; diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 615e841c0..035a820eb 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -19763,11 +19763,12 @@ public: for (u32 i = 0; i < 4; i++) { shaders.SetGsUserSgpr(i, static_cast(tables[i / 2] >> (i % 2 * 32))); } - const auto check = [&](const char *step, uint64_t vertex_shader, bool indexed, u32 first) { + const auto check = [&](const char *step, uint64_t vertex_shader, bool indexed, u32 first, + ShaderIndirectOffsets offsets = {}) { shaders.SetEsShaderBase(vertex_shader); TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), sparse_color.image_id, {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, {}); - const DrawIndirectArgs records{args_address + first * stride, 2, stride}; + const DrawIndirectArgs records{args_address + first * stride, 2, stride, offsets}; const bool drawn = indexed ? RenderExecutorTestAccess::DrawIndex(executor, scheduler.Current(), {.index_count = 3, .index_addr = reinterpret_cast(index_address), @@ -19793,6 +19794,16 @@ public: "no cached graphics pipeline named only compiled modules"); check("recreated indexed embedded fetch", fetch_vs, true, 0); check("recreated auto native fetch", vertex_address, false, 6); + // Patched user SGPRs 0/1 read the records' base vertex 7 and first instance 5, not the + // table pointers they hold, and the index VGPRs exclude both bases. + check("indexed patched offsets", indirect_vs, true, 0, {0, 1}); + check("auto patched offsets", indirect_vs, false, 4, {0, 1}); + // Slots 4/5 lie past USER_SGPR, so the tables survive; rewritten fetches keep Vulkan's + // vertex offset. + check("indexed patched embedded fetch", fetch_vs, true, 0, {4, 5}); + // Patching one offset still leaves both bases out of the index VGPRs. + check("auto instance-only patch", vertex_address, false, 5, + {ShaderIndirectOffsets::None, 1}); } // A fourth VS invocation reads beyond the descriptor instead of reconstructing the corner. @@ -51528,6 +51539,16 @@ void CheckPm4DrawIndirectMultiPacket(RenderContext &renderer) { auto_packet[3] = Pm4::SH_NOP; arguments[3] = 103; check_offsets(auto_packet, hs, {53, 101}, "disabled destinations"); + // GS destinations, with an index buffer size for indexed draws, leave the records to the + // GPU and the CP writes no SGPR. A first-index destination stays on the CPU. + processor.SetIndexBufferSize(3); + arguments = {0, 1, 61, 17, 19}; + check_offsets(indexed_packet, gs, {41, 7, 8}, "GPU indexed records"); + auto_packet[2] = Pm4::SPI_SHADER_USER_DATA_GS_0 + 9; + auto_packet[3] = Pm4::SPI_SHADER_USER_DATA_GS_0 + 10; + check_offsets(auto_packet, gs, {41, 7, 8}, "GPU records"); + indexed_packet[3] |= 1u << 28u; + check_offsets(indexed_packet, gs, {17, 19, 61}, "first-index destination"); processor.BufferFlush(); std::printf("[host] %-32s ok\n", "Pm4DrawIndirectMulti"); }