diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 18bf4a2ab338..09acff2eb323 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1095,6 +1095,8 @@ if(BUILD_TESTING) 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) + add_test(NAME agc_draw_snapshot_epoch COMMAND agc_driver_recorder_tests --draw-snapshot-epoch-only) + set_tests_properties(agc_draw_snapshot_epoch PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_multisample_guest_access COMMAND agc_driver_recorder_tests --multisample-only) set_tests_properties(agc_multisample_guest_access PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_draw_inputs_in_place COMMAND agc_driver_recorder_tests --draw-inputs-in-place-only) @@ -1113,7 +1115,7 @@ if(BUILD_TESTING) add_test(NAME agc_metadata_pass COMMAND agc_driver_recorder_tests --metadata-pass-only) set_tests_properties(agc_metadata_pass PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_draw_snapshot_epoch agc_multisample_guest_access agc_draw_inputs_in_place agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only) set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 0987f2333e52..18e72f40dae0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include "prx/libSceAgcDriver/Graphics/include/PassHazards.hpp" +#include #include #include #include @@ -96,8 +97,11 @@ class Recorder { enum class SnapshotUse : std::uint8_t { Storage, Vertex, Index16, Index32, Index16Restart, Index32Restart }; static constexpr std::size_t DrawSnapshotBudget = std::size_t{1024} << 20u; static constexpr std::size_t DrawSnapshotEntries = 1024; + static constexpr std::size_t UnwrittenSnapshotEntries = 4096; static constexpr std::size_t DrawInputBudget = std::size_t{1024} << 20u; static constexpr std::size_t DrawInputEntries = 16384; + std::shared_ptr UnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp); + void NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, std::shared_ptr buffer); std::shared_ptr ReusableDrawSnapshot(std::uint64_t address, std::size_t bytes, SnapshotUse use = SnapshotUse::Storage, std::uint32_t* derived = nullptr, std::uint64_t generation = 0); void KeepDrawSnapshot(std::uint64_t address, std::size_t bytes, std::uint64_t generation, std::uint64_t registryGeneration, std::shared_ptr buffer, SnapshotUse use = SnapshotUse::Storage, std::uint32_t derived = 0); static constexpr std::size_t IndexRangeEntries = 16384; @@ -825,6 +829,14 @@ class Recorder { std::size_t bytes = 0; }; std::map drawSnapshots; + struct UnwrittenSnapshot { + std::size_t bytes; + std::shared_ptr buffer; + }; + std::unordered_map unwrittenSnapshots; + std::array unwrittenStamp{}; + std::size_t unwrittenBytes = 0; + void retireUnwrittenSnapshots(const std::array& stamp); std::array drawSnapshotPools; void eraseDrawSnapshot(std::map::iterator entry); struct IndexRange { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 11765fad89e4..ca01cdebeb6d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -505,6 +505,15 @@ class ShaderResources { std::vector descriptorBuffers; std::vector descriptorImages; std::vector descriptorWrites; + struct MovedWordBuffer { + std::size_t allocation; + std::vector words; + std::size_t size; + std::shared_ptr buffer; + }; + mutable std::vector movedWordBuffers; + mutable std::size_t movedWordBufferNext = 0; + mutable std::vector drawPoolSizes; // Guest buffer elements bound read-only: each use of this object skips that many pending-write // notes (counted in MarkGpuWrites for the [buffers] line). std::size_t readOnlyBuffers = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 9eba317ada82..89cbc5420810 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -2516,6 +2516,31 @@ void Recorder::eraseDrawSnapshot(std::map::iterat drawSnapshots.erase(entry); } +void Recorder::retireUnwrittenSnapshots(const std::array& stamp) { + if (stamp == unwrittenStamp) return; + unwrittenSnapshots.clear(); + unwrittenBytes = 0; + unwrittenStamp = stamp; +} + +std::shared_ptr Recorder::UnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp) { + retireUnwrittenSnapshots(stamp); + const auto found = unwrittenSnapshots.find(address); + if (found == unwrittenSnapshots.end() || found->second.bytes != bytes) return {}; + return found->second.buffer; +} + +void Recorder::NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, std::shared_ptr buffer) { + retireUnwrittenSnapshots(stamp); + if (const auto found = unwrittenSnapshots.find(address); found != unwrittenSnapshots.end()) { + unwrittenBytes -= found->second.bytes; + unwrittenSnapshots.erase(found); + } + if (unwrittenSnapshots.size() >= UnwrittenSnapshotEntries || bytes > DrawSnapshotBudget - unwrittenBytes) return; + unwrittenSnapshots.emplace(address, UnwrittenSnapshot{bytes, std::move(buffer)}); + unwrittenBytes += bytes; +} + std::shared_ptr Recorder::ReusableDrawSnapshot(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::uint32_t* derived, std::uint64_t generation) { auto found = use == SnapshotUse::Vertex ? drawSnapshots.lower_bound({address, use, bytes}) : drawSnapshots.find({address, use, bytes}); if (found == drawSnapshots.end() || std::get<0>(found->first) != address || std::get<1>(found->first) != use) return {}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index cd10a21e0d87..21ea3f92e938 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -4398,14 +4398,26 @@ std::shared_ptr ShaderResources::PrepareDrawBindi std::size_t totalSnapshotBytes = 0; auto result = std::make_shared(); std::vector selected; + std::array stamp{}; + bool stamped = false; for (std::size_t index = 0; index < allocations.size(); ++index) { const auto& item = allocations[index]; const auto override = std::find_if(moved.begin(), moved.end(), [&](const MovedBuffer& entry) { return entry.allocation == index; }); if (override != moved.end() && !override->words.empty()) { - auto buffer = std::make_shared(context, override->size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); - std::memcpy(buffer->Bytes().data(), override->words.data(), override->size); - for (const auto& patch : dataPatches) { - if (patch.allocation == index && patch.byte < override->size) buffer->Bytes()[patch.byte] = static_cast(patch.adjustment); + constexpr std::size_t MovedWordBufferSlots = 8; + const auto held = std::find_if(movedWordBuffers.begin(), movedWordBuffers.end(), [&](const MovedWordBuffer& entry) { return entry.allocation == index && entry.size == override->size && entry.words == override->words; }); + std::shared_ptr buffer; + if (held != movedWordBuffers.end()) { + buffer = held->buffer; + } else { + buffer = std::make_shared(context, override->size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::memcpy(buffer->Bytes().data(), override->words.data(), override->size); + for (const auto& patch : dataPatches) { + if (patch.allocation == index && patch.byte < override->size) buffer->Bytes()[patch.byte] = static_cast(patch.adjustment); + } + MovedWordBuffer entry{index, override->words, override->size, buffer}; + if (movedWordBuffers.size() < MovedWordBufferSlots) movedWordBuffers.push_back(std::move(entry)); + else movedWordBuffers[movedWordBufferNext++ % MovedWordBufferSlots] = std::move(entry); } selected.push_back(index); result->snapshots.push_back({0, std::move(buffer)}); @@ -4425,12 +4437,29 @@ std::shared_ptr ShaderResources::PrepareDrawBindi 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); + if (!stamped) { + stamp = {GuestMemory::TrackerGeneration(), GuestMemory::CollectEpoch(), registryGeneration}; + stamped = true; + } + auto buffer = recorder.UnwrittenDrawSnapshot(begin, bytes, stamp); + static const bool verifySnapshots = std::getenv("APS5_VERIFY_DRAW_SNAPSHOTS") != nullptr; + if (verifySnapshots && buffer != nullptr) { + static std::atomic checked{0}, differing{0}; + const auto slow = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, GuestMemory::CollectWrites(begin, bytes)); + const bool same = slow == buffer || (slow != nullptr && slow->Bytes().size() == buffer->Bytes().size() && std::memcmp(slow->Bytes().data(), buffer->Bytes().data(), buffer->Bytes().size()) == 0) || (slow == nullptr && std::memcmp(buffer->Bytes().data(), reinterpret_cast(begin), bytes) == 0); + if (!same) differing.fetch_add(1); + if (checked.fetch_add(1) % 200000 == 199999) std::fprintf(stderr, "[draw-snapshots] verify: %llu fast answers checked, %llu differing%c", static_cast(checked.load()), static_cast(differing.load()), 10); + } if (buffer == nullptr) { - buffer = std::make_shared(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); - std::memcpy(buffer->Bytes().data(), reinterpret_cast(begin), bytes); - recorder.KeepDrawSnapshot(begin, bytes, generation, registryGeneration, buffer); + const auto generation = GuestMemory::CollectWrites(begin, bytes); + buffer = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, generation); + if (buffer == nullptr) { + buffer = std::make_shared(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::memcpy(buffer->Bytes().data(), reinterpret_cast(begin), bytes); + recorder.KeepDrawSnapshot(begin, bytes, generation, registryGeneration, buffer); + } + if (GuestMemory::TrackerGeneration() == stamp[0]) recorder.NoteUnwrittenDrawSnapshot(begin, bytes, stamp, buffer); + else stamped = false; } selected.push_back(index); result->snapshots.push_back({begin, std::move(buffer)}); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index c551d5f73b4e..3093d5222142 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2675,6 +2675,83 @@ void drawSnapshotPatchTests(const Device& device) { } } +void drawSnapshotEpochTests(const Device& device) { + const auto& context = device.GetContext(); + constexpr std::size_t bytes = 4 * 65536; + std::unique_lock gpu(GpuMutex()); + void* block = context.hostImportAlignment != 0 ? AllocateWatched(bytes, 65536) : nullptr; + if (block == nullptr) { + std::cout << "host imports unavailable: draw snapshot reuse within a collect epoch not tested\n"; + return; + } + struct Release { + void* block; + ~Release() { ReleaseWatched(block, bytes); } + } release{block}; + auto* guest = static_cast(block); + for (std::size_t at = 0; at < bytes; ++at) guest[at] = static_cast(at * 7u + 3u); + 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 block refused: draw snapshot reuse within a collect epoch not tested\n"; + return; + } + constexpr std::size_t elementOffset = 4096; + constexpr std::size_t elementBytes = 2 * 65536; + const auto element = address + elementOffset; + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding binding; + binding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + binding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + binding.descriptorSet = 0; + binding.binding = 0; + binding.count = 1; + binding.guestDescriptor = {static_cast(element), static_cast(element >> 32u) & 0xffffu, static_cast(elementBytes), 0x31000000u}; + binding.bufferWritten = {false}; + program.bindings.push_back(binding); + 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 snapshot = [&] { + auto bindings = resources.PrepareDrawBindings(snapshotRecorder); + Require(bindings != nullptr && bindings->snapshots.size() == 1, "read-only draw input was not snapshotted"); + auto buffer = bindings->snapshots[0].buffer; + const auto contents = buffer->Bytes(); + Require(contents.size() == elementBytes && std::memcmp(contents.data(), reinterpret_cast(element), elementBytes) == 0, "a draw snapshot does not hold the guest bytes of its draw"); + return buffer; + }; + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto first = snapshot(); + Require(snapshot() == first, "a second draw in the same collect epoch copied its unchanged input again"); + guest[elementOffset + 65536 + 17] ^= 0xffu; + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto changed = snapshot(); + Require(changed != first, "a draw after a store and a new collect epoch reused the snapshot taken before the store"); + Require(snapshot() == changed, "a second draw after the store copied its input again"); + snapshotRecorder.Sync(); + } + std::cout << "draw snapshot reuse within a collect epoch: ok\n"; +} + void drawSnapshotCapTests(const Device& device, Recorder& recorder) { using namespace AgcDriver::GuestMemory; const auto& context = device.GetContext(); @@ -2871,6 +2948,24 @@ void drawSnapshotEvictionTests(const Device& device) { Require(cache.ReusableDrawSnapshot(address, 16) == nullptr && cache.ReusableDrawSnapshot(address, 16) == nullptr, "a snapshot outlived a CPU store"); } +void unwrittenSnapshotTests(const Device& device) { + Recorder cache(device.GetContext()); + const auto buffer = std::make_shared(device.GetContext(), 16, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + constexpr std::uint64_t address = 0x10000000; + const std::array stamp{1, 1, 1}; + const std::array next{1, 2, 1}; + cache.NoteUnwrittenDrawSnapshot(address, 16, stamp, buffer); + Require(cache.UnwrittenDrawSnapshot(address, 16, stamp) == buffer, "a noted unwritten snapshot is not found under its stamp"); + cache.NoteUnwrittenDrawSnapshot(address + 8192, Recorder::DrawSnapshotBudget, stamp, buffer); + Require(cache.UnwrittenDrawSnapshot(address + 8192, Recorder::DrawSnapshotBudget, stamp) == nullptr, "unwritten snapshots grew past the byte budget"); + cache.NoteUnwrittenDrawSnapshot(address, Recorder::DrawSnapshotBudget, stamp, buffer); + Require(cache.UnwrittenDrawSnapshot(address, Recorder::DrawSnapshotBudget, stamp) == buffer, "replacing an unwritten snapshot kept the bytes of the old one"); + Require(cache.UnwrittenDrawSnapshot(address, Recorder::DrawSnapshotBudget, next) == nullptr && cache.UnwrittenDrawSnapshot(address, Recorder::DrawSnapshotBudget, stamp) == nullptr, "an unwritten snapshot outlived a newer stamp"); + for (std::size_t entry = 0; entry < Recorder::UnwrittenSnapshotEntries; ++entry) cache.NoteUnwrittenDrawSnapshot(address + entry * 16, 1, next, buffer); + cache.NoteUnwrittenDrawSnapshot(address + Recorder::UnwrittenSnapshotEntries * 16, 1, next, buffer); + Require(cache.UnwrittenDrawSnapshot(address, 1, next) == buffer && cache.UnwrittenDrawSnapshot(address + Recorder::UnwrittenSnapshotEntries * 16, 1, next) == nullptr, "unwritten snapshots grew past the entry cap"); +} + void drawInputReuseTests(const Device& device, Recorder& recorder) { using namespace AgcDriver::GuestMemory; using Use = Recorder::SnapshotUse; @@ -7602,9 +7697,14 @@ int main(int argc, char** argv) { } if (argc == 2 && std::string_view(argv[1]) == "--draw-snapshot-cap-only") { drawSnapshotCapTests(device, recorder); + unwrittenSnapshotTests(device); std::cout << "Draw snapshot cap tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--draw-snapshot-epoch-only") { + drawSnapshotEpochTests(device); + return 0; + } if (argc == 2 && (std::string_view(argv[1]) == "--vertex-target-range-copied" || std::string_view(argv[1]) == "--vertex-target-range-imported")) { return vertexTargetRangeTests(device, recorder, std::string_view(argv[1]) == "--vertex-target-range-imported") ? 0 : 77; @@ -7644,6 +7744,7 @@ int main(int argc, char** argv) { drawDescriptorWriteTests(device, recorder); drawSnapshotCapTests(device, recorder); drawSnapshotEvictionTests(device); + unwrittenSnapshotTests(device); drawInputReuseTests(device, recorder); drawInputInPlaceTests(device, recorder); RunResidentPresentTests(device.GetContext()); @@ -7689,6 +7790,7 @@ int main(int argc, char** argv) { gpu.unlock(); hostImportUnmapTests(device, recorder); drawSnapshotPatchTests(device); + drawSnapshotEpochTests(device); gpu.lock(); recorder.Activate(); std::cout << "Recorder read tracking and label tests passed\n"; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index ca8db69c907d..9ff9e046c537 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -54,7 +54,7 @@ - [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`. - [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending, and ranges that leave the watch through a failed collect, have no saved bytes; storing such results throws. -- Windows host-pointer imports of the guest mapping itself are compared instead of write-watched: a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for excluded blocks; restoring tracking on import retirement without replacing the backing remains unimplemented. +- Windows host-pointer imports of the guest mapping itself are compared instead of write-watched: a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. - [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. - [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when its offset in the backing file is not a multiple of the buffer's sparse block size (64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view.