Adds the SDL3 gamepad service (near-verbatim port of the GTK client's — SDL3 is cross-platform) and wires it into the winit app: per-session capture (buttons/axes, DualSense touchpad + motion 0xCC), feedback (rumble, lightbar, raw DualSense effects), single-pad-forwarded model with auto pad-type from the physical controller. Built from source on Windows (no system SDL3). - gamepad.rs: GamepadService (app-lifetime SDL thread) attach/detach on session connect/end; auto_pref resolves "Automatic" to the attached pad's type. - app.rs: hold the service, attach on Connected, detach on Ended/Failed/close. Also simplify the keydown path (drop the identical if/else arms). - main.rs: start the service for the windowed path, resolve GamepadPref from settings + the physical pad. Build gotcha documented + fixed in the dev loop: SDL3's build-from-source MSVC precompiled-header chokes on the `ü` in the dev box's username embedded in the cargo registry path (MSB8084/C4828) — CARGO_HOME must be an ASCII path (C:\Users\Public\.cargo). Unrelated to our code. Docs: CLAUDE.md M4 + docs/windows-client-bootstrap.md status banner (winit-not-Reactor rationale, CARGO_HOME gotcha, what's pending) + docs-site clients.md "Windows desktop client (in development)". Crate is build + clippy + fmt + test green on x86_64-pc-windows-msvc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
11 KiB
Windows native client — bootstrap handoff
A handoff for an agent picking up the native Windows punktfunk/1 client. The host side is done and live-validated on a real RTX 4090; the client is the remaining piece. This doc is the concrete starting point: the locked decisions, the reference code to port, the stack swaps, the dev loop, and the gotchas. Read it top to bottom, then start at Phase 1 (de-risk Reactor first).
Status — stage 1 landed (2026-06-15)
The client is implemented in crates/punktfunk-client-windows (binary punktfunk-client) and is
build + clippy + fmt + test green on x86_64-pc-windows-msvc (built on the dev VM). Done: winit
window + Direct3D11 flip-model swapchain present (WARP on the GPU-less box; runtime-compiled
fullscreen-triangle shaders, Contain-fit letterbox), FFmpeg software HEVC decode, WASAPI render
- mic capture, keyboard/mouse/wheel capture (physical-
KeyCode→VK, click-to-capture), SDL3 gamepads,mdns-sddiscovery, and the full trust surface (identity + TOFU + SPAKE2 PIN over--connect/--discover/--pair/--headless).
- Reactor was evaluated and rejected (a research pass + the points below): windows-rs Reactor
ships no
SwapChainPaneland noISwapChainPanelNative::SetSwapChainescape hatch, so it cannot host a DXGI presenter. The client uses the doc's sanctioned fallback — winit + a raw D3D11 swapchain on the HWND — which builds and runs against WARP on the GPU-less VM. A native WinUI look would need theSwapChainPanelhatch to land upstream first. - Build gotcha (in addition to the ASCII output path below):
CARGO_HOMEitself must be on an ASCII path (e.g.C:\Users\Public\.cargo). SDL3'sbuild-from-sourcecompiles a precompiled header whose#includeembeds the registry source path; theüin the dev box's username makes MSVC's PCH/structured-output fail (MSB8084/C4828). SetCARGO_HOME=C:\Users\Public\.cargo. - Still pending: live host validation (the dev box has no GPU → glass-to-glass numbers defer to the RTX box), D3D11VA hardware decode + 10-bit/HDR present, a native host-list/settings GUI (CLI flags for now), and RAWINPUT relative-mouse pointer-lock. Phases 4–7 below are the map.
What we're building
A native Windows client that connects to a punktfunk/1 host (serve --native / m3-host), decodes
HEVC, presents it low-latency, plays Opus audio, and captures local mouse/keyboard/gamepad to send
back — i.e. the Windows analogue of the GTK4 Linux client (crates/punktfunk-client-linux),
which is the architectural template. The Windows client is close to a 1:1 port of the Linux client
with the platform layers swapped.
Locked decisions (from the Windows-host/client plan, docs/windows-host.md + project memory)
- Pure Rust.
windows-rs+ Windows App SDK "Reactor" (WinUI 3 from Rust, merged windows-rs PR #4479). No C++/C#. De-risk Reactor +SwapChainPanelFIRST — it's the only novel/uncertain piece; everything else is a known-good port. - Links
punktfunk-coredirectly (Cargo path dep,features = ["quic"]) — no C ABI, exactly like the GTK client.NativeClientis alreadySync(mutexed plane receivers), so it drops into a UI app cleanly. The C ABI (punktfunk_connect+next_au/next_audio/next_rumble/next_hidout/send_input/send_rich_input) is the Apple path; the native Rust clients callcrates/punktfunk-core/src/client.rs(NativeClient) methods directly. - Video widget = WinUI 3
SwapChainPanel(built-in), fed a D3D11 swapchain viaISwapChainPanelNative::SetSwapChain. - Decode = FFmpeg-next + D3D11VA (HEVC; Main10 for 10-bit/HDR — see below).
- Audio playback = WASAPI render + Opus decode (
opuscrate, vendors libopus via cmake; setCMAKE_POLICY_VERSION_MINIMUM=3.5). - Input capture→send: the client captures LOCAL input and sends it. Mouse (abs + relative) +
keyboard via the inverse VK table (port
keymap.rs); gamepad via SDL3 (already a workspace dep, cross-platform) →NativeClient::send_input/send_rich_input. (SendInput/ViGEmare HOST-side injection — not used by the client.) - Discovery =
mdns-sd(cross-platform, browses_punktfunk._udp). - Trust = shared client identity + SPAKE2 PIN pairing + TOFU (port
trust.rs; same identity files/logic as the other native clients).
The reference: crates/punktfunk-client-linux/src/
Port these files (near 1:1; only the platform layers change):
| Linux file | Role | Windows swap |
|---|---|---|
main.rs / app.rs |
app shell, lifecycle | WinUI 3 App/Window via Reactor |
ui_hosts.rs |
host list / connect screen | WinUI 3 page |
ui_settings.rs |
settings | WinUI 3 page |
ui_stream.rs |
the streaming view | WinUI 3 page hosting SwapChainPanel |
video.rs |
FFmpeg decode + present | FFmpeg D3D11VA → D3D11 swapchain in SwapChainPanel |
audio.rs |
Opus decode + playback | WASAPI render (was PipeWire) |
session.rs |
NativeClient connect + plane pumps |
reuse almost verbatim (core is cross-platform) |
trust.rs |
identity, PIN, TOFU | reuse almost verbatim |
discovery.rs |
mDNS browse | reuse verbatim (mdns-sd) |
keymap.rs |
inverse VK table | reuse; Windows VK is the native source so this is simpler |
gamepad.rs |
SDL3 pad capture + rumble/feedback | reuse almost verbatim (SDL3 is cross-platform) |
session.rs, trust.rs, discovery.rs, keymap.rs, gamepad.rs are mostly platform-neutral
(they touch punktfunk-core + SDL3 + mdns, all cross-platform) — expect to reuse them with minimal
changes. The real work is video.rs (D3D11VA + swapchain), audio.rs (WASAPI), and the WinUI shell.
10-bit + HDR (NEW — landed this session, the client MUST handle it)
The host now negotiates and emits HEVC Main10 + BT.2020 PQ HDR10 when the captured desktop is HDR (and 10-bit SDR Main10 when negotiated). The Apple client already does the matching present; the Windows client should mirror it:
- Advertise caps in the
Hello:video_caps = VIDEO_CAP_10BIT | VIDEO_CAP_HDR(crates/punktfunk-core/src/quic.rs). The host enables 10-bit only if the client advertised it. (The native-client connector inclient.rscurrently hardcodesvideo_caps: 0with a TODO — thread the real caps through when you wire decode; or detect HDR purely in-band, see next.) - Detect HDR in-band from the HEVC VUI (transfer characteristics = SMPTE ST 2084 / PQ), exactly
like the Apple client's
VideoDecoder.isHDRFormat(clients/apple/Sources/PunktfunkKit/). This handles a mid-session HDR toggle without renegotiation.Welcome.bit_depth(8/10) is also available. - Decode Main10 → P010 (10-bit) via D3D11VA.
- Present HDR: swapchain in
DXGI_FORMAT_R10G10B10A2_UNORM(orR16G16B16A16_FLOAT),IDXGISwapChain3::SetColorSpace1(DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020)+SetHDRMetaDatafor HDR10; the host's stream is BT.2020 PQ, so present PQ. For SDR, the existingDXGI_FORMAT_B8G8R8A8_UNORM+ BT.709 path. (The host-side HDR conversion math is incrates/punktfunk-host/src/capture/dxgi.rsHDR_PS/HdrConverterif you need the inverse.)
Dev boxes
- No-GPU dev box (UI + connect + software decode):
ssh "Enrico Bühler"@192.168.1.57— Win11 Pro 25H2 (build 26200), QEMU Q35, 8 vCPU/12 GB, no working GPU (so no NVENC, no D3D11VA hardware decode — use FFmpeg software decode here; this box is for UI/connect/protocol work). Has Rust 1.96 MSVC, VS 2026 + VC tools + Win SDK, Win App Runtime 2.2, SudoVDA + Parsec VDD. - Real-GPU box (HDR / hardware decode / end-to-end):
ssh "Enrico Bühler"@192.168.1.174— Win11, RTX 4090, runs the host. Use it to test the client against a live HDR host.
Dev-loop gotchas (both boxes)
- Build under an ASCII path (
C:\Users\Public\…). The username "Enrico Bühler" has aü→ MSVCLNK1201PDB-write failure under~/Developer. - Toolchain gaps:
winget install NASM.NASM Kitware.CMake LLVM.LLVM(aws-lc-rs on the quic path, ffmpeg-sys needs libclang). CMAKE_POLICY_VERSION_MINIMUM=3.5in the build env (CMake 4 rejects libopus's old minimum).- File transfer =
sftp(scp is broken under the PowerShell DefaultShell):printf 'put %s /C:/Users/Public/REL/PATH\n' LOCAL | sftp -b - "Enrico Bühler@192.168.1.57"— note the leading slash/C:/…. Let the VM regenerate its ownCargo.lock(don't transfer it). - Windows clippy is stricter than Linux CI and
cfg(windows)code is excluded from Linux CI → runcargo clippy -p punktfunk-client-windows -- -D warningsON THE VM before committing. - Work on
main; fetch+mergeorigin/mainbefore pushing.
Suggested phased plan
- De-risk Reactor (do this first). A windows-rs Reactor (WinUI 3) hello-world that hosts a
SwapChainPaneland presents a cleared D3D11 swapchain into it. Confirm the windows-rs Reactor version/API (PR #4479) andISwapChainPanelNative::SetSwapChaininterop. If Reactor proves too raw, the fallback iswinit+ a child HWND swapchain, but try Reactor first per the decision. - Crate scaffold.
crates/punktfunk-client-windows,[target.'cfg(windows)'.dependencies]:punktfunk-core { path, features=["quic"] },windows, the Reactor crate,ffmpeg-next,opus,sdl3,mdns-sd,anyhow,tracing. Mirrorcrates/punktfunk-client-linux/Cargo.toml. - Connect + control plane. Port
session.rs+trust.rs; validate headless against the 4090 box (m3-host/serve --native) — handshake, PIN/TOFU, plane counters — before any UI/decode. - Decode + present. FFmpeg D3D11VA →
SwapChainPanel. SDR (8-bit BGRA) first, then P010 + HDR colorspace (see the HDR section). - Audio. WASAPI render + Opus decode (port
audio.rs). - Input. Mouse + keyboard capture→send (port
keymap.rs), gamepad via SDL3 (portgamepad.rs), feedback fromnext_rumble/next_hidout. - Discovery + UI. Port
discovery.rs+ui_hosts.rs+ui_settings.rsto WinUI pages.
Key references
- Template:
crates/punktfunk-client-linux/src/*(the client to port). - Apple HDR present (the pattern to mirror):
clients/apple/Sources/PunktfunkKit/{VideoDecoder, MetalVideoPresenter,Stage2Pipeline}.swift— in-band PQ detection, P010 decode, EDR present. - Core client API:
crates/punktfunk-core/src/client.rs(NativeClient). - Protocol:
crates/punktfunk-core/src/quic.rs(Hello.video_caps,Welcome.bit_depth,VIDEO_CAP_10BIT/VIDEO_CAP_HDR). - Full Windows plan + SudoVDA/host details:
docs/windows-host.md. - Host HDR conversion (for the inverse math):
crates/punktfunk-host/src/capture/dxgi.rs(HDR_PS,HdrConverter) +crates/punktfunk-host/src/encode/nvenc.rs(BT.2020/PQ VUI).