From beaf0279aa73bdd388d7e6747d5903e388fc55ce Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 27 Oct 2026 22:57:31 +0000 Subject: [PATCH 1/5] fix(agc): bound the SPIR-V that background compute pipeline builds compile at once Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 11 +++ .../Execution/include/CompileBudget.hpp | 65 ++++++++++++++++ .../Execution/src/VulkanDevice.cpp | 4 + .../libSceAgcDriver/tests/CompileBudget.cpp | 78 +++++++++++++++++++ docs/dev/ENVIRONMENT.md | 1 + 5 files changed, 159 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/CompileBudget.hpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/CompileBudget.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 49a6952558cc..d90eeca1e00f 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -594,6 +594,17 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(agc_compile_budget_tests tests/CompileBudget.cpp) + target_include_directories(agc_compile_budget_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_compile_budget_tests PRIVATE libc) + configure_windows_unwind(agc_compile_budget_tests) + add_test(NAME agc_compile_budget COMMAND agc_compile_budget_tests) + set_tests_properties(agc_compile_budget PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_compile_budget PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(agc_capture_trace_tests tests/CaptureTrace.cpp) target_include_directories(agc_capture_trace_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_capture_trace_tests PRIVATE libc) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/CompileBudget.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/CompileBudget.hpp new file mode 100644 index 000000000000..6cabdf78a2d5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/CompileBudget.hpp @@ -0,0 +1,65 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_COMPILEBUDGET_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_COMPILEBUDGET_HPP + +#include +#include +#include +#include + +namespace AgcDriver { + +class CompileBudget { +public: + class Hold { + public: + Hold(CompileBudget& budget, std::uint64_t bytes) : budget(budget), bytes(bytes) { + std::unique_lock lock(budget.mutex); + budget.released.wait(lock, [&] { return budget.held == 0 || budget.held + bytes <= budget.limit; }); + budget.held += bytes; + if (budget.held > budget.peak) budget.peak = budget.held; + } + + ~Hold() { + { + std::lock_guard lock(budget.mutex); + budget.held -= bytes; + } + budget.released.notify_all(); + } + + Hold(const Hold&) = delete; + Hold& operator=(const Hold&) = delete; + + private: + CompileBudget& budget; + std::uint64_t bytes; + }; + + explicit CompileBudget(std::uint64_t limit) : limit(limit) {} + + std::uint64_t Peak() const { + std::lock_guard lock(mutex); + return peak; + } + +private: + mutable std::mutex mutex; + std::condition_variable released; + std::uint64_t limit; + std::uint64_t held = 0; + std::uint64_t peak = 0; +}; + +inline std::uint64_t BackgroundCompileLimitFor(const char* configured) { + constexpr std::uint64_t automaticKiB = 4096; + return (configured != nullptr ? std::strtoull(configured, nullptr, 10) : automaticKiB) * 1024u; +} + +inline CompileBudget& BackgroundCompileBudget() { + static CompileBudget budget(BackgroundCompileLimitFor(std::getenv("APS5_BACKGROUND_COMPILE_KIB"))); + return budget; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 150654934e03..691e717dbdc4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" +#include "prx/libSceAgcDriver/Execution/include/CompileBudget.hpp" #include "BdaAbi.hpp" #if ANYPS5_ENABLE_SPIRV_TOOLS #include "SpirvBackend/SpirvOptimizer.hpp" @@ -2329,6 +2330,7 @@ bool VulkanDevice::ReplayComputePipeline(const ShaderRecompiler::RecompileResult std::lock_guard pipelines(state->computePipelinesMutex); if (state->computePipelines.contains(key) || state->computePipelinesBuilding.contains(key)) return false; } + const CompileBudget::Hold hold(BackgroundCompileBudget(), shader.spirv.size() * sizeof(std::uint32_t)); const auto context = graphicsContext(); const std::array shaders{{{ShaderRecompiler::ShaderStage::Compute, &shader, 0}}}; const auto layouts = Graphics::DescribeSetLayouts(context, shaders); @@ -2353,6 +2355,8 @@ void VulkanDevice::PrebuildComputePipeline(const ShaderRecompiler::RecompileResu const auto pushStages = Graphics::PushConstantStages(shaders); if (pushStages != 0 && state->properties.limits.maxPushConstantsSize < Graphics::ComputePushConstantBytes) return; if (HasComputePipeline(shader)) return; + const CompileBudget::Hold hold(BackgroundCompileBudget(), shader.spirv.size() * sizeof(std::uint32_t)); + if (HasComputePipeline(shader)) return; const auto layouts = Graphics::DescribeSetLayouts(context, shaders); static_cast(computePipeline(context, shader, layouts.layouts.main, layouts.mainKey, pushStages, programAddress)); } diff --git a/core/libs/prx/libSceAgcDriver/tests/CompileBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/CompileBudget.cpp new file mode 100644 index 000000000000..6f171f8e4ce1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/CompileBudget.cpp @@ -0,0 +1,78 @@ +#include "prx/libSceAgcDriver/Execution/include/CompileBudget.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace std::chrono_literals; +using AgcDriver::BackgroundCompileLimitFor; +using AgcDriver::CompileBudget; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +void CheckLimitPolicy() { + Require(BackgroundCompileLimitFor(nullptr) == 4096u * 1024u, "an unset APS5_BACKGROUND_COMPILE_KIB is not 4 MiB"); + Require(BackgroundCompileLimitFor("16") == 16u * 1024u, "APS5_BACKGROUND_COMPILE_KIB=16 is not 16 KiB"); + Require(BackgroundCompileLimitFor("0") == 0u, "APS5_BACKGROUND_COMPILE_KIB=0 leaves room"); +} + +std::uint64_t MostHeldAtOnce(CompileBudget& budget, const std::vector& sizes) { + std::atomic held{0}; + std::atomic most{0}; + std::vector threads; + for (const auto bytes : sizes) { + threads.emplace_back([&, bytes] { + const CompileBudget::Hold hold(budget, bytes); + const auto now = held.fetch_add(bytes) + bytes; + auto seen = most.load(); + while (seen < now && !most.compare_exchange_weak(seen, now)) {} + std::this_thread::sleep_for(20ms); + held.fetch_sub(bytes); + }); + } + for (auto& thread : threads) thread.join(); + return most.load(); +} + +void CheckSharedLimit() { + CompileBudget budget(100); + const auto most = MostHeldAtOnce(budget, std::vector(8, 40)); + Require(most <= 80, "eight 40-byte holds under a 100-byte budget held " + std::to_string(most) + " bytes at once"); + Require(budget.Peak() >= 40 && budget.Peak() <= 80, "eight 40-byte holds under a 100-byte budget peaked at " + std::to_string(budget.Peak()) + " bytes"); +} + +void CheckZeroLimitRunsOneAtATime() { + CompileBudget budget(0); + const auto most = MostHeldAtOnce(budget, std::vector(6, 1)); + Require(most == 1 && budget.Peak() == 1, "a zero budget let " + std::to_string(most) + " one-byte holds run at once"); +} + +void CheckLargerHoldRunsAlone() { + CompileBudget budget(100); + const auto most = MostHeldAtOnce(budget, {500, 10, 10, 10}); + Require(most == 500 && budget.Peak() == 500, "a 500-byte hold under a 100-byte budget did not run alone: " + std::to_string(most) + " bytes were held at once"); +} + +} + +int main() { + try { + CheckLimitPolicy(); + CheckSharedLimit(); + CheckZeroLimitRunsOneAtATime(); + CheckLargerHoldRunsAlone(); + std::cout << "compile budget tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 13dd3116109b..aea98a5c6470 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -90,6 +90,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_ALL_BUFFERS_WRITTEN` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | notes every shader buffer element as written, as before bufferWritten existed | | `APS5_ASSERT_GPU_LOCK` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | aborts when a guarded path is reached without holding GuestMemory::GpuMutex | | `APS5_ATOMIC_STAGE_MAX_KIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 1024 | size cap in KiB on an atomic shader element covered by device-local staging | +| `APS5_BACKGROUND_COMPILE_KIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/include/CompileBudget.hpp` | 4096 | KiB of SPIR-V that compute pipelines built ahead of their dispatch (dispatch prebuilds and the pipeline journal's replay) may be compiling at once; a build that does not fit waits, one larger than the limit builds alone, and 0 builds them one at a time. Dispatches that need a pipeline now are not limited | | `APS5_BARRIER_VALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | counts the barriers a hazard-tracking validator would emit and skip, by class and hazard kind | | `APS5_BATCH_CAP` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | 64 | recorded work since the last submit at which a label batch, flush or reap is forced (0 disables the cap) | | `APS5_BDA_FLUSH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | also flushes pending storage results over address-based shader regions, which are otherwise exempt | From f07182a3871245b85772679fcf6b74002ac13b1d Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 28 Oct 2026 00:15:20 +0000 Subject: [PATCH 2/5] fix(agc): keep new compile holds out while a larger one waits to run alone Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/CompileBudget.hpp | 14 +++++++- .../libSceAgcDriver/tests/CompileBudget.cpp | 34 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/CompileBudget.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/CompileBudget.hpp index 6cabdf78a2d5..45631c8ade6b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/CompileBudget.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/CompileBudget.hpp @@ -14,7 +14,13 @@ class CompileBudget { public: Hold(CompileBudget& budget, std::uint64_t bytes) : budget(budget), bytes(bytes) { std::unique_lock lock(budget.mutex); - budget.released.wait(lock, [&] { return budget.held == 0 || budget.held + bytes <= budget.limit; }); + if (bytes > budget.limit) { + ++budget.waitingAlone; + budget.released.wait(lock, [&] { return budget.held == 0; }); + --budget.waitingAlone; + } else { + budget.released.wait(lock, [&] { return budget.waitingAlone == 0 && budget.held + bytes <= budget.limit; }); + } budget.held += bytes; if (budget.held > budget.peak) budget.peak = budget.held; } @@ -42,12 +48,18 @@ class CompileBudget { return peak; } + std::uint64_t WaitingAlone() const { + std::lock_guard lock(mutex); + return waitingAlone; + } + private: mutable std::mutex mutex; std::condition_variable released; std::uint64_t limit; std::uint64_t held = 0; std::uint64_t peak = 0; + std::uint64_t waitingAlone = 0; }; inline std::uint64_t BackgroundCompileLimitFor(const char* configured) { diff --git a/core/libs/prx/libSceAgcDriver/tests/CompileBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/CompileBudget.cpp index 6f171f8e4ce1..2d6bc851b356 100644 --- a/core/libs/prx/libSceAgcDriver/tests/CompileBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/CompileBudget.cpp @@ -61,6 +61,39 @@ void CheckLargerHoldRunsAlone() { Require(most == 500 && budget.Peak() == 500, "a 500-byte hold under a 100-byte budget did not run alone: " + std::to_string(most) + " bytes were held at once"); } +void CheckWaitingLargerHoldKeepsNewHoldsOut() { + CompileBudget budget(100); + std::atomic firstHeld{false}; + std::atomic releaseFirst{false}; + std::atomic entries{0}; + std::atomic largeEntry{-1}; + std::atomic smallEntry{-1}; + std::thread first([&] { + const CompileBudget::Hold hold(budget, 40); + firstHeld = true; + while (!releaseFirst) std::this_thread::sleep_for(1ms); + }); + while (!firstHeld) std::this_thread::sleep_for(1ms); + std::thread large([&] { + const CompileBudget::Hold hold(budget, 500); + largeEntry = entries++; + std::this_thread::sleep_for(10ms); + }); + while (budget.WaitingAlone() == 0) std::this_thread::sleep_for(1ms); + std::thread small([&] { + const CompileBudget::Hold hold(budget, 40); + smallEntry = entries++; + }); + std::this_thread::sleep_for(20ms); + const bool enteredEarly = smallEntry != -1; + releaseFirst = true; + first.join(); + large.join(); + small.join(); + Require(!enteredEarly, "a 40-byte hold that fits entered while a 500-byte hold was waiting to run alone"); + Require(largeEntry == 0 && smallEntry == 1, "the waiting 500-byte hold did not run before the 40-byte hold that came after it"); +} + } int main() { @@ -69,6 +102,7 @@ int main() { CheckSharedLimit(); CheckZeroLimitRunsOneAtATime(); CheckLargerHoldRunsAlone(); + CheckWaitingLargerHoldKeepsNewHoldsOut(); std::cout << "compile budget tests passed\n"; return 0; } catch (const std::exception& error) { From feb568b46dd51e9cb3f17e90714461605a47984f Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 28 Oct 2026 01:35:09 +0000 Subject: [PATCH 3/5] fix(agc): raise the default background compile budget to 8 MiB of SPIR-V Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/Execution/include/CompileBudget.hpp | 2 +- core/libs/prx/libSceAgcDriver/tests/CompileBudget.cpp | 2 +- docs/dev/ENVIRONMENT.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/CompileBudget.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/CompileBudget.hpp index 45631c8ade6b..1c9913e331fb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/CompileBudget.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/CompileBudget.hpp @@ -63,7 +63,7 @@ class CompileBudget { }; inline std::uint64_t BackgroundCompileLimitFor(const char* configured) { - constexpr std::uint64_t automaticKiB = 4096; + constexpr std::uint64_t automaticKiB = 8192; return (configured != nullptr ? std::strtoull(configured, nullptr, 10) : automaticKiB) * 1024u; } diff --git a/core/libs/prx/libSceAgcDriver/tests/CompileBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/CompileBudget.cpp index 2d6bc851b356..f8abc3d7b1c5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/CompileBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/CompileBudget.cpp @@ -19,7 +19,7 @@ void Require(bool condition, const std::string& message) { } void CheckLimitPolicy() { - Require(BackgroundCompileLimitFor(nullptr) == 4096u * 1024u, "an unset APS5_BACKGROUND_COMPILE_KIB is not 4 MiB"); + Require(BackgroundCompileLimitFor(nullptr) == 8192u * 1024u, "an unset APS5_BACKGROUND_COMPILE_KIB is not 8 MiB"); Require(BackgroundCompileLimitFor("16") == 16u * 1024u, "APS5_BACKGROUND_COMPILE_KIB=16 is not 16 KiB"); Require(BackgroundCompileLimitFor("0") == 0u, "APS5_BACKGROUND_COMPILE_KIB=0 leaves room"); } diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index aea98a5c6470..13fd9779e125 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -90,7 +90,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_ALL_BUFFERS_WRITTEN` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | notes every shader buffer element as written, as before bufferWritten existed | | `APS5_ASSERT_GPU_LOCK` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | aborts when a guarded path is reached without holding GuestMemory::GpuMutex | | `APS5_ATOMIC_STAGE_MAX_KIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 1024 | size cap in KiB on an atomic shader element covered by device-local staging | -| `APS5_BACKGROUND_COMPILE_KIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/include/CompileBudget.hpp` | 4096 | KiB of SPIR-V that compute pipelines built ahead of their dispatch (dispatch prebuilds and the pipeline journal's replay) may be compiling at once; a build that does not fit waits, one larger than the limit builds alone, and 0 builds them one at a time. Dispatches that need a pipeline now are not limited | +| `APS5_BACKGROUND_COMPILE_KIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/include/CompileBudget.hpp` | 8192 | KiB of SPIR-V that compute pipelines built ahead of their dispatch (dispatch prebuilds and the pipeline journal's replay) may be compiling at once; a build that does not fit waits, one larger than the limit builds alone, and 0 builds them one at a time. Dispatches that need a pipeline now are not limited | | `APS5_BARRIER_VALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | counts the barriers a hazard-tracking validator would emit and skip, by class and hazard kind | | `APS5_BATCH_CAP` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | 64 | recorded work since the last submit at which a label batch, flush or reap is forced (0 disables the cap) | | `APS5_BDA_FLUSH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | also flushes pending storage results over address-based shader regions, which are otherwise exempt | From 759ec763c984ae6ffacd84e5fb55dfbbf0c52075 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 28 Oct 2026 05:31:02 +0000 Subject: [PATCH 4/5] fix(agc): keep compute prebuilds waiting on the budget to half the prebuild pool A compute pipeline prebuild waits for the background compile budget inside a worker of the shared prebuild pool, and the graphics library prebuilds use the same pool. During a burst of large compute modules every worker could be waiting on the budget, so the library prebuilds queued behind them did not run and their draws built the libraries themselves. Compute prebuilds now go through SubmitComputePrebuild, and the pool starts one only while fewer than half of its workers run them. A worker takes the first queued task it may start, so library prebuilds behind waiting compute prebuilds run on the other workers, and compute prebuilds still start in the order they were queued. Co-Authored-By: Claude Opus 5.5 --- .../include/Driver/Shaders/ShaderRegistry.hpp | 1 + .../src/Driver/Dispatch/Dispatch.cpp | 2 +- .../src/Driver/Shaders/ShaderRegistry.cpp | 30 ++++-- .../tests/execution/ComputePrebuildShare.cpp | 96 +++++++++++++++++++ docs/dev/ENVIRONMENT.md | 2 +- 5 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ComputePrebuildShare.cpp 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 0c19b88668ef..ee349e6e4bf6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -224,6 +224,7 @@ void SubmitRegistrationPreparation(std::function task, const PreparedSha void PrioritizeRegistrationPreparation(PreparedShaders& prepared); void AwaitRegistrationPreparation(); void SubmitLibraryPrebuild(std::function task); +void SubmitComputePrebuild(std::function task); void AwaitLibraryPrebuilds(); void CloseRegistrationPreparation(); unsigned RegistrationPreparationThreads(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index a08d54121cc5..55f96a2abe82 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -437,7 +437,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, ++groupStats.resolved; if (cached) ++groupStats.resolvedHits; if (!cached && compiledResult != nullptr && !localDevice->HasComputePipeline(*compiledResult)) { - SubmitLibraryPrebuild([localDevice, compiledResult, address] { + SubmitComputePrebuild([localDevice, compiledResult, address] { try { localDevice->PrebuildComputePipeline(*compiledResult, address); } catch (const std::exception&) {} }); } 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 069a5e418fd7..22aca329b7d0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1544,13 +1544,13 @@ class RegistrationPreparePool { public: - void Submit(std::function task, const PreparedShaders* prepared = nullptr) { + void Submit(std::function task, const PreparedShaders* prepared = nullptr, bool budgeted = false) { { std::unique_lock lock(mutex); if (threads != 0 && !closed) { const auto sequence = ++submitted; - if (prepared != nullptr && prepared->demanded.load()) tasks.push_front({std::move(task), prepared, sequence}); - else tasks.push_back({std::move(task), prepared, sequence}); + if (prepared != nullptr && prepared->demanded.load()) tasks.push_front({std::move(task), prepared, sequence, budgeted}); + else tasks.push_back({std::move(task), prepared, sequence, budgeted}); unfinished.insert(sequence); lock.unlock(); ready.notify_one(); @@ -1638,6 +1638,7 @@ class RegistrationPreparePool { } pthread_attr_destroy(&attributes); #endif + budgetedLimit = std::max(1u, threads / 2u); } #ifdef _WIN32 @@ -1654,26 +1655,34 @@ class RegistrationPreparePool { Task task; { std::unique_lock lock(pool.mutex); - pool.ready.wait(lock, [&] { return !pool.tasks.empty() || pool.closed; }); - if (pool.tasks.empty()) { + auto next = pool.tasks.end(); + pool.ready.wait(lock, [&] { + next = std::ranges::find_if(pool.tasks, [&](const Task& queued) { return !queued.budgeted || pool.budgetedRunning < pool.budgetedLimit; }); + return next != pool.tasks.end() || (pool.closed && pool.tasks.empty()); + }); + if (next == pool.tasks.end()) { liveWorkers.fetch_sub(1, std::memory_order_acq_rel); return nullptr; } - task = std::move(pool.tasks.front()); - pool.tasks.pop_front(); + task = std::move(*next); + pool.tasks.erase(next); ++pool.running; + if (task.budgeted) ++pool.budgetedRunning; } const auto sequence = task.sequence; + const bool budgeted = task.budgeted; task.function(); task = {}; bool drained = false; { std::lock_guard lock(pool.mutex); --pool.running; + if (budgeted) --pool.budgetedRunning; pool.unfinished.erase(sequence); drained = pool.tasks.empty() && pool.running == 0; pool.idle.notify_all(); } + if (budgeted) pool.ready.notify_all(); #ifdef __GLIBC__ if (drained) trimFreedMemory(); #endif @@ -1685,6 +1694,7 @@ class RegistrationPreparePool { std::function function; const PreparedShaders* prepared = nullptr; std::uint64_t sequence = 0; + bool budgeted = false; }; std::mutex mutex; @@ -1695,6 +1705,8 @@ class RegistrationPreparePool { unsigned threads = 0; inline static std::atomic liveWorkers{0}; unsigned running = 0; + unsigned budgetedRunning = 0; + unsigned budgetedLimit = 1; std::uint64_t submitted = 0; std::set unfinished; bool closed = false; @@ -1750,6 +1762,10 @@ void SubmitLibraryPrebuild(std::function task) { RegistrationPreparePool::Prebuild().Submit(std::move(task)); } +void SubmitComputePrebuild(std::function task) { + RegistrationPreparePool::Prebuild().Submit(std::move(task), nullptr, true); +} + void AwaitLibraryPrebuilds() { if (auto* pool = RegistrationPreparePool::ExistingPrebuild()) pool->AwaitSubmitted(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ComputePrebuildShare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ComputePrebuildShare.cpp new file mode 100644 index 000000000000..58c6d43f5918 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ComputePrebuildShare.cpp @@ -0,0 +1,96 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error("compute prebuild share: " + message); +} + +unsigned PrebuildThreads() { + const char* text = std::getenv("APS5_PREBUILD_THREADS"); + return text != nullptr ? std::max(1u, static_cast(std::strtoul(text, nullptr, 10))) : std::max(2u, std::thread::hardware_concurrency() / 2u); +} + +struct Gate { + std::mutex mutex; + std::condition_variable changed; + bool open = false; + bool libraryBuilt = false; + unsigned running = 0; + unsigned most = 0; + unsigned finished = 0; +}; + +struct OpenOnExit { + std::shared_ptr gate; + ~OpenOnExit() { + { + std::lock_guard lock(gate->mutex); + gate->open = true; + } + gate->changed.notify_all(); + AgcDriver::DriverDetail::AwaitLibraryPrebuilds(); + } +}; + +void CheckLibraryPrebuildRunsBesideBlockedComputePrebuilds() { + const unsigned threads = PrebuildThreads(); + const unsigned limit = std::max(1u, threads / 2u); + const auto gate = std::make_shared(); + const OpenOnExit release{gate}; + for (unsigned index = 0; index < threads; ++index) { + AgcDriver::DriverDetail::SubmitComputePrebuild([gate] { + std::unique_lock lock(gate->mutex); + ++gate->running; + gate->most = std::max(gate->most, gate->running); + gate->changed.notify_all(); + gate->changed.wait(lock, [&] { return gate->open; }); + --gate->running; + ++gate->finished; + gate->changed.notify_all(); + }); + } + std::unique_lock lock(gate->mutex); + Require(gate->changed.wait_for(lock, std::chrono::seconds(10), [&] { return gate->running == limit; }), std::to_string(gate->running) + " of " + std::to_string(threads) + " blocked compute prebuilds started, not " + std::to_string(limit)); + lock.unlock(); + AgcDriver::DriverDetail::SubmitLibraryPrebuild([gate] { + std::lock_guard built(gate->mutex); + gate->libraryBuilt = true; + gate->changed.notify_all(); + }); + lock.lock(); + const bool built = gate->changed.wait_for(lock, std::chrono::seconds(10), [&] { return gate->libraryBuilt; }); + Require(built, "a library prebuild queued behind " + std::to_string(threads) + " blocked compute prebuilds did not run while they waited"); + Require(gate->running == limit, std::to_string(gate->running) + " compute prebuilds ran beside the library prebuild, not " + std::to_string(limit)); + gate->open = true; + gate->changed.notify_all(); + Require(gate->changed.wait_for(lock, std::chrono::seconds(10), [&] { return gate->finished == threads; }), "only " + std::to_string(gate->finished) + " of " + std::to_string(threads) + " compute prebuilds finished after they were released"); + Require(gate->most == limit, std::to_string(gate->most) + " compute prebuilds ran at once, more than " + std::to_string(limit)); +} + +} + +int main() { + try { + AgcDriver::DriverDetail::Driver::Get(); + CheckLibraryPrebuildRunsBesideBlockedComputePrebuilds(); + AgcDriver::DriverDetail::CloseRegistrationPreparation(); + std::puts("compute prebuild share tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 13fd9779e125..5b7361baf71f 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -372,7 +372,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_PM4_RING` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp` | 16 | number of submissions the PM4 ring keeps for dumps, from 0 to 4096; 0 disables the ring | | `APS5_POLL_REAP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | reaps completions on every poll tick | | `APS5_POLL_TRY_EACH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | tries the poll service on every tick instead of at the poll interval | -| `APS5_PREBUILD_THREADS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | | threads in the pool that prebuilds registration-form shader libraries; unset uses half the hardware threads, at least two | +| `APS5_PREBUILD_THREADS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | | threads in the pool that prebuilds registration-form shader libraries and compute pipelines; unset uses half the hardware threads, at least two. Compute pipeline prebuilds, which wait for `APS5_BACKGROUND_COMPILE_KIB`, run on at most half of them (at least one) | | `APS5_PRESENT_POLL_FENCE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | polls the present fence every 50 us instead of blocking on it | | `APS5_PROBE_DISPATCH` | string | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | probes one dispatch at a hex program address (optionally `:index`) with debug output | | `APS5_PROFILE_DRAW` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | prints the per-phase driver and draw timing totals | From a937addfb8b0229ec05ce1048db5f4985015a5e9 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Thu, 29 Oct 2026 04:29:43 +0000 Subject: [PATCH 5/5] fix(agc): check again after the compile budget wait before a journal replay compiles a pipeline ReplayComputePipeline checked for the pipeline, then waited for the budget, then compiled without checking again. A dispatch that built the pipeline during that wait made the replay compile it a second time and throw the result away (XairRrR's review). It now checks again under the pipeline mutex after the hold, as PrebuildComputePipeline does. agc_driver_compute_prebuild_share now runs with APS5_PREBUILD_THREADS=4, so it doesn't depend on the host's core count or a developer's setting: with one worker the library prebuild can't run beside the blocked one. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 + core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index d90eeca1e00f..f3979c38aff1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -3283,6 +3283,7 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() set_tests_properties(agc_driver_queued_index_read_order PROPERTIES TIMEOUT 60) + set_property(TEST agc_driver_compute_prebuild_share APPEND PROPERTY ENVIRONMENT "APS5_PREBUILD_THREADS=4") foreach(agcTest IN ITEMS agc_spirv_narrow_constant_store_tests agc_spirv13_image_tests agc_spirv_optimizer_tests agc_driver_constant_store_tests) if(NOT TARGET ${agcTest}) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 691e717dbdc4..2996a91141ef 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2331,6 +2331,10 @@ bool VulkanDevice::ReplayComputePipeline(const ShaderRecompiler::RecompileResult if (state->computePipelines.contains(key) || state->computePipelinesBuilding.contains(key)) return false; } const CompileBudget::Hold hold(BackgroundCompileBudget(), shader.spirv.size() * sizeof(std::uint32_t)); + { + std::lock_guard pipelines(state->computePipelinesMutex); + if (state->computePipelines.contains(key) || state->computePipelinesBuilding.contains(key)) return false; + } const auto context = graphicsContext(); const std::array shaders{{{ShaderRecompiler::ShaderStage::Compute, &shader, 0}}}; const auto layouts = Graphics::DescribeSetLayouts(context, shaders);