Support for gyro aiming? #112

Closed
opened 2026-11-07 11:33:18 +00:00 by vladone97 · 11 comments
vladone97 commented 2026-11-07 11:33:18 +00:00 (Migrated from github.com)

Would it be possible to add support for gyro aiming controls from controllers that have motion features, like the DualSense? Currently, I can use the DualSense gyro controls on emulators just fine, but not on Hookshot

Would it be possible to add support for gyro aiming controls from controllers that have motion features, like the DualSense? Currently, I can use the DualSense gyro controls on emulators just fine, but not on Hookshot
CarlosKnoll commented 2026-11-07 15:46:26 +00:00 (Migrated from github.com)

My 8BitDo Ultimate 2C also has gyro that can be used in emulators and does not work with the gyro aiming in Hookshot.

My 8BitDo Ultimate 2C also has gyro that can be used in emulators and does not work with the gyro aiming in Hookshot.
RegalPixelKing commented 2026-11-07 19:57:23 +00:00 (Migrated from github.com)

Even after the update that just came out I can confirm it still does not function.

Even after the update that just came out I can confirm it still does not function.
spluton commented 2026-11-08 03:41:58 +00:00 (Migrated from github.com)

Got this working (DualSense over USB, Ocarina of Time: Switch 2 Edition). Three things were needed:

1. SDL sensors. Enable them when a pad opens (SDL_GameControllerSetSensorEnabled for accel and gyro) and feed the states into the six-axis HLE: acceleration in m/s², gyro in rad/s. The SDL z axis points toward the player, so the game's gravity reading flips sign.

2. The game folds the euler angles, the pitch rate and the acceleration into its aim's recenter — anything not body-relative and neutral gets cancelled, which is why naive six-axis delivery looks dead. The working state:

  • the euler angles zero, the pitch rate zero, the acceleration pinned to at-rest (0, 0, −1)
  • the vertical aim carried by a pure tilt-swing direction matrix (Rx(pitch)), integrated from the body-frame pitch rate
  • the horizontal aim as the yaw rate (into angvel slots 1 and 2)

The body-frame rates come from an up-vector complementary filter: up predicted from the gyro (up + (ω×up)·dt, normalized), pulled toward the measured gravity whenever |a| is 0.65..1.35 g; the rates projected onto right/up/forward. That makes the hold orientation irrelevant — works with the pad held flat or vertical (portrait, face to the player; verified on both with a DualSense).

3. The motion pipeline is gated by the handle count. The game asks GetSixAxisSensorHandles for 2 handles, but its motion branch (JCMS) only runs when the returned count is ≥ 3 — returning 3 turns the whole path on.

Patch against 1720815 is ready — will clean it into a PR.

Patch (against 1720815): https://gist.github.com/spluton/d4b175de0013e1cd0b5754efd18eeb87

Got this working (DualSense over USB, Ocarina of Time: Switch 2 Edition). Three things were needed: **1. SDL sensors.** Enable them when a pad opens (`SDL_GameControllerSetSensorEnabled` for accel and gyro) and feed the states into the six-axis HLE: acceleration in m/s², gyro in rad/s. The SDL z axis points toward the player, so the game's gravity reading flips sign. **2. The game folds the euler angles, the pitch rate and the acceleration into its aim's recenter** — anything not body-relative and neutral gets cancelled, which is why naive six-axis delivery looks dead. The working state: - the euler angles zero, the pitch rate zero, the acceleration pinned to at-rest (0, 0, −1) - the vertical aim carried by a pure tilt-swing direction matrix (`Rx(pitch)`), integrated from the body-frame pitch rate - the horizontal aim as the yaw rate (into angvel slots 1 and 2) The body-frame rates come from an up-vector complementary filter: `up` predicted from the gyro (`up + (ω×up)·dt`, normalized), pulled toward the measured gravity whenever |a| is 0.65..1.35 g; the rates projected onto right/up/forward. That makes the hold orientation irrelevant — works with the pad held flat or vertical (portrait, face to the player; verified on both with a DualSense). **3. The motion pipeline is gated by the handle count.** The game asks `GetSixAxisSensorHandles` for 2 handles, but its motion branch (JCMS) only runs when the returned count is ≥ 3 — returning 3 turns the whole path on. Patch against 1720815 is ready — will clean it into a PR. **Patch (against 1720815):** https://gist.github.com/spluton/d4b175de0013e1cd0b5754efd18eeb87
Link-22 commented 2026-11-08 16:55:32 +00:00 (Migrated from github.com)

I ported @spluton's gyro patch by hand to the current source (VERSION 3601726, it no longer applies cleanly) and tested it on Linux. The controller is a GameSir T3 Pro in Switch Pro Controller mode. Gyro aiming works, with two fixes:

1. Horizontal aim was ~6x too fast: wrong unit for the yaw rate.
The Switch reports angular velocity in turns per second. Yuzu-based emulators convert this way too: SDL's rad/s divided by 2π. The patch passes the yaw rate (v[4]) straight from SDL, in rad/s, so horizontal aiming came out 2π times too fast.

The vertical axis was fine, because it goes through the integrated pitch matrix, which is an angle in radians. So the fix only scales the horizontal rate:

    const float gx = vsum;
    /* the game reads the angular velocity in turns per second, as a real Switch (and the emulators) give it: SDL's
       rad/s over 2 pi. The vertical runs through the tilt matrix, an angle in radians, so only the yaw rate scales */
    hsum *= 0.15915494f;

With this, aiming feels the same as in Eden on the same controller. Eden applies no extra gyro scaling.

2. A sensitivity option.
This adds motion_sens=<x> (0.05–5, default 1) to hookshot_options.ini. It is parsed in motion_bind() next to motion_axis_x/y/z, and it multiplies the angular velocity:

static float motion_sens = 1.0f;
...
                } else if (!strncmp(line, "motion_sens=", 12)) {
                    const float x = (float)atof(line + 12);
                    if (x >= 0.05f && x <= 5.0f) motion_sens = x;
                }
...
    const float w = (deg ? 57.29578f : 1.0f) * motion_sens;

Note: motion_bind() re-reads the file every 0.5 s. In my test, though, changes only took effect after restarting the game.

Default axis bindings (2, 3, 0) are correct for this controller. Thanks to @spluton for the patch!

I ported @spluton's gyro patch by hand to the current source (VERSION 3601726, it no longer applies cleanly) and tested it on Linux. The controller is a GameSir T3 Pro in Switch Pro Controller mode. Gyro aiming works, with two fixes: **1. Horizontal aim was ~6x too fast: wrong unit for the yaw rate.** The Switch reports angular velocity in **turns per second**. Yuzu-based emulators convert this way too: SDL's rad/s divided by 2π. The patch passes the yaw rate (`v[4]`) straight from SDL, in rad/s, so horizontal aiming came out 2π times too fast. The vertical axis was fine, because it goes through the integrated pitch matrix, which is an angle in radians. So the fix only scales the horizontal rate: ```c const float gx = vsum; /* the game reads the angular velocity in turns per second, as a real Switch (and the emulators) give it: SDL's rad/s over 2 pi. The vertical runs through the tilt matrix, an angle in radians, so only the yaw rate scales */ hsum *= 0.15915494f; ``` With this, aiming feels the same as in Eden on the same controller. Eden applies no extra gyro scaling. **2. A sensitivity option.** This adds `motion_sens=<x>` (0.05–5, default 1) to `hookshot_options.ini`. It is parsed in `motion_bind()` next to `motion_axis_x/y/z`, and it multiplies the angular velocity: ```c static float motion_sens = 1.0f; ... } else if (!strncmp(line, "motion_sens=", 12)) { const float x = (float)atof(line + 12); if (x >= 0.05f && x <= 5.0f) motion_sens = x; } ... const float w = (deg ? 57.29578f : 1.0f) * motion_sens; ``` Note: `motion_bind()` re-reads the file every 0.5 s. In my test, though, changes only took effect after restarting the game. Default axis bindings (2, 3, 0) are correct for this controller. Thanks to @spluton for the patch!
tstambaugh92 commented 2026-11-09 01:46:34 +00:00 (Migrated from github.com)

