From cae4c4683aee7c075d8d28a55b28222d619d6c9d Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:35:36 -0700 Subject: [PATCH 01/13] refactor(agc): split the cached address space checks out of AcquireRegistered An address-based build takes the cached address space when it is the device's, of the registry generation read before any lease, and no import was retired since; it then stamps the space's imports as used and brings the space's sparse, writable and heap mirrors up to date. Those steps now live in helpers of their own (spaceMiss, stampSpaceImports, refreshSparseMirrors, appendWritableMirrors), so a later use of an existing build can make the same proof and the same refresh as a build that takes the space. No behaviour change. --- .../Graphics/src/GuestBufferMemory.cpp | 68 ++++++++++++------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index fcfb60616a06..44ba6d3cac6e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1814,6 +1814,40 @@ std::vector> refreshHeapRuns(const GuestBu return checked; } +enum class SpaceMiss { None, First, Device, Generation, Epoch }; + +SpaceMiss spaceMiss(const Context& context, const GuestBufferMemory::AddressSpace* current, std::uint64_t generation, const HostImports& state) { + if (current == nullptr) return SpaceMiss::First; + if (current->device != context.device) return SpaceMiss::Device; + if (current->generation != generation) return SpaceMiss::Generation; + if (current->importsEpoch != state.epoch) return SpaceMiss::Epoch; + return SpaceMiss::None; +} + +void stampSpaceImports(const GuestBufferMemory::AddressSpace& space, HostImports& state) { + const auto stamp = ++state.useCounter; + for (const auto& region : space.base) { + if (region.direct != nullptr) region.direct->lastUse = stamp; + } +} + +bool refreshSparseMirrors(std::shared_ptr& current, std::vector& blocks) { + for (auto* mirror : current->sparseMirrors) { + if (refreshSparseMirror(*mirror, blocks)) continue; + blocks.clear(); + Spaces().current.compare_exchange_strong(current, std::shared_ptr{}); + return false; + } + return true; +} + +void appendWritableMirrors(const GuestBufferMemory::AddressSpace& space, std::vector& blocks) { + for (const auto index : space.writableMirrors) { + const auto& region = space.base[index]; + if (prepareRefresh(*region.mirror, region.begin, region.end - region.begin)) appendBlocks(blocks, *region.mirror, region.begin, region.end - region.begin); + } +} + // The mirror for a leased image range: the existing one, or a new one when none exists or the // registered Range object changed (a protection change). Null when the range cannot be mirrored. An // existing writable mirror's blocks are appended to `blocks` for the caller's one compare of every @@ -2699,28 +2733,17 @@ void GuestBufferMemory::AcquireRegistered() { { auto& state = Imports(); std::lock_guard lock(state.mutex); - if (current == nullptr) (spaces.droppedByWaiter.exchange(false, std::memory_order_relaxed) ? spaces.rebuiltWaiterDrop : spaces.rebuiltFirst).fetch_add(1, std::memory_order_relaxed); - else if (current->device != context.device) spaces.rebuiltDevice.fetch_add(1, std::memory_order_relaxed); - else if (current->generation != generation) spaces.rebuiltGeneration.fetch_add(1, std::memory_order_relaxed); - else if (current->importsEpoch != state.epoch) spaces.rebuiltEpoch.fetch_add(1, std::memory_order_relaxed); - else hit = true; - importFloor = state.useCounter + 1; - if (hit) { - const auto stamp = ++state.useCounter; - for (const auto& region : current->base) { - if (region.direct != nullptr) region.direct->lastUse = stamp; - } - } - } - if (hit) { - for (auto* mirror : current->sparseMirrors) { - if (refreshSparseMirror(*mirror, blocks)) continue; - hit = false; - blocks.clear(); - spaces.current.compare_exchange_strong(current, std::shared_ptr{}); - break; + switch (spaceMiss(context, current.get(), generation, state)) { + case SpaceMiss::First: (spaces.droppedByWaiter.exchange(false, std::memory_order_relaxed) ? spaces.rebuiltWaiterDrop : spaces.rebuiltFirst).fetch_add(1, std::memory_order_relaxed); break; + case SpaceMiss::Device: spaces.rebuiltDevice.fetch_add(1, std::memory_order_relaxed); break; + case SpaceMiss::Generation: spaces.rebuiltGeneration.fetch_add(1, std::memory_order_relaxed); break; + case SpaceMiss::Epoch: spaces.rebuiltEpoch.fetch_add(1, std::memory_order_relaxed); break; + case SpaceMiss::None: hit = true; break; } + importFloor = state.useCounter + 1; + if (hit) stampSpaceImports(*current, state); } + if (hit) hit = refreshSparseMirrors(current, blocks); if (hit) { spaces.hits.fetch_add(1, std::memory_order_relaxed); space = std::move(current); @@ -2734,10 +2757,7 @@ void GuestBufferMemory::AcquireRegistered() { // before any of them is dereferenced. served = true; mirrored = space->mirrored; - for (const auto index : space->writableMirrors) { - const auto& region = space->base[index]; - if (prepareRefresh(*region.mirror, region.begin, region.end - region.begin)) appendBlocks(blocks, *region.mirror, region.begin, region.end - region.begin); - } + appendWritableMirrors(*space, blocks); lap(timing.mirrorsUs, at); } } From cb9b3b48f3857ea486fabaa2654b3a00ac1c07ed Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:46:13 -0700 Subject: [PATCH 02/13] perf(agc): reuse address-based draw builds whose inputs are unchanged A draw whose stages reach guest memory through the BDA table built its resources on every draw: the lease of the registered memory, the upload, the page table and its fault buffer, the descriptor set and the image lookups. #2657 skipped the resource cache for such draws because the build was never kept: its completion checked the faults, marked the pages the shader stored to, wrote the written ranges back and released the lease, once. A draw build over the cached address space is now kept when everything it depends on can be proved again cheaply: the space it was built over must be the one a build would take now (the device's, of the current registry generation, with no import retired since), its own regions must be imports or read-only image mirrors with unchanged serials, or snapshot copies whose bytes the draw's snapshots repeat in the same order; the images are proved as for any cached build. The read-only buffers and data words the content key leaves out must equal the build's: a hit with other ones rebinds them per draw, which an address-based build cannot. A reused draw takes the space again and refreshes its mirrors as a build does, and the imports are checked once more after the image proofs. Each draw keeps its own completion: its batch holds the space until then, and its completion checks the faults, marks the written pages and the written ranges served in place. Between draws the build pins no guest allocation, so a guest unmap never waits for the resource cache. The fault buffer, which holds the fault and written-page records, belongs to the build, so one draw at a time uses a build: a draw that repeats it before the previous draw's completion ran builds anew. A build whose draw faulted is not reused. Dispatches and draw recipes do not reuse address-based builds. APS5_NO_ADDRESS_DRAW_REUSE=1 builds every one as before. --- .../Graphics/include/GuestBufferMemory.hpp | 14 + .../Graphics/include/ShaderResources.hpp | 11 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 32 +- .../Graphics/src/GuestBufferMemory.cpp | 94 +++- .../Graphics/src/ShaderResources.cpp | 74 ++- .../libSceAgcDriver/tests/BdaResources.cpp | 104 +++- .../tests/execution/AddressDrawReuse.cpp | 483 ++++++++++++++++++ 7 files changed, 796 insertions(+), 16 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 7f932aad6081..908749677d77 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -308,8 +308,20 @@ class GuestBufferMemory { }; std::optional SplitInPlaceReads() const; std::vector> DeviceReads() const; + std::optional>> SpaceRegions() const; + std::vector> DirectWrites() const; + bool SameSnapshots(std::span snapshots) const; + void SuspendSpace() noexcept; + bool ResumeSpace(); + bool SpaceImportsCurrent(); + std::shared_ptr SpaceHold() const { return space; } private: + struct SnapshotUse { + std::uint64_t address; + std::size_t bytes; + bool live; + }; struct Region { std::uint64_t begin; std::uint64_t end; @@ -433,6 +445,8 @@ class GuestBufferMemory { std::vector> writes; std::vector> readsOverBase; std::vector>> heapReferences; + std::vector snapshotUses; + std::uint64_t suspendedSpace = 0; // UploadPrepare ran (regions are frozen); `uploaded` once UploadFinish ran. bool prepared = false; bool uploaded = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index efefda3a7f18..8aa62c9e1f71 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -8,6 +8,7 @@ #include "Recompiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp" #include +#include #include #include #include @@ -229,6 +230,10 @@ class ShaderResources { const std::vector>& SampledTextures() const { return textures; } bool ChecksFaults() const { return bda != nullptr; } void CheckFaults() const; + bool InUse() const { return usesInFlight.load(std::memory_order_acquire) != 0; } + std::shared_ptr BeginUse(); + void CompleteUse(); + void EndUse() noexcept; // `shaders` are the stages the object was built from, in build order (a recorded draw's vertex // and fragment stages, or one compute stage): their bindings are walked like the build did. // How a Revalidate proved (or refused) the object, for the [recipe] line: the proof path taken @@ -239,7 +244,7 @@ class ShaderResources { ProofPath path = ProofPath::Fast; ProofFailure failure = ProofFailure::None; }; - bool Revalidate(std::span shaders, ProofReport* report = nullptr); + bool Revalidate(std::span shaders, ProofReport* report = nullptr, std::span snapshots = {}); bool Revalidate(const CompiledShader& shader, ProofReport* report = nullptr) { return Revalidate(std::span(&shader, 1), report); } // The proof a recipe hit runs on its template (design_cpu_final M4): Revalidate by that name. bool ProveCurrent(std::span shaders, ProofReport* report = nullptr) { return Revalidate(shaders, report); } @@ -425,6 +430,7 @@ class ShaderResources { void prepareAddressBindings(std::span shaders, std::span snapshots); VkDescriptorBufferInfo descriptor(Allocation& allocation); void noteReusable(); + bool proveBuild(std::span shaders, ProofReport* report); void reportDescriptorCaches() const; // What a sampled texture was proved current against when the build (or the last full Revalidate) // looked it up, so the next Revalidate can repeat the proof from write stamps and the DCC keys @@ -548,6 +554,9 @@ class ShaderResources { }; std::vector samplerSources; bool reusable = false; + bool addressReuse = false; + std::atomic usesInFlight{0}; + std::vector> addressWrites; std::vector directRegions; std::vector validatedTextures; // The pending registry's serial at the last Revalidate that proved this object, taken before diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 7fb4593c6e2f..1ef86e5be27d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1254,6 +1254,16 @@ std::shared_ptr refreshResidentTarget(const Context& context, co return resident; } +struct ResourceUse { + std::shared_ptr resources; + ResourceUse() = default; + ResourceUse(ResourceUse&& other) noexcept : resources(std::move(other.resources)) {} + ResourceUse& operator=(ResourceUse&& other) = delete; + ~ResourceUse() { + if (resources != nullptr) resources->EndUse(); + } +}; + // A draw's resources (resolveDrawResources): the resource-cache template a recordable draw's // stages repeat, or a fresh build. struct ResolvedResources { @@ -1262,6 +1272,7 @@ struct ResolvedResources { ResourceCache::Key contentKey; bool cacheable = false; const ShaderResources::BuildTiming* built = nullptr; + ResourceUse use; }; ResolvedResources resolveDrawResources(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots, std::uint64_t indexBytes, bool recordable, DrawOutcome& outcome, DrawTimer& timer) { @@ -1281,18 +1292,20 @@ ResolvedResources resolveDrawResources(const Context& context, const State& stat resolved.cacheable = recordable && !noDrawResourceCache && !noTextureCache && std::all_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->PipelineVariantId() != 0; }) && !ShaderResources::NeverReusable(shaders); if (resolved.cacheable) { resolved.contentKey = DrawResourceKey(context, shaders, state.color, static_cast(state.colors.size()), draw.indexAddress, indexBytes, !trimKey); - if (auto cached = SharedResourceCache().Find(resolved.contentKey)) { - const bool valid = cached->Revalidate(shaders); + if (auto cached = SharedResourceCache().Find(resolved.contentKey); cached != nullptr && !cached->InUse()) { + const bool valid = cached->Revalidate(shaders, nullptr, snapshots); auto* recorder = Recorder::Active(); std::optional> moved; if (valid && recorder != nullptr) moved = cached->MovedReadOnlyBuffers(shaders, *recorder); if (moved.has_value()) { - if (trimKey) CheckBufferAliases(shaders, state.color, draw.indexAddress, indexBytes); resolved.resources = std::move(cached); + resolved.use.resources = resolved.resources; resolved.moved = std::move(*moved); + if (trimKey) CheckBufferAliases(shaders, state.color, draw.indexAddress, indexBytes); outcome.kind = KindTemplateHit; countCache(&DrawProfile::cacheHits); } else if (valid) { + cached->EndUse(); countCache(&DrawProfile::cacheMisses); } else { SharedResourceCache().Remove(resolved.contentKey); @@ -1305,6 +1318,7 @@ ResolvedResources resolveDrawResources(const Context& context, const State& stat timer.phase(PhaseLookup); if (resolved.resources == nullptr) { resolved.resources = std::make_shared(context, shaders, state.color, static_cast(state.colors.size()), draw.indexAddress, static_cast(indexBytes), snapshots); + resolved.use.resources = resolved.resources; resolved.built = &resolved.resources->Timing(); outcome.addressBased = resolved.resources->HoldsLease(); outcome.kind = outcome.addressBased ? KindBda : KindBuild; @@ -1529,8 +1543,14 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr }); writers->push_back(resources); } else if (completion) { - if (resources->Reusable()) recorder.OnComplete([resources] { resources->CheckFaults(); }); - else recorder.OnComplete([resources] { resources->WriteBackBuffers(); }); + if (resources->Reusable()) { + recorder.OnComplete([resources, hold = resources->BeginUse()]() mutable { + const auto held = std::move(hold); + resources->CompleteUse(); + }); + } else { + recorder.OnComplete([resources] { resources->WriteBackBuffers(); }); + } } for (std::size_t index = 0; index < residents.size(); ++index) { if (residents[index] == nullptr) continue; @@ -2267,7 +2287,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // only a template the resource cache serves under this content key (reusable: no lease, // no copied writes, every direct region import- or mirror-served) of a direct draw, so a // hit's proof is the template's ProveCurrent and nothing needs completion work. - if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && resolved.moved.empty() && !multisampled) { + if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && !resources->UsesAddressTable() && resolved.moved.empty() && !multisampled) { auto recipe = std::make_shared(); recipe->device = context.device; recipe->templateRef = resources; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 44ba6d3cac6e..6442ea78b010 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -2070,6 +2070,11 @@ SnapshotStats& Snapshots() { return stats; } +bool liveSnapshotCompared(std::uint64_t address, std::size_t bytes) { + static const bool checkLive = std::getenv("APS5_NO_SNAPSHOT_CHECK") == nullptr; + return checkLive && !AnyShadowedOverlaps(address, bytes); +} + bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) noexcept { const auto start = std::chrono::steady_clock::now(); bool synced = false; @@ -3154,12 +3159,12 @@ void GuestBufferMemory::AddSnapshot(const GuestMemorySnapshot& snapshot) { // descriptors and specialization still equal what the GPU reads. APS5_NO_SNAPSHOT_CHECK=1 // skips the compare of live-backed regions (the region serves the GPU whatever the snapshot // held; only the diagnostic is lost). - static const bool checkLive = std::getenv("APS5_NO_SNAPSHOT_CHECK") == nullptr; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const bool live = owner->writable || owner->hostBacked || owner->mirror != nullptr; + snapshotUses.push_back({snapshot.address, snapshot.bytes.size(), live}); // A live region over a unit shadow's fresh results reads the import's stale bytes here // (the publish happens when the region is bound, under the device lock): not compared. - if (live && (!checkLive || AnyShadowedOverlaps(snapshot.address, snapshot.bytes.size()))) { + if (live && !liveSnapshotCompared(snapshot.address, snapshot.bytes.size())) { if (profile) Snapshots().skipped.fetch_add(1, std::memory_order_relaxed); return; } @@ -3187,6 +3192,7 @@ void GuestBufferMemory::AddSnapshot(const GuestMemorySnapshot& snapshot) { if (profile) Snapshots().checked.fetch_add(1, std::memory_order_relaxed); return; } + snapshotUses.push_back({snapshot.address, snapshot.bytes.size(), false}); regions.push_back({snapshot.address, end, false, {snapshot.bytes.begin(), snapshot.bytes.end()}, nullptr}); regionsSorted = false; } @@ -4543,6 +4549,90 @@ std::vector> GuestBufferMemory::DeviceRe return result; } +std::optional>> GuestBufferMemory::SpaceRegions() const { + if (!uploaded || committed || space == nullptr || !lease.empty()) return std::nullopt; + std::vector> result; + for (const auto& region : regions) { + const bool snapshotCopy = region.buffer != nullptr && region.direct == nullptr && region.mirror == nullptr && !region.hostBacked && !region.writable && !region.sparse && !region.gpuCopy; + if (snapshotCopy) continue; + const bool fixedMirror = region.mirror != nullptr && !region.mirror->writable && !region.mirror->heap; + if (region.direct == nullptr && !fixedMirror) return std::nullopt; + result.emplace_back(region.begin, region.end); + } + return result; +} + +std::vector> GuestBufferMemory::DirectWrites() const { + std::vector> result; + for (const auto& [begin, end] : mergeWrites(writes)) { + const auto* found = owner(begin, end); + if (found != nullptr && found->direct != nullptr) result.emplace_back(begin, end); + } + return result; +} + +bool GuestBufferMemory::SameSnapshots(std::span snapshots) const { + if (snapshots.size() != snapshotUses.size()) return false; + for (std::size_t index = 0; index < snapshots.size(); ++index) { + const auto& snapshot = snapshots[index]; + const auto& use = snapshotUses[index]; + if (snapshot.address != use.address || snapshot.bytes.size() != use.bytes) return false; + if (use.live) { + if (liveSnapshotCompared(snapshot.address, snapshot.bytes.size()) && std::memcmp(reinterpret_cast(snapshot.address), snapshot.bytes.data(), snapshot.bytes.size()) != 0) return false; + continue; + } + const auto found = std::upper_bound(regions.begin(), regions.end(), snapshot.address, [](std::uint64_t value, const Region& region) { return value < region.begin; }); + if (found == regions.begin()) return false; + const auto& region = *std::prev(found); + if (region.buffer == nullptr || region.direct != nullptr || region.mirror != nullptr || snapshot.address + snapshot.bytes.size() > region.end) return false; + const auto bytes = region.buffer->Bytes(); + const auto offset = static_cast(snapshot.address - region.begin); + if (offset + snapshot.bytes.size() > bytes.size() || std::memcmp(bytes.data() + offset, snapshot.bytes.data(), snapshot.bytes.size()) != 0) return false; + } + return true; +} + +void GuestBufferMemory::SuspendSpace() noexcept { + if (space == nullptr) return; + suspendedSpace = space->serial; + space.reset(); +} + +bool GuestBufferMemory::ResumeSpace() { + SuspendSpace(); + if (!uploaded || committed || !lease.empty() || suspendedSpace == 0) return false; + const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + auto current = Spaces().current.load(); + { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + if (current == nullptr || current->serial != suspendedSpace || spaceMiss(context, current.get(), generation, state) != SpaceMiss::None) return false; + stampSpaceImports(*current, state); + } + std::vector blocks; + if (!refreshSparseMirrors(current, blocks)) return false; + appendWritableMirrors(*current, blocks); + const auto checked = refreshHeapRuns(*current, blocks); + compareBlocks(blocks); + for (const auto& [run, collected] : checked) current->verified[run].store(collected, std::memory_order_relaxed); + if (mirrorsEnabled()) { + sweepMirrors(); + reportMirrors(); + } + space = std::move(current); + completeSpaceWrites(); + return true; +} + +bool GuestBufferMemory::SpaceImportsCurrent() { + if (space == nullptr) return false; + if (context.hostImportAlignment == 0) return true; + auto& state = Imports(); + std::lock_guard lock(state.mutex); + if (importsStale(context, state)) refreshImports(context, state, space->lease); + return state.epoch == space->importsEpoch; +} + void GuestBufferMemory::RecordStagingCopies(Recorder& recorder) { if (!uploaded || committed) return; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index e31084e0c4e9..f02a13e82f9e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2071,17 +2071,25 @@ bool LoopGuardEnabled() { return enabled; } +bool AddressReuseEnabled() { + static const bool enabled = std::getenv("APS5_NO_ADDRESS_DRAW_REUSE") == nullptr; + return enabled; +} + } void ShaderResources::noteReusable() { // Taken whether or not the object turns out reusable: the lookups' records are this build's. captureValidation(); reusable = false; + addressReuse = false; directRegions.clear(); - if (guestMemory.HasCopiedWrites() || HoldsLease() || usesBda || !multisampleSources.empty() || (bda != nullptr && LoopGuardEnabled())) return; + addressWrites.clear(); + const bool addressBased = usesBda && drawBuild && AddressReuseEnabled(); + if (guestMemory.HasCopiedWrites() || (!addressBased && (HoldsLease() || usesBda)) || !multisampleSources.empty() || (bda != nullptr && LoopGuardEnabled())) return; if (std::any_of(textures.begin(), textures.end(), [](const std::shared_ptr& texture) { return texture != nullptr && texture->RefreshedPerUse(); })) return; if (TemplateDataRefresh() && std::any_of(allocations.begin(), allocations.end(), [](const Allocation& allocation) { return allocation.buffer != nullptr && !allocation.guest && allocation.role != ShaderRecompiler::DescriptorRole::ImageTableMap && allocation.size > MaxRefreshBytes; })) return; - const auto regions = guestMemory.DirectRegions(); + const auto regions = addressBased ? guestMemory.SpaceRegions() : guestMemory.DirectRegions(); if (!regions.has_value()) return; for (const auto& [begin, end] : *regions) { // No reconcile here: the upload just took these imports, and the set is about to be recorded @@ -2100,13 +2108,16 @@ void ShaderResources::noteReusable() { region.committedEnd = cut.committedEnd; } } + if (addressBased) addressWrites = guestMemory.DirectWrites(); reusable = true; + addressReuse = addressBased; } bool ShaderResources::NeverReusable(std::span shaders) { const bool loopGuard = LoopGuardEnabled(); - return std::any_of(shaders.begin(), shaders.end(), [loopGuard](const CompiledShader& shader) { - return shader.program != nullptr && std::any_of(shader.program->bindings.begin(), shader.program->bindings.end(), [loopGuard](const ShaderRecompiler::DescriptorBinding& binding) { return binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable || (loopGuard && binding.role == ShaderRecompiler::DescriptorRole::FaultBuffer); }); + const bool addressReuse = AddressReuseEnabled(); + return std::any_of(shaders.begin(), shaders.end(), [loopGuard, addressReuse](const CompiledShader& shader) { + return shader.program != nullptr && std::any_of(shader.program->bindings.begin(), shader.program->bindings.end(), [loopGuard, addressReuse](const ShaderRecompiler::DescriptorBinding& binding) { return (binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable && !addressReuse) || (loopGuard && binding.role == ShaderRecompiler::DescriptorRole::FaultBuffer); }); }); } @@ -2806,7 +2817,28 @@ const ShaderRecompiler::DescriptorBinding* ShaderResources::tableImages(std::spa return nullptr; } -bool ShaderResources::Revalidate(std::span shaders, ProofReport* report) { +bool ShaderResources::Revalidate(std::span shaders, ProofReport* report, std::span snapshots) { + if (!addressReuse) return proveBuild(shaders, report); + if (report != nullptr) *report = {ProofPath::Full, ProofFailure::Other}; + if (!reusable || InUse() || !guestMemory.SameSnapshots(snapshots)) return false; + struct Suspend { + GuestBufferMemory& memory; + bool armed = true; + ~Suspend() { + if (armed) memory.SuspendSpace(); + } + } suspend{guestMemory}; + if (!guestMemory.ResumeSpace()) return false; + const bool proved = proveBuild(shaders, report); + if (proved && guestMemory.SpaceImportsCurrent()) { + suspend.armed = false; + return true; + } + if (proved && report != nullptr) report->failure = ProofFailure::Imports; + return false; +} + +bool ShaderResources::proveBuild(std::span shaders, ProofReport* report) { if (report != nullptr) *report = {ProofPath::Full, ProofFailure::Other}; if (!reusable || shaders.empty()) return false; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; @@ -4448,7 +4480,7 @@ ShaderResources::DrawBindings::~DrawBindings() { std::optional> ShaderResources::MovedReadOnlyBuffers(std::span shaders, Recorder& recorder) const { std::vector moved; - if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return moved; + if (_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) return moved; for (const auto& shader : shaders) { if (shader.program == nullptr) return std::nullopt; for (const auto& binding : shader.program->bindings) { @@ -4491,6 +4523,7 @@ std::optional> ShaderResources::MovedR } } } + if (usesBda && !moved.empty()) return std::nullopt; return moved; } @@ -4697,6 +4730,35 @@ void ShaderResources::CheckFaults() const { if (bda) bda->CheckFault(); } +std::shared_ptr ShaderResources::BeginUse() { + if (!addressReuse) return {}; + usesInFlight.fetch_add(1, std::memory_order_acq_rel); + return guestMemory.SpaceHold(); +} + +void ShaderResources::CompleteUse() { + if (!addressReuse) { + CheckFaults(); + return; + } + struct Completed { + std::atomic& uses; + ~Completed() { uses.fetch_sub(1, std::memory_order_acq_rel); } + } completed{usesInFlight}; + try { + CheckFaults(); + } catch (...) { + reusable = false; + throw; + } + if (SkipWriteBack()) return; + for (const auto& [begin, end] : addressWrites) GuestMemory::MarkWritten(begin, static_cast(end - begin)); +} + +void ShaderResources::EndUse() noexcept { + if (addressReuse) guestMemory.SuspendSpace(); +} + void ShaderResources::WriteBackBuffers() { if (bda) bda->CheckFault(); if (SkipWriteBack()) return; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 98aaf121d1ca..3dda6b2a2cc1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -156,6 +156,67 @@ void snapshotTableTests(const Context& context, const BdaTestAccess& access) { ::operator delete(block, std::align_val_t{piece}); } +void spaceReuseTests(const Context& context) { + constexpr std::size_t piece = 65536; + auto* block = static_cast(::operator new(2 * piece, std::align_val_t{piece})); + std::memset(block, 0x5a, 2 * piece); + const auto address = reinterpret_cast(block); + auto importing = context; + importing.hostImportAlignment = piece; + const auto change = [&](std::byte* range, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, true, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + change(block, true); + alignas(256) static std::array captured{}; + captured.fill(std::byte{0x21}); + std::array liveBytes{}; + std::memcpy(liveBytes.data(), block + 128, liveBytes.size()); + const std::array snapshots{{{reinterpret_cast(captured.data()), captured}, {address + 128, liveBytes}}}; + auto otherCapture = captured; + otherCapture[3] = std::byte{0x22}; + const std::array otherCopied{{{reinterpret_cast(captured.data()), otherCapture}, snapshots[1]}}; + const std::array fewer{{snapshots[0]}}; + { + GuestBufferMemory memory(importing); + memory.AcquireRegistered(); + for (const auto& snapshot : snapshots) memory.AddSnapshot(snapshot); + memory.AddWritable(address + 256, 64); + memory.Upload(true); + if (AddressSpaceCounters().enabled && memory.CopiedBytes() != captured.size()) { + Require(!memory.SpaceRegions().has_value(), "an upload with ranges copied per build offered its regions for reuse"); + std::cout << "registered ranges are copied per build here: address space reuse not tested\n"; + memory.WriteBack(); + } else if (AddressSpaceCounters().enabled) { + const auto regions = memory.SpaceRegions(); + Require(regions.has_value() && regions->empty(), "an upload over the address space with only a snapshot copy of its own was refused reuse"); + const auto written = memory.DirectWrites(); + Require(written.size() == 1 && written.front() == std::pair{address + 256, address + 320}, "the written range served in place by the address space is not its direct write"); + Require(memory.SameSnapshots(snapshots), "an upload refused the snapshots it was built from"); + Require(!memory.SameSnapshots(otherCopied) && !memory.SameSnapshots(fewer), "an upload took snapshots with other copied bytes, or fewer of them"); + memory.SuspendSpace(); + Require(!memory.HoldsLease(), "a suspended upload still holds its address space"); + Require(memory.ResumeSpace() && memory.HoldsLease() && memory.SpaceImportsCurrent(), "an upload could not take its unchanged address space again"); + memory.SuspendSpace(); + block[128] ^= std::byte{0xff}; + Require(!memory.SameSnapshots(snapshots), "an upload took a snapshot that no longer equals the registered memory it covers"); + block[128] ^= std::byte{0xff}; + change(block + piece, true); + Require(!memory.ResumeSpace() && !memory.HoldsLease(), "an upload took its address space again after another range was registered"); + change(block + piece, false); + Require(!memory.ResumeSpace(), "an upload took a replaced address space again"); + } else { + Require(!memory.SpaceRegions().has_value(), "an upload without the cached address space offered its regions for reuse"); + memory.WriteBack(); + } + } + ClearSnapshotCopies(importing.device); + change(block, false); + ::operator delete(block, std::align_val_t{piece}); +} + void importCrossingTests(const Context& context, const BdaTestAccess& access) { constexpr std::size_t half = 65536; void* block = ::operator new(2 * half, std::align_val_t{half}); @@ -1720,6 +1781,44 @@ void sparseHeapMirrorTests(const Context& context, const BdaTestAccess& access) #endif } +void addressMovedBufferTests(const Context& context) { + alignas(64) static std::array first{}; + alignas(64) static std::array second{}; + const auto vsharp = [](const void* data) { + const auto address = reinterpret_cast(data); + return std::vector{static_cast(address), static_cast(address >> 32) & 0xffffu, static_cast(sizeof(first)), 0x31000000u}; + }; + ShaderRecompiler::RecompileResult vertex; + auto buffer = binding(Role::GuestBuffers, 6); + buffer.guestDescriptor = vsharp(first.data()); + buffer.bufferWritten = {false}; + auto data = binding(Role::FlattenedSrt, 7); + data.guestDescriptor = {7u, 8u, 9u, 10u}; + vertex.bindings = {buffer, data}; + ShaderRecompiler::RecompileResult fragment; + fragment.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + fragment.bindings = {binding(Role::BdaPagetable, 4), binding(Role::FaultBuffer, 5)}; + auto moved = vertex; + moved.bindings[0].guestDescriptor = vsharp(second.data()); + auto rewritten = vertex; + rewritten.bindings[1].guestDescriptor = {7u, 8u, 9u, 11u}; + const std::array built{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; + const std::array movedStages{{{ShaderRecompiler::ShaderStage::Vertex, &moved, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; + const std::array rewrittenStages{{{ShaderRecompiler::ShaderStage::Vertex, &rewritten, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Recorder recorder(context); + { + ShaderResources resources(context, built, ColorTarget{}, 0, 0, 0, {}); + Require(resources.UsesAddressTable(), "a draw build with a BDA table stage does not use the BDA table"); + const auto unchanged = resources.MovedReadOnlyBuffers(built, recorder); + Require(unchanged.has_value() && unchanged->empty(), "an address-based draw build found a buffer moved in the stages it was built from"); + Require(!resources.MovedReadOnlyBuffers(movedStages, recorder).has_value(), "an address-based draw build would serve a draw whose read-only buffer moved, which it cannot bind per draw"); + Require(!resources.MovedReadOnlyBuffers(rewrittenStages, recorder).has_value(), "an address-based draw build would serve a draw with other data words, which it cannot bind per draw"); + resources.WriteBack(); + } + recorder.Sync(); +} + void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { alignas(64) std::array guest{}; guest[0] = 123; @@ -1797,7 +1896,8 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { } { ShaderResources resources(context, compiled, snapshots); - Require(ShaderResources::NeverReusable(std::span(&compiled, 1)) && !resources.Reusable(), "an address-based build is reusable"); + Require(!ShaderResources::NeverReusable(std::span(&compiled, 1)), "address-based stages are refused the resource cache"); + Require(!resources.Reusable() && !resources.InUse(), "an address-based dispatch build is reusable"); Require(resources.UsesAddressTable(), "an address-based build does not use the BDA table"); ShaderRecompiler::RecompileResult plain; plain.bindings = {binding(Role::GuestBuffers, 6)}; @@ -1922,6 +2022,8 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { importCrossingTests(context, access); addressTableKeyTests(); snapshotTableTests(context, access); + spaceReuseTests(context); + addressMovedBufferTests(context); importSpanTests(context); failedImportRemapTests(context, access); spaceImportRunTests(context); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp new file mode 100644 index 000000000000..489cacad7aa5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp @@ -0,0 +1,483 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#else +#include +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 64; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t PageBytes = 4096; +constexpr std::uint32_t PointerOffset = 0; +constexpr std::uint32_t StoreOffset = 8; +constexpr std::uint32_t ValueOffset = 16; +constexpr std::uint32_t Sentinel = 0xdeadbeefu; +constexpr float One = 1.0f; +constexpr float Zero = 0.0f; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e000280u, + 0xdc348000u, 0x02000000u, + 0xbf8c3f70u, + 0xdc308000u, 0x017d0002u, + 0xbf8c3f70u, + 0xdc708008u, 0x00000100u, + 0xf800180fu, 0x01010101u, + 0xbf810000u, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +const ColorTargetLayout TargetLayout(Width, Height, ColorTileMode::RenderTarget, 4); + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +class Pages { +public: + explicit Pages(std::size_t bytes) : bytes(bytes) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + if (data != nullptr) GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* mapped = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(mapped != MAP_FAILED, "address draw reuse: cannot map guest pages"); + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "address draw reuse: cannot allocate guest pages"); + std::memset(data, 0, bytes); + } + + ~Pages() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + for (const auto offset : registered) GuestAllocations::Mutation().Remove(data + offset); +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + + Pages(const Pages&) = delete; + Pages& operator=(const Pages&) = delete; + + void Register(std::size_t offset, std::size_t length, bool writable) { + GuestAllocations::Mutation().Add(data + offset, length, true, writable, true); + registered.push_back(offset); + } + + void Unregister(std::size_t offset) { + GuestAllocations::Mutation().Remove(data + offset); + registered.erase(std::remove(registered.begin(), registered.end(), offset), registered.end()); + } + + std::uint64_t Address(std::size_t offset = 0) const { return AddressOf(data + offset); } + std::uint8_t* Data() { return data; } + +private: + std::size_t bytes; + bool watched = AgcDriver::GuestMemory::WriteWatched(); + std::uint8_t* data = nullptr; + std::vector registered; +}; + +class StderrCapture { +public: + StderrCapture() { + std::fflush(stderr); + file = std::tmpfile(); + Require(file != nullptr, "cannot open a temporary file for stderr"); +#ifdef _WIN32 + saved = _dup(_fileno(stderr)); + _dup2(_fileno(file), _fileno(stderr)); +#else + saved = dup(fileno(stderr)); + dup2(fileno(file), fileno(stderr)); +#endif + } + + std::string Finish() { + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t read = 0; (read = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, read); + std::fclose(file); + std::fwrite(text.data(), 1, text.size(), stderr); + return text; + } + +private: + std::FILE* file = nullptr; + int saved = -1; +}; + +std::uint64_t Builds() { + const auto counters = AgcDriver::Graphics::BdaResources::TableCacheCounters(); + return counters.hits + counters.misses; +} + +struct Scene { + AgcDriver::VulkanDevice& device; + Pages pixels{TargetLayout.Bytes()}; + Pages vertices{BlockBytes}; + Pages guest{BlockBytes}; + Pages spare{BlockBytes}; + Pages extra; + std::size_t ranges; + std::vector vertexUserData; + std::vector pixelUserData; + ShaderRecompiler::RecompileResult vertexResult; + ShaderRecompiler::RecompileResult pixelResult; + std::array shaders{}; + std::array snapshots{}; + AgcDriver::Graphics::State state{}; + + Scene(AgcDriver::VulkanDevice& device, std::size_t ranges) : device(device), extra(std::max(ranges, 1) * 2 * PageBytes), ranges(ranges) { + pixels.Register(0, TargetLayout.Bytes(), true); + std::memcpy(vertices.Data(), Triangle.data(), sizeof(Triangle)); + vertices.Register(0, BlockBytes, false); + guest.Register(0, BlockBytes, true); + for (std::size_t range = 0; range < ranges; ++range) extra.Register(range * 2 * PageBytes, PageBytes, false); + Point(guest.Address(ValueOffset)); + Store(guest, ValueOffset, One); + const auto target = device.Target(); + const auto vertexAddress = vertices.Address(); + vertexUserData = {static_cast(vertexAddress), static_cast((vertexAddress >> 32u) & 0xffffu) | (16u << 16u), static_cast(BlockBytes / 16u), 0x01016facu}; + const std::array vertexMemory{{{AddressOf(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, AddressOf(VertexCode.data()), VertexCode, 0, {}}, + {64, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const auto pointer = guest.Address(); + pixelUserData = {static_cast(pointer), static_cast(pointer >> 32u), 0u, 0u}; + const std::array pixelMemory{{{AddressOf(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, AddressOf(PixelCode.data()), PixelCode, 0, {}}, + {64, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + pixelResult = ShaderRecompiler::Recompile(fragment); + Require(std::any_of(pixelResult.bindings.begin(), pixelResult.bindings.end(), [](const auto& binding) { return binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable; }), "address draw reuse: the pixel shader does not reach guest memory through the BDA table"); + shaders = {{{ShaderStage::Vertex, &vertexResult, 0}, {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}}}; + snapshots = {{{AddressOf(VertexCode.data()), std::as_bytes(std::span(VertexCode))}, {AddressOf(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; + state.color = {pixels.Address(), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, TargetLayout.Bytes(), 0xe4u, ColorTileMode::RenderTarget}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + } + + static void Store(Pages& pages, std::size_t offset, float value) { + std::memcpy(pages.Data() + offset, &value, sizeof(value)); + } + + void Point(std::uint64_t address) { + std::memcpy(guest.Data() + PointerOffset, &address, sizeof(address)); + } + + void Clear() { + std::memcpy(guest.Data() + StoreOffset, &Sentinel, sizeof(Sentinel)); + } + + std::uint32_t Stored() { + std::uint32_t value = 0; + std::memcpy(&value, guest.Data() + StoreOffset, sizeof(value)); + return value; + } + + double Draw() { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + const auto start = std::chrono::steady_clock::now(); + device.Draw(state, draw, shaders, snapshots); + return std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + } + + void Wait() { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.WaitIdle(); + } + + std::uint8_t Color() { + AgcDriver::Graphics::StorageTexture::FlushPending(pixels.Address(), TargetLayout.Bytes(), nullptr, "test read-back"); + Wait(); + std::vector texels(TargetLayout.Bytes()); + AgcDriver::GuestMemory::Read(pixels.Address(), std::as_writable_bytes(std::span(texels)), 1); + const auto first = texels[TargetLayout.Offset(0, 0)]; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto offset = TargetLayout.Offset(x, y); + for (std::size_t channel = 0; channel < 4; ++channel) Require(texels[offset + channel] == first, "address draw reuse: the full-screen draw left texels of different values"); + } + } + return first; + } +}; + +std::uint32_t Bits(float value) { + return std::bit_cast(value); +} + +struct Timing { + std::uint64_t builds = 0; + double firstUs = 0; + double averageUs = 0; +}; + +Timing Repeat(Scene& scene, std::size_t draws, bool waited) { + Timing timing; + const auto before = Builds(); + double total = 0; + for (std::size_t draw = 0; draw < draws; ++draw) { + const auto us = scene.Draw(); + if (draw == 0) timing.firstUs = us; + else total += us; + if (waited) scene.Wait(); + } + scene.Wait(); + timing.builds = Builds() - before; + timing.averageUs = draws > 1 ? total / static_cast(draws - 1) : 0.0; + return timing; +} + +void Report(const char* what, std::size_t draws, const Timing& timing) { + std::printf("[address-reuse] %s: %zu draws, %llu builds, %llu reuses; first draw %.1f us, then %.1f us per draw\n", what, draws, static_cast(timing.builds), static_cast(draws - timing.builds), timing.firstUs, timing.averageUs); +} + +bool Imported(Scene& scene) { + scene.Draw(); + scene.Wait(); + if (AgcDriver::Graphics::HostImportLiveBytes() == 0) return false; + Require(scene.Stored() == Bits(One), "address draw reuse: the first draw's store through the BDA table did not reach guest memory"); + return true; +} + +void Synthetic(Scene& scene, std::size_t draws) { + const auto waited = Repeat(scene, draws, true); + Report("each draw waited for", draws, waited); + const auto backToBack = Repeat(scene, draws, false); + Report("back to back", draws, backToBack); + Require(scene.Color() == 255 && scene.Stored() == Bits(One), "address draw reuse: repeated draws did not read and store the guest value"); + Require(waited.builds == 0, "address draw reuse: " + std::to_string(waited.builds) + " of " + std::to_string(draws) + " repeated address-based draws with unchanged inputs built their resources again"); +} + +void GuestWrite(Scene& scene) { + scene.Clear(); + scene.Draw(); + Require(scene.Color() == 255 && scene.Stored() == Bits(One), "address draw reuse: the draw before the guest write did not read the guest value"); + Scene::Store(scene.guest, ValueOffset, Zero); + scene.Clear(); + const auto before = Builds(); + scene.Draw(); + Require(scene.Color() == 0, "address draw reuse: a draw after a guest write rendered the stale value"); + Require(scene.Stored() == Bits(Zero), "address draw reuse: a draw after a guest write stored the stale value through the BDA table"); + Require(Builds() == before, "address draw reuse: a guest write into memory the draw reads in place made it build its resources again"); + Scene::Store(scene.guest, ValueOffset, One); + for (int draw = 0; draw < 3; ++draw) { + scene.Clear(); + scene.Draw(); + scene.Wait(); + Require(scene.Stored() == Bits(One), "address draw reuse: reused draw " + std::to_string(draw) + " did not store through the BDA table"); + } +} + +void Fault(Scene& scene) { + void* reserved = nullptr; +#ifdef _WIN32 + reserved = VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE, PAGE_NOACCESS); +#else + reserved = mmap(nullptr, BlockBytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (reserved == MAP_FAILED) reserved = nullptr; +#endif + Require(reserved != nullptr, "address draw reuse: cannot reserve an inaccessible range"); + const auto unmapped = AddressOf(reserved); + scene.Clear(); + scene.Draw(); + scene.Wait(); + scene.Point(unmapped); + scene.Clear(); + const auto before = Builds(); + StderrCapture log; + std::string text; + try { + scene.Draw(); + scene.Wait(); + } catch (...) { + static_cast(log.Finish()); + throw; + } + text = log.Finish(); + const auto reused = Builds() == before; + char address[64]; + std::snprintf(address, sizeof(address), "address=0x%llx", static_cast(unmapped)); + Require(text.find("BDA access failed") != std::string::npos && text.find(address) != std::string::npos && text.find("reason=1") != std::string::npos, "address draw reuse: a reused draw reading an unmapped pointer did not report its BDA fault:\n" + text); + Require(reused, "address draw reuse: the faulting draw built its resources again instead of reusing them"); + scene.Point(scene.guest.Address(ValueOffset)); + scene.Clear(); + const auto afterFault = Builds(); + scene.Draw(); + Require(scene.Color() == 255 && scene.Stored() == Bits(One), "address draw reuse: the draw after a fault did not read the guest value"); + Require(Builds() == afterFault + 1, "address draw reuse: a build whose draw faulted served the next draw"); +#ifdef _WIN32 + VirtualFree(reserved, 0, MEM_RELEASE); +#else + munmap(reserved, BlockBytes); +#endif +} + +void Remap(Scene& scene) { + scene.Clear(); + scene.Draw(); + scene.Wait(); + Scene::Store(scene.spare, ValueOffset, Zero); + scene.spare.Register(0, BlockBytes, false); + scene.Point(scene.spare.Address(ValueOffset)); + scene.Clear(); + auto before = Builds(); + scene.Draw(); + Require(scene.Color() == 0 && scene.Stored() == Bits(Zero), "address draw reuse: a draw reading a newly mapped range read something else"); + Require(Builds() == before + 1, "address draw reuse: a draw reused resources built before a range was mapped"); + scene.Point(scene.guest.Address(ValueOffset)); + scene.spare.Unregister(0); + scene.Clear(); + before = Builds(); + scene.Draw(); + Require(scene.Color() == 255 && scene.Stored() == Bits(One), "address draw reuse: a draw after a range was unmapped did not read the guest value"); + Require(Builds() == before + 1, "address draw reuse: a draw reused resources built before a range was unmapped"); + if (scene.ranges == 0) return; + scene.Clear(); + scene.Draw(); + scene.Wait(); + scene.extra.Unregister(0); + scene.Clear(); + before = Builds(); + scene.Draw(); + Require(scene.Color() == 255 && scene.Stored() == Bits(One), "address draw reuse: a draw after another range was unmapped did not read the guest value"); + Require(Builds() == before + 1, "address draw reuse: a draw reused resources whose BDA table maps an unmapped range"); +} + +} + +int main(int argc, char** argv) { + try { + const std::size_t ranges = argc > 1 ? std::strtoull(argv[1], nullptr, 10) : 512; + const std::size_t draws = argc > 2 ? std::strtoull(argv[2], nullptr, 10) : 64; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + { + Scene scene(*device, ranges); + if (!Imported(scene)) { + std::puts("skipped, host imports are unavailable: a store through the BDA table needs one"); + return VulkanTestSkipped; + } + Synthetic(scene, draws); + GuestWrite(scene); + Fault(scene); + Remap(scene); + scene.Wait(); + } + std::printf("address draw reuse tests passed (%zu extra registered ranges)\n", ranges); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From ec17620356c3f3207cd8548cfd91fb5e9d7eeffa Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:24:41 -0700 Subject: [PATCH 03/13] docs(agc): record what address-based draw reuse proves and leaves out The reuse of address-based draw builds rests on choices that are not measured on titles: one draw at a time per build (the fault buffer holds one draw's records), the snapshot list repeated in the same order, and no reuse for dispatches or draw recipes. Recorded as technical debt so they can be revisited with title measurements. --- docs/dev/debt/libSceAgcDriver.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 09bd4b943289..0bdc777fc307 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -157,3 +157,4 @@ - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; a title that waits only on release interrupts sees one extra event per flip, with `data` counting it. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less 2 GiB (imports and type-3 mirrors share it; #167's rule). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. +- [Reused address-based draw builds](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): a recorded draw whose stages reach guest memory through the BDA table reuses the build of an earlier draw with the same content (`APS5_NO_ADDRESS_DRAW_REUSE=1` builds each one) while the cached address space it was built over is the one a build would take now, its regions outside the space are imports or read-only image mirrors with unchanged serials or snapshot copies whose bytes the draw's snapshots repeat in the same order, the read-only buffers and data words its content key leaves out equal the build's (it cannot rebind them per draw), and the completion of its previous draw has run. Each draw takes the space for its batch, brings the space's mirrors up to date as a build does, and checks its faults and marks its written pages at its own completion; between draws the build pins no guest allocation. The build's fault buffer holds the fault and written-page records of one draw, so a draw that repeats it before that completion ran (two in one batch) builds anew and replaces it in the resource cache. Dispatches and draw recipes do not reuse address-based builds. Measured only in a synthetic draw on lavapipe; how often titles repeat a draw's content and snapshot list between batches is not measured. From bae2cd48ec883f3d5aefc6b85b4a6d3f64b2e229 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 02:44:20 +0000 Subject: [PATCH 04/13] fix(agc): reuse draws whose buffer descriptors reach past their allocation A guest buffer descriptor can describe more than its build binds. Past the device's range limit the build cuts the size at the registered end, or binds an empty buffer when nothing is registered there, and it trims a stride overhang that reaches past the allocation. A reuse compared each descriptor's raw size with what the build had bound, so such a buffer always looked moved: an address-based draw built its resources again on every draw, and a cached build of another draw rebound a trimmed buffer at its raw size, past its allocation. The moved-buffer check and the alias checks a reuse repeats now both take the size from the build's own computation. The alias checks need it as well: with only the moved-buffer check changed, the reused draw failed with "shader buffer aliases the render target", which fails the driver. agc_driver_address_draw_reuse draws twice with a vertex buffer descriptor of 0xffffffff records whose render target lies above its 64 KiB allocation. The second draw must reuse the first one's build; without the fix it built again. --- .../Graphics/include/ShaderResources.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 8 +-- .../Graphics/src/ShaderResources.cpp | 52 +++++++++++++------ .../tests/execution/AddressDrawReuse.cpp | 26 +++++++++- 4 files changed, 64 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 8aa62c9e1f71..bc32d6e3ae15 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -227,6 +227,7 @@ class ShaderResources { bool Reusable() const { return reusable; } const std::vector& MultisampleSources() const { return multisampleSources; } static bool NeverReusable(std::span shaders); + static std::uint64_t GuestBufferBytes(const Context& context, std::span words); const std::vector>& SampledTextures() const { return textures; } bool ChecksFaults() const { return bda != nullptr; } void CheckFaults() const; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 1ef86e5be27d..45131daea4ee 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -763,7 +763,7 @@ ResourceCache::Key DrawResourceKey(const Context& context, std::span shaders, const ColorTarget& target, std::uint64_t indexAddress, std::uint64_t indexBytes) { +void CheckBufferAliases(const Context& context, std::span shaders, const ColorTarget& target, std::uint64_t indexAddress, std::uint64_t indexBytes) { const auto overlap = [](std::uint64_t first, std::uint64_t firstSize, std::uint64_t second, std::uint64_t secondSize) { return first < second + secondSize && second < first + firstSize; }; for (const auto& shader : shaders) { for (const auto& binding : shader.program->bindings) { @@ -772,7 +772,7 @@ void CheckBufferAliases(std::span shaders, const ColorTarg for (std::size_t offset = 0; offset + 4 <= words.size(); offset += 4) { const ShaderRecompiler::ShaderBufferResource descriptor{{words[offset], words[offset + 1], words[offset + 2], words[offset + 3]}}; const auto address = descriptor.Base48(); - const auto size = descriptor.GetSize(); + const auto size = ShaderResources::GuestBufferBytes(context, std::span(words).subspan(offset, 4)); if (size == 0 || address == 0) continue; const auto element = offset / 4; const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; @@ -1301,7 +1301,7 @@ ResolvedResources resolveDrawResources(const Context& context, const State& stat resolved.resources = std::move(cached); resolved.use.resources = resolved.resources; resolved.moved = std::move(*moved); - if (trimKey) CheckBufferAliases(shaders, state.color, draw.indexAddress, indexBytes); + if (trimKey) CheckBufferAliases(context, shaders, state.color, draw.indexAddress, indexBytes); outcome.kind = KindTemplateHit; countCache(&DrawProfile::cacheHits); } else if (valid) { @@ -2640,7 +2640,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con timer.phase(PhaseLookup); return miss(DrawRecipeMiss::Proof); } - CheckBufferAliases(shaders, state.color, draw.indexAddress, inputs.indexBytes); + CheckBufferAliases(context, shaders, state.color, draw.indexAddress, inputs.indexBytes); SharedResourceCache().Touch(recipe.key); countCache(&DrawProfile::cacheHits); timer.phase(PhaseLookup); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index f02a13e82f9e..f2636bf9fa2f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -3567,34 +3567,52 @@ DescriptorCache::Stats DescriptorCache::Counters() const { return stats; } +namespace { + +std::uint64_t registeredBufferBytes(const Context& context, const ShaderRecompiler::ShaderBufferResource& descriptor) { + const auto address = descriptor.Base48(); + auto bytes = descriptor.GetSize(); + if (bytes > context.limits.maxStorageBufferRange) { + const auto mappedEnd = RegisteredReadableEnd(address); + if (mappedEnd <= address) bytes = 0; + else if (mappedEnd - address < context.limits.maxStorageBufferRange) bytes = mappedEnd - address; + } + return bytes; +} + +std::uint64_t trimmedBufferBytes(const ShaderRecompiler::ShaderBufferResource& descriptor, std::uint64_t bytes) { + const std::uint64_t stride = descriptor.Stride(); + if (stride == 0) return bytes; + constexpr std::uint64_t GuestPageBytes = 0x4000; + const auto address = descriptor.Base48(); + const auto end = address + bytes; + const auto overhang = end % GuestPageBytes; + if (overhang == 0 || overhang >= stride) return bytes; + const auto allocationEnd = RegisteredReadableEnd(address); + return allocationEnd > address && allocationEnd < end && end - allocationEnd < stride ? allocationEnd - address : bytes; +} + +} + +std::uint64_t ShaderResources::GuestBufferBytes(const Context& context, std::span words) { + const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}}; + return trimmedBufferBytes(descriptor, registeredBufferBytes(context, descriptor)); +} + std::size_t ShaderResources::addGuestBuffer(std::span words, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes, bool written, bool atomic, bool read) { Require(words.size() == 4, "buffer descriptor must contain four DWORDs"); Require((words[1] & 0x40000000u) == 0, "buffer descriptor has reserved bits set"); const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}}; Require(descriptor.Type() == 0u, "buffer descriptor uses an unsupported type"); const auto address = descriptor.Base48(); - auto byteSize = descriptor.GetSize(); - if (byteSize > context.limits.maxStorageBufferRange) { - const auto mappedEnd = RegisteredReadableEnd(address); - if (mappedEnd <= address) byteSize = 0; - else if (mappedEnd - address < context.limits.maxStorageBufferRange) byteSize = mappedEnd - address; - } + const auto byteSize = registeredBufferBytes(context, descriptor); if (byteSize == 0 || address == 0) { allocations.push_back({0, EmptyBufferBytes, false, nullptr, ShaderRecompiler::DescriptorRole::GuestBuffers, false}); return allocations.size() - 1; } Require(byteSize <= context.limits.maxStorageBufferRange, "shader buffer exceeds descriptor range limit"); Require(byteSize <= std::numeric_limits::max(), "shader buffer size exceeds host address space"); - auto size = static_cast(byteSize); - if (const std::uint64_t stride = descriptor.Stride(); stride != 0) { - constexpr std::uint64_t GuestPageBytes = 0x4000; - const auto end = address + size; - const auto overhang = end % GuestPageBytes; - if (overhang != 0 && overhang < stride) { - const auto allocationEnd = RegisteredReadableEnd(address); - if (allocationEnd > address && allocationEnd < end && end - allocationEnd < stride) size = static_cast(allocationEnd - address); - } - } + const auto size = static_cast(trimmedBufferBytes(descriptor, byteSize)); Require(target == nullptr || !overlap(address, size, target->address, target->bytes), "shader buffer aliases the render target"); // APS5_ALL_BUFFERS_WRITTEN=1: every element is noted as written, as before bufferWritten existed. static const bool allWritten = std::getenv("APS5_ALL_BUFFERS_WRITTEN") != nullptr; @@ -4504,7 +4522,7 @@ std::optional> ShaderResources::MovedR const auto* words = binding.guestDescriptor.data() + static_cast(element) * 4u; const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}}; const auto address = descriptor.Base48(); - const auto size = descriptor.GetSize(); + const auto size = GuestBufferBytes(context, std::span(words, 4)); const auto index = kept->allocations[element]; const auto& item = allocations[index]; const bool empty = size == 0 || address == 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp index 489cacad7aa5..7684c566e381 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp @@ -208,7 +208,7 @@ struct Scene { std::array snapshots{}; AgcDriver::Graphics::State state{}; - Scene(AgcDriver::VulkanDevice& device, std::size_t ranges) : device(device), extra(std::max(ranges, 1) * 2 * PageBytes), ranges(ranges) { + Scene(AgcDriver::VulkanDevice& device, std::size_t ranges, std::uint32_t vertexRecords = static_cast(BlockBytes / 16u)) : device(device), extra(std::max(ranges, 1) * 2 * PageBytes), ranges(ranges) { pixels.Register(0, TargetLayout.Bytes(), true); std::memcpy(vertices.Data(), Triangle.data(), sizeof(Triangle)); vertices.Register(0, BlockBytes, false); @@ -218,7 +218,7 @@ struct Scene { Store(guest, ValueOffset, One); const auto target = device.Target(); const auto vertexAddress = vertices.Address(); - vertexUserData = {static_cast(vertexAddress), static_cast((vertexAddress >> 32u) & 0xffffu) | (16u << 16u), static_cast(BlockBytes / 16u), 0x01016facu}; + vertexUserData = {static_cast(vertexAddress), static_cast((vertexAddress >> 32u) & 0xffffu) | (16u << 16u), vertexRecords, 0x01016facu}; const std::array vertexMemory{{{AddressOf(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; ShaderRecompiler::RecompileRequest vertex{ {ShaderStage::Vertex, AddressOf(VertexCode.data()), VertexCode, 0, {}}, @@ -454,6 +454,27 @@ void Remap(Scene& scene) { Require(Builds() == before + 1, "address draw reuse: a draw reused resources whose BDA table maps an unmapped range"); } +void OversizedVertexBuffer(AgcDriver::VulkanDevice& device) { + Scene scene(device, 0, 0xffffffffu); + Pages later{TargetLayout.Bytes()}; + const bool pixelsAbove = scene.pixels.Address() > scene.vertices.Address(); + auto& target = pixelsAbove ? scene.pixels : later; + if (!pixelsAbove) later.Register(0, TargetLayout.Bytes(), true); + Require(target.Address() >= scene.vertices.Address() + BlockBytes, "address draw reuse: no render target lies above the vertex buffer"); + scene.state.color.address = target.Address(); + scene.state.colors = {scene.state.color}; + scene.Clear(); + scene.Draw(); + scene.Wait(); + Require(scene.Stored() == Bits(One), "address draw reuse: the draw whose vertex buffer descriptor reaches past its allocation did not store through the BDA table"); + const auto before = Builds(); + scene.Clear(); + scene.Draw(); + scene.Wait(); + Require(scene.Stored() == Bits(One), "address draw reuse: the reused draw whose vertex buffer descriptor reaches past its allocation did not store through the BDA table"); + Require(Builds() == before, "address draw reuse: a draw whose vertex buffer descriptor reaches past its allocation built its resources again"); +} + } int main(int argc, char** argv) { @@ -474,6 +495,7 @@ int main(int argc, char** argv) { Remap(scene); scene.Wait(); } + OversizedVertexBuffer(*device); std::printf("address draw reuse tests passed (%zu extra registered ranges)\n", ranges); return 0; } catch (const std::exception& error) { From bea388d1b4fd1b6eae89ef07791f271b4340b435 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 04:28:33 +0000 Subject: [PATCH 05/13] fix(agc): refuse address-based reuse after rejections or a throw Both walks of the reuse proof skip a rejected image-table element, so it is never proved again. Two rejections are transient (a surface that is not mapped, a texture lookup that throws), and pages committed in a registered reserved range leave the registry generation alone: an address-based build could be reused with the element still rejected after its surface became valid. Such a build is now not reusable. The address-based proof could also throw out of Revalidate and fail the draw, where a fresh build would reject the element or report the error itself. Anything the proof throws now refuses the reuse. --- .../Graphics/src/ShaderResources.cpp | 17 ++-- .../libSceAgcDriver/tests/BdaResources.cpp | 61 ++++++++++++++ .../tests/execution/AddressDrawReuse.cpp | 80 +++++++++++++++++++ 3 files changed, 152 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index f2636bf9fa2f..b10d253ea0c6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2087,6 +2087,7 @@ void ShaderResources::noteReusable() { addressWrites.clear(); const bool addressBased = usesBda && drawBuild && AddressReuseEnabled(); if (guestMemory.HasCopiedWrites() || (!addressBased && (HoldsLease() || usesBda)) || !multisampleSources.empty() || (bda != nullptr && LoopGuardEnabled())) return; + if (addressBased && std::any_of(tableStages.begin(), tableStages.end(), [](const TableStage& table) { return std::find(table.elementTextures.begin(), table.elementTextures.end(), std::numeric_limits::max()) != table.elementTextures.end(); })) return; if (std::any_of(textures.begin(), textures.end(), [](const std::shared_ptr& texture) { return texture != nullptr && texture->RefreshedPerUse(); })) return; if (TemplateDataRefresh() && std::any_of(allocations.begin(), allocations.end(), [](const Allocation& allocation) { return allocation.buffer != nullptr && !allocation.guest && allocation.role != ShaderRecompiler::DescriptorRole::ImageTableMap && allocation.size > MaxRefreshBytes; })) return; const auto regions = addressBased ? guestMemory.SpaceRegions() : guestMemory.DirectRegions(); @@ -2828,13 +2829,17 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR if (armed) memory.SuspendSpace(); } } suspend{guestMemory}; - if (!guestMemory.ResumeSpace()) return false; - const bool proved = proveBuild(shaders, report); - if (proved && guestMemory.SpaceImportsCurrent()) { - suspend.armed = false; - return true; + try { + if (!guestMemory.ResumeSpace()) return false; + const bool proved = proveBuild(shaders, report); + if (proved && guestMemory.SpaceImportsCurrent()) { + suspend.armed = false; + return true; + } + if (proved && report != nullptr) report->failure = ProofFailure::Imports; + } catch (const std::exception&) { + if (report != nullptr) report->failure = ProofFailure::Other; } - if (proved && report != nullptr) report->failure = ProofFailure::Imports; return false; } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 3dda6b2a2cc1..1a1e21857f20 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -1,4 +1,5 @@ #include "BdaTests.hpp" +#include "ImageTableAbi.hpp" #include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" @@ -1819,6 +1820,65 @@ void addressMovedBufferTests(const Context& context) { recorder.Sync(); } +void addressRejectedTableElementTests(const Context& context) { + namespace Abi = ShaderRecompiler::ImageTableAbi; + constexpr std::size_t surfaceBytes = 65536; + constexpr std::uint32_t width = 64; + constexpr std::uint32_t height = 4; + constexpr std::uint32_t format32Float = 22; + constexpr std::uint32_t type2D = 9; +#ifdef _WIN32 + auto* surface = static_cast(VirtualAlloc(nullptr, surfaceBytes, MEM_RESERVE, PAGE_NOACCESS)); +#else + void* mapped = mmap(nullptr, surfaceBytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + auto* surface = mapped == MAP_FAILED ? nullptr : static_cast(mapped); +#endif + Require(surface != nullptr, "cannot reserve an image table element's surface"); + const auto address = reinterpret_cast(surface); + auto tableContext = context; + tableContext.hostImportAlignment = 65536; + tableContext.imageTableCapacity = 2; + tableContext.limits.maxDescriptorSetSampledImages = 2; + tableContext.limits.maxPerStageDescriptorSampledImages = 2; + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + fragment.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + auto map = binding(Role::ImageTableMap, static_cast(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap)); + map.guestDescriptor.assign(Abi::HeaderWords + 1u, 0u); + map.guestDescriptor[Abi::HeaderVersion] = Abi::Version; + map.guestDescriptor[Abi::HeaderElementCount] = 1u; + map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords; + map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords + 1u; + auto images = binding(Role::ImageTable, ShaderRecompiler::RuntimeAbi::ImageTableBinding); + images.kind = ShaderRecompiler::DescriptorKind::SampledImage; + images.guestDescriptor = {static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (format32Float << 20u) | (((width - 1u) & 3u) << 30u), ((width - 1u) >> 2u) | ((height - 1u) << 14u) | (1u << 31u), 0xfacu | (type2D << 28u), 0u, 0u, 0u, 0u}; + images.imageShapes = {ShaderRecompiler::DescriptorImageShape::Image2D}; + images.imageDepthCompare = {false}; + images.imageSamplers = {0u}; + fragment.bindings = {binding(Role::BdaPagetable, 4), binding(Role::FaultBuffer, 5), map, images}; + const std::array stages{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Recorder recorder(tableContext); + { + ShaderResources resources(tableContext, stages, ColorTarget{}, 0, 0, 0, {}); + Require(resources.UsesAddressTable(), "a draw build with a BDA table stage and an image table does not use the BDA table"); +#ifdef _WIN32 + Require(VirtualAlloc(surface, surfaceBytes, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot commit the image table element's surface"); +#else + Require(mprotect(surface, surfaceBytes, PROT_READ | PROT_WRITE) == 0, "cannot commit the image table element's surface"); +#endif + resources.EndUse(); + Require(!resources.Revalidate(stages, nullptr, {}), "an address-based draw build whose image table element was rejected as unmapped was reused after the element's surface was committed"); + resources.WriteBack(); + } + recorder.Sync(); +#ifdef _WIN32 + VirtualFree(surface, 0, MEM_RELEASE); +#else + munmap(surface, surfaceBytes); +#endif +} + void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { alignas(64) std::array guest{}; guest[0] = 123; @@ -2024,6 +2084,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { snapshotTableTests(context, access); spaceReuseTests(context); addressMovedBufferTests(context); + addressRejectedTableElementTests(context); importSpanTests(context); failedImportRemapTests(context, access); spaceImportRunTests(context); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp index 7684c566e381..4dfc8916d221 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp @@ -4,6 +4,9 @@ #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" @@ -35,6 +38,14 @@ #include #include +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + namespace { using AgcDriver::Graphics::ColorTargetLayout; @@ -475,6 +486,74 @@ void OversizedVertexBuffer(AgcDriver::VulkanDevice& device) { Require(Builds() == before, "address draw reuse: a draw whose vertex buffer descriptor reaches past its allocation built its resources again"); } +void ThrowingProof(AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if ((context.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0) { + std::puts("address draw reuse: a throwing reuse proof not tested, the device has no 4x color targets"); + return; + } + constexpr std::uint32_t format32UInt = 20; + constexpr std::uint32_t type2D = 9; + Pages surface{BlockBytes}; + surface.Register(0, BlockBytes, true); + struct ClearTargets { + AgcDriver::VulkanDevice& device; + VkDevice handle; + ~ClearTargets() { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + device.WaitIdle(); + AgcDriver::Graphics::ClearMultisampleTargets(handle); + AgcDriver::Graphics::ClearCachedTextures(handle); + } + } clearTargets{device, context.device}; + const auto address = surface.Address(); + ShaderRecompiler::DescriptorBinding image{}; + image.kind = ShaderRecompiler::DescriptorKind::SampledImage; + image.role = ShaderRecompiler::DescriptorRole::GuestImages; + image.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding + AgcDriver::Graphics::PlaceholderDimensionsPerClass + 2u; + image.count = 1; + image.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + image.imageSamplers = {0u}; + image.guestDescriptor = {static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (format32UInt << 20u) | (((Width - 1u) & 3u) << 30u), ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), 0xfacu | (type2D << 28u), 0u, 0u, 0u, 0u}; + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + fragment.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + fragment.bindings = { + {ShaderRecompiler::DescriptorKind::StorageBuffer, ShaderRecompiler::DescriptorRole::BdaPagetable, 0, static_cast(ShaderRecompiler::RuntimeAbi::Binding::BdaPagetable), 1, {}, false}, + {ShaderRecompiler::DescriptorKind::StorageBuffer, ShaderRecompiler::DescriptorRole::FaultBuffer, 0, static_cast(ShaderRecompiler::RuntimeAbi::Binding::FaultBuffer), 1, {}, false}, + image, + }; + const std::array stages{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::Fragment, &fragment, 0}}}; + AgcDriver::Graphics::ColorTarget target{}; + target.address = address; + target.surfaceAddress = address; + target.extent = {Width, Height}; + target.surfaceExtent = target.extent; + target.format = VK_FORMAT_R8G8B8A8_UNORM; + target.tileMode = ColorTileMode::RenderTarget; + target.elementBytes = 4; + target.bytes = BlockBytes / 4; + target.samples = 4; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + AgcDriver::Graphics::ShaderResources resources(context, stages, AgcDriver::Graphics::ColorTarget{}, 0, 0, 0, {}); + Require(resources.UsesAddressTable() && resources.Reusable(), "address draw reuse: an address-based draw build that samples an image is not reusable"); + resources.EndUse(); + Require(AgcDriver::Graphics::MultisampleTargetView(context, target) != VK_NULL_HANDLE, "address draw reuse: the multisampled target over the sampled image has no view"); + device.SubmitRecorded(true); + device.WaitIdle(); + bool threw = false; + bool reused = false; + try { + reused = resources.Revalidate(stages, nullptr, {}); + } catch (const std::runtime_error&) { + threw = true; + } + Require(!threw, "address draw reuse: the reuse proof of an address-based draw build whose sampled image's memory became a multisampled target threw instead of refusing the reuse"); + Require(!reused, "address draw reuse: an address-based draw build whose sampled image's memory became a multisampled target was reused"); + resources.WriteBack(); +} + } int main(int argc, char** argv) { @@ -496,6 +575,7 @@ int main(int argc, char** argv) { scene.Wait(); } OversizedVertexBuffer(*device); + ThrowingProof(*device); std::printf("address draw reuse tests passed (%zu extra registered ranges)\n", ranges); return 0; } catch (const std::exception& error) { From 82b35b7a8fd521ae0d1ea78271e46a707e74d596 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 04:43:05 +0000 Subject: [PATCH 06/13] fix(agc): release a draw build's use when its batch is discarded A batch whose end or submit fails is discarded without running its completions. The use count BeginUse took was only dropped by CompleteUse, so the build stayed in use and was never reused again. The count now goes with the token BeginUse returns, which the completion owns: it drops when the completion runs or is discarded. The mock's queue submit can now fail for the test. --- .../Graphics/include/ShaderResources.hpp | 4 +-- .../Graphics/src/ShaderResources.cpp | 13 ++++---- .../libSceAgcDriver/tests/BdaResources.cpp | 33 +++++++++++++++++++ .../prx/libSceAgcDriver/tests/BdaTests.hpp | 1 + .../prx/libSceAgcDriver/tests/Graphics.cpp | 8 +++-- 5 files changed, 48 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index bc32d6e3ae15..ee0b82d2b144 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -231,7 +231,7 @@ class ShaderResources { const std::vector>& SampledTextures() const { return textures; } bool ChecksFaults() const { return bda != nullptr; } void CheckFaults() const; - bool InUse() const { return usesInFlight.load(std::memory_order_acquire) != 0; } + bool InUse() const { return usesInFlight->load(std::memory_order_acquire) != 0; } std::shared_ptr BeginUse(); void CompleteUse(); void EndUse() noexcept; @@ -556,7 +556,7 @@ class ShaderResources { std::vector samplerSources; bool reusable = false; bool addressReuse = false; - std::atomic usesInFlight{0}; + std::shared_ptr> usesInFlight = std::make_shared>(0u); std::vector> addressWrites; std::vector directRegions; std::vector validatedTextures; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index b10d253ea0c6..9323add6116c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -4755,8 +4755,13 @@ void ShaderResources::CheckFaults() const { std::shared_ptr ShaderResources::BeginUse() { if (!addressReuse) return {}; - usesInFlight.fetch_add(1, std::memory_order_acq_rel); - return guestMemory.SpaceHold(); + struct Use { + Use(std::shared_ptr> counter, std::shared_ptr hold) : uses(std::move(counter)), space(std::move(hold)) { uses->fetch_add(1, std::memory_order_acq_rel); } + ~Use() { uses->fetch_sub(1, std::memory_order_acq_rel); } + std::shared_ptr> uses; + std::shared_ptr space; + }; + return std::make_shared(usesInFlight, guestMemory.SpaceHold()); } void ShaderResources::CompleteUse() { @@ -4764,10 +4769,6 @@ void ShaderResources::CompleteUse() { CheckFaults(); return; } - struct Completed { - std::atomic& uses; - ~Completed() { uses.fetch_sub(1, std::memory_order_acq_rel); } - } completed{usesInFlight}; try { CheckFaults(); } catch (...) { diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 1a1e21857f20..529a8e4cda21 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -1879,6 +1879,38 @@ void addressRejectedTableElementTests(const Context& context) { #endif } +void addressFailedSubmissionTests(const Context& context, const BdaTestAccess& access) { + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + fragment.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + fragment.bindings = {binding(Role::BdaPagetable, 4), binding(Role::FaultBuffer, 5)}; + const std::array stages{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Recorder recorder(context); + { + auto resources = std::make_shared(context, stages, ColorTarget{}, 0, 0, 0, std::span{}); + Require(resources->UsesAddressTable() && resources->Reusable(), "an address-based draw build is not reusable"); + recorder.OnComplete([resources, hold = resources->BeginUse()]() mutable { + const auto held = std::move(hold); + resources->CompleteUse(); + }); + resources->EndUse(); + Require(resources->InUse(), "an address-based draw build taken by a recorded batch is not in use"); + access.submitResult(VK_ERROR_OUT_OF_HOST_MEMORY); + bool failed = false; + try { + recorder.Submit(); + } catch (const std::exception&) { + failed = true; + } + access.submitResult(VK_SUCCESS); + Require(failed, "a batch whose submission failed did not report it"); + Require(!resources->InUse(), "an address-based draw build stayed in use after the batch that took it failed to submit"); + resources->WriteBack(); + } + recorder.Sync(); +} + void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { alignas(64) std::array guest{}; guest[0] = 123; @@ -2085,6 +2117,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { spaceReuseTests(context); addressMovedBufferTests(context); addressRejectedTableElementTests(context); + addressFailedSubmissionTests(context, access); importSpanTests(context); failedImportRemapTests(context, access); spaceImportRunTests(context); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp index b8c859c022c8..b354a013958b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp @@ -12,6 +12,7 @@ struct BdaTestAccess { std::function(VkDeviceAddress)> addressBytes; std::function)> limitMemory; std::function allocationAttempts; + std::function submitResult; }; void RunBdaResourceTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 2e1d26e11fcb..9efb2543ef01 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3420,6 +3420,7 @@ struct MockVulkan { std::map descriptorPools; std::uint32_t descriptorPoolLimit = 1000000; VkResult descriptorResetResult = VK_SUCCESS; + VkResult submitResult = VK_SUCCESS; std::optional variableDescriptorCount; std::vector writes; std::map samplerFlags; @@ -3701,7 +3702,7 @@ VKAPI_ATTR VkResult VKAPI_CALL mockResetFences(VkDevice, std::uint32_t, const Vk VKAPI_ATTR VkResult VKAPI_CALL mockWaitForFences(VkDevice, std::uint32_t, const VkFence*, VkBool32, std::uint64_t) { return VK_SUCCESS; } VKAPI_ATTR VkResult VKAPI_CALL mockBeginCommandBuffer(VkCommandBuffer, const VkCommandBufferBeginInfo*) { return VK_SUCCESS; } VKAPI_ATTR VkResult VKAPI_CALL mockEndCommandBuffer(VkCommandBuffer) { return VK_SUCCESS; } -VKAPI_ATTR VkResult VKAPI_CALL mockQueueSubmit(VkQueue, std::uint32_t, const VkSubmitInfo*, VkFence) { return VK_SUCCESS; } +VKAPI_ATTR VkResult VKAPI_CALL mockQueueSubmit(VkQueue, std::uint32_t, const VkSubmitInfo*, VkFence) { return mock.submitResult; } VKAPI_ATTR void VKAPI_CALL mockCmdPipelineBarrier(VkCommandBuffer, VkPipelineStageFlags, VkPipelineStageFlags, VkDependencyFlags, std::uint32_t, const VkMemoryBarrier*, std::uint32_t, const VkBufferMemoryBarrier*, std::uint32_t, const VkImageMemoryBarrier*) {} VKAPI_ATTR void VKAPI_CALL mockCmdBeginQuery(VkCommandBuffer, VkQueryPool, std::uint32_t, VkQueryControlFlags) {} VKAPI_ATTR void VKAPI_CALL mockCmdEndQuery(VkCommandBuffer, VkQueryPool, std::uint32_t) {} @@ -7270,7 +7271,7 @@ int main(int argc, char** argv) { auto context = mockContext(); context.limits.maxStorageBufferRange = 1u << 27; passed = RunSparseBufferHoleTests(context, { - [](VkBuffer buffer) -> std::span { return mockBufferMemory(buffer); }, {}, {}, {}, {} + [](VkBuffer buffer) -> std::span { return mockBufferMemory(buffer); }, {}, {}, {}, {}, {} }); } Require(mock.live == 0, "sparse buffer hole tests leaked Vulkan objects"); @@ -7487,7 +7488,8 @@ int main(int argc, char** argv) { [](std::optional headroom) { mock.memoryLimit = headroom.has_value() ? std::optional(mock.allocatedBytes + *headroom) : std::nullopt; }, - [] { return mock.allocationAttempts; } + [] { return mock.allocationAttempts; }, + [](VkResult result) { mock.submitResult = result; } }); Require(mock.live == 0, "BDA resources leaked Vulkan objects"); RunGuestAllocationTests(); From 3fe43a3ff0ce19deccb5240d85944a69c952af74 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 04:50:20 +0000 Subject: [PATCH 07/13] fix(agc): order a draw's snapshots the same on decode misses and hits A fresh capture lists a draw's snapshot regions sorted by address (ShaderMemory::Regions). A decode-cache hit lists the stages' code and their matched regions in stage order instead. SameSnapshots compares the two lists entry by entry, so the first hit after a decode miss failed the address-based reuse and built the draw again whenever the orders differed, as they do when the pixel shader's code lies below the vertex shader's. The draw now sorts its snapshots by address before it hands them on. --- .../Execution/src/Driver/Draw/Draw.cpp | 1 + .../tests/execution/AddressDrawReuse.cpp | 147 ++++++++++++++++-- 2 files changed, 135 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index a6421e61c976..6ab6fb9f909b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -407,6 +407,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe const auto snapshot = [&] { snapshots.clear(); for (const auto& region : memory) snapshots.push_back({region.guestAddress, region.bytes}); + std::sort(snapshots.begin(), snapshots.end(), [](const Graphics::GuestMemorySnapshot& left, const Graphics::GuestMemorySnapshot& right) { return left.address != right.address ? left.address < right.address : left.bytes.size() < right.bytes.size(); }); }; snapshot(); timing.Mark("post_compile_prepare"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp index 4dfc8916d221..0080f7139468 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" @@ -8,6 +9,7 @@ #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" @@ -31,11 +33,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include namespace AgcDriver { @@ -79,6 +83,21 @@ alignas(256) constexpr std::array PixelCode{ 0xbf810000u, }; +alignas(256) constexpr std::array FullScreenVertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, +}; + +alignas(256) std::array SubmittedCode{}; + +struct ShaderHeader { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) ShaderHeader SubmittedPixelHeader{}; +alignas(8) ShaderHeader SubmittedVertexHeader{}; + constexpr std::array, 3> Triangle{{ {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} }}; @@ -554,32 +573,134 @@ void ThrowingProof(AgcDriver::VulkanDevice& device) { resources.WriteBack(); } +void RegisterShader(ShaderHeader& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(ShaderHeader); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +std::vector SubmittedDraw(const std::uint32_t* vertexCode, const std::uint32_t* pixelCode, std::uint64_t color, std::uint64_t pointer, std::uint64_t label) { + const auto vertex = AddressOf(vertexCode); + const auto pixel = AddressOf(pixelCode); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000u | (static_cast(ColorTileMode::RenderTarget) << 14u)}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, + {0x10f, Bits(32.0f)}, {0x110, Bits(32.0f)}, + {0x111, Bits(-2.0f)}, {0x112, Bits(2.0f)}, + {0x113, Bits(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, Bits(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 0}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 2u << 1u}, + {0x00c, static_cast(pointer)}, {0x00d, static_cast(pointer >> 32u)}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + Append(commands, 0x2d, {3, 2}); + Append(commands, 0x49, {0x14u | (5u << 8u), 1u << 29u, static_cast(label), static_cast(label >> 32u), 1u, 0u, 0u}); + return commands; +} + +void DecodeCacheHit() { + auto* pixelCode = SubmittedCode.data(); + auto* vertexCode = SubmittedCode.data() + SubmittedCode.size() / 2; + std::memcpy(pixelCode, PixelCode.data(), sizeof(PixelCode)); + std::memcpy(vertexCode, FullScreenVertexCode.data(), sizeof(FullScreenVertexCode)); + RegisterShader(SubmittedPixelHeader, pixelCode, sizeof(PixelCode), 1); + RegisterShader(SubmittedVertexHeader, vertexCode, sizeof(FullScreenVertexCode), 2); + Pages color{BlockBytes}; + color.Register(0, BlockBytes, true); + Pages guest{BlockBytes}; + guest.Register(0, BlockBytes, true); + const auto value = guest.Address(ValueOffset); + std::memcpy(guest.Data() + PointerOffset, &value, sizeof(value)); + Scene::Store(guest, ValueOffset, One); + constexpr std::size_t labelWord = 8; + auto commands = SubmittedDraw(vertexCode, pixelCode, color.Address(), guest.Address(), guest.Address(labelWord * sizeof(std::uint32_t))); + const auto submit = [&] { + std::memcpy(guest.Data() + StoreOffset, &Sentinel, sizeof(Sentinel)); + std::memset(guest.Data() + labelWord * sizeof(std::uint32_t), 0, sizeof(std::uint32_t)); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "address draw reuse: the submitted draw failed"); + AgcDriverWaitIdle_nid_postfix(); + std::array words{}; + AgcDriver::GuestMemory::Read(guest.Address(), std::as_writable_bytes(std::span(words)), 4); + Require(words[labelWord] == 1u, "address draw reuse: the label after the submitted draw did not land"); + Require(words[StoreOffset / sizeof(std::uint32_t)] == Bits(One), "address draw reuse: the submitted draw did not store through the BDA table"); + }; + auto& driver = AgcDriver::DriverDetail::Driver::Get(); + submit(); + const auto built = Builds(); + submit(); + Require(Builds() == built, "address draw reuse: a second decode miss of a submitted draw built its resources again"); + const auto before = driver.DrawCacheCounters(); + submit(); + const auto after = driver.DrawCacheCounters(); + if (after.hits == before.hits) { + std::puts("address draw reuse: reuse across a decode-cache hit not tested, the third submitted draw did not hit the decode cache"); + return; + } + Require(Builds() == built, "address draw reuse: a submitted draw that hit the decode cache built its resources again after a decode miss of the same draw"); +} + } int main(int argc, char** argv) { try { const std::size_t ranges = argc > 1 ? std::strtoull(argv[1], nullptr, 10) : 512; const std::size_t draws = argc > 2 ? std::strtoull(argv[2], nullptr, 10) : 64; - const auto device = OpenVulkanTestDevice(); - if (!device) return VulkanTestSkipped; { - Scene scene(*device, ranges); - if (!Imported(scene)) { - std::puts("skipped, host imports are unavailable: a store through the BDA table needs one"); - return VulkanTestSkipped; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + { + Scene scene(*device, ranges); + if (!Imported(scene)) { + std::puts("skipped, host imports are unavailable: a store through the BDA table needs one"); + return VulkanTestSkipped; + } + Synthetic(scene, draws); + GuestWrite(scene); + Fault(scene); + Remap(scene); + scene.Wait(); } - Synthetic(scene, draws); - GuestWrite(scene); - Fault(scene); - Remap(scene); - scene.Wait(); + OversizedVertexBuffer(*device); + ThrowingProof(*device); } - OversizedVertexBuffer(*device); - ThrowingProof(*device); + DecodeCacheHit(); + AgcDriverShutdown_nid_postfix(); std::printf("address draw reuse tests passed (%zu extra registered ranges)\n", ranges); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); return 1; } } From 89ab0ea75a987e130068334b462d2373558ec2dc Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 04:44:41 +0000 Subject: [PATCH 08/13] perf(agc): count address-based draw reuses and refusals in the draw profile A tester of address-based draw reuse could not tell how often a draw reused its build: the driver counted no reuse, and two runs averaged 4.4 and 10.2 ms per address-based draw where others took about 1 ms. A refused reuse pays for its proof and then for the build, so the refusals and their reasons are what tell such runs apart. The resource cache now counts, for address-based draw builds, the proofs Revalidate runs, the reuses, and the refusals by reason: in use (the build's previous draw has not completed; refused before any proof), not reusable (its draw faulted), snapshots differ, the space did not resume (by its state, a replaced space, the device, the registry generation, the import epoch, or a sparse mirror that could not be refreshed), the image or buffer proof failed, the imports changed, the proof threw, and read-only buffers that moved after the proof passed. The time of refused proofs is summed under APS5_PROFILE_DRAW. A reused draw's hold of its space is counted until its use ends, and host import refusals, over the budget or of an import that would replace a held one, are counted with how many came while such a hold was live. The reused flag travels in the use token BeginUse returns, so the hold is released with the use count when the token goes, also when a batch is discarded without running its completions. A proof that throws is counted on its own, as "threw": the throw can come from resuming the space, from the proof or from the imports check, and counting it under any of those would hide an error among ordinary refusals. APS5_PROFILE_DRAW prints them every 10 s right after the [rescache] draws line, beside the pin-contention waits of the same window (the [address-sync] leases line prints those cumulatively on its own timer): "[rescache] address-based draws: P proofs, R reused; refused: in use N, ..., threw N, moved buffers N; ...; host import refusals: budget B (H while a reused draw held its space), held import ...; pin-contention waits W (T ms)". agc_driver_address_draw_reuse reads the counters: repeated unchanged draws count as reuses, a draw repeated before the previous one completed counts one refusal in use, a draw after the guest rewrote its pixel program counts one refusal for a changed snapshot, a draw after a guest write into memory it reads in place counts a reuse, the draw after a fault counts one refusal of a build that is no longer reusable, and a proof that throws counts one refusal that threw. The BDA resource tests check that a reused draw's hold is counted while its batch is pending and released when the batch fails to submit. --- .../Graphics/include/GuestBufferMemory.hpp | 19 +++++ .../Graphics/include/ShaderResources.hpp | 2 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 21 ++++- .../Graphics/src/GuestBufferMemory.cpp | 84 ++++++++++++++++++- .../Graphics/src/ShaderResources.cpp | 50 +++++++---- .../libSceAgcDriver/tests/BdaResources.cpp | 9 +- .../tests/execution/AddressDrawReuse.cpp | 55 +++++++++++- 7 files changed, 219 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 908749677d77..5ce141f39545 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -5,6 +5,7 @@ #include "BdaAbi.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include +#include #include #include #include @@ -180,6 +181,24 @@ struct AddressSpaceStats { }; AddressSpaceStats AddressSpaceCounters(); +enum class AddressReuse : std::uint8_t { Reused, NotReusable, InUse, Snapshots, Space, Proof, Imports, Threw, Moved, Count }; +enum class SpaceResumeMiss : std::uint8_t { State, Replaced, Device, Generation, Epoch, Mirrors, Count }; +struct AddressReuseStats { + std::uint64_t proofs = 0; + std::array(AddressReuse::Count)> outcomes{}; + std::array(SpaceResumeMiss::Count)> resumeMisses{}; + std::uint64_t refusedProofNs = 0; + std::uint64_t reusedHolds = 0; + std::uint64_t budgetRefusals = 0; + std::uint64_t budgetRefusalsHeld = 0; + std::uint64_t heldImportRefusals = 0; + std::uint64_t heldImportRefusalsHeld = 0; +}; +void CountAddressReuseProof(); +void CountAddressReuse(AddressReuse outcome, std::uint64_t proofNs = 0); +void HoldReusedSpace(bool held); +AddressReuseStats AddressReuseCounters(); + struct MirrorStats { std::uint64_t heapMirrors = 0; std::uint64_t heapBytes = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index ee0b82d2b144..90d67b75cc1c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -232,6 +232,7 @@ class ShaderResources { bool ChecksFaults() const { return bda != nullptr; } void CheckFaults() const; bool InUse() const { return usesInFlight->load(std::memory_order_acquire) != 0; } + bool ReusesAddressSpace() const { return addressReuse; } std::shared_ptr BeginUse(); void CompleteUse(); void EndUse() noexcept; @@ -556,6 +557,7 @@ class ShaderResources { std::vector samplerSources; bool reusable = false; bool addressReuse = false; + bool reuseProved = false; std::shared_ptr> usesInFlight = std::make_shared>(0u); std::vector> addressWrites; std::vector directRegions; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 45131daea4ee..5479d2984a97 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -456,6 +456,8 @@ struct DrawProfile { // Draw packets that drew nothing, by DrawSkip, and their time. std::array(DrawSkip::Count)> skips{}; std::array(DrawSkip::Count)> skipUs{}; + AddressReuseStats reuseSeen; + LeaseStats leaseSeen; std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now(); }; constexpr std::array(DrawSkip::Count)> DrawSkipNames{"nothing to draw", "prechecked", "thrown"}; @@ -558,6 +560,16 @@ void reportDraw(const std::array& us, const ShaderResources: } std::fprintf(stderr, "%s\n", line); std::fprintf(stderr, "[rescache] draws: %llu hits, %llu misses, %llu invalidated, %llu uncacheable; validation memo %llu hits / %llu misses (which key words the misses differ in: the miss churn line)\n", static_cast(profile.cacheHits), static_cast(profile.cacheMisses), static_cast(profile.cacheInvalidated), static_cast(profile.uncacheable), static_cast(profile.validateHits), static_cast(profile.validateMisses)); + const auto reuse = AddressReuseCounters(); + const auto leases = LeaseCounters(); + const auto& reuseSeen = profile.reuseSeen; + const auto reuseCount = [&](AddressReuse kind) { return static_cast(reuse.outcomes[static_cast(kind)] - reuseSeen.outcomes[static_cast(kind)]); }; + const auto resumeMiss = [&](SpaceResumeMiss kind) { return static_cast(reuse.resumeMisses[static_cast(kind)] - reuseSeen.resumeMisses[static_cast(kind)]); }; + if (reuse.proofs != reuseSeen.proofs || reuseCount(AddressReuse::InUse) != 0) { + std::fprintf(stderr, "[rescache] address-based draws: %llu proofs, %llu reused; refused: in use %llu, not reusable %llu, snapshots %llu, space %llu (state %llu, replaced %llu, device %llu, generation %llu, epoch %llu, mirrors %llu), images or buffers %llu, imports %llu, threw %llu, moved buffers %llu; %.1f ms in refused proofs; host import refusals: budget %llu (%llu while a reused draw held its space), held import %llu (%llu); pin-contention waits %llu (%.1f ms)\n", static_cast(reuse.proofs - reuseSeen.proofs), reuseCount(AddressReuse::Reused), reuseCount(AddressReuse::InUse), reuseCount(AddressReuse::NotReusable), reuseCount(AddressReuse::Snapshots), reuseCount(AddressReuse::Space), resumeMiss(SpaceResumeMiss::State), resumeMiss(SpaceResumeMiss::Replaced), resumeMiss(SpaceResumeMiss::Device), resumeMiss(SpaceResumeMiss::Generation), resumeMiss(SpaceResumeMiss::Epoch), resumeMiss(SpaceResumeMiss::Mirrors), reuseCount(AddressReuse::Proof), reuseCount(AddressReuse::Imports), reuseCount(AddressReuse::Threw), reuseCount(AddressReuse::Moved), static_cast(reuse.refusedProofNs - reuseSeen.refusedProofNs) / 1e6, static_cast(reuse.budgetRefusals - reuseSeen.budgetRefusals), static_cast(reuse.budgetRefusalsHeld - reuseSeen.budgetRefusalsHeld), static_cast(reuse.heldImportRefusals - reuseSeen.heldImportRefusals), static_cast(reuse.heldImportRefusalsHeld - reuseSeen.heldImportRefusalsHeld), static_cast(leases.contentionWaits - profile.leaseSeen.contentionWaits), leases.contentionMs - profile.leaseSeen.contentionMs); + } + profile.reuseSeen = reuse; + profile.leaseSeen = leases; profile.totalsUs.fill(0); profile.maxUs.fill(0); profile.maxDrawUs = profile.hookWaitUs = profile.maxHookWaitUs = profile.ownSyncUs = 0; @@ -1292,7 +1304,12 @@ ResolvedResources resolveDrawResources(const Context& context, const State& stat resolved.cacheable = recordable && !noDrawResourceCache && !noTextureCache && std::all_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->PipelineVariantId() != 0; }) && !ShaderResources::NeverReusable(shaders); if (resolved.cacheable) { resolved.contentKey = DrawResourceKey(context, shaders, state.color, static_cast(state.colors.size()), draw.indexAddress, indexBytes, !trimKey); - if (auto cached = SharedResourceCache().Find(resolved.contentKey); cached != nullptr && !cached->InUse()) { + auto cached = SharedResourceCache().Find(resolved.contentKey); + const bool inUse = cached != nullptr && cached->InUse(); + if (inUse) CountAddressReuse(AddressReuse::InUse); + if (cached != nullptr && !inUse) { + const bool addressBased = cached->ReusesAddressSpace(); + const auto proofStart = addressBased && timer.profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; const bool valid = cached->Revalidate(shaders, nullptr, snapshots); auto* recorder = Recorder::Active(); std::optional> moved; @@ -1304,7 +1321,9 @@ ResolvedResources resolveDrawResources(const Context& context, const State& stat if (trimKey) CheckBufferAliases(context, shaders, state.color, draw.indexAddress, indexBytes); outcome.kind = KindTemplateHit; countCache(&DrawProfile::cacheHits); + if (addressBased) CountAddressReuse(AddressReuse::Reused); } else if (valid) { + if (addressBased) CountAddressReuse(AddressReuse::Moved, timer.profile ? static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - proofStart).count()) : 0); cached->EndUse(); countCache(&DrawProfile::cacheMisses); } else { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 3c2465595ccc..81be32bfbc4b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -149,6 +149,33 @@ AddressSpaceCache& Spaces() { return cache; } +struct AddressReuseCounts { + std::atomic proofs{0}; + std::array, static_cast(AddressReuse::Count)> outcomes{}; + std::array, static_cast(SpaceResumeMiss::Count)> resumeMisses{}; + std::atomic refusedProofNs{0}; + std::atomic reusedHolds{0}; + std::atomic budgetRefusals{0}; + std::atomic budgetRefusalsHeld{0}; + std::atomic heldImportRefusals{0}; + std::atomic heldImportRefusalsHeld{0}; +}; + +AddressReuseCounts& ReuseCounts() { + static AddressReuseCounts counts; + return counts; +} + +void countResumeMiss(SpaceResumeMiss miss) { + ReuseCounts().resumeMisses[static_cast(miss)].fetch_add(1, std::memory_order_relaxed); +} + +void countImportRefusal(std::atomic AddressReuseCounts::*all, std::atomic AddressReuseCounts::*held) { + auto& counts = ReuseCounts(); + (counts.*all).fetch_add(1, std::memory_order_relaxed); + if (counts.reusedHolds.load(std::memory_order_relaxed) != 0) (counts.*held).fetch_add(1, std::memory_order_relaxed); +} + bool addressSpaceCacheEnabled() { static const bool disabled = std::getenv("APS5_NO_ADDRESS_SPACE_CACHE") != nullptr; return !disabled; @@ -1067,7 +1094,10 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import return !expandedWindow || Recorder::Active() == nullptr; }; for (auto it = first; it != state.imports.end() && it->first < base + bytes; ++it) { - if (held(it->second)) return nullptr; + if (held(it->second)) { + countImportRefusal(&AddressReuseCounts::heldImportRefusals, &AddressReuseCounts::heldImportRefusalsHeld); + return nullptr; + } replaced += it->second.bytes; } const auto budget = importBudget(context); @@ -1082,6 +1112,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import lastReport = std::chrono::steady_clock::now(); std::fprintf(stderr, "[gpu] host import budget: %llu MiB live of %llu MiB (APS5_HOST_IMPORT_MIB); %llu imports (%llu MiB) refused so far, last 0x%llx+0x%llx\n", static_cast(state.liveBytes >> 20u), static_cast(budget >> 20u), static_cast(refused), static_cast(refusedBytes >> 20u), static_cast(base), static_cast(bytes)); } + countImportRefusal(&AddressReuseCounts::budgetRefusals, &AddressReuseCounts::budgetRefusalsHeld); return nullptr; } for (auto it = first; it != state.imports.end() && it->first < base + bytes;) { @@ -2271,6 +2302,38 @@ AddressSpaceStats AddressSpaceCounters() { return {addressSpaceCacheEnabled(), load(cache.hits), load(cache.rebuiltFirst), load(cache.rebuiltGeneration), load(cache.rebuiltEpoch), load(cache.rebuiltDevice), load(cache.rebuiltWaiterDrop), load(cache.unpublished), load(cache.dissolvedOverlap), load(cache.dissolvedImports), load(cache.waiterDrops)}; } +void CountAddressReuseProof() { + ReuseCounts().proofs.fetch_add(1, std::memory_order_relaxed); +} + +void CountAddressReuse(AddressReuse outcome, std::uint64_t proofNs) { + auto& counts = ReuseCounts(); + counts.outcomes[static_cast(outcome)].fetch_add(1, std::memory_order_relaxed); + if (outcome != AddressReuse::Reused && proofNs != 0) counts.refusedProofNs.fetch_add(proofNs, std::memory_order_relaxed); +} + +void HoldReusedSpace(bool held) { + auto& holds = ReuseCounts().reusedHolds; + if (held) holds.fetch_add(1, std::memory_order_relaxed); + else holds.fetch_sub(1, std::memory_order_relaxed); +} + +AddressReuseStats AddressReuseCounters() { + const auto& counts = ReuseCounts(); + const auto load = [](const std::atomic& counter) { return counter.load(std::memory_order_relaxed); }; + AddressReuseStats stats; + stats.proofs = load(counts.proofs); + for (std::size_t index = 0; index < stats.outcomes.size(); ++index) stats.outcomes[index] = load(counts.outcomes[index]); + for (std::size_t index = 0; index < stats.resumeMisses.size(); ++index) stats.resumeMisses[index] = load(counts.resumeMisses[index]); + stats.refusedProofNs = load(counts.refusedProofNs); + stats.reusedHolds = load(counts.reusedHolds); + stats.budgetRefusals = load(counts.budgetRefusals); + stats.budgetRefusalsHeld = load(counts.budgetRefusalsHeld); + stats.heldImportRefusals = load(counts.heldImportRefusals); + stats.heldImportRefusalsHeld = load(counts.heldImportRefusalsHeld); + return stats; +} + std::string AddressCopyOverflow(std::vector copies, std::uint64_t limit) { std::uint64_t total = 0; for (const auto& copy : copies) total += copy.committed; @@ -4602,17 +4665,30 @@ void GuestBufferMemory::SuspendSpace() noexcept { bool GuestBufferMemory::ResumeSpace() { SuspendSpace(); - if (!uploaded || committed || !lease.empty() || suspendedSpace == 0) return false; + if (!uploaded || committed || !lease.empty() || suspendedSpace == 0) { + countResumeMiss(SpaceResumeMiss::State); + return false; + } const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); auto current = Spaces().current.load(); { auto& state = Imports(); std::lock_guard lock(state.mutex); - if (current == nullptr || current->serial != suspendedSpace || spaceMiss(context, current.get(), generation, state) != SpaceMiss::None) return false; + if (current == nullptr || current->serial != suspendedSpace) { + countResumeMiss(SpaceResumeMiss::Replaced); + return false; + } + if (const auto miss = spaceMiss(context, current.get(), generation, state); miss != SpaceMiss::None) { + countResumeMiss(miss == SpaceMiss::Device ? SpaceResumeMiss::Device : miss == SpaceMiss::Generation ? SpaceResumeMiss::Generation : miss == SpaceMiss::Epoch ? SpaceResumeMiss::Epoch : SpaceResumeMiss::Replaced); + return false; + } stampSpaceImports(*current, state); } std::vector blocks; - if (!refreshSparseMirrors(current, blocks)) return false; + if (!refreshSparseMirrors(current, blocks)) { + countResumeMiss(SpaceResumeMiss::Mirrors); + return false; + } appendWritableMirrors(*current, blocks); const auto checked = refreshHeapRuns(*current, blocks); compareBlocks(blocks); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 9323add6116c..3df0a274cfa9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2820,8 +2820,19 @@ const ShaderRecompiler::DescriptorBinding* ShaderResources::tableImages(std::spa bool ShaderResources::Revalidate(std::span shaders, ProofReport* report, std::span snapshots) { if (!addressReuse) return proveBuild(shaders, report); - if (report != nullptr) *report = {ProofPath::Full, ProofFailure::Other}; - if (!reusable || InUse() || !guestMemory.SameSnapshots(snapshots)) return false; + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + const auto refuse = [&](AddressReuse reason) { + CountAddressReuse(reason, profile ? static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count()) : 0); + return false; + }; + CountAddressReuseProof(); + ProofReport proof; + auto& outcome = report != nullptr ? *report : proof; + outcome = {ProofPath::Full, ProofFailure::Other}; + if (!reusable) return refuse(AddressReuse::NotReusable); + if (InUse()) return refuse(AddressReuse::InUse); + if (!guestMemory.SameSnapshots(snapshots)) return refuse(AddressReuse::Snapshots); struct Suspend { GuestBufferMemory& memory; bool armed = true; @@ -2830,17 +2841,19 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR } } suspend{guestMemory}; try { - if (!guestMemory.ResumeSpace()) return false; - const bool proved = proveBuild(shaders, report); - if (proved && guestMemory.SpaceImportsCurrent()) { - suspend.armed = false; - return true; + if (!guestMemory.ResumeSpace()) return refuse(AddressReuse::Space); + if (!proveBuild(shaders, &outcome)) return refuse(outcome.failure == ProofFailure::Imports ? AddressReuse::Imports : AddressReuse::Proof); + if (!guestMemory.SpaceImportsCurrent()) { + outcome.failure = ProofFailure::Imports; + return refuse(AddressReuse::Imports); } - if (proved && report != nullptr) report->failure = ProofFailure::Imports; } catch (const std::exception&) { - if (report != nullptr) report->failure = ProofFailure::Other; + outcome.failure = ProofFailure::Other; + return refuse(AddressReuse::Threw); } - return false; + suspend.armed = false; + reuseProved = true; + return true; } bool ShaderResources::proveBuild(std::span shaders, ProofReport* report) { @@ -4756,12 +4769,19 @@ void ShaderResources::CheckFaults() const { std::shared_ptr ShaderResources::BeginUse() { if (!addressReuse) return {}; struct Use { - Use(std::shared_ptr> counter, std::shared_ptr hold) : uses(std::move(counter)), space(std::move(hold)) { uses->fetch_add(1, std::memory_order_acq_rel); } - ~Use() { uses->fetch_sub(1, std::memory_order_acq_rel); } + Use(std::shared_ptr> counter, std::shared_ptr hold, bool reusedSpace) : uses(std::move(counter)), space(std::move(hold)), reused(reusedSpace) { + uses->fetch_add(1, std::memory_order_acq_rel); + if (reused) HoldReusedSpace(true); + } + ~Use() { + if (reused) HoldReusedSpace(false); + uses->fetch_sub(1, std::memory_order_acq_rel); + } std::shared_ptr> uses; std::shared_ptr space; + bool reused; }; - return std::make_shared(usesInFlight, guestMemory.SpaceHold()); + return std::make_shared(usesInFlight, guestMemory.SpaceHold(), std::exchange(reuseProved, false)); } void ShaderResources::CompleteUse() { @@ -4780,7 +4800,9 @@ void ShaderResources::CompleteUse() { } void ShaderResources::EndUse() noexcept { - if (addressReuse) guestMemory.SuspendSpace(); + if (!addressReuse) return; + reuseProved = false; + guestMemory.SuspendSpace(); } void ShaderResources::WriteBackBuffers() { diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 529a8e4cda21..b43934dc7f48 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -1887,15 +1887,21 @@ void addressFailedSubmissionTests(const Context& context, const BdaTestAccess& a const std::array stages{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); Recorder recorder(context); - { + for (const bool reuse : {false, true}) { auto resources = std::make_shared(context, stages, ColorTarget{}, 0, 0, 0, std::span{}); Require(resources->UsesAddressTable() && resources->Reusable(), "an address-based draw build is not reusable"); + if (reuse) { + resources->EndUse(); + Require(resources->Revalidate(stages, nullptr, {}), "an unchanged address-based draw build was not proved reusable"); + } + const auto holds = AddressReuseCounters().reusedHolds; recorder.OnComplete([resources, hold = resources->BeginUse()]() mutable { const auto held = std::move(hold); resources->CompleteUse(); }); resources->EndUse(); Require(resources->InUse(), "an address-based draw build taken by a recorded batch is not in use"); + Require(AddressReuseCounters().reusedHolds == holds + (reuse ? 1u : 0u), reuse ? "a reused address-based draw did not count its hold of the space" : "a built address-based draw counted a reused hold of the space"); access.submitResult(VK_ERROR_OUT_OF_HOST_MEMORY); bool failed = false; try { @@ -1906,6 +1912,7 @@ void addressFailedSubmissionTests(const Context& context, const BdaTestAccess& a access.submitResult(VK_SUCCESS); Require(failed, "a batch whose submission failed did not report it"); Require(!resources->InUse(), "an address-based draw build stayed in use after the batch that took it failed to submit"); + Require(AddressReuseCounters().reusedHolds == holds, "a reused address-based draw's hold of its space stayed counted after the batch that took it failed to submit"); resources->WriteBack(); } recorder.Sync(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp index 0080f7139468..99e512d3eb70 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp @@ -222,6 +222,16 @@ std::uint64_t Builds() { return counters.hits + counters.misses; } +std::uint64_t Counted(const AgcDriver::Graphics::AddressReuseStats& stats, AgcDriver::Graphics::AddressReuse outcome) { + return stats.outcomes[static_cast(outcome)]; +} + +std::uint64_t Refusals(const AgcDriver::Graphics::AddressReuseStats& stats) { + std::uint64_t refusals = 0; + for (const auto count : stats.outcomes) refusals += count; + return refusals - Counted(stats, AgcDriver::Graphics::AddressReuse::Reused); +} + struct Scene { AgcDriver::VulkanDevice& device; Pages pixels{TargetLayout.Bytes()}; @@ -232,6 +242,7 @@ struct Scene { std::size_t ranges; std::vector vertexUserData; std::vector pixelUserData; + std::array pixelProgram = PixelCode; ShaderRecompiler::RecompileResult vertexResult; ShaderRecompiler::RecompileResult pixelResult; std::array shaders{}; @@ -278,7 +289,7 @@ struct Scene { pixelResult = ShaderRecompiler::Recompile(fragment); Require(std::any_of(pixelResult.bindings.begin(), pixelResult.bindings.end(), [](const auto& binding) { return binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable; }), "address draw reuse: the pixel shader does not reach guest memory through the BDA table"); shaders = {{{ShaderStage::Vertex, &vertexResult, 0}, {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}}}; - snapshots = {{{AddressOf(VertexCode.data()), std::as_bytes(std::span(VertexCode))}, {AddressOf(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + snapshots = {{{AddressOf(VertexCode.data()), std::as_bytes(std::span(VertexCode))}, {AddressOf(PixelCode.data()), std::as_bytes(std::span(pixelProgram))}}}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; state.color = {pixels.Address(), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, TargetLayout.Bytes(), 0xe4u, ColorTileMode::RenderTarget}; state.colors = {state.color}; @@ -389,6 +400,38 @@ void Synthetic(Scene& scene, std::size_t draws) { Require(waited.builds == 0, "address draw reuse: " + std::to_string(waited.builds) + " of " + std::to_string(draws) + " repeated address-based draws with unchanged inputs built their resources again"); } +void ReuseCounters(Scene& scene) { + using AgcDriver::Graphics::AddressReuse; + using AgcDriver::Graphics::AddressReuseCounters; + constexpr std::uint64_t Repeats = 4; + scene.Clear(); + scene.Draw(); + scene.Wait(); + auto before = AddressReuseCounters(); + for (std::uint64_t draw = 0; draw < Repeats; ++draw) { + scene.Draw(); + scene.Wait(); + } + auto after = AddressReuseCounters(); + const auto reused = Counted(after, AddressReuse::Reused) - Counted(before, AddressReuse::Reused); + Require(reused == Repeats && after.proofs - before.proofs == Repeats && Refusals(after) == Refusals(before), "address draw reuse: " + std::to_string(Repeats) + " repeated draws with unchanged inputs counted " + std::to_string(reused) + " reuses, " + std::to_string(after.proofs - before.proofs) + " proofs and " + std::to_string(Refusals(after) - Refusals(before)) + " refusals"); + before = after; + scene.Draw(); + scene.Draw(); + scene.Wait(); + after = AddressReuseCounters(); + Require(Counted(after, AddressReuse::InUse) - Counted(before, AddressReuse::InUse) == 1 && Counted(after, AddressReuse::Reused) - Counted(before, AddressReuse::Reused) == 1 && Refusals(after) - Refusals(before) == 1, "address draw reuse: a draw repeated before the previous one completed was not counted as one refusal in use"); + before = after; + scene.pixelProgram.back() ^= 1u; + scene.Draw(); + scene.Wait(); + after = AddressReuseCounters(); + Require(Counted(after, AddressReuse::Snapshots) - Counted(before, AddressReuse::Snapshots) == 1 && Refusals(after) - Refusals(before) == 1 && Counted(after, AddressReuse::Reused) == Counted(before, AddressReuse::Reused), "address draw reuse: a draw after the guest rewrote its pixel program was not counted as one refusal for a changed snapshot"); + scene.pixelProgram = PixelCode; + scene.Draw(); + scene.Wait(); +} + void GuestWrite(Scene& scene) { scene.Clear(); scene.Draw(); @@ -396,10 +439,13 @@ void GuestWrite(Scene& scene) { Scene::Store(scene.guest, ValueOffset, Zero); scene.Clear(); const auto before = Builds(); + const auto counted = AgcDriver::Graphics::AddressReuseCounters(); scene.Draw(); Require(scene.Color() == 0, "address draw reuse: a draw after a guest write rendered the stale value"); Require(scene.Stored() == Bits(Zero), "address draw reuse: a draw after a guest write stored the stale value through the BDA table"); Require(Builds() == before, "address draw reuse: a guest write into memory the draw reads in place made it build its resources again"); + const auto after = AgcDriver::Graphics::AddressReuseCounters(); + Require(Counted(after, AgcDriver::Graphics::AddressReuse::Reused) == Counted(counted, AgcDriver::Graphics::AddressReuse::Reused) + 1 && Refusals(after) == Refusals(counted), "address draw reuse: a draw after a guest write into memory it reads in place was not counted as one reuse"); Scene::Store(scene.guest, ValueOffset, One); for (int draw = 0; draw < 3; ++draw) { scene.Clear(); @@ -443,9 +489,12 @@ void Fault(Scene& scene) { scene.Point(scene.guest.Address(ValueOffset)); scene.Clear(); const auto afterFault = Builds(); + const auto counted = AgcDriver::Graphics::AddressReuseCounters(); scene.Draw(); Require(scene.Color() == 255 && scene.Stored() == Bits(One), "address draw reuse: the draw after a fault did not read the guest value"); Require(Builds() == afterFault + 1, "address draw reuse: a build whose draw faulted served the next draw"); + const auto refused = AgcDriver::Graphics::AddressReuseCounters(); + Require(Counted(refused, AgcDriver::Graphics::AddressReuse::NotReusable) == Counted(counted, AgcDriver::Graphics::AddressReuse::NotReusable) + 1 && Refusals(refused) == Refusals(counted) + 1, "address draw reuse: the draw after a fault was not counted as one refusal of a build that is no longer reusable"); #ifdef _WIN32 VirtualFree(reserved, 0, MEM_RELEASE); #else @@ -563,6 +612,7 @@ void ThrowingProof(AgcDriver::VulkanDevice& device) { device.WaitIdle(); bool threw = false; bool reused = false; + const auto counted = AgcDriver::Graphics::AddressReuseCounters(); try { reused = resources.Revalidate(stages, nullptr, {}); } catch (const std::runtime_error&) { @@ -570,6 +620,8 @@ void ThrowingProof(AgcDriver::VulkanDevice& device) { } Require(!threw, "address draw reuse: the reuse proof of an address-based draw build whose sampled image's memory became a multisampled target threw instead of refusing the reuse"); Require(!reused, "address draw reuse: an address-based draw build whose sampled image's memory became a multisampled target was reused"); + const auto refused = AgcDriver::Graphics::AddressReuseCounters(); + Require(refused.proofs == counted.proofs + 1 && Counted(refused, AgcDriver::Graphics::AddressReuse::Threw) == Counted(counted, AgcDriver::Graphics::AddressReuse::Threw) + 1 && Refusals(refused) == Refusals(counted) + 1 && Counted(refused, AgcDriver::Graphics::AddressReuse::Reused) == Counted(counted, AgcDriver::Graphics::AddressReuse::Reused), "address draw reuse: a reuse proof that threw was not counted as one refusal that threw"); resources.WriteBack(); } @@ -686,6 +738,7 @@ int main(int argc, char** argv) { return VulkanTestSkipped; } Synthetic(scene, draws); + ReuseCounters(scene); GuestWrite(scene); Fault(scene); Remap(scene); From 6e5b5bdf4bd6b440a7f7ce3b38cdb91d44fd289b Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 22:17:45 +0000 Subject: [PATCH 09/13] fix(agc): complete recorded GPU writes over snapshots before an address reuse proof The reuse proof of an address-based draw build compares the draw's snapshots with guest memory for live regions and with the build's copies for the others. A write that earlier work in the batch recorded over a snapshot's range (a fill, a copy, a dispatch storing through a descriptor) may still be pending when the proof runs: memory then still holds the bytes the capture saw, the proof passes, and the draw runs after the write on a program the decoder specialized on the old bytes. Revalidate now syncs the recorder through every snapshot range that a recorded write overlaps before it compares the snapshots, so the comparison sees the memory the earlier work leaves, and a capture made before that write is refused as a changed snapshot. A range with no pending write costs one overlap test. Stores through the BDA table are not recorded as pending writes; a later use reads them in place. agc_driver_address_draw_reuse gains the case: a draw whose snapshots include the guest pointer its pixel shader reads, reused once, then a FillBuffer recorded over that pointer and the same draw with the old capture in the same batch. The draw is refused for its snapshots, builds again and reads the new pointer; without the sync it was reused ("1 reuses, 0 builds"). --- .../Graphics/src/ShaderResources.cpp | 11 +++++ .../tests/execution/AddressDrawReuse.cpp | 44 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 3df0a274cfa9..96f758094303 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2076,6 +2076,16 @@ bool AddressReuseEnabled() { return enabled; } +void CompleteSnapshotWrites(std::span snapshots) { + auto* recorder = Recorder::Active(); + if (recorder == nullptr || Recorder::InCompletion()) return; + for (const auto& snapshot : snapshots) { + if (snapshot.bytes.empty() || !recorder->PendingWriteOverlaps(snapshot.address, snapshot.bytes.size())) continue; + Recorder::CountSync(4); + recorder->SyncThrough(snapshot.address, snapshot.bytes.size()); + } +} + } void ShaderResources::noteReusable() { @@ -2832,6 +2842,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR outcome = {ProofPath::Full, ProofFailure::Other}; if (!reusable) return refuse(AddressReuse::NotReusable); if (InUse()) return refuse(AddressReuse::InUse); + CompleteSnapshotWrites(snapshots); if (!guestMemory.SameSnapshots(snapshots)) return refuse(AddressReuse::Snapshots); struct Suspend { GuestBufferMemory& memory; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp index 99e512d3eb70..71ea1d2023e7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp @@ -455,6 +455,49 @@ void GuestWrite(Scene& scene) { } } +void PendingSnapshotWrite(Scene& scene) { + using AgcDriver::Graphics::AddressReuse; + constexpr std::size_t RewrittenOffset = 32; + const std::array captured{scene.guest.Address(ValueOffset)}; + const std::array snapshots{{ + scene.snapshots[0], + scene.snapshots[1], + {scene.guest.Address(PointerOffset), std::as_bytes(std::span(captured))}, + }}; + const auto draw = [&] { + const AgcDriver::Pm4::DrawParameters parameters{0, static_cast(Triangle.size()), 0, 1, 0, false}; + scene.device.Draw(scene.state, parameters, scene.shaders, snapshots); + }; + Scene::Store(scene.guest, RewrittenOffset, Zero); + scene.Point(scene.guest.Address(ValueOffset)); + for (int warm = 0; warm < 2; ++warm) { + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + draw(); + } + scene.Wait(); + } + Require(scene.Color() == 255, "address draw reuse: the draw with a captured pointer did not read the guest value"); + const auto rewritten = scene.guest.Address(RewrittenOffset); + const std::array pattern{static_cast(rewritten), static_cast(rewritten >> 32u), 0u, 0u}; + const auto before = AgcDriver::Graphics::AddressReuseCounters(); + const auto built = Builds(); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Require(scene.device.FillBuffer(scene.guest.Address(PointerOffset), 16, pattern), "address draw reuse: the fill over the captured pointer was not recorded on the GPU"); + draw(); + } + const auto color = scene.Color(); + const auto after = AgcDriver::Graphics::AddressReuseCounters(); + const auto reused = Counted(after, AddressReuse::Reused) - Counted(before, AddressReuse::Reused); + Require(color == 0, "address draw reuse: a draw after a recorded fill rewrote its captured pointer read the old pointer"); + Require(Counted(after, AddressReuse::Snapshots) == Counted(before, AddressReuse::Snapshots) + 1 && reused == 0 && Builds() == built + 1, "address draw reuse: a draw whose captured snapshot a pending GPU write changed was not refused for a changed snapshot and rebuilt (" + std::to_string(reused) + " reuses, " + std::to_string(Builds() - built) + " builds)"); + scene.Point(scene.guest.Address(ValueOffset)); + scene.Clear(); + scene.Draw(); + Require(scene.Color() == 255 && scene.Stored() == Bits(One), "address draw reuse: the draw after the captured pointer was restored did not read the guest value"); +} + void Fault(Scene& scene) { void* reserved = nullptr; #ifdef _WIN32 @@ -740,6 +783,7 @@ int main(int argc, char** argv) { Synthetic(scene, draws); ReuseCounters(scene); GuestWrite(scene); + PendingSnapshotWrite(scene); Fault(scene); Remap(scene); scene.Wait(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 3c5e63bb5639..c285c48bd57e 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -158,4 +158,4 @@ - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; a title that waits only on release interrupts sees one extra event per flip, with `data` counting it. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less 2 GiB (imports and type-3 mirrors share it; #167's rule). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. -- [Reused address-based draw builds](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): a recorded draw whose stages reach guest memory through the BDA table reuses the build of an earlier draw with the same content (`APS5_NO_ADDRESS_DRAW_REUSE=1` builds each one) while the cached address space it was built over is the one a build would take now, its regions outside the space are imports or read-only image mirrors with unchanged serials or snapshot copies whose bytes the draw's snapshots repeat in the same order, the read-only buffers and data words its content key leaves out equal the build's (it cannot rebind them per draw), and the completion of its previous draw has run. Each draw takes the space for its batch, brings the space's mirrors up to date as a build does, and checks its faults and marks its written pages at its own completion; between draws the build pins no guest allocation. The build's fault buffer holds the fault and written-page records of one draw, so a draw that repeats it before that completion ran (two in one batch) builds anew and replaces it in the resource cache. Dispatches and draw recipes do not reuse address-based builds. Measured only in a synthetic draw on lavapipe; how often titles repeat a draw's content and snapshot list between batches is not measured. +- [Reused address-based draw builds](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): a recorded draw whose stages reach guest memory through the BDA table reuses the build of an earlier draw with the same content (`APS5_NO_ADDRESS_DRAW_REUSE=1` builds each one) while the cached address space it was built over is the one a build would take now, its regions outside the space are imports or read-only image mirrors with unchanged serials or snapshot copies whose bytes the draw's snapshots repeat in the same order, the read-only buffers and data words its content key leaves out equal the build's (it cannot rebind them per draw), and the completion of its previous draw has run. Recorded GPU writes over a snapshot's range are completed before the snapshots are compared, so a capture made before such a write lands is refused rather than proven against the old bytes. Each draw takes the space for its batch, brings the space's mirrors up to date as a build does, and checks its faults and marks its written pages at its own completion; between draws the build pins no guest allocation. The build's fault buffer holds the fault and written-page records of one draw, so a draw that repeats it before that completion ran (two in one batch) builds anew and replaces it in the resource cache. Dispatches and draw recipes do not reuse address-based builds. Measured only in a synthetic draw on lavapipe; how often titles repeat a draw's content and snapshot list between batches is not measured. From 4bacab407a8a471bae7b32e5993a5c3a9b090381 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 18 Oct 2026 04:55:16 -0700 Subject: [PATCH 10/13] docs(environment): add the row for the address-based draw reuse switch --- docs/dev/ENVIRONMENT.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 27bf42846ed9..5891341d90ca 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -171,6 +171,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_LOG_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp` | | writes AGC log lines on the calling thread instead of through the asynchronous sink thread | | `APS5_LOOP_GUARD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | enables the fault buffer that guards shader loops running past their bound | | `APS5_MIRROR_REFRESH_THREADS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 3 | number of helper threads that refresh heap mirrors, clamped to 0 through 16 | +| `APS5_NO_ADDRESS_DRAW_REUSE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | turns off the reuse of an address-based draw's build, so every such draw builds again | | `APS5_NO_ADDRESS_READ_PASSES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | stops a draw that only reads through its buffer descriptors from counting as read-only, so it breaks the pass instead of continuing the producer's | | `APS5_NO_ADDRESS_SPACE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | disables the address-space cache so every build rebuilds its space | | `APS5_NO_ADJACENT_GENERATION` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | leaves adjacent pending images at their generation during a write-back, as before | From fc9c5416fce9d5440422503206de93372d5235e3 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 04:13:36 +0000 Subject: [PATCH 11/13] test(agc): check reused draw builds over a new multisampled target with watched imports jotitadev's macOS run of agc_driver_address_draw_reuse failed at its ThrowingProof case while imports stay watched: the fast proof found the sampled image's memory unchanged after MultisampleTargetView took it over, and the build was reused. Main's 67575aae8, merged in the parent, checks every ordinary sampled and storage range against the multisampled color targets before either proof, so the case is refused before the fast proof runs; no Linux test forced watched imports. agc_driver_address_draw_reuse_watched_imports runs the test with APS5_WRITE_WATCH_IMPORTS=watch where the switch exists (not on Windows), and the throwing proof also runs on a draw build without the address table, whose proof throws as its fresh build does. Without that check, the watched run fails at "an address-based draw build whose sampled image's memory became a multisampled target was reused", and both runs fail at the build without the address table. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +++ .../tests/execution/AddressDrawReuse.cpp | 28 +++++++++++++------ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 38bbf8564c4c..459e17270c87 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1874,6 +1874,10 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources) configure_windows_unwind(${agcExecutionTest}) list(APPEND agcExecutionTests ${agcExecutionTest}) endforeach() +if(BUILD_TESTING AND NOT WIN32) + add_test(NAME agc_driver_address_draw_reuse_watched_imports COMMAND agc_driver_address_draw_reuse_tests) + set_tests_properties(agc_driver_address_draw_reuse_watched_imports PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_WRITE_WATCH_IMPORTS=watch" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") +endif() if(BUILD_TESTING) add_test(NAME agc_driver_address_passes_targets_split COMMAND agc_driver_address_passes_tests --targets) set_tests_properties(agc_driver_address_passes_targets_split PROPERTIES ENVIRONMENT "APS5_NO_ADDRESS_READ_PASSES=1" SKIP_RETURN_CODE 77 TIMEOUT 60) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp index 71ea1d2023e7..f6e6638160f4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp @@ -597,7 +597,7 @@ void OversizedVertexBuffer(AgcDriver::VulkanDevice& device) { Require(Builds() == before, "address draw reuse: a draw whose vertex buffer descriptor reaches past its allocation built its resources again"); } -void ThrowingProof(AgcDriver::VulkanDevice& device) { +void ThrowingProof(AgcDriver::VulkanDevice& device, bool addressTable) { const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); if ((context.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0) { std::puts("address draw reuse: a throwing reuse proof not tested, the device has no 4x color targets"); @@ -629,12 +629,16 @@ void ThrowingProof(AgcDriver::VulkanDevice& device) { image.guestDescriptor = {static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (format32UInt << 20u) | (((Width - 1u) & 3u) << 30u), ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), 0xfacu | (type2D << 28u), 0u, 0u, 0u, 0u}; ShaderRecompiler::RecompileResult vertex; ShaderRecompiler::RecompileResult fragment; - fragment.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; - fragment.bindings = { - {ShaderRecompiler::DescriptorKind::StorageBuffer, ShaderRecompiler::DescriptorRole::BdaPagetable, 0, static_cast(ShaderRecompiler::RuntimeAbi::Binding::BdaPagetable), 1, {}, false}, - {ShaderRecompiler::DescriptorKind::StorageBuffer, ShaderRecompiler::DescriptorRole::FaultBuffer, 0, static_cast(ShaderRecompiler::RuntimeAbi::Binding::FaultBuffer), 1, {}, false}, - image, - }; + if (addressTable) { + fragment.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + fragment.bindings = { + {ShaderRecompiler::DescriptorKind::StorageBuffer, ShaderRecompiler::DescriptorRole::BdaPagetable, 0, static_cast(ShaderRecompiler::RuntimeAbi::Binding::BdaPagetable), 1, {}, false}, + {ShaderRecompiler::DescriptorKind::StorageBuffer, ShaderRecompiler::DescriptorRole::FaultBuffer, 0, static_cast(ShaderRecompiler::RuntimeAbi::Binding::FaultBuffer), 1, {}, false}, + image, + }; + } else { + fragment.bindings = {image}; + } const std::array stages{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::Fragment, &fragment, 0}}}; AgcDriver::Graphics::ColorTarget target{}; target.address = address; @@ -648,7 +652,7 @@ void ThrowingProof(AgcDriver::VulkanDevice& device) { target.samples = 4; std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); AgcDriver::Graphics::ShaderResources resources(context, stages, AgcDriver::Graphics::ColorTarget{}, 0, 0, 0, {}); - Require(resources.UsesAddressTable() && resources.Reusable(), "address draw reuse: an address-based draw build that samples an image is not reusable"); + Require(resources.UsesAddressTable() == addressTable && resources.Reusable(), addressTable ? "address draw reuse: an address-based draw build that samples an image is not reusable" : "address draw reuse: a draw build that samples an image without the address table is not reusable"); resources.EndUse(); Require(AgcDriver::Graphics::MultisampleTargetView(context, target) != VK_NULL_HANDLE, "address draw reuse: the multisampled target over the sampled image has no view"); device.SubmitRecorded(true); @@ -661,6 +665,11 @@ void ThrowingProof(AgcDriver::VulkanDevice& device) { } catch (const std::runtime_error&) { threw = true; } + if (!addressTable) { + Require(threw && !reused, "address draw reuse: a draw build without the address table whose sampled image's memory became a multisampled target was reused"); + resources.WriteBack(); + return; + } Require(!threw, "address draw reuse: the reuse proof of an address-based draw build whose sampled image's memory became a multisampled target threw instead of refusing the reuse"); Require(!reused, "address draw reuse: an address-based draw build whose sampled image's memory became a multisampled target was reused"); const auto refused = AgcDriver::Graphics::AddressReuseCounters(); @@ -789,7 +798,8 @@ int main(int argc, char** argv) { scene.Wait(); } OversizedVertexBuffer(*device); - ThrowingProof(*device); + ThrowingProof(*device, true); + ThrowingProof(*device, false); } DecodeCacheHit(); AgcDriverShutdown_nid_postfix(); From 7ee1eb9234707573b92fa8948534ae6300e7bfa1 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:42:21 -0700 Subject: [PATCH 12/13] fix(agc): size a moved read-only buffer at the render target as its build does Since #8224, a read-only strided buffer that runs less than one stride into the render target is bound only up to the target, and the cached build keeps that size. MovedReadOnlyBuffers compared it with the size without that cut, so a hit with the same words took the buffer as moved and rebound all of it, over the render target, and an address-based build holding such a buffer was never reused. GuestBufferBytes takes the target and applies the cut. The build, CheckBufferAliases and MovedReadOnlyBuffers pass the draw's target for read-only buffers, so all three size a buffer the same way. agc_driver_graphics checks a cached build whose read-only stride-60, 4-record buffer runs 8 bytes into the target: the same words list nothing moved, a buffer moved to run 4 bytes in is listed at 236 bytes, and one moved to run a whole stride in keeps its 240. --- .../Graphics/include/ShaderResources.hpp | 4 +-- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 7 +++-- .../Graphics/src/ShaderResources.cpp | 16 ++++++------ .../libSceAgcDriver/tests/BdaResources.cpp | 9 ++++--- .../prx/libSceAgcDriver/tests/Graphics.cpp | 26 +++++++++++++++++++ 5 files changed, 44 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 040f602d7fdd..ccbb314daaf5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -203,7 +203,7 @@ class ShaderResources { std::span words; }; std::shared_ptr PrepareDrawBindings(Recorder& recorder, std::span moved = {}, bool movedOnly = false) const; - std::optional> MovedReadOnlyBuffers(std::span shaders, Recorder& recorder) const; + std::optional> MovedReadOnlyBuffers(std::span shaders, Recorder& recorder, const ColorTarget* target) const; enum class MoveRefusal : std::size_t { None, Shape, NotGuest, Written, Limit, Inaccessible, Overlap, Bindings, Count }; struct MoveProof { MoveRefusal refusal = MoveRefusal::None; @@ -276,7 +276,7 @@ class ShaderResources { void KeepStorageImageBytes(Recorder& recorder) const; const std::vector& MultisampleSources() const { return multisampleSources; } static bool NeverReusable(std::span shaders); - static std::uint64_t GuestBufferBytes(const Context& context, std::span words); + static std::uint64_t GuestBufferBytes(const Context& context, std::span words, const ColorTarget* target); static std::uint64_t DepthGuardRefusals(); const std::vector>& SampledTextures() const { return textures; } static std::size_t LookupLogCapacity(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 79149a30c48d..e959c6a76abf 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -815,11 +815,10 @@ void CheckBufferAliases(const Context& context, std::span for (std::size_t offset = 0; offset + 4 <= words.size(); offset += 4) { const ShaderRecompiler::ShaderBufferResource descriptor{{words[offset], words[offset + 1], words[offset + 2], words[offset + 3]}}; const auto address = descriptor.Base48(); - auto size = ShaderResources::GuestBufferBytes(context, std::span(words).subspan(offset, 4)); - if (size == 0 || address == 0) continue; const auto element = offset / 4; const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; - if (const std::uint64_t stride = descriptor.Stride(); !written && stride != 0 && address < target.address && address + size > target.address && address + size - target.address < stride) size = target.address - address; + const auto size = ShaderResources::GuestBufferBytes(context, std::span(words).subspan(offset, 4), written ? nullptr : &target); + if (size == 0 || address == 0) continue; Require(!overlap(address, size, target.address, target.bytes), "shader buffer aliases the render target"); Require(!written || !overlap(address, size, indexAddress, indexBytes), "writable shader buffer aliases the index buffer"); } @@ -1460,7 +1459,7 @@ ResolvedResources resolveDrawResources(const Context& context, const State& stat const bool valid = cached->Revalidate(shaders, nullptr, snapshots); auto* recorder = Recorder::Active(); std::optional> moved; - if (valid && recorder != nullptr) moved = cached->MovedReadOnlyBuffers(shaders, *recorder); + if (valid && recorder != nullptr) moved = cached->MovedReadOnlyBuffers(shaders, *recorder, &state.color); if (moved.has_value()) { resolved.resources = std::move(cached); resolved.use.resources = resolved.resources; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index d06481cf1b40..b9c42a1229c8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -4205,7 +4205,7 @@ DescriptorCache::Stats DescriptorCache::Counters() const { return stats; } -std::uint64_t ShaderResources::GuestBufferBytes(const Context& context, std::span words) { +std::uint64_t ShaderResources::GuestBufferBytes(const Context& context, std::span words, const ColorTarget* target) { const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1] & ~0x40000000u, words[2], words[3]}}; const auto address = descriptor.Base48(); auto byteSize = descriptor.GetSize(); @@ -4223,12 +4223,13 @@ std::uint64_t ShaderResources::GuestBufferBytes(const Context& context, std::spa const auto allocationEnd = RegisteredReadableEnd(address); if (allocationEnd > address && allocationEnd < end && end - allocationEnd < stride) byteSize = allocationEnd - address; } + if (target != nullptr && address < target->address && address + byteSize > target->address && address + byteSize - target->address < stride) byteSize = target->address - address; } return byteSize; } std::uint64_t ShaderResources::guestBufferBytes(std::span words) const { - return GuestBufferBytes(context, words); + return GuestBufferBytes(context, words, nullptr); } std::size_t ShaderResources::addGuestBuffer(std::span words, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes, bool written, bool atomic, bool read, std::uint32_t extent) { @@ -4236,15 +4237,14 @@ std::size_t ShaderResources::addGuestBuffer(std::span words const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1] & ~0x40000000u, words[2], words[3]}}; Require(descriptor.Type() == 0u, "buffer descriptor uses an unsupported type"); const auto address = descriptor.Base48(); - const auto byteSize = guestBufferBytes(words); + const auto byteSize = GuestBufferBytes(context, words, written ? nullptr : target); if (byteSize == 0 || address == 0) { allocations.push_back({0, EmptyBufferBytes, false, nullptr, ShaderRecompiler::DescriptorRole::GuestBuffers, false}); return allocations.size() - 1; } Require(byteSize <= context.limits.maxStorageBufferRange, "shader buffer exceeds descriptor range limit"); Require(byteSize <= std::numeric_limits::max(), "shader buffer size exceeds host address space"); - auto size = static_cast(byteSize); - if (const std::uint64_t stride = descriptor.Stride(); !written && target != nullptr && stride != 0 && address < target->address && address + size > target->address && address + size - target->address < stride) size = static_cast(target->address - address); + const auto size = static_cast(byteSize); Require(target == nullptr || !overlap(address, size, target->address, target->bytes), "shader buffer aliases the render target"); // APS5_ALL_BUFFERS_WRITTEN=1: every element is noted as written, as before bufferWritten existed. static const bool allWritten = std::getenv("APS5_ALL_BUFFERS_WRITTEN") != nullptr; @@ -5341,7 +5341,7 @@ ShaderResources::DrawBindings::~DrawBindings() { release(fragmentLayout, fragmentAllocation); } -std::optional> ShaderResources::MovedReadOnlyBuffers(std::span shaders, Recorder& recorder) const { +std::optional> ShaderResources::MovedReadOnlyBuffers(std::span shaders, Recorder& recorder, const ColorTarget* target) const { std::vector moved; if (_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) return moved; for (const auto& shader : shaders) { @@ -5368,7 +5368,8 @@ std::optional> ShaderResources::MovedR const auto* words = binding.guestDescriptor.data() + static_cast(element) * 4u; const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}}; const auto address = descriptor.Base48(); - const auto size = GuestBufferBytes(context, std::span(words, 4)); + const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; + const auto size = GuestBufferBytes(context, std::span(words, 4), written ? nullptr : target); const auto index = kept->allocations[element]; const auto& item = allocations[index]; const bool empty = size == 0 || address == 0; @@ -5377,7 +5378,6 @@ std::optional> ShaderResources::MovedR return std::nullopt; } if (item.address == address && item.size == size) continue; - const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; if (written || empty || item.written || size > context.limits.maxStorageBufferRange) return std::nullopt; const auto begin = address - item.adjustment; const auto bytes = static_cast(GuestBufferMemory::ViewBytes(size, item.adjustment)); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index fb54fb34c9ba..2fe8cf6ae5f4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -3258,12 +3258,13 @@ void addressMovedBufferTests(const Context& context) { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); Recorder recorder(context); { - ShaderResources resources(context, built, ColorTarget{}, 0, 0, 0, {}); + const ColorTarget target{}; + ShaderResources resources(context, built, target, 0, 0, 0, {}); Require(resources.UsesAddressTable(), "a draw build with a BDA table stage does not use the BDA table"); - const auto unchanged = resources.MovedReadOnlyBuffers(built, recorder); + const auto unchanged = resources.MovedReadOnlyBuffers(built, recorder, &target); Require(unchanged.has_value() && unchanged->empty(), "an address-based draw build found a buffer moved in the stages it was built from"); - Require(!resources.MovedReadOnlyBuffers(movedStages, recorder).has_value(), "an address-based draw build would serve a draw whose read-only buffer moved, which it cannot bind per draw"); - Require(!resources.MovedReadOnlyBuffers(rewrittenStages, recorder).has_value(), "an address-based draw build would serve a draw with other data words, which it cannot bind per draw"); + Require(!resources.MovedReadOnlyBuffers(movedStages, recorder, &target).has_value(), "an address-based draw build would serve a draw whose read-only buffer moved, which it cannot bind per draw"); + Require(!resources.MovedReadOnlyBuffers(rewrittenStages, recorder, &target).has_value(), "an address-based draw build would serve a draw with other data words, which it cannot bind per draw"); resources.WriteBack(); } recorder.Sync(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index f9402df50f0b..6510797e66a6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -5796,6 +5796,32 @@ void resourceTests() { expectSingleAccepted(changed([&](auto& binding) { binding.guestDescriptor = stridedBefore(8); binding.bufferWritten = {false}; }), "a read-only strided V# whose last record runs 8 bytes into the render target"); expectSingleFailure(changed([&](auto& binding) { binding.guestDescriptor = stridedBefore(8); binding.bufferWritten = {true}; }), "aliases the render target"); expectSingleFailure(changed([&](auto& binding) { binding.guestDescriptor = stridedBefore(60); }), "aliases the render target"); + { + mock = MockVulkan{}; + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + AgcDriver::Graphics::Recorder recorder(context); + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + auto strided = changed([&](auto& binding) { binding.guestDescriptor = stridedBefore(8); }); + strided.bufferWritten = {false}; + vertex.bindings.push_back(strided); + AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, state.color, 0, 0); + const auto movedFor = [&](std::uint32_t past) { + auto drawn = vertex; + drawn.bindings[0].guestDescriptor = stridedBefore(past); + const std::array stages{{{ShaderRecompiler::ShaderStage::Vertex, &drawn, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; + return resources.MovedReadOnlyBuffers(stages, recorder, &state.color); + }; + const auto listed = [](const auto& moved) { return !moved.has_value() ? std::string("refused") : std::to_string(moved->size()) + " moved" + (moved->empty() ? std::string() : ", the first " + std::to_string(moved->front().size) + " bytes"); }; + auto moved = movedFor(8); + Require(moved.has_value() && moved->empty(), "a cached build whose strided buffer runs 8 bytes into the render target took it as moved for the same words and target (" + listed(moved) + ")"); + moved = movedFor(4); + Require(moved.has_value() && moved->size() == 1 && moved->front().address == state.color.address - 236u && moved->front().size == 236, "a strided buffer that moved to run 4 bytes into the render target was not listed at its size up to the target (" + listed(moved) + ")"); + moved = movedFor(60); + Require(moved.has_value() && moved->size() == 1 && moved->front().address == state.color.address - 180u && moved->front().size == 240, "a strided buffer that moved to run a whole stride into the render target was cut (" + listed(moved) + ")"); + recorder.Sync(); + } + Require(mock.live == 0, "the strided buffer's moved check leaked Vulkan objects"); expectSingleAccepted(changed([](auto& binding) { binding.count = 3; binding.guestDescriptor = join(join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)), vsharp(reinterpret_cast(0x1000), 8)); }), "an unmapped V# element"); expectSingleFailure(changed([&](auto& binding) { binding.count = 2; binding.guestDescriptor = join(vsharp(guestFirst.data(), 16), vsharp(reinterpret_cast(state.color.address), 64)); }), "aliases the render target"); expectSingleFailure(changed([](auto& binding) { binding.count = 17; binding.guestDescriptor.assign(68, 0); }), "per-stage limits"); From 7cef9bcf8f88c143cba8feeaee9fafef73199df7 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:45:55 -0700 Subject: [PATCH 13/13] docs(agc): cut the reused address-based draw builds entry to its gaps The entry described each proof the reuse makes. It now names where they are (ShaderResources::Revalidate) and keeps what stays open: one draw at a time per build, no reuse for dispatches and draw recipes, and no measurement in a title. It goes from 1,537 to 871 characters. --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index dce3932ad29b..ecf7e964ac57 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -206,7 +206,7 @@ - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less the larger of 2 GiB and half that heap (imports, type-3 mirrors and buffers and textures placed in host memory share it; on a 16 GiB machine whose host heap is half the memory, a 2 GiB reserve left too little for the rest). Where the device enables `VK_EXT_memory_budget`, that host heap's reported budget less its other usage and an eighth, in 64 MiB steps, read at most once a second, also bounds the default budget once an import's `vkAllocateMemory` has failed; a set `APS5_HOST_IMPORT_MIB` is never lowered by the report. The report's other usage includes the host-visible copies and heap mirrors that stand in for refused imports, so it grows with the refusals; the first time the report would lower the budget, `[gpu] host import budget: host heap ...` prints the heap's reported budget and usage and the live imports. An import's `vkAllocateMemory` failure within an eighth of the budget, or the third in a row, lowers the budget to the live imports until the device or the configured budget changes; an isolated failure further below only marks its range failed. Live imports are not capped by `maxMemoryAllocationCount`: NVIDIA's Windows driver reports 4096, while Village held 27,167 live imports without a failure. A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A heap mirror, sparse or not, whose range has left the write watch is refreshed in full at each use, every block counted as written; a build plans its range again only once the import path takes the range. A request that may not evict (an address-based build, an import when a range is mapped) remembers a refused range or block, with the live bytes and the budget at the refusal, and is answered from that memory, still counted as refused, until live imports drop below those bytes, the budget changes or one of its ranges changes. A request that may evict remembers its refusal too, answering later requests for the same run from memory with the room the refusal left (a request whose range fits in that room steps down to a smaller block without scanning the imports again, and a refusal that stepped down is remembered for each block it tried), while no import was made or retired, the budget has not grown, the import window and the run are unchanged, and no import can have become evictable since: the use count stays below the first use at which an import that was neither evictable nor held could leave the 256-lookup protection, and either the build floor has not risen or no held import could leave it yet. A join remembers its refusal the same way, apart from other requests for the same run, so a buffer bound across refused ranges does not try the join again at every upload. The shutdown line `[gpu] host imports at shutdown` gives the live, made, refused (and remembered) and failed counts per requester. The 64 MiB window, the 256-lookup protection, the 2 GiB reserve, the eighth, the 64 MiB steps and the three failures are choices, not measured. - [Periodic pipeline cache saves skip busy intervals](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp) (libSceAgcDriver) - the saver thread snapshots the Vulkan pipeline cache only when no pipeline creation holds it (`TryLockForSnapshot`); otherwise it skips that 10 s interval. A blocking snapshot queued behind a long creation (an NVIDIA compile of a 1.5M-word compute pipeline runs for minutes) makes the writer-preferring `shared_mutex` hold every later pipeline creation on every queue until that compile ends, which froze Yōtei's (PPSA26344) loading at frame 797-799 for the whole compile. While creations overlap without a gap, no periodic save happens, so a crash in that stretch loses the pipelines built since the last save; `SaveNow` and the teardown save still wait for the lock. How often busy stretches skip saves in practice is not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. -- [Reused address-based draw builds](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): a recorded draw whose stages reach guest memory through the BDA table reuses the build of an earlier draw with the same content (`APS5_NO_ADDRESS_DRAW_REUSE=1` builds each one) while the cached address space it was built over is the one a build would take now, its regions outside the space are imports or read-only image mirrors with unchanged serials or snapshot copies whose bytes the draw's snapshots repeat in the same order, the read-only buffers and data words its content key leaves out equal the build's (it cannot rebind them per draw), and the completion of its previous draw has run. Recorded GPU writes over a snapshot's range are completed before the snapshots are compared, so a capture made before such a write lands is refused rather than proven against the old bytes. Each draw takes the space for its batch, brings the space's mirrors up to date as a build does, and checks its faults and marks its written pages at its own completion; between draws the build pins no guest allocation. The build's fault buffer holds the fault and written-page records of one draw, so a draw that repeats it before that completion ran (two in one batch) builds anew and replaces it in the resource cache. Dispatches and draw recipes do not reuse address-based builds. Measured only in a synthetic draw on lavapipe; how often titles repeat a draw's content and snapshot list between batches is not measured. +- [Reused address-based draw builds](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): a draw whose stages reach guest memory through the BDA table reuses the build of an earlier draw with the same content while `ShaderResources::Revalidate`'s proofs hold (the same cached address space, unchanged import and mirror serials, the same snapshots and read-only buffers); `APS5_NO_ADDRESS_DRAW_REUSE=1` builds each one. The build's fault buffer holds one draw's fault and written-page records, so a draw that repeats it before the previous draw's completion ran (two in one batch) builds anew and replaces it in the resource cache. Dispatches and draw recipes do not reuse address-based builds. Measured only in a synthetic draw on lavapipe; how often titles repeat a draw's content and snapshot list between batches is not measured. - [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (1,024 at start, up to 16,384 after growth, or `APS5_RESOURCE_CACHE_ENTRIES`, each with its textures); that cost is not measured. When `ReleaseTextureCaches` runs on device memory exhaustion, the sampled textures these entries hold are only marked released and are freed at the next insert, so the immediate allocation retry gets no memory back from them. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured on one title on one RTX 4090: with the default bounds, sampled textures held outside the cache peaked at 2,836 MiB (7,830 MiB before) and device-local heap 0 stayed under its 22.9 GB budget for the whole run (26,357 MiB before, which aborted with `VK_ERROR_OUT_OF_DEVICE_MEMORY` at the wolf scene). Whether the extra descriptor-set builds cost frame time elsewhere is not measured. - [Sampled MSAA surface geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) supplies the fragment-aware linear size and one mip for a tiled 2D surface at two, four or eight fragments. Unsupported arrays, mip chains, extents and block sizes throw. Linear descriptors retain the existing geometry path, including its use when sampling an existing native target; a linear MSAA guest layout is not modeled. The range calculation follows AMD Addrlib's thin-block dimensions. Console layout equivalence and title effects of the additional mip metadata remain unmeasured. This does not add sample upload or readback.