Skip to content

Add Steam Controller (2026) support on macOS - #221

Open
thehobbit85 wants to merge 1 commit into
seedds:mainfrom
thehobbit85:codex/steam-controller
Open

thehobbit85 wants to merge 1 commit into
seedds:mainfrom
thehobbit85:codex/steam-controller

Conversation

@thehobbit85

@thehobbit85 thehobbit85 commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

The direct Steam Controller adapter, shared Mac mappings and per-player motion and rumble remain distinct from main.

Conflict refresh — 2026-11-13 UTC

Rebased 5bd183c4e71aa97762403a63c914bd45ea17e00e onto 054eea113895c212c9ff8bf6573a7f2db7e8f6af as 17662266924e371d5b3f54ce49ef65dbd4dddbd4. The multi-program compile failure reported in review is fixed in input_preferences_mac.h. The fallback preference domain joins the selected runtime title ID with its prefix as an NSString, while the app's bundle ID still takes precedence. A regression generates a real game.py header_multi header, compiles the host-side preference code and checks the domain for both selected programs. The original literal concatenation reproduces the reported compiler error against the same generated header.

The HID integration keeps main's sensor history, interpolation, rest estimation, side freshness, IMU conversion and SDK handle forms. Direct Steam poses use an independent stream under the same selection and sampling policy. Per-player Shake remains routed with its controller and preserves the complete synthetic cycle before returning to direct Steam motion. Main's first-button admission for generic HID and its live-report counting of identical controllers remain active with the adapter's registry identity and open/close ownership. Bluetooth player synchronization and split rejection preserve the paired controller's input. The reference index was regenerated. The remaining diff and range-diff were reviewed, including the superseded older sensor-handle expectations.

Validation

On source-identical predecessor cf769509f46f0e0784c1c1049e571f2b745c6f1f (main 72ca2e09d565e8c144ad27f191948082e22464fa), the Mac host linked at the current -O2 setting with Xcode 27.2. HID and Bluetooth sources compiled against the arm64 iPhone SDK. Fourteen focused native fixtures passed. HID passed 530 checks both normally and under TSan, the HID liveness fixture passed 22 recounts, Switch 2 protocol passed 122 under ASan/UBSan, motion passed 229, and controller/Home prompts passed 32/40. The full controller and Apple haptics fixtures also passed ASan/UBSan with leak detection disabled. These include current upstream sensor and liveness checks and the retained direct-motion, ownership, focus and mapping assertions.

On that predecessor, the focused packaging, docs, environment and HMMA suite ran 95 tests, with 94 passing and one optional translator test skipped. The multi-program preference test passed, and its old-code negative control failed with the review's exact compiler error. Pinned Ruff 0.17.2 and Pyright 1.1.414 passed at 321 findings against 321 recorded. LLVM 23.1.4 checked nine affected translation units with eight baseline findings and zero new findings. The final upstream-only rebase incorporates the unified shader-stamp reader. Its range-diff is patch-identical; every host/Mac source and controller fixture is byte-identical to the tested predecessor. On immediate predecessor 5c20560f0499db7a8b95313d6bbb80d124e9091a (main 2401b288717463f81080ab20034aec9351a37fa6), the host build and all 109 packaging, chain/stamp and shader-library tests passed, covering the unified stamp reader without rebuilding game shaders. The final upstream-only replay adds main's archive-harvest drain fix. Those packaging tools and fixtures remain identical; the final host build and six documentation checks passed on 17662266924e371d5b3f54ce49ef65dbd4dddbd4. Main ancestry, source review and git diff --check passed. Remote CI on this replacement head is pending verification.

Neither Games root exposed a runnable Mac build to the regression harness. Priority games remain Hades for rumble and player APIs, Breath of the Wild and Tears of the Kingdom for motion, Mario Kart World for controller changes, and LUNAR Remastered Collection for multi-program packaging. The last is the reviewer's report; its game profile and runtime data were unavailable locally. There is no fresh gameplay, physical controller, pairing, hotplug, actuator or device result. The feature changes no production shader-chain code.

Historical proposal, validation and original commit context

The previous description follows in full. Its results apply to the revisions it records.

The direct Steam Controller adapter, shared Mac mappings and per-player motion and rumble remain distinct from main.

Conflict refresh — 2026-11-10 UTC

Rebased 892b35043bcca3a1fad390cdf5940dd774504023 onto bbea4c90f136f478e25228bc3d4ab33c77420f2e as 5bd183c4e71aa97762403a63c914bd45ea17e00e. Core Haptics keeps main's exception guard and recovery when an engine stops during a player call. The guarded apply path uses the same gain and intensity cutoff as the other Apple, Triton and sampled Bluetooth routes. The shared player registry, Joy-Con side motion and prompt release behavior retain their prior implementations. The symptom index was regenerated from the combined references.

Validation

  • The Apple Silicon Mac host linked using make -C dm/host GAME=010015100B514000 with Xcode 27.2. The changed haptics implementation compiled against the arm64 iPhone SDK. Seven current docs and environment checks passed. Targeted LLVM 23.1.4 checked eight controller, HID and haptics source and fixture units, with eight baseline findings and zero new findings.
  • On f4a54ca279965cabf8cb4d89adda752830e731a8, all focused controller, haptics and prompt targets passed. The HID fixture passed 324 checks in both plain and TSan runs, the Switch 2 protocol passed 117 under ASan/UBSan, and the controller and Home prompts passed 32 and 40. The controller and haptics fixtures passed ASan/UBSan with leak detection disabled. The haptics test covers raised Core Haptics exceptions and retries, sampled Bluetooth handoff and native output isolation. 87 of 88 focused Python tests passed with one optional translator test skipped. The host, packaging and HMMA test sources stayed byte-identical across the final base advance.
  • Pinned Ruff 0.17.2 and Pyright 1.1.414 reported 339 findings against 336 recorded. The three excess diagnostics in dmsh_build.py are identical to the exact-main run, and the last main advance changed no Python or lint configuration. Ancestry, range-diff and git diff --check passed. Remote CI on this replacement head is pending verification.

The local regression harness found no runnable Mac builds in either configured Games root. The priority coverage would be Hades for rumble and player APIs, Breath of the Wild and Tears of the Kingdom for motion aiming, Mario Kart World for the stopped-engine report, and Nintendo Switch 2 Welcome Tour for the Joy-Con 2 path. Gameplay, physical controller and actuator behavior, hotplug and device coverage remain open. The change adds no production shader code. The fixture run did not assess the ARC allocations noted by main after an engine exception.

Historical proposal, validation and original commit context

The previous description follows in full. Its results apply to the revisions it records.

The direct Steam Controller adapter, shared Mac mappings and per-player motion and rumble remain distinct from main.

Conflict refresh — 2026-11-09 UTC

Rebased 54d923a3764371ae9130a63ae9d2a3d54b9b72d9 onto 413e8808e0799eab8730688f082835aeeaca84c5 as 892b35043bcca3a1fad390cdf5940dd774504023. The Mac registry gives Bluetooth LE Switch 2 controllers the same player slots, press-to-join, Home and applet input gates as the other transports. The Bluetooth handshake can keep a controller connected while it waits for a button. A Joy-Con pair's physical split still needs an immediate slot. The shared map applies to standard gamepad input. Main's individual Joy-Con input, side motion, mouse and native Joy-Con 2 styles keep their transport paths. Sensor state and rest queries use the correct physical side for both a dual pair and its virtual split player.

Apple haptics keeps the sampled Bluetooth sink and Triton's 40 ms refresh as distinct player routes. A replacement silences the previous output and invalidates pending work. In merged mode a newly connected or idle pad leaves the current route in place, and fresh buttons transfer it between Bluetooth and native or Triton. The retained DS4 test double explicitly consumes its matching dictionary under the same contract as IOKit. The symptom index was regenerated, and the controller and front-end documentation describes the combined behavior.

Validation

  • make -C dm/host GAME=010015100B514000 linked the Mac host on Apple Silicon with Xcode 27.2. The 58 warnings are in unchanged graphics, SDK and macOS activity code. The host build used 6ed2d281, whose production, headers, Makefiles and documentation are byte-identical to the final revision. The two later changes are limited to HID and haptics fixture source.
  • Sixteen focused native controller, motion, mapping, HID, haptics and prompt targets passed. The controller registry and Apple haptics fixtures also passed with ASan/UBSan. The HID fixture passed 324 checks in plain and TSan builds, the Switch 2 protocol passed 117 under ASan/UBSan, the controller applet passed 32, and Home passed 40. Fixtures cover waiting BLE admission, full-slot split rejection, focus and modal release, merged BLE/Triton input, sampled rumble handoffs, and distinct dual/split motion and rest.
  • The focused Python suite ran 91 tests using test_docs.py, test_nsp_mac_app.py, test_env_switches.py and test_dmsh_hmma.py. 90 passed and one optional translator test skipped. The separate final docs run passed 11 of 11. LLVM 23.1.4 checked the eight affected production and fixture translation units with eight baseline findings and zero new findings. The two new fixture analyzer issues were corrected at their source. Pinned Ruff 0.17.2 and Pyright 1.1.414 still report 339 findings against 336 recorded. The three unity_bucket.py findings reproduce byte-for-byte on pinned main. Ancestry, range-diff and git diff --check were reviewed. Remote CI on this replacement head is pending verification.

Impact analysis identified Hades for rumble and player APIs, Breath of the Wild and Tears of the Kingdom for motion aiming and HID styles, and Pokémon Scarlet for controller style and identity queries. Nintendo Switch 2 Welcome Tour is main's reference for the new Joy-Con 2 mouse path. Neither local Games root exposed a runnable Mac build to the regression harness. The current results cover compiled host and synthetic fixtures. Gameplay, physical-controller pairing, hotplug, actuator and device validation remain unverified for this revision. The feature changes no production graphics or shader chain source. No fresh shader comparison or full graphics campaign was run.

Historical proposal, validation and original commit context

The previous description follows in full. Its results apply to the revisions it records.

The direct Steam Controller adapter, shared Mac mappings and per-player motion and rumble remain distinct from main.

Conflict refresh — 2026-11-08 UTC

Rebased 6adc10487401add3428d7e11951ca5ee68359787 onto 62f6a52c4fa9b42b73e5c37ef9a88794c8d15ae5 as 54d923a3764371ae9130a63ae9d2a3d54b9b72d9. The generic HID reader retains main's registry-ID deduplication from #1722 before applying this PR's transport admission. The same identity also deduplicates deferred callbacks. When either callback reference signals removal, the router releases the retained device once. An unknown registry ID falls back to reference equality, and the existing timed GameController fallback compares its captured reference without querying a possibly removed device. Existing Triton ownership, controller applet focus/release behavior and the fixture's CF_RETURNS_RETAINED ownership annotation are preserved. The symptom index was regenerated from the combined sources.

Validation

On this exact head, 16 native controller, HID, mapping, motion, haptics and Home prompt fixtures passed using Wonder's profile. The adapter fixture covers duplicate registry identities across active, waiting, deferred and GameController-held routes, one close on alias removal, and separate players for distinct devices of the same model. It passed normally and with ASan/UBSan, using ASAN_OPTIONS=detect_leaks=0. HID passed 283 checks both normally and under TSan. The controller prompt passed 32, the Pro Controller fixture passed 55, and WaitSynchronization passed seven cases. The Mac host built with 57 compiler warnings. The compile database and targeted LLVM 23.1.4 check passed with zero findings in controllers_mac.m, t_controllers_mac.m and front_mac.m. The range-diff, main ancestry, generated docs and git diff --check were verified.

The focused packaging, documentation, settings and HMMA Python suite ran 97 tests. 95 passed and one optional translator test skipped. The environment-table check still fails on DM_NVN_BC_ALIAS and references to DM_NVN_CUT_FIRST, DM_NVN_RT_RECLAIM and DM_NVN_VC_WAYS. All four graphics entries and references match pinned main. Pinned Ruff 0.17.2 and Pyright 1.1.414 reported 376 findings against 371 recorded. The five excess counts are in the byte-identical upstream test_patches.py registry fixture, also reproduced in the earlier exact-main check with the same file and baseline. Remote CI remains pending.

