e5166c6e6e7a515570b1e53635a6987ad8cb0647
60 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e5166c6e6e |
fix(host/steam): load vhci_hcd at boot on sysext hosts so the Deck pad is Steam-Input-promotable
ci / web (push) Successful in 1m2s
ci / docs-site (push) Successful in 1m9s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
decky / build-publish (push) Successful in 30s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
apple / swift (push) Successful in 4m25s
ci / bench (push) Successful in 6m50s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5m49s
docker / deploy-docs (push) Successful in 26s
windows-host / package (push) Successful in 8m40s
arch / build-publish (push) Successful in 11m22s
deb / build-publish (push) Successful in 12m27s
android / android (push) Successful in 16m50s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m19s
apple / screenshots (push) Successful in 19m19s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m14s
ci / rust (push) Successful in 25m14s
The virtual Steam Deck pad only appears in the host's Game Mode (and is navigable) when it arrives as a real USB device via the usbip/vhci_hcd transport — Steam Input won't promote the UHID hid-steam fallback (Interface: -1). The host runs as an unprivileged --user service, so it cannot modprobe vhci_hcd itself; the module must be loaded at boot and the vhci attach/detach sysfs files chgrp'd to the `input` group by the udev rule. Packaging ships modules-load.d/punktfunk.conf + 60-punktfunk.rules under the sysext's /usr/lib, but a systemd-sysext image MERGES after systemd-modules-load and early udev have already run, so on a plain reboot of a sysext host (e.g. Bazzite) those files are read too late: vhci_hcd is never loaded, usbip fails, and the pad silently degrades to non-promoted UHID — the controller vanishes from Game Mode. (deb/arch/rpm are unaffected: real /usr is present at early boot.) Fix: sysext post_merge now mirrors BOTH files into real /etc (read at the normal early-boot time, shadowing the /usr copies by filename; refreshed every merge since neither is user config), then reloads udev, modprobes vhci-hcd, and re-triggers the vhci platform device for the live session. Also raise the UHID-fallback log INFO->WARN with an actionable hint. Verified on the .41 sysext host: after the /etc mirror, unloading vhci_hcd and restarting systemd-modules-load (the real reader of /etc/modules-load.d) reloads the module; a udev coldplug trigger makes attach/detach root:input 0660; the unprivileged host user can then write attach — the exact working precondition for the usbip transport, now durable across reboot. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
76be4c3e12 |
feat(gamepad): multi-controller support on the native plane
Host was already built for 16 pads; the blocker was every client hard-coding pad 0. This lands the host-side + reference-client contract: - input.rs: new wire kinds GamepadArrival=14 (declares a pad's type: code=GamepadPref byte, flags=pad) and GamepadRemove=13 (flags=seq<<24|pad, shares the snapshot seq space via encode/decode_gamepad_remove). - pf-client-core/gamepad.rs: reworked from a single `open` pad to a slots: Vec<Slot> model — every forwarded controller gets a stable lowest-free wire index held for its lifetime, per-slot held/axis/touch/ rumble state, GamepadArrival on open + GamepadRemove on close, and feedback routed back per wire index. Automatic forwards all real pads; a pin forces single-player. - punktfunk1.rs: replaced the single-session PadBackend enum with a Pads router — per-pad kinds[]/owner[] arrays, lazily-created per-kind managers, pure route_decision keeping a live device in its manager across a kind change (no ghost/dup). Input thread seq-gates GamepadRemove (clears the pad_mask bit, resets rumble) and applies GamepadArrival kinds. - inject linux/windows backends: add the two new no-op InputKind arms. Native/session + default-Windows clients (both spawn punktfunk-session) inherit this. 57 core + 33 client-core + 272 host tests green; clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
19e9828e8d |
fix(host/windows): force off a stale rumble residual the game abandoned
XInput vibration is level-triggered — it persists until the game sets it to zero — so a game that latches a rumble and then stops calling XInputSetState (a residual left at a menu/loading screen, or a plain forgotten stop) drones to the client forever (measured: a stuck (0,512) resent every 500 ms for 5.5 minutes). A real controller stops when the app stops driving it; mirror that. Keyed on game ACTIVITY (any SET_STATE, even an unchanged one), so a rumble the game keeps asserting is never cut — only an abandoned residual is; kept above SDL's ~2 s resend so an SDL-driven host game refreshes the activity clock before it fires. This is the game-facing half of the rumble-stop story; the wire-facing half is the self-terminating envelope model in the following commit. They compose: this bounds a game-abandoned rumble at the host, envelopes bound a host-abandoned rumble at the client. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7b25868a19 |
fix(input): rock-solid held gamepad state — Android device pinning + seq'd snapshots
apple / swift (push) Successful in 1m12s
release / apple (push) Successful in 8m54s
windows-host / package (push) Successful in 7m35s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m49s
ci / web (push) Successful in 1m16s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m8s
ci / docs-site (push) Successful in 1m21s
android / android (push) Successful in 12m25s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 56s
decky / build-publish (push) Successful in 18s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m23s
apple / screenshots (push) Successful in 5m45s
arch / build-publish (push) Successful in 11m9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
ci / bench (push) Successful in 5m26s
flatpak / build-publish (push) Failing after 33s
ci / rust (push) Successful in 17m39s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9m38s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8m39s
docker / deploy-docs (push) Successful in 22s
deb / build-publish (push) Successful in 11m51s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m8s
Two causes behind one field report (a held trigger jittering mid-game, Android client → Windows host): Android folded joystick ACTION_MOVEs from EVERY device into one axis state. A controller's joystick-classified sibling node (DualSense/DS4 motion sensors) or a second/drifting pad reports every pad axis as 0, so a held trigger flapped value→0→value on each event interleave. The mapper now qualifies the source DEVICE (its source classes must include GAMEPAD — a joystick event's own source is always plain JOYSTICK), pins to one deviceId until that device disconnects, and merges LTRIGGER/BRAKE (and RTRIGGER/GAS) with max, the same fold as the Controllers probe. Underneath, gamepad input rode per-transition events over unreliable, unordered QUIC datagrams — no sequence numbers, sharing the 4 KiB oldest-first-shed send buffer — so one dropped or reordered event corrupted held pad state until the NEXT change. Gamepad state now travels the way rumble already does: idempotent state, refreshed. InputKind::GamepadState packs the whole pad + a wrapping u8 seq into the existing 18-byte layout; the host advertises HOST_CAP_GAMEPAD_STATE (Welcome trailing byte, offset 67) and applies snapshots through a per-pad stale-seq gate, skipping frame emits for unchanged refreshes; the client folds embedder events into snapshots inside NativeClient's input task (send on change + 100 ms refresh of touched pads), so the SDL clients (Linux/Windows/session), Android, and Apple (C ABI) are all covered with zero capture-code changes. Either end older ⇒ the legacy per-transition path runs unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
dc834ea478 |
fix(rumble): accept the v2 vibration valid-flag; bound stale buzz to 1.5 s
Two hardenings against runaway vibration (one Deck session buzzed on a ~2 s cadence): the DualSense output-report parse now also treats valid_flag2 COMPATIBLE_VIBRATION2 (data[39] BIT2) as rumble-carrying — a writer hardcoding the ≥2.24-firmware convention previously had its rumble INCLUDING stops silently ignored, and a missed stop re-sends stale nonzero state forever via the host's 500 ms refresh. And the client's SDL rumble duration drops 5 s → 1.5 s: long enough that a couple of lost 500 ms refreshes don't gap genuine rumble, short enough that stale state dies on its own. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e6d9454251 |
feat(host): fold Steam Deck to DualSense on Windows; split-pad touch + pad clicks
A Deck client asking for 'steamdeck' on a Windows host resolved to Xbox360, whose apply_rich is a no-op — gyro and both trackpads silently discarded. Windows already ships a full DualSense backend (UMDF driver, touchpad + motion, wire units 1:1), so pick_gamepad now folds SteamDeck -> DualSense there. The three near-identical DualSense-family appliers (Linux UHID, Windows DualSense, Windows DS4) are hoisted into one shared dualsense_proto::DsState::apply_rich, with two mapping upgrades for Steam dual-pad clients everywhere: * the Deck's two pads SPLIT the single DualSense touchpad — left pad -> contact 0 on the left half, right pad -> contact 1 on the right half — mirroring the physical thumb layout and the split-pad zones games and Steam Input already use (the left pad was previously dropped outright) * TouchpadEx pad clicks now press the touchpad-click button (persisted in DsState::touch_click, OR-ed in by both serializers; previously dropped by every DualSense-family backend, Linux included) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9ca672d434 |
fix(host): trackpad Y rides the wire in screen convention, flips at the Deck boundary
Both Deck trackpads were inverted on the virtual pad (first live Deck-to-Deck session): the client sends SDL's screen-convention coordinates (+y down) onto the wire, but the Deck's raw report fields are stick convention (+y up) — and Steam Input parses our report as real Deck hardware — while the host applier passed the value straight through. Pin the wire meaning down (quic.rs: TouchpadEx/Touchpad are screen convention, +x right +y down — what every client capture API produces) and translate to device-raw in the host applier, the layer that knows the quirk: steam_proto's apply_rich now negates y (saturating: -32768 has no i16 negation) for both TouchpadEx surfaces and the DualSense-standin Touchpad arm, which carried the same latent inversion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
959ec30003 |
fix(client-linux,host): Deck video defaults to software decode + input-interception diagnostics
Video (Deck): the VAAPI zero-copy path renders corrupt/gray/washed-out on the Deck — root-caused to Mesa >= 25.1 exporting radeonsi VCN decode surfaces TILED (the Flatpak runtime's Mesa 26 drives both the decoder and GTK's GL, and GTK's tiled-NV12 dmabuf import mishandles it; desktop Tier-1 validations ran distro Mesa with linear export). `auto` now resolves to software on a Deck (clean, correct-colour, easily handles 1280x800 HEVC); PUNKTFUNK_DECODER=vaapi still forces the hw path, with the descriptor modifier dump + GSK_RENDERER as the bisect levers. Also reserve extra_hw_frames=4 on the VAAPI decoder: the presenter pins mapped surfaces past receive_frame, and the fixed pool recycling a surface the renderer still samples is intermittent block corruption anywhere. Input (Deck): with Steam Input ON for Punktfunk, SDL sees only Steam's virtual X360 pad — the right trackpad arrives as a plain right stick and the left trackpad/paddles/gyro not at all, silently. The client now checks once the post-attach enumeration settles and raises a toast + warn naming the fix (disable Steam Input for the shortcut). The host logs a one-shot warning when InputPlumber is running (Bazzite default) since it can grab the virtual Deck pad and re-emit it under a different identity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
796d381926 |
fix(windows): opt-in pad-driver file logs + size-capped service log rotation
Two disk-write fixes: - pf-xusb/pf-dualsense no longer write C:\Users\Public\pf*-driver.log unconditionally — the file log is now opt-in (debug builds, or the PFXUSB_DEBUG_LOG / PFDS_DEBUG_LOG system env var), mirroring the audit-§4.4 fix pf-vdisplay already got: a release driver never writes the world-writable Public file (info-leak/DoS surface), and the per-report OUTPUT/SET_STATE hex dumps stop being a sustained per-rumble disk-write path during gameplay. OutputDebugStringA stays unconditional; the host's driver-silence WARN and the gamepad-driver-health failure-mode table now say the log is opt-in. - service.log/host.log get one-generation rotation: at each (re)open a file over 10 MB is renamed to .old, so a crash-restart loop or a RUST_LOG=debug left in host.env can't grow the append-forever logs without bound. Rotation runs only before an open (never under a live appender — host.log's handle lacks FILE_SHARE_DELETE, so a racing rename harmlessly fails). Windows CI compile/clippy pending (drivers workspace + host are not Linux-cross-checkable); rides along with the next pad-driver redeploy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8b47be668f |
feat(host/windows): seal the host↔driver channels (frame + gamepad, proto v2)
Frame ring (pf-vdisplay) and both gamepad SHM channels move off named Global\ objects (openable by any sibling LocalService) to UNNAMED sections/events whose handles the host DuplicateHandles into the driver's verified WUDFHost with least access — frame delivery over the SYSTEM+admins-only IOCTL_SET_FRAME_CHANNEL, pads over a 32-byte named bootstrap mailbox (pid + handle value only, DoS-bounded; HID minidrivers have no control device). Driver-validated pad_index kills cross-pad redirects; v1↔v2 mixes fail closed with diagnosis logs on both sides. Sibling-LocalService denial proven empirically (design/idd-push-security.md, design/gamepad-channel-sealing.md). Driver-side raw ops now live behind pf-umdf-util (checked shm accessors, the forbid(unsafe_code) ChannelClient state machine, WDF request tokens) — the pad drivers' logic is 100% safe Rust; whole drivers workspace clippy-gated in CI. driver install --gamepad now sweeps SWD\punktfunk phantom devnodes: a re-created SwDevice REVIVES the old devnode with its previously-bound driver (never re-ranks), so an upgrade otherwise leaves the old driver serving — or, across the v1→v2 fence, a dead pad (found live on the RTX box). On-glass validated on the RTX 4090 box: frame path 7007 frames p50 2.06 ms cross-machine; DualSense + XUSB "sealed pad channel mapped"/proto=2 attach via both the test harness and a real streaming session; phantom-sweep repro. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6bbe7ae137 |
feat(host/windows,drivers): gamepad driver attach/heartbeat health surfaced in logs
The gamepad drivers have no IOCTL plane (hidclass gates the stack), so until now the host had ZERO visibility into whether a driver ever bound: a pad could be "created" with no driver installed and nothing was logged. Two health fields are carved from reserved shm space (layout-compatible; pf-driver-proto pins the offsets): driver_proto — stamped by pf-xusb at device add + per serviced XInput IOCTL (movement = the game-visible path) and by pf-dualsense/DS4 from its ~125Hz timer — and driver_heartbeat. Host-side, every pad owns a DriverAttach watcher fed from the existing service() poll: INFO on attach (WARN on proto mismatch), and after 3s of silence ONE diagnosis WARN combining a cached pnputil /enum-drivers store check, the devnode's CM problem code (CM_Locate_DevNodeW/CM_Get_DevNode_Status on the instance id now captured from the create callback, with plain-language hints: 28 = not installed, 52 = signature/Memory Integrity, …) and the driver's debug log path. Also fixes a real bug both SwDeviceCreate wrappers shared: the 10s WaitForSingleObject result was ignored and the callback HRESULT zero-initialised, so a PnP timeout read as SUCCESS (now E_FAIL init + explicit timeout error). Failure-mode table: design/gamepad-driver-health.md. Linux workspace green; Windows host + drivers CI-compile only, on-box recipe at the bottom of the design doc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
76e2a1f5a7 |
fix(host/linux,clients/android): honor the host/device keyboard layout in keymaps
wlroots injector: the virtual keyboard keymap now defers to the standard XKB_DEFAULT_RULES/MODEL/LAYOUT/VARIANT/OPTIONS env vars (libxkbcommon built-ins as fallback) instead of hardcoding evdev/pc105/us, matching the libei path where the session compositor's own keymap applies. Android: Keymap gains the same positional-key coverage for non-US layouts (+ tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
03ad2406b6 |
fix(host/windows): layout-correct keyboard injection - semantic vs positional VKs
First-party punktfunk clients send US-positional VKs (the physical key's US-layout VK), GameStream/Moonlight clients send layout-semantic VKs (Sunshine's model). The SendInput injector previously resolved everything through the SYSTEM service's layout - on a German host that is the y/z swap and u-umlaut-on-o-umlaut scramble. GameStream ingest now tags its key events KEY_FLAG_SEMANTIC_VK (stripped from punktfunk/1 wire events so a network client can't flip the convention); the injector maps semantic VKs under the foreground app's layout and positional VKs through a fixed scancode table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f3986b8f82 |
feat(host/steam): shippable usbip/vhci_hcd virtual Deck + client leave-shortcuts
Steam Deck pass-through (design/steam-deck-passthrough-plan.md), code-complete + all CI checks green on Linux + adversarially reviewed; on-glass validation pending: - usbip/`vhci_hcd` virtual Deck transport (inject/linux/steam_usbip.rs) for non-SteamOS hosts (Bazzite/generic) — presents a real interface-2 USB Deck so Steam Input promotes it. In-process vhci attach (loopback OP_REQ_IMPORT handshake → sysfs attach) with a bounded `usbip`-CLI fallback; detach on drop. - Backed by a vendored, libusb-free trim of the `usbip` crate (crates/punktfunk-host/vendor/usbip-sim, MIT + NOTICE; host/cdc/hid + rusb/nusb removed; interrupt-IN paced by bInterval). - Selection ladder raw_gadget (SteamOS fast-path) → usbip (universal) → UHID, with PUNKTFUNK_STEAM_USBIP / PUNKTFUNK_USBIP_ATTACH knobs. - Shared Deck descriptors + the 0x83/0xAE feature contract + a Steam-accepted serial consolidated into steam_proto.rs; the raw_gadget backend reuses them. - Linux client leave-shortcuts: Ctrl+Alt+Shift+D + holding the escape chord (L1+R1+Start+Select) >=1.5s end the session (short press still exits fullscreen); the chord state resets across sessions. Also bundles in-progress work already staged in the tree: - host(kwin): xdg-output logical-geometry mapping so the KWin fake_input backend places absolute coordinates correctly under display scaling. - docs: design/README index entries + design/controller-only-mode.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a41eac933f |
feat(host/steam): default the gadget Deck on for SteamOS (glass-confirmed)
The virtual Steam Deck is validated glass-to-glass on a Deck: it appears as a
distinct second Steam controller, a held A drives Steam's overlay ("Resume
Game"), and a button press registers in a real game (confirmed in-game).
gadget_preferred() now defaults ON for SteamOS hosts (/etc/os-release ID=steamos
or ID_LIKE), OFF elsewhere where the universal UHID path stays the default;
PUNKTFUNK_STEAM_GADGET=1/0 forces it. A Deck-as-host with a physical Deck never
reaches this path — resolve_gamepad's conflict gate degrades SteamDeck → DualSense
first, so the two-Deck case never happens in production (it was only a test-rig
confound on the dev Deck).
The feature is complete: a virtual Steam Deck that Steam Input recognizes +
promotes, churn-free, with input flowing to games. Workspace clippy/fmt/test
green. Not pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
8b46b821f2 |
feat(host/steam): harden the gadget feature contract — fixes the evdev churn
The virtual Deck's gamepad evdev was churning (destroyed + recreated) because Steam kept re-probing: GetControllerInfo reads HID feature reports, and the gadget served zeros for them. Captured the real contract off a physical Deck (packaging/linux/steam-deck-gadget/get_deck_attrs.c, hidraw HIDIOCGFEATURE — usbmon truncates to 32B) and implemented it in steam_gadget.rs::feature_reply: - 0x83 GET_ATTRIBUTES_VALUES: [83, 2d, 9×(attr-id, u32-LE)] — product id 0x1205, a per-instance unit serial (0x0a/0x04, so a gadget never collides with a real Deck or another gadget), and the capability attrs (0x09=0x2e, 0x0b=0x0fa0, rest 0). - 0xAE GET_STRING_ATTRIBUTE: [ae, len, attr, ascii] — serial (attr 1) / board serial (attr 0). - other commands (0x87 settings): echo the last write. Validated on the Deck: 1 connect / 0 disconnect / 1 gamepad evdev (was constant churn), Steam activates the gadget cleanly (no GetControllerInfo failed, no zombie) and emits its X-Box 360 pad. usbmon on the gadget's bus confirms our state reports (pressed button at byte 8) are delivered on the interrupt-IN and consumed by hid-steam — so with M1/M2's byte-8→BTN_SOUTH decode the input chain is proven end-to-end. Remaining: a foreground-game confirmation of Steam Input's XInput mapping, then default the gadget on for SteamOS. Workspace clippy/fmt/test green. Not pushed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3cc8f7dc65 |
feat(host/steam): raw_gadget Deck host backend (Steam-Input path, opt-in)
Port the proven raw_gadget virtual Deck to a Rust host gamepad backend, the SteamOS-only transport that gets Steam Input to actually promote the Deck. - inject/linux/steam_gadget.rs (new): SteamDeckGadget — a userspace raw_gadget emulator of the real 3-interface USB Deck (mouse=0/keyboard=1/controller=2, 28DE:1205) on a dummy_hcd loopback UDC, descriptors captured from a physical Deck, answering every control transfer incl. the HID feature reports. Driven by the same steam_proto::serialize_deck_state as the UHID pad; rumble feedback via parse_steam_output. The raw_gadget UAPI is funneled through 4 documented ioctl wrappers (the crate denies undocumented unsafe). - inject/linux/steam_controller.rs: the manager pad is now a DeckTransport enum (Uhid | Gadget); ensure() prefers the gadget when PUNKTFUNK_STEAM_GADGET=1 (best-effort modprobe dummy_hcd+raw_gadget), gracefully falling back to the universal UHID SteamDeckPad. write/pump/heartbeat dispatch through the enum. Validated on a real Deck via a static musl harness that #[path]-includes the module: enumerates, hid-steam binds + reads our serial + creates the Steam Deck + Motion Sensors evdevs — identical to the C PoC. Caught a real portability bug: raw_gadget's no-arg ioctls (RUN/CONFIGURE/EP0_STALL) reject a non-zero `value` with EINVAL, and on musl an omitted ioctl vararg is a garbage register — so they must pass an explicit 0. Opt-in (default off) while the Steam GetControllerInfo feature contract is hardened (to stop the gamepad-evdev churn). Workspace clippy/fmt/test green. Not pushed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
71e0cace77 |
feat(host/steam): M5 — fallback remap, motion rescale, degrade ladder
Keep the rich Steam inputs from silently dropping when the resolved backend
isn't the virtual hid-steam device, and fix a cross-device motion-scale bug.
- inject/proto/steam_remap.rs (new, pure + unit-tested):
* motion_wire_to_deck — the wire carries DualSense-convention units (20 LSB/
deg.s gyro, 10000 LSB/g accel — what every client capture emits), but the
Deck's hid-steam report wants 16 LSB/deg.s + 16384 LSB/g. The Deck backend
now rescales (gyro x16/20, accel x16384/10000): a real Deck<->Deck gyro/
accel correctness fix (the DualSense/DS4 backends consume the wire 1:1).
* fold_paddles + RemapConfig (PUNKTFUNK_STEAM_REMAP=paddles=drop|stickclicks|
shoulders, default drop) — the DualSense + DS4 managers fold a client's back
grips onto standard buttons rather than dropping them (those pads have no
back-button HID slot; the uinput Xbox pad already exposes them as Elite
paddles BTN_TRIGGER_HAPPY5-8).
- resolve_gamepad: a runtime degrade ladder — a UHID backend (DualSense / DS4 /
Steam Deck) on a host where /dev/uhid isn't writable now falls back to the
uinput Xbox 360 pad instead of a dead controller (the device-create would
just fail). Separate from pick_gamepad's compile-time platform check, so the
existing pick_gamepad tests are untouched.
- Delete the throwaway M0/M1 spike (src/bin/steam_uhid_spike.rs) — M2's
#[ignore]d backend test subsumes its validation, and removing it frees
steam_proto to reference steam_remap cleanly.
On-box backend test still green; workspace clippy/fmt/test green (incl. the new
steam_remap tests). Deferred as optional RemapConfig growth: gyro->mouse /
trackpad->stick synthesis on an Xbox target (no slot — documented drop today).
Not pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
7cc2ed85cb |
feat(proto/steam): M3 — rich Steam wire (back buttons + 2nd trackpad)
Carry the rich Steam Controller / Steam Deck inputs end-to-end on the wire — strictly additive + forward-compatible (unknown kinds/bits drop on old peers). Core (punktfunk-core): - input.rs: BTN_PADDLE1..4 + BTN_MISC1 in Moonlight's buttonFlags2<<16 namespace (so the GameStream paddle path and native grips share one host injector map; Steam L4/L5/R4/R5 reuse the four Xbox-Elite paddle slots). - quic.rs: RichInput::TouchpadEx (kind 0x03 — surface 0/1/2, touch+click, signed coords, pressure; the second trackpad the single Touchpad can't express) and HidOutput::TrackpadHaptic (kind 0x04 — the SC voice-coil pulse). Round-tripped. - abi.rs: PUNKTFUNK_GAMEPAD_STEAMDECK=6 / _STEAMCONTROLLER=5, the paddle bits, RICH_TOUCHPAD_EX / HIDOUT_TRACKPAD_HAPTIC constants. from_hid packs TrackpadHaptic into the existing which + effect[0..6] — the legacy structs do NOT grow (guarded by new size_of==20/19 asserts); GamepadPref lockstep + paddle-bit lockstep asserts extended. include/punktfunk_core.h regenerated. Host (punktfunk-host): - steam_proto::from_gamepad maps the wire paddles -> the four Deck grips + QAM; apply_rich routes TouchpadEx left/right -> the matching pad. - every DualSense/DS4 manager (Linux + Windows) gained a TouchpadEx arm (surface 0/2 -> its one touchpad; surface 1 ignored) so the variant compiles everywhere and a Steam client streaming to a DS host keeps its right pad. - the xpad BUTTON_MAP finally consumes the GameStream paddle bits (BTN_TRIGGER_HAPPY5-8) — Sunshine/Moonlight paddle clients were silently no-op'd before (design §5.6). - Android feedback: drop TrackpadHaptic (no coils; rumble rides 0xCA). Validated on-box: the ignored backend test now drives the full wire path — from_gamepad (BTN_A + the L4 grip) + apply_rich (a left-pad TouchpadEx) reach the evdev as BTN_A + ABS_HAT0X=-8000. Wire round-trips + paddle/TouchpadEx mapping unit-tested. Workspace clippy/fmt/test green. Not pushed. Deferred to M4: the C-ABI PunktfunkRichInputEx + send_rich_input2 (only the Apple/embedder *send* path needs it; the host decodes TouchpadEx today). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
baa20ef377 |
feat(host/steam): M2 — virtual Steam Deck as a wired PadBackend (Linux)
Make the virtual hid-steam device a selectable per-session host gamepad,
end-to-end on Linux: PUNKTFUNK_GAMEPAD=steamdeck now builds a
SteamControllerManager that creates a /dev/uhid 28DE:1205 Deck, enters
gamepad_mode, and feeds the byte-exact Deck report (M1).
- inject/linux/steam_controller.rs: SteamControllerManager / SteamDeckPad,
mirroring dualsense.rs (open/create2, GET/SET_REPORT pump, heartbeat, RAII
destroy). Two Steam-specific quirks beyond the DualSense path:
* gamepad_mode entry — best-effort `lizard_mode=0` via sysfs, plus a b9.6
creation pulse (MODE_ENTER) so steam_do_deck_input_event stops
early-returning, plus an anti-toggle guard (MENU_HOLD_CAP) so a long
in-game Start-hold can't flip gamepad_mode back off.
* UHID_SET_REPORT answered err=0 (DualSense omits it; the kernel stalls
~5s/cmd otherwise); the 0xEB rumble report parsed onto the 0xCA plane.
- core config.rs: GamepadPref::SteamDeck (wire byte 6) + SteamController
(byte 5, reserved — folds to Xbox360 until its backend lands); from_u8 /
from_name / as_str. Forward-compatible (unknown byte -> Auto); the C-ABI
PUNKTFUNK_GAMEPAD_* constants stay M3, so no generated-header drift.
- punktfunk1.rs: PadBackend::SteamDeck variant + select / handle / apply_rich
/ pump / heartbeat arms; pick_gamepad Linux arm.
On-box: an #[ignore]d backend test (backend_binds_and_input_flows) drives the
real SteamDeckPad — it binds hid-steam (gamepad + IMU evdevs), enters gamepad
mode, BTN_A reaches the evdev, and the device tears down on drop. Workspace
clippy/fmt/test green. Not pushed. Next: M3 (protocol/ABI wire) + M4 (client
capture).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
15b9287865 |
feat(host/steam): M1 — byte-exact Deck input serializer, on-box validated
Flesh out inject/proto/steam_proto.rs into the full Steam Deck HID contract, transcribed verbatim from the kernel steam_do_deck_input_event / steam_do_deck_sensors_event and validated field-for-field against kernel 7.0: - SteamState: the u64 button map (bytes 8..16), sticks/triggers/trackpads/IMU stored as raw little-endian report values; serialize_deck_state is a pure, byte-exact memcpy into the 64-byte unnumbered frame. - from_gamepad (XInput frame -> Deck buttons/sticks/triggers) + apply_rich (RichInput touchpad -> right pad, motion -> IMU). - parse_steam_output: the 0xEB ID_TRIGGER_RUMBLE_CMD feedback -> (low, high) for the universal rumble plane. - serial_reply fixed: prepend the report-id-0 byte the kernel strips (steam_recv_report does memcpy(data, buf+1, ...)); M0's reply lacked it, so the kernel fell back to the "XXXXXXXXXX" serial. - SteamModel (Deck now; classic Controller later), command/feature IDs. The spike is repurposed as the M1 validator: it pulses the b9.6 mode-switch to enter gamepad_mode (steam_do_deck_input_event early-returns under the default lizard_mode otherwise), then holds a known test pattern. Reading both evdevs via EVIOCGABS/EVIOCGKEY, every field matched: ABS_X/Y/RX/RY (incl. the kernel Y-negation), both triggers, the touched right-pad HAT1X/Y, the IMU accel/gyro (with ABS_Z/RZ negations), and the 6 expected buttons incl. the L4/R5 grips. 5 unit tests + workspace clippy/fmt/test green. Next: M2 (SteamControllerManager UHID backend + PadBackend wiring). Not pushed — pipeline not yet shippable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
567b0d70e9 |
feat(host/steam): M0 — virtual hid-steam UHID device binds + parses (Linux)
Greenfield virtual Steam Deck controller, the Steam analogue of the shipped virtual DualSense. Proves the kernel hid-steam driver binds a /dev/uhid 28DE:1205 device, registers it as a real Steam Deck, and parses our input reports — the go/no-go gate for the full Steam Controller/Deck pipeline. - inject/proto/steam_proto.rs: keeper module — the vendor HID descriptor (one feature report, the sole thing steam_is_valve_interface() checks), the command/feature IDs, serialize_deck_state, and the serial GET_REPORT reply. Unit-tested. - src/bin/steam_uhid_spike.rs: throwaway M0 spike (Linux-only) — opens /dev/uhid, creates the device, services the handshake including UHID_SET_REPORT (which the DualSense backend omits and which hid-steam stalls ~5s/cmd without), and heartbeats a neutral report. - design/steam-controller-deck-support.md: full design + M0–M7 plan; the two walls (Steam Input capture ownership; virtual-Steam recognition) and the fidelity ceiling. Status: M0 GREEN. On-box (headless Ubuntu 26.04, kernel 7.0, no Steam): journalctl -k shows hid-steam binding the device (rebind off hid-generic), "Steam Controller connected", and the kernel creating BOTH a "Steam Deck" gamepad evdev and a "Steam Deck Motion Sensors" IMU evdev (INPUT_PROP_ACCELEROMETER). A layout-agnostic mash-probe drove 23 distinct BTN_* codes through hid-steam -> evdev, proving the input-report parse path. M1 line-checks the exact per-bit report layout against the lab kernel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
42d7f7ab2a |
fix(host/security): scope Windows shared-section SDDL to SYSTEM+LocalService (close #5)
The gamepad host<->UMDF-driver shared sections (Global\pfds-shm-*, pfxusb-shm-*) and the IDD-push frame ring/event (Global\pfvd-*) were created with `D:(A;;GA;;;WD)` — GENERIC_ALL to **Everyone** — on the assumption the driver's WUDFHost ran under a restricted token needing broad access. So any local unprivileged user could OpenFileMapping the section to inject controller input, tamper the trusted HID channel, or read captured screen frames (security-review 2026-06-28 #5). On-box validation (RTX box, 2026-06-29) disproved the restricted-token premise: the WUDFHost token is NT AUTHORITY\LocalService (S-1-5-19), SYSTEM integrity, with ZERO restricted SIDs. So the section only needs SYSTEM (the host creates + writes it) and LocalService (the driver opens it). Scope both SDDL sites to `D:(A;;GA;;;SY)(A;;GA;;;LS)`; rename the now-misnamed `permissive_sa` -> `shared_object_sa`; correct the stale "restricted-token / Everyone" docs. Validated live: a full DualSense + 1280x720x60 session — 6943 frames received, HID output round-tripped, device status OK (pf_dualsense + pf_vdisplay WUDFHosts both LocalService open the scoped sections fine), while OpenFileMapping from a non-SYSTEM admin session now returns ACCESS_DENIED (was a granted handle under WD). Host-only change (the SDDL is set when the host CREATES the section); drivers unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
46b82d4eee |
fix(host/security): close audit findings S1,#1,#4,#10,#12,#7,#6,S2-S6 (Linux/cross-platform)
Remediations from design/security-review-2026-06-28.md verified on Linux (cargo check/clippy/test green; Windows-gated paths verify in CI): - S1 [HIGH]: bump quinn-proto 0.11.14 -> 0.11.15 (RUSTSEC-2026-0185, pre-auth out-of-order STREAM reassembly memory exhaustion on the always-on default QUIC listener). - #1 [HIGH]: remove the unauthenticated nvhttp `GET /pin` endpoint; the GameStream PIN is delivered ONLY via the bearer-gated mgmt API, so a network client can no longer submit its own displayed PIN and self-pair. - #4 [HIGH->MED]: gate the unauthenticated RTSP/UDP media plane on a paired `/launch` and bind it to the launching client's source IP (threaded through the HTTPS handler), so an unpaired peer can neither start capture on an idle host nor ride a paired client's active launch. - #12: bound concurrent parked pairing waiters (MAX_PARKED_WAITERS) so a pre-auth peer can't pin unbounded 300s handshakes. +regression test. - #10: throttle the per-packet ENet control GCM-decrypt-failed warn (exponential backoff) so a junk flood can't spam the log. - #7 [MED->LOW]: serialize all process-global env mutation on the session-setup path under a new vdisplay::ENV_LOCK (apply_session_env / apply_input_env / the launch-cmd set_var / the gamescope env read), so concurrent native sessions can't race set_var/getenv (data-race UB -> host-wide DoS). Full per-session SessionContext threading remains a follow-up for cross-session value confusion. - #6 [MED]: move the gamescope EIS socket relay from world-writable /tmp to $XDG_RUNTIME_DIR (per-user 0700) and reject a symlinked relay file, so a local user can't intercept (keylog) or deny the remote session's input. - S2: a malformed client Opus mic frame now drops that frame instead of tearing down the shared host-lifetime virtual mic (cross-session DoS). - S3: track held buttons/keys in capped HashSets (was unbounded Vec with O(n) scans) so a paired client can't grow per-session input state. - S5: reject fps==0/absurd at the open_video chokepoint (covers Hello, ANNOUNCE, Reconfigure) so the encoder time_base/pts math can't div-by-0. - S6: bound the shared mic mpsc (drop-newest when full). - S4: cap Epic launcher-cache reads (catcache.bin/.item) so a planted giant can't OOM the host during library enumeration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d0e071034a |
feat(host/input): headless KDE input via org_kde_kwin_fake_input
Desktop-mode (KWin) streaming had no input: the path was libei via the RemoteDesktop portal, which (a) isn't reachable from the host service env and (b) requires a human to approve "Allow remote control?" — a non-starter on a headless box. KWin's own headless RDP server (krdpserver) solves this with org_kde_kwin_fake_input, authorized by the exact same .desktop X-KDE-Wayland-Interfaces grant we already ship (org_kde_kwin_fake_input is listed alongside zkde_screencast_unstable_v1). Add a fake_input injector: vendor the protocol XML, bind the global as an ordinary Wayland client, authenticate (auto-accepted for an interface-authorized client — no dialog), and translate pointer (rel/abs), button, scroll, keyboard (raw evdev keycodes resolved by KWin's own keymap) and touch. Select it for KWin (compositor=="kwin" or XDG_CURRENT_DESKTOP KDE); GNOME stays on libei (it has neither fake_input nor the wlr protocols). PUNKTFUNK_INPUT_BACKEND=kwin forces it. cargo check + clippy + fmt green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0925f1aaa1 |
fix: complete the docs/→design/ and openapi→api/ rename references
The file moves (docs/ → design/, docs/api/openapi.json → api/openapi.json) landed
in
|
||
|
|
33328ee417 |
docs(host): prove the last 3 files + crate-root deny (unsafe-proof program 4/N, final)
Completes the unsafe-proof program now that the parallel WIP has landed: - idd_push.rs (25 sites), nvenc.rs (7), punktfunk1.rs (21): a SAFETY proof on every unsafe block — D3D11/DXGI COM (same-device textures, immediate-context single-thread, keyed-mutex-held convert), the NVENC SDK table (versioned POD, register/map/lock-bitstream pairing), cross-process shm reads (atomic magic/generation handshake), and the C-ABI harness (each call cross-checked against its abi.rs `# Safety` doc). No SUSPECT (UB) blocks. - capture.rs / encode.rs: the parent-module deny is restored (their WIP children are now proven), and main.rs gains a crate-root #![deny(clippy::undocumented_unsafe_blocks)] — the permanent catch-all gate so no future unsafe block anywhere in the crate can land without a proof. - Fixed 4 blocks the agents missed: unsafe blocks nested inside `assert_eq!(...)` macro args (the comment-above-statement didn't associate) — hoisted to a `let`. - rustfmt-canonicalized the Windows files (the agents' SAFETY comments + some pre-existing 1.9.0 drift) so `cargo fmt --all --check` is clean. Verified: cargo clippy -p punktfunk-host --all-targets -- -D warnings AND cargo fmt -p punktfunk-host --check both green with the crate-root deny active. Windows cfg(windows) re-verified on the box next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
79c8641c3f |
docs(host): prove unsafe blocks in the Windows + cross-platform files + gate them (unsafe-proof program 3/N)
Continues the unsafe-proof program across the Windows/cross-platform host files
(~75 blocks, 21 files), each with a SAFETY proof of the real invariant and a
per-file #![deny(clippy::undocumented_unsafe_blocks)] gate:
capture/windows: dxgi.rs, wgc_relay.rs, wgc.rs, desktop_watch.rs, composed_flip.rs
(windows-rs COM: interface validity, same-D3D11-device textures,
immediate-context single-thread, borrowed args outlive the call)
windows: service.rs (SCM/token/CreateProcessAsUserW/event handles — OwnedHandle
liveness, no double-close/signal race), win_display, wgc_helper, interactive
vdisplay/windows: manager.rs, pf_vdisplay.rs (SwDeviceCreate/IddCx/ioctl handle
liveness via the OnceLock VDM singleton + OwnedHandle)
encode/windows: ffmpeg_win.rs (full AVBufferRef refcount audit — balanced, NO leaks,
unlike the vaapi sibling), sw.rs
cross-platform: gamestream/audio.rs (libopus), gamestream/stream.rs (sendmmsg),
inject/windows/sendinput.rs, audio/windows/wasapi_mic.rs,
session_tuning.rs, vdisplay.rs
Two findings (handled separately):
- wgc_relay.rs `unsafe impl Sync for HelperRelay` is UNSOUND (its mpsc Receiver is
!Sync) though not live-exploited — marked SUSPECT inline; fix pending box check
(it touches the in-flight punktfunk1.rs).
- capture.rs / encode.rs (PARENT modules of the WIP idd_push.rs / nvenc.rs) do NOT
get the file deny yet — it would propagate the lint into the undocumented WIP
children. The deny lands there once those are documented (after the WIP commits).
Linux-visible parts verified green (cargo clippy -p punktfunk-host --all-targets
-- -D warnings). The cfg(windows) deny gates are box-verified next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
b54695739d |
docs(host): prove every unsafe block in the Linux FFI files + gate them (unsafe-proof program 2/N)
Continues the structural unsafe-proof program (every unsafe carries a documented
proof of soundness; the file gains #![deny(clippy::undocumented_unsafe_blocks)]
so it stays proven). This batch covers all 10 remaining pure-Linux files
(104 blocks), each proof stating the REAL invariant — not boilerplate:
zerocopy/cuda.rs (26) leaked process-lifetime libcuda fn-ptr table; opaque
CUcontext never dereferenced; free-exactly-once via the
Arc<Mutex<PoolInner>> ownership graph; dmabuf fd take/close split
zerocopy/egl.rs (18) eglGetProcAddress'd procs with the GL context current;
EGLImage liveness; the two-call modifier-query bounds
zerocopy/vulkan.rs (4) copy-bounds arithmetic (src_size>=span); Send = thread
confinement to the punktfunk-pipewire thread
dmabuf_fence.rs (4) poll/ioctl/close fd liveness + ownership
capture/linux/mod.rs (16) spa_data repr(transparent) cast; null-checked spa
derefs; single-loop-thread buffer ownership until requeue
inject/linux/gamepad.rs (10) uinput ioctl request-number ↔ struct-size match
(static-asserted); InputEventRaw no-padding for the byte cast
encode/linux/vaapi.rs (15) + encode/linux/mod.rs (9) ffmpeg object ownership/
free ladders; VAAPI/DRM graph; Send = single-thread transfer
inject/linux/wlr.rs (2), vdisplay/linux/kwin.rs (1)
No memory-unsafety SUSPECT blocks were found — the unsafe is sound. The vaapi
agent did flag two real AVBufferRef *leaks* (not UB) in DmabufInner::open; marked
inline with NOTE(leak) and addressed in a follow-up.
Verified: cargo clippy -p punktfunk-host --all-targets -- -D warnings is clean
(each file's deny gate hard-errors on any undocumented block).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
759f871ff8 |
refactor(windows-host): shared Shm/SwDevice RAII for the 3 gamepad backends (Goal-3 unsafe reduction)
The DualSense, DualShock 4, and XUSB Windows pad backends each hand-rolled the SAME per-pad resource handling: a `CreateFileMappingW` + `MapViewOfFile` shared section (with the permissive D:(A;;GA;;;WD) SDDL the restricted-token driver needs) and an identical `Drop` doing `SwDeviceClose` + `UnmapViewOfFile` + `CloseHandle` — three copies, each a chance to drift or leak on an error path. New `inject/windows/gamepad_raii.rs` owns both resources with RAII: - `Shm` — the section handle (`OwnedHandle`) + its view; `Shm::create(name, size)` does the SDDL + map + zero-fill leak-safely, `base()` gives the mapped pointer, `Drop` unmaps then closes (in that order). - `SwDevice` — the `SwDeviceCreate`'d devnode; `Drop` calls `SwDeviceClose`. All three backends now hold `_sw: Option<SwDevice>` + `shm: Shm` instead of raw `hsw`/`map`/`view`, access the section via `self.shm.base()`, and have NO manual `Drop`. Deletes the duplicated `create_shm_section` (DualSense/DS4 now use `Shm::create`) and the three hand-written Drops; the DS4 device-type byte is still written before the magic, the SwDeviceCreate `None` fallback still works, and the field drop order (devnode removed, then section unmapped+closed) matches the old manual order. Net: 3 manual `Drop`s + a duplicated section-creation path → one shared RAII module; fewer unsafe ops, leak-on-error fixed by construction. Linux `cargo check` clean (the inject mod wiring); the backends are #[cfg(windows)] → CI-gated. Drafted + adversarially verified (no double-free, imports correct under -D warnings, behavior preserved); my own spot-checks confirm. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
764c814483 |
refactor: rename pf-vdisplay-proto -> pf-driver-proto (it spans all drivers)
The shared host<->driver ABI crate already contains more than the virtual display: the IDD-push frame ring + control plane AND the gamepad shared-memory layouts (XusbShm / PadShm). "pf-vdisplay-proto" was a misnomer — the name now represents all the drivers it serves. Mechanical rename, no behavior change: - git mv crates/pf-vdisplay-proto -> crates/pf-driver-proto (package name + path-deps in the host crate and the driver workspace). - pf_vdisplay_proto -> pf_driver_proto across host + driver Rust, both Cargo.lock files, the workspace members, the CI path triggers (windows-drivers.yml), and the docs/INF comments. The runtime Global\pfvd-* shared-object names are a SEPARATE contract and are deliberately untouched (host<->driver name matching). - The pf-vdisplay DRIVER crate + its INF service name (Root\pf_vdisplay, UmdfService=pf_vdisplay, pf_vdisplay.dll) are unchanged — only the full `pf_vdisplay_proto` token was replaced, never the `pf_vdisplay` driver name. Linux-verified: cargo test -p pf-driver-proto (const size-asserts compile) + cargo clippy -p punktfunk-host -D warnings clean; Cargo.lock regenerated. The driver-workspace side (path-dep + imports + its Cargo.lock) is Windows-CI-gated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c2ee47cc3b |
refactor(windows-host): delete the SudoVDA backend — pf-vdisplay is the sole vdisplay (Goal 2)
Goal 2 ("drop every trace of SudoVDA") is done. The SudoVDA driver is no longer
shipped (only pf-vdisplay; the old vdisplay-driver tree was deleted in
|
||
|
|
fced221684 |
refactor(windows-host): confine platform code under windows/ + linux/ folders (Goal-1 stage 6)
Move 36 platform-specific files into per-module `windows/` and `linux/` subfolders (and the
shared HID codecs into `inject/proto/`):
capture/{windows,linux}/ encode/{windows,linux}/ inject/{windows,linux,proto}/
audio/{windows,linux}/ vdisplay/{windows,linux}/
src/windows/ (service, wgc_helper, win_adapter, win_display)
src/linux/ (dmabuf_fence, drm_sync, zerocopy/)
Done with `#[path]`, NOT a module rename: every file moves into its folder while the
`crate::*::*` module names stay FLAT, so all caller paths and every internal `super::`/`crate::`
reference are unchanged — only the parent `mod` decls gained `#[path = "..."]`. This is the
codebase's existing pattern (inject's gamepad_windows) and makes the move byte-identical in
behaviour with ZERO reference churn, far lower risk than collapsing to a single
`crate::capture::windows::` namespace (that deeper rename is an optional follow-on; this delivers
the cfg-sprawl folder confinement the stage is about). Done LAST, after the semantic stages, so
the path churn didn't fight them.
Verified: Linux cargo check + clippy (-D warnings) clean; my mod-decl changes fmt-clean (the 3
remaining fmt diffs are pre-existing local-rustfmt-version skew that moved with their files); all
36 `#[path]` targets exist; no internal `#[path]`/`include!`/file-child-mod in any moved file
(the inline `mod X {` blocks are self-contained). Box build to follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
0aa5f360f8 |
feat(windows-host): activate render-adapter pin; gamepad SHM from proto (audit §4.2h/§6.1)
§4.2h (C2): the host already pins the discrete GPU via IOCTL_SET_RENDER_ADAPTER on the IDD-push path; now that the pf-vdisplay driver implements it (
|
||
|
|
8b6b4a32fa |
feat(gamepad): pure-user-mode Windows DualShock 4 + Xbox 360 (drop ViGEm) + installer + multi-pad
Windows virtual gamepads now have zero external dependencies - ViGEmBus is removed. - DualShock 4: Windows UMDF backend (inject/dualshock4_windows.rs + dualshock4_proto.rs), reusing the DualSense SwDeviceCreate game-detection identity fix. The one UMDF driver serves the DS5 or DS4 identity/descriptor/features/strings per a device_type byte the host stamps into shared memory. Driver also gains IOCTL_HID_GET_STRING and a 41-byte calibration feature. - Xbox 360: a new UMDF2 XUSB companion driver (packaging/windows/xusb-driver/) that registers GUID_DEVINTERFACE_XUSB and answers the buffered XInput IOCTLs from a shared section, so classic XInputGetState/SetState work with no kernel bus driver. inject/gamepad_windows.rs is rewritten to drive it and the vigem-client dependency is removed. Xbox One folds to the 360 XInput path. - Installer: vendor + pnputil-install the three UMDF drivers (packaging/windows/gamepad-drivers/ + install-gamepad-drivers.ps1, wired into pack-host-installer.ps1 + punktfunk-host.iss). - Multi-pad: the host stamps each pad index into the device Location (pszDeviceLocation); the driver reads it via WdfDeviceAllocAndQueryProperty to map its own *-shm-<index>, with UmdfHostProcessSharing=ProcessSharingDisabled giving each pad its own host (per-pad statics). Validated live on the Windows host: Cyberpunk native DualSense detection, DS4 identity + descriptor, XInputGetState + rumble round-trip, two pads -> two distinct XInput slots, and a full installer build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
7466f2b4b7 |
style(host): blank line before the uniq comment so rustfmt is clean
dualshock4.rs left `cargo fmt --all --check` red on main (it landed with the Windows-host DualSense work): a standalone comment placed directly after a line ending in a trailing comment gets absorbed and re-aligned to the trailing-comment column. A blank line before the comment block keeps rustfmt happy — and the comment readable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2eb69759b9 |
fix(gamepad): working per-session SwDeviceCreate for the Windows DualSense
create_swdevice now succeeds. The two requirements (each E_INVALIDARG otherwise): the
enumerator name must have no underscore (use "punktfunk"), and the completion callback is
mandatory (the docs mark pCallback [in], not optional -- NULL is rejected). Back on the
typed windows-rs SwDeviceCreate (a raw-FFI diagnosis confirmed it's the OS, not the
binding), parameterized by pad index (instance pf_pad_<index>), waiting on the callback.
Per-session device: created on connect, SwDeviceClose'd on drop -- no leftovers, no phantom.
Live-verified on the RTX box: device materializes, the UMDF driver binds, SDL3 identifies it
as a PS5 ("DualSense Wireless Controller"), input flows; removed on disconnect. The
dualsense-windows-test CLI now cycles input + prints any 0x02 feedback for diagnosis.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
fe2b49ccc4 |
feat(gamepad): SwDeviceCreate per-session devnode (best-effort) + windows self-test
DualSenseWindowsManager now SwDeviceCreate's the pf_dualsense devnode per session (SwDeviceClose on drop), matching the Linux UHID pad's lifecycle. It's best-effort: SwDeviceCreate currently hits an unresolved E_INVALIDARG when a completion callback is passed (an underscore in the enumerator name was a second cause, fixed by using "punktfunk"), so on failure the host keeps the section + data plane and falls back to an out-of-band devnode (installer/devgen) — see docs/windows-dualsense-scoping.md. Add a `dualsense-windows-test` host CLI that drives the manager (create devnode + push a frame + hold), used to validate the path. Live on the RTX box: the manager creates the section + pushes report 0x01 and a devnode serves it to a HID read (b1=0xC0, b8=0x28) — the host-side data plane works end to end. cargo check + clippy -D warnings clean on x86_64-pc-windows-msvc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
3548c70152 |
feat(gamepad): virtual DualSense on the Windows host (UMDF shm channel)
Wire the Windows UMDF DualSense driver into the host as a real pad backend, so a client that requests a DualSense gets a genuine one on a Windows host (instead of folding to Xbox 360). - Extract the transport-independent DualSense contract (DsState + from_gamepad, serialize_state, parse_ds_output, DUALSENSE_RDESC, feature blobs, DS_* consts) out of the Linux-only UHID backend into inject/dualsense_proto.rs, shared by both platforms; dualsense.rs is now just the /dev/uhid plumbing. - Add inject/dualsense_windows.rs: DualSenseWindowsManager mirroring the Linux DualSenseManager (same new/handle/apply_rich/pump/heartbeat surface) over a DsWinPad that creates the Global\pfds-shm-<idx> section (CreateFileMappingW + SDDL D:(A;;GA;;;WD) so WUDFHost can open it), writes serialize_state -> input slot, polls output_seq -> parse_ds_output -> rumble/hidout callbacks. - Un-gate the seam: PadBackend::DualSenseWindows arm; pick_gamepad gains a windows flag (DualSense honored on linux||windows; DS4/Xbox One stay Linux-only). Verified: Linux cargo test gamepad_resolution_precedence + clippy clean; Windows cargo check + clippy -D warnings clean (on the RTX box). Device lifecycle still uses an out-of-band devnode (devgen/installer); SwDeviceCreate per session is next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
99ae1b9a17 |
feat(gamepad): add virtual Xbox One/Series + DualShock 4 pad types
Extends virtual-controller support beyond Xbox 360 + DualSense. Goal: a physical Xbox One or PS4 pad on the client gets a near-native matching virtual pad on the host, auto-resolved from the controller type. Protocol/core: - GamepadPref gains XboxOne (wire 3) + DualShock4 (wire 4); to_u8/from_u8/ from_name/as_str + C ABI PUNKTFUNK_GAMEPAD_XBOXONE/_DUALSHOCK4 constants (compile-time guard ties them to the enum). Single-byte wire form is unchanged, so it's forward-compatible (older peers degrade to Auto). Host (Linux): - New UHID DualShock 4 backend (inject/dualshock4.rs) bound by hid-playstation: lightbar, touchpad, motion, rumble — DualSense minus adaptive triggers / player LEDs / mute. Reuses the DualSense pure state + button mapping; only the report byte layout, the real-DS4 HID descriptor, the GET_REPORT handshake (0x12 MAC mandatory; 0x02 calibration; 0xa3 firmware) and the touchpad resolution (1920x942) differ. Touchpad/motion ride the existing 0xCC plane, lightbar the 0xCD Led plane (deduped); rumble the universal 0xCA plane. - Xbox One/Series is the uinput Xbox-360 backend parameterized with the One S USB identity (045e:02ea) for matching glyphs — XInput-identical otherwise. - PadBackend dispatch + resolver handle both; off Linux the UHID pads and One/Series fold into Xbox 360. Windows-host DS4 (ViGEm) deferred. Clients (auto-resolve physical pad -> virtual type, plus manual settings): - Linux/Windows (SDL3): SDL_GAMEPAD_TYPE_PS4 -> DualShock 4, _XBOXONE -> Xbox One; PadInfo carries the resolved pref; DS4 touchpad/motion capture + lightbar already type-agnostic. Linux settings combo + label updated. - Apple (GameController): GCDualShockGamepad/GCXboxGamepad detection, DS4 touchpad capture, settings picker entries. - Android (Kotlin): InputDevice VID/PID auto-detect (matching the other clients) + settings entries. - probe: --gamepad help/aliases. Also hardens the Android JNI boundary: wrap the teardown + poll-thread shims in catch_unwind so a panic degrades to a logged no-op instead of aborting the app. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
36c3b11a7f |
perf(host/windows): SendInput retry-on-failure model (two-process step 2)
The injector reattached the input desktop (OpenInputDesktop + SetThreadDesktop, two syscalls) before EVERY event. Now it stays bound to its desktop and only reattaches on a SendInput short write (the input desktop switched into UAC/lock) + retries once — Sunshine's model. No steady-state per-event overhead; still follows the desktop across the secure boundary, serving both desktops. Validated on the RTX 4090 (host as SYSTEM): client-rs --input-test injected for ~6s with no "blocked desktop" errors. Completes all 6 steps of the two-process secure-desktop build; only a real-UAC user smoke test remains. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3458256338 |
feat(host/windows): ViGEm rumble back-channel + Windows clippy clean
Wire the host→client rumble path on Windows, the analogue of the Linux uinput EV_FF read loop: a game's force-feedback on the virtual Xbox 360 pad is delivered by ViGEm's notification API (`request_notification` → `spawn_thread`, gated by the crate's `unstable_xtarget_notification` feature). A per-pad background thread stores the latest motor levels; `pump_rumble` relays changes to the client on the universal 0xCA plane (motors scaled 0..255 → 0..65535). Dropping the target aborts the notification, so the thread exits with the session. Live verification still needs a physical pad. Also fix the Windows backends' clippy debt — these modules are cfg- excluded from Linux CI, so `clippy -D warnings` never saw them, and the VM's rustc 1.96 clippy is stricter on shared code than the CI image: - dxgi: manual checked division → checked_div().map_or - sendinput: `x = x | y` → `x |= y` - sudovda: `.then(|| ptr)` → `.then_some(ptr)` - m3 pick_compositor: drop the needless early return (match form) - m3 resolve_compositor: Windows arm is a tail expr, not `return` All Windows backends now build + clippy clean (default and --features nvenc); Linux unaffected (fmt/clippy/check green). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
87a4aaed89 |
fix(host/dualsense): heartbeat virtual DualSense so it isn't dropped when idle
"Controller disconnected every few seconds" (Forza Horizon, held steady): the virtual UHID DualSense emitted HID report 0x01 ONLY on state change, but a real DualSense streams it continuously (~250 Hz). When the player holds the wheel/throttle steady the client sends no wire events, so the host wrote nothing and /dev/uhid went silent for seconds — the kernel hid-playstation driver / Proton / SDL treat that as an unplugged controller. (The uinput X-Box pad is immune: evdev holds last-known state with no periodic-report requirement.) Add DualSenseManager::heartbeat(max_gap): re-emit each live pad's CURRENT report when it's been silent for max_gap (idempotent — a stale-but-correct frame, never a phantom input; write_state bumps seq+timestamp). write() resets the per-pad timer, so an actively-used pad emits no extra reports — the heartbeat only fills genuine silence. PadBackend::heartbeat() drives it at an 8 ms gap (~125 Hz) for DualSense (no-op for X-Box), called every input-thread tick (the loop already runs ≤4 ms). GET_REPORT feature replies + the pad lifecycle were ruled out by the investigation (pad is created once, never torn down mid-session). Compiles, clippy/fmt clean, 78 host tests pass. Verify on the box: held-idle DualSense stays present in evtest / no SDL CONTROLLERDEVICEREMOVED; Forza no longer toasts "controller disconnected". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
50dec9e7cb |
style(host/windows): rustfmt the Windows backends
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
46faa0dfc9 |
feat(host/windows): ViGEm virtual gamepad backend
Windows GamepadManager via vigem-client (ViGEmBus) — the uinput-xpad analogue: one virtual Xbox 360 controller per client pad index, created lazily on first State. GameStream/Moonlight already uses the XInput conventions (low-16 button bits, sticks -32768..32767 +Y up, triggers 0..255), so the GamepadFrame->XGamepad mapping is 1:1. Replaces the non-Linux GamepadManager stub (same new/handle/pump_rumble API the m3 PadBackend drives, so no m3 change). Graceful when ViGEmBus is absent (gamepad disabled, session continues). Compiles clean on Windows + Linux; live-test needs the ViGEmBus driver + a physical pad. Rumble back-channel is a TODO (ViGEm notification API). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
87ecd7e7ac |
feat(host/windows): SendInput input-injection backend
Windows InputInjector via SendInput (Win32 KeyboardAndMouse), mirroring the wlroots backend: absolute mouse (MOUSEEVENTF_VIRTUALDESK normalized to the virtual desktop), relative mouse, scancode keyboard (MapVirtualKeyExW + extended-key flagging), scroll (no sign flip — Windows wheel matches GameStream), buttons. Client already sends Windows VK codes (no keycode table). Reattaches the thread to the input desktop (OpenInputDesktop/SetThreadDesktop) to survive UAC/lock switches. New Backend::SendInput, the Windows auto-default in default_backend(), open() arm, windows-crate features. Compiles clean on Windows + Linux. Live injection validates with the in-session host run (SendInput is desktop-isolated from an SSH network logon). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4dd37a3e08 |
fix(host/dualsense): report full battery + log rumble forwarding
Two DualSense (UHID) fixes surfaced live on the Bazzite host: - Battery: serialize_state never set the input report's status byte (struct off 52 → r[53]), so hid-playstation read battery capacity 0 and SteamOS warned "low battery" even on a fully-charged pad. Set it to 0x0A (discharging, low nibble 0xA → 100 %) — a virtual pad has no real cell. (Forwarding the client pad's real charge is a later feature.) Regression assert added to the layout test. - Rumble diagnostic: log the silent→active transition when forwarding a buzz on the 0xCA plane, so a live test can tell "host never receives rumble from the game" (Steam Input / parse) apart from "client doesn't render it". Once per buzz, no spam. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4baf97343a |
fix(inject/libei): emit the continuous scroll axis so small scrolls register
The libei backend forwarded mouse wheel only via scroll_discrete (120-per-detent). Mutter floors a sub-detent delta — a trackpad, a precise/high-res wheel, or a fractional smooth-scroll event — to zero whole clicks, so small scrolls never land and you have to spin the wheel a lot before anything moves. Emit the continuous `scroll` axis (logical px, ~15 px/detent) alongside the discrete steps, matching the wlroots backend's 15-px/notch behaviour, so every delta moves proportionally while full detents still drive line/page scrolling. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
97ee75db3d |
fix(capture/mutter): stale-frame flashes + stuck input after disconnect on GNOME
Deep dive into the two GNOME-only host bugs (KWin/gamescope clean):
1. Stale-frame flashes (windows at old positions, typed text reverting):
Mutter renders its virtual monitors DIRECTLY into the PipeWire buffer
pool, and NVIDIA has no implicit dmabuf fencing — our zero-copy
import raced the render and encoded each pool buffer's PREVIOUS
contents. Fix, in order of preference:
- Consumer-side PipeWire explicit sync (SPA_META_SyncTimeline): new
drm_sync module (DRM timeline-syncobj wait/signal via raw ioctls,
unit-tested incl. a live signal->wait round trip); announced
post-format via update_params (the OBS pattern — at connect time
the meta makes producers fail allocation, observed on KWin), with
a blocks=3 Buffers filter so the producer's sync pod wins; acquire
point awaited before any read (GPU import or CPU mmap), release
point signaled on every path.
- Where the producer can't do explicit sync (Mutter on NVIDIA today:
no cogl sync_fd, "error alloc buffers"), a sticky fallback flips
the capture to the synchronous CPU/shm path — Mutter's glReadPixels
download orders against its render, so frames are correct by
construction. First session pays one ~10 s probe+retry; later
sessions go straight there. Validated live on home-worker-3
(GNOME 50 + RTX 4090): clean fallback, 30 MB HEVC streamed.
- Sync is only announced on Mutter sessions (new VirtualOutput.mutter
tag): KWin+NVIDIA fails allocation when merely asked, and doesn't
need it (verified unchanged: zero-copy CUDA import + 1.1 MB/10 s).
PUNKTFUNK_EXPLICIT_SYNC=0 disables the probe outright.
2. Clicks wedged in the focused app after disconnect+reconnect: a client
vanishing mid-press left keys/buttons latched in the compositor —
Mutter keeps the destroyed EIS device's implicit grab and the focused
app stops taking clicks until restarted. EiState now tracks held
keys/buttons/touches (wire codes) and synthesizes releases through
the normal inject path before the EIS connection goes away.
GNOME hosts on NVIDIA temporarily lose zero-copy (correctness over
throughput); the moment Mutter+driver gain working explicit sync, the
sync path engages automatically and zero-copy returns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
fcd36f34d1 |
fix(inject/mutter): GNOME input via Mutter's direct EIS, not the xdg portal
On a headless GNOME host the xdg-desktop-portal RemoteDesktop Start() blocks on an
interactive "Allow remote control?" approval nobody can click, so libei input timed out
("EIS setup timed out") and neither mouse nor keyboard worked — even though video worked
(it uses Mutter's direct RemoteDesktop API).
Add EiSource::MutterEis: obtain the EIS fd from
org.gnome.Mutter.RemoteDesktop.Session.ConnectToEIS (CreateSession → Start → ConnectToEIS),
no portal and no approval. Selected for GNOME/Mutter; KWin keeps the RemoteDesktop portal,
gamescope keeps its own EIS socket.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|