From 029dcdacac47e19040cedfcd8d0aa4a002da6212 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 11:19:48 +0300 Subject: [PATCH] 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 --- .../libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 5 +++++ core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp | 6 ++++++ .../libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp | 4 ++++ .../prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp | 6 +++++- .../IntermediateRepresentation/IrMetadata/Resources.hpp | 3 +++ .../include/Optimization/ResourceMaterializer.hpp | 1 + .../recompiler/Optimization/src/ResourceMaterializer.cpp | 4 +++- .../SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp | 4 ++++ 8 files changed, 31 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 60ef67cdf..b0d983b5e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -218,6 +218,8 @@ struct VulkanDevice::State { std::map> computePipelines; std::unique_ptr colorTransfer; std::shared_ptr bufferPool; + // Bound for null V#s (Context::emptyBuffer). + std::unique_ptr emptyBuffer; std::unique_ptr textureCache; std::unique_ptr pipelineCache; std::unique_ptr 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("vkCreateCommandPool")(state->device, &poolInfo, nullptr, &state->pool), "vkCreateCommandPool"); state->bufferPool = std::make_shared(graphicsContext()); + state->emptyBuffer = std::make_unique(graphicsContext(), Graphics::EmptyBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); state->pipelineCache = std::make_unique(graphicsContext()); state->detiler = std::make_unique(graphicsContext()); state->textureCache = std::make_unique(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; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index e10a8c504..69dc27774 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -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. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp index 8077e8882..630eba344 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp @@ -34,6 +34,10 @@ void ShaderResources::prepareAddressBindings(std::span 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(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 40682d222..ef3cfe76d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2015,8 +2015,12 @@ std::size_t ShaderResources::addGuestBuffer(std::span 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::max(), "shader buffer size exceeds host address space"); const auto size = static_cast(byteSize); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index a99cedc31..1701a2927 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -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; }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp index b81416778..8ef7c5cdb 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp @@ -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; }; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 0bc94901a..199bc18a2 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -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 { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index 4954e162c..d97843e58 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -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; }