d5ae8dcc3ea3526bd63138b763c39dd7a15e3278
939
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d5ae8dcc3e |
feat(capture): gamescope cursor via XFixes shape + QueryPointer position (Phase C)
gamescope excludes its pointer from the PipeWire node it feeds us and can't embed one either (`set_hw_cursor` is inert), so every gamescope stream was cursorless. Read the pointer from gamescope's nested Xwayland instead — XFixesGetCursorImage for shape/hotspot/visibility, core QueryPointer for position — and publish a CursorOverlay into the capturer's existing `cursor_live` slot, so the encoder blend composites it into the video exactly like the SPA_META_Cursor path. - pf-capture/src/linux/xfixes_cursor.rs (new): the XFixes reader. Connects to EVERY nested Xwayland (Gaming Mode runs one per --xwayland-count) and follows the focused one each tick (the display whose pointer moves), reading that display's own cursor shape. Un-premultiplies ARGB -> straight RGBA. Drop stops the thread. - Capturer::attach_gamescope_cursor + the PortalCapturer override spawn it into the same `cursor_live` slot; pf_vdisplay::gamescope_xwayland_cursor_targets discovers the (DISPLAY, XAUTHORITY) pairs via the GAMESCOPE_WAYLAND_DISPLAY scan. - host: SessionPlan.gamescope_cursor (set from the compositor at both resolve sites AND on a mid-stream Desktop->Gaming switch); the blend gate now builds the encoder blend for gamescope; a sibling composite arm attaches capturer.cursor() per tick for capture-mode clients (no channel needed). native NV12 is disabled for these sessions — that encode path can't blend the cursor (it assumes gamescope embeds the pointer), so we capture RGB and route to the proven CUDA VkSlotBlend / compute-CSC blend. - the pipewire thread no longer clobbers `cursor_live` with None on a buffer that carries no SPA_META_Cursor (gamescope) — that raced the XFixes writer and strobed the composited pointer on/off. On-glass (home-bazzite-2, RTX 5070 Ti, Gaming Mode): cursor visible + steady in Big Picture and CS2 menus, follows focus between the two Xwaylands, hidden in-game. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
925130d1f9 |
feat(vdisplay): compositor-embedded pointer for sessions without the cursor channel (Phase B)
Since the cursor-channel work, every Linux virtual output was created in metadata pointer mode — making ALL sessions depend on host-side cursor compositing, including the ones that can never use the channel (Moonlight/ GameStream, legacy clients, capture-mode starts). Those sessions paid the blend bring-up per session and, whenever a visible cursor was composited, the loss of NVENC's stream-ordered submit — for a strictly worse cursor than the compositor's own. Mirror the Windows no-regression gate: the already-wired per-session set_hw_cursor(cursor_forward) now drives the Linux backends too. A cursor-channel session gets metadata (shapes forwarded, composite flip blends host-side — today's validated path, unchanged); every other session gets the pointer compositor-EMBEDDED at creation (KWin zkde pointer=2, Mutter cursor-mode=1, wlroots/hyprland portal CursorMode::Embedded — their pre-channel default; the portal pair also gains the Metadata arm for channel sessions, wired but untested on-glass). SessionPlan.cursor_blend narrows to cursor-forward sessions and rides into the direct-SDK NVENC open, which skips the Vulkan slot-blend bring-up entirely when off — embedded sessions ring on plain CUDA surfaces and pay zero cursor cost, per-session or per-frame. The keep-alive registry's reuse key grows the created pointer mode (new VirtualDisplay::hw_cursor getter): a kept embedded display has no cursor metadata for a channel session to forward, and a kept metadata display would leave a channel-less session with no pointer in its frames. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d2c46eaf3c |
feat(encode/nvenc): SPIR-V cursor blend over Vulkan-allocated input slots — retire the PTX kernels
A vendored PTX blob is JIT'd against the driver's ISA ceiling, so the cursor-blend module silently dies on drivers older than the generating toolkit (CUDA_ERROR_UNSUPPORTED_PTX_VERSION/INVALID_PTX, 222/218 — the KWin leg's invisible composite cursor on driver 595/CUDA 13.2 vs a CUDA 13.3 blob). SPIR-V has no such coupling, and the in-tree precedent already exists twice (vulkan_video's CSC blend, VkBridge's exportable OPAQUE_FD → cuImportExternalMemory bridge). New pf_zerocopy::vkslot::VkSlotBlend: the direct-SDK NVENC encoder now allocates its input ring as exportable Vulkan buffers CUDA-imports (same contiguous InputSurface layouts, pitch = row bytes rounded to 256), and the cursor composite is a compute dispatch over the cursor's rectangle (cursor_blend.comp, vendored .spv; spec-constant selects ARGB/NV12/YUV444; BT.709 limited, matching the retired .cu). The surface SSBO is uint[] with every invocation owning whole words — no 8-bit-storage device dependency. Cursor-bearing frames force the existing CPU-synced submit path so the CUDA copy → Vulkan dispatch (fence-waited) → NVENC encode ordering is CPU-established; cursorless frames keep the stream-ordered fast path untouched. Any bring-up/alloc/registration failure falls back wholesale to plain pitched CUDA surfaces (never a mixed or short ring): sessions always encode, composite mode just loses the cursor, warned once. cursor_blend.cu / cursor_blend.ptx and the CursorBlend PTX loader are deleted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
33121ece4d |
chore(kwin-composite): TEMP blend-path probes — DROP BEFORE MERGE
Rate-limited journal logging bracketing the silent segment of the Linux composite path: what overlay (if any) the encode loop's composite arm hands the encoder, and whether the NVENC cursor-blend kernel launches and with what geometry. On-glass (KWin leg) the blend module loads yet the composite cursor stays invisible — these two probes attribute it to no-overlay / stripped-en-route / kernel-draws-nothing. The module-loaded INFO line is permanent (success must be as attributable as failure); everything else in this commit is temporary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
af87549052 |
fix(linux/vdisplay): KWin virtual outputs stream above 60 Hz via sacrificial-birth renegotiation
KWin's ScreenCastStream builds its PipeWire format offer — including the maxFramerate cap it actively throttles delivery to — ONCE at stream creation, when the virtual output sits at its hardcoded birth 60 Hz. A kscreen custom-mode change afterwards updates the OUTPUT (readback says 240) but never the offer, so every consumer negotiates max=60 and the stream delivers 60 (pw-dump-verified on KWin 6.6.4). The only path that rebuilds the offer is the stream's own resize handling: a source SIZE change while recording re-runs buildFormats — picking up the output's current refresh — and renegotiates the live stream. So above 60 Hz, birth the output at a sacrificial height (+16), then install + select the real WxH@hz custom mode: the first frame recorded after the consumer connects triggers KWin's resize path, which renegotiates the live stream to WxH@hz. The capturer holds (requeues) buffers until the negotiated size matches — new expect_exact_dims plumbing VirtualOutput → registry (fresh creates only) → open_virtual_output → a self-disarming gate in the process callback — bounded by a 3 s deadline that accepts the producer's dims rather than wedging the session into the first-frame retry loop. set_custom_refresh reads back the full active mode; a size reject (pre-6.6 KWin) recreates plain at the real size @60. Every kscreen operation now addresses the output by its NUMERIC id, resolved by matching the managed name-prefix AND the just-created birth size (newest id wins): a mode-switch supersede reuses the per-slot output NAME (deliberately, for KWin's per-name config persistence) while the superseded sibling is still alive, and name addressing hit the FIRST match = the OLD output — on-glass that resized the live session's display out from under it (wrong-res/black), read the old output back as 'mode applied', and set the old output primary while the replacement starved at its birth mode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
09113c9899 |
fix(encode/nvenc): cursor-blend PTX must carry a driver-portable ISA version
The vendored cursor_blend.ptx was regenerated with the CUDA 13.3 toolkit, which stamps '.version 9.3' — and a driver whose JIT predates that ISA refuses the whole module with CUDA_ERROR_UNSUPPORTED_PTX_VERSION (222), silently killing host-side cursor compositing (on-glass: composite-mode cursor invisible on driver 595.58, KWin leg). The kernels use only baseline arithmetic, so hand-lower the version directive to 8.0 (CUDA 12.0), which every supported Turing+ driver JITs. Both files now warn that an nvcc regeneration re-stamps the version and must be re-lowered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4436c7fb61 |
fix(host/stream): composite mode re-blends the LIVE cursor on repeat ticks
The frame-attached overlay is the pointer position at the last damage frame; repeats re-encoding a static desktop froze the blended cursor between redraws — on-glass the composite-mode cursor stuttered while window drags (constant damage) were smooth. Refresh the repeat's overlay from Capturer::cursor each tick so pointer-only motion re-blends at tick rate — the bandwidth the pre-channel embedded mode already paid. Linux only: the Windows capturer composites internally and its frames must never carry an overlay a blend path would double-draw. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
088004cac4 |
feat(linux/capture): serve the LIVE cursor overlay through Capturer::cursor
The forwarder read only frame.cursor — the overlay attached to the LAST EMITTED frame. Mutter's pointer-only buffers update the capture-side cursor state but are skipped as frames, so between damage frames the forwarder re-sent a stale snapshot: on-glass the client cursor appeared exactly when clicking or crossing hover targets (damage) and froze/vanished otherwise — the same pointer-only-motion gap the Windows IddCx channel fills, which is why the encode tick already prefers the capturer's LIVE cursor. Publish the overlay from every dequeued buffer (frames or not) into a shared slot and implement the Capturer::cursor hook on PortalCapturer. Host-composite mode on Linux still updates the blended pointer only on damage frames (no re-encode of a static desktop on cursor motion) — the regen-on-cursor-change the Windows path has is a follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
63d83217ef |
fix(linux/capture): a stale id-0 cursor meta is 'no information', not 'hidden'
Mutter only rewrites a buffer's SPA_META_Cursor region when the cursor changed; recycled buffers between damage frames carry a stale id-0 meta. Treating id 0 as a hidden pointer flickered the client cursor off between hovers on-glass — per the SPA contract id 0 is 'invalid/no cursor info', so keep the last-known state (OBS's consumer does the same). A genuinely hidden pointer stops producing updates; the M3 relative-mode hint keeps its Windows CURSOR_SUPPRESSED source. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9db4841ce4 |
fix(linux/capture): cursor meta size range must cover Mutter's fixed 384x384 offer
Mutter offers SPA_META_Cursor with a FIXED size pod — SPA_POD_Int(CURSOR_META_SIZE(384, 384)) in meta-screen-cast-stream-src.c — while our request capped the range at meta_size(256, 256). The intersection is empty, so the Meta param silently failed to negotiate and no buffer ever carried the meta region: the entire Linux cursor pipeline (forward AND blend) was blind on GNOME, proven by an on-glass probe counting 32k+ buffers with zero metas. KWin's offer fits inside 256², which is why the same consumer code passed there. Raise the max to 1024² headroom (the negotiated allocation follows the producer's value, not our max) and align the bitmap parse guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
151adc4bcc |
chore(m2b): TEMP cursor-meta probe — DROP BEFORE MERGE
Rate-limited journal logging of SPA_META_Cursor arrival/absence, reported id/position/bitmap offset, and bitmap acceptance — the Linux cursor pipeline is otherwise blind end-to-end during Mutter RecordVirtual metadata bring-up on .21. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0619af391e |
fix(linux/inject): map absolute pointer/touch into the streamed output's region
The libei absolute device advertises one region per logical monitor, and both MouseMoveAbs and touch mapped into regions().first() — whichever output the compositor announced first. Next to a physical monitor (on-glass: GNOME with a dummy HDMI beside the virtual primary) that put the pointer and every click on the WRONG output: the seat cursor never entered the streamed monitor, so neither embedded nor metadata cursor capture could ever see it, and the cursor channel had nothing to forward. Pick the region whose logical size matches the streamed mode (the wire flags already carry it); fall back to first() for the single-monitor case. Region geometry now rides the device-RESUMED log line for diagnosability; matching the screencast mapping_id instead of the size is the follow-up for same-sized-monitor ambiguity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
afe120bf34 |
fix(linux/vdisplay): kwin virtual output ships the cursor as metadata too
Same trap as the Mutter backend: zkde_screencast pointer mode 2 (embedded) never delivers SPA_META_Cursor, starving both the cursor channel and the encoder blend. Mode 4 (metadata) feeds both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c29186bd1a |
fix(host+vdisplay): a mode-switch replacement display inherits the group topology
The resize rebuild creates the new virtual display BEFORE retiring the old
one (create-before-drop), so the registry saw a live same-backend sibling —
its own dying predecessor — and told the backend it was not first in group.
Mutter then skipped the Primary/Exclusive apply ("joining an existing
display group — extending") and the retiring owner took the topology with
it: every resize silently demoted the virtual output to an extended,
shell-less desktop. Thread the superseded pool gen through acquire so the
replacement establishes topology, and stop counting kept (Lingering/Pinned)
entries as demoting siblings — no session owns them, so there is no live
desktop to clobber.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
315ffaf144 |
fix(linux/vdisplay): mutter virtual monitor ships the cursor as SPA_META_Cursor
RecordVirtual was created with cursor-mode EMBEDDED, which never delivers cursor metadata — the cursor channel had nothing to forward (client-drawn cursor invisible) and the always-built encoder blend had nothing to composite (host-drawn cursor invisible too). Metadata mode feeds both: non-forwarding sessions get the blended pointer (same pixels Mutter would have embedded), forwarding sessions strip the overlay and send shape/state. The capturer already negotiates the meta on every stream (meta_param). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c63ac48c9c |
fix(windows/capture): actually QUERY the HDR SDR-white scale — the refresh block was silently dropped
faad4b57 shipped the field, shader scale, and plumbing but the scripted edit inserting the sdr_white_level_scale() refresh in the scratch-rebuild arm matched nothing and silently no-op'd (no assert) — sdr_white_scale stayed at its 1.0 init, i.e. 80 nits, i.e. exactly the old dark cursor. Now queried on every scratch rebuild, with an info log of queried/applied so an HDR session shows what it uses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cecc059830 |
fix(windows/capture): 250 Hz cursor poll + HDR white-scale observability
- The polled position is also the composite-blend position: at 16 ms a 240 fps session reused a stale position for ~4 consecutive frames and the composited pointer visibly stuttered against the video. 4 ms out-paces every session rate; a tick is one GetCursorInfo syscall. - Log the queried DISPLAYCONFIG_SDR_WHITE_LEVEL scale on each blend- scratch rebuild so an HDR session shows what is actually applied (the virtual display's own SDR-brightness setting is the suspect when the cursor still reads dark). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d31d41b6b8 |
fix(windows/capture): match the HDR-composited cursor to the target's SDR white level
sRGB→linear alone lands cursor-white at 80 nits (scRGB 1.0) while DWM composes the surrounding SDR desktop at the user's SDR-brightness setting (~200+ nits by default) — the cursor read visibly dark on HDR. Scale by DISPLAYCONFIG_SDR_WHITE_LEVEL (new win_display::sdr_white_level_scale, queried on blend-scratch rebuilds — the CCD query stays off the per-frame path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6e37da8b4e |
fix(windows/capture): clippy unnecessary_lazy_evaluations in scratch build
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0af280a793 |
feat(windows/capture): host-side cursor compositing for the capture model — the deterministic fix
Root cause, proven on-glass across five driver builds: a declared IddCx hardware cursor is IRREVOCABLE. There is no un-declare DDI, the empty-caps re-setup is rejected INVALID_PARAMETER, and after a successful same-mode re-commit with the driver's re-declare provably suppressed (sticky per-target flag, zero re-setups logged) DWM still never composites the software cursor back into the monitor's frames. Every DWM-based composite flip is therefore unfixable. The composite (capture) model is now implemented in OUR pipeline: - the driver keeps its hardware cursor declared for the session's whole life (the state that works) — frames stay pointer-free always; - in composite mode try_consume routes the conversion through a blend scratch: slot copy + one alpha-blended quad of the GDI poller's shape at its polled position (CursorBlendPass — fullscreen-triangle VS with viewport placement, straight-alpha PS, sRGB→linear for FP16/HDR rings), covering all four convert paths (NV12, 4:4:4 copy, P010, PyroWave) GPU-side under the slot's keyed mutex; - pointer-only motion produces no driver publish (declared hw cursor), so try_consume REGENERATES from the last slot whenever the polled cursor state changes — the cursor moves on a static desktop at tick rate, without faking freshness for the stall/death watchdogs; - set_cursor_forward becomes purely host-internal state; the SET_CURSOR_FORWARD IOCTL machinery is no longer used by the host (driver keeps it, dormant) and its sender plumbing is removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b50e4767de |
fix(windows/cursor-flip): flag + forced same-mode re-commit — the working un-declare
The empty-caps un-declare is REJECTED by IddCx (STATUS_INVALID_PARAMETER, on-glass driver 9.9.0722.1407) — there is no un-declare DDI. The composite flip now works with the only lever the OS gives us: every mode COMMIT reverts the path to the software cursor, and the driver skips its per-commit re-declare while cursor_forward_on is off. So: - driver: disable stores the flag only (no DDI call); enable still declares immediately against the live worker's event. - host capturer: after a successful disable flip, force a same-mode re-commit (win_display::force_mode_reenumeration) — DWM composites the pointer from the very next commit; the swap-chain flap is the class the driver's preserved-publisher machinery already rides out. - state now survives re-arrivals end-to-end: the capturer caches the APPLIED state (cleared on every channel (re)delivery, which re-created driver entries need — their flag defaults to declared), and the stream loop re-applies every tick instead of edge-gating (steady-state cost: one Option compare). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
79d30fde64 |
fix(windows/capture): unsafe block for the extracted dup_into_public call
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6a8df2ba97 |
fix(windows/cursor-flip): survive driver-side monitor re-arrival — flag-based flip + channel re-delivery
On-glass (.173, match-window session): every window-size convergence re-creates the driver-side monitor (re-arrival resize / sibling-session slot churn), destroying the cursor worker the channel was delivered to — the flip then died NOT_FOUND (no entry with target_id ∧ worker) and the declared state was lost entirely. Driver: set_cursor_forward becomes STATE, not an edge on one monitor generation — find by target_id ∧ hw_cursor, store cursor_forward_on even without a live worker (it steers the next delivery/re-setup); declare/un-declare only when a worker exists. Channel delivery honors the flag: setup_and_spawn(declare=false) adopts + spawns WITHOUT declaring (composite mode), so a later enable-flip declares against the worker's event. Host: retain the SET_CURSOR_CHANNEL sender; extract deliver_cursor_channel and RE-deliver the surviving section on every ring recreate and — with one retry — when a flip IOCTL fails (the re-arrived entry gets a fresh worker, declared per its flag). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
92761813a9 |
feat(core+host+driver+client): mid-stream cursor-render flip — host composites in the capture model
The cursor channel excluded the host pointer from the video for the whole session, but the client only draws it under the DESKTOP mouse model — flipping ⌃⌥⇧M to capture mid-stream left the session pointer-blind (user-found). The fix makes render ownership LIVE state: - wire: CursorRenderMode 0x51 (client→host, control stream) — client_draws: true = desktop model (host excludes + forwards), false = capture model / released (host composites, full fidelity — DWM on Windows incl. real XOR inversion, encoder blend on Linux). Sessions start client_draws=true (the pre-message behavior). - host: control task stores the flag; the encode loop edge-detects it — forwarding + frame.cursor strip while the client draws, quiet forwarder + overlay-into-blend while composited. SessionPlan now grants blend CAPABILITY wherever the capture has a pointer (dropping the !cursor_forward gate) so the composite side needs no encoder rebuild; per-tick frame.cursor decides what is drawn. - Windows: Capturer::set_cursor_forward → retained IOCTL_SET_CURSOR_FORWARD sender (proto v6) → driver (un)declares the IddCx hardware cursor on the LIVE monitor. Un-declare re-issues the setup with EMPTY caps (candidate mechanism — the DDI has no documented un-setup); resetup-on-commit is skipped while off, so a mode commit's software-cursor default is the fallback path. Failure against a pre-v6 driver logs and keeps declared-at-ADD behavior. - SDL client: one edge-detected reconciler at the cursor pump site — desktop-active = client draws; capture model or released = host composites. Covers the chord, the M3 auto-flip, and engage/release. - C ABI v12: punktfunk_connection_set_cursor_render (additive; wire version unchanged — pre-§8 hosts ignore the unknown message type). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f2082f8b57 |
fix(windows/capture): drop redundant unsafe at the poller-spawn CCD call (inside open_on's unsafe region)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e59634a578 |
feat(windows/capture): GDI cursor-shape poller — masked/monochrome cursors for the cursor channel
The M2c redesign (design/remote-desktop-sweep.md §8): the IddCx hardware- cursor query is alpha-only by design — IDDCX_CURSOR_SHAPE_TYPE has no monochrome value, the OS pre-converts monochrome to masked-color, and masked-color delivery is dead code on modern builds (proven on-glass at every ColorXorCursorSupport level). The driver keeps its hardware cursor declared (XOR FULL) purely so DWM excludes every cursor type from the IDD frame; the SHAPE now comes from a GDI poller in the capture host — which runs as SYSTEM inside the interactive session, so no helper process is needed. CursorPoller (idd_push/cursor_poll.rs, the RustDesk/WebRTC/OBS pattern): ~60 Hz GetCursorInfo on a dedicated thread; rasterise via CopyIcon→GetIconInfo→GetDIBits only when the HCURSOR value changes; monochrome via the WebRTC truth table with invert pixels as opaque black plus a white outline; AND-mask alpha for alpha-less color cursors; CURSOR_SUPPRESSED = hidden; per-output visibility via the target's desktop rect; per-monitor-V2 DPI awareness thread-scoped; input-desktop reattach on failure + 2 s cadence (GetCursorInfo on a stale desktop succeeds with stale data). Spawned iff the cursor channel was delivered; a live poller is the sole overlay source (no serial-namespace mixing) — the driver shm read remains as fallback if the poller dies. Not chosen: DXGI Desktop Duplication GetFramePointerShape — PointerPosition.Visible goes stale under injected-only input on current Win11 (Sunshine #5293, exactly our topology), it burns one of the session's four duplication slots, and IDD-output metadata has conflicting field reports. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a99ef7f940 |
fix(windows): hw_cursor_capable must not assume an initialised display manager
The FIRST session's Welcome runs before any backend construction (vdisplay::open happens at display prep, after the Welcome), so the capability probe's vdm() expect panicked the very first handshake of a fresh service — the client saw a dead connect and auto-wake kicked in. init() is idempotent and the driver facade is a unit struct, so the probe now initialises the manager itself. Found on-glass (.173, first Mac-client connect), fix deployed there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ec8ca9a535 |
feat(apple): cursor channel on the Mac client — ABI v11 + NSCursor rendering
The Mac client now exercises the full cursor channel against capable hosts: desktop-mode sessions advertise CLIENT_CAP_CURSOR, the host stops compositing the pointer, and StreamView draws the forwarded shapes as the real NSCursor — plus the M3 host-driven model flip. - ABI v11: punktfunk_connect_ex9 (adds client_caps; ex7/ex8 pass 0 — Android untouched), PunktfunkCursorShape/State + the two next_cursor_* poll fns (audio-style borrow contract), and PUNKTFUNK_CLIENT_CAP_CURSOR. Fixed in passing: the first insertion split next_host_timing's cfg/no_mangle attributes off the fn, which silently dropped the symbol from the header AND dylib — caught by the Swift build, reattached with its docs. - PunktfunkConnection: clientCaps connect param, hostSupportsCursor, CursorShapeEvent/CursorStateEvent + nextCursorShape/nextCursorState (one cursor thread drains both planes; cursorLock joins the close() ladder). - SessionModel: desktop-mode sessions (DefaultsKey.mouseMode) connect with the cursor cap on macOS. - StreamView: shape cache (serial → NSCursor, straight-alpha RGBA via CGImage), resetCursorRects wears the HOST shape while the desktop model is engaged (hidden host pointer ⇒ invisible), latest-wins state pump, and the M3 auto-flip — edge-triggered on relative_hint, ⌃⌥⇧M sets an override latch cleared by the next host edge, leaving relative warps the pointer to the host position via the inverse letterbox mapping (CGWarpMouseCursorPosition, CursorCapture's coordinate convention). Verified: swift build + full test suites green (xcframework rebuilt at ABI v11, signed); core 218 tests + clippy -D warnings on Linux (.21). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3a34440a6b |
feat(windows): IddCx hardware-cursor channel — remote-desktop sweep M2c
Brings the cursor channel to Windows hosts. The pf-vdisplay driver declares an IddCx hardware cursor for sessions that negotiated cursor-forward — DWM then EXCLUDES the pointer from the IDD frame and delivers shape/position out-of-band, into the same CursorOverlay → forwarder → wire → client pipeline the Linux portal path uses. - pf-driver-proto v5 (additive, host floor stays 3): AddRequest's spare tail becomes hw_cursor (same size/offsets); IOCTL_SET_CURSOR_CHANNEL delivers a host-created CursorShm section (64-byte seqlock header + 256² shape buffer, layout pinned + tested). No event crosses the boundary — the host polls at encode-tick pace. - driver: wdk-iddcx grows the two cursor DDI wrappers; a per-monitor cursor worker (event wait → QueryHardwareCursor → seqlock publish) starts only when BOTH the ADD asked and the channel arrived, so a failed delivery leaves DWM compositing as today. Shape bytes ship raw (BGRA/masked + pitch); the host converts. - host: the section rides the existing sealed-channel broker (least- privilege dup, remote reap on failure); IddPushCapturer::cursor() seqlock-reads → CursorOverlay (BGRA→RGBA, masked-color approximation, desktop→frame origin shift, per-shape conversion cache). New Capturer::cursor() trait hook — the encode loop prefers it over the frame-attached overlay because hardware-cursor moves produce NO new frame on a static desktop. hw_cursor survives the re-arrival resize (carried on the manager's Monitor). - negotiation: cursor_forward grows the Windows arm (client cap ∧ driver proto ≥ 5, probed once via the control device); SessionPlan carries cursor_forward → OutputFormat.hw_cursor. - drive-by: wdk-probe's two pre-existing same-type casts (clippy) and pf-vdisplay's stale spike-test refs (crate::win_display moved to pf-win-display; tracing-subscriber was never a dep) repaired. Verified: proto tests on Mac AND MSVC (15/15 incl. the CursorShm layout pin); clippy -D warnings for proto/frame/capture/vdisplay/host on Linux (.21) and native Windows (.173); the DRIVER workspace clippy -D warnings green against the real WDK 10.0.26100 bindgen (DDI names, enum variants and IDARG layouts all bind). On-box driver deploy + on-glass validation follow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c3cbffe662 |
feat(host+client): host-driven mouse-model flip — remote-desktop sweep M3
The full Parsec model: launching a game from a desktop session flips the client into captured relative automatically, and back — no chords. - Capture overlay now distinguishes 'hidden' from 'no cursor yet': CursorOverlay grows a visible flag, overlay() returns Some whenever a bitmap is known (the encode loop strips invisible overlays after forwarding so no blend path ever draws one; the CPU composite guarded on visibility already). - Host forwarder maps it onto the reserved wire bit: visible ⇒ VISIBLE, hidden-but-known ⇒ RELATIVE_HINT (an app grabbed/hid the pointer), never any hint before the first bitmap — a cold start can't flip a desktop session into capture. - Presenter: edge-triggered auto-flip on hint changes via the new Capture::set_desktop (chord semantics untouched); a manual ⌃⌥⇧M sets an override latch that holds until the HOST's intent next changes, so the hint never fights the user. Leaving relative warps the local cursor to the host's last pointer position through the new content_to_window inverse-letterbox mapping (unit-tested against finger_to_content) — the hand-back is seamless. Verified on .21: fmt + clippy -D warnings + tests (presenter 20 incl. the inverse-mapping roundtrip, host 245). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4a01bc4463 |
feat(core+host+client): cursor channel — remote-desktop sweep M2a+M2b
The host cursor stops riding the video and becomes a real OS cursor on the client (the Parsec/RDP model): pointer feel no longer pays the capture→encode→network→decode→present round trip. Wire (M2a): - Hello grows a client_caps trailing byte (CLIENT_CAP_CURSOR) after the fixed display_hdr block — presence disambiguated by remaining length, which caps the post-HDR tail at 27 bytes (documented); Welcome answers HOST_CAP_CURSOR (capable-and-asked, the 444/clipboard precedent). - CursorShape (0x50, control stream): serial + dims + hotspot + straight RGBA, ≤120px/side so the u16 frame always fits (128² would overshoot); client caches by serial — re-showing a known shape costs 14 bytes, not a bitmap (RDP pointer-cache for free). - CursorState (0xD0 datagram): serial + visible/relative_hint flags + position, sent once per encode-loop tick — latest-wins, self-healing under loss, no refresh timer. relative_hint is reserved for M3. - Client core: two new planes (control-task + datagram-task arms) → next_cursor_shape/next_cursor_state; connect() grows client_caps (C ABI passes 0 until the v11 cursor poll fns exist). Host (M2b, Linux portal only): - handshake::cursor_forward is THE predicate (client asked ∧ Linux ∧ compositor ≠ gamescope) — Welcome bit and session wiring both read it. - SessionPlan.cursor_blend goes false for a forwarding session; the encode loop ticks a CursorForwarder every iteration: shape-serial diff → control-task bridge (mirrors probe_result), state datagram → conn. - CursorOverlay/capture CursorState carry the hotspot through (nearest-neighbor downscale backstop for XL cursors, unit-tested). Presenter: - CursorChannel drains both planes per loop iteration; shapes become SDL color cursors (from_surface + hotspot), applied while the desktop mouse model is engaged; visibility follows the host; capture/released hands back the system cursor. Sessions advertise the cap when they START in desktop mode. Verified on .21: fmt + clippy -D warnings (7 crates) + tests green (core 218 incl. new wire roundtrips, host 245 incl. e2e + forwarder downscale tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
df04880273 |
feat(client/input): desktop (absolute) mouse mode — remote-desktop sweep M1
New physical-mouse model beside capture: Desktop leaves the pointer uncaptured and sends absolute positions through the letterbox (MouseMoveAbs — every host injector already consumes it). Capture stays the default and the game model. - pf-client-core: MouseMode (capture|desktop) persisted in Settings, default capture so existing stores are unchanged. - pf-presenter: Capture grows a desktop model — latest-wins pending abs position coalesced per loop iteration (same 1000 Hz discipline as relative), flushed before clicks/keys/wheel so they land where the cursor is; Ctrl+Alt+Shift+M flips the model live; local cursor stays hidden over the window (the host's composited cursor is the one you see until the M2 cursor channel); Windows keyboard grab only engages for capture (a desktop stream is something you Alt-Tab away from). - gamescope gating: its EIS is relative-only, so desktop mode is pinned off there (resolved_compositor), with a log note. - Settings surfaces: GTK row (dynamic caption), WinUI combo, console-UI row + step test, capture-hint line. Verified: fmt + clippy -D warnings + tests (33/40/19) on Linux .21; clippy -D warnings for all five crates incl. punktfunk-client-windows native on .173. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0e7f6d85d6 |
feat(gamestream): signal HDR mode to the client via the 0x010e control message
ci / web (push) Successful in 1m14s
apple / swift (push) Successful in 1m20s
ci / docs-site (push) Successful in 1m20s
decky / build-publish (push) Successful in 20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 7s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
ci / bench (push) Successful in 6m41s
apple / screenshots (push) Successful in 6m12s
deb / build-publish (push) Successful in 9m48s
docker / deploy-docs (push) Successful in 25s
deb / build-publish-host (push) Successful in 10m26s
windows-host / package (push) Successful in 16m13s
android / android (push) Successful in 16m27s
arch / build-publish (push) Successful in 17m20s
ci / rust (push) Successful in 20m1s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m43s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m41s
punktfunk negotiates HDR correctly (the stream is BT.2020 PQ 10-bit) but never told the client to switch its display into HDR picture mode, so HDR streams stayed in SDR on the TV. Sunshine sends an async control-stream message (IDX_HDR_MODE, type 0x010e, carrying SS_HDR_METADATA) shortly after the stream starts; Moonlight only flips the TV into HDR on that cue. We sent exactly one host→client control message (rumble 0x010B) and never this one. (aurora-tv PR #53 worked around it client-side by inferring HDR from the negotiated format; this fixes it host-side so stock Moonlight clients switch too.) Once the control stream is live and the session negotiated HDR, send a one-shot 0x010e message (enable + 26-byte SS_HDR_METADATA from generic HDR10 defaults), latched per connection and re-armed on Disconnect. The rumble sequence counter is unified into a single monotonic `host_seq` shared by both message types — the GCM nonce derives from `seq`, so a separate per-type counter would reuse (key, nonce) pairs in the host direction. The BT.2020 PQ VUI already rides the bitstream (the encoder derives it from the P010 capture format), so this is purely the missing "switch now" signal; no encoder change needed. Wire layout locked by unit tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2dddf0fefc |
fix(gamestream): accept legacy X.509 v2 Moonlight client certs in mTLS pairing
rustls-webpki 0.103 accepts only X.509 v3 certs and returns `UnsupportedCertVersion` for anything older, aborting the mTLS handshake at the `CertificateVerify` step before it ever checks the signature. The moonlight-embedded client family (moonlight-embedded, aurora-tv, …) still mint self-signed **v2** certs with no keyUsage extension, so pairing fails against punktfunk while it works against Sunshine — whose OpenSSL verify callback never inspects the cert version. (aurora-tv PR #53 worked around this client-side by switching to v3 + keyUsage; this fixes it host-side for stock clients too.) The cert's X.509 version and extensions carry no security weight here: the client cert is self-signed and pinned by SHA-256 (`peer_is_paired`), and the PIN pairing ceremony is the real proof of identity. The property that DOES matter — that the peer holds the private key for the cert it presented — is still enforced: when webpki rejects the cert we re-run exactly that `CertificateVerify` check with a version-agnostic parser (x509-parser SPKI + the same `rsa` verify `pairing::verify256` uses). This is a full cryptographic check, never a bypass: a bad signature still fails, and non-RSA / unsupported schemes fall through to webpki's original error. Matches Sunshine's leniency without loosening the pinned trust model. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
abe6228b42 |
feat(core+host+android): committed-text input plane (IME path) — InputKind::TextInput
The VK key-event vocabulary cannot express text an input method COMMITS (autocorrect, gesture typing, non-Latin scripts, emoji). Add a first-class text event and negotiate it: - punktfunk-core: InputKind::TextInput (= 15) carries one Unicode scalar per event in `code`; HOST_CAP_TEXT_INPUT (0x04) in Welcome::host_caps. - Host advertises the cap only where the session's inject backend can type text: Windows SendInput (KEYEVENTF_UNICODE, surrogate-pair aware) and the Linux wlroots backend — a dedicated second zwp_virtual_keyboard whose xkb keymap grows Unicode keysyms on demand (the wtype model), so keymap re-uploads never disturb the main device's layout/modifier state. The KWin-fake-input/libei/gamescope backends can only press layout keycodes, so those sessions don't set the bit and clients keep the VK fallback. - GameStream plane: Moonlight's UTF-8 text packet (MAGIC_UTF8, previously recognized-and-dropped) now decodes to the same TextInput events. - Android: KeyCaptureView picks a real editable InputConnection when the host has the cap — the IME runs its full machinery, mirrored to the host live via common-prefix diffs of the composition (backspaces + new suffix), with setComposingRegion adopting committed text so autocorrect-revert flows diff instead of retyping; newline→Enter, deleteSurroundingText→Backspace/Delete. Older hosts keep the TYPE_NULL raw-key path unchanged. - keymap: media VKs (0xB0-0xB3) → evdev so the Android media keys land on Linux hosts too. Verified: punktfunk-core + host gamestream + pf-inject tests green on Linux (Ubuntu box), clippy clean; Android app+native builds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5e088af7ed |
fix(pf-win-display): unpin mode indexes on paths the CCD isolate deactivates
ci / web (push) Successful in 49s
apple / swift (push) Successful in 1m24s
ci / docs-site (push) Successful in 55s
decky / build-publish (push) Successful in 21s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
apple / screenshots (push) Successful in 6m33s
ci / bench (push) Successful in 6m41s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 5m23s
android / android (push) Successful in 12m50s
docker / deploy-docs (push) Successful in 25s
deb / build-publish-host (push) Successful in 10m10s
arch / build-publish (push) Successful in 17m18s
deb / build-publish (push) Successful in 12m51s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m3s
ci / rust (push) Successful in 27m3s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m32s
Field report: with Exclusive Topology, the physical monitor sometimes stayed lit — every isolate attempt re-committed and failed SetDisplayConfig with 0x57 ERROR_INVALID_PARAMETER. Clearing DISPLAYCONFIG_PATH_ACTIVE alone leaves the deactivated path's source/target modeInfoIdx pinned to the queried mode entries, and per the SetDisplayConfig contract a path being turned off must have BOTH mode indexes set to DISPLAYCONFIG_PATH_MODE_IDX_INVALID; some driver/topology combinations (notably when the doomed path shares a source with the kept virtual path) reject the supplied config outright, so the retry loop could never converge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b7a00137eb |
feat(host): hold a logind sleep/idle inhibitor while clients stream
ci / docs-site (push) Successful in 56s
ci / web (push) Successful in 1m0s
android / android (push) Failing after 1m6s
apple / swift (push) Successful in 1m19s
decky / build-publish (push) Successful in 48s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 52s
ci / bench (push) Successful in 8m13s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 13s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m40s
deb / build-publish (push) Successful in 11m0s
release / apple (push) Successful in 9m50s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10m19s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8m57s
docker / deploy-docs (push) Successful in 35s
deb / build-publish-host (push) Successful in 14m3s
apple / screenshots (push) Successful in 6m27s
arch / build-publish (push) Successful in 17m49s
windows-host / package (push) Successful in 18m15s
flatpak / build-publish (push) Failing after 8m25s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m24s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m37s
ci / rust (push) Successful in 25m51s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m35s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m39s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m48s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m52s
Remote INPUT resets the compositor's idle timers, but a passive (video-only) viewer sends none — observed live: a SteamOS Game-Mode host s2idled mid-day and dropped off the network (in a VM with GPU passthrough it never woke again). Refcounted across planes: the native LiveSessionGuard and the GameStream video worker each hold a share; first hold takes the logind sleep:idle block inhibitor on a dedicated plain thread (zbus blocking must not run on a tokio worker), last drop releases. Best-effort: no logind → warn once and stream on. No-op off Linux (macOS/Windows manage their own assertions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
35923080fb |
fix(host/linux): scale degenerate-region absolute coords by the session's real output size
The raw-client-pixels fallback is exact only while the managed session runs at the client's mode. When they diverge — foreign-gamescope attach at its own resolution, supersample/under-render, transitions — raw pixels drift or land out of range. The EIS relay file now carries the session's current output size as a second "WxH" line (from current_gamescope_output_size()); the injector scales normalized client positions into it, keeping raw pixels only as the last resort. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0c9461242c |
fix(host/linux): don't normalize absolute coords into gamescope's degenerate INT32_MAX EIS region
The previous commit's no-region fallback never fired: gamescope DOES advertise a region — (0,0,INT32_MAX,INT32_MAX), meaning "coordinates are raw". Mapping normalized positions into it explodes a center tap to x≈1e9, clamped by gamescope to the far corner — the cursor pinned at (1279,799) and every degraded-touch tap landing there. Treat a region as an output geometry only when plausibly sized (≤16384 px); otherwise emit raw client pixels. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cc6b37fef0 |
fix(host/linux): emit absolute pointer/touch on region-less EIS devices + abs input-test probe
gamescope's "Gamescope Virtual Input" advertises pointer_abs but no region, so every MouseMoveAbs (and the degraded-touch moves built on it) was silently dropped (emitted=false) — clicks then landed at a stale cursor position. With no region, the managed session runs at the client's mode, so client pixels are output pixels: emit them raw. Also: log region count/dims at device-resume, and add PUNKTFUNK_INPUT_TEST_ABS=WxH to `input-test` (corners + center, 1s apart) so the degraded-touch path is verifiable with xdotool alone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1927077cbe |
fix(host/linux): degrade touch to absolute pointer when the EIS has no touchscreen device
gamescope's EIS ("Gamescope Virtual Input") advertises pointer/
pointer_abs/button but never a touchscreen — so in Game Mode every
remote TouchDown/Move/Up was dropped on the floor (Desktop Mode works
via KwinFakeInput, which does touch). Headless KWin's libei has the
same gap. Instead of dropping: degrade to a single-finger absolute
pointer — down = abs-move + left press, move = abs-move, up = release —
synthesized through the normal Mouse* inject paths so region mapping,
held-state tracking, and release_all apply. First finger drives the
pointer; later fingers are ignored (a pinch degrades to a drag). A
touchscreen device appearing later takes over on the next touch —
checked per event, never latched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
256aa5f7f2 |
fix(steamdeck): install the plugin runner + harden Deck pad typing + repair update.sh
- scripts/steamdeck/install.sh (§2b) + update.sh: build and install the scripting runner user-scoped (~/.local wrapper + pinned bun + bundle) — SteamOS's read-only /usr can't take the .deb layout, so the console's plugin store reported "the plugin runner isn't installed" on every SteamOS host. update.sh retrofits it onto existing installs. - plugins.rs runner discovery: check the ~/.local layout after the /usr ones; mention the SteamOS path in the not-installed error. - update.sh: define warn() — it was used but never defined, so under `set -e` the first warn aborted the update before services restarted. - pf-client-core gamepad: a Steam Input virtual pad forwarded on a real Deck (the only-pad case) now declares the DECK kind in its per-pad arrival instead of the wrapper's Xbox 360 identity — the session-level auto_pref already resolved SteamDeck, but the arrival overrides it on current hosts. - reject tests: cover SetupFailed; move the foreign-code probe off 0x68. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2562663fc6 |
fix(host/linux): survive and self-heal dead gamescope sessions on headless SteamOS boxes
Three fixes for the "most connects simply fail" trap on a SteamOS host with no physical display (VM testbox, panel-less mini PC): - restore path: skip the restore-to-physical-panel when no DRM connector reports a connected display — removing the headless drop-in and restarting gamescope-session.target on such a box just crash-loops gamescope and strands every later connect on "no usable compositor". Keep the headless session (and the takeover state) instead. - connect path: when no live graphical session exists but the MANAGED gamescope infra is present (SteamOS gamescope-session / Bazzite session-plus), route to the gamescope takeover — which rebuilds the session at the client's mode — instead of failing the connect. - error delivery: a session-setup failure used to just drop the QUIC connection, reaching the client as "control stream finished mid-frame" (indistinguishable from transport trouble). Close with a typed setup-failed code (0x68, RejectedSetupFailed = -29) carrying the error text, and give the client handshake/pairing error paths a short grace for the CONNECTION_CLOSE to beat the stream error it caused. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d36bec6e9d |
feat(core+host): negotiate ChaCha20-Poly1305 as the session cipher for soft-AES clients
android / android (push) Has been cancelled
apple / screenshots (push) Has been cancelled
apple / swift (push) Has been cancelled
arch / build-publish (push) Has been cancelled
audit / bun-audit (push) Failing after 13s
audit / cargo-audit (push) Has been cancelled
ci / bench (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / web (push) Has been cancelled
deb / build-publish (push) Has been cancelled
deb / build-publish-host (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
flatpak / build-publish (push) Has been cancelled
release / apple (push) Successful in 10m1s
windows-host / package (push) Successful in 11m20s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m29s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 22m16s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 22m27s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m28s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 6m39s
Lifts the ~100 Mbps decrypt ceiling on clients without hardware AES — the armv7 soft-AES targets (webOS TVs), where AES-128-GCM resolves to fixsliced software AES + software GHASH (~50-100 cpb) while ChaCha20-Poly1305's ARX construction runs ~10-17 cpb portable, a 4-7x lift that PyroWave-on-TV needs (design/chacha20-session-cipher.md). Phase 1 (core crypto, no wire change): SessionKey merges cipher choice and key material (invalid combinations unrepresentable, zeroize + redacted-Debug discipline kept); SessionCrypto dispatches both aead-0.5 ciphers per call — the salt||seq nonce scheme, per-direction salts, seq-as-AAD and replay window carry over verbatim (same 96-bit nonce / 16-byte tag, const-asserted). Config.key becomes SessionKey; validate's zero-key rejection follows the active variant. The C ABI keeps its fixed 16-byte key mapped to AES — no ABI_VERSION bump. Phase 2 (negotiation): VIDEO_CAP_CHACHA20 (0x40) — support-plus-request in one bit, the VIDEO_CAP_444 precedent. Welcome grows cipher@68 + key_chacha@69..101, emitted only when non-zero so an AES session's Welcome stays byte-identical to the pre-cipher form; decode is fail-closed (short key or unknown id -> Err, never a silent AES fallback). No WIRE_VERSION bump; downgrade resistance inherited from the pinned-TLS control channel. Phase 3 (host): grant only when the client advertised the bit and the PUNKTFUNK_CHACHA20 kill-switch (default on, documented) allows; fresh 32-byte per-session key from the same RNG discipline, legacy key field stays independently random; resolved cipher logged at session start. Verification: seal/open suites parameterized over both ciphers + a cross-cipher tamper case; Welcome roundtrip/truncation/fail-closed tests; ChaCha lossy-loopback soak (loss/replay is cipher-independent); bench gains _chacha20 series (AES ids unchanged for CI history) — host-side sealing line-rate-trivial on both x86 (~640 MiB/s) and Apple Silicon (~535 MiB/s). punktfunk-probe drives the interop matrix via PUNKTFUNK_CLIENT_CHACHA20=1 and logs the negotiated cipher. Phase 4 (pf-webos pin bump + unconditional cap bit) follows the next core release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
309e37f1e1 |
fix(host/linux): scope the GPU clock pin to live client sessions
The PUNKTFUNK_PIN_CLOCKS clock pin (AMD power_dpm_force_performance_level=high / NVIDIA nvmlDeviceSetGpuLockedClocks) was armed once at host start and released only at host exit, so the box's clocks stayed pinned the whole time the host ran — even with no client connected. Move the pin behind a box-wide refcount (gpuclocks::session_pin()) shared across both streaming planes (native + GameStream): it arms on the first live client and releases on the last disconnect. on_host_start() now only installs the process-scoped NVIDIA P2-cap application profile. No behavior change when PUNKTFUNK_PIN_CLOCKS is unset. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
56d21f9445 |
fix(client/wol): retry the QUIC dial across the connect budget so wake-from-sleep launches survive
ci / web (push) Successful in 57s
ci / docs-site (push) Successful in 1m3s
apple / swift (push) Successful in 1m19s
decky / build-publish (push) Successful in 40s
ci / bench (push) Successful in 6m51s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 18s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 17s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 16s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 15s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 7m6s
deb / build-publish (push) Successful in 9m54s
release / apple (push) Successful in 9m5s
deb / build-publish-host (push) Successful in 9m35s
flatpak / build-publish (push) Failing after 8m19s
android / android (push) Successful in 16m57s
apple / screenshots (push) Successful in 6m29s
arch / build-publish (push) Successful in 17m35s
windows-host / package (push) Successful in 18m46s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Has been cancelled
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Has been cancelled
windows / build (aarch64-pc-windows-msvc) (push) Has been cancelled
windows / build (x86_64-pc-windows-msvc) (push) Has been cancelled
docker / deploy-docs (push) Successful in 26s
ci / rust (push) Successful in 25m24s
Launching from the Decky plugin against a sleeping host fired the Wake-on-LAN packet and dialed immediately — but the dial was a single quinn attempt that gave up after the transport's 8 s idle window, far shorter than a suspend-to-RAM resume. The host woke to find the client already gone (its retransmitted Initials show up host-side as "QUIC accept failed error=timed out"), the stream shortcut exited, and the launch looked cancelled. - punktfunk-core: dial in a loop until the embedder's connect budget is spent — short 3 s attempts keep the Initial cadence dense, so the connect lands within ~a second of the host's network returning. Only silence is retried: pin mismatches, typed rejections, and any real answer surface immediately, and the shutdown flag stops the loop. - decky: stretch the budget to 75 s when a magic packet actually went out (PF_CONNECT_TIMEOUT → --connect-timeout via the wrapper), so the retry window covers the resume; an awake host still connects in <1 s. - host: a clean close before the control handshake is a reachability probe (hosts-page online pips), not a failed session — log it at debug instead of WARN "session ended with error", which buried the real failures in the reporter's log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
42198eb1b6 |
feat(linux/vulkan-encode): default-on native NV12 capture + RGB true-extent
ci / web (push) Successful in 48s
ci / docs-site (push) Successful in 1m5s
apple / swift (push) Successful in 1m20s
decky / build-publish (push) Successful in 21s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 25s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 42s
ci / bench (push) Successful in 5m59s
apple / screenshots (push) Successful in 6m17s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6m29s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 5m12s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m16s
deb / build-publish (push) Successful in 12m58s
deb / build-publish-host (push) Successful in 13m40s
windows-host / package (push) Successful in 10m46s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6m52s
android / android (push) Successful in 16m18s
arch / build-publish (push) Successful in 16m20s
docker / deploy-docs (push) Successful in 30s
ci / rust (push) Successful in 19m25s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m19s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m19s
Flip both zero-copy levers from opt-in to default: - The capture negotiation now PREFERS gamescope's producer-side NV12 pod by default (PUNKTFUNK_PIPEWIRE_NV12=0 restores the packed-RGB negotiation). Codec-aware gating rides a new OutputFormat::nv12_native -> ZeroCopyPolicy::native_nv12_session edge resolved by the host facade from the session plan (pf_encode::linux_native_nv12_ok): only H265/AV1 sessions whose backend can open the raw Vulkan Video encoder ever see the NV12 pod -- an H264/Moonlight session (libav VAAPI, which would misread the two-plane buffer) keeps today's BGRx negotiation, as do the GameStream-resolve and portal-mirror paths, PyroWave, and NVENC prefs. - RGB-direct's unaligned modes default to the true-extent direct import (PUNKTFUNK_VULKAN_RGB_TRUE_EXTENT=0 restores the padded-copy staging). Guarded-tested on Van Gogh with the kernel journal watched: clean, and at 5.38 ms p50 the fastest 1080p encode path measured on that hardware. The EFC only exists on Mesa >= 26, where the codedExtent-driven session_init padding is guaranteed (verified back to Mesa 24.2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b4fbc94e36 |
feat(linux/vulkan-encode): PUNKTFUNK_VULKAN_RGB_TRUE_EXTENT bring-up gate
Opt-in alternative to RGB-direct's padded-copy staging at unaligned modes: direct-import the visible-size capture and declare the TRUE-SIZE source codedExtent. RADV has derived the VCN session_init firmware padding from srcPictureResource.codedExtent since Mesa 24.2, so the EFC is told the source lacks the alignment rows and the hardware edge-extends them internally -- which also reframes the 2026-07-20 field GPU reset: that crash passed the ALIGNED extent as the source codedExtent (firmware padding zero) over a visible-size buffer. Off by default until a guarded live test proves the EFC front-end honors the padding like the YUV fetch path does; the padded-copy staging remains the shipped behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
004f0cacb8 |
perf(linux/vulkan-encode): true-size headers make native NV12 zero-copy at every mode
Drop the padded-copy staging for producer-native NV12 and direct-import the visible-size buffer at unaligned modes (1080p) too. Safe by the driver's own contract rather than by allocation luck: native sessions now author the H265 SPS / AV1 sequence header at the RENDER size and pass the matching codedExtent on every picture resource. RADV rounds the bitstream SPS up itself (with a conformance window -- radv_video_patch_encode_session_parameters, per the VK_KHR_video_encode_h265 proposal's "implementations may override" clause) and programs the VCN session_init with the true extent plus nonzero firmware padding, so the hardware edge-extends the alignment rows internally and never fetches past the source's real extent -- the exact mechanism VAAPI/radeonsi has always used to encode 1080p. The driver-emitted header NALs already carry the patched SPS, so the wire format is unchanged. The CSC and RGB-direct paths keep the app-aligned convention (their sources genuinely cover the aligned extent); RGB padded-copy staging is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
49c6dd00c9 |
fix(presenter): CPU frames never take the HDR10 passthrough surface
audit / bun-audit (push) Failing after 15s
ci / docs-site (push) Successful in 56s
ci / web (push) Successful in 58s
apple / swift (push) Successful in 1m22s
audit / cargo-audit (push) Successful in 2m20s
decky / build-publish (push) Successful in 32s
ci / bench (push) Successful in 7m0s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 13s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 12s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 15s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 14s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 7m18s
release / apple (push) Successful in 9m28s
deb / build-publish-host (push) Successful in 10m32s
deb / build-publish (push) Successful in 11m48s
docker / deploy-docs (push) Successful in 23s
android / android (push) Successful in 14m36s
arch / build-publish (push) Successful in 15m4s
windows-host / package (push) Successful in 16m11s
flatpak / build-publish (push) Failing after 8m19s
apple / screenshots (push) Successful in 6m40s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m11s
ci / rust (push) Successful in 19m38s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m22s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m29s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m39s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m39s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m36s
Software decode uploads swscale RGBA with no CSC/tonemap pass. On the new desktop-compositor HDR10 surfaces (GNOME 48 / Plasma 6 + Mesa >= 25.1 offer ST2084 even on SDR desktops) that sRGB-encoded content was composed as PQ — the field-reported psychedelic picture (Fedora clients decode HEVC in software because stock Mesa strips it; reproduced + fix live-validated on GNOME Wayland: mode-0 soup -> HDR→SDR washed-out-but-correct). The CPU lane keeps the SDR swapchain and its known untonemapped-PQ gap; a real PQ→sRGB pass for CPU frames is the follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |