graphics: draw patched-offset indirect draws from GPU-read records

This commit is contained in:
nmzik
2026-10-27 09:31:50 +01:00
parent 09753bf8ff
commit a5b3673a39
14 changed files with 118 additions and 21 deletions
+26 -7
View File
@@ -814,6 +814,27 @@ static void PatchIndirectDrawOffsets(CommandProcessor& cp, IndirectDrawRegisters
write(registers.draw_index, draw_index); 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<uint8_t>(index);
}
return true;
}
void CommandProcessor::DrawIndirect(uint32_t data_offset, IndirectDrawRegisters registers, void CommandProcessor::DrawIndirect(uint32_t data_offset, IndirectDrawRegisters registers,
uint32_t draw_initiator, bool indexed) { uint32_t draw_initiator, bool indexed) {
const auto args_size = 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. // 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 + m_num_instances_addr = args_addr + static_cast<uint64_t>(draw_count - 1) * stride_in_bytes +
offsetof(vk::DrawIndirectCommand, instanceCount); offsetof(vk::DrawIndirectCommand, instanceCount);
// With no SGPRs to patch, the GPU reads the records if the shaders allow. Shaders apply // The GPU reads the records if the shaders allow; the VS prolog loads the patched SGPRs from
// the records' offsets only through those SGPRs; Vulkan applies them, which matches // Vulkan's bases. With no SGPR patched, Vulkan applies the offsets, which matches while they
// while they are zero. // are zero.
const DrawIndirectArgs records {args_addr, draw_count, stride_in_bytes}; DrawIndirectArgs records {args_addr, draw_count, stride_in_bytes};
if (registers.vertex_offset == Pm4::SH_NOP && registers.instance_offset == Pm4::SH_NOP && if (IndirectDrawOffsets(registers, records.offsets) && (!indexed || m_index_buffer_size != 0) &&
registers.index_offset == Pm4::SH_NOP && registers.draw_index == Pm4::SH_NOP &&
(!indexed || m_index_buffer_size != 0) &&
(indexed ? executor.DrawIndex(m_submit_id, CurrentBuffer(), (indexed ? executor.DrawIndex(m_submit_id, CurrentBuffer(),
{.index_count = m_index_buffer_size, {.index_count = m_index_buffer_size,
.index_addr = IndexAddress(0), .index_addr = IndexAddress(0),
@@ -687,6 +687,9 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender
vertex_params[0] = PrepareProgram(vertex_regs, context, user_config, vertex_info[0]); vertex_params[0] = PrepareProgram(vertex_regs, context, user_config, vertex_info[0]);
} }
const bool mesh_active = vertex_info[0].logical_stage == ShaderType::Mesh; 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) { if (object_primitive_id) {
// CxObjectIdMode: register source plus a 32-bit pipelined primitive-ID addition. // CxObjectIdMode: register source plus a 32-bit pipelined primitive-ID addition.
EXIT_NOT_IMPLEMENTED(sh.m_paClObjprimIdCntl != 2u || mesh_active || tess_active || EXIT_NOT_IMPLEMENTED(sh.m_paClObjprimIdCntl != 2u || mesh_active || tess_active ||
@@ -137,6 +137,8 @@ struct DrawRenderState {
bool ps_active = false; bool ps_active = false;
std::array<ShaderVertexInputInfo, 3> vertex_info; std::array<ShaderVertexInputInfo, 3> vertex_info;
ShaderPixelInputInfo ps_input_info; ShaderPixelInputInfo ps_input_info;
// Input: the user-data slots a GPU-read indirect draw patches.
ShaderIndirectOffsets indirect_offsets;
[[nodiscard]] std::span<const ShaderVertexInputInfo> VertexStages() const { [[nodiscard]] std::span<const ShaderVertexInputInfo> VertexStages() const {
return {vertex_info.data(), vertex_info[1].stage ? 3u : 1u}; return {vertex_info.data(), vertex_info[1].stage ? 3u : 1u};
+4 -3
View File
@@ -45,9 +45,10 @@ enum class DrawOffsetSource : uint8_t {
// Nonzero address: the GPU reads count Vulkan indirect draw records, stride bytes apart. // Nonzero address: the GPU reads count Vulkan indirect draw records, stride bytes apart.
struct DrawIndirectArgs { struct DrawIndirectArgs {
uint64_t address = 0; uint64_t address = 0;
uint32_t count = 0; uint32_t count = 0;
uint32_t stride = 0; uint32_t stride = 0;
ShaderIndirectOffsets offsets;
}; };
struct DrawIndexArgs { struct DrawIndexArgs {
@@ -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 // Vulkan offsets for a prepared draw, or nullopt when its indirect records need the CPU: mesh
// mesh and quad-list draws do. Indirect records carry their own offsets. // and quad-list draws for counts, tessellation draws for patched SGPRs. Indirect records carry
// their own offsets.
static std::optional<DrawEmitInfo> ResolveDrawEmit(const HW::UserConfig& ucfg, static std::optional<DrawEmitInfo> ResolveDrawEmit(const HW::UserConfig& ucfg,
const ShaderVertexInputInfo& vs, const ShaderVertexInputInfo& vs,
DrawOffsetSource source, DrawOffsetSource source,
@@ -1260,7 +1261,8 @@ static std::optional<DrawEmitInfo> ResolveDrawEmit(const HW::UserConfig&
const DrawIndirectArgs& indirect) { const DrawIndirectArgs& indirect) {
if (indirect.address != 0) { if (indirect.address != 0) {
if (vs.stage.module->info.stage == ShaderType::Mesh || 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 std::nullopt;
} }
return DrawEmitInfo {.indirect = indirect}; return DrawEmitInfo {.indirect = indirect};
@@ -1324,7 +1326,7 @@ bool RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
hw_check(buffer); hw_check(buffer);
DrawRenderState state {}; DrawRenderState state {.indirect_offsets = args.indirect.offsets};
if (!GetDrawTopology(ucfg, state.topology)) { if (!GetDrawTopology(ucfg, state.topology)) {
return true; return true;
} }
@@ -1429,7 +1431,7 @@ bool RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
const DrawCallInfo draw {CommandBufferDebugOp::DrawIndexAuto, const DrawCallInfo draw {CommandBufferDebugOp::DrawIndexAuto,
args.vertex_count, args.instance_count, args.first_instance}; args.vertex_count, args.instance_count, args.first_instance};
DrawRenderState state {}; DrawRenderState state {.indirect_offsets = args.indirect.offsets};
if (!GetDrawTopology(ucfg, state.topology)) { if (!GetDrawTopology(ucfg, state.topology)) {
ResetBindings(); ResetBindings();
return true; return true;
@@ -51,6 +51,7 @@ struct VulkanExtensions {
vk::PhysicalDeviceVulkan11Features WindowContext::RequiredVulkan11Features() noexcept { vk::PhysicalDeviceVulkan11Features WindowContext::RequiredVulkan11Features() noexcept {
vk::PhysicalDeviceVulkan11Features features {}; vk::PhysicalDeviceVulkan11Features features {};
features.storageBuffer16BitAccess = VK_TRUE; features.storageBuffer16BitAccess = VK_TRUE;
features.shaderDrawParameters = VK_TRUE;
return features; return features;
} }
@@ -278,6 +279,8 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
required_features12.samplerMirrorClampToEdge); required_features12.samplerMirrorClampToEdge);
check_feature(features11.storageBuffer16BitAccess, "storageBuffer16BitAccess", check_feature(features11.storageBuffer16BitAccess, "storageBuffer16BitAccess",
WindowContext::RequiredVulkan11Features().storageBuffer16BitAccess); WindowContext::RequiredVulkan11Features().storageBuffer16BitAccess);
check_feature(features11.shaderDrawParameters, "shaderDrawParameters",
WindowContext::RequiredVulkan11Features().shaderDrawParameters);
check_feature(features12.storageBuffer8BitAccess, "storageBuffer8BitAccess", check_feature(features12.storageBuffer8BitAccess, "storageBuffer8BitAccess",
required_features12.storageBuffer8BitAccess); required_features12.storageBuffer8BitAccess);
check_feature(features12.timelineSemaphore, "timelineSemaphore", check_feature(features12.timelineSemaphore, "timelineSemaphore",
@@ -63,7 +63,8 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c
} }
if (kind == IR::StageInputKind::VertexIndex || kind == IR::StageInputKind::InstanceIndex || if (kind == IR::StageInputKind::VertexIndex || kind == IR::StageInputKind::InstanceIndex ||
kind == IR::StageInputKind::InvocationId || kind == IR::StageInputKind::PrimitiveId || 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 value = state.builder.AllocateId();
const auto bits = state.builder.AllocateId(); const auto bits = state.builder.AllocateId();
state.builder.AddFunction(spv::OpLoad, TypeI32(state), value, variable); state.builder.AddFunction(spv::OpLoad, TypeI32(state), value, variable);
@@ -476,6 +476,8 @@ uint32_t BuiltInForInput(IR::StageInputKind kind) {
case IR::StageInputKind::LocalInvocationId: return spv::BuiltInLocalInvocationId; case IR::StageInputKind::LocalInvocationId: return spv::BuiltInLocalInvocationId;
case IR::StageInputKind::LocalInvocationIndex: return spv::BuiltInLocalInvocationIndex; case IR::StageInputKind::LocalInvocationIndex: return spv::BuiltInLocalInvocationIndex;
case IR::StageInputKind::GlobalInvocationId: return spv::BuiltInGlobalInvocationId; 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; default: return UINT32_MAX;
} }
} }
@@ -526,7 +528,9 @@ void DefineInputs(EmitterState& state) {
case IR::StageInputKind::PrimitiveId: case IR::StageInputKind::PrimitiveId:
case IR::StageInputKind::InstanceIndex: case IR::StageInputKind::InstanceIndex:
case IR::StageInputKind::Layer: 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::WorkgroupId:
case IR::StageInputKind::NumWorkgroups: case IR::StageInputKind::NumWorkgroups:
case IR::StageInputKind::LocalInvocationId: case IR::StageInputKind::LocalInvocationId:
@@ -802,6 +806,10 @@ void DefineModule(EmitterState& state) {
if (InputVariableForKind(state, IR::StageInputKind::SampleId) != 0) { if (InputVariableForKind(state, IR::StageInputKind::SampleId) != 0) {
state.builder.RequireCapability(spv::CapabilitySampleRateShading); 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) { if (state.program.info.image_gather_extended) {
state.builder.RequireCapability(spv::CapabilityImageGatherExtended); state.builder.RequireCapability(spv::CapabilityImageGatherExtended);
} }
@@ -1389,10 +1389,27 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
IR::U32(IR::Value(options.wave_size << 12u))); IR::U32(IR::Value(options.wave_size << 12u)));
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(3), entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(3),
entry_ir.BitwiseOr(vertex_count, IR::U32(IR::Value(1u << 28u)))); 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<IR::ScalarReg>(options.user_data_base + slot),
base);
}
return entry_ir.ISub(builtin(kind), base);
};
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(5), entry_ir.SetVectorReg(static_cast<IR::VectorReg>(5),
builtin(IR::StageInputKind::VertexIndex)); index(offsets.vertex, IR::StageInputKind::VertexIndex,
IR::StageInputKind::BaseVertex));
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(8), entry_ir.SetVectorReg(static_cast<IR::VectorReg>(8),
builtin(IR::StageInputKind::InstanceIndex)); index(offsets.instance, IR::StageInputKind::InstanceIndex,
IR::StageInputKind::BaseInstance));
} }
} }
const bool flush_f32_inputs = options.stage == ShaderType::Compute && const bool flush_f32_inputs = options.stage == ShaderType::Compute &&
@@ -251,6 +251,8 @@ enum class StageInputKind {
LocalInvocationId, LocalInvocationId,
LocalInvocationIndex, LocalInvocationIndex,
GlobalInvocationId, GlobalInvocationId,
BaseVertex,
BaseInstance,
Parameter, Parameter,
}; };
@@ -175,6 +175,8 @@ void AddBuiltinInput(ShaderInfo& info, StageInputKind kind) {
case StageInputKind::GlobalInvocationId: case StageInputKind::GlobalInvocationId:
AddInput(info, kind, 0, 3, "gl_GlobalInvocationID"); AddInput(info, kind, 0, 3, "gl_GlobalInvocationID");
break; 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::PackedAncillary:
case StageInputKind::Parameter: break; case StageInputKind::Parameter: break;
} }
@@ -444,6 +446,8 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
case StageInputKind::InstanceIndex: case StageInputKind::InstanceIndex:
case StageInputKind::FrontFacing: case StageInputKind::FrontFacing:
case StageInputKind::LocalInvocationIndex: case StageInputKind::LocalInvocationIndex:
case StageInputKind::BaseVertex:
case StageInputKind::BaseInstance:
if (component != 0u) { if (component != 0u) {
return Fail("typed scalar builtin component is out of range"); return Fail("typed scalar builtin component is out of range");
} }
+1
View File
@@ -422,6 +422,7 @@ void BuildStageStaticKey(const ShaderVertexInputInfo& info, std::vector<uint32_t
key.push_back(info.lds_size_dwords); key.push_back(info.lds_size_dwords);
key.push_back(info.pa_cl_vs_out_cntl); key.push_back(info.pa_cl_vs_out_cntl);
key.push_back(info.user_clip_planes); key.push_back(info.user_clip_planes);
key.push_back(info.indirect_offsets.vertex | (uint32_t {info.indirect_offsets.instance} << 8u));
key.push_back(static_cast<uint32_t>(info.clip_space.enabled)); key.push_back(static_cast<uint32_t>(info.clip_space.enabled));
if (info.clip_space.enabled) { if (info.clip_space.enabled) {
for (const float value: info.clip_space.scale) { for (const float value: info.clip_space.scale) {
+13
View File
@@ -124,6 +124,18 @@ struct ShaderTessellationInputInfo {
uint32_t output_topology = 0; 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 { struct ShaderVertexInputInfo {
static constexpr int RES_MAX = 32; static constexpr int RES_MAX = 32;
@@ -146,6 +158,7 @@ struct ShaderVertexInputInfo {
ShaderClipSpaceTransform clip_space; ShaderClipSpaceTransform clip_space;
ShaderMeshInputInfo mesh; ShaderMeshInputInfo mesh;
ShaderTessellationInputInfo tess; ShaderTessellationInputInfo tess;
ShaderIndirectOffsets indirect_offsets;
bool fetch_external = false; bool fetch_external = false;
bool fetch_embedded = false; bool fetch_embedded = false;
}; };
+23 -2
View File
@@ -19763,11 +19763,12 @@ public:
for (u32 i = 0; i < 4; i++) { for (u32 i = 0; i < 4; i++) {
shaders.SetGsUserSgpr(i, static_cast<u32>(tables[i / 2] >> (i % 2 * 32))); shaders.SetGsUserSgpr(i, static_cast<u32>(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); shaders.SetEsShaderBase(vertex_shader);
TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), sparse_color.image_id, TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), sparse_color.image_id,
{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, {}); {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 const bool drawn = indexed
? RenderExecutorTestAccess::DrawIndex(executor, scheduler.Current(), ? RenderExecutorTestAccess::DrawIndex(executor, scheduler.Current(),
{.index_count = 3, .index_addr = reinterpret_cast<const void *>(index_address), {.index_count = 3, .index_addr = reinterpret_cast<const void *>(index_address),
@@ -19793,6 +19794,16 @@ public:
"no cached graphics pipeline named only compiled modules"); "no cached graphics pipeline named only compiled modules");
check("recreated indexed embedded fetch", fetch_vs, true, 0); check("recreated indexed embedded fetch", fetch_vs, true, 0);
check("recreated auto native fetch", vertex_address, false, 6); 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. // 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; auto_packet[3] = Pm4::SH_NOP;
arguments[3] = 103; arguments[3] = 103;
check_offsets(auto_packet, hs, {53, 101}, "disabled destinations"); 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(); processor.BufferFlush();
std::printf("[host] %-32s ok\n", "Pm4DrawIndirectMulti"); std::printf("[host] %-32s ok\n", "Pm4DrawIndirectMulti");
} }