DXMT, Madeira Dock and madeira-d3d12 began under research/ as experiments and are now core parts of every build, so they move to the top level (dxmt/ and madeira-dock/ stay submodules, renamed to match). The test programs and host checks move out of build/ into tests/ (host, x64, x86, dxmt, proc, net), the helper scripts join tools/, and the example madeira.cfg moves to docs/. Every reference in the build scripts, tests, docs, notices and code comments follows the new paths; the adopted Converter Exception text is left as adopted. Removed: the Wine patches from before the Wine fork (the fork carries them), a game-specific deploy script and a parked session note. The README gains a repository layout table. Checked: the app builds, the host checks pass and fail exactly as before the move, and DXMT's build directories regenerate cleanly at the new path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 KiB
Physical and touch controllers through XInput
Paired GameController devices and the existing landscape touch editor feed Windows games through a shared XInput snapshot. Up to four physical extended profiles have stable slots; disconnecting one does not renumber the others. Touch input merges into player 1 (slot 0).
Physical input and transport
The app captures live profiles and samples them on a serial queue at 250 Hz (4 ms with 1 ms scheduling leeway), with change callbacks for prompt updates. The timer stops while inactive or with no connected physical pads. Inactive controllers remain connected but report neutral input. Disconnects clear slots unless the touch layout still supplies slot 0. Buttons, triggers, and signed stick axes reach XInput without mouse synthesis or an app-imposed dead zone. iOS 18 event claims prevent focus navigation from consuming controller events.
WiniosGamepad.c publishes snapshots under a short mutex. Packet numbers change
only when the sample or connection changes. The win32u query copies state or
capabilities into the Windows caller's buffer. Wine's paired XInput change tries
this query before its existing HID path.
Touch controls
In the existing landscape touch editor, assign a control using its Controller
tab. Saved pad names remain compatible: A/B/X/Y, D-pad directions, LB/RB,
L3/R3, Menu/View/Guide, LT/RT and LS/RS. LT/RT are full-press triggers; LS/RS are
analogue sticks with radial clamping and the full signed XInput range. Stick
motion is relative to the initial touch position. Guide follows Wine's existing
XInput filtering rules; not every API/game exposes it.
A visible, non-editing landscape layout with at least one supported controller mapping connects a virtual player 1, even before a button is pressed. Hiding controls, entering editing, switching to portrait, removing the overlay or remapping releases input. Backgrounding releases holds while retaining the connected identity. UIKit handles independent fingers and cancellation, so releasing one of two controls mapped to the same button leaves the other held. The pinch recognizer is enabled only during editing.
Touch and physical buttons combine; triggers use the larger value. A physical stick outside its standard XInput dead zone takes priority over touch on that stick. Otherwise a deflected touch stick takes priority; resting touch preserves the physical value. Touch updates are event-driven and require no polling timer. Keyboard/mouse mappings and the existing layout format are retained.
Layouts
While touch controls are shown, the landscape top bar has a layout button
(stacked squares) next to the show/hide controller glyph. In a library session
that top bar is replaced by the library's menu button, and the same menu is the
Controller layout row of the in-game Session menu, below Touch
controls (docs/LIBRARY.md). Either one lists:
- Xbox controller, a built-in full XInput layout: LT/LB and RB/RT rows in the top corners, a D-pad cross above the left stick, the A/B/X/Y diamond above the right stick, View/Menu at the bottom centre and L3/R3 beside the sticks. It is laid out in points from the safe-area edges each time it is loaded, so it fits phones and tablets, and it keeps clear of the top bar. It is offered only while touch XInput is enabled, since its buttons are controller mappings.
- The user's own layouts, Custom Layout 1, Custom Layout 2, ...
- Create new layout, which adds the next free "Custom Layout N" with no controls and opens the editor on it.
- Delete for the active custom layout (confirmed first). The deleted layout's controls are replaced by the built-in, when it is available.
A library game remembers the layout its controls were loaded from (an optional
controlLayout field of its library entry, so older files still load; a layout
deleted since is treated as none), and the session restores the shared layout
when it ends. A game with no saved controls of its own draws the shared working
copy, as before.
The two key sticks, WASD and Arrows, draw a small symbol on the knob in the middle of the stick (a keyboard, and four arrows) so they can be told apart; the controller sticks keep their LS/RS label.
Built-ins cannot be changed. Custom layouts live in
Documents/madeira-control-presets.json; madeira-controls.json stays the working
copy the overlay draws and records which layout it came from (an optional
layout field, so older files still load). When an edit ends, the controls are
written back to the active custom layout. Editing the built-in leaves it as
shipped: the edited controls become unsaved controls. Choosing another layout
while unsaved controls are on screen asks first and offers to keep them as the
next custom layout, so an existing hand-made layout is never lost.
The built-in is never applied automatically by default: it is only loaded
when chosen from the menu. A missing madeira-controls.json does not identify a
new user (the file is only written once the controls or their visibility
change, so an existing user who never touched them has none either), and
nothing else on disk tells the two apart reliably. With
env.MADEIRA_CONTROLS_XBOX_DEFAULT = 1, a user with no madeira-controls.json
at launch gets the built-in the first time the landscape overlay appears; it is
never applied over an existing controls file, even an empty one.
The editor shows Done in place of the checkmark and hides the show/hide glyph while editing; + still adds a control. Menus and confirmation dialogs raised from the overlay window take the touches they cover.
Player 1 at session start
Some input layers enumerate XInput once when a game starts and only look again on a device-arrival broadcast, which this port cannot deliver. Touch player 1 connects only once the landscape overlay shows its controller mappings, and a paired controller may not have reported an extended profile yet, so such a game would never see a pad.
This is opt-in (env.MADEIRA_PAD_EARLY_SLOT = 1), because the reserved
player 1 stays connected for the whole session. With the switch, when a Wine
session starts with visible touch controller mappings (or the built-in about
to be applied by MADEIRA_CONTROLS_XBOX_DEFAULT = 1) or with a paired
controller, slot 0 is published as connected with neutral input. Live touch or
physical input takes it over. Hiding the controls or disconnecting the pad then
leaves player 1 connected at rest until the app exits. Without the switch,
slot 0 connects only when a real source appears, as before.
Audio route with wired controllers
Some controllers enumerate as a USB audio output when wired. iOS then routes all app audio to the controller and the device sounds silent while the audio engine reports healthy signal levels. This is system routing, not a Madeira audio fault: unplug the cable or pair the controller over Bluetooth.
Rollback and scope
Set env.MADEIRA_XINPUT = 0 in Documents/madeira.cfg and restart to disable the
whole producer and controller event claims. Set env.MADEIRA_TOUCH_XINPUT = 0
to disable only touch gamepad input. [xinput] ml1920 logs physical enablement
and connections; [touch-xinput] ml1930 logs touch enablement once.
Each follow-up behaviour has its own switch (env.NAME = value in
madeira.cfg, or the process environment). The two that change what an existing
user sees are opt-in (only 1 enables); the others are on unless set to 0:
| Switch | Default | Effect |
|---|---|---|
MADEIRA_CONTROL_PRESETS |
on | 0: no layout menu, no write-back |
MADEIRA_CONTROLS_EDITOR_DONE |
on | 0: the checkmark and show/hide glyph while editing |
MADEIRA_CONTROLS_XBOX_DEFAULT |
off | 1: a user with no controls file gets the built-in once |
MADEIRA_PAD_EARLY_SLOT |
off | 1: player 1 is reserved at session start (see above) |
[controls-layout] ml1970 logs layout loads, saves, creation and deletion
(never layout names); [xinput] ml1990 logs the session slot reservation.
DirectInput has a separate, opt-in device in the companion Wine change: one
joystick with the standard XInput controller object set (X/Y and Rx/Ry sticks,
Z as the combined triggers, an 8-way POV, ten buttons), read from the same host
query. It is hidden unless env.MADEIRA_DINPUT_PAD = 1 is in madeira.cfg
(exported to the Wine environment), because a game that reads both APIs would
otherwise see two controllers. MADEIRA_DINPUT_TRACE=1 adds a rate-limited
state trace.
Vibration, battery telemetry, controller-driven navigation of the app itself, binding physical buttons to keyboard/mouse controls, shaped (non-round) controls, a layout-wide size slider and a movable top bar remain outside this contribution.
Integration prerequisite
The XInput path needs the Wine change, which is merged and pinned. The opt-in DirectInput device needs its companion Wine change and rebuilt dinput.dll/dinput8.dll; the app side needs nothing more. Source PRs keep upstream's submodule pins and prebuilt DLLs.
Rebuild the native win32u library and affected PE win32u/XInput modules using the paired Wine source. Rebuild wow64win for a WOW64 configuration. XInput 1.1/1.2/1.3/1.4/UAP share the implementation; 9.1.0 forwards to 1.4. Copying just the app changes over the existing prebuilt DLLs will not enable the feature. No binaries from the larger fork are included here.
Validation
Run on a POSIX host with a C compiler and Swift installed:
python3 tests/host/check-gamepad.py
python3 tests/host/check-touch-gamepad.py
python3 tests/host/check-control-presets.py
The first compiles production snapshot/query code and checks packets, ranges,
slots, invalid queries, disconnect/reconnect and concurrent readers/writers.
The second compiles production touch state and checks independent button holds,
layout/lifecycle clearing, analogue ranges, duplicate sticks and physical/touch
arbitration, and the session slot reservation. The third compiles the
production layout store with the controller actions: the built-in layout on
phone, tablet and portrait-reported screens (complete, supported mappings,
inside the safe area and clamps, no overlaps, top bar clear), built-ins
read-only, "Custom Layout N" naming, JSON round trips and what loading a layout
puts on screen; it also checks the switch and write-back wiring in the source.
Set SWIFTC if Swift is not on PATH. These are logic tests, not UIKit gesture
or device integration tests.
The Swift bridge, touch view and changed touch-editor section type-check against the arm64 iOS 17 SDK with the production config reader and stubs for unrelated app UI/logging/input sinks. The C transport compiles for arm64 iOS 17. Companion Wine XInput source compiles for x86-64/i386; its standalone native Windows API test passes using a synthetic host query.
The full combined upstream app has not been linked or device-tested. Before merge, rebuild the paired components and test:
- Physical buttons, sticks, triggers, disconnect/reconnect and multiple pads.
- Touch-only player 1, both sticks plus buttons together, and duplicate mappings.
- Mixed physical/touch holds; releasing either source must preserve the other.
- Hold then hide, edit, remap, remove, rotate, background or interrupt the app; no input should remain stuck, and fresh touches should work afterward.
- Both rollback flags, saved layouts, and existing keyboard/mouse controls.
- Layouts: by default nothing is applied to a user with or without a controls
file; the built-in loads from the menu (the overlay's button, and in a library
session the Session menu's Controller layout row); switching away from unsaved controls
asks first; create, edit with Done, relaunch and reload a custom layout;
delete it; the layout menu and its dialogs respond anywhere on screen; each
kill switch at
0. WithMADEIRA_CONTROLS_XBOX_DEFAULT = 1: a user without a controls file gets the built-in once and an existing file is kept. - By default player 1 is not connected until a real source appears. With
MADEIRA_PAD_EARLY_SLOT = 1, a game that enumerates XInput only at startup sees player 1 with touch controls shown or a controller paired before launch.
The fork's existing device history does not prove this isolated extraction.