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.
177 lines
6.8 KiB
C++
177 lines
6.8 KiB
C++
// 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
|