Merge pull request #3412 from pushkarreddyy/feat/agc-draw-snapshot-cap

perf(libSceAgcDriver): cap draw input snapshot allocation size
This commit is contained in:
Adrià 2026-10-11 23:28:42 +02:00 committed by GitHub
commit a9a34e91cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 165 additions and 1 deletions

View File

@ -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:$<TARGET_FILE_DIR:libc>")
endif()
endif()

View File

@ -136,6 +136,7 @@ public:
};
std::shared_ptr<DrawBindings> PrepareDrawBindings(Recorder& recorder, std::span<const MovedBuffer> moved = {}) const;
std::optional<std::vector<MovedBuffer>> MovedReadOnlyBuffers(std::span<const CompiledShader> 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

View File

@ -3383,9 +3383,17 @@ std::optional<std::vector<ShaderResources::MovedBuffer>> 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<std::size_t>(mib * 1024ull * 1024ull);
}
std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span<const MovedBuffer> 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<DrawBindings>();
std::vector<std::size_t> selected;
for (std::size_t index = 0; index < allocations.size(); ++index) {
@ -3413,6 +3421,7 @@ std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindi
}
const auto begin = address - item.adjustment;
const auto bytes = static_cast<std::size_t>(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::DrawBindings> 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<unsigned long long>(recorder.Submissions() + 1), static_cast<unsigned long long>(begin), bytes);
}
if (selected.empty()) return {};

View File

@ -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<std::uint64_t>(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<std::uint64_t>(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<std::uint32_t>(smallAddress), static_cast<std::uint32_t>(smallAddress >> 32u) & 0xffffu, static_cast<std::uint32_t>(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<std::uint32_t>(largeAddress), static_cast<std::uint32_t>(largeAddress >> 32u) & 0xffffu, static_cast<std::uint32_t>(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<void*>(smallAddress), 0x22, smallBytes);
std::memset(reinterpret_cast<void*>(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<const std::byte*>(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<std::uint32_t>(firstAddress), static_cast<std::uint32_t>(firstAddress >> 32u) & 0xffffu, static_cast<std::uint32_t>(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<std::uint32_t>(secondAddress), static_cast<std::uint32_t>(secondAddress >> 32u) & 0xffffu, static_cast<std::uint32_t>(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);

View File

@ -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: