b24c10a723b8923beee64bce45694622c75dd6c2
580 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dc734c711b |
fix(host/windows): re-sync thread desktop on EVERY recovery (symmetric enter/leave secure)
User's observation: entering UAC/lock works instantly, but clicking OUT of it breaks (with the disconnect sound) — Apollo's enter and leave are symmetric. Root cause: attach_input_desktop() (SetThreadDesktop to the current input desktop) was gated behind is_secure_desktop() in recreate_dupl, so: - Default->Winlogon (enter): is_secure==true -> re-attach to Winlogon -> works. - Winlogon->Default (leave): is_secure==false -> SKIP re-attach -> the capture thread stays stuck on the now-gone Winlogon desktop -> every rebuild fails -> no frames -> client timeout -> session ends -> SudoVDA removed (the disconnect sound). Fix: call attach_input_desktop() UNCONDITIONALLY on every rebuild (Apollo calls syncThreadDesktop before every duplicate), so leaving secure re-attaches to the returned desktop. reassert_isolation stays secure-only. Also stop leaking the HDESK (CloseDesktop right after SetThreadDesktop, like Apollo) so calling it on every recovery is safe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
9a9214a2d8 |
fix(host/windows): gentle DDA recovery — stop the tight teardown/recreate loop
Per the user's insight: on the secure (Winlogon) desktop the duplication dies on
every independent-flip, and our tight recovery loop tore it down + recreated it
hundreds of times/sec — that release/recreate cycle is the real kernel stress,
and it stalled the send thread long enough that the client timed out ('display
disconnected'). Normal-desktop streaming is already solid (per-session GUID
killed the collision); this only changes the loss-recovery cadence.
Gentle recovery (user chose 'keep session alive'):
- cap the cheap re-duplicate to PUNKTFUNK_RECOVER_MS (default 250ms, was 5ms)
- cap the heavy new-device rebuild to PUNKTFUNK_REBUILD_MS (default 1500ms, was
250ms) — it's the costliest teardown, throttled hardest
- repeat the last frame between attempts (no busy-spin, no 8ms sleep)
~200/s -> ~4/s teardown/recreate during a secure dwell. The session survives
lock/UAC (frozen/laggy secure screen, then clean resume on unlock) instead of
churning the kernel into a disconnect. Both cadences env-tunable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
2f7c021cac |
fix(host/windows): per-session SudoVDA monitor GUID (stop overlapping-session monitor teardown)
User observed: 'display disconnected' + freeze with NO context change, and 'first switch happy, subsequent slower, then chaos under stress'. Log shows the cause: MONITOR_GUID was a FIXED constant, so overlapping sessions (a client RECONNECTING after a freeze before the old session tore down, or concurrent sessions) all map to the SAME SudoVDA monitor (same GUID -> IOCTL_ADD reuses target 257). When the old session ends, its IOCTL_REMOVE tears the monitor down OUT FROM UNDER the live session -> 'display disconnected' + the late E_INVALIDARG/MODE_CHANGE failures (output vanished mid-session) -> cascade. Fix: next_monitor_guid() returns a unique GUID per (process, session) [base GUID with low 48-bit node = pid<<16 | session#]; create() threads it into AddParams AND the keepalive (which REMOVEs by it). Each session now owns its own monitor; one ending can't kill another. (The 200ms DuplicateOutput1 retry confirmed working — 'succeeded on retry' logged; the residual failures were this collision, not the race.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
ce84861e3a |
fix(host/windows): DuplicateOutput1 retry wait 200ms (Apollo's value), env-tunable
The old-dup kernel teardown takes ~200ms (Apollo waits exactly that), so the previous 2-16ms retries were too short and still fell through to the churning legacy dup. Bump to PUNKTFUNK_DUP_RETRY_MS (default 200) x PUNKTFUNK_DUP_RETRY_N (default 6) so the robust DuplicateOutput1 dup wins the race. Env-tunable for on-box dialing without a rebuild. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
1bcb786382 |
fix(android): request NEARBY_WIFI_DEVICES at runtime so mDNS discovery works on real devices
apple / swift (push) Successful in 53s
ci / web (push) Successful in 30s
ci / docs-site (push) Successful in 38s
android / android (push) Successful in 3m23s
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 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
ci / bench (push) Successful in 4m31s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m8s
docker / deploy-docs (push) Successful in 5s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 7m58s
ci / rust (push) Successful in 4m4s
NsdManager service discovery needs NEARBY_WIFI_DEVICES on Android 13+. The app DECLARED it but never REQUESTED it, so on a real device the permission stayed denied and discoverServices silently found nothing — no prompt, no hosts. (It only worked on the emulator because the permission was granted via `adb pm grant`.) Request it (mirroring the mic RECORD_AUDIO flow) when the connect screen appears, and start/restart discovery once granted; on API < 33 discovery starts immediately (the permission doesn't apply there). The advertised hosts the Apple clients already see will then appear here too. Co-Authored-By: Claude Opus 4.8 (1M context) <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> |
||
|
|
0c1afeefea |
fix(android): shrink the colored launcher-icon foreground to match the themed layer
apple / swift (push) Successful in 54s
ci / rust (push) Failing after 40s
ci / web (push) Successful in 27s
ci / docs-site (push) Successful in 28s
android / android (push) Successful in 5m41s
ci / bench (push) Successful in 4m28s
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
deb / build-publish (push) Successful in 2m8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m27s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m38s
docker / deploy-docs (push) Successful in 18s
On the test phone's launcher the standard (colored) adaptive foreground rendered noticeably larger than the themed (monochrome) layer — identical geometry, but the launcher insets/scales the two differently — so the colored circles overflowed the circle mask. Shrink only the foreground group (scale 0.105 → 0.073, re-centred) to match the correctly-sized monochrome; the monochrome layer is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <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> |
||
|
|
18ec32d21e |
feat(android): adaptive launcher icon with Material You themed-icon support
apple / swift (push) Successful in 53s
ci / rust (push) Failing after 1m36s
ci / web (push) Successful in 36s
ci / docs-site (push) Successful in 36s
deb / build-publish (push) Successful in 2m4s
decky / build-publish (push) Successful in 10s
android / android (push) Successful in 3m14s
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 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
ci / bench (push) Successful in 4m46s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m16s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 7m46s
docker / deploy-docs (push) Successful in 19s
Replace the placeholder system icon with the Punktfunk brand mark (two overlapping violet circles,
from the shared logo in clients/apple/.../punktfunk_Logo.icon).
- drawable/ic_launcher_foreground.xml: the violet logo (3 exact paths) scaled + centered into the
108dp adaptive-icon safe zone via a group transform.
- drawable/ic_launcher_monochrome.xml: single-tone silhouette for Android 13+ themed icons
(Material You) — the launcher recolors it to the wallpaper.
- mipmap-anydpi-v26/ic_launcher{,_round}.xml: adaptive-icon (background + foreground + monochrome);
dark-indigo background (@color/ic_launcher_background) so the violet pops.
- Manifest: android:icon=@mipmap/ic_launcher + roundIcon (was @android:drawable/sym_def_app_icon).
minSdk 31 → anydpi-v26 covers every device (no legacy PNG mipmaps needed). Verified on a physical
phone (Android 16): the icon renders centered + circle-masked; the themed-icon layer is wired.
Co-Authored-By: Claude Opus 4.8 (1M context) <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> |
||
|
|
be18797df8 |
feat(client): request a recovery keyframe on unrecoverable loss
apple / swift (push) Successful in 54s
windows-msix / package (push) Successful in 1m0s
windows / build (push) Successful in 54s
android / android (push) Successful in 2m30s
ci / web (push) Successful in 37s
ci / docs-site (push) Successful in 38s
ci / rust (push) Successful in 4m24s
deb / build-publish (push) Successful in 2m5s
decky / build-publish (push) Successful in 25s
ci / bench (push) Successful in 4m25s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 16s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m38s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m24s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 22s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m15s
flatpak / build-publish (push) Failing after 5m13s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 4m37s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 7m26s
Under infinite GOP the punktfunk/1 plane has no periodic IDR — the only recovery keyframe is one the client requests. But the reassembler drops unrecoverable AUs silently (frames_dropped) and hands the decoder reference-missing delta frames that libavcodec conceals and returns Ok for, so keying recovery off a decode error mostly never fires under real loss → a long/permanent freeze. Surface the data-plane pump's Session.frames_dropped to NativeClient via a shared atomic (NativeClient::frames_dropped()), updated every pump iteration so it stays current through a total-loss drought. The Linux and Windows client video loops watch it and call request_keyframe() when it climbs, throttled to 100 ms (the decode stays wedged for several frames until the IDR lands). macOS already does this; client-rs doesn't decode. Resolves reliability backlog #2. Co-Authored-By: Claude Opus 4.8 <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> |
||
|
|
3e2888de26 |
docs(apollo): mark GSO #4 (GameStream Windows USO) done
apple / swift (push) Successful in 54s
windows-msix / package (push) Successful in 1m31s
android / android (push) Successful in 2m29s
windows / build (push) Successful in 1m3s
ci / web (push) Successful in 36s
ci / docs-site (push) Successful in 35s
ci / rust (push) Successful in 4m18s
deb / build-publish (push) Successful in 2m3s
decky / build-publish (push) Successful in 13s
ci / bench (push) Successful in 4m22s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 15s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m30s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m35s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 22s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m14s
flatpak / build-publish (push) Failing after 5m17s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 7m45s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 7m17s
Co-Authored-By: Claude Opus 4.8 <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> |
||
|
|
ba4e9a8672 |
docs(apollo): mark cursor #13 done, reclassify #21 as already-handled
apple / swift (push) Successful in 54s
ci / rust (push) Failing after 1m21s
ci / web (push) Successful in 27s
ci / docs-site (push) Successful in 29s
android / android (push) Failing after 5m44s
ci / bench (push) Failing after 3m26s
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 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 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 2m5s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m3s
docker / deploy-docs (push) Successful in 21s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 7m50s
#13 (two-pass alpha+XOR cursor) implemented in capture/dxgi.rs. #21 (composite moved cursor without a new desktop frame) is already handled: DXGI returns S_OK for pointer-only updates so punktfunk recomposites in present_acquired; the original premise (stutter via timeout) was incorrect. Adds status banner + per-item resolution notes in Part 4 and Part 3. 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> |
||
|
|
f44317fb33 |
feat(windows): stable code-signing cert for the MSIX (one-time per-machine trust)
apple / swift (push) Successful in 54s
windows-msix / package (push) Successful in 1m0s
windows / build (push) Successful in 55s
android / android (push) Failing after 56s
ci / web (push) Successful in 32s
ci / docs-site (push) Successful in 39s
ci / rust (push) Failing after 3m21s
deb / build-publish (push) Successful in 2m5s
decky / build-publish (push) Successful in 14s
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
ci / bench (push) Successful in 4m45s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 5m27s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 5m28s
docker / deploy-docs (push) Successful in 10s
Sign every MSIX build with one STABLE self-signed cert instead of a fresh per-build cert, so the Trusted People import is a one-time, per-machine step that survives upgrades (a fresh cert each build forced a re-import every time). The cert (CN=unom, SHA-1 CD1EFDEE…E941, valid to 2036) lives in the MSIX_CERT_PFX_B64 / MSIX_CERT_PASSWORD Actions secrets; its public half is checked in as packaging/punktfunk-codesign.cer and published next to each .msix. pack-msix.ps1 now always exports the signing cert's public .cer (extracted from a supplied pfx too, not just the ephemeral-generated path) and warns if the cert subject != manifest Publisher (the mismatch Add-AppxPackage would otherwise reject). Documents the path to a publicly-trusted (no-import) cert: swap the two secrets + pass a matching -Publisher. 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> |
||
|
|
cbeece119f |
fix(windows): link the client as a GUI subsystem — no console window on launch
apple / swift (push) Successful in 56s
windows-msix / package (push) Successful in 1m0s
windows / build (push) Successful in 55s
android / android (push) Failing after 57s
ci / web (push) Successful in 33s
ci / docs-site (push) Successful in 39s
ci / rust (push) Failing after 3m24s
deb / build-publish (push) Successful in 2m7s
decky / build-publish (push) Successful in 10s
ci / bench (push) Successful in 4m37s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 14s
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
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m32s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 4m8s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 3m34s
docker / deploy-docs (push) Failing after 17s
The binary had no windows_subsystem attribute, so it linked as a console (CUI) app and Windows opened a console window alongside the WinUI window on every launch (incl. the MSIX). Add #![cfg_attr(windows, windows_subsystem = "windows")] so the windowed/MSIX launch is window-free (verified: the built exe's PE subsystem flips from WINDOWS_CUI=3 to WINDOWS_GUI=2). To keep the CLI paths usable, main now calls AttachConsole(ATTACH_PARENT_PROCESS) at startup — it binds to an existing parent console only (never creates one), so --headless/--discover still print to the launching terminal while Explorer/MSIX launches stay console-free. Adds the Win32_System_Console windows feature. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
183ddd5fa1 |
docs: Apollo (Sunshine fork) vs punktfunk architecture map + transfer backlog
apple / swift (push) Successful in 54s
android / android (push) Failing after 36s
ci / web (push) Failing after 25s
ci / docs-site (push) Successful in 34s
ci / rust (push) Failing after 3m18s
ci / bench (push) Failing after 3m9s
decky / build-publish (push) Successful in 12s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Failing after 38s
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 2m50s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m17s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 26s
docker / deploy-docs (push) Has been skipped
deb / build-publish (push) Successful in 7m46s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 5m2s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 6m39s
Map Apollo's architecture for future agents and compare against punktfunk, with a deep-dive on the Windows host (the focus area). Produced by the apollo-vs-punktfunk multi-agent workflow; every claim carries file:line into both codebases. Contents: Apollo architecture map + Apollo->punktfunk file index; subsystem parity; a reference-grade Windows-host deep-dive (DXGI/WGC capture, cursor compositing, HDR, NVENC-on-D3D11, SendInput/ViGEm, SudoVDA, SYSTEM/secure desktop); and a prioritized 96-item improvement backlog (89 Windows-host, 24 high-severity). Top confirmed Windows gaps: GameStream TLS accepts any client cert (verify_client_cert returns assertion()), no NVENC reference-frame invalidation, SudoVDA watchdog ignores its ioctl result, absolute-mouse mapping discards the virtual-desktop rect. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
bb11b2faf7 |
feat(windows): MSIX packaging + publish workflow for the WinUI client
apple / swift (push) Successful in 54s
ci / rust (push) Failing after 55s
windows-msix / package (push) Successful in 1m2s
ci / web (push) Successful in 31s
windows / build (push) Successful in 55s
ci / docs-site (push) Successful in 31s
android / android (push) Successful in 2m6s
deb / build-publish (push) Successful in 2m24s
decky / build-publish (push) Successful in 11s
ci / bench (push) Successful in 4m21s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m39s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m32s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m49s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 1m21s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 3m20s
docker / deploy-docs (push) Successful in 22s
Package the Windows client as a signed MSIX (Start tile, clean install/uninstall) and publish it to
Gitea's generic registry, mirroring the host's .deb/.rpm and the Mac's DMG. Validated end-to-end on
the build VM: cargo build --release -> makeappx pack (16 payload files, 58 MB) -> signtool ->
Add-AppxPackage deploy -> framework-dependency resolution all green.
- packaging/AppxManifest.xml: full-trust Win32 app (Windows.FullTrustApplication + runFullTrust),
templated {VERSION}/{PUBLISHER}. windows-reactor packages cleanly despite being built "unpackaged"
because it calls MddBootstrapInitialize2 with OnPackageIdentity_NOOP — under MSIX identity the
bootstrapper no-ops and the App SDK resolves from the manifest's PackageDependency on
Microsoft.WindowsAppRuntime.2 (reactor pins MAJORMINOR 0x20000 = 2.0).
- packaging/pack-msix.ps1: assemble layout (exe + reactor/SDL3 auto-staged DLLs + resources.pri +
FFmpeg DLLs + tile assets), makeappx, signtool. Cert precedence: MSIX_CERT_PFX_B64 secret, else an
ephemeral self-signed cert whose .cer is published alongside (swap in a real cert later, no
manifest change).
- assets: tile/store logos rasterized from packaging/flatpak/io.unom.Punktfunk.svg.
- .gitea/workflows/windows-msix.yml: runs on the Windows runner on main pushes + win-v* tags +
dispatch. MSIX version is 4-part numeric — win-vX.Y.Z -> X.Y.Z.0, else 0.2.<run>.0. shell: pwsh +
CARGO_TARGET_DIR=C:\t like windows.yml.
Co-Authored-By: Claude Opus 4.8 <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> |
||
|
|
1e8f210948 |
docs(windows-secure-desktop): steps 1/3/4/5/6 live-validated; soak results
apple / swift (push) Successful in 55s
android / android (push) Failing after 34s
ci / web (push) Failing after 5s
ci / docs-site (push) Failing after 1s
ci / bench (push) Failing after 0s
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 (., web/Dockerfile, punktfunk-web) (push) Failing after 1s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-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 1s
docker / deploy-docs (push) Has been skipped
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 0s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 0s
ci / rust (push) Failing after 2m50s
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>
|
||
|
|
e39f65a228 |
ci(windows): set CARGO_TARGET_DIR=C:\t — dodge MAX_PATH in CMake-from-source builds
apple / swift (push) Successful in 54s
windows / build (push) Successful in 3m22s
android / android (push) Failing after 34s
ci / web (push) Successful in 50s
ci / docs-site (push) Successful in 31s
ci / rust (push) Failing after 2m32s
decky / build-publish (push) Successful in 11s
deb / build-publish (push) Successful in 2m59s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 30s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
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 4m55s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 4m23s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 4m17s
docker / deploy-docs (push) Failing after 0s
With the BOM fixed (shell: pwsh), the build got far enough to compile audiopus_sys, which does a CMake-from-source build of libopus. The runner's host workdir sits deep under C:\Windows\System32\config\systemprofile\.cache\act\<hash>\hostexecutor\, so target\debug\build\ audiopus_sys-*\out\build\CMakeFiles\CMakeScratch\TryCompile-*\...\.tlog overran Windows' 260-char MAX_PATH and MSBuild's tracker failed to create its .tlog (DirectoryNotFoundException -> MSB6003, "CL.exe konnte nicht ausgeführt werden"). Pointing CARGO_TARGET_DIR at C:\t shortens every nested build path well under the limit (fixes audiopus_sys + SDL3, both CMake-from-source). Co-Authored-By: Claude Opus 4.8 <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 (
|
||
|
|
372483abf0 |
ci(windows): use shell: pwsh (PowerShell 7) — fixes GITHUB_ENV BOM corruption
apple / swift (push) Successful in 54s
ci / rust (push) Failing after 58s
ci / web (push) Successful in 36s
windows / build (push) Failing after 1m47s
android / android (push) Successful in 1m56s
ci / docs-site (push) Successful in 28s
deb / build-publish (push) Successful in 2m35s
decky / build-publish (push) Successful in 11s
ci / bench (push) Successful in 4m26s
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 2m40s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m37s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 24s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m17s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 5m2s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 4m57s
docker / deploy-docs (push) Failing after 14s
Windows PowerShell 5.1's Out-File -Encoding utf8 prepends a UTF-8 BOM, corrupting the first GITHUB_ENV line so CARGO_WORKSPACE_DIR silently never got set -> windows-reactor build.rs panic -> CI build failed (runs 8765/8768). pwsh 7 writes UTF-8 without a BOM. Installed PowerShell 7.6.2 MSI on the runner and put C:\Program Files\PowerShell\7 on the daemon wrapper PATH so jobs find pwsh; switched all windows.yml steps to shell: pwsh. (Reproduced locally with CARGO_WORKSPACE_DIR set: the build is green in 2m37s — the BOM was the only issue.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
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> |