perf(agc): drop the guest byte snapshot of a large cached texture the write tracker covers

Every snapshot texture in the sampled texture cache kept a host copy of
its guest bytes, used only when the write tracker reports the surface
written, to tell a rewrite of the same bytes from a change. The cache
budget counts the GPU allocation alone, so the host copies grew with it:
PPSA05684 held 2.7 GB of them in its first level.

A snapshot larger than 256 KiB is released after the upload when the
write tracker (a write generation or an untracked key) can prove the
surface unchanged. A tracked write to such a surface re-uploads it
instead of comparing bytes; smaller snapshots, and surfaces the tracker
cannot vouch for, keep their copy as before.

agc_driver_large_tracked_snapshot samples a 1 MiB surface, committed
and partially resident: an unchanged surface is reused without reading
guest memory, a CPU write is uploaded, and a driver store of the same
bytes is uploaded again because no snapshot is left to compare.

AI-assisted: yes
This commit is contained in:
XairRrR committed 2026-10-17 16:22:53 +02:00
1 parent 47a266cf97
commit 6849eeaec0
3 files changed
+71 -12

No files matched your search

@@ -1599,6 +1599,11 @@ if(BUILD_TESTING)
if(WIN32)
set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_large_tracked_snapshot COMMAND agc_driver_partially_resident_snapshot_tests --large-tracked)
set_tests_properties(agc_driver_large_tracked_snapshot PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_driver_large_tracked_snapshot PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
if(WIN32)
add_test(NAME agc_driver_untracked_snapshot_hint COMMAND agc_driver_partially_resident_snapshot_tests --untracked-hint)
set_tests_properties(agc_driver_untracked_snapshot_hint PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30
@@ -118,6 +118,7 @@ struct CachedTexture {
// The untrackedKey the snapshot was last read or compared equal at, for a surface outside the
// write watch; zero when none was taken.
std::array<std::uint64_t, 4> untracked{};
std::uint64_t surfaceBytes = 0;
};
// Entries in use order (front = most recent) with a hash index by key: a lookup is O(1) and the
@@ -729,18 +730,19 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
if (profile) LookupOutcomes::Add(*keys != DccKeys::Uncompressed ? LookupOutcomes::SampledHitClearedView : LookupOutcomes::SampledHitView, start);
return it->texture;
}
} else if (source == nullptr && it->bytes.size() == guestBytes && it->keys == *keys) {
} else if (source == nullptr && it->surfaceBytes == guestBytes && it->keys == *keys) {
// Unwritten pages need no comparison; partially resident textures compare only committed
// pages. The compare goes through the flush hook: it waits for recorded work over the
// surface (counted, and named for the [hooksync] line).
const auto equalsCommitted = [&] {
if (it->bytes.empty()) return false;
if (Recorder::SnapshotWriteOverlaps(address, bytes)) counters.pendingReads.fetch_add(1, std::memory_order_relaxed);
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureCompare);
return GuestMemory::EqualsCommitted(address, it->bytes);
};
const auto unchanged = [&] {
if (it->committed != committed) return false;
return committed.has_value() ? !Recorder::SnapshotWriteOverlaps(address, it->bytes.size()) && GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation);
return committed.has_value() ? !Recorder::SnapshotWriteOverlaps(address, static_cast<std::size_t>(it->surfaceBytes)) && GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, static_cast<std::size_t>(it->surfaceBytes), it->generation);
};
const auto kept = [&] {
if (untracked[0] == 0) return false;
@@ -764,6 +766,7 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
}
CachedTexture entry{key, address, std::vector<std::byte>(source != nullptr ? 0u : bytes), nullptr, *keys, snapshotGeneration};
entry.committed = std::move(committed);
entry.surfaceBytes = bytes;
if (source != nullptr) {
entry.source = source;
entry.sourceVersion = source->Version();
@@ -792,6 +795,8 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
}
entry.texture = std::make_shared<Texture>(context, *context.detiler, resource, components, entry.bytes, depthCompare);
entry.accounted = entry.texture->AllocationBytes();
constexpr std::size_t SnapshotCompareBytes = 256u << 10u;
if (entry.bytes.size() > SnapshotCompareBytes && (entry.generation != 0 || entry.untracked[0] != 0)) std::vector<std::byte>().swap(entry.bytes);
counters.snapshots.fetch_add(1, std::memory_order_relaxed);
}
const auto budget = sampledBudget(context, cache);
@@ -45,6 +45,8 @@ constexpr std::uint32_t TileR64KBX = 0x1b;
constexpr std::uint32_t Type2D = 9;
constexpr std::size_t BlockBytes = 65536;
constexpr std::size_t SurfaceBytes = std::size_t{Side} * Side * 4u;
constexpr std::uint32_t LargeSide = 512;
constexpr std::size_t LargeSurfaceBytes = std::size_t{LargeSide} * LargeSide * 4u;
constexpr int Skipped = 77;
class Device {
@@ -148,16 +150,16 @@ private:
class ReservedSurface {
public:
ReservedSurface() {
block = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, BlockBytes));
explicit ReservedSurface(std::size_t bytes) : size(bytes) {
block = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(size, BlockBytes));
Require(block != nullptr, "partially resident snapshot: cannot reserve the guest surface");
GuestAllocations::Mutation().Add(block, SurfaceBytes, true, true, true);
GuestAllocations::Mutation().Add(block, size, true, true, true);
}
~ReservedSurface() {
GuestAllocations::Mutation().Remove(block);
Decommit(0, SurfaceBytes);
GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes);
Decommit(0, size);
GuestArena::GuestArenaRelease_nid_postfix(block, size);
}
ReservedSurface(const ReservedSurface&) = delete;
@@ -188,6 +190,7 @@ public:
std::uint64_t Address() const { return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(block)); }
private:
std::size_t size = 0;
std::uint8_t* block = nullptr;
};
@@ -199,11 +202,11 @@ void CountReads(std::uint64_t address, std::size_t bytes) {
if (address < surfaceEnd.load() && address + bytes > surfaceBegin.load()) surfaceReads.fetch_add(1);
}
std::array<std::uint32_t, 8> Descriptor(std::uint64_t address) {
std::array<std::uint32_t, 8> Descriptor(std::uint64_t address, std::uint32_t side = Side) {
return {
static_cast<std::uint32_t>(address >> 8u),
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u),
((Side - 1u) >> 2u) | ((Side - 1u) << 14u),
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((side - 1u) & 3u) << 30u),
((side - 1u) >> 2u) | ((side - 1u) << 14u),
0xfacu | (TileR64KBX << 20u) | (Type2D << 28u),
0u,
0u,
@@ -262,6 +265,48 @@ void Run(const Context& context, ReservedSurface& surface) {
AgcDriver::GuestMemory::SetFlushHook(nullptr);
}
void LargeTracked(const Context& context, ReservedSurface& surface, std::size_t committedBytes) {
const auto address = surface.Address();
const auto words = Descriptor(address, LargeSide);
const auto layout = committedBytes == LargeSurfaceBytes ? std::string("committed") : std::string("partially resident");
Require(DescribeSurface(DecodeTextureResource(words)).guestBytes == LargeSurfaceBytes, "the large test descriptor does not describe the reserved surface");
surface.Commit(0, committedBytes);
std::memset(surface.Data(), 0x5a, committedBytes);
surfaceBegin = address;
surfaceEnd = address + LargeSurfaceBytes;
std::shared_ptr<Texture> previous;
const auto sample = [&](const std::string& when, bool uploaded) {
AgcDriver::GuestMemory::BumpCollectEpoch();
surfaceReads = 0;
auto texture = CachedSampledTexture(context, words);
Require(texture != nullptr && !texture->ViewsStorageImage(), "the large " + layout + " surface was not sampled through a snapshot " + when);
const auto reads = surfaceReads.load();
if (uploaded && (reads == 0 || texture == previous)) throw std::runtime_error("sampling the large " + layout + " surface " + when + " kept the texture uploaded before it");
if (!uploaded && (reads != 0 || texture != previous)) throw std::runtime_error("sampling the large " + layout + " surface " + when + " uploaded it again although the write tracker saw no write (" + std::to_string(reads) + " reads)");
previous = texture;
};
AgcDriver::GuestMemory::SetFlushHook(&CountReads);
try {
sample("at its first use", true);
sample("again", false);
surface.Data()[0x100] = 0x11;
sample("after a CPU write", true);
sample("after the upload of the written surface", false);
surface.Data()[committedBytes - 0x40] = 0x22;
sample("after a CPU write to its last committed block", true);
sample("after the upload of the written block", false);
AgcDriver::GuestMemory::MarkWritten(address + 0x200, 4);
sample("after a driver store of the bytes already there, which a snapshot it did not keep cannot tell from a change", true);
sample("after the upload that followed the driver store", false);
} catch (...) {
AgcDriver::GuestMemory::SetFlushHook(nullptr);
throw;
}
AgcDriver::GuestMemory::SetFlushHook(nullptr);
ClearCachedTextures(context.device);
surface.Decommit(0, committedBytes);
}
#ifdef _WIN32
void UntrackedHint(const Context& context, ReservedSurface& surface) {
const auto address = surface.Address();
@@ -368,9 +413,13 @@ int main(int argc, char** argv) {
auto context = device->GetContext();
TextureDetiler detiler(context);
context.detiler = &detiler;
ReservedSurface surface;
const bool large = argc == 2 && std::string_view(argv[1]) == "--large-tracked";
ReservedSurface surface(large ? LargeSurfaceBytes : SurfaceBytes);
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
if (argc == 2 && std::string_view(argv[1]) == "--pending-stores") PendingStores(context, surface);
if (large) {
LargeTracked(context, surface, LargeSurfaceBytes);
LargeTracked(context, surface, BlockBytes);
} else if (argc == 2 && std::string_view(argv[1]) == "--pending-stores") PendingStores(context, surface);
else if (argc == 2 && std::string_view(argv[1]) == "--untracked-hint") {
#ifdef _WIN32
UntrackedHint(context, surface);