feat(agc-driver): bind guest buffers below their view at the storage offset alignment
A view whose offset in its GPU owner is not a multiple of minStorageBufferOffsetAlignment could not be bound, and an address-based build (its registered heaps imported whole) put descriptor-bound buffers at arbitrary offsets inside them, so such dispatches failed. GuestBufferMemory::Descriptor now binds from the aligned offset below the view and reports the difference; ShaderResources stores it, per guest buffer element, into the push constant block at the shader's memory byte offsets (RecompileResult::memoryOffsetDword), which the shader already adds to every access. Every push constant path (dispatches, recorded and synchronous draws, draw recipes) takes the patches; the assembling overload of Pipeline::PushConstants is gone. A shader whose shader data is a buffer fails when it would need one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
000d0bf4f0
commit
3dec6ceda7
|
|
@ -2937,7 +2937,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
|
|||
if (pushStages != 0 && state->properties.limits.maxPushConstantsSize < Graphics::PipelinePushConstantBytes) {
|
||||
throw std::runtime_error("Vulkan dispatch: compute push constant range exceeds device limit");
|
||||
}
|
||||
const auto pushBytes = Graphics::AssemblePushConstants(shaders);
|
||||
auto pushBytes = Graphics::AssemblePushConstants(shaders);
|
||||
const auto context = graphicsContext();
|
||||
const auto* limit = state->properties.limits.maxComputeWorkGroupCount;
|
||||
if (arguments == 0 && (x > limit[0] || y > limit[1] || z > limit[2])) {
|
||||
|
|
@ -3067,6 +3067,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
|
|||
if (resources->Reusable()) insert();
|
||||
}
|
||||
}
|
||||
resources->PatchPushConstants(pushBytes);
|
||||
if (profile) timer.add(PhaseResourcesHookWaits, Graphics::Recorder::ThreadWaitedMs() - waitedBefore);
|
||||
// The lookup outcomes of this call: rows with counts on the [indirect] line, and a summary for
|
||||
// the longest call.
|
||||
|
|
|
|||
|
|
@ -163,7 +163,10 @@ public:
|
|||
void Upload(bool addressable);
|
||||
void UploadPrepare(bool addressable);
|
||||
void UploadFinish(bool addressable);
|
||||
VkDescriptorBufferInfo Descriptor(std::uint64_t address, std::size_t bytes) const;
|
||||
// A view off minStorageBufferOffsetAlignment in its GPU owner (a view inside a merged or
|
||||
// imported range) is bound from the aligned offset below it; `adjustment` is the difference,
|
||||
// which the shader adds to every access (RecompileResult::memoryOffsetDword).
|
||||
VkDescriptorBufferInfo Descriptor(std::uint64_t address, std::size_t bytes, std::uint32_t& adjustment) const;
|
||||
std::vector<ShaderRecompiler::BdaAbi::Range> AddressRanges() const;
|
||||
// The BDA table of the cached address space when it serves this upload alone (an address-based
|
||||
// build with no region outside it): its ranges, immutable while the space lives, and the
|
||||
|
|
|
|||
|
|
@ -51,8 +51,7 @@ public:
|
|||
// The same inside a render pass another pipeline of the same attachments began (compatible by
|
||||
// construction: the attachment formats alone decide).
|
||||
void Continue(VkCommandBuffer commands, const VkViewport& viewport, const VkRect2D& scissor) const;
|
||||
void PushConstants(VkCommandBuffer commands, std::span<const CompiledShader> shaders) const;
|
||||
// The same from an assembled block (a draw recipe's, made by AssemblePushConstants once).
|
||||
// An assembled block (AssemblePushConstants, then ShaderResources::PatchPushConstants).
|
||||
void PushConstants(VkCommandBuffer commands, VkShaderStageFlags stages, std::span<const std::byte, PipelinePushConstantBytes> bytes) const;
|
||||
// Forgets the Vulkan objects without destroying them: for entries of a device that is already gone.
|
||||
void Abandon() noexcept;
|
||||
|
|
|
|||
|
|
@ -185,6 +185,11 @@ public:
|
|||
// compiled shader's descriptors. A recipe hit refreshes the template iff the two differ.
|
||||
std::uint64_t DataWordsHash() const { return dataWordsHash; }
|
||||
static std::uint64_t DataWordsHash(const CompiledShader& shader);
|
||||
// Stores the byte offsets of the guest buffers bound below their views (GuestBufferMemory::
|
||||
// Descriptor) into a push constant block assembled from the shaders this object was built for.
|
||||
void PatchPushConstants(std::span<std::byte, PipelinePushConstantBytes> bytes) const {
|
||||
for (const auto& [position, adjustment] : pushPatches) bytes[position] = static_cast<std::byte>(adjustment);
|
||||
}
|
||||
// Whether RefreshData would record anything for `shader` (the per-word compare; verification).
|
||||
bool DataWordsDiffer(const CompiledShader& shader) const;
|
||||
// Why the fast proof of a Revalidate left the object to the full walk (the [rescache]
|
||||
|
|
@ -237,6 +242,11 @@ private:
|
|||
bool written = true;
|
||||
// Data buffers: the words the buffer holds once the recorded work ran (see RefreshData).
|
||||
std::vector<std::uint32_t> dataWords;
|
||||
// Guest buffers: how far below the view the descriptor starts (GuestBufferMemory::
|
||||
// Descriptor), and where in the push constant block the shader reads that (-1: the
|
||||
// shader data is a buffer).
|
||||
std::uint32_t adjustment = 0;
|
||||
std::int32_t pushByte = -1;
|
||||
};
|
||||
|
||||
struct Binding {
|
||||
|
|
@ -308,7 +318,7 @@ private:
|
|||
void forgetDeferredInputs();
|
||||
void release() noexcept;
|
||||
void prepareAddressBindings(std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots);
|
||||
VkDescriptorBufferInfo descriptor(const Allocation& allocation) const;
|
||||
VkDescriptorBufferInfo descriptor(Allocation& allocation);
|
||||
void noteReusable();
|
||||
void reportDescriptorCaches() const;
|
||||
// What a sampled texture was proved current against when the build (or the last full Revalidate)
|
||||
|
|
@ -400,6 +410,8 @@ private:
|
|||
// FNV-1a offset basis: the hash of no data buffers (DataWordsHash).
|
||||
std::uint64_t dataWordsHash = 14695981039346656037ull;
|
||||
void rehashDataWords();
|
||||
// The nonzero guest buffer adjustments by push constant byte (PatchPushConstants).
|
||||
std::vector<std::pair<std::uint32_t, std::uint32_t>> pushPatches;
|
||||
BuildTiming timing;
|
||||
// Build state carried from stage A to stage B: the bindings in plan order, the image bindings
|
||||
// still to look up (index into `bindings`; the DescriptorBinding lives in the compiled shader),
|
||||
|
|
|
|||
|
|
@ -1159,7 +1159,11 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
|
|||
}
|
||||
APS5_LOG_CHARS_OUT_DEBUG("Resources bound");
|
||||
if (record.pushBytes != nullptr) record.pipeline->PushConstants(commands, record.pushStages, *record.pushBytes);
|
||||
else record.pipeline->PushConstants(commands, shaders);
|
||||
else {
|
||||
auto bytes = AssemblePushConstants(shaders);
|
||||
resources.PatchPushConstants(bytes);
|
||||
record.pipeline->PushConstants(commands, PushConstantStages(shaders), bytes);
|
||||
}
|
||||
APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded");
|
||||
recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset);
|
||||
if (args != nullptr) CountIndirectDraw(record.indirect->path, record.indirect->readMs, rewritten);
|
||||
|
|
@ -1488,7 +1492,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
|
|||
recipe->passKey = passKey;
|
||||
recipe->vertexInput = inputs.vertexInput;
|
||||
recipe->pushStages = PushConstantStages(shaders);
|
||||
if (recipe->pushStages != 0) recipe->pushBytes = AssemblePushConstants(shaders);
|
||||
if (recipe->pushStages != 0) {
|
||||
recipe->pushBytes = AssemblePushConstants(shaders);
|
||||
resources->PatchPushConstants(recipe->pushBytes);
|
||||
}
|
||||
recipe->masked = masked;
|
||||
recipe->fragmentOutputs = inputs.fragmentOutputs;
|
||||
recipe->shaderStages = inputs.shaderStages;
|
||||
|
|
@ -1577,7 +1584,9 @@ 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");
|
||||
pipeline->PushConstants(commands, shaders);
|
||||
auto pushBytes = AssemblePushConstants(shaders);
|
||||
resources->PatchPushConstants(pushBytes);
|
||||
pipeline->PushConstants(commands, PushConstantStages(shaders), pushBytes);
|
||||
APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded");
|
||||
recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset);
|
||||
if (args != nullptr) CountIndirectDraw(indirect.path, indirect.readMs, rewritten);
|
||||
|
|
|
|||
|
|
@ -1927,7 +1927,7 @@ void GuestBufferMemory::RecordCopyBacks(Recorder& recorder) {
|
|||
recorder.EndGpuTiming(timing, copiedBytes);
|
||||
}
|
||||
|
||||
VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std::size_t bytes) const {
|
||||
VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std::size_t bytes, std::uint32_t& adjustment) const {
|
||||
Require(uploaded && !committed, "guest GPU memory is not available");
|
||||
Require(bytes != 0 && bytes <= std::numeric_limits<std::uint64_t>::max() - address, "invalid guest buffer view");
|
||||
const auto* found = owner(address);
|
||||
|
|
@ -1936,10 +1936,13 @@ VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std:
|
|||
Require(address >= region.begin && address + bytes <= region.end && (region.buffer != nullptr || region.direct != nullptr || region.mirror != nullptr), "guest buffer view exceeds its GPU owner");
|
||||
const auto base = region.direct != nullptr ? region.direct->base : region.mirror != nullptr ? region.mirror->base : region.begin;
|
||||
const auto offset = address - base;
|
||||
Require(context.limits.minStorageBufferOffsetAlignment != 0 && offset % context.limits.minStorageBufferOffsetAlignment == 0, "guest buffer view violates storage buffer offset alignment");
|
||||
Require(bytes <= context.limits.maxStorageBufferRange, "guest buffer view exceeds descriptor range limit");
|
||||
Require(context.limits.minStorageBufferOffsetAlignment != 0, "no storage buffer offset alignment");
|
||||
// At most 255 (the alignment is at most 256); the shader adds it in DWORDs.
|
||||
adjustment = static_cast<std::uint32_t>(offset % context.limits.minStorageBufferOffsetAlignment);
|
||||
Require(adjustment % 4 == 0, "guest buffer view off the storage buffer offset alignment is not DWORD aligned");
|
||||
Require(bytes + adjustment <= context.limits.maxStorageBufferRange, "guest buffer view exceeds descriptor range limit");
|
||||
const auto handle = region.direct != nullptr ? region.direct->buffer : region.mirror != nullptr ? region.mirror->buffer->Handle() : region.buffer->Handle();
|
||||
return {handle, offset, bytes};
|
||||
return {handle, offset - adjustment, bytes + adjustment};
|
||||
}
|
||||
|
||||
ShaderRecompiler::BdaAbi::Range GuestBufferMemory::addressRange(const Region& region) {
|
||||
|
|
|
|||
|
|
@ -277,13 +277,6 @@ void Pipeline::Continue(VkCommandBuffer commands, const VkViewport& viewport, co
|
|||
context.Resolved(&DeviceFunctions::cmdSetScissor, "vkCmdSetScissor")(commands, 0, 1, &scissor);
|
||||
}
|
||||
|
||||
void Pipeline::PushConstants(VkCommandBuffer commands, std::span<const CompiledShader> shaders) const {
|
||||
const auto stages = PushConstantStages(shaders);
|
||||
if (stages == 0) return;
|
||||
const auto bytes = AssemblePushConstants(shaders);
|
||||
context.Resolved(&DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, layout, stages, 0, PipelinePushConstantBytes, bytes.data());
|
||||
}
|
||||
|
||||
void Pipeline::PushConstants(VkCommandBuffer commands, VkShaderStageFlags stages, std::span<const std::byte, PipelinePushConstantBytes> bytes) const {
|
||||
if (stages == 0) return;
|
||||
context.Resolved(&DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, layout, stages, 0, PipelinePushConstantBytes, bytes.data());
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ void ShaderResources::prepareAddressBindings(std::span<const CompiledShader> sha
|
|||
}
|
||||
}
|
||||
|
||||
VkDescriptorBufferInfo ShaderResources::descriptor(const Allocation& allocation) const {
|
||||
if (allocation.guest) return guestMemory.Descriptor(allocation.address, allocation.size);
|
||||
VkDescriptorBufferInfo ShaderResources::descriptor(Allocation& allocation) {
|
||||
if (allocation.guest) return guestMemory.Descriptor(allocation.address, allocation.size, allocation.adjustment);
|
||||
if (allocation.role == ShaderRecompiler::DescriptorRole::GuestBuffers) {
|
||||
Require(context.emptyBuffer != VK_NULL_HANDLE, "no placeholder buffer for a null V#");
|
||||
return {context.emptyBuffer, 0, allocation.size};
|
||||
|
|
|
|||
|
|
@ -828,7 +828,15 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
|
|||
// vector) counts as written, like imageWritten below.
|
||||
const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element];
|
||||
const bool atomic = element < binding.bufferAtomic.size() && binding.bufferAtomic[element];
|
||||
item.allocations.push_back(addGuestBuffer(std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * 4, 4), target, indexAddress, indexBytes, written, atomic));
|
||||
const auto index = addGuestBuffer(std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * 4, 4), target, indexAddress, indexBytes, written, atomic);
|
||||
// The element's byte offset in the shader data (RecompileResult::memoryOffsetDword).
|
||||
const auto& push = shader.program->pushConstants;
|
||||
if (!push.empty()) {
|
||||
const auto position = shader.program->memoryOffsetDword * 4u + element;
|
||||
Require(position < push.size(), "guest buffer offset lies outside the shader's push constants");
|
||||
allocations[index].pushByte = static_cast<std::int32_t>(shader.pushConstantOffset + position);
|
||||
}
|
||||
item.allocations.push_back(index);
|
||||
}
|
||||
} else if (addressRole) {
|
||||
item.allocations.push_back(allocations.size());
|
||||
|
|
@ -966,6 +974,11 @@ void ShaderResources::buildComplete() {
|
|||
}
|
||||
context.Resolved(&DeviceFunctions::updateDescriptorSets, "vkUpdateDescriptorSets")(context.device, static_cast<std::uint32_t>(writes.size()), writes.data(), 0, nullptr);
|
||||
}
|
||||
for (const auto& allocation : allocations) {
|
||||
if (allocation.adjustment == 0) continue;
|
||||
Require(allocation.pushByte >= 0, "a guest buffer off the storage buffer offset alignment in a shader whose shader data is a buffer is not implemented");
|
||||
pushPatches.emplace_back(static_cast<std::uint32_t>(allocation.pushByte), allocation.adjustment);
|
||||
}
|
||||
timing.descriptorsMs += phase(BuildPhase::Descriptors);
|
||||
noteReusable();
|
||||
completed = true;
|
||||
|
|
|
|||
|
|
@ -36,9 +36,11 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
|
|||
memory.AddWritable(address, sizeof(guest));
|
||||
memory.AddWritable(address + 16, 16);
|
||||
memory.Upload(true);
|
||||
const auto first = memory.Descriptor(address, sizeof(guest));
|
||||
const auto alias = memory.Descriptor(address + 16, 16);
|
||||
Require(first.buffer == alias.buffer && alias.offset == 16, "aliased guest buffers have different owners");
|
||||
std::uint32_t adjustment = 0;
|
||||
const auto first = memory.Descriptor(address, sizeof(guest), adjustment);
|
||||
Require(adjustment == 0, "a view at its owner's start is bound off it");
|
||||
const auto alias = memory.Descriptor(address + 16, 16, adjustment);
|
||||
Require(first.buffer == alias.buffer && alias.offset + adjustment == 16 && alias.range == 16 + adjustment, "aliased guest buffers have different owners");
|
||||
const auto ranges = memory.AddressRanges();
|
||||
Require(ranges.size() == 1 && ranges[0].begin == address && ranges[0].end == address + sizeof(guest), "incorrect BDA range bounds");
|
||||
Require(ranges[0].deviceAddress != 0 && ranges[0].permissions == ShaderRecompiler::BdaAbi::Read, "incorrect BDA address or permissions");
|
||||
|
|
@ -118,8 +120,10 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
|
|||
GuestBufferMemory unaligned(aligned);
|
||||
unaligned.AddWritable(address, sizeof(guest));
|
||||
unaligned.Upload(true);
|
||||
reject([&] { unaligned.Descriptor(address + 4, 4); }, "offset alignment");
|
||||
reject([&] { unaligned.Descriptor(address + sizeof(guest), 4); }, "exceeds its GPU owner");
|
||||
std::uint32_t adjustment = 0;
|
||||
const auto view = unaligned.Descriptor(address + 4, 4, adjustment);
|
||||
Require(adjustment == 4 && view.offset == 0 && view.range == 8, "a view off the offset alignment binds from below it");
|
||||
reject([&] { unaligned.Descriptor(address + sizeof(guest), 4, adjustment); }, "exceeds its GPU owner");
|
||||
}
|
||||
{
|
||||
// The cached address space: a second build in an unchanged registry takes the first one's
|
||||
|
|
@ -139,8 +143,9 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
|
|||
leased.AcquireRegistered();
|
||||
Require(leased.HoldsLease(), "address-based build holds no lease");
|
||||
leased.Upload(true);
|
||||
const auto view = leased.Descriptor(blockAddress, 64);
|
||||
Require(view.range == 64, "leased block has no descriptor");
|
||||
std::uint32_t adjustment = 0;
|
||||
const auto view = leased.Descriptor(blockAddress, 64, adjustment);
|
||||
Require(view.range == 64 + adjustment, "leased block has no descriptor");
|
||||
const auto ranges = leased.AddressRanges();
|
||||
Require(std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin == blockAddress && range.end == blockAddress + 64; }), "leased block is missing from the BDA table");
|
||||
leased.WriteBack();
|
||||
|
|
|
|||
|
|
@ -314,6 +314,7 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra
|
|||
#endif
|
||||
|
||||
result.bdaAbiVersion = program.Info().usesDma ? request.target.bdaAbiVersion : 0u;
|
||||
result.memoryOffsetDword = bindings.layout.memoryOffsetDword;
|
||||
result.vertexOffsetSgpr = program.Info().vertexOffsetSgpr;
|
||||
result.instanceOffsetSgpr = program.Info().instanceOffsetSgpr;
|
||||
result.vertexOffsetShared = program.Info().vertexOffsetShared;
|
||||
|
|
|
|||
|
|
@ -327,6 +327,10 @@ struct RecompileResult {
|
|||
SharedSpirv spirv;
|
||||
std::vector<DescriptorBinding> bindings;
|
||||
std::vector<std::byte> pushConstants;
|
||||
// The shader data dword of the guest buffers' byte offsets, one byte per GuestBuffers element
|
||||
// (four per dword), zero as populated: a driver binding an element below its view (the storage
|
||||
// buffer offset alignment) stores the difference there, the shader adds it to every access.
|
||||
std::uint32_t memoryOffsetDword = 0;
|
||||
std::uint32_t bdaAbiVersion = 0;
|
||||
std::vector<VertexAttribute> vertexAttributes;
|
||||
std::int32_t vertexOffsetSgpr = -1;
|
||||
|
|
|
|||
Loading…
Reference in New Issue