diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 59daae04e6f7..8eb5ad91dd96 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2478,7 +2478,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 || address == 0) { + const auto describesBuffer = [&] { + if (!GuestMemory::Accessible(reinterpret_cast(address), 1)) return false; + const auto limit = context.limits.maxStorageBufferRange; + return byteSize <= limit || GuestMemory::Accessible(reinterpret_cast(address), static_cast(limit) + 1u); + }; + if (byteSize == 0 || address == 0 || !describesBuffer()) { allocations.push_back({0, EmptyBufferBytes, false, nullptr, ShaderRecompiler::DescriptorRole::GuestBuffers, false}); return allocations.size() - 1; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index cdc87794276f..9b7198b938f3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1434,6 +1434,8 @@ PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { return it == table.end() ? nullptr : it->second; } +const VkBuffer MockEmptyBuffer = reinterpret_cast(std::uintptr_t{0xe0e0}); + AgcDriver::Graphics::Context mockContext() { AgcDriver::Graphics::Context context{}; context.deviceProc = mockProc; @@ -1446,6 +1448,7 @@ AgcDriver::Graphics::Context mockContext() { context.limits.maxPerStageDescriptorStorageBuffers = 16; context.limits.maxPerStageResources = 128; context.limits.maxDescriptorSetStorageBuffers = 32; + context.emptyBuffer = MockEmptyBuffer; return context; } @@ -1679,9 +1682,24 @@ void resourceTests() { expectSingleFailure(changed([](auto& binding) { binding.role = Role::FlattenedSrt; binding.guestDescriptor.clear(); }), "empty shader data descriptor"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x40000000u; }), "reserved bits"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[3] |= 0x40000000u; }), "unsupported type"); + { + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + vertex.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 2, join(vsharp(reinterpret_cast(0x2000), 8), vsharp(reinterpret_cast(0x1000), 8)))); + mock = MockVulkan{}; + const auto nullContext = mockContext(); + { + AgcDriver::Graphics::ShaderResources resources(nullContext, vertex, fragment, state.color, 0, 0); + const auto& write = findWrite(0); + Require(write.buffers.size() == 2 && write.buffers[0].buffer == MockEmptyBuffer && write.buffers[1].buffer == MockEmptyBuffer, "unreachable V#s did not bind the placeholder buffer"); + } + Require(mock.live == 0, "null V# resources leaked Vulkan objects"); + } expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x3fffu << 16u; binding.guestDescriptor[2] = 0xffffffffu; }), "descriptor range limit"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[2] = 32768; }), "descriptor range limit"); expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast(0x1000), 8); }), "an unmapped V#"); + expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast(0x1000), 8); binding.guestDescriptor[1] |= 0x3fffu << 16u; binding.guestDescriptor[2] = 0xffffffffu; }), "an unmapped V# beyond the descriptor range limit"); + expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast(0xfffff0000000ull), 8); }), "a V# outside the user address space"); expectSingleFailure(changed([&](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast(state.color.address), 64); }), "aliases the render target"); expectSingleAccepted(changed([](auto& binding) { binding.count = 3; binding.guestDescriptor = join(join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)), vsharp(reinterpret_cast(0x1000), 8)); }), "an unmapped V# element"); expectSingleFailure(changed([&](auto& binding) { binding.count = 2; binding.guestDescriptor = join(vsharp(guestFirst.data(), 16), vsharp(reinterpret_cast(state.color.address), 64)); }), "aliases the render target"); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 700549634853..ad2429aaa966 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -315,6 +315,7 @@ Throughout the project, every function at every stage either **does exactly what - The executable file that [relinker](../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens the console when launched, which is inconvenient for playability. - [PA_CL_CLIP_CNTL](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented. - [Per-stage descriptor count](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a pixel shader's colour attachments are not counted against `maxPerStageResources`, so its buffers and images can fit the limit while the pipeline exceeds it +- [Unreachable buffer V#s](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) read as null, as invalid image descriptors do: a base address with no readable page, or a size over `maxStorageBufferRange` over memory that is not readable that far. PPSA29141's shaders declare SRT slots they do not reach on the path taken, and those keep unrelated words (pointers, terabyte sizes over host memory). A sparse heap bound whole whose first page is uncommitted also reads as null rather than through the sparse path - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. v_sin_f32/v_cos_f32 with inputs beyond ±256 cycles were not compared with the console. - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. - The length-changing path of the [instruction rewriter](../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length.