diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index 5321424ce..b118da7bf 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -101,6 +101,7 @@ std::uint64_t CollectWritesUncached(std::uint64_t address, std::size_t bytes); // tracker mutex, so every collect that bumped before it has finished its walk and every later // collect stamps newer. std::uint64_t TrackerGeneration(); +std::uint64_t NewestStamp(std::uint64_t address, std::size_t bytes); // Whether no CPU write touched the range since `generation`: an uncached resetting collect of the // range's pages first (a game store not collected yet becomes a block stamp), then the compare over // the stamps collects make (dirty pages), not the MarkWritten stamps of the driver's own GPU label diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 5a9ba4cd2..834dccfce 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -873,6 +873,40 @@ struct WriteTracker { #endif } + static constexpr std::size_t UnstampedSlots = 1u << 16; + std::vector unstampedStores; + + void noteUnstampedStore(std::uint64_t address, std::uint64_t end) { + if (unstampedStores.empty()) unstampedStores.assign(UnstampedSlots, 0); + const auto first = address / WriteBlockBytes; + const auto last = (end - 1) / WriteBlockBytes; + if (last - first >= UnstampedSlots) { + std::fill(unstampedStores.begin(), unstampedStores.end(), generation); + return; + } + for (auto block = first; block <= last; ++block) unstampedStores[block % UnstampedSlots] = generation; + } + + std::uint32_t newestStamp(std::uint64_t address, std::uint64_t end) const { + std::uint32_t newest = 0; + if (!unstampedStores.empty()) { + const auto first = address / WriteBlockBytes; + const auto last = (end - 1) / WriteBlockBytes; + if (last - first >= UnstampedSlots) newest = *std::max_element(unstampedStores.begin(), unstampedStores.end()); + else for (auto block = first; block <= last; ++block) newest = std::max(newest, unstampedStores[block % UnstampedSlots]); + } + if (!watched) return newest; +#ifdef _WIN32 + address = std::max(address, base); + end = std::min(end, base + size); +#else + end = std::min(end, LeafCount * LeafBlocks * WriteBlockBytes); +#endif + if (address >= end) return newest; + for (auto block = blockOf(address); block <= blockOf(end - 1); ++block) newest = std::max(newest, stampOf(block)); + return newest; + } + bool unchanged(std::uint64_t first, std::uint64_t last, std::uint64_t since) const { if (first == last) return stampOf(first) <= since; const auto newer = [since](std::uint32_t stamp) { return stamp > since; }; @@ -1237,7 +1271,10 @@ std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::func const auto stampStored = [](WriteTracker& tracker, std::pair stored) -> std::uint64_t { if (stored.second <= stored.first) return 0; ++tracker.generation; - if (!tracker.watched || !tracker.covers(stored.first, static_cast(stored.second - stored.first))) return 0; + if (!tracker.watched || !tracker.covers(stored.first, static_cast(stored.second - stored.first))) { + tracker.noteUnstampedStore(stored.first, stored.second); + return 0; + } for (auto block = tracker.blockOf(stored.first); block <= tracker.blockOf(stored.second - 1); ++block) { tracker.stamp(block, tracker.generation, StampKind::Driver); tracker.noteDriverStore(block, stored.first, stored.second, tracker.generation); @@ -1279,7 +1316,10 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { tracker.initialize(); if (bytes == 0) return 0; ++tracker.generation; - if (!tracker.watched || !tracker.covers(address, bytes)) return 0; + if (!tracker.watched || !tracker.covers(address, bytes)) { + tracker.noteUnstampedStore(address, address + bytes); + return 0; + } #ifndef _WIN32 { const auto blockFirst = address & ~(std::uint64_t{WriteBlockBytes} - 1); @@ -1287,6 +1327,7 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { StampRuns runs{tracker, StampKind::Fresh}; if (!GuestWriteWatch::GuestWriteWatchCollectFresh_nid_postfix(static_cast(blockFirst), static_cast(blockStop - blockFirst), &stampWrittenRun, &runs)) { unwatchLocked(tracker, blockFirst, static_cast(blockStop - blockFirst)); + tracker.noteUnstampedStore(address, address + bytes); return 0; } if (runs.stamped != 0) ++tracker.generation; @@ -1346,6 +1387,13 @@ std::uint64_t TrackerGeneration() { return tracker.generation; } +std::uint64_t NewestStamp(std::uint64_t address, std::size_t bytes) { + auto& tracker = Tracker(); + const auto lock = lockTracker(tracker); + tracker.initialize(); + return bytes == 0 ? 0 : tracker.newestStamp(address, address + bytes); +} + bool UnchangedSinceCollected(std::uint64_t address, std::size_t bytes, std::uint64_t generation) { if (CollectWritesUncached(address, bytes) == 0) return false; auto& tracker = Tracker(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 2ac6da038..b6072c675 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1909,7 +1909,7 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, const auto first = address / blockBytes; Require(changed.size() == (end - 1) / blockBytes - first + 1, "texture comparison block count differs"); const auto* recorder = Recorder::Active(); - const std::array stamp{GuestMemory::CollectEpoch(), GuestMemory::TrackerGeneration(), GuestMemory::ForgetSerial(), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0}; + const std::array stamp{GuestMemory::CollectEpoch(), GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetSerial(), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0}; const bool cacheable = memoize && stamp[0] != 0 && (stamp[2] & 1u) == 0 && address == descriptor.baseAddress && bytes == guestBytes; if (cacheable && comparedGuestBytes == stamp) { std::fill(changed.begin(), changed.end(), GuestMemory::BlockUnchanged); diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 5489cac1f..8528450d4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -152,6 +152,32 @@ void CheckUnwatch() { Require(!UnchangedSince(base, Block, adjacent), "unwatch hid a CPU edit in unrelated memory"); } +void CheckNewestStamp() { + void* memory = AllocateWatched(4 * Block); + const auto base = reinterpret_cast(memory); + const auto first = base + Block; + const auto second = base + 2 * Block; + std::memset(memory, 0x11, 4 * Block); + BumpCollectEpoch(); + CollectWrites(base, 4 * Block); + Unwatch(first, 2 * Block); + const auto firstBefore = NewestStamp(first, Block); + const auto secondBefore = NewestStamp(second, Block); + MarkWritten(second + 16, 4); + Require(NewestStamp(second, Block) > secondBefore, "a driver store into unwatched memory did not advance its stamp"); + Require(NewestStamp(first, Block) == firstBefore, "a driver store advanced the stamp of other unwatched memory"); + const auto generation = TrackerGeneration(); + static_cast(memory)[0] = 0x22; + CollectWritesUncached(base, Block); + Require(TrackerGeneration() > generation && NewestStamp(first, Block) == firstBefore, "a CPU store into watched memory advanced the stamp of unwatched memory"); + StoreOwnBytes(first, 1, [&] { *reinterpret_cast(first) = 0x42; }); + Require(NewestStamp(first, Block) > firstBefore, "an own store into unwatched memory did not advance its stamp"); + const auto partial = NewestStamp(base + 3 * Block - 8, Block + 8); + static_cast(memory)[3 * Block] = 0x33; + CollectWritesUncached(base + 3 * Block, Block); + Require(NewestStamp(base + 3 * Block - 8, Block + 8) > partial, "a CPU store into the watched part of a partly watched range did not advance its stamp"); +} + #ifdef _WIN32 void CheckPrivateMappingReuse() { void* memory = AllocateWatched(3 * Block); @@ -371,6 +397,7 @@ int main() { CheckSharedBlock(); CheckOwnStore(); CheckUnwatch(); + CheckNewestStamp(); #ifdef _WIN32 CheckPrivateMappingReuse(); CheckHostHole(); diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 4d5121846..a1d0da4e4 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -97,6 +97,17 @@ flowchart LR - A draw's `Graphics::Pipeline` comes from `CachedPipeline`. When the device has `VK_EXT_graphics_pipeline_library` with fast linking, `VK_EXT_extended_dynamic_state` and `VK_KHR_dynamic_rendering`, draws render with `vkCmdBeginRenderingKHR` instead of render passes and the pipeline is linked from four libraries (vertex input, pre-rasterization shaders, fragment shader, fragment output) kept per device in [`PipelineLibrary.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp). A library built for one pipeline is reused by the next: another blend, vertex layout or target format builds only that part (the shader libraries name no attachment format), and cull mode, front face, depth and stencil state are dynamic, so changing them only links. The fast link is what the draw waits for; a background thread then links the same libraries with link-time optimization and the pipeline binds that one from the next draw on. Push constants then use a 256-byte block with a 128-byte slot per side: the pre-rasterization stages stack from byte 0, the pixel shader always starts at byte 128 (`StagePushOffset`), so its library does not depend on how much push data its vertex shader uses. Pipelines with per-face depth bias, a separate alpha blend into an 8_8 target with the alternate component order (both draw twice), rect lists, geometry shaders or a stage without a variant id are created whole, still with dynamic rendering; devices without the extensions (or with `APS5_NO_GPL=1`) keep render passes and whole pipelines. - A draw whose `PA_SC_AA_CONFIG` selects 2, 4 or 8 samples renders into host-only multisampled images. [`MultisampleTarget.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) keeps one image per device, surface address, format, extent and sample count (`MultisampleTargetView`); the pipeline, render pass and library keys carry `State::samples`, and a multisampled depth target is a multisampled depth surface. The guest sample planes and FMASK are never read or written, so the registry also guards the target's guest range: a CPU read (`GuestMemory::SetReadGuard`), a DMA copy, and a sampled or storage image read of it throw. A `CB_COLOR_CONTROL` resolve mode over a multisampled CB0 runs as a render pass whose resolve attachment is CB1's resident image (`ResolveMultisampleTarget`). A zero fill of the target's CMASK (`NoteColorMetadataFill`, from fill kernels and CP DMA fills) clears the image at its next use. +### Untracked storage image comparisons + +A storage image over guest memory that the write watch does not cover (a host-pointer import on a driver that drops its write protection, or an unwatched range) has no block stamps to tell a CPU edit apart, so `StorageTexture::compareUntracked` in [`Texture.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) compares the guest bytes with the image's snapshot. A whole-surface comparison that found no change is remembered under a stamp of: + +- the calling queue worker's collect epoch (`GuestMemory::CollectEpoch`), which advances at every ordering point where a CPU store may have landed (submission start, a satisfied `WAIT_REG_MEM`, a drain); a thread that never bumps it gets epoch 0 and is never memoized; +- `GuestMemory::NewestStamp` of the surface: the newest block stamp of its watched part, and the generation of the newest driver store (`MarkWritten`, `StoreOwnBytes`) into its unwatched part; +- `GuestMemory::ForgetSerial`, which changes when guest memory is released or remapped; +- the recorder's newest pending GPU write note over the surface. + +The comparison runs again when any of them changes. A CPU store into unwatched memory is therefore seen at the next ordering point, the same bound the collect memo gives watched memory. Driver stores into unwatched memory are recorded per 64 KiB block in a fixed table of 65,536 slots indexed by block number modulo the table size, each slot holding the newest generation stored to any of its blocks. A collision can only raise a surface's stamp, which costs one comparison and never skips one. `agc_driver_dcc_keys_first_write` (`RunUntrackedReuse`) checks that a CPU edit to an unwatched image is uploaded after the next epoch, and `agc_write_tracking` (`CheckNewestStamp`) checks which stores advance `NewestStamp`. + ### Shader MODE register access `GuestContext::floatMode` carries the initial shader mode through `Recompile` and `TranslateOptions` into each block's `TranslationContext`. `s_getreg_b32` reads confined to MODE bits 0–3 return the selected initial rounding bits in the destination's low bits. Bits 0–1 select f32 rounding; bits 2–3 select f16/f64 rounding. Missing mode metadata retains the legacy round-to-nearest-even assumption. Reading other MODE fields or hardware registers throws.