mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
camera_speed.c rewrite reads the guest's heap and alias regions in 16 MiB pieces under mem_read_hold and skipped only untouched lazy-heap runs. A piece holding pages the lazy write-back had armed (GPU-written buffers the CPU has not read since) faulted into memory.c lazy_fault on each armed range, which fetches it from the GPU under frz_lock while the game runs, disarms those buffers, and keeps the game's mapping calls waiting on the read hold meanwhile. This happened at every change of the stick sensitivity with the lazy write-back on (the Windows default except on AMD's driver). A piece that mem_lazy_touches reports armed is now gone through in 64 KiB steps, and the armed steps are not read; the camera table never lies in a GPU-written buffer. The step after a piece still starts a little before its end, so a copy across two steps or two pieces is found as before. The first search that leaves memory out logs how much, since with NX2NAVI_WB_PROBE every range counts as armed and the search then reports the table "not in memory". A range armed between the check and the read still costs one fetch, as before. Not reproduced before the change (no log of a sensitivity change under trace). tests/camera_search_check.py cuts factor and rewrite from camera_speed.c over a 40 MiB stand-in heap with one armed 64 KiB step: the copies in plain memory, across two pieces and across two steps are rewritten, the copy in armed memory is left alone and no read touches it, the left-out memory is logged once, and with every range armed nothing is read. Against the previous camera_speed.c it fails six of its checks. mouse_stick_check passes, and the compile check adds no warnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
304 lines
15 KiB
C
304 lines
15 KiB
C
// Camera speed: NX2NAVI_STICK_SENS (0.1 to 3, default 1) multiplies the game's own right-stick camera speeds.
|
|
// The game's Options menu offers five camera sensitivities, Very Low to Very High; their turn rates are the array
|
|
// StickSensitivity {0.8, 1.2, 1.6, 2.0, 2.4} in Component/PlayerCameraParam/PlayerCamera.game__component__
|
|
// PlayerCameraParam.bgyml inside Pack/Actor/PlayerCamera.pack.zs. romfs.c serves a copy of that pack with every entry
|
|
// multiplied (cam_pack_build), so the setting raises the camera's top speed as well as its speed at a partial tilt, at
|
|
// any of the five sensitivities. The mouse camera reaches the game as a right-stick deflection (input.c, mouse_camera),
|
|
// divided by the same factor (cam_speed_applied) before it is limited to full tilt (input_mouse_stick), so
|
|
// NX2NAVI_MOUSE_SENS alone sets the mouse's turn per movement, and its top speed is the game's with the factor in it.
|
|
//
|
|
// The pack is a SARC archive compressed with the dictionary pack.zsdic of Pack/ZsDic.pack.zs. A float in a BYML array
|
|
// is stored in the array itself, so the multiplied values take the bytes of the old ones and no offset moves.
|
|
//
|
|
// During a run (cam_speed_tick): the pack served stays the one made at start, and the game has read it by the time the
|
|
// player changes the setting. So a change is written into the game's memory instead: the array as the game holds it
|
|
// (the five values in a row, times the factor in effect) is looked for in the guest's heap and every copy found is
|
|
// overwritten with the values times the new factor. The game reads the array each time it steps the camera, so the
|
|
// camera's speed follows at once. When the game opens the pack again (cam_pack_opened: romfs.c), what it reads holds
|
|
// the start's factor again, and the search runs once more a moment later.
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <zstd.h>
|
|
|
|
#include "cfg.h"
|
|
#include "host.h"
|
|
#include "platform/platform.h"
|
|
#include "sarc.h"
|
|
|
|
#define MEMBER "Component/PlayerCameraParam/PlayerCamera.game__component__PlayerCameraParam.bgyml"
|
|
#define MAX_RAW (64u << 20)
|
|
|
|
static float applied = 1.0f; // the factor the game's values in memory carry: what cam_speed_applied reports
|
|
static float base[8]; // the game's own StickSensitivity values, from the pack as it is before multiplying
|
|
static int base_n; // how many; 0 until cam_pack_build has read the pack (or when it could not)
|
|
static float served_k = 1.0f; // the factor of the pack the game reads from the RomFS: the start's
|
|
|
|
float cam_speed_wanted(void) {
|
|
double v = cfg_double("NX2NAVI_STICK_SENS");
|
|
return (float)(v < 0.1 ? 0.1 : v > 3 ? 3 : v);
|
|
}
|
|
|
|
float cam_speed_applied(void) { return applied; }
|
|
|
|
static int u32_at(const uint8_t *p, size_t n, size_t off, uint32_t *v) {
|
|
if (off > n || n - off < 4) return 0;
|
|
memcpy(v, p + off, 4);
|
|
return 1;
|
|
}
|
|
|
|
// The StickSensitivity floats of the BYML p[0..n), multiplied by k; their count, 0 when the file has no such array.
|
|
static int scale_sensitivity(uint8_t *p, size_t n, float k, float was[8]) {
|
|
uint32_t keys, root, head;
|
|
if (n < 16 || memcmp(p, "YB", 2) || !u32_at(p, n, 4, &keys) || !u32_at(p, n, 12, &root) ||
|
|
!u32_at(p, n, root, &head) || (head & 0xff) != 0xc1 || !u32_at(p, n, keys, &head) || (head & 0xff) != 0xc2)
|
|
return 0;
|
|
uint32_t nkeys = head >> 8;
|
|
if (!u32_at(p, n, root, &head)) return 0;
|
|
for (uint32_t i = 0; i < head >> 8; i++) {
|
|
uint32_t e, at, arr, ah;
|
|
if (!u32_at(p, n, root + 4 + 8 * (size_t)i, &e) || (e & 0xffffff) >= nkeys ||
|
|
!u32_at(p, n, keys + 4 + 4 * (size_t)(e & 0xffffff), &at))
|
|
return 0;
|
|
// in size_t: a mod's key offset near 2^32 would wrap the 32-bit sum below n
|
|
if ((size_t)keys + at + sizeof "StickSensitivity" > n) continue;
|
|
const char *name = (const char *)p + (size_t)keys + at;
|
|
if (strcmp(name, "StickSensitivity")) continue;
|
|
if (e >> 24 != 0xc0 || !u32_at(p, n, root + 8 + 8 * (size_t)i, &arr) || !u32_at(p, n, arr, &ah) ||
|
|
(ah & 0xff) != 0xc0)
|
|
return 0;
|
|
uint32_t count = ah >> 8;
|
|
size_t vals = arr + 4 + ((count + 3) & ~3u);
|
|
if (!count || count > 8 || vals + 4 * (size_t)count > n) return 0;
|
|
for (uint32_t j = 0; j < count; j++)
|
|
if (p[arr + 4 + j] != 0xd2) return 0;
|
|
for (uint32_t j = 0; j < count; j++) {
|
|
float v;
|
|
memcpy(&v, p + vals + 4 * j, 4);
|
|
was[j] = v;
|
|
v *= k;
|
|
memcpy(p + vals + 4 * j, &v, 4);
|
|
}
|
|
return (int)count;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int cam_pack_build(const void *in, size_t n, const void *zsdic, size_t zsdic_n, void **out, size_t *out_n) {
|
|
float k = cam_speed_wanted();
|
|
*out = NULL;
|
|
int ok = 0;
|
|
uint8_t *dics = NULL, *raw = NULL;
|
|
ZSTD_DDict *dd = NULL;
|
|
ZSTD_CDict *cd = NULL;
|
|
void *z = NULL;
|
|
// The frame names its dictionary (pack.zsdic) by id; Pack/ZsDic.pack.zs holds them.
|
|
unsigned id = ZSTD_getDictID_fromFrame(in, n);
|
|
size_t dn, dic_at, dic_n;
|
|
if (!id || !(dics = zsdic_unpack(zsdic, zsdic_n, MAX_RAW, &dn)) || !zsdic_find(dics, dn, id, &dic_at, &dic_n)) {
|
|
rt_log("camera speed: Pack/ZsDic.pack.zs has no dictionary %u for Pack/Actor/PlayerCamera.pack.zs; the camera "
|
|
"keeps its own speed", id);
|
|
goto done;
|
|
}
|
|
unsigned long long rn = ZSTD_getFrameContentSize(in, n);
|
|
dd = ZSTD_createDDict(dics + dic_at, dic_n);
|
|
if (!dd || rn == ZSTD_CONTENTSIZE_UNKNOWN || rn == ZSTD_CONTENTSIZE_ERROR || rn > MAX_RAW ||
|
|
!(raw = malloc((size_t)rn))) {
|
|
rt_log("camera speed: Pack/Actor/PlayerCamera.pack.zs is not a zstd frame with a size; the camera keeps its "
|
|
"own speed");
|
|
goto done;
|
|
}
|
|
ZSTD_DCtx *dctx = ZSTD_createDCtx();
|
|
size_t got = dctx ? ZSTD_decompress_usingDDict(dctx, raw, (size_t)rn, in, n, dd) : 0;
|
|
ZSTD_freeDCtx(dctx);
|
|
size_t at, size;
|
|
float was[8];
|
|
int count = !ZSTD_isError(got) && got == rn && sarc_member(raw, (size_t)rn, MEMBER, &at, &size)
|
|
? scale_sensitivity(raw + at, size, k, was) : 0;
|
|
if (!count) {
|
|
rt_log("camera speed: no StickSensitivity array in %s; the camera keeps its own speed", MEMBER);
|
|
goto done;
|
|
}
|
|
memcpy(base, was, sizeof(float) * (size_t)count); // what a change during the run looks for in memory (cam_speed_tick)
|
|
base_n = count;
|
|
if (k == 1.0f) goto done; // the game's own pack is served as it is
|
|
cd = ZSTD_createCDict(dics + dic_at, dic_n, 9);
|
|
size_t cap = ZSTD_compressBound((size_t)rn), zn = 0;
|
|
ZSTD_CCtx *cctx = cd ? ZSTD_createCCtx() : NULL;
|
|
if (cctx && (z = malloc(cap))) zn = ZSTD_compress_usingCDict(cctx, z, cap, raw, (size_t)rn, cd);
|
|
ZSTD_freeCCtx(cctx);
|
|
if (!z || !zn || ZSTD_isError(zn)) goto done;
|
|
*out = z;
|
|
*out_n = zn;
|
|
z = NULL;
|
|
ok = 1;
|
|
rt_log("camera speed: x%.2f, StickSensitivity %.2f %.2f %.2f %.2f %.2f -> %.2f %.2f %.2f %.2f %.2f", k, was[0],
|
|
count > 1 ? was[1] : 0, count > 2 ? was[2] : 0, count > 3 ? was[3] : 0, count > 4 ? was[4] : 0, was[0] * k,
|
|
count > 1 ? was[1] * k : 0, count > 2 ? was[2] * k : 0, count > 3 ? was[3] * k : 0,
|
|
count > 4 ? was[4] * k : 0);
|
|
done:
|
|
free(z);
|
|
free(raw);
|
|
free(dics);
|
|
ZSTD_freeDDict(dd);
|
|
ZSTD_freeCDict(cd);
|
|
return ok;
|
|
}
|
|
|
|
void cam_pack_served(void) { applied = served_k = cam_speed_wanted(); }
|
|
|
|
// ---- during a run
|
|
|
|
#define COPIES 16
|
|
static uint64_t copies[COPIES]; // where the game's memory held the array at the last search
|
|
static int ncopies;
|
|
|
|
static void factor(float out[8], float k) { // the array at factor k, as cam_pack_build's multiplication gives it
|
|
for (int i = 0; i < base_n; i++) out[i] = base[i], out[i] *= k;
|
|
}
|
|
|
|
// A pass over the guest's heap and alias regions for the array at factor k, byte for byte, in memory the game may
|
|
// read and write; each copy found becomes the array at factor to and is remembered (copies, when fresh is set).
|
|
// Returns how many were rewritten. Runs on its own thread: the regions are read 16 MiB at a time under mem_read_hold,
|
|
// so the game's own mapping calls wait for one piece at most.
|
|
// Pages the lazy write-back has armed (GPU-written buffers the CPU has not read since, never the camera table) are
|
|
// left alone: a read would fetch each armed range from the GPU inside the fault handler (memory.c lazy_fault) while
|
|
// the game runs, and hold the game's mapping calls meanwhile. A piece that holds some is gone through in STEP pieces,
|
|
// without the armed ones; the first search that leaves some out says so, since with NX2NAVI_WB_PROBE every piece
|
|
// counts as armed and the search then finds nothing.
|
|
static int rewrite(float k, float to, int fresh) {
|
|
enum { PIECE = 16 << 20, STEP = 64 << 10 };
|
|
static int told;
|
|
uint64_t left_out = 0;
|
|
float from[8], into[8];
|
|
factor(from, k);
|
|
factor(into, to);
|
|
const size_t len = sizeof(float) * (size_t)base_n;
|
|
const uint8_t first = *(const uint8_t *)from;
|
|
const RtGuestLayout *l = mem_layout();
|
|
const RtRange ranges[2] = {l->heap, l->alias};
|
|
int found = 0;
|
|
if (fresh) ncopies = 0;
|
|
for (int r = 0; r < 2; r++) {
|
|
uint64_t a = ranges[r].base, end = ranges[r].base + ranges[r].size;
|
|
while (a < end) {
|
|
RtMemoryInfo mi;
|
|
if (mem_query(a, &mi) || !mi.size) break;
|
|
uint64_t stop = mi.base + mi.size < end ? mi.base + mi.size : end;
|
|
if (!mi.state || (mi.perm & 3) != 3) { // free, or not the game's to read and write
|
|
a = stop;
|
|
continue;
|
|
}
|
|
while (a < stop) {
|
|
uint64_t skip = mem_untouched_run(a, stop); // heap pages the lazy heap never committed hold nothing
|
|
if (skip) {
|
|
a = skip;
|
|
continue;
|
|
}
|
|
uint64_t n = stop - a < PIECE ? stop - a : PIECE;
|
|
if (mem_lazy_touches(a, n)) {
|
|
if (n > STEP) n = STEP;
|
|
if (mem_lazy_touches(a, n)) {
|
|
a += n;
|
|
left_out += n;
|
|
continue;
|
|
}
|
|
}
|
|
if (n >= len && mem_read_hold(a, n)) {
|
|
const uint8_t *p = (const uint8_t *)(uintptr_t)a, *e = p + n;
|
|
for (const uint8_t *q = p; q + len <= e && (q = memchr(q, first, (size_t)(e - q) - len + 1)); q++) {
|
|
if (memcmp(q, from, len)) continue;
|
|
mem_thaw((uint64_t)(uintptr_t)q, len); // a write-protected page of a tracked region
|
|
memcpy((void *)(uintptr_t)q, into, len);
|
|
if (ncopies < COPIES) copies[ncopies++] = (uint64_t)(uintptr_t)q;
|
|
found++;
|
|
}
|
|
mem_read_release();
|
|
}
|
|
// a copy across two pieces: the next piece starts a little before this one's end
|
|
a += a + n >= stop ? n : n - (len - 1);
|
|
}
|
|
}
|
|
}
|
|
if (left_out && !told) {
|
|
told = 1;
|
|
rt_log("camera speed: the search left out %.1f MiB the GPU wrote and the CPU has not read since (lazy "
|
|
"write-back; with NX2NAVI_WB_PROBE all of it)", left_out / 1048576.0);
|
|
}
|
|
return found;
|
|
}
|
|
|
|
// The copies of the last search, still holding the array at factor k, rewritten to factor to: 1 when every one of them
|
|
// did (a step of the menu's slider then takes no search), 0 when one is gone or changed, with none of them touched.
|
|
static int rewrite_known(float k, float to) {
|
|
float from[8], into[8];
|
|
factor(from, k);
|
|
factor(into, to);
|
|
const size_t len = sizeof(float) * (size_t)base_n;
|
|
if (!ncopies) return 0;
|
|
for (int pass = 0; pass < 2; pass++) // all checked first, then all written
|
|
for (int i = 0; i < ncopies; i++) {
|
|
if (!mem_read_hold(copies[i], len)) return 0;
|
|
int same = !memcmp((const void *)(uintptr_t)copies[i], pass ? into : from, len) ||
|
|
(pass && !memcmp((const void *)(uintptr_t)copies[i], from, len));
|
|
if (same && pass) {
|
|
mem_thaw(copies[i], len);
|
|
memcpy((void *)(uintptr_t)copies[i], into, len);
|
|
}
|
|
mem_read_release();
|
|
if (!same) return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static volatile int32_t searching; // the search thread runs
|
|
static volatile int32_t reopened; // the game opened the pack since the last search (cam_pack_opened)
|
|
static uint64_t due_ns; // when cam_speed_tick starts the next search; 0: none due
|
|
static int tries; // searches left after this one that found nothing
|
|
static float asked = 1.0f; // the factor the last search was started for
|
|
|
|
static void search_main(void *arg) {
|
|
(void)arg;
|
|
float to = cam_speed_wanted(), was = applied;
|
|
uint64_t t0 = plat_now_ns();
|
|
// what the game holds: the array at the factor in effect, and at the start's after it read the pack again
|
|
int n = to != was ? rewrite(was, to, 1) : 0;
|
|
if (served_k != was && served_k != to) n += rewrite(served_k, to, 0); // new copies beside the ones known
|
|
if (n) {
|
|
applied = to;
|
|
rt_log("camera speed: x%.2f now, written over %d cop%s of the game's StickSensitivity in memory (%.0f ms)", to, n,
|
|
n == 1 ? "y" : "ies", (plat_now_ns() - t0) / 1e6);
|
|
} else if (to != was) {
|
|
rt_log("camera speed: x%.2f asked for, but the game's StickSensitivity (x%.2f) is not in memory%s", to, was,
|
|
tries ? "; looking again in a moment" : ": the camera keeps its speed until the game loads it");
|
|
}
|
|
plat_xchg32(&searching, 0);
|
|
}
|
|
|
|
void cam_pack_opened(void) { plat_xchg32(&reopened, 1); }
|
|
|
|
// Every present (gfx_present.c). A factor other than the one in effect goes to the copies the last search found at
|
|
// once; without them (the first change of a run, or the game moved them) a search starts, and a few more while one
|
|
// finds nothing (the game is still loading). A moment after the game opened the pack again a search looks for the
|
|
// start's factor, which is what the game has then read.
|
|
void cam_speed_tick(void) {
|
|
if (!base_n) return;
|
|
float want = cam_speed_wanted();
|
|
if (plat_xchg32(&reopened, 0) && want != served_k) due_ns = plat_now_ns() + 1500000000ull, tries = 3;
|
|
if (want == applied && !due_ns) return;
|
|
if (plat_cas32(&searching, 0, 0)) return; // a search writes applied and the copies
|
|
uint64_t now = plat_now_ns();
|
|
if (want != applied) {
|
|
if (rewrite_known(applied, want)) {
|
|
applied = asked = want;
|
|
return;
|
|
}
|
|
if (want != asked) asked = want, due_ns = now, tries = 3;
|
|
}
|
|
if (!due_ns || now < due_ns) return;
|
|
due_ns = 0;
|
|
if (tries > 0 && want != applied) tries--, due_ns = now + 4000000000ull; // again unless this one finds it
|
|
plat_xchg32(&searching, 1);
|
|
if (!plat_thread_spawn(search_main, NULL, "camera speed", 0)) plat_xchg32(&searching, 0);
|
|
}
|