diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 198d4a0865d3..a1f1b8319f1c 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1912,6 +1912,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/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index acc9d7ea2670..58f82bacece5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -500,6 +500,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe snapshots.clear(); snapshots.reserve(memory.size()); 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/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index ceec04cf3ecb..03820718a8ea 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -235,6 +235,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; @@ -365,9 +383,21 @@ 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; } void DeviceReads(std::vector>& out) const; private: + struct SnapshotUse { + std::uint64_t address; + std::size_t bytes; + bool live; + }; struct Region { std::uint64_t begin; std::uint64_t end; @@ -491,6 +521,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 c0d65d1f74b2..ccbb314daaf5 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 @@ -202,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; @@ -275,11 +276,17 @@ 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, const ColorTarget* target); static std::uint64_t DepthGuardRefusals(); const std::vector>& SampledTextures() const { return textures; } static std::size_t LookupLogCapacity(); 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; // `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 @@ -290,7 +297,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); } @@ -486,6 +493,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 @@ -624,6 +632,10 @@ 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; 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 2438c18aa223..c51ee59815c3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -465,6 +465,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"}; @@ -575,6 +577,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; @@ -795,7 +807,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) { @@ -804,11 +816,10 @@ 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(); - auto size = descriptor.GetSize(); - 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"); } @@ -1402,6 +1413,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 { @@ -1410,6 +1431,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) { @@ -1429,18 +1451,27 @@ 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); + 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; - if (valid && recorder != nullptr) moved = cached->MovedReadOnlyBuffers(shaders, *recorder); + if (valid && recorder != nullptr) moved = cached->MovedReadOnlyBuffers(shaders, *recorder, &state.color); 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(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 { SharedResourceCache().Remove(resolved.contentKey); @@ -1453,6 +1484,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; @@ -1686,8 +1718,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; @@ -2549,7 +2587,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 && !pipeline->ReordersSamples()) { + if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && !resources->UsesAddressTable() && resolved.moved.empty() && !multisampled && !pipeline->ReordersSamples()) { auto recipe = std::make_shared(); recipe->device = context.device; recipe->templateRef = resources; @@ -2918,7 +2956,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/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 610c43736952..6d13d02cd17a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -152,6 +152,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; @@ -1712,7 +1739,11 @@ 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 stepDown ? importAllocation(context, state, address, lease, requester, end, evict, bytes / 2) : nullptr; + if (held(it->second)) { + if (stepDown) return importAllocation(context, state, address, lease, requester, end, evict, bytes / 2); + countImportRefusal(&AddressReuseCounts::heldImportRefusals, &AddressReuseCounts::heldImportRefusalsHeld); + return nullptr; + } replaced += it->second.pinnedBytes; } const auto live = state.liveBytes - replaced; @@ -1744,6 +1775,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import if (evict) state.evictingRefusals.insert_or_assign(std::pair{base, bytes}, EvictingRefusal{base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end()), state.epoch, state.liveBytes, budget, room, importWindow.load(std::memory_order_relaxed), state.buildFloor, ready, readyHeld, joined}); if (stepDown && containing != run.ranges.end() && room >= (*containing)->bytes) return importAllocation(context, state, address, lease, requester, end, evict, std::min(bytes / 2, room)); if (containing != run.ranges.end() && run.ranges.size() > 1) remember((*containing)->address, (*containing)->bytes, GuestAllocations::Mapped{*containing}); + countImportRefusal(&AddressReuseCounts::budgetRefusals, &AddressReuseCounts::budgetRefusalsHeld); return nullptr; } for (auto it = first; it != state.imports.end() && it->first < base + bytes;) { @@ -2615,6 +2647,60 @@ bool importsNow(const Context& context, HostImports& state, const GuestAllocatio return context.hostImportAlignment != 0 && (importsStale(context, state) || importAllocation(context, state, address, lease, ImportRequester::Build, 0, false) != nullptr); } +bool spaceMirrorsKept(const Context& context, HostImports& state, const GuestBufferMemory::AddressSpace& space) { + const auto keeps = [&](ImageMirror* mirror) { return heapMirrorWatched(*mirror) || !importsNow(context, state, space.lease, mirror->base); }; + return std::all_of(space.heapMirrors.begin(), space.heapMirrors.end(), keeps) && std::all_of(space.sparseMirrors.begin(), space.sparseMirrors.end(), keeps); +} +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; + static const bool stampOnce = std::getenv("APS5_NO_SPACE_STAMP_ONCE") == nullptr; + auto& spaceUse = state.spaceUses[space.serial]; + if (!stampOnce || spaceUse == 0) { + for (const auto& region : space.base) { + if (region.direct == nullptr) continue; + region.direct->lastUse = stamp; + if (stampOnce) region.direct->spaceSerial = space.serial; + } + } + spaceUse = stamp; + if (state.spaceUses.size() > 64) { + const auto oldest = std::min_element(state.spaceUses.begin(), state.spaceUses.end(), [](const auto& left, const auto& right) { return left.second < right.second; }); + for (auto& [base, entry] : state.imports) { + if (entry.spaceSerial != oldest->first) continue; + entry.lastUse = std::max(entry.lastUse, oldest->second); + entry.spaceSerial = 0; + } + state.spaceUses.erase(oldest); + } +} + +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 @@ -2852,6 +2938,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) { const auto start = std::chrono::steady_clock::now(); bool synced = false; @@ -3053,6 +3144,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; @@ -3648,45 +3771,17 @@ void GuestBufferMemory::AcquireRegistered() { { auto& state = Imports(); std::lock_guard lock(state.mutex); - const auto keeps = [&](ImageMirror* mirror) { return heapMirrorWatched(*mirror) || !importsNow(context, state, current->lease, mirror->base); }; - 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 = std::all_of(current->heapMirrors.begin(), current->heapMirrors.end(), keeps) && std::all_of(current->sparseMirrors.begin(), current->sparseMirrors.end(), keeps); - importFloor = state.useCounter + 1; - if (hit) { - const auto stamp = ++state.useCounter; - static const bool stampOnce = std::getenv("APS5_NO_SPACE_STAMP_ONCE") == nullptr; - auto& spaceUse = state.spaceUses[current->serial]; - if (!stampOnce || spaceUse == 0) { - for (const auto& region : current->base) { - if (region.direct == nullptr) continue; - region.direct->lastUse = stamp; - if (stampOnce) region.direct->spaceSerial = current->serial; - } - } - spaceUse = stamp; - if (state.spaceUses.size() > 64) { - const auto oldest = std::min_element(state.spaceUses.begin(), state.spaceUses.end(), [](const auto& left, const auto& right) { return left.second < right.second; }); - for (auto& [base, entry] : state.imports) { - if (entry.spaceSerial != oldest->first) continue; - entry.lastUse = std::max(entry.lastUse, oldest->second); - entry.spaceSerial = 0; - } - state.spaceUses.erase(oldest); - } - } - } - 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 = spaceMirrorsKept(context, state, *current); 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); @@ -3700,10 +3795,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); } } @@ -4111,12 +4203,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; } @@ -4143,6 +4235,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; } @@ -5521,6 +5614,107 @@ 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) { + 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) { + 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; + } + if (!spaceMirrorsKept(context, state, *current)) { + countResumeMiss(SpaceResumeMiss::Mirrors); + return false; + } + stampSpaceImports(*current, state); + } + std::vector blocks; + if (!refreshSparseMirrors(current, blocks)) { + countResumeMiss(SpaceResumeMiss::Mirrors); + 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 277eea93c5d9..b9c42a1229c8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2489,17 +2489,36 @@ bool LoopGuardEnabled() { return enabled; } +bool AddressReuseEnabled() { + static const bool enabled = std::getenv("APS5_NO_ADDRESS_DRAW_REUSE") == nullptr; + 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() { // 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 (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 = 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 @@ -2518,7 +2537,9 @@ void ShaderResources::noteReusable() { region.committedEnd = cut.committedEnd; } } + if (addressBased) addressWrites = guestMemory.DirectWrites(); reusable = true; + addressReuse = addressBased; } std::uint64_t ShaderResources::DepthGuardRefusals() { @@ -2527,8 +2548,9 @@ std::uint64_t ShaderResources::DepthGuardRefusals() { 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); }); }); } @@ -3262,7 +3284,46 @@ 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); + 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); + CompleteSnapshotWrites(snapshots); + if (!guestMemory.SameSnapshots(snapshots)) return refuse(AddressReuse::Snapshots); + struct Suspend { + GuestBufferMemory& memory; + bool armed = true; + ~Suspend() { + if (armed) memory.SuspendSpace(); + } + } suspend{guestMemory}; + try { + 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); + } + } catch (const std::exception&) { + outcome.failure = ProofFailure::Other; + return refuse(AddressReuse::Threw); + } + suspend.armed = false; + reuseProved = true; + return true; +} + +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; @@ -4144,7 +4205,7 @@ DescriptorCache::Stats DescriptorCache::Counters() const { return stats; } -std::uint64_t ShaderResources::guestBufferBytes(std::span words) const { +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(); @@ -4162,24 +4223,28 @@ std::uint64_t ShaderResources::guestBufferBytes(std::span w 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, 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) { Require(words.size() == 4, "buffer descriptor must contain four DWORDs"); 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; @@ -5276,9 +5341,9 @@ 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) || 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) { @@ -5303,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 = descriptor.GetSize(); + 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; @@ -5312,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)); @@ -5323,6 +5388,7 @@ std::optional> ShaderResources::MovedR } } } + if (usesBda && !moved.empty()) return std::nullopt; return moved; } @@ -5628,6 +5694,45 @@ void ShaderResources::CheckFaults() const { if (bda) bda->CheckFault(); } +std::shared_ptr ShaderResources::BeginUse() { + if (!addressReuse) return {}; + struct Use { + 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(), std::exchange(reuseProved, false)); +} + +void ShaderResources::CompleteUse() { + if (!addressReuse) { + CheckFaults(); + return; + } + 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) return; + reuseProved = false; + guestMemory.SuspendSpace(); +} + void ShaderResources::WriteBackBuffers() { std::exception_ptr unaligned; if (bda) { diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index ceacffde725a..2fe8cf6ae5f4 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" @@ -158,6 +159,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}); @@ -3169,6 +3231,143 @@ 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); + { + 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, &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, &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(); +} + +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 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); + 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 { + 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"); + 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(); +} + void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { alignas(64) std::array guest{}; guest[0] = 123; @@ -3246,7 +3445,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)}; @@ -3392,6 +3592,10 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { importCrossingTests(context, access); addressTableKeyTests(); snapshotTableTests(context, 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 6aea082fffc2..f1b5c5f2edf3 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; std::function)> limitImports; }; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 4f7619a16330..84e8b696b0c7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -4429,6 +4429,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; @@ -4758,7 +4759,7 @@ VKAPI_ATTR VkResult VKAPI_CALL mockResetFences(VkDevice, std::uint32_t count, co VKAPI_ATTR VkResult VKAPI_CALL mockWaitForFences(VkDevice, std::uint32_t count, const VkFence* fences, VkBool32, std::uint64_t) { for (std::uint32_t index = 0; index < count; ++index) mock.signaled.at(fences[index]) = true; ++mock.fenceWaits; 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 mockCmdCopyBuffer(VkCommandBuffer, VkBuffer source, VkBuffer destination, std::uint32_t count, const VkBufferCopy* regions) { for (std::uint32_t index = 0; index < count; ++index) mock.bufferCopies.push_back({source, destination, regions[index]}); } @@ -5795,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"); @@ -10643,7 +10670,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"); @@ -10968,6 +10995,7 @@ int main(int argc, char** argv) { mock.memoryLimit = headroom.has_value() ? std::optional(mock.allocatedBytes + *headroom) : std::nullopt; }, [] { return mock.allocationAttempts; }, + [](VkResult result) { mock.submitResult = result; }, [](std::optional limit) { mock.importLimit = limit; } }); Require(mock.live == 0, "BDA resources leaked Vulkan objects"); 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..f6e6638160f4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AddressDrawReuse.cpp @@ -0,0 +1,813 @@ +#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" +#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/libSceAgcDriver/Submit/include/Dcb.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 +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +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, +}; + +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} +}}; + +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; +} + +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()}; + Pages vertices{BlockBytes}; + Pages guest{BlockBytes}; + Pages spare{BlockBytes}; + Pages extra; + std::size_t ranges; + std::vector vertexUserData; + std::vector pixelUserData; + std::array pixelProgram = PixelCode; + ShaderRecompiler::RecompileResult vertexResult; + ShaderRecompiler::RecompileResult pixelResult; + std::array shaders{}; + std::array snapshots{}; + AgcDriver::Graphics::State state{}; + + 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); + 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), 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, {}}, + {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(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}; + 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 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(); + 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(); + 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(); + 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 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 + 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(); + 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 + 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"); +} + +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"); +} + +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"); + 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; + 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; + 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() == 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); + 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&) { + 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(); + 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(); +} + +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; + } + Synthetic(scene, draws); + ReuseCounters(scene); + GuestWrite(scene); + PendingSnapshotWrite(scene); + Fault(scene); + Remap(scene); + scene.Wait(); + } + OversizedVertexBuffer(*device); + ThrowingProof(*device, true); + ThrowingProof(*device, false); + } + 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; + } +} diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 7d6f4d537b22..c6852e64bf6f 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -183,6 +183,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `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_MIMALLOC` | presence | `core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp` | | loads `mimalloc.dll` in the Windows entry stub, so the process allocates through mimalloc instead of the C runtime's heap | | `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 | diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 05682afa750a..186bbcb2e696 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -206,6 +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 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.