From 86fed10b810d5f73f386eddb8d7c2d1b65b5732e Mon Sep 17 00:00:00 2001 From: nmzik Date: Thu, 22 Oct 2026 15:11:39 +0200 Subject: [PATCH] graphics: draw non-indexed and native-fetch indirect draws from GPU-read records --- src/graphics/guest_gpu/graphicsRun.cpp | 20 +-- src/graphics/host_gpu/renderer/render.h | 7 +- src/graphics/host_gpu/renderer/renderDraw.cpp | 121 +++++++++--------- tests/ShaderRecompilerComputeTests.cpp | 69 +++++----- 4 files changed, 118 insertions(+), 99 deletions(-) diff --git a/src/graphics/guest_gpu/graphicsRun.cpp b/src/graphics/guest_gpu/graphicsRun.cpp index 75b980f51..80e6cdb63 100644 --- a/src/graphics/guest_gpu/graphicsRun.cpp +++ b/src/graphics/guest_gpu/graphicsRun.cpp @@ -903,14 +903,18 @@ 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 indexed draws' arguments if the shaders allow. - if (indexed && m_index_buffer_size != 0 && registers.vertex_offset == Pm4::SH_NOP && - registers.instance_offset == Pm4::SH_NOP && registers.index_offset == Pm4::SH_NOP && - executor.DrawIndex(m_submit_id, CurrentBuffer(), - {.index_count = m_index_buffer_size, - .index_addr = IndexAddress(0), - .index_type_and_size = m_index_type_and_size, - .indirect = {args_addr, draw_count, stride_in_bytes}})) { + // 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 && (!indexed || m_index_buffer_size != 0) && + (indexed ? executor.DrawIndex(m_submit_id, CurrentBuffer(), + {.index_count = m_index_buffer_size, + .index_addr = IndexAddress(0), + .index_type_and_size = m_index_type_and_size, + .indirect = records}) + : executor.DrawAuto(m_submit_id, CurrentBuffer(), {.indirect = records}))) { return; } diff --git a/src/graphics/host_gpu/renderer/render.h b/src/graphics/host_gpu/renderer/render.h index dbd1a2637..f7d58a979 100644 --- a/src/graphics/host_gpu/renderer/render.h +++ b/src/graphics/host_gpu/renderer/render.h @@ -48,7 +48,7 @@ enum class DrawOffsetSource : uint8_t { IndirectArgs, }; -// Nonzero address: the GPU reads count VkDrawIndexedIndirectCommand records, stride bytes apart. +// Nonzero address: the GPU reads count Vulkan indirect draw records, stride bytes apart. struct DrawIndirectArgs { uint64_t address = 0; uint32_t count = 0; @@ -75,6 +75,7 @@ struct DrawAutoArgs { uint32_t first_instance = 0; DrawOffsetSource offset_source = DrawOffsetSource::DrawState; uint32_t render_target_slice_offset = 0; + DrawIndirectArgs indirect; }; struct SubmitInfo { @@ -172,9 +173,9 @@ public: std::span bindings); private: - // False, with nothing drawn, when the shaders cannot take indirect records from the GPU. + // False, with nothing drawn, when the draw's indirect records need CPU counts. bool DrawIndex(uint64_t submit_id, CommandBuffer& buffer, const DrawIndexArgs& args); - void DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const DrawAutoArgs& args); + bool DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const DrawAutoArgs& args); struct GraphicsBindings { std::array vertex; diff --git a/src/graphics/host_gpu/renderer/renderDraw.cpp b/src/graphics/host_gpu/renderer/renderDraw.cpp index 1843e00fe..8a6f181d7 100644 --- a/src/graphics/host_gpu/renderer/renderDraw.cpp +++ b/src/graphics/host_gpu/renderer/renderDraw.cpp @@ -631,9 +631,9 @@ bool RenderExecutor::ConsumeMetadataColorOperation(CommandBuffer& buffer, mode == static_cast(CbColorMode::DccDecompress); } +// vertex_offset is the first vertex of non-indexed draws. struct DrawEmitInfo { int32_t vertex_offset = 0; - uint32_t first_vertex = 0; uint32_t first_instance = 0; DrawIndirectArgs indirect; }; @@ -1044,8 +1044,8 @@ static void EmitDrawPrimitives(const HW::UserConfig& ucfg, vk::CommandBuffer vk_ vk_buffer.drawIndexed(draw.index_count, draw.instance_count, 0, emit.vertex_offset, emit.first_instance); } else { - vk_buffer.draw(draw.index_count, draw.instance_count, emit.first_vertex, - emit.first_instance); + vk_buffer.draw(draw.index_count, draw.instance_count, + static_cast(emit.vertex_offset), emit.first_instance); } break; case Prospero::PrimitiveType::kQuadListLegacy: @@ -1055,7 +1055,8 @@ static void EmitDrawPrimitives(const HW::UserConfig& ucfg, vk::CommandBuffer vk_ vk_buffer.drawIndexed(4, draw.instance_count, i, emit.vertex_offset, emit.first_instance); } else { - vk_buffer.draw(4, draw.instance_count, i + emit.first_vertex, + vk_buffer.draw(4, draw.instance_count, + i + static_cast(emit.vertex_offset), emit.first_instance); } } @@ -1076,7 +1077,7 @@ bool RenderExecutor::CopyDepthToColor(CommandBuffer& buffer, const DrawCallInfo& EXIT_NOT_IMPLEMENTED(!m_context.GetGraphics().maintenance8_enabled || !Prospero::IsRectList(ucfg.GetPrimType()) || draw.IsIndexed() || draw.index_count != 3 || draw.instance_count != 1 || - emit.first_vertex != 0 || emit.first_instance != 0 || + emit.vertex_offset != 0 || emit.first_instance != 0 || state.color_count != 1 || color.target_slot != 0 || !depth.image_id || color.desc.info.samples != 1 || depth.desc.info.extent.width != color.Extent().width || @@ -1193,7 +1194,8 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff std::pair args_buffer {}; if (emit.indirect.address != 0) { const auto size = static_cast(emit.indirect.count - 1) * emit.indirect.stride + - sizeof(vk::DrawIndexedIndirectCommand); + (draw.IsIndexed() ? sizeof(vk::DrawIndexedIndirectCommand) + : sizeof(vk::DrawIndirectCommand)); args_buffer = m_context.GetBufferCache().ObtainBuffer(emit.indirect.address, size, false); EXIT_IF((args_buffer.second & 3u) != 0); } @@ -1233,12 +1235,12 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff if (draw_dwords != 0) { std::array draw_data {}; if (mesh_active) { - draw_data = { - draw.index_count, - draw.IsIndexed() ? static_cast(emit.vertex_offset) : emit.first_vertex, - emit.first_instance, index_source.guest_element_size, - static_cast(index_source.address), - static_cast(index_source.address >> 32u)}; + draw_data = {draw.index_count, + static_cast(emit.vertex_offset), + emit.first_instance, + index_source.guest_element_size, + static_cast(index_source.address), + static_cast(index_source.address >> 32u)}; } else { draw_data[0] = buffer.GetUserConfig().GetObjectId(); } @@ -1283,11 +1285,14 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff } if (mesh_active) { vk_buffer.drawMeshTasksEXT(mesh_groups, draw.instance_count, 1); - } else if (args_buffer.first != nullptr) { + } else if (args_buffer.first == nullptr) { + EmitDrawPrimitives(ucfg, vk_buffer, draw, emit); + } else if (draw.IsIndexed()) { vk_buffer.drawIndexedIndirect(args_buffer.first->Handle(), args_buffer.second, emit.indirect.count, emit.indirect.stride); } else { - EmitDrawPrimitives(ucfg, vk_buffer, draw, emit); + vk_buffer.drawIndirect(args_buffer.first->Handle(), args_buffer.second, + emit.indirect.count, emit.indirect.stride); } Common::DebugCounters::Add(Common::DebugCounters::Counter::Draws); Common::DebugCounters::Add(Common::DebugCounters::Counter::DrawInstances, draw.instance_count); @@ -1305,6 +1310,29 @@ 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. +static std::optional ResolveDrawEmit(const HW::UserConfig& ucfg, + const ShaderVertexInputInfo& vs, + DrawOffsetSource source, + int32_t first_vertex, + uint32_t first_instance, + const DrawIndirectArgs& indirect) { + if (indirect.address != 0) { + if (vs.stage.program->stage == ShaderType::Mesh || + ucfg.GetPrimType() == Prospero::PrimitiveType::kQuadListLegacy) { + return std::nullopt; + } + return DrawEmitInfo {.indirect = indirect}; + } + if (source == DrawOffsetSource::IndirectArgs) { + // Native fetches consume the patched SGPRs; rewritten fetches need Vulkan offsets. + return vs.fetch_embedded ? DrawEmitInfo {first_vertex, first_instance} : DrawEmitInfo {}; + } + const auto [vertex_offset, instance_offset] = ResolveDrawOffsets(ucfg.GetIndexOffset(), vs); + return DrawEmitInfo {vertex_offset + first_vertex, instance_offset}; +} + bool RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer, const DrawIndexArgs& args) { KYTY_PROFILER_FUNCTION(); @@ -1402,35 +1430,18 @@ bool RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer, LogDrawStateIfNeeded(buffer, draw, state, args.index_type_and_size, args.index_addr); - // Only embedded fetches apply Vulkan's offsets; mesh and quad-list draws need CPU counts. - if (args.indirect.address != 0 && - (!state.vertex_info[0].fetch_embedded || - state.vertex_info[0].stage.program->stage == ShaderType::Mesh || - ucfg.GetPrimType() == Prospero::PrimitiveType::kQuadListLegacy)) { - ResetBindings(); - return false; + const auto emit = ResolveDrawEmit(ucfg, state.vertex_info[0], args.offset_source, + args.base_vertex, args.first_instance, args.indirect); + if (emit) { + ExecutePreparedDraw(submit_id, buffer, draw, state, topology, *emit, index_source, + primitive_restart); } - - const bool indirect = args.offset_source == DrawOffsetSource::IndirectArgs; - const auto [vertex_offset, instance_offset] = - indirect ? std::pair {0, args.first_instance} - : ResolveDrawOffsets(ucfg.GetIndexOffset(), state.vertex_info[0]); - - DrawEmitInfo emit {}; - // Native fetches consume the patched SGPRs; rewritten fetches need Vulkan offsets. - const bool native_indirect = indirect && !state.vertex_info[0].fetch_embedded; - emit.vertex_offset = native_indirect ? 0 : vertex_offset + args.base_vertex; - emit.first_instance = native_indirect ? 0 : instance_offset; - emit.indirect = args.indirect; - - ExecutePreparedDraw(submit_id, buffer, draw, state, topology, emit, index_source, - primitive_restart); ResetBindings(); - return true; + return emit.has_value(); } // NOLINTNEXTLINE(readability-function-cognitive-complexity) -void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const DrawAutoArgs& args) { +bool RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const DrawAutoArgs& args) { KYTY_PROFILER_FUNCTION(); EXIT_IF(buffer.IsInvalid()); @@ -1444,19 +1455,19 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D args.first_instance); Common::LockGuard lock(m_context.GetMutex()); - if (args.vertex_count == 0 || args.instance_count == 0) { - return; + if (args.indirect.address == 0 && (args.vertex_count == 0 || args.instance_count == 0)) { + return true; } if (ConsumeMetadataColorOperation(buffer, args.render_target_slice_offset) || DepthStencilCopy(buffer) || ResolveColorTargets(buffer, args.render_target_slice_offset)) { ResetBindings(); - return; + return true; } if (!DrawHasValidVertexShader(sh_ctx)) { - return; + return true; } if (graphics_debug_dump_enabled()) { @@ -1482,12 +1493,12 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList; if (!GetDrawTopology(ucfg, topology)) { ResetBindings(); - return; + return true; } DrawRenderState state {}; if (!PrepareDrawRenderState(buffer, draw, args.render_target_slice_offset, state)) { ResetBindings(); - return; + return true; } const bool rect_list = Prospero::IsRectList(ucfg.GetPrimType()); @@ -1501,24 +1512,18 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D sh_ctx.GetVs().es_regs.data_addr, sh_ctx.GetVs().gs_regs.data_addr); } ResetBindings(); - return; + return true; } LogDrawStateIfNeeded(buffer, draw, state, 0, nullptr); - - const bool indirect = args.offset_source == DrawOffsetSource::IndirectArgs; - const auto [vertex_offset, instance_offset] = - indirect ? std::pair {0, args.first_instance} - : ResolveDrawOffsets(ucfg.GetIndexOffset(), state.vertex_info[0]); - DrawEmitInfo emit {}; - const bool native_indirect = indirect && !state.vertex_info[0].fetch_embedded; - emit.first_vertex = native_indirect ? 0 : - static_cast(vertex_offset + static_cast(args.first_vertex)); - emit.first_instance = native_indirect ? 0 : instance_offset; - - DrawIndexBufferSource index_source {}; - ExecutePreparedDraw(submit_id, buffer, draw, state, topology, emit, index_source, false); + const auto emit = ResolveDrawEmit(ucfg, state.vertex_info[0], args.offset_source, + static_cast(args.first_vertex), args.first_instance, + args.indirect); + if (emit) { + ExecutePreparedDraw(submit_id, buffer, draw, state, topology, *emit, {}, false); + } ResetBindings(); + return emit.has_value(); } bool RenderExecutor::ResolveColorTargets(CommandBuffer& buffer, uint32_t render_target_slice_offset) { diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 9908bce1c..a46283424 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -404,9 +404,9 @@ struct RenderExecutorTestAccess { return executor.DrawIndex(0, command, args); } - static void DrawAuto(RenderExecutor &executor, CommandBuffer &command, + static bool DrawAuto(RenderExecutor &executor, CommandBuffer &command, const DrawAutoArgs &args) { - executor.DrawAuto(0, command, args); + return executor.DrawAuto(0, command, args); } static bool TryConsumeComputeFill(RenderExecutor &executor, @@ -18659,9 +18659,9 @@ public: } } - // Embedded fetches take indirect records from the GPU copy: the first draws degenerate - // vertices 0-2, the second's vertex offset 7 selects the covering triangle. The CPU - // backing holds zeros. Other vertex shaders read the records on the CPU. + // Indirect records come from the GPU copy; the CPU backing holds zeros. In each pair the + // first record draws nothing and the second covers the target, through vertex offset 7 + // for embedded fetches. { constexpr uint64_t vertices_address = depth_address + 0x38000; constexpr uint64_t args_address = vertices_address + 0x100; @@ -18671,14 +18671,20 @@ public: std::memcpy(reinterpret_cast(vertices_address), positions.data(), sizeof(positions)); constexpr uint32_t stride = 32; - const vk::DrawIndexedIndirectCommand degenerate{3, 1, 0, 0, 5}; - const vk::DrawIndexedIndirectCommand covering{3, 1, 0, 7, 5}; - std::memcpy(reinterpret_cast(args_address), °enerate, sizeof(degenerate)); - std::memcpy(reinterpret_cast(args_address + stride), &covering, sizeof(covering)); + const vk::DrawIndexedIndirectCommand indexed_records[]{ + {3, 1, 0, 0, 5}, {3, 1, 0, 7, 5}, {0, 1, 0, 0, 0}, {3, 1, 0, 0, 0}}; + const vk::DrawIndirectCommand auto_records[]{{3, 1, 0, 5}, {3, 1, 7, 5}, {0, 1, 0, 0}, + {3, 1, 0, 0}}; + for (u32 i = 0; i < 4; i++) { + std::memcpy(reinterpret_cast(args_address + i * stride), &indexed_records[i], + sizeof(indexed_records[i])); + std::memcpy(reinterpret_cast(args_address + (4 + i) * stride), &auto_records[i], + sizeof(auto_records[i])); + } // Larger than a caching page, so the arguments are cached instead of streamed. (void)context.GetBufferCache().ObtainBuffer( vertices_address, BufferCache::CACHING_PAGESIZE * 2, false); - const std::array zeros{}; + const std::array zeros{}; Require("GpuIndirectDraw", "CPU backing", LibKernel::Memory::TryWriteBacking(args_address, zeros.data(), sizeof(zeros)), "the CPU copy of the arguments could not be cleared"); @@ -18719,26 +18725,29 @@ public: shaders.SetGsUserSgpr(i, static_cast(tables[i / 2] >> (i % 2 * 32)), HW::UserSgprType::Unknown); } - const DrawIndexArgs indirect{ - .index_count = 3, .index_addr = reinterpret_cast(index_address), - .index_type_and_size = 1, .indirect = {args_address, 2, stride}}; - shaders.SetEsShaderBase(fetch_vs); - TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), sparse_color.image_id, - {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, {}); - Require("GpuIndirectDraw", "embedded fetch", - RenderExecutorTestAccess::DrawIndex(executor, scheduler.Current(), indirect), - "an embedded-fetch indirect draw fell back to CPU arguments"); - const auto pixels = ReadCachedTexel(name, context, sparse_color.image_id, - {}, {extent, extent, 1}); - for (size_t component = 0; component < pixels.size(); component++) { - Require("GpuIndirectDraw", "vertex offset", - pixels[component] == (component % 4 == 3 ? 0x3f800000u : 0x3e800000u), - "the draw did not take every record, vertex offset included, from the GPU"); - } - shaders.SetEsShaderBase(indirect_vs); - Require("GpuIndirectDraw", "CPU fallback", - !RenderExecutorTestAccess::DrawIndex(executor, scheduler.Current(), indirect), - "GPU-read arguments reached a shader without embedded fetch"); + const auto check = [&](const char *step, uint64_t vertex_shader, bool indexed, u32 first) { + 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 bool drawn = indexed + ? RenderExecutorTestAccess::DrawIndex(executor, scheduler.Current(), + {.index_count = 3, .index_addr = reinterpret_cast(index_address), + .index_type_and_size = 1, .indirect = records}) + : RenderExecutorTestAccess::DrawAuto(executor, scheduler.Current(), + {.indirect = records}); + const auto pixels = ReadCachedTexel(name, context, sparse_color.image_id, + {}, {extent, extent, 1}); + for (size_t component = 0; component < pixels.size(); component++) { + Require("GpuIndirectDraw", step, + drawn && pixels[component] == (component % 4 == 3 ? 0x3f800000u : 0x3e800000u), + "the draw did not take every record, offsets included, from the GPU"); + } + }; + check("indexed embedded fetch", fetch_vs, true, 0); + check("auto embedded fetch", fetch_vs, false, 4); + check("indexed native fetch", vertex_address, true, 2); + check("auto native fetch", vertex_address, false, 6); } // A fourth VS invocation reads beyond the descriptor instead of reconstructing the corner.