Swing the shake key's stick by the game's updates instead of the clock, two updates a side at any frame rate.

shake_stick picked the side from the time since the press, 67 ms a side (two frames at the game's 30 fps), so above
30 fps each side lasted more of the game's updates: 8 at 120 fps and 16 at 240, slower swings than the game reads at
its own rate. The side now comes from the game's present count, which frame_rate.c already kept for the flight
recorder and now gives through frame_step_presents, two presents a side. The press still swings for 800 ms of the
clock and a hold keeps it going, as before; while no present comes (a load) the stick keeps its side.

Not reproduced before the change (read from the code). The new tests/shake_key_check.py builds shake_stick from
input.c and passes (it fails 118 times on the old code); the runtime compile check and the -Wall comparison of
input.c and frame_rate.c show no new warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 19:00:47 -05:00
parent 5f9374e1d9
commit 5c9e480f90
5 changed files with 147 additions and 7 deletions

View File

@@ -756,6 +756,11 @@ files.
A change to how a gamepad's axes are read (`runtime/input.c` `pad_axis`: an axis counts once it has been seen at rest)
passes `python tests\pad_rest_check.py`, which builds that function with stand-ins for SDL. No game files.
A change to the shake key (`runtime/input.c` `shake_stick`) passes `python tests\shake_key_check.py`, which builds that
function with stand-ins for the clock and the game's present count: each side lasts two of the game's updates from 20
to 240 fps, also with two reads an update, a press swings for 800 ms, a hold keeps swinging until its release, and a
swing without presents keeps its side. No game files.
A change to HD rumble's encoding or report schedule (`runtime/rumble_hd.c`: a vibration value as the bytes of a Switch
controller's rumble report, and which reports are written when) passes `python tests\rumble_hd_check.py`, which builds
that file alone: known values and the neutral one, the frequency and amplitude clamps (no amplitude step above the

View File

@@ -425,7 +425,10 @@ static double ocarina_step_s(void) {
return base * speed / 60.0;
}
static _Atomic uint64_t guest_presents; // frame_step_present calls so far (frame_step_flight)
static _Atomic uint64_t guest_presents; // frame_step_present calls so far (frame_step_flight, frame_step_presents)
// The game's presents so far, one per update of the game at any frame rate (input.c shake_stick counts its updates).
uint64_t frame_step_presents(void) { return atomic_load_explicit(&guest_presents, memory_order_relaxed); }
// The flight recorder's view of the step at a host present (flight.c, render thread): the newest step the patch page
// holds in 60 Hz frames per update (set ostep; -1 without), the game's own step from its time manager in 60 Hz frames

View File

@@ -469,6 +469,7 @@ void frame_rate_sound_gate_host(Cpu *c); // the sound job at wh
void frame_rate_sound_step_host(Cpu *c); // the step the sound job hands on (set osound, page +0xf00)
float frame_step_frames(void); // the 60 Hz frames the game steps per update (set ostep), else 1
void frame_step_flight(float *page, float *game, uint64_t *presents); // flight.c: the step at a host present
uint64_t frame_step_presents(void); // the game's presents so far: one per update of the game
// rtss_limit.c: RivaTuner Statistics Server takes the frame limit where it runs
void rtss_limit_want(int64_t mhz); // the pacer's limit in millihertz: >0, 0 none; or why RTSS is
// kept out (rtss_decide.h RTSS_OUT_*)

View File

@@ -1032,21 +1032,23 @@ static void pick(int32_t *x, int32_t *y, int32_t kx, int32_t ky) {
}
// The shake key (B_SHAKE): the game detaches a part fused to the held object when the right stick swings left and
// right a few times. A press swings it for SHAKE_NS, holding keeps it swinging; each side lasts SHAKE_SIDE_NS, two
// frames at the game's 30 fps, so every side is read even at 20 fps. Returns the stick x, 0 while not shaking.
// right a few times. A press swings it for SHAKE_NS, holding keeps it swinging. Each side lasts SHAKE_SIDE_STEPS
// updates of the game, counted by its presents (frame_rate.c frame_step_presents), as two frames at the game's 30 fps
// did, so the game sees the same swings at any frame rate. A side of a fixed time (two 30 fps frames, 67 ms) lasted 8
// updates at 120 fps and 16 at 240. Returns the stick x, 0 while not shaking.
// Under lock.
#define SHAKE_NS 800000000ull
#define SHAKE_SIDE_NS 66666667ull
#define SHAKE_SIDE_STEPS 2
static int32_t shake_stick(int held) {
static int was;
static uint64_t from, until;
uint64_t now = plat_now_ns();
if (held && !was && now >= until) from = now;
uint64_t now = plat_now_ns(), steps = frame_step_presents();
if (held && !was && now >= until) from = steps;
if (held && !was) until = now + SHAKE_NS;
if (held && until < now) until = now;
was = held;
if (now >= until && !held) return 0;
return (now - from) / SHAKE_SIDE_NS % 2 ? STICK_MAX : -STICK_MAX;
return (steps - from) / SHAKE_SIDE_STEPS % 2 ? STICK_MAX : -STICK_MAX;
}
// Under lock and inside sdl_enter (the gamepads' state is up to date), state 1: the open gamepad's inputs by position

129
tests/shake_key_check.py Normal file
View File

@@ -0,0 +1,129 @@
"""The shake key swings the right stick by the game's updates, not by the clock (runtime/input.c shake_stick): builds
shake_stick, cut from input.c by its text, with stand-ins for the clock and the present count
(frame_rate.c frame_step_presents), and checks that
- each side lasts two updates at 20, 30, 60, 120, 144 and 240 fps, starting left, also when the stick is read
twice an update;
- a press swings it for 800 ms, a hold keeps it swinging and the release ends it at once;
- a press inside the swing keeps its side, and a swing without presents (a load) keeps one side.
No game files, no SDL. Usage: python tests/shake_key_check.py [--cc <clang>]
"""
import argparse
import os
import subprocess
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
HARNESS = r'''
#include <stdint.h>
#include <stdio.h>
enum { STICK_MAX = 32767 };
static uint64_t now_ns = 5000000000ull, presents = 1000;
static uint64_t plat_now_ns(void) { return now_ns; }
static uint64_t frame_step_presents(void) { return presents; }
%s
static int fails;
#define CHECK(cond, ...) do { if (!(cond)) { printf("FAIL: " __VA_ARGS__); putchar('\n'); fails++; } } while (0)
// One update at hz: the clock moves a frame, the game presents once, and the stick is read reads times.
static int32_t update(int hz, int held, int reads) {
now_ns += 1000000000ull / (uint64_t)hz;
presents++;
int32_t x = 0;
for (int r = 0; r < reads; r++) {
int32_t y = shake_stick(held);
CHECK(r == 0 || y == x, "two reads in one update differ: %%d %%d", x, y);
x = y;
}
return x;
}
static void settle(void) {
for (int i = 0; i < 4; i++) now_ns += 1000000000ull, presents++, shake_stick(0);
}
int main(void) {
static const int rates[] = {20, 30, 60, 120, 144, 240};
for (int i = 0; i < 6; i++) {
for (int reads = 1; reads <= 2; reads++) {
int hz = rates[i];
settle();
int32_t first = shake_stick(1); // the press, read before the update that shows it
CHECK(first == -STICK_MAX, "%%d fps: the swing starts left (%%d)", hz, first);
int n = 0, sides = 0;
int32_t last = first, run = 1;
for (int k = 0; k < hz * 2; k++) {
int32_t x = update(hz, 0, reads);
if (!x) break;
n++;
if (x != last) {
CHECK(run == 2, "%%d fps, %%d reads: a side lasted %%d updates", hz, reads, run);
sides++, run = 0, last = x;
}
run++;
}
// the press swings for 800 ms: 0.8 * hz updates after the press, the first counted at the press
int want = (int)(0.8 * hz + 0.5) - 1;
CHECK(n >= want - 1 && n <= want + 1, "%%d fps: %%d updates swung, want about %%d", hz, n, want);
CHECK(sides >= n / 2 - 1, "%%d fps: %%d sides in %%d updates", hz, sides, n);
}
}
// A hold keeps swinging past 800 ms; the release ends it at once.
settle();
shake_stick(1);
int swung = 0;
for (int k = 0; k < 240 * 2; k++) swung += update(240, 1, 1) != 0;
CHECK(swung == 480, "a 2 s hold swung %%d of 480 updates", swung);
CHECK(update(240, 0, 1) == 0, "the release after a long hold ends the swing");
// A second press inside the swing keeps the side it is on.
settle();
shake_stick(1);
update(60, 0, 1), update(60, 0, 1);
int32_t on = update(60, 0, 1);
int32_t again = shake_stick(1);
CHECK(on == STICK_MAX && again == on, "a press inside the swing moved the side: %%d to %%d", on, again);
// No presents (a load screen): the stick keeps its side while the swing lasts.
settle();
shake_stick(1);
for (int k = 0; k < 10; k++) {
now_ns += 50000000ull;
CHECK(shake_stick(0) == -STICK_MAX, "without presents the side changed at %%d", k);
}
printf(fails ? "shake key: %%d failures\n" : "shake key: ok\n", fails);
return fails != 0;
}
'''
def cut(text, start, end_marker):
a = text.index(start)
b = text.index(end_marker, a)
return text[a:b]
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('--cc', help='the C compiler (default: MSYS2 UCRT64 clang on Windows, clang elsewhere)')
args = ap.parse_args()
win = os.name == 'nt'
cc = args.cc or (str(MSYS_BIN / 'clang.exe') if win and MSYS_BIN.exists() else 'clang')
env = dict(os.environ)
if win and MSYS_BIN.exists():
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
src = (ROOT / 'runtime' / 'input.c').read_text(encoding='utf-8')
body = cut(src, '#define SHAKE_NS', '\n// Under lock and inside sdl_enter')
with tempfile.TemporaryDirectory(prefix='nx2navi-shake-') as tmp:
c = Path(tmp) / 'check.c'
c.write_text(HARNESS % body, encoding='utf-8')
exe = Path(tmp) / ('check.exe' if win else 'check')
subprocess.check_call([cc, '-O2', '-Wall', '-Wextra', '-Wno-unused-function', str(c), '-o', str(exe)], env=env)
return subprocess.call([str(exe)])
if __name__ == '__main__':
sys.exit(main())