Impact analysis lists Hades, Breath of the Wild, Pokémon Scarlet and Tears of the Kingdom through shared HID paths, with 149 profiles of unknown coverage. Tears of the Kingdom and Breath of the Wild cover motion aiming, Hades covers rumble and player APIs, and Mario Kart 8 Deluxe and Mario Kart World cover the applet and multiplayer join behavior. Ocarina of Time is main's report for the duplicate-controller case. Read-only build discovery found Hollow Knight and Link's Awakening, without matching builds for those selected paths. The refresh adds no gameplay, physical-controller or hotplug run, device test, performance result, shader translation or full graphics campaign. The earlier USB/Bluetooth/Puck, calibration, trackpad, rear button and persistence limits remain open.

Historical proposal, validation and original commit context

The previous description follows in full. Its results apply to the revisions it records.

The direct Triton adapter, shared Mac mappings and per-player Steam Controller motion and rumble remain absent from main.

Conflict refresh — 2026-11-07

Rebased 26266759d330308ac7db7811a9274d8a49d85313 onto main 267ef2d356c42e41480a2fcb5560756413030593 as 92c893c07a981ced82834ec5364c01bf82fb655c. The reviewed controller patch at 7e657cafdbee57f3cd7cf7e5448687ac09bb69ca replayed unchanged after main advanced with the counter-clock fix. Main's controller applet now reads the same slots as the shared Mac controller router. Press-to-join uses the first physical button and lowest free slot across the Mac routes. The joining press cannot also confirm or cancel. The prompt reads physical buttons before remapping, ignores new actions while inactive or modal, and waits for a held confirm or cancel button to be released before returning input to the game. A native GameController device still needs a fresh event for the Triton ownership check, because macOS may retain a cached profile for a seized device. Direct Steam ownership and generic HID routing stay separate.

The HID merge retains main's delayed fallback for unlisted controllers and identical-pad takeover by USB location. The router opens the model's HID peers before giving up their GameController route. A failed open preserves that route. Main's applet state machine, WaitSynchronization implementation and wait fixture are byte-identical. The retained HMMA negative tests now mutate the current fixture's real parameter names and assert the input exists first.

Validation

On 92c893c07a981ced82834ec5364c01bf82fb655c, the Mac host built with its existing graphics warnings. Sixteen native controller, HID, mapping, motion, haptics and Home prompt fixtures passed. HID passed 283/283 normally and under TSan. The controller prompt passed 32/32 under ASan/UBSan, and the adapter's own ASan/UBSan run passed with leak detection disabled. Seven WaitSynchronization checks passed. Main's new counter-clock fixture passed 11/11 and its timestamp fixture passed 25/25.

On the reviewed pre-replay head 7e657cafdbee57f3cd7cf7e5448687ac09bb69ca, the packaging, documentation, settings and synthetic HMMA suite ran 97 tests. 95 passed, one optional translator check skipped and one environment-table check failed. The replay changes only upstream counter-clock files. Python, packaging, iOS, shader-tool and HMMA reference inputs match that tested head. On the published head, 20 focused documentation/settings checks passed and pinned Python lint matched 371 baseline findings with zero new or stale findings. The environment-table failure reproduced again. It matches the independently reproduced upstream failure at 01abf279: main lacks the meaning for DM_NVN_BC_ALIAS and still names DM_NVN_CUT_FIRST, DM_NVN_RT_RECLAIM and DM_NVN_VC_WAYS. Those source rows are unaffected by the upstream counter-clock change. Generated symptoms, ancestry, range-diff and git diff --check were reviewed.

Impact analysis identifies Hades, Breath of the Wild, Pokémon Scarlet and Tears of the Kingdom through shared HID paths, with 147 profiles of unknown manifest coverage. Tears of the Kingdom and Breath of the Wild cover motion aiming, Hades covers rumble and player APIs, and Mario Kart 8 Deluxe and Mario Kart World cover the controller applet and multiplayer join behavior. Read-only build discovery found Hollow Knight and Link's Awakening, with no build for those five selected games. This refresh adds no gameplay, physical controller, display-sleep, device, performance, full graphics-suite or LLVM clang-tidy result. Hardware gyro calibration, trackpads, rear buttons, persistence, USB/Bluetooth/Puck and platform limits from earlier runs remain open.

Front-ends retain the shared controller mapping contract in the frontend guide. The existing prompt localization switch and press-to-join setting continue to apply.

Historical proposal, validation and original commit context

The complete previous description follows. Its earlier test runs and revisions remain historical.

The direct Triton controller adapter, per-player motion/rumble and shared mapping UI remain absent from main. This clean rebase preserves the entire existing feature patch, including its event-base lint workflow handling. No new squash was performed.

Current rebase and validation — 2026-11-05

Rebased 5e2b1c7276e0745825f17d7875ec0e56b6913f7b onto main f8a5618d5f505a4c0a0bcac7c413872d3a53c009, producing 26266759d330308ac7db7811a9274d8a49d85313 on the same existing PR branch. Main's current lint fixes are included. References to the previous shared-main lint failure below are historical. The existing single feature commit was replayed without another squash, and the original proposal, commit context and validation details are preserved below.

Fresh native controller routing/ownership, pad HID (19 checks), pad mapping, motion, HID (270 checks), Apple haptics, audio and upstream OutputStream fixtures passed. The pad-HID target used ASan/UBSan. All 30 focused documentation/settings/launcher tests and the Mac host build passed. The host build retains 56 compiler warnings; no zero-warning claim is made.

No physical controller, display-sleep, gameplay, device or performance validation was performed. Prior full sanitizer/LLVM/game coverage below retains its original revision. The feature changes shared input paths; the previously documented impacted-game rationale and unavailable runtime coverage remain applicable. No current-head remote CI success is claimed; Agent 3 receives this published head for the authorized CI pass. Repository documentation contains durable behavior/findings, with run-specific evidence kept in this PR description.

Original PR description and commit/validation context (historical)

Main still lacks the direct Triton USB/Bluetooth/Puck adapter, per-player physical motion/direct rumble, and shared controller-model mapping UI. All feature commits remain relevant; the event-base lint workflow fix is also absent from main.

Current refresh — 2026-11-04

Candidate 5e2b1c7276e0745825f17d7875ec0e56b6913f7b is exactly one commit directly atop fresh main 975bb4c14456d145eb3d969814534eecdd996ce9. The canonical branch and existing PR destination are unchanged. The published head was 3a620145aca8a6ba4271036c2c71cc98253b87ca; preserved local pre-pass candidate was 3a620145aca8a6ba4271036c2c71cc98253b87ca. Rebase range-diff is patch-equivalent, and the pre-squash tree 30cd5447a85136ed508e774b389c149e4abdc978 equals final tree 30cd5447a85136ed508e774b389c149e4abdc978. No unrelated prerequisite or feature was dropped. Original author credit is preserved.

Validation and limits

53 ABI/documentation/settings/launcher/chain tests pass on this exact candidate. The generated symptom index is current. The complete tracked dm/host and dm/mac trees, settings, and HMMA regression are identical to tested 3a620145: its 270-check plain/TSan HID runs, controller and haptics ASan/UBSan fixtures, and Home/motion/input-map fixtures are reused for those identical sources. The prior 93 tooling tests and native arm64 host build remain historical results at 3a620145; they are not a full build/test claim for the new renderer. The new main task/mesh ABI is covered by the fresh ABI tests, and the current production renderer compiles and runs in the separately tested #959/#1066 fixtures. Those fixture runs do not establish full controller gameplay.

The five selected regression games are TotK/Breath of the Wild (motion aiming), Odyssey (motion actions), Hades (rumble/player APIs), and Wonder (multiplayer). The fresh check --no-smoke attempt exited 2 when sandbox access to the harness worktree metadata was denied, before discovery or shader comparisons. No game was launched, no chain rebuild was attempted, and the runtime/static regression is incomplete. Physical USB/Bluetooth/Puck, gyro calibration, trackpads/rear buttons, persistence on hardware, iOS and sustained gameplay remain unverified.

Shared Python lint is blocked by unchanged main dm/games/settings.py:115 Ruff B023 (390 findings versus 389 baseline), confirmed on the pinned base. No unrelated repair or baseline alteration is included. Generated documentation, final main diff and git diff --check were reviewed for PR #895 placement/artifact rules; no planning journals or run-specific evidence were added to repository documentation. Remote CI for this new unpublished SHA has not run; previous CI results are historical.

Historical context and evidence

The description below predates this refresh. Its revision names, validation runs, measurements and limitations are retained as historical evidence rather than current all-pass claims.

Adds direct macOS support for the 2026 Steam Controller (Triton) over USB-C, Bluetooth and Puck. GameController, generic HID and Triton share player slots and disconnect handling. Triton supplies trackpads, rear buttons, per-player gyro and rumble. A configurable 10% radial stick deadzone keeps resting drift from suppressing motion aiming.

The launcher and mapping panel share each game's DM_KEYMAP and DM_PAD_MAP. Extra inputs and trackpad settings are saved per model, launch overrides stay fixed, and DM_SAVED_CHOICES=0 keeps edits in the current session. Shared controls · Mac controller guide

Current integration and validation — 2026-11-04

Head 3a620145aca8a6ba4271036c2c71cc98253b87ca rebases published 1415831a4519f0aaa3495816e7ad15ed08f6a752 onto fixed main 02c91cd4cc7abd86d7e2e28b81e98fa19d76b2ec. Main is an ancestor. The feature remains relevant: main has no direct Triton adapter or shared per-model mappings. The rebase preserves current main’s offline web applet, independent input peek/hold latches, mouse/SDK input APIs, Home behavior and new 120 fps settings.

Conflicts were in the host source list and input interfaces. The host builds both controllers_mac.m and web_apple.m, retaining WebKit/UserNotifications. Per-player motion remains alongside the applet input APIs. A new ten-check fixture verifies that held applet input suppresses every guest player, keeps an independent merged latch and does not replay applet taps. The raw-rumble release fixture now uses its existing fake HID device type, matching main’s close-on-release contract and checking other-player isolation. Range-diff retains the original series, including the event-base lint fix; the two extra commits contain only integration tests/fixture repairs.

Fresh completed checks on the candidate’s exact source content, arm64 macOS/Xcode and native Python 3.14:

  • HID: 270 checks each in plain and TSan runs. Motion fusion: 209 checks. Home state: 40 checks; AppKit Home UI: 54 checks. Generic HID: 19 checks; shared input maps: 48 checks. Pad mapping/motion, controller routing/preferences/focus and Apple haptics fixtures passed.
  • Controller integration and Apple haptics fixtures passed ASan/UBSan (leak detection disabled for Apple framework allocations). The controller fixture uses an isolated preferences test domain. These are synthetic fixtures, not physical transport tests.
  • 93 focused documentation, preferences, environment and packaged-launcher tests passed. Generators and git diff --check passed. LLVM 23.1.4 clang-tidy on both changed integration-test files matches its baseline, with zero new findings.
  • The native arm64 Tears of the Kingdom host linked from the combined sources, including the real Metal source compilation. Existing codec archives were reused after complete libvpx/dav1d source-tree hashes matched tested revision 6d3a068f (respectively 1065d407aad31a966d583cafe991dfbba8b91ee5 and 5809c0831163029603ddb466e3b58f74b90ef7fa). The build has existing main warnings; no tool lock was relaxed. No full GPU suite or iOS build was repeated.
  • Shared Python lint has an inherited failure: unchanged dm/games/settings.py:115 Ruff B023, 390 findings versus 389 baseline, independently confirmed on this exact main. Its blob is identical here (7dce6a29da8087d3cdacf972b320ba710b4422bb). No unrelated repair or baseline change is included. Remote CI for this exact head is pending; historical checks do not validate it.

regress.py impact 02c91cd4..HEAD identifies four profiles with recorded affected HID consumers and 138 without manifests. The selected five are Tears of the Kingdom/Breath of the Wild (motion aiming), Odyssey (motion actions), Hades (rumble/player APIs), and Wonder (multiplayer). The explicit five-game check --no-smoke exited 2 at the normal disk-space guard (2.1 GB available versus 3.0 GB required), before game discovery or shader checking. The known local inventory lacks those Mac builds. This is incomplete regression coverage, not a pass; no game, performance, hardware or device validation is claimed. The translator and chain header are byte-identical to this main. All historical hardware/runtime limitations below remain open.

Earlier validation — historical tested revisions

  • The arm64 Tears of the Kingdom host linked with Xcode 27.2 (27B52), the unchanged codec archives and the host's source-only Metal fallback. Ten native fixtures passed, including 40 Home state-machine checks, 54 AppKit view checks and 253 HID assertions.
  • The controller fixture passed natively and under ASan/UBSan. It exercised a GameController Home callback, native glyph selection, direct Triton reports, mixed-controller cancel/confirm, remapping, prompt touch suppression, generic HID restoration and a Guide held across focus loss. Removing the admission check in a separate negative control failed the cached-profile assertion.
  • The compile database contains 230 sources. LLVM 23.1.4 reported zero findings in the three checked sources (front_mac.m, controllers_mac.m, t_controllers_mac.m). The gallery and generated symptom index matched their sources.
  • Of the 35 selected documentation, packaging, preference and HMMA tests, 34 passed with the default tools. The unchanged HMMA GPU fixture could not invoke the Metal compiler. All 35 passed with the installed supplemental Metal 32023.921.6. At that historical run, tool-lock preflight reported the supported macOS/iOS 32023.922.4 compiler missing. The current environment now has the supported compiler; that former limitation is not a current claim.
  • Static impact identified four profiles with recorded affected HID calls and 130 without usage manifests. The five-game regression check stopped at discovery. Tears of the Kingdom and Breath of the Wild cover motion, Wonder and Luigi's Mansion 3 cover multiplayer, and Legends: Z-A covers another controller-driven title. Only Z-A v393216 was present and it has no smoke route. Gameplay and physical-controller checks remain open for this revision.

The queue-origin fixture and lint baseline match integrated main. On the previous 0848cf8e revision the synthetic queue-origin fixture passed 398 assertions in each encoder mode with Metal validation. CI for this candidate is pending. Checks for this revision

Hardware evidence and remaining coverage

A contributor’s USB-C test on an M4 Pro confirmed detection, buttons, sticks, rumble and delivery of gyro data on an earlier head. TotK initially ignored bow-aiming motion. The contributor traced it to sticks resting slightly off center and reported that the radial deadzone restored gyro aiming after walking and while moving. Six-axis handle encoding was a separate hardware finding. USB-C report · Gyro and deadzone follow-up

Gyro scale calibration, trackpads, rear buttons, real-device mapping persistence, Bluetooth, Puck operation and physical hotplug still require hardware verification on this revision. Earlier sanitizer and complete host-suite results apply to their tested revisions. The checks on this revision use synthetic controllers. Contributor’s validation and remaining limitations

Verification of the queued lint job

At 6d3a068f, the full clang-tidy scan matched all 268 baseline findings, with zero file/check pairs above or below their counts. The job then compared that snapshot against a later main revision and reported false growth in t_movie_vpx.c (1 to 2).

This branch carries only the workflow and developer-guide commit f07d8b24 from #1060. Both lint jobs fetch the base SHA in the PR event. In a temporary Git repository with two successive base commits, running each workflow’s fetch command selected the event SHA after the branch advanced. Locally, the C comparison reproduced the reported failure against fd14f3bc and passed against the tested base ad7af9cb. Both C and Python rejected deliberately increased counts. All 40 tests in python3 -m unittest dm.tools.tests.test_lint_c dm.tools.tests.test_lint_python dm.tools.tests.test_docs passed on 3813aaf2. This repair changes CI inputs and developer documentation only.

Historical original commit context and preserved corrective messages

These are the original published and preserved local messages before this squash. They explain historical intent and limitations; any validation named in them belongs to the original commit, not the new candidate. Repeated equivalent messages are shown once with their original identities.

seedds seedds@gmail.com — f46575fefe3be95aff6ed24d5485b71bc7723d0e (published), f46575fefe3be95aff6ed24d5485b71bc7723d0e (preserved unpublished/local)

Preserve per-player motion and support direct controller rumble

Keep independent motion histories and complete mapped Shake gestures per player, while retaining live native orientation and upstream source selection. Encode SDK six-axis handles consistently. Add lifecycle-safe direct rumble sinks using the shared intensity cutoff and deterministic refresh tests.

seedds seedds@gmail.com — 3817797568d77e98531b3b417ebc083d3fb80ed2 (published), 3817797568d77e98531b3b417ebc083d3fb80ed2 (preserved unpublished/local)

Add Steam Controller support and shared control mappings

Add physical-device-scoped Triton USB-C, Bluetooth and Puck input without disturbing other controllers. Support rumble, gyro, trackpads, extra buttons and the resting-stick deadzone through the existing player, motion and haptics policies.

Use the shared DM_KEYMAP and DM_PAD_MAP parser and saved-choice contract from PR #386 at 97eaa989, with an additive Shake target. Keep launch overrides temporary, support session-only edits and explicit legacy import, and test both adapters, the mapping panel and the launcher/host preference domain.

seedds seedds@gmail.com — 3fda84dfa3eacb76f18c823da0279ae9d12ff66c (published), 3fda84dfa3eacb76f18c823da0279ae9d12ff66c (preserved unpublished/local)

Document Mac controller setup and shared control choices

Describe Steam Controller capabilities, per-player Shake, shared frontend preferences, explicit legacy import and Input Monitoring recovery after a rebuild. Record the TotK hardware findings and remaining calibration limits.

seedds seedds@gmail.com — 20577c0a9f136e9aed37e3be39eebab635c714a3 (published), 20577c0a9f136e9aed37e3be39eebab635c714a3 (preserved unpublished/local)

Make controller mapping and fixture invariants explicit for C lint

seedds seedds@gmail.com — f407629054127391c52ce1b035a2d48e54ffe0f0 (published), f407629054127391c52ce1b035a2d48e54ffe0f0 (preserved unpublished/local)

Share generic HID ownership and input routing with controller adapters

seedds seedds@gmail.com — d7d353ee7682be41a164cd27705f82570b58671c (published), d7d353ee7682be41a164cd27705f82570b58671c (preserved unpublished/local)

Match IOKit ownership contracts in controller fixture mocks

seedds seedds@gmail.com — 7277424bdf50593587ae36570fc7e8f9f23fcc95 (published), 7277424bdf50593587ae36570fc7e8f9f23fcc95 (preserved unpublished/local)

Validate HMMA helper parameter captures before rewriting

seedds seedds@gmail.com — a878c09dcabf9e45351729f77852f15c22b44caa (published), a878c09dcabf9e45351729f77852f15c22b44caa (preserved unpublished/local)

Keep controller guides focused on behavior and limitations

seedds seedds@gmail.com — 12ebb864093367e39932734f7f9f383d58ef46bb (published), 12ebb864093367e39932734f7f9f383d58ef46bb (preserved unpublished/local)

Preserve physical Home state across focus and verify quit-prompt routing

seedds seedds@gmail.com — aa9a069f7f8cde4034155631648f06cfc39ecffd (published), aa9a069f7f8cde4034155631648f06cfc39ecffd (preserved unpublished/local)

ci: pin lint ratchets to the pull request event base

(cherry picked from commit f07d8b24dc01f460d672ad99ebf0be4948aedf18)

seedds seedds@gmail.com — 7162a8b2e9c3f68a477d09f0907468135cbc20d0 (published), 7162a8b2e9c3f68a477d09f0907468135cbc20d0 (preserved unpublished/local)

Verify applet input alongside per-player controller routes

seedds seedds@gmail.com — 3a620145aca8a6ba4271036c2c71cc98253b87ca (published), 3a620145aca8a6ba4271036c2c71cc98253b87ca (preserved unpublished/local)

Adapt rumble release fixture to the current HID close contract

CI fixture ownership correction

The HID element mock now declares CF_RETURNS_RETAINED, matching IOHIDDeviceCopyMatchingElements and its existing CFRetain return. LLVM 23.1.4 previously treated the mocked return as borrowed and reported a false over-release at steam_protocol in controllers_mac.m; the production translation unit had no finding. The same analyzer invocation on the test translation unit now has zero diagnostics. The rebuilt native t_controllers_mac suite passes, including HID lifetime, takeover/handback, remapping, persistence and disconnect checks. No production behavior, test assertion, suppression or lint baseline changes. Shared main CI failures remain separate in #1746.

@hrdkbhatnagar

hrdkbhatnagar commented Oct 26, 2026 •

Copy link
Copy Markdown

I tested this PR with a real 2026 Steam Controller. Rumble works. Gyro data reaches the game, but TotK's gyro aiming does not respond to it. Details below.

Setup

  • MacBook Pro M4 Pro, macOS 27.0.1 (26A434), Xcode 26.3
  • Steam Controller (2026) over USB-C: 28de:1302, report 0x42 (54 bytes, about 250 per second). Steam was closed.
  • PR head f7c2f2f, merged locally into ff44c59 (the build my TotK Mac app came from) and into c484b13 (current main). Each merge has 3 conflicts, all additive (dm/host/Makefile, dm/host/tests/Makefile, dm/host/tests/t_hid.c). In tests/Makefile, the run: … t_controllers_mac line ends up in the middle of the main run recipe, so $(B)/t_symbols has to move above it.
  • t_hid (129 checks), t_pad_map, t_pad_motion, t_controllers_mac and t_haptics_apple pass on both merges. dmhost builds for TotK.

Results

Feature Result
Detection controller 'Steam Controller (2026)': player 1, no [RED] lines
Buttons / sticks worked in play
Rumble works
Gyro: report decoding (steam_input.h) matches the hardware (see below)
Gyro: delivery to the game works: TotK reads live values
Gyro: TotK bow aiming (motion controls on) no response
Trackpads, rear buttons, Bluetooth, Puck not tested

Axes and signs

A read-only IOKit probe (sends nothing to the controller) guided four moves and integrated the raw gyro at offset 40, using the PR's scale. The accelerometer is at offset 34.

Move Raw gyro (x, y, z), degrees Raw accel (x, y, z), G Expected with Switch axes (x right, y forward, z up)
Flat on desk (rest) (+0.01, +0.00, +0.99) +1 G on z (specific force); the PR negates it to (0, 0, −1) ✓
Turn left (CCW from above) (+4.5, −3.8, +27.7) positive about z ✓
Tilt far edge up (+16.9, −1.9, −2.4) y +0.11 positive about x ✓; accel +y ✓
Roll right (right grip down) (−4.5, +25.2, +2.4) x −0.35 positive about y ✓; accel −x ✓

So the controller's axes are the Switch's, and the PR's mapping (acc = -raw, gyro = +raw) has the right signs. The angles were a third to a half of what I asked for. That may be my movement, but I have not confirmed the ±2000 dps scale against gravity.

What TotK receives

I added a temporary trace, DM_GYRO_TRACE=1, to controller_publish_motion and six_states:

gyro trace: controller motion #1 focus 1 (app 1 window 1 modal 0) slot 0 merge 0 gyro_enabled 1 -> gyro (0.000, 0.000, -0.000) rev/s acc (-0.00, 0.15, -1.00)
gyro trace: GetSixAxisSensorHandles npad 0 style 0x1 -> 1 handle(s), first 0
gyro trace: six_states #2760 handle 0 n 16 player 0 physical 1 (history 7054) -> gyro (-0.090, 0.005, 0.096) rev/s acc (-0.48, -0.43, -0.63) dir row0 (0.83, -0.37, 0.42)
gyro trace: six_states #3480 handle 0 n 16 player 0 physical 1 (history 8341) -> gyro (0.017, -0.044, 0.269) rev/s acc (0.03, -0.05, -1.00) dir row0 (0.95, -0.31, -0.07)

TotK 1.4.3 imports only GetSixAxisSensorHandles, StartSixAxisSensor and GetSixAxisSensorStates. Its poller at main+0x47aec0 reads 16 states per npad into a 0xe98-byte per-npad record (states at +0x290). main+0x47a140 onwards then:

  • counts new samples from the sampling number (newest − last seen, at most 16);
  • converts acceleration and gyro to (−x, z, y);
  • remaps the angle and direction the same way;
  • flags an "untouched sensor" pattern (zero x/z acceleration with an identity direction).

A DM_PROBE at main+0x47a1d4 (the store of the new-sample count) gave x15 = 2 to 4 every frame with the sampling number advancing, so the game's input layer takes the samples. The bow aim still did not move. Whatever drops the motion is further up, in TotK's aiming or camera code. I didn't get further than that.

Side finding: six-axis handle encoding

GetSixAxisSensorHandles puts the style bit number in byte 0 (FullKey → 0). On the console, byte 0 is the SDK style index (FullKey 3, Handheld 4, JoyDual 5, JoyLeft 6, JoyRight 7, with 0 = None), and the host's vibration handles already use that. I switched both the handles and six_connected to the index (ctz(style) + 3) and TotK then got handle 0x3. It made no difference to TotK's aiming, but it is a divergence from hardware that another game could trip on. It predates this PR (main has the same encoding), and t_hid's h = 0x100 would need updating if it changes.

Not done

  • Trackpads, rear buttons and the mapping window were not exercised.
  • Bluetooth and Puck were not tested; this controller was on USB-C only.
  • The gyro scale against gravity is unconfirmed.

@hrdkbhatnagar

Copy link
Copy Markdown

Follow-up: gyro aiming works with a stick deadzone

I found why TotK's bow aiming ignored the gyro. The motion data was correct throughout. The Steam Controller's sticks have no deadzone in this PR.

steam_decode passes the raw stick axes through. At rest they sit a few counts off zero, while the console reports a centred stick as exactly 0. TotK treats any non-zero stick as stick input and ignores motion while aiming. In play, gyro aiming started working briefly after I used an ability (which reset TotK's input state) and stopped for good once I touched the left stick.

With a radial deadzone in steam_decode, gyro aiming works: right away, after walking with the left stick, and while moving and aiming at once. The camera stick feels normal.

--- a/dm/host/steam_input.h
+++ b/dm/host/steam_input.h
@@ #pragma once
+#include <math.h>
 #include <stddef.h>
+#include <stdlib.h>
 #include <string.h>
@@
+// The raw HID sticks rest a few counts off centre; the console's input service reports a centred stick as exactly 0 (a radial deadzone), and games rely on it:
+// Tears of the Kingdom treats any non-zero stick as stick input and stops gyro aiming. Radial deadzone of DM_STEAM_DEADZONE (fraction of full scale, default 0.10),
+// rescaled so the edge still reaches full scale
+static inline void steam_deadzone(int32_t *x, int32_t *y) {
+    static float dz = -1; if (dz < 0) { const char *e = getenv("DM_STEAM_DEADZONE"); dz = e ? (float)atof(e) : 0.10f; if (!(dz >= 0 && dz < 0.9f)) dz = 0.10f; }
+    float fx = *x / 32767.0f, fy = *y / 32767.0f, m = sqrtf(fx * fx + fy * fy);
+    if (m <= dz) { *x = *y = 0; return; }
+    float k = (m - dz) / (1 - dz) / m;   // each axis clamped on its own, so a full diagonal stays at full scale on both axes
+    *x = (int32_t)lrintf(fmaxf(-1, fminf(1, fx * k)) * 32767); *y = (int32_t)lrintf(fmaxf(-1, fminf(1, fy * k)) * 32767);
+}
@@ steam_decode
     s->rx = steam_axis(p + 14); s->ry = steam_axis(p + 16);
+    steam_deadzone(&s->lx, &s->ly); steam_deadzone(&s->rx, &s->ry);

t_pad_map (including its full-diagonal case), t_controllers_mac, t_hid (129 checks) and t_haptics_apple pass. The 10% default worked well; I didn't try other values.

How it was traced (TotK 1.4.3, main offsets):

  • main+0x100d030 copies the newest sample (count, gyro, direction rows) into a per-frame snapshot every frame.
  • main+0x874c1c updates the player's input object. Its motion object is at +0x430, and main+0x874e6c updates it from that snapshot.
  • The same function sets bit 0x10 of [input+0x418] when a button in mask 0xf60ff is down, or when the stick at [snapshot+8] is not exactly 0.0. I didn't trace this further than that, but it is probably what the aiming code checks.
  • Hardware read watchpoints showed nothing reading the motion object while aiming before the fix. Gyro worked as soon as the sticks rested at exactly 0.

Corrections to my comment above:

  • The validity check in the input layer flags a sample when its acceleration is all zero or its direction is exactly the identity. It isn't one combined pattern.
  • main+0x874e6c treats an exactly-identity direction as "no sensor" and resets its aim reference. The PR's orientation starts at identity, as it does after Recenter gyro. That may be worth knowing, though once the controller is moving it didn't cause problems here.
  • The six-axis handle encoding note stands, but it isn't related to this problem.

Still not tested: trackpads, rear buttons, the mapping window, Bluetooth and the Puck.

@v0lumehi

Copy link
Copy Markdown
Contributor

Heads-up: #388 proposes treating controls as data that every UI writes (#388 (comment)): the key and button maps (#386's DM_KEYMAP / DM_PAD_MAP) in one per-game store, so a controls page in any launcher and an in-app panel never disagree. Support for a new controller sits underneath that and is independent of it. Mentioning it so the thread has the full picture.

@thehobbit85
thehobbit85 force-pushed the codex/steam-controller branch 2 times, most recently from e2bdbaf to f115432 Compare October 29, 2026 20:18
@thehobbit85

Copy link
Copy Markdown
Contributor Author

The fixes for the USB-C findings and gyro-aiming follow-up are included in the cleaned three-commit series:

  • Both raw sticks have a 10% radial deadzone, configurable with DM_STEAM_DEADZONE, with rescaled travel and preserved outer diagonals. Trackpad coordinates are unchanged.
  • Six-axis handles use SDK style indices and device bytes, with matching validation on reads.
  • The complete test recipe, including t_symbols, is retained. Added tests receive the sanitizer flags.
  • Mapped Shake follows the controller's player without discarding native/Pro/phone orientation; direct Steam histories remain independent.

Implementation and regressions: motion/rumble foundations and controller input. The full host suite, ASan/UBSan and thread checks passed locally, as did the Mac host build. Both code commits also build and pass the host suite independently.

The remaining hardware limits are documented: gyro scale calibration is still unconfirmed, and this final revision needs physical trackpad/rear-button/transport retesting. Recenter keeps a truthful identity pose; we have not added a fabricated rotation to avoid TotK's identity check. No game was launched during this update.

@thehobbit85

Copy link
Copy Markdown
Contributor Author

The shared-controls coordination comment is now implemented in 075ce81.

The launcher and host use #386's input_map.h parser and launcher.DM_KEYMAP / launcher.DM_PAD_MAP keys in the same per-game app preference domain. The panel writes the shared standard button map; extra physical inputs, pad modes and gyro settings use model-specific dictionaries in that same domain and cannot override standard mappings. Shake is an additive target to preserve this PR's per-player bindings. The HID transport remains independent of the mapping UI.

Launch environment and env.txt overrides remain temporary and fixed in the panel. DM_SAVED_CHOICES=0 ignores saved choices and prevents writes; restoring defaults removes the relevant keys. Legacy controllers.plist mappings are imported explicitly, keeping the old file intact. Standalone hosts use the same lowercase default domain as packaged apps, and custom bundle IDs are preserved.

Synthetic integration tests cover launcher/host domain agreement, precedence, malformed choices, session-only edits, model isolation and panel actions. They passed, along with the packaging/docs tests and lint ratchet. The store contract and player guide describe the behavior.

This incorporates #386 at 97eaa989. The later proposed type: syntax remains subject to maintainer review and is not claimed as implemented here. The PR description also records a separate failure in unchanged graphics code: t_pso_bundle builds an archive for the installed macOS 27 SDK, which this macOS 26.6 runtime rejects.

@github-actions github-actions Bot added mac The Mac front end and app (dm/mac, front_mac.m) tools Tools and scripts (dm/tools, scripts) docs Documentation game: 0100F2C0115B6000 The Legend of Zelda: Tears of the Kingdom labels Nov 3, 2026
@thehobbit85
thehobbit85 force-pushed the codex/steam-controller branch 4 times, most recently from 0848cf8 to 6d3a068 Compare November 3, 2026 19:19
@github-actions github-actions Bot added the ci Continuous integration and repository settings (.github) label Nov 3, 2026
@thehobbit85
thehobbit85 force-pushed the codex/steam-controller branch 3 times, most recently from 3a62014 to 5e2b1c7 Compare November 4, 2026 17:46
@thehobbit85
thehobbit85 force-pushed the codex/steam-controller branch from 5e2b1c7 to 2626675 Compare November 5, 2026 06:11
@thehobbit85

Copy link
Copy Markdown
Contributor Author

Hi @seedds — I've refreshed the branches that had conflicts with current main. Could you help me prioritize review of these PRs?

Validation and remaining limitations are recorded in each PR. CI on the refreshed heads is being verified separately. Which of these best fits the project's current priorities, and are there scope changes or specific validation you would like before review? I'm happy to focus on a smaller subset first. Thanks for your time.

@thehobbit85
thehobbit85 force-pushed the codex/steam-controller branch 3 times, most recently from 6adc104 to 54d923a Compare November 8, 2026 08:00
@thehobbit85
thehobbit85 force-pushed the codex/steam-controller branch 2 times, most recently from 892b350 to 5bd183c Compare November 10, 2026 02:45
v0lumehi added a commit to LNPANDY/NX2Apple that referenced this pull request Nov 10, 2026
0115 and 0117 extend fork-only parts upstream declined (#492, #488). 0118, 0120 and 0121 build on the touch patches 0065 to 0070 and 0090, 0122 on seedds/nxnative#221. They are forwarded once those land.
@gxt43

gxt43 commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

dm/host/input_preferences_mac.h in this PR does not compile for a multi-program game: [@"org.nxnative." DM_GAME_ID lowercaseString] joins two literals, but game.py header_multi defines DM_GAME_ID as (dm_game_cur()->id), read at run time (called object type 'NSString *' is not a function or function pointer in front_mac.m and controllers_mac.m, found on LUNAR Remastered Collection, 0100DCD01E9EE000). Joining at run time gives the same string for a one-program game:

return NSBundle.mainBundle.bundleIdentifier ?: [[@"org.nxnative." stringByAppendingString:@(DM_GAME_ID)] lowercaseString];

Preserve per-player motion, direct rumble, shared launcher mappings and exclusive HID ownership alongside the current applet input and Home-prompt routes. Include focused regression and fixture lifetime checks.

Front-ends: Mac launchers share DM_KEYMAP and DM_PAD_MAP in the per-game preference domain; use the controller-specific choice contract in docs/frontends.md for extra inputs and model settings.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci Continuous integration and repository settings (.github) docs Documentation game: 0100F2C0115B6000 The Legend of Zelda: Tears of the Kingdom host The Horizon host: SVCs, services, threads, memory (dm/host) mac The Mac front end and app (dm/mac, front_mac.m) nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) 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.

5 participants