eb451d8bc60398f706865aaec39248c5b595f3c0
138 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
eb451d8bc6 |
fix(host/windows): retry DuplicateOutput1 to ride out the old-dup teardown race
User's insight, and it fits the evidence exactly: in duplicate_output the FIRST
DuplicateOutput1 (called microseconds after the caller releases the old
duplication via self.dupl=None) returns E_ACCESSDENIED, but the legacy
DuplicateOutput a beat later SUCCEEDS — the only difference is TIMING. The
kernel-side teardown of the just-released duplication is async, so the immediate
DuplicateOutput1 races it ('output still duplicated' -> E_ACCESSDENIED). We then
fell straight through to legacy DuplicateOutput, which 'succeeds' into a FRAGILE
dup that churns ACCESS_LOST/MODE_CHANGE every few ms on this cross-GPU IDD
(causing the post-login freeze + UAC-confirm drop).
Fix: retry DuplicateOutput1 up to 5x with escalating 2/4/8/16 ms waits before
falling back to legacy, so the teardown finishes and the ROBUST DuplicateOutput1
dup succeeds (no churn). Bounded (~30 ms worst case) so a genuine failure still
falls back quickly. This is exactly Apollo's 2x/200ms retry rationale.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
1e1e5ce9b5 |
fix(host/windows): Option-handle the multi-line dupl.GetFramePointerShape call too
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
da43b5e8d3 |
fix(host/windows): release the old duplication before re-duplicating (THE born-lost bug)
DuplicateOutput1 returned E_ACCESSDENIED ~8815x even with PER_MONITOR_AWARE_V2 confirmed on the capture thread (thread_is_v2=true) — so DPI was NOT the cause. The real cause: DXGI permits only ONE IDXGIOutputDuplication per output, and on ACCESS_LOST you MUST release the old one before re-duplicating. Our recovery (try_reduplicate / recreate_dupl) created the NEW duplication while the OLD self.dupl was still alive → the output stayed held → DuplicateOutput1 E_ACCESSDENIED and the legacy fallback returned a BORN-LOST dup. It never converged because there was always exactly one stale dup alive at creation time. The initial open() works precisely because there's no prior dup; Apollo is clean because it releases (dup.reset()) before every re-DuplicateOutput. Fix: make self.dupl an Option and set it to None (drop → release the output) BEFORE duplicate_output in try_reduplicate and before reopen_duplication in recreate_dupl, then Some(new). acquire() gets a None-guard that synthesizes ACCESS_LOST (routes into recovery) so a transient None can't panic. All ReleaseFrame/AcquireNextFrame sites updated for the Option. This is the documented DDA recovery requirement and the one thing that distinguished our failing DuplicateOutput1 from Apollo's working one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
c8fb4822a2 |
fix(host/windows): per-thread Per-Monitor-V2 DPI awareness so DuplicateOutput1 succeeds
The remaining born-lost ACCESS_LOST storm traces to ONE thing: our
IDXGIOutput5::DuplicateOutput1 returns E_ACCESSDENIED (0x80070005) ~4370x, so
we fall back to legacy DuplicateOutput, which yields a BORN-LOST duplication on
this hybrid box. Apollo's DuplicateOutput1 SUCCEEDS on the identical
desktop/output/4090-device → a working dup, clean capture.
Root cause: DuplicateOutput1 REQUIRES Per-Monitor-Aware-V2. At startup our
SetProcessDpiAwarenessContext(PER_MONITOR_AWARE_V2) FAILS with E_ACCESSDENIED
('already set' — a manifest/runtime locked the process to a lower awareness),
and GetAwarenessFromDpiAwarenessContext reports 2 for BOTH Per-Monitor V1 and
V2, so the earlier 'awareness=2' was misleading — the process is likely V1,
which DuplicateOutput1 rejects with E_ACCESSDENIED. (Legacy DuplicateOutput has
no V2 requirement, so it 'worked' but born-lost.)
Fix: SetThreadDpiAwarenessContext(PER_MONITOR_AWARE_V2) on the capture thread
in open() — a per-thread override that takes regardless of the process default,
so DuplicateOutput1 can succeed (the working dup Apollo gets). Logs set_ok +
thread_is_v2 (via AreDpiAwarenessContextsEqual) to confirm V2 actually applied.
Topology fixes (sole display, no MODE_CHANGE) and the recovery backstops stay.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
c60a05dbe9 |
fix(host/windows): make SudoVDA the sole display via clean CCD (the IDD needs to be primary/composited)
Live result of the previous build: the MODE_CHANGE_IN_PROGRESS storm was FIXED
(0 occurrences) by dropping primary-promotion — but it exposed the regression
the review predicted: a non-primary EXTENDED SudoVDA is NOT DWM-composited on
this box, so DDA gets born-lost ACCESS_LOST (0x887a0026) + black frames. The
IDD genuinely must be the sole/primary/composited display here.
Apollo reaches that end state ('Virtual Desktop: 5120x1440', sole display) via
Windows AUTO-promoting the real WDDM display over the box's leftover 1024x768
basic display — but Windows does NOT auto-promote for us, leaving the IDD
extended. So make it sole explicitly, the clean way:
- create(): deactivate the other display(s) via the atomic CCD path
(isolate_displays_ccd) by DEFAULT (opt out with PUNKTFUNK_NO_ISOLATE). Drop
the legacy per-device GDI detach from the path (it misses iGPU-attached
monitors and churns; kept #[allow(dead_code)] for reference).
- set_active_mode(): CDS_UPDATEREGISTRY only — set the mode in place, NO
CDS_SET_PRIMARY / CDS_GLOBAL / DM_POSITION. A sole display is already primary,
so there's nothing to contest → no MODE_CHANGE storm (that storm came from
promoting primary at (0,0) WHILE the basic display was still active).
Net: sole SudoVDA → primary → composited → capturable, with no topology
contest. Keeps the prior MODE_CHANGE-as-transient handling + removed born-lost
escape as backstops.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
769fd96b87 |
fix(host/windows): stop SudoVDA MODE_CHANGE_IN_PROGRESS storm — don't force IDD primary by default
ROOT CAUSE (verified by multi-agent compare vs Apollo + adversarial review): set_active_mode() applied the SudoVDA mode with CDS_UPDATEREGISTRY | CDS_GLOBAL | CDS_SET_PRIMARY + DM_POSITION(0,0) — promoting the freshly-added IDD to PRIMARY at the virtual-screen origin and persisting it globally. On this box (baseline active display = a 1024x768 basic 'WinDisc') that primary-promotion contests the existing display so the desktop topology never reaches a stable fixed point → every DuplicateOutput/AcquireNextFrame during the unending settle returns DXGI_ERROR_MODE_CHANGE_IN_PROGRESS (0x887A0025). Apollo, live on this EXACT box with an empty config, never promotes primary and captures the same SudoVDA at 5120x1440 with zero DXGI errors. (Ruled out earlier on the live box: win32u hook, DPI, independent-flip/overlay, isolation, render pin.) Fixes (subtractive, gated per adversarial review): - sudovda.rs set_active_mode: default to CDS_UPDATEREGISTRY only (no primary promotion, no GLOBAL, no DM_POSITION) = Apollo-parity for the multi-display default. Promote to primary (CDS_GLOBAL|CDS_SET_PRIMARY+DM_POSITION) ONLY when PUNKTFUNK_ISOLATE_DISPLAYS=1 (sole display, where a blank extended IDD would otherwise yield no frames). Avoids regressing headless/isolated + mid-stream Reconfigure. - dxgi.rs acquire: treat MODE_CHANGE_IN_PROGRESS (0x887A0025) as a TRANSIENT (Ok(None), repeat last frame, wait it out) instead of falling through to the fatal Err arm → cold-rebuild → create()→set_active_mode (which re-issued the mode change and amplified the storm). - dxgi.rs acquire: remove the born-lost cold-rebuild escape — it re-created the SudoVDA (IOCTL REMOVE/ADD = the audible PnP chime the user heard) and never converged; now repeat last frame in-process (never tear the IDD down mid- session, like Apollo). Overlay + cheap-spin/HDR recovery left intact. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
900089c44c |
fix(host/windows): don't pin SudoVDA render adapter by default (Apollo parity)
GROUND TRUTH from Apollo streaming live on this exact box (empty config): captures the SudoVDA at 5120x1440@240 on the RTX 4090 with ZERO ACCESS_LOST / born-lost / MODE_CHANGE -- clean, no overlay, no isolation, no render pin. That disproves the independent-flip theory (a sole SudoVDA captures fine here) and points at something WE do that Apollo doesn't. The concrete culprit: we call SET_RENDER_ADAPTER, which this driver IGNORES (logs 'render adapter DIFFERS from pinned add=0x23664 pinned=0x15768') and the IDD ends up rendering on adapter 0x23664 while its DXGI output is enumerated under the 4090 (0x15768) where we create the capture device -- a cross-GPU mismatch that is the real source of the perpetual ACCESS_LOST + MODE_CHANGE_IN_PROGRESS (0x887A0025) storm. Apollo never pins (empty config), so its IDD stays on its natural adapter, aligned with capture. Make the render pin OPT-IN (PUNKTFUNK_RENDER_ADAPTER=<name>); default to NOT pinning, matching Apollo. The startup log now shows the resulting AddOut LUID so we can confirm the IDD lands on the 4090. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
cd72164db2 |
fix(host/windows): keep multi-display (Apollo parity) instead of sole-display isolation
CONFIRMED on the live RTX4090+iGPU box: hook fires+verified, DPI=2, overlay running, yet the stream STILL freezes -- born-lost dropped but MODE_CHANGE_IN_ PROGRESS (0x887A0025) churn took over (2284x) and frames go stale. Root cause is the topology itself: create() makes SudoVDA the SOLE active display (CDS_SET_PRIMARY + isolate_displays + isolate_displays_ccd), and a sole display on a hybrid box goes into fullscreen independent-flip / MPO that Desktop Duplication cannot capture. Apollo is rock solid on this EXACT box because it does the opposite: it keeps the physical monitor ACTIVE and arranges the virtual display alongside it (rearrangeVirtualDisplayForLowerRight, 'Do not change the primary'). Multi- display is DWM-composited, so the output never independent-flips. Make isolation OPT-IN (PUNKTFUNK_ISOLATE_DISPLAYS=1) and default to NOT isolating -- match Apollo's multi-display topology. SudoVDA stays primary (so it carries the shell -> frames) but other monitors stay active, which disables independent-flip. reassert_isolation honors the same flag (re-isolating mid- stream would itself trigger the storm). Keeps the overlay + born-lost escape as belt-and-suspenders. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
5f84c5785c |
fix(host/windows): force-composed-flip overlay in the single-process DDA path
CONFIRMED root cause via instrumented build: hook_hits=1+ (win32u hook fires, verified-patched) and DPI awareness=2 (PER_MONITOR), yet the born-lost ACCESS_LOST storm persists with 100% DuplicateOutput1 E_ACCESSDENIED. That rules out reparenting (the hook works) and DPI -> it is fullscreen independent-flip / MPO: the SudoVDA virtual display, isolated as the SOLE active output, scans out one plane on one display, bypassing DWM composition, so Desktop Duplication gets a born-lost duplication. Apollo never hits this because it runs WITH a physical monitor attached (multi-display is already DWM-composited); we isolate to sole-display, so we must force composition ourselves. The fix already existed (ForceComposedFlip, a tiny topmost layered overlay that disqualifies independent-flip) but was only wired into the WGC relay path's secure branch, which PUNKTFUNK_NO_WGC=1 disables. Wire it into virtual_stream unconditionally (DDA owns the normal desktop here, where the storm is). Held for the session; Drop tears it down; PUNKTFUNK_FORCE_COMPOSED=0 disables. Keeps the prior build's born-lost escape as a safety net. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
63b63a4010 |
fix(host/windows): instrument + harden DDA against the born-lost ACCESS_LOST storm
The hybrid RTX4090+iGPU box storms DXGI_ERROR_ACCESS_LOST (0x887A0026) + MODE_CHANGE_IN_PROGRESS (0x887A0025) ~3s after first frame: every rebuilt duplication is born-lost (created OK, first AcquireNextFrame instantly ACCESS_LOST), seeds black, retries forever. The steady-state m3 loop calls try_latest()->acquire() which returns Ok(None) on every recovery, so the cold-rebuild escape (MAX_CAPTURE_REBUILDS) was unreachable -> frozen stream. Multi-agent root-cause + adversarial review point at the win32u GPU-pref hook being ineffective (patched on the main thread, no FlushInstructionCache, never verified) rather than the synthesis's independent-flip theory (Apollo has no overlay yet is stable on this exact box). This build instruments + applies the safe, high-probability fixes: - Hook: FlushInstructionCache after the inline patch (cross-thread i-cache); read back the 12 patched bytes and error! if they didn't land; per-call hit counter (hybrid_hook_hits) logged after open -- hits==0 proves the hook is off DXGI's reparent path. - DPI: log SetProcessDpiAwarenessContext result + effective awareness (need 2=PER_MONITOR for DuplicateOutput1; explains the 100% E_ACCESSDENIED). - SetThreadExecutionState(ES_CONTINUOUS|ES_DISPLAY_REQUIRED|ES_SYSTEM_REQUIRED) at capture open, restored on Drop -- stop IDD idle-invalidation (Apollo does this too). - Born-lost escape: count consecutive born-lost rebuilds; on the NORMAL desktop (never the secure/Winlogon dwell) escalate to Err after ~5s so the m3 loop cold-rebuilds the whole pipeline instead of freezing on the last frame. Diagnostic-forward: one test now tells us hook-hits + DPI awareness + whether ExecutionState/desktop-sync alone fixes it, and the stream self-recovers instead of wedging. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
60bb9727d6 |
fix(host/windows): correct SetDisplayConfig slice signature + local DISPLAYCONFIG_PATH_ACTIVE
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
2ac1014e8e |
fix(host/windows): CCD-based display isolation (detach hybrid-attached monitors)
The freeze on context change is the lock/login rendering on a PHYSICAL monitor instead of the captured SudoVDA display. Root cause: the legacy isolate_displays (EnumDisplayDevices + ChangeDisplaySettings) found NOTHING to detach on this hybrid box (4090 + AMD iGPU) — an iGPU-attached monitor isn't flagged ATTACHED_TO_DESKTOP in the GDI enum, so it's never detached and the secure desktop lands on it while the virtual output freezes. (Log: isolate ran, logged zero "detaching" lines.) Add CCD-based isolation (QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS) + SetDisplayConfig) — the API Apollo uses, which sees every active path. Deactivate all active paths except the SudoVDA target's, leaving the virtual display the sole desktop so ALL content (incl. Winlogon) renders to it. Runs alongside the legacy pass (now a no-op fallback); the original topology is saved and restored on teardown before REMOVE. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
3237ca31cd |
feat(host/windows): capture via IDXGIOutput5::DuplicateOutput1 (Apollo's capture API)
The one major capture-API difference left vs Apollo: punktfunk used legacy IDXGIOutput1::DuplicateOutput; Apollo uses IDXGIOutput5::DuplicateOutput1 with a format list, the modern path that's more robust to overlay/format changes (a candidate for the SudoVDA-on-hybrid 0x887A0026 churn). Add a duplicate_output() helper used at all 3 duplication sites (open, reopen_duplication, try_reduplicate): QI to IDXGIOutput5 and DuplicateOutput1, falling back to legacy DuplicateOutput. DuplicateOutput1 requires per-monitor-v2 DPI awareness, so set that at process start alongside the GPU-pref hook (matches Apollo). Format list is BGRA8-only for now (SDR test): DuplicateOutput1 returns the first format it can CONVERT to, so FP16-first would hand back FP16 even on SDR and trip the HDR path. Real FP16/HDR capture (with IDXGIOutput6 colorspace detection) is the follow-up once the churn is settled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
7cfeddc770 |
fix(host/windows): install the GPU-preference hook at process start (before any DXGI)
The win32u hook only works if it patches before DXGI caches the hybrid preference. It was installed in DuplCapturer::open (first capture), but the SudoVDA render-adapter selection creates a DXGI factory during virtual-display setup — seconds earlier — so the preference was already cached and the hook had no effect (churn persisted; log showed "render adapter chosen" at :02, "hook installed" at :04). Call install_gpu_pref_hook() at the top of real_main(), before any command runs, so it beats the first DXGI factory. (open() still calls it too; Once makes the earliest call win.) Also fix the cosmetic function-cast-as-integer warning. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
a01f8a2f58 |
feat(host/windows): port Apollo's win32u GPU-preference hook (fix hybrid-GPU DDA churn)
Root cause of the ACCESS_LOST (0x887A0026) churn + context-change freeze, found live: the box is a HYBRID system (RTX 4090 + AMD Radeon iGPU + SudoVDA). DXGI does hybrid GPU-preference resolution and REPARENTS the SudoVDA output between adapters (SET_RENDER_ADAPTER is ignored — the IDD lands on the iGPU 0x23664 while we duplicate on the 4090 0x15768), which constantly invalidates Desktop Duplication. Apollo runs fine on this same box because it hooks this away. Port Apollo's hook: replace win32u.dll!NtGdiDdDDIGetCachedHybridQueryValue to always report D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED, so DXGI skips preference resolution and never reparents the output → DDA stays on one adapter. Installed once before the first DXGI factory/enumeration (DuplCapturer::open). We fully replace the function (never call the original) so a 12-byte absolute-jmp prologue patch suffices — no detour crate / C length-disassembler dependency, just VirtualProtect. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
61fd75dc33 |
fix(host/windows): re-isolate/re-attach desktop ONLY on the secure desktop
recreate_dupl called reassert_isolation (a display-TOPOLOGY change via isolate_displays) + attach_input_desktop on EVERY ACCESS_LOST rebuild — 200× in a 6 s SDR session. A topology change itself invalidates the freshly-rebuilt duplication, so the next acquire is ACCESS_LOST → recreate → reassert → a self-feeding 0x887A0026 churn that freezes the stream and never recovers across context changes (lock / login / post-login). Gate both behind is_secure_desktop(): the heavy topology work runs only on the actual Winlogon (secure/login) desktop — where a physical monitor can grab the secure desktop off our virtual output. Routine churn, the lock screen, and post-login are all on the normal desktop, so they take a light re-duplicate with no topology meddling. Apollo isolates once at startup; its recovery just re-duplicates — this matches that. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d11f2bf800 |
fix(host/windows): stop the DDA freeze — kill the HDR format-change storm + throttle ACCESS_LOST recovery
Two freeze drivers found live on the RTX box (DDA-only, 5K@240 HDR SudoVDA):
Step 1 — the per-frame format-change check (
|
||
|
|
995db69387 |
fix(host/windows): detect format/size change on the DDA acquire path
DDA only re-read the duplication format/size on rebuild (recreate_dupl) and initial open. A mid-stream HDR<->SDR flip (FP16<->BGRA — e.g. the SudoVDA output dropping out of HDR for the secure desktop) or a resolution change that does NOT raise ACCESS_LOST left hdr_fp16/width/height stale, so present_acquired copied into a mismatched-format/size target — the secure-desktop "works once, then HDR breaks" symptom. Re-read the acquired texture's desc every frame (as Apollo does) and rebuild on a real change instead of presenting a mismatched frame; throttled like the ACCESS_LOST path so a flapping toggle can't hammer DuplicateOutput. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
3d04ce92a1 |
feat(host/windows): PUNKTFUNK_NO_WGC — force single-process DDA everywhere
A single test flag to bring up / validate DDA on its own and as the base for the secure-desktop work. When set it (1) skips WGC in capture_virtual_output (forces dxgi::DuplCapturer, same as PUNKTFUNK_CAPTURE=dda) and (2) makes should_use_helper return false, so even a SYSTEM host bypasses the two-process WGC relay and captures in-process with one DDA capturer for both the normal AND the secure desktop — Apollo's model. All the WGC / relay code stays compiled; unset the flag to restore. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
6ea52b0372 |
feat(host/windows): SDR-while-secure — drop SudoVDA out of HDR on Winlogon so DDA captures it
When the DDA-on-secure path is enabled (PUNKTFUNK_SECURE_DDA=1), the mux now toggles the SudoVDA's advanced-color (HDR) state via the CCD API (sudovda::set_advanced_color → DisplayConfigSetDeviceInfo + DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE): on entering the secure (Winlogon) desktop it disables HDR so the lock/UAC renders SDR/composed (no fullscreen independent-flip → DDA can duplicate it instead of storming ACCESS_LOST/black), opens DDA fresh on the now-SDR output; on returning to normal it re-enables HDR and rebuilds the helper so WGC re-detects the restored colorspace. Also debounce the DesktopWatcher (publish a Default↔Winlogon change only after it is stable ~80ms) so transient flaps during the transition don't thrash the mux. Default (no flag) is unchanged: WGC stays live through a lock, no DDA switch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
55d5a4278f |
fix(host): self-heal capture loss + audio-thread death mid-session
Two steady-state faults previously bubbled a bare `?` to conn.close / silently muted the rest of a session. Recover in place instead. #4 — capture loss (virtual_stream): a mid-session capture stall/disconnect (`try_latest` Err: PipeWire/compositor thread ended, virtual output gone) ended the whole session — and the native client has no reconnect path, so it had to cold-restart the handshake. Now rebuild the pipeline IN PLACE at the current mode via build_pipeline_with_retry (same primitive the mode/session switch uses), force a keyframe, and only propagate when the bounded retry is exhausted. A consecutive-rebuild cap stops a flapping source from looping the client through endless cold IDRs. Track the live mode so a rebuild after a mode switch targets the right mode (also fixes the session-switch rebuild using the stale mode). #3 — native audio thread (audio_thread): broke the loop on ANY next_chunk Err, spawned once per session and never restarted, so a transient 5 s quiet-sink timeout permanently muted a multi-hour session. Make a quiet sink return an empty chunk (not an Err) in both backends so only a genuinely dead capture thread is an Err, and reopen-with-backoff (INJECTOR_REOPEN_BACKOFF) on death, keeping the Opus encoder + monotonic seq. Documents the next_chunk contract; also makes the GameStream audio sender survive quiet sinks for free. Resolves reliability backlog #3 and #4. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
e8619c2362 |
fix(host/windows): keep WGC through the secure desktop by default (DDA-secure opt-in)
apple / swift (push) Successful in 56s
ci / rust (push) Failing after 1m32s
ci / web (push) Successful in 29s
android / android (push) Successful in 3m15s
ci / docs-site (push) Successful in 41s
deb / build-publish (push) Successful in 2m5s
decky / build-publish (push) Successful in 11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
ci / bench (push) Successful in 4m47s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m2s
docker / deploy-docs (push) Successful in 37s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m6s
Regression fix. The DDA-on-secure mux + force-composed overlay + rebuild-on-switch
made the stream worse than just staying on WGC: DDA can't reliably capture the
secure desktop's HDR independent-flip (storms ACCESS_LOST → instant black), and
rebuilding the output on every Default↔Winlogon flip thrashed (frequent freezes).
Meanwhile the WGC helper STAYS LIVE through a lock/UAC.
So make the DDA-on-secure path OPT-IN (PUNKTFUNK_SECURE_DDA=1, or the test
toggle). By default the mux keeps WGC the whole session — the DesktopWatcher and
the force-composed overlay aren't even started — so a lock/UAC no longer black-
screens or freezes the stream. The DDA-secure machinery stays in the tree for
future experimentation behind the flag.
(Reverts the rebuild-on-every-switch change
|
||
|
|
555ec2a3b7 |
Revert "fix(host/windows): rebuild the output fresh on every WGC↔DDA source switch"
This reverts commit
|
||
|
|
3f191ba2ea |
fix(host/windows): rebuild the output fresh on every WGC↔DDA source switch
Key insight (from the user): a fresh RECONNECT shows the secure desktop but the live transition does not — so the difference is what a fresh session does that the live switch skipped. A reconnect runs build() = REMOVE + fresh ADD of the SudoVDA monitor + re-isolate + a fresh capturer; the live transition instead reused the session-start output (created while on the NORMAL desktop), which goes born-lost (ACCESS_LOST storm → black) on the secure desktop. Fix: virtual_stream_relay now calls build() on EVERY source switch (both WGC→DDA and DDA→WGC), then opens DDA on the new target for secure / uses the fresh helper for normal. This makes each transition equivalent to the reconnect that works — fixing both the WGC→DDA cutover (secure desktop now in the clean output state DDA can duplicate) and the DDA→WGC cutover (a fresh helper's first frame is its opening IDR, so await_idr clears immediately instead of waiting on a wedged helper). Costs a ~1-2s rebuild per transition, acceptable for UAC/lock events. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ef4786387e |
feat(host/windows): force-composed-flip overlay to capture the secure desktop
The secure (Winlogon: UAC/lock/login) desktop presents via fullscreen independent-flip/MPO — it scans out bypassing DWM composition, so DXGI Desktop Duplication returns born-lost DXGI_ERROR_ACCESS_LOST (the client sees black; the UAC only "flashes" during the brief composed transition). Confirmed live: stable 4090 LUID across the storm (NOT reparenting) on an FP16 HDR output, recovering only when the screen changes. Fix (non-input, no system-wide registry change): capture/composed_flip.rs keeps a tiny click-through near-invisible TOPMOST LAYERED window alive on the current input desktop. Any visible window on the output disqualifies independent-flip → DWM composites → DDA can capture. A dedicated thread follows the input desktop (Default↔Winlogon) and recreates the window there on each switch (a window is bound to its desktop), re-asserting topmost + pumping messages every 200ms. Started for the two-process stream's lifetime; gated by PUNKTFUNK_FORCE_COMPOSED (default on, =0 to disable). Needs GENERIC_ALL on OpenInputDesktop for DESKTOP_CREATEWINDOW (0x80070005 otherwise). Validated: overlay creates on the Default desktop; live lock test pending. Also includes SET_RENDER_ADAPTER (sudovda.rs, Apollo item #16): pins the IDD render GPU to the NVENC GPU before ADD — issued + accepted live, though the secure-desktop storm was proven to be independent-flip (stable LUID), not reparenting, so it's correctness/hygiene here rather than this bug's fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0324719b6e |
feat(host/windows): USO batched send for the GameStream video plane
The GameStream video sender did one send() syscall per packet on Windows (the #[cfg(not(target_os="linux"))] sendmmsg_all fallback), capping throughput at high packet rates. Wire it to UDP Send Offload (the Windows analogue of Linux GSO) so each paced 16-packet burst goes out in one WSASendMsg(UDP_SEND_MSG_SIZE) syscall instead of 16, preserving the microburst pacing. Expose a reusable punktfunk_core::transport::send_uso_all (Windows-only) that reuses the proven native-plane USO primitive (send_one_uso + the uso on/off latch + uso_unsupported), with the same uniform-size guard and ≤512-segment chunking as UdpTransport::send_gso. It returns how many leading packets it sent via USO; the GameStream sendmmsg_all sends any remainder (USO off via PUNKTFUNK_GSO=0, a size-mixed burst, or a frame's short final packet) with per-packet send. On-wire packet boundaries are unchanged. Resolves #4 in docs/apollo-comparison.md. Linux build unaffected; punktfunk-core type-checks for x86_64-pc-windows-msvc. Host Windows compile deferred to CI / dev box. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
6d7301ccf5 |
fix(windows): two-pass cursor compositing (alpha + XOR) in DXGI capture
A single DXGI cursor shape can need BOTH an alpha-blended layer AND a screen-inverting (XOR) layer at once — a masked-color text I-beam (opaque hot-spot + inverting bar) or a monochrome cursor mixing opaque and invert pixels. The old path produced ONE BGRA image per shape and picked ONE blend (cursor_invert) for the whole shape, so such mixed cursors rendered wrong (masked-color opaque pixels forced through the invert blend; monochrome (AND=1,XOR=1) invert pixels approximated as solid black). Port Apollo/Sunshine's decomposition: convert_pointer_shape now returns a CursorShape with optional alpha/xor layers; CursorCompositor holds tex_alpha + tex_xor and draw_layer renders each with its own blend (alpha = src-over, HDR-scaled; XOR = inversion, unscaled — it operates on the framebuffer reference). The CPU software path blends both layers too. Empty layers are never uploaded or drawn. Removes the single cursor_invert flag. Fixes #13 in docs/apollo-comparison.md. Independently reviewed (ship); Windows-only code — compile verified by CI / dev VM. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
7bf2899301 |
fix(host/windows): secure-desktop black screen — capture the real frame, don't seed black
apple / swift (push) Successful in 56s
android / android (push) Failing after 54s
ci / web (push) Successful in 39s
ci / docs-site (push) Successful in 31s
ci / rust (push) Failing after 2m15s
deb / build-publish (push) Successful in 2m4s
decky / build-publish (push) Successful in 12s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
ci / bench (push) Successful in 4m52s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 4m11s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 3m29s
docker / deploy-docs (push) Failing after 6s
Root cause (confirmed live: "black until I pressed a key, then the image came back"): the secure desktop (lock/login/UAC) is STATIC, and DXGI Desktop Duplication only emits a frame on CHANGE. On the normal→secure switch the duplication is rebuilt (recreate_dupl / try_reduplicate), and we then SEEDED A BLACK frame as last_present — which the static secure desktop never replaced (no change-frame) until the user pressed a key. So we streamed black. Fix: after rebuilding the duplication, CAPTURE the current desktop frame instead of seeding black. A freshly-created duplication's first AcquireNextFrame returns the full current desktop; grab it and present it. New `present_acquired` factors the frame-processing out of `acquire`; both recovery paths now call it: - recreate_dupl: after adopting the new duplication, acquire+present the real frame (born-lost ACCESS_LOST / no-initial-frame → seed black as fallback and let the 250ms-throttled caller retry — a brief flash, then real content). - try_reduplicate: adopt-first, then capture its probe frame (was discarded). Also (independently-correct safe fixes, per the adversarial review): - DesktopWatcher computes the current desktop synchronously in start() before returning, so a session that begins on the secure desktop (reconnect to a locked box) doesn't relay one stale normal-desktop frame (the "flash"). - DuplCapturer::open reasserts SudoVDA isolation at open time (mirrors recreate_dupl) — forces the secure desktop back onto the virtual output if a lock/UAC re-attached a physical monitor. - Instrumentation: dbg_black_seeds counter + a throttled warn when black is seeded, and an info when a real secure-desktop frame is captured on recovery. Pending: the user's real-lock smoke test on the 4090 (a headless PsExec LockWorkStation runs as SYSTEM and can't lock an interactive session, so this must be validated with an actual lock). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ec2907fc32 |
perf(host/windows): SendInput retry-on-failure model (two-process step 2)
apple / swift (push) Successful in 54s
android / android (push) Failing after 0s
ci / rust (push) Failing after 0s
ci / docs-site (push) Failing after 0s
ci / bench (push) Failing after 0s
deb / build-publish (push) Failing after 0s
ci / web (push) Failing after 1s
decky / build-publish (push) Failing after 0s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Failing after 1s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Failing after 0s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 1s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Failing after 0s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 1s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 0s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Failing after 0s
docker / deploy-docs (push) Has been skipped
The injector reattached the input desktop (OpenInputDesktop + SetThreadDesktop, two syscalls) before EVERY event. Now it stays bound to its desktop and only reattaches on a SendInput short write (the input desktop switched into UAC/lock) + retries once — Sunshine's model. No steady-state per-event overhead; still follows the desktop across the secure boundary, serving both desktops. Validated on the RTX 4090 (host as SYSTEM): client-rs --input-test injected for ~6s with no "blocked desktop" errors. Completes all 6 steps of the two-process secure-desktop build; only a real-UAC user smoke test remains. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1b68890dbf |
feat(host/windows): two-process step 6 — helper relaunch watchdog
A WGC-helper exit (crash, or a console disconnect killing its session) used to
end the stream. Now virtual_stream_relay rebuilds the output + helper and resumes
on the new helper's opening IDR. Rebuild — not respawn-on-the-old-target —
because an abruptly-killed helper leaves the SudoVDA's DXGI output briefly
unresolvable ("no DXGI output for target N yet"), and a console reconnect needs
a fresh output in the new session; `build` (the same path reconfigure uses)
recreates both. Bounded: 500ms backoff per attempt, give up after
MAX_HELPER_FAILS (20) consecutive failures; the counter resets on the first
relayed frame.
Live-validated on the RTX 4090 (host as SYSTEM): force-killed the helper PID
mid-stream → exactly one "WGC helper exited — rebuilt output + helper fails=1" →
the stream recovered and client-rs decoded 645 HEVC Main-10 frames continuously
across the kill (an earlier respawn-on-stale-target attempt storm-failed with
"no DXGI output", which the rebuild fixes).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
4edfcd4b43 |
feat(host/windows): two-process mux test toggle + live-validate step 5
PUNKTFUNK_SECURE_TEST_PERIOD_MS=N drives a square-wave secure/normal toggle in
virtual_stream_relay (instead of the real DesktopWatcher), to exercise the
mid-session helper↔DDA mux without a live UAC/lock. Gated behind the env var,
in the style of PUNKTFUNK_VIDEO_DROP / PUNKTFUNK_FEC_PCT.
Live-validated on the RTX 4090 (host as SYSTEM): with a 4s toggle the mux
switched secure(DDA)↔normal(WGC relay) cleanly 5× in one session and the client
decoded 308 HEVC Main-10 frames continuously across every switch — the
wait-for-IDR latch held with no decode break. The real Winlogon DDA capture is
pre-proven by the single-process secure path (
|
||
|
|
8d6cbb81fe |
fix(host/windows): merge host PUNKTFUNK_* env into the WGC helper's environment
CreateProcessAsUserW gives the spawned helper the *user's* environment block, so the host's PUNKTFUNK_ENCODER=nvenc (and ZEROCOPY/PERF/…) were dropped and the helper fell back to the software (H.264-only) encoder — the client negotiated H265 → "WGC helper exited". `merged_env_block` now parses the user block, strips any PUNKTFUNK_* it carried, overlays this (host) process's PUNKTFUNK_* vars, and passes the merged UTF-16 block. Validated live on the RTX 4090 (host as SYSTEM): the helper spawns via CreateProcessAsUserW, runs WGC with no hang (HDR FP16 BT.2020 PQ), opens NVENC (D3D11 Main10), and relays AUs over the pipe — client-rs decoded 411 HEVC Main-10 frames over the LAN. Step 4 (spawn + relay) complete. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
140209bbfc |
feat(host/windows): two-process secure-desktop step 5 — DDA mux on Winlogon
`virtual_stream_relay` now muxes the AU source by input desktop. A DesktopWatcher (SYSTEM-only Winlogon-name poll) drives it: the user-session WGC helper relay feeds the normal (Default) desktop; the host's OWN DDA capturer+encoder — opened lazily on the first secure transition, on the same SudoVDA target with a no-op keepalive (the host still holds the real isolation owner) — captures the secure (Winlogon: UAC/lock/login) desktop that WGC can't see. Every switch latches "wait for IDR" and forces the now-active source to emit a keyframe (the two encoders keep independent infinite-GOP state, so the client must resume on an IDR); returning to the helper also drains its stale buffered AUs first. Reconfigure drops the stale-target DDA; keyframe requests route to the live source. Send path (FEC/seal/paced-send) unchanged. Also: wgc_relay gains try_recv (drain on switch-back); open_dda takes dims as args (avoids a closure borrow of the reassigned cur_mode); the forward! macro returns bool with `break 'outer` at the call site (no in-macro label hygiene). cfg-gated windows-only. Live validation (UAC switch over a session) pending. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9f50b3930d |
feat(host/windows): two-process secure-desktop step 4 — spawn helper + relay AUs
The SYSTEM host now sources the normal-desktop video from a user-session WGC helper instead of capturing in-process (WGC won't activate as SYSTEM). New `capture/wgc_relay.rs`: `HelperRelay::spawn` launches `m3-host wgc-helper` in the interactive user session via CreateProcessAsUserW (WTSQueryUserToken → DuplicateTokenEx(TokenPrimary) → lpDesktop="winsta0\\default", CREATE_NO_WINDOW) with three anonymous pipes — stdout (framed Annex-B AUs → parsed back to RelayAu), stdin (control: force-keyframe), stderr (helper logs → host tracing). The host holds the SudoVDA keepalive (sole isolation/topology owner); the helper captures by GDI name only. m3.rs: `virtual_stream` dispatches to the new `virtual_stream_relay` when `should_use_helper()` (running as SYSTEM, or PUNKTFUNK_FORCE_HELPER; disable with PUNKTFUNK_NO_HELPER). The relay loop feeds the existing send thread — same FEC/seal/paced-send path. Reconfigure rebuilds the output + re-spawns the helper; keyframe requests forward over the control pipe; helper pts_ns (same-machine monotonic clock) is used directly as capture_ns. Disconnect ends the stream (step 6 adds the relaunch watchdog). wgc_helper.rs: reads the stdin control byte to request an IDR; --bit-depth flag threaded through so SDR 10-bit (Main10) negotiation reaches the helper's encoder. cfg-gated windows-only; Linux/macOS build unaffected. Step 5 (DesktopWatcher mux to host DDA on the Winlogon secure desktop) is next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a0f6cddc70 |
feat(host/windows): WGC helper subcommand (two-process secure-desktop, step 3)
`m3-host wgc-helper --target-id N --gdi NAME --mode WxHxHz --bitrate K`: the USER-session half of the two-process secure-desktop design (docs/windows-secure-desktop.md). Opens WGC on the EXISTING SudoVDA output by GDI name only (never creates a virtual output — a second topology owner re-trips the ACCESS_LOST born-lost storm), encodes via NVENC, and ships framed Annex-B AUs on stdout for the SYSTEM host to relay onto the live QUIC session: `[u32 magic "PFAU"][u32 len][u64 pts_ns][u8 keyframe][data]`. tracing → stderr so stdout stays the pure AU stream. cfg-gated windows-only; Linux build unaffected. scripts/headless/win-build.cmd: the canonical box build script (sets PUNKTFUNK_BUILD_VERSION so build.rs stamps the version + the NVENC LIB path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
80e222d3b8 |
feat(host/windows): DesktopWatcher (secure-desktop detection) — step 1 of the two-process build
apple / swift (push) Successful in 53s
android / android (push) Has been cancelled
ci / web (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / bench (push) Has been cancelled
ci / rust (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (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.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
Polls the input-desktop name (OpenInputDesktop + GetUserObjectInformationW(UOI_NAME)) on its own thread → Default/Winlogon atomic; the authoritative normal-vs-secure signal for the capture mux + input path (WTS notifications miss UAC). Not yet wired into the mux (needs the SYSTEM host + WGC helper, steps 3-5 in docs/windows-secure-desktop.md). NOTE: detecting the secure desktop requires the host to run as SYSTEM (a user-token process can't OpenInputDesktop the Winlogon desktop). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5c2bcbc2a2 |
docs(windows): secure-desktop two-process design + WGC impersonation attempt (vestigial)
apple / swift (push) Successful in 55s
android / android (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / web (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / bench (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (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 (., web/Dockerfile, punktfunk-web) (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.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
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
Validated design for adding secure-desktop (UAC/lock/login) coverage on top of the shipped WGC animation fix. Key verified constraint: WGC won't activate under SYSTEM (0x80070424) even with thread-level ImpersonateLoggedOnUser, and DDA+SendInput on Winlogon need LOCAL_SYSTEM — so one process can't do both. Architecture: SYSTEM host (QUIC + SudoVDA + DDA-secure + SendInput + AU mux) + a USER-session WGC helper (CreateProcessAsUser) that relays encoded Annex-B AUs over a named pipe; the host muxes helper-AUs (normal desktop) vs its own DDA encoder (secure desktop), switched by a desktop-name watcher. No shared GPU texture (rejected — MIC/keyed-mutex pain); just AU bytes. docs/windows-secure-desktop.md has the ordered, box-testable steps. The impersonate_active_user() in wgc.rs is kept as a harmless no-op (under a user-token process WTSQueryUserToken fails → no impersonation → WGC works natively); it does NOT make WGC work under SYSTEM (the two-process design uses a real user process for WGC instead). + Win32_System_RemoteDesktop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
28ab448a29 |
feat(host/windows): WGC capture backend (overlay/HDR-correct) with watchdog'd DDA fallback
android / android (push) Failing after 46s
apple / swift (push) Successful in 54s
ci / rust (push) Failing after 1m16s
ci / web (push) Successful in 31s
ci / docs-site (push) Successful in 27s
deb / build-publish (push) Successful in 2m23s
decky / build-publish (push) Successful in 10s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
ci / bench (push) Successful in 4m31s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m15s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 7m50s
The capture-architecture reset from the research: add a Windows.Graphics.Capture (WGC) backend that captures the COMPOSED desktop — including the overlay/independent-flip/MPO planes DXGI Desktop Duplication misses — which structurally fixes the frozen HDR animations + video (proven live: a WGC frame decodes to the real 5120x1440 HDR content DDA freezes on). It reuses the whole pipeline unchanged: the WGC frame's GPU texture → same scRGB→BT.2020-PQ shader → NVENC zero-copy; the OS composites the cursor (IsCursorCaptureEnabled) so no manual cursor pass. crates/punktfunk-host/src/ capture/wgc.rs; find_output/make_device/HdrConverter/nudge_cursor_onto made pub(crate) for reuse. Reliability findings + mitigations (live on the RTX 4090): - WGC can't activate under the SYSTEM account (0x80070424) — it needs the interactive user token. The host must run as the user for WGC (run.cmd: drop PsExec -s). DDA still needs SYSTEM for the secure desktop — that token reconciliation (impersonation) is the remaining task. - WGC's Direct3D11CaptureFramePool::CreateFreeThreaded intermittently HANGS on the headless SudoVDA (IddCx) display, correlated with accumulated SudoVDA churn (failed REMOVEs leaving lingering displays); clean-state opens reliably. Since it's a blocking hang, capture_virtual_output runs WGC open on a watchdog thread with a 5s timeout and falls back to DDA on hang/error — the session is NEVER left black: WGC when it opens (fixed animations), DDA otherwise. First-frame nudge added (WGC fires FrameArrived on change; a static desktop otherwise never delivers the first frame). - Default WGC; PUNKTFUNK_CAPTURE=dda forces DDA. DDA path unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b9f4cf1f3e |
fix(host/windows): don't 2-way-split-encode Main10 — it's SLOWER on Ada (fixes broken HDR animations)
apple / swift (push) Successful in 53s
audit / cargo-audit (push) Failing after 1m9s
android / android (push) Successful in 2m3s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
ci / web (push) Successful in 29s
ci / docs-site (push) Successful in 29s
ci / bench (push) Successful in 1m31s
ci / rust (push) Successful in 4m26s
decky / build-publish (push) Successful in 11s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
flatpak / build-publish (push) Successful in 3m34s
deb / build-publish (push) Successful in 6m55s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m25s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m10s
The "broken animations in HDR" was an encode-throughput cliff, not the ACCESS_LOST churn. Measured at 5120x1440@240 HEVC Main10 on the RTX 4090: forced 2-way split-encode = 7.6 ms/frame (~131 fps, well over the 4.17 ms/240fps budget → choppy), while SINGLE engine = 2.8-3.9 ms/frame (~256-357 fps, fits 240). The split/merge overhead dominates for 10-bit; a single Ada NVENC engine already handles 5K@240 Main10 comfortably. So the split decision now forces DISABLE for Main10 (bit_depth >= 10), keeping the existing forced-2 only for 8-bit above 1 Gpix/s. PUNKTFUNK_SPLIT_ENCODE still overrides. Added a split-mode log line. Validated live on the 4090: encode_us_p50 7.6 ms → 3.9 ms at 5K240 HDR with no env override. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b1e95a386f |
fix(host/windows): tiered DXGI recovery — cheap re-DuplicateOutput for the HDR ACCESS_LOST churn
apple / swift (push) Successful in 53s
ci / web (push) Successful in 28s
android / android (push) Successful in 1m46s
ci / docs-site (push) Successful in 30s
ci / bench (push) Successful in 1m49s
decky / build-publish (push) Successful in 11s
ci / rust (push) Successful in 1m4s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
deb / build-publish (push) Successful in 3m24s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m17s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m56s
The HDR path produced a constant ACCESS_LOST churn during real desktop activity (window resize / Start menu / DWM transitions): the duplication keeps getting invalidated but the OUTPUT stays valid (probe passes — 0 born-lost over 72 rebuilds). The old recovery did a FULL rebuild (new device + factory) on every loss, which re-inits NVENC + seeds black + was throttled to 4x/s → mostly-frozen, re-init churn = "broken animations". Now recovery is tiered (mirrors Sunshine): try_reduplicate() does a fresh DuplicateOutput on the EXISTING device+output — no new device, so NO encoder re-init, NO black seed, gpu_copy/HDR textures/last_present kept → frames resume immediately. Only a genuine output loss (secure-desktop switch) or a dead device (DEVICE_REMOVED/RESET) falls back to the full, throttled recreate_dupl. Both paths probe the new duplication and reject a born-lost one. Validated synthetically (1080p60 + 5120x1440@240 HDR): pipeline stable, 0 churn, frames flow. The real-desktop churn needs live validation (can't synthesize DWM animations). Secure-desktop "UI never appears in-session" is a separate issue (output gone in-session; only a fresh monitor re-add works) — still open. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0a3b92d994 |
fix(host/windows): HDR cursor brightness (203-nit) + probe-before-adopt recovery; windows-client bootstrap doc
apple / swift (push) Successful in 55s
android / android (push) Successful in 2m43s
ci / web (push) Successful in 31s
ci / docs-site (push) Successful in 37s
ci / bench (push) Successful in 1m35s
ci / rust (push) Successful in 7m7s
decky / build-publish (push) Successful in 11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
deb / build-publish (push) Successful in 2m18s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m33s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m33s
docker / deploy-docs (push) Successful in 18s
- HDR cursor: sRGB→linear decode + scale to HDR graphics white (PUNKTFUNK_HDR_CURSOR_NITS, default 203 per BT.2408) in the FP16 cursor composite, so it's no longer ~2.5x too dim. SDR path unchanged; the masked-color (I-beam) inversion blend left unscaled. Cursor cbuffer widened 16→32 + bound to PS. (Validated live: cursor now correct brightness in HDR.) - Secure-desktop recovery: recreate_dupl now PROBES the rebuilt duplication with a 50ms AcquireNextFrame and only adopts it when live (Ok/WAIT_TIMEOUT); a born-lost one (immediate ACCESS_LOST) is dropped so the caller repeats the last frame + retries. Plus reassert_isolation() re-detaches physical displays on every recovery (re-routing the secure/HDR desktop to the virtual output, the delta a fresh reconnect has). NOTE: the born-lost ACCESS_LOST storm in HDR is NOT yet resolved by these — still under investigation (animations/secure-UI/cursor-trail in HDR remain). - docs/windows-client-bootstrap.md: handoff for the native Windows Rust client (windows-rs Reactor + WinUI 3 SwapChainPanel, D3D11VA decode, WASAPI audio, SDL3 input; ports crates/punktfunk-client-linux; 10-bit/HDR present; dev boxes + gotchas). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bbabc04bca |
feat(hdr): Windows HDR10 + 10-bit end-to-end, negotiated; non-blocking capture recovery
apple / swift (push) Successful in 54s
ci / rust (push) Successful in 1m32s
android / android (push) Successful in 1m49s
ci / web (push) Successful in 26s
ci / docs-site (push) Successful in 30s
ci / bench (push) Successful in 1m36s
decky / build-publish (push) Successful in 12s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
deb / build-publish (push) Successful in 2m20s
flatpak / build-publish (push) Successful in 4m6s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m11s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m32s
Adds true HDR (BT.2020 PQ) and 10-bit (HEVC Main10) streaming, negotiated so an 8-bit/SDR client is never sent a stream it can't decode, plus a robust fix for the capture losing the stream across a secure-desktop transition. Protocol (punktfunk-core/quic.rs): - Hello gains `video_caps` (VIDEO_CAP_10BIT / VIDEO_CAP_HDR), Welcome gains `bit_depth`, both as optional trailing bytes (back-compat). client-rs advertises 10-bit via PUNKTFUNK_CLIENT_10BIT; the connector advertises 0 for now (in-band detection drives the native clients). Regenerated punktfunk_core.h. Windows host: - 10-bit Main10: host enables it only when the client advertised VIDEO_CAP_10BIT AND PUNKTFUNK_10BIT is set; threaded through open_video → NVENC (profile Main10, pixelBitDepthMinus8). - HDR: when the captured desktop is scRGB FP16 (R16G16B16A16_FLOAT, HDR on), copy it to an FP16 surface, composite the cursor there, convert scRGB → BT.2020 PQ 10-bit (R10G10B10A2) via a shader, and encode HEVC Main10 with the BT.2020/PQ colour VUI (ABGR10 input). Fixes the freeze + cursor-trail that came from feeding FP16 into the BGRA path. Reacts dynamically to the HDR toggle. - Capture recovery: rebuild is now a single NON-BLOCKING attempt, throttled to ~4×/s, repeating the last good frame between attempts (format-tagged last_present). During a secure-desktop dwell SudoVDA's output is gone; the old blocking 12 s retry starved the send loop for seconds so the client timed out and disconnected — now the session stays fed (frozen) until the desktop returns. Also seeds a black frame on recovery. Apple client (PunktfunkKit): - Detects HDR in-band from the stream VUI (PQ transfer function), decodes to 10-bit P010, and presents via an rgba16Float + BT.2020 PQ CAMetalLayer with EDR; SDR path unchanged. Switches automatically on a mid-session HDR toggle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
26fbd9ec64 |
perf(host/windows): zero-copy NVENC — encode the capturer's texture in place (halve 3D-engine load)
ci / rust (push) Failing after 43s
apple / swift (push) Successful in 53s
ci / web (push) Successful in 35s
android / android (push) Successful in 1m45s
ci / docs-site (push) Successful in 29s
ci / bench (push) Successful in 1m35s
decky / build-publish (push) Successful in 32s
deb / build-publish (push) Successful in 2m21s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 17s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m59s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3m52s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 21s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m37s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m37s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m4s
docker / deploy-docs (push) Successful in 18s
The Windows host pegged the GPU 3D engine at ~97% during high-fps desktop streaming — measured (per- process GPU-engine counters) as OUR process, not DWM. Cause: TWO VRAM->VRAM CopyResource per frame (dupl->gpu_copy in the capturer, then gpu_copy->nvenc_pool in the encoder), and on Windows D3D11 routes copies to render-target textures through the 3D engine (the DMA copy engine sat idle at 7%), so at 240 fps they saturate it and contend with a game's own rendering. Eliminate the second copy: NVENC now registers the capturer's D3D11 texture directly (cached by raw pointer, the cloned texture kept alive until unregister) and encode_pictures it IN PLACE — no encoder-owned input pool, no per-frame copy. Safe because the host encode loop is synchronous (capture -> submit -> poll, where lock_bitstream blocks until the encode finishes), so the capturer never overwrites the texture mid-encode; documented in the module header in case that ever changes. 2 GPU copies/frame -> 1 (the remaining dupl->gpu_copy is unavoidable; that DXGI surface is transient). Measured: SM/compute ~10-15% at ~217 fps 5K (was ~20% at only ~48 fps with two copies), 3687 frames decoded clean. Windows-only; Linux/macOS unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c830246037 |
feat(host/windows): UDP send offload + NVENC 2-way split-encode (1 Gbps+ / 5K@240)
apple / swift (push) Successful in 53s
audit / cargo-audit (push) Failing after 1m7s
ci / rust (push) Failing after 40s
android / android (push) Successful in 2m11s
ci / web (push) Successful in 29s
ci / docs-site (push) Successful in 30s
ci / bench (push) Successful in 1m49s
decky / build-publish (push) Successful in 11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
flatpak / build-publish (push) Successful in 3m42s
deb / build-publish (push) Successful in 6m58s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m30s
docker / deploy-docs (push) Successful in 30s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m10s
The Windows host couldn't sustain high-throughput / high-fps streams — two gaps vs the Linux host, both found via live RTX 4090 measurement (PERF timing + nvidia-smi per-engine attribution): - UDP Send Offload (USO). punktfunk-core's UdpTransport sent one packet per `send` syscall on Windows (send_batch/send_gso were Linux-only), capping throughput at high packet rates. Add a Windows `send_gso` override using `WSASendMsg` + `UDP_SEND_MSG_SIZE` (the Windows analogue of Linux UDP GSO) via windows-sys — one syscall segments a coalesced <=512-segment super-buffer to the connected peer. On by default with auto-fallback (PUNKTFUNK_GSO=0 disables, error latches off); plugs into the existing paced send path. SO_SNDBUF (32MB) was already cross-platform. - NVENC 2-way split-frame encoding. A single Ada NVENC session tops out ~0.8 Gpix/s, so 5K@240 (1.77 Gpix/s) took ~8 ms/frame -> a ~125 fps ceiling at high motion (the in-game stutter). Set NV_ENC_INITIALIZE_PARAMS.splitEncodeMode = TWO_FORCED above ~1 Gpix/s (matching the Linux libavcodec split_encode_mode path) to use both 4090 encoders — measured ~8 ms -> ~4 ms/frame at throughput. Env override PUNKTFUNK_SPLIT_ENCODE; init-failure fallback disables it (e.g. H264). Windows-only paths; Linux/macOS unaffected. Builds clean on x86_64-pc-windows-msvc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f4b4a6c1e4 |
feat(host/windows): native res, cursor, secure-desktop capture, windowless SYSTEM launch
apple / swift (push) Successful in 52s
ci / rust (push) Failing after 36s
ci / web (push) Successful in 31s
android / android (push) Successful in 1m52s
ci / docs-site (push) Successful in 29s
ci / bench (push) Successful in 1m39s
decky / build-publish (push) Successful in 11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 4s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
deb / build-publish (push) Successful in 3m19s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m15s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m57s
docker / deploy-docs (push) Successful in 17s
Live-validated Mac <-> RTX 4090 at the display's native 5120x1440@240: - Resolution: set_active_mode enumerates the IDD's advertised modes and sets the requested resolution at the best supported refresh (keeps 5120x1440@240; no more silent fallback to the 1080p OS default when an exact mode is briefly unavailable). - Bitrate auto-cap: NVENC init probes and steps the average bitrate down to the GPU's codec-level max so a high client bitrate connects (matches the Linux host; we do not split NVENC sessions). - Mouse cursor: DXGI duplication excludes the HW cursor; capture the pointer shape/position (GetFramePointerShape) and GPU-composite it before NVENC. Color cursors alpha-blend; masked-color (the text I-beam) uses an INV_DEST_COLOR inversion blend so the caret inverts the screen and shows on any background (no black box); monochrome handled too. - Secure desktop (lock / login / UAC): run as SYSTEM in the interactive session, follow the input desktop via SetThreadDesktop, and on the WinSta switch recreate the D3D11 device and re-resolve the virtual output's GDI name from the stable SudoVDA target id (the name changes across the topology rebuild; the old failure hunted the stale \\.\DISPLAYn and dropped). ACCESS_LOST / INVALID_CALL / device-removed are recoverable, and a mid-stream resolution change is followed (capturer + NVENC re-init at the new size). isolate_displays detaches other monitors so Winlogon renders to the virtual output. One real session recovered 1012 desktop switches and completed cleanly. Windows-only backends; Linux/macOS unaffected. Builds clean on x86_64-pc-windows-msvc. Deployment (windowless SYSTEM launch via PsExec + hidden VBScript) documented in docs/windows-host.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8ab262f8f8 |
feat(trust): host-gated trust-on-first-use — PIN pairing mandatory by default
apple / swift (push) Successful in 54s
ci / rust (push) Failing after 1m12s
ci / web (push) Successful in 29s
android / android (push) Failing after 1m49s
ci / docs-site (push) Successful in 31s
ci / bench (push) Successful in 1m48s
decky / build-publish (push) Successful in 12s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 19s
flatpak / build-publish (push) Failing after 3s
deb / build-publish (push) Failing after 2m43s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m22s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m20s
TOFU let anyone who could reach the host click "Trust" and stream, which defeats the point on a LAN. Make SPAKE2 PIN pairing the default and only way to trust a NEW host; TOFU survives as an explicit HOST opt-in (for fully trusted networks), advertised over mDNS so clients render their trust UI from the host's policy rather than offering trust on faith. Contract: - Host advertises pair=required (default) or pair=optional. pair=required rejects unpaired clients at the handshake; pair=optional accepts them (TOFU). - Clients: a pinned host whose fingerprint matches connects silently; a pinned host whose fingerprint CHANGED forces re-pairing via PIN (no re-trust shortcut); a NEW host is offered TOFU only if it advertised pair=optional, otherwise PIN pairing is mandatory; a manually-typed or unknown-policy host is always PIN. Host (crates/punktfunk-host/src/main.rs): - m3-host now REQUIRES pairing by default (was open by default). New --allow-tofu opts into accepting unpaired clients + advertising pair=optional; pairing is always armed (PIN logged at startup). serve --native was already secure-by-default (serve --open). The mDNS advert and the accept loop already mapped require_pairing -> pair=required + reject; only the m3-host CLI default + help text changed. Clients honor the advertised policy: - Android (MainActivity.kt): TOFU only for a discovered pair=optional host; manual/unknown -> PIN; fp-change -> re-pair only (dropped the "Forget & re-TOFU" shortcut). - Apple (HostDiscovery/SessionModel/ContentView/HostCards/HostStore): new allowsTofu (pair==optional, distinct from unknown); connect() gates .awaitingTrust on it; unpinned non-optional hosts route to the PIN sheet; "Forget Identity" re-pairs rather than re-TOFUs. - Linux (app.rs/ui_hosts.rs/session.rs): ConnectRequest.pair_required -> pair_optional; initiate_connect routes pinned/fp-changed/optional/else; manual + --connect unknown -> PIN; a pinned connect rejected on trust grounds re-pairs. Docs (CLAUDE.md, README.md, docs-site/content/docs/pairing.md): describe the gated model — PIN is the default, TOFU an explicit opt-in with an impostor warning. Verified: host cargo check/clippy/fmt clean; Android built + live (emulator -> home-worker-2): a manual connect now opens the PIN dialog (no Trust button) and the PIN ceremony streams; Apple swift build clean; Linux clippy -D warnings + fmt clean on the Linux box. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fe9921cc1c |
fix(dist): kill the version-shadow + add build provenance (P0)
apple / swift (push) Successful in 53s
android / android (push) Failing after 2m8s
ci / web (push) Successful in 36s
ci / docs-site (push) Successful in 39s
ci / bench (push) Successful in 1m38s
ci / rust (push) Successful in 4m59s
decky / build-publish (push) Successful in 16s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
flatpak / build-publish (push) Failing after 2s
deb / build-publish (push) Failing after 2m58s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
docker / deploy-docs (push) Successful in 17s
The stale code a default install/upgrade got was a TAG LEAK: deb.yml/rpm.yml shared
`tags: ['v*']` with the Apple-client release.yml, so the v0.1.0/v0.1.1 tags cut to ship
the macOS app ALSO published host packages versioned 0.1.1 — which outranks every rolling
0.0.1~ciN / 0.0.1-0.ciN build in both registries (dpkg/rpm version compares confirm), so
`apt install`/`rpm-ostree install` silently fetched ~99-commits-stale code while the READMEs
claimed auto-tracking. Two fixes:
- Decouple host publishing from Apple `v*` tags: deb.yml/rpm.yml now trigger on `host-v*`
only, so a client tag can never poison the host channel again.
- Bump the rolling base 0.0.1 -> 0.2.0 (deb `0.2.0~ciN`, rpm `0.2.0-0.ciN`): sits ABOVE the
stray 0.1.1 yet BELOW a future 0.2.0 tag, and still climbs monotonically by run number — so
`apt upgrade`/`rpm-ostree upgrade` genuinely move forward. Spec default + build scripts +
PKGBUILD pkgver bumped to match.
Build provenance (so a stale/shadowed host is detectable): build.rs stamps PUNKTFUNK_BUILD_VERSION
(set by CI = the full package version, e.g. 0.2.0~ci120.g802e98d; falls back to the crate version
for a plain `cargo build`) into the binary via rustc-env. Surfaced in `punktfunk-host --version`,
the startup log, and the mgmt /health + /host `version` field (was a hardcoded CARGO_PKG_VERSION).
Deliberately env-driven, not git-derived — the RPM builds from a git-archive tarball with no .git.
Version computed BEFORE the build in deb.yml; the spec %build exports it from %{version}-%{release}
(and gains --locked for reproducibility parity with the .deb path). Validated: plain build reports
0.0.1, env-stamped build reports 0.2.0~ci999.gdeadbee.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
7654b20b2a |
fix(host/windows): NVENC capture on real GPU + HOME-less config dir
apple / swift (push) Successful in 54s
android / android (push) Failing after 1m44s
ci / rust (push) Successful in 1m18s
ci / web (push) Successful in 28s
ci / docs-site (push) Successful in 31s
ci / bench (push) Successful in 1m50s
decky / build-publish (push) Successful in 10s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 4s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 3s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
flatpak / build-publish (push) Failing after 2s
deb / build-publish (push) Failing after 2m56s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m4s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m48s
docker / deploy-docs (push) Successful in 17s
Validated live on an RTX 4090 (Windows 11) host streaming to the Rust reference client over the LAN: SudoVDA virtual display → DXGI Desktop Duplication (D3D11 zero-copy) → NVENC HEVC → punktfunk/1. 720p60 and 1080p60 both clean (181 / 177 frames, 0 mismatched, p50 1.6 / 3.45 ms cross-machine), coexisting with Apollo. Two real-hardware bugs the GPU-less VM couldn't surface: - DXGI capturer: the SudoVDA virtual monitor's DXGI output is enumerated under the GPU that *renders* it (the 4090, LUID 0x15df6), NOT under the SudoVDA "adapter" LUID SudoVDA reports (0x23276). Restricting the output search to that LUID found nothing → "adapter has no output named \\.\DISPLAYn". Now search ALL adapters for the GDI name, bind the D3D11 device to whichever adapter exposes it (NVENC then shares that device), with a settle-retry (the output appears a beat after display creation) and topology logging. - native_pairing / apps: keyed config paths off raw $HOME, which a Windows service/scheduled-task context doesn't set → "HOME unset" hard-fail at m3-host startup. Route both through gamestream::config_dir(), which falls back to %APPDATA% on Windows (cert/paired/apps now under AppData\Roaming). clippy -D warnings + build green on x86_64-pc-windows-msvc (default and --features nvenc) and Linux (78/78 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b2e5878711 |
feat(host/mgmt): HTTPS + token auth by default (no loopback no-auth fallback)
android / android (push) Failing after 21s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Failing after 0s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 1s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 1s
ci / rust (push) Failing after 2m27s
ci / web (push) Failing after 10s
ci / docs-site (push) Failing after 0s
ci / bench (push) Failing after 1s
deb / build-publish (push) Failing after 0s
decky / build-publish (push) Failing after 1s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Failing after 0s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Failing after 0s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Failing after 0s
docker / deploy-docs (push) Has been skipped
flatpak / build-publish (push) Failing after 0s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 0s
apple / swift (push) Successful in 53s
The mgmt API already always serves HTTPS (the host identity cert), but on a loopback bind with no token it ran unauthenticated — any local process could drive it. Make auth required ALWAYS: - new mgmt_token::load_or_generate(): token precedence is --mgmt-token > env PUNKTFUNK_MGMT_TOKEN > persisted ~/.config/punktfunk/mgmt-token > freshly generated 32-byte hex, persisted 0600 in KEY=VALUE form (so the bundled web console can source it directly as a systemd EnvironmentFile — one source of truth). config_dir() made pub(crate). - parse_serve() resolves the token via load_or_generate() when unset, so a bare `serve` Just Works with auth on and no operator step. - mgmt::run() drops the loopback no-token exemption and requires a token; require_auth()'s unauthenticated fallback now returns 401. The paired-cert (mTLS) branch is unchanged — Apple client + library auth unaffected. - web /api proxy: 503 (legible) instead of forwarding an empty bearer. - tests: test_app/test_app_native default a token, send() auto-attaches the bearer; blank-token test asserts the new "no token" refusal. 80 pass. - docs: mgmt module doc + host.env.example reflect always-on auth + auto-gen. Compiles, clippy/fmt clean, openapi no drift. Part B (bundle the web console into apt, auto-wired to this token) follows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2bca89c555 |
feat(host/windows): Steam library auto-discovery on Windows
apple / swift (push) Successful in 53s
android / android (push) Failing after 44s
ci / web (push) Successful in 40s
ci / docs-site (push) Successful in 32s
ci / rust (push) Failing after 2m28s
decky / build-publish (push) Successful in 44s
ci / bench (push) Failing after 1m22s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
flatpak / build-publish (push) Failing after 2s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 37s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 45s
deb / build-publish (push) Failing after 2m46s
docker / deploy-docs (push) Successful in 10s
The Steam `LibraryProvider` keyed off `$HOME` + Linux paths, so the game library was empty on Windows. Add Windows discovery: the default Steam install dirs under Program Files (`ProgramFiles(x86)`/`ProgramFiles`/ `ProgramW6432`), with games on other drives picked up via each root's `libraryfolders.vdf` — whose Windows values are backslash-escaped, so unescape `\\` → `\`. The existing root-scan/dedup logic is shared via a new `steam_roots_existing` helper. The custom store (mgmt JSON CRUD) was already cross-platform; only Steam auto-discovery was Linux-only. Not yet covered: a non-default Steam install dir (the registry `Valve\Steam\InstallPath`). Degrades gracefully — no Steam → empty list. clippy -D warnings + library tests green on Windows and Linux. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |