mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
A player on an AMD card reported crashes in fights and that a line added to nx2navi.ini cured them. That line, async_compute = 1, is no setting of the game, and neither are two more in the same file; the one line of the file that changes how memory reaches the GPU is wb_lazy = 0. A second report, an RX 6600 on 0.7.1, lost its device 3.5 minutes into play with the lazy write-back on. Neither proves it the cause: the player changed several lines at once and sent no log, the RX 6600's last pass mark is inside a draw pass and not a fetch, cards with 8 GB lost their device the same way on NVIDIA before the lazy write-back existed, and reading the fault handler, the fetch, the arming and the buffers' lifetimes found no failure path. What depends on the driver and cannot be checked without the card: pages of guest memory are inaccessible while a host-pointer import of them is bound and written by the GPU, and Vulkan calls run inside the access-fault handler. The default is now decided once the driver is known (wb_lazy_choice.h, vk_memory.c vk_mem_lazy_decide): on under Windows as before, except on AMD's driver branch for RDNA 1 and 2, which none of us has run it on; AMD's driver for RDNA 3 and newer keeps it on, since builds with that default were measured on an RX 7900 XT. What the player sets wins everywhere, and one line at start says the choice and its reason. On those cards open areas run at the 0.7.0 frame rate again (on an RTX 5090 that was 65 fps against 90 in Hyrule Field; not measured on AMD). tests/wb_lazy_check.c checks the choice by platform, driver and setting without a GPU. For reports of this kind the log now holds what was missing. At start: every setting nx2navi.ini sets, and in one line the keys that are no settings of this version and do nothing; the settings menu shows those in its bottom row. At a fatal error or a crash: one line with the card, its driver, the memory mode, the lazy write-back with its reason, armed ranges, fetches so far and a fetch in progress, and the texture detail (dispatch.c rt_fatal_note_add, vk_device.c fatal_note), since a report is often the end of a log alone. --probe-cfg loads the player's file: the three lines are named, and the menu's save keeps them and every other line it does not change byte for byte. Nothing here ran on an AMD card, and no fatal error was provoked to read the new line. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
243 lines
11 KiB
C
243 lines
11 KiB
C
// Indirect-branch dispatch: guest address -> lifted function, O(1).
|
|
//
|
|
// Layout of the table: one uint32_t slot per 4-byte guest instruction over [lo, hi), where lo is the lowest function
|
|
// start of any module and hi is past the highest. A slot is 0 when no function starts there, else the host function's
|
|
// address minus `anchor` (an address just below the lowest host function), so 4 bytes are enough as long as all lifted
|
|
// code lies within 4 GiB, which a single PE image guarantees. Memory: 4 bytes per 4 guest bytes, i.e. the size of the
|
|
// guest range covered. The gaps between code ranges (rodata, data, bss, alignment) are reserved but never written, so
|
|
// the OS never backs them with RAM; resident memory is about the total .text size of the modules.
|
|
//
|
|
// The table is filled once by rt_dispatch_init and then made read-only, so any number of threads read it without
|
|
// locks or per-thread caches. Host (HLE) functions at addresses outside the modules live in a small hash table that
|
|
// is also fixed at init; they are looked up only after the flat table misses.
|
|
#include "dispatch.h"
|
|
#include "host.h"
|
|
#include "prof.h"
|
|
#include "platform/platform.h"
|
|
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
|
|
#define TABLE_SPAN_LIMIT (1ull << 30) // guest bytes covered by the table, and its size in bytes
|
|
#define HOST_CAP 8192 // host function registry, power of two
|
|
#define MAX_MODULES 16
|
|
|
|
// Hot data read by every rt_dispatch: one cache line.
|
|
static struct __attribute__((aligned(64))) {
|
|
const uint32_t *slot;
|
|
uintptr_t anchor;
|
|
uint64_t lo;
|
|
uint64_t slots;
|
|
} D;
|
|
|
|
static const RtModule *g_mods[MAX_MODULES]; // sorted by base
|
|
static int g_nmods;
|
|
static bool g_ready;
|
|
static uint64_t g_table_bytes, g_functions;
|
|
|
|
static struct { uint64_t addr; GuestFn fn; } g_host[HOST_CAP];
|
|
static uint64_t g_nhost;
|
|
|
|
static void (*fatal_notes[4])(void);
|
|
static int n_fatal_notes;
|
|
|
|
void rt_fatal_note_add(void (*fn)(void)) {
|
|
for (int i = 0; i < n_fatal_notes; i++)
|
|
if (fatal_notes[i] == fn) return;
|
|
if (n_fatal_notes < (int)(sizeof fatal_notes / sizeof *fatal_notes)) fatal_notes[n_fatal_notes++] = fn;
|
|
}
|
|
|
|
// Once per process: a second fatal error, on another thread or inside a note, adds none.
|
|
void rt_fatal_notes(void) {
|
|
static volatile int32_t done;
|
|
if (plat_xchg32(&done, 1)) return;
|
|
for (int i = 0; i < n_fatal_notes; i++) fatal_notes[i]();
|
|
}
|
|
|
|
void rt_fatal(const char *fmt, ...) {
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
rt_log_sync(); // main.c: the lines queued for the log thread, then these, written here
|
|
rt_log_raw("FATAL: ", 7);
|
|
rt_vlogf(fmt, ap);
|
|
rt_log_raw("\n", 1);
|
|
va_end(ap);
|
|
// A failed allocation (malloc, realloc, VirtualAlloc) means the commit limit was reached: this line tells a leak of
|
|
// ours (process private bytes near the limit) from a machine short of page file.
|
|
double proc, used, limit;
|
|
plat_commit_info(&proc, &used, &limit);
|
|
rt_log("fatal: memory commit: this process %.0f MB, the system %.0f of %.0f MB", proc, used, limit);
|
|
rt_fatal_notes();
|
|
kern_free_cores_note();
|
|
prof_sampler_write(); // NX2NAVI_PROFILE: what was sampled until now, else a crash loses the whole profile
|
|
// RivaTuner Statistics Server's profile as it was: left with the game's limit in it, the next run would take that
|
|
// limit for the player's own (rtss_limit.c; it gives up after a second if the failing thread holds its lock)
|
|
rtss_limit_restore();
|
|
fflush(stdout);
|
|
fflush(stderr);
|
|
_Exit(70);
|
|
}
|
|
|
|
static const RtModule *module_at(uint64_t pc) {
|
|
for (int i = 0; i < g_nmods; i++)
|
|
if (pc - g_mods[i]->base < g_mods[i]->size) return g_mods[i];
|
|
return NULL;
|
|
}
|
|
|
|
const char *rt_guest_name(uint64_t pc, char *buf, int size) {
|
|
const RtModule *m = module_at(pc);
|
|
if (m) snprintf(buf, size, "%#llx (%s+%#llx)", (unsigned long long)pc, m->name, (unsigned long long)(pc - m->base));
|
|
else snprintf(buf, size, "%#llx", (unsigned long long)pc);
|
|
return buf;
|
|
}
|
|
|
|
static uint64_t host_hash(uint64_t addr) { return (addr >> 2) * 0x9E3779B97F4A7C15ull >> 51; } // 13 bits
|
|
|
|
static GuestFn host_lookup(uint64_t addr) {
|
|
for (uint64_t h = host_hash(addr), n = 0; n < HOST_CAP; h = (h + 1) & (HOST_CAP - 1), n++) {
|
|
if (g_host[h].addr == addr) return g_host[h].fn;
|
|
if (!g_host[h].addr) return NULL;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
void rt_register_host(uint64_t addr, GuestFn fn) {
|
|
if (g_ready) rt_fatal("rt_register_host(%#llx) after rt_dispatch_init", (unsigned long long)addr);
|
|
if (!addr || (addr & 3) || !fn) rt_fatal("rt_register_host: bad entry %#llx", (unsigned long long)addr);
|
|
if (g_nhost >= HOST_CAP / 2) rt_fatal("rt_register_host: more than %d host functions", HOST_CAP / 2);
|
|
uint64_t h = host_hash(addr);
|
|
while (g_host[h].addr && g_host[h].addr != addr) h = (h + 1) & (HOST_CAP - 1);
|
|
if (g_host[h].addr) rt_fatal("rt_register_host: %#llx registered twice", (unsigned long long)addr);
|
|
g_host[h].addr = addr;
|
|
g_host[h].fn = fn;
|
|
g_nhost++;
|
|
}
|
|
|
|
static void *table_alloc(uint64_t bytes) {
|
|
return plat_mem_alloc(bytes); // zero pages, backed on first write
|
|
}
|
|
|
|
static void table_seal(void *p, uint64_t bytes) {
|
|
if (!plat_mem_protect((uint64_t)(uintptr_t)p, bytes, PLAT_PROT_READ)) rt_fatal("dispatch: sealing the table failed");
|
|
}
|
|
|
|
void rt_dispatch_init(const RtModule *mods, int count) {
|
|
if (g_ready) rt_fatal("rt_dispatch_init called twice");
|
|
if (count <= 0 || count > MAX_MODULES) rt_fatal("rt_dispatch_init: %d modules", count);
|
|
|
|
// Modules by base, checked for overlap.
|
|
for (int i = 0; i < count; i++) {
|
|
int j = g_nmods++;
|
|
while (j > 0 && g_mods[j - 1]->base > mods[i].base) { g_mods[j] = g_mods[j - 1]; j--; }
|
|
g_mods[j] = &mods[i];
|
|
}
|
|
for (int i = 1; i < g_nmods; i++)
|
|
if (g_mods[i - 1]->base + g_mods[i - 1]->size > g_mods[i]->base)
|
|
rt_fatal("dispatch: modules %s and %s overlap", g_mods[i - 1]->name, g_mods[i]->name);
|
|
|
|
// Function tables: sorted, aligned, inside their module. Range of guest starts and of host functions.
|
|
uint64_t lo = UINT64_MAX, hi = 0;
|
|
uintptr_t host_lo = UINTPTR_MAX, host_hi = 0;
|
|
for (int i = 0; i < g_nmods; i++) {
|
|
const RtModule *m = g_mods[i];
|
|
for (uint64_t k = 0; k < m->count; k++) {
|
|
uint64_t a = m->addrs[k];
|
|
uintptr_t f = (uintptr_t)m->fns[k];
|
|
if ((a & 3) || a - m->base >= m->size)
|
|
rt_fatal("dispatch: %s_fn_addrs[%llu] = %#llx is misaligned or outside the module", m->name,
|
|
(unsigned long long)k, (unsigned long long)a);
|
|
if (k && a <= m->addrs[k - 1])
|
|
rt_fatal("dispatch: %s_fn_addrs is not strictly ascending at [%llu] = %#llx", m->name,
|
|
(unsigned long long)k, (unsigned long long)a);
|
|
if (!f) rt_fatal("dispatch: %s_fn_ptrs[%llu] is NULL", m->name, (unsigned long long)k);
|
|
if (f < host_lo) host_lo = f;
|
|
if (f > host_hi) host_hi = f;
|
|
}
|
|
if (m->count) {
|
|
if (m->addrs[0] < lo) lo = m->addrs[0];
|
|
if (m->addrs[m->count - 1] + 4 > hi) hi = m->addrs[m->count - 1] + 4;
|
|
}
|
|
}
|
|
if (hi <= lo) rt_fatal("dispatch: no functions in any module");
|
|
if (hi - lo > TABLE_SPAN_LIMIT)
|
|
rt_fatal("dispatch: modules span %#llx guest bytes, over the %#llx the flat table allows",
|
|
(unsigned long long)(hi - lo), TABLE_SPAN_LIMIT);
|
|
uintptr_t anchor = (host_lo & ~(uintptr_t)15) - 16;
|
|
if (host_hi - anchor > UINT32_MAX)
|
|
rt_fatal("dispatch: lifted code spans %#llx host bytes; slots hold 32-bit offsets (4 GiB)",
|
|
(unsigned long long)(host_hi - anchor));
|
|
|
|
for (uint64_t h = 0; h < HOST_CAP; h++)
|
|
if (g_host[h].addr && module_at(g_host[h].addr))
|
|
rt_fatal("dispatch: host function at %#llx lies inside module %s", (unsigned long long)g_host[h].addr,
|
|
module_at(g_host[h].addr)->name);
|
|
|
|
uint64_t slots = (hi - lo) >> 2, bytes = (slots * 4 + 0xFFFF) & ~0xFFFFull;
|
|
uint32_t *slot = table_alloc(bytes);
|
|
if (!slot) rt_fatal("dispatch: cannot allocate a %llu-byte table", (unsigned long long)bytes);
|
|
for (int i = 0; i < g_nmods; i++) {
|
|
const RtModule *m = g_mods[i];
|
|
for (uint64_t k = 0; k < m->count; k++)
|
|
slot[(m->addrs[k] - lo) >> 2] = (uint32_t)((uintptr_t)m->fns[k] - anchor);
|
|
g_functions += m->count;
|
|
}
|
|
table_seal(slot, bytes);
|
|
|
|
g_table_bytes = bytes;
|
|
D.slot = slot;
|
|
D.anchor = anchor;
|
|
D.lo = lo;
|
|
D.slots = slots;
|
|
__atomic_store_n(&g_ready, true, __ATOMIC_RELEASE);
|
|
}
|
|
|
|
GuestFn rt_dispatch_lookup(uint64_t pc) {
|
|
uint64_t i = __builtin_rotateright64(pc - D.lo, 2); // a misaligned pc lands far above D.slots
|
|
if (i < D.slots && D.slot[i]) return (GuestFn)(D.anchor + D.slot[i]);
|
|
return g_ready ? host_lookup(pc) : NULL;
|
|
}
|
|
|
|
__attribute__((noinline, noreturn, cold)) static void dispatch_fail(Cpu *c) {
|
|
char at[64], lr[64];
|
|
uint64_t pc = c->pc;
|
|
if (!g_ready) rt_fatal("rt_dispatch to %#llx before rt_dispatch_init", (unsigned long long)pc);
|
|
const char *why = (pc & 3) ? "misaligned address"
|
|
: module_at(pc) ? "not a function start"
|
|
: "outside every module, no host function registered";
|
|
rt_fatal("rt_dispatch: no function at guest pc %s: %s (lr %s)", rt_guest_name(pc, at, sizeof at), why,
|
|
rt_guest_name(c->x[30], lr, sizeof lr));
|
|
}
|
|
|
|
// Host functions, and the fatal error. A separate tail call keeps the fast path below free of a stack frame.
|
|
__attribute__((noinline)) static void dispatch_slow(Cpu *c) {
|
|
GuestFn f = g_ready ? host_lookup(c->pc) : NULL;
|
|
if (!f) dispatch_fail(c);
|
|
__attribute__((musttail)) return f(c);
|
|
}
|
|
|
|
// A pending preempt request at an indirect call (a safe point: the guest state is all in Cpu). Out of line, so
|
|
// rt_dispatch keeps no stack frame.
|
|
static __attribute__((noinline)) void dispatch_preempt(Cpu *c) {
|
|
kern_checkpoint();
|
|
__attribute__((musttail)) return rt_dispatch(c);
|
|
}
|
|
|
|
// Jump to the guest address in c->pc. Always a tail call, so chains of indirect jumps keep the host stack flat. A
|
|
// pending preempt request costs two loads and a branch to check.
|
|
void rt_dispatch(Cpu *c) {
|
|
if (__builtin_expect(c->thread && kern_preempt_pending(c->thread), 0))
|
|
__attribute__((musttail)) return dispatch_preempt(c);
|
|
uint64_t i = __builtin_rotateright64(c->pc - D.lo, 2);
|
|
uint32_t off = i < D.slots ? D.slot[i] : 0;
|
|
if (__builtin_expect(!off, 0)) __attribute__((musttail)) return dispatch_slow(c);
|
|
GuestFn f = (GuestFn)(D.anchor + off);
|
|
__attribute__((musttail)) return f(c);
|
|
}
|
|
|
|
RtDispatchStats rt_dispatch_stats(void) {
|
|
return (RtDispatchStats){D.lo, D.lo + D.slots * 4, D.slots, g_table_bytes, g_functions, g_nhost};
|
|
}
|