aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NX2_GPU_THREAD_SPLIT.md3
-rw-r--r--NX2_NOTES.md3
-rw-r--r--src/video_core/engines/maxwell_3d.cpp4
-rw-r--r--src/video_core/gpu_split.cpp36
-rw-r--r--src/video_core/gpu_split.h51
-rw-r--r--src/video_core/nx2_profile.h6
6 files changed, 58 insertions, 45 deletions
diff --git a/NX2_GPU_THREAD_SPLIT.md b/NX2_GPU_THREAD_SPLIT.md
index 2807b930dc..75fd0e4105 100644
--- a/NX2_GPU_THREAD_SPLIT.md
+++ b/NX2_GPU_THREAD_SPLIT.md
@@ -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.
diff --git a/NX2_NOTES.md b/NX2_NOTES.md
index e24a87f774..620708a59e 100644
--- a/NX2_NOTES.md
+++ b/NX2_NOTES.md
@@ -206,6 +206,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),
@@ -220,7 +221,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;
diff --git a/src/video_core/engines/maxwell_3d.cpp b/src/video_core/engines/maxwell_3d.cpp
index 10527c2473..6fdc6a74b9 100644
--- a/src/video_core/engines/maxwell_3d.cpp
+++ b/src/video_core/engines/maxwell_3d.cpp
@@ -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);
}
diff --git a/src/video_core/gpu_split.cpp b/src/video_core/gpu_split.cpp
index 970fc0b559..07fda589ff 100644
--- a/src/video_core/gpu_split.cpp
+++ b/src/video_core/gpu_split.cpp
@@ -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);
diff --git a/src/video_core/gpu_split.h b/src/video_core/gpu_split.h
index 61644b9b80..d6f91e4141 100644
--- a/src/video_core/gpu_split.h
+++ b/src/video_core/gpu_split.h
@@ -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{};
diff --git a/src/video_core/nx2_profile.h b/src/video_core/nx2_profile.h
index 0ff69f9631..a921b72082 100644
--- a/src/video_core/nx2_profile.h
+++ b/src/video_core/nx2_profile.h
@@ -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