feat(agc-driver): null V#s access nothing, as on hardware
A V# without records (all zero, say, for a slot the taken path never uses) makes every access out of bounds on hardware: loads read zero or the format's default, stores and atomics are dropped. The recompiler now specializes such a buffer as empty, so every access through it is out of bounds, and the driver binds a small per-device placeholder buffer to the element instead of rejecting the V#. A V# with records at address zero still throws. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
21cb5676c7
commit
029dcdacac
|
|
@ -218,6 +218,8 @@ struct VulkanDevice::State {
|
|||
std::map<std::uint64_t, std::shared_ptr<ComputePipelineObjects>> computePipelines;
|
||||
std::unique_ptr<Graphics::GpuColorTransfer> colorTransfer;
|
||||
std::shared_ptr<Graphics::BufferPool> bufferPool;
|
||||
// Bound for null V#s (Context::emptyBuffer).
|
||||
std::unique_ptr<Graphics::Buffer> emptyBuffer;
|
||||
std::unique_ptr<Graphics::TextureCache> textureCache;
|
||||
std::unique_ptr<Graphics::PipelineCache> pipelineCache;
|
||||
std::unique_ptr<Graphics::DescriptorCache> descriptorCache;
|
||||
|
|
@ -502,6 +504,7 @@ struct VulkanDevice::State {
|
|||
resourceCache.Clear();
|
||||
Graphics::ClearCachedTextures(device);
|
||||
descriptorCache.reset();
|
||||
emptyBuffer.reset();
|
||||
samplerCache.reset();
|
||||
textureCache.reset();
|
||||
detiler.reset();
|
||||
|
|
@ -869,6 +872,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
|
|||
poolInfo.queueFamilyIndex = family;
|
||||
check(state->DeviceFunction<PFN_vkCreateCommandPool>("vkCreateCommandPool")(state->device, &poolInfo, nullptr, &state->pool), "vkCreateCommandPool");
|
||||
state->bufferPool = std::make_shared<Graphics::BufferPool>(graphicsContext());
|
||||
state->emptyBuffer = std::make_unique<Graphics::Buffer>(graphicsContext(), Graphics::EmptyBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
|
||||
state->pipelineCache = std::make_unique<Graphics::PipelineCache>(graphicsContext());
|
||||
state->detiler = std::make_unique<Graphics::TextureDetiler>(graphicsContext());
|
||||
state->textureCache = std::make_unique<Graphics::TextureCache>(graphicsContext());
|
||||
|
|
@ -2167,6 +2171,7 @@ Graphics::Context VulkanDevice::buildContext() const {
|
|||
context.multiDrawIndirect = state->multiDrawIndirect;
|
||||
context.drawIndirectCount = state->drawIndirectCount;
|
||||
context.occlusionQueryPrecise = state->occlusionQueryPrecise;
|
||||
context.emptyBuffer = state->emptyBuffer ? state->emptyBuffer->Handle() : VK_NULL_HANDLE;
|
||||
context.copiedWriters = state->copiedWriters.get();
|
||||
context.functions = state->functionsReady ? &state->deviceFunctions : nullptr;
|
||||
context.descriptorIndexing = state->descriptorIndexing;
|
||||
|
|
|
|||
|
|
@ -74,6 +74,9 @@ inline std::uint64_t& DeviceProcLookups() {
|
|||
return count;
|
||||
}
|
||||
|
||||
// The size of Context::emptyBuffer.
|
||||
inline constexpr std::size_t EmptyBufferBytes = 16;
|
||||
|
||||
struct Context {
|
||||
VkDevice device;
|
||||
VkPhysicalDevice physical;
|
||||
|
|
@ -118,6 +121,9 @@ struct Context {
|
|||
bool drawIndirectCount = false;
|
||||
// Occlusion queries count exact samples (occlusionQueryPrecise), not just any-passed.
|
||||
bool occlusionQueryPrecise = false;
|
||||
// A small storage buffer bound for a null V# (no records): the recompiler specializes such a
|
||||
// buffer as empty (BufferResource::empty), so shaders never access it.
|
||||
VkBuffer emptyBuffer = VK_NULL_HANDLE;
|
||||
// The device's list of recorded dispatches whose copied written buffers await a CPU write-back
|
||||
// (VulkanDevice's State::copiedWriters; the draw counterpart is DrawCopiedWriters): an indirect
|
||||
// draw whose records one of them produces reads them on the CPU. Null in tests.
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ 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);
|
||||
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};
|
||||
}
|
||||
if (allocation.role == ShaderRecompiler::DescriptorRole::BdaPagetable || allocation.role == ShaderRecompiler::DescriptorRole::FaultBuffer) {
|
||||
Require(bda != nullptr, "BDA descriptors have no memory owner");
|
||||
return allocation.role == ShaderRecompiler::DescriptorRole::BdaPagetable ? bda->Table() : bda->Fault();
|
||||
|
|
|
|||
|
|
@ -2015,8 +2015,12 @@ std::size_t ShaderResources::addGuestBuffer(std::span<const std::uint32_t> words
|
|||
Require(descriptor.Type() == 0u, "buffer descriptor uses an unsupported type");
|
||||
const auto address = descriptor.Base48();
|
||||
const auto byteSize = descriptor.GetSize();
|
||||
if (byteSize == 0) {
|
||||
// A null V#: every access is out of bounds in the shader (BufferResource::empty).
|
||||
allocations.push_back({0, EmptyBufferBytes, false, nullptr, ShaderRecompiler::DescriptorRole::GuestBuffers, false});
|
||||
return allocations.size() - 1;
|
||||
}
|
||||
Require(address != 0, "null shader buffer descriptor address");
|
||||
Require(byteSize != 0, "empty shader buffer descriptor");
|
||||
Require(byteSize <= context.limits.maxStorageBufferRange, "shader buffer exceeds descriptor range limit");
|
||||
Require(byteSize <= std::numeric_limits<std::size_t>::max(), "shader buffer size exceeds host address space");
|
||||
const auto size = static_cast<std::size_t>(byteSize);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ struct BufferResource {
|
|||
bool atomic = false;
|
||||
bool formatted = false;
|
||||
bool scalar = false;
|
||||
// Specialized: the V# has no records (a null V#), so every access is out of bounds: loads read
|
||||
// zero (or the format's default), stores and atomics are dropped, as on hardware.
|
||||
bool empty = false;
|
||||
|
||||
bool operator==(const BufferResource& other) const = default;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ struct ResourceSpecialization {
|
|||
std::uint32_t packedStride = 0;
|
||||
IrBufferFormat descriptorFormat = IrBufferFormat::Invalid;
|
||||
std::uint32_t descriptorSwizzle = ShaderImageIdentitySwizzle;
|
||||
bool empty = false;
|
||||
|
||||
bool operator==(const Buffer& other) const;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -507,6 +507,7 @@ void buildResourceSpecialization(const IrResourcePlan& plan, ResourceSnapshot& s
|
|||
entry.packedStride = packedStride;
|
||||
entry.descriptorFormat = buffer.formatted ? decoded.Format() : IrBufferFormat::Invalid;
|
||||
entry.descriptorSwizzle = buffer.formatted ? decoded.DstSelXYZW() : DstSel(4, 5, 6, 7);
|
||||
entry.empty = decoded.GetSize() == 0u;
|
||||
result.buffers.push_back(entry);
|
||||
}
|
||||
|
||||
|
|
@ -590,6 +591,7 @@ void ResourceMaterializer::Apply(IrProgram& program, const ResourceSpecializatio
|
|||
buffers[i].packedStride = specialization.buffers[i].packedStride;
|
||||
buffers[i].descriptorFormat = specialization.buffers[i].descriptorFormat;
|
||||
buffers[i].descriptorSwizzle = specialization.buffers[i].descriptorSwizzle;
|
||||
buffers[i].empty = specialization.buffers[i].empty;
|
||||
}
|
||||
|
||||
auto images = resources.info.images;
|
||||
|
|
@ -874,7 +876,7 @@ void ResourceMaterializer::CountBindlessRejection(BindlessRejection reason) {
|
|||
}
|
||||
|
||||
bool ResourceSpecialization::Buffer::operator==(const Buffer& other) const {
|
||||
return packedStride == other.packedStride && descriptorFormat == other.descriptorFormat && descriptorSwizzle == other.descriptorSwizzle;
|
||||
return packedStride == other.packedStride && descriptorFormat == other.descriptorFormat && descriptorSwizzle == other.descriptorSwizzle && empty == other.empty;
|
||||
}
|
||||
|
||||
bool ResourceSpecialization::Image::operator==(const Image& other) const {
|
||||
|
|
|
|||
|
|
@ -111,6 +111,10 @@ MemoryResourceAccess PrepareStorageBufferResourceAccess(SpirvEmitterState& state
|
|||
access.byteOffset = state.memoryByteOffsets.at(arrayIndex);
|
||||
access.length = state.module.AllocateId();
|
||||
state.module.AddFunction(spv::OpArrayLength, TypeU32(state), access.length, access.objectPointer, 0u);
|
||||
// A null V# binds a placeholder the shader must not touch: nothing is in bounds.
|
||||
if (mem.resource < state.program.Info().buffers.size() && state.program.Info().buffers[mem.resource].empty) {
|
||||
access.length = ConstantU32(state, 0u);
|
||||
}
|
||||
return access;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue