diff options
| author | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-10-29 11:16:43 -0400 |
|---|---|---|
| committer | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-10-29 20:49:48 -0400 |
| commit | 7a251d3a92f870649dab6068007ca3a1c910bbb5 (patch) | |
| tree | 556633cb13fc4dc2f181f33989a3395919886b78 /src/video_core/gpu_split.h | |
| parent | 00c059cd39f4fd9d1244345d6727a171acec4f86 (diff) | |
video_core: split the GPU thread in two (EDEN_NX2_SPLIT, off by default)
The GPU thread (front) takes the CPU's commands, parses command lists
and runs the puller; engine method calls and everything behind them
(registers, draws, dispatches, the rasterizer and its caches) run in
order on a second thread (back), fed through a single-producer
single-consumer ring (gpu_split.h). Work the front did on back-owned
state (end of command list flushes, channel binds, semaphore releases,
syncpoint increments, flush/invalidate requests, presentation) is
deferred into the same ring; the front waits for the back only where it
needs a result from that state: semaphore acquires and MME DMA reads.
EDEN_NX2_SPLIT=1 runs macros on the back. EDEN_NX2_SPLIT=2 runs the 3D
and compute macros on the front against copies of the registers they
read (Maxwell3D/KeplerCompute Front*), drops state writes that keep a
register's value there, and sends the rest. EDEN_NX2_SPLIT_VERIFY=1
compares the copies with the back's registers every 4096 macro calls
(0 differences in 40,193 checks in title 1); EDEN_NX2_SPLIT_CHECK=1
logs the stack of any other thread using the Vulkan scheduler while the
back runs. Switch 2 titles, Vulkan, non-accurate GPU only.
Title 1, 3D attract loop: ~44,000 draws a second on one thread, ~47,400
with EDEN_NX2_SPLIT=1, 50,000-66,500 with EDEN_NX2_SPLIT=2.
Diffstat (limited to 'src/video_core/gpu_split.h')
| -rw-r--r-- | src/video_core/gpu_split.h | 142 |
1 files changed, 142 insertions, 0 deletions
diff --git a/src/video_core/gpu_split.h b/src/video_core/gpu_split.h new file mode 100644 index 0000000000..61644b9b80 --- /dev/null +++ b/src/video_core/gpu_split.h @@ -0,0 +1,142 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <atomic> +#include <cstring> +#include <functional> +#include <memory> +#include <thread> +#include <type_traits> + +#include "common/common_types.h" + +namespace Core { +class System; +} + +namespace Tegra::Engines { +class EngineInterface; +} + +namespace VideoCommon { + +// Splits the GPU thread in two (NX2_GPU_THREAD_SPLIT.md). The GPU thread (the front) takes the +// CPU's commands, parses command lists and runs the puller; engine method calls and everything +// behind them (register state, macros, draws, the rasterizer and its caches) run in order on a +// 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. +class GpuSplit { +public: + static GpuSplit& Get(); + + /// Whether the front sends work to the back (front thread only). + [[nodiscard]] bool Active() const { + return active; + } + /// Whether the front runs the engines' macros (EDEN_NX2_SPLIT=2). + [[nodiscard]] bool FrontMacros() const { + return front_macros; + } + + /// Starts the back thread once the split is wanted (EDEN_NX2_SPLIT=1, a Switch 2 title, + /// commands read without cache flushes). Called by the front between commands. + void MaybeStart(Core::System& system); + void Stop(); + + /// 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); + 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); + + /// Runs `func` on the back after everything sent before it. Must be trivially copyable. + template <typename Func> + void Defer(const Func& func) { + static_assert(std::is_trivially_copyable_v<Func> && alignof(Func) <= alignof(u64)); + constexpr size_t payload = (sizeof(Func) + sizeof(u64) - 1) / sizeof(u64); + u64* const entry = Reserve(2 + payload); + entry[0] = static_cast<u64>(Kind::Call) | (u64{payload} << 8); + entry[1] = reinterpret_cast<u64>(&CallThunk<Func>); + std::memcpy(entry + 2, &func, sizeof(Func)); + Commit(2 + payload); + } + void DeferFunction(std::function<void()>&& func); + + /// EDEN_NX2_SPLIT_CHECK=1 (debug): reports, with its stack, a thread other than the back + /// using back-owned state (the Vulkan scheduler) while the back runs entries. + static inline bool check_owner{}; + static void CheckOwner(const char* where); + + /// Makes what was sent visible to the back. + void Publish(); + /// Waits until the back executed everything sent so far. + void Drain(); + +private: + enum class Kind : u64 { + Wrap, ///< The rest of the ring is unused: continue at its start + Engine, ///< The engine the next methods go to: word 1 + Method, ///< Bits 8-23 method, 32-63 argument; flags IsLast, HasSegment (word 1) + Multi, ///< Bits 8-23 method, 32-63 amount; word 1 methods pending, [segment], words + Call, ///< Bits 8-31 payload size in words; word 1 thunk, then the payload + }; + static constexpr u64 IsLast = 1 << 4; + static constexpr u64 HasSegment = 1 << 5; + static constexpr u64 Direct = 1 << 6; ///< Method: call it even if not executable (macros) + + static constexpr size_t RingWords = size_t{1} << 20; // 8 MiB + static constexpr size_t RingMask = RingWords - 1; + static constexpr u32 MaxMultiWords = 8192; + static constexpr size_t PublishEvery = 256; + + template <typename Func> + static void CallThunk(const u64* payload) { + (*reinterpret_cast<const Func*>(payload))(); + } + + u64* Reserve(size_t words); + void Commit(size_t words) { + write_position += words; + if (write_position - published_local >= publish_every) { + Publish(); + } + } + void WaitForSpace(size_t words); + void SelectEngine(Tegra::Engines::EngineInterface* engine); + + void BackLoop(std::stop_token stop_token); + size_t Execute(const u64* entry); + + Core::System* system{}; + bool active{}; + bool front_macros{}; + std::unique_ptr<u64[]> ring; + + // Front side. + u64 write_position{}; + u64 published_local{}; + u64 consumed_cache{}; + /// Words between publishes: small right after a drain, while the back has nothing to do. + size_t publish_every{PublishEvery}; + Tegra::Engines::EngineInterface* front_engine{}; + + // Back side. + Tegra::Engines::EngineInterface* back_engine{}; + + alignas(64) std::atomic<u64> published{}; + alignas(64) std::atomic<u64> consumed{}; + alignas(64) std::atomic<bool> back_waiting{}; + std::atomic<u32> wake{}; + std::jthread back_thread; +}; + +} // namespace VideoCommon |
