graphics: draw non-indexed and native-fetch indirect draws from GPU-read records

This commit is contained in:
nmzik
2026-10-22 15:11:39 +02:00
parent 949de19bb0
commit 86fed10b81
4 changed files with 118 additions and 99 deletions
+12 -8
View File
@@ -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<uint64_t>(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;
}
+4 -3
View File
@@ -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<PreparedBindings* const> 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<PreparedBindings, 3> vertex;
+63 -58
View File
@@ -631,9 +631,9 @@ bool RenderExecutor::ConsumeMetadataColorOperation(CommandBuffer& buffer,
mode == static_cast<uint8_t>(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<uint32_t>(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<uint32_t>(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<Buffer*, uint64_t> args_buffer {};
if (emit.indirect.address != 0) {
const auto size = static_cast<uint64_t>(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<uint32_t, PushData::MeshDrawDwordCount> draw_data {};
if (mesh_active) {
draw_data = {
draw.index_count,
draw.IsIndexed() ? static_cast<uint32_t>(emit.vertex_offset) : emit.first_vertex,
emit.first_instance, index_source.guest_element_size,
static_cast<uint32_t>(index_source.address),
static_cast<uint32_t>(index_source.address >> 32u)};
draw_data = {draw.index_count,
static_cast<uint32_t>(emit.vertex_offset),
emit.first_instance,
index_source.guest_element_size,
static_cast<uint32_t>(index_source.address),
static_cast<uint32_t>(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<DrawEmitInfo> 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<int32_t, uint32_t> {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<int32_t, uint32_t> {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<uint32_t>(vertex_offset + static_cast<int32_t>(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<int32_t>(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) {
+39 -30
View File
@@ -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<void *>(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<void *>(args_address), &degenerate, sizeof(degenerate));
std::memcpy(reinterpret_cast<void *>(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<void *>(args_address + i * stride), &indexed_records[i],
sizeof(indexed_records[i]));
std::memcpy(reinterpret_cast<void *>(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<u32, 2 * stride / 4> zeros{};
const std::array<u32, 8 * stride / 4> 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<u32>(tables[i / 2] >> (i % 2 * 32)),
HW::UserSgprType::Unknown);
}
const DrawIndexArgs indirect{
.index_count = 3, .index_addr = reinterpret_cast<const void *>(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<const void *>(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.