perf(agc): draw mesh-stage indirect draws on the GPU

A mesh program took its draw's index count from push constants, so an
indirect mesh-path (NGG) draw needed the CPU to read its record first,
waiting for the GPU to finish everything recorded before. In PPSA21564
that was about 170 waits per 10 s at the title, menu and level (about
1 s), and one per frame in the intro (2.2 s per 10 s).

The two free mesh push dwords now carry the device address of an argument
record (a VkDrawMeshTasksIndirectCommandEXT followed by the index count
and first index). When the address is set, the prologue takes the index
count and the first index from the record (the new MeshArgument IR value,
a load through a physical storage buffer pointer) and offsets its index
reads by the first index; a direct draw leaves the address 0 and draws as
before. Mesh modules declare the physical storage buffer capability for
it, and the driver's SPIR-V validation accepts the extension in mesh
modules.

An indexed mesh draw with one record that patches no user SGPR now stays
on the GPU. A one-thread compute pass (Graphics/shaders/MeshArguments.comp)
turns the record, read in place from its host import, into the draw's
argument record: the groups and instances of vkCmdDrawMeshTasksIndirectEXT
and the index count and first index the mesh program reads, with the CPU
path's rules (count clamped to the index buffer, nothing for an empty
record or past the device's group limits). The index buffer descriptor
covers the whole buffer and the program offsets by the first index. When
the records cannot be read in place, the CPU reads them and fills the
same argument record (ResolveMeshArguments).
This commit is contained in:
Dean Galvin 2026-10-04 03:02:27 +13:00
parent dca710d602
commit fb55cb2c0e
19 changed files with 332 additions and 20 deletions

View File

@ -46,6 +46,19 @@ add_custom_command(
VERBATIM
)
set(agcMeshArgumentsShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/MeshArguments.comp)
set(agcMeshArgumentsShaderSpv ${agcTextureDetileGeneratedDir}/MeshArguments.spv)
set(agcMeshArgumentsShaderHeader ${agcTextureDetileGeneratedDir}/MeshArguments_spv.h)
add_custom_command(
OUTPUT "${agcMeshArgumentsShaderHeader}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}"
COMMAND "$<TARGET_FILE:glslang-standalone>" -V --target-env vulkan1.1 -o "${agcMeshArgumentsShaderSpv}" "${agcMeshArgumentsShaderSource}"
COMMAND ${CMAKE_COMMAND} -DINPUT=${agcMeshArgumentsShaderSpv} -DOUTPUT=${agcMeshArgumentsShaderHeader}
-DSYMBOL=MESH_ARGUMENTS_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${agcMeshArgumentsShaderSource}" glslang-standalone
VERBATIM
)
set(agcDriverSources
${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/DisplayBuffer.cpp
@ -130,6 +143,7 @@ set(agcDriverSources
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/UnitShadow.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Sampler.cpp
${agcTextureDetileShaderHeader}
${agcMeshArgumentsShaderHeader}
${CMAKE_CURRENT_SOURCE_DIR}/Eq/src/Event.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Eq/src/Query.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Resource/src/Lifecycle.cpp
@ -388,6 +402,7 @@ add_test_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL
Graphics/src/Recorder.cpp
Graphics/src/ShaderInputState.cpp
${agcTextureDetileShaderHeader}
${agcMeshArgumentsShaderHeader}
Execution/src/GuestMemory.cpp
Execution/src/Pm4.cpp
)

View File

@ -19,7 +19,10 @@ std::optional<Graphics::IndirectDrawPath> Driver::classifyIndirectDraw(const Sha
using Path = Graphics::IndirectDrawPath;
const auto support = localDevice->DrawIndirectSupport();
const auto fail = [&](Path reason) { if (!indirectCpu) indirectCpu = reason; };
if (graphics.stages.path != Graphics::ShaderPath::Vertex || graphics.rectList) fail(Path::NonVertexPath);
constexpr std::uint32_t NoLocation = 0x280u;
const bool meshPath = graphics.stages.mesh.has_value() && !graphics.stages.tessellation && !graphics.rectList;
const bool meshGpuSide = meshPath && drawParameters.indexed && indirect.count == 1 && !indirect.countIndirect && indirect.baseVertexLocation == NoLocation && indirect.startInstanceLocation == NoLocation && (!indirect.drawIndexEnabled || indirect.drawIndexLocation == NoLocation);
if ((graphics.stages.path != Graphics::ShaderPath::Vertex || graphics.rectList) && !meshGpuSide) fail(Path::NonVertexPath);
if (indirect.drawIndexEnabled && indirect.drawIndexSgpr >= 0) fail(Path::DrawIndex);
if (indirect.countIndirect && !support.count) fail(Path::FeatureGap);
if (indirect.countIndirect && indirect.count > 1 && !support.multi) fail(Path::FeatureGap);

View File

@ -30,6 +30,26 @@ void KeepDrawInput(Recorder* recorder, std::uint64_t address, const DrawInputCop
std::array<std::uint32_t, 4> MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw, std::uint64_t unreadAddress);
struct MeshArguments {
std::uint32_t groups;
std::uint32_t instances;
std::uint32_t layers;
std::uint32_t indexCount;
std::uint32_t firstIndex;
};
static_assert(sizeof(MeshArguments) == ShaderRecompiler::MeshArgumentBytes);
struct MeshArgumentRules {
std::uint32_t indexCount;
std::uint32_t inputSize;
std::uint32_t step;
std::uint32_t primitivesPerGroup;
std::uint32_t maxGroups;
std::uint32_t maxInstances;
std::uint32_t maxTotal;
};
MeshArgumentRules MeshArgumentRulesFor(const Context& context, const ShaderRecompiler::MeshConfiguration& mesh, std::uint32_t indexCount);
MeshArguments ResolveMeshArguments(const Pm4::DrawArguments& record, const MeshArgumentRules& rules);
// 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

View File

@ -179,6 +179,7 @@ public:
void Sync();
void CountSamples();
static std::uint64_t SamplesPassed();
bool RecordMeshArguments(VkCommandBuffer commands, VkDeviceAddress record, VkDeviceAddress arguments, std::span<const std::uint32_t, 7> rules);
// Waits only for the batches up to the newest one that writes the range (submitting the open
// batch when it is that one); later batches stay in flight. Fences of one queue signal in
// submission order, so completions still run in order. Debug aid: APS5_NO_SYNC_THROUGH=1 syncs all.
@ -603,6 +604,9 @@ private:
void beginSamples(Batch& batch);
void readSamples(Batch& batch);
bool countingSamples = false;
int meshArgumentState = 0;
VkPipelineLayout meshArgumentLayout = VK_NULL_HANDLE;
VkPipeline meshArgumentPipeline = VK_NULL_HANDLE;
// BeginGpuTiming on the open batch without Commands() (RecordStore times its own run, which
// Commands() would close).
std::uint32_t beginTiming(std::uint64_t key);

View File

@ -0,0 +1,34 @@
#version 450
#extension GL_EXT_buffer_reference : require
layout(local_size_x = 1) in;
layout(buffer_reference, std430, buffer_reference_align = 4) buffer Words { uint words[]; };
layout(push_constant) uniform Parameters {
Words record;
Words arguments;
uint indexCount;
uint inputSize;
uint step;
uint primitivesPerGroup;
uint maxGroups;
uint maxInstances;
uint maxTotal;
uint unused;
} parameters;
void main() {
uint count = parameters.record.words[0];
uint instances = parameters.record.words[1];
uint first = parameters.record.words[2];
uint effective = first < parameters.indexCount ? min(count, parameters.indexCount - first) : 0u;
uint groups = 0u;
if (effective >= parameters.inputSize && instances != 0u) {
groups = (effective - parameters.inputSize) / parameters.step / parameters.primitivesPerGroup + 1u;
if (groups > parameters.maxGroups || instances > parameters.maxInstances || groups > parameters.maxTotal / instances) groups = 0u;
}
parameters.arguments.words[0] = groups;
parameters.arguments.words[1] = groups != 0u ? instances : 0u;
parameters.arguments.words[2] = groups != 0u ? 1u : 0u;
parameters.arguments.words[3] = effective;
parameters.arguments.words[4] = first;
}

View File

@ -594,6 +594,24 @@ std::array<std::uint32_t, 4> MeshIndexBufferDescriptor(const Pm4::DrawParameters
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u) & 0xffffu, static_cast<std::uint32_t>(bytes), RawWord3};
}
MeshArgumentRules MeshArgumentRulesFor(const Context& context, const ShaderRecompiler::MeshConfiguration& mesh, std::uint32_t indexCount) {
const auto inputSize = mesh.inputPrimitive == 1 ? 1u : mesh.inputPrimitive == 2 ? 2u : 3u;
const auto step = mesh.inputPrimitive == 5 || mesh.inputPrimitive == 6 ? 1u : inputSize;
Require(mesh.primitivesPerGroup != 0, "mesh draw contains no complete primitive");
return {indexCount, inputSize, step, mesh.primitivesPerGroup, context.meshLimits.maxMeshWorkGroupCount[0], context.meshLimits.maxMeshWorkGroupCount[1], context.meshLimits.maxMeshWorkGroupTotalCount};
}
MeshArguments ResolveMeshArguments(const Pm4::DrawArguments& record, const MeshArgumentRules& rules) {
const auto first = record.firstVertexOrIndex;
const auto effective = first < rules.indexCount ? std::min(record.count, rules.indexCount - first) : 0u;
std::uint32_t groups = 0;
if (effective >= rules.inputSize && record.instances != 0) {
groups = (effective - rules.inputSize) / rules.step / rules.primitivesPerGroup + 1u;
if (groups > rules.maxGroups || record.instances > rules.maxInstances || groups > rules.maxTotal / record.instances) groups = 0;
}
return {groups, groups != 0 ? record.instances : 0u, groups != 0 ? 1u : 0u, effective, first};
}
std::shared_ptr<std::vector<std::shared_ptr<ShaderResources>>> 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::vector<std::shared_ptr<ShaderResources>>>(std::make_shared<std::vector<std::shared_ptr<ShaderResources>>>());
@ -744,7 +762,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
}
}
if (args == nullptr) Require(draw.indexCount != 0 && draw.instanceCount != 0, "zero-count indexed draws are unsupported");
else Require(!state.stages.mesh && !state.stages.tessellation && !state.rectList, "indirect draw on a non-vertex path must be resolved by the driver");
else Require((!state.stages.mesh || draw.indexed) && !state.stages.tessellation && !state.rectList, "indirect draw on a non-vertex path must be resolved by the driver");
inputs.indexBytes = static_cast<std::uint64_t>(draw.indexCount) * draw.indexSize;
const auto indexBytes = inputs.indexBytes;
APS5_LOG_OUT_DEBUG("Index buffer bytes=%llu", static_cast<unsigned long long>(indexBytes));
@ -769,11 +787,13 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
const auto& mesh = *state.stages.mesh;
const auto inputSize = mesh.inputPrimitive == 1 ? 1u : mesh.inputPrimitive == 2 ? 2u : 3u;
Require(draw.indexCount >= inputSize && mesh.primitivesPerGroup != 0, "mesh draw contains no complete primitive");
const auto step = mesh.inputPrimitive == 5 || mesh.inputPrimitive == 6 ? 1u : inputSize;
const auto primitives = (draw.indexCount - inputSize) / step + 1u;
inputs.meshGroups = (primitives - 1u) / mesh.primitivesPerGroup + 1u;
APS5_LOG_OUT_DEBUG("Mesh primitives=%u groups=%u", primitives, inputs.meshGroups);
Require(inputs.meshGroups <= context.meshLimits.maxMeshWorkGroupCount[0] && draw.instanceCount <= context.meshLimits.maxMeshWorkGroupCount[1] && static_cast<std::uint64_t>(inputs.meshGroups) * draw.instanceCount <= context.meshLimits.maxMeshWorkGroupTotalCount, "mesh draw exceeds workgroup count limits");
if (args == nullptr) {
const auto step = mesh.inputPrimitive == 5 || mesh.inputPrimitive == 6 ? 1u : inputSize;
const auto primitives = (draw.indexCount - inputSize) / step + 1u;
inputs.meshGroups = (primitives - 1u) / mesh.primitivesPerGroup + 1u;
APS5_LOG_OUT_DEBUG("Mesh primitives=%u groups=%u", primitives, inputs.meshGroups);
Require(inputs.meshGroups <= context.meshLimits.maxMeshWorkGroupCount[0] && draw.instanceCount <= context.meshLimits.maxMeshWorkGroupCount[1] && static_cast<std::uint64_t>(inputs.meshGroups) * draw.instanceCount <= context.meshLimits.maxMeshWorkGroupTotalCount, "mesh draw exceeds workgroup count limits");
}
}
if (state.stages.tessellation) Require(draw.indexCount % state.stages.tessellation->inputControlPoints == 0, "incomplete tessellation patch");
// Viewport and scissor are dynamic pipeline state, so their limits are checked here per draw.
@ -943,6 +963,10 @@ struct IndirectRecord {
// GPU-side indirect draw from `argumentBuffer` or the CPU-read records with the driver's rules.
void recordDrawCommands(const Context& context, VkCommandBuffer commands, const State& state, const Pm4::DrawParameters& draw, const DrawInputs& inputs, const IndirectRecord* indirect, VkBuffer argumentBuffer, VkDeviceSize argumentOffset) {
const auto* args = indirect != nullptr ? indirect->args : nullptr;
if (state.stages.mesh && args != nullptr) {
context.Function<PFN_vkCmdDrawMeshTasksIndirectEXT>("vkCmdDrawMeshTasksIndirectEXT")(commands, argumentBuffer, argumentOffset, 1, sizeof(VkDrawMeshTasksIndirectCommandEXT));
return;
}
if (state.stages.mesh) {
APS5_LOG_OUT_DEBUG("vkCmdDrawMeshTasksEXT groups=%u instances=%u", inputs.meshGroups, draw.instanceCount);
context.Function<PFN_vkCmdDrawMeshTasksEXT>("vkCmdDrawMeshTasksEXT")(commands, inputs.meshGroups, draw.instanceCount, 1);
@ -1025,6 +1049,24 @@ bool recordIndirectArguments(const Context& context, VkCommandBuffer commands, R
return true;
}
std::shared_ptr<Buffer> recordMeshArguments(const Context& context, VkCommandBuffer commands, Recorder* recorder, bool recorded, const State& state, const Pm4::DrawParameters& draw, const IndirectRecord& indirect, const std::function<void(std::uint32_t)>& countBarrier) {
const auto* args = indirect.args;
const auto rules = MeshArgumentRulesFor(context, *state.stages.mesh, draw.indexCount);
auto arguments = std::make_shared<Buffer>(context, ShaderRecompiler::MeshArgumentBytes, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT);
if (indirect.path == IndirectDrawPath::Gpu && recorder != nullptr && indirect.argumentImport->address != 0) {
const std::array<std::uint32_t, 7> words{rules.indexCount, rules.inputSize, rules.step, rules.primitivesPerGroup, rules.maxGroups, rules.maxInstances, rules.maxTotal};
if (recorder->RecordMeshArguments(commands, indirect.argumentImport->address + (args->arguments - indirect.argumentImport->base), arguments->DeviceAddress(), words)) {
countBarrier(2);
if (recorded) recorder->NotePendingRead(args->arguments, static_cast<std::size_t>(args->RangeBytes()), Recorder::ReadKind::Indirect);
return arguments;
}
}
const auto record = !indirect.records.empty() ? indirect.records.front() : Pm4::ReadDrawArguments(*args, 0);
const auto resolved = ResolveMeshArguments(record, rules);
std::memcpy(arguments->Bytes().data(), &resolved, sizeof(resolved));
return arguments;
}
// Debug aid: APS5_CHECK_INDIRECT_ARGS=1 reads the records of a GPU-side draw back on the CPU once
// its batch completed (the GPU has finished writing them by then) and prints the first 16, counting
// records whose Constant dimension holds a value the hardware would have ignored. Empty otherwise.
@ -1123,7 +1165,7 @@ struct RecordedDraw {
VkShaderStageFlags pushStages = 0;
};
void pushDrawConstants(const Pipeline& pipeline, VkCommandBuffer commands, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, const ShaderResources& resources, const std::array<std::byte, PipelinePushConstantBytes>* bytes, VkShaderStageFlags stages) {
void pushDrawConstants(const Pipeline& pipeline, VkCommandBuffer commands, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, const ShaderResources& resources, const std::array<std::byte, PipelinePushConstantBytes>* bytes, VkShaderStageFlags stages, VkDeviceAddress meshArguments = 0) {
auto block = bytes != nullptr ? *bytes : AssemblePushConstants(shaders);
if (bytes == nullptr) {
resources.PatchPushConstants(block);
@ -1133,7 +1175,9 @@ void pushDrawConstants(const Pipeline& pipeline, VkCommandBuffer commands, const
pipeline.PushConstants(commands, stages, block);
return;
}
const std::array<std::uint32_t, ShaderRecompiler::MeshDrawPushBytes / 4> words{draw.indexCount, draw.firstVertex, draw.firstInstance, draw.indexed ? draw.indexSize : 0u, 0u, 0u};
const auto firstVertex = draw.indirect ? draw.indirect->vertexConstant : draw.firstVertex;
const auto firstInstance = draw.indirect ? draw.indirect->instanceConstant : draw.firstInstance;
const std::array<std::uint32_t, ShaderRecompiler::MeshDrawPushBytes / 4> words{draw.indexCount, firstVertex, firstInstance, draw.indexed ? draw.indexSize : 0u, static_cast<std::uint32_t>(meshArguments), static_cast<std::uint32_t>(meshArguments >> 32u)};
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);
@ -1257,7 +1301,8 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
if (recorder->HasQueuedStores() && (resources.HoldsLease() || recorder->AnyQueuedStore(touches))) recorder->FlushStores();
const auto drawBindings = resources.PrepareDrawBindings(*recorder);
const bool capture = CaptureInputsEnabled();
const bool continued = !capture && !readsTarget && !gpuIndirect && recorder->ContinuesRenderPass(passKey);
const bool meshIndirect = state.stages.mesh && args != nullptr;
const bool continued = !capture && !readsTarget && !gpuIndirect && !meshIndirect && recorder->ContinuesRenderPass(passKey);
outcome.passContinued = continued;
outcome.passBegun = !continued;
const auto commands = continued ? recorder->CommandsInRenderPass() : recorder->Commands();
@ -1276,6 +1321,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
recorder->NoteAccess(CommandClass::Draw, Recorder::Access{reads, resources.GpuWrites(), images, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, resources.HoldsLease()});
}
std::unique_ptr<DeviceBuffer> scratch;
std::shared_ptr<Buffer> meshArguments;
VkBuffer argumentBuffer = VK_NULL_HANDLE;
VkDeviceSize argumentOffset = 0;
bool rewritten = false;
@ -1286,7 +1332,12 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, 0, 1, &before, 0, nullptr, 0, nullptr);
countBarrier(1);
APS5_LOG_CHARS_OUT_DEBUG("Upload barrier recorded");
if (gpuIndirect) rewritten = recordIndirectArguments(context, commands, recorder, true, *record.indirect, scratch, argumentBuffer, argumentOffset, countBarrier);
if (meshIndirect) {
meshArguments = recordMeshArguments(context, commands, recorder, true, state, draw, *record.indirect, countBarrier);
argumentBuffer = meshArguments->Handle();
} else if (gpuIndirect) {
rewritten = recordIndirectArguments(context, commands, recorder, true, *record.indirect, scratch, argumentBuffer, argumentOffset, countBarrier);
}
// Earlier recorded work (dispatches, the previous draw) wrote the images in the general
// layout, which a lean draw renders in: no transitions.
APS5_LOG_OUT_DEBUG("Beginning pipeline renderExtent=%ux%u", state.renderExtent.width, state.renderExtent.height);
@ -1300,9 +1351,10 @@ 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");
pushDrawConstants(*record.pipeline, commands, state, draw, shaders, resources, record.pushBytes, record.pushStages);
pushDrawConstants(*record.pipeline, commands, state, draw, shaders, resources, record.pushBytes, record.pushStages, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0);
APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded");
recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset);
if (meshArguments != nullptr) recorder->Keep(meshArguments);
if (args != nullptr) CountIndirectDraw(record.indirect->path, record.indirect->readMs, rewritten);
auto checkRecords = indirectRecordCheck(record.indirect);
APS5_LOG_CHARS_OUT_DEBUG("Draw recorded");
@ -1704,7 +1756,13 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
countBarrier();
}
APS5_LOG_CHARS_OUT_DEBUG("Upload barrier recorded");
if (gpuIndirect) rewritten = recordIndirectArguments(context, commands, recorder, recorded, indirect, scratch, argumentBuffer, argumentOffset, countBarrier);
std::shared_ptr<Buffer> meshArguments;
if (state.stages.mesh && args != nullptr) {
meshArguments = recordMeshArguments(context, commands, recorder, recorded, state, draw, indirect, countBarrier);
argumentBuffer = meshArguments->Handle();
} else if (gpuIndirect) {
rewritten = recordIndirectArguments(context, commands, recorder, recorded, indirect, scratch, argumentBuffer, argumentOffset, countBarrier);
}
for (auto& binding : targets) {
if (binding.resident != nullptr) {
imageBarrier(context, commands, binding.resident->Image(), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);
@ -1731,9 +1789,10 @@ 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");
pushDrawConstants(*pipeline, commands, state, draw, shaders, *resources, nullptr, 0);
pushDrawConstants(*pipeline, commands, state, draw, shaders, *resources, nullptr, 0, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0);
APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded");
recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset);
if (meshArguments != nullptr && recorded) recorder->Keep(meshArguments);
if (args != nullptr) CountIndirectDraw(indirect.path, indirect.readMs, rewritten);
auto checkRecords = indirectRecordCheck(args != nullptr ? &indirect : nullptr);
APS5_LOG_CHARS_OUT_DEBUG("Draw recorded");

View File

@ -3,6 +3,7 @@
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libSceAgcDriver/Graphics/shaders/MeshArguments_spv.h"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp"
@ -1094,6 +1095,8 @@ Recorder::~Recorder() {
std::lock_guard tableLock(labelTableMutex);
if (labelTableOwner == this) labelTableOwner = nullptr;
}
if (meshArgumentPipeline != VK_NULL_HANDLE) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, meshArgumentPipeline, nullptr);
if (meshArgumentLayout != VK_NULL_HANDLE) context.Function<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout")(context.device, meshArgumentLayout, nullptr);
for (auto& [commands, fence] : spare) {
context.Function<PFN_vkFreeCommandBuffers>("vkFreeCommandBuffers")(context.device, context.pool, 1, &commands);
context.Function<PFN_vkDestroyFence>("vkDestroyFence")(context.device, fence, nullptr);
@ -1817,6 +1820,51 @@ std::uint64_t Recorder::SamplesPassed() {
return samplesPassed.load(std::memory_order_acquire);
}
bool Recorder::RecordMeshArguments(VkCommandBuffer commands, VkDeviceAddress record, VkDeviceAddress arguments, std::span<const std::uint32_t, 7> rules) {
constexpr std::uint32_t PushBytes = 48;
if (meshArgumentState == 0) {
meshArgumentState = -1;
VkShaderModule module = VK_NULL_HANDLE;
try {
const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, PushBytes};
VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
layoutInfo.pushConstantRangeCount = 1;
layoutInfo.pPushConstantRanges = &push;
Check(context.Function<PFN_vkCreatePipelineLayout>("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &meshArgumentLayout), "vkCreatePipelineLayout mesh arguments");
VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
moduleInfo.codeSize = sizeof(MESH_ARGUMENTS_SPV);
moduleInfo.pCode = MESH_ARGUMENTS_SPV;
Check(context.Function<PFN_vkCreateShaderModule>("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule mesh arguments");
VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
pipelineInfo.stage.module = module;
pipelineInfo.stage.pName = "main";
pipelineInfo.layout = meshArgumentLayout;
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &meshArgumentPipeline), "vkCreateComputePipelines mesh arguments");
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
meshArgumentState = 1;
} catch (const std::exception& error) {
if (module != VK_NULL_HANDLE) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
std::fprintf(stderr, "[gpu] mesh indirect draws read their records on the CPU: %s\n", error.what());
}
}
if (meshArgumentState < 0) return false;
recordBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT);
context.Function<PFN_vkCmdBindPipeline>("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, meshArgumentPipeline);
struct {
VkDeviceAddress record;
VkDeviceAddress arguments;
std::array<std::uint32_t, 8> rules;
} parameters{record, arguments, {rules[0], rules[1], rules[2], rules[3], rules[4], rules[5], rules[6], 0u}};
static_assert(sizeof(parameters) == PushBytes);
context.Function<PFN_vkCmdPushConstants>("vkCmdPushConstants")(commands, meshArgumentLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, PushBytes, &parameters);
context.Function<PFN_vkCmdDispatch>("vkCmdDispatch")(commands, 1, 1, 1);
recordBarrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT | VK_PIPELINE_STAGE_MESH_SHADER_BIT_EXT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_SHADER_READ_BIT);
CountBarriers(CommandClass::Draw, 2);
return true;
}
void Recorder::beginSamples(Batch& batch) {
VkQueryPoolCreateInfo info{VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO};
info.queryType = VK_QUERY_TYPE_OCCLUSION;

View File

@ -251,7 +251,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
Require(fragment && fragmentShaderBarycentric, "SPV_KHR_fragment_shader_barycentric requires enabled fragmentShaderBarycentric in a fragment shader");
break;
}
Require(extension == "SPV_KHR_float_controls" || (mesh && extension == "SPV_EXT_mesh_shader") || (descriptorIndexing && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension");
Require(extension == "SPV_KHR_float_controls" || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || (descriptorIndexing && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension");
break;
}
case spv::OpDecorateId:

View File

@ -1660,6 +1660,39 @@ bool recompilesDebugBranch(std::uint32_t opcode) {
return !ShaderRecompiler::Recompile(request).spirv.Words().empty();
}
void meshArgumentTests() {
using AgcDriver::Graphics::MeshArguments;
using AgcDriver::Graphics::ResolveMeshArguments;
AgcDriver::Graphics::Context context{};
context.meshLimits.maxMeshWorkGroupCount[0] = 1000;
context.meshLimits.maxMeshWorkGroupCount[1] = 600;
context.meshLimits.maxMeshWorkGroupTotalCount = 4000;
const ShaderRecompiler::MeshConfiguration points{1u, 1u, 1u, 1u, 1u, 64u, 1024u, 0u, 4u};
const ShaderRecompiler::MeshConfiguration triangles{4u, 32u, 96u, 96u, 32u, 128u, 2048u, 0u, 4u};
const ShaderRecompiler::MeshConfiguration strip{6u, 8u, 10u, 10u, 8u, 64u, 1024u, 0u, 4u};
const ShaderRecompiler::MeshConfiguration fan{5u, 8u, 10u, 10u, 8u, 64u, 1024u, 0u, 4u};
const auto same = [](const MeshArguments& a, const MeshArguments& b) { return a.groups == b.groups && a.instances == b.instances && a.layers == b.layers && a.indexCount == b.indexCount && a.firstIndex == b.firstIndex; };
const auto rules = [&](const ShaderRecompiler::MeshConfiguration& mesh, std::uint32_t indexCount) { return AgcDriver::Graphics::MeshArgumentRulesFor(context, mesh, indexCount); };
const auto record = [](std::uint32_t count, std::uint32_t instances, std::uint32_t first) { return AgcDriver::Pm4::DrawArguments{count, instances, first, 0, 0}; };
Require(same(ResolveMeshArguments(record(1, 512, 0), rules(points, 1)), {1, 512, 1, 1, 0}), "one point, 512 instances");
Require(same(ResolveMeshArguments(record(1, 0, 0), rules(points, 1)), {0, 0, 0, 1, 0}), "no instances draws nothing");
Require(same(ResolveMeshArguments(record(0, 4, 0), rules(points, 1)), {0, 0, 0, 0, 0}), "no indices draws nothing");
Require(same(ResolveMeshArguments(record(96, 2, 0), rules(triangles, 300)), {1, 2, 1, 96, 0}), "one full group");
Require(same(ResolveMeshArguments(record(99, 2, 0), rules(triangles, 300)), {2, 2, 1, 99, 0}), "a partial second group");
Require(same(ResolveMeshArguments(record(200, 3, 150), rules(triangles, 300)), {2, 3, 1, 150, 150}), "count clamped to the index buffer");
Require(same(ResolveMeshArguments(record(9, 1, 300), rules(triangles, 300)), {0, 0, 0, 0, 300}), "first index past the index buffer draws nothing");
Require(same(ResolveMeshArguments(record(2, 1, 0), rules(triangles, 300)), {0, 0, 0, 2, 0}), "no complete triangle draws nothing");
Require(same(ResolveMeshArguments(record(10, 1, 0), rules(strip, 64)), {1, 1, 1, 10, 0}), "eight strip triangles are one group");
Require(same(ResolveMeshArguments(record(11, 1, 0), rules(strip, 64)), {2, 1, 1, 11, 0}), "a ninth strip triangle starts a group");
Require(same(ResolveMeshArguments(record(10, 1, 0), rules(fan, 64)), {1, 1, 1, 10, 0}), "eight fan triangles are one group");
Require(same(ResolveMeshArguments(record(11, 1, 0), rules(fan, 64)), {2, 1, 1, 11, 0}), "a ninth fan triangle starts a group");
Require(same(ResolveMeshArguments(record(1, 601, 0), rules(points, 1)), {0, 0, 0, 1, 0}), "instances over the device limit draw nothing");
Require(same(ResolveMeshArguments(record(96 * 7, 600, 0), rules(triangles, 96 * 7)), {0, 0, 0, 96 * 7, 0}), "groups times instances over the device limit draw nothing");
Require(same(ResolveMeshArguments(record(96 * 6, 600, 0), rules(triangles, 96 * 6)), {6, 600, 1, 96 * 6, 0}), "groups times instances at the device limit");
Require(same(ResolveMeshArguments(record(0xffffffffu, 1, 0xfffffff0u), rules(points, 0xffffffffu)), {15, 1, 1, 15, 0xfffffff0u}), "first index near the end of a huge index buffer");
Require(same(ResolveMeshArguments(record(0xffffffffu, 1, 0), rules(points, 0xffffffffu)), {0, 0, 0, 0xffffffffu, 0}), "groups over the device limit draw nothing");
}
void debugBranchTests() {
for (const auto opcode : {0x17u, 0x18u, 0x19u, 0x1au}) Require(recompilesDebugBranch(opcode), "a conditional debug branch did not recompile");
}
@ -1741,6 +1774,7 @@ int main() {
resourceTests();
misalignedShaderDataTests();
debugBranchTests();
meshArgumentTests();
validationTests();
vertexCopyTests();
pixelParameterSlotTests();

View File

@ -1,5 +1,6 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "Recompiler.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include <array>
#include <cmath>
#include <cstdio>
@ -251,6 +252,39 @@ int main() {
CheckTriangles((std::string("non-indexed triangle list, ") + name).c_str());
}
constexpr std::size_t recordBlockBytes = 65536;
auto* record = static_cast<std::uint32_t*>(std::aligned_alloc(65536, recordBlockBytes));
Require(record != nullptr, "cannot allocate the indirect record block");
{
GuestAllocations::Mutation mutation;
mutation.Add(record, recordBlockBytes, true, true);
}
const auto drawIndirect = [&](std::uint32_t count, std::uint32_t instances, std::uint32_t first) {
const std::array<std::uint32_t, 5> words{count, instances, first, 0u, 0u};
std::copy(words.begin(), words.end(), record);
AgcDriver::Pm4::DrawParameters draw{reinterpret_cast<std::uintptr_t>(Indices.data()), static_cast<std::uint32_t>(Indices.size()), 2, 1, 0, true};
draw.indirect = AgcDriver::Pm4::DrawParameters::IndirectDraw{reinterpret_cast<std::uintptr_t>(record), 0x25u, 20u, 20u, 1u, false, 0u, 0x280u, 0x280u, 0x280u, false, 0u};
ClearPixels();
DrawMesh(device, {WideSubgroup, draw, VertexBufferDescriptor(Scrambled.data(), static_cast<std::uint32_t>(Scrambled.size()))});
};
drawIndirect(static_cast<std::uint32_t>(Indices.size()), 1, 0);
CheckTriangles("indirect indexed triangle list");
drawIndirect(3u * 10u, 1, 3u * 5u);
for (std::uint32_t triangle = 0; triangle < Triangles; ++triangle) {
const auto offset = CellPixel(triangle);
const bool drawn = triangle >= 5u && triangle < 15u;
Require(drawn ? PixelIs(offset, TriangleColor(triangle)) : PixelIs(offset, Background), "indirect index range: triangle " + std::to_string(triangle) + " pixel " + PixelText(offset));
}
drawIndirect(1000u, 1, 3u * (Triangles - 2u));
for (std::uint32_t triangle = 0; triangle < Triangles; ++triangle) {
const auto offset = CellPixel(triangle);
Require(triangle >= Triangles - 2u ? PixelIs(offset, TriangleColor(triangle)) : PixelIs(offset, Background), "indirect index count past the buffer: triangle " + std::to_string(triangle) + " pixel " + PixelText(offset));
}
for (const auto& [count, instances, first] : {std::array<std::uint32_t, 3>{static_cast<std::uint32_t>(Indices.size()), 0u, 0u}, std::array<std::uint32_t, 3>{0u, 1u, 0u}, std::array<std::uint32_t, 3>{3u, 1u, static_cast<std::uint32_t>(Indices.size())}}) {
drawIndirect(count, instances, first);
for (std::uint32_t triangle = 0; triangle < Triangles; ++triangle) Require(PixelIs(CellPixel(triangle), Background), "an empty indirect record drew triangle " + std::to_string(triangle));
}
ClearPixels();
DrawMesh(device, {SmallSubgroup, {0, static_cast<std::uint32_t>(Ordered.size()), 0, 1, 0, false}, VertexBufferDescriptor(Ordered.data(), static_cast<std::uint32_t>(Ordered.size())), 0});
CheckTriangles("mesh program without push data");
@ -285,6 +319,13 @@ int main() {
if (!refused) throw;
}
Require(refused, "a triangle fan with a restart index was drawn");
const std::array<std::uint32_t, 5> fanWords{static_cast<std::uint32_t>(FanIndices.size()), 1u, 0u, 0u, 0u};
std::copy(fanWords.begin(), fanWords.end(), record);
AgcDriver::Pm4::DrawParameters fanDraw{reinterpret_cast<std::uintptr_t>(FanIndices.data()), static_cast<std::uint32_t>(FanIndices.size()), 2, 1, 0, true};
fanDraw.indirect = AgcDriver::Pm4::DrawParameters::IndirectDraw{reinterpret_cast<std::uintptr_t>(record), 0x25u, 20u, 20u, 1u, false, 0u, 0x280u, 0x280u, 0x280u, false, 0u};
ClearPixels();
DrawMesh(device, {fan, fanDraw, VertexBufferDescriptor(FanScrambled.data(), static_cast<std::uint32_t>(FanScrambled.size()))});
CheckFan("indirect indexed triangle fan");
std::puts("Mesh tests passed");
return 0;

View File

@ -78,6 +78,7 @@ enum class IrOpcode : std::uint16_t {
ShaderClock,
RealtimeClock,
MeshDrawParameter,
MeshArgument,
MeshAllocate,
TessellationBase,
GetTessellationAttribute,

View File

@ -119,6 +119,7 @@ constexpr std::array<OpcodeMeta, static_cast<std::size_t>(IrOpcode::Count)> Meta
makeMeta("ShaderClock", U64),
makeMeta("RealtimeClock", U64),
makeMeta("MeshDrawParameter", U32, U32),
makeMeta("MeshArgument", U32, U32),
makeMeta("MeshAllocate", Void, U32),
makeMeta("TessellationBase", U32, U32),
makeMeta("GetTessellationAttribute", U32, U32, U32, U1),

View File

@ -238,6 +238,10 @@ struct TessellationConfiguration {
inline constexpr std::uint32_t MeshDrawPushOffsetBytes = 104;
inline constexpr std::uint32_t MeshDrawPushBytes = 24;
inline constexpr std::uint32_t MeshArgumentAddressDword = 4;
inline constexpr std::uint32_t MeshArgumentIndexCountDword = 3;
inline constexpr std::uint32_t MeshArgumentFirstIndexDword = 4;
inline constexpr std::uint32_t MeshArgumentBytes = 20;
inline constexpr std::uint32_t MeshIndexBufferUserWord = 4;
struct GraphicsDrawParameters {

View File

@ -62,6 +62,7 @@ std::uint32_t EmitIdentity(SpirvValueEmitContext& ctx, std::uint32_t value);
void EmitVoid(SpirvValueEmitContext& context);
void EmitBarrier(SpirvEmitterState& state);
std::uint32_t EmitMeshDrawParameter(SpirvValueEmitContext& ctx, const IrValue& inst);
std::uint32_t EmitMeshArgument(SpirvValueEmitContext& ctx, const IrValue& inst);
std::uint32_t EmitGetTessellationAttribute(SpirvValueEmitContext& ctx, const IrValue& inst);
void EmitSetTessellationAttribute(SpirvValueEmitContext& ctx, const IrValue& inst);
std::uint32_t EmitGetUserData(SpirvEmitterState& state, ScalarReg reg);

View File

@ -16,6 +16,7 @@ bool UniformSource(const IrValue& value) {
case IrOpcode::ReadConst:
case IrOpcode::ReadConstBuffer:
case IrOpcode::MeshDrawParameter:
case IrOpcode::MeshArgument:
case IrOpcode::GetSrtResource:
case IrOpcode::GetBufferResource:
case IrOpcode::GetAddressResource:

View File

@ -252,6 +252,7 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) {
case IrOpcode::ShaderClock: return Invoke(EmitShaderClock, ctx, inst);
case IrOpcode::RealtimeClock: return Invoke(EmitRealtimeClock, ctx, inst);
case IrOpcode::MeshDrawParameter: return Invoke(EmitMeshDrawParameter, ctx, inst);
case IrOpcode::MeshArgument: return Invoke(EmitMeshArgument, ctx, inst);
case IrOpcode::MeshAllocate: return Invoke(EmitMeshAllocate, ctx, inst);
case IrOpcode::TessellationBase: return Invoke(EmitTessellationBase, ctx, inst);
case IrOpcode::GetTessellationAttribute: return Invoke(EmitGetTessellationAttribute, ctx, inst);

View File

@ -161,14 +161,17 @@ void CheckBindings(const IrProgram& program, const BindingAllocationResult& bind
void EmitBaseHeader(SpirvModule& module, const IrProgram& program) {
module.EmitCapability(spv::CapabilityShader);
if (program.Info().usesDma) {
const bool physical = program.Info().usesDma || program.Resources().stage == IrShaderStage::Mesh;
if (physical) {
module.EmitCapability(spv::CapabilityInt64);
module.EmitCapability(spv::CapabilityPhysicalStorageBufferAddresses);
module.EmitCapability(spv::CapabilityStorageBuffer8BitAccess);
module.EmitExtension("SPV_KHR_physical_storage_buffer");
}
if (program.Info().usesDma) {
module.EmitCapability(spv::CapabilityStorageBuffer8BitAccess);
module.EmitExtension("SPV_KHR_8bit_storage");
}
module.AddMemoryModel(program.Info().usesDma ? spv::AddressingModelPhysicalStorageBuffer64 : spv::AddressingModelLogical, spv::MemoryModelGLSL450);
module.AddMemoryModel(physical ? spv::AddressingModelPhysicalStorageBuffer64 : spv::AddressingModelLogical, spv::MemoryModelGLSL450);
}
void DefineInputs(SpirvEmitterState& state) {

View File

@ -2,6 +2,7 @@
#include "SpirvBackend/SpirvEmitterHelpers.hpp"
#include "SpirvBackend/SpirvEmitterInstructions.hpp"
#include "SpirvBackend/SpirvFlowEmitter.hpp"
#include "SpirvBackend/SpirvBda.hpp"
#include <spirv/unified1/GLSL.std.450.h>
#include <spirv/unified1/spirv.hpp>
#include <algorithm>
@ -986,6 +987,41 @@ std::uint32_t EmitMeshDrawParameter(SpirvValueEmitContext& ctx, const IrValue& i
return result;
}
std::uint32_t EmitMeshArgument(SpirvValueEmitContext& ctx, const IrValue& inst) {
auto& state = ctx.state;
const auto index = inst.Argument(0)->ImmediateU32();
if (state.program.Resources().stage != IrShaderStage::Mesh || index >= MeshArgumentBytes / 4u) {
ctx.Fail(inst, "invalid mesh argument");
}
const auto push = [&](std::uint32_t dword) {
const auto pointer = state.module.AllocateId();
state.module.AddFunction(spv::OpAccessChain, TypePushConstantElementPointer(state), pointer, state.pushConstantVariable, ConstantU32(state, 0u), ConstantU32(state, MeshDrawPushOffsetBytes / 4u + dword));
const auto value = state.module.AllocateId();
state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer);
return Unary(state, spv::OpUConvert, TypeScalarU64(state), value);
};
const auto low = push(MeshArgumentAddressDword);
const auto high = push(MeshArgumentAddressDword + 1u);
const auto address = Binary(state, spv::OpBitwiseOr, TypeScalarU64(state), low, Binary(state, spv::OpShiftLeftLogical, TypeScalarU64(state), high, ConstantU32(state, 32u)));
const auto present = Binary(state, spv::OpINotEqual, TypeBool(state), address, BdaConstant(state, 0u));
const auto before = state.currentLabel;
const auto loadLabel = state.module.AllocateId();
const auto merge = state.module.AllocateId();
state.module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone);
state.module.AddFunction(spv::OpBranchConditional, present, loadLabel, merge);
EmitLabel(state, loadLabel);
const auto element = Binary(state, spv::OpIAdd, TypeScalarU64(state), address, BdaConstant(state, 4ull * index));
const auto pointer = state.module.AllocateId();
state.module.AddFunction(spv::OpConvertUToPtr, TypePointer(state, spv::StorageClassPhysicalStorageBuffer, TypeU32(state)), pointer, element);
const auto loaded = state.module.AllocateId();
state.module.AddFunction(spv::OpLoad, TypeU32(state), loaded, pointer, spv::MemoryAccessAlignedMask, 4u);
state.module.AddFunction(spv::OpBranch, merge);
EmitLabel(state, merge);
const auto value = state.module.AllocateId();
state.module.AddFunction(spv::OpPhi, TypeU32(state), value, loaded, loadLabel, ConstantU32(state, 0u), before);
return value;
}
std::uint32_t EmitGetTessellationAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) {
return EmitValueOrZeroIfCondition(ctx.state, ctx.Arg(inst, 2), [&]() {
const auto value = ctx.state.module.AllocateId();

View File

@ -252,6 +252,9 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO
const auto draw = [&entryIr](std::uint32_t index) -> IrValue& {
return entryIr.Emit(IrOpcode::MeshDrawParameter, IrOpcodeType(IrOpcode::MeshDrawParameter), {&entryIr.Constant(index)});
};
const auto argument = [&entryIr](std::uint32_t index) -> IrValue& {
return entryIr.Emit(IrOpcode::MeshArgument, IrOpcodeType(IrOpcode::MeshArgument), {&entryIr.Constant(index)});
};
const auto minimum = [&entryIr](IrValue& lhs, IrValue& rhs) -> IrValue& {
return entryIr.Emit(IrOpcode::UMin32, IrOpcodeType(IrOpcode::UMin32), {&lhs, &rhs});
};
@ -262,7 +265,10 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO
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& indirect = entryIr.INotEqual(entryIr.BitwiseOr(draw(MeshArgumentAddressDword), draw(MeshArgumentAddressDword + 1u)), u32(0u));
IrValue& indexCount = entryIr.Select(indirect, argument(MeshArgumentIndexCountDword), draw(0u));
IrValue& firstIndex = argument(MeshArgumentFirstIndexDword);
IrValue& vertices = minimum(subtractSaturate(indexCount, 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<ScalarReg>(2), entryIr.BitwiseOr(entryIr.ShiftLeftLogical(vertices, u32(12u)), entryIr.ShiftLeftLogical(primitives, u32(22u))));
IrValue& wave = entryIr.ShiftRightLogical(local, u32(6u));
@ -288,7 +294,7 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO
IrValue& inputVertex = fan ? entryIr.Select(entryIr.IEqual(local, u32(0u)), u32(0u), entryIr.IAdd(firstVertex, local)) : entryIr.IAdd(firstVertex, local);
IrValue& indexBytes = draw(3u);
IrValue& indexed = entryIr.INotEqual(indexBytes, u32(0u));
IrValue& byteOffset = entryIr.IMul(inputVertex, indexBytes);
IrValue& byteOffset = entryIr.IMul(entryIr.IAdd(inputVertex, firstIndex), indexBytes);
IrValue& indexResource = entryIr.Emit(IrOpcode::GetBufferResource, IrOpcodeType(IrOpcode::GetBufferResource), {&entryIr.GetUserData(static_cast<ScalarReg>(4)), &entryIr.GetUserData(static_cast<ScalarReg>(5)), &entryIr.GetUserData(static_cast<ScalarReg>(6)), &entryIr.GetUserData(static_cast<ScalarReg>(7))});
const std::uint32_t memoryIndex = static_cast<std::uint32_t>(program.Resources().memoryInfo.size());
program.Resources().memoryInfo.push_back(MemoryInfo{.kind = ResourceKind::Buffer, .resource = 1u, .offen = true});