mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-18 23:07:07 -05:00
perf(agc): count the processors the process may run on for the hand-off spin guard
The automatic window needed 8 logical processors, which a 4-core CPU with SMT passes while the waited-for thread may run on a sibling thread, and ignored the process affinity. It now needs more than 8 processors in the affinity mask (CpuTopology), falling back to hardware_concurrency.
This commit is contained in:
1 parent
e74484e0fb
commit
23a804e393
3 files changed
+24
-8
No files matched your search
@@ -1,7 +1,9 @@
|
||||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_HANDOFFSPIN_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_HANDOFFSPIN_HPP
|
||||
|
||||
#include "prx/libc/include/CpuTopology.hpp"
|
||||
#include <algorithm>
|
||||
#include <bitset>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
@@ -18,16 +20,21 @@ inline void SpinPause() {
|
||||
#endif
|
||||
}
|
||||
|
||||
inline std::chrono::nanoseconds HandoffSpinWindowFor(const char* configured, unsigned logicalProcessors) {
|
||||
inline unsigned UsableProcessors() {
|
||||
const auto count = static_cast<unsigned>(std::bitset<64>(CpuTopology::Get().process).count());
|
||||
return count != 0 ? count : std::thread::hardware_concurrency();
|
||||
}
|
||||
|
||||
inline std::chrono::nanoseconds HandoffSpinWindowFor(const char* configured, unsigned usableProcessors) {
|
||||
constexpr std::chrono::microseconds automaticWindow{150};
|
||||
constexpr unsigned automaticProcessors = 8;
|
||||
constexpr unsigned automaticProcessorsAbove = 8;
|
||||
constexpr long long longestWindowUs = 10000;
|
||||
if (configured != nullptr) return std::chrono::microseconds(std::clamp<long long>(std::strtoll(configured, nullptr, 10), 0, longestWindowUs));
|
||||
return logicalProcessors >= automaticProcessors ? std::chrono::nanoseconds(automaticWindow) : std::chrono::nanoseconds::zero();
|
||||
return usableProcessors > automaticProcessorsAbove ? std::chrono::nanoseconds(automaticWindow) : std::chrono::nanoseconds::zero();
|
||||
}
|
||||
|
||||
inline std::chrono::nanoseconds HandoffSpinWindow() {
|
||||
static const std::chrono::nanoseconds window = HandoffSpinWindowFor(std::getenv("APS5_HANDOFF_SPIN_US"), std::thread::hardware_concurrency());
|
||||
static const std::chrono::nanoseconds window = HandoffSpinWindowFor(std::getenv("APS5_HANDOFF_SPIN_US"), UsableProcessors());
|
||||
return window;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp"
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
@@ -21,9 +22,10 @@ void Require(bool condition, const std::string& message) {
|
||||
}
|
||||
|
||||
void CheckWindowPolicy() {
|
||||
Require(HandoffSpinWindowFor(nullptr, 20) == 150us, "20 logical processors do not spin for 150 us");
|
||||
Require(HandoffSpinWindowFor(nullptr, 8) == 150us, "8 logical processors do not spin for 150 us");
|
||||
Require(HandoffSpinWindowFor(nullptr, 7) == 0ns, "7 logical processors spin");
|
||||
Require(HandoffSpinWindowFor(nullptr, 20) == 150us, "20 usable processors do not spin for 150 us");
|
||||
Require(HandoffSpinWindowFor(nullptr, 9) == 150us, "9 usable processors do not spin for 150 us");
|
||||
Require(HandoffSpinWindowFor(nullptr, 8) == 0ns, "8 usable processors spin");
|
||||
Require(HandoffSpinWindowFor(nullptr, 4) == 0ns, "4 usable processors spin");
|
||||
Require(HandoffSpinWindowFor(nullptr, 0) == 0ns, "an unknown processor count spins");
|
||||
Require(HandoffSpinWindowFor("0", 20) == 0ns, "APS5_HANDOFF_SPIN_US=0 still spins");
|
||||
Require(HandoffSpinWindowFor("400", 2) == 400us, "APS5_HANDOFF_SPIN_US=400 does not set 400 us on 2 logical processors");
|
||||
@@ -32,6 +34,12 @@ void CheckWindowPolicy() {
|
||||
Require(HandoffSpinWindowFor("99999999999", 20) == 10000us, "a huge window is not limited to 10000 us");
|
||||
}
|
||||
|
||||
void CheckUsableProcessors() {
|
||||
const unsigned usable = AgcDriver::UsableProcessors();
|
||||
Require(usable >= 1, "no usable processor was counted");
|
||||
Require(usable <= (std::max)(std::thread::hardware_concurrency(), 64u), "more usable processors than the machine has");
|
||||
}
|
||||
|
||||
void CheckSpinUntil() {
|
||||
int calls = 0;
|
||||
Require(SpinUntil(0ns, [&] { ++calls; return true; }) && calls == 1, "a ready condition was evaluated more than once");
|
||||
@@ -109,6 +117,7 @@ void CheckRecursion() {
|
||||
int main() {
|
||||
try {
|
||||
CheckWindowPolicy();
|
||||
CheckUsableProcessors();
|
||||
CheckSpinUntil();
|
||||
Contend<std::mutex>(0ns, "plain lock");
|
||||
Contend<std::mutex>(50us, "spin 50 us");
|
||||
|
||||
@@ -98,7 +98,7 @@ flowchart LR
|
||||
- The Vulkan pipelines are journaled as well ([`PipelineJournal.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp)): a graphics pipeline the store builds (`CachedPipeline`) or a compute pipeline a dispatch builds is appended to `pipelines.journal` next to the shader cache entries, as the materialized result of each stage (the specialized SPIR-V, bindings and push constants, through `ShaderDiskCache::EncodeResult`) plus the recipe the pipeline was built from (`GraphicsPipelineRecipe`: the stages' identities, vertex input layout, descriptor set layout keys and the state fields the `Pipeline` constructor reads, `CanonicalPipelineState`). When a `VulkanDevice` is created, the journal is loaded and its pipelines are built on a pool of `hardware_concurrency() / 2` threads (`APS5_PIPELINE_JOURNAL_THREADS`) while the title loads, so the first draw or dispatch of a configuration seen on an earlier run finds its pipeline resident. The pipeline stores key on `RecompileResult::Identity`, a hash of the module words, specialization constants, subgroup width and bindings that is the same on every run (`SharedSpirv::ContentHash`, computed once per shared module), rather than on the process-lifetime variant ids, and a graphics pipeline is built outside the store's mutex, so the replay threads and the queue workers build in parallel. A draw that arrives before its replayed entry builds the pipeline itself; the first inserted wins. A replayed entry enters the store at its least recently used end and a full store takes none, so the title's own pipelines are never evicted for a replayed one, and the optimized relink of a replayed pipeline (`OptimizedPipeline::Schedule`) waits for the first draw that binds it, so the relink thread serves the title's pipelines first. A device created without a window starts its replay at its first draw or dispatch rather than at creation, since `Driver::AttachWindow` usually replaces it before the title draws. The journal records a pipeline once (modules and recipes are deduplicated by content), keeps at most `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024), replays the first `APS5_PIPELINE_JOURNAL_ENTRIES` recorded (1024, the pipeline store's bound, `APS5_PIPELINE_STORE_ENTRIES`; a run reaches those first), and is disabled with `APS5_NO_PIPELINE_JOURNAL=1` or without a shader cache directory. The file header names the device (`PipelineJournalDeviceFingerprint`, the properties the driver's pipeline cache file is named by), so a journal written for another GPU or driver is started over rather than replayed; appends and reads hold `pipelines.journal.lock` against another process sharing the directory, and a device cancels its replay before it is replaced (`PrepareForReplacement`) or destroyed, so a replaced headless device never inserts entries the window's device then sweeps. `APS5_PROFILE_DRAW=1` prints a `[pipeline-journal]` summary when the replay finishes and at teardown.
|
||||
- A compute shader whose LDS plus its lock dword exceeds the host's `maxComputeSharedMemorySize` is not given a `Workgroup` array. `Recompile` sets `ShaderInfo::sharedMemoryBytes` for it and the SPIR-V backend addresses the LDS in a coherent storage buffer at descriptor set 1 (`WorkgroupMemoryDescriptorSet`), with a slice of `RecompileResult::workgroupMemoryDwords` per workgroup, indexed by `WorkgroupID` and `NumWorkGroups`. `VulkanDevice` keeps one such buffer, sized from the dispatch dimensions, and binds it after the resource set; programs that fit keep using shared memory.
|
||||
- `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks).
|
||||
- A thread that needs the GPU mutex, the committer's next item or the end of the committer's queue spins before it blocks (`HandoffSpin.hpp`): `GpuMutexType::lock` retries `try_lock` for the window, the committer polls its item count, and `DrawPipeline::Drain` polls it for twice the window, so a hand-off that completes within the window costs no kernel wake-up. The window is 150 us on machines with at least 8 logical processors and 0 below that, where the spinning threads would compete with the holder; `APS5_HANDOFF_SPIN_US=<n>` sets it (0 turns it off, at most 10000). Ordering and the lock's semantics are unchanged.
|
||||
- A thread that needs the GPU mutex, the committer's next item or the end of the committer's queue spins before it blocks (`HandoffSpin.hpp`): `GpuMutexType::lock` retries `try_lock` for the window, the committer polls its item count, and `DrawPipeline::Drain` polls it for twice the window, so a hand-off that completes within the window costs no kernel wake-up. The window is 150 us when the process may run on more than 8 logical processors (its affinity mask, via `CpuTopology`) and 0 otherwise, where the spinning threads would compete with the holder; `APS5_HANDOFF_SPIN_US=<n>` sets it (0 turns it off, at most 10000). Ordering and the lock's semantics are unchanged.
|
||||
- Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and (with `APS5_PIPELINE_DMA=1`) DMA packets are queued as items instead. With `APS5_PIPELINE_DISPATCH=1` (off by default), a `DISPATCH_DIRECT` whose dispatch-cache entry hits with a recipe is queued as an item too, carrying the dispatch's written buffers and storage images (`DispatchWriteRanges`); the capture and the cache lookup stay on the worker and the recipe is recorded on the committer. A dispatch still drains when its lookup misses, when what it reads overlaps a write still pending, when the hit is verified against data or when its writes cannot be ranged (bindless access, GDS, fault buffer). An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates.
|
||||
- The shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`, which also takes a hull header fused by `sceAgcFuseShaderHalves` (type 3) as the registered back half (type 7) it copies, the fused registers aside; graphics ABI resolution then uses the fused header's shader registers); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers.
|
||||
- The snapshot is published as pending. `SourceHandleFor`, `InvocationFor` and `ShaderPreparationTransaction::Edit` / `Read` wait for the pending job before reading `PreparedShaders`, and rethrow its failure. `APS5_SYNC_SHADER_PREPARE=1` prepares on the registering thread instead.
|
||||
|
||||
Reference in new issue
Block a user