video_core: GPU thread split on by default for Switch 2 titles; cheaper front paths
Clean A/B in title 1 (3 rounds each): 1.46x the draws a second of one thread, frames 17% shorter, no crashes, front register copies equal to the back's in 57,089 checks. EDEN_NX2_SPLIT=0 runs everything on one thread, =1 keeps macros on the back. The front's per-write paths no longer check guarded statics (GpuSplit::Get, Nx2Profile::Enabled, the skip-same switches) and the ring's method writes are inline. Notes row 145.
This commit is contained in:
parent
b884c25d6e
commit
ac7ccd35f9
6 changed files with 58 additions and 45 deletions
|
|
@ -59,4 +59,5 @@ on the back, `EDEN_NX2_SPLIT=2` also runs the 3D and compute macros on the front
|
|||
copies. Measured in title 1 (NX2_NOTES rows 142-144): ~44,000 draws a second on one thread, ~47,400
|
||||
with stage 2, 50,000-66,500 with stage 3. The front waits ~4,000-5,000 times a second (semaphore
|
||||
acquires, MME DMA reads); the back still spends ~200 ms a second outside its zones (ring dispatch,
|
||||
method sink). Next: a clean A/B for the default, then trimming the back's ring overhead.
|
||||
method sink). Clean A/B (NX2_NOTES row 145): 1.46x throughput, frames 17% shorter; on by
|
||||
default since. The back is ~92% busy, ~53% of it draw setup: the next step is there, not the ring.
|
||||
|
|
|
|||
|
|
@ -200,6 +200,7 @@ With the patched decoder, games 1/3/4 only leave ~60-110 rare SIMD encodings in
|
|||
| 142 | GPU thread ~100% busy, host GPU 20-40%: one thread parses, runs macros, sets state and records draws | the work is sequential but only a few points need a result from what came before (semaphore acquires, MME DMA reads) | GPU thread split (babd395, `gpu_split.h`, `EDEN_NX2_SPLIT=1`): engine methods run in order on a second thread fed by a lock-free ring; releases, syncpoints, wait-for-idle, flushes and channel binds are deferred into the ring, only acquires wait (~2,400 drains a second). ~47,400 draws a second against ~44,000 (3 A/B rounds), same per-call costs |
|
||||
| 143 | crashes with the split: CommandChunk::Record (null chunk), vkCmdEndRenderPass on the Vulkan worker | two threads recording into the scheduler: (a) the DMA pusher's split-method debug log read guest memory with the cache-flushing ReadBlock on the front; (b) with async presentation, Swapchain::AcquireNextImage -> Scheduler::Wait flushed the main scheduler from the present thread whenever its tick was not submitted yet (pre-existing race, the split made it frequent) | (a) ReadBlockUnsafe; (b) e4523ab: the present thread waits up to 20 ms for the GPU thread to submit, never flushes. Found with `EDEN_NX2_SPLIT_CHECK=1` (stack of any other thread using the scheduler while the back runs) |
|
||||
| 144 | macros on the split's front (`EDEN_NX2_SPLIT=2`) need the registers they read before the back has them | the front keeps copies: every write it sends plus what the back changes on its own (constant buffer offset, shadow_scratch[0], macro bind pointer, shadow RAM). `EDEN_NX2_SPLIT_VERIFY=1` found three differences: writes still in the back's method sink at the handover; a multi-word run that changes the shadow RAM mode mid-run; and shadow_state.shadow_ram_control being the shadow copy of register 0x49 itself (a Replay write reads the mode back) | all three modelled: 0 differences in 40,193 checks (~165 million macro calls). Front: macros 128 ns a call (180 on one thread, cache-hot), drops ~55% of 3D state writes as same-value. Back: 50,000-66,500 draws a second against 41,500-47,000 without the split; frames 20.5 / 25.5 ms on two courses where the unsplit runs measured 25.6-40.7 ms. Default stays off until a clean A/B |
|
||||
| 145 | is the split worth having on by default | clean A/B, nothing else running, 3 rounds each (profiler on in both): one thread 41,100 / 48,600 / 37,000 draws a second, frames 30.5 / 35.0 / 27.1 ms; `EDEN_NX2_SPLIT=2` 59,200 / 62,400 / 62,700 draws a second, frames 28.6 / 23.3 / 25.1 ms. Throughput 1.46x, frames 17% shorter (the back is now ~92% busy, ~53% of it draw setup); 0 crashes, 0 register differences in 57,089 checks after the front's fast paths (no guarded statics, inline ring writes) | on by default for Switch 2 titles (`EDEN_NX2_SPLIT=0` one thread, `=1` macros on the back). Default run without the profiler: 16.9 ms frames (~59 fps) on a light course |
|
||||
|
||||
Run settings used (in `build/bin/user/config/sdl2-config.ini`): `memory_layout_mode=4`
|
||||
(12 GB). Debug knobs: `EDEN_NX2_TC_EXPECTED_MIB` / `EDEN_NX2_TC_CRITICAL_MIB` (collector thresholds),
|
||||
|
|
@ -214,7 +215,7 @@ faulting stack as module+offset, then a minidump), `EDEN_NX2_AUTO_STUB`, `EDEN_N
|
|||
`EDEN_NX2_BUFFER_DOWNLOADS=0/1`, `EDEN_NX2_INLINE_DEFER=0`, `EDEN_NX2_TC_MIN_AGE=<frames>` (rows 131, 134, 133),
|
||||
`EDEN_NX2_SKIP_SAME=0` (row 135),
|
||||
`EDEN_NX2_MME_JIT=0` (row 140),
|
||||
`EDEN_NX2_SPLIT=1/2`, `EDEN_NX2_SPLIT_VERIFY=1`, `EDEN_NX2_SPLIT_CHECK=1`, `EDEN_NX2_SPLIT_SKIP=0` (rows 142-144),
|
||||
`EDEN_NX2_SPLIT=0/1` (default 2), `EDEN_NX2_SPLIT_VERIFY=1`, `EDEN_NX2_SPLIT_CHECK=1`, `EDEN_NX2_SPLIT_SKIP=0` (rows 142-145),
|
||||
`EDEN_NX2_FRAME_CAPTURE=<dir>` (every 120th presented frame as TGA), `EDEN_NX2_PB_CHECK`
|
||||
(pushbuffer entries changed between submit and execution), `EDEN_NX2_QUERY_TRACE` (every GPU
|
||||
report/semaphore write). `C:\dev\crashrun.sh` runs eden-cli under cdb and prints the crash stack;
|
||||
|
|
|
|||
|
|
@ -800,6 +800,7 @@ struct Maxwell3D::FrontState {
|
|||
/// A method the draw manager handles was sent since the back last ran DrawDeferred: only
|
||||
/// then can it have a deferred instanced draw to issue at the end of a macro.
|
||||
bool draw_pending{true};
|
||||
bool skip_same{}; ///< SkipSameValueWrites() && FrontSkipSame(), read once
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
|
@ -865,6 +866,7 @@ Maxwell3D::FrontState& Maxwell3D::Front(Core::System& system, VideoCommon::GpuSp
|
|||
std::memcpy(front->regs.data(), regs.reg_array.data(), sizeof(front->regs));
|
||||
std::memcpy(front->shadow.data(), shadow_state.reg_array.data(), sizeof(front->shadow));
|
||||
front->control = shadow_state.shadow_ram_control;
|
||||
front->skip_same = SkipSameValueWrites() && FrontSkipSame();
|
||||
front->executing_macro = executing_macro;
|
||||
front->parameters = std::move(macro_params);
|
||||
macro_params.clear();
|
||||
|
|
@ -897,7 +899,7 @@ void Maxwell3D::FrontWrite(VideoCommon::GpuSplit& split, FrontState& fs, u32 met
|
|||
if (!execution_mask[method]) {
|
||||
// Nothing would change on the back: CallMethod and ConsumeSinkImpl skip same-value state
|
||||
// writes, and the shadow RAM (tracked by default) holds the value already.
|
||||
if (fs.regs[method] == value && shadow_kept && SkipSameValueWrites() && FrontSkipSame()) {
|
||||
if (fs.regs[method] == value && shadow_kept && fs.skip_same) {
|
||||
if (Profile::Enabled()) [[unlikely]] {
|
||||
Profile::Add(Profile::Counter::SameValueWrites, 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,15 +80,12 @@ void GpuSplit::CheckOwner(const char* where) {
|
|||
#endif
|
||||
}
|
||||
|
||||
GpuSplit& GpuSplit::Get() {
|
||||
static GpuSplit split;
|
||||
return split;
|
||||
}
|
||||
GpuSplit GpuSplit::instance;
|
||||
|
||||
void GpuSplit::MaybeStart(Core::System& system_) {
|
||||
static const char mode = [] {
|
||||
const char* value = std::getenv("EDEN_NX2_SPLIT");
|
||||
return value != nullptr ? value[0] : '0';
|
||||
return value != nullptr ? value[0] : '2';
|
||||
}();
|
||||
const bool wanted = mode == '1' || mode == '2';
|
||||
if (active || !wanted || !Tegra::g_nx2_ampere.load(std::memory_order_relaxed)) {
|
||||
|
|
@ -127,7 +124,7 @@ void GpuSplit::Stop() {
|
|||
active = false;
|
||||
}
|
||||
|
||||
u64* GpuSplit::Reserve(size_t words) {
|
||||
u64* GpuSplit::ReserveSlow(size_t words) {
|
||||
size_t index = write_position & RingMask;
|
||||
if (index + words > RingWords) {
|
||||
// Entries never wrap: skip to the start of the ring.
|
||||
|
|
@ -194,10 +191,7 @@ void GpuSplit::Drain() {
|
|||
}
|
||||
}
|
||||
|
||||
void GpuSplit::SelectEngine(Tegra::Engines::EngineInterface* engine) {
|
||||
if (engine == front_engine) {
|
||||
return;
|
||||
}
|
||||
void GpuSplit::SwitchEngine(Tegra::Engines::EngineInterface* engine) {
|
||||
front_engine = engine;
|
||||
u64* const entry = Reserve(2);
|
||||
entry[0] = static_cast<u64>(Kind::Engine);
|
||||
|
|
@ -205,28 +199,6 @@ void GpuSplit::SelectEngine(Tegra::Engines::EngineInterface* engine) {
|
|||
Commit(2);
|
||||
}
|
||||
|
||||
void GpuSplit::Method(Tegra::Engines::EngineInterface* engine, u32 method, u32 argument,
|
||||
bool is_last, const GPUVAddr* segment) {
|
||||
SelectEngine(engine);
|
||||
const size_t words = segment != nullptr ? 2 : 1;
|
||||
u64* const entry = Reserve(words);
|
||||
entry[0] = static_cast<u64>(Kind::Method) | (is_last ? IsLast : 0) |
|
||||
(segment != nullptr ? HasSegment : 0) | (u64{method & 0xffff} << 8) |
|
||||
(u64{argument} << 32);
|
||||
if (segment != nullptr) {
|
||||
entry[1] = *segment;
|
||||
}
|
||||
Commit(words);
|
||||
}
|
||||
|
||||
void GpuSplit::MacroWrite(Tegra::Engines::EngineInterface* engine, u32 method, u32 argument) {
|
||||
SelectEngine(engine);
|
||||
u64* const entry = Reserve(1);
|
||||
entry[0] = static_cast<u64>(Kind::Method) | IsLast | Direct | (u64{method & 0xffff} << 8) |
|
||||
(u64{argument} << 32);
|
||||
Commit(1);
|
||||
}
|
||||
|
||||
void GpuSplit::MultiMethod(Tegra::Engines::EngineInterface* engine, u32 method, const u32* words,
|
||||
u32 amount, u32 methods_pending, const GPUVAddr* segment) {
|
||||
SelectEngine(engine);
|
||||
|
|
|
|||
|
|
@ -28,12 +28,14 @@ namespace VideoCommon {
|
|||
// second thread (the back), fed through a single-producer single-consumer ring. Work the front
|
||||
// would do on back-owned state is deferred into the same ring; the front waits for the back to
|
||||
// catch up (Drain) only where it needs a result from that state, e.g. a semaphore it acquires.
|
||||
// Opt-in for Switch 2 titles: EDEN_NX2_SPLIT=1. EDEN_NX2_SPLIT=2 also runs the 3D and compute
|
||||
// engines' macros on the front, against copies of the registers they read (Maxwell3D and
|
||||
// KeplerCompute Front*), and sends the writes they make.
|
||||
// On for Switch 2 titles with the 3D and compute engines' macros on the front, against copies of
|
||||
// the registers they read (Maxwell3D and KeplerCompute Front*), which sends the writes they make.
|
||||
// EDEN_NX2_SPLIT=1 keeps the macros on the back; EDEN_NX2_SPLIT=0 runs everything on one thread.
|
||||
class GpuSplit {
|
||||
public:
|
||||
static GpuSplit& Get();
|
||||
static GpuSplit& Get() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// Whether the front sends work to the back (front thread only).
|
||||
[[nodiscard]] bool Active() const {
|
||||
|
|
@ -52,11 +54,28 @@ public:
|
|||
/// An engine method write, executed as DmaPusher would (method sink, ConsumeSink, call).
|
||||
/// `segment`: the engine's current_dma_segment for the call, when the engine reads it.
|
||||
void Method(Tegra::Engines::EngineInterface* engine, u32 method, u32 argument, bool is_last,
|
||||
const GPUVAddr* segment);
|
||||
const GPUVAddr* segment) {
|
||||
SelectEngine(engine);
|
||||
const size_t words = segment != nullptr ? 2 : 1;
|
||||
u64* const entry = Reserve(words);
|
||||
entry[0] = static_cast<u64>(Kind::Method) | (is_last ? IsLast : 0) |
|
||||
(segment != nullptr ? HasSegment : 0) | (u64{method & 0xffff} << 8) |
|
||||
(u64{argument} << 32);
|
||||
if (segment != nullptr) {
|
||||
entry[1] = *segment;
|
||||
}
|
||||
Commit(words);
|
||||
}
|
||||
void MultiMethod(Tegra::Engines::EngineInterface* engine, u32 method, const u32* words,
|
||||
u32 amount, u32 methods_pending, const GPUVAddr* segment);
|
||||
/// A write made by a macro: CallMethod without the method sink, as the engines' macros do.
|
||||
void MacroWrite(Tegra::Engines::EngineInterface* engine, u32 method, u32 argument);
|
||||
void MacroWrite(Tegra::Engines::EngineInterface* engine, u32 method, u32 argument) {
|
||||
SelectEngine(engine);
|
||||
u64* const entry = Reserve(1);
|
||||
entry[0] = static_cast<u64>(Kind::Method) | IsLast | Direct | (u64{method & 0xffff} << 8) |
|
||||
(u64{argument} << 32);
|
||||
Commit(1);
|
||||
}
|
||||
|
||||
/// Runs `func` on the back after everything sent before it. Must be trivially copyable.
|
||||
template <typename Func>
|
||||
|
|
@ -103,7 +122,16 @@ private:
|
|||
(*reinterpret_cast<const Func*>(payload))();
|
||||
}
|
||||
|
||||
u64* Reserve(size_t words);
|
||||
/// Room for an entry of `words` at the write position (never across the ring's end).
|
||||
u64* Reserve(size_t words) {
|
||||
const size_t index = write_position & RingMask;
|
||||
if (index + words <= RingWords && write_position + words - consumed_cache <= RingWords)
|
||||
[[likely]] {
|
||||
return &ring[index];
|
||||
}
|
||||
return ReserveSlow(words);
|
||||
}
|
||||
u64* ReserveSlow(size_t words);
|
||||
void Commit(size_t words) {
|
||||
write_position += words;
|
||||
if (write_position - published_local >= publish_every) {
|
||||
|
|
@ -111,7 +139,12 @@ private:
|
|||
}
|
||||
}
|
||||
void WaitForSpace(size_t words);
|
||||
void SelectEngine(Tegra::Engines::EngineInterface* engine);
|
||||
void SelectEngine(Tegra::Engines::EngineInterface* engine) {
|
||||
if (engine != front_engine) [[unlikely]] {
|
||||
SwitchEngine(engine);
|
||||
}
|
||||
}
|
||||
void SwitchEngine(Tegra::Engines::EngineInterface* engine);
|
||||
|
||||
void BackLoop(std::stop_token stop_token);
|
||||
size_t Execute(const u64* entry);
|
||||
|
|
@ -132,6 +165,8 @@ private:
|
|||
// Back side.
|
||||
Tegra::Engines::EngineInterface* back_engine{};
|
||||
|
||||
static GpuSplit instance;
|
||||
|
||||
alignas(64) std::atomic<u64> published{};
|
||||
alignas(64) std::atomic<u64> consumed{};
|
||||
alignas(64) std::atomic<bool> back_waiting{};
|
||||
|
|
|
|||
|
|
@ -64,9 +64,11 @@ inline constexpr std::array<const char*, static_cast<size_t>(Zone::Count)> zone_
|
|||
"fence skipped", "method", "split drain", "split wait",
|
||||
};
|
||||
|
||||
// Read once at startup: Enabled() runs several times per method write (no guard to check).
|
||||
inline const bool g_enabled = std::getenv("EDEN_NX2_GPU_PROFILE") != nullptr;
|
||||
|
||||
inline bool Enabled() {
|
||||
static const bool enabled = std::getenv("EDEN_NX2_GPU_PROFILE") != nullptr;
|
||||
return enabled;
|
||||
return g_enabled;
|
||||
}
|
||||
|
||||
// Nanoseconds; from the host's own counter where the wall clock reads it directly (the zones
|
||||
|
|
|
|||
Loading…
Reference in a new issue