diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp index fdd4ff1baa09..611fc0f3c01f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -58,6 +59,13 @@ struct QueueWorker { std::thread thread; }; +struct ReferencedProgramScan { + std::vector addresses; + bool complete = true; +}; + +ReferencedProgramScan ReferencedPrograms(std::span commands, const std::map>& registerLists); + } #endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index 253d285864c0..7336b2a68a1a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -332,6 +332,7 @@ void PublishRegisteredShader(std::shared_ptr& registry, const st void SubmitRegistrationPreparation(std::function task, const PreparedShaders* prepared = nullptr); void PrioritizeRegistrationPreparation(PreparedShaders& prepared); void AwaitRegistrationPreparation(); +void AwaitReferencedPreparations(const ShaderRegistry& registry, std::span addresses); void SubmitLibraryPrebuild(std::function task, std::uint64_t program = 0); bool LibraryPrebuildsWaiting(); void PrioritizeLibraryPrebuilds(std::span programs); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp index 0551e52ba315..7d7e780febe0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -14,6 +14,7 @@ #include "ThreadOwned.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include +#include #include #include #include @@ -225,6 +226,60 @@ bool Driver::copySegment(Submission& submission, const std::uint32_t* guest, std return false; } +ReferencedProgramScan ReferencedPrograms(std::span commands, const std::map>& registerLists) { + constexpr std::array programRegisters{0x008u, 0x048u, 0x088u, 0x0c8u, 0x108u, 0x148u, 0x20cu}; + std::array values{}; + std::array written{}; + std::array bound{}; + ReferencedProgramScan scan; + const auto write = [&](std::uint32_t offset, std::uint32_t value, bool conditional) { + const auto base = std::ranges::find_if(programRegisters, [&](std::uint32_t program) { return offset - program < 2u; }); + if (base == programRegisters.end()) return; + if (conditional) { + scan.complete = false; + return; + } + const auto slot = static_cast(base - programRegisters.begin()) * 2u + (offset - *base); + values[slot] = value; + written[slot] = true; + const auto low = slot & ~std::size_t{1}; + if (written[low] && written[low + 1]) { + bound[low / 2] = true; + scan.addresses.push_back((static_cast(values[low]) << 8u) | (static_cast(values[low + 1] & 0xffu) << 40u)); + } + }; + std::size_t guardedEnd = 0; + for (std::size_t cursor = 0; cursor < commands.size(); cursor += Pm4::PacketWords(commands[cursor])) { + const auto header = commands[cursor]; + if ((header >> 30u) != 3u) continue; + const auto opcode = (header >> 8u) & 0xffu; + const auto words = std::min(Pm4::PacketWords(header), commands.size() - cursor); + const bool conditional = (header & 1u) != 0 || cursor < guardedEnd; + if (opcode == 0x22u && words == 5) guardedEnd = std::max(guardedEnd, cursor + words + Pm4::ConditionalWords(commands.subspan(cursor, words))); + if (Pm4::DrawOpcode(opcode) && !std::any_of(bound.begin(), bound.end() - 1, [](bool pair) { return pair; })) scan.complete = false; + if ((opcode == 0x15u || opcode == 0x16u) && !bound.back()) scan.complete = false; + if (opcode == 0x76u && words >= 3) { + const auto packet = commands.subspan(cursor, words); + const auto offset = packet[1] & ~0x70000000u; + for (std::size_t i = 2; i < words; ++i) write(offset + static_cast(i - 2), packet[i], conditional); + } else if (opcode == 0x63u) { + const auto list = registerLists.find(cursor); + if (list == registerLists.end()) continue; + if (!Pm4::IndirectRegistersValid(list->second)) { + scan.complete = false; + continue; + } + for (std::size_t i = 0; i + 1 < list->second.size(); i += 2) write(list->second[i] & ~0x70000000u, list->second[i + 1], conditional); + } + } + for (std::size_t slot = 0; slot < written.size(); slot += 2) { + if (written[slot] != written[slot + 1]) scan.complete = false; + } + std::ranges::sort(scan.addresses); + scan.addresses.erase(std::unique(scan.addresses.begin(), scan.addresses.end()), scan.addresses.end()); + return scan; +} + void Driver::readRegisterLists(Submission& submission) { submission.registerLists.clear(); for (std::size_t cursor = 0; cursor < submission.commands.size(); cursor += Pm4::PacketWords(submission.commands[cursor])) { @@ -511,7 +566,17 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue, bool cpuFlip) { if (APS5_ENABLE_TIMING_LOG) submission.validatedAt = std::chrono::steady_clock::now(); waitForFlipRoom(submission); holdFlipBehindWorker(submission, cpuFlip); - AwaitRegistrationPreparation(); + const auto scan = ReferencedPrograms(submission.commands, submission.registerLists); + if (!scan.complete || submission.rewindTail != nullptr) { + AwaitRegistrationPreparation(); + } else { + std::shared_ptr registry; + { + std::lock_guard lock(mutex); + registry = shaders; + } + if (registry != nullptr) AwaitReferencedPreparations(*registry, scan.addresses); + } if (LibraryPrebuildsWaiting()) PrioritizeLibraryPrebuilds(BoundGraphicsPrograms(submission)); if (APS5_ENABLE_TIMING_LOG) submission.roomReadyAt = std::chrono::steady_clock::now(); static const bool trace = std::getenv("APS5_TRACE_GPU") != nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 5c42933ab942..d042b4a9af34 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -52,6 +52,7 @@ #include #include #include +#include #include "Optimization/ShaderStageInputInfo.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/RegistrationPreparationBudget.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" @@ -1640,6 +1641,7 @@ class RegistrationPreparePool { else tasks.push_back({std::move(task), prepared, sequence, budgeted, program}); if (program != 0) waitingPrograms.fetch_add(1, std::memory_order_relaxed); unfinished.insert(sequence); + if (prepared != nullptr) owned.emplace(prepared, sequence); lock.unlock(); ready.notify_one(); return; @@ -1650,10 +1652,17 @@ class RegistrationPreparePool { void Prioritize(const PreparedShaders& prepared) { std::lock_guard lock(mutex); - const auto found = std::ranges::find_if(tasks, [&](const auto& task) { return task.prepared == &prepared; }); - if (found == tasks.end()) return; - if (std::getenv("APS5_TRACE_PREPARATION_WAIT") != nullptr) std::fprintf(stderr, "[shader-prepare] demanded queued task at position %zu of %zu\n", static_cast(std::distance(tasks.begin(), found)), tasks.size()); - std::rotate(tasks.begin(), found, std::next(found)); + moveToFront(prepared); + } + + void AwaitOwned(const PreparedShaders& prepared) { + std::unique_lock lock(mutex); + const auto [first, last] = owned.equal_range(&prepared); + if (first == last) return; + std::vector sequences; + for (auto entry = first; entry != last; ++entry) sequences.push_back(entry->second); + moveToFront(prepared); + idle.wait(lock, [&] { return std::ranges::none_of(sequences, [&](const auto sequence) { return unfinished.contains(sequence); }); }); } void Prioritize(std::span programs) { @@ -1681,7 +1690,10 @@ class RegistrationPreparePool { if (dropQueued) { dropped.swap(tasks); waitingPrograms.store(0, std::memory_order_relaxed); - for (const auto& task : dropped) unfinished.erase(task.sequence); + for (const auto& task : dropped) { + unfinished.erase(task.sequence); + disown(task.prepared, task.sequence); + } idle.notify_all(); } idle.wait(lock, [&] { return tasks.empty() && running == 0; }); @@ -1694,6 +1706,20 @@ class RegistrationPreparePool { private: static constexpr std::size_t StackBytes = 64u << 20u; + void moveToFront(const PreparedShaders& prepared) { + const auto found = std::ranges::find_if(tasks, [&](const auto& task) { return task.prepared == &prepared; }); + if (found == tasks.end()) return; + if (std::getenv("APS5_TRACE_PREPARATION_WAIT") != nullptr) std::fprintf(stderr, "[shader-prepare] demanded queued task at position %zu of %zu\n", static_cast(std::distance(tasks.begin(), found)), tasks.size()); + std::rotate(tasks.begin(), found, std::next(found)); + } + + void disown(const PreparedShaders* prepared, std::uint64_t sequence) { + if (prepared == nullptr) return; + const auto [first, last] = owned.equal_range(prepared); + const auto found = std::find_if(first, last, [&](const auto& entry) { return entry.second == sequence; }); + if (found != last) owned.erase(found); + } + #ifdef _WIN32 struct Worker { HANDLE handle; @@ -1771,6 +1797,7 @@ class RegistrationPreparePool { } const auto sequence = task.sequence; const bool budgeted = task.budgeted; + const auto* const prepared = task.prepared; task.function(); task = {}; bool drained = false; @@ -1779,6 +1806,7 @@ class RegistrationPreparePool { --pool.running; if (budgeted) --pool.budgetedRunning; pool.unfinished.erase(sequence); + pool.disown(prepared, sequence); drained = pool.tasks.empty() && pool.running == 0; pool.idle.notify_all(); } @@ -1811,6 +1839,7 @@ class RegistrationPreparePool { unsigned budgetedLimit = 1; std::uint64_t submitted = 0; std::set unfinished; + std::unordered_multimap owned; bool closed = false; }; @@ -1887,6 +1916,24 @@ void AwaitRegistrationPreparation() { if (auto* pool = RegistrationPreparePool::Existing()) pool->AwaitSubmitted(); } +void AwaitReferencedPreparations(const ShaderRegistry& registry, std::span addresses) { + for (const auto address : addresses) { + auto found = registry.upper_bound(address); + if (found == registry.begin()) continue; + --found; + for (const auto& snapshot : found->second) { + if (address - snapshot->codeAddress >= snapshot->code.size() * sizeof(std::uint32_t)) continue; + auto& prepared = *snapshot->prepared; + { + std::lock_guard lock(prepared.mutex); + if (!prepared.pending) continue; + } + prepared.demanded.store(true); + if (auto* pool = RegistrationPreparePool::Existing()) pool->AwaitOwned(prepared); + } + } +} + void SubmitLibraryPrebuild(std::function task, std::uint64_t program) { RegistrationPreparePool::Prebuild().Submit(std::move(task), nullptr, false, program); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index f02dadb11991..818ea2d2a4f7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -3,6 +3,7 @@ #include "ControlFlow/RequestSerializer.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -18,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -28,7 +30,10 @@ #include #include #include +#include +#include #include +#include #include #include #include @@ -472,6 +477,194 @@ void AwaitedRegistrationPreparation() { Require(fast.load(std::memory_order_acquire), "the later preparation did not run"); } +void ReferencedRegistrationPreparation() { + using AgcDriver::DriverDetail::ShaderSnapshot; + alignas(256) static const std::array boundCode{0xbf810000u}; + alignas(256) static const std::array unrelatedCode{0xbf810000u}; + const auto bound = static_cast(reinterpret_cast(boundCode.data())); + const auto unrelated = static_cast(reinterpret_cast(unrelatedCode.data())); + const auto boundSnapshot = std::make_shared(ShaderSnapshot{bound, 0, 0, {boundCode.begin(), boundCode.end()}, {}}); + const auto unrelatedSnapshot = std::make_shared(ShaderSnapshot{unrelated, 0, 0, {unrelatedCode.begin(), unrelatedCode.end()}, {}}); + AgcDriver::DriverDetail::ShaderRegistry registry; + registry[bound].push_back(boundSnapshot); + registry[unrelated].push_back(unrelatedSnapshot); + const std::vector commands{0xc0027600u, 0x20cu, static_cast(bound >> 8u), static_cast(bound >> 40u)}; + Require(AgcDriver::DriverDetail::ReferencedPrograms(commands, {}).addresses == std::vector{bound}, "a compute program bound with SET_SH_REG was not found in the submission"); + const std::vector indirect{0xc0036300u, 0, 0, 0, 2}; + const std::map> lists{{0, {0xc8u, static_cast(unrelated >> 8u), 0xc9u, static_cast(unrelated >> 40u)}}}; + Require(AgcDriver::DriverDetail::ReferencedPrograms(indirect, lists).addresses == std::vector{unrelated}, "a vertex program bound through an indirect register list was not found in the submission"); + const std::vector lowOnly{0xc0017600u, 0x008u, static_cast(bound >> 8u)}; + Require(AgcDriver::DriverDetail::ReferencedPrograms(lowOnly, {}).addresses.empty(), "a program address without its high register was resolved"); + Require(!AgcDriver::DriverDetail::ReferencedPrograms(lowOnly, {}).complete, "a program address without its high register was reported as fully scanned"); + Require(AgcDriver::DriverDetail::ReferencedPrograms(commands, {}).complete, "a program address written as a pair was reported as not fully scanned"); + const auto workers = std::max(2u, std::thread::hardware_concurrency() / 2u); + std::barrier started(static_cast(workers + 1u)); + std::counting_semaphore<> release(0); + std::latch finished(static_cast(workers)); + for (unsigned index = 0; index < workers; ++index) { + AgcDriver::DriverDetail::SubmitRegistrationPreparation([&] { + started.arrive_and_wait(); + release.acquire(); + finished.count_down(); + }); + } + started.arrive_and_wait(); + const auto pending = [](const std::shared_ptr& snapshot) { + std::lock_guard lock(snapshot->prepared->mutex); + return snapshot->prepared->pending; + }; + const auto settle = [](const std::shared_ptr& snapshot) { + { + std::lock_guard lock(snapshot->prepared->mutex); + snapshot->prepared->pending = false; + } + snapshot->prepared->settled.notify_all(); + }; + boundSnapshot->prepared->pending = true; + unrelatedSnapshot->prepared->pending = true; + std::atomic order{0}; + std::atomic boundOrder{~0u}; + std::atomic unrelatedOrder{~0u}; + std::counting_semaphore<> unrelatedGate(0); + AgcDriver::DriverDetail::SubmitRegistrationPreparation([unrelatedSnapshot, settle, &order, &unrelatedOrder, &unrelatedGate] { + unrelatedOrder.store(order.fetch_add(1u)); + static_cast(unrelatedGate.try_acquire_for(std::chrono::seconds(5))); + settle(unrelatedSnapshot); + }, unrelatedSnapshot->prepared.get()); + AgcDriver::DriverDetail::SubmitRegistrationPreparation([boundSnapshot, settle, &order, &boundOrder] { + boundOrder.store(order.fetch_add(1u)); + settle(boundSnapshot); + }, boundSnapshot->prepared.get()); + auto releaser = std::async(std::launch::async, [&] { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!boundSnapshot->prepared->demanded.load() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + release.release(); + }); + AgcDriver::DriverDetail::AwaitReferencedPreparations(registry, AgcDriver::DriverDetail::ReferencedPrograms(commands, {}).addresses); + const bool boundPending = pending(boundSnapshot); + const bool unrelatedPending = pending(unrelatedSnapshot); + const auto boundRank = boundOrder.load(); + unrelatedGate.release(); + releaser.get(); + release.release(static_cast(workers - 1u)); + const std::array unrelatedAddress{unrelated}; + AgcDriver::DriverDetail::AwaitReferencedPreparations(registry, unrelatedAddress); + const bool unrelatedSettled = !pending(unrelatedSnapshot); + finished.wait(); + Require(!boundPending, "a submission did not wait for the preparation of a program its commands bind"); + Require(boundRank == 0u, "the queued preparation of a bound program was not moved ahead of unrelated queued work"); + Require(unrelatedPending, "a preparation of a program the commands do not bind held the submission back"); + Require(unrelatedSettled && unrelatedOrder.load() == 1u, "a submission binding the queued program did not wait for its preparation"); +} + +std::chrono::milliseconds SubmitBehindSlowPreparation(std::vector& commands, bool& preparationDone) { + std::atomic ran{false}; + AgcDriver::DriverDetail::SubmitRegistrationPreparation([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); + ran.store(true, std::memory_order_release); + }); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + const auto began = std::chrono::steady_clock::now(); + Require(sceAgcDriverSubmitDcb(&packet) == 0, "a submission behind a slow preparation failed"); + const auto waited = std::chrono::duration_cast(std::chrono::steady_clock::now() - began); + preparationDone = ran.load(std::memory_order_acquire); + AgcDriverWaitIdle_nid_postfix(); + while (!ran.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(10)); + return waited; +} + +void UnprovenProgramsAtSubmit() { + alignas(256) static const std::array code{0xbf810000u}; + const auto address = static_cast(reinterpret_cast(code.data())); + const auto low = static_cast(address >> 8u); + const auto high = static_cast(address >> 40u); + const std::vector lowOnly{0xc0017600u, 0x20cu, low}; + const std::vector highOnly{0xc0017600u, 0x20du, high}; + const std::vector pair{0xc0027600u, 0x20cu, low, high}; + Require(!AgcDriver::DriverDetail::ReferencedPrograms(lowOnly, {}).complete, "a low address register alone was reported as fully scanned"); + Require(!AgcDriver::DriverDetail::ReferencedPrograms(highOnly, {}).complete, "a high address register alone was reported as fully scanned"); + Require(AgcDriver::DriverDetail::ReferencedPrograms(pair, {}).complete, "a register pair was reported as not fully scanned"); + const std::vector split{0xc0017600u, 0x20cu, low, 0xc0017600u, 0x20du, high}; + Require(AgcDriver::DriverDetail::ReferencedPrograms(split, {}).complete, "a register pair written by two packets was reported as not fully scanned"); + std::vector commands = lowOnly; + bool done = false; + auto waited = SubmitBehindSlowPreparation(commands, done); + std::cout << "unproven low-only submit waited " << waited.count() << " ms\n"; + Require(done, "a submission writing half of a program address did not wait for the queued preparations"); + commands = highOnly; + waited = SubmitBehindSlowPreparation(commands, done); + std::cout << "unproven high-only submit waited " << waited.count() << " ms\n"; + Require(done, "a submission writing the other half of a program address did not wait for the queued preparations"); + alignas(4) static std::array buffer{}; + std::ranges::copy(lowOnly, buffer.begin()); + const auto target = static_cast(reinterpret_cast(buffer.data())); + commands = {0xc0023f00u, static_cast(target), static_cast(target >> 32u) & 0xffffu, static_cast(buffer.size())}; + waited = SubmitBehindSlowPreparation(commands, done); + std::cout << "unproven low-only submit through an indirect buffer waited " << waited.count() << " ms\n"; + Require(done, "a submission writing half of a program address inside an indirect buffer did not wait for the queued preparations"); + const std::vector dispatch{0xc0031500u, 0, 0, 0, 0}; + const std::vector draw{0xc0012d00u, 0, 0}; + const auto joined = [](std::initializer_list*> parts) { + std::vector all; + for (const auto* part : parts) all.insert(all.end(), part->begin(), part->end()); + return all; + }; + const std::vector graphicsPair{0xc0027600u, 0x008u, low, high}; + Require(!AgcDriver::DriverDetail::ReferencedPrograms(dispatch, {}).complete, "a dispatch that binds no program in its submission was reported as fully scanned"); + Require(AgcDriver::DriverDetail::ReferencedPrograms(joined({&pair, &dispatch}), {}).complete, "a dispatch after its program pair was reported as not fully scanned"); + Require(!AgcDriver::DriverDetail::ReferencedPrograms(joined({&dispatch, &pair}), {}).complete, "a dispatch before its program pair was reported as fully scanned"); + Require(!AgcDriver::DriverDetail::ReferencedPrograms(draw, {}).complete, "a draw that binds no program in its submission was reported as fully scanned"); + Require(AgcDriver::DriverDetail::ReferencedPrograms(joined({&graphicsPair, &draw}), {}).complete, "a draw after a graphics program pair was reported as not fully scanned"); + Require(!AgcDriver::DriverDetail::ReferencedPrograms(joined({&pair, &draw}), {}).complete, "a draw after only a compute program pair was reported as fully scanned"); + const std::vector indirectList{0xc0036300u, 0, 0, 0, 1}; + const auto vertexAddress = address ^ std::uint64_t{0x100}; + const std::vector vertexPair{0xc0027600u, 0x048u, low ^ 1u, high}; + auto expectedPrograms = std::vector{address, vertexAddress}; + std::ranges::sort(expectedPrograms); + const auto mixedGraphics = AgcDriver::DriverDetail::ReferencedPrograms(joined({&graphicsPair, &vertexPair, &draw}), {}); + Require(mixedGraphics.complete && mixedGraphics.addresses == expectedPrograms, "a pixel and classic vertex program pair did not both reach the submit preparation wait"); + const auto vertexOnly = AgcDriver::DriverDetail::ReferencedPrograms(joined({&vertexPair, &draw}), {}); + Require(vertexOnly.complete && vertexOnly.addresses == std::vector{vertexAddress}, "a classic vertex program pair was not fully scanned"); + const std::vector vertexLowOnly{0xc0017600u, 0x048u, low ^ 1u}; + Require(!AgcDriver::DriverDetail::ReferencedPrograms(joined({&graphicsPair, &vertexLowOnly, &draw}), {}).complete, "an incomplete classic vertex pair was hidden by a complete pixel pair"); + const std::vector indirectGraphics{0xc0036300u, 0, 0, 0, 4}; + const std::map> graphicsRegisters{{0, {0x008u, low, 0x009u, high, 0x048u, low ^ 1u, 0x049u, high}}}; + const auto indirectPrograms = AgcDriver::DriverDetail::ReferencedPrograms(joined({&indirectGraphics, &draw}), graphicsRegisters); + Require(indirectPrograms.complete && indirectPrograms.addresses == expectedPrograms, "an indirect list lost its classic vertex program pair"); + const std::map> invalidList{{0, {0xffffffffu, 0}}}; + const std::map> validList{{0, {0x20cu, low}}}; + Require(!AgcDriver::DriverDetail::ReferencedPrograms(indirectList, invalidList).complete, "an indirect register list the worker reads again was reported as fully scanned"); + Require(AgcDriver::DriverDetail::ReferencedPrograms(indirectList, validList).addresses.empty(), "a lone indirect register was resolved to a program"); + const std::vector predicatedPair{0xc0027601u, 0x20cu, low, high}; + const std::vector conditionalRange{0xc0032200u, 0, 0, 0, 4}; + Require(!AgcDriver::DriverDetail::ReferencedPrograms(predicatedPair, {}).complete, "a predicated program pair was reported as fully scanned"); + Require(!AgcDriver::DriverDetail::ReferencedPrograms(joined({&conditionalRange, &pair}), {}).complete, "a program pair inside a conditional execution range was reported as fully scanned"); + Require(AgcDriver::DriverDetail::ReferencedPrograms(joined({&conditionalRange, &pair}), {}).addresses.empty(), "a program pair inside a conditional execution range was resolved to a program"); + const std::vector unrelatedWrite{0xc0017600u, 0x2e0u, 1}; + const std::vector shortRange{0xc0032200u, 0, 0, 0, 3}; + Require(AgcDriver::DriverDetail::ReferencedPrograms(joined({&shortRange, &unrelatedWrite, &pair}), {}).complete, "a pair after a conditional execution range holding another register was reported as not fully scanned"); + alignas(4) static std::array rewound{0xc0005900u, 0, 0xc0001000u, 0}; + { + std::atomic ran{false}; + AgcDriver::DriverDetail::SubmitRegistrationPreparation([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); + ran.store(true, std::memory_order_release); + }); + Packet packet{rewound.data(), static_cast(rewound.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "a submission ending in a REWIND failed"); + const bool rewindWaited = ran.load(std::memory_order_acquire); + std::atomic_ref(rewound[1]).store(0x80000000u, std::memory_order_release); + AgcDriverWaitIdle_nid_postfix(); + while (!ran.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(10)); + Require(rewindWaited, "a submission ending in a REWIND did not wait for the queued preparations, though its tail is not scanned"); + } + commands = pair; + waited = SubmitBehindSlowPreparation(commands, done); + std::cout << "proven pair submit waited " << waited.count() << " ms\n"; + Require(!done, "a submission writing a whole program address waited for an unrelated preparation"); +} + void PendingRegistrationPreparation(AgcDriver::VulkanDevice& device) { alignas(256) const std::array code{0xbf810000u}; std::array users{}; @@ -1702,12 +1895,14 @@ int main(int argc, char** argv) { PendingRegistrationPreparation(*device); PendingPreparationOutsideTransactionLock(); AwaitedRegistrationPreparation(); + ReferencedRegistrationPreparation(); Require(argc != 2 || std::string_view(argv[1]) != "--fail-before-registration", "injected failure before registration"); PrepareMultisampledStorage(*device); device.reset(); SharedCodeHeaders(); RelocatedHeaders(); CommittedAbis(); + UnprovenProgramsAtSubmit(); RegistrationWithoutSpecials(); Registration(argc == 2); std::cout << "prepared shader and transactional registration tests passed\n"; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index b422a4b8d3fa..42ce2834afd7 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -357,7 +357,7 @@ - [Committed reads of released pages](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) - `ReadCommitted` and `EqualsCommitted` find the readable pages of a range first and read them afterwards. On Windows they read through `ReadProcessMemory`, so a page released in between reads as zero and is left out of the comparison instead of faulting: Demon's Souls (PPSA01342) samples a texture at 0x38400 of 352 MiB while it loads the tutorial level, a range that holds no guest allocation and on Windows covers host heap pages other threads release, which stopped it with an access violation. A texture outside every registered guest allocation still takes the host bytes found there, as it did before; what the console returns for it is not measured. On Linux the pages are copied directly, so a page released between the check and the copy still faults. - [Flip submissions hold the next flip until their work completed](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - a queue-0 submission that carries a flip in its command buffer returns only after every earlier flip submission ran to its end and its recorded GPU work completed and was reaped, so the labels a frame writes after its flip are in memory when the title reads them after its next submission. A CPU flip (`sceVideoOutSubmitFlip`) does not wait, and the next flip is still released before the previous one is presented. Flips raise no graphics end-of-pipe event (from the PS5 13.60 kernel: a flip's interrupt context is always typed and goes to the display path, never to this event). Not measured: how far the console lets a title run ahead of frames its GPU has not finished. - [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. +- [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits only for pool-held preparations of programs it binds (`*_PGM_LO/HI` by `SET_SH_REG` or `SET_SH_REG_INDIRECT`). It waits for all queued ones if a pair is half written, a draw (dispatch) has no graphics (compute) pair written before it, a list has an invalid register, a predicated or `COND_EXEC`-guarded write sets a program register, or it ends in a REWIND. Not covered: a program re-registered at an address an earlier submission bound, used without rebinding while this one writes another graphics pair; its first draw waits in the queue worker, unbounded (74 s for the largest shader seen). Longest submit wait on Ghost of Yōtei, cold: 3.2 s, 7 s total over 900 s (RTX 4090). Goat Simulator 3 (PPSA04159)'s 10 s margin and how often titles write half a pair are 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. - [Host import refresh](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): after a change to the guest allocation registry, an import is checked again only when its import window, or its own range when larger, touches a range that changed since the last check, or when it was made since then; every import is checked when the window is 0 or the window or the budget changed. An unchanged import whose run could now be larger, because a remembered refusal no longer holds, is kept until its window changes or a request for the larger run retires it; before, any registry change anywhere retired it. Remembered failures and refusals are dropped the same way and stay checked where they are used.