Skip to content

Mac: one Pro Controller is one pad (IOHIDManager hands a device over once per matching dictionary) - #1722

Closed
LudovicLemarinel wants to merge 1 commit into
seedds:mainfrom
LudovicLemarinel:hid-one-pad-per-registry-entry
Closed

LudovicLemarinel wants to merge 1 commit into
seedds:mainfrom
LudovicLemarinel:hid-one-pad-per-registry-entry

Conversation

@LudovicLemarinel

Copy link
Copy Markdown
Contributor

Problem

With one Switch Pro Controller connected to the Mac over Bluetooth, the host reads it as two players. The log shows the same pad taken twice, at the same USB location:

[hid] 'Pro Controller' (057e:2009) at USB location 0x5879dc38: GameController lists 1 of 2 pads of this model: read here, from its report 0x30
controller 'Pro Controller' (HID) connected: player 1 (npad 0)
[hid] 'Pro Controller' (057e:2009) at USB location 0x5879dc38: GameController lists 1 of 2 pads of this model: read here, from its report 0x30
controller 'Pro Controller' (HID) connected: player 2 (npad 1)

A one-player game then sees more controllers than it asked for, so it opens the controller applet. The applet closes and the game opens it again. In a session of about 9 minutes of The Legend of Zelda: Ocarina of Time (0400486024DEC000), this happened 2,286 times, each time with a focus loss:

oe library applet: controller support
oe focus state → 2 (library applet open)
hid controller applet: 1 to 1 players wanted, 2 connected
oe focus state → 1 (library applet closed)

Effect on the game. Ocarina of Time suspends its music while an applet has focus, as games do under the HOME menu. As a result:

  • No area or cutscene music. Kokiri Forest gameplay, its cutscenes and other scenes were silent. With file logging on (DM_FS=1), the game never opens BGM_KokiriForest.bwav, BGM_Kokiri_* or BGM_Dm_Kokiri_*, although it loads the music table (bgm.Product.111.byml.zs), BGM_Static.bars.zs and the area-music actors.
  • Other sound kept playing. Sound effects, ambience, voices and the positional shop music (BGM_Shop_KokiriForest.bwav, played from a sound locator) worked.
  • Ruled out on the audio side. A temporary tracer on the renderer's voice calls ruled out the audio path: no muted voice, no paused voice, no zero volume, no wrong splitter routing, and no buffers of zeros. Every voice decoded to WAV held sound effects and ambience only.

The applet loop by itself also costs every one-player game a focus change every few frames, whatever it does with its sound.

Cause

IOHIDManager hands a device over once per matching dictionary it meets, and each time as a new IOHIDDeviceRef for the same I/O Registry entry.

Since #1595 (identical pads that GameController lists fewer of than IOKit has), hidpad_start matches three dictionaries:

  • the GamePad usage;
  • the Joystick usage;
  • vendor 057E / product 2009, so that every Pro Controller is counted whatever usage it declares.

A Pro Controller declares the GamePad usage, so it meets two of them. A small IOKit program using the same matching shows it:

0x101564d00 entry 0x100010cfb IOHIDUserDevice 'Pro Controller' loc 1484381240 usage 1/5 transport Bluetooth
0x10156cb20 entry 0x100010cfb IOHIDUserDevice 'Pro Controller' loc 1484381240 usage 1/5 transport Bluetooth

There are two references and one registry entry (0x100010cfb). hidpad_found recognised a known device only by reference (d->hid == hid), so it stored the pad twice. Two steps then followed:

  • Two pads counted. hidpad_model_count found 2 Pro Controllers in IOKit against GameController's 1.
  • Both read as twins. pad_dupes_want concluded that GameController hid one of them, so both references were read from report 0x30 as twins, one player each, and GameController's own controller was left unread.

Fix

pad_hid_mac.h now identifies a device by its I/O Registry entry ID (IORegistryEntryGetRegistryEntryID on IOHIDDeviceGetService), through hidpad_entry. hidpad_found ignores a reference whose entry is already in g_hidpads, g_hidpads_waiting or g_hidpads_gc.

This is a general fix at the source, not a game-specific rule:

  • Where it acts. The duplicate never enters the lists, so the twin count, the GameController hand-back and the removal path all see one pad per physical device again.
  • Removal. Only the stored reference is linked in the lists. When the pad disconnects, the removal callback for the ignored reference finds nothing to unlink, and the one for the stored reference cleans up as before.
  • Two identical pads. Two real identical pads have two registry entries, so Mac: identical pads that GameController lists as one are each a player of their own (issue #1218) #1595's case (two Pro Controllers over USB) is unchanged.

The finding is recorded as a new Input entry, One Pro Controller read as two players, in docs/reference/services.md. It is also in the symptom index, regenerated with dm/tools/docs_symptoms.py, and under "Found on this game" in Ocarina of Time's note.

Testing

  • On hardware. Ocarina of Time on an M-series Mac, macOS 27, one Pro Controller over Bluetooth:
    • Before. Players 1 and 2 on the same pad, 2,286 controller applet openings, no music in Kokiri Forest.
    • After. One controller 'Pro Controller' connected: player 1 (npad 0), with motion (gyro) line, no controller applet, and music in gameplay, confirmed by ear.
  • Unit tests. t_hid, t_pad_hid, t_pad_apple, t_pad_pro and t_motion_pro_mac pass. No unit test includes pad_hid_mac.h: it is compiled only in front_mac.m.
  • Docs. dm/tools/tests/test_docs.py passes.
  • Regression. regress.py impact origin/main..HEAD reports no affected game, and pad_hid_mac.h is not in impact_map.tsv. The change only concerns how the Mac front end enumerates physical controllers, which the headless regression runs do not have, so regress.py check --games was not run.
  • Not run. Two Pro Controllers over USB, the Mac: identical pads that GameController lists as one are each a player of their own (issue #1218) #1595 case, was not tested with this change.

🤖 Generated with Claude Code

…once per matching dictionary it meets, so the HID reader knows a pad by its I/O Registry entry, not by its IOHIDDeviceRef

A Pro Controller meets both the GamePad usage dictionary and the vendor / product one added for identical pads (PR seedds#1595), and each match came as a new
IOHIDDeviceRef for the same registry entry. hidpad_found compared references only, so one pad counted as two of its model against GameController's one
controller: both were read as twins, players 1 and 2. A one-player game then opened the controller applet every few frames, and The Legend of Zelda:
Ocarina of Time, which suspends its music while an applet has focus, played no area or cutscene music.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added host The Horizon host: SVCs, services, threads, memory (dm/host) docs Documentation game: 0400486024DEC000 A game labels Nov 7, 2026
seedds added a commit that referenced this pull request Nov 8, 2026
…once per matching dictionary it meets, so the HID reader knows a pad by its I/O Registry entry, not by its IOHIDDeviceRef (PR #1722)

A Pro Controller meets both the GamePad usage dictionary and the vendor / product one added for identical pads (PR #1595), and each match came as a new
IOHIDDeviceRef for the same registry entry. hidpad_found compared references only, so one pad counted as two of its model against GameController's one
controller: both were read as twins, players 1 and 2. A one-player game then opened the controller applet every few frames, and The Legend of Zelda:
Ocarina of Time, which suspends its music while an applet has focus, played no area or cutscene music.

Co-authored-by: Ludovic LEMARINEL - Adesin.fr <10656218+LudovicLemarinel@users.noreply.github.com>
@seedds

seedds commented Nov 8, 2026

Copy link
Copy Markdown
Owner

Landed unchanged as a00f03b (only the Ocarina note's "Found on this game" list needed a merge with lines added on main since), with you credited as co-author. Thanks — a clean root-cause fix: knowing a pad by its registry entry holds for any overlap among the matching dictionaries, not only the Pro Controller's. Still untested with this change: two Pro Controllers over USB (the #1595 case); if you have two, a run would close that gap.

@seedds seedds closed this Nov 8, 2026
dandelion711 pushed a commit to dandelion711/nxnative that referenced this pull request Nov 8, 2026
…once per matching dictionary it meets, so the HID reader knows a pad by its I/O Registry entry, not by its IOHIDDeviceRef (PR seedds#1722)

A Pro Controller meets both the GamePad usage dictionary and the vendor / product one added for identical pads (PR seedds#1595), and each match came as a new
IOHIDDeviceRef for the same registry entry. hidpad_found compared references only, so one pad counted as two of its model against GameController's one
controller: both were read as twins, players 1 and 2. A one-player game then opened the controller applet every few frames, and The Legend of Zelda:
Ocarina of Time, which suspends its music while an applet has focus, played no area or cutscene music.

Co-authored-by: Ludovic LEMARINEL - Adesin.fr <10656218+LudovicLemarinel@users.noreply.github.com>
Know-It-All-Brother pushed a commit to Know-It-All-Brother/nxnative that referenced this pull request Nov 9, 2026
…once per matching dictionary it meets, so the HID reader knows a pad by its I/O Registry entry, not by its IOHIDDeviceRef (PR seedds#1722)

A Pro Controller meets both the GamePad usage dictionary and the vendor / product one added for identical pads (PR seedds#1595), and each match came as a new
IOHIDDeviceRef for the same registry entry. hidpad_found compared references only, so one pad counted as two of its model against GameController's one
controller: both were read as twins, players 1 and 2. A one-player game then opened the controller applet every few frames, and The Legend of Zelda:
Ocarina of Time, which suspends its music while an applet has focus, played no area or cutscene music.

Co-authored-by: Ludovic LEMARINEL - Adesin.fr <10656218+LudovicLemarinel@users.noreply.github.com>
Fasen24-AI added a commit to Fasen24-AI/swordboiMacNative that referenced this pull request Nov 10, 2026
…ands a device over once per matching dictionary)

Upstream pull requests applied in this commit (their authors' work, brought onto this tree with a three-way merge; the description is in each PR):
- seedds#1722 by LudovicLemarinel (closed): Mac: one Pro Controller is one pad (IOHIDManager hands a device over once per matching dictionary)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation game: 0400486024DEC000 A game host The Horizon host: SVCs, services, threads, memory (dm/host)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants