Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions dm/games/010069401ADB8000/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. 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

- `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).
Expand All @@ -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 (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

Expand Down
5 changes: 5 additions & 0 deletions dm/host/hle_haptics.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions dm/host/hle_haptics.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
16 changes: 14 additions & 2 deletions dm/host/hle_hid.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 3 additions & 1 deletion dm/host/tests/t_hid.c
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
1 change: 1 addition & 0 deletions docs/agents/symptoms.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
- `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)
Expand Down
2 changes: 1 addition & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<id> 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=<id> 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.

Expand Down
7 changes: 7 additions & 0 deletions docs/reference/services.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:** `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`), 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

- **Symptom:** taps in handheld mode do nothing, though the game reads the touch screen (the title's PRESS ANY BUTTON, the menus' cards).
Expand Down