Commit Graph
21 Commits
Author SHA1 Message Date
enricobuehlerandClaude Fable 5 7058647264 fix(vdisplay/capture): channel-less sessions composite the pointer on a sticky-declared target
A declared IddCx hardware cursor is IRREVOCABLE for its OS target's life
(§8.6), and the sticky exclusion survives monitor REMOVE→ADD because each
client gets a STABLE target id — so once any desktop-mode session declared,
every later pure-capture session on that target streamed a cursor-less
desktop: DWM excluded the pointer, no channel forwarded it, no blend drew it
(the exact no-regression gap §8.6's per-session cap gate cannot see).

Driver: track every successful SetupHardwareCursor per target
(DECLARED_TARGETS — scoped to the WUDFHost's life, exactly the sticky
state's scope) and report it in a new AddReply::cursor_excluded tail field
(dual-size discipline, both skews degrade cleanly; no proto bump).

Host: the flag rides AddedMonitor → Monitor → WinCaptureTarget; a session
WITHOUT the cursor channel on a flagged target forces composite mode in the
IDD-push capturer — GDI poller + blend for the session's life, pinned on
(set_cursor_forward cannot clear it: with no client drawing, un-compositing
would erase the pointer entirely).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 23:47:53 +02:00
enricobuehlerandClaude Opus 4.8 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>
2026-07-22 23:47:53 +02:00
enricobuehlerandClaude Fable 5 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>
2026-07-22 23:47:53 +02:00
enricobuehlerandClaude Fable 5 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>
2026-07-22 23:47:53 +02:00
enricobuehlerandClaude Fable 5 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>
2026-07-22 23:43:23 +02:00
enricobuehlerandClaude Fable 5 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>
2026-07-22 23:43:23 +02:00
enricobuehlerandClaude Fable 5 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>
2026-07-22 23:43:23 +02:00
enricobuehlerandClaude Opus 4.8 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>
2026-07-22 23:43:23 +02:00
enricobuehlerandClaude Fable 5 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>
2026-07-22 23:43:23 +02:00
enricobuehlerandClaude Fable 5 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>
2026-07-22 23:43:23 +02:00
enricobuehlerandClaude Fable 5 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>
2026-07-22 10:53:14 +02:00
enricobuehlerandClaude Fable 5 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>
2026-07-22 09:50:30 +02:00
enricobuehlerandClaude Opus 4.8 58b27acbd5 fix(vdisplay/session): scrub the dead desktop's WAYLAND_DISPLAY from the user-manager env
ci / web (push) Successful in 1m2s
ci / docs-site (push) Successful in 1m14s
apple / swift (push) Successful in 1m22s
decky / build-publish (push) Successful in 27s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 19s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 17s
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 12s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 12s
ci / bench (push) Successful in 5m44s
apple / screenshots (push) Successful in 6m18s
android / android (push) Successful in 13m0s
docker / deploy-docs (push) Successful in 27s
deb / build-publish (push) Successful in 13m36s
deb / build-publish-host (push) Successful in 14m44s
windows-host / package (push) Successful in 16m32s
arch / build-publish (push) Successful in 17m49s
ci / rust (push) Successful in 19m58s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m35s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m42s
settle_desktop_portal pushes the live desktop's WAYLAND_DISPLAY into the
systemd --user manager (import-environment) so a re-activated portal inherits
the right session — but the import PERSISTS after that desktop dies. Every
later user unit then inherits the stale socket, including
gamescope-session.target: gamescope sees WAYLAND_DISPLAY, runs NESTED, and
aborts with "Failed to connect to wayland socket: wayland-0" — observed live
on a Deck, where Game Mode could not start at all (autologin crash-looped)
until the var was unset by hand.

Two-layer fix, mirroring the protection launch_session already gives its
transient unit:
- observe_session_instance: when a desktop compositor instance goes away,
  unset WAYLAND_DISPLAY/DISPLAY from the manager env (a desktop bounce is
  harmless — the next portal settle re-imports).
- write_steamos_dropin: UnsetEnvironment=DISPLAY WAYLAND_DISPLAY on the
  takeover drop-in, unit-scoped belt-and-suspenders for host-initiated
  restarts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 11:06:55 +02:00
enricobuehlerandClaude Opus 4.8 333c8979e9 fix(vdisplay/gamescope): capture-loss rebuild probes must not steal the box session
Observed live on a Deck host: the user switched Game Mode → Desktop, KDE came
up in 0.8 s — and the capture-loss rebuild's FIRST detection ran inside that
gap, still said Gaming, and its gamescope re-acquire restarted
gamescope-session.target. On SteamOS that steals the seat: the user was
yanked out of the KDE session they had just chosen, the two session managers
fought (job canceled / dependency failed), no gamescope node appeared within
30 s, and the stream died at the 40 s rebuild budget.

A rebuild probe acting on possibly-stale detection must never stop, relaunch,
or take over box sessions. New pf_vdisplay::rebuild_probe_scope (RAII,
counted): while held, the gamescope managed/takeover create paths only attach
to a live node and fail fast otherwise — no stop_autologin_sessions, no
session-plus relaunch, no gamescope-session.target restart. The capture-loss
loop holds it for the first 4 s after a loss (the detection-ambiguity
window); after that, destructive rebuilds are allowed again, so a genuine
switch INTO Game Mode still gets its headless takeover. Probe failures are
instant, so the loop now paces itself at ~2 Hz instead of spinning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 11:00:02 +02:00
enricobuehlerandClaude Opus 4.8 59b766fb6c chore(release): bump workspace version to 0.14.0
android / android (push) Successful in 15m52s
flatpak / build-publish (push) Failing after 8m8s
apple / screenshots (push) Successful in 6m24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 8m8s
ci / rust (push) Successful in 19m20s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 6m48s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m47s
docker / deploy-docs (push) Successful in 23s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 8m2s
audit / bun-audit (push) Successful in 13s
ci / web (push) Successful in 54s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 8m52s
apple / swift (push) Successful in 1m15s
ci / docs-site (push) Successful in 1m7s
audit / cargo-audit (push) Successful in 2m7s
windows-host / package (push) Failing after 5m28s
ci / bench (push) Successful in 6m10s
decky / build-publish (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
deb / build-publish-host (push) Failing after 6m8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 38s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
deb / build-publish (push) Successful in 9m29s
release / apple (push) Successful in 10m7s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 7m23s
arch / build-publish (push) Successful in 14m31s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6m52s
MINOR, not patch: 31 commits since v0.13.0 carry 10 features, including a new
/api/v1/plugins surface with a regenerated OpenAPI spec + SDK client. Since
sdk-publish pushes this version to consumers, a patch bump would understate a
new API surface for anyone pinning ~0.13.

Headline work: GNOME 50 HDR screencast capture + Linux Main10 encode, PyroWave
4:4:4 + HDR end to end (Linux/Windows/Apple), the console-hosted plugin UI
surface, and the pyrowave perf work that lifted the 2.5 Gbps wall. Notable
fixes: the RSA host-identity keygen that broke every fresh Windows install on
0.13.0, and SDR negotiation on the Windows IDD-push path.

pf-clipboard / pf-inject / pf-vdisplay were still hardcoding 0.12.0 — the three
the 22b352c1 sweep missed. Switched to version.workspace = true so the next
bump stays one line. (fec-rs, pf-driver-proto and usbip-sim are deliberately
versioned independently and stay put.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 10:25:27 +02:00
enricobuehlerandClaude Opus 4.8 b89dbfa979 feat(gamescope): end dedicated stream on Steam game exit + auto --steam
ci / web (push) Successful in 54s
ci / docs-site (push) Successful in 1m12s
apple / swift (push) Successful in 1m19s
decky / build-publish (push) Successful in 32s
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 11s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
ci / bench (push) Successful in 6m57s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 4m51s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 14s
apple / screenshots (push) Successful in 6m13s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m1s
deb / build-publish (push) Successful in 9m37s
docker / deploy-docs (push) Successful in 29s
deb / build-publish-host (push) Successful in 9m29s
windows-host / package (push) Successful in 16m24s
arch / build-publish (push) Successful in 17m40s
android / android (push) Successful in 18m22s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m39s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m52s
ci / rust (push) Successful in 26m16s
Dedicated single-game sessions now end cleanly when the launched game
quits, and Steam launches get gamescope's Steam integration without an
operator env knob.

End-on-exit: the existing APP_EXITED close only fired when gamescope's
PipeWire node vanished, which never happens for Steam — the nested
`steam` is the resident singleton client and stays up after a game
quits, so gamescope (and its node) never die and the stream sat on a
hidden Steam session forever. Add a Steam AppId reaper watcher
(pf-vdisplay .../gamescope/discovery.rs): steam_appid_from_launch()
parses steam://rungameid/<id>; wait_for_steam_game_exit() waits for the
game to start (<=300s grace) then exit (3s confirm); steam_game_running()
scans same-uid /proc for Steam's launch reaper matching both the
`SteamLaunch` and `AppId=<id>` argv tokens (exact-match; reaper lifetime
== game lifetime, so shader precompile can't false-trigger). The host
spawns a pf1-gamewatch thread for nested Steam launches that closes the
connection with APP_EXITED (launcher clients return to their library)
and sets quit/stop; cancelled via stop if the session ends first.
Non-Steam nested launches keep the node-death path (gamescope's child
IS the game).

Flags: auto-enable `--steam` whenever the launch is a Steam launch (was
only the global PUNKTFUNK_GAMESCOPE_STEAM knob, default-off) — in-game
overlay / Steam+X / gamepad-UI nav for Steam titles with no operator
config. New opt-in PUNKTFUNK_GAMESCOPE_GRAB_CURSOR adds
`--force-grab-cursor` for a real game launch (FPS mouselook); default
OFF because it forces relative mode, which breaks absolute-pointer
games/menus.

Verified: fmt clean; clippy -D warnings clean on the three crates;
pf-vdisplay 64/0 (incl. new steam-appid parse test); punktfunk-host
builds + 186/0; reaper /proc detection smoke-tested (detect, exact
non-match, gone-after-exit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 16:24:34 +02:00
enricobuehlerandClaude Opus 4.8 c8ee4b9902 fix(pf-vdisplay,pf-capture,pf-win-display): pre-split paths in the auto-merged v4 code
windows-drivers / probe-and-proto (push) Successful in 31s
ci / web (push) Successful in 43s
apple / swift (push) Failing after 1m1s
ci / docs-site (push) Successful in 1m0s
apple / screenshots (push) Has been skipped
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
decky / build-publish (push) Successful in 19s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 44s
windows-drivers / driver-build (push) Successful in 1m38s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m1s
ci / bench (push) Successful in 5m34s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5m53s
android / android (push) Has been cancelled
arch / build-publish (push) Has been cancelled
ci / rust (push) Has been cancelled
deb / build-publish (push) Has been cancelled
windows-host / package (push) Successful in 15m6s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m48s
docker / deploy-docs (push) Successful in 23s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m28s
The rename-followed perf hunks still said crate::win_display:: (the pre-W6
layout) — point them at pf_win_display::win_display:: and widen the four
helpers they call cross-crate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:13:15 +02:00
enricobuehlerandClaude Opus 4.8 1197415216 fix(pf-vdisplay,pf-win-display): v4 trait surface on the extracted driver.rs + cross-crate visibility
The W-refactor extracted VdisplayDriver into manager/driver.rs (the merge
resolution assumed it deleted) — carry the v4 changes there: open() returns the
driver's protocol version, update_modes() default-errs to the re-arrival
fallback. wait_target_departed goes pub for the manager's cross-crate call.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:11:34 +02:00
enricobuehlerandClaude Opus 4.8 09849906e9 Merge perf/first-frame-latency: driver proto v4 + first-frame/resize latency (P0-P2)
Brings the first-frame-latency branch (P0.1 transition tracing, P1.1/P1.2
Welcome-time display prep, P2 in-place resize; pf-driver-proto v3 -> v4 with
IOCTL_UPDATE_MODES) onto current main. The branch predates the W6.2/W7 splits,
so git's rename detection carried most of it into the moved crates
(pf-capture idd_push, pf-vdisplay manager/pf_vdisplay, pf-win-display,
pf-driver-proto, the driver workspace) and the punktfunk1.rs remainder was
re-homed by hand:

- native/handshake.rs: welcome/start trace marks + the Welcome-time display
  prep spawn (the prep thread BECOMES the stream thread; hand-off via a
  SyncSender<SessionContext>). negotiate() gains bringup/quit/stop and returns
  the PrepHandle.
- native.rs: bringup/resize_ms creation + the stop/quit flags hoisted BEFORE
  the handshake (the close watcher splits: flags pre-handshake, lifecycle
  events post-handshake where `hello` exists); punch_done stamp; the data
  plane adopts the prep thread's result or builds inline.
- native/stream.rs: SessionContext/SendStats carry the trace; send_loop
  finishes it on the first video packet; the resize path gains the in-place
  fast path (try_inplace_resize) with the full rebuild as fallback, restructured
  so both share the post-rebuild bookkeeping; prepare_display/PreparedDisplay/
  PrepHandle; build_pipeline(+retry) thread the stage marks.
- session_status/mgmt: ttff_ms + last_resize_ms per session (union with the
  lifecycle-events fields main added to the same spots).
- pf-capture: Capturer gains capture_target_id() + resize_output() defaults.
- pf-vdisplay manager: perf's faster activation poll (60x50ms) + the settle
  floor before the PnP sweep, on main's knobs/no-trait shape.

Also: packaging/windows/build-gamepad-drivers.ps1 is ASCII again (an em-dash
from the pf-mouse work tripped windows-host.yml's locale-safety gate on main).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:08:16 +02:00
enricobuehlerandClaude Opus 4.8 86d9f49473 style(pf-vdisplay): rustfmt under the pinned 1.96.0 toolchain
audit / bun-audit (push) Successful in 12s
windows-drivers / probe-and-proto (push) Successful in 31s
ci / web (push) Successful in 48s
apple / swift (push) Failing after 1m1s
ci / docs-site (push) Successful in 1m2s
decky / build-publish (push) Successful in 21s
windows-drivers / driver-build (push) Successful in 1m35s
windows-host / package (push) Failing after 16s
audit / cargo-audit (push) Successful in 2m46s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (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 8s
release / apple (push) Successful in 3m56s
apple / screenshots (push) Has been skipped
ci / bench (push) Successful in 6m26s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 7m0s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m16s
flatpak / build-publish (push) Failing after 8m9s
docker / deploy-docs (push) Successful in 24s
arch / build-publish (push) Successful in 13m9s
android / android (push) Successful in 13m11s
deb / build-publish (push) Successful in 14m12s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m8s
ci / rust (push) Successful in 19m29s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m23s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m10s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 6m1s
The W6.2 pf-vdisplay extraction (27a5d8da) committed six spots that the pinned
rustfmt (rust-toolchain.toml = 1.96.0) reformats — a short closure collapse and
chain-call wrapping. No semantic change; makes cargo fmt --all --check green so
the combined push does not land CI red on pre-existing format drift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 15:38:12 +02:00
enricobuehlerandClaude Opus 4.8 27a5d8daac refactor(host/W6.2): extract virtual-display orchestration into the pf-vdisplay crate
vdisplay.rs + vdisplay/* (the per-compositor Linux backends — KWin zkde-screencast,
wlroots swaymsg, Mutter RemoteDesktop, Hyprland — and the Windows IddCx/pf-vdisplay
driver backend, behind one VirtualDisplay trait; the mode-conflict admission
registry, the display policy/identity/custom-preset state, and the session-env /
gamescope routing) move into crates/pf-vdisplay (plan §W6). The DDC/CI panel-power
control (used only here) and the KWin zkde protocol XML move with it. This
completes the host-crate decomposition: capture, encode, inject, and vdisplay are
now four subsystem crates over the shared leaves, and punktfunk-host is the
orchestrator (serve/supervisor + native + gamestream + mgmt).

Coupling breaks (all down-only, cargo-tree acyclic):
- capture::dxgi identity -> pf_frame::dxgi; win_display/monitor_devnode/
  console_session_mismatch -> pf-win-display leaf; can_open_another_session ->
  pf-encode (the NVENC session-budget admission gate — acyclic peer edge).
- The registry's DisplayCreated/DisplayReleased emits into the host SSE event bus
  invert to a leaf hook: pf-vdisplay emits a neutral DisplayEvent to a
  host-registered DISPLAY_EVENT_SINK, so it never reaches the orchestrator's
  events module.
- The IddCx driver module is renamed pf_vdisplay -> driver (its old name collided
  with the crate name through the host's `mod vdisplay` shim glob).

The host keeps `mod vdisplay { pub use pf_vdisplay::* }` so every crate::vdisplay::*
path (serve/mgmt/native/the capture FrameChannelSender seam) is unchanged; the
heavy deps (wayland/ashpd/tokio + the zkde protocol) moved with the crate.
Co-authored: a fail-closed IOCTL-reply-length security fix (reject short/zeroed
pf-vdisplay driver replies before trusting protocol_version/target_id/wudf_pid/luid,
security-review 2026-07-17) rides this commit in the moved driver module.

Verified: Linux clippy -D warnings (pf-vdisplay + host nvenc,vulkan-encode,pyrowave
--all-targets) + pf-vdisplay 63/63 + host 167/167 tests; Windows clippy -D warnings
(pf-vdisplay --all-targets + host nvenc,amf-qsv --all-targets) Finished exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 12:14:08 +02:00