Be super cool if one of their PRs made it. Aiming without gyro on a controller can feel painful, especially when I'm used to having it from the Ship port lol

Be super cool if one of their PRs made it. Aiming without gyro on a controller can feel painful, especially when I'm used to having it from the Ship port lol
NotRexed commented 2026-11-09 05:44:14 +00:00 (Migrated from github.com)

Thanks for the idea. Feature requests are now collected in #1007, which lists this one with a link back here, so closing it in favour of that list (this is not a decision against the idea). The gyro patches posted here are linked from the list.

Thanks for the idea. Feature requests are now collected in #1007, which lists this one with a link back here, so closing it in favour of that list (this is not a decision against the idea). The gyro patches posted here are linked from the list.
Link-22 commented 2026-11-09 13:03:57 +00:00 (Migrated from github.com)

Update: the gyro patch, ported to v2.0.7 (source 9b541c9c). It's attached as one unified diff against the v2.0.7 AppImage's source: hookshot-v2.0.7-gyro.patch.txt. It applies with patch -p1 from usr/share/hookshot.

I built it with the v2.0.7 AppImage on Linux (CachyOS, RTX 4070 Super) and tested it with a GameSir T3 Pro in Switch Pro Controller mode. Gyro aiming works with the same feel as Eden/Lichii.

What changed since my port in this thread:

  • SDL3 and SDL2. Controllers now go through hos_sdl.c, so the sensor calls live there:

    • hos_sdl_pad_sensors_on() uses SDL_SetGamepadSensorEnabled on SDL3 and SDL_GameControllerSetSensorEnabled on SDL2.
    • hos_sdl_pad_sensor() uses SDL_GetGamepadSensorData on SDL3 and SDL_GameControllerGetSensorData on SDL2. It handles each version's return convention (bool vs 0 for success).
    • Both functions are looked up as optional: if the SDL in use lacks them, the game runs as before, without motion.
  • pad_sdl.c enables the sensors when a pad opens. On each poll it reads the accelerometer and gyro from the first pad that has them, and hos_sdl_sensors() hands that data to nn_hid.c.

  • nn_hid.c is unchanged from my earlier port:

    • spluton's six-axis/sensor-fusion code;
    • the yaw rate in turns per second (hsum *= 1/(2π));
    • motion_sens= in hookshot_options.ini.

    In v2.0.7 that file is read from beside bin/hookshot.

Default axis bindings (2, 3, 0) are correct on this controller. Thanks again to @spluton for the original patch. It would be great to see this in an official build!

Update: the gyro patch, ported to **v2.0.7** (source 9b541c9c). It's attached as one unified diff against the v2.0.7 AppImage's source: `hookshot-v2.0.7-gyro.patch.txt`. It applies with `patch -p1` from `usr/share/hookshot`. I built it with the v2.0.7 AppImage on Linux (CachyOS, RTX 4070 Super) and tested it with a GameSir T3 Pro in Switch Pro Controller mode. Gyro aiming works with the same feel as Eden/Lichii. What changed since my port in this thread: - **SDL3 and SDL2.** Controllers now go through `hos_sdl.c`, so the sensor calls live there: - `hos_sdl_pad_sensors_on()` uses `SDL_SetGamepadSensorEnabled` on SDL3 and `SDL_GameControllerSetSensorEnabled` on SDL2. - `hos_sdl_pad_sensor()` uses `SDL_GetGamepadSensorData` on SDL3 and `SDL_GameControllerGetSensorData` on SDL2. It handles each version's return convention (bool vs 0 for success). - Both functions are looked up as optional: if the SDL in use lacks them, the game runs as before, without motion. - **`pad_sdl.c`** enables the sensors when a pad opens. On each poll it reads the accelerometer and gyro from the first pad that has them, and `hos_sdl_sensors()` hands that data to `nn_hid.c`. - **`nn_hid.c`** is unchanged from my earlier port: - spluton's six-axis/sensor-fusion code; - the yaw rate in turns per second (`hsum *= 1/(2π)`); - `motion_sens=` in `hookshot_options.ini`. In v2.0.7 that file is read from beside `bin/hookshot`. Default axis bindings (2, 3, 0) are correct on this controller. Thanks again to @spluton for the original patch. It would be great to see this in an official build!
wartechnology commented 2026-11-09 22:45:04 +00:00 (Migrated from github.com)

Port of the gyro patch to v2.0.11, gyro only, as a diff against the stock v2.0.11 source. (The v2.0.7 file mentioned above never got attached, so here it is inline.)

Based on @spluton's gist and the v2.0.7 port notes above, including the horizontal speed fix (the yaw rate in turns per second: SDL's rad/s over 2π). Changes from the gist:

  • the sensors go through hos_sdl.c, SDL3 and SDL2, looked up as optional: without them the game runs as before;
  • no keyboard rebinding (v2.0.11 has its Controls page);
  • the "Motion axis x/y/z" rows replaced by Controls rows: Gyro aiming (on by default), Gyro sensitivity (25–300 %), Gyro invert vertical / horizontal (options 26–29 through rx_option); the default axes are the gist's (2, 3, 0);
  • GetSensorFusionHandles / FinalizeSensorFusion are not redefined: v2.0.11 already has them.

It applies with patch -p1 from the tools folder and builds with 0 failures. Tested on Windows 11 (RTX 5080): aiming works with the slingshot, bow, hookshot and first-person view. HOS_MOTION_DEBUG=1 logs what goes to the game twice a second.

hookshot-v2.0.11-gyro.patch
--- a/host/hos_sdl.c
+++ b/host/hos_sdl.c
@@ -20,6 +20,7 @@
     void *OpenAudioDevice, *PauseAudioDevice, *OpenAudioDeviceStream, *ResumeAudioStreamDevice, *PutAudioStreamData;
     /* controllers */
     void *PadOpen, *PadClose, *PadConnected, *PadsUpdate, *PadButton, *PadAxis, *PadName, *PadRumble;
+    void *PadSensorOn, *PadSensorData;   /* (optional: NULL when this SDL lacks them) */
     void *GetGamepads, *GetGamepadID, *Free;   /* (SDL3) */
     void *NumJoysticks, *IsGameController, *DeviceInstanceID, *GetJoystick, *JoystickInstanceID;   /* (SDL2) */
 } f;
@@ -58,6 +59,9 @@
     GET2(PadAxis, "SDL_GetGamepadAxis", "SDL_GameControllerGetAxis");
     GET2(PadName, "SDL_GetGamepadName", "SDL_GameControllerName");
     GET2(PadRumble, "SDL_RumbleGamepad", "SDL_GameControllerRumble");
