From bf0c51e4c19e6cea182bfeea7e21a4e50b895f16 Mon Sep 17 00:00:00 2001 From: XairRrR Date: Thu, 15 Oct 2026 16:59:32 +0200 Subject: [PATCH] perf(recompiler): scan image-table key buffers in bulk Narrowing a table column read its key buffer one key at a time, with a page lookup and a single-word read per key; PPSA05684 scans about 20,000 keys per table snapshot. The scan now checks each page once and reads the keys with the bulk reader, and maps a key to its record without dividing when the multiplication cannot wrap. Gameplay, image-table time per snapshot: about 6.5 ms before, 5 ms after. AI-assisted: yes --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 38 +++++++++++++++++++ .../Optimization/src/ResourceMaterializer.cpp | 36 +++++++++++++----- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 51dee75d38..e8d81d8fff 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1240,6 +1240,43 @@ void verifyImageTableBulkReads() { require(cutByRun->snapshot.tables == cutByWord->snapshot.tables && cutWordReads.wordReads == 12u && cutRunReads.wordReads == 0u && cutRunReads.bulkWords == 12u, "image table bulk reads: a record cut by an unmapped page read other words in runs than word by word"); } +void verifyImageTableBulkKeyScan() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + static TableTexture textures[2]; + alignas(4096) static TableGuest guest; + std::fill(guest.heap.begin(), guest.heap.end(), std::array{}); + guest.heap[0] = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D); + guest.heap[1] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D); + guest.heap[3] = guest.heap[0]; + guest.materials = {{{0u, 1u, 0u, 0u}, {0u, 0x08000003u, 0u, 0u}, {0u, 1u, 0u, 0u}}}; + FillTableSrt(guest, guest.heap.data(), 32u, 4u); + const auto userData = TableUserData(guest); + const auto request = TableRequest(TableMaterialCode, userData); + const auto capture = [&](CountingMemory& memory, bool bulk) { + SrtRuntime runtime; + runtime.userData = request.context.userData; + runtime.shaderBase = request.shader.codeAddress; + runtime.userContext = &memory; + runtime.readMemory = &CountingRead; + runtime.readSpecializationMemory = &CountingRead; + runtime.accessible = &CountingAccessible; + if (bulk) runtime.readWords = &CountingReadWords; + return CaptureResources(request, runtime); + }; + const auto begin = reinterpret_cast(guest.materials.data()); + CountingMemory byWordReads{begin, begin + sizeof(guest.materials)}; + CountingMemory byRunReads = byWordReads; + const auto byWord = capture(byWordReads, false); + const auto byRun = capture(byRunReads, true); + require(byWordReads.wordReads == guest.materials.size(), "image table bulk key scan: without a bulk reader the keys were not read one word each"); + require(byRunReads.wordReads == 0u, "image table bulk key scan: the keys were read word by word although a bulk reader exists"); + require(byRun->snapshot.tables == byWord->snapshot.tables, "image table bulk key scan: scanning the keys in bulk changed the snapshot"); + const auto& snapshot = byRun->snapshot.tables.tables.at(0); + const auto outside = Abi::PoisonCode(static_cast(Abi::PoisonReason::OutsideDomain)); + require(snapshot.keys == 4u && snapshot.codes[0] == snapshot.codes[3] && snapshot.codes[0] != snapshot.codes[1] && snapshot.codes[2] == outside, "image table bulk key scan: the narrowed codes are wrong"); +} + void verifyImageTables() { using namespace ShaderRecompiler; namespace Abi = ImageTableAbi; @@ -5212,6 +5249,7 @@ int main(int argc, char** argv) { verifyImageTableKeys(); verifyImageTables(); verifyImageTableBulkReads(); + verifyImageTableBulkKeyScan(); verifyImageTableSrtKey(); verifyImageTableMaskedOffset(); verifyImageTableKeyUnion(); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index ff8686cb2f..878a44dfce 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -505,27 +505,42 @@ private: skip(); return; } - for (std::uint64_t index = 0; index < positions; index++) { - const auto address = base + first + index * domain.stride; - if (!PageAccessible(address, base, base + size)) { - skip(); - return; + if (runtime.readWords != nullptr && positions != 0u && domain.stride % 4u == 0u && domain.stride <= 64u) { + const auto words = static_cast(((positions - 1u) * domain.stride) / 4u + 1u); + for (std::uint64_t page = (base + first) & ~(PageBytes - 1u); page < base + first + words * 4u; page += PageBytes) { + if (!PageAccessible(std::max(page, base + first), base, base + size)) { + skip(); + return; + } + } + if (keyWords.size() < words) keyWords.resize(words); + if (!runtime.readWords(runtime.userContext, base + first, keyWords.data(), words)) throw std::runtime_error("image table: failed to read guest memory at 0x" + hexText(base + first)); + const auto step = static_cast(domain.stride / 4u); + values.reserve(values.size() + positions); + for (std::size_t index = 0; index < words; index += step) values.push_back(keyWords[index]); + } else { + for (std::uint64_t index = 0; index < positions; index++) { + const auto address = base + first + index * domain.stride; + if (!PageAccessible(address, base, base + size)) { + skip(); + return; + } + values.push_back(Read(address)); } - values.push_back(Read(address)); } if (positions != 0u) scanned.emplace_back(base, size); } records.states.assign(records.keys, RecordState::OutsideDomain); records.outside = false; + const std::uint32_t unwrapped = std::numeric_limits::max() / column.stride; for (const auto value : values) { const std::uint32_t relative = value * column.stride; - const bool aligned = relative % column.stride == 0u; - const auto record = relative / column.stride; - if (aligned && record < records.keys) { + const auto record = value <= unwrapped ? value : relative % column.stride == 0u ? relative / column.stride : records.keys; + if (record < records.keys) { records.states[record] = RecordState::Read; continue; } - if (column.address || ImageTableAbi::ScalarBufferDword(relative + column.addend, column.offset, records.size).has_value()) records.outside = true; + if (!records.outside && (column.address || ImageTableAbi::ScalarBufferDword(relative + column.addend, column.offset, records.size).has_value())) records.outside = true; } records.narrowed = true; for (const auto& [base, size] : scanned) tables.ranges.emplace_back(base, size); @@ -774,6 +789,7 @@ private: std::unordered_map wordsIndex; std::unordered_map pages; std::unordered_map wholePages; + std::vector keyWords; }; void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime, SrtWalker& walker, ResourceSnapshot& snapshot, std::vector& activeSources) {