Protocol 2: negotiated versioning, the POINTER frame, and feedback to a Direct-claimed pad - #47
Merged
Conversation
… a Direct-claimed pad The client half of the protocol-2 push, following satellite#86/#87 and dish-android#174/#175, and the companion to the dish-windows change of the same name. No virtual controllers: this client forwards physical pads, so everything below is about what a real pad can send and be driven with. ## Versioning is negotiated now The client offers 2; the satellite settles the session on that offer and echoes it back. The SETTLED version keys the wire frames, not the offered one, because a pre-versioning satellite ignores the offer and answers 1 -- reading the offer back would send 19-byte frames to a 16-byte decoder. A 409 whose range still overlaps ours is re-offered at the satellite's ceiling instead of dead-ending the user, and one that does not names which end has to update. An absent `protocolVersion` was being read as this build's version rather than as 1. That is fixed here, and it is the bug that would have made every pre-versioning satellite look like it had agreed to protocol 2. ## The POINTER frame (0x000C, v2, 19 bytes) The click moved out of the finger flags into a buttons byte and a signed wheel was appended. This client has no on-screen trackpad, so the only producer is a pad's own touch surface: it reports one click and no wheel, and the right, middle and scroll fields ride as zero rather than being synthesised from gestures the user never made. dish-android forwards a physical pad the same way. ## Feedback to a Direct-claimed pad `UsbDeviceGateway` gained a `writeOutputReport` seam, so a raw-HID claim now drives the pad as well as reading it. The bytes are built by the new pure `core/input/UsbOutputReports.h` (byte-exact, host-tested); hidraw wants no framing of its own, so the builder's length is the transfer length and the gateway only retries EINTR. That lands four things on a Direct claim: rumble and the lightbar (which previously fired only on the SDL path -- the "no output write path" note in `CapabilitySolver` is gone), `MSG_TRIGGER_EFFECTS` replaying the game's own DualSense blocks verbatim, and `MSG_PLAYER_LEDS` driving the DualSense bar and the Switch Pro's player lights. `core/reducer/FeedbackRouting.h` is the single owner of both "may the descriptor claim this" and "where does a dispatch go", so an advertised capability and a delivered message cannot disagree. Standard (SDL) carries rumble and a colour and nothing else; Direct carries everything its family has, but only while the claim is live. ## Moonlight Three real gaps closed here, not just additions: - Motion was forwarded as the satellite's raw fixed-point int16 cast to a float, where the wire wants deg/s and m/s^2. A pad at rest read correctly and a moving one read as spinning at tens of thousands of degrees per second. The conversion now lives in one owner (`MoonlightTelemetry.h`) shared by both transports. - Motion was gated on a single session-wide flag, so one game opening one sensor started the stream for every pad on that host, at whatever rate the hardware polled. It is per (pad, motion type) at the requested rate now. - A Moonlight-bound pad never declared `CAP_BATTERY` and never forwarded its charge, though the pad was already publishing one. Plus `CONTROLLER_TOUCH`: the pad's full-state frame is diffed into per-pointer DOWN / MOVE / UP, with a tracking-id change closing the old contact first so the host's pointer set cannot leak an id that is never closed. `RUMBLE_TRIGGERS` folds onto the body motors, which the arrival packet already advertised. No pad this client can claim has impulse-trigger motors -- xpad binds an Xbox pad as evdev-only and publishes no hidraw node for it -- so the fold is the honest maximum rather than a shortcut. The two host rumble streams mix per motor by maximum, so neither can cancel the other; before, whichever arrived last won and a trigger update could cancel a live body rumble. ## Tests New suites: protocol negotiation, feedback routing, the output report builders (byte-exact, every offset and every must-stay-zero byte), the touch differ, the telemetry units and motion gate, the trigger-rumble mix, the POINTER frame and the two feedback messages, and the manager's OUT-direction dispatch over a recording gateway. The pure core headers were syntax-checked locally; the full build, ctest, clang-format, clang-tidy and qmllint runs are CI's (this branch was developed on a Windows box). The translation gate passes locally: six catalogues, 1094/1094 each.
… mapping Three things a Windows box could not catch by compiling: - AppModel.cpp uses the new MoonlightTelemetry / MoonlightTouchDiffer headers and std::make_shared, and AppModel.h names proto::kTriggerEffectBlockBytes. All four were resolving transitively or not at all; include them directly. - The Moonlight rumble sink had the two motors crossed. The wire's lowFrequency is the LARGE motor -- what SDL calls low_frequency_rumble and what dish-windows and dish-android both map to `strong` -- and this sink passed (high, low) into applyRumble(strong, weak). The session now hands the sink motors rather than wire fields, so the mapping lives in one place and the actuator cannot re-derive it differently.
Three findings, all in code this branch added, fixed rather than suppressed: - `bugprone-unchecked-optional-access` on `directTarget`. The dereference was guarded, but by the second half of an `||` whose first half tests a different object, which the analysis cannot follow. Splitting the two conditions makes it provable and says the truer thing anyway: a model being tracked and a model holding a live claim are different facts. - `modernize-use-auto` twice, on a `static_cast` whose type was spelled on both sides.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Protocol 2: negotiated versioning, the POINTER frame, and feedback to a Direct-claimed pad
The client half of the protocol-2 push, following satellite#86/#87 and
dish-android#174/#175, and the companion to the dish-windows change of the same
name. No virtual controllers: this client forwards physical pads, so everything
below is about what a real pad can send and be driven with.
Versioning is negotiated now
The client offers 2; the satellite settles the session on that offer and echoes
it back. The SETTLED version keys the wire frames, not the offered one, because
a pre-versioning satellite ignores the offer and answers 1 -- reading the offer
back would send 19-byte frames to a 16-byte decoder. A 409 whose range still
overlaps ours is re-offered at the satellite's ceiling instead of dead-ending
the user, and one that does not names which end has to update.
An absent
protocolVersionwas being read as this build's version rather thanas 1. That is fixed here, and it is the bug that would have made every
pre-versioning satellite look like it had agreed to protocol 2.
The POINTER frame (0x000C, v2, 19 bytes)
The click moved out of the finger flags into a buttons byte and a signed wheel
was appended. This client has no on-screen trackpad, so the only producer is a
pad's own touch surface: it reports one click and no wheel, and the right,
middle and scroll fields ride as zero rather than being synthesised from
gestures the user never made. dish-android forwards a physical pad the same way.
Feedback to a Direct-claimed pad
UsbDeviceGatewaygained awriteOutputReportseam, so a raw-HID claim nowdrives the pad as well as reading it. The bytes are built by the new pure
core/input/UsbOutputReports.h(byte-exact, host-tested); hidraw wants noframing of its own, so the builder's length is the transfer length and the
gateway only retries EINTR.
That lands four things on a Direct claim: rumble and the lightbar (which
previously fired only on the SDL path -- the "no output write path" note in
CapabilitySolveris gone),MSG_TRIGGER_EFFECTSreplaying the game's ownDualSense blocks verbatim, and
MSG_PLAYER_LEDSdriving the DualSense bar andthe Switch Pro's player lights.
core/reducer/FeedbackRouting.his the single owner of both "may thedescriptor claim this" and "where does a dispatch go", so an advertised
capability and a delivered message cannot disagree. Standard (SDL) carries
rumble and a colour and nothing else; Direct carries everything its family has,
but only while the claim is live.
Moonlight
Three real gaps closed here, not just additions:
float, where the wire wants deg/s and m/s^2. A pad at rest read correctly and
a moving one read as spinning at tens of thousands of degrees per second. The
conversion now lives in one owner (
MoonlightTelemetry.h) shared by bothtransports.
started the stream for every pad on that host, at whatever rate the hardware
polled. It is per (pad, motion type) at the requested rate now.
CAP_BATTERYand never forwarded itscharge, though the pad was already publishing one.
Plus
CONTROLLER_TOUCH: the pad's full-state frame is diffed into per-pointerDOWN / MOVE / UP, with a tracking-id change closing the old contact first so the
host's pointer set cannot leak an id that is never closed.
RUMBLE_TRIGGERSfolds onto the body motors, which the arrival packet alreadyadvertised. No pad this client can claim has impulse-trigger motors -- xpad
binds an Xbox pad as evdev-only and publishes no hidraw node for it -- so the
fold is the honest maximum rather than a shortcut. The two host rumble streams
mix per motor by maximum, so neither can cancel the other; before, whichever
arrived last won and a trigger update could cancel a live body rumble.
Tests
New suites: protocol negotiation, feedback routing, the output report builders
(byte-exact, every offset and every must-stay-zero byte), the touch differ, the
telemetry units and motion gate, the trigger-rumble mix, the POINTER frame and
the two feedback messages, and the manager's OUT-direction dispatch over a
recording gateway. The pure core headers were syntax-checked locally; the full
build, ctest, clang-format, clang-tidy and qmllint runs are CI's (this branch
was developed on a Windows box). The translation gate passes locally: six
catalogues, 1094/1094 each.