21 Commits

Author SHA1 Message Date
chrg6
5c9e480f90 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>
2026-11-13 20:13:03 -05:00
chrg6
87fd385b7b Stop HD rumble at a fatal error and a crash too, on a thread of its own the end waits for 300 ms at most.
input_rumble_hd_stop ran only in the orderly ends, so a FATAL or a crash while a controller vibrated on HD rumble
left it buzzing: nothing renews or stops the value after the process is gone (input.c says so). It also took
hd_lock with a blocking lock while it only tried the other two.

dispatch.c rt_rumble_stop_bounded runs input_rumble_hd_stop (weak in dispatch.c, as fs_save_crash_wait is, for
builds without input.c) on a new "rumble stop" thread and waits at most 300 ms for it, never joining it: the stop
goes through SDL, which takes its own joystick lock, and a crash on SDL's thread while it held that lock would
otherwise keep the ending thread there and the process would never end. fatal_end and main.c crash_handler call
it right after rtss_limit_restore. input.c tries hd_lock 50 times 1 ms apart, as it does `lock` and sdl_lock, and
skips the stop when it is not taken.

Not reproduced before the change. tests/fatal_exit_check.py (now linking dispatch.c as its own unit: the stop runs
at a fatal error; a stop that hangs leaves the end at code 70 after 0.3 s; statically both failed ends call the
bounded stop after the RTSS restore), tests/rumble_hd_check.py and the compile checks pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:42:10 -05:00
chrg6
b399d8f15c Keep a late HD rumble report from opening a Joy-Con pair again after the stop.
hd_tick picks a report under the locks and writes it to the pair after letting them go. When input_rumble_hd_stop
wrote the neutral value and closed the pair in between, that write opened the two devices again and left the
Joy-Con vibrating after the exit. The stop now ends the pair (hd_pair_end), and hd_pair_write checks that under the
lock it holds for the write, so nothing is written after the stop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 06:06:27 -05:00
chrg6
df8431da7b Send the game's vibration values as they are to a Switch Pro Controller or Joy-Con, behind rumble_hd.
With NX2NAVI_RUMBLE_HD=1 (live, off by default) the gamepad the game reads, when it is a Switch Pro Controller, a
single Joy-Con or a pair of them, gets each side's low and high band with its own frequency and amplitude in the
controller's rumble report, and no dual-motor call any more. A Pro Controller and a single Joy-Con take the report
through SDL's effect call, over USB and Bluetooth; SDL refuses that call for a combined pair, so a pair's reports are
written to its two HID devices directly. Every other gamepad, the Switch 2 controllers included, keeps the motors.

The game's thread posts its newest values and a thread of input.c writes the reports: at most one every 15 ms
(rumble_hd_ms), a held value again every 30 ms, and the neutral value when the game stops sending for 250 ms, when
another gamepad takes over, when the setting is turned off and before the process ends. The rumble strength scales the
amplitude and rumble = 0 silences this path as well. With the setting off nothing is written to the controller: the
log line at the take names it by its ids, and the first report is the trial.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 22:28:36 -05:00
chrg6
0533b3028e Keep the asynchronous replay, the hot-pool rule and the -O2 game code off unless a player turns them on.
The three change what every card does: the asynchronous replay can show a dark or flickering frame now and then, the hot-pool rule moves pools into system memory on every discrete card, and -O2 makes each rebuild of the game several times longer. Their rows stay in the settings tab. The USB joystick mapping is described by the device's own name, and the tab's introduction says what its settings are for.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 12:17:55 -05:00
Tobim6
496c163d86 Map the "Nyx Joy-Con" USB HID joystick as a gamepad, and log joysticks SDL cannot use.
A Switch can present its Joy-Con over USB from its boot menu's tools as a plain HID joystick, "Nyx Joy-Con" (VID
0x11EC, PID 0xA7E1): X, Y, Z, Rz for the sticks, a hat for the D-pad and twelve buttons. SDL has no gamepad mapping for
it, so it was a joystick only and input.c, which opens gamepads, did not see it ("input: no gamepad"). scan_pads now
gives every known joystick without a mapping one built from its own GUID (buttons 1-4 B, A, Y, X as south, east, west,
north, then L, R, ZL, ZR, Minus, Plus and the stick clicks), and logs any other unmapped joystick once with its name,
ids and GUID.

(cherry picked from commit ed4c68c3e8460e247bab3ad4f3f02e2672868ddf)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 16:21:52 +01:00
chrg6
c60abba2e5 Count a gamepad axis only once it has been seen at rest.
A device that reports an axis held from the start (a virtual pad nobody holds, a trigger or pedal mapped onto a stick,
a stick resting far off its middle) turned the camera or moved Link on its own, and since pad_active counted it as
pressed, it could take the gamepad role from the controller in the player's hands whenever that one was idle, and keep
it. input.c now reads every axis through pad_axis: 0 until the axis has been within a quarter of its range of rest
(a trigger's rest is 0), then the device's value. The game's sticks and triggers, the menu's stick readout and the
choice of gamepad all use it. A stick the player holds at the start counts once it has been let go.

Checked: the runtime compiles; new tests/pad_rest_check.py (pad_axis cut from input.c, SDL stood in for) passes: a
held axis reads 0 for 100 polls, a released one reads the device, and axes and gamepads keep separate states;
tests/mouse_stick_check.py still passes. Not run with a real device.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 19:38:18 -05:00
chrg6
2d48afb052 Open the menu when the gamepad in use goes away and no other is connected.
The game is always told one controller is connected, so a disconnect read as nothing pressed and play went on without
the player noticing; the console would stop and ask for a controller. Audit finding F-8-04.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 07:47:08 -05:00
chrg6
861357b2a4 Make the direct mouse camera work on game 1.1.1.
mouse_mode = direct is the default, but on 1.1.1 it did nothing: only the 1.4.3 patch table had the sites, so the mouse always went through the stick emulation, which gives no turn at slow mouse speeds and tops out at about 107 degrees a second. The 1.1.1 table now has the set mouse_direct (on by default): at main+0x37c45c, where the camera's input update holds the processed stick in s1 and s0, a host call performs the displaced store and adds the raw mouse counts to both, so the mouse meets no dead zone and no curve; at main+0x37c4f0 a second call clears the idle bit for an update the mouse moved in. The counts are divided by the frames the game steps per update and by the stick camera speed, so the same hand movement turns the same angle at every frame rate. The Controls tab gets the mouse mode and mouse sensitivity rows, and mouse_mode = stick or NX2NAVI_MOUSE_DIRECT_PATCH=0 gives the old behaviour.

Checked here: the generator lifts the 1.1.1 game with the two sites (it compares each site's original word), the lifted game compiles and links with the runtime, and tests/mouse_direct_check.py passes its 1.1.1 harness (register contract, displaced store and load, equal turn at every frame rate and stick speed, the reset pause, capture and mode). Not checked here: the game was not run, so aiming with the mouse was not tried, and the differential tests against Unicorn were not run because no Python on this PC has Unicorn. The contributor measured 0.0800 degrees per count on both axes in the game.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
(cherry picked from commit bb8fecfbe43517006eafd7be9c22e8dbe9601de7)
2026-11-11 02:36:54 -05:00
chrg6
ecd34fb513 Under the perf trace, report frame times per route segment and take a memory snapshot after each area load.
An input script line "<tick> MARK <name>" (or "@<ms> MARK <name>") ends the running segment with one line of its frame count, mean, 1% low and worst frame, the work marks on any thread, process memory and VRAM, then the 600-frame memory lines, and starts a segment of that name. RomFS opens of an area's static placement file give a "scene:" line, and once no area has loaded for 10 s the memory snapshot is taken with a "settled" line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 19:40:02 -05:00
Michael Schienbein
919aa7b2a8 Let the mouse camera in stick mode reach the game's top turn speed at a camera speed above 1.
With the mouse camera in stick mode and a camera speed above 1, the mouse could never turn faster than a full stick does at camera speed 1, however high the mouse sensitivity was set. The mouse's counts were limited to full tilt first and divided by the camera speed afterwards, so at camera speed 1.5 the most the mouse could send was two thirds of full tilt, which the game's own factor brought back to the old top speed. The division now comes before the limit (runtime/input_map.c input_mouse_stick): a slow movement turns as far as before, and a fast flick reaches full tilt, which is the game's top speed with the camera speed in it. Direct mouse mode, the default, does not use this code. A camera speed of 1 or less behaves as before.

From pull request #424. tests/mouse_stick_check.py is new here: it builds input_map.c alone and checks the deflection at each camera speed without the game.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 12:48:50 -05:00
chrg6
11898757d1 Name the gamepad probe probe_gamepad, so it does not clash with ov_probe.c's own static probe_input.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 23:53:29 -05:00
chrg6
be8c2620e5 Run the gamepad update on a thread of its own, so a controller that comes or goes no longer stalls the game.
When a device came or went, SDL's gamepad update first found every device again, 100 ms and more, on the game's
polling thread with the input lock held: a controller disconnecting gave five frames of 120 to 240 ms in a row, with
the game's other pollers queued on the lock. The pad thread now makes SDL's initialisation and every update. A poll
asks for an update and waits at most 2 ms for it; an ordinary one is done in microseconds and the poll reads fresh
state as before, and during a device search the poll goes on with the state it read last. Rumble, the motion samples
and the menu's stick reading skip a beat then instead of waiting.
--probe-input checks it on a virtual gamepad, with an update made to take 300 ms.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-09 23:51:58 -05:00
chrg6
745d48bc8c Free released recording copies outside their lock, skip the yield's lock and scans when nobody waits for a core, and wait instead of spinning before a paced frame.
Found by profiling a play session and reading the kernel and the command recording:
- nvn_api_cmd.c: a released recording copy had its pins uncounted (a walk over every op) and was freed while copy_lock was held exclusively; the main-loop thread waited for that lock 2.8% of its time. The copies are now chained under the lock and freed after it. The walks over a recording's records test a mask of the five op kinds that name pins before calling op_pins.
- kernel.c: SleepThread(0) took the kernel lock and computed the caller's inherited priority, a scan over every blocked thread, also when no thread waited for a core, which is nearly always. It now yields at once then. A wait without a timeout no longer reads the clock.
- nvn_api_obj.c sleep_until: the wait for a paced frame's slot slept coarsely and left the last 1 to 2 ms to a yield loop; the high-resolution wait now takes all but its own margin.
- input.c: the keyboard scan (a system call per key) is reused for reads within a millisecond.
- nvn_api_core.c: the read-lock observer's commit-point address is 1.2.1's and is now only compared there.
- kernel_probe.c: the probe expected three virtual cores and wait type 3 to be refused; the kernel has six cores and implements the 64-bit wait. It now checks both as they are, counts a core number past the cores as a clash without unbalancing core 0, and --probe-kernel passes again.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
(cherry picked from commit a3dbd63878d7d6de096ceb4c25988192b4bce738)
2026-11-09 20:48:10 -05:00
FemOrWhat
844940c6fb Add DualShock 3 support on Windows, read straight from the controller when no driver presents it, off by default.
runtime/input_ds3_win.c finds a DualShock 3 through hid.dll and setupapi (loaded at run time), sends it the command
that makes it report, sets its first player light and decodes its reports (runtime/input_ds3.c, tested by
tests/ds3_check) into the pad state, with rumble through its light motor. input.c follows whichever pad was pressed
last when an SDL pad is connected too. ds3 = 1 in nx2navi.ini turns it on: it starts a scan for the controller every
three seconds and opens it read-write, so it stays off unless a player asks for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 08:21:07 -05:00
chrg6
ccbb02a04b Empty SDL's event queue after each gamepad update on Windows.
Nothing reads SDL's events there (the window is our own), yet every axis and button change of a gamepad was queued
until SDL's cap of about 65,000 events, several MB, after which each further event failed and set SDL's error text,
which later log lines report. The gamepad state the input code reads does not come from the queue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 15:51:48 -05:00
chrg6
1b39aabd37 Describe what the code does in comments and docs.
Comments say what the code does and why, docs/maxwell.md keeps the Maxwell facts, and fetch_refs.sh clones only the checkout the build reads. Code is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 05:04:40 -05:00
chrg6
29cab01da7 Guard float colour outputs against NaN, start mesh counts at zero, and read XInput pads SDL misses.
Patch 0027: a float colour output turns NaN into 0 and clamps the rest to +-1e30 before it reaches its target, so one
bad texel cannot spread through bloom, blur and downsample passes as black or white blocks; integer targets keep their
bits. Task and mesh shaders start their mesh and primitive counts at 0 behind a barrier, and a task emits at most one
mesh group per thread, so a task that culls everything no longer launches what the shared memory last held. Without a
gamepad SDL can open, the first connected XInput controller (slots 0-3, empty slots asked once a second) is read
directly. Kokiri Forest: 60 fps, GPU 5.5 ms a frame, unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 05:04:39 -05:00
chrg6
4943b50ff2 Unlock the frame rate, shorten cold starts, and describe the Ocarina build in the docs.
Ocarina steps its game by the clock (the same frame-numbered walk covers half the distance at 120 fps), so in a build
without the fps60 and realtime patch sets the pacer holds frames to the frame limit alone: any rate from 30 to 1000,
the monitor's rate, or unlimited (164 fps average in Kokiri Forest). The menu always shows the row, with 165, 240 and
Unlimited added. NX2NAVI_PACE_HZ sets the pacer's rate for tests.

A compute pipeline is compiled outside the pipeline lock, and one compile serves the replay and the boot prebuild
alike; the prebuild uses half the CPUs. The guest waits at most NX2NAVI_PREWARM_WAIT_MS (3000) for the boot prewarm,
which then goes on in the background (a cold start waited 9.8 s). NX2NAVI_TEST_PAD_AT attaches the test gamepad late
and presses it, to test a controller switched on in play.

README, docs/architecture.md, docs/controls.md and AGENTS.md describe the Ocarina build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 05:04:39 -05:00
chrg6
f5e3322bf5 Follow the gamepad the player presses and show the project's name.
Every connected gamepad is kept open and rescanned each second, and the game reads the one pressed last: taking only
the first one SDL listed left the game deaf when that was a virtual or idle device. SDL may deliver gamepad presses in
the background, since it cannot see the Win32 window's focus; the keyboard stays gated on it. The log notes each
gamepad's first press. The version line, the window title and the Discord status say the project's name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 05:04:39 -05:00
Valiantsin Dzerakh
0478c13e45 Import the base tree (MIT, see LICENSE) 2026-11-07 05:04:38 -05:00