35d97ae6acd97860bb6f541fe571b6fab5d59af1
30 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
35d97ae6ac |
feat(windows): parallel virtual displays — proto v3 ring binding, manager slot map, group topology (W0–W3)
windows-drivers / probe-and-proto (push) Successful in 41s
ci / web (push) Successful in 1m2s
ci / docs-site (push) Successful in 1m7s
apple / swift (push) Successful in 1m11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
decky / build-publish (push) Successful in 15s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 38s
windows-drivers / driver-build (push) Successful in 1m46s
ci / bench (push) Successful in 6m35s
docker / deploy-docs (push) Successful in 20s
windows-host / package (push) Successful in 9m37s
deb / build-publish (push) Successful in 13m48s
arch / build-publish (push) Successful in 14m21s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m34s
android / android (push) Successful in 15m45s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m20s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m18s
ci / rust (push) Successful in 18m48s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m41s
release / apple (push) Successful in 19m30s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m49s
flatpak / build-publish (push) Failing after 2m9s
apple / screenshots (push) Successful in 19m3s
design/windows-parallel-virtual-displays.md (display-management Stage 7 / §6.6): N simultaneously-live pf-vdisplay monitors, one sealed ring each, every idd-push-security invariant preserved per-ring. - proto v3: SharedHeader._pad → target_id — the ring NAMES its monitor, host-stamped before the magic; the driver publisher refuses a cross-bound ring via the shared, unit-tested frame::check_attach (new DRV_STATUS_BIND_FAIL — the gamepad pad_index validation applied to frames, invariant #10); the host's wait_for_attach surfaces the refusal loudly and self-checks its own stamp. - manager: the one-monitor MgrState becomes a slot map keyed by the client's identity slot (0 = anonymous/GameStream); per-slot reconnect + dead-WUDFHost preempts, slot-scoped begin_idd_setup (a different identity is an admission question, never a preempt), ONE device-level watchdog pinger, per-slot /display/state + /display/release. - group topology: isolate_displays_ccd takes the managed target SET (a sibling slot is never deactivated); SavedConfig + the DDC/PnP axes move to the group record (first-in captures, last-out restores); desktop layout via CCD source origins from the pure layout::arrange (auto-row default, manual pins win), re-applied on create + reconfigure. - admission: the Windows separate→reject override now sits behind the PUNKTFUNK_WIN_SEPARATE=1 validation hatch (the wedge it guarded is structurally gone — a second identity gets its own monitor + ring; default flips in W5 after soak); max_displays and NVENC session-unit budgets decline an unaffordable display AT admission; kick_dwm_compose is process-globally throttled and per-display — cursor jump + 35 ms dwell (a sub-tick jump composes nothing; DWM reads dirties from current state at the next vsync tick). On-glass on the RTX box: V1/V2/V4/V5/V6/V9 green — two paired clients on two monitors streaming ~60 fps each with zero mismatches and zero bind failures, churn-hammer clean (no 0x80070490), per-ring mode-change recreate leaves the sibling untouched, typed budget rejection, fault-injected cross-bind refused loudly with the sibling undisturbed. V7: WUDFHost-kill shared fate is clean; in-process device recovery is a known follow-up (the retired-never-closed control handles block the adapter cycle — reset-pf-vdisplay.ps1 recovers). DWM composes two IDD monitors concurrently at 60 fps — the plan's load-bearing unknown, answered yes. Also carries the client-HDR EDID forwarding that shared this working tree (Hello::display_hdr → AddRequest luminance tail → the monitor's CTA-861.3 HDR block, PUNKTFUNK_CLIENT_PEAK_NITS hatch) and the Deck client fixes (40 ms rumble keep-alive with 1-LSB jitter, HDR self-diagnosing presenter warn, flatpak HDR env). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1fcf9e11ec |
fix(video): honor the signaled CSC matrix end-to-end + tvOS HDR presentation
Clients derive Y'CbCr->RGB from the stream's SIGNALED matrix x range x depth via shared csc rows (Rust csc_rows + Swift CscRows) instead of hardcoded 709/2020 - a BT.601-signaled stream (a Linux host's RGB-input NVENC) no longer renders with a constant hue error. Host-side signaling made honest across NVENC/VAAPI/openh264/GameStream and the session plan's chroma/bit-depth. Decoded color-bar fixtures (601/709 x limited/full) pin the math in tests on both cores. Same presenter, tvOS HDR: tvOS has no Metal EDR API and a bare PQ colorspace tag composites UNTONE-MAPPED (the "overblown" Apple TV report), so HDR now splits on the display's live EDR headroom - PQ passthrough when the per-session AVDisplayManager mode switch landed (a real HDR10 output tone-maps itself), else an in-shader PQ->SDR tone-map (203-nit reference white, extended-Reinhard 1000-nit knee, 2020->709) into the proven SDR layer config. The 10-bit stream keeps its full decode depth either way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7ab97bb1a3 |
feat(host): capture-stall watch — DWM-level self-diagnosis for the Exclusive-topology stutter
ci / web (push) Successful in 54s
ci / docs-site (push) Successful in 1m7s
apple / swift (push) Successful in 1m18s
ci / bench (push) Successful in 5m17s
decky / build-publish (push) Successful in 36s
windows-host / package (push) Successful in 8m13s
arch / build-publish (push) Successful in 11m2s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 32s
apple / screenshots (push) Successful in 5m38s
android / android (push) Successful in 16m12s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
deb / build-publish (push) Successful in 15m13s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10m33s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 13m43s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11m46s
ci / rust (push) Successful in 22m46s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m45s
docker / deploy-docs (push) Successful in 22s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m10s
Field repro (Mounjay, still present on 0.9.0): the ~4 s double-jolt stutter appears ONLY while the virtual display is the sole active display (Exclusive topology) and stops the instant Windows switches to Extend — live, both ways. Cross-project research (Apollo #179/#358/#368/#563/#776, VDD #36, Tom's HW) points at the display/present path BELOW capture: an inactive-but-connected DisplayPort head being periodically serviced (standby HPD/AUX/link events), with a DWM software-vsync clock beat as the secondary (different-signature) class. Neither ends in anything our recovery-side detector can see unless the client actually loses data — so give the HOST a direct sensor at the ring: - StallWatch (idd_push.rs): a >150 ms hole in DWM frame delivery counts as a capture stall only when the 8 preceding frames arrived within 400 ms — sustained >=20 fps flow, so an idle desktop, a caret blink, or a paused video can never trip it. Per-stall debug line; when stalls settle into an evenly-spaced multi-second cycle, one rate-limited WARN names the class: 'capture stalls are METRONOMIC', with the topology=primary/extend and refresh-rate leads. Ring-recreate recovery gaps reset the watch (self- inflicted, already logged by the recreate path). - The evenly-spaced-cycle detector moves out of punktfunk1.rs into metronome.rs (RecoveryCadence -> Metronome, unchanged logic + tests) so the IDR-serve detector and the stall watch share one implementation; the recovery WARN now cross-references the capture-stall lines. Diagnosis map for an Exclusive-mode stutter log: 'slow display-descriptor poll' = something holds the win32k display lock; 'capture stalls are METRONOMIC' without it = DWM stopped composing (DP servicing / present clock, below us); recovery-IDR METRONOMIC alone = frames flowed but clients lost data. Verified: Linux tests+clippy+fmt clean; Windows (RTX box) 220/220 + clippy clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b7cb75a48a |
fix(host): dual identical GPUs stream again — self-heal the IDD-push ring onto the driver's real render adapter
ci / web (push) Successful in 50s
apple / swift (push) Successful in 1m13s
ci / docs-site (push) Successful in 1m5s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
decky / build-publish (push) Successful in 15s
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 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
ci / bench (push) Successful in 5m30s
docker / deploy-docs (push) Successful in 20s
apple / screenshots (push) Successful in 5m35s
windows-host / package (push) Successful in 7m57s
deb / build-publish (push) Successful in 12m0s
arch / build-publish (push) Successful in 14m51s
android / android (push) Successful in 16m21s
ci / rust (push) Successful in 17m20s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m3s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m0s
A box with two identical GPUs (twin RTX 4090s in the field report) died out of pipeline retries with driver_status=2 (DRV_STATUS_TEX_FAIL, detail 0x80070057): SET_RENDER_ADAPTER pinned the driver to one twin at monitor ADD, but the ring open re-ran the GPU picker, whose max-VRAM tie is settled by DXGI enumeration order — which follows the primary display that the vdisplay flow itself moves mid-session. Ring on one twin, swap-chain on the other → the driver can't open the shared textures, and every retry repeats the same wrong pick. - wait_for_attach: TEX_FAIL becomes a typed AttachTexFail carrying the render LUID the driver reports in the shared frame header (written before the texture opens, so valid on failure) — the error now names both adapters instead of guessing "mismatch?". - open_inner: on AttachTexFail with a different, non-zero driver LUID, rebind the ring there and reopen ONCE. Both candidates are real GPU adapters, so NVENC keeps getting a device it accepts. - gpu::enumerate (Windows): sort the inventory by LUID so the max-VRAM tie, the env-substring first match, and twin occurrence numbering are stable for the boot instead of tracking the primary display. - manager: record the ADD-time render pin on Monitor and warn on reuse when the current pick has moved away from it (the pick only takes effect on the next monitor create; /display/release forces one). - pf_vdisplay: drop the "ADD render adapter DIFFERS from pinned" warn — the ADD reply carries IDARG_OUT_MONITORARRIVAL.OsAdapterLuid (the IddCx DISPLAY adapter, verified on-glass), not the render GPU, so the check compared unrelated LUIDs and fired on every ADD. Verified on the RTX 4090 box: single-GPU streaming is unchanged (IddPush + NVENC AV1 session, no new warnings). The rebind path needs a twin-GPU box — until validated there, /display/release remains the manual recovery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a1285dfa13 |
fix(host): IDD capture immune to display-config stalls/flaps + metronomic-recovery self-diagnosis
ci / docs-site (push) Successful in 1m7s
ci / web (push) Successful in 1m8s
decky / build-publish (push) Successful in 32s
android / android (push) Failing after 2m38s
windows-host / package (push) Failing after 11m13s
ci / rust (push) Successful in 22m2s
ci / bench (push) Successful in 5m36s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10m9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 12m15s
deb / build-publish (push) Successful in 15m40s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m41s
apple / swift (push) Successful in 1m12s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 21s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 12s
arch / build-publish (push) Successful in 14m26s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 12m11s
apple / screenshots (push) Successful in 5m30s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12m52s
docker / deploy-docs (push) Failing after 6m14s
Field report (0.8.2→0.8.4): metronomic stutter every ~3.7-4 s, each event a double-jolt, on ALL clients — and the same box stuttered on Apollo (virtual display) but not Sunshine (physical). The disturbance class is host-side display-topology events / display-poller software (SteelSeries-GG class, Apollo #480/#358/#368) hitting the virtual-display path. Three changes make the IDD path resilient to it and self-diagnosing: - DescriptorPoller: the display-descriptor CCD queries (QueryDisplayConfig twice per sample) serialize on the session-global display-config lock, which topology churn and display-poller software can hold for tens-to- hundreds of ms. Polled inline, that stall landed ON the capture/encode thread 4x/s — a periodic frame hitch, invisible in any log. A dedicated thread now samples every 250 ms and publishes a snapshot; the capture loop pays one uncontended mutex read, and a sample slower than 50 ms logs a rate-limited warn — the stall is measured instead of streamed. - Flap immunity: advanced_color_enabled returns Option (None on a failed query or the target briefly missing from the active paths during a re-probe) and the poller keeps last-known-good per field — a transient no longer reads as "HDR off", which used to cost an HDR session TWO spurious ring recreates (false, then true again a poll later). On top, a two-strikes debounce: only two consecutive samples agreeing on the same new descriptor trigger the recreate; a real "Use HDR" flip or game mode-set still lands in ~1/2 s. - RecoveryCadence: served recovery IDRs within 1.5 s coalesce into one event (a double-jolt's paired IDRs count once); four consecutive events evenly spaced (±20%) warn "client keyframe recoveries are METRONOMIC — a periodic host/display disturbance, not random network loss" with the measured period (re-warn ≤ every 30 s) — so an affected host self-diagnoses in its own log / web console instead of reporting "nothing in the logs". Unit tests include the field report's exact double-jolt-pair schedule. Verified: host tests + clippy clean on Linux and on the RTX box (211/211, --features nvenc). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d4aba755b6 |
fix(windows-host): IDD-push compose kick — idle desktop no longer fails the attach gate
DWM presents a display only when something dirties it. On an idle desktop a perfectly healthy session sat at E_PENDING: the driver attached but no first frame ever landed, so wait_for_attach's 4 s gate failed the open (and a mid-session ring recreate hit the same stall against the 3 s recover-or-drop). A real client escaped only because its own input soon dirtied the desktop; a headless probe / input-less connect never did. kick_dwm_compose() injects two net-zero 1 px relative mouse moves via SendInput — pf-vdisplay has no hardware-cursor plane, so a cursor move is composited into the frame, a guaranteed real present onto the IDD swap-chain (the mechanism --input-test always relied on; the pointer ends where it started). Wired into wait_for_attach (first kick at 600 ms, then every 800 ms) and, rate-limited, into the GB1 recovery window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
341b31f7c1 |
fix(windows-host): IDD-push resilience — driver-death recovery, reopenable control device, full interface discovery
Batch A of the audit's medium tier (M1+M2+M3): - M1 driver-death detection: a dead WUDFHost stops publishing, which at the ring is indistinguishable from an idle desktop — SDR sessions streamed a frozen frame forever (next_frame's 20 s bail is unreachable once anything presented). The ChannelBroker's process handle now doubles as a liveness probe (SYNCHRONIZE at OpenProcess); while no fresh frame arrives, try_consume polls it (rate-limited) and fails the capturer, landing in the session's bounded in-place rebuild. - M2 reopenable control device: the manager's OnceLock-cached handle is now a retire/reopen DeviceSlot — a gone-classified IOCTL failure (driver upgrade / WUDFHost restart; pinger, create, or REMOVE) retires the handle and the next use reopens + re-handshakes. Retired handles are deliberately kept alive forever: bare-HANDLE holders (pinger, ChannelBroker) rely on never-closed, and a retired handle only fails IOCTLs. CLEAR_ALL runs on the FIRST open only (a reopen races live-ish sessions); acquire retries the monitor create once after a reopen. The JOIN path now probes the active monitor's WUDFHost pid and preempts a DEAD monitor instead of handing the rebuilding session its stale target — without this the whole recovery chain starved to the rebuild budget. - M3 interface discovery: enumerate ALL interface instances with an SPINT_ACTIVE filter (a Code-10 devnode at index 0 no longer shadows the live interface), HDEVINFO behind RAII (error paths leaked one per probe), the raw device handle wrapped before GET_INFO (leaked on handshake failure), and the detail-sizing result guarded before the cbSize write. - pf-driver-proto: SetFrameChannelRequest doc now states the real adopt-on-success contract (the old wording invited a driver-side close-on-error — a cross-process double-close against the host's reap). - install: pf_vdisplay_present() passes /connected so a phantom devnode can't suppress creating a live ROOT node. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ea8e996989 |
fix(windows): IDD-push audit highs — keyed-mutex timeout, two per-frame leaks, IDD_PUSH knob, pooled-device threading
Five verified findings from the IDD-push/pf-vdisplay deep audit: - Keyed-mutex acquire (BOTH endpoints): AcquireSync returns WAIT_TIMEOUT (0x102) / WAIT_ABANDONED (0x80) as SUCCESS-severity HRESULTs, which the windows-rs Result wrapper erases — a busy slot read as "acquired", so driver and host could race the same ring texture (torn frames) and the designed busy-skip backpressure was dead code. Both sides now classify the raw vtable HRESULT; WAIT_ABANDONED counts as acquired (ownership transfers — refusing it would wedge the slot forever). - Host SDR hot path leaked one ID3D11VideoProcessorInputView per converted frame: the D3D11_VIDEO_PROCESSOR_STREAM ManuallyDrop field suppressed the release after VideoProcessorBlt. Released by hand now, success or not. - Driver leaked IddCx's per-acquire surface reference (from_raw_borrowed on a TRANSFERRED reference — the MS sample Attach/Reset's it): the swap-chain surface set survived swap-chain destruction, the likely true root cause of the ~50 MB-per-reconnect VRAM loss that device pooling only mitigated. Now adopted via from_raw (publisher or not) and dropped pre-Finished. - PUNKTFUNK_IDD_PUSH removed: capture is unconditionally IDD-push, but the vdisplay manager still gated the lingering-monitor preempt (and render pin) on the knob, whose default was OFF — dev/CLI runs reused a lingering monitor whose IddCx swap-chain is dead (black reconnect). The preempt and the render-GPU pin are now unconditional; host.env comments no longer promise the removed DDA/WGC fallback. - Driver D3D device: dropped D3D11_CREATE_DEVICE_SINGLETHREADED (unsound since DEVICE_POOL shares one device across processors) and the pooled immediate context is now SetMultithreadProtected — two concurrent monitors' workers otherwise race an unlocked context (UB in the UMD). No wire-contract change (pf-driver-proto untouched); the driver fixes take effect on the next pf-vdisplay redeploy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8b47be668f |
feat(host/windows): seal the host↔driver channels (frame + gamepad, proto v2)
Frame ring (pf-vdisplay) and both gamepad SHM channels move off named Global\ objects (openable by any sibling LocalService) to UNNAMED sections/events whose handles the host DuplicateHandles into the driver's verified WUDFHost with least access — frame delivery over the SYSTEM+admins-only IOCTL_SET_FRAME_CHANNEL, pads over a 32-byte named bootstrap mailbox (pid + handle value only, DoS-bounded; HID minidrivers have no control device). Driver-validated pad_index kills cross-pad redirects; v1↔v2 mixes fail closed with diagnosis logs on both sides. Sibling-LocalService denial proven empirically (design/idd-push-security.md, design/gamepad-channel-sealing.md). Driver-side raw ops now live behind pf-umdf-util (checked shm accessors, the forbid(unsafe_code) ChannelClient state machine, WDF request tokens) — the pad drivers' logic is 100% safe Rust; whole drivers workspace clippy-gated in CI. driver install --gamepad now sweeps SWD\punktfunk phantom devnodes: a re-created SwDevice REVIVES the old devnode with its previously-bound driver (never re-ranks), so an upgrade otherwise leaves the old driver serving — or, across the v1→v2 fence, a dead pad (found live on the RTX box). On-glass validated on the RTX 4090 box: frame path 7007 frames p50 2.06 ms cross-machine; DualSense + XUSB "sealed pad channel mapped"/proto=2 attach via both the test harness and a real streaming session; phantom-sweep repro. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
dacc3b5209 |
feat(host,web): multi-GPU selection — GPU inventory + preference API, web-console GPU card
- new crate::gpu (compiled on all platforms so the OpenAPI doc stays platform-independent): DXGI / sysfs GPU inventory with reboot-stable ids (PCI vendor:device + occurrence — LUIDs are per-boot), persisted auto/manual preference (<config>/gpu-settings.json, atomic temp+rename with in-memory rollback), one selection with precedence console preference > PUNKTFUNK_RENDER_ADAPTER > max VRAM and graceful fallback when the preferred GPU is absent, plus a live "in use" record (RAII session guard wrapped around every encoder open_video returns) - fix: windows_gpu_vendor derived the encoder backend from DXGI adapter 0 instead of the selected render adapter — on a hybrid box (e.g. Intel iGPU at index 0 + NVIDIA dGPU) the backend could disagree with the GPU the capture ring / IddCx render pin sit on. The NVENC 4:4:4 probe now also runs on the selected adapter (was: OS default), the codec/4:4:4 probe caches are keyed per selected GPU (were process-lifetime OnceLocks), and an explicit PUNKTFUNK_ENCODER conflicting with the selected GPU's vendor warns up front - mgmt API: GET /api/v1/gpus (inventory + mode + preferred + next-session selection with reason + in-use GPU/backend/session-count) and PUT /api/v1/gpus/preference (validates mode/gpu_id before writing); openapi.json regenerated; the vdisplay render pin now also engages for a console preference (not just the env pin) - web console: GPU card on the Host page — list with vendor + VRAM, Automatic / Prefer controls, Preferred / Next session / "In use · backend" badges, missing-preferred-GPU warning and env-pin note; en + de messages - Linux: a matched manual preference picks the VAAPI render node and the NVENC-vs-VAAPI auto choice; auto mode is exactly the previous behavior Validated live on the hybrid laptop (RTX 3500 Ada + Intel Arc Pro, which enumerates twice — the occurrence ids disambiguate): enumerate, prefer, bad-id 400, restart persistence, auto-restore keeping the stored pick. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1a8027f8b7 |
fix(fmt): remove extra blank line in dxgi.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
3fb9d081d1 | fix build | ||
|
|
b9c8d5ab43 |
refactor(host/windows): collapse Windows capture to IDD-push only
Remove DXGI Desktop Duplication (DuplCapturer), Windows.Graphics.Capture
(WgcCapturer), the two-process SYSTEM+helper relay (virtual_stream_relay /
HelperRelay / DesktopWatcher / composed_flip), and the five source files that
implemented them. IDD direct-push is now the sole Windows capture path; the
session topology is always SingleProcess.
Deleted files: wgc.rs, wgc_relay.rs, desktop_watch.rs, composed_flip.rs,
windows/wgc_helper.rs (+ wgc-helper subcommand in main.rs).
dxgi.rs is kept but carved to shared GPU primitives only (make_device,
HdrP010Converter, VideoConverter, install_gpu_pref_hook, WinCaptureTarget,
pack_luid) — ~2237 lines of DDA-only code removed; imports cleaned.
capture.rs: IDD-push open failure fails the session cleanly (no fallback).
Adds capturer_supports_444() — returns false on Windows (IDD-push 4:4:4 is a
follow-up), replacing the stale single_process gate in 4:4:4 negotiation.
session_plan.rs: CaptureBackend{Dda,Wgc} and SessionTopology::TwoProcessRelay
removed. config.rs: no_helper/force_helper/no_wgc/capture_backend/secure_dda
removed. merged_env_block relocated from wgc_relay to windows/interactive.rs.
Linux cargo check clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
42d7f7ab2a |
fix(host/security): scope Windows shared-section SDDL to SYSTEM+LocalService (close #5)
The gamepad host<->UMDF-driver shared sections (Global\pfds-shm-*, pfxusb-shm-*) and the IDD-push frame ring/event (Global\pfvd-*) were created with `D:(A;;GA;;;WD)` — GENERIC_ALL to **Everyone** — on the assumption the driver's WUDFHost ran under a restricted token needing broad access. So any local unprivileged user could OpenFileMapping the section to inject controller input, tamper the trusted HID channel, or read captured screen frames (security-review 2026-06-28 #5). On-box validation (RTX box, 2026-06-29) disproved the restricted-token premise: the WUDFHost token is NT AUTHORITY\LocalService (S-1-5-19), SYSTEM integrity, with ZERO restricted SIDs. So the section only needs SYSTEM (the host creates + writes it) and LocalService (the driver opens it). Scope both SDDL sites to `D:(A;;GA;;;SY)(A;;GA;;;LS)`; rename the now-misnamed `permissive_sa` -> `shared_object_sa`; correct the stale "restricted-token / Everyone" docs. Validated live: a full DualSense + 1280x720x60 session — 6943 frames received, HID output round-tripped, device status OK (pf_dualsense + pf_vdisplay WUDFHosts both LocalService open the scoped sections fine), while OpenFileMapping from a non-SYSTEM admin session now returns ACCESS_DENIED (was a granted handle under WD). Host-only change (the SDDL is set when the host CREATES the section); drivers unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f7a9549cd1 |
chore(licensing): LGPL FFmpeg swap, third-party notices, attribution hygiene
The MIT OR Apache-2.0 SOURCE license is clean (audit found no copied copyleft); the
gaps were all binary-distribution (Layer-2). This makes the shipped artifacts honest:
- Windows host + client: bundled FFmpeg BtbN gpl-shared -> lgpl-shared (AMF/QSV/decode
unaffected; the GPL-only x264/x265 were never used), and ship the FFmpeg LGPL notice
+ license text in the installer + MSIX (licenses/).
- THIRD-PARTY-NOTICES.txt generated + bundled into installer/MSIX/deb/rpm. Offline
generator (scripts/gen-third-party-notices.{py,sh}) + cargo-about config (about.toml/
.hbs) with a permissive-only accepted-license allow-list as a copyleft regression gate.
- Reword the win32u GPU-preference hook comments to reflect independent reimplementation
(no Apollo/Sunshine GPL-3.0 source copied).
- README dual-license + inbound=outbound contributor clause + non-affiliation trademark
disclaimer; new CONTRIBUTING.md.
- LICENSE files into the standalone driver + vk-layer workspaces; deb copyright holder
aligned to "unom and the punktfunk contributors".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
7612238a59 |
feat(audio): end-to-end 5.1/7.1 surround across the native path + all clients
Adds negotiated 5.1/7.1 surround to the punktfunk/1 protocol and every client (previously stereo-only): - core: new shared `audio` layout table (LAYOUT_51/71 + identity multistream mapping, canonical wire order FL FR FC LFE RL RR SL SR); Hello/Welcome `audio_channels` negotiation via the trailing-byte back-compat pattern (old peers fall back to stereo); C-ABI `punktfunk_connect_ex6`, `punktfunk_connection_audio_channels`, and in-core multistream decode `punktfunk_connection_next_audio_pcm` for embedders without a multistream Opus decoder. Real-libopus channel-identity round-trip test. - host: native audio thread captures + Opus-(multi)stream-encodes at the negotiated count (with a cross-session cached-capturer channel-mismatch fix); GameStream surround unified onto the safe `opus::MSEncoder`, dropping `audiopus_sys` (~4 unsafe blocks) and un-gating Windows GameStream surround; WASAPI loopback capture relaxed to 2/6/8 with the correct dwChannelMask. - clients: Linux (PipeWire), Windows (WASAPI), Android (AAudio) decode via `opus::MSDecoder` + render multichannel; Apple decodes in-core to PCM → AVAudioEngine with an explicit wire-order channel layout; each gains a Stereo/5.1/7.1 setting. `punktfunk-probe --audio-channels N` is the headless validator. Verified on Linux: core/host/linux/probe test suites + the Android Rust (cargo-ndk) build, clippy -D warnings, and rustfmt all green. Windows/Apple builds, all on-glass checks, and the live native loopback are pending (CI / a free box). Also lands the concurrent in-tree HEVC 4:4:4 host work (PUNKTFUNK_444): it shares the same touched files (quic.rs, punktfunk1.rs, encode/*, ...) and so cannot be committed separately from the surround changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d492c2147d |
docs(design): trim shipped plans, consolidate cluster, add index
Much of design/ described work that has since shipped. Trim each doc to
its durable rationale + still-open items (the code is the source of truth
for shipped detail; git history holds the full originals).
- Shipped plans -> status stubs: stats-capture, gamestream-host-plan,
apple-stage2-presenter, windows-service.
- Trimmed completed-out / open-kept: implementation-plan, hdr-pipeline,
host-latency, gpu-contention (fixed stale status table), game-library,
linux-setup (fixed m0->spike + stale zero-copy claim),
session-aware-host-followups, windows-client-bootstrap,
windows-dualsense-{scoping,game-detection}, windows-virtual-display,
security-review (per-finding status table; #12 still open),
apollo-comparison (shipped backlog collapsed to one-liners).
- Windows-host cluster consolidated: windows-host.md -> redirect into
windows-host-rewrite.md (whose stale scorecard is corrected -- goal1 is
merged, M4 done); windows-secure-desktop.md archived (now a fallback
behind IDD-push primary).
- Kept evergreen: ci.md, gamescope-multiuser.md, windows-build-and-packaging.md.
- New design/README.md: per-doc status table + consolidated open-items
roll-up so nothing is tracked in only one buried doc.
- Repoint 5 code comments to the archived secure-desktop doc path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
0925f1aaa1 |
fix: complete the docs/→design/ and openapi→api/ rename references
The file moves (docs/ → design/, docs/api/openapi.json → api/openapi.json) landed
in
|
||
|
|
bdc2e44fb2 |
fix(host): remove unsound unsafe impl Sync for HelperRelay
The one genuine soundness defect the unsafe-proof program surfaced (flagged SUSPECT in program 3/N). `HelperRelay` holds an `rx: Receiver<RelayAu>`, which is `!Sync` (std mpsc is single-consumer), so asserting `Sync` claimed more than the fields support — an `Arc<HelperRelay>` recv'd from two threads would compile and be UB. It was never live-exploited, and it turns out `Sync` is also unnecessary: the relay is a single-owner `mut relay` local in the punktfunk1 two-process mux loop (recv_timeout/try_recv/request_keyframe all called on the owning thread; no `Arc`, no `thread::spawn` capturing it). So the fix is simply to delete the impl — the struct keeps its sound `unsafe impl Send` (needed for the raw `HANDLE` fields), which is all the code uses. Box-verified: cargo clippy -p punktfunk-host --features nvenc --target x86_64-pc-windows-msvc -- -D warnings stays green without the Sync impl. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
33328ee417 |
docs(host): prove the last 3 files + crate-root deny (unsafe-proof program 4/N, final)
Completes the unsafe-proof program now that the parallel WIP has landed: - idd_push.rs (25 sites), nvenc.rs (7), punktfunk1.rs (21): a SAFETY proof on every unsafe block — D3D11/DXGI COM (same-device textures, immediate-context single-thread, keyed-mutex-held convert), the NVENC SDK table (versioned POD, register/map/lock-bitstream pairing), cross-process shm reads (atomic magic/generation handshake), and the C-ABI harness (each call cross-checked against its abi.rs `# Safety` doc). No SUSPECT (UB) blocks. - capture.rs / encode.rs: the parent-module deny is restored (their WIP children are now proven), and main.rs gains a crate-root #![deny(clippy::undocumented_unsafe_blocks)] — the permanent catch-all gate so no future unsafe block anywhere in the crate can land without a proof. - Fixed 4 blocks the agents missed: unsafe blocks nested inside `assert_eq!(...)` macro args (the comment-above-statement didn't associate) — hoisted to a `let`. - rustfmt-canonicalized the Windows files (the agents' SAFETY comments + some pre-existing 1.9.0 drift) so `cargo fmt --all --check` is clean. Verified: cargo clippy -p punktfunk-host --all-targets -- -D warnings AND cargo fmt -p punktfunk-host --check both green with the crate-root deny active. Windows cfg(windows) re-verified on the box next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
28cc6acb50 |
feat(windows-host): IDD-push encodes native NV12/P010 (skip NVENC's SM-side CSC)
GPU-contention work (host-latency plan §5.A): the IDD-push output ring now hands NVENC native YUV instead of RGB, so NVENC skips its internal RGB→YUV colour conversion on the SM/3D engine the running game saturates. - idd_push.rs: out_ring is now NV12 (SDR, BT.709 limited) via a D3D11 VIDEO-engine BGRA→NV12 VideoConverter (keeps the CSC off the contended 3D/compute engine), or P010 (HDR, BT.2020 PQ limited) via the FP16→P010 shader (NVIDIA's VideoProcessor can't do RGB→P010). The ring drops its per-slot RTV (textures only), matching the WGC YUV ring; converters rebuild on a size/HDR flip. - nvenc.rs: NV12 input forces bit_depth=8 so an HDR→SDR toggle (or a 10-bit- negotiated client on an SDR display) re-inits the session at the matching depth — NV12 can't feed a 10-bit session (register_resource rejects it). - punktfunk1.rs: per-stage latency instrumentation under PUNKTFUNK_PERF (cap=try_latest, submit=encode_picture, wait=lock_bitstream µs p50/p99/max) to pinpoint where capture→encoded latency goes under GPU saturation. |
||
|
|
79c8641c3f |
docs(host): prove unsafe blocks in the Windows + cross-platform files + gate them (unsafe-proof program 3/N)
Continues the unsafe-proof program across the Windows/cross-platform host files
(~75 blocks, 21 files), each with a SAFETY proof of the real invariant and a
per-file #![deny(clippy::undocumented_unsafe_blocks)] gate:
capture/windows: dxgi.rs, wgc_relay.rs, wgc.rs, desktop_watch.rs, composed_flip.rs
(windows-rs COM: interface validity, same-D3D11-device textures,
immediate-context single-thread, borrowed args outlive the call)
windows: service.rs (SCM/token/CreateProcessAsUserW/event handles — OwnedHandle
liveness, no double-close/signal race), win_display, wgc_helper, interactive
vdisplay/windows: manager.rs, pf_vdisplay.rs (SwDeviceCreate/IddCx/ioctl handle
liveness via the OnceLock VDM singleton + OwnedHandle)
encode/windows: ffmpeg_win.rs (full AVBufferRef refcount audit — balanced, NO leaks,
unlike the vaapi sibling), sw.rs
cross-platform: gamestream/audio.rs (libopus), gamestream/stream.rs (sendmmsg),
inject/windows/sendinput.rs, audio/windows/wasapi_mic.rs,
session_tuning.rs, vdisplay.rs
Two findings (handled separately):
- wgc_relay.rs `unsafe impl Sync for HelperRelay` is UNSOUND (its mpsc Receiver is
!Sync) though not live-exploited — marked SUSPECT inline; fix pending box check
(it touches the in-flight punktfunk1.rs).
- capture.rs / encode.rs (PARENT modules of the WIP idd_push.rs / nvenc.rs) do NOT
get the file deny yet — it would propagate the lint into the undocumented WIP
children. The deny lands there once those are documented (after the WIP commits).
Linux-visible parts verified green (cargo clippy -p punktfunk-host --all-targets
-- -D warnings). The cfg(windows) deny gates are box-verified next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
1d3b08bca6 |
fix(windows): clippy/build cleanups the on-glass build surfaced (-D warnings)
Built the host crate (`cargo clippy --features nvenc -D warnings`) and the driver workspace (`cargo build`) on the RTX box — the project's intended Windows gate, which `cargo check` (what the goal1/§2.5 work used) never runs. It surfaced lint issues accumulated across the goal1 / §2.5 / this-session Windows work: - 9× redundant `as *mut c_void` after `.as_raw_handle()` (already `*mut c_void`): idd_push.rs (3, this session), service.rs (3, this session), manager.rs (3, pre-existing §2.5 — my OwnedHandle work copied the idiom). Removed the casts + the now-unused `use std::ffi::c_void` in idd_push.rs / manager.rs (service still uses it). - `if_same_then_else` in session_plan.rs::resolve_topology (pre-existing goal1 stage 3): collapsed the two `false` arms into one condition (behavior identical). - `unused_unsafe` in the driver `pod_init!` macro: it expands at call sites already inside an `unsafe` block, where its own `unsafe` is redundant — `#[allow( unused_unsafe)]` (needed at the non-unsafe sites, redundant at the nested ones). After these, BOTH builds are clean on the box — validating the whole session's blind Windows + driver work compiles + passes clippy on real hardware. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ff6af06240 |
refactor(windows-host): KeyedMutexGuard RAII for the IDD-push consume hot loop (Goal-3, hw-validated)
The IDD-push consume loop acquired the slot's keyed mutex by hand (`AcquireSync(0,8)` … work … `ReleaseSync(0)`), with a comment warning that a `?`-return between acquire and release would leak the lock and stall the driver on that slot — the reason the HDR converter is built *before* the acquire. Replace with a `KeyedMutexGuard` RAII (acquire → `ReleaseSync` on drop), scoped to JUST the convert/copy block so the lock releases at the EXACT same point as before (the driver gets the slot back immediately; not held across the rest of `try_consume`). Now the release can't be skipped on any early return/panic — the leak footgun is gone by construction, and the hot loop has no raw `ReleaseSync`. Behavior/latency-equivalent (same acquire params, same release point). Windows- only (CI + on-glass gated); to be validated on the RTX box (host clippy build + a PERF=1 latency A/B vs the shipping binary — the change should show no delta). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c5de4f9fe0 |
refactor(windows-host): RAII for IDD-push handles/views — fix a leak (Goal-3 unsafe reduction #1)
The IDD-push capturer held raw `HANDLE`s for the shared header mapping, the
frame-ready event, the debug section, and each ring slot's shared texture, with
manual `CloseHandle` scattered across two `Drop` impls — and the MapViewOfFile
VIEWS (header/dbg_block) were never UnmapViewOfFile'd (a real view leak).
- New `MappedSection { handle: OwnedHandle, view }` RAII: `Drop` UnmapViewOfFile's
the view THEN the `OwnedHandle` closes the mapping (unmap-before-close).
- `map`+`header` → `section: MappedSection` (+ a cached `header` ptr borrowing into
it, declared after `section` for drop order); same for `dbg_map`+`dbg_block`.
- `event: HANDLE` → `OwnedHandle` (borrowed as `HANDLE(as_raw_handle() as *mut
c_void)` for WaitForSingleObject); `HostSlot.shared` → `OwnedHandle` (its manual
`Drop` deleted). Removed the manual `CloseHandle`s + the `CloseHandle` import.
Net: deletes two `Drop` impls' worth of manual handle/view teardown and fixes the
view leak — fewer unsafe ops, RAII-correct. Behavior preserved (recreate_ring
writes the header in place; the keepalive still drops last so REMOVE is last).
Windows-only (CI-gated); adversarially reviewed (no double-free / UAF / dangling
header; handle interop matches manager.rs). Linux check unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
764c814483 |
refactor: rename pf-vdisplay-proto -> pf-driver-proto (it spans all drivers)
The shared host<->driver ABI crate already contains more than the virtual display: the IDD-push frame ring + control plane AND the gamepad shared-memory layouts (XusbShm / PadShm). "pf-vdisplay-proto" was a misnomer — the name now represents all the drivers it serves. Mechanical rename, no behavior change: - git mv crates/pf-vdisplay-proto -> crates/pf-driver-proto (package name + path-deps in the host crate and the driver workspace). - pf_vdisplay_proto -> pf_driver_proto across host + driver Rust, both Cargo.lock files, the workspace members, the CI path triggers (windows-drivers.yml), and the docs/INF comments. The runtime Global\pfvd-* shared-object names are a SEPARATE contract and are deliberately untouched (host<->driver name matching). - The pf-vdisplay DRIVER crate + its INF service name (Root\pf_vdisplay, UmdfService=pf_vdisplay, pf_vdisplay.dll) are unchanged — only the full `pf_vdisplay_proto` token was replaced, never the `pf_vdisplay` driver name. Linux-verified: cargo test -p pf-driver-proto (const size-asserts compile) + cargo clippy -p punktfunk-host -D warnings clean; Cargo.lock regenerated. The driver-workspace side (path-dep + imports + its Cargo.lock) is Windows-CI-gated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c2ee47cc3b |
refactor(windows-host): delete the SudoVDA backend — pf-vdisplay is the sole vdisplay (Goal 2)
Goal 2 ("drop every trace of SudoVDA") is done. The SudoVDA driver is no longer
shipped (only pf-vdisplay; the old vdisplay-driver tree was deleted in
|
||
|
|
e785591bf7 |
docs(windows-host): consolidate 5 scattered docs into one current source of truth
The Windows-host docs were scattered across a design plan, a staged-refactor plan, an audit, an audit-remediation tracker, and a game-capture-bug analysis — several badly stale (the audit/remediation predate the Goal-1 branch landing and call DONE items "not started"). Verified the true state of every audit finding / goal / milestone against current code+git (4-agent workflow), then rewrote windows-host-rewrite.md as ONE consolidated, accurate doc: - §1 Status scorecard (Goals 1-3, M0-M6, GB1, audit P0/P1/P2) with DONE/PARTIAL/ OPEN + commit evidence. - §2 Architecture as-built (layering, HostConfig→SessionPlan→SessionContext, the VirtualDisplayManager ownership model, IDD-push-primary capture incl. secure desktop + GB1 recovery, encode/EncoderCaps, pf-vdisplay-proto, the driver, service/packaging). - §3 Validated invariants (the jewels). - §4 Prioritized open tasks (the genuine remaining work). - §5 Operations (RTX-box recipe, CI, env, build). - §6 Deep reference (/INTEGRITYCHECK answer, the 6 iddcx bindgen knobs, the driver port checklist, resolved decisions). Deleted the four now-redundant docs (content folded in; history in git): windows-host-goal1-plan.md, windows-host-rewrite-audit.md, windows-host-rewrite-remediation.md, windows-host-rewrite-game-capture-bug.md. Repointed the 6 code/proto/driver doc-comment refs that targeted them at the consolidated windows-host-rewrite.md sections. Linux cargo check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
43ede380a0 |
refactor(windows-host): §2.5 step 1 — delete the dead/write-only monitor-lifecycle code
Removes the cruft the §2.5 ownership-model rewrite would otherwise carry forward, and corrects a
false invariant the docs described:
* CURRENT_MON_GEN (sudovda) — the "current monitor generation" global was WRITE-ONLY. It was
stored on every mgr_acquire (both backends) but its only reader, idd_push's `my_gen`, was set
and NEVER read. The "session capturer re-checks the monitor gen each frame and bails on a
reconnect" behaviour the doc describes was never wired — per-frame staleness is the SEPARATE
ring FrameToken.generation / IDD_GENERATION mechanism (which works and is untouched). So the
monitor-gen-via-WinCaptureTarget carry the design proposed is unnecessary. Deleted the static,
its stores in both backends, the pf_vdisplay import, and idd_push's dead `my_gen` field/read.
(MON_GEN — the lease-generation counter behind the stale-lease no-op — is REAL and kept.)
* IDD_PERSIST + open_or_reuse + IddReuseHandle (idd_push) — a persistent-capturer reuse path
from an early prototype, defined but with ZERO callers across the crate. Deleted, plus the now
-orphaned `use std::sync::Mutex` and the now-dead `set_client_10bit` setter.
Windows-only; grep confirms no remaining references to any deleted symbol. Box build to follow.
First of the incremental §2.5 steps (user-approved OnceLock VirtualDisplayManager design).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
fced221684 |
refactor(windows-host): confine platform code under windows/ + linux/ folders (Goal-1 stage 6)
Move 36 platform-specific files into per-module `windows/` and `linux/` subfolders (and the
shared HID codecs into `inject/proto/`):
capture/{windows,linux}/ encode/{windows,linux}/ inject/{windows,linux,proto}/
audio/{windows,linux}/ vdisplay/{windows,linux}/
src/windows/ (service, wgc_helper, win_adapter, win_display)
src/linux/ (dmabuf_fence, drm_sync, zerocopy/)
Done with `#[path]`, NOT a module rename: every file moves into its folder while the
`crate::*::*` module names stay FLAT, so all caller paths and every internal `super::`/`crate::`
reference are unchanged — only the parent `mod` decls gained `#[path = "..."]`. This is the
codebase's existing pattern (inject's gamepad_windows) and makes the move byte-identical in
behaviour with ZERO reference churn, far lower risk than collapsing to a single
`crate::capture::windows::` namespace (that deeper rename is an optional follow-on; this delivers
the cfg-sprawl folder confinement the stage is about). Done LAST, after the semantic stages, so
the path churn didn't fight them.
Verified: Linux cargo check + clippy (-D warnings) clean; my mod-decl changes fmt-clean (the 3
remaining fmt diffs are pre-existing local-rustfmt-version skew that moved with their files); all
36 `#[path]` targets exist; no internal `#[path]`/`include!`/file-child-mod in any moved file
(the inline `mod X {` blocks are self-contained). Box build to follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|