perf(agc): memoize untracked texture comparisons by the stores that reach the surface
The memo stamp held TrackerGeneration, which every collect and driver store advances, so it almost never matched. Driver stores into uncovered ranges record their generation per 64 KiB block, and NewestStamp of the surface replaces TrackerGeneration in the stamp.
This commit is contained in:
parent
e7c70a2df9
commit
ca99658cef
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -859,6 +859,40 @@ struct WriteTracker {
|
|||
#endif
|
||||
}
|
||||
|
||||
static constexpr std::size_t UnstampedSlots = 1u << 16;
|
||||
std::vector<std::uint32_t> 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<std::uint64_t>(address, base);
|
||||
end = std::min<std::uint64_t>(end, base + size);
|
||||
#else
|
||||
end = std::min<std::uint64_t>(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; };
|
||||
|
|
@ -1235,7 +1269,10 @@ std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::func
|
|||
const auto stampStored = [](WriteTracker& tracker, std::pair<std::uint64_t, std::uint64_t> stored) -> std::uint64_t {
|
||||
if (stored.second <= stored.first) return 0;
|
||||
++tracker.generation;
|
||||
if (!tracker.watched || !tracker.covers(stored.first, static_cast<std::size_t>(stored.second - stored.first))) return 0;
|
||||
if (!tracker.watched || !tracker.covers(stored.first, static_cast<std::size_t>(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);
|
||||
|
|
@ -1280,7 +1317,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);
|
||||
|
|
@ -1288,6 +1328,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<std::uintptr_t>(blockFirst), static_cast<std::size_t>(blockStop - blockFirst), &stampWrittenRun, &runs)) {
|
||||
unwatchLocked(tracker, blockFirst, static_cast<std::size_t>(blockStop - blockFirst));
|
||||
tracker.noteUnstampedStore(address, address + bytes);
|
||||
return 0;
|
||||
}
|
||||
if (runs.stamped != 0) ++tracker.generation;
|
||||
|
|
@ -1347,6 +1388,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();
|
||||
|
|
|
|||
|
|
@ -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<std::uint64_t, 4> stamp{GuestMemory::CollectEpoch(), GuestMemory::TrackerGeneration(), GuestMemory::ForgetSerial(), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0};
|
||||
const std::array<std::uint64_t, 4> 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);
|
||||
|
|
|
|||
|
|
@ -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<std::uint64_t>(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<volatile std::uint8_t*>(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<std::uint8_t*>(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<volatile std::uint8_t*>(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);
|
||||
|
|
@ -310,6 +336,7 @@ int main() {
|
|||
CheckSharedBlock();
|
||||
CheckOwnStore();
|
||||
CheckUnwatch();
|
||||
CheckNewestStamp();
|
||||
#ifdef _WIN32
|
||||
CheckPrivateMappingReuse();
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue