diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 281991e53..675ae9ca6 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -788,8 +788,10 @@ if(BUILD_TESTING) set_tests_properties(agc_unit_shadow PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") add_test(NAME agc_descriptor_pool_recycling COMMAND agc_driver_recorder_tests --descriptor-pool-only) set_tests_properties(agc_descriptor_pool_recycling PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_draw_snapshot_cap COMMAND agc_driver_recorder_tests --draw-snapshot-cap-only) + set_tests_properties(agc_draw_snapshot_cap PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_host_import_unmap agc_unit_shadow agc_descriptor_pool_recycling PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_host_import_unmap agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index daa271581..27df868bd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -136,6 +136,7 @@ public: }; std::shared_ptr PrepareDrawBindings(Recorder& recorder, std::span moved = {}) const; std::optional> MovedReadOnlyBuffers(std::span shaders, Recorder& recorder) const; + static std::size_t MaxDrawSnapshotBytes(); void WriteBack(); // Deferred completion: MarkGpuWrites registers the results the recorded work leaves on the GPU // (storage images stay there; buffer ranges are noted so CPU reads wait); WriteBackBuffers runs diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index bfee2423e..8ce6009bf 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -3383,9 +3383,17 @@ std::optional> ShaderResources::MovedR return moved; } +std::size_t ShaderResources::MaxDrawSnapshotBytes() { + const char* text = std::getenv("APS5_DRAW_SNAPSHOT_MAX_MIB"); + const auto mib = text != nullptr ? std::strtoull(text, nullptr, 10) : 64ull; + return static_cast(mib * 1024ull * 1024ull); +} + std::shared_ptr ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span moved) const { if (_set == VK_NULL_HANDLE || usesBda) return {}; const auto reads = guestMemory.InPlaceReads(); + const auto maxSnapshotBytes = MaxDrawSnapshotBytes(); + std::size_t totalSnapshotBytes = 0; auto result = std::make_shared(); std::vector selected; for (std::size_t index = 0; index < allocations.size(); ++index) { @@ -3413,6 +3421,7 @@ std::shared_ptr ShaderResources::PrepareDrawBindi } const auto begin = address - item.adjustment; const auto bytes = static_cast(GuestBufferMemory::ViewBytes(size, item.adjustment)); + if (override == moved.end() && (bytes > maxSnapshotBytes || totalSnapshotBytes + bytes > maxSnapshotBytes)) continue; const auto registryGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); const auto generation = GuestMemory::CollectWrites(begin, bytes); auto buffer = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, generation); @@ -3423,6 +3432,7 @@ std::shared_ptr ShaderResources::PrepareDrawBindi } selected.push_back(index); result->snapshots.push_back({begin, std::move(buffer)}); + totalSnapshotBytes += bytes; CaptureTrace::Log("draw-snapshot batch=%llu address=%llx bytes=%zu", static_cast(recorder.Submissions() + 1), static_cast(begin), bytes); } if (selected.empty()) return {}; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 3a67d7e6c..0c7912500 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -1764,6 +1764,150 @@ void drawSnapshotPatchTests(const Device& device) { } } +void drawSnapshotCapTests(const Device& device, Recorder& recorder) { + using namespace AgcDriver::GuestMemory; + const auto& context = device.GetContext(); + const char* previous = std::getenv("APS5_DRAW_SNAPSHOT_MAX_MIB"); + const std::string saved = previous != nullptr ? previous : ""; +#ifdef _WIN32 + Require(_putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", "1") == 0, "cannot set snapshot cap"); +#else + Require(setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", "1", 1) == 0, "cannot set snapshot cap"); +#endif + struct RestoreCap { + const char* previous; + std::string saved; + ~RestoreCap() { +#ifdef _WIN32 + _putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", saved.c_str()); +#else + if (previous != nullptr) { + setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", saved.c_str(), 1); + } else { + unsetenv("APS5_DRAW_SNAPSHOT_MAX_MIB"); + } +#endif + } + } restore{previous, saved}; + constexpr std::size_t bytes = 3 * 1024 * 1024; + void* block = context.hostImportAlignment != 0 ? AllocateWatched(bytes, 65536) : nullptr; + if (block == nullptr) { + std::cout << "host imports or write watching unavailable: draw snapshot cap not tested\n"; + return; + } + struct Release { + void* block; + ~Release() { ReleaseWatched(block, bytes); } + } release{block}; + std::memset(block, 0x11, bytes); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, reinterpret_cast(block), bytes); + } + } unregister{context, block}; + if (HostImportFor(context, address, bytes) == nullptr) { + std::cout << "host import of the watched block refused: draw snapshot cap not tested\n"; + return; + } + const auto smallAddress = address + 4096; + constexpr std::size_t smallBytes = 1024 * 1024 - 4096; + const auto largeAddress = address + 1024 * 1024 + 8192; + constexpr std::size_t largeBytes = 1024 * 1024 + 4096; + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding smallBinding; + smallBinding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + smallBinding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + smallBinding.descriptorSet = 0; + smallBinding.binding = 0; + smallBinding.count = 1; + smallBinding.guestDescriptor = {static_cast(smallAddress), static_cast(smallAddress >> 32u) & 0xffffu, static_cast(smallBytes), 0x31000000u}; + smallBinding.bufferWritten = {false}; + program.bindings.push_back(smallBinding); + ShaderRecompiler::DescriptorBinding largeBinding; + largeBinding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + largeBinding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + largeBinding.descriptorSet = 0; + largeBinding.binding = 1; + largeBinding.count = 1; + largeBinding.guestDescriptor = {static_cast(largeAddress), static_cast(largeAddress >> 32u) & 0xffffu, static_cast(largeBytes), 0x31000000u}; + largeBinding.bufferWritten = {false}; + program.bindings.push_back(largeBinding); + const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + { + auto snapshotContext = context; + DescriptorCache cache(snapshotContext); + snapshotContext.descriptorCache = &cache; + Recorder snapshotRecorder(snapshotContext); + snapshotRecorder.Activate(); + ShaderResources resources(snapshotContext, compute); + const auto bindings = resources.PrepareDrawBindings(snapshotRecorder); + Require(bindings != nullptr, "read-only draw bindings were not prepared"); + Require(bindings->snapshots.size() == 1, "an input exceeding the draw snapshot cap was snapshotted"); + Require(bindings->snapshots[0].address == smallAddress, "the snapshotted input does not match the smaller buffer below the cap"); + Require(snapshotRecorder.ReusableDrawSnapshot(largeAddress, largeBytes) == nullptr, "an input exceeding the draw snapshot cap was kept in the snapshot cache"); + if (Watched(address, bytes)) { + Require(snapshotRecorder.ReusableDrawSnapshot(smallAddress, smallBytes) != nullptr, "an input below the draw snapshot cap was not kept in the snapshot cache"); + } + const auto contents = bindings->snapshots[0].buffer->Bytes(); + Require(contents.size() == smallBytes && std::all_of(contents.begin(), contents.end(), [](std::byte value) { return value == std::byte{0x11}; }), "a draw snapshot does not hold the guest bytes of its draw"); + std::memset(reinterpret_cast(smallAddress), 0x22, smallBytes); + std::memset(reinterpret_cast(largeAddress), 0x33, largeBytes); + Require(std::all_of(contents.begin(), contents.end(), [](std::byte value) { return value == std::byte{0x11}; }), "snapshotted input observed CPU overwrite"); + const auto* inPlace = reinterpret_cast(largeAddress); + Require(inPlace[0] == std::byte{0x33} && std::all_of(inPlace, inPlace + largeBytes, [](std::byte value) { return value == std::byte{0x33}; }), "in-place bound input did not observe CPU write"); + std::memset(block, 0x11, bytes); + snapshotRecorder.Sync(); + } + const auto firstAddress = address + 4096; + constexpr std::size_t firstBytes = 600 * 1024; + const auto secondAddress = address + 600 * 1024 + 8192; + constexpr std::size_t secondBytes = 600 * 1024; + ShaderRecompiler::RecompileResult cumulativeProgram; + ShaderRecompiler::DescriptorBinding firstBinding; + firstBinding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + firstBinding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + firstBinding.descriptorSet = 0; + firstBinding.binding = 0; + firstBinding.count = 1; + firstBinding.guestDescriptor = {static_cast(firstAddress), static_cast(firstAddress >> 32u) & 0xffffu, static_cast(firstBytes), 0x31000000u}; + firstBinding.bufferWritten = {false}; + cumulativeProgram.bindings.push_back(firstBinding); + ShaderRecompiler::DescriptorBinding secondBinding; + secondBinding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + secondBinding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + secondBinding.descriptorSet = 0; + secondBinding.binding = 1; + secondBinding.count = 1; + secondBinding.guestDescriptor = {static_cast(secondAddress), static_cast(secondAddress >> 32u) & 0xffffu, static_cast(secondBytes), 0x31000000u}; + secondBinding.bufferWritten = {false}; + cumulativeProgram.bindings.push_back(secondBinding); + const CompiledShader cumulativeCompute{ShaderRecompiler::ShaderStage::Compute, &cumulativeProgram, 0}; + { + auto snapshotContext = context; + DescriptorCache cache(snapshotContext); + snapshotContext.descriptorCache = &cache; + Recorder snapshotRecorder(snapshotContext); + snapshotRecorder.Activate(); + ShaderResources resources(snapshotContext, cumulativeCompute); + const auto bindings = resources.PrepareDrawBindings(snapshotRecorder); + Require(bindings != nullptr, "read-only draw bindings were not prepared for cumulative test"); + Require(bindings->snapshots.size() == 1, "cumulative cap permitted both inputs to snapshot"); + Require(bindings->snapshots[0].address == firstAddress, "the earlier binding in the layout was not prioritised for the snapshot budget"); + snapshotRecorder.Sync(); + } + recorder.Activate(); +} void drawSnapshotEvictionTests(const Device& device) { using namespace AgcDriver::GuestMemory; @@ -4979,6 +5123,11 @@ int main(int argc, char** argv) { std::cout << "Draw descriptor writes and snapshot overrides passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--draw-snapshot-cap-only") { + drawSnapshotCapTests(device, recorder); + std::cout << "Draw snapshot cap tests passed\n"; + return 0; + } readTrackingTests(device, recorder); writeSettledTests(device, recorder); @@ -5001,6 +5150,7 @@ int main(int argc, char** argv) { misalignedRegionTests(device, recorder); drawSnapshotReuseTests(device, recorder); drawDescriptorWriteTests(device, recorder); + drawSnapshotCapTests(device, recorder); drawSnapshotEvictionTests(device); drawInputReuseTests(device, recorder); drawInputInPlaceTests(device, recorder); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 13949c695..324062d6b 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -373,6 +373,7 @@ Throughout the project, every function at every stage either **does exactly what - [Wide-character conversions](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [nan and nanf](../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. +- [Draw input snapshots](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. - [SPIRV-Tools optimization](../../core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp) skips CCP and Simplification for modules with storage-only 8- or 16-bit types because the pinned folder produces illegal narrow constants. This also disables unrelated constant folding; the restricted full pipeline mirrors the pinned performance recipe. - [SRT signed comparison](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) evaluates `SLessThan32` as signed 32-bit less-than, matching the existing SPIR-V backend and [SPIR-V `OpSLessThan`](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpSLessThan). Signed boundary descriptor expressions are tested on the host; native RDNA2/PS5 execution and title behavior remain unverified. - On macOS the [write watch](../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: