`ci.yml`'s clippy gate has been failing on main since the pre-0.16.0 sweep landed, and
because clippy stops at the first crate it can't compile, the visible error was only
ever the first of four. `ci.yml` is not tag-triggered, so v0.16.0 cut and shipped over
a red main; none of this reaches the release artifacts (the two E0308s are in a test
binary and a dev tool, and the rest are lints) but the gate has been blind since.
Fixed, in the order clippy surfaced them:
- clients/probe failed to COMPILE (E0308, x2). `810d918d` moved `clock_sync` onto the
resumable `io::MsgReader` but only updated the client pump, leaving the probe passing
a bare `&mut RecvStream`. The probe now wraps the control stream in a `MsgReader` at
`open_bi` and threads that everywhere — Welcome, the --remode and --bitrate watchers,
and the speed-test result read — which is also what the refactor was for: those reads
sit behind timeouts and `select!`, exactly where a straddling frame would desync the
stream for the rest of the run.
- pf-capture: `SPA_META_Cursor as u32` is a `u32 -> u32` no-op
(`clippy::unnecessary_cast`). Line 1274 already passes the same constant uncast, so
the type is not in question.
- pf-inject: `noop as usize` on the SIGUSR1 wake handler added in `986402f7` trips
`clippy::function_casts_as_integer`; goes via `*const ()` as the lint asks. Same value
in the `usize`-typed `sa_sigaction` slot.
- punktfunk-core: the `ctrl_framing` test module's `use super::*` is unused, which
`-D warnings` promotes to an error.
Verified with CI's own commands on a Linux box (this is all Linux-gated code, so a Mac
cannot check it): `cargo clippy --workspace --all-targets --locked -- -D warnings`
finishes clean, `cargo build --workspace --locked` succeeds, and
`cargo test --workspace --locked` exits 0 with no failures — including punktfunk-core's
196-test suite, which covers the control-stream framing the probe change touches.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two medium findings from the round-1 sweep, each applied to both siblings.
- create_swdevice stack-allocated the SwCreateCtx that the async PnP completion
callback writes through (result + up to 127 u16 of instance id) and then
SetEvents. The wait is bounded at 10s, so on a wedged-PnP timeout the callback
can still be PENDING: the frame is popped, the input thread reuses that stack,
and a late callback corrupts it and SetEvents an already-closed (possibly
recycled) handle. The context is now heap-allocated and reclaimed only where
the callback provably ran; on the timeout path the box is deliberately leaked
and the event left open, so a late write always targets live memory. Costs a
one-off ~264 B + one HANDLE on that rare path. Applied to the DualSense path
and its XUSB sibling in gamepad_windows.rs.
- Ds4WinPad::open swallowed a create_swdevice failure into a WARN and returned
Ok with no devnode. PadSlots::ensure then stored Some(pad) AND called
gate.on_success(), so the slot short-circuited on is_some() forever and the
capped-backoff retry that exists precisely to self-heal a transient PnP failure
never ran — the game saw no controller for the rest of the session unless the
client unplugged the pad. Now propagates, matching the XUSB sibling. Same fix
applied to steam_deck_windows.rs.
Windows .173: pf-inject 53/0. Linux .21: pf-inject 74/0 (8 ignored).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The three high-severity defects from the round-1 sweep of pf-inject /
pf-zerocopy / pf-capture (adjudicated against source — all 7 reported criticals
in these crates downgraded; these were the real highs).
- pf-inject steam_gadget: `SteamDeckGadget::drop` set `running=false` then joined
the control thread, which spends steady state parked in a blocking, no-timeout
`EVENT_FETCH` ioctl that only tests `running` at its loop top. The flag never
reaches it, closing the fd can't wake an in-flight ioctl (the syscall holds a
file reference, and the fd is shared via Arc by the very threads being joined),
so the join hung — and it runs on the session input thread via
`PadSlots::sweep`, driven by the client's `active_mask`, so a remote peer
clearing its pad bit could freeze all session input. Now wakes the parked
threads with SIGUSR1 (no-op, non-SA_RESTART handler → the ioctl returns EINTR
and the loop exits), retried until each reports done and bounded (~1s).
- pf-zerocopy cuda: the GL→CUDA "sync point" was never established for the copy.
`cuGraphicsMapResources`/`UnmapResources` were issued on the NULL stream, but
the D2D copy runs on `copy_stream()`, a `CU_STREAM_NON_BLOCKING` stream exempt
from implicit NULL-stream ordering — and the GL de-tile/CSC that produced the
texture ends with only `glFlush` (no fence). So the copy could race ahead of
the not-yet-retired GL draw: intermittent stale/torn frames under GPU load, on
the default NVIDIA capture→encode path. Map, copy, and unmap now share
`copy_stream()`, so map's device-side guarantee orders the GL work before the
copy. Zero-copy preserved (no GPU→CPU→GPU roundtrip).
- pf-capture cursor meta: `update_cursor_meta` trusted three producer-written
fields (bitmap_offset, pixel offset, stride) with no bound against the metadata
region, driving OOB pointer arithmetic and an oversized `from_raw_parts` — an
OOB read that SIGSEGVs inside the PipeWire `.process` callback (uncatchable by
the surrounding `catch_unwind`). Switched to `spa_buffer_find_meta` to obtain
the region's real `size` and validate every offset with checked arithmetic
before each deref/slice, mirroring the fd-length guard the main frame path
already applies.
Compile + existing tests green on Linux .21 (real RTX 5070 Ti): pf-inject 74/0,
pf-zerocopy 17/0, pf-capture 1/0. The gadget deadlock path only executes on a
SteamOS host with raw_gadget/dummy_hcd (not reproducible on the CachyOS box), so
that fix is reasoned + compile-verified, not runtime-exercised.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
B: PadFeedback.game_drove -> rumble_drove, keyed on vibration-asserting reports — an
LED/adaptive-trigger stream can no longer feed the abandoned-rumble force-off while a
coalesced stop never re-asserts (the confirmed unbounded stuck-ON path). C: Linux parity —
every UHID backend now arms the shared watchdog (Steam Input drives these pads over hidraw
with Windows abandonment semantics) and the uinput mixer force-stops abandoned
infinite-replay FF effects (FfState, unit-tested). Shared PUNKTFUNK_RUMBLE_IDLE_MS hatch
(0 = off; non-zero floored above SDL's ~2 s rumble resend).
A: PadShm v2.1 — a 1024 B tail extension carrying an 8-slot lossless output-report ring,
feature-negotiated via zeroed reserved fields (out_ring_ver; deliberately NO
GAMEPAD_PROTO_VERSION bump — mixed generations degrade to the legacy latest-report slot
instead of failing closed). The pf-dualsense driver dual-writes both planes
(publish_output); the host's shared OutputDrain drains oldest->newest with a torn-read
recheck and an overflow->resync path (PadFeedback.resync force-stops + re-arms dedups).
pf-umdf-util grows a min_data_size map fallback. Ds*Feedback.fresh removed (dead).
design/rumble-root-fix.md par. A-C. Verified: pf-inject tests+clippy Linux+Windows (53/53
on winbox incl. the stop-coalesce repro); drivers ws check+clippy on the CI runner.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
inject.rs + inject/* (the per-OS injectors — wlroots virtual-input, KWin
fake_input, libei/reis, gamescope-EI on Linux; SendInput on Windows — plus the
virtual-gamepad HID stack: DualSense/DualShock4/Switch Pro/Steam Controller/Deck
over uhid/usbip and the Windows UMDF drivers, the proto codecs, the injector
service, and the uhid manager) move into crates/pf-inject behind the
InputInjector trait (plan §W6). It consumes punktfunk_core::input (the neutral
GamepadEvent/InputEvent vocabulary, moved to core in W5) + the pf-driver-proto
wire contract, and reaches pf-capture only for the Windows gamepad-channel
WUDFHost check + the resident-mouse compose-kick hook.
The one inject->vdisplay coupling (the libei gamescope-EI backend needs the EIS
relay socket path) is broken via a leaf: gamescope_ei_socket_file moves to
pf-paths as the shared contract — the gamescope producer (host vdisplay) keeps
its session-env-lock wrapper around it, the libei consumer (pf-inject) reads it
directly post-retarget. The host keeps a `mod inject { pub use pf_inject::* }`
shim so every crate::inject::* path (the native/gamestream input planes + devtest)
is unchanged; the heavy input deps (wayland/reis/xkbcommon/usbip + the KWin
fake-input protocol XML) moved with the crate.
Verified: Linux clippy -D warnings (pf-inject + host nvenc,vulkan-encode,pyrowave
--all-targets) + pf-inject 69/69 + host 230/230 tests; Windows clippy -D warnings
(pf-inject --all-targets + host nvenc,amf-qsv --all-targets) Finished exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>