aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/video_core/CMakeLists.txt1
-rw-r--r--src/video_core/macro.cpp54
-rw-r--r--src/video_core/macro.h6
-rw-r--r--src/video_core/macro_memo.h110
-rw-r--r--src/video_core/macro_turing.h25
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