Merge pull request #8026 from NFSS10/perf/agc-sampled-snapshot-buffer-reuse

perf(agc): reuse sampled snapshot byte buffers across cache entry replacement
This commit is contained in:
Adrià authored and GitHub committed 2026-10-23 18:26:36 +02:00
commit 990f45e17d
5 files changed
+139 -18

No files matched your search

@@ -37,9 +37,12 @@ std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memor
bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport);
std::optional<std::uint32_t> LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory);
std::uint64_t StorageTextureCacheBudget(const Context& context);
inline constexpr std::uint64_t SpareSnapshotPresents = 16;
struct TextureCacheUse {
std::size_t sampledEntries = 0;
std::size_t spareEntries = 0;
std::uint64_t sampledBytes = 0;
std::uint64_t spareBytes = 0;
std::uint64_t storageBytes = 0;
};
TextureCacheUse TextureCacheUsage();
@@ -665,7 +665,10 @@ bool FillDccClear(VkFormat format, DccKeys keys, bool alphaOnMsb, std::span<std:
return true;
}
Layout layout{};
if (!IsDccClear(keys) || keys == DccKeys::ClearRegister || !LayoutFor(format, layout)) return false;
if (!IsDccClear(keys) || keys == DccKeys::ClearRegister || !LayoutFor(format, layout)) {
std::fill(bytes.begin(), bytes.end(), std::byte{0});
return false;
}
const bool color = keys == DccKeys::Clear1110 || keys == DccKeys::Clear1111;
const bool alpha = keys == DccKeys::Clear0001 || keys == DccKeys::Clear1111;
const int alphaChannel = layout.channels == 3 ? -1 : alphaOnMsb ? static_cast<int>(layout.channels) - 1 : 0;
@@ -680,7 +683,9 @@ bool FillDccClear(VkFormat format, DccKeys keys, bool alphaOnMsb, std::span<std:
if (((value >> i) & 1u) != 0) element[bit / 8u] |= static_cast<std::byte>(1u << (bit % 8u));
}
}
for (std::size_t offset = 0; offset + element.size() <= bytes.size(); offset += element.size()) std::memcpy(bytes.data() + offset, element.data(), element.size());
std::size_t offset = 0;
for (; offset + element.size() <= bytes.size(); offset += element.size()) std::memcpy(bytes.data() + offset, element.data(), element.size());
std::fill(bytes.begin() + offset, bytes.end(), std::byte{0});
return true;
}
@@ -133,6 +133,8 @@ struct TextureCache {
std::list<CachedTexture> entries;
std::unordered_map<TextureKey, std::list<CachedTexture>::iterator, TextureKeyHash> index;
std::unordered_multimap<const StorageTexture*, std::list<CachedTexture>::iterator> views;
std::vector<std::pair<std::vector<std::byte>, std::uint64_t>> spare;
std::uint64_t spareBytes = 0;
std::uint64_t bytes = 0;
std::size_t passed = 0;
std::chrono::steady_clock::time_point passedSince{};
@@ -260,6 +262,39 @@ void touchTexture(TextureCache& cache, std::list<CachedTexture>::iterator it) {
it->present = Recorder::Presents();
}
constexpr std::uint64_t SpareSnapshotBytes = 256ull << 20;
void parkSnapshot(TextureCache& cache, std::vector<std::byte>& bytes) {
const auto size = bytes.size();
if (size == 0) return;
const auto present = Recorder::Presents();
while (!cache.spare.empty() && (present - cache.spare.front().second > SpareSnapshotPresents || cache.spareBytes + size > SpareSnapshotBytes)) {
cache.spareBytes -= cache.spare.front().first.size();
cache.spare.erase(cache.spare.begin());
}
if (cache.spareBytes + size <= SpareSnapshotBytes) {
cache.spareBytes += size;
cache.spare.emplace_back(std::move(bytes), present);
}
bytes = std::vector<std::byte>{};
}
std::vector<std::byte> spareSnapshot(TextureCache& cache, std::size_t bytes) {
const auto present = Recorder::Presents();
while (!cache.spare.empty() && present - cache.spare.front().second > SpareSnapshotPresents) {
cache.spareBytes -= cache.spare.front().first.size();
cache.spare.erase(cache.spare.begin());
}
for (auto spare = cache.spare.begin(); spare != cache.spare.end(); ++spare) {
if (spare->first.size() != bytes) continue;
cache.spareBytes -= bytes;
std::vector<std::byte> taken = std::move(spare->first);
cache.spare.erase(spare);
return taken;
}
return {};
}
bool admitTexture(TextureCache& cache, const TextureKey& key, std::uint64_t incoming, std::uint64_t budget) {
if (cache.bytes + incoming <= budget) return true;
if (const auto now = std::chrono::steady_clock::now(); now - cache.passedSince >= std::chrono::seconds(1)) {
@@ -859,6 +894,7 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
std::lock_guard lock(cache.mutex);
const CacheHold held(sampledCacheHeld);
dropUncachedViews(cache);
std::vector<std::byte> recycled;
if (auto it = findTexture(cache, key); it != cache.entries.end()) {
if (it->source != nullptr) {
// The view follows the storage image, whatever the GPU wrote to it since; guest memory
@@ -905,10 +941,14 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
return it->texture;
}
}
if (it->bytes.size() == bytes) recycled = std::move(it->bytes);
else parkSnapshot(cache, it->bytes);
eraseTexture(cache, it);
++cache.counts.stale;
}
CachedTexture entry{key, address, std::vector<std::byte>(source != nullptr ? 0u : bytes), nullptr, *keys, snapshotGeneration};
if (source != nullptr) parkSnapshot(cache, recycled);
else if (recycled.empty()) recycled = spareSnapshot(cache, bytes);
CachedTexture entry{key, address, source != nullptr ? std::vector<std::byte>{} : (recycled.empty() ? std::vector<std::byte>(bytes) : std::move(recycled)), nullptr, *keys, snapshotGeneration};
entry.committed = std::move(committed);
entry.surfaceBytes = bytes;
const auto createTexture = [&](auto&&... args) {
@@ -950,7 +990,7 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
entry.texture = createTexture(*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);
if (entry.bytes.size() > SnapshotCompareBytes && (entry.generation != 0 || entry.untracked[0] != 0)) parkSnapshot(cache, entry.bytes);
counters.snapshots.fetch_add(1, std::memory_order_relaxed);
}
const auto budget = sampledBudget(context, cache);
@@ -1266,6 +1306,8 @@ TextureCacheUse TextureCacheUsage() {
dropUncachedViews(sampled);
use.sampledEntries = sampled.entries.size();
use.sampledBytes = sampled.bytes;
use.spareEntries = sampled.spare.size();
use.spareBytes = sampled.spareBytes;
}
use.storageBytes = storageCacheBytes();
return use;
@@ -1311,6 +1353,8 @@ void ClearCachedTextures(VkDevice device) {
if (it->key.device == device) eraseTexture(sampled, it++);
else ++it;
}
sampled.spare.clear();
sampled.spareBytes = 0;
}
auto& storage = StorageTextures();
std::vector<std::shared_ptr<StorageTexture>> cleared;
@@ -223,6 +223,22 @@ void packedFloatDccClearTests() {
Require(TextureClearKeys(resource, 1024) == DccKeys::Clear1111, "packed float texture discarded its white fast-clear keys");
}
void fillContractTests() {
std::array<std::uint32_t, 4> texels;
texels.fill(0xdeadbeefu);
auto span = std::as_writable_bytes(std::span(texels));
Require(!FillDccClear(VK_FORMAT_R8G8B8A8_UNORM, DccKeys::ClearRegister, false, span), "a register clear encoded a texel");
for (const auto texel : texels) Require(texel == 0u, "a declined clear left its span untouched");
texels.fill(0xdeadbeefu);
Require(!FillDccClear(VK_FORMAT_R8G8B8A8_UNORM, DccKeys::Uncompressed, false, span), "an uncompressed code encoded a texel");
for (const auto texel : texels) Require(texel == 0u, "a declined clear left its span untouched");
std::array<std::byte, 10> partial;
partial.fill(std::byte{0x5a});
Require(FillDccClear(VK_FORMAT_B10G11R11_UFLOAT_PACK32, DccKeys::Clear1111, false, partial), "packed float fast clear was not encoded");
const std::array<std::byte, 4> white{std::byte{0xc0}, std::byte{0x03}, std::byte{0x1e}, std::byte{0x78}};
for (std::size_t index = 0; index < partial.size(); ++index) Require(partial[index] == (index < 8u ? white[index % 4u] : std::byte{0}), "a clear fill over a partial trailing element did not zero the tail");
}
}
void RunTextureFormatTests() {
@@ -287,5 +303,6 @@ void RunTextureFormatTests() {
convertedDccClearTests();
singleDccKeysTests();
packedFloatDccClearTests();
fillContractTests();
gcnBufferFormatTests();
}
@@ -283,38 +283,38 @@ private:
class WatchedSurface {
public:
explicit WatchedSurface(const Context& context) : context(context) {
explicit WatchedSurface(const Context& context, std::size_t bytes = SurfaceBytes) : context(context), span(bytes) {
#ifdef _WIN32
memory = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, 65536));
if (memory != nullptr) GuestArena::GuestArenaCommit_nid_postfix(memory, SurfaceBytes, PAGE_READWRITE, SurfaceBytes);
memory = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536));
if (memory != nullptr) GuestArena::GuestArenaCommit_nid_postfix(memory, bytes, PAGE_READWRITE, bytes);
#else
constexpr std::uintptr_t alignment = 65536;
void* mapped = mmap(nullptr, SurfaceBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
void* mapped = mmap(nullptr, bytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mapped != MAP_FAILED) {
const auto begin = reinterpret_cast<std::uintptr_t>(mapped);
const auto aligned = (begin + alignment - 1) & ~(alignment - 1);
if (aligned != begin) munmap(mapped, aligned - begin);
if (aligned + SurfaceBytes != begin + SurfaceBytes + alignment) munmap(reinterpret_cast<void*>(aligned + SurfaceBytes), begin + alignment - aligned);
if (aligned + bytes != begin + bytes + alignment) munmap(reinterpret_cast<void*>(aligned + bytes), begin + alignment - aligned);
memory = reinterpret_cast<std::uint8_t*>(aligned);
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory, SurfaceBytes);
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory, bytes);
}
#endif
Require(memory != nullptr, "cannot allocate the watched surface");
Require(AgcDriver::GuestMemory::Watched(Address(), SurfaceBytes), "the watched surface is not write-watched");
std::memset(memory, 0x5a, SurfaceBytes);
GuestAllocations::Mutation().Add(memory, SurfaceBytes, true, true, true);
AgcDriver::GuestMemory::CollectWritesUncached(Address(), SurfaceBytes);
Require(AgcDriver::GuestMemory::Watched(Address(), bytes), "the watched surface is not write-watched");
std::memset(memory, 0x5a, bytes);
GuestAllocations::Mutation().Add(memory, bytes, true, true, true);
AgcDriver::GuestMemory::CollectWritesUncached(Address(), bytes);
}
~WatchedSurface() {
ClearCachedTextures(context.device);
GuestAllocations::Mutation().Remove(memory);
#ifdef _WIN32
GuestArena::GuestArenaReset_nid_postfix(memory, SurfaceBytes);
GuestArena::GuestArenaRelease_nid_postfix(memory, SurfaceBytes);
GuestArena::GuestArenaReset_nid_postfix(memory, span);
GuestArena::GuestArenaRelease_nid_postfix(memory, span);
#else
munmap(memory, SurfaceBytes);
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, SurfaceBytes);
munmap(memory, span);
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, span);
#endif
}
@@ -325,6 +325,7 @@ public:
private:
const Context& context;
std::size_t span = 0;
std::uint8_t* memory = nullptr;
};
@@ -674,6 +675,55 @@ void snapshotTests(const Context& context) {
Require(TextureCacheUsage().sampledBytes == tiny->AllocationBytes(), "a snapshot whose " + std::to_string(tiny->AllocationBytes()) + "-byte image is smaller than its " + std::to_string(guestBytes) + " guest bytes counted " + std::to_string(TextureCacheUsage().sampledBytes) + " bytes, not its allocation");
}
void recycleTests(const Context& context) {
Block block(context);
ClearCachedTextures(context.device);
const auto address = block.Surface(0);
std::memset(reinterpret_cast<void*>(address), 0xa1, SurfaceBytes);
const auto first = Sampled(context, address);
Require(Sampled(context, address) == first, "an unchanged surface did not serve its snapshot");
std::memset(reinterpret_cast<void*>(address), 0xb2, SurfaceBytes);
const auto second = Sampled(context, address);
Require(second != first, "a changed surface did not replace its snapshot");
std::memset(reinterpret_cast<void*>(address), 0xa1, SurfaceBytes);
const auto third = Sampled(context, address);
Require(third != second, "the replaced snapshot's size-equal buffer was not reread from guest memory");
Require(Sampled(context, address) == third, "an unchanged surface did not serve its remade snapshot");
constexpr std::uint32_t BigSide = Side * 2;
constexpr std::size_t BigBytes = std::size_t{BigSide} * BigSide * 4u;
const WatchedSurface watchedFirst(context, BigBytes);
const WatchedSurface watchedSecond(context, BigBytes);
const auto bigFirst = Sampled(context, watchedFirst.Address(), BigSide);
Require(TextureCacheUsage().spareEntries == 1 && TextureCacheUsage().spareBytes == BigBytes, "the first big snapshot did not park its dropped " + std::to_string(BigBytes) + "-byte buffer in the spare pool");
const auto bigSecond = Sampled(context, watchedSecond.Address(), BigSide);
Require(TextureCacheUsage().spareEntries == 1 && TextureCacheUsage().spareBytes == BigBytes, "the second big snapshot did not take the parked spare of its size");
}
void spareAgeTests(const Context& context) {
Block block(context);
ClearCachedTextures(context.device);
constexpr std::uint32_t BigSide = Side * 2;
constexpr std::size_t BigBytes = std::size_t{BigSide} * BigSide * 4u;
{
const WatchedSurface watched(context, BigBytes);
const auto first = Sampled(context, watched.Address(), BigSide);
Require(TextureCacheUsage().spareEntries == 1 && TextureCacheUsage().spareBytes == BigBytes, "the big snapshot did not park its dropped buffer in the spare pool");
for (std::uint64_t present = 0; present <= SpareSnapshotPresents; ++present) Recorder::CountPresent();
const auto small = Sampled(context, block.Surface(0));
Require(TextureCacheUsage().spareEntries == 0 && TextureCacheUsage().spareBytes == 0, "a spare idle for " + std::to_string(SpareSnapshotPresents + 1u) + " presents was not released");
}
ClearCachedTextures(context.device);
{
const WatchedSurface watchedFirst(context, BigBytes);
const WatchedSurface watchedSecond(context, BigBytes);
const auto second = Sampled(context, watchedFirst.Address(), BigSide);
Require(TextureCacheUsage().spareEntries == 1 && TextureCacheUsage().spareBytes == BigBytes, "the second big snapshot did not park its dropped buffer in the spare pool");
for (std::uint64_t present = 0; present < SpareSnapshotPresents; ++present) Recorder::CountPresent();
const auto third = Sampled(context, watchedSecond.Address(), BigSide);
Require(TextureCacheUsage().spareEntries == 1 && TextureCacheUsage().spareBytes == BigBytes, "a spare taken within " + std::to_string(SpareSnapshotPresents) + " presents was released instead of reused");
}
}
void heldTests(const Context& context) {
Block block(context);
auto held = Sampled(context, block.Surface(0));
@@ -1204,6 +1254,7 @@ int main(int argc, char** argv) {
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
reportedBudgetTests(context);
snapshotTests(context);
recycleTests(context);
heldTests(context);
releasedTests(context);
rescanTests(context);
@@ -1215,6 +1266,7 @@ int main(int argc, char** argv) {
secondUseTests(context);
earlyMissTests(context);
collidingKeyTests(context);
spareAgeTests(context);
ClearCachedTextures(context.device);
}
std::puts("sampled texture cache budget tests passed");