The pnputil /add-driver call in windows/install.rs was committed unwrapped;
`cargo fmt --all --check` (which checks cfg(windows) files too) flagged it and
failed the `rust` CI job at the Format step, skipping clippy/build/test. Apply
rustfmt — no behavior change. Clears the way to cut the v0.2.0 release from
green main.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The wordmark was light violet only — low-contrast on a light README
background. Swap to a single theme-adaptive SVG: an internal
`prefers-color-scheme` media query paints it deep violet (the brand-mark
palette) on light backgrounds and the original light violet on dark, so it
reads on both GitHub/Gitea themes with no markup change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Option A removed install-pf-vdisplay.ps1 / install-gamepad-drivers.ps1 / web-setup.ps1;
the installer now calls the exe subcommands. Drop the stale table rows + reword the
install-flow + 'thin installer' notes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add the centered punktfunk wordmark banner at the top (assets/punktfunk-logo.svg,
the same logo + layout the marketing site's README uses).
- Refresh the now-stale Windows-host facts: all-vendor (NVENC + AMF/QSV), its own
all-Rust pf-vdisplay IddCx virtual display (was SudoVDA), bundled UMDF virtual-gamepad
drivers (ViGEmBus gone), HDR incl. Vulkan-game HDR; x64-only, no longer NVIDIA-only.
- Note punktfunk-host covers Linux + Windows; point design/ at its new README index.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port the three install-time PowerShell *files* (install-pf-vdisplay.ps1,
install-gamepad-drivers.ps1, web-setup.ps1) into punktfunk-host.exe subcommands:
`driver install [--gamepad] --dir <stage>` and `web setup --app-dir <app>
[--password-file <f>]` (windows/install.rs).
Why: PowerShell 5.1 reads a BOM-less .ps1 FILE in the machine ANSI codepage, so a
stray non-ASCII byte mis-decodes and aborts on a non-English box - exactly how the
pf-vdisplay driver install silently failed. A compiled subcommand drives the same
external tools (certutil/pnputil/nefconc/schtasks/netsh/icacls) as fixed string
literals, with no file-codepage surface. (The .iss's INLINE -Command PowerShell is a
command-line string, not a file read, so it's unaffected and stays.)
- windows/install.rs: faithful port - cert trust, gated nefconc node create + pnputil
for pf-vdisplay; pnputil per-inf for gamepads; web-password ACL, the PunktfunkWeb task
(generated UTF-16 XML), firewall rule, start. Best-effort (a hiccup warns, never aborts).
- punktfunk-host.iss [Run]: call the exe instead of `powershell -File`; drop the
web-setup.ps1 staging + WebSetup define; WebSetupParams emits --app-dir/--password-file.
- pack-host-installer.ps1: stop copying the three install scripts into the stages.
- delete the three .ps1 files.
The `mod install;` + dispatch arms in main.rs landed in the preceding docs commit
(swept up by a concurrent commit); this commit adds the module + installer wiring.
CI-compile-validated via windows-host; the install path is on-glass-validated on the
next canary install (the test box is offline).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Much of design/ described work that has since shipped. Trim each doc to
its durable rationale + still-open items (the code is the source of truth
for shipped detail; git history holds the full originals).
- Shipped plans -> status stubs: stats-capture, gamestream-host-plan,
apple-stage2-presenter, windows-service.
- Trimmed completed-out / open-kept: implementation-plan, hdr-pipeline,
host-latency, gpu-contention (fixed stale status table), game-library,
linux-setup (fixed m0->spike + stale zero-copy claim),
session-aware-host-followups, windows-client-bootstrap,
windows-dualsense-{scoping,game-detection}, windows-virtual-display,
security-review (per-finding status table; #12 still open),
apollo-comparison (shipped backlog collapsed to one-liners).
- Windows-host cluster consolidated: windows-host.md -> redirect into
windows-host-rewrite.md (whose stale scorecard is corrected -- goal1 is
merged, M4 done); windows-secure-desktop.md archived (now a fallback
behind IDD-push primary).
- Kept evergreen: ci.md, gamescope-multiuser.md, windows-build-and-packaging.md.
- New design/README.md: per-doc status table + consolidated open-items
roll-up so nothing is tracked in only one buried doc.
- Repoint 5 code comments to the archived secure-desktop doc path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A single repo-internal source of truth for the Windows build/packaging: what ships, the
all-Rust driver workspace built FROM SOURCE in CI (+ the anti-stale rationale), the
toolchain (clang 22 + bindgen 0.72, no LLVM pin), the Inno installer, the web console
bundle, the CI workflows, signing, and the dev loop. (design/, not the docs-site.)
packaging/windows/README.md: drop the deleted vendored-driver dir + its "Vendored driver"
callout, add the build-* / install-gamepad / clear-force-integrity rows, point at the new
design doc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Now that the drivers build from source in CI, remove the dead checked-in binaries and
the toolchain cruft they left behind:
- Delete packaging/windows/{pf-vdisplay,gamepad-drivers}/ (the prebuilt .dll/.inf/.cat/.cer).
pack-host-installer.ps1 builds + signs all three drivers from the drivers/ workspace and
nothing reads the vendored dirs anymore; stage-pf-vdisplay.ps1's -VendorDir is now a
mandatory build-output path, not a vendored default.
- Drop the LLVM-21 pin. The vendored bindgen 0.71->0.72 bump (the shipping pack already
builds green on the runner-default clang 22) retired the bindgen-0.71 layout-test overflow
that needed LLVM 21.1.2, so windows-drivers.yml + provision-windows-wdk.ps1 no longer
install/point at C:\llvm-21 (~898 MB off a fresh provision) - both driver builds now use one
toolchain (clang 22 + bindgen 0.72).
- pack -SkipBuild on the gamepad build (build-pf-vdisplay.ps1 already builds the whole
workspace), build-web.ps1 reaps a stale node too, deploy-dev.ps1 nefconc path + comments.
- Reword the vendored-driver references (build scripts, .iss, READMEs, the vite web-bundle
comment) to the build-from-source reality.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Bug C [UninstallRun] one-liner had `ForEach-Object { Stop-Process ... }`; Inno
Setup parses `{...}` as a constant in [Run]/[UninstallRun] sections, so ISCC aborted
with "Unknown constant" and the windows-host pack failed at the ISCC step (the host
build, clippy, driver build + web smoke-boot all passed). Escape `{` as `{{`. The
same one-liner in the [Code] StopWebConsole proc is inside a Pascal string literal,
so its brace is literal and must NOT be escaped. Validated: ISCC now parses past
[UninstallRun] + [Code] (fails only later on the absent dummy payload).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fold the pf-dualsense (DualSense / DualShock 4) and pf-xusb (Xbox 360 / XInput)
UMDF drivers into the in-tree drivers workspace (their source had stale
../../crates/wdk-* path-deps from before the wdk vendoring reorg and could no
longer build at all) and build them from source per release, exactly like
pf-vdisplay - same anti-stale reasoning. One `cargo build --release` now builds
all three drivers against the vendored wdk-sys (incl. the bindgen 0.72 pin), and
build-gamepad-drivers.ps1 signs pf_dualsense + pf_xusb (clear FORCE_INTEGRITY ->
sign dll -> stampinf -> Inf2Cat -> sign cat) with one shared cert + .cer,
matching the layout install-gamepad-drivers.ps1 expects. pack-host-installer.ps1
builds + stages them instead of the retired checked-in binaries.
Validated on the runner: the whole workspace (pf-vdisplay + pf-dualsense +
pf-xusb) builds with CARGO_TARGET_DIR=C:\t set, and build-gamepad-drivers.ps1
produces signed pf_dualsense.{dll,inf,cat} + pf_xusb.{dll,inf,cat} + the .cer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CI driver build panicked in wdk-sys's build script - "a Cargo.lock file should
exist in the same directory as the top-level Cargo.toml". wdk-build's
find_top_level_cargo_manifest() walks UP from OUT_DIR for the first ancestor holding a
Cargo.lock and explicitly does NOT support non-default target dirs - but
build-pf-vdisplay.ps1 pointed CARGO_TARGET_DIR at an out-of-tree dir (to isolate from
CI's shared C:\t), so no ancestor of OUT_DIR had a Cargo.lock. Build into the driver
workspace's DEFAULT target dir instead (its ancestors include the driver Cargo.lock);
the driver's own [workspace] already isolates it and it has no CMake deps needing C:\t.
Also make the Test-FileCatalog coverage guard non-fatal (it can't open a catalog
signed by a not-yet-trusted cert). Validated on the runner with CARGO_TARGET_DIR=C:\t.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pf-vdisplay virtual-display driver shipped as a checked-in PREBUILT binary
that went stale - two field failures on a fresh install (live-repro'd on a
German-locale Dell laptop):
* Bug A (every box): a repo-wide rename edited the vendored pf_vdisplay.inf
but never re-signed pf_vdisplay.cat, so the catalog stopped covering the INF
-> `pnputil /add-driver` fails SPAPI_E_FILE_HASH_NOT_IN_CATALOG -> driver
never installs -> every session dies "pf-vdisplay driver interface not
found".
* the prebuilt binary also predated IOCTL_SET_RENDER_ADAPTER (added to the
driver source after the vendor freeze) that the host needs to pin the IDD
render GPU on hybrid/Optimus boxes.
Fix: build the driver FROM SOURCE every release (build-pf-vdisplay.ps1, wired
into pack-host-installer.ps1) so .dll/.inf/.cat are always in lockstep and
current driver features ship. The runner's clang 22 made the driver's pinned
bindgen 0.71 emit opaque structs (157 layout-assert errors), so bump the
vendored wdk-sys/wdk-build bindgen 0.71 -> 0.72 (+ lock). The build self-signs
the driver per build (installer trusts the bundled .cer); a stable
DRIVER_CERT_PFX_B64 secret can override.
* Bug B (non-English boxes): the installer runs install-pf-vdisplay.ps1 etc.
via powershell.exe (5.1), which reads a BOM-less script in the ANSI codepage
- an em-dash's trailing 0x94 byte becomes a curly quote on German
Windows-1252 and the script aborts "unterminated string", so the driver
never installed (the gamepad script survived only because it was already
ASCII). Scrub every installer-run .ps1/.cmd to ASCII + add a CI gate that
fails on any non-ASCII so it can't regress.
* Bug C (upgrades): nothing stopped the OLD web console before re-registering
its task, so a stale server kept :3000 (the new one restart-looped on
EADDRINUSE) and served a broken old bundle (500 on /login). Stop + reap it
(runtime-agnostic, by the :3000 listener owner) in web-setup.ps1 and in the
.iss before the file copy + on uninstall.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A freshly-added IddCx virtual display lands in CLONE/duplicate mode when a
physical display is already active (a laptop panel, an attached monitor): the
cloned output shares that display's source, so the OS never commits a distinct
path for it, never calls ASSIGN_SWAPCHAIN, and capture sees no frames - the
session fails "not an active display path / needs a WDDM GPU to activate" and
tears down with 0 frames (seen live on an Intel-iGPU + NVIDIA-Optimus laptop).
force_extend_topology() applies the EXTEND preset (the programmatic Win+P
"Extend") right after ADD so the IDD comes up as its own active path; the
existing resolve_gdi_name -> set_active_mode -> isolate_displays_ccd bring-up
then proceeds. Idempotent / no-op on a sole-display (headless single-GPU) box,
so it's safe on the path that already worked.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Arm streaming-perf-stats capture from the web console, play, stop, and review the
run as graphs; finished captures are saved to disk as browsable/exportable
recordings. Covers both the native punktfunk/1 path and GameStream.
- stats_recorder.rs: one shared Arc<StatsRecorder> ring (created in gamestream::serve,
shared with the mgmt API + both streaming loops, mirroring NativePairing). The
hot-path gate is a runtime AtomicBool that replaces the startup-only PUNKTFUNK_PERF
for *recording* (PERF stdout logging unchanged); bounded ring (~3 h); atomic
temp+rename writes to ~/.config/punktfunk/captures/*.json; path-traversal-safe ids;
poison-resilient locks.
- native (punktfunk1.rs) + GameStream (stream.rs) emit a StatsSample at their existing
~2 s / ~1 s aggregation boundary — per-stage latency p50/p99, fps new/repeat, goodput,
loss/FEC deltas — with no new per-frame work beyond the cheap atomic check.
FrameMsg.was_measured keeps pre-arm in-flight frames out of the first window's
percentiles (without zeroing the Windows-relay path's fps/encode).
- mgmt.rs: 7 bearer-only /api/v1/stats/* endpoints (capture start/stop/status/live;
recordings list/get/delete); api/openapi.json regenerated, in sync.
- web: new "Performance" page (recharts, rendered SSR-safe) — capture control, live
graphs while armed, recordings table (view / download-JSON / delete), and a detail
view with the latency stacked-area bottleneck breakdown (p50/p99 toggle) + throughput
+ health. Charts adapt to either path's stage set.
Design: design/stats-capture-plan.md. Built and adversarially reviewed via a multi-agent
workflow; workspace build/clippy(-D warnings)/fmt/tests green, OpenAPI no-drift. Not yet
on-glass validated against a live session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Android app is in Google Play Internal Testing, so the public Play Store URL
doesn't resolve for non-testers. Lead the Android install instructions with a
"request a tester invite on Discord" CTA (the Play listing unlocks once a Google
account is added to the test track), and surface the Discord + r/Punktfunk
community links in the README, the docs intro, and the docs-site nav.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the "Linux-first" framing across the README and docs site in favor of
first-class Linux AND Windows hosts, and surface the Windows IDD-push
virtual-display path as a distinct differentiator (punktfunk's own indirect
display driver the host pushes frames into — a real virtual display, no physical
monitor or dummy plug, even on the secure desktop).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The file moves (docs/ → design/, docs/api/openapi.json → api/openapi.json) landed
in 8897286, but the matching reference updates did not — so mgmt.rs's drift-test
`include_str!("../../../docs/api/openapi.json")` pointed at a path that no longer
exists and the host failed to build. This restores it and updates every reference:
- mgmt.rs include_str! → ../../../api/openapi.json (fixes the build)
- web/orval.config.ts codegen target, web/Dockerfile, .dockerignore
- deb/rpm/Arch packaging install paths
- CLAUDE.md, the .gitea CI workflows, code doc-comments, design-doc cross-links
docs-site route URLs (/docs/...) untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
NVIDIA/AMD Vulkan ICDs refuse to *advertise* an HDR color space for a surface on an
IddCx indirect/virtual display, so Vulkan games (Doom: The Dark Ages, id Tech, Indiana
Jones, …) report "device does not support HDR" — even though Windows HDR, DWM compose,
and the client PQ stream all work, and the ICD happily *accepts + presents* a forced HDR
swapchain there. The whole gap is enumeration; the community (Apollo/Sunshine/VDD) wrote
this off as kernel-side / unfixable.
Add VK_LAYER_PUNKTFUNK_hdr_inject (packaging/windows/pf-vkhdr-layer/): a standalone
cdylib Vulkan implicit layer that appends {A2B10G10R10, HDR10_ST2084} + {RGBA16F, scRGB}
to vkGetPhysicalDeviceSurfaceFormats[2]KHR (no need to hook vkCreateSwapchainKHR — the
ICD doesn't validate the color space there). Self-gated on the surface monitor's actual
advanced-color state (DisplayConfig GET_ADVANCED_COLOR_INFO), so it is a complete no-op
on SDR sessions and real monitors (dedup). Always-on (registry-discovered) so it works
regardless of how a game is launched — env-scoping silently fails for already-running
Steam. Escape hatches: DISABLE_PF_VKHDR, PF_VKHDR_EXCLUDE, and a built-in kernel-anti-
cheat denylist.
The installer builds/signs/stages it and registers it under
HKLM64\SOFTWARE\Khronos\Vulkan\ImplicitLayers (opt-out "Install the HDR Vulkan layer"
task); windows-host CI fmt+clippy-gates it (msvc-only FFI).
Live-validated on the RTX box: Doom: The Dark Ages enables HDR over the pf-vdisplay
virtual display.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The one genuine soundness defect the unsafe-proof program surfaced (flagged
SUSPECT in program 3/N). `HelperRelay` holds an `rx: Receiver<RelayAu>`, which is
`!Sync` (std mpsc is single-consumer), so asserting `Sync` claimed more than the
fields support — an `Arc<HelperRelay>` recv'd from two threads would compile and
be UB.
It was never live-exploited, and it turns out `Sync` is also unnecessary: the
relay is a single-owner `mut relay` local in the punktfunk1 two-process mux loop
(recv_timeout/try_recv/request_keyframe all called on the owning thread; no `Arc`,
no `thread::spawn` capturing it). So the fix is simply to delete the impl — the
struct keeps its sound `unsafe impl Send` (needed for the raw `HANDLE` fields),
which is all the code uses.
Box-verified: cargo clippy -p punktfunk-host --features nvenc --target
x86_64-pc-windows-msvc -- -D warnings stays green without the Sync impl.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the unsafe-proof program now that the parallel WIP has landed:
- idd_push.rs (25 sites), nvenc.rs (7), punktfunk1.rs (21): a SAFETY proof on
every unsafe block — D3D11/DXGI COM (same-device textures, immediate-context
single-thread, keyed-mutex-held convert), the NVENC SDK table (versioned POD,
register/map/lock-bitstream pairing), cross-process shm reads (atomic
magic/generation handshake), and the C-ABI harness (each call cross-checked
against its abi.rs `# Safety` doc). No SUSPECT (UB) blocks.
- capture.rs / encode.rs: the parent-module deny is restored (their WIP children
are now proven), and main.rs gains a crate-root
#![deny(clippy::undocumented_unsafe_blocks)] — the permanent catch-all gate so
no future unsafe block anywhere in the crate can land without a proof.
- Fixed 4 blocks the agents missed: unsafe blocks nested inside `assert_eq!(...)`
macro args (the comment-above-statement didn't associate) — hoisted to a `let`.
- rustfmt-canonicalized the Windows files (the agents' SAFETY comments + some
pre-existing 1.9.0 drift) so `cargo fmt --all --check` is clean.
Verified: cargo clippy -p punktfunk-host --all-targets -- -D warnings AND
cargo fmt -p punktfunk-host --check both green with the crate-root deny active.
Windows cfg(windows) re-verified on the box next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GPU-contention work (host-latency plan §5.A): the IDD-push output ring now hands
NVENC native YUV instead of RGB, so NVENC skips its internal RGB→YUV colour
conversion on the SM/3D engine the running game saturates.
- idd_push.rs: out_ring is now NV12 (SDR, BT.709 limited) via a D3D11 VIDEO-engine
BGRA→NV12 VideoConverter (keeps the CSC off the contended 3D/compute engine), or
P010 (HDR, BT.2020 PQ limited) via the FP16→P010 shader (NVIDIA's VideoProcessor
can't do RGB→P010). The ring drops its per-slot RTV (textures only), matching the
WGC YUV ring; converters rebuild on a size/HDR flip.
- nvenc.rs: NV12 input forces bit_depth=8 so an HDR→SDR toggle (or a 10-bit-
negotiated client on an SDR display) re-inits the session at the matching depth —
NV12 can't feed a 10-bit session (register_resource rejects it).
- punktfunk1.rs: per-stage latency instrumentation under PUNKTFUNK_PERF
(cap=try_latest, submit=encode_picture, wait=lock_bitstream µs p50/p99/max) to
pinpoint where capture→encoded latency goes under GPU saturation.
Continues the unsafe-proof program across the Windows/cross-platform host files
(~75 blocks, 21 files), each with a SAFETY proof of the real invariant and a
per-file #![deny(clippy::undocumented_unsafe_blocks)] gate:
capture/windows: dxgi.rs, wgc_relay.rs, wgc.rs, desktop_watch.rs, composed_flip.rs
(windows-rs COM: interface validity, same-D3D11-device textures,
immediate-context single-thread, borrowed args outlive the call)
windows: service.rs (SCM/token/CreateProcessAsUserW/event handles — OwnedHandle
liveness, no double-close/signal race), win_display, wgc_helper, interactive
vdisplay/windows: manager.rs, pf_vdisplay.rs (SwDeviceCreate/IddCx/ioctl handle
liveness via the OnceLock VDM singleton + OwnedHandle)
encode/windows: ffmpeg_win.rs (full AVBufferRef refcount audit — balanced, NO leaks,
unlike the vaapi sibling), sw.rs
cross-platform: gamestream/audio.rs (libopus), gamestream/stream.rs (sendmmsg),
inject/windows/sendinput.rs, audio/windows/wasapi_mic.rs,
session_tuning.rs, vdisplay.rs
Two findings (handled separately):
- wgc_relay.rs `unsafe impl Sync for HelperRelay` is UNSOUND (its mpsc Receiver is
!Sync) though not live-exploited — marked SUSPECT inline; fix pending box check
(it touches the in-flight punktfunk1.rs).
- capture.rs / encode.rs (PARENT modules of the WIP idd_push.rs / nvenc.rs) do NOT
get the file deny yet — it would propagate the lint into the undocumented WIP
children. The deny lands there once those are documented (after the WIP commits).
Linux-visible parts verified green (cargo clippy -p punktfunk-host --all-targets
-- -D warnings). The cfg(windows) deny gates are box-verified next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surfaced while writing the unsafe-soundness proofs (2/N): both are refcount
leaks (sound — never dangling/double-free — so the SAFETY proofs held, but real
bugs on the persistent punktfunk1-host listener that opens a fresh encoder per
session).
1. Per-session leak: `par->hw_frames_ctx = av_buffer_ref(drm_frames)` created a
second owned ref. `av_buffersrc_parameters_set` takes its OWN ref of
`par->hw_frames_ctx`, and `av_free(par)` frees only the struct, not the ref —
so the extra ref leaked every session, pinning the DRM frames ctx + device.
Fix: assign `drm_frames` borrowed (the standard ffmpeg pattern); our single
owned ref lives in DmabufInner and is unref'd in Drop.
2. Error-path leak: the final `open_vaapi_encoder(...)?` returned without the
unref ladder every other error path runs, leaking graph/drm_frames/
vaapi_device/drm_device on encoder-open failure. Fix: match + clean up before
returning (nv12_ctx is borrowed from the sink → freed by graph teardown).
cargo clippy -p punktfunk-host --all-targets -- -D warnings clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Continues the structural unsafe-proof program (every unsafe carries a documented
proof of soundness; the file gains #![deny(clippy::undocumented_unsafe_blocks)]
so it stays proven). This batch covers all 10 remaining pure-Linux files
(104 blocks), each proof stating the REAL invariant — not boilerplate:
zerocopy/cuda.rs (26) leaked process-lifetime libcuda fn-ptr table; opaque
CUcontext never dereferenced; free-exactly-once via the
Arc<Mutex<PoolInner>> ownership graph; dmabuf fd take/close split
zerocopy/egl.rs (18) eglGetProcAddress'd procs with the GL context current;
EGLImage liveness; the two-call modifier-query bounds
zerocopy/vulkan.rs (4) copy-bounds arithmetic (src_size>=span); Send = thread
confinement to the punktfunk-pipewire thread
dmabuf_fence.rs (4) poll/ioctl/close fd liveness + ownership
capture/linux/mod.rs (16) spa_data repr(transparent) cast; null-checked spa
derefs; single-loop-thread buffer ownership until requeue
inject/linux/gamepad.rs (10) uinput ioctl request-number ↔ struct-size match
(static-asserted); InputEventRaw no-padding for the byte cast
encode/linux/vaapi.rs (15) + encode/linux/mod.rs (9) ffmpeg object ownership/
free ladders; VAAPI/DRM graph; Send = single-thread transfer
inject/linux/wlr.rs (2), vdisplay/linux/kwin.rs (1)
No memory-unsafety SUSPECT blocks were found — the unsafe is sound. The vaapi
agent did flag two real AVBufferRef *leaks* (not UB) in DmabufInner::open; marked
inline with NOTE(leak) and addressed in a follow-up.
Verified: cargo clippy -p punktfunk-host --all-targets -- -D warnings is clean
(each file's deny gate hard-errors on any undocumented block).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Start of the structural unsafe-proof program (per the "every unsafe needs a
documented proof of soundness" goal): each `unsafe` block gets an accurate
`// SAFETY:` proof of WHY it is sound, and the file gains
`#![deny(clippy::undocumented_unsafe_blocks)]` so the proof requirement is
permanently enforced (a future undocumented unsafe in this file fails CI).
drm_sync.rs (10 blocks: libc open/ioctl/clock_gettime/close + 3 in tests): each
proof states the real invariant — fd liveness/ownership, the ioctl request number
encoding the matching struct size, the `&mut req` being a live correctly-sized
`#[repr(C)]` struct, and (for the timeline ioctls) the `handles`/`points` arrays
outliving the synchronous call with `count_handles` matching their length.
The gate grows file-by-file (CI stays green; undone files don't carry the lint
yet); it promotes to a crate-root deny once every file is done. ~122 Linux blocks
+ the Windows files remain.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GameStream's apps.json `cmd` is delivered via set_launch_command, which ONLY the Linux
gamescope backend nests. On Windows (no gamescope) and Linux kwin/mutter/wlroots (which
stream the existing desktop) the command was silently dropped. Now, after capture is live,
stream.rs spawns it via library::launch_gamestream_command for those backends — Windows:
into the interactive USER session (spawn_in_active_session, since the host is SYSTEM);
Linux: a plain `sh -c` spawn into the host's own graphical session so the app lands on the
streamed (primary) output. Linux gamescope keeps nesting via set_launch_command and is
skipped here to avoid a double launch. The command is operator-typed apps.json (trusted),
never client-set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A disk-backed art cache (library-art-cache.json in the canonical host config dir) is the
source of truth read by all_games(), so the library list + launch-resolve never block on
the network. A host-lifetime background warmer (start_art_warmer, started in serve())
fetches uncached art OFF the hot path: GOG via the public no-auth api.gog.com product API,
Xbox via the unofficial no-auth displaycatalog (keyed by StoreId). Both best-effort
(protocol-relative URLs normalized to https; results cached even when empty so they aren't
re-fetched). The GOG + Xbox providers now read cached_art() (title-only until warmed).
Cross-platform (ureq blocking HTTP — no tokio on this path) so the fetch/parse code is
compiled + checked everywhere; a host whose stores all self-provide art (Steam CDN /
Heroic CDN / Lutris data: URLs) does no fetching. Dep: ureq (webpki roots, no system certs).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
XboxProvider scans each fixed drive's <drive>:\XboxGames for GDK games (presence of
Content\MicrosoftGame.config marks a game vs. an ordinary UWP app), parsing title /
Identity name / Executable Id / StoreId via roxmltree. The PackageFamilyName is READ
from the AppRepository\Packages\<PackageFullName> dir name (reduced to Name_Hash) —
never computed from the publisher. Launch via the AUMID (shell:AppsFolder\<PFN>!<AppId>)
through explorer in the interactive user session (UWP activation needs the user token,
which spawn_in_active_session already provides). Cover art (displaycatalog) is deferred
→ title-only. Known v1 gaps: custom .GamingRoot install folders + non-GDK pure-UWP Store
games (under the ACL-locked WindowsApps) aren't enumerated.
New windows_launch_for `aumid` arm; XboxProvider wired into all_games() under cfg(windows).
Dep: roxmltree (Windows). Windows unit tests cover MicrosoftGame.config parsing (incl. the
ms-resource title fallback), the PackageFullName→PFN reduction, and the aumid launch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EpicProvider reads the launcher's local .item manifests under %ProgramData% (no auth,
launcher need not run) with Playnite's exclusion filter (skip UE_* components +
non-launchable addons + dead install dirs); cover art from the base64 catcache.bin
(public Epic CDN, best-effort). Launch via the com.epicgames.launcher:// URI opened
through explorer.exe — the namespace:catalogItemId:appName triple, with a bare-appName
fallback so a launch is never dropped.
GogProvider enumerates HKLM\SOFTWARE\WOW6432Node\GOG.com\Games (winreg) + each
goggame-<id>.info primary FileTask into a direct-exe spawn (no Galaxy, dodges its
cold-start/anti-cheat). GOG cover art (public api.gog.com) is deferred — it needs an
HTTP fetch + cache off the hot all_games() path — so GOG is title-only for now.
windows_launch_for gains epic/gog arms; both providers wired into all_games() under
cfg(windows). Deps: base64 moved to the cross-platform table (Epic catcache decode +
Lutris art encode both need it); winreg added on the Windows target. Windows unit tests
cover the Epic exclusion filter + URI builder and the GOG spawn + play-task parsing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The service's STOP/SESSION manual-reset events were smuggled across the C SCM
control-handler boundary as raw `isize` in `AtomicIsize` statics (the handler is a
capture-free `'static` closure, so it can't hold a non-`Send` `HANDLE` — it has to
reach the events through statics), reconstructed via `load_event`, and explicitly
`CloseHandle`d at `run_service` end.
Replace the raw-`isize` statics with `OnceLock<OwnedHandle>`:
- `run_service` creates each event, wraps it in an `OwnedHandle`, derives a borrowed
`HANDLE` for `supervise` (unchanged signature), and `set`s the OnceLock (once per
process) — all BEFORE the handler is registered, so the handler always sees `Some`.
- The handler reads `event_handle(&STOP_EVENT)` (a borrow) and `SetEvent`s it, with a
defensive `None` guard (matches the old `SetEvent(HANDLE(0))` no-op if it ever fired
pre-init).
- The events are owned by the OnceLocks for the process lifetime (the service process
exits right after `run_service` returns, so the OS reaps them at exit). Dropping the
explicit `CloseHandle` also removes the latent close-then-signal window the old
statics had (the raw isize lingered after the close).
Deletes the `AtomicIsize`/`Ordering` import + `load_event` + the raw-isize smuggle —
the last host-side raw-handle reduction. Behaviour-preserving (same events, same
signal/wait/reset, same once-per-process init order). Linux check + fmt clean; the
file is #[cfg(windows)] → to be box-validated (compile + a service stop/restart).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The GameCard badge hard-coded steam-vs-custom, so any non-Steam non-custom store
rendered with the "Steam" label. Add storeLabel(store): steam/custom keep their
localized strings, every other store is shown as a capitalized proper noun — so the
new Lutris/Heroic providers (and future ones) surface correctly with no per-store
translation. tsc --noEmit clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LutrisProvider reads the local pga.db (rusqlite, read-only/immutable so a running
Lutris can't block us) → installed games, launch via `lutris lutris:rungameid/<id>`,
cover art from Lutris's on-disk cache inlined as data: URLs (no public CDN keyed by a
stable id, unlike Steam/Heroic). HeroicProvider parses Heroic's store_cache JSON —
legendary/gog/nile = Epic+GOG+Amazon in one provider — installed-only with an
install-dir existence cross-check (works around Heroic's gog is_installed bug #2691),
free public CDN cover art, launch via `heroic --no-gui heroic://launch?...` (the
single-instance-Electron gamescope-escape caveat is documented; needs live confirm).
New command_for arms (lutris_id digits-guard, heroic runner+appName-guard) + both
providers wired into all_games(); everything Linux-gated (the launchers are
Linux-only), so the Windows/macOS host build is unaffected. Deps rusqlite (bundled
SQLite, no system dep) + base64 added to the Linux target only. Unit tests with
sqlite/json fixtures (installed-only filtering, CDN-art mapping, launch guards); live
`library` enumeration returns [] gracefully on a box without the launchers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Built the host crate (`cargo clippy --features nvenc -D warnings`) and the driver
workspace (`cargo build`) on the RTX box — the project's intended Windows gate,
which `cargo check` (what the goal1/§2.5 work used) never runs. It surfaced lint
issues accumulated across the goal1 / §2.5 / this-session Windows work:
- 9× redundant `as *mut c_void` after `.as_raw_handle()` (already `*mut c_void`):
idd_push.rs (3, this session), service.rs (3, this session), manager.rs (3,
pre-existing §2.5 — my OwnedHandle work copied the idiom). Removed the casts +
the now-unused `use std::ffi::c_void` in idd_push.rs / manager.rs (service still
uses it).
- `if_same_then_else` in session_plan.rs::resolve_topology (pre-existing goal1
stage 3): collapsed the two `false` arms into one condition (behavior identical).
- `unused_unsafe` in the driver `pod_init!` macro: it expands at call sites already
inside an `unsafe` block, where its own `unsafe` is redundant — `#[allow(
unused_unsafe)]` (needed at the non-unsafe sites, redundant at the nested ones).
After these, BOTH builds are clean on the box — validating the whole session's
blind Windows + driver work compiles + passes clippy on real hardware.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The IDD-push consume loop acquired the slot's keyed mutex by hand
(`AcquireSync(0,8)` … work … `ReleaseSync(0)`), with a comment warning that a
`?`-return between acquire and release would leak the lock and stall the driver
on that slot — the reason the HDR converter is built *before* the acquire.
Replace with a `KeyedMutexGuard` RAII (acquire → `ReleaseSync` on drop), scoped
to JUST the convert/copy block so the lock releases at the EXACT same point as
before (the driver gets the slot back immediately; not held across the rest of
`try_consume`). Now the release can't be skipped on any early return/panic — the
leak footgun is gone by construction, and the hot loop has no raw `ReleaseSync`.
Behavior/latency-equivalent (same acquire params, same release point). Windows-
only (CI + on-glass gated); to be validated on the RTX box (host clippy build +
a PERF=1 latency A/B vs the shipping binary — the change should show no delta).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pre-existing working-tree changes committed to the branch on request: the
gpu-contention investigation doc, host-latency-plan additions, and small
pack-host-installer / stage-pf-vdisplay packaging-script edits.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the no-op Windows `set_launch_command` real. New `windows/interactive.rs`
`spawn_in_active_session` (WTSGetActiveConsoleSessionId → WTSQueryUserToken →
CreateProcessAsUserW(winsta0\default) under the LOGGED-IN USER token, factored from
the wgc_relay primitive) + `library::launch_title` resolving a store-qualified id to
a concrete process via `windows_launch_for` (steam_appid → Steam.exe/explorer.exe
steam:// URI; command → cmd.exe /c). Threaded as `SessionContext.launch` into both
native data-plane paths (`virtual_stream`, `virtual_stream_relay`) and fired after
capture is live so the title renders onto the captured desktop and grabs foreground.
Security invariant intact: the client sends only the store-qualified id; the host
resolves the recipe from its own library and the URI/flags are handed to a concrete
EXE as plain args (never cmd /c of a client string). Linux unchanged (gamescope
nesting via the handshake PUNKTFUNK_GAMESCOPE_APP path).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The DualSense, DualShock 4, and XUSB Windows pad backends each hand-rolled the
SAME per-pad resource handling: a `CreateFileMappingW` + `MapViewOfFile` shared
section (with the permissive D:(A;;GA;;;WD) SDDL the restricted-token driver
needs) and an identical `Drop` doing `SwDeviceClose` + `UnmapViewOfFile` +
`CloseHandle` — three copies, each a chance to drift or leak on an error path.
New `inject/windows/gamepad_raii.rs` owns both resources with RAII:
- `Shm` — the section handle (`OwnedHandle`) + its view; `Shm::create(name, size)`
does the SDDL + map + zero-fill leak-safely, `base()` gives the mapped pointer,
`Drop` unmaps then closes (in that order).
- `SwDevice` — the `SwDeviceCreate`'d devnode; `Drop` calls `SwDeviceClose`.
All three backends now hold `_sw: Option<SwDevice>` + `shm: Shm` instead of raw
`hsw`/`map`/`view`, access the section via `self.shm.base()`, and have NO manual
`Drop`. Deletes the duplicated `create_shm_section` (DualSense/DS4 now use
`Shm::create`) and the three hand-written Drops; the DS4 device-type byte is still
written before the magic, the SwDeviceCreate `None` fallback still works, and the
field drop order (devnode removed, then section unmapped+closed) matches the old
manual order.
Net: 3 manual `Drop`s + a duplicated section-creation path → one shared RAII
module; fewer unsafe ops, leak-on-error fixed by construction. Linux `cargo check`
clean (the inject mod wiring); the backends are #[cfg(windows)] → CI-gated.
Drafted + adversarially verified (no double-free, imports correct under
-D warnings, behavior preserved); my own spot-checks confirm.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The driver zero-initialised C POD structs (IddCx/WDF descriptors) with 27
scattered `let mut x: T = unsafe { core::mem::zeroed() };`, each carrying its own
`// SAFETY` about the all-zero bit pattern being valid + the caller setting `.Size`
etc. right after.
Replace with one `pod_init!(T)` macro (in log.rs, reachable everywhere via the
existing `#[macro_use] mod log;` — same mechanism as `dbglog!`) that owns the
single `unsafe { zeroed::<T>() }` + the SAFETY rationale. All 27 sites
(adapter 6, callbacks 3, entry 4, monitor 10, swap_chain_processor 4) now read
`let mut x = pod_init!(T)`. Zero behavior change (mem::zeroed semantics identical);
the type is passed explicitly so no inference depends on the removed annotation.
27 `unsafe` blocks → 1. Driver still `deny(unsafe_op_in_unsafe_fn)`-clean (the
macro expands to an explicit `unsafe {}`; the one nested-in-user-unsafe site is
fine — no `unused_unsafe` for macro-generated blocks). Driver-only (CI-gated);
adversarially reviewed (macro scoping, all sites, no leftover raw zeroed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The SCM supervisor scattered manual `CloseHandle(pi.hProcess)`/`(pi.hThread)`
across ~5 supervise-loop match arms and hand-closed the job object — easy to miss
an arm (leak) or double-close.
- `spawn_host` returns an owned `Child { process: OwnedHandle, _thread: OwnedHandle,
pid }` instead of raw `PROCESS_INFORMATION`; the supervise loop borrows
`child.process` (`HANDLE(as_raw_handle() as *mut c_void)`) for wait/Terminate and
the `Child` auto-closes both handles when it drops / is replaced each iteration.
- The job object → `OwnedHandle` (borrowed for AssignProcessToJobObject), auto-closed.
- Deletes ~9 manual `CloseHandle` calls. The `_thread` handle is RAII-only (`_`-prefixed
so `dead_code`/`-D warnings` doesn't flag it).
Deliberately LEFT the `STOP_EVENT`/`SESSION_EVENT` `AtomicIsize` statics as-is — they
are smuggled into the C SCM control handler, so `OwnedHandle`-ifying them is a separate,
riskier supervisor redesign out of scope here (noted in a comment).
Behavior preserved (the supervise state machine / wait semantics / restart-on-
session-change / kill-on-close are unchanged). Windows-only (CI-gated); adversarially
reviewed (no double-close, handles outlive their borrows, idiom matches manager.rs).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The IDD-push capturer held raw `HANDLE`s for the shared header mapping, the
frame-ready event, the debug section, and each ring slot's shared texture, with
manual `CloseHandle` scattered across two `Drop` impls — and the MapViewOfFile
VIEWS (header/dbg_block) were never UnmapViewOfFile'd (a real view leak).
- New `MappedSection { handle: OwnedHandle, view }` RAII: `Drop` UnmapViewOfFile's
the view THEN the `OwnedHandle` closes the mapping (unmap-before-close).
- `map`+`header` → `section: MappedSection` (+ a cached `header` ptr borrowing into
it, declared after `section` for drop order); same for `dbg_map`+`dbg_block`.
- `event: HANDLE` → `OwnedHandle` (borrowed as `HANDLE(as_raw_handle() as *mut
c_void)` for WaitForSingleObject); `HostSlot.shared` → `OwnedHandle` (its manual
`Drop` deleted). Removed the manual `CloseHandle`s + the `CloseHandle` import.
Net: deletes two `Drop` impls' worth of manual handle/view teardown and fixes the
view leak — fewer unsafe ops, RAII-correct. Behavior preserved (recreate_ring
writes the header in place; the keepalive still drops last so REMOVE is last).
Windows-only (CI-gated); adversarially reviewed (no double-free / UAF / dangling
header; handle interop matches manager.rs). Linux check unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The shared host<->driver ABI crate already contains more than the virtual
display: the IDD-push frame ring + control plane AND the gamepad shared-memory
layouts (XusbShm / PadShm). "pf-vdisplay-proto" was a misnomer — the name now
represents all the drivers it serves.
Mechanical rename, no behavior change:
- git mv crates/pf-vdisplay-proto -> crates/pf-driver-proto (package name +
path-deps in the host crate and the driver workspace).
- pf_vdisplay_proto -> pf_driver_proto across host + driver Rust, both Cargo.lock
files, the workspace members, the CI path triggers (windows-drivers.yml), and
the docs/INF comments. The runtime Global\pfvd-* shared-object names are a
SEPARATE contract and are deliberately untouched (host<->driver name matching).
- The pf-vdisplay DRIVER crate + its INF service name (Root\pf_vdisplay,
UmdfService=pf_vdisplay, pf_vdisplay.dll) are unchanged — only the full
`pf_vdisplay_proto` token was replaced, never the `pf_vdisplay` driver name.
Linux-verified: cargo test -p pf-driver-proto (const size-asserts compile) +
cargo clippy -p punktfunk-host -D warnings clean; Cargo.lock regenerated. The
driver-workspace side (path-dep + imports + its Cargo.lock) is Windows-CI-gated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Goal 2 ("drop every trace of SudoVDA") is done. The SudoVDA driver is no longer
shipped (only pf-vdisplay; the old vdisplay-driver tree was deleted in 416862a),
and F1 (d5c6b7e/03da47b) already moved the display-utility helpers out of the
backend into neutral modules (win_adapter/win_display), breaking the reach-in.
So the backend is now cleanly removable:
- Deleted crates/punktfunk-host/src/vdisplay/windows/sudovda.rs (350 lines: the
SudoVdaDisplay VirtualDisplay impl + its VdisplayDriver/probe).
- vdisplay::open()/probe() are now unconditional pf-vdisplay; deleted the
windows_use_pf_vdisplay() backend selector. open() now ensure!s
pf_vdisplay::is_available() with a clear "driver not installed" error instead
of the old silent SudoVDA fallback (no fallback driver exists anymore).
- Scrubbed the dangling references to the deleted symbols (manager/sendinput/dxgi
comments, the config + host.env PUNKTFUNK_VDISPLAY docs); the var stays as an
informational forward-seam. Updated the F1 module docs (Goal 2 now done).
All changes are #[cfg(windows)] except the config doc; Linux clippy
-p punktfunk-host -D warnings clean; zero `sudovda::`/`SudoVdaDisplay` code refs
remain (comments only). Windows build is CI-gated.
Scorecard Goal 2 -> DONE; recorded the E1 "do NOT do it" stability decision in
windows-host-rewrite.md §4 (the process-global driver design is sound given
ProcessSharingDisabled; a device-owned variant adds a use-after-free window for
no gain).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The driver assigned each virtual monitor a monotonically-increasing NEXT_ID used
as the EDID serial / IddCx ConnectorIndex / container GUID, and never reclaimed
it on REMOVE. Under sustained ADD/REMOVE churn the connector index kept climbing,
so IddCx/PnP allocated a NEW OS target slot every cycle and orphaned the old one
(ghost "Generic Monitor (punktfunk)" nodes) until the adapter's target capacity
was exhausted and ADD failed 0x80070490 ERROR_NOT_FOUND.
Fix: `create_monitor` now allocates the LOWEST free id (`alloc_monitor_id`,
computed under the MONITOR_MODES lock with the push) instead of a counter, so a
departed monitor's id is reclaimed and a fresh ADD reuses its target slot rather
than orphaning it. With <= N live monitors the id stays bounded to 1..=N+1.
Deleted the now-unused NEXT_ID + AtomicU32/Ordering import.
CI-compile-gated only — the wedge reproduces solely under sustained churn on the
RTX box, so this needs an on-glass reconnect-storm A/B to confirm (box is
ephemeral/down). Marked on-glass-pending in windows-host-rewrite.md §4; keep
reset-pf-vdisplay.ps1 as the recovery until validated. NOT to be relied on (or
merged to main) until that A/B passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the validated IDD-push zero-copy path the default for a fresh install,
without penalising dev / non-pf-driver runs:
- The shipped default config now enables it. Both seed sites set
`PUNKTFUNK_VDISPLAY=pf` + `PUNKTFUNK_IDD_PUSH=1`: the hardcoded default the
service writes on `service install` (`ensure_default_host_env`) AND the
`host.env.example` template the installer bundles. A fresh install therefore
runs the validated path (the installer also bundles the pf-vdisplay driver);
it falls back to DDA if the driver can't attach.
- `idd_push` is now **value-aware** instead of a bare presence flag, so an
operator can turn it OFF with `PUNKTFUNK_IDD_PUSH=0` in host.env — a `var_os`
presence check read `=0` as "on". Unset still ⇒ off (the code default is
unchanged, so existing host.env files and dev/CI runs are unaffected; only the
shipped default config opts in).
Also scrubbed the stale "SudoVDA" wording in host.env.example. Linux cargo
clippy -p punktfunk-host -D warnings clean; the service.rs default string is
Windows-only (CI-gated).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stand up Storybook so the management console can be designed without a running
host, plus the design-system work that surfaced along the way.
Storybook (@storybook/react-vite):
- Slim Start/Nitro-free vite config; the preview imports the app's real
src/styles.css directly so the design tokens stay single-sourced (no mirror).
- Stories for the @unom/ui primitives (Button/Card/Inputs/Badge), brand marks,
the AppShell (throwaway in-memory TanStack router), and every data-driven page
(Dashboard/Host/Clients/Library/Settings) rendered offline via a window.fetch
stub + typed fixtures. The route page components are exported so stories can
render them.
Light theme:
- styles.css now carries a light :root (lavender, from the docs palette) with the
existing violet chrome moved to .dark; the live console still pins html.dark by
default, so this only adds the option (Storybook's toolbar toggles it).
- Fixes a stray `*/` inside a comment that prematurely closed it and silently
broke Tailwind's @theme processing.
Spinner:
- The punktfunk lens recreated with motion/react: two circles surge through one
another in depth (JS perspective scale + z-index — robust where mix-blend-mode
flattens CSS preserve-3d) with a screen-blend lens highlight. Replaces the
skeleton loading state in QueryState; removes ui/skeleton.tsx.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Windows-host docs were scattered across a design plan, a staged-refactor
plan, an audit, an audit-remediation tracker, and a game-capture-bug analysis —
several badly stale (the audit/remediation predate the Goal-1 branch landing and
call DONE items "not started"). Verified the true state of every audit finding /
goal / milestone against current code+git (4-agent workflow), then rewrote
windows-host-rewrite.md as ONE consolidated, accurate doc:
- §1 Status scorecard (Goals 1-3, M0-M6, GB1, audit P0/P1/P2) with DONE/PARTIAL/
OPEN + commit evidence.
- §2 Architecture as-built (layering, HostConfig→SessionPlan→SessionContext, the
VirtualDisplayManager ownership model, IDD-push-primary capture incl. secure
desktop + GB1 recovery, encode/EncoderCaps, pf-vdisplay-proto, the driver,
service/packaging).
- §3 Validated invariants (the jewels).
- §4 Prioritized open tasks (the genuine remaining work).
- §5 Operations (RTX-box recipe, CI, env, build).
- §6 Deep reference (/INTEGRITYCHECK answer, the 6 iddcx bindgen knobs, the driver
port checklist, resolved decisions).
Deleted the four now-redundant docs (content folded in; history in git):
windows-host-goal1-plan.md, windows-host-rewrite-audit.md,
windows-host-rewrite-remediation.md, windows-host-rewrite-game-capture-bug.md.
Repointed the 6 code/proto/driver doc-comment refs that targeted them at the
consolidated windows-host-rewrite.md sections. Linux cargo check clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The last §2.3 seam-trait tightening: give `Encoder` a `caps() -> EncoderCaps`
so the session glue routes by *query* instead of relying on the no-op/`false`
defaults of `invalidate_ref_frames`/`set_hdr_meta`.
`EncoderCaps { supports_rfi, supports_hdr_metadata }` is a cheap `Copy` struct.
The trait gains a default `caps()` returning `EncoderCaps::default()` (all
false) — correct for every SDR/libavcodec backend (Linux NVENC, VAAPI, AMF/QSV,
software openh264), so they need no change. Only the Windows direct-NVENC path
(`NvencD3d11Encoder`) overrides it, reporting the real `rfi_supported` (probed
once at open via `nvEncGetEncodeCaps`) and `hdr` (HDR-SEI on keyframes).
Consumer: the GameStream encode loop (`gamestream/stream.rs`) hoists
`supports_rfi` once before the loop and gates the loss-recovery path on it —
`!(supports_rfi && enc.invalidate_ref_frames(..))` forces a keyframe directly
on non-RFI encoders instead of making an always-`false` call every loss event.
Behaviour-preserving (same keyframe/RFI outcome), one fewer no-op call, intent
explicit. The native host (punktfunk1) uses FEC+keyframes, no RFI consumer.
Linux `cargo clippy -p punktfunk-host --all-targets -D warnings` clean; the
three edited files are rustfmt-clean. The NVENC override is Windows-only
(1:1 with the existing impl style) → CI/on-glass gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Today's manual driver recovery (wedged under ADD/REMOVE churn → ERROR_NOT_FOUND) and the manual
host-stop/install/host-start dance around drivers/deploy-dev.ps1 are now two scripts:
* reset-pf-vdisplay.ps1 — recover a wedged driver: stop host → pnputil /remove-device the ghost
"Generic Monitor (punktfunk)" nodes → Disable+Enable the adapter
(Restart-PnpDevice doesn't exist on the box PS) → start host. No reboot
(the box boots to Proxmox). -Verify probes to confirm ADD recovered.
* redeploy-pf-vdisplay.ps1 — one-shot dev redeploy wrapping deploy-dev.ps1 with the host stop/start
(the running host holds the driver DLL) + a post-install adapter reload
(pnputil updates the store but the live device keeps the old binary).
Both standalone (don't touch deploy-dev.ps1). README gains a "Dev iteration on the test box" section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>