Skip to content

The Joy-Con 2's mouse sensor and pointer, and the Joy-Con 2 npad styles (after #1363, issue #1267) - #1426

Closed
JakePre wants to merge 4 commits into
seedds:mainfrom
JakePre:switch2-pr4-mouse
Closed

JakePre wants to merge 4 commits into
seedds:mainfrom
JakePre:switch2-pr4-mouse

Conversation

@JakePre

@JakePre JakePre commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #1363 (and so on #1349 and #1346). The first three commits are theirs; only the last, "The Joy-Con 2's mouse sensor and pointer…", is new. I'll rebase once they land.

What and why

This follows the three PRs agreed in #1267. It's small (about 200 lines) and stays within that device: it adds the Joy-Con 2's mouse sensor, which those PRs left out. A Switch 2 game's mouse mode did nothing with Joy-Con 2s. The host answered as having no sensor (GetJcmsStates none, GetNmouseHandles none). A Joy-Con 2 pad also had only the plain Joy-Con styles, while a Switch 2 game can support only their Joy-Con 2 forms.

The npad layer (hle_hid.c, dm_input.h), neutral like the rest of the Joy-Con API in #1363:

  • dm_joy_mouse(pad, side, dx, dy, state): a Joy-Con's movement since its last report, and whether it is connected and on a surface. Nothing changes for a Joy-Con that never calls it.
  • dm_joy_attach takes DM_JOY_2: the pad's npad then shows the Joy-Con 2 style beside the plain one, to a game that supports it.
    • Style bits 12, 13 and 14 are taken as the Joy-Con 2 forms of JoyDual, JoyLeft and JoyRight. This is inferred, not documented: Nintendo Switch 2 Welcome Tour's mouse exhibits support only 0x1000, and test a pair with 0x1004, a left Joy-Con with 0x2008 and a right one with 0x4000 beside JoyRight.
  • GetJcmsStates / GetJcmsState: a ring of 16 states per player and Joy-Con.
    • A JcmsState, as Welcome Tour reads it (main+0x253540), is 8 bytes it doesn't read, the sampling number at 8 (newest first), the movement at 0x10 and 0x14, and attributes at 0x18 (bit 0 connected, bit 1 on a surface).
    • JcmsType 0 is the left Joy-Con, 1 the right.
  • Nmouse (the pointer): a handle per Joy-Con whose sensor reports.
    • The one last used on a surface comes first, and stays first while lifted, so you can lift it to press a button. Welcome Tour moves its pointer by the first handle only.
    • An NmouseState (main+0x253b2c) is 48 bytes: 8 bytes, the Switch's MouseState fields, buttons at 0x28 (left 1 is R or ZR on a right Joy-Con, L or ZL on a left one; right 2), and attributes at 0x2c (bit 1 connected, which the game requires).
    • SetNmousePointerPosition moves it. The movement is a quarter of the sensor's counts.
  • DM_JCMS_LOG=1: logs the game's sensor and pointer calls, at most once a second each kind.

The Switch 2 transport turns the sensor on with its own feature bit, 0x10 (S2_FEATURE_MOUSE). It's left off for the Pro Controller 2, which has no sensor. Measured on a right Joy-Con 2 slid on a desk:

  • Report bytes 0x10 and 0x12 are 16-bit position counters that wrap (x right, y toward the player).
  • 0x14 and 0x16 jump from about 1,600 and 150 on a surface to about 4,600 and 3,200 when lifted. 0x16 under 1,000 counts as on a surface.
  • While lifted, no movement is reported, since the counters drift with nothing under them.

Overlap with the Welcome Tour PR (#1425): that one adds GetJcmsState as a cleared state (no sensor), so the game runs on main. This one fills it from the sensor. Whichever lands second, I'll rebase.

Reference entry: The Joy-Con 2 mouse sensor and pointer. docs_symptoms.py was run.

Checked

  • Tests: these all pass:

    • make -C dm/host/tests GAME=0100000000010000 run
    • t_hid, now 201 checks: the states and their order, none before a Joy-Con reports, the pointer handles and their order (also while lifted), the pointer state, the Joy-Con 2 style beside JoyDual, and none for a plain pair
    • t_switch2, now 117 checks: the counters, their wrap and the surface
    • make -C dm/gfx/tests GAME=0100000000010000 run
    • test_docs.py, env_switches.py --check
    • lint_c.py tidy (LLVM 23.1.5): no new findings
  • Games run: Nintendo Switch 2 Welcome Tour (040057C02159A000, v262144), with a left and a right Joy-Con 2. The cursor follows either Joy-Con on the desk, the one last used takes it, R clicks, and the mouse exhibits play.

  • Not checked:

    • other Switch 2 games' mouse modes
    • a USB mouse (none is reported, as before)
  • Devices and OS: M1 Pro, macOS 27.2.

  • regress.py: I didn't run check.

    • impact names Hades, Breath of the Wild, Pokémon Scarlet and Tears of the Kingdom, the four profiles with a usage manifest. They reach it through the hid npad calls next to the changed lines.
    • For every game, nothing changes until a front end calls dm_joy_mouse or dm_joy_attach with DM_JOY_2. With no sensor, GetJcmsStates and GetNmouseHandles still give none and the styles are as before; t_hid checks both.
    • This Mac has no game set up for the harness: regress.py list finds none, since my games are archived off it.

    If you want check on any of these, tell me which.

Front ends

Front-ends: new: dm_joy_mouse (dm_input.h) reports a Joy-Con's mouse sensor, and dm_joy_attach takes DM_JOY_2 for Joy-Con 2s; until a front end calls them, nothing changes

Docs

  • Reference entry, docs_symptoms.py run
  • No chain tool changed
  • No game files, keys or extracted game data

🤖 Generated with Claude Code

@github-actions github-actions Bot added host The Horizon host: SVCs, services, threads, memory (dm/host) packing Building an IPA or a Mac app: the NSP script, extraction, the shader split, the packers mac The Mac front end and app (dm/mac, front_mac.m) tools Tools and scripts (dm/tools, scripts) docs Documentation game: 0100000000010000 Super Mario Odyssey game: 010015100B514000 Super Mario Bros. Wonder game: 01006A800016E000 Super Smash Bros. Ultimate game: 010093801237C000 Metroid Dread labels Nov 6, 2026
@JakePre
JakePre force-pushed the switch2-pr4-mouse branch 4 times, most recently from ea0763f to d8011bc Compare November 7, 2026 19:26
JakePre and others added 4 commits November 8, 2026 13:40
…troller 2): an off-by-default setting, pairing by Sync during the game, motion, and Home through the hold-to-quit prompt (issue seedds#1267)

macOS lists these controllers neither as GameController nor as HID devices: they are Bluetooth LE devices with Nintendo's own GATT service, and a Joy-Con 2 forgets a connection the moment it drops unless it is bonded to the host. switch2_proto.h is the protocol in pure C (advertisement and what the host does with it, command framing, bonding, the input report, stick calibration, the motion maps; tests/t_switch2.c, 61 checks on recorded reports and replies). pad_switch2_apple.m is one CoreBluetooth transport for the Mac and iOS front ends, as haptics_apple.m is; the front end gives each pad a player slot through dm_switch2_front. The Mac-only address lookup is behind TARGET_OS_OSX; where the host's own address cannot be known (iOS) a controller in pairing mode is used without bonding.

Settings row DM_SW2, off by default: with it off no Bluetooth client is created, so no permission prompt appears. With it on, a controller held in Sync advertises no host and the game bonds it with the console's 0x15 commands (this Mac's address and two fresh random keys; the console's constants are not needed), and it keeps playing on the same link; from then on a button press wakes it toward this Mac and it connects without Sync. A controller advertising another host (a Switch 2 nearby) is never written to. A Pro Controller 2, or a left and a right Joy-Con 2 together, is one Pro-style pad with motion as the player's six-axis sensor; one Joy-Con alone is a sideways pad. Home, A and B take part in the hold-to-quit prompt.

Checked on an M1 Pro, macOS 27.2, with a left and a right Joy-Con 2: Metroid Dread 2.1.0 (both woke and played as one pair, a Joy-Con held in Sync during the game bonded and kept playing, Home hold and A quit; a Joy-Con paired back to a Switch 2 beside the Mac woke for it and was left alone until Sync paired it with the Mac again; macOS asked for Bluetooth on the first start of a freshly packed app and not on the second) and Super Mario Odyssey 1.3.0 (motion: the cap throw by shaking). The Pro Controller 2 is untested. Protocol facts are from the community's notes (switch2mac, ndeadly/switch2_controller_research, trevlars/switch2-controllers-linux, Nadeflore/switch2-controllers); the code was written from them and copies none.

Motion: the gyro scale is measured (0.0607 degrees per second per count: 14 tilts edge to wall, 90 degrees by the accelerometer, read 1.153 times as much at the nominal 0.070, on a left and a right Joy-Con 2); each sample is timed by the controller's millisecond clock (the report's first four bytes) instead of its arrival, so a burst of notifications or a lost report no longer bends the integration (s2_clock_sample); and the gyro offset is there from the first sample: the one learned for that controller before (kept under its serial number), or a right Joy-Con 2's factory offset at 0x13040, which matches its rest offset.

Front-ends: new: DM_SW2=1 (settings row, off by default) turns on Switch 2 controllers over Bluetooth LE; the Mac app's Info.plist needs NSBluetoothAlwaysUsageDescription (pack_app.py writes it), without which the host stays off

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dles, coded as the console codes it: logarithmic frequency codes, the sample count, each of the game's values in its own 5 ms sample (2 of 3, issue seedds#1267)

The game's vibration of a player's two full-key motors reaches a Switch 2 pad through haptics_apple.m: dm_haptics_apple_set_sink gives the player an output of its own instead of a Core Haptics engine, on the rumble queue, with DM_RUMBLE_GAIN and DM_RUMBLE_MIN applied as the engines get them, and silence on a suspension or a route change. switch2_proto.h s2_rumble_pick says which motor a controller plays: each Joy-Con of a pair its side, a Joy-Con alone the stronger of the two in each band, a Pro Controller 2 one per grip. No npad change: the existing full-key and handheld vibration handles drive it.

The report to a controller's vibration channel (the right Joy-Con 2's FA19B0FB-CD1F-46A7-84A1-BBB09E00C149, the left one's 289326CB-A471-485D-A8F4-240C14F18241) is a zero byte and a 16-byte block per actuator: 0x40 | the number of samples << 4 | a sequence, then up to three five-byte samples, played in turn. A sample is two bands, low then high, each a 10-bit frequency code and a 10-bit amplitude. The frequency code is logarithmic, 96 steps an octave with 333 at 110 Hz (9.935 Hz to 16034 Hz); the amplitude is linear, 1023 at full; both bands together are limited to 0.98 (s2_rumble_cap). Hertz and amplitude are rounded as the console rounds them: truncated, then of that code and the next the one that decodes nearer. Checked against the Switch 2's own encoder on 200,000 random values (one to three samples, 5 Hz to 3 kHz, amplitudes past full scale too): no byte differed.

The game sets its vibration about every 5 ms and holds each value until the next, so a value set for one step is a 5 ms pulse (a bounce, a click). Each value plays once, in the first 5 ms sample that ends after it was set (switch2_proto.h, Rumble timing: s2_rumble_log, s2_rumble_slot), three samples to a packet every 15 ms; a sample with no new value repeats the one before, and values that pile up wait at most five samples. That is how the console plays them: one sample per value, in order. Holding a value until the next packet instead stretched each one-step pulse to two or three samples: replayed against a recording of the game's values in Nintendo Switch 2 Welcome Tour's bead and rubber-ball exhibits (892 non-zero values over 8 minutes), the energy per Joy-Con came to 1.8 and 2.2 times the console's, and the pulses averaged 2 samples instead of 1. With the slots it is 1.00, with the same 667 and 1,142 pulses, each one sample long. A buzz ends with three packets of silence, since one write without response can be lost. DM_SW2_VERBOSE logs the tick's spacing and the packets each second: on an M1 Pro, 67 ticks a second, the longest gap 16 ms, no write refused for a full buffer.

The console also corrects each band's amplitude by its frequency, with tables of its own per controller model; that is not done here.

