Skip to content

Sdl3 - #949

Open
qdot wants to merge 16 commits into
devfrom
sdl3
Open

Sdl3#949
qdot wants to merge 16 commits into
devfrom
sdl3

Conversation

@qdot

@qdot qdot commented Sep 6, 2026

Copy link
Copy Markdown
Member

Implement support for SDL3, removing the xinput and hid managers at the same time (as SDL3 will handle those)

qdot added 16 commits September 13, 2026 15:35
…fier

Adds the SdlGamepadSpecifier enum variant, schema component, protocol YAML
(two 0-65535 vibrate features, low/high motors), and the regenerated device
config (v5.43). Structural inspiration: chiefautism's abandoned PR #860.
Stateful two-motor handler mirroring the XInput protocol: every vibrate
command carries the full motor state as two u16 LE values on Tx, so each
write is a complete command with no batching step.
Cross-platform (Windows/macOS/Linux) gamepad rumble via SDL3, statically
built from source. One process-lifetime thread owns the SDL context and
multiplexes all gamepads addressed by SDL3 instance ID; discovery is
on-demand enumeration and removal is connected-state polling - the thread
never pumps SDL events. Rumble is armed finitely (60s) and refreshed before
expiry so one-shot commands hold indefinitely. Includes the Phase 0
threading spike example (headless init + no-pump enumeration on a spawned
thread). Joins the workspace as both a member and default member so plain
cargo build compiles it on all three OSes. Structural inspiration:
chiefautism's abandoned PR #860.
Registered in the v1-v4 embedded/json lists. v0 is excluded with a comment:
SingleMotorVibrateCmd broadcasts one speed to all motors and cannot express
the per-motor addressing this case verifies. Test harness gains an
sdl-gamepad specifier branch (Tx-only endpoints) alongside the BLE default.
Registers the SDL gamepad communication manager on all platforms (no OS
gate, unlike XInput) and logs a warning on Windows when both gamepad
managers are enabled, since the same physical controller may then appear as
two devices. Flag plumbing, default-false options, and the registration
decision are unit-tested.
Non-default feature (SDL3 source build is too heavy for default library
consumers): enabling it registers the SDL gamepad manager, cross-platform
with no OS gate. Manager registration is refactored into a named-returns
helper asserted by a feature-gated test.
On all three OS matrix entries: run the in-process client tests with
--features sdl-gamepad-manager, assert via cargo tree that sdl3 is absent
from default features and present with the feature, and run the SDL3
headless threading spike example (automated half of the Phase 0 gate; CI
runners have no controllers, so an empty gamepad set passes).
…devices

SDL3 on macOS routes wired gamepads to GCController (MFI) by default: the
hidapi device drivers decline wired pads (DevSrvsID paths) while MFI is
enabled, and GCController discovery only fires via Cocoa runloop
notifications - which a headless, no-video process never spins. Result: zero
gamepads ever discovered, even on the main thread with event pumping.

Verified against SDL 3.4.14 source (SDL_PLATFORM_MACOS && SDL_JOYSTICK_MFI
guard in SDL_hidapi_xboxone.c) and on hardware: with SDL_JOYSTICK_MFI=0 the
wired Xbox One S enumerates via hidapi and set_rumble succeeds. iOS keeps
the MFI default (GCController is the only backend there).

The spike example mirrors the factory's platform policy. Windows discovery
failure remains under investigation (likely analogous WGI behavior; needs an
on-hardware SDL_LOGGING=3 run).
On-hardware diagnosis (wired Xbox One S, USB): with the MFI=0 hidapi
policy, wired pads enumerate but rumble silently fails - Apple exposes
only read-only shortened HID reports for them, and the working path
(GCController/CoreHaptics) requires a main-thread runloop this
architecture deliberately does not host. SDL2 has the same Apple-side
guard, so a version switch would not help.

Per operator decision, v1 is Bluetooth-only on macOS: scan probes each
candidate's SDL connection state and skips Wired pads with a logged
explanation pointing users at Bluetooth pairing. Verified on hardware:
SDL reports the wired pad as connection Wired. Bluetooth pads and all of
Windows/Linux are unaffected.

The spike example now enables verbose SDL logging and prints each pad's
connection state, so a single run is a complete diagnostic - this is the
tool for the still-open Windows zero-device diagnosis (likely the
analogous XGIP/WGI claiming dance).
On-hardware validation (Bluetooth DualSense, macOS) showed rumble stops
after a few seconds despite the 60s arm - some controllers do not honor
long durations. Re-sending the current non-zero command every second
keeps the effect alive; the long finite arm stays as a safety net if a
keepalive is missed. Same refresh_decision machinery, tighter interval.
SDL gamepads expose two, three, or four logical rumble output channels
(main low/high, trigger left/right) depending on hardware capabilities.
The existing configuration system hardcodes the two-channel main layout
and cannot reconcile a cached user definition against a different base
definition, so trigger channels could never be advertised.

- Add a layout/constant module to the device config crate sharing the
  SDL base identifiers, protocol variants, selector keys, and the
  visible-index to logical-channel-slot mapping for the three layouts.
- Add a neutral DeviceDefinitionSelection value (selected base key plus
  canonical hardware name) and an optional carrier on Hardware, so
  connectors can declare which base definition they selected without
  SDL dependencies leaking into buttplug_server or identity fields.
- Add DeviceConfigurationManager::device_definition_with_selection,
  which resolves the selected base explicitly, reconciles cached user
  definitions (preserving user UUID, index, display name, allow/deny,
  message gap, and surviving-channel customizations; refreshing the
  canonical name and base id), and errors on invalid selections
  instead of silently falling back.
- Register the four-channel and trigger-only base configurations with
  fixed UUIDs and channel descriptions alongside the default two-
  channel base in the SDL protocol YAML.
SDL gamepads report main rumble and trigger rumble as independent
capabilities. Discovery now probes both (pure property reads, no motor
activation) and pads with neither capability are not published; failed
probes and capability changes are rechecked on later scans and on the
actual leased open. The macOS wired-pad exclusion is unchanged.

The protocol-to-hardware transport carries the complete four-slot
desired state (low, high, left trigger, right trigger as u16 LE, 8
bytes) in one command. The protocol handler derives its visible-index
to logical-slot mapping from the final definition's protocol variant,
never from feature count. The ownership thread dispatches only pairs
the pad reports as supported - including zero, stop, and keepalive
calls - tracks last-applied state per pair, refreshes each active pair
before expiry, and on any pair failure best-effort zeroes both
supported pairs, removes the lease, and delivers removal so no
successfully applied half keeps running untracked.

Adds a test-only Shutdown command routed through the production loop
teardown with a joinable spawn seam, plus deterministic coverage for
the capability matrix, probe retries, fresh connect snapshots, name
fallbacks (including whitespace-only names), per-pair lifecycle and
keepalive, partial-pair failure cleanup, stale-generation isolation,
hardware packet validation, and supported-pair dispatch.
Integration harness: test devices can carry an explicit SDL base
selection that flows through the production definition-selection
path, so fixtures exercise real capability-driven layout selection
rather than prebuilt definitions. Fixtures cover main-only,
main+trigger, and trigger-only layouts with eight-byte packet
assertions, plus a disabled-channel scenario proving filtered
features keep their logical channel slots and receive no commands.
Named regressions assert harness metadata propagation, v4 advertised
feature order and descriptions straight from the client device, v3
advertisement, and per-channel command routing.

Diagnostics: the sdl3_thread_spike example prints both capability
booleans alongside name and connection state; it remains purely
non-actuating.

Docs: the crate README documents the three layouts, capability-based
per-connection selection, skip-and-retry policy for output-less
devices, naming fallback, connection-scoped identity, settings
reconciliation (including that removed channels lose customizations),
internal eight-byte framing, simple trigger rumble versus adaptive
resistance, and a manual hardware release checklist. A
sdl_docs_contract_check test pins the documented contract to the
implementation.
The shutdown tests waited on the SDL thread join through
spawn_blocking inside a tokio timeout, which bounds the wait but
cannot cancel the blocking task: a hung thread would still wedge the
test runtime after the timeout fired, defeating the bounded-completion
guarantee the seam is meant to prove. Poll is_finished on the async
timer with a deadline instead, joining only after the thread has
actually exited.
Merge origin/dev's protocol additions (luvmazer, sexverse-v6, joyhub
updates) with the sdl-gamepad definitions in the generated config.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant