diff options
| -rw-r--r-- | src/video_core/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | src/video_core/macro.cpp | 54 | ||||
| -rw-r--r-- | src/video_core/macro.h | 6 | ||||
| -rw-r--r-- | src/video_core/macro_memo.h | 110 | ||||
| -rw-r--r-- | src/video_core/macro_turing.h | 25 |
5 files changed, 188 insertions, 8 deletions
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=<file>: write the first 200000 calls (u32 start, u32 count, the // parameters) and then the macro code to <file>.code, to check interpreter changes - // offline against the previous interpreter. + // offline against the previous interpreter. EDEN_NX2_MME_RECORD_SKIP=<n>: 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<u32>(parameters.size()); record.write(reinterpret_cast<const char*>(&method), sizeof(method)); record.write(reinterpret_cast<const char*>(&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 <memory> +#include "video_core/macro_memo.h" #include "video_core/macro_turing.h" #include <span> #include <variant> @@ -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<u32, 0x3000> native_code{}; TuringMacroInterpreter native_interpreter; std::vector<u32> native_fifo; + // Calls replayed instead of interpreted (macro_memo.h), and what the current call wrote. + u64 native_code_generation{}; + BasicMacroMemo<::Common::unordered_map<u64, u32>> 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 <algorithm> +#include <cstddef> +#include <cstdint> +#include <span> +#include <utility> +#include <vector> + +namespace Tegra { + +using MacroMemoPairs = std::vector<std::pair<std::uint32_t, std::uint32_t>>; + +// 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 <typename Map> +class BasicMacroMemo { +public: + /// A remembered call's effects: (address, value) pairs, flattened. + struct Effects { + std::span<const std::uint32_t> data_ram_writes; + std::span<const std::uint32_t> 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<const std::uint32_t> parameters, + std::span<const std::uint32_t> 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<const std::uint32_t> 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<std::uint32_t>(arena.size()); + arena.push_back(start); + arena.push_back(static_cast<std::uint32_t>(parameters.size())); + arena.push_back(static_cast<std::uint32_t>(data_ram_reads.size())); + arena.push_back(static_cast<std::uint32_t>(data_ram_writes.size())); + arena.push_back(static_cast<std::uint32_t>(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<const std::uint32_t> 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<std::uint32_t> 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<std::size_t>(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> 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<Word, Word>; + bool read_registers{}; + bool reread_data_ram{}; + std::vector<DataRamAccess> data_ram_reads; + std::vector<DataRamAccess> data_ram_writes; }; } // namespace Tegra |