Checked on an M1 Pro, macOS 27.2, with a left and a right Joy-Con 2 in Super Mario Odyssey 1.3.0 as one pair (cap throw, ground pound, captures, hits) and in Nintendo Switch 2 Welcome Tour's vibration exhibits. t_switch2: the codes and their rounding, the 0.98 limit, the block layout with its count, which motor each controller plays, and the slots (a one-step pulse is one sample, a value for the other motor takes no slot, two values in a slot, a pile-up, a late tick).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sor per Joy-Con, splits when the game asks, and the Switch 2 Joy-Cons choose by a three-second hold (3 of 3, issue seedds#1267)

A front end that reads Joy-Cons one by one reports them to the npad layer (dm_input.h: dm_joy_attach, dm_joy_set, dm_motion_set_joy, dm_motion_clear_joy; no controller-specific names, so Switch 1 Joy-Cons and seedds#585 / seedds#878 can feed it later). Nothing changes until one does. hle_hid.c then shows the game what it asks for, from the styles it supports (SetSupportedNpadStyleSet, recorded before and never consulted): docked, a pad holding both Joy-Cons is one JoyDual npad (both Joy-Cons' buttons, rail buttons included, which fill no longer masks away for these styles; each stick on its side; a six-axis handle per Joy-Con); on single assignment (SetNpadJoyAssignmentModeSingle and its forms, SetNpadJoyAssignmentModeDual, GetNpadJoyAssignment) the left Joy-Con stays on the npad, or the one the game named, and the other takes the lowest free supported npad; one Joy-Con alone is JoyLeft or JoyRight. The controller applet splits a pair when the game wants more players than there are controllers. A game that supports no Joy-Con style reads the pad as a full-key controller, as before. Vibration handles exist for the Joy-Con styles (types 5, 6 and 7) and reach the motor of their Joy-Con on whichever npad it is; a full-key handle the game got before the Joy-Cons connected still reaches them (Super Mario Odyssey keeps sending to its full-key handles after asking for the JoyDual ones). Handheld mode: player 1's Joy-Cons count as attached to the console (the Handheld npad) and are never split.

The Switch 2 transport (pad_switch2_apple.m) reports each Joy-Con's own buttons, stick and sensor (not while the game has no input: the front end's new blocked callback), keeps a left and a right one that connect together as a pair, and lets the player choose on the controllers, as a Switch's Change Grip/Order screen does, without its screen: SL + SR held three seconds on a Joy-Con of a pair gives it a player of its own; L on a left one alone and R on a right one alone, held together three seconds, pairs them (switch2_proto.h s2_gesture, s2_hold_update). The player lights show the result. DM_PAD_MERGE keeps every pad full-key.

Checked on an M1 Pro, macOS 27.2, with a left and a right Joy-Con 2: Super Mario Odyssey 1.3.0 (a JoyDual npad: the two-handed up and spin throws; two-player mode splits the pair through the applet; rumble on both; the split and join holds), Super Mario Bros. Wonder 1.2.1 (choosing two players splits the pair), Super Smash Bros. Ultimate 13.0.5 (the SL + SR hold makes two sideways players; a 1v1 with a Joy-Con each, rail buttons as shoulders, rumble on each). t_hid: 175 checks (t_joycons: inert until a Joy-Con is reported, JoyDual with rail buttons, the unsupported-style fallback, single assignment and the other half's npad, the applet split, vibration handles per style, handheld); t_switch2: 80 checks (the Joy-Con buttons, the gestures and their timing). Not checked: more than two Joy-Cons, the Pro Controller 2.

Front-ends: new: dm_joy_attach / dm_joy_set / dm_motion_set_joy / dm_motion_clear_joy (dm_input.h) let a front end report Joy-Cons one by one; until it does, nothing changes
Front-ends: interface: dm_switch2_front has a blocked callback (NULL: never blocked)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y-Con's sensor (dm_joy_mouse), the game reads it as JcmsStates and as an Nmouse pointer, and a Joy-Con 2 pad shows the Joy-Con 2 npad styles (issue seedds#1267, after seedds#1363)

A Switch 2 game's mouse mode did nothing with Joy-Con 2s: the host answered as having no sensor (GetJcmsStates none, GetNmouseHandles none), and a Joy-Con 2 pad had only the plain Joy-Con styles, while a Switch 2 game can support only their Joy-Con 2 forms. Nintendo Switch 2 Welcome Tour's mouse exhibits support only style 0x1000 and test a pair with 0x1004, a left Joy-Con with 0x2008 and a right one with 0x4000 beside JoyRight: so bits 12, 13 and 14 are taken as the Joy-Con 2 forms of JoyDual, JoyLeft and JoyRight (inferred, not documented).

The npad layer (hle_hid.c, dm_input.h), neutral as the rest of the Joy-Con API:
- dm_joy_mouse(pad, side, dx, dy, state): a Joy-Con's movement since its last report and whether it is connected and on a surface. Nothing changes for a Joy-Con that never calls it.
- dm_joy_attach takes DM_JOY_2: the pad's npad then shows the Joy-Con 2 style beside the plain one, to a game that supports it.
- GetJcmsStates / GetJcmsState read a ring of 16 states per player and Joy-Con. A JcmsState as Welcome Tour reads it (main+0x253540): 8 bytes it does not read, the sampling number at 8 (newest first), the movement at 0x10 and 0x14, attributes at 0x18 (bit 0 connected, bit 1 on a surface). JcmsType 0 is the left Joy-Con, 1 the right.
- Nmouse: a handle per Joy-Con whose sensor reports, the one last used on a surface first and kept first while lifted (Welcome Tour moves its pointer by the first handle only). An NmouseState (main+0x253b2c) is 48 bytes: 8 bytes, the Switch's MouseState fields (sampling number, position on the game's pointer range, movement, wheel), buttons at 0x28 (left 1 is R / ZR on a right Joy-Con, L / ZL on a left one; right 2), attributes at 0x2c (bit 1 connected, which the game requires). SetNmousePointerPosition moves it; the movement is a quarter of the sensor's counts. GetMousePointerSpeedScale answers the default, 1.
- DM_JCMS_LOG=1 logs the game's sensor and pointer calls, at most once a second each kind.

The Switch 2 transport turns the sensor on with its own feature bit, 0x10 (S2_FEATURE_MOUSE; not for the Pro Controller 2, which has none). Measured on a right Joy-Con 2 slid on a desk: report bytes 0x10 and 0x12 are 16-bit position counters that wrap (x right, y toward the player); 0x14 and 0x16 jump from about 1,600 and 150 on a surface to about 4,600 and 3,200 when lifted, and 0x16 under 1,000 is taken as on a surface. While lifted no movement is reported, since the counters drift with nothing under them.

Checked on an M1 Pro, macOS 27.2, with a left and a right Joy-Con 2 in Nintendo Switch 2 Welcome Tour (040057C02159A000, v262144; its profile and host fixes are their own pull request): the cursor follows either Joy-Con on the desk, the one last used takes it, R clicks, and the mouse exhibits play. t_hid: the states and their order, none before a Joy-Con reports, the pointer handles and their order (also while lifted), the pointer state, the Joy-Con 2 style beside JoyDual and none for a plain pair. t_switch2: the counters, their wrap and the surface.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
seedds added a commit that referenced this pull request Nov 9, 2026
…y-Con's sensor (dm_joy_mouse), the game reads it as JcmsStates and as an Nmouse pointer, and a Joy-Con 2 pad shows the Joy-Con 2 npad styles (issue #1267, after #1363)

A Switch 2 game's mouse mode did nothing with Joy-Con 2s: the host answered as having no sensor (GetJcmsStates none, GetNmouseHandles none), and a Joy-Con 2 pad had only the plain Joy-Con styles, while a Switch 2 game can support only their Joy-Con 2 forms. Nintendo Switch 2 Welcome Tour's mouse exhibits support only style 0x1000 and test a pair with 0x1004, a left Joy-Con with 0x2008 and a right one with 0x4000 beside JoyRight: so bits 12, 13 and 14 are taken as the Joy-Con 2 forms of JoyDual, JoyLeft and JoyRight (inferred, not documented).

