From 4de5726d32303d3ade07e9fb3e4f73c5151f91d9 Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Sat, 10 Oct 2026 15:14:33 -0300 Subject: [PATCH 1/3] fix(graphics): distinguish reused memory from depth storage aliases --- .../Graphics/include/DepthSurface.hpp | 2 ++ .../Graphics/src/DepthSurface.cpp | 18 ++++++++++++++++ .../Graphics/src/ShaderResources.cpp | 6 +++--- .../tests/DepthSurfaceReuse.cpp | 21 +++++++++++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index f5b6cad4a..a3f6c027d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -16,6 +16,8 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target); std::uint64_t DepthSliceBytes(VkExtent2D extent, std::uint32_t bytesPerTexel); void ClearDepthSurfaces(VkDevice device); bool DepthSurfaceAt(std::uint64_t address); +// Descriptor-aware lookup distinguishes a depth plane from temporary memory reused for color. +bool DepthSurfaceAt(const Context& context, std::span words, const GuestTextureResource& resource); std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads); void NoteHtileDepthClear(std::uint64_t htileAddress); bool DepthSurfaceHolds(const Context& context, const GuestTextureResource& resource, const Texture* texture); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 0b5d5eb79..781a4105b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -284,6 +284,24 @@ void NoteDepthMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32 } } +bool DepthSurfaceAt(const Context& context, std::span words, const GuestTextureResource& resource) { + std::lock_guard lock(surfacesMutex()); + const auto& list = surfaces(); + const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { + return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); + }); + if (found == list.rend()) return false; + // As in the sampled lookup, a previous depth allocation at the same address alone does not + // identify this descriptor. Games reuse temporary GPU memory for ordinary color textures. + const auto& target = (*found)->target; + if (resource.width != target.extent.width || resource.height != target.extent.height) return false; + const bool stencil = target.stencilAddress != 0 && target.stencilAddress == resource.baseAddress; + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; + const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u); + return ResolveTextureFormat(resource.format) == expected || depthBits; +} + bool DepthSurfaceAt(std::uint64_t address) { std::lock_guard lock(surfacesMutex()); return std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& surface) { return surface->target.address == address || surface->target.stencilAddress == address; }); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 0383b7571..a45572e58 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -834,9 +834,9 @@ GuestTextureResource StorageSurface(const Context& context, const GuestTextureRe // `guestBytes` is the surface size when the caller described the surface already (0: described here). std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& viewed, std::uint32_t mip, std::uint64_t guestBytes) { RequireNoMultisampleTarget(viewed.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a storage image access"); - if (DepthSurfaceAt(viewed.baseAddress)) { - char text[112]; - std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented", static_cast(viewed.baseAddress)); + if (DepthSurfaceAt(context, words, viewed)) { + char text[256]; + std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented (storage=%ux%u guest format=%u mip=%u dimension=%d tile=%d)", static_cast(viewed.baseAddress), viewed.width, viewed.height, viewed.format, mip, static_cast(viewed.dimension), static_cast(viewed.tileMode)); throw std::runtime_error(text); } static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp index 63ddb1ae1..bf83e852c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp @@ -186,5 +186,26 @@ void RunDepthSurfaceReuseTests() { Require(lookup(context, {stencil, FormatR8G8B8A8Unorm, 320, 180, TileRenderTarget64KB}) == nullptr, "an RGBA8 view of a stencil plane's own extent must be read as reused memory"); Require(lookup(context, {stencil, FormatR32Float, 320, 180}) == nullptr, "an R32F view of a stencil plane's own extent must be read as reused memory"); + const auto storageAliasesDepth = [&](const Context& owner, const View& view) { + const auto words = tsharp(view); + return DepthSurfaceAt(owner, words, DecodeTextureResource(words)); + }; + Require(storageAliasesDepth(context, {depth32, FormatR32Float, 384, 384}), "storage access to the actual D32 plane must remain rejected"); + Require(storageAliasesDepth(context, {depth16, FormatR16Uint, 256, 256}), "raw D16 storage access must remain rejected"); + Require(!storageAliasesDepth(context, {depth32, FormatR32Float, 128, 128}), "temporary memory reused at a different extent is not a depth storage alias"); + Require(!storageAliasesDepth(context, {depth32, FormatR11G11B10Float, 384, 384}), "temporary memory reused for a color format is not a depth storage alias"); + Require(!storageAliasesDepth(context, {depth32, FormatR32Uint, 384, 384, TileRenderTarget64KB}), "raw integer color memory without a depth layout is not a depth storage alias"); + auto otherDevice = context; + static int otherDeviceTag = 0; + otherDevice.device = reinterpret_cast(&otherDeviceTag); + Require(!storageAliasesDepth(otherDevice, {depth32, FormatR32Float, 384, 384}), "a depth surface on another device must not block a storage lookup"); + + constexpr std::uint64_t stencil16 = 0x60000000; + DepthSurfaceView(context, {depth16, stencil16, {128, 128}, VK_FORMAT_D16_UNORM_S8_UINT, 1.0f, 0}); + Require(!storageAliasesDepth(context, {depth16, FormatR16Unorm, 256, 256}), "an older depth extent at the same address must not override the newest surface"); + Require(storageAliasesDepth(context, {depth16, FormatR16Unorm, 128, 128}), "the newest depth extent must still reject actual storage access"); + Require(storageAliasesDepth(context, {stencil16, FormatR8Uint, 128, 128}), "storage access to the actual stencil plane must remain rejected"); + Require(!storageAliasesDepth(context, {stencil16, FormatR11G11B10Float, 128, 128}), "a stencil address reused for color is not a stencil storage alias"); + ClearDepthSurfaces(context.device); } From b9b83cef2b18c9f3df5da8680cd934eda2f4af40 Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Sat, 10 Oct 2026 15:34:38 -0300 Subject: [PATCH 2/3] style(graphics): follow the code comment convention --- core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp | 1 - core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp | 2 -- 2 files changed, 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index a3f6c027d..f27b19b65 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -16,7 +16,6 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target); std::uint64_t DepthSliceBytes(VkExtent2D extent, std::uint32_t bytesPerTexel); void ClearDepthSurfaces(VkDevice device); bool DepthSurfaceAt(std::uint64_t address); -// Descriptor-aware lookup distinguishes a depth plane from temporary memory reused for color. bool DepthSurfaceAt(const Context& context, std::span words, const GuestTextureResource& resource); std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads); void NoteHtileDepthClear(std::uint64_t htileAddress); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 781a4105b..9ceaca0cd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -291,8 +291,6 @@ bool DepthSurfaceAt(const Context& context, std::span words return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); }); if (found == list.rend()) return false; - // As in the sampled lookup, a previous depth allocation at the same address alone does not - // identify this descriptor. Games reuse temporary GPU memory for ordinary color textures. const auto& target = (*found)->target; if (resource.width != target.extent.width || resource.height != target.extent.height) return false; const bool stencil = target.stencilAddress != 0 && target.stencilAddress == resource.baseAddress; From ef218ef928ac83ba06e06c5ed9bf846c0c4bb490 Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Sun, 11 Oct 2026 09:31:33 -0300 Subject: [PATCH 3/3] test(agc): distinguish GPU depth from reused storage bytes --- .../tests/execution/DepthStorageReuse.cpp | 78 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 + 2 files changed, 80 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp new file mode 100644 index 000000000..91db433d8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp @@ -0,0 +1,78 @@ +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::Width; + +alignas(4096) std::array Depth{}; +alignas(256) std::array Output{}; +alignas(256) constexpr std::array LoadCode{ + 0x7e3c0280u, 0x7e3e0280u, 0x7e140280u, 0xf0001108u, 0x00010a1eu, + 0xbf8c3f70u, 0xf0201108u, 0x00010a1eu, 0x34060082u, 0xe0701000u, 0x80000a03u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array SampleCode{ + 0x7e3c02ffu, 0x3f000000u, 0x7e3e02ffu, 0x3f000000u, 0xf09c0108u, + 0x00610a1eu, 0x34060082u, 0xe0701000u, 0x80000a03u, 0xbf810000u, +}; + +void Read(AgcDriver::VulkanDevice& device, std::uint32_t format, std::uint32_t width, bool sampled, float expected) { + Output.fill(0xdeadbeefu); + const auto output = reinterpret_cast(Output.data()); + const auto depth = reinterpret_cast(Depth.data()); + const std::array data{ + static_cast(output), static_cast(output >> 32u), sizeof(Output), 0x31016facu, + static_cast(depth >> 8u), static_cast(depth >> 40u) | (format << 20u) | (((width - 1u) & 3u) << 30u), + ((width - 1u) >> 2u) | ((Height - 1u) << 14u), 0x90000facu, 0u, 0u, 0u, 0u, + 0x92u, 0u, 0u, 0u, + }; + const auto code = sampled ? std::span(SampleCode) : std::span(LoadCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{32, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, data, compute, std::nullopt, std::nullopt, memory}, device.Target(), {0, 0, 0, 128}, + }; + request.useCache = false; + const auto shader = ShaderRecompiler::Recompile(request); + device.Dispatch(shader, 1, 1, 1); + device.WaitIdle(); + for (std::size_t lane = 0; lane < 32; ++lane) { + Require((sampled && format == 7u ? std::abs(std::bit_cast(Output[lane]) - expected) <= 1.0f / 65535.0f : Output[lane] == std::bit_cast(expected)), "depth reuse returned the wrong GPU value at lane " + std::to_string(lane) + ": got " + std::to_string(Output[lane]) + ", expected " + std::to_string(std::bit_cast(expected))); + } + Require(std::all_of(Depth.begin(), Depth.end(), [](float value) { return value == 0.25f; }), "depth reuse changed the guest bytes"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto shaders = DepthFastClearHarness::Compile(*device); + const auto address = reinterpret_cast(Depth.data()); + for (const auto format : {VK_FORMAT_D32_SFLOAT, VK_FORMAT_D16_UNORM}) { + AgcDriver::Graphics::ClearDepthSurfaces(device->Device()); + Depth.fill(0.25f); + const DepthFastClearHarness::Surface surface{address, 0, 0, false, format}; + Require(DepthFastClearHarness::Draw(*device, shaders, surface, {.depthWrite = true}) == DepthFastClearHarness::Covered, "depth producer did not cover the surface"); + Read(*device, format == VK_FORMAT_D16_UNORM ? 7u : 22u, Width, true, 0.5f); + Read(*device, 22u, format == VK_FORMAT_D16_UNORM ? Width : Width / 2u, false, 0.25f); + } + std::puts("depth storage reuse GPU tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0fa78fec4..72dee2ce4 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -371,6 +371,8 @@ Throughout the project, every function at every stage either **does exactly what ### Functional +- [Reused depth storage](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. Matching depth/stencil storage access still throws. + - [sceAudioInHqOpen, sceAudioInAsyncOpen](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) open the same port as `sceAudioInOpen`, so `sceAudioInInput`, `sceAudioInGetSilentState` and `sceAudioInClose` accept their handles. The signatures agree across [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/fc5d2cc219c43c8c8f354153e7b3d0c04a5d506e/src/core/libraries/audio/audioin.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp). HqOpen is limited to 128 samples at 48 kHz; AsyncOpen rejects sizes above 384 and throws for currently unverified inputs. Host capture pacing and console behavior remain unverified. - [sem_timedwait](../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified.