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:
STL1te 2026-09-29 13:58:52 +03:00
parent 000d0bf4f0
commit 3dec6ceda7
12 changed files with 72 additions and 29 deletions

View File

@ -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.

View File

@ -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

View File

@ -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;

View File

@ -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),

View File

@ -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);

View File

@ -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) {

View File

@ -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());

View File

@ -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};

View File

@ -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;

View File

@ -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();

View File

@ -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;

View File

@ -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;