Skip to content

Switch 2 controllers: a Controller menu and panel in the game window test a stick's drift and save its calibration in the controller - #2129

Open
UserDerezzed wants to merge 3 commits into
seedds:mainfrom
UserDerezzed:sw2-stick-calibration-pr
Open

UserDerezzed wants to merge 3 commits into
seedds:mainfrom
UserDerezzed:sw2-stick-calibration-pr

Conversation

@UserDerezzed

@UserDerezzed UserDerezzed commented Nov 10, 2026 •

Copy link
Copy Markdown

Both sticks have been calibrated and saved on one Pro Controller 2. A Joy-Con 2 and a controller calibrated on a console have not been tried (see the right stick question below). Suggestions, a different approach or a different shape for any of it are welcome.

What and why

Switch 2 controllers drift, and a Pro Controller 2's left stick drifting makes Link creep in Ocarina of Time (0400486024DEC000). The host already raises the dead zone (DM_SW2_DEADZONE), but that only hides drift. On a console, the fix is to recalibrate the stick in System Settings. Without a console, there was no way to do that.

This adds a Controller menu to the game window's menu bar, between Cheats and Window:

  • Show Controller Window (⌘⇧P), then one line per connected Switch 2 controller (kind, serial's last digits, player). With Switch 2 controllers off, the menu says where to turn them on.
  • The panel shows the stick live (whole range and a 12× view of the centre with the square dead zone). It marks where the stick comes to rest after each release and keeps a tally of releases inside and past the dead zone.
  • Dead zone slider: it changes the game's dead zone at once and keeps it for the next launch, in the same launcher.DM_SW2_DEADZONE setting as the launcher's Stick dead zone row.
  • Guided calibration: let go, then the edges, then review (old against new centre and travel) and save. Save writes the stick's centre and travel into the controller as its user calibration, as the console's calibration does. The game uses the new calibration at once, with no restart.
  • Input while open: the game gets no Switch 2 input while the panel is open, so the circles drawn for calibration don't move the player.

The write (dm/host/switch2_ops_apple.m, behind a small S2Commander protocol so it runs against a fake controller in t_switch2_ops):

  1. It backs up the 4 KB user calibration sector (0x1FC000) to a file named after the controller's serial.
  2. It erases the sector with subcommand 0x03 and checks that it reads blank.
  3. It writes with 0x05 and reads the sector back.
  4. On a mismatch it puts the backup back.

Writes below 0x1F5000 are never sent. A memory read is taken only when its reply carries that read's length (byte 8) and address (bytes 12-15), so a late reply can't stand in for another read's data. A Pro Controller 2 doesn't echo the request's 0x7E at byte 9, so that byte isn't compared. Quitting (⌘Q, the Home prompt, closing the window) waits for a running save, up to 30 s.

DM_SW2_CALIBRATE=1 writes the released stick's rest at connect without the panel. DM_CONTROLLER_WINDOW=1 opens the panel at launch.

Both sticks of a Pro Controller 2 can be saved: the left at 0x1FC040, the right at 0x1FC080. At each connection the log shows which stick blocks (0x1FC040, 0x1FC060, 0x1FC080) carry the marker, with their bytes. This only reads.

Open questions:

  • Right stick address: this PR moves the right stick's user calibration from 0x1FC062, which main reads, to 0x1FC080 / 0x1FC082, where SDL (SDL_hidapi_switch2.c) and Linux (hid-nintendo.c) read it. Main's 0x1FC062 came with no source. No controller calibrated on a console has been seen yet to confirm either. The one tested here had never been calibrated on a console: its whole 4 KB block was 0xFF before the first save. A dump from a controller calibrated on a Switch 2 would settle it.
  • Read-back: it compares through the same read path, so it can't tell a bad write from a bad read.
  • Where it lives: the panel lives in the game window rather than the launcher because the game already holds the controller's Bluetooth link. A first version in the launcher needed a second Bluetooth client and a hand-over at Play.

Checked

  • Tests: make -C dm/host/tests GAME=0400486024DEC000 build/t_switch2 build/t_switch2_ops: 173 and 97 checks pass. They cover the payloads, the reply check, release markers, reach and recentre, and the whole save sequence for both stick slots against a fake controller, including late replies, failed erases and restores. make -C dm/host GAME=0400486024DEC000 dmhost builds. env_switches.py --check reports only the three switches it already reports on main.
  • Played: Ocarina of Time (0400486024DEC000, v131072) in the Mac app with a Pro Controller 2.
    • The menu, the panel, the live stick, the release markers and the dead zone slider work.
    • Left stick: several panel saves were written and read back, and the game used each one at once. DM_SW2_CALIBRATE=1 also wrote and read back.
    • Right stick:
      • The first save into an empty 0x1FC080 was written straight.
      • The second went through erase and write-back, with the left stick's block kept.
      • Both read back, and the game used them.
    • Each save logs a few command 0x02 timed out reads, which the retries absorb.
    • The first hardware run found the reply check too strict (byte 9, above). That is fixed here.
    • The quit wait and a controller disconnecting mid-save have not been tried.
  • Device: M3 Max, macOS 27.2.
  • Not checked: Joy-Con 2, iOS (the panel is Mac only; the host change builds there), fullscreen, a controller calibrated on a console.

Docs

  • docs/settings.md (Controller menu and panel, Stick dead zone), docs/reference/services.md, docs/playing.md, docs/env_switches.tsv, dm/tools/impact_map.tsv
  • A chain tool changed: n/a
  • No game files, keys or data extracted from a game in the change

@github-actions github-actions Bot added host The Horizon host: SVCs, services, threads, memory (dm/host) mac The Mac front end and app (dm/mac, front_mac.m) tools Tools and scripts (dm/tools, scripts) docs Documentation labels Nov 10, 2026
@UserDerezzed
UserDerezzed marked this pull request as ready for review November 10, 2026 09:39
…test a stick's drift and save its calibration in the controller

The game window's menu bar gets a Controller menu between Cheats and Window: Show Controller Window (Cmd-Shift-P) and a line for each connected Switch 2 controller. The panel shows a stick live, with markers where it comes to rest after each release and a tally of releases inside and past the dead zone. It has a dead zone slider that changes the game's dead zone at once and keeps it for the next launch (launcher.DM_SW2_DEADZONE), and a guided calibration (let go, edges, review and save) that writes the stick's centre and travel into the controller as its user calibration, as the console's calibration does. While the panel is open the game gets no Switch 2 input.

The write (switch2_ops_apple.m) backs the 4 KB user calibration sector up to a file first, erases, checks it reads blank, writes, reads back, and puts the backup back on a mismatch. Only the first stick slot is saved (a Pro Controller 2's left stick, a Joy-Con 2's stick): where the second slot's user calibration lives is not settled. A saved calibration applies to the game without a restart. DM_SW2_CALIBRATE=1 writes the released stick's rest at connect without the panel; DM_CONTROLLER_WINDOW=1 opens the panel at launch.
… slots (0x1FC040, 0x1FC060, 0x1FC080) carry the marker, and their bytes (read only), to settle where the second stick's lives
…ted and saved too, its user calibration read and written at 0x1FC080 as SDL and Linux read it (was read at 0x1FC062)
@UserDerezzed
UserDerezzed force-pushed the sw2-stick-calibration-pr branch from daa1fc6 to 55f5332 Compare November 14, 2026 09:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation host The Horizon host: SVCs, services, threads, memory (dm/host) mac The Mac front end and app (dm/mac, front_mac.m) tools Tools and scripts (dm/tools, scripts)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant