aboutsummaryrefslogtreecommitdiff
path: root/src/video_core/gpu_split.h
diff options
context:
space:
mode:
authorDjihads <76681596+Djihads80@users.noreply.github.com>2026-10-30 11:33:03 +0100
committerGitHub <noreply@github.com>2026-10-30 11:33:03 +0100
commit9560dec549134cf50a7bcde3f86f87ed7658b25f (patch)
tree75493cfac29ed9d1dc350658e17c366f7e10a3c4 /src/video_core/gpu_split.h
parent666d2447639594c7cd2b184acf2117b38766ac22 (diff)
parent937bb4dd30401a339a062386c121b9714e33e38d (diff)
Merge pull request #26 from Djihads80/nx2-experimental
Nx2 experimental
Diffstat (limited to 'src/video_core/gpu_split.h')
-rw-r--r--src/video_core/gpu_split.h177
1 files changed, 177 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..d6f91e4141
--- /dev/null
+++ b/src/video_core/gpu_split.h
@@ -0,0 +1,177 @@
+// 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.
+// 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() {
+ return instance;
+ }
+
+ /// 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) {
+ 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) {
+ 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>
+ 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))();
+ }
+
+ /// 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) {
+ Publish();
+ }
+ }
+ void WaitForSpace(size_t words);
+ 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);
+
+ 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{};
+
+ static GpuSplit instance;
+
+ 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