From 74a7b17033b08ffaab089a9b9e6933b14afa4065 Mon Sep 17 00:00:00 2001 From: parsifal295 <130349765+parsifal295@users.noreply.github.com> Date: Mon, 26 Oct 2026 09:13:49 +0900 Subject: [PATCH 1/2] Vibration handles before a controller's haptics are routed: Unicorn Overlord asks for its handles once at startup, before GameController reports the pad, and kept an empty answer for the whole session hid::GetVibrationDeviceHandles answered 0 handles until a controller's haptics were routed (vib_targets: dm_haptics_available). Unicorn Overlord calls it once at startup; on the Mac the DualSense is reported a moment later, so the game kept no handles and sent every value (two hid::SendVibrationValue a frame per player) to handle 0, which reached no output: no rumble for the whole session. On Horizon the handles are computed from the npad id and style alone (libnx _hidGetVibrationDeviceHandles), and vib_send already checks for an output at each send. The handles now need only rumble on with an output sink installed (new dm_haptics_enabled) and the npad's connected style; no sink, rumble off or a disconnected npad still get none, and IsVibrationPermitted still follows the routed outputs (t_hid's checks for both are unchanged). DM_RUMBLE_LOG=1 logs the handle requests and, once a second, the values sent, those that reached no output and the peak amplitude. Docs: services.md "Vibration handles before a controller is reported" (symptom index regenerated), the rumble switches in development.md, and Unicorn Overlord's note (Input: what its two vibration paths do; Found on this game). Checked: t_hid (61 checks, one new: handles before routing), t_haptics and t_haptics_apple pass; the tool tests and the docs check pass; dmhost builds for Unicorn Overlord. On the Mac with a DualSense (Unicorn Overlord, Asian release): before, the request was answered with none and every value reached no output; after, the handles are 0x3 / 0x10003 and none is dropped --- dm/games/010069401ADB8000/README.md | 3 +++ dm/host/hle_haptics.c | 5 +++++ dm/host/hle_haptics.h | 1 + dm/host/hle_hid.c | 16 ++++++++++++++-- dm/host/tests/t_hid.c | 4 +++- docs/agents/symptoms.md | 1 + docs/development.md | 2 +- docs/reference/services.md | 7 +++++++ 8 files changed, 35 insertions(+), 4 deletions(-) diff --git a/dm/games/010069401ADB8000/README.md b/dm/games/010069401ADB8000/README.md index 76990d502d..b40ee5e9e3 100644 --- a/dm/games/010069401ADB8000/README.md +++ b/dm/games/010069401ADB8000/README.md @@ -40,6 +40,8 @@ Each found by one boot ([Hardware Opus decoders built on the SDK's software deco - The game reads a USB mouse and no touch screen, as OCTOPATH TRAVELER 0 and Tears of the Kingdom do; a rule that turns touches into a mouse would need testing on it ([ROMANCE OF THE THREE KINGDOMS 8 REMAKE](../0100C2A01A730000/README.md)). +- Rumble: `main` sends each player's vibration every frame (two `hid::SendVibrationValue` calls per player; Asian release `main` `9c3116f0…` at `+0x5ead8c`) from fields of its pad object that only the pad reset writes (amplitude 0, 160 / 320 Hz), so that path never vibrates. CRI's audio-driven vibration (`hid::VibrationConverter`, behind the sound renderer type 6 that CRI registers at startup, `+0x790b60`) never opened in the opening scene nor in a battle on the Mac. With a DualSense the values reach the controller since [Vibration handles before a controller is reported](../../../docs/reference/services.md#vibration-handles-before-a-controller-is-reported); nothing was felt, so this game may not rumble at all. + ## Mac routes - `DM_KEYS=f2400:A*20/300` goes from the title screen (about frame 2,500) through a new game into the opening story scene (about frame 4,900). @@ -50,6 +52,7 @@ General host changes first needed here; each applies to every game: - `alloc_big_align` was first needed on 13 Sentinels ([its note](../01003FC01670C000/README.md#memory), [the profile keys](../README.md#keys)). - **The hardware multi-stream Opus decoder's C interface** forwarded to the SDK's software decoder (`hle_codec.c`) and **`GenerateCryptographicallyRandomBytes` under the `sdk`'s own `getentropy`** (`HLE_FORCE`, `hle_os.c`): [Hardware Opus decoders built on the SDK's software decoders](../../../docs/reference/audio.md#hardware-opus-decoders-built-on-the-sdks-software-decoders), [Cryptographically random bytes](../../../docs/reference/services.md#cryptographically-random-bytes). +- **Vibration handles answered before a controller's haptics are routed** (`hle_hid.c`): the game asks for them once at startup, before GameController reports the pad, and every value went to handle 0 ([Vibration handles before a controller is reported](../../../docs/reference/services.md#vibration-handles-before-a-controller-is-reported)). ## Open diff --git a/dm/host/hle_haptics.c b/dm/host/hle_haptics.c index 2b31d064b8..6f2fbdb255 100644 --- a/dm/host/hle_haptics.c +++ b/dm/host/hle_haptics.c @@ -42,6 +42,11 @@ uint64_t dm_haptics_epoch(int p) { uint64_t e = valid(p) ? states[p].epoch : 0; pthread_mutex_unlock(&lock); return e; } +int dm_haptics_enabled(void) { + pthread_mutex_lock(&lock); + int on = enabled && output != NULL; + pthread_mutex_unlock(&lock); return on; +} uint32_t dm_haptics_available(void) { pthread_mutex_lock(&lock); uint32_t m = enabled && output ? available : 0; diff --git a/dm/host/hle_haptics.h b/dm/host/hle_haptics.h index 1704d4f2e9..b21a5f6338 100644 --- a/dm/host/hle_haptics.h +++ b/dm/host/hle_haptics.h @@ -15,6 +15,7 @@ void dm_haptics_route(int player, int available); // clears old state even whe void dm_haptics_clear(int player); // engine reset: needs a fresh game signal uint64_t dm_haptics_epoch(int player); uint32_t dm_haptics_available(void); +int dm_haptics_enabled(void); // an output sink is installed and rumble is on (whether or not a controller has connected yet) void dm_haptics_suspend(unsigned reason, int suspended); void dm_haptics_send(const dm_vibration_update *updates, int count); // one atomic batch dm_vibration dm_haptics_actual(int player, int motor); diff --git a/dm/host/hle_hid.c b/dm/host/hle_hid.c index a6e9296586..7ccc3f7257 100644 --- a/dm/host/hle_hid.c +++ b/dm/host/hle_hid.c @@ -365,16 +365,28 @@ static void vib_send(const uint32_t *handles, const dm_vibration *values, int n) } int count = 0; for (int k = 0; k < DM_PLAYERS * 2; k++) if (used >> k & 1) updates[count++] = updates[k]; + static int vlog = -1; if (vlog < 0) vlog = getenv("DM_RUMBLE_LOG") != NULL; + if (vlog) { // DM_RUMBLE_LOG: once a second, how many values the game sent, how many reached no output, and the strongest amplitude + static uint64_t sends, dropped, t0; static float peak; struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); + uint64_t now = ts.tv_sec; sends += n; if (!count) dropped += n; + for (int i = 0; i < n; i++) peak = fmaxf(peak, fmaxf(values[i].amplitude_low, values[i].amplitude_high)); + if (now != t0) { LOG("rumble: %llu values sent, %llu with no output, peak amplitude %.2f, handle %#x", sends, dropped, peak, handles[0]); sends = dropped = 0; peak = 0; t0 = now; } + } dm_haptics_send(updates, count); } HLE_FORCE("_ZN2nn3hid20IsVibrationPermittedEv", "vibration permission: enabled and an output available", unsigned, h_hid_VibPermit, void) { return dm_haptics_available() != 0; } +// Handles do not wait for a controller: games ask once at startup, before GameController reports the pad, and keep the answer (Unicorn Overlord muted rumble for the +// whole session). On Horizon they are computed from the npad id and style alone (libnx _hidGetVibrationDeviceHandles); vib_send checks for an output at each send. HLE_FORCE("_ZN2nn3hid25GetVibrationDeviceHandlesEPNS0_21VibrationDeviceHandleEiRKjNS_4util10BitFlagSetILi32ENS0_12NpadStyleTagEEE", "vibration devices of the connected controller", int, h_hid_VibHandles, uint32_t *h, int n, const uint32_t *id, uint32_t style) { - if (!h || !id || n <= 0 || !(style & style_of(*id))) return 0; + if (!h || !id || n <= 0 || !(style & style_of(*id)) || !dm_haptics_enabled()) { + if (getenv("DM_RUMBLE_LOG") && id) LOG("rumble: game asked for vibration handles of npad %u style %#x: none (connected style %#x, rumble %s)", *id, style, style_of(*id), dm_haptics_enabled() ? "on" : "off"); + return 0; + } uint32_t base = (*id << 8) | (*id == NPAD_HANDHELD ? 4u : 3u); - if (!vib_targets(base)) return 0; int count = n < 2 ? n : 2; for (int i = 0; i < count; i++) h[i] = base | (uint32_t)i << 16; + if (getenv("DM_RUMBLE_LOG")) LOG("rumble: game asked for vibration handles of npad %u style %#x: %d, first %#x", *id, style, count, h[0]); return count; } HLE_FORCE("_ZN2nn3hid25InitializeVibrationDeviceERKNS0_21VibrationDeviceHandleE", "vibration: initialize one channel", void, h_hid_InitVib, const uint32_t *h) { diff --git a/dm/host/tests/t_hid.c b/dm/host/tests/t_hid.c index c6cb7f4082..fb08f1e9ba 100644 --- a/dm/host/tests/t_hid.c +++ b/dm/host/tests/t_hid.c @@ -116,7 +116,9 @@ static void t_vibration(void) { dm_vibration v = {0.25, 160, 0.5, 320}, got; g_mode = 1; ids((uint32_t[]){0, 1, NPAD_HANDHELD}, 3); dm_pad_players(3); CHECK(!h_hid_VibPermit() && !h_hid_VibHandles(h, 2, &id, STYLE_FULLKEY), "no sink: no vibration devices"); - dm_haptics_init(vibration_sink, 1); dm_haptics_route(0, 1); dm_haptics_route(1, 1); + dm_haptics_init(vibration_sink, 1); + CHECK(h_hid_VibHandles(h, 2, &id, STYLE_FULLKEY) == 2, "handles before the controller's haptics are routed (games ask once, at startup)"); + dm_haptics_route(0, 1); dm_haptics_route(1, 1); CHECK(h_hid_VibPermit() && h_hid_VibHandles(h, 2, &id, STYLE_FULLKEY) == 2, "two vibration motors available"); CHECK(h[0] == 3 && h[1] == 0x10003, "SDK vibration handle bytes: style 3, npad 0, motors 0 and 1"); CHECK(!h_hid_VibHandles(h, 2, &id, STYLE_HANDHELD), "wrong controller style gets no motors"); diff --git a/docs/agents/symptoms.md b/docs/agents/symptoms.md index a5285ace6d..eab86d76cf 100644 --- a/docs/agents/symptoms.md +++ b/docs/agents/symptoms.md @@ -111,6 +111,7 @@ One line per symptom in the [subsystem references](../reference/): what you see - motion moves never happen; shaking does nothing (Super Mario Odyssey: cap throws by shaking). → [Six-axis sensors](../reference/services.md#six-axis-sensors) - SDK 20 calls that reach a service; `nfp`'s `AttachActivateEvent` / `AttachDeactivateEvent` crashed in the SDK at `sdk+0x3f2a78`. → [SDK 20 six-axis sensor and amiibo (`nfp`) calls](../reference/services.md#sdk-20-six-axis-sensor-and-amiibo-nfp-calls) - abort in `nn::sm::Initialize` under `nn::hid::VibrationNode::Update`. → [Vibration values flushed by the SDK's vibration graph](../reference/services.md#vibration-values-flushed-by-the-sdks-vibration-graph) +- no rumble for the whole session with a controller that has haptics (a DualSense on the Mac); `DM_RUMBLE_LOG=1` shows the game's handle request answered with none, then every value sent to handle 0 and reaching no output. → [Vibration handles before a controller is reported](../reference/services.md#vibration-handles-before-a-controller-is-reported) - taps in handheld mode do nothing, though the game reads the touch screen (the title's PRESS ANY BUTTON, the menus' cards). → [Touch gestures recognized from the touch points](../reference/services.md#touch-gestures-recognized-from-the-touch-points) - the first boot aborted in `hid::GetMouseState` (the SDK's mouse resource was never initialized). → [Mouse, keyboard and key code maps](../reference/services.md#mouse-keyboard-and-key-code-maps) - the first boot crashed in `hid::detail::SetPalmaBoostMode`, a null read at `sdk+0x238eb0` (the hid service never opened). → [Poké Ball Plus (Palma)](../reference/services.md#poké-ball-plus-palma) diff --git a/docs/development.md b/docs/development.md index 0b8b5d2495..bfbb49c464 100644 --- a/docs/development.md +++ b/docs/development.md @@ -146,7 +146,7 @@ The game language (`--language`, `LANGUAGE`; English by default) is what `nn::oe On the device, environment switches go in `Documents/env.txt` (one `KEY=VALUE` per line), and game files are also accepted at `Documents/romfs`. -Game rumble is enabled by default (`DM_RUMBLE=0` disables it); `DM_RUMBLE_GAIN=0..3` scales its strength (default 1, zero is silent). Both are launcher settings, independent of `DM_TOUCH_HAPTICS`. `hle_hid.c` decodes SDK vibration handles (style index / npad / motor bytes, as in libnx's `_hidGetVibrationDeviceHandles`) and routes them through the same player assignments as input. `hle_haptics.c` retains each motor's two amplitude/frequency bands, commits batches atomically and invalidates queued output on lifecycle or route changes. `haptics_apple.m` provides shared Core Haptics output for Mac controllers and iOS: left/right handle localities when available, otherwise one combined output; on iPhone, the phone is used while no controller is selected. A connected controller without haptics produces no game rumble. Continuous effects keep running until the game stops them; short effects are not extended. Engine resets, focus loss and interruptions clear the old values and require a fresh game signal. The frequency-to-sharpness mapping is approximate and physical feel / controller compatibility require device testing. The headless Mac host has no output. `make -C dm/host/tests GAME= run` covers state and HID routing without hardware. +Game rumble is enabled by default (`DM_RUMBLE=0` disables it); `DM_RUMBLE_GAIN=0..3` scales its strength (default 1, zero is silent). `DM_RUMBLE_LOG=1` logs the game's vibration handle requests and, once a second, the values it sent, how many reached no output and the peak amplitude. Both are launcher settings, independent of `DM_TOUCH_HAPTICS`. `hle_hid.c` decodes SDK vibration handles (style index / npad / motor bytes, as in libnx's `_hidGetVibrationDeviceHandles`) and routes them through the same player assignments as input. `hle_haptics.c` retains each motor's two amplitude/frequency bands, commits batches atomically and invalidates queued output on lifecycle or route changes. `haptics_apple.m` provides shared Core Haptics output for Mac controllers and iOS: left/right handle localities when available, otherwise one combined output; on iPhone, the phone is used while no controller is selected. A connected controller without haptics produces no game rumble. Continuous effects keep running until the game stops them; short effects are not extended. Engine resets, focus loss and interruptions clear the old values and require a fresh game signal. The frequency-to-sharpness mapping is approximate and physical feel / controller compatibility require device testing. The headless Mac host has no output. `make -C dm/host/tests GAME= run` covers state and HID routing without hardware. Haptic engines are created on demand, after the game has activated its audio session. Stop/reset handlers are installed before starting an engine. Idle release, focus loss and engine resets stop an engine but keep it, so the next effect restarts the same one. An engine or player failure releases that connection; fresh game signals retry with a new engine after a 1–8 second backoff, without replaying effects from before the failure. If a controller advertises separate handle localities but those engines cannot be created or started, the route falls back to the controller's default locality until it is reconnected. The log names the controller, localities and output channels, and errors name the failed stage with the error domain/code and underlying service error. The Apple adapter test covers failed creation/start, player and parameter failures, default-locality fallback and stale callbacks using fake controllers and engines; an iOS Settings Identify response alone does not verify the app's own haptics connection. diff --git a/docs/reference/services.md b/docs/reference/services.md index ab4b107716..f8bf6a92bf 100644 --- a/docs/reference/services.md +++ b/docs/reference/services.md @@ -235,6 +235,13 @@ Most service calls can be found before they crash, before the first boot or the - **Fix:** the flush calls it through the PLT, so an `HLE_FORCE` on that symbol catches it and returns success (`hle_hid.c`). - **Found on:** [Metroid Dread](../../dm/games/010093801237C000/README.md). +### Vibration handles before a controller is reported + +- **Symptom:** no rumble for the whole session with a controller that has haptics (a DualSense on the Mac); `DM_RUMBLE_LOG=1` shows the game's handle request answered with none, then every value sent to handle 0 and reaching no output. +- **Cause:** `hid::GetVibrationDeviceHandles` answered 0 handles until a controller's haptics were routed. Games ask once at startup, before GameController reports the pad, and keep the answer. On Horizon the handles are computed from the npad id and style alone (libnx `_hidGetVibrationDeviceHandles`). +- **Fix:** the handles no longer wait for a routed output: with rumble on and an output sink installed (`dm_haptics_enabled`), a connected npad style gets its two handles; `vib_send` already checks for an output at each send (`hle_hid.c`). No sink, rumble off or a disconnected npad still get none, and `IsVibrationPermitted` still follows the routed outputs. `DM_RUMBLE_LOG=1` logs the handle requests and, once a second, the values sent, those that reached no output and the peak amplitude. +- **Found on:** [Unicorn Overlord](../../dm/games/010069401ADB8000/README.md) (the Asian release, `010054B01AD92000`). + ### Touch gestures recognized from the touch points - **Symptom:** taps in handheld mode do nothing, though the game reads the touch screen (the title's PRESS ANY BUTTON, the menus' cards). From 5e9363527d6044b1a599cf624bf01247127a9c5f Mon Sep 17 00:00:00 2001 From: parsifal295 <130349765+parsifal295@users.noreply.github.com> Date: Mon, 26 Oct 2026 10:02:51 +0900 Subject: [PATCH 2/2] Docs: Unicorn Overlord has no rumble of its own; the handle bug was seen in the log only The reference entry and the game note said the game lost its rumble. It has none (on the Switch it does not vibrate either): it sends amplitude 0 throughout, so nothing could be felt before or after the fix. The symptom is now the log's (handles answered with none, every value reaching no output), what a rumbling game would lose is said as such, and the Found on line says the fix is not yet checked on a game that rumbles. Symptom index regenerated; the docs check passes --- dm/games/010069401ADB8000/README.md | 4 ++-- docs/agents/symptoms.md | 2 +- docs/reference/services.md | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dm/games/010069401ADB8000/README.md b/dm/games/010069401ADB8000/README.md index b40ee5e9e3..a99da3188e 100644 --- a/dm/games/010069401ADB8000/README.md +++ b/dm/games/010069401ADB8000/README.md @@ -40,7 +40,7 @@ Each found by one boot ([Hardware Opus decoders built on the SDK's software deco - The game reads a USB mouse and no touch screen, as OCTOPATH TRAVELER 0 and Tears of the Kingdom do; a rule that turns touches into a mouse would need testing on it ([ROMANCE OF THE THREE KINGDOMS 8 REMAKE](../0100C2A01A730000/README.md)). -- Rumble: `main` sends each player's vibration every frame (two `hid::SendVibrationValue` calls per player; Asian release `main` `9c3116f0…` at `+0x5ead8c`) from fields of its pad object that only the pad reset writes (amplitude 0, 160 / 320 Hz), so that path never vibrates. CRI's audio-driven vibration (`hid::VibrationConverter`, behind the sound renderer type 6 that CRI registers at startup, `+0x790b60`) never opened in the opening scene nor in a battle on the Mac. With a DualSense the values reach the controller since [Vibration handles before a controller is reported](../../../docs/reference/services.md#vibration-handles-before-a-controller-is-reported); nothing was felt, so this game may not rumble at all. +- Rumble: `main` sends each player's vibration every frame (two `hid::SendVibrationValue` calls per player; Asian release `main` `9c3116f0…` at `+0x5ead8c`) from fields of its pad object that only the pad reset writes (amplitude 0, 160 / 320 Hz), so that path never vibrates. CRI's audio-driven vibration (`hid::VibrationConverter`, behind the sound renderer type 6 that CRI registers at startup, `+0x790b60`) never opened in the opening scene nor in a battle on the Mac. The game has no rumble: on the Switch it does not vibrate either. With a DualSense its (zero) values reach the controller since [Vibration handles before a controller is reported](../../../docs/reference/services.md#vibration-handles-before-a-controller-is-reported). ## Mac routes @@ -52,7 +52,7 @@ General host changes first needed here; each applies to every game: - `alloc_big_align` was first needed on 13 Sentinels ([its note](../01003FC01670C000/README.md#memory), [the profile keys](../README.md#keys)). - **The hardware multi-stream Opus decoder's C interface** forwarded to the SDK's software decoder (`hle_codec.c`) and **`GenerateCryptographicallyRandomBytes` under the `sdk`'s own `getentropy`** (`HLE_FORCE`, `hle_os.c`): [Hardware Opus decoders built on the SDK's software decoders](../../../docs/reference/audio.md#hardware-opus-decoders-built-on-the-sdks-software-decoders), [Cryptographically random bytes](../../../docs/reference/services.md#cryptographically-random-bytes). -- **Vibration handles answered before a controller's haptics are routed** (`hle_hid.c`): the game asks for them once at startup, before GameController reports the pad, and every value went to handle 0 ([Vibration handles before a controller is reported](../../../docs/reference/services.md#vibration-handles-before-a-controller-is-reported)). +- **Vibration handles answered before a controller's haptics are routed** (`hle_hid.c`): the game asks for them once at startup, before GameController reports the pad, and every value went to handle 0 (seen in the log; the game has no rumble, so nothing changed in play) ([Vibration handles before a controller is reported](../../../docs/reference/services.md#vibration-handles-before-a-controller-is-reported)). ## Open diff --git a/docs/agents/symptoms.md b/docs/agents/symptoms.md index eab86d76cf..9cae616e00 100644 --- a/docs/agents/symptoms.md +++ b/docs/agents/symptoms.md @@ -111,7 +111,7 @@ One line per symptom in the [subsystem references](../reference/): what you see - motion moves never happen; shaking does nothing (Super Mario Odyssey: cap throws by shaking). → [Six-axis sensors](../reference/services.md#six-axis-sensors) - SDK 20 calls that reach a service; `nfp`'s `AttachActivateEvent` / `AttachDeactivateEvent` crashed in the SDK at `sdk+0x3f2a78`. → [SDK 20 six-axis sensor and amiibo (`nfp`) calls](../reference/services.md#sdk-20-six-axis-sensor-and-amiibo-nfp-calls) - abort in `nn::sm::Initialize` under `nn::hid::VibrationNode::Update`. → [Vibration values flushed by the SDK's vibration graph](../reference/services.md#vibration-values-flushed-by-the-sdks-vibration-graph) -- no rumble for the whole session with a controller that has haptics (a DualSense on the Mac); `DM_RUMBLE_LOG=1` shows the game's handle request answered with none, then every value sent to handle 0 and reaching no output. → [Vibration handles before a controller is reported](../reference/services.md#vibration-handles-before-a-controller-is-reported) +- `DM_RUMBLE_LOG=1` shows the game's startup handle request answered with none, then every vibration value sent to handle 0 and reaching no output for the whole session, although a controller with haptics (a DualSense on the Mac) is connected. A game that rumbles would stay silent. → [Vibration handles before a controller is reported](../reference/services.md#vibration-handles-before-a-controller-is-reported) - taps in handheld mode do nothing, though the game reads the touch screen (the title's PRESS ANY BUTTON, the menus' cards). → [Touch gestures recognized from the touch points](../reference/services.md#touch-gestures-recognized-from-the-touch-points) - the first boot aborted in `hid::GetMouseState` (the SDK's mouse resource was never initialized). → [Mouse, keyboard and key code maps](../reference/services.md#mouse-keyboard-and-key-code-maps) - the first boot crashed in `hid::detail::SetPalmaBoostMode`, a null read at `sdk+0x238eb0` (the hid service never opened). → [Poké Ball Plus (Palma)](../reference/services.md#poké-ball-plus-palma) diff --git a/docs/reference/services.md b/docs/reference/services.md index f8bf6a92bf..07e3b906fd 100644 --- a/docs/reference/services.md +++ b/docs/reference/services.md @@ -237,10 +237,10 @@ Most service calls can be found before they crash, before the first boot or the ### Vibration handles before a controller is reported -- **Symptom:** no rumble for the whole session with a controller that has haptics (a DualSense on the Mac); `DM_RUMBLE_LOG=1` shows the game's handle request answered with none, then every value sent to handle 0 and reaching no output. +- **Symptom:** `DM_RUMBLE_LOG=1` shows the game's startup handle request answered with none, then every vibration value sent to handle 0 and reaching no output for the whole session, although a controller with haptics (a DualSense on the Mac) is connected. A game that rumbles would stay silent. - **Cause:** `hid::GetVibrationDeviceHandles` answered 0 handles until a controller's haptics were routed. Games ask once at startup, before GameController reports the pad, and keep the answer. On Horizon the handles are computed from the npad id and style alone (libnx `_hidGetVibrationDeviceHandles`). - **Fix:** the handles no longer wait for a routed output: with rumble on and an output sink installed (`dm_haptics_enabled`), a connected npad style gets its two handles; `vib_send` already checks for an output at each send (`hle_hid.c`). No sink, rumble off or a disconnected npad still get none, and `IsVibrationPermitted` still follows the routed outputs. `DM_RUMBLE_LOG=1` logs the handle requests and, once a second, the values sent, those that reached no output and the peak amplitude. -- **Found on:** [Unicorn Overlord](../../dm/games/010069401ADB8000/README.md) (the Asian release, `010054B01AD92000`). +- **Found on:** [Unicorn Overlord](../../dm/games/010069401ADB8000/README.md) (the Asian release, `010054B01AD92000`), in the log only: the game has no rumble of its own (it sends amplitude 0 throughout), so nothing could be felt before or after. Not yet checked on a game that rumbles. ### Touch gestures recognized from the touch points