+    /* the pads' accelerometer and gyro (gyro aiming, nn_hid.c), optional: without them the game runs as before */
+    *(FARPROC *)&f.PadSensorOn = GetProcAddress(m, three ? "SDL_SetGamepadSensorEnabled" : "SDL_GameControllerSetSensorEnabled");
+    *(FARPROC *)&f.PadSensorData = GetProcAddress(m, three ? "SDL_GetGamepadSensorData" : "SDL_GameControllerGetSensorData");
     if (three) {
         GET(SetWindowPosition, "SDL_SetWindowPosition");
         GET(GetPrimaryDisplay, "SDL_GetPrimaryDisplay");
@@ -532,4 +536,17 @@
     if (version == 3) CALL(_Bool (*)(void *, uint16_t, uint16_t, uint32_t), PadRumble)(pad, low, high, ms);
     else CALL(int (*)(void *, uint16_t, uint16_t, uint32_t), PadRumble)(pad, low, high, ms);
 }
+/* the accelerometer (1) and gyro (2), SDL_SensorType in both SDLs, turned on where the pad has them */
+void hos_sdl_pad_sensors_on(void *pad) {
+    if (!f.PadSensorOn) return;
+    for (int t = 1; t <= 2; t++) {
+        if (version == 3) CALL(_Bool (*)(void *, int, _Bool), PadSensorOn)(pad, t, 1);
+        else CALL(int (*)(void *, int, int), PadSensorOn)(pad, t, 1);
+    }
+}
+int hos_sdl_pad_sensor(void *pad, int type, float *data, int n) {   /* 1 when read */
+    if (!f.PadSensorData) return 0;
+    return version == 3 ? CALL(_Bool (*)(void *, int, float *, int), PadSensorData)(pad, type, data, n)
+                        : CALL(int (*)(void *, int, float *, int), PadSensorData)(pad, type, data, n) == 0;
+}
 #undef CALL
--- a/host/hos_sdl.h
+++ b/host/hos_sdl.h
@@ -71,3 +71,5 @@
 const char *hos_sdl_pad_name(void *pad);
 int32_t hos_sdl_pad_id(void *pad);
 void hos_sdl_pad_rumble(void *pad, uint16_t low, uint16_t high, uint32_t ms);
+void hos_sdl_pad_sensors_on(void *pad);   /* the accelerometer and gyro on, where the pad has them */
+int hos_sdl_pad_sensor(void *pad, int type, float *data, int n);   /* type 1 accel (m/s^2), 2 gyro (rad/s): 1 when read */
--- a/host/pad_sdl.c
+++ b/host/pad_sdl.c
@@ -87,6 +87,9 @@
     out[1] = (uint16_t)l;
 }
 
+static float state_accel[3], state_gyro[3];   /* (the first pad's motion, hos_sdl_sensors) */
+static int state_motion;
+
 /* one poll: controllers plugged in or gone (each second), then every open one's buttons, sticks and rumble */
 static void pads_poll(void) {
     static int64_t next_scan = 0;
@@ -120,11 +123,14 @@
                 pads[slot].rumble_lo = pads[slot].rumble_hi = 0;
                 pads[slot].rumble_ns = 0;
                 hos_log("hid: controller \"%s\" connected (SDL)", hos_sdl_pad_name(gc));
+                hos_sdl_pad_sensors_on(gc);
             }
         }
         uint64_t b = 0;
         int32_t s[4] = {0};
         int count = 0;
+        float accel[3] = {0}, gyro[3] = {0};   /* (the first pad with motion sensors) */
+        int motion = 0;
         int rx_option(int id, int otherwise);
         const int strength = rx_option(9, 100);   /* (the menu's Rumble strength) */
         uint16_t motors[2];
@@ -134,6 +140,7 @@
             void *gc = pads[i].gc;
             if (!gc) continue;
             count++;
+            if (!motion && hos_sdl_pad_sensor(gc, 1, accel, 3) && hos_sdl_pad_sensor(gc, 2, gyro, 3)) motion = 1;
             for (int k = BTN_A; k <= BTN_RIGHT; k++)   /* (SDL3's gamepad buttons are in SDL2's order) */
                 if (hos_sdl_pad_button(gc, k)) b |= 1ULL << k;
             if (hos_sdl_pad_axis(gc, AX_LT) > 8000) b |= 1ULL << HOS_PAD_LTRIGGER;
@@ -155,6 +162,9 @@
         state_buttons = b;
         memcpy(state_stick, s, sizeof s);
         state_count = count;
+        memcpy(state_accel, accel, sizeof accel);
+        memcpy(state_gyro, gyro, sizeof gyro);
+        state_motion = motion;
         ReleaseSRWLockExclusive(&state_lock);
 }
 
@@ -209,6 +219,19 @@
     return n;
 }
 
+/* The first pad's motion (nn_hid.c's six-axis), in SDL's units: acceleration m/s^2, angular velocity rad/s. Returns
+   whether a pad with sensors is open. */
+int hos_sdl_sensors(float accel[3], float gyro[3]) {
+    AcquireSRWLockShared(&state_lock);
+    const int motion = state_motion;
+    if (motion) {
+        memcpy(accel, state_accel, 12);
+        memcpy(gyro, state_gyro, 12);
+    }
+    ReleaseSRWLockShared(&state_lock);
+    return motion;
+}
+
 /* Vibration from the game (0..1 amplitudes of the low and high bands), for the pads that rumble. */
 void hos_sdl_rumble(float low, float high) {
     if (low < 0) low = 0;
--- a/host/nn_hid.c
+++ b/host/nn_hid.c
@@ -826,22 +826,127 @@
 
 /* Handles: six-axis ones are opaque u32s; a vibration handle is {u8 style type (3 = full key), u8 npad id, u8 device
    index (0 left, 1 right), u8 reserved}, which the SDK's vibration classes read. Vibration goes nowhere yet. */
+/* (gyro patch, after Hookshot issue 112: spluton's patch, and its port's fix of the horizontal speed) Gyro aiming from the
+   pads SDL reads (pad_sdl.c: DualSense, DualShock 4, Switch Pro and the pads in their modes).
+   The game asks for two handles (a Joy-Con pair) and gates its motion pipeline on the count: at two, its third source
+   (the console's own) stays dead, and that is the branch that flags motion as active for the aim. Three, all filled. */
 HLE(_ZN2nn3hid23GetSixAxisSensorHandlesEPNS0_19SixAxisSensorHandleEiRKjNS_4util10BitFlagSetILi32ENS0_12NpadStyleTagEEE) {
-    for (int i = 0; i < (int)ARG(1); i++) G32(ARG(0) + 4 * (uint64_t)i) = 0x100u + (uint32_t)i;
-    RET((int)ARG(1));
+    const int n = (int)ARG(1) < 3 ? 3 : (int)ARG(1) > 16 ? 16 : (int)ARG(1);
+    for (int i = 0; i < n; i++) G32(ARG(0) + 4 * (uint64_t)i) = 0x100u + (uint32_t)i;
+    RET(n);
 }
+/* The game aims with the controller's own body frame, and folds the euler angles, the pitch rate and the acceleration
+   into a recenter that cancels anything not body-relative and neutral. So the states go out as: the angles zero, the
+   acceleration at rest, the vertical aim as a pure tilt-swing matrix integrated from the pitch rate, the horizontal as
+   the yaw rate in turns per second (as a Switch gives it: SDL's rad/s over 2 pi). The rates are the gyro projected on
+   a gravity-tracked frame (an up-vector complementary filter), so the pad may be held flat or upright.
+   The Insert menu (Controls): Gyro aiming (26, on), Gyro sensitivity (27, percent), Gyro invert vertical / horizontal
+   (28 / 29). HOS_MOTION_DEBUG=1: what goes to the game, twice a second, in the log. */
 HLE(_ZN2nn3hid22GetSixAxisSensorStatesEPNS0_18SixAxisSensorStateEiRKNS0_19SixAxisSensorHandleE) {
     static int64_t six_sampling;
     const uint64_t out = ARG(0);
     const int count = (int)ARG(1) < 1 ? 0 : (int)ARG(1) > 32 ? 32 : (int)ARG(1);
     const int64_t s = ++six_sampling;
     static const float at_rest[18] = {0, 0, -1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1};
+    float accel[3] = {0, 0, -9.80665f}, gyro[3] = {0};
+    int hos_sdl_sensors(float accel[3], float gyro[3]);   /* pad_sdl.c */
+    const int motion = rx_option(26, 1) && hos_sdl_sensors(accel, gyro);
+    static int said;
+    if (motion && !said) {
+        said = 1;
+        hos_log("hid: gyro aiming from the controller's motion sensors");
+    }
+    const float g = 1.0f / 9.80665f;
+    static float bias[3];   /* (the gyro's drift, learnt while the pad is still) */
+    static int bias_n;
+    const float bx = gyro[0] - bias[0], by = gyro[1] - bias[1], bz = gyro[2] - bias[2];
+    if (motion && bx * bx + by * by + bz * bz < 0.0064f) {   /* (still: under 0.08 rad/s) */
+        const float k = bias_n < 256 ? 0.06f : 0.004f;
+        bias[0] += bx * k, bias[1] += by * k, bias[2] += bz * k;
+        bias_n++;
+    }
+    const int64_t now = hos_now_ns();
+    static float up[3] = {0, 1, 0};
+    static int up_ready;
+    static int64_t gravity_ns;
+    float dt = gravity_ns ? (now - gravity_ns) / 1e9f : 0.004f;
+    gravity_ns = now;
+    if (dt < 0.001f || dt > 0.05f) dt = 0.004f;
+    if (!motion) {
+        up_ready = 0;
+        gravity_ns = 0;
+    } else {
+        const float ax = accel[0], ay = accel[1], az = accel[2];
+        const float accel_len = sqrtf(ax * ax + ay * ay + az * az);
+        const int accel_valid = accel_len > 0.65f / g && accel_len < 1.35f / g;
+        const float l = accel_valid ? accel_len : 1.0f;
+        const float observed[3] = {ax / l, ay / l, az / l};
+        if (!up_ready && accel_valid) {
+            memcpy(up, observed, sizeof up);
+            up_ready = 1;
+        } else if (up_ready) {
+            const float gx = gyro[0] - bias[0], gy = gyro[1] - bias[1], gz = gyro[2] - bias[2];
+            float pr[3] = {up[0] + (-gy * up[2] + gz * up[1]) * dt, up[1] + (-gz * up[0] + gx * up[2]) * dt,
+                           up[2] + (-gx * up[1] + gy * up[0]) * dt};
+            const float pl = sqrtf(pr[0] * pr[0] + pr[1] * pr[1] + pr[2] * pr[2]);
+            if (pl > 0.001f)
+                for (int i = 0; i < 3; i++) pr[i] /= pl;
+            if (accel_valid && pl > 0.001f) {
+                const float dir = observed[0] * pr[0] + observed[1] * pr[1] + observed[2] * pr[2];
+                const float corr = dt / (0.5f + dt);
+                for (int i = 0; i < 3; i++) pr[i] += ((dir < 0.0f ? -observed[i] : observed[i]) - pr[i]) * corr;
+                const float cl = sqrtf(pr[0] * pr[0] + pr[1] * pr[1] + pr[2] * pr[2]);
+                if (cl > 0.001f)
+                    for (int i = 0; i < 3; i++) pr[i] /= cl;
+            }
+            memcpy(up, pr, sizeof up);
+        }
+    }
+    /* the pad's right axis on the plane across gravity */
+    float right[3] = {1.0f - up[0] * up[0], -up[0] * up[1], -up[0] * up[2]};
+    float rl = sqrtf(right[0] * right[0] + right[1] * right[1] + right[2] * right[2]);
+    if (rl < 0.1f) {
+        right[0] = -up[0] * up[2], right[1] = -up[1] * up[2], right[2] = 1.0f - up[2] * up[2];
+        rl = sqrtf(right[0] * right[0] + right[1] * right[1] + right[2] * right[2]);
+    }
+    for (int i = 0; i < 3; i++) right[i] /= rl;
+    const float om[3] = {gyro[0] - bias[0], gyro[1] - bias[1], gyro[2] - bias[2]};
+    const float pitch_rate = om[0] * right[0] + om[1] * right[1] + om[2] * right[2];
+    const float yaw_rate = om[0] * up[0] + om[1] * up[1] + om[2] * up[2];
+    /* (spluton's default bindings, right for a Switch Pro controller and a DualSense: the vertical from the pitch rate,
+       inverted, the horizontal from the yaw rate; then the menu's sensitivity and inversions) */
+    const float sens = rx_option(27, 100) / 100.0f;
+    const float vrate = -pitch_rate * sens * (rx_option(28, 0) ? -1.0f : 1.0f);
+    const float hrate = yaw_rate * sens * (rx_option(29, 0) ? -1.0f : 1.0f) * (1.0f / 6.2831853f);   /* (turns a second) */
+    float v[18] = {0, 0, -1, 0, hrate, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1};
+    static float pitch;   /* the vertical's matrix: the tilt swing, integrated on its own */
+    static int64_t pitch_ns;
+    if (motion) {
+        float pdt = pitch_ns ? (now - pitch_ns) / 1e9f : 0.005f;
+        pitch_ns = now;
+        if (pdt <= 0.0f || pdt > 0.05f) pdt = 0.005f;
+        pitch += vrate * pdt;
+        const float c = cosf(pitch), sn = sinf(pitch);
+        v[9] = 1, v[10] = 0, v[11] = 0;
+        v[12] = 0, v[13] = c, v[14] = -sn;
+        v[15] = 0, v[16] = sn, v[17] = c;
+    } else {
+        pitch_ns = 0;
+    }
+    static int dbg = -1;
+    if (dbg < 0) dbg = getenv("HOS_MOTION_DEBUG") != NULL;
+    static int64_t dbg_next;
+    if (dbg && motion && now >= dbg_next) {
+        dbg_next = now + 500000000LL;
+        hos_log("hid: gyro (%+.2f, %+.2f, %+.2f), up (%+.2f, %+.2f, %+.2f), aim rates (%+.2f, %+.2f), pitch %+.2f",
+                gyro[0], gyro[1], gyro[2], up[0], up[1], up[2], vrate, hrate, pitch);
+    }
     for (int i = 0; i < count; i++) {
         const uint64_t e = out + (uint64_t)i * 0x60;
         memset(GP(e), 0, 0x60);
         G64(e) = 5000000;
         G64(e + 8) = (uint64_t)(s - i);
-        memcpy(GP(e + 0x10), at_rest, sizeof at_rest);
+        memcpy(GP(e + 0x10), motion ? v : at_rest, sizeof at_rest);
         G32(e + 0x58) = 1;
     }
     RET(count);
--- a/gpu/vk/vk_menu.cpp
+++ b/gpu/vk/vk_menu.cpp
@@ -278,6 +278,12 @@
      "Inverts vertical look (up/down) on the right analog stick.", true},
     {TAB_CONTROLS, "Rumble strength", &opts.rumble, 0, 150, 25, tx_rumble,
      "How strongly the controller rumbles, in percent of the game's rumble. Off turns it off.", true},
+    {TAB_CONTROLS, "Gyro aiming", &opts.gyro, 0, 1, 1, tx_onoff,
+     "Aims the slingshot, bow, hookshot and first-person view by tilting a controller with motion sensors (DualSense, Switch Pro).", true},
+    {TAB_CONTROLS, "Gyro sensitivity", &opts.gyro_sens, 25, 300, 25, tx_percent,
+     "How far the aim moves for a tilt of the controller.", true},
+    {TAB_CONTROLS, "Gyro invert vertical", &opts.gyro_inv_y, 0, 1, 1, tx_onoff, "Tilting forward aims up instead of down.", true},
+    {TAB_CONTROLS, "Gyro invert horizontal", &opts.gyro_inv_x, 0, 1, 1, tx_onoff, "Turning right aims left.", true},
     {TAB_CONTROLS, "Layout preset", &opts.pad_layout, 0, 3, 1, tx_pad_layout,
      "Nintendo (default Switch order), Xbox / PC (A for action/talk), or Swap Face. Individual bindings switch this to Custom."},
     {TAB_CONTROLS, "Controls", nullptr, 0, 0, 0, nullptr,   // (hookshot: opens its page)
@@ -419,7 +425,7 @@
 int list_value_l[kTeleportLines];
 bool is_slider(const Row& r) { return r.value == &opts.sharpness || r.value == &opts.mouse_sens; }
 /// (a scale: the arrows, a click and the wheel stop at its ends; Rumble strength would go from Off to its strongest)
-bool stops_at_ends(const Row& r) { return r.value == &opts.mouse_sens || r.value == &opts.rumble; }
+bool stops_at_ends(const Row& r) { return r.value == &opts.mouse_sens || r.value == &opts.rumble || r.value == &opts.gyro_sens; }
 /// (the ini's Rumble strength on the menu's steps: 0 to 150, the nearest 25)
 int rumble_step(int v) { return (std::min(std::max(v, 0), 150) + 12) / 25 * 25; }
 std::string ini_path() {
--- a/gpu/vk/vk_menu.h
+++ b/gpu/vk/vk_menu.h
@@ -26,7 +26,8 @@
     int text_speed = 0;   // how fast dialogue prints: 0 the game's, 1 2x, 2 4x, 3 instant (host/nn_vi.c, nx2/oot_patches.py; HOS_TEXT_SPEED)
     int invert_cam_x = 0; // right-stick camera horizontal inversion: 0 normal, 1 inverted
     int invert_cam_y = 0; // right-stick camera vertical inversion: 0 normal, 1 inverted
-    int rumble = 100;     // the controllers' rumble in percent of the game's: 0 Off, 25 to 150 (host/pad_sdl.c)
+    int rumble = 100;
+    int gyro = 1, gyro_sens = 100, gyro_inv_y = 0, gyro_inv_x = 0;   // gyro aiming (host/nn_hid.c, options 26..29)     // the controllers' rumble in percent of the game's: 0 Off, 25 to 150 (host/pad_sdl.c)
     int pad_layout = 0;   // 0 Nintendo (default), 1 Xbox / PC, 2 Swap Face Buttons, 3 Custom
     int robust = 0;       // bounds-checked buffer access: 0 Auto (on), 1 Off, 2 On (gpu/vk/vk_main.cpp; RX_VK_ROBUST)
     int preset = 0;       // quality preset: 0 Custom, 1 Minimum (4GB), 2 Low (6GB), 3 Balanced (8GB), 4 High (12GB), 5 Ultra (16GB+)
--- a/gpu/vk/vk_replay.cpp
+++ b/gpu/vk/vk_replay.cpp
@@ -3672,7 +3672,8 @@
         if (id == HOS_OPT_CHEAT_ACTION) return menu_take_cheat_action();
         if (id == HOS_OPT_CHEAT_CAMERA) return menu_free_camera();
         return id == 0 ? opts.realtime : id == 1 ? menu_is_open() : id == 2 ? resolution_auto() : id == 3 ? resolution_scale() : id == 4 ? opts.pad_background : id == 5 ? opts.text_speed : id == 6 ? opts.mouse_native :
-               id == 7 ? opts.mouse_sens : id == 8 ? opts.mouse_invert : id == 9 ? opts.rumble : id == 16 ? opts.invert_cam_x : id == 17 ? opts.invert_cam_y : 0;
+               id == 7 ? opts.mouse_sens : id == 8 ? opts.mouse_invert : id == 9 ? opts.rumble : id == 16 ? opts.invert_cam_x : id == 17 ? opts.invert_cam_y :
+               id == 26 ? opts.gyro : id == 27 ? opts.gyro_sens : id == 28 ? opts.gyro_inv_y : id == 29 ? opts.gyro_inv_x : 0;
     };
     t.option_set = menu_option_set;
     t.nvn_register_sampler = register_sampler_queued;
Port of the gyro patch to **v2.0.11**, gyro only, as a diff against the stock v2.0.11 source. (The v2.0.7 file mentioned above never got attached, so here it is inline.) Based on @spluton's gist and the v2.0.7 port notes above, including the horizontal speed fix (the yaw rate in turns per second: SDL's rad/s over 2π). Changes from the gist: - the sensors go through `hos_sdl.c`, SDL3 and SDL2, looked up as optional: without them the game runs as before; - no keyboard rebinding (v2.0.11 has its Controls page); - the "Motion axis x/y/z" rows replaced by Controls rows: **Gyro aiming** (on by default), **Gyro sensitivity** (25–300 %), **Gyro invert vertical / horizontal** (options 26–29 through `rx_option`); the default axes are the gist's (2, 3, 0); - `GetSensorFusionHandles` / `FinalizeSensorFusion` are not redefined: v2.0.11 already has them. It applies with `patch -p1` from the tools folder and builds with 0 failures. Tested on Windows 11 (RTX 5080): aiming works with the slingshot, bow, hookshot and first-person view. `HOS_MOTION_DEBUG=1` logs what goes to the game twice a second. <details><summary>hookshot-v2.0.11-gyro.patch</summary> ```diff --- a/host/hos_sdl.c +++ b/host/hos_sdl.c @@ -20,6 +20,7 @@ void *OpenAudioDevice, *PauseAudioDevice, *OpenAudioDeviceStream, *ResumeAudioStreamDevice, *PutAudioStreamData; /* controllers */ void *PadOpen, *PadClose, *PadConnected, *PadsUpdate, *PadButton, *PadAxis, *PadName, *PadRumble; + void *PadSensorOn, *PadSensorData; /* (optional: NULL when this SDL lacks them) */ void *GetGamepads, *GetGamepadID, *Free; /* (SDL3) */ void *NumJoysticks, *IsGameController, *DeviceInstanceID, *GetJoystick, *JoystickInstanceID; /* (SDL2) */ } f; @@ -58,6 +59,9 @@ GET2(PadAxis, "SDL_GetGamepadAxis", "SDL_GameControllerGetAxis"); GET2(PadName, "SDL_GetGamepadName", "SDL_GameControllerName"); GET2(PadRumble, "SDL_RumbleGamepad", "SDL_GameControllerRumble"); + /* the pads' accelerometer and gyro (gyro aiming, nn_hid.c), optional: without them the game runs as before */ + *(FARPROC *)&f.PadSensorOn = GetProcAddress(m, three ? "SDL_SetGamepadSensorEnabled" : "SDL_GameControllerSetSensorEnabled"); + *(FARPROC *)&f.PadSensorData = GetProcAddress(m, three ? "SDL_GetGamepadSensorData" : "SDL_GameControllerGetSensorData"); if (three) { GET(SetWindowPosition, "SDL_SetWindowPosition"); GET(GetPrimaryDisplay, "SDL_GetPrimaryDisplay"); @@ -532,4 +536,17 @@ if (version == 3) CALL(_Bool (*)(void *, uint16_t, uint16_t, uint32_t), PadRumble)(pad, low, high, ms); else CALL(int (*)(void *, uint16_t, uint16_t, uint32_t), PadRumble)(pad, low, high, ms); } +/* the accelerometer (1) and gyro (2), SDL_SensorType in both SDLs, turned on where the pad has them */ +void hos_sdl_pad_sensors_on(void *pad) { + if (!f.PadSensorOn) return; + for (int t = 1; t <= 2; t++) { + if (version == 3) CALL(_Bool (*)(void *, int, _Bool), PadSensorOn)(pad, t, 1); + else CALL(int (*)(void *, int, int), PadSensorOn)(pad, t, 1); + } +} +int hos_sdl_pad_sensor(void *pad, int type, float *data, int n) { /* 1 when read */ + if (!f.PadSensorData) return 0; + return version == 3 ? CALL(_Bool (*)(void *, int, float *, int), PadSensorData)(pad, type, data, n) + : CALL(int (*)(void *, int, float *, int), PadSensorData)(pad, type, data, n) == 0; +} #undef CALL --- a/host/hos_sdl.h +++ b/host/hos_sdl.h @@ -71,3 +71,5 @@ const char *hos_sdl_pad_name(void *pad); int32_t hos_sdl_pad_id(void *pad); void hos_sdl_pad_rumble(void *pad, uint16_t low, uint16_t high, uint32_t ms); +void hos_sdl_pad_sensors_on(void *pad); /* the accelerometer and gyro on, where the pad has them */ +int hos_sdl_pad_sensor(void *pad, int type, float *data, int n); /* type 1 accel (m/s^2), 2 gyro (rad/s): 1 when read */ --- a/host/pad_sdl.c +++ b/host/pad_sdl.c @@ -87,6 +87,9 @@ out[1] = (uint16_t)l; } +static float state_accel[3], state_gyro[3]; /* (the first pad's motion, hos_sdl_sensors) */ +static int state_motion; + /* one poll: controllers plugged in or gone (each second), then every open one's buttons, sticks and rumble */ static void pads_poll(void) { static int64_t next_scan = 0; @@ -120,11 +123,14 @@ pads[slot].rumble_lo = pads[slot].rumble_hi = 0; pads[slot].rumble_ns = 0; hos_log("hid: controller \"%s\" connected (SDL)", hos_sdl_pad_name(gc)); + hos_sdl_pad_sensors_on(gc); } } uint64_t b = 0; int32_t s[4] = {0}; int count = 0; + float accel[3] = {0}, gyro[3] = {0}; /* (the first pad with motion sensors) */ + int motion = 0; int rx_option(int id, int otherwise); const int strength = rx_option(9, 100); /* (the menu's Rumble strength) */ uint16_t motors[2]; @@ -134,6 +140,7 @@ void *gc = pads[i].gc; if (!gc) continue; count++; + if (!motion && hos_sdl_pad_sensor(gc, 1, accel, 3) && hos_sdl_pad_sensor(gc, 2, gyro, 3)) motion = 1; for (int k = BTN_A; k <= BTN_RIGHT; k++) /* (SDL3's gamepad buttons are in SDL2's order) */ if (hos_sdl_pad_button(gc, k)) b |= 1ULL << k; if (hos_sdl_pad_axis(gc, AX_LT) > 8000) b |= 1ULL << HOS_PAD_LTRIGGER; @@ -155,6 +162,9 @@ state_buttons = b; memcpy(state_stick, s, sizeof s); state_count = count; + memcpy(state_accel, accel, sizeof accel); + memcpy(state_gyro, gyro, sizeof gyro); + state_motion = motion; ReleaseSRWLockExclusive(&state_lock); } @@ -209,6 +219,19 @@ return n; } +/* The first pad's motion (nn_hid.c's six-axis), in SDL's units: acceleration m/s^2, angular velocity rad/s. Returns + whether a pad with sensors is open. */ +int hos_sdl_sensors(float accel[3], float gyro[3]) { + AcquireSRWLockShared(&state_lock); + const int motion = state_motion; + if (motion) { + memcpy(accel, state_accel, 12); + memcpy(gyro, state_gyro, 12); + } + ReleaseSRWLockShared(&state_lock); + return motion; +} + /* Vibration from the game (0..1 amplitudes of the low and high bands), for the pads that rumble. */ void hos_sdl_rumble(float low, float high) { if (low < 0) low = 0; --- a/host/nn_hid.c +++ b/host/nn_hid.c @@ -826,22 +826,127 @@ /* Handles: six-axis ones are opaque u32s; a vibration handle is {u8 style type (3 = full key), u8 npad id, u8 device index (0 left, 1 right), u8 reserved}, which the SDK's vibration classes read. Vibration goes nowhere yet. */ +/* (gyro patch, after Hookshot issue 112: spluton's patch, and its port's fix of the horizontal speed) Gyro aiming from the + pads SDL reads (pad_sdl.c: DualSense, DualShock 4, Switch Pro and the pads in their modes). + The game asks for two handles (a Joy-Con pair) and gates its motion pipeline on the count: at two, its third source + (the console's own) stays dead, and that is the branch that flags motion as active for the aim. Three, all filled. */ HLE(_ZN2nn3hid23GetSixAxisSensorHandlesEPNS0_19SixAxisSensorHandleEiRKjNS_4util10BitFlagSetILi32ENS0_12NpadStyleTagEEE) { - for (int i = 0; i < (int)ARG(1); i++) G32(ARG(0) + 4 * (uint64_t)i) = 0x100u + (uint32_t)i; - RET((int)ARG(1)); + const int n = (int)ARG(1) < 3 ? 3 : (int)ARG(1) > 16 ? 16 : (int)ARG(1); + for (int i = 0; i < n; i++) G32(ARG(0) + 4 * (uint64_t)i) = 0x100u + (uint32_t)i; + RET(n); } +/* The game aims with the controller's own body frame, and folds the euler angles, the pitch rate and the acceleration + into a recenter that cancels anything not body-relative and neutral. So the states go out as: the angles zero, the + acceleration at rest, the vertical aim as a pure tilt-swing matrix integrated from the pitch rate, the horizontal as + the yaw rate in turns per second (as a Switch gives it: SDL's rad/s over 2 pi). The rates are the gyro projected on + a gravity-tracked frame (an up-vector complementary filter), so the pad may be held flat or upright. + The Insert menu (Controls): Gyro aiming (26, on), Gyro sensitivity (27, percent), Gyro invert vertical / horizontal + (28 / 29). HOS_MOTION_DEBUG=1: what goes to the game, twice a second, in the log. */ HLE(_ZN2nn3hid22GetSixAxisSensorStatesEPNS0_18SixAxisSensorStateEiRKNS0_19SixAxisSensorHandleE) { static int64_t six_sampling; const uint64_t out = ARG(0); const int count = (int)ARG(1) < 1 ? 0 : (int)ARG(1) > 32 ? 32 : (int)ARG(1); const int64_t s = ++six_sampling; static const float at_rest[18] = {0, 0, -1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1}; + float accel[3] = {0, 0, -9.80665f}, gyro[3] = {0}; + int hos_sdl_sensors(float accel[3], float gyro[3]); /* pad_sdl.c */ + const int motion = rx_option(26, 1) && hos_sdl_sensors(accel, gyro); + static int said; + if (motion && !said) { + said = 1; + hos_log("hid: gyro aiming from the controller's motion sensors"); + } + const float g = 1.0f / 9.80665f; + static float bias[3]; /* (the gyro's drift, learnt while the pad is still) */ + static int bias_n; + const float bx = gyro[0] - bias[0], by = gyro[1] - bias[1], bz = gyro[2] - bias[2]; + if (motion && bx * bx + by * by + bz * bz < 0.0064f) { /* (still: under 0.08 rad/s) */ + const float k = bias_n < 256 ? 0.06f : 0.004f; + bias[0] += bx * k, bias[1] += by * k, bias[2] += bz * k; + bias_n++; + } + const int64_t now = hos_now_ns(); + static float up[3] = {0, 1, 0}; + static int up_ready; + static int64_t gravity_ns; + float dt = gravity_ns ? (now - gravity_ns) / 1e9f : 0.004f; + gravity_ns = now; + if (dt < 0.001f || dt > 0.05f) dt = 0.004f; + if (!motion) { + up_ready = 0; + gravity_ns = 0; + } else { + const float ax = accel[0], ay = accel[1], az = accel[2]; + const float accel_len = sqrtf(ax * ax + ay * ay + az * az); + const int accel_valid = accel_len > 0.65f / g && accel_len < 1.35f / g; + const float l = accel_valid ? accel_len : 1.0f; + const float observed[3] = {ax / l, ay / l, az / l}; + if (!up_ready && accel_valid) { + memcpy(up, observed, sizeof up); + up_ready = 1; + } else if (up_ready) { + const float gx = gyro[0] - bias[0], gy = gyro[1] - bias[1], gz = gyro[2] - bias[2]; + float pr[3] = {up[0] + (-gy * up[2] + gz * up[1]) * dt, up[1] + (-gz * up[0] + gx * up[2]) * dt, + up[2] + (-gx * up[1] + gy * up[0]) * dt}; + const float pl = sqrtf(pr[0] * pr[0] + pr[1] * pr[1] + pr[2] * pr[2]); + if (pl > 0.001f) + for (int i = 0; i < 3; i++) pr[i] /= pl; + if (accel_valid && pl > 0.001f) { + const float dir = observed[0] * pr[0] + observed[1] * pr[1] + observed[2] * pr[2]; + const float corr = dt / (0.5f + dt); + for (int i = 0; i < 3; i++) pr[i] += ((dir < 0.0f ? -observed[i] : observed[i]) - pr[i]) * corr; + const float cl = sqrtf(pr[0] * pr[0] + pr[1] * pr[1] + pr[2] * pr[2]); + if (cl > 0.001f) + for (int i = 0; i < 3; i++) pr[i] /= cl; + } + memcpy(up, pr, sizeof up); + } + } + /* the pad's right axis on the plane across gravity */ + float right[3] = {1.0f - up[0] * up[0], -up[0] * up[1], -up[0] * up[2]}; + float rl = sqrtf(right[0] * right[0] + right[1] * right[1] + right[2] * right[2]); + if (rl < 0.1f) { + right[0] = -up[0] * up[2], right[1] = -up[1] * up[2], right[2] = 1.0f - up[2] * up[2]; + rl = sqrtf(right[0] * right[0] + right[1] * right[1] + right[2] * right[2]); + } + for (int i = 0; i < 3; i++) right[i] /= rl; + const float om[3] = {gyro[0] - bias[0], gyro[1] - bias[1], gyro[2] - bias[2]}; + const float pitch_rate = om[0] * right[0] + om[1] * right[1] + om[2] * right[2]; + const float yaw_rate = om[0] * up[0] + om[1] * up[1] + om[2] * up[2]; + /* (spluton's default bindings, right for a Switch Pro controller and a DualSense: the vertical from the pitch rate, + inverted, the horizontal from the yaw rate; then the menu's sensitivity and inversions) */ + const float sens = rx_option(27, 100) / 100.0f; + const float vrate = -pitch_rate * sens * (rx_option(28, 0) ? -1.0f : 1.0f); + const float hrate = yaw_rate * sens * (rx_option(29, 0) ? -1.0f : 1.0f) * (1.0f / 6.2831853f); /* (turns a second) */ + float v[18] = {0, 0, -1, 0, hrate, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1}; + static float pitch; /* the vertical's matrix: the tilt swing, integrated on its own */ + static int64_t pitch_ns; + if (motion) { + float pdt = pitch_ns ? (now - pitch_ns) / 1e9f : 0.005f; + pitch_ns = now; + if (pdt <= 0.0f || pdt > 0.05f) pdt = 0.005f; + pitch += vrate * pdt; + const float c = cosf(pitch), sn = sinf(pitch); + v[9] = 1, v[10] = 0, v[11] = 0; + v[12] = 0, v[13] = c, v[14] = -sn; + v[15] = 0, v[16] = sn, v[17] = c; + } else { + pitch_ns = 0; + } + static int dbg = -1; + if (dbg < 0) dbg = getenv("HOS_MOTION_DEBUG") != NULL; + static int64_t dbg_next; + if (dbg && motion && now >= dbg_next) { + dbg_next = now + 500000000LL; + hos_log("hid: gyro (%+.2f, %+.2f, %+.2f), up (%+.2f, %+.2f, %+.2f), aim rates (%+.2f, %+.2f), pitch %+.2f", + gyro[0], gyro[1], gyro[2], up[0], up[1], up[2], vrate, hrate, pitch); + } for (int i = 0; i < count; i++) { const uint64_t e = out + (uint64_t)i * 0x60; memset(GP(e), 0, 0x60); G64(e) = 5000000; G64(e + 8) = (uint64_t)(s - i); - memcpy(GP(e + 0x10), at_rest, sizeof at_rest); + memcpy(GP(e + 0x10), motion ? v : at_rest, sizeof at_rest); G32(e + 0x58) = 1; } RET(count); --- a/gpu/vk/vk_menu.cpp +++ b/gpu/vk/vk_menu.cpp @@ -278,6 +278,12 @@ "Inverts vertical look (up/down) on the right analog stick.", true}, {TAB_CONTROLS, "Rumble strength", &opts.rumble, 0, 150, 25, tx_rumble, "How strongly the controller rumbles, in percent of the game's rumble. Off turns it off.", true}, + {TAB_CONTROLS, "Gyro aiming", &opts.gyro, 0, 1, 1, tx_onoff, + "Aims the slingshot, bow, hookshot and first-person view by tilting a controller with motion sensors (DualSense, Switch Pro).", true}, + {TAB_CONTROLS, "Gyro sensitivity", &opts.gyro_sens, 25, 300, 25, tx_percent, + "How far the aim moves for a tilt of the controller.", true}, + {TAB_CONTROLS, "Gyro invert vertical", &opts.gyro_inv_y, 0, 1, 1, tx_onoff, "Tilting forward aims up instead of down.", true}, + {TAB_CONTROLS, "Gyro invert horizontal", &opts.gyro_inv_x, 0, 1, 1, tx_onoff, "Turning right aims left.", true}, {TAB_CONTROLS, "Layout preset", &opts.pad_layout, 0, 3, 1, tx_pad_layout, "Nintendo (default Switch order), Xbox / PC (A for action/talk), or Swap Face. Individual bindings switch this to Custom."}, {TAB_CONTROLS, "Controls", nullptr, 0, 0, 0, nullptr, // (hookshot: opens its page) @@ -419,7 +425,7 @@ int list_value_l[kTeleportLines]; bool is_slider(const Row& r) { return r.value == &opts.sharpness || r.value == &opts.mouse_sens; } /// (a scale: the arrows, a click and the wheel stop at its ends; Rumble strength would go from Off to its strongest) -bool stops_at_ends(const Row& r) { return r.value == &opts.mouse_sens || r.value == &opts.rumble; } +bool stops_at_ends(const Row& r) { return r.value == &opts.mouse_sens || r.value == &opts.rumble || r.value == &opts.gyro_sens; } /// (the ini's Rumble strength on the menu's steps: 0 to 150, the nearest 25) int rumble_step(int v) { return (std::min(std::max(v, 0), 150) + 12) / 25 * 25; } std::string ini_path() { --- a/gpu/vk/vk_menu.h +++ b/gpu/vk/vk_menu.h @@ -26,7 +26,8 @@ int text_speed = 0; // how fast dialogue prints: 0 the game's, 1 2x, 2 4x, 3 instant (host/nn_vi.c, nx2/oot_patches.py; HOS_TEXT_SPEED) int invert_cam_x = 0; // right-stick camera horizontal inversion: 0 normal, 1 inverted int invert_cam_y = 0; // right-stick camera vertical inversion: 0 normal, 1 inverted - int rumble = 100; // the controllers' rumble in percent of the game's: 0 Off, 25 to 150 (host/pad_sdl.c) + int rumble = 100; + int gyro = 1, gyro_sens = 100, gyro_inv_y = 0, gyro_inv_x = 0; // gyro aiming (host/nn_hid.c, options 26..29) // the controllers' rumble in percent of the game's: 0 Off, 25 to 150 (host/pad_sdl.c) int pad_layout = 0; // 0 Nintendo (default), 1 Xbox / PC, 2 Swap Face Buttons, 3 Custom int robust = 0; // bounds-checked buffer access: 0 Auto (on), 1 Off, 2 On (gpu/vk/vk_main.cpp; RX_VK_ROBUST) int preset = 0; // quality preset: 0 Custom, 1 Minimum (4GB), 2 Low (6GB), 3 Balanced (8GB), 4 High (12GB), 5 Ultra (16GB+) --- a/gpu/vk/vk_replay.cpp +++ b/gpu/vk/vk_replay.cpp @@ -3672,7 +3672,8 @@ if (id == HOS_OPT_CHEAT_ACTION) return menu_take_cheat_action(); if (id == HOS_OPT_CHEAT_CAMERA) return menu_free_camera(); return id == 0 ? opts.realtime : id == 1 ? menu_is_open() : id == 2 ? resolution_auto() : id == 3 ? resolution_scale() : id == 4 ? opts.pad_background : id == 5 ? opts.text_speed : id == 6 ? opts.mouse_native : - id == 7 ? opts.mouse_sens : id == 8 ? opts.mouse_invert : id == 9 ? opts.rumble : id == 16 ? opts.invert_cam_x : id == 17 ? opts.invert_cam_y : 0; + id == 7 ? opts.mouse_sens : id == 8 ? opts.mouse_invert : id == 9 ? opts.rumble : id == 16 ? opts.invert_cam_x : id == 17 ? opts.invert_cam_y : + id == 26 ? opts.gyro : id == 27 ? opts.gyro_sens : id == 28 ? opts.gyro_inv_y : id == 29 ? opts.gyro_inv_x : 0; }; t.option_set = menu_option_set; t.nvn_register_sampler = register_sampler_queued; ``` </details>
XDM-Inc commented 2026-11-10 03:15:43 +00:00 (Migrated from github.com)

shame that gyro is not planned officially as its a core part of the modern zelda games. other then that it works in emulators

shame that gyro is not planned officially as its a core part of the modern zelda games. other then that it works in emulators
youcia190 commented 2026-11-10 08:46:23 +00:00 (Migrated from github.com)

Port of the gyro patch to v2.0.11, gyro only, as a diff against the stock v2.0.11 source. (The v2.0.7 file mentioned above never got attached, so here it is inline.)

Based on @spluton's gist and the v2.0.7 port notes above, including the horizontal speed fix (the yaw rate in turns per second: SDL's rad/s over 2π). Changes from the gist:

  • the sensors go through hos_sdl.c, SDL3 and SDL2, looked up as optional: without them the game runs as before;
  • no keyboard rebinding (v2.0.11 has its Controls page);
  • the "Motion axis x/y/z" rows replaced by Controls rows: Gyro aiming (on by default), Gyro sensitivity (25–300 %), Gyro invert vertical / horizontal (options 26–29 through rx_option); the default axes are the gist's (2, 3, 0);
  • GetSensorFusionHandles / FinalizeSensorFusion are not redefined: v2.0.11 already has them.

It applies with patch -p1 from the tools folder and builds with 0 failures. Tested on Windows 11 (RTX 5080): aiming works with the slingshot, bow, hookshot and first-person view. HOS_MOTION_DEBUG=1 logs what goes to the game twice a second.

hookshot-v2.0.11-gyro.patch

I am someone who knows nothing.

I asked ChatGPT to handle your source code, and it made the gyroscope work.

Thank you.

I am a free ChatGPT user.

> Port of the gyro patch to **v2.0.11**, gyro only, as a diff against the stock v2.0.11 source. (The v2.0.7 file mentioned above never got attached, so here it is inline.) > > Based on [@spluton](https://github.com/spluton)'s gist and the v2.0.7 port notes above, including the horizontal speed fix (the yaw rate in turns per second: SDL's rad/s over 2π). Changes from the gist: > > * the sensors go through `hos_sdl.c`, SDL3 and SDL2, looked up as optional: without them the game runs as before; > * no keyboard rebinding (v2.0.11 has its Controls page); > * the "Motion axis x/y/z" rows replaced by Controls rows: **Gyro aiming** (on by default), **Gyro sensitivity** (25–300 %), **Gyro invert vertical / horizontal** (options 26–29 through `rx_option`); the default axes are the gist's (2, 3, 0); > * `GetSensorFusionHandles` / `FinalizeSensorFusion` are not redefined: v2.0.11 already has them. > > It applies with `patch -p1` from the tools folder and builds with 0 failures. Tested on Windows 11 (RTX 5080): aiming works with the slingshot, bow, hookshot and first-person view. `HOS_MOTION_DEBUG=1` logs what goes to the game twice a second. > > hookshot-v2.0.11-gyro.patch I am someone who knows nothing. I asked ChatGPT to handle your source code, and it made the gyroscope work. Thank you. I am a free ChatGPT user.
XDM-Inc commented 2026-11-11 05:37:36 +00:00 (Migrated from github.com)

and here we are with working Gyro implemented now all of a sudden! in latest build

and here we are with working Gyro implemented now all of a sudden! in latest build
Commenting is not possible because the repository is archived.
No milestone
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Hookshot#112
No description provided.