The npad layer (hle_hid.c, dm_input.h), neutral as the rest of the Joy-Con API:
- dm_joy_mouse(pad, side, dx, dy, state): a Joy-Con's movement since its last report and whether it is connected and on a surface. Nothing changes for a Joy-Con that never calls it.
- dm_joy_attach takes DM_JOY_2: the pad's npad then shows the Joy-Con 2 style beside the plain one, to a game that supports it.
- GetJcmsStates / GetJcmsState read a ring of 16 states per player and Joy-Con. A JcmsState as Welcome Tour reads it (main+0x253540): 8 bytes it does not read, the sampling number at 8 (newest first), the movement at 0x10 and 0x14, attributes at 0x18 (bit 0 connected, bit 1 on a surface). JcmsType 0 is the left Joy-Con, 1 the right.
- Nmouse: a handle per Joy-Con whose sensor reports, the one last used on a surface first and kept first while lifted (Welcome Tour moves its pointer by the first handle only). An NmouseState (main+0x253b2c) is 48 bytes: 8 bytes, the Switch's MouseState fields (sampling number, position on the game's pointer range, movement, wheel), buttons at 0x28 (left 1 is R / ZR on a right Joy-Con, L / ZL on a left one; right 2), attributes at 0x2c (bit 1 connected, which the game requires). SetNmousePointerPosition moves it; the movement is a quarter of the sensor's counts. GetMousePointerSpeedScale answers the default, 1.
- DM_JCMS_LOG=1 logs the game's sensor and pointer calls, at most once a second each kind.

The Switch 2 transport turns the sensor on with its own feature bit, 0x10 (S2_FEATURE_MOUSE; not for the Pro Controller 2, which has none), only once the game has set up the sensors or the pointer (InitializeJcms / InitializeNmouse: dm_joy_mouse_wanted), as the console turns it on only for a game that uses it; a controller that connected before gets the two feature commands again with the bit, and keeps it while connected. Measured on a right Joy-Con 2 slid on a desk: report bytes 0x10 and 0x12 are 16-bit position counters that wrap (x right, y toward the player); 0x14 and 0x16 jump from about 1,600 and 150 on a surface to about 4,600 and 3,200 when lifted, and 0x16 under 1,000 is taken as on a surface. While lifted no movement is reported, since the counters drift with nothing under them.

Checked on an M1 Pro, macOS 27.2, with a left and a right Joy-Con 2 in Nintendo Switch 2 Welcome Tour (040057C02159A000, v262144; its profile and host fixes are their own pull request): the cursor follows either Joy-Con on the desk, the one last used takes it, R clicks, and the mouse exhibits play. t_hid: the states and their order, none before a Joy-Con reports, the pointer handles and their order (also while lifted), the pointer state, the Joy-Con 2 style beside JoyDual and none for a plain pair. t_switch2: the counters, their wrap and the surface.

Front-ends: new: dm_joy_mouse (dm_input.h) reports a Joy-Con's mouse sensor, dm_joy_mouse_wanted says whether the game has set the sensors up (turn a sensor on only then), and dm_joy_attach takes DM_JOY_2 for Joy-Con 2s; until a front end calls them, nothing changes

PR #1426

Co-authored-by: JakePre <130175182+JakePre@users.noreply.github.com>
@seedds

seedds commented Nov 9, 2026

Copy link
Copy Markdown
Owner

Landed as 413e880, with the mouse sensor turned on only once the game calls InitializeJcms or InitializeNmouse (dm_joy_mouse_wanted). Turning it on for a Joy-Con that connected earlier hasn't run on hardware yet; see #1267. #1425 now needs a rebase: main already has GetJcmsState. Thanks!

@seedds seedds closed this Nov 9, 2026
Know-It-All-Brother pushed a commit to Know-It-All-Brother/nxnative that referenced this pull request Nov 9, 2026
…y-Con's sensor (dm_joy_mouse), the game reads it as JcmsStates and as an Nmouse pointer, and a Joy-Con 2 pad shows the Joy-Con 2 npad styles (issue seedds#1267, after seedds#1363)

A Switch 2 game's mouse mode did nothing with Joy-Con 2s: the host answered as having no sensor (GetJcmsStates none, GetNmouseHandles none), and a Joy-Con 2 pad had only the plain Joy-Con styles, while a Switch 2 game can support only their Joy-Con 2 forms. Nintendo Switch 2 Welcome Tour's mouse exhibits support only style 0x1000 and test a pair with 0x1004, a left Joy-Con with 0x2008 and a right one with 0x4000 beside JoyRight: so bits 12, 13 and 14 are taken as the Joy-Con 2 forms of JoyDual, JoyLeft and JoyRight (inferred, not documented).

The npad layer (hle_hid.c, dm_input.h), neutral as the rest of the Joy-Con API:
- dm_joy_mouse(pad, side, dx, dy, state): a Joy-Con's movement since its last report and whether it is connected and on a surface. Nothing changes for a Joy-Con that never calls it.
- dm_joy_attach takes DM_JOY_2: the pad's npad then shows the Joy-Con 2 style beside the plain one, to a game that supports it.
- GetJcmsStates / GetJcmsState read a ring of 16 states per player and Joy-Con. A JcmsState as Welcome Tour reads it (main+0x253540): 8 bytes it does not read, the sampling number at 8 (newest first), the movement at 0x10 and 0x14, attributes at 0x18 (bit 0 connected, bit 1 on a surface). JcmsType 0 is the left Joy-Con, 1 the right.
- Nmouse: a handle per Joy-Con whose sensor reports, the one last used on a surface first and kept first while lifted (Welcome Tour moves its pointer by the first handle only). An NmouseState (main+0x253b2c) is 48 bytes: 8 bytes, the Switch's MouseState fields (sampling number, position on the game's pointer range, movement, wheel), buttons at 0x28 (left 1 is R / ZR on a right Joy-Con, L / ZL on a left one; right 2), attributes at 0x2c (bit 1 connected, which the game requires). SetNmousePointerPosition moves it; the movement is a quarter of the sensor's counts. GetMousePointerSpeedScale answers the default, 1.
- DM_JCMS_LOG=1 logs the game's sensor and pointer calls, at most once a second each kind.

The Switch 2 transport turns the sensor on with its own feature bit, 0x10 (S2_FEATURE_MOUSE; not for the Pro Controller 2, which has none), only once the game has set up the sensors or the pointer (InitializeJcms / InitializeNmouse: dm_joy_mouse_wanted), as the console turns it on only for a game that uses it; a controller that connected before gets the two feature commands again with the bit, and keeps it while connected. Measured on a right Joy-Con 2 slid on a desk: report bytes 0x10 and 0x12 are 16-bit position counters that wrap (x right, y toward the player); 0x14 and 0x16 jump from about 1,600 and 150 on a surface to about 4,600 and 3,200 when lifted, and 0x16 under 1,000 is taken as on a surface. While lifted no movement is reported, since the counters drift with nothing under them.

Checked on an M1 Pro, macOS 27.2, with a left and a right Joy-Con 2 in Nintendo Switch 2 Welcome Tour (040057C02159A000, v262144; its profile and host fixes are their own pull request): the cursor follows either Joy-Con on the desk, the one last used takes it, R clicks, and the mouse exhibits play. t_hid: the states and their order, none before a Joy-Con reports, the pointer handles and their order (also while lifted), the pointer state, the Joy-Con 2 style beside JoyDual and none for a plain pair. t_switch2: the counters, their wrap and the surface.

Front-ends: new: dm_joy_mouse (dm_input.h) reports a Joy-Con's mouse sensor, dm_joy_mouse_wanted says whether the game has set the sensors up (turn a sensor on only then), and dm_joy_attach takes DM_JOY_2 for Joy-Con 2s; until a front end calls them, nothing changes

PR seedds#1426

Co-authored-by: JakePre <130175182+JakePre@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation game: 0100000000010000 Super Mario Odyssey game: 01006A800016E000 Super Smash Bros. Ultimate game: 010015100B514000 Super Mario Bros. Wonder game: 010093801237C000 Metroid Dread host The Horizon host: SVCs, services, threads, memory (dm/host) mac The Mac front end and app (dm/mac, front_mac.m) packing Building an IPA or a Mac app: the NSP script, extraction, the shader split, the packers tools Tools and scripts (dm/tools, scripts)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants