From b905580202f0da5ca0a6268b176b4591e347fd8d Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Thu, 29 Oct 2026 05:46:59 -0400 Subject: video_core: compact the macro memo arena; download profiling counters Calls stored again with another shape leave their old record in the memo's arena; title 1 filled its 16 MiB every ~2 s and clearing it made the next ~20,000 calls interpret again. The live records are now copied to a new arena (a clear only when they alone fill three quarters of it). The memo statistics count hits on the last call's slot and compactions. With EDEN_NX2_GPU_PROFILE the GPU thread also reports how much downloaded buffer data lands on pages the native MME has DMA-read. --- src/video_core/macro.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) (limited to 'src/video_core/macro.cpp') diff --git a/src/video_core/macro.cpp b/src/video_core/macro.cpp index 3422cb16fa..50e32ce999 100644 --- a/src/video_core/macro.cpp +++ b/src/video_core/macro.cpp @@ -1420,6 +1420,14 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u // from a culling dispatch) and still be only in host buffers: flush them first, // or the macro draws with an older frame's arguments (objects pop in and out). const size_t read_bytes = size_t{std::min(value, 0x1000)} * 4; + if (VideoCommon::Nx2Profile::Enabled()) { + VideoCommon::Nx2Profile::Add(VideoCommon::Nx2Profile::Counter::DmaReadBytes, read_bytes); + for (GPUVAddr page = source & ~GPUVAddr{0xfff}; page < source + read_bytes; page += 0x1000) { + if (const auto device = memory.GpuToCpuAddress(page)) { + VideoCommon::Nx2Profile::DmaReadPages().insert(*device >> 12); + } + } + } if (memory.IsMemoryDirty(source, read_bytes, VideoCommon::CacheType::BufferCache)) { memory.FlushRegion(source, read_bytes, VideoCommon::CacheType::BufferCache); static u64 flushes{}; @@ -1462,10 +1470,11 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u memo_stats && (++native_calls & 0xffffff) == 0) { const auto& stats = native_memo.GetStatistics(); LOG_INFO(HW_GPU, - "Macro memo: {} hits, {} misses, {} guard failures, {} stores ({} in " - "place), {} clears; {} entries, {} KiB", - stats.hits, stats.misses, stats.guard_failures, stats.stores, - stats.overwrites, stats.clears, native_memo.Size(), + "Macro memo: {} hits ({} on the last call's slot), {} misses, {} guard " + "failures, {} stores ({} in place), {} compactions, {} clears; {} entries, " + "{} KiB", + stats.hits, stats.slot_hits, stats.misses, stats.guard_failures, stats.stores, + stats.overwrites, stats.compactions, stats.clears, native_memo.Size(), native_memo.ArenaWords() * 4 / 1024); } native_memo.Synchronize(native_code_generation); -- cgit v1.2.3 From c0643a675b1382cfa7357626a63a623b56a72666 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Thu, 29 Oct 2026 09:39:25 -0400 Subject: video_core: compile Turing MME macro code to x86-64 Title 1 calls macros ~1.3 million times a second; most were replayed from the macro memo, whose lookups missed the CPU caches inside the GPU thread (~8%), and the rest interpreted. macro_turing_jit.h compiles all of the uploaded macro memory with Xbyak: each instruction a block falling through to the next, branches and loops through a jump table, with the interpreter's semantics (lanes reading operands before either writes, predication as two variants, the END delay slot, loops, carry). Loop checks only where a loop can end, END checks only after an END, the instruction limit counted on indirect jumps. Emits and register reads call back into C++. It is recompiled when the macro code changes and used for 3D and compute macros on x86-64; EDEN_NX2_MME_JIT=0 goes back to the memo and the interpreter. Checked against the interpreter on 200,000 recorded calls (registers, data RAM, every method write, FIFO use): identical. In the game: macro calls 148 ns instead of 174 (two profiled runs each), no failures, 0 crashes in 3 runs. NX2_GPU_THREAD_SPLIT.md describes the next step, splitting the GPU thread. --- NX2_GPU_THREAD_SPLIT.md | 53 +++ src/video_core/engines/kepler_compute.cpp | 29 ++ src/video_core/macro.cpp | 28 ++ src/video_core/macro.h | 6 + src/video_core/macro_turing_jit.h | 662 ++++++++++++++++++++++++++++++ 5 files changed, 778 insertions(+) create mode 100644 NX2_GPU_THREAD_SPLIT.md create mode 100644 src/video_core/macro_turing_jit.h (limited to 'src/video_core/macro.cpp') diff --git a/NX2_GPU_THREAD_SPLIT.md b/NX2_GPU_THREAD_SPLIT.md new file mode 100644 index 0000000000..f739704dbf --- /dev/null +++ b/NX2_GPU_THREAD_SPLIT.md @@ -0,0 +1,53 @@ +# Splitting the GPU thread (design, not implemented yet) + +Title 1 (Mario Kart World) is bound by one host thread, the GPU thread: ~100% busy while the +host GPU is 20-40% busy and the game's own CPU thread mostly polls the counter. Per second of +3D scenes (profiled, 2026-10-29): command processing and register writes ~335 ms, macros +~200 ms (1.3 million calls), draw setup ~345 ms (55,000 draws), compute ~70 ms. Speeding up the +pieces (NX2_NOTES rows 123-139) took frames from ~22-38 ms to ~18-26 ms; a locked 60 fps on the +heavy courses needs work on more than one core. + +## Split point: method calls + +- **Front thread** (the current GPU thread): takes the CPU's command queue, parses GPFIFO + entries and command lists (DmaPusher), runs macros (compiled, macro_turing_jit.h), and keeps + what macros read: a mirror of each engine's registers, its macro code and data RAM. +- **Back thread**: executes the resulting stream of method writes in order: Maxwell3D / + KeplerCompute / Fermi2D / DMA engines' CallMethod and everything behind them (register + state, dirty flags, constant buffer and inline uploads, draws, dispatches, queries, fences, + the rasterizer and its caches). Only the back thread touches the rasterizer. +- The stream: an SPSC ring of {engine, method, argument} plus copied data for multi-word + methods (command lists are often guest memory read in place, which the guest reuses). + CPU-side commands (flush/invalidate requests, RunInOrder actions, presentation) are + forwarded into the same ring so they keep their order. + +## What the front needs without waiting + +- Register values macros read (MME_READ): the front mirrors every register write it sends. + Back-side changes to registers it must reproduce: constant buffer offset (+4 per + const_buffer.buffer word), shadow_scratch[0] = 1 (falcon[4]), shadow RAM control (Track / + Replay change the effective value), macro upload pointers (the front handles load_mme + itself). Compute: native_registers (0xad is changed temporarily by the inline QMD path). +- Macro code uploads and binds (load_mme.*, compute 0x45-0x48): handled by the front. + +## Where the front waits for the back (sync points) + +- Macro DMA reads (MME_DMA_READ / _FIFOED, ~450 a second): read guest memory as of their + position, so the back must have executed everything before them. +- Semaphore acquires that are not satisfied yet (~10,500 a second, cross-channel waits on QMD + releases): the release is executed by the back, so the front waits until the semaphore + holds (or parks the channel and runs another one, as the pending-channel code does now). +- Syncpoint waits, WaitForIdle-style methods, and anything else that reads guest memory or + host state on the front. +- Expected cost: each wait drains the back only up to the dependency, then the back idles + until the front sends the next method (a few microseconds), so even ~10,000 waits a second + leave most of the overlap. Ceiling: front ~35-40% of the work, back ~60-65% -> ~1.4-1.6x. + +## Stages + +1. Instrument: count sync points per second and the front/back share precisely (zones). +2. Ring + back thread executing method writes; the front still does everything else. + Correctness first: an EDEN_NX2_SPLIT=0 switch runs both on one thread. +3. Move macros to the front with the register mirrors; verify mirrors against the back's + registers in a debug mode (compare on every MME_READ). +4. Sync points; crash and screenshot tests across courses; then title 4. diff --git a/src/video_core/engines/kepler_compute.cpp b/src/video_core/engines/kepler_compute.cpp index 786acc04de..9102a366a8 100644 --- a/src/video_core/engines/kepler_compute.cpp +++ b/src/video_core/engines/kepler_compute.cpp @@ -23,6 +23,9 @@ #include "video_core/engines/maxwell_3d.h" #include "video_core/memory_manager.h" #include "video_core/native_query_trace.h" +#ifdef ARCHITECTURE_x86_64 +#include "video_core/macro_turing_jit.h" +#endif #include "video_core/rasterizer_interface.h" #include "video_core/textures/decoders.h" @@ -171,6 +174,32 @@ bool KeplerCompute::ProcessNativeMethod(Core::System& system, u32 method, u32 ar CallMethod(system, address, value, true); return true; }; +#ifdef ARCHITECTURE_x86_64 + // Compiled macro code, as for 3D macros (macro_turing_jit.h); EDEN_NX2_MME_JIT=0 off. + static const bool use_jit = [] { + const char* value = std::getenv("EDEN_NX2_MME_JIT"); + return value == nullptr || value[0] != '0'; + }(); + if (use_jit && !watched) { + if (!native_macro->native_jit) { + native_macro->native_jit = std::make_shared(); + } + native_macro->native_jit->Prepare(native_macro->native_code, + native_macro->native_code_generation); + const auto result = native_macro->native_jit->Execute( + start, native_parameters, + std::span{*native_macro->native_interpreter.active_dram}, + [&](u32 address) { return address < native_registers.size() ? native_registers[address] : 0U; }, + emit); + if (!result.success) { + native_macro->native_execution_failed = true; + LOG_ERROR(HW_GPU, "Native MME (JIT) failed: compute start={:#x}, consumed={}/{}, error={}", + start, result.consumed, native_parameters.size(), result.error); + } + native_parameters.clear(); + return true; + } +#endif // Repeated calls replay their writes, as for 3D macros (macro_memo.h). static const bool use_memo = [] { const char* value = std::getenv("EDEN_NX2_MME_MEMO"); diff --git a/src/video_core/macro.cpp b/src/video_core/macro.cpp index 50e32ce999..74e25da067 100644 --- a/src/video_core/macro.cpp +++ b/src/video_core/macro.cpp @@ -17,6 +17,7 @@ #ifdef ARCHITECTURE_x86_64 #include "common/x64/xbyak.h" +#include "video_core/macro_turing_jit.h" #endif #include "common/assert.h" @@ -1459,6 +1460,33 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u } return true; }; +#ifdef ARCHITECTURE_x86_64 + // Macro code runs compiled to x86-64 (macro_turing_jit.h) instead of through the memo + // and the interpreter; EDEN_NX2_MME_JIT=0 goes back to those. + static const bool use_jit = [] { + const char* value = std::getenv("EDEN_NX2_MME_JIT"); + return value == nullptr || value[0] != '0'; + }(); + if (use_jit) { + if (!native_jit) { + native_jit = std::make_shared(); + } + native_jit->Prepare(native_code, native_code_generation); + fifo.assign(parameters.begin(), parameters.end()); + const auto result = native_jit->Execute( + method, fifo, std::span{*native_interpreter.active_dram}, + [&](u32 address) { + return address < Maxwell3D::Regs::NUM_REGS ? maxwell3d.GetRegisterValue(address) : 0U; + }, + emit); + if (!result.success) { + native_execution_failed = true; + LOG_ERROR(HW_GPU, "Native MME (JIT) failed: start={:#x}, consumed={}/{}, error={}", + method, result.consumed, parameters.size(), result.error); + } + return; + } +#endif // EDEN_NX2_MME_MEMO=0: interpret every call (see macro_memo.h). static const bool use_memo = [] { const char* value = std::getenv("EDEN_NX2_MME_MEMO"); diff --git a/src/video_core/macro.h b/src/video_core/macro.h index a6ca4c4bd2..0d574636e9 100644 --- a/src/video_core/macro.h +++ b/src/video_core/macro.h @@ -9,6 +9,10 @@ #include #include "video_core/macro_memo.h" #include "video_core/macro_turing.h" + +namespace Tegra { +class TuringMacroJit; +} #include #include #include @@ -257,6 +261,8 @@ struct MacroEngine { // Calls replayed instead of interpreted (macro_memo.h), and what the current call wrote. u64 native_code_generation{}; BasicMacroMemo<::Common::unordered_map> native_memo; + /// The macro code compiled to x86-64 (EDEN_NX2_MME_JIT=0 turns it off), created on first use. + std::shared_ptr native_jit; MacroMemoPairs native_writes; u64 native_calls{}; diff --git a/src/video_core/macro_turing_jit.h b/src/video_core/macro_turing_jit.h new file mode 100644 index 0000000000..d0f24080a3 --- /dev/null +++ b/src/video_core/macro_turing_jit.h @@ -0,0 +1,662 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +// Eden's Xbyak configuration (its hash maps): every translation unit must see the same one. +#include "common/x64/xbyak.h" + +#include "video_core/macro_turing.h" + +namespace Tegra { + +// x86-64 code for the TU104/GA10x MME: every instruction of the uploaded macro memory compiled +// once (each a block that falls through to the next one, branches and loops through a jump +// table), with the semantics of TuringMacroInterpreter::Execute. Title 1 calls macros ~1.3 +// million times a second; the interpreter and the macro memo's lookups were ~10% of its GPU +// thread. Emits (method writes) and register reads call back into C++. +class TuringMacroJit { +public: + using Word = std::uint32_t; + using Result = TuringMacroInterpreter::Result; + + struct State { + std::array regs; + Word carry; + Word method; + Word increment; + Word loop_count; + Word loop_start; + Word loop_end; + Word next_pc; + Word steps; + Word start; + Word consumed; + Word params_size; + Word fail_pc; + std::uint8_t end_next; + std::uint8_t stopped; + std::uint8_t cancel_end; + std::uint8_t pad; + Word scratch_x; + Word scratch_y; + const Word* params; + Word* dram; + void* user; + bool (*emit)(State*, Word method, Word value); + Word (*read)(State*, Word address); + const std::vector* fifo; + }; + + enum Error : Word { + None = 0, + FifoUnderflow, + InvalidSource, + InvalidDestination, + UnsupportedExtended, + InvalidMerge, + InvalidLoop, + DataRamReadOutOfBounds, + DataRamWriteOutOfBounds, + InvalidOutput, + UnsupportedEmit, + OutsideCode, + StepLimit, + }; + + static std::string_view ErrorText(Word error) { + constexpr std::array texts{ + "", + "parameter FIFO underflow", + "invalid source register", + "invalid destination register", + "unsupported extended operation", + "invalid merge destination", + "invalid loop", + "data RAM read out of bounds", + "data RAM write out of bounds", + "invalid output selector", + "unsupported emitted method", + "instruction outside uploaded RAM", + "instruction limit exceeded", + }; + return error < texts.size() ? texts[error] : "unknown"; + } + + /// Compiles `code` (3 words per instruction) when it differs from what was compiled. + void Prepare(std::span code, std::uint64_t generation) { + if (program && generation == compiled_generation) { + return; + } + program = std::make_unique(code); + compiled_generation = generation; + } + + template + Result Execute(Word start, std::vector& parameters, std::span data_ram, Read read, + Emit emit) { + State state; + state.regs.fill(0); + state.carry = state.method = state.increment = 0; + state.loop_count = state.loop_start = state.loop_end = 0; + state.consumed = 0; + state.fail_pc = 0; + state.end_next = state.stopped = state.cancel_end = 0; + state.start = start; + state.steps = 1000000; + state.params = parameters.data(); + state.params_size = static_cast(parameters.size()); + state.dram = data_ram.data(); + state.fifo = ¶meters; + Thunks thunks{read, emit}; + state.user = &thunks; + state.emit = &Thunks::CallEmit; + state.read = &Thunks::CallRead; + const Word error = program->entry(&state); + if (error == None) { + return {true, state.fail_pc, state.consumed, {}}; + } + return {false, state.fail_pc, state.consumed, ErrorText(error)}; + } + +private: + template + struct Thunks { + Read& read; + Emit& emit; + static bool CallEmit(State* state, Word method, Word value) { + const bool ok = static_cast(state->user)->emit(method, value); + // MME_DMA_READ_FIFOED appends to the FIFO from inside the emit. + state->params = state->fifo->data(); + state->params_size = static_cast(state->fifo->size()); + return ok; + } + static Word CallRead(State* state, Word address) { + return static_cast(state->user)->read(address); + } + }; + + struct Program : Xbyak::CodeGenerator { + explicit Program(std::span code) + : Xbyak::CodeGenerator(1 << 20, Xbyak::AutoGrow) { + Generate(code); + ready(); + entry = getCode(); + } + + Word (*entry)(State*) = nullptr; + + private: + using Decoded = TuringMacroInterpreter::Decoded; +#ifdef _WIN32 + const Xbyak::Reg64 arg0 = rcx, arg1 = rdx, arg2 = r8; +#else + const Xbyak::Reg64 arg0 = rdi, arg1 = rsi, arg2 = rdx; +#endif + // rbx: State*; r12d/r13d: the instruction's FIFO loads; r14d/r15d: its ALU results. + const Xbyak::Reg64 st = rbx; + + static constexpr std::size_t RegOffset(Word reg) { + return offsetof(State, regs) + reg * sizeof(Word); + } + + void Generate(std::span code) { + const Word count = static_cast(code.size() / 3); + labels.resize(count); + std::vector decoded(count); + for (Word pc = 0; pc < count; ++pc) { + decoded[pc] = TuringMacroInterpreter::Decode( + {code[pc * 3 + 2], code[pc * 3 + 1], code[pc * 3]}); + } + Analyze(decoded); + push(rbx); push(rbp); push(r12); push(r13); push(r14); push(r15); + sub(rsp, 40); + mov(st, arg0); + mov(eax, dword[st + offsetof(State, start)]); + cmp(eax, count); + jae(fail_outside, T_NEAR); + lea(rcx, ptr[rip + table]); + jmp(ptr[rcx + rax * 8]); + + for (Word pc = 0; pc < count; ++pc) { + L(labels[pc]); + Instruction(pc, count, decoded[pc]); + } + + // Dispatch on eax (the next pc). Every backward edge comes through here, so the + // instruction limit is counted here. + L(dispatch); + sub(dword[st + offsetof(State, steps)], 1); + jz(fail_steps, T_NEAR); + cmp(eax, count); + jae(fail_outside, T_NEAR); + lea(rcx, ptr[rip + table]); + jmp(ptr[rcx + rax * 8]); + + L(success); + xor_(eax, eax); + L(leave); + add(rsp, 40); + pop(r15); pop(r14); pop(r13); pop(r12); pop(rbp); pop(rbx); + ret(); + const auto fail_with = [&](Xbyak::Label& label, Word error) { + L(label); + mov(eax, error); + jmp(leave, T_NEAR); + }; + fail_with(fail_underflow, FifoUnderflow); + fail_with(fail_source, InvalidSource); + fail_with(fail_destination, InvalidDestination); + fail_with(fail_extended, UnsupportedExtended); + fail_with(fail_merge, InvalidMerge); + fail_with(fail_loop, InvalidLoop); + fail_with(fail_dread, DataRamReadOutOfBounds); + fail_with(fail_dwrite, DataRamWriteOutOfBounds); + fail_with(fail_output, InvalidOutput); + fail_with(fail_emit, UnsupportedEmit); + fail_with(fail_outside, OutsideCode); + fail_with(fail_steps, StepLimit); + + align(8); + L(table); + for (Word pc = 0; pc < count; ++pc) { + putL(labels[pc]); + } + } + + // Loads MME register `reg` (as an operand of `lane`) into `dst`. + void Get(const Xbyak::Reg32& dst, const Decoded& d, Word reg, Word lane) { + if (reg < 24) { + mov(dst, dword[st + RegOffset(reg)]); + return; + } + switch (reg) { + case 24: xor_(dst, dst); return; + case 25: mov(dst, Word(std::int32_t(std::int16_t(d.imm[lane])))); return; + case 26: mov(dst, Word(std::int32_t(std::int16_t(d.imm[1 - lane])))); return; + case 27: mov(dst, (d.imm[0] << 16) | d.imm[1]); return; + case 28: mov(dst, r12d); return; + case 29: mov(dst, r13d); return; + default: jmp(fail_source, T_NEAR); return; + } + } + + static bool IsFlow(Word op) { + return op >= 23 && op <= 29; + } + + // Where a loop can end (its back edge is checked only there) and which instructions can + // run right after an END (only those check for the end of the macro). + void Analyze(const std::vector& decoded) { + const Word count = static_cast(decoded.size()); + loop_end.assign(count, false); + after_end.assign(count, false); + std::vector loop_starts_of_end(count, ~0U); + const auto mark = [&](std::vector& set, std::int64_t pc) { + if (pc >= 0 && pc < count) set[static_cast(pc)] = true; + }; + for (Word pc = 0; pc < count; ++pc) { + for (Word lane = 0; lane < 2; ++lane) { + const Decoded& d = decoded[pc]; + if (d.op[lane] == 23 && d.imm[lane] >= 2 && pc + d.imm[lane] - 1 < count) { + loop_end[pc + d.imm[lane] - 1] = true; + } + } + } + for (Word pc = 0; pc < count; ++pc) { + const Decoded& d = decoded[pc]; + if (!d.end) continue; + mark(after_end, std::int64_t{pc} + 1); + for (Word lane = 0; lane < 2; ++lane) { + const Word imm = d.imm[lane]; + switch (d.op[lane]) { + case 23: mark(after_end, std::int64_t{pc} + imm); break; + case 24: mark(after_end, std::int64_t{pc} + (imm & 0x7fff)); break; + case 25: case 26: case 27: case 28: case 29: + mark(after_end, std::int64_t{pc} + (std::int32_t(imm << 19) >> 19)); + break; + default: break; + } + } + } + // An END at a loop's last instruction can be followed by the loop's second one. + for (Word pc = 0; pc < count; ++pc) { + for (Word lane = 0; lane < 2; ++lane) { + const Decoded& d = decoded[pc]; + if (d.op[lane] == 23 && d.imm[lane] >= 2) { + const Word end = pc + d.imm[lane] - 1; + if (end < count && decoded[end].end) mark(after_end, std::int64_t{pc} + 1); + } + } + } + } + + void Instruction(Word pc, Word count, const Decoded& d) { + // FIFO loads, in lane order. + for (Word i = 0; i < 2; ++i) { + const Xbyak::Reg32 load = i == 0 ? r12d : r13d; + if (!d.load[i]) { + xor_(load, load); + continue; + } + mov(eax, dword[st + offsetof(State, consumed)]); + cmp(eax, dword[st + offsetof(State, params_size)]); + jae(fail_underflow, T_NEAR); + mov(rcx, qword[st + offsetof(State, params)]); + mov(load, dword[rcx + rax * 4]); + inc(eax); + mov(dword[st + offsetof(State, consumed)], eax); + } + if (!d.pred_mode) { + Body(pc, count, d, 15); + return; + } + Xbyak::Label when_false; + Get(eax, d, d.pred_reg, 0); + test(eax, eax); + jz(when_false, T_NEAR); + Body(pc, count, d, d.mask_true); + L(when_false); + Body(pc, count, d, d.mask_false); + } + + void Body(Word pc, Word count, const Decoded& d, Word mask) { + // An inactive lane's result reads as 0 when an output selects it. + for (Word lane = 0; lane < 2; ++lane) { + if (mask & (1U << lane)) continue; + bool selected = false; + for (Word out = 0; out < 2; ++out) { + if (!(mask & (4U << out))) continue; + selected |= d.out_method[out] == lane + 1 || d.out_emit[out] == lane + 1; + } + if (selected) { + xor_(lane == 0 ? r14d : r15d, lane == 0 ? r14d : r15d); + } + } + const bool flow = ((mask & 1) && IsFlow(d.op[0])) || ((mask & 2) && IsFlow(d.op[1])); + if (flow) { + mov(dword[st + offsetof(State, next_pc)], pc + 1); + mov(byte[st + offsetof(State, cancel_end)], 0); + } + // Lane 1's operands are read before lane 0 commits its destination (or calls out). + const bool lane0_active = (mask & 1) != 0; + const bool early_operands = + (mask & 2) && lane0_active && + (d.op[0] == 22 || (d.dst[0] < 24 && (d.dst[0] == d.src_a[1] || d.dst[0] == d.src_b[1]))); + if (early_operands) { + Get(eax, d, d.src_a[1], 1); + mov(dword[st + offsetof(State, scratch_x)], eax); + Get(eax, d, d.src_b[1], 1); + mov(dword[st + offsetof(State, scratch_y)], eax); + } + for (Word lane = 0; lane < 2; ++lane) { + if (!(mask & (1U << lane))) { + continue; + } + if (lane == 1 && early_operands) { + mov(ecx, dword[st + offsetof(State, scratch_x)]); + mov(edx, dword[st + offsetof(State, scratch_y)]); + } else { + Get(ecx, d, d.src_a[lane], lane); + Get(edx, d, d.src_b[lane], lane); + } + Lane(pc, d, lane); + // eax holds the lane's value. + mov(lane == 0 ? r14d : r15d, eax); + if (d.dst[lane] < 24) { + mov(dword[st + RegOffset(d.dst[lane])], eax); + } else if (d.dst[lane] != 24) { + jmp(fail_destination, T_NEAR); + } + } + for (Word lane = 0; lane < 2; ++lane) { + if (!(mask & (4U << lane))) { + continue; + } + if (d.out_method[lane]) { + Output(eax, d, d.out_method[lane]); + mov(ecx, eax); + and_(ecx, 0xfff); + mov(dword[st + offsetof(State, method)], ecx); + shr(eax, 12); + and_(eax, 15); + mov(dword[st + offsetof(State, increment)], eax); + } + if (d.out_emit[lane]) { + Output(eax, d, d.out_emit[lane]); + mov(arg2.cvt32(), eax); + mov(arg1.cvt32(), dword[st + offsetof(State, method)]); + mov(arg0, st); + call(ptr[st + offsetof(State, emit)]); + test(al, al); + jz(fail_emit, T_NEAR); + mov(eax, dword[st + offsetof(State, increment)]); + add(dword[st + offsetof(State, method)], eax); + } + } + // An END in the previous instruction ends the macro after this one. + if (after_end[pc]) { + cmp(byte[st + offsetof(State, end_next)], 0); + jne(success, T_NEAR); + } + if (flow) { + cmp(byte[st + offsetof(State, stopped)], 0); + jne(success, T_NEAR); + } + if (d.end) { + if (flow) { + mov(al, byte[st + offsetof(State, cancel_end)]); + xor_(al, 1); + mov(byte[st + offsetof(State, end_next)], al); + } else { + mov(byte[st + offsetof(State, end_next)], 1); + } + } + if (loop_end[pc]) { + Xbyak::Label no_loop; + mov(eax, dword[st + offsetof(State, loop_count)]); + test(eax, eax); + jz(no_loop); + cmp(dword[st + offsetof(State, loop_end)], pc); + jne(no_loop); + dec(dword[st + offsetof(State, loop_count)]); + mov(eax, dword[st + offsetof(State, loop_start)]); + inc(eax); + jmp(dispatch, T_NEAR); + L(no_loop); + } + if (flow) { + mov(eax, dword[st + offsetof(State, next_pc)]); + if (pc + 1 < count) { + cmp(eax, pc + 1); + jne(dispatch, T_NEAR); + jmp(labels[pc + 1], T_NEAR); + } else { + jmp(dispatch, T_NEAR); + } + } else if (pc + 1 < count) { + jmp(labels[pc + 1], T_NEAR); + } else { + jmp(fail_outside, T_NEAR); + } + } + + void Output(const Xbyak::Reg32& dst, const Decoded& d, Word selector) { + switch (selector) { + case 1: mov(dst, r14d); return; + case 2: mov(dst, r15d); return; + case 3: mov(dst, r12d); return; + case 4: mov(dst, r13d); return; + case 5: mov(dst, d.imm[0]); return; + case 6: mov(dst, d.imm[1]); return; + case 8: mov(dst, d.imm[0] >> 12); return; + case 9: mov(dst, d.imm[1] >> 12); return; + case 10: mov(dst, (d.imm[0] << 16) | d.imm[1]); return; + default: jmp(fail_output, T_NEAR); return; + } + } + + // ecx = x, edx = y; leaves the value in eax. + void Compare(Word op) { + cmp(ecx, edx); + switch (op) { + case 17: case 25: setl(al); break; + case 18: case 26: setb(al); break; + case 19: case 27: setle(al); break; + case 20: case 28: setbe(al); break; + default: sete(al); break; + } + movzx(eax, al); + } + + void Lane(Word pc, const Decoded& d, Word lane) { + const Word imm = d.imm[lane]; + const Word op = d.op[lane]; + switch (op) { + case 0: + mov(eax, ecx); + add(eax, edx); + setc(r8b); + movzx(r8d, r8b); + mov(dword[st + offsetof(State, carry)], r8d); + return; + case 1: + lea(eax, ptr[rcx + rdx]); + add(eax, dword[st + offsetof(State, carry)]); + return; + case 2: + mov(eax, ecx); + sub(eax, edx); + setc(r8b); + movzx(r8d, r8b); + mov(dword[st + offsetof(State, carry)], r8d); + return; + case 3: + mov(eax, ecx); + sub(eax, edx); + sub(eax, dword[st + offsetof(State, carry)]); + return; + case 4: + movsxd(rax, ecx); + movsxd(rdx, edx); + imul(rax, rdx); + mov(rcx, rax); + shr(rcx, 32); + mov(dword[st + offsetof(State, carry)], ecx); + return; + case 6: + mov(eax, ecx); + mov(edx, edx); + imul(rax, rdx); + mov(rcx, rax); + shr(rcx, 32); + mov(dword[st + offsetof(State, carry)], ecx); + return; + case 5: + mov(eax, dword[st + offsetof(State, carry)]); + return; + case 7: + cmp(ecx, 0x1000); + jne(fail_extended, T_NEAR); + xor_(eax, eax); + return; + case 8: { + Xbyak::Label zero, done; + test(ecx, ecx); + jz(zero); + bsr(eax, ecx); + xor_(eax, 31); + jmp(done); + L(zero); + mov(eax, 32); + L(done); + return; + } + case 9: + mov(eax, ecx); + mov(ecx, edx); + and_(ecx, 31); + shl(eax, cl); + return; + case 10: + mov(eax, ecx); + mov(ecx, edx); + and_(ecx, 31); + shr(eax, cl); + return; + case 11: + mov(eax, ecx); + mov(ecx, edx); + and_(ecx, 31); + sar(eax, cl); + return; + case 12: mov(eax, ecx); and_(eax, edx); return; + case 13: mov(eax, ecx); and_(eax, edx); not_(eax); return; + case 14: mov(eax, ecx); or_(eax, edx); return; + case 15: mov(eax, ecx); xor_(eax, edx); return; + case 16: { + const Word dest = (imm >> 10) & 63, bit_count = (imm >> 5) & 31; + const Word source = imm & 31; + if (dest >= 32) { + jmp(fail_merge, T_NEAR); + return; + } + const Word bits_mask = (1U << bit_count) - 1; + mov(eax, ecx); + and_(eax, ~(bits_mask << dest)); + shr(edx, source); + and_(edx, bits_mask); + shl(edx, dest); + or_(eax, edx); + return; + } + case 17: case 18: case 19: case 20: case 21: + Compare(op); + neg(eax); + return; + case 22: + lea(eax, ptr[rcx + rdx]); + and_(eax, 0xffff); + mov(arg1.cvt32(), eax); + mov(arg0, st); + call(ptr[st + offsetof(State, read)]); + return; + case 23: { + if (imm < 2) { + jmp(fail_loop, T_NEAR); + return; + } + Xbyak::Label skip, done; + cmp(dword[st + offsetof(State, loop_count)], 0); + jne(fail_loop, T_NEAR); + test(ecx, ecx); + jz(skip); + dec(ecx); + mov(dword[st + offsetof(State, loop_count)], ecx); + mov(dword[st + offsetof(State, loop_start)], pc); + mov(dword[st + offsetof(State, loop_end)], pc + imm - 1); + jmp(done); + L(skip); + mov(dword[st + offsetof(State, next_pc)], pc + imm); + L(done); + xor_(eax, eax); + return; + } + case 24: + mov(dword[st + offsetof(State, next_pc)], pc + (imm & 0x7fff)); + xor_(eax, eax); + return; + case 25: case 26: case 27: case 28: case 29: { + Xbyak::Label not_taken; + Compare(op); + cmp(eax, (imm & 0x8000) ? 1 : 0); + jne(not_taken); + const auto offset = std::int32_t(imm << 19) >> 19; + if (offset == -4096) { + mov(byte[st + offsetof(State, stopped)], 1); + } else { + mov(dword[st + offsetof(State, next_pc)], Word(std::int32_t(pc) + offset)); + mov(byte[st + offsetof(State, cancel_end)], (imm & 0x4000) ? 1 : 0); + } + L(not_taken); + xor_(eax, eax); + return; + } + case 30: + cmp(ecx, 0xc00); + jae(fail_dread, T_NEAR); + mov(rax, qword[st + offsetof(State, dram)]); + mov(eax, dword[rax + rcx * 4]); + return; + case 31: + cmp(ecx, 0xc00); + jae(fail_dwrite, T_NEAR); + mov(rax, qword[st + offsetof(State, dram)]); + mov(dword[rax + rcx * 4], edx); + xor_(eax, eax); + return; + default: + xor_(eax, eax); + return; + } + } + + std::vector labels; + std::vector loop_end, after_end; + Xbyak::Label table, dispatch, success, leave; + Xbyak::Label fail_underflow, fail_source, fail_destination, fail_extended, fail_merge, + fail_loop, fail_dread, fail_dwrite, fail_output, fail_emit, fail_outside, fail_steps; + }; + + std::unique_ptr program; + std::uint64_t compiled_generation{~std::uint64_t{0}}; +}; + +} // namespace Tegra -- cgit v1.2.3 From 24d3a1dbb3f2bb6dc5b8a515c1be1f7bad956b4b Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Thu, 29 Oct 2026 09:45:30 -0400 Subject: nx2: profile engine method calls and semaphore acquires; notes 140-141 EDEN_NX2_GPU_PROFILE now times engine method calls (the work a split GPU thread's back half would run) and counts semaphore acquires and those that wait. Title 1: method calls ~343 ms a second (6.9 million), front-half work (command parsing, macros) ~333 ms, acquires ~2,700 a second of which ~220 wait. Notes 140 (macro compiler) and 141 (draw setup). --- NX2_NOTES.md | 3 +++ src/video_core/dma_pusher.cpp | 3 +++ src/video_core/engines/kepler_compute.cpp | 2 ++ src/video_core/engines/puller.cpp | 3 +++ src/video_core/macro.cpp | 5 ++++- src/video_core/nx2_profile.h | 10 ++++++++-- 6 files changed, 23 insertions(+), 3 deletions(-) (limited to 'src/video_core/macro.cpp') diff --git a/NX2_NOTES.md b/NX2_NOTES.md index c5856ab8e3..cf076db45e 100644 --- a/NX2_NOTES.md +++ b/NX2_NOTES.md @@ -201,6 +201,8 @@ With the patched decoder, games 1/3/4 only leave ~60-110 rare SIMD encodings in | 137 | 2D blits (Fermi2D) 3.6% on heavy courses | mip chains 64x64..4x1 copied near the GPU-only regions; neither image is GPU-written, so the software blitter (un)swizzles them on the CPU | open (expected for small copies; many of them) | | 138 | `async_presentation=true` crashed 4 of 8 runs ~26 s in (row 118) | retested after the fence-operation race (row 118) and translation cache race (row 125) were fixed: 0 crashes in 4 runs, screenshots correct | on in the dev config (`async_presentation\default=false`, `async_presentation=true`); every later run is a crash test for it | | 139 | buffer collector: up to 64 buffers evicted every frame, GPU-written ones downloaded with a GPU wait (~5% of the GPU thread on 8 GB) | it compared the whole device's use (textures, ~6.8 GB) with thresholds from the device size (6.14 GiB reported): aggressive every frame, while the buffers were ~200 MiB | Switch 2 titles collect on the buffers' own size (1 GiB, aggressive 2 GiB) and keep GPU-written buffers outside aggressive mode (c4eaa4b): 0%, presents no longer blocked on those courses (22-26 ms in t23) | +| 140 | macros ~20% of the GPU thread; the memo's lookups (~76 ns a call in the game, 48 ns in a warm harness) missed the CPU caches | memo records and its map fall out of cache between calls; 4-way last-call slots raised slot hits 48% -> 63% but saved nothing | x86-64 compiler for the Turing MME (a95cedd, `macro_turing_jit.h`, `EDEN_NX2_MME_JIT=0` off): identical to the interpreter on 200,000 recorded calls (`C:\dev\nx2work\mme_test\jit_test.cpp`); in the game 148 ns a call instead of 174, 0 failures, 0 crashes in 3 runs | +| 141 | draw setup ~6.4 us a draw (55,000 a second) | spread over ~12 steps: uniform buffer copies (2.4%, the fast path streams them every draw and bypasses write tracking), texture views (8.5% incl. ~3.6% streaming new course textures), handle reads, vertex/storage buffer lookups | no single cache-able step; open: small per-step trims. The way past it is a second thread (NX2_GPU_THREAD_SPLIT.md) | 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,6 +216,7 @@ faulting stack as module+offset, then a minidump), `EDEN_NX2_AUTO_STUB`, `EDEN_N `EDEN_NX2_MME_MEMO_STATS` (memo counters), `EDEN_NX2_BUFFER_DOWNLOADS=0/1`, `EDEN_NX2_INLINE_DEFER=0`, `EDEN_NX2_TC_MIN_AGE=` (rows 131, 134, 133), `EDEN_NX2_SKIP_SAME=0` (row 135), +`EDEN_NX2_MME_JIT=0` (row 140), `EDEN_NX2_FRAME_CAPTURE=` (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/dma_pusher.cpp b/src/video_core/dma_pusher.cpp index 3cc5afe967..44a63d789a 100644 --- a/src/video_core/dma_pusher.cpp +++ b/src/video_core/dma_pusher.cpp @@ -12,6 +12,7 @@ #include "common/settings.h" #include "core/core.h" #include "core/memory.h" +#include "video_core/nx2_profile.h" #include "video_core/dma_pusher.h" #include "video_core/control/channel_state.h" #include "video_core/engines/maxwell_3d.h" @@ -335,6 +336,7 @@ void DmaPusher::CallMethod(u32 argument) { if (!subchannel->execution_mask[dma_state.method]) { subchannel->method_sink.emplace_back(dma_state.method, argument); } else { + VideoCommon::Nx2Profile::Scope method_scope{VideoCommon::Nx2Profile::Zone::Method}; subchannel->ConsumeSink(system); subchannel->current_dma_segment = dma_state.dma_get + dma_state.dma_word_offset; subchannel->CallMethod(system, dma_state.method, argument, dma_state.is_last_call); @@ -356,6 +358,7 @@ void DmaPusher::CallMultiMethod(const u32* base_start, u32 num_methods) { ReportUnboundSubchannel(); return; } + VideoCommon::Nx2Profile::Scope method_scope{VideoCommon::Nx2Profile::Zone::Method}; subchannel->ConsumeSink(system); subchannel->current_dma_segment = dma_state.dma_get + dma_state.dma_word_offset; subchannel->CallMultiMethod(system, dma_state.method, base_start, num_methods, dma_state.method_count); diff --git a/src/video_core/engines/kepler_compute.cpp b/src/video_core/engines/kepler_compute.cpp index 9102a366a8..ed4e9674b6 100644 --- a/src/video_core/engines/kepler_compute.cpp +++ b/src/video_core/engines/kepler_compute.cpp @@ -17,6 +17,7 @@ #include "common/logging.h" #include "shader_recompiler/exception.h" #include "core/core.h" +#include "video_core/nx2_profile.h" #include "video_core/nx2.h" #include "video_core/engines/fermi_2d.h" #include "video_core/engines/kepler_compute.h" @@ -171,6 +172,7 @@ bool KeplerCompute::ProcessNativeMethod(Core::System& system, u32 method, u32 ar if (watched && log_emits) LOG_INFO(HW_GPU, "Semaphore watch: compute macro emit {:#x} = {:#x}", address, value); if (diagnostic_writes++ < 4096) LOG_DEBUG(HW_GPU, "Native compute MME emit method={:#x}, value={:#x}", address, value); if (address >= 0xe00) return false; + VideoCommon::Nx2Profile::Scope method_scope{VideoCommon::Nx2Profile::Zone::Method}; CallMethod(system, address, value, true); return true; }; diff --git a/src/video_core/engines/puller.cpp b/src/video_core/engines/puller.cpp index b607f20161..c9073f1eb0 100644 --- a/src/video_core/engines/puller.cpp +++ b/src/video_core/engines/puller.cpp @@ -11,6 +11,7 @@ #include "common/logging.h" #include "common/settings.h" #include "core/core.h" +#include "video_core/nx2_profile.h" #include "video_core/control/channel_state.h" #include "video_core/dma_pusher.h" #include "video_core/engines/fermi_2d.h" @@ -217,6 +218,7 @@ void Puller::ProcessSemaphoreTriggerMethod(DmaPusher& dma_pusher) { LOG_ERROR(HW_GPU, "Invalid semaphore operation {}", static_cast(op)); return; } + VideoCommon::Nx2Profile::Add(VideoCommon::Nx2Profile::Counter::AcquireChecks, 1); if (acquire.Matches(ReadSemaphoreWord(dma_pusher, acquire.address))) { return; } @@ -229,6 +231,7 @@ void Puller::ProcessSemaphoreTriggerMethod(DmaPusher& dma_pusher) { dma_pusher.rasterizer->ReleaseFences(); if (!waiters.Latched(this) && !acquire.Matches(ReadSemaphoreWord(dma_pusher, acquire.address))) { pending_acquire = acquire; + VideoCommon::Nx2Profile::Add(VideoCommon::Nx2Profile::Counter::AcquireWaits, 1); } else { waiters.Remove(this); } diff --git a/src/video_core/macro.cpp b/src/video_core/macro.cpp index 74e25da067..485b8eaf10 100644 --- a/src/video_core/macro.cpp +++ b/src/video_core/macro.cpp @@ -1405,7 +1405,10 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u modes[0], modes[1], modes[2], modes[3]); } } - maxwell3d.CallMethod(system, address, value, true); + { + VideoCommon::Nx2Profile::Scope method_scope{VideoCommon::Nx2Profile::Zone::Method}; + maxwell3d.CallMethod(system, address, value, true); + } // Turing MME memory reads (methods 0x550 SET_MME_MEM_ADDRESS_A/B, 0x558 data RAM // address, 0x55c MME_DMA_READ into data RAM, 0x560 MME_DMA_READ_FIFOED into // the load FIFO, which the macro then reads after a load barrier). diff --git a/src/video_core/nx2_profile.h b/src/video_core/nx2_profile.h index ccedb7b77d..0c071c85d3 100644 --- a/src/video_core/nx2_profile.h +++ b/src/video_core/nx2_profile.h @@ -48,6 +48,7 @@ enum class Zone : u32 { TextureUpload, Fence, ///< Fences made (SignalFence) FenceSkipped, ///< Empty fences not made + Method, ///< Engine method calls (what a split GPU thread's back half would run) Count, }; @@ -58,7 +59,7 @@ inline constexpr std::array(Zone::Count)> zone_ "sync descriptors", "read handles", "fill image views", "texture buffers", "update buffers", "descriptor acquire", "bind stage buffers", "push descriptors", "render targets", "configure draw", "dynamic state", "draw record", "texture upload", "fence", - "fence skipped", + "fence skipped", "method", }; inline bool Enabled() { @@ -75,7 +76,7 @@ inline u64 Now() { struct ThreadData { std::array(Zone::Count)> exclusive_ns{}; std::array(Zone::Count)> count{}; - std::array counters{}; ///< See Counter + std::array counters{}; ///< See Counter u64 child_ns{}; ///< Time of the zones nested in the current one u64 window_start{}; }; @@ -123,6 +124,8 @@ enum class Counter : u32 { DownloadBytesOnDmaPages, ///< Of DownloadBytes: on pages the MME has DMA-read MethodWrites, ///< 3D register writes (Maxwell3D::CallMethod below the macro range) SameValueWrites, ///< Of MethodWrites: the register already held the value + AcquireChecks, ///< Semaphore acquires evaluated + AcquireWaits, ///< Of AcquireChecks: not met yet (the channel waits) }; /// Device pages (4 KiB) the native MME has DMA-read since start (GPU thread only; filled only @@ -179,6 +182,9 @@ inline void Report() { line += fmt::format(" | 3D register writes {:.0f} ({:.0f} same value)", static_cast(data.counters[4]) * 1e9 / window, static_cast(data.counters[5]) * 1e9 / window); + line += fmt::format(" | acquires {:.0f} ({:.0f} waited)", + static_cast(data.counters[6]) * 1e9 / window, + static_cast(data.counters[7]) * 1e9 / window); LOG_INFO(HW_GPU, "GPU thread per second:{}", line); data = {}; data.window_start = now; -- cgit v1.2.3