From 51207c73c889dbdc4a586a5da23314f6d33e4eb9 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:26:16 -0400 Subject: Switch 2: Turing macros, native compute, Ampere shader lowering, native tiling From changes.patch: the TU104/GA10x MME interpreter (shared macro context for the process's channels, also on the compute class), native compute with QMD 3.0 dependency scheduling, the Ampere-to-Maxwell shader lowering, native vertex stream sizes, and the console's GOB swizzle for textures, DMA copies and the host unswizzle shaders. Switch 2 channels turn all of it on at init. The lowering is widened for title 4 (all 32 of its programs lower): general TEX (any dimension/LOD/mask, operand split 1+1/2+1/2+2/4+n, any but consistent descriptor buffer), cube/array, SULD channel masks, 128-bit STG, indexed IPA, VOTE, SHFL with any immediate or predicate output, R2P/P2R byte select, uniform ULOP3/ULEA/UBREV/UFLO.SH/UISETP signed/UMOV URZ, IMNMX cbuf, IADD3 and FFMA uniform operands, FMUL /4 /8 x8, and BSSY/BSYNC/BREAK/WARPSYNC as no-ops. The capture stops at unmapped memory. EDEN_NX2_SHADER_DUMP dumps every native program; tools in C:\dev\nx2work test the lowering offline. --- src/video_core/macro.cpp | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) (limited to 'src/video_core/macro.cpp') diff --git a/src/video_core/macro.cpp b/src/video_core/macro.cpp index 86f528af15..a8acd81682 100644 --- a/src/video_core/macro.cpp +++ b/src/video_core/macro.cpp @@ -4,6 +4,8 @@ // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include #include #include #include @@ -1339,6 +1341,34 @@ static void Dump(u64 hash, std::span code, bool decompiled = false) { void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u32 method, std::span parameters) { + if (native_turing) { + maxwell3d.RefreshParameters(); + const auto context = reinterpret_cast(this); + static std::unordered_set captured_contexts; + if (std::getenv("EDEN_NATIVE_FRAME_CAPTURE") && captured_contexts.insert(context).second) { + std::ofstream dump("native-graphics-mme-" + std::to_string(context) + ".bin", std::ios::binary); + dump.write(reinterpret_cast(native_code.data()), sizeof(native_code)); + } + LOG_DEBUG(HW_GPU, "Native MME context={:#x}", context); + LOG_DEBUG(HW_GPU, "Native MME start={:#x}, parameters={}, data={:08x}", method, parameters.size(), fmt::join(parameters, ",")); + const auto result = native_interpreter.Execute(native_code, method, parameters, + [&](u32 address) { return address < Maxwell3D::Regs::NUM_REGS ? maxwell3d.GetRegisterValue(address) : 0U; }, + [&](u32 address, u32 value) { + if (address >= 0xe00) return false; + maxwell3d.CallMethod(system, address, value, true); + return true; + }); + if (!result.success) { + native_execution_failed = true; + if (std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS")) { + std::ofstream dump("native-mme-" + std::to_string(method) + ".bin", std::ios::binary); + dump.write(reinterpret_cast(native_code.data()), sizeof(native_code)); + } + LOG_ERROR(HW_GPU, "Native MME failed: start={:#x}, pc={:#x}, consumed={}/{}, error={}", + method, result.pc, result.consumed, parameters.size(), result.error); + } + return; + } const auto execute_variant = [&system, &maxwell3d, ¶meters, method](AnyCachedMacro& cached) { if (std::holds_alternative(cached) || -- cgit v1.2.3 From a5e528e2d0bc784ebf24850e5913cece912c685e Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:24:14 -0400 Subject: nx2: get title 4 past the difficulty confirm - MME: implement MME_DMA_READ / MME_DMA_READ_FIFOED (Turing macros read query results into their load FIFO; the conditional-render macro at 0x142 needs it). - vi2 present waits up to 4 s for the frame fence (was 250 ms) so shader compile stalls don't let the game lap its equal-value gate semaphores. - Native QMD: fire the third release slot at word 52, which NVN2 patches for queue-to-queue syncs (compute queue releasing a gate the 3D queue waits on). - Compute class SET_REPORT_SEMAPHORE (0x6c0-0x6c3) releases. - Ampere lowering: AOFFI, any LOP3 truth table, STG RZ, LDG 0x981, IABS, I2FP rounding, IMAD negated addend, SHFL 0x989 (all 77 title-4 shaders lower). - Debug: EDEN_NX2_SEM_WATCH= traces acquires, releases, method arguments and compute macro emits for one semaphore block; EDEN_NX2_KEY_SCRIPT scripted key presses; per-submit logs moved to TRACE. --- src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp | 4 +- src/core/hle/service/nx2_gsv.h | 10 +- .../frontend/ampere/lower_to_maxwell.h | 144 ++++++++++++++------- src/video_core/dma_pusher.cpp | 28 ++++ src/video_core/dma_pusher.h | 1 + src/video_core/engines/kepler_compute.cpp | 63 +++++++++ src/video_core/engines/kepler_compute.h | 1 + src/video_core/engines/maxwell_3d.h | 5 + src/video_core/engines/puller.cpp | 22 +++- src/video_core/macro.cpp | 35 ++++- src/video_core/macro_turing.h | 4 +- src/video_core/native_qmd.h | 20 +-- src/video_core/query_cache/query_cache.h | 7 +- src/video_core/renderer_vulkan/vk_rasterizer.cpp | 8 ++ src/yuzu_cmd/yuzu.cpp | 46 +++++++ 15 files changed, 334 insertions(+), 64 deletions(-) (limited to 'src/video_core/macro.cpp') diff --git a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp index cf3948c321..2a144cc7ab 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp +++ b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp @@ -421,7 +421,7 @@ NvResult nvhost_gpu::SubmitGPFIFOImpl(IoctlSubmitGpfifo& params, Tegra::CommandL if (g_nx2_driver) { for (const auto& entry : entries.command_lists) { - LOG_DEBUG(Service_NVDRV, "nx2 GPFIFO entry raw={:#018x} addr={:#x} size={:#x}", entry.raw, + LOG_TRACE(Service_NVDRV, "nx2 GPFIFO entry raw={:#018x} addr={:#x} size={:#x}", entry.raw, entry.addr.Value(), entry.size.Value()); } static const bool check = std::getenv("EDEN_NX2_PB_CHECK") != nullptr; @@ -471,7 +471,7 @@ NvResult nvhost_gpu::SubmitGPFIFOImpl(IoctlSubmitGpfifo& params, Tegra::CommandL } if (g_nx2_driver) { - LOG_DEBUG(Service_NVDRV, + LOG_TRACE(Service_NVDRV, "nx2 SubmitGPFIFO entries={} flags={:#x} fence_out=({}, {}) guest_value={}", params.num_entries, flags.raw, params.fence.id, params.fence.value, system.Host1x().GetSyncpointManager().GetGuestSyncpointValue(params.fence.id)); diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h index 8073e0bd96..89f3a9fd50 100644 --- a/src/core/hle/service/nx2_gsv.h +++ b/src/core/hle/service/nx2_gsv.h @@ -580,17 +580,25 @@ private: // free, so pace the game here instead: wait (bounded, in case the GPU waits on the CPU) until // Eden's GPU thread has processed the frame. Otherwise the game laps the GPU thread and // rewrites command memory it hasn't read yet. + u32 m_wait_timeouts{}; + void WaitForFrame(u32 fence_id, u32 fence_value) { if (fence_id >= 192) { return; } auto& syncpoints = system.Host1x().GetSyncpointManager(); const auto start = std::chrono::steady_clock::now(); - const auto deadline = start + std::chrono::milliseconds{250}; + // Long enough to cover shader/pipeline compile stalls: a game that gets more than a few + // command buffers ahead misses the equal-value gates its other channels acquire. + const auto deadline = start + std::chrono::milliseconds{4000}; while (!syncpoints.IsReadyGuest(fence_id, fence_value) && std::chrono::steady_clock::now() < deadline && system.IsPoweredOn()) { std::this_thread::sleep_for(std::chrono::microseconds{200}); } + if (!syncpoints.IsReadyGuest(fence_id, fence_value) && m_wait_timeouts++ < 64) { + LOG_WARNING(Service, "nx2:vi2 present #{} fence ({}, {}) not reached in 4 s (now {})", + m_presents, fence_id, fence_value, syncpoints.GetGuestSyncpointValue(fence_id)); + } if (m_presents <= 8 || (m_presents % 600) == 0) { LOG_INFO(Service, "nx2:vi2 present #{} fence ({}, {}) now {}, waited {} us", m_presents, fence_id, fence_value, syncpoints.GetGuestSyncpointValue(fence_id), diff --git a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h index 2c3ef28751..01dc1d8425 100644 --- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h +++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h @@ -53,7 +53,7 @@ inline LoweredProgram LowerToMaxwell(std::span code) { for (const auto& instruction : code) { const u32 op = instruction.Bits(0, 12); uses_uniform_bank |= op == 0x3c4 || op == 0x2bd || op == 0xc0c || op == 0x986 || op == 0x9a8 || op == 0xc12 || op == 0xc17 || op == 0xe24 || op == 0xc24 || op == 0xd00 || op == 0xd09 || op == 0x9c3 || op == 0xc45 || op == 0x2a4 || op == 0x891 || op == 0x890 || op == 0x290 || op == 0x28c || op == 0x88c || op == 0x287 || op == 0x887 || op == 0x958 || op == 0xab9 || op == 0xabb || op == 0x899 || op == 0xc20 || op == 0xe21 || op == 0xe23 || op == 0xc02 || - op == 0x886 || op == 0x3c2 || op == 0xc82 || op == 0x882 || op == 0x292 || op == 0x892 || op == 0x291 || op == 0xc10 || op == 0xc23 || op == 0x526 || op == 0x2be || (op == 0x81c && instruction.Bits(67, 1)); + op == 0x886 || op == 0x3c2 || op == 0xc82 || op == 0x882 || op == 0x292 || op == 0x892 || op == 0x291 || op == 0x981 || op == 0xc13 || op == 0xc10 || op == 0xc23 || op == 0x526 || op == 0x2be || (op == 0x81c && instruction.Bits(67, 1)); } const auto address_of = [](u32 index) { return 0x50U + index * 8 + ((index + 1) / 3) * 8; }; const auto fail = [&](u32 index, std::string_view reason) { @@ -454,16 +454,23 @@ inline LoweredProgram LowerToMaxwell(std::span code) { } break; } - case 0x824: { // Captured low-word IMAD forms - if (insn.words[1] == 1 && (insn.words[2] & 0xffffff00U) == 0x078e0200) { + case 0x824: { // Captured low-word IMAD forms; bit 75 negates the addend. + const bool negate_c = insn.Bits(75, 1) != 0; + if (insn.words[1] == 1 && (insn.words[2] & 0xfffff700U) == 0x078e0200) { emit(base(0x5c10000000000000ULL) | (u64{insn.Bits(24, 8)} << 8) | - (u64{insn.Bits(64, 8)} << 20)); + (u64{insn.Bits(64, 8)} << 20) | (u64{negate_c} << 48)); break; } if (insn.words[1] != 48 || insn.words[2] != 0x078e02ff) { - if ((insn.words[2] & ~0x2ffU) != 0x078e0000) + if ((insn.words[2] & ~0xaffU) != 0x078e0000) return fail(index, "native integer multiply mode"); - multiply_low(insn.Bits(24, 8), insn.words[1], insn.Bits(64, 8), true); + u32 c = insn.Bits(64, 8); + if (negate_c && c != 255) { + emit(0x5c10000000000000ULL | predicate | 252 | (u64{255} << 8) | (u64{c} << 20) | + (u64{1} << 48)); + c = 252; + } + multiply_low(insn.Bits(24, 8), insn.words[1], c, true); break; } const u64 source = insn.Bits(24, 8); @@ -631,6 +638,14 @@ inline LoweredProgram LowerToMaxwell(std::span code) { (u64{offset} << 36) | (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51)); break; } + case 0x989: { // SHFL, immediate lane and register clamp/segmentation mask, any mode. + if ((insn.words[1] & 0xf01fffffU) || (insn.words[2] & ~0x000e00ffU)) + return fail(index, "native register-mask shuffle mode"); + emit(base(0xef10000000000000ULL) | (u64{insn.Bits(24, 8)} << 8) | + (u64{insn.Bits(53, 5)} << 20) | (u64{1} << 28) | (u64{insn.Bits(58, 2)} << 30) | + (u64{insn.Bits(64, 8)} << 39) | (u64{insn.Bits(81, 3)} << 48)); + break; + } case 0x589: { // SHFL.IDX, register lane and immediate segmentation mask. if ((insn.words[2] & ~0x000e0000U) || insn.Bits(53, 11)) return fail(index, "native shuffle mode"); @@ -882,22 +897,50 @@ inline LoweredProgram LowerToMaxwell(std::span code) { emit(0x38f0000000070000ULL | (u64{253} << 8) | (u64{mask} << 20)); break; } - case 0x381: { // LDG.E of 32/64/128 bits through a 64-bit register address. + case 0x381: case 0x981: { // LDG.E of 32/64/128 bits, register or uniform 64-bit address. const u32 size = insn.Bits(73, 3); - if ((insn.words[2] & ~0xe00U) != 0x001e8100 || size < 4 || size > 6 || - insn.Bits(32, 8) || (insn.Bits(24, 8) & 1) || insn.Bits(24, 8) >= 246 || + u32 address = insn.Bits(24, 8); + if (size < 4 || size > 6 || (opcode == 0x381 && insn.Bits(32, 8)) || (size == 5 && (dest & 1)) || (size == 6 && (dest & 3))) return fail(index, "native global load mode"); - emit(base(0xeed0000000000000ULL) | (u64{insn.Bits(24, 8)} << 8) | + if (opcode == 0x381) { + if ((insn.words[2] & ~0xe00U) != 0x001e8100 || (address & 1) || address >= 246) + return fail(index, "native global load mode"); + } else { + // The uniform base sits where a register source would (bits 32-37). + const u32 uniform_address = + insn.Bits(38, 2) ? ~0U : uniform_register(insn.Bits(32, 6)); + if ((insn.words[2] & ~(0x04000000U | 0x3fU | 0xe00U | 0x2000U)) != 0x081e8100 || + uniform_address == ~0U || (uniform_address & 1)) + return fail(index, "native global load uniform addressing mode"); + if (address == 255) { + address = uniform_address; + } else { + if (insn.Bits(90, 1) && ((address & 1) || address >= 246)) + return fail(index, "native global load register address pair"); + emit(0x5c10000000000000ULL | predicate | 250 | (u64{uniform_address} << 8) | + (u64{address} << 20) | (u64{1} << 47)); + emit(0x5c10000000000000ULL | predicate | 251 | (u64{uniform_address + 1} << 8) | + (u64{insn.Bits(90, 1) ? address + 1 : 255} << 20) | (u64{1} << 43)); + address = 250; + } + } + emit(base(0xeed0000000000000ULL) | (u64{address} << 8) | (u64{insn.Bits(40, 24)} << 20) | (u64{1} << 45) | (u64{size} << 48)); break; } case 0x386: case 0x986: { // Global store of 32/64/128 bits, 64-bit address. - const u32 data = insn.Bits(32, 8); + u32 data = insn.Bits(32, 8); const u32 size = insn.Bits(73, 3); - if (size < 4 || size > 6 || (size == 5 && (data & 1)) || (size == 6 && (data & 3)) || - data + (1U << (size - 4)) > 246) + if (size < 4 || size > 6 || + (data != 255 && ((size == 5 && (data & 1)) || (size == 6 && (data & 3)) || + data + (1U << (size - 4)) > 246))) return fail(index, "native global store size"); + if (data == 255 && size > 4) { + // Zeros wider than one register: store from zeroed scratch registers. + for (u32 i = 0; i < (1U << (size - 4)); ++i) mov_imm(250 + i, 0); + data = 250; + } u32 address = insn.Bits(24, 8); bool extended = insn.Bits(72, 1) != 0; if (opcode == 0x986) { @@ -1060,13 +1103,14 @@ inline LoweredProgram LowerToMaxwell(std::span code) { const auto general_tex = [&]() -> bool { constexpr std::array coordinate_count{1, 2, 3, 3, 2, 3, 0, 4}; constexpr std::array maxwell_type{0, 2, 4, 6, 1, 3, 0, 7}; - if (dimension == 6 || insn.Bits(76, 2) || insn.Bits(81, 3) != 7 || insn.Bits(59, 1) || + const bool offsets = insn.Bits(76, 1) != 0; + if (dimension == 6 || insn.Bits(77, 1) || insn.Bits(81, 3) != 7 || insn.Bits(59, 1) || insn.Bits(40, 14) > 0x1fff || insn.Bits(54, 5) >= 18 || (lod > 3 && lod != 6 && lod != 7) || (depth_compare && mask != 1) || mask == 0) return false; const u32 coordinates = coordinate_count[dimension]; const bool has_lod = lod == 2 || lod == 3 || lod == 6 || lod == 7; - const u32 count = coordinates + (has_lod ? 1 : 0) + (depth_compare ? 1 : 0); + const u32 count = coordinates + (has_lod ? 1 : 0) + (offsets ? 1 : 0) + (depth_compare ? 1 : 0); const u32 in_a = count <= 4 ? (count + 1) / 2 : 4; const auto value = [&](u32 i) -> u32 { if (i < in_a) return source_a == 255 ? 255U : source_a + i; @@ -1082,16 +1126,18 @@ inline LoweredProgram LowerToMaxwell(std::span code) { const auto copy = [&](u32 target, u32 source) { emit(0x5c98000000000000ULL | predicate | target | (u64{15} << 39) | (u64{source} << 20)); }; - // Coordinates to R250.., LOD/bias then reference to R246/R247. + // Coordinates to R250.., then LOD/bias, offsets and reference to R246... for (u32 i = 0; i < coordinates; ++i) copy(250 + i, value(i)); u32 meta = 246; - if (has_lod) copy(meta++, value(coordinates)); - if (depth_compare) copy(meta++, value(coordinates + (has_lod ? 1 : 0))); + u32 next = coordinates; + if (has_lod) copy(meta++, value(next++)); + if (offsets) copy(meta++, value(next++)); + if (depth_compare) copy(meta++, value(next++)); emit(0xc000000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{meta == 246 ? 255U : 246U} << 20) | (u64{maxwell_type[dimension]} << 28) | (u64{mask} << 31) | (u64{insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49) | (u64{depth_compare} << 50) | (u64{7} << 51) | - (u64{lod} << 55)); + (u64{offsets} << 54) | (u64{lod} << 55)); const u32 components = static_cast(std::popcount(mask)); if (insn.Bits(79, 1)) { // FP16 results: (c0, c1) packed into the first destination, (c2, c3) into @@ -1320,9 +1366,11 @@ inline LoweredProgram LowerToMaxwell(std::span code) { break; } case 0x245: case 0xa45: case 0xc45: { // I2FP.F32.U32 - if ((insn.words[2] & ~(0x400U | (opcode == 0xc45 ? 0x08000000U : 0U))) != 0x00201000) return fail(index, "native packed integer-to-float format"); + // Bits 78-79: rounding (RN/RM/RP/RZ), as Maxwell I2F's bits 39-40. + if ((insn.words[2] & ~(0x400U | 0xc000U | (opcode == 0xc45 ? 0x08000000U : 0U))) != 0x00201000) return fail(index, "native packed integer-to-float format"); const u32 src = operand(1, false); - emit(base(0x5cb8000000000000ULL) | (u64{2} << 8) | (u64{2} << 10) | (u64{insn.Bits(74, 1)} << 13) | (u64{src} << 20)); + emit(base(0x5cb8000000000000ULL) | (u64{2} << 8) | (u64{2} << 10) | (u64{insn.Bits(74, 1)} << 13) | (u64{src} << 20) | + (u64{insn.Bits(78, 2)} << 39)); break; } case 0x207: case 0x807: case 0xa07: @@ -1357,38 +1405,38 @@ inline LoweredProgram LowerToMaxwell(std::span code) { emit(out); break; } - case 0x212: case 0x812: case 0xc12: { // Bounded LOP3 bitwise forms, no predicate output + case 0x212: case 0x812: case 0xa12: case 0xc12: { // LOP3.LUT, any table, optional P = (result != 0) const u32 lut = insn.Bits(72, 8); - if ((lut != 0xc0 && lut != 0xfc && lut != 0xf8 && lut != 0x48 && lut != 0xe2) || insn.Bits(80, 1) || - insn.Bits(87, 4) != 15 || - (opcode == 0x212 && insn.Bits(40, 24))) return fail(index, "native LUT/predicate mode"); - u32 b = opcode == 0xc12 ? uniform_register(insn.Bits(32, 8)) : insn.Bits(32, 8); - if (b == ~0U) return fail(index, "native uniform logic source"); - if (opcode == 0x812) { + const u32 pred_target = insn.Bits(81, 3); + if (insn.Bits(80, 1) || insn.Bits(87, 4) != 15 || + (opcode == 0x212 && insn.Bits(40, 24)) || (opcode == 0xa12 && insn.Bits(62, 2))) + return fail(index, "native LUT/predicate mode"); + u32 b = 255; + if (opcode == 0x212) { + b = insn.Bits(32, 8); + } else if (opcode == 0x812) { mov_imm(251, insn.words[1]); b = 251; - } - const u32 a = insn.Bits(24, 8), c = insn.Bits(64, 8); - const auto logical = [&](u32 target, u32 lhs, u32 rhs, u32 operation, u32 pred_target = 7) { - emit(0x5c40000000000000ULL | predicate | target | (u64{lhs} << 8) | - (u64{rhs} << 20) | (u64{operation} << 41) | - (u64{pred_target == 7 ? 0U : 3U} << 44) | (u64{pred_target} << 48)); - }; - const u32 pred_target = insn.Bits(81, 3); - if (lut == 0xe2) { - logical(250, a, b, 0); - logical(252, c, b, 0); - result.instructions.back() |= u64{1} << 40; - logical(static_cast(dest), 250, 252, 1, pred_target); - } else if (lut == 0xf8) { - logical(250, b, c, false); - logical(static_cast(dest), a, 250, 1, pred_target); - } else if (lut == 0x48) { - logical(250, a, c, 2); - logical(static_cast(dest), 250, b, 0, pred_target); + } else if (opcode == 0xc12) { + b = uniform_register(insn.Bits(32, 8)); + if (b == ~0U) return fail(index, "native uniform logic source"); } else { - logical(static_cast(dest), a, b, lut == 0xfc ? 1U : 0U, pred_target); + b = operand(1, false); } + const u32 a = insn.Bits(24, 8), c = insn.Bits(64, 8); + emit(0x5be0000000000000ULL | predicate | dest | (u64{a} << 8) | (u64{b} << 20) | + (u64{lut} << 28) | (u64{pred_target == 7 ? 0U : 3U} << 36) | (u64{c} << 39) | + (u64{pred_target} << 48)); + break; + } + case 0x213: case 0xa13: case 0xc13: { // IABS: max(x, 0 - x), signed. + if (insn.words[2] & ~(opcode == 0xc13 ? 0x08000000U : 0U)) + return fail(index, "native integer absolute mode"); + const u32 source = operand(1, false); + emit(0x5c10000000000000ULL | predicate | 250 | (u64{255} << 8) | (u64{source} << 20) | + (u64{1} << 48)); + emit(base(0x5c20000000000000ULL) | (u64{source} << 8) | (u64{250} << 20) | + (u64{15} << 39) | (u64{1} << 48)); break; } case 0x217: case 0x817: case 0xa17: case 0xc17: { // IMNMX register, immediate, cbuf or uniform operand. diff --git a/src/video_core/dma_pusher.cpp b/src/video_core/dma_pusher.cpp index 2c95b6de03..040bf18362 100644 --- a/src/video_core/dma_pusher.cpp +++ b/src/video_core/dma_pusher.cpp @@ -13,6 +13,7 @@ #include "core/core.h" #include "core/memory.h" #include "video_core/dma_pusher.h" +#include "video_core/control/channel_state.h" #include "video_core/engines/maxwell_3d.h" #include "video_core/gpu.h" #include "video_core/guest_memory.h" @@ -134,6 +135,16 @@ bool DmaPusher::Step() { if (++dma_pushbuffer_subindex >= command_list_size) { // A split method resumes within the submission that split it; ring addresses repeat // every frame, so a stale one could otherwise swallow the headers of a later entry. + for (const auto& parked : parked_methods) { + static std::atomic reported{}; + if (reported.fetch_add(1) < 16) { + LOG_WARNING(HW_GPU, + "Submission ended with parked method {:#x} (sub {}, {} words left, " + "resume {:#x})", + parked.method, parked.subchannel, parked.method_count, + parked.resume_address); + } + } parked_methods.clear(); dma_pushbuffer.pop(); dma_pushbuffer_subindex = 0; @@ -264,7 +275,21 @@ void DmaPusher::SetState(const CommandHeader& command_header) { dma_state.method_count = command_header.method_count; } +// EDEN_NX2_SEM_WATCH=: log every method whose argument is the low word of an +// address in the 16 bytes there (finds the engine and method that release a semaphore). +void DmaPusher::WatchArgument(u32 method, u32 argument) const { + static const u32 watch = [] { + const char* value = std::getenv("EDEN_NX2_SEM_WATCH"); + return value ? static_cast(std::strtoull(value, nullptr, 16)) : 0U; + }(); + if (watch != 0 && (argument & ~0xfU) == watch) { + LOG_INFO(HW_GPU, "Semaphore watch: channel {} subchannel {} method {:#x} argument {:#x}", + channel_state.bind_id, dma_state.subchannel, method, argument); + } +} + void DmaPusher::CallMethod(u32 argument) { + WatchArgument(dma_state.method, argument); if (dma_state.method < non_puller_methods) { puller.CallPullerMethod(*this, Engines::Puller::MethodCall{ dma_state.method, @@ -289,6 +314,9 @@ void DmaPusher::CallMethod(u32 argument) { } void DmaPusher::CallMultiMethod(const u32* base_start, u32 num_methods) { + for (u32 i = 0; i < num_methods; ++i) { + WatchArgument(dma_state.method + (dma_state.non_incrementing ? 0 : i), base_start[i]); + } if (dma_state.method < non_puller_methods) { puller.CallMultiMethod(*this, dma_state.method, dma_state.subchannel, base_start, num_methods, dma_state.method_count); } else { diff --git a/src/video_core/dma_pusher.h b/src/video_core/dma_pusher.h index 24fedba21b..bbcc345c84 100644 --- a/src/video_core/dma_pusher.h +++ b/src/video_core/dma_pusher.h @@ -173,6 +173,7 @@ private: void CallMethod(u32 argument); void CallMultiMethod(const u32* base_start, u32 num_methods); void ReportUnboundSubchannel(); + void WatchArgument(u32 method, u32 argument) const; public: /// Logs the GPFIFO entry being processed (and the one before it), read through the GPU MMU diff --git a/src/video_core/engines/kepler_compute.cpp b/src/video_core/engines/kepler_compute.cpp index 7710ea7dd2..689f85a1d3 100644 --- a/src/video_core/engines/kepler_compute.cpp +++ b/src/video_core/engines/kepler_compute.cpp @@ -4,6 +4,7 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include #include #include @@ -46,6 +47,46 @@ void KeplerCompute::ConsumeSinkImpl(Core::System& system) { method_sink.clear(); } +// SET_REPORT_SEMAPHORE_A-D (0x6c0-0x6c3): address high/low, payload, then operation in bits 1:0 +// (release / acquire / report only / trap), reduction enable bit 3 with the op in bits 11:9, and +// a one-word (no timestamp) structure in bit 28. NVN2 releases queue syncs from compute this way. +void KeplerCompute::ProcessNativeReportSemaphore(u32 argument) { + const GPUVAddr address = (GPUVAddr{native_registers[0x6c0] & 0x1ffffff} << 32) | + native_registers[0x6c1]; + const u32 payload = native_registers[0x6c2]; + const u32 operation = argument & 3; + const bool one_word = (argument >> 28) & 1; + static u32 logged{}; + if (logged++ < 32) { + LOG_INFO(HW_GPU, "Native compute report semaphore {:#x} = {:#x}, control {:#x}", address, + payload, argument); + } + if (operation != 0 && operation != 2) { + LOG_WARNING(HW_GPU, "Native compute report semaphore operation {} not supported", operation); + return; + } + if ((argument >> 3) & 1) { + const u32 reduction = (argument >> 9) & 7; + rasterizer->SignalFence([this, address, payload, reduction] { + const u32 old_value = memory_manager.Read(address); + const u32 value = reduction == 0 ? old_value + payload + : reduction == 1 ? std::min(old_value, payload) + : reduction == 2 ? std::max(old_value, payload) + : reduction == 3 ? (old_value >= payload ? 0 : old_value + 1) + : reduction == 4 ? (old_value == 0 || old_value > payload ? payload : old_value - 1) + : reduction == 5 ? (old_value & payload) + : reduction == 6 ? (old_value | payload) + : (old_value ^ payload); + memory_manager.Write(address, value); + }); + return; + } + rasterizer->Query(address, VideoCommon::QueryType::Payload, + one_word ? VideoCommon::QueryPropertiesFlags::IsAFence + : VideoCommon::QueryPropertiesFlags::HasTimeout, + payload, 0); +} + bool KeplerCompute::ProcessNativeMethod(Core::System& system, u32 method, u32 argument, bool is_last_call) { if (native_context) { if (method == 0x45 || method == 0x47) native_compute_uploaded = true; @@ -65,11 +106,22 @@ bool KeplerCompute::ProcessNativeMethod(Core::System& system, u32 method, u32 ar if (!is_last_call) return true; const u32 entry = ((*native_executing_macro - 0xe00) / 2) % native_macro_positions.size(); native_executing_macro.reset(); + static const u32 watch = [] { + const char* value = std::getenv("EDEN_NX2_SEM_WATCH"); + return value ? static_cast(std::strtoull(value, nullptr, 16)) : 0U; + }(); + const bool watched = watch != 0 && std::ranges::any_of(native_parameters, + [](u32 parameter) { return (parameter & ~0xfU) == watch; }); + if (watched) { + LOG_INFO(HW_GPU, "Semaphore watch: compute macro start={:#x} parameters={:08x}", + native_macro_positions[entry], fmt::join(native_parameters, ",")); + } const auto result = native_macro->native_interpreter.Execute(native_macro->native_code, native_macro_positions[entry], native_parameters, [&](u32 address) { return address < native_registers.size() ? native_registers[address] : 0U; }, [&](u32 address, u32 value) { static u32 diagnostic_writes{}; + if (watched) 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; CallMethod(system, address, value, true); @@ -89,6 +141,10 @@ bool KeplerCompute::ProcessNativeMethod(Core::System& system, u32 method, u32 ar return true; } native_registers[method] = argument; + if (method == 0x6c3) { // SET_REPORT_SEMAPHORE_D: the compute class's semaphore release + ProcessNativeReportSemaphore(argument); + return true; + } if (method == 0x5a || (method >= 0xc6 && method <= 0x108)) { VideoCommon::TraceNativeQuery(12, memory_manager.GetID(), method, 0, 0, argument, 0); } @@ -157,6 +213,13 @@ bool KeplerCompute::ProcessNativeMethod(Core::System& system, u32 method, u32 ar [&](u64 address, NativeQmdScheduler::Descriptor& descriptor) { if (!memory_manager.IsFullyMappedRange(address, sizeof(descriptor))) return false; memory_manager.ReadBlockUnsafe(address, descriptor.data(), sizeof(descriptor)); + if (static u32 logged{}; (descriptor[53] & (1U << 23)) && logged < 8) { + ++logged; + LOG_INFO(HW_GPU, "Native QMD {:#x} releases: {:08x} | {:08x} | {:08x}", address, + fmt::join(std::span{descriptor}.subspan(24, 4), ","), + fmt::join(std::span{descriptor}.subspan(28, 4), ","), + fmt::join(std::span{descriptor}.subspan(52, 4), ",")); + } for (u32 slot = 0; slot < 2; ++slot) { const u32 flags = descriptor[25 + slot * 4]; if (!(flags & (1U << 23))) continue; diff --git a/src/video_core/engines/kepler_compute.h b/src/video_core/engines/kepler_compute.h index 99cf74d1e8..51d31b9c95 100644 --- a/src/video_core/engines/kepler_compute.h +++ b/src/video_core/engines/kepler_compute.h @@ -233,6 +233,7 @@ public: private: bool DispatchNativeCompute(const NativeQmdScheduler::Descriptor& descriptor); + void ProcessNativeReportSemaphore(u32 argument); bool ProcessNativeMethod(Core::System& system, u32 method, u32 argument, bool is_last_call); NativeQmdScheduler native_qmd; bool native_mode{}; diff --git a/src/video_core/engines/maxwell_3d.h b/src/video_core/engines/maxwell_3d.h index 8cbc4f4274..29db8022e5 100644 --- a/src/video_core/engines/maxwell_3d.h +++ b/src/video_core/engines/maxwell_3d.h @@ -3135,6 +3135,11 @@ public: /// Reads a register value located at the input method address u32 GetRegisterValue(u32 method) const; + /// GPU memory, for Turing macros that read memory into their FIFO or data RAM. + MemoryManager& GetMemoryManager() { + return memory_manager; + } + void SetNativeTuringMacroMode(bool enabled) { GetNativeMacroEngine().native_turing = enabled; upload_state.SetNativePartialTransfers(enabled); diff --git a/src/video_core/engines/puller.cpp b/src/video_core/engines/puller.cpp index 97a58903fe..047e383b85 100644 --- a/src/video_core/engines/puller.cpp +++ b/src/video_core/engines/puller.cpp @@ -68,9 +68,16 @@ bool Puller::CanResumeAcquire(DmaPusher& dma_pusher) { if (!pending_acquire->reported && std::chrono::steady_clock::now() - pending_acquire->since > std::chrono::seconds{2}) { pending_acquire->reported = true; - LOG_WARNING(HW_GPU, "Channel {} waits on semaphore {:#x} (op {}, {:#x}, now {:#x}) for 2 s", + const auto device = dma_pusher.memory_manager.GpuToCpuAddress(pending_acquire->address); + LOG_WARNING(HW_GPU, "Channel {} waits on semaphore {:#x} (op {}, {:#x}, now {:#x}) for 2 s; " + "device {:#x}, buffer cache holds GPU data: {}, words {:08x} {:08x} {:08x} {:08x}", dma_pusher.channel_state.bind_id, pending_acquire->address, - pending_acquire->operation, pending_acquire->value, current); + pending_acquire->operation, pending_acquire->value, current, device.value_or(0), + device && dma_pusher.rasterizer->MustFlushRegion(*device, 16, VideoCommon::CacheType::All), + dma_pusher.memory_manager.Read(pending_acquire->address & ~GPUVAddr{0xf}), + dma_pusher.memory_manager.Read((pending_acquire->address & ~GPUVAddr{0xf}) + 4), + dma_pusher.memory_manager.Read((pending_acquire->address & ~GPUVAddr{0xf}) + 8), + dma_pusher.memory_manager.Read((pending_acquire->address & ~GPUVAddr{0xf}) + 12)); } return false; } @@ -157,6 +164,17 @@ void Puller::ProcessFenceActionMethod(DmaPusher& dma_pusher) { void Puller::ProcessSemaphoreTriggerMethod(DmaPusher& dma_pusher) { const auto semaphoreOperationMask = 0xF; const auto op = GpuSemaphoreOperation(regs.semaphore_trigger & semaphoreOperationMask); + // EDEN_NX2_SEM_WATCH=: log every release/acquire on the 16 bytes there. + static const GPUVAddr watch = [] { + const char* value = std::getenv("EDEN_NX2_SEM_WATCH"); + return value ? GPUVAddr{std::strtoull(value, nullptr, 16)} : GPUVAddr{}; + }(); + if (watch != 0 && (regs.semaphore_address.SemaphoreAddress() & ~GPUVAddr{0xf}) == watch) { + LOG_INFO(HW_GPU, "Semaphore watch: channel {} op {} {:#x} value {:#x} (now {:#x})", + dma_pusher.channel_state.bind_id, static_cast(op), + regs.semaphore_address.SemaphoreAddress(), regs.semaphore_sequence, + dma_pusher.memory_manager.Read(regs.semaphore_address.SemaphoreAddress())); + } if (op == GpuSemaphoreOperation::WriteLong) { const GPUVAddr sequence_address{regs.semaphore_address.SemaphoreAddress()}; const u32 payload = regs.semaphore_sequence; diff --git a/src/video_core/macro.cpp b/src/video_core/macro.cpp index a8acd81682..06c649260d 100644 --- a/src/video_core/macro.cpp +++ b/src/video_core/macro.cpp @@ -27,6 +27,7 @@ #include "video_core/engines/maxwell_3d.h" #include "video_core/engines/maxwell_3d.h" #include "video_core/dirty_flags.h" +#include "video_core/memory_manager.h" #include "video_core/rasterizer_interface.h" #include "video_core/macro.h" @@ -1351,11 +1352,43 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u } LOG_DEBUG(HW_GPU, "Native MME context={:#x}", context); LOG_DEBUG(HW_GPU, "Native MME start={:#x}, parameters={}, data={:08x}", method, parameters.size(), fmt::join(parameters, ",")); - const auto result = native_interpreter.Execute(native_code, method, parameters, + std::vector fifo(parameters.begin(), parameters.end()); + const auto result = native_interpreter.Execute(native_code, method, fifo, [&](u32 address) { return address < Maxwell3D::Regs::NUM_REGS ? maxwell3d.GetRegisterValue(address) : 0U; }, [&](u32 address, u32 value) { if (address >= 0xe00) return false; + if (address == 0x556) { + static u32 enable_logs{}; + if (enable_logs++ < 400) LOG_INFO(HW_GPU, "Native MME start={:#x} render enable mode={}", method, value); + } 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). + constexpr u32 MemAddressA = 0x154, MemAddressB = 0x155, DataRamAddress = 0x156; + constexpr u32 DmaRead = 0x157, DmaReadFifoed = 0x158; + if (address == DmaRead || address == DmaReadFifoed) { + const GPUVAddr source = (GPUVAddr{maxwell3d.GetRegisterValue(MemAddressA) & 0x1ffffff} << 32) | + maxwell3d.GetRegisterValue(MemAddressB); + auto& memory = maxwell3d.GetMemoryManager(); + const u32 ram = maxwell3d.GetRegisterValue(DataRamAddress); + static u32 dma_logs{}; + if (dma_logs < 400) { + ++dma_logs; + std::array words{}; + for (u32 i = 0; i < std::min(value, 8); ++i) words[i] = memory.Read(source + u64{i} * 4); + LOG_INFO(HW_GPU, "Native MME DMA read{} start={:#x} source={:#x} count={} words={:08x}", + address == DmaReadFifoed ? " fifoed" : "", method, source, value, fmt::join(words, ",")); + } + for (u32 i = 0; i < std::min(value, 0x1000); ++i) { + const u32 word = memory.Read(source + u64{i} * 4); + if (address == DmaReadFifoed) { + fifo.push_back(word); + } else if (ram + i < native_interpreter.dram.size()) { + native_interpreter.dram[ram + i] = word; + } + } + } return true; }); if (!result.success) { diff --git a/src/video_core/macro_turing.h b/src/video_core/macro_turing.h index fe82ce4a4f..a0a40000c2 100644 --- a/src/video_core/macro_turing.h +++ b/src/video_core/macro_turing.h @@ -10,6 +10,7 @@ #include #include #include +#include namespace Tegra { @@ -25,8 +26,9 @@ public: }; template + // `parameters` is the load FIFO; `emit` may append to it (MME_DMA_READ_FIFOED). Result Execute(std::span code, Word start, - std::span parameters, Read read, Emit emit) { + std::vector& parameters, Read read, Emit emit) { std::array regs{}; Word initialized{}, carry{}, method{}, increment{}; Word pc = start, loop_count{}, loop_start{}, loop_end{}; diff --git a/src/video_core/native_qmd.h b/src/video_core/native_qmd.h index c9ae3b8cc0..c06f14e6b2 100644 --- a/src/video_core/native_qmd.h +++ b/src/video_core/native_qmd.h @@ -61,9 +61,9 @@ public: if (!read(request.address, state.descriptor)) return "unmapped QMD"; const auto qmd = state.descriptor; if ((qmd[18] & 0xff) != 0x30 || (qmd[4] & 0x100)) return "unsupported QMD version or queue"; - // Validate both release slots before making either visible. - for (Word slot = 0; slot < 2; ++slot) { - const Word flags = qmd[25 + slot * 4]; + // Validate the release slots before making any visible. + for (const Word base : ReleaseSlots) { + const Word flags = qmd[base + 1]; if (!(flags & (1U << 23))) continue; const bool add = (flags & (1U << 26)) != 0; if ((flags & (0x3bU << 24)) || (flags >> 30) != 1 || @@ -76,15 +76,15 @@ public: if (qmd[12] && (qmd[13] & 0xffff) && (qmd[14] & 0xffff) && !dispatch(qmd)) return "native shader dispatch required"; state.completed = true; - for (Word slot = 0; slot < 2; ++slot) { - const Word flags = qmd[25 + slot * 4]; + for (const Word base : ReleaseSlots) { + const Word flags = qmd[base + 1]; if (!(flags & (1U << 23))) continue; - const Address destination = (Address{flags & 0xff} << 32) | qmd[24 + slot * 4]; + const Address destination = (Address{flags & 0xff} << 32) | qmd[base]; bool released{}; if constexpr (std::is_invocable_r_v) { - released = release(destination, qmd[26 + slot * 4], flags); + released = release(destination, qmd[base + 2], flags); } else { - released = release(destination, qmd[26 + slot * 4]); + released = release(destination, qmd[base + 2]); } if (!released) return "unmapped QMD release"; } @@ -99,6 +99,10 @@ public: return {}; } private: + // Release slots (address low, flags | address high, payload). NVN2 patches a third one at + // word 52 for queue syncs (e.g. a compute queue releasing a semaphore another queue waits on). + static constexpr std::array ReleaseSlots{24, 28, 52}; + struct State { Descriptor descriptor{}; std::int32_t dependencies{}; diff --git a/src/video_core/query_cache/query_cache.h b/src/video_core/query_cache/query_cache.h index 11b2f631a5..930e5626aa 100644 --- a/src/video_core/query_cache/query_cache.h +++ b/src/video_core/query_cache/query_cache.h @@ -234,7 +234,12 @@ void QueryCacheBase::CounterReport(GPUVAddr addr, QueryType counter_type QueryPropertiesFlags flags, u32 payload, u32 subreport) { const bool has_timestamp = True(flags & QueryPropertiesFlags::HasTimeout); const bool is_fence = True(flags & QueryPropertiesFlags::IsAFence); - if (static const bool trace = std::getenv("EDEN_NX2_QUERY_TRACE") != nullptr; trace) { + static const GPUVAddr watch = [] { + const char* value = std::getenv("EDEN_NX2_SEM_WATCH"); + return value ? GPUVAddr{std::strtoull(value, nullptr, 16)} : GPUVAddr{}; + }(); + if (static const bool trace = std::getenv("EDEN_NX2_QUERY_TRACE") != nullptr; + trace || (watch != 0 && (addr & ~GPUVAddr{0xf}) == watch)) { LOG_INFO(HW_GPU, "nx2 report addr={:#x} type={} flags={:#x} payload={:#x}", addr, static_cast(counter_type), static_cast(flags), payload); } diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 1d9965a9a5..044c783a99 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -1103,6 +1103,14 @@ void RasterizerVulkan::AccelerateInlineToMemory(GPUVAddr address, size_t copy_si if (memory.size() >= sizeof(first_word)) std::memcpy(&first_word, memory.data(), sizeof(first_word)); VideoCommon::TraceNativeQuery(11, gpu_memory->GetID(), address, cpu_addr.value_or(0), 0, first_word, static_cast(copy_size)); + static const GPUVAddr watch = [] { + const char* value = std::getenv("EDEN_NX2_SEM_WATCH"); + return value ? GPUVAddr{std::strtoull(value, nullptr, 16)} : GPUVAddr{}; + }(); + if (watch != 0 && address < watch + 16 && address + copy_size > watch) { + LOG_INFO(Render_Vulkan, "Semaphore watch: inline write {:#x} size {} first word {:#x}", + address, copy_size, first_word); + } } if (!cpu_addr) [[unlikely]] { gpu_memory->WriteBlock(address, memory.data(), copy_size); diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp index 3aaba78fd1..b46534ec9d 100644 --- a/src/yuzu_cmd/yuzu.cpp +++ b/src/yuzu_cmd/yuzu.cpp @@ -4,6 +4,9 @@ // SPDX-FileCopyrightText: 2014 Citra Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include +#include #include #include #include @@ -34,6 +37,7 @@ #include "core/hle/service/filesystem/filesystem.h" #include "core/loader/loader.h" #include "frontend_common/config.h" +#include "input_common/drivers/keyboard.h" #include "input_common/main.h" #include "network/network.h" #include "sdl_config.h" @@ -327,8 +331,50 @@ extern "C" SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) { state->system.InitializeDebugger(); return SDL_APP_CONTINUE; } +// EDEN_NX2_KEY_SCRIPT="seconds:code,seconds:-code,...": press (code) or release (-code) keyboard +// keys, by SDL scancode, that many seconds after start. Lets hidden test runs drive menus. +static void RunKeyScript(InputCommon::InputSubsystem& input_subsystem) { + struct KeyEvent { + double seconds; + int code; + }; + static const std::vector script = [] { + std::vector events; + if (const char* text = std::getenv("EDEN_NX2_KEY_SCRIPT")) { + std::string list{text}; + size_t start = 0; + while (start < list.size()) { + const size_t end = std::min(list.find(',', start), list.size()); + const std::string item = list.substr(start, end - start); + if (const size_t colon = item.find(':'); colon != std::string::npos) { + events.push_back({std::stod(item.substr(0, colon)), + std::stoi(item.substr(colon + 1))}); + } + start = end + 1; + } + } + return events; + }(); + static const auto begin = std::chrono::steady_clock::now(); + static size_t next = 0; + const double elapsed = + std::chrono::duration(std::chrono::steady_clock::now() - begin).count(); + while (next < script.size() && script[next].seconds <= elapsed) { + const int code = script[next].code; + LOG_INFO(Frontend, "Key script: {} key {} at {:.1f} s", code < 0 ? "release" : "press", + code < 0 ? -code : code, elapsed); + if (code < 0) { + input_subsystem.GetKeyboard()->ReleaseKey(static_cast(-code)); + } else { + input_subsystem.GetKeyboard()->PressKey(static_cast(code)); + } + ++next; + } +} + extern "C" SDL_AppResult SDL_AppIterate(void *appstate) { SdlState *state = (SdlState *)appstate; + RunKeyScript(state->input_subsystem); return state->emu_window && state->emu_window->IsOpen() ? SDL_APP_CONTINUE : SDL_APP_SUCCESS; } extern "C" SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) { -- cgit v1.2.3 From 32d876ded1aefc52f839d65570e3e3c43ec04415 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Wed, 28 Oct 2026 07:47:45 -0400 Subject: nx2: read debug switches once; key scripts from a file Several debug switches were read with std::getenv on every macro call, QMD, semaphore write or draw. The UCRT getenv locks and scans the environment, thousands of times a frame; a long EDEN_NX2_KEY_SCRIPT made title 4 crawl at 2 fps. Each is now read once. EDEN_NX2_KEY_SCRIPT=@ reads the script from a file. gdbrsp.py gains 'hits' (log registers, frame chain and object words at breakpoints, following the stopped thread). --- src/video_core/engines/kepler_compute.cpp | 11 +++--- src/video_core/macro.cpp | 6 ++-- src/video_core/native_query_trace.h | 3 +- src/video_core/renderer_vulkan/vk_rasterizer.cpp | 8 +++-- src/yuzu_cmd/yuzu.cpp | 10 +++++- tools/nx2/gdbrsp.py | 43 +++++++++++++++++++++++- 6 files changed, 70 insertions(+), 11 deletions(-) (limited to 'src/video_core/macro.cpp') diff --git a/src/video_core/engines/kepler_compute.cpp b/src/video_core/engines/kepler_compute.cpp index dc3f9b25b6..4954fe613a 100644 --- a/src/video_core/engines/kepler_compute.cpp +++ b/src/video_core/engines/kepler_compute.cpp @@ -134,7 +134,8 @@ bool KeplerCompute::ProcessNativeMethod(Core::System& system, u32 method, u32 ar }); if (!result.success) { native_macro->native_execution_failed = true; - if (std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS")) { + if (static const bool diagnostics = std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS") != nullptr; + diagnostics) { std::ofstream dump("native-compute-mme.bin", std::ios::binary); dump.write(reinterpret_cast(native_macro->native_code.data()), sizeof(native_macro->native_code)); } @@ -162,7 +163,8 @@ bool KeplerCompute::ProcessNativeMethod(Core::System& system, u32 method, u32 ar LOG_DEBUG(HW_GPU, "Native inline QMD: address={:#x}, program={:#x}, control={:#x}, grid={}/{}/{}", address, program, control, native_registers[0xc8 + 12], native_registers[0xc8 + 13] & 0xffff, native_registers[0xc8 + 14] & 0xffff); - if (std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS")) { + if (static const bool diagnostics = std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS") != nullptr; + diagnostics) { std::ofstream dump("native-inline-qmd.bin", std::ios::binary); dump.write(reinterpret_cast(native_registers.data() + 0xc8), 256); std::ofstream registers("native-inline-compute-registers.bin", std::ios::binary); @@ -203,7 +205,8 @@ bool KeplerCompute::ProcessNativeMethod(Core::System& system, u32 method, u32 ar memory_manager.GpuToCpuAddress(qmd_address).value_or(0), method, argument, 0); if (method == 0xae || (method == 0xaf && !(argument & 2)) || (method == 0xb0 && ((argument & 15) == 0 || (argument & 15) == 1))) return true; - if ((argument & 15) == 12 && std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS")) { + static const bool qmd_diagnostics = std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS") != nullptr; + if ((argument & 15) == 12 && qmd_diagnostics) { static u32 captured{}; if (captured++ == 0 && memory_manager.IsFullyMappedRange(qmd_address, 256)) { std::array descriptor{}; @@ -288,7 +291,7 @@ bool KeplerCompute::ProcessNativeMethod(Core::System& system, u32 method, u32 ar if (memory_manager.IsFullyMappedRange(qmd_address, sizeof(qmd))) { memory_manager.ReadBlockUnsafe(qmd_address, qmd.data(), sizeof(qmd)); } - if (std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS") && memory_manager.IsFullyMappedRange(qmd_address, sizeof(qmd))) { + if (qmd_diagnostics && memory_manager.IsFullyMappedRange(qmd_address, sizeof(qmd))) { std::ofstream dump("native-qmd.bin", std::ios::binary); dump.write(reinterpret_cast(qmd.data()), sizeof(qmd)); std::ofstream macros("native-dispatch-mme.bin", std::ios::binary); diff --git a/src/video_core/macro.cpp b/src/video_core/macro.cpp index 06c649260d..e6279deab4 100644 --- a/src/video_core/macro.cpp +++ b/src/video_core/macro.cpp @@ -1346,7 +1346,8 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u maxwell3d.RefreshParameters(); const auto context = reinterpret_cast(this); static std::unordered_set captured_contexts; - if (std::getenv("EDEN_NATIVE_FRAME_CAPTURE") && captured_contexts.insert(context).second) { + static const bool capture = std::getenv("EDEN_NATIVE_FRAME_CAPTURE") != nullptr; + if (capture && captured_contexts.insert(context).second) { std::ofstream dump("native-graphics-mme-" + std::to_string(context) + ".bin", std::ios::binary); dump.write(reinterpret_cast(native_code.data()), sizeof(native_code)); } @@ -1393,7 +1394,8 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u }); if (!result.success) { native_execution_failed = true; - if (std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS")) { + if (static const bool diagnostics = std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS") != nullptr; + diagnostics) { std::ofstream dump("native-mme-" + std::to_string(method) + ".bin", std::ios::binary); dump.write(reinterpret_cast(native_code.data()), sizeof(native_code)); } diff --git a/src/video_core/native_query_trace.h b/src/video_core/native_query_trace.h index eae258dc04..036c3de936 100644 --- a/src/video_core/native_query_trace.h +++ b/src/video_core/native_query_trace.h @@ -30,7 +30,8 @@ inline State& GetState() { // 10=DMA destination (value=source, flags=line bytes), 11=inline write. // Keep this opt-in and bounded. inline void TraceNativeQuery(u32 event, u64 asid, u64 gpu, u64 cpu, u32 type, u64 value, u32 flags) { - if (!std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS")) return; + static const bool diagnostics = std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS") != nullptr; + if (!diagnostics) return; auto& state = NativeQueryDiagnostics::GetState(); const std::scoped_lock lock{state.mutex}; if (state.records++ < 1000000 || event == 4 || event == 5) { diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 1b0493c4b5..609ac12fb4 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -288,7 +288,10 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) { submitted = true; static u32 native_capture_count{}; if (maxwell3d->GetNativeMacroEngine().native_turing && - std::getenv("EDEN_NATIVE_DRAW_CAPTURE") && native_capture_count < 16) { + native_capture_count < 16 && [] { + static const bool capture = std::getenv("EDEN_NATIVE_DRAW_CAPTURE") != nullptr; + return capture; + }()) { const u32 capture = native_capture_count++; const auto prefix = fmt::format("native-draw-{}", capture); const auto dump = [&](const std::string& name, GPUVAddr address, size_t size) { @@ -1252,7 +1255,8 @@ std::optional RasterizerVulkan::AccelerateDisplay( const auto [image_view, scaled] = texture_cache.TryFindFramebufferImageView(config, framebuffer_addr); static u32 display_trace_count{}; - if (std::getenv("EDEN_NATIVE_FRAME_CAPTURE") && display_trace_count++ < 64) { + static const bool frame_capture = std::getenv("EDEN_NATIVE_FRAME_CAPTURE") != nullptr; + if (frame_capture && display_trace_count++ < 64) { LOG_INFO(HW_GPU, "Native display source: address={:#x}, offset={:#x}, dimensions={}x{}, stride={}, accelerated={}, mask={:#x}", framebuffer_addr, config.offset, config.width, config.height, pixel_stride, image_view != nullptr, config.layer_stack_mask); diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp index 96146abec3..f55da5fd61 100644 --- a/src/yuzu_cmd/yuzu.cpp +++ b/src/yuzu_cmd/yuzu.cpp @@ -4,6 +4,8 @@ // SPDX-FileCopyrightText: 2014 Citra Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include #include #include #include @@ -331,7 +333,7 @@ extern "C" SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) { state->system.InitializeDebugger(); return SDL_APP_CONTINUE; } -// EDEN_NX2_KEY_SCRIPT="seconds:code,seconds:-code,...": press (code) or release (-code) keyboard +// EDEN_NX2_KEY_SCRIPT="seconds:code,seconds:-code,..." (or "@"): press (code) or release (-code) keyboard // keys, by SDL scancode, that many seconds after start. Lets hidden test runs drive menus. double Nx2KeyClockSeconds(); // emu_window_sdl3.cpp @@ -344,7 +346,13 @@ static void RunKeyScript(InputCommon::InputSubsystem& input_subsystem) { static const std::vector script = [] { std::vector events; if (const char* text = std::getenv("EDEN_NX2_KEY_SCRIPT")) { + // "@" reads the list from a file: long scripts in the environment slow down + // every getenv in the process. std::string list{text}; + if (!list.empty() && list[0] == '@') { + std::ifstream file{list.substr(1)}; + list.assign(std::istreambuf_iterator{file}, {}); + } size_t start = 0; while (start < list.size()) { const size_t end = std::min(list.find(',', start), list.size()); diff --git a/tools/nx2/gdbrsp.py b/tools/nx2/gdbrsp.py index 51968254fa..f7e70c8fbe 100644 --- a/tools/nx2/gdbrsp.py +++ b/tools/nx2/gdbrsp.py @@ -15,7 +15,7 @@ class Rsp: import time for _ in range(240): # wait up to ~2 min for the stub to listen try: - self.s = socket.create_connection(("127.0.0.1", port), timeout=600) + self.s = socket.create_connection(("127.0.0.1", port), timeout=3600) break except OSError: time.sleep(0.5) @@ -139,6 +139,47 @@ def main(): prev = pc if pc == stop_at: break + elif op == "hits": + # hits ...: break at the addresses and, on each hit, print the registers, + # the frame-pointer chain and the 0x40 bytes at x0, then step past and continue. + # Stops after hits or when the target stops anywhere else. + limit, addrs = int(parts[1]), [int(a, 16) for a in parts[2:]] + for a in addrs: + rsp.cmd(f"Z0,{a:x},4") + for hit in range(limit): + reason = rsp.cmd("c") + # Several threads can stop at once: read and step the one this reply names. + tid = re.search(r"thread:([0-9a-f]+);", reason) + if tid: + rsp.cmd(f"Hg{tid.group(1)}") + r = rsp.regs() + pc = r.get("pc", 0) + stamp = time.strftime("%H:%M:%S") + label = (f"hit {hit}" if pc in addrs else f"stopped elsewhere ({reason})") + (f" thread {tid.group(1)}" if tid else "") + print(f"[{stamp}] {label} pc={pc:#x} " + " ".join( + f"{k}={r.get(k, 0):#x}" for k in ("x0", "x1", "x2", "x19", "x20", "x21", "x30", "sp")), flush=True) + obj = rsp.mem(r.get("x0", 0), 0x40) + if len(obj) == 0x40: + print(" *x0: " + " ".join(f"{w:016x}" for w in struct.unpack("<8Q", obj)), flush=True) + fp = r.get("x29", 0) + chain = [] + for _ in range(16): + d = rsp.mem(fp, 16) + if len(d) < 16: + break + nfp, ret = struct.unpack(" Date: Wed, 28 Oct 2026 13:22:59 -0400 Subject: Switch 2: per-channel MME data RAM; one QMD table for all channels Macro code stays shared by the channels of a process, but each channel now has its own MME data RAM. Title 1's channels keep their own QMD ring base in data word 0x27; with one shared RAM every channel used the last channel's ring, QMDs collided, releases came early or never and the GPU waited on semaphores (4 s timeouts per frame). The QMD scheduler state is one table for all channels: a QMD's dependency counter is state of the QMD, scheduled and decremented from different channels. Diagnostics: EDEN_NX2_QMD_TRACE (scheduler loads, counts, launches) and EDEN_NX2_MME_DUMP (compute macro code, calls, data word 0x27 changes). --- src/video_core/control/channel_state.cpp | 1 + src/video_core/engines/kepler_compute.cpp | 30 ++++++++++++++++++++++++++++++ src/video_core/engines/kepler_compute.h | 5 +++++ src/video_core/engines/maxwell_3d.h | 3 +++ src/video_core/macro.cpp | 10 ++++++++-- src/video_core/macro_turing.h | 15 ++++++++++----- src/video_core/native_qmd.h | 18 ++++++++++++++++-- 7 files changed, 73 insertions(+), 9 deletions(-) (limited to 'src/video_core/macro.cpp') diff --git a/src/video_core/control/channel_state.cpp b/src/video_core/control/channel_state.cpp index 57d7dc8e57..8e0b6c1b9e 100644 --- a/src/video_core/control/channel_state.cpp +++ b/src/video_core/control/channel_state.cpp @@ -82,6 +82,7 @@ void ChannelState::Init(Core::System& system, u64 program_id_) { payload->maxwell_3d.SetNativeTuringMacroMode(true); payload->kepler_compute.SetNativeMacroMode(¯os->engine, macros->positions, macros.get()); + payload->kepler_compute.SetNativeDataRam(&payload->maxwell_3d.GetNativeDataRam()); } initialized = true; } diff --git a/src/video_core/engines/kepler_compute.cpp b/src/video_core/engines/kepler_compute.cpp index 4954fe613a..8a3855468f 100644 --- a/src/video_core/engines/kepler_compute.cpp +++ b/src/video_core/engines/kepler_compute.cpp @@ -29,6 +29,13 @@ KeplerCompute::KeplerCompute(MemoryManager& memory_manager_) : memory_manager{memory_manager_} , upload_state{memory_manager, regs.upload} { + // EDEN_NX2_QMD_TRACE: log the QMD scheduler's loads, dependency counts and launches. + if (std::getenv("EDEN_NX2_QMD_TRACE")) { + native_qmd.trace = [this](std::string_view event, u64 address, s32 value) { + LOG_INFO(HW_GPU, "QMD trace {} {:#x} {} (engine {} as {})", event, address, value, + fmt::ptr(this), memory_manager.GetID()); + }; + } execution_mask.reset(); execution_mask[KEPLER_COMPUTE_REG_INDEX(exec_upload)] = true; execution_mask[KEPLER_COMPUTE_REG_INDEX(data_upload)] = true; @@ -109,6 +116,7 @@ bool KeplerCompute::ProcessNativeMethod(Core::System& system, u32 method, u32 ar native_parameters.push_back(argument); if (!is_last_call) return true; const u32 entry = ((*native_executing_macro - 0xe00) / 2) % native_macro_positions.size(); + native_executing_macro_logged = native_executing_macro; native_executing_macro.reset(); static const u32 watch = [] { const char* value = std::getenv("EDEN_NX2_SEM_WATCH"); @@ -120,6 +128,22 @@ bool KeplerCompute::ProcessNativeMethod(Core::System& system, u32 method, u32 ar LOG_INFO(HW_GPU, "Semaphore watch: compute macro start={:#x} parameters={:08x}", native_macro_positions[entry], fmt::join(native_parameters, ",")); } + // EDEN_NX2_MME_DUMP: write the compute macro code once per engine, and log each macro + // call with its entry and parameters (diagnostics). + if (static const bool mme_dump = std::getenv("EDEN_NX2_MME_DUMP") != nullptr; mme_dump) { + if (!native_mme_dumped && native_macro_positions[entry] == 0xda) { + native_mme_dumped = true; + std::ofstream dump(fmt::format("native-compute-mme-{}.bin", fmt::ptr(this)), + std::ios::binary); + dump.write(reinterpret_cast(native_macro->native_code.data()), + sizeof(native_macro->native_code)); + } + LOG_INFO(HW_GPU, "MME call engine {} method {:#x} start={:#x} parameters={:08x}", + fmt::ptr(this), *native_executing_macro_logged, native_macro_positions[entry], + fmt::join(native_parameters, ",")); + } + if (native_data_ram) native_macro->native_interpreter.active_dram = native_data_ram; + const u32 dram0_before = (*native_macro->native_interpreter.active_dram)[0x27]; const auto result = native_macro->native_interpreter.Execute(native_macro->native_code, native_macro_positions[entry], native_parameters, [&](u32 address) { return address < native_registers.size() ? native_registers[address] : 0U; }, @@ -132,6 +156,12 @@ bool KeplerCompute::ProcessNativeMethod(Core::System& system, u32 method, u32 ar CallMethod(system, address, value, true); return true; }); + if (static const bool mme_dump = std::getenv("EDEN_NX2_MME_DUMP") != nullptr; + mme_dump && (*native_macro->native_interpreter.active_dram)[0x27] != dram0_before) { + LOG_INFO(HW_GPU, "MME data RAM[0x27] {:#x} -> {:#x} by engine {} macro start={:#x}", + dram0_before, (*native_macro->native_interpreter.active_dram)[0x27], fmt::ptr(this), + native_macro_positions[entry]); + } if (!result.success) { native_macro->native_execution_failed = true; if (static const bool diagnostics = std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS") != nullptr; diff --git a/src/video_core/engines/kepler_compute.h b/src/video_core/engines/kepler_compute.h index dd69211f7b..7cecab378b 100644 --- a/src/video_core/engines/kepler_compute.h +++ b/src/video_core/engines/kepler_compute.h @@ -57,6 +57,8 @@ public: /// Binds a rasterizer to this engine. void BindRasterizer(VideoCore::RasterizerInterface* rasterizer); + /// The channel's MME data RAM, shared with its 3D engine. + void SetNativeDataRam(std::array* data_ram) { native_data_ram = data_ram; } void SetNativeMacroMode(MacroEngine* shared_macro, std::span positions, NativeMacroContext* context = nullptr) { if (native_context != context) native_compute_uploaded = false; native_context = context; @@ -242,6 +244,9 @@ private: bool UpscaleInsteadOfNativeProgram(); bool ProcessNativeMethod(Core::System& system, u32 method, u32 argument, bool is_last_call); NativeQmdScheduler native_qmd; + std::array* native_data_ram{}; + bool native_mme_dumped{}; + std::optional native_executing_macro_logged; bool native_mode{}; NativeMacroContext* native_context{}; bool native_compute_uploaded{}; diff --git a/src/video_core/engines/maxwell_3d.h b/src/video_core/engines/maxwell_3d.h index 2a10795a4e..82e716b822 100644 --- a/src/video_core/engines/maxwell_3d.h +++ b/src/video_core/engines/maxwell_3d.h @@ -3150,6 +3150,8 @@ public: native_macro_context = std::move(context); } MacroEngine& GetNativeMacroEngine() { return native_macro_context ? native_macro_context->engine : macro_engine; } + /// This channel's MME data RAM (shared with its compute engine). + std::array& GetNativeDataRam() { return native_data_ram; } std::span GetNativeMacroPositions() { return native_macro_context ? std::span{native_macro_context->positions} : std::span{macro_positions}; } @@ -3302,6 +3304,7 @@ private: /// Interpreter for the macro codes uploaded to the GPU. std::shared_ptr native_macro_context; + std::array native_data_ram{}; MacroEngine macro_engine; Upload::State upload_state; diff --git a/src/video_core/macro.cpp b/src/video_core/macro.cpp index e6279deab4..59396cb58b 100644 --- a/src/video_core/macro.cpp +++ b/src/video_core/macro.cpp @@ -1354,6 +1354,7 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u LOG_DEBUG(HW_GPU, "Native MME context={:#x}", context); LOG_DEBUG(HW_GPU, "Native MME start={:#x}, parameters={}, data={:08x}", method, parameters.size(), fmt::join(parameters, ",")); std::vector fifo(parameters.begin(), parameters.end()); + native_interpreter.active_dram = &maxwell3d.GetNativeDataRam(); const auto result = native_interpreter.Execute(native_code, method, fifo, [&](u32 address) { return address < Maxwell3D::Regs::NUM_REGS ? maxwell3d.GetRegisterValue(address) : 0U; }, [&](u32 address, u32 value) { @@ -1385,8 +1386,13 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u const u32 word = memory.Read(source + u64{i} * 4); if (address == DmaReadFifoed) { fifo.push_back(word); - } else if (ram + i < native_interpreter.dram.size()) { - native_interpreter.dram[ram + i] = word; + } else if (ram + i < native_interpreter.active_dram->size()) { + if (static const bool mme_dump = std::getenv("EDEN_NX2_MME_DUMP") != nullptr; + mme_dump && ram + i == 0x27 && (*native_interpreter.active_dram)[0x27] != word) { + LOG_INFO(HW_GPU, "MME data RAM[0x27] {:#x} -> {:#x} by 3D engine {} (DMA from {:#x})", + (*native_interpreter.active_dram)[0x27], word, fmt::ptr(&maxwell3d), source); + } + (*native_interpreter.active_dram)[ram + i] = word; } } } diff --git a/src/video_core/macro_turing.h b/src/video_core/macro_turing.h index a0a40000c2..063e22c297 100644 --- a/src/video_core/macro_turing.h +++ b/src/video_core/macro_turing.h @@ -167,11 +167,11 @@ public: } break; case 30: - if (x >= dram.size()) { fail("data RAM read out of bounds"); break; } - value = dram[x]; break; + if (x >= active_dram->size()) { fail("data RAM read out of bounds"); break; } + value = (*active_dram)[x]; break; case 31: - if (x >= dram.size()) { fail("data RAM write out of bounds"); break; } - dram[x] = y; break; + if (x >= active_dram->size()) { fail("data RAM write out of bounds"); break; } + (*active_dram)[x] = y; break; } alu[lane] = value; if (dst[lane] < 24) { regs[dst[lane]] = value; initialized |= 1U << dst[lane]; } @@ -209,7 +209,12 @@ public: return {false, pc, consumed, error}; } - std::array dram{}; + using DataRam = std::array; + DataRam dram{}; + // The data RAM DREAD/DWRITE use: a channel's own copy when the caller sets one. Macro code is + // shared by the channels of a process, but each channel keeps its own data RAM (title 1's + // channels store their own QMD ring base in word 0x27). + DataRam* active_dram{&dram}; }; } // namespace Tegra diff --git a/src/video_core/native_qmd.h b/src/video_core/native_qmd.h index 41961c1aea..15c59cb7c6 100644 --- a/src/video_core/native_qmd.h +++ b/src/video_core/native_qmd.h @@ -3,6 +3,7 @@ #pragma once #include #include +#include #include #include #include @@ -16,6 +17,8 @@ public: using Address = std::uint64_t; using Descriptor = std::array; struct RejectShaderDispatch { bool operator()(const Descriptor&) const { return false; } }; + // Optional diagnostics: called with "load"/"count"/"defer"/"run" events. + std::function trace; template std::string_view Signal(Address address, Word argument, Read read, Release release) { return Signal(address, argument, read, release, RejectShaderDispatch{}); @@ -41,6 +44,8 @@ public: return "native shader dispatch required"; } state.dependencies = std::int32_t((qmd[4] >> 14) & 0xffff) + state.deferred; + if (trace) trace("base", request.address, std::int32_t((qmd[4] >> 14) & 0xffff)); + if (trace) trace("load", request.address, state.dependencies); state.deferred = 0; state.loaded = true; state.completed = false; @@ -48,8 +53,13 @@ public: state.scheduled = true; } else if (request.action == 7 || request.action == 12) { const auto delta = request.action == 7 ? -std::int32_t(request.count) : std::int32_t(request.count); - if (!state.loaded || state.completed) { state.deferred += delta; continue; } + if (!state.loaded || state.completed) { + state.deferred += delta; + if (trace) trace("defer", request.address, state.deferred); + continue; + } state.dependencies += delta; + if (trace) trace("count", request.address, state.dependencies); } else if (request.action == 0 || request.action == 1) { continue; } else { @@ -78,6 +88,7 @@ public: // are what other queues and the CPU wait on (the console never fails a launch). const bool dispatched = !(qmd[12] && (qmd[13] & 0xffff) && (qmd[14] & 0xffff)) || dispatch(qmd); if (!dispatched) error = "native shader dispatch required"; + if (trace) trace("run", request.address, dispatched ? 1 : 0); state.completed = true; for (const Word base : ReleaseSlots) { const Word flags = qmd[base + 1]; @@ -114,6 +125,9 @@ private: bool scheduled{}; bool completed{}; }; - std::unordered_map states; + // One table for every channel: a QMD's dependency counter is state of the QMD itself, and + // title 1 schedules a QMD on one channel while another decrements it (a decrement that + // arrived first was kept by the other channel's scheduler and the QMD never launched). + static inline std::unordered_map states; }; } // namespace Tegra -- cgit v1.2.3 From cc3d73762a41dba370aadd3468cb510dcbc2b74d Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:44:24 -0400 Subject: Performance diagnostics and small wins for title 1 - eden-cli: SDL called SDL_AppIterate back to back, keeping a host core at 100% polling input; run the main callbacks at 240 Hz. - EDEN_NX2_IDLE_SPIN_US: optional spin before an idle core blocks (measured: no gain on title 1, so off by default). - EDEN_NX2_SVC_TRACE=:: trace SVCs (with thread ids) for a range of presents only. - MME DMA reads flush GPU-written memory first (never triggered on title 1, kept for correctness); render-enable override histogram instead of the first-400 log. Measured on title 1's title screen (RTX 4070 Laptop): GPU ~20% busy, the six emulated cores ~0.9 host cores in total, Eden's GPU thread 45-70% busy and largely waiting: on host fences for semaphore acquires (GPU accuracy High delays every release to host completion), on ASTC upload Finish(), and in a 1 ms poll sleep. gpu_accuracy=Low: ~12.5 -> ~16 fps. --- src/core/hle/kernel/physical_core.cpp | 19 ++++++++++++++++++- src/core/hle/kernel/physical_core.h | 3 ++- src/core/hle/kernel/svc.cpp | 22 +++++++++++++++++----- src/video_core/macro.cpp | 23 +++++++++++++++++++++-- src/yuzu_cmd/yuzu.cpp | 3 +++ 5 files changed, 61 insertions(+), 9 deletions(-) (limited to 'src/video_core/macro.cpp') diff --git a/src/core/hle/kernel/physical_core.cpp b/src/core/hle/kernel/physical_core.cpp index c5236c64e3..c3ae7629aa 100644 --- a/src/core/hle/kernel/physical_core.cpp +++ b/src/core/hle/kernel/physical_core.cpp @@ -4,6 +4,9 @@ // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include +#include #include "common/scope_exit.h" #include "common/settings.h" #include "core/core.h" @@ -222,8 +225,22 @@ void PhysicalCore::LogBacktrace(KernelCore& kernel) { } void PhysicalCore::Idle() { + // Guest job systems hand work between cores many times per frame; blocking at once adds the + // host's thread wake-up latency to every handoff. EDEN_NX2_IDLE_SPIN_US=: poll for that + // many microseconds before blocking. + static const auto spin = std::chrono::microseconds{[] { + const char* text = std::getenv("EDEN_NX2_IDLE_SPIN_US"); + return text ? std::strtoll(text, nullptr, 10) : 0LL; + }()}; + if (spin.count() > 0) { + const auto deadline = std::chrono::steady_clock::now() + spin; + while (!m_is_interrupted.load(std::memory_order_acquire) && + std::chrono::steady_clock::now() < deadline) { + std::this_thread::yield(); + } + } std::unique_lock lk{m_guard}; - m_on_interrupt.wait(lk, [this] { return m_is_interrupted; }); + m_on_interrupt.wait(lk, [this] { return m_is_interrupted.load(); }); } bool PhysicalCore::IsInterrupted() const { diff --git a/src/core/hle/kernel/physical_core.h b/src/core/hle/kernel/physical_core.h index b92201204a..20371d6717 100644 --- a/src/core/hle/kernel/physical_core.h +++ b/src/core/hle/kernel/physical_core.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -71,7 +72,7 @@ private: std::condition_variable m_on_interrupt; Core::ArmInterface* m_arm_interface{}; KThread* m_current_thread{}; - bool m_is_interrupted{}; + std::atomic m_is_interrupted{}; bool m_is_single_core{}; }; diff --git a/src/core/hle/kernel/svc.cpp b/src/core/hle/kernel/svc.cpp index e458a5057d..53d964ecbc 100644 --- a/src/core/hle/kernel/svc.cpp +++ b/src/core/hle/kernel/svc.cpp @@ -7,6 +7,7 @@ // DO NOT MODIFY IT MANUALLY #include +#include #include #include "core/arm/arm_interface.h" @@ -2506,10 +2507,20 @@ void Call(Core::System& system, u32 imm) { LOG_TRACE(Kernel_SVC, "{} [0]={:#x} [1]={:#x} [2]={:#x} [3]={:#x} [4]={:#x} [5]={:#x} [6]={:#x}", imm, GetArg64(args, 0), GetArg64(args, 1), GetArg64(args, 2), GetArg64(args, 3), GetArg64(args, 4), GetArg64(args, 5), GetArg64(args, 6)); - // Set EDEN_NX2_SVC_TRACE=1 to log every SVC with its arguments and results. - static const bool svc_trace = std::getenv("EDEN_NX2_SVC_TRACE") != nullptr; + // Set EDEN_NX2_SVC_TRACE=1 to log every SVC with its arguments and results, or + // EDEN_NX2_SVC_TRACE=: for presents first..last only (a frame-time profile). + static const std::pair trace_range = [] { + const char* text = std::getenv("EDEN_NX2_SVC_TRACE"); + if (text == nullptr) return std::pair{1, 0}; + const char* colon = std::strchr(text, ':'); + if (colon == nullptr) return std::pair{0, ~u64{}}; + return std::pair{std::strtoull(text, nullptr, 10), std::strtoull(colon + 1, nullptr, 10)}; + }(); + const u64 presents = Core::g_nx2_present_count.load(std::memory_order_relaxed); + const bool svc_trace = presents >= trace_range.first && presents <= trace_range.second; if (svc_trace) { - LOG_INFO(Kernel_SVC, "svc {:#04x} in [0]={:#x} [1]={:#x} [2]={:#x} [3]={:#x}", imm, + LOG_INFO(Kernel_SVC, "svc {:#04x} in t{} [0]={:#x} [1]={:#x} [2]={:#x} [3]={:#x}", imm, + GetCurrentThread(kernel).GetThreadId(), GetArg64(args, 0), GetArg64(args, 1), GetArg64(args, 2), GetArg64(args, 3)); } if (imm == 0x80) { @@ -2546,8 +2557,9 @@ void Call(Core::System& system, u32 imm) { Call32(system, imm, args); } if (svc_trace) { - LOG_INFO(Kernel_SVC, "svc {:#04x} out [0]={:#x} [1]={:#x} [2]={:#x}", imm, - GetArg64(args, 0), GetArg64(args, 1), GetArg64(args, 2)); + LOG_INFO(Kernel_SVC, "svc {:#04x} out t{} [0]={:#x} [1]={:#x} [2]={:#x}", imm, + GetCurrentThread(kernel).GetThreadId(), GetArg64(args, 0), GetArg64(args, 1), + GetArg64(args, 2)); } //kernel.ExitSVCProfile(); kernel.CurrentPhysicalCore().LoadSvcArguments(process, args); diff --git a/src/video_core/macro.cpp b/src/video_core/macro.cpp index 59396cb58b..d70cfefd2f 100644 --- a/src/video_core/macro.cpp +++ b/src/video_core/macro.cpp @@ -4,6 +4,7 @@ // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include #include #include @@ -1360,8 +1361,15 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u [&](u32 address, u32 value) { if (address >= 0xe00) return false; if (address == 0x556) { - static u32 enable_logs{}; - if (enable_logs++ < 400) LOG_INFO(HW_GPU, "Native MME start={:#x} render enable mode={}", method, value); + // SET_RENDER_ENABLE_OVERRIDE: 0 use the render-enable condition, 1 always, + // 2 never. Log a histogram every 20000 macro writes (pop-in diagnostics). + static std::array modes{}; + static u64 enable_writes{}; + ++modes[value & 3]; + if (++enable_writes % 20000 == 0) { + LOG_INFO(HW_GPU, "Native MME render enable modes: use {} always {} never {} other {}", + modes[0], modes[1], modes[2], modes[3]); + } } maxwell3d.CallMethod(system, address, value, true); // Turing MME memory reads (methods 0x550 SET_MME_MEM_ADDRESS_A/B, 0x558 data RAM @@ -1374,6 +1382,17 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u maxwell3d.GetRegisterValue(MemAddressB); auto& memory = maxwell3d.GetMemoryManager(); const u32 ram = maxwell3d.GetRegisterValue(DataRamAddress); + // The words may have just been written by the GPU (e.g. indirect draw arguments + // 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 (memory.IsMemoryDirty(source, read_bytes, VideoCommon::CacheType::BufferCache)) { + memory.FlushRegion(source, read_bytes, VideoCommon::CacheType::BufferCache); + static u64 flushes{}; + if (++flushes % 2000 == 1) { + LOG_INFO(HW_GPU, "Native MME DMA read flushed GPU-written memory ({} times)", flushes); + } + } static u32 dma_logs{}; if (dma_logs < 400) { ++dma_logs; diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp index f55da5fd61..f44916a2e3 100644 --- a/src/yuzu_cmd/yuzu.cpp +++ b/src/yuzu_cmd/yuzu.cpp @@ -172,6 +172,9 @@ struct SdlState { }; extern "C" SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) { + // SDL calls SDL_AppIterate back to back by default, which kept a host core at 100% polling + // input; 240 Hz is plenty for input and the key script. + SDL_SetHint(SDL_HINT_MAIN_CALLBACK_RATE, "240"); SdlState* state = new SdlState(); *appstate = state; -- cgit v1.2.3 From e4deef6ba4ea76aac997ed3102f8e9801fd170b9 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:43:00 -0400 Subject: Native MME: decode each instruction once; reuse the parameter FIFO The Turing macro interpreter re-extracted every field of each 96-bit instruction on every execution and copied the register file per instruction; title 1's GPU thread spent about half its busy time there. Instructions are now decoded once per PC (re-decoded when their words change) and both lanes' operands are read up front instead of copying the registers. The parameter FIFO is a member instead of a vector allocated per macro call. Rendering checked unchanged on the attract loop; the GPU thread went from ~68% to ~45-50% busy. --- src/video_core/macro.cpp | 4 +- src/video_core/macro.h | 1 + src/video_core/macro_turing.h | 155 ++++++++++++++++++++++++++---------------- 3 files changed, 99 insertions(+), 61 deletions(-) (limited to 'src/video_core/macro.cpp') diff --git a/src/video_core/macro.cpp b/src/video_core/macro.cpp index d70cfefd2f..d3ba353ca0 100644 --- a/src/video_core/macro.cpp +++ b/src/video_core/macro.cpp @@ -1354,7 +1354,9 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u } LOG_DEBUG(HW_GPU, "Native MME context={:#x}", context); LOG_DEBUG(HW_GPU, "Native MME start={:#x}, parameters={}, data={:08x}", method, parameters.size(), fmt::join(parameters, ",")); - std::vector fifo(parameters.begin(), parameters.end()); + // Reused between calls (a macro never calls another: methods >= 0xe00 are refused below). + auto& fifo = native_fifo; + fifo.assign(parameters.begin(), parameters.end()); native_interpreter.active_dram = &maxwell3d.GetNativeDataRam(); const auto result = native_interpreter.Execute(native_code, method, fifo, [&](u32 address) { return address < Maxwell3D::Regs::NUM_REGS ? maxwell3d.GetRegisterValue(address) : 0U; }, diff --git a/src/video_core/macro.h b/src/video_core/macro.h index 52dfe7c82c..f744c12942 100644 --- a/src/video_core/macro.h +++ b/src/video_core/macro.h @@ -251,6 +251,7 @@ struct MacroEngine { u32 native_write_offset{}; std::array native_code{}; TuringMacroInterpreter native_interpreter; + std::vector native_fifo; }; diff --git a/src/video_core/macro_turing.h b/src/video_core/macro_turing.h index 063e22c297..b7032eb4a4 100644 --- a/src/video_core/macro_turing.h +++ b/src/video_core/macro_turing.h @@ -25,6 +25,56 @@ public: std::string_view error; }; + // One instruction's fields, decoded once and reused while its words stay the same (macro + // code runs thousands of times a frame; decoding it every time was most of the interpreter). + struct Decoded { + std::array raw{~0U, ~0U, ~0U}; + std::array imm, op, dst, src_a, src_b, out_method, out_emit; + Word pred_reg, pred_mode; + // Lane/output enable masks when the predicate register is non-zero / zero. + Word mask_true, mask_false; + bool end; + std::array load; + }; + + static Decoded Decode(const std::array& bits) { + const auto field = [&](Word low, Word count) { + std::uint64_t value = bits[low / 32]; + if (low / 32 < 2) value |= std::uint64_t{bits[low / 32 + 1]} << 32; + return Word((value >> (low % 32)) & ((std::uint64_t{1} << count) - 1)); + }; + Decoded d; + d.raw = bits; + d.imm = {field(30, 16), field(66, 16)}; + d.op = {field(10, 5), field(46, 5)}; + d.dst = {field(15, 5), field(51, 5)}; + d.src_a = {field(20, 5), field(56, 5)}; + d.src_b = {field(25, 5), field(61, 5)}; + d.out_method = {field(82, 3), field(89, 3)}; + d.out_emit = {field(85, 4), field(92, 4)}; + d.pred_reg = field(5, 5); + d.pred_mode = field(1, 4); + d.end = field(0, 1) != 0; + constexpr std::array predicates{ + "UUUU", "TTTT", "FFFF", "TTUU", "FFUU", "TFUU", "TUUU", "FUUU", + "UUTT", "UUTF", "UUTU", "UUFT", "UUFF", "UUFU", "UUUT", "UUUF"}; + d.mask_true = d.mask_false = 15; + if (d.pred_mode) { + d.mask_true = d.mask_false = 0; + for (Word i = 0; i < 4; ++i) { + if (predicates[d.pred_mode][i] != 'T') d.mask_true |= 1U << i; + if (predicates[d.pred_mode][i] != 'F') d.mask_false |= 1U << i; + } + } + for (Word i = 0; i < 2; ++i) { + const Word reg = 28 + i, out = 3 + i; + d.load[i] = d.pred_reg == reg || d.src_a[0] == reg || d.src_a[1] == reg || + d.src_b[0] == reg || d.src_b[1] == reg || d.out_method[0] == out || + d.out_method[1] == out || d.out_emit[0] == out || d.out_emit[1] == out; + } + return d; + } + template // `parameters` is the load FIFO; `emit` may append to it (MME_DMA_READ_FIFOED). Result Execute(std::span code, Word start, @@ -36,70 +86,53 @@ public: bool end_next{}, stopped{}; std::string_view error; const auto fail = [&](std::string_view message) { if (error.empty()) error = message; }; + if (decoded.size() < code.size() / 3) decoded.resize(code.size() / 3); for (Word steps = 0; steps < 1000000; ++steps) { if (static_cast(pc) * 3 + 2 >= code.size()) { fail("instruction outside uploaded RAM"); break; } const std::array bits{code[pc * 3 + 2], code[pc * 3 + 1], code[pc * 3]}; - const auto field = [&](Word low, Word count) { - std::uint64_t value = bits[low / 32]; - if (low / 32 < 2) value |= std::uint64_t{bits[low / 32 + 1]} << 32; - return Word((value >> (low % 32)) & ((std::uint64_t{1} << count) - 1)); - }; - const std::array imm{field(30, 16), field(66, 16)}; - const std::array op{field(10, 5), field(46, 5)}; - const std::array dst{field(15, 5), field(51, 5)}; - const std::array src_a{field(20, 5), field(56, 5)}; - const std::array src_b{field(25, 5), field(61, 5)}; - const std::array out_method{field(82, 3), field(89, 3)}; - const std::array out_emit{field(85, 4), field(92, 4)}; - const Word pred_reg = field(5, 5), pred_mode = field(1, 4); + Decoded& d = decoded[pc]; + if (d.raw != bits) d = Decode(bits); std::array load{}, alu{}; for (Word i = 0; i < 2; ++i) { - const Word reg = 28 + i, out = 3 + i; - const bool needed = pred_reg == reg || src_a[0] == reg || src_a[1] == reg || - src_b[0] == reg || src_b[1] == reg || out_method[0] == out || - out_method[1] == out || out_emit[0] == out || out_emit[1] == out; - if (needed) { + if (d.load[i]) { if (consumed >= parameters.size()) { fail("parameter FIFO underflow"); break; } load[i] = parameters[consumed++]; } } - // Both ALUs read the register file before either destination is - // committed. Carry chaining remains explicit between the lanes. - const auto input_regs = regs; - const Word input_initialized = initialized; + // Both ALUs read the register file before either destination is committed: their + // operands are read up front. Carry chaining remains explicit between the lanes. const auto get = [&](Word reg, Word lane) -> Word { if (reg < 24) { - if (!(input_initialized & (1U << reg))) fail("uninitialized register read"); - return input_regs[reg]; + if (!(initialized & (1U << reg))) fail("uninitialized register read"); + return regs[reg]; } switch (reg) { case 24: return 0; - case 25: return Word(std::int32_t(std::int16_t(imm[lane]))); - case 26: return Word(std::int32_t(std::int16_t(imm[1 - lane]))); - case 27: return (imm[0] << 16) | imm[1]; + case 25: return Word(std::int32_t(std::int16_t(d.imm[lane]))); + case 26: return Word(std::int32_t(std::int16_t(d.imm[1 - lane]))); + case 27: return (d.imm[0] << 16) | d.imm[1]; case 28: return load[0]; case 29: return load[1]; default: fail("invalid source register"); return 0; } }; - constexpr std::array predicates{ - "UUUU", "TTTT", "FFFF", "TTUU", "FFUU", "TFUU", "TUUU", "FUUU", - "UUTT", "UUTF", "UUTU", "UUFT", "UUFF", "UUFU", "UUUT", "UUUF"}; - Word mask = 15; - if (pred_mode) { - const char skipped = get(pred_reg, 0) ? 'T' : 'F'; - mask = 0; - for (Word i = 0; i < 4; ++i) - if (predicates[pred_mode][i] != skipped) mask |= 1U << i; + const Word mask = d.pred_mode ? (get(d.pred_reg, 0) ? d.mask_true : d.mask_false) : 15; + std::array xs{}, ys{}; + for (Word lane = 0; lane < 2; ++lane) { + if (mask & (1U << lane)) { + xs[lane] = get(d.src_a[lane], lane); + ys[lane] = get(d.src_b[lane], lane); + } } Word next_pc = pc + 1; bool branch_cancels_end{}; for (Word lane = 0; lane < 2 && error.empty(); ++lane) { if (!(mask & (1U << lane))) continue; - const Word x = get(src_a[lane], lane), y = get(src_b[lane], lane); + const Word x = xs[lane], y = ys[lane]; + const Word imm = d.imm[lane]; Word value{}; const auto condition = [&](Word comparison) { switch (comparison) { @@ -110,14 +143,14 @@ public: default: return x == y; } }; - switch (op[lane]) { + switch (d.op[lane]) { case 0: value = x + y; carry = value < x; break; case 1: value = x + y + carry; break; case 2: value = x - y; carry = value > x; break; case 3: value = x - y - carry; break; case 4: case 6: { - const std::uint64_t a = op[lane] == 4 ? std::uint64_t(std::int64_t(std::int32_t(x))) : x; - const std::uint64_t b = op[lane] == 4 ? std::uint64_t(std::int64_t(std::int32_t(y))) : y; + const std::uint64_t a = d.op[lane] == 4 ? std::uint64_t(std::int64_t(std::int32_t(x))) : x; + const std::uint64_t b = d.op[lane] == 4 ? std::uint64_t(std::int64_t(std::int32_t(y))) : y; const auto product = a * b; value = Word(product); carry = Word(product >> 32); break; } @@ -136,33 +169,33 @@ public: case 14: value = x | y; break; case 15: value = x ^ y; break; case 16: { - const Word dest = (imm[lane] >> 10) & 63, count = (imm[lane] >> 5) & 31; - const Word source = imm[lane] & 31; + const Word dest = (imm >> 10) & 63, count = (imm >> 5) & 31; + const Word source = imm & 31; if (dest >= 32) { fail("invalid merge destination"); break; } const Word bits_mask = (1U << count) - 1; value = (x & ~(bits_mask << dest)) | (((y >> source) & bits_mask) << dest); break; } case 17: case 18: case 19: case 20: case 21: - value = condition(op[lane]) ? ~0U : 0; break; + value = condition(d.op[lane]) ? ~0U : 0; break; case 22: value = read((x + y) & 0xffff); break; case 23: - if (loop_count || imm[lane] < 2) { fail("invalid loop"); break; } + if (loop_count || imm < 2) { fail("invalid loop"); break; } // The native SDK emits zero-count loops around FIFO reads. // Unlike Mesa's test simulator, these must skip the body. - if (x == 0) { next_pc = pc + imm[lane]; break; } - loop_count = x - 1; loop_start = pc; loop_end = pc + imm[lane] - 1; + if (x == 0) { next_pc = pc + imm; break; } + loop_count = x - 1; loop_start = pc; loop_end = pc + imm - 1; break; - case 24: next_pc = pc + (imm[lane] & 0x7fff); break; + case 24: next_pc = pc + (imm & 0x7fff); break; case 25: case 26: case 27: case 28: case 29: - if (condition(op[lane]) == bool(imm[lane] & 0x8000)) { - const auto offset = std::int32_t(imm[lane] << 19) >> 19; + if (condition(d.op[lane]) == bool(imm & 0x8000)) { + const auto offset = std::int32_t(imm << 19) >> 19; if (offset == -4096) stopped = true; else { next_pc = Word(std::int32_t(pc) + offset); // Native back edges use bit 14 to suppress the // fallthrough termination when the branch is taken. - branch_cancels_end = (imm[lane] & 0x4000) != 0; + branch_cancels_end = (imm & 0x4000) != 0; } } break; @@ -174,34 +207,34 @@ public: (*active_dram)[x] = y; break; } alu[lane] = value; - if (dst[lane] < 24) { regs[dst[lane]] = value; initialized |= 1U << dst[lane]; } - else if (dst[lane] != 24) fail("invalid destination register"); + if (d.dst[lane] < 24) { regs[d.dst[lane]] = value; initialized |= 1U << d.dst[lane]; } + else if (d.dst[lane] != 24) fail("invalid destination register"); } const auto output = [&](Word selector) -> Word { switch (selector) { case 1: return alu[0]; case 2: return alu[1]; case 3: return load[0]; case 4: return load[1]; - case 5: return imm[0]; case 6: return imm[1]; - case 8: return imm[0] >> 12; case 9: return imm[1] >> 12; - case 10: return (imm[0] << 16) | imm[1]; + case 5: return d.imm[0]; case 6: return d.imm[1]; + case 8: return d.imm[0] >> 12; case 9: return d.imm[1] >> 12; + case 10: return (d.imm[0] << 16) | d.imm[1]; default: fail("invalid output selector"); return 0; } }; for (Word lane = 0; lane < 2 && error.empty(); ++lane) { if (!(mask & (4U << lane))) continue; - if (out_method[lane]) { - const Word address = output(out_method[lane]); + if (d.out_method[lane]) { + const Word address = output(d.out_method[lane]); method = address & 0xfff; increment = (address >> 12) & 15; } - if (out_emit[lane]) { - const Word value = output(out_emit[lane]); + if (d.out_emit[lane]) { + const Word value = output(d.out_emit[lane]); if (error.empty() && !emit(method, value)) fail("unsupported emitted method"); method += increment; } } if (!error.empty()) break; if (end_next || stopped) return {true, pc, consumed, {}}; - end_next = field(0, 1) != 0 && !branch_cancels_end; + end_next = d.end && !branch_cancels_end; if (loop_count && pc == loop_end) { --loop_count; next_pc = loop_start + 1; } pc = next_pc; } @@ -215,6 +248,8 @@ public: // shared by the channels of a process, but each channel keeps its own data RAM (title 1's // channels store their own QMD ring base in word 0x27). DataRam* active_dram{&dram}; + // Decoded instructions by PC, re-decoded whenever their words differ. + std::vector decoded; }; } // namespace Tegra -- cgit v1.2.3 From 25b674eabdb92212a6d8149d6616b2af59317695 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Wed, 28 Oct 2026 23:07:49 -0400 Subject: nx2: measure the TSC on AMD, no rethrows for rejected shaders, debug hooks off the hot path - AMD CPUs have no CPUID leaf 0x15, so the TSC frequency stayed 0 and every guest CNTPCT read went through QueryPerformanceCounter (~10% of the game's main core in title 1). Measure it like Intel CPUs without a crystal frequency. - A rejected native shader was rethrown (two C++ throws) every time it was bound; return false instead (~1-2% of the GPU thread). - The semaphore watch, pushbuffer entry hashing, first-write trace and per-macro debug logs ran on every command word or macro call; they now cost a flag check unless their environment variable is set (~4% of the GPU thread). Also starts the macro engine's profile zone. --- src/common/cpu_features.cpp | 7 +++++++ src/video_core/dma_pusher.cpp | 28 ++++++++++++++++++++-------- src/video_core/engines/maxwell_3d.cpp | 9 ++++++--- src/video_core/macro.cpp | 9 +++++++-- src/video_core/shader_cache.cpp | 12 ++++++++++++ src/video_core/shader_cache.h | 2 ++ 6 files changed, 54 insertions(+), 13 deletions(-) (limited to 'src/video_core/macro.cpp') diff --git a/src/common/cpu_features.cpp b/src/common/cpu_features.cpp index 8ccb6847f7..52fd003c44 100644 --- a/src/common/cpu_features.cpp +++ b/src/common/cpu_features.cpp @@ -225,6 +225,13 @@ const CPUCaps g_cpu_caps = [] { } } + // AMD CPUs have no leaf 0x15 (Zen 4 stops at 0x10): measure their invariant TSC too, or every + // guest clock read (CNTPCT, which games read thousands of times a frame) goes through the + // host's much slower QueryPerformanceCounter. + if (caps.invariant_tsc && caps.tsc_frequency == 0) { + caps.tsc_frequency = X64::EstimateRDTSCFrequency(); + } + if (max_std_fn >= 0x16) { __cpuid(cpu_id, 0x16); caps.base_frequency = cpu_id[0]; diff --git a/src/video_core/dma_pusher.cpp b/src/video_core/dma_pusher.cpp index a1d9ef3f91..3cc5afe967 100644 --- a/src/video_core/dma_pusher.cpp +++ b/src/video_core/dma_pusher.cpp @@ -299,19 +299,26 @@ void DmaPusher::SetState(const CommandHeader& command_header) { // EDEN_NX2_SEM_WATCH=: log every method whose argument is the low word of an // address in the 16 bytes there (finds the engine and method that release a semaphore). +static const u32 g_semaphore_watch = [] { + const char* value = std::getenv("EDEN_NX2_SEM_WATCH"); + return value ? static_cast(std::strtoull(value, nullptr, 16)) : 0U; +}(); + +// EDEN_NX2_PB_CHECK also keeps the last parsed entries, for DumpCurrentEntry. +static const bool g_record_parsed_entries = std::getenv("EDEN_NX2_PB_CHECK") != nullptr; + void DmaPusher::WatchArgument(u32 method, u32 argument) const { - static const u32 watch = [] { - const char* value = std::getenv("EDEN_NX2_SEM_WATCH"); - return value ? static_cast(std::strtoull(value, nullptr, 16)) : 0U; - }(); - if (watch != 0 && (argument & ~0xfU) == watch) { + const u32 watch = g_semaphore_watch; + if ((argument & ~0xfU) == watch) { LOG_INFO(HW_GPU, "Semaphore watch: channel {} subchannel {} method {:#x} argument {:#x}", channel_state.bind_id, dma_state.subchannel, method, argument); } } void DmaPusher::CallMethod(u32 argument) { - WatchArgument(dma_state.method, argument); + if (g_semaphore_watch != 0) [[unlikely]] { + WatchArgument(dma_state.method, argument); + } if (dma_state.method < non_puller_methods) { puller.CallPullerMethod(*this, Engines::Puller::MethodCall{ dma_state.method, @@ -336,8 +343,10 @@ void DmaPusher::CallMethod(u32 argument) { } void DmaPusher::CallMultiMethod(const u32* base_start, u32 num_methods) { - for (u32 i = 0; i < num_methods; ++i) { - WatchArgument(dma_state.method + (dma_state.non_incrementing ? 0 : i), base_start[i]); + if (g_semaphore_watch != 0) [[unlikely]] { + for (u32 i = 0; i < num_methods; ++i) { + WatchArgument(dma_state.method + (dma_state.non_incrementing ? 0 : i), base_start[i]); + } } if (dma_state.method < non_puller_methods) { puller.CallMultiMethod(*this, dma_state.method, dma_state.subchannel, base_start, num_methods, dma_state.method_count); @@ -382,6 +391,9 @@ void DmaPusher::CheckSubmitHash(const CommandListHeader& header, u64 submit_hash void DmaPusher::RecordParsedEntry(const CommandListHeader& header, std::span commands) { + if (!g_record_parsed_entries) { + return; + } u64 hash = 0xcbf29ce484222325ULL; for (const CommandHeader& command : commands) { hash = (hash ^ command.argument) * 0x100000001b3ULL; diff --git a/src/video_core/engines/maxwell_3d.cpp b/src/video_core/engines/maxwell_3d.cpp index 7884d8009c..60efef9b8e 100644 --- a/src/video_core/engines/maxwell_3d.cpp +++ b/src/video_core/engines/maxwell_3d.cpp @@ -393,10 +393,11 @@ void Maxwell3D::CallMacroMethod(Core::System& system, u32 method, const std::vec } // EDEN_NX2_3D_METHODS: log the first write to each 3D register (finds methods Eden doesn't model). +static const bool g_trace_first_method_writes = std::getenv("EDEN_NX2_3D_METHODS") != nullptr; + void Maxwell3D::TraceFirstMethodWrite(u32 method, u32 argument) { - static const bool trace = std::getenv("EDEN_NX2_3D_METHODS") != nullptr; static std::array seen{}; - if (trace && method < seen.size() && !seen[method]) { + if (method < seen.size() && !seen[method]) { seen[method] = true; LOG_INFO(HW_GPU, "3D method first write: {:#x} (byte {:#x}) = {:#x}", method, method * 4, argument); } @@ -419,7 +420,9 @@ void Maxwell3D::CallMethod(Core::System& system, u32 method, u32 method_argument ProcessMacro(system, method, &method_argument, 1, is_last_call); return; } - TraceFirstMethodWrite(method, method_argument); + if (g_trace_first_method_writes) [[unlikely]] { + TraceFirstMethodWrite(method, method_argument); + } ASSERT(method < Regs::NUM_REGS && "Invalid Maxwell3D register, increase the size of the Regs structure"); const u32 argument = ProcessShadowRam(method, method_argument); diff --git a/src/video_core/macro.cpp b/src/video_core/macro.cpp index d3ba353ca0..c74cd6087a 100644 --- a/src/video_core/macro.cpp +++ b/src/video_core/macro.cpp @@ -36,6 +36,7 @@ #include "common/bit_field.h" #include "common/logging.h" #include "video_core/engines/maxwell_3d.h" +#include "video_core/nx2_profile.h" namespace Tegra { @@ -1343,6 +1344,7 @@ static void Dump(u64 hash, std::span code, bool decompiled = false) { void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u32 method, std::span parameters) { + VideoCommon::Nx2Profile::Scope profile_scope{VideoCommon::Nx2Profile::Zone::Macro}; if (native_turing) { maxwell3d.RefreshParameters(); const auto context = reinterpret_cast(this); @@ -1352,8 +1354,11 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u std::ofstream dump("native-graphics-mme-" + std::to_string(context) + ".bin", std::ios::binary); dump.write(reinterpret_cast(native_code.data()), sizeof(native_code)); } - LOG_DEBUG(HW_GPU, "Native MME context={:#x}", context); - LOG_DEBUG(HW_GPU, "Native MME start={:#x}, parameters={}, data={:08x}", method, parameters.size(), fmt::join(parameters, ",")); + // EDEN_NX2_MME_LOG: log every macro call (runs ~750,000 times a second in title 1). + if (static const bool log_calls = std::getenv("EDEN_NX2_MME_LOG") != nullptr; log_calls) { + LOG_DEBUG(HW_GPU, "Native MME context={:#x}", context); + LOG_DEBUG(HW_GPU, "Native MME start={:#x}, parameters={}, data={:08x}", method, parameters.size(), fmt::join(parameters, ",")); + } // Reused between calls (a macro never calls another: methods >= 0xe00 are refused below). auto& fifo = native_fifo; fifo.assign(parameters.begin(), parameters.end()); diff --git a/src/video_core/shader_cache.cpp b/src/video_core/shader_cache.cpp index da5417be02..73a2bf6646 100644 --- a/src/video_core/shader_cache.cpp +++ b/src/video_core/shader_cache.cpp @@ -79,6 +79,13 @@ bool ShaderCache::RefreshStages(std::array& unique_hashes) try { return false; } const ShaderInfo* shader_info{TryGet(*cpu_shader_addr)}; + if (!shader_info && native && IsRejected(*cpu_shader_addr)) { + // A rejected program fails the same way every time it is bound: skip its draws + // without rethrowing (two C++ throws each time cost ~1-2% of the GPU thread). + maxwell3d->GetNativeMacroEngine().native_execution_failed = true; + last_shaders_valid = false; + return false; + } if (!shader_info) { const u32 start_address{native ? 0 : shader_config.offset}; try { @@ -165,6 +172,11 @@ void ShaderCache::RethrowIfRejected(VAddr addr) { } } +bool ShaderCache::IsRejected(VAddr addr) { + std::scoped_lock lock{rejected_mutex}; + return rejected_native.contains(addr); +} + void ShaderCache::RememberRejection(VAddr addr) { std::scoped_lock lock{rejected_mutex}; rejected_native.emplace(addr, std::current_exception()); diff --git a/src/video_core/shader_cache.h b/src/video_core/shader_cache.h index eac2c5ba75..9ffb7bb44d 100644 --- a/src/video_core/shader_cache.h +++ b/src/video_core/shader_cache.h @@ -146,6 +146,8 @@ private: /// Rethrows the error a native program at this address was rejected with, if any. void RethrowIfRejected(VAddr addr); + /// Whether a native program at this address was rejected. + bool IsRejected(VAddr addr); /// Remembers the exception being handled as this address's rejection. void RememberRejection(VAddr addr); -- cgit v1.2.3 From 0f99a77250d089d5ee9afe011564c71866c2dadd Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Wed, 28 Oct 2026 23:46:54 -0400 Subject: nx2: EDEN_NX2_MME_RECORD records macro calls for offline interpreter checks Writes the first 200,000 native macro calls (start, parameters) and the macro code, so an interpreter change can be replayed against the previous interpreter (all 200,000 matched for the operand-slot rewrite, which was then dropped as no faster). --- src/video_core/macro.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) (limited to 'src/video_core/macro.cpp') diff --git a/src/video_core/macro.cpp b/src/video_core/macro.cpp index c74cd6087a..73d5d8e5e1 100644 --- a/src/video_core/macro.cpp +++ b/src/video_core/macro.cpp @@ -1363,6 +1363,25 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u auto& fifo = native_fifo; fifo.assign(parameters.begin(), parameters.end()); native_interpreter.active_dram = &maxwell3d.GetNativeDataRam(); + // EDEN_NX2_MME_RECORD=: write the first 200000 calls (u32 start, u32 count, the + // parameters) and then the macro code to .code, to check interpreter changes + // offline against the previous interpreter. + if (static const char* const record_path = std::getenv("EDEN_NX2_MME_RECORD"); record_path != nullptr) { + static std::ofstream record{record_path, std::ios::binary}; + static u32 recorded{}; + if (recorded < 200000) { + const u32 count = static_cast(parameters.size()); + record.write(reinterpret_cast(&method), sizeof(method)); + record.write(reinterpret_cast(&count), sizeof(count)); + record.write(reinterpret_cast(parameters.data()), parameters.size_bytes()); + if (++recorded == 200000) { + record.flush(); + std::ofstream code{std::string{record_path} + ".code", std::ios::binary}; + code.write(reinterpret_cast(native_code.data()), sizeof(native_code)); + LOG_INFO(HW_GPU, "Recorded 200000 macro calls to {}", record_path); + } + } + } const auto result = native_interpreter.Execute(native_code, method, fifo, [&](u32 address) { return address < Maxwell3D::Regs::NUM_REGS ? maxwell3d.GetRegisterValue(address) : 0U; }, [&](u32 address, u32 value) { -- cgit v1.2.3 From 863d3afa90f2dfde8e715b45f28fc75c1b472739 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:27:35 -0400 Subject: nx2: replay repeated native macro calls instead of interpreting them Title 1 makes ~1,000,000 native macro calls a second in 3D scenes, 7 instructions each on average, and 96% repeat an earlier call exactly. A call whose effects depend only on its start, its parameters and the data RAM words it read (no MME_READ, no memory reads, no re-read of its own data RAM writes, no code upload while it ran) is remembered with those reads as guards; a later call with the same start and parameters whose guards still hold replays its data RAM and method writes. Uploading macro code clears the memo. EDEN_NX2_MME_MEMO=0 interprets every call. Checked offline against the previous interpreter on 200,000 recorded 3D-scene calls (method writes, registers and data RAM all identical, 94% replayed, 1.9x faster); in the game the macro zone fell from ~0.26 to ~0.20 us per call. --- src/video_core/CMakeLists.txt | 1 + src/video_core/macro.cpp | 54 +++++++++++++++++++-- src/video_core/macro.h | 6 +++ src/video_core/macro_memo.h | 110 ++++++++++++++++++++++++++++++++++++++++++ src/video_core/macro_turing.h | 25 ++++++++-- 5 files changed, 188 insertions(+), 8 deletions(-) create mode 100644 src/video_core/macro_memo.h (limited to 'src/video_core/macro.cpp') diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 513a644166..e97b425412 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -90,6 +90,7 @@ add_library(video_core STATIC host1x/vic.h macro.cpp macro.h + macro_memo.h macro_turing.h fence_manager.h gpu.cpp diff --git a/src/video_core/macro.cpp b/src/video_core/macro.cpp index 73d5d8e5e1..ee64ef5ab7 100644 --- a/src/video_core/macro.cpp +++ b/src/video_core/macro.cpp @@ -1365,11 +1365,18 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u native_interpreter.active_dram = &maxwell3d.GetNativeDataRam(); // EDEN_NX2_MME_RECORD=: write the first 200000 calls (u32 start, u32 count, the // parameters) and then the macro code to .code, to check interpreter changes - // offline against the previous interpreter. + // offline against the previous interpreter. EDEN_NX2_MME_RECORD_SKIP=: skip the first + // n calls (title 1 makes ~1,000,000 a second in 3D scenes). if (static const char* const record_path = std::getenv("EDEN_NX2_MME_RECORD"); record_path != nullptr) { static std::ofstream record{record_path, std::ios::binary}; static u32 recorded{}; - if (recorded < 200000) { + static u64 to_skip = [] { + const char* value = std::getenv("EDEN_NX2_MME_RECORD_SKIP"); + return value != nullptr ? std::strtoull(value, nullptr, 10) : 0ULL; + }(); + if (to_skip > 0) { + --to_skip; + } else if (recorded < 200000) { const u32 count = static_cast(parameters.size()); record.write(reinterpret_cast(&method), sizeof(method)); record.write(reinterpret_cast(&count), sizeof(count)); @@ -1382,9 +1389,9 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u } } } - const auto result = native_interpreter.Execute(native_code, method, fifo, - [&](u32 address) { return address < Maxwell3D::Regs::NUM_REGS ? maxwell3d.GetRegisterValue(address) : 0U; }, - [&](u32 address, u32 value) { + // A method write of the macro (also what a remembered call replays). + bool read_memory = false; + const auto emit = [&](u32 address, u32 value) { if (address >= 0xe00) return false; if (address == 0x556) { // SET_RENDER_ENABLE_OVERRIDE: 0 use the render-enable condition, 1 always, @@ -1404,6 +1411,7 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u constexpr u32 MemAddressA = 0x154, MemAddressB = 0x155, DataRamAddress = 0x156; constexpr u32 DmaRead = 0x157, DmaReadFifoed = 0x158; if (address == DmaRead || address == DmaReadFifoed) { + read_memory = true; const GPUVAddr source = (GPUVAddr{maxwell3d.GetRegisterValue(MemAddressA) & 0x1ffffff} << 32) | maxwell3d.GetRegisterValue(MemAddressB); auto& memory = maxwell3d.GetMemoryManager(); @@ -1442,7 +1450,43 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u } } return true; + }; + // 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"); + return value == nullptr || value[0] != '0'; + }(); + if (use_memo) { + native_memo.Synchronize(native_code_generation); + if (decltype(native_memo)::Effects effects; + native_memo.Find(method, parameters, *native_interpreter.active_dram, effects)) { + auto& data_ram = *native_interpreter.active_dram; + for (size_t i = 0; i < effects.data_ram_writes.size(); i += 2) { + data_ram[effects.data_ram_writes[i]] = effects.data_ram_writes[i + 1]; + } + for (size_t i = 0; i < effects.writes.size(); i += 2) { + emit(effects.writes[i], effects.writes[i + 1]); + } + return; + } + native_writes.clear(); + } + const u64 code_generation = native_code_generation; + const auto result = native_interpreter.Execute(native_code, method, fifo, + [&](u32 address) { return address < Maxwell3D::Regs::NUM_REGS ? maxwell3d.GetRegisterValue(address) : 0U; }, + [&](u32 address, u32 value) { + if (use_memo) { + native_writes.emplace_back(address, value); + } + return emit(address, value); }); + // Remember the call if its effects depend only on its parameters and the data RAM words + // it read (and the macro code did not change under it). + if (use_memo && result.success && !read_memory && !native_interpreter.read_registers && + !native_interpreter.reread_data_ram && code_generation == native_code_generation) { + native_memo.Store(method, parameters, native_interpreter.data_ram_reads, + native_interpreter.data_ram_writes, native_writes); + } if (!result.success) { native_execution_failed = true; if (static const bool diagnostics = std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS") != nullptr; diff --git a/src/video_core/macro.h b/src/video_core/macro.h index f744c12942..faae54c3e9 100644 --- a/src/video_core/macro.h +++ b/src/video_core/macro.h @@ -7,6 +7,7 @@ #pragma once #include +#include "video_core/macro_memo.h" #include "video_core/macro_turing.h" #include #include @@ -226,6 +227,7 @@ struct MacroEngine { inline void AddCode(u32 method, u32 data) noexcept { if (native_turing) { if (native_write_offset < native_code.size()) native_code[native_write_offset++] = data; + ++native_code_generation; return; } uploaded_macro_code[method].push_back(data); @@ -252,6 +254,10 @@ struct MacroEngine { std::array native_code{}; TuringMacroInterpreter native_interpreter; std::vector native_fifo; + // Calls replayed instead of interpreted (macro_memo.h), and what the current call wrote. + u64 native_code_generation{}; + BasicMacroMemo<::Common::unordered_map> native_memo; + MacroMemoPairs native_writes; }; diff --git a/src/video_core/macro_memo.h b/src/video_core/macro_memo.h new file mode 100644 index 0000000000..f43bfdceb6 --- /dev/null +++ b/src/video_core/macro_memo.h @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace Tegra { + +using MacroMemoPairs = std::vector>; + +// The effects of native macro calls that depend on nothing but their start address, their +// parameters and data RAM words they read (no register or memory reads), so that a repeated +// call replays its data RAM and method writes instead of being interpreted. In title 1's 3D +// scenes ~96% of the ~1,000,000 calls a second repeat an earlier one exactly, and they run 7 +// instructions on average: the per-call interpreter cost, not the instructions, was the time. +// `Map` maps a u64 (the call's hash) to a u32 (its record's offset in the arena). +template +class BasicMacroMemo { +public: + /// A remembered call's effects: (address, value) pairs, flattened. + struct Effects { + std::span data_ram_writes; + std::span writes; ///< (method, value) in order + }; + + /// An earlier call with this start and these parameters whose data RAM reads still hold. + bool Find(std::uint32_t start, std::span parameters, + std::span data_ram, Effects& effects) const { + const auto it = entries.find(Hash(start, parameters)); + if (it == entries.end()) { + return false; + } + // Record: start, parameter count, read count, data RAM write count, write count, then + // the parameters and the (address, value) pairs. + const std::uint32_t* record = arena.data() + it->second; + const std::uint32_t num_parameters = record[1], num_reads = record[2]; + const std::uint32_t num_data_ram_writes = record[3], num_writes = record[4]; + if (record[0] != start || num_parameters != parameters.size() || + !std::equal(parameters.begin(), parameters.end(), record + HeaderWords)) { + return false; // another call with the same hash + } + const std::uint32_t* reads = record + HeaderWords + num_parameters; + for (std::uint32_t i = 0; i < num_reads; ++i) { + if (data_ram[reads[i * 2]] != reads[i * 2 + 1]) { + return false; + } + } + const std::uint32_t* data_ram_writes = reads + num_reads * 2; + effects.data_ram_writes = {data_ram_writes, num_data_ram_writes * 2}; + effects.writes = {data_ram_writes + num_data_ram_writes * 2, num_writes * 2}; + return true; + } + + void Store(std::uint32_t start, std::span parameters, + const MacroMemoPairs& data_ram_reads, const MacroMemoPairs& data_ram_writes, + const MacroMemoPairs& writes) { + if (arena.size() >= MaxArenaWords) { + arena.clear(); + entries.clear(); + } + const auto offset = static_cast(arena.size()); + arena.push_back(start); + arena.push_back(static_cast(parameters.size())); + arena.push_back(static_cast(data_ram_reads.size())); + arena.push_back(static_cast(data_ram_writes.size())); + arena.push_back(static_cast(writes.size())); + arena.insert(arena.end(), parameters.begin(), parameters.end()); + for (const MacroMemoPairs* pairs : {&data_ram_reads, &data_ram_writes, &writes}) { + for (const auto& [address, value] : *pairs) { + arena.push_back(address); + arena.push_back(value); + } + } + entries.insert_or_assign(Hash(start, parameters), offset); + } + + /// Forgets every call when the macro code is no longer the code they ran. + void Synchronize(std::uint64_t code_generation) { + if (code_generation != generation) { + generation = code_generation; + arena.clear(); + entries.clear(); + } + } + +private: + static constexpr std::size_t HeaderWords = 5; + static constexpr std::size_t MaxArenaWords = std::size_t{1} << 22; // 16 MiB + + static std::uint64_t Hash(std::uint32_t start, std::span parameters) { + std::uint64_t hash = 0x9e3779b97f4a7c15ULL ^ (std::uint64_t{start} << 32) ^ parameters.size(); + for (const std::uint32_t word : parameters) { + hash = (hash ^ word) * 0xff51afd7ed558ccdULL; + hash ^= hash >> 32; + } + return hash; + } + + Map entries; + std::vector arena; + std::uint64_t generation{}; +}; + +} // namespace Tegra diff --git a/src/video_core/macro_turing.h b/src/video_core/macro_turing.h index b7032eb4a4..600db48119 100644 --- a/src/video_core/macro_turing.h +++ b/src/video_core/macro_turing.h @@ -87,6 +87,10 @@ public: std::string_view error; const auto fail = [&](std::string_view message) { if (error.empty()) error = message; }; if (decoded.size() < code.size() / 3) decoded.resize(code.size() / 3); + read_registers = false; + reread_data_ram = false; + data_ram_reads.clear(); + data_ram_writes.clear(); for (Word steps = 0; steps < 1000000; ++steps) { if (static_cast(pc) * 3 + 2 >= code.size()) { fail("instruction outside uploaded RAM"); @@ -178,7 +182,7 @@ public: } case 17: case 18: case 19: case 20: case 21: value = condition(d.op[lane]) ? ~0U : 0; break; - case 22: value = read((x + y) & 0xffff); break; + case 22: value = read((x + y) & 0xffff); read_registers = true; break; case 23: if (loop_count || imm < 2) { fail("invalid loop"); break; } // The native SDK emits zero-count loops around FIFO reads. @@ -201,10 +205,17 @@ public: break; case 30: if (x >= active_dram->size()) { fail("data RAM read out of bounds"); break; } - value = (*active_dram)[x]; break; + value = (*active_dram)[x]; + if (std::ranges::find(data_ram_writes, x, &DataRamAccess::first) != data_ram_writes.end()) { + reread_data_ram = true; + } + data_ram_reads.emplace_back(x, value); + break; case 31: if (x >= active_dram->size()) { fail("data RAM write out of bounds"); break; } - (*active_dram)[x] = y; break; + (*active_dram)[x] = y; + data_ram_writes.emplace_back(x, y); + break; } alu[lane] = value; if (d.dst[lane] < 24) { regs[d.dst[lane]] = value; initialized |= 1U << d.dst[lane]; } @@ -250,6 +261,14 @@ public: DataRam* active_dram{&dram}; // Decoded instructions by PC, re-decoded whenever their words differ. std::vector decoded; + // What the last Execute read besides its parameters, for the macro memo: whether it read a + // register (MME_READ), the data RAM words it read and wrote (address, value) in order, and + // whether it read a word it had written itself. + using DataRamAccess = std::pair; + bool read_registers{}; + bool reread_data_ram{}; + std::vector data_ram_reads; + std::vector data_ram_writes; }; } // namespace Tegra -- cgit v1.2.3 From 322a5afce3d6c5fbcf16ed9a8914805087b33a55 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:37:06 -0400 Subject: nx2: macro memo hashes its keys as they are, rewrites re-stored calls in place The flat map re-hashed the already mixed u64 keys with MSVC's byte-wise FNV-1a (~1.4% of the GPU thread), and calls stored again after a data RAM guard failed appended new records, so the arena filled with stale copies and was cleared every few seconds. EDEN_NX2_MME_MEMO_STATS logs the counters: in title 1, 97.3% of calls replay, 2.1% fail a guard. --- src/video_core/macro.cpp | 11 +++++++++ src/video_core/macro.h | 3 ++- src/video_core/macro_memo.h | 57 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 68 insertions(+), 3 deletions(-) (limited to 'src/video_core/macro.cpp') diff --git a/src/video_core/macro.cpp b/src/video_core/macro.cpp index ee64ef5ab7..2864d1c5ad 100644 --- a/src/video_core/macro.cpp +++ b/src/video_core/macro.cpp @@ -1457,6 +1457,17 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u return value == nullptr || value[0] != '0'; }(); if (use_memo) { + // EDEN_NX2_MME_MEMO_STATS: log the memo's counters every 2^24 calls. + if (static const bool memo_stats = std::getenv("EDEN_NX2_MME_MEMO_STATS") != nullptr; + 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(), + native_memo.ArenaWords() * 4 / 1024); + } native_memo.Synchronize(native_code_generation); if (decltype(native_memo)::Effects effects; native_memo.Find(method, parameters, *native_interpreter.active_dram, effects)) { diff --git a/src/video_core/macro.h b/src/video_core/macro.h index faae54c3e9..a6ca4c4bd2 100644 --- a/src/video_core/macro.h +++ b/src/video_core/macro.h @@ -256,8 +256,9 @@ struct MacroEngine { std::vector native_fifo; // Calls replayed instead of interpreted (macro_memo.h), and what the current call wrote. u64 native_code_generation{}; - BasicMacroMemo<::Common::unordered_map> native_memo; + BasicMacroMemo<::Common::unordered_map> native_memo; MacroMemoPairs native_writes; + u64 native_calls{}; }; diff --git a/src/video_core/macro_memo.h b/src/video_core/macro_memo.h index f43bfdceb6..be8bfc0d7b 100644 --- a/src/video_core/macro_memo.h +++ b/src/video_core/macro_memo.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -14,6 +15,15 @@ namespace Tegra { using MacroMemoPairs = std::vector>; +/// The memo's keys are already well mixed hashes: use them as they are (std::hash is a +/// byte-wise FNV-1a on MSVC, ~20% of a lookup). +struct MacroMemoHash { + using is_avalanching = std::true_type; + std::size_t operator()(std::uint64_t key) const noexcept { + return static_cast(key); + } +}; + // The effects of native macro calls that depend on nothing but their start address, their // parameters and data RAM words they read (no register or memory reads), so that a repeated // call replays its data RAM and method writes instead of being interpreted. In title 1's 3D @@ -29,11 +39,16 @@ public: std::span writes; ///< (method, value) in order }; + struct Statistics { + std::uint64_t hits{}, misses{}, guard_failures{}, stores{}, overwrites{}, clears{}; + }; + /// An earlier call with this start and these parameters whose data RAM reads still hold. bool Find(std::uint32_t start, std::span parameters, - std::span data_ram, Effects& effects) const { + std::span data_ram, Effects& effects) { const auto it = entries.find(Hash(start, parameters)); if (it == entries.end()) { + ++statistics.misses; return false; } // Record: start, parameter count, read count, data RAM write count, write count, then @@ -43,14 +58,17 @@ public: const std::uint32_t num_data_ram_writes = record[3], num_writes = record[4]; if (record[0] != start || num_parameters != parameters.size() || !std::equal(parameters.begin(), parameters.end(), record + HeaderWords)) { + ++statistics.misses; return false; // another call with the same hash } const std::uint32_t* reads = record + HeaderWords + num_parameters; for (std::uint32_t i = 0; i < num_reads; ++i) { if (data_ram[reads[i * 2]] != reads[i * 2 + 1]) { + ++statistics.guard_failures; return false; } } + ++statistics.hits; const std::uint32_t* data_ram_writes = reads + num_reads * 2; effects.data_ram_writes = {data_ram_writes, num_data_ram_writes * 2}; effects.writes = {data_ram_writes + num_data_ram_writes * 2, num_writes * 2}; @@ -60,9 +78,31 @@ public: void Store(std::uint32_t start, std::span parameters, const MacroMemoPairs& data_ram_reads, const MacroMemoPairs& data_ram_writes, const MacroMemoPairs& writes) { + ++statistics.stores; + const std::uint64_t hash = Hash(start, parameters); + // A call stored again (its data RAM reads no longer held) usually has the same shape: + // rewrite its record in place, or the arena fills with stale copies. + if (const auto it = entries.find(hash); it != entries.end()) { + std::uint32_t* record = arena.data() + it->second; + if (record[0] == start && record[1] == parameters.size() && + record[2] == data_ram_reads.size() && record[3] == data_ram_writes.size() && + record[4] == writes.size() && + std::equal(parameters.begin(), parameters.end(), record + HeaderWords)) { + std::uint32_t* out = record + HeaderWords + parameters.size(); + for (const MacroMemoPairs* pairs : {&data_ram_reads, &data_ram_writes, &writes}) { + for (const auto& [address, value] : *pairs) { + *out++ = address; + *out++ = value; + } + } + ++statistics.overwrites; + return; + } + } if (arena.size() >= MaxArenaWords) { arena.clear(); entries.clear(); + ++statistics.clears; } const auto offset = static_cast(arena.size()); arena.push_back(start); @@ -77,7 +117,19 @@ public: arena.push_back(value); } } - entries.insert_or_assign(Hash(start, parameters), offset); + entries.insert_or_assign(hash, offset); + } + + const Statistics& GetStatistics() const { + return statistics; + } + + std::size_t Size() const { + return entries.size(); + } + + std::size_t ArenaWords() const { + return arena.size(); } /// Forgets every call when the macro code is no longer the code they ran. @@ -105,6 +157,7 @@ private: Map entries; std::vector arena; std::uint64_t generation{}; + Statistics statistics; }; } // namespace Tegra -- cgit v1.2.3 From e8127c3759ae260bfae09e3a3a74853204bcd1ee Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Thu, 29 Oct 2026 02:15:53 -0400 Subject: nx2: macro memo tries each macro's last call first; FIFO filled only when interpreting 46% of title 1's macro calls repeat the previous call of the same macro exactly: a per-start slot holding that record's offset skips the hash and the map probe for them (the record is still in the CPU cache). The interpreter's parameter FIFO was filled before the memo lookup; it is now filled only for calls that are interpreted. Checked against the previous interpreter on 200,000 recorded 3D-scene calls: identical writes, registers and data RAM. --- src/video_core/macro.cpp | 5 +++-- src/video_core/macro_memo.h | 52 +++++++++++++++++++++++++++++++++++++-------- 2 files changed, 46 insertions(+), 11 deletions(-) (limited to 'src/video_core/macro.cpp') diff --git a/src/video_core/macro.cpp b/src/video_core/macro.cpp index 2864d1c5ad..3422cb16fa 100644 --- a/src/video_core/macro.cpp +++ b/src/video_core/macro.cpp @@ -1359,9 +1359,9 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u LOG_DEBUG(HW_GPU, "Native MME context={:#x}", context); LOG_DEBUG(HW_GPU, "Native MME start={:#x}, parameters={}, data={:08x}", method, parameters.size(), fmt::join(parameters, ",")); } - // Reused between calls (a macro never calls another: methods >= 0xe00 are refused below). + // Reused between calls (a macro never calls another: methods >= 0xe00 are refused below); + // filled only for calls that are interpreted. auto& fifo = native_fifo; - fifo.assign(parameters.begin(), parameters.end()); native_interpreter.active_dram = &maxwell3d.GetNativeDataRam(); // EDEN_NX2_MME_RECORD=: write the first 200000 calls (u32 start, u32 count, the // parameters) and then the macro code to .code, to check interpreter changes @@ -1482,6 +1482,7 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u } native_writes.clear(); } + fifo.assign(parameters.begin(), parameters.end()); const u64 code_generation = native_code_generation; const auto result = native_interpreter.Execute(native_code, method, fifo, [&](u32 address) { return address < Maxwell3D::Regs::NUM_REGS ? maxwell3d.GetRegisterValue(address) : 0U; }, diff --git a/src/video_core/macro_memo.h b/src/video_core/macro_memo.h index be8bfc0d7b..0e994fc78a 100644 --- a/src/video_core/macro_memo.h +++ b/src/video_core/macro_memo.h @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include #include @@ -43,38 +44,64 @@ public: std::uint64_t hits{}, misses{}, guard_failures{}, stores{}, overwrites{}, clears{}; }; + BasicMacroMemo() { + last_by_start.fill({0, NoRecord}); + } + /// An earlier call with this start and these parameters whose data RAM reads still hold. bool Find(std::uint32_t start, std::span parameters, std::span data_ram, Effects& effects) { + // Nearly half the calls repeat the previous call of the same macro: try its record before + // hashing (no probe, and the record is still in the cache). + auto& last = last_by_start[start % last_by_start.size()]; + if (last.first == start && last.second != NoRecord && + Match(last.second, start, parameters, data_ram, effects)) { + ++statistics.hits; + return true; + } const auto it = entries.find(Hash(start, parameters)); if (it == entries.end()) { ++statistics.misses; return false; } + if (!Match(it->second, start, parameters, data_ram, effects)) { + ++statistics.guard_failures; // or another call with the same hash + return false; + } + last = {start, it->second}; + ++statistics.hits; + return true; + } + +private: + static constexpr std::uint32_t NoRecord = ~0U; + + /// Whether the record at `offset` is this call with its data RAM reads holding; fills + /// `effects` if so. + bool Match(std::uint32_t offset, std::uint32_t start, std::span parameters, + std::span data_ram, Effects& effects) { // Record: start, parameter count, read count, data RAM write count, write count, then // the parameters and the (address, value) pairs. - const std::uint32_t* record = arena.data() + it->second; + const std::uint32_t* record = arena.data() + offset; const std::uint32_t num_parameters = record[1], num_reads = record[2]; const std::uint32_t num_data_ram_writes = record[3], num_writes = record[4]; if (record[0] != start || num_parameters != parameters.size() || !std::equal(parameters.begin(), parameters.end(), record + HeaderWords)) { - ++statistics.misses; - return false; // another call with the same hash + return false; // another call } const std::uint32_t* reads = record + HeaderWords + num_parameters; for (std::uint32_t i = 0; i < num_reads; ++i) { if (data_ram[reads[i * 2]] != reads[i * 2 + 1]) { - ++statistics.guard_failures; return false; } } - ++statistics.hits; const std::uint32_t* data_ram_writes = reads + num_reads * 2; effects.data_ram_writes = {data_ram_writes, num_data_ram_writes * 2}; effects.writes = {data_ram_writes + num_data_ram_writes * 2, num_writes * 2}; return true; } +public: void Store(std::uint32_t start, std::span parameters, const MacroMemoPairs& data_ram_reads, const MacroMemoPairs& data_ram_writes, const MacroMemoPairs& writes) { @@ -100,8 +127,7 @@ public: } } if (arena.size() >= MaxArenaWords) { - arena.clear(); - entries.clear(); + Clear(); ++statistics.clears; } const auto offset = static_cast(arena.size()); @@ -118,6 +144,7 @@ public: } } entries.insert_or_assign(hash, offset); + last_by_start[start % last_by_start.size()] = {start, offset}; } const Statistics& GetStatistics() const { @@ -136,8 +163,7 @@ public: void Synchronize(std::uint64_t code_generation) { if (code_generation != generation) { generation = code_generation; - arena.clear(); - entries.clear(); + Clear(); } } @@ -154,8 +180,16 @@ private: return hash; } + void Clear() { + arena.clear(); + entries.clear(); + last_by_start.fill({0, NoRecord}); + } + Map entries; std::vector arena; + /// The last record found or stored per macro start (direct mapped). + std::array, 256> last_by_start{}; std::uint64_t generation{}; Statistics statistics; }; -- cgit v1.2.3