Skip to content

Motion controls: gyro aiming from SDL pads, with sensitivity and inverts - #1114

Merged
hckrman101 merged 7 commits into
NotRexed:mainfrom
wyatt-poole:hid/motion-controls
Nov 9, 2026
Merged

hckrman101 merged 7 commits into
NotRexed:mainfrom
wyatt-poole:hid/motion-controls

Conversation

@wyatt-poole

@wyatt-poole wyatt-poole commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

The game starts the six-axis sensor and reads GetSixAxisSensorStates, but nn_hid.c always answered with a pad lying still, so gyro aiming did nothing. This reads the controller's gyroscope and accelerometer through SDL and gives the game the state it actually aims with.

The game-specific part is the approach spluton found in #112 (tested on a DualSense) and Link-22 corrected (the yaw rate's unit; tested on a GameSir T3 Pro in Switch Pro mode, same feel as Eden). Thanks to both. The first version of this PR sent a plain sensor state, which OoT does not read that way (see the review comment below).

What changed

  • host/hos_motion.h (new, header-only, no SDL or Windows):
    • the horizontal aim is the yaw rate, in the angular velocity's y slot, in turns per second;
    • the vertical aim is the direction matrix as a pure tilt, Rx(pitch), integrated from the pitch rate on its own;
    • the angles, the pitch rate and the acceleration go out neutral (zero, zero, (0, 0, -1)), because the game folds them into an aim recenter;
    • the rates are measured about the world's axes from an up vector (complementary filter: the gyro's prediction pulled toward gravity while the accelerometer reads 0.65 to 1.35 G), so the pad works held flat or upright;
    • the gyroscope's resting bias is learned while the pad is still.
  • host/nn_hid.c: GetSixAxisSensorStates fills its states from that; GetSixAxisSensorHandles returns 3 (the game runs its motion path only with a third source).
  • host/hos_sdl.c, host/pad_sdl.c: SDL3/SDL2 HasSensor, SetSensorEnabled, GetSensorData, looked up optionally (an SDL without them still plays, without gyro). The first pad with both sensors is read on the pad thread every poll; sensors are on only while the option is on.
  • Insert menu, Controls tab:
    • Motion controls (On by default), showing whether the connected pad has a gyroscope: On (gyro found), On (no gyro on this pad), On (no controller);
    • Motion sensitivity X and Motion sensitivity Y, each 10% to 300% (100% is the console's), so horizontal and vertical aim can be tuned apart;
    • Invert motion X, Invert motion Y.
      These are controls, so changing them does not reset the quality preset. MkwVkHost gets gyro_status (API version 8); rx_option 18 to 21 are the rows.
  • HOS_MOTION_DEBUG=1 logs what the game gets twice a second.
  • README: the rows in the menu table and a line under it.

On by default because the game expects motion on Switch; happy to flip the default if you prefer it off.

Testing

  • tests/test_motion.py (game-free, added to tests/run_all.py): bias learning, a left turn at 1 turn/s in the yaw slot, a front-up tilt as -0.5 rad in the matrix, the same turn with the pad held upright, each sensitivity scaling only its own axis, the inverts, the up vector settling on gravity, the stall clamp, and the neutral fields throughout. Passes with gcc and clang.
  • Full game-free suite on Linux: 120 passed, 0 failed, 15 skipped (tools not present here).
  • The changed host files compile with mingw-w64 against the Windows headers.
  • SDL 3.4.20 (the bundled SDL3.dll) reads an 8BitDo Ultimate 2 in Switch mode as "Nintendo Switch Pro Controller" with both sensors.
  • In game: being tested now on rc3 (RTX 3060 Laptop, Windows 11, 8BitDo Ultimate 2 in Switch mode). Draft until that's done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BsKC8mjbyEUFAv6uy3SQiW

…sensor

The game starts the six-axis sensor and reads GetSixAxisSensorStates, but nn_hid.c always answered with a pad lying
still, so gyro aiming did nothing. With the new Controls row Motion controls on (the default), the first SDL pad with
a gyroscope and an accelerometer (Switch Pro and pads in Switch mode, DualSense, DualShock 4) has them read on the pad
thread every poll and turned into the Switch's state:

- host/hos_motion.h (header-only, no SDL or Windows): SDL's gamepad axes to the Switch's (the change of axes the yuzu
  family makes), m/s^2 to G and rad/s to revolutions a second, the rotation summed, and the orientation as a
  quaternion with a gentle pull toward gravity (Mahony, proportional term). The gyroscope's resting bias is learned
  while the pad lies still and taken off, so aiming does not creep. HOS_GYRO_FLIP=<axes> reverses an axis for a pad
  whose driver reports one the other way.
- host/hos_sdl.c: SDL3/SDL2 HasSensor, SetSensorEnabled and GetSensorData, looked up optionally (an SDL without them
  still plays, with no gyro).
- host/pad_sdl.c: sensors on while the row is on and off otherwise; hos_sdl_motion for nn_hid.c and
  hos_sdl_gyro_status for the menu.
- host/nn_hid.c: GetSixAxisSensorStates fills the states from it; the sampling number grows only with new readings.
- Insert menu: the row shows whether the connected pad has a gyroscope ("On (gyro found)", "no gyro on this pad", "no
  controller"); either choice can be made whatever is connected. It is a control, so it does not reset the quality
  preset to Custom. MkwVkHost gets gyro_status (API version 8); rx_option 18 is the row.

Tested: tests/test_motion.py (new, game-free: axes and units, a quarter turn at 1 rev/s, bias learning, the tilt
filter settling on the accelerometer's gravity, HOS_GYRO_FLIP, the stall clamp) with gcc and clang; the game-free
suite passes (120 passed, 15 skipped for tools). The four host files compile with mingw-w64 against the Windows
headers. SDL 3.4.20 (the bundled SDL3.dll) reads an 8BitDo Ultimate 2 in Switch mode as a Nintendo Switch Pro
Controller with both sensors: face up 10.1 m/s^2 on +Y, as the axis change assumes, and a resting gyro bias of about
0.8 deg/s. Not yet tried in game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsKC8mjbyEUFAv6uy3SQiW
@Link-22

Link-22 commented Nov 9, 2026

Copy link
Copy Markdown

Tested this PR (d43ea5c) on Linux (CachyOS, RTX 4070 Super) with a GameSir T3 Pro in Switch Pro Controller mode, over SDL. The PR's tree was built on top of the v2.0.7 AppImage.

Result: the gyro is detected and the aim moves, but left/right is inverted and much too fast. This is the same behaviour I saw with my first port of spluton's patch. HOS_GYRO_FLIP=z did not fix it. My patch from #112 aims correctly with the same controller, so it isn't a controller or driver issue.

From what I found while fixing my port, OoT doesn't seem to read the six-axis state like a plain sensor:

  • Horizontal aim comes from the yaw rate. In my patch the gravity-corrected yaw is sent in the angular velocity's y slot, in turns per second. Sending the raw Switch-frame gyro made it inverted and too fast.
  • Vertical aim comes from the direction matrix, as a pure tilt (pitch) rotation integrated on its own. The game folds the angles and the pitch rate into an aim recenter, which cancels the matrix's tilt. So in my patch those go out as zero, and the acceleration stays at rest (0, 0, -1).
  • GetSixAxisSensorHandles returns 3 handles in my patch. The game gates part of its motion path on that count.

The relevant code is in GetSixAxisSensorStates in host/nn_hid.c of my v2.0.7 patch, attached in #112. I'm happy to test any change to this PR with this controller.

@wyatt-poole

Copy link
Copy Markdown
Contributor Author

Thanks a lot for testing, and for the detailed write-up. You're right: this PR delivered the six-axis state as a plain sensor, which isn't how OoT reads it. That also explains what I saw on my 8BitDo Ultimate 2 (Switch mode): roll moved the aim left/right because it landed in the y slot, and up/down came out mirrored.

I'm reworking it to use the approach @spluton found and you corrected in #112, with credit to both of you in the code and the commit:

  • the gravity-corrected yaw rate in the angular velocity's y slot, in turns per second
  • the vertical as a pure Rx(pitch) direction matrix, integrated from the body-frame pitch rate
  • angles and pitch rate zero, acceleration at rest (0, 0, -1)
  • the up-vector complementary filter, so flat and upright holds both work
  • GetSixAxisSensorHandles returning 3

It keeps this PR's SDL2/SDL3 plumbing, the Motion controls row (with its gyro found / no gyro status) and the game-free test, and adds Motion sensitivity (10 to 300%) plus Invert motion X / Invert motion Y toggles in the Controls tab, in place of HOS_GYRO_FLIP.

I'm testing it locally on rc3 with my 8BitDo first, then I'll push it here and ping you. A retest on the T3 Pro would be great.

…dings), sensitivity and inverts

The first version delivered the six-axis state as a plain sensor. OoT does not read it that way: Link-22 tested it on
a GameSir T3 Pro in Switch Pro mode and left/right came out inverted and much too fast, and on an 8BitDo Ultimate 2 in
Switch mode roll moved the aim left/right and up/down was mirrored. What the game aims with was found by spluton in
issue 112 (tested on a DualSense) and corrected by Link-22 (the yaw rate's unit; the same feel as Eden on the T3 Pro):

- the horizontal aim is the yaw rate, in the angular velocity's y slot, in turns a second;
- the vertical aim is the direction matrix as a pure tilt, Rx(pitch), integrated from the pitch rate on its own;
- the angles, the pitch rate and the acceleration go out neutral (zero, zero, at rest), since the game folds them
  into an aim recenter that cancels the tilt otherwise;
- GetSixAxisSensorHandles returns 3: the game runs its motion path only with a third source.

host/hos_motion.h now computes that state: an up vector from a complementary filter (the gyro's prediction pulled
toward gravity while the accelerometer reads 0.65 to 1.35 G), the gyro projected onto right/up/forward for the pitch,
yaw and roll rates about the world's axes (so the pad works held flat or upright), and the resting gyro bias learned
while still. The SDL plumbing, the Motion controls row and its gyro status stay. New Controls rows: Motion sensitivity
(10% to 300%, 100% the console's), Invert motion X and Invert motion Y, in place of HOS_GYRO_FLIP. HOS_MOTION_DEBUG=1
logs what the game gets twice a second.

tests/test_motion.py: the bias, a left turn at one turn a second in the yaw slot, a front-up tilt as -0.5 rad in the
matrix, the same turn held upright, sensitivity and the inverts, the up vector settling on gravity, the stall clamp,
and the neutral fields throughout.

Co-Authored-By: spluton <spluton@mail.ru>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsKC8mjbyEUFAv6uy3SQiW
@wyatt-poole wyatt-poole changed the title Motion controls: the controller's gyroscope as the Switch's six-axis sensor Motion controls: gyro aiming from SDL pads, with sensitivity and inverts Nov 9, 2026
@wyatt-poole

Copy link
Copy Markdown
Contributor Author

@Link-22 pushed (5768280): the rework from my last comment. It's still a draft while I try it in game, but if you get a chance to run it on the T3 Pro, I'd love to know how it compares to your patch.

wyatt-poole and others added 2 commits November 9, 2026 10:13
Motion sensitivity X scales the horizontal aim (yaw) and Motion
sensitivity Y the vertical (pitch), each 10 to 300 percent, 100 by
default, so a controller whose axes feel uneven can be evened out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsKC8mjbyEUFAv6uy3SQiW
- GetSixAxisSensorHandles writes only the handles asked for and still
  reports three, so a two-handle array is never written past; logs the
  count asked for once.
- The filter integrates the real time between readings: a repeat of the
  last reading is no reading (its time goes into the next), and a long
  gap is capped at 100 ms rather than replaced with 4 ms.
- The bias is learned only while the pad is still: the gyro's and the
  accelerometer's spread over a short window both under a sensor's
  noise, about 1 G, for half a second. A slow, steady turn in the hand
  is no longer learned away, and an offset of any size is learned.
- The vertical aim is pulled gently (1 s) toward the tilt gravity
  measures, scaled like the aim, so leftover bias cannot drift it.
- GetSixAxisSensorStates hands out one entry per reading: the sampling
  number counts readings, each entry has its own state and the time
  since the reading before (the last 32 kept).
- Motion follows the pad used last; a pad reconnecting into a slot gets
  a fresh filter; a sensor enable that fails turns both off and is
  retried every 2 s.
- HOS_MOTION_DEBUG is read once; the filter runs outside state_lock,
  which is taken only to publish the reading.
- The motion options are HOS_OPT_GYRO* in vk_api.h on both sides.
- Menu rows have a scale flag (slider, clamped), and the Controls tab
  as a whole stays out of the quality preset.
- tests/motion_test.c simulates the pad (gravity turns with the true
  rotation, sensor noise or a hand's tremor) and covers the above.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsKC8mjbyEUFAv6uy3SQiW
@wyatt-poole

Copy link
Copy Markdown
Contributor Author

@hckrman101 thanks for such a careful review. All ten are addressed in 66828d2:

  1. Handle count: GetSixAxisSensorHandles now writes only the handles the caller asked for and still returns 3, so a two-handle array is never written past. The handles are opaque and GetSixAxisSensorStates doesn't read them, so whatever the game finds in a third slot doesn't matter. It also logs the count the game asked for once, so we can confirm what OoT passes.
  2. dt: the filter now integrates the real time between readings. An exact repeat of the last reading counts as no reading, and its time carries into the next one. A long gap is capped at 100 ms instead of being replaced with 4 ms. The tests cover 1 ms polls, 60 ms polls and a 1 s stall.
  3. Bias gate: stillness now means the gyro's spread over a short window (0.2 s) is below 0.01 rad/s, the accelerometer's spread is below 0.01 G, and the magnitude is about 1 G, held for 0.5 s. The bias then moves toward the window's mean (fast for the first 2 s of stillness, then slowly). A slow, steady turn in the hand is no longer learned away, and an offset of any size is learned while the pad rests. There are tests for a 3°/s turn with hand tremor and a 0.2 rad/s offset.
  4. Pitch drift: pitch is now pulled toward the tilt that up measures (the pad's tilt about its x axis since the filter started, scaled by the same sensitivity and invert), with a 1 s time constant. The pull pauses while the pad is shaken or lying on its side. In the test, a 0.01 rad/s leftover bias over 60 s now stays within 0.03 rad.
  5. Sampling: the sampling number counts readings, moving on past the last number handed out. Each entry is its own reading from a 32-deep history, with the time since the reading before it. One question here: spluton's and Link-22's patches reported a fixed 5 ms, and the feel was tuned with that. If OoT uses delta_time for the yaw, a 1 kHz pad would now report about 1 ms. I'll ask Link-22 to compare on the T3 Pro.
  6. Which pad: motion now follows the pad with the latest button or stick change. Each pad opening has a serial, so a different pad that reconnects into the same slot gets a fresh filter.
  7. Enable failure: if either sensor fails to enable, both are turned off and the pad is retried every 2 s. Toggling Motion controls works again too.
  8. Lock: HOS_MOTION_DEBUG is read once. The filter and the log run outside state_lock, which is taken only to publish the new reading.
  9. Option ids: there's now HOS_OPT_GYRO, _SENS_X, _INV_X, _INV_Y and _SENS_Y in vk_api.h, used by both pad_sdl.c and vk_replay.cpp.
  10. Row lists: Row has a scale flag (drawn as a slider, stops at its ends) that replaces both slider lists, and the preset check is now r.tab != TAB_CONTROLS plus fullscreen, FPS counter and GPU device. One side effect: "Controller in background" no longer turns the preset to Custom, which I think was a bug anyway.

tests/motion_test.c now simulates the pad: gravity turns with the true rotation, and the gyro adds a bias plus either sensor noise or hand tremor. It passes with gcc and clang, and the full run_all.py passes. I'm testing in game on rc3 next.

@Link-22

Link-22 commented Nov 9, 2026

Copy link
Copy Markdown

Retested 66828d2 on Linux (CachyOS, RTX 4070 Super) with the GameSir T3 Pro in Switch Pro Controller mode, over SDL. The PR's tree was built on top of the v2.0.7 AppImage. Menu defaults: Motion controls on ("On (gyro found)"), both sensitivities at 100%, both inverts off.

Everything works:

  • Direction: left/right and up/down both aim the right way, with no invert needed.
  • Speed: feels right at 100%, on par with my patch from Support for gyro aiming? #112.
  • Drift: holding the pad still on a target, the aim stays put.
  • Rolled pad: rolled about 30°, aimed up and held, the aim doesn't creep (the case from @hckrman101's leftover note).
  • Slow, precise turns: the aim follows them and doesn't drift back when I stop.

Great work @wyatt-poole, thanks for picking this up. From my side it's ready to merge.

- The vertical aim's pull toward the measured tilt runs only while the
  pad's x is about level (|up[0]| < 0.25, about 15 degrees of roll).
  Rolled, the tilt about the pad's own x moves faster than the pitch
  about the levelled x, and the pull dragged each vertical move past
  where the pad went (hckrman101). New test: rolled 30 degrees, a tilt
  up holds.
- A pad keeps its gyroscope's learned bias while another pad has motion
  (or Motion controls is off), and gets it back with the filter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsKC8mjbyEUFAv6uy3SQiW
@wyatt-poole

Copy link
Copy Markdown
Contributor Author

@hckrman101 thanks for the approval and the follow-ups. Two of them are in 8a5c178:

  • Tilt pull while rolled: the pull now runs only while fabsf(up[0]) < 0.25f (about 15° of roll), as you suggested. Anything that builds up while rolled is corrected once the pad is level again. I added the test case: rolled 30°, nose tilted up 0.5 rad, held for 3 s, and pitch stays at -0.5. With the old 0.5 gate it creeps to -0.56.
  • Switching pads: each pad now keeps the bias it learned while another pad has motion, or while Motion controls is off, and gets it back when it has motion again. A pad that reconnects (new serial) starts fresh.
  • Noisier pads: I'll check with HOS_MOTION_DEBUG that the bias gets learned on my 8BitDo Ultimate 2 resting on a table, and loosen the thresholds if it doesn't.

@Link-22 when you have a moment, a retest on the T3 Pro would be the last thing before this leaves draft. In particular, does the yaw speed match your patch, and does it stay the same between sensors-only and mouse-wiggling polls?

@Link-22

Link-22 commented Nov 9, 2026

Copy link
Copy Markdown

@wyatt-poole quick answer on 8a5c178, same setup as before (T3 Pro, Linux, defaults):

  • Yaw speed matches my patch.
  • Sensors only vs mouse wiggling over the window: I see no difference in yaw speed.

One thing I hit in game, which may be the late-anchor case @hckrman101 described. If I turn Motion controls on while the pad isn't held level, the gyro aim comes out wrong afterwards: it feels inverted. Turning it off and on again with the pad lying flat on the desk fixes it. So it seems to depend on how the pad is held at the moment the filter starts.

I'll do a full retest on the T3 Pro once the anchor fix and the merge with main are in, and check this case again.

wyatt-poole and others added 2 commits November 9, 2026 13:09
Conflicts with NotRexed#937 (microphone) and the Rumble strength row:
- vk_api.h: MKWVK_API_VERSION 9 (main and this branch each took 8 with
  different layouts); gyro_status after the mic_* pointers, and
  rx_backend.c's host table in the same order.
- vk_menu.cpp: main's preset_exempt and stops_at_ends instead of this
  branch's scale flag; the motion rows are preset-exempt, the two
  sensitivities sliders that stop at their ends.
- vk_replay.cpp: option 9 (rumble) and 18 to 22 (motion) side by side.
- pad_sdl.c: rumble_strength and motion_poll both kept.
- tests/test_rumble_menu.py reads every HOS_OPT_* enum for the option
  ids, not only the cheats'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsKC8mjbyEUFAv6uy3SQiW
The pull's level (tilt0) was taken as pitch 0 on the first reading that
passed the roll gate. With the gate at 15 degrees, a filter that starts
rolled (at start, when Motion controls is turned on, on a pad switch or
reconnect) integrates pitch before the pad first comes level, and the
pull then dragged the aim back by that much and kept the level off
(hckrman101; Link-22 saw it as a wrong aim after turning Motion controls
on with the pad not level). tilt0 is now anchored so the target equals
the current pitch at that moment.

Test: started rolled 30 degrees, aimed up, rolled back to level; the aim
holds (without the fix it slid from -0.30 to 0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsKC8mjbyEUFAv6uy3SQiW
@wyatt-poole

Copy link
Copy Markdown
Contributor Author

@hckrman101 @Link-22 both points are in, and the branch is merged with main:

  • Late anchor (76fc99b): tilt0 is now anchored to the aim as it is (tilt - pitch / sv), as you suggested, so the pull's target equals the current pitch when the pad first comes level. The rolled test now finishes the job: it starts the filter rolled 30°, aims up, rolls back to level (about the pad's z, which keeps the nose's height), and checks that the aim holds for 3 s. Without the fix it slides from -0.30 to 0. This should also be Link-22's "wrong after turning Motion controls on while not level" case.
  • Merge with main (33d39bc):
    • MKWVK_API_VERSION is 9, with gyro_status after the mic_* pointers in both vk_api.h and rx_backend.c's host table.
    • In the menu, I dropped this branch's scale field in favour of main's preset_exempt and stops_at_ends. The motion rows are preset-exempt, and both sensitivities are sliders that stop at their ends.
    • Option 9 (rumble) and 18 to 22 (motion) sit side by side in the option table.
    • pad_sdl.c keeps both rumble_strength and motion_poll.
    • tests/test_rumble_menu.py now reads every HOS_OPT_* enum when it checks the option ids for collisions (before, it read only the cheats').

run_all.py passes on Linux (140 passed, 0 failed), and test_motion.py passes with both gcc and clang. Taking it out of draft.

@wyatt-poole
wyatt-poole marked this pull request as ready for review November 9, 2026 18:11
@hckrman101
hckrman101 merged commit a2949e1 into NotRexed:main Nov 9, 2026
@Link-22

Link-22 commented Nov 9, 2026

Copy link
Copy Markdown

@wyatt-poole @hckrman101 retested 76fc99b (merged with main, built on the v2.0.10 AppImage), same setup: T3 Pro in Switch Pro mode, Linux, menu defaults.

Working well:

  • Direction and calibration: aiming at a fixed point, turning away and turning back brings the aim back to exactly the same point.
  • Holding still: the aim feels steadier than before.
  • Sensors only vs mouse wiggling: no change in yaw speed.
  • Rumble still works after the merge with main.

Still there: the vertical aim depends on how the pad is held when the filter starts. It happens at game start, and when I turn Motion controls off and on while holding the pad tilted. The aim is then off and feels almost inverted. Toggling it off and on with the pad flat on the desk fixes it every time.

I ran it with HOS_MOTION_DEBUG=1 and attached the hid: lines. There are four filter starts, one per motion controls from line:

Start Pad at start (up) Later, same hold: up aim tilt Aim
1 flat (0, 1, 0), game start — — —
2 rolled, little pitch (0.55, 0.78, -0.30) (0.02, 0.76, -0.65) -0.78 seemed fine
3 rolled and pitched (0.75, 0.36, -0.55) (0.01, 0.67, -0.74) +0.09 wrong
4 flat on the desk (0.05, 1.00, 0.06) (-0.03, 0.70, -0.72) -0.91 fine

So with the same hold, start 3 gives a vertical aim about 0.9 rad away from starts 2 and 4. That's roughly the pitch the pad had when the filter started. It looks like pitch starts at 0 in whatever pose the pad is in. While the pad is rolled the tilt pull is off, so nothing brings it back to the gravity-level reference. When it comes level, the anchor (tilt - pitch / sv) then keeps that offset for good.

A guess at a fix: start pitch from the measured tilt when up is first set, e.g. sv * asin(-up[2]), the nose's angle above level, which also holds when rolled. Then anchor the pull to the absolute tilt (tilt0 = 0). The vertical would always be relative to level, whatever the pose at start. I'm happy to retest.
hookshot-motion-debug-76fc99b.txt

SadTransGirl pushed a commit to SadTransGirl/Hookshot-Gummi that referenced this pull request Nov 9, 2026
…ampling)

- The tilt pull follows the tilt unwrapped since its anchor and scales
  that, so at a sensitivity that is not a whole number the target no
  longer jumps by about pi once the pad turns past half a turn.
- Changing Motion sensitivity Y or Invert motion Y re-anchors the pull:
  the aim stays where it is rather than sliding to the new scale with
  the pad held still.
- Stillness reads the accelerometer's direction as well as its size: a
  pad turning at a steady rate is no longer still, so the turn is not
  learned as bias. The gyro threshold is 0.015 rad/s over the three
  axes (about 0.009 on each), room for noisier third-party sensors, and
  a pad that has not read still for 30 s says so in the log once.
- The delta time handed to the game is capped at 100 ms, as the filter
  caps its own: a stalled pad's next reading is no jump.
- Sampling numbers move a whole history on at each switch between live
  and at rest, so no two entries ever share one.
- Tests: a sensitivity and an invert change while held tilted, a
  half-turn and more at 50%, a 0.0067 rad/s noisy sensor learning its
  bias on a table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsKC8mjbyEUFAv6uy3SQiW
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants