Architecture
The crates and clients, how a frame and an input event travel through them, and the rules every change keeps.
A map of the code: the parts, the path of a frame and an input event, and the invariants a change must keep. The user-level version is How it works.
A frame, host to glass
- Mode. The client's Hello names
WxH@Hz. Thepf-vdisplaycrate creates a virtual output at that mode through the compositor's backend (KWin, Mutter, Hyprland, niri, Umbriel, wlroots, gamescope) or, on Windows, through the IddCx driver of the same name. - Capture. On Linux,
pf-capturetakes dmabufs straight from the compositor overext-image-copy-capture-v1where it can, and otherwise over PipeWire (the ScreenCast portal, or KWin's or niri's own screencast).pf-zerocopyimports them into CUDA or Vulkan without a CPU copy. - Encode. On Linux,
pf-encodeopens one backend: NVENC, Vulkan Video, VAAPI, PyroWave or software openh264 (PUNKTFUNK_ENCODERpins one). On Windows the driver encodes inside WUDFHost withpf-encode-win(NVENC, AMF, QSV, Media Foundation, PyroWave) and hands access units to the host through a shared section defined inpf-driver-proto. - Send.
punktfunk-coresplits each access unit into packets, adds FEC, seals them with AES-GCM (ChaCha20-Poly1305 for clients without hardware AES), and the host paces them out over UDP on native threads. - Decode. The client's session pump (
pf-client-core) reassembles and decrypts, then walks the decode ladder (pf-client-video):pf-vkdecode(Vulkan Video),pf-vaapiorpf-v4l2dec(Linux) orpf-dxvadec(Windows) overpf-bitstream's access-unit plans, then CPU openh264 or rav1d. PyroWave decodes in Vulkan compute. - Present.
pf-presenterdraws the picture into an SDL3 window through an ash swapchain, andpf-console-uicomposites the Skia overlay on top. Apple decodes with VideoToolbox and draws with Metal; Android usesAMediaCodec. Both linkpunktfunk-core.
Audio rides the same session: desktop audio goes to the client as Opus, the client's microphone comes back.
An input event, glass to host
- The client captures keyboard, mouse, touch, pen and gamepads (SDL3 in
pf-presenteron the desktop clients). punktfunk-coreencodes them as input events and sends them as QUIC datagrams.- The host hands them to
pf-inject. On Linux that is libei, KWin fake input or the wlroots virtual pointer and keyboard, plus virtual pads over uhid or USB/IP. On Windows mouse and keyboard leave as reports from the UMDF HID mouse driver, withSendInputfor text and as the fallback, plus the UMDF gamepad drivers. - Rumble, lights and other HID output go back to the client on the same session.
Two protocols
Both run in one punktfunk-host serve process.
punktfunk/2 | GameStream | |
|---|---|---|
| Clients | The native clients | Moonlight |
| On | Always | serve --gamestream (trusted LAN only) |
| Control | QUIC: SPAKE2 pairing, mode changes, clock sync, adaptive bitrate, clipboard | HTTPS pairing (nvhttp), RTSP, ENet |
| Data | The QUIC port's UDP path, GF(2¹⁶) Leopard FEC, keys from the TLS exporter | UDP, GF(2⁸) Reed–Solomon FEC |
| Discovery | mDNS _punktfunk._udp | mDNS _nvstream._tcp |
| Code | punktfunk-core, punktfunk-host/src/native* | punktfunk-host/src/gamestream |
--no-default-features --features pyrowave builds a host without the GameStream code. A third
plane, WebTransport for the browser client, is off unless --webtransport is passed. Port numbers:
Ports.
punktfunk/2 is ALPN pkf2, and timestamps run on a per-session clock. The browser plane serves
it at /pf2. A client that offers only pkf1 is closed with the wire-version code, which it shows
as "update both".
Control plane and management API
The management API (crates/host/punktfunk-host/src/mgmt) is axum over HTTPS on the host's identity
certificate, versioned under /api/v1. It takes three credentials:
- a paired device, by its client certificate or a browser's device token, from anywhere: status reads, log upload, host actions and ending its own games;
- a plugin's scoped bearer token, from loopback only: an allowlist of admin routes;
- the admin bearer token in the host's
mgmt-tokenfile, from loopback only: everything.
utoipa generates the OpenAPI spec (punktfunk-host openapi → api/openapi.json). The web console,
the SDK, punktfunk-host ctl and the plugin runner all talk to it. Endpoints and auth in detail:
Management API.
Web console
web/ is a TanStack Start app, server-rendered on Bun (Nitro bun preset). It serves HTTPS with
the host's own certificate and gates every request behind a password and a sealed session cookie.
Its server holds the management token and proxies /api/** to the loopback API, so the token never
reaches the browser. The API client is generated by orval from api/openapi.json; strings live in
l10n/messages/web/, with the shared ones in l10n/messages/common/ and host/. Setup: Build from source.
Scripting runner and plugins
sdk/ is @punktfunk/host, the TypeScript client for the management API, plus the runner
punktfunk-scripting (sdk/src/runner-cli.ts). The runner is a Bun process: a systemd user unit on
Linux, the PunktfunkScripting scheduled task as LocalService on Windows. It runs loose scripts
from ~/.config/punktfunk/scripts/ and punktfunk-plugin-* packages from
~/.config/punktfunk/plugins/. On Linux each plugin gets its own bwrap sandbox
(sdk/src/sandbox.ts) and a scoped plugin token. plugin-kit/ (@punktfunk/plugin-kit) is the
authoring kit; plugins add game-library sources and automation. Writing one:
Writing plugins.
Windows drivers
packaging/windows/drivers/ is its own cargo workspace, built with the WDK:
| Crate | Role |
|---|---|
pf-vdisplay | IddCx virtual display. Encodes in WUDFHost and publishes access units to the host. |
pf-gamepad | HID minidriver that presents the pad the host names: PlayStation, Xbox, Steam, Switch, 8BitDo and Hori models. |
pf-xusb | Xbox 360 pad visible to XInput, with no kernel bus driver. |
pf-mouse | Resident virtual HID mouse and keyboard: Windows draws a cursor on a headless host, and input reaches raw input as a device's. |
wdk-iddcx, pf-umdf-util | IddCx bindings and the safe UMDF layer the drivers share. |
crates/host/pf-driver-proto is the host↔driver contract, a path dependency of both workspaces. The
host refuses a driver older than MIN_DRIVER_PROTOCOL_VERSION, so the installer ships host and
driver together. packaging/windows/pf-vkhdr-layer is a Vulkan layer that lets Vulkan games turn on
HDR over the virtual display.
Where the code lives
Rust crates sit in crates/<group>/. core and codec serve both sides. client and host
never depend on each other; scripts/ci/check-crate-groups.py fails CI if one does. install
holds the installers, the tray and the update helper.
| Path | What |
|---|---|
crates/core/punktfunk-core | Wire format, FEC, crypto, pacing and the QUIC control plane (quic feature) |
crates/core/pf-dmabuf | dmabuf fence wait and read-only mapping, shared by host and clients (Linux) |
crates/core/pf-update-check | Signed update manifest, shared by the host, the update helper and the clients |
crates/core/pf-fluent | WinUI theme colours and motion for the Windows client and installer |
crates/codec/pf-bitstream | Access-unit parsing and decode plans for H.264, HEVC and AV1 |
crates/codec/pf-vkdecode, pf-vaapi, pf-dxvadec | Native decode rungs: Vulkan Video, VAAPI buffers, D3D11VA |
crates/codec/pf-libva | The dlopen'd libva the VAAPI decoder and encoder share |
crates/codec/pf-v4l2, pf-v4l2dec | V4L2 decode: the ioctl half; the uAPI and decoder flows, CPU-testable |
crates/codec/pyrowave-sys, libvpl-sys | Vendored PyroWave and Intel VPL, built from source |
crates/client/punktfunk-ffi | The C ABI over the core, and the demo-mode loopback host |
crates/client/pf-client-core | Client plumbing: session pump, audio, gamepads, trust, discovery |
crates/client/pf-client-video | Decode ladder, Vulkan device handoff, colour maths and PyroWave decode |
crates/client/pf-presenter | SDL3 window, Vulkan swapchain, input capture |
crates/client/pf-console-ui | Skia console: stats overlay, gamepad home, library, settings |
crates/host/punktfunk-host | The host: sessions, both protocol planes, management API, game library, CLI |
crates/host/punktfunk-encode-worker | Separate binary that runs PyroWave and the NVIDIA frame convert at raised GPU priority |
crates/host/punktfunk-capture-worker | Windows Graphics Capture and the encoder for a mirror or shared-screen session, as the signed-in user |
crates/host/pf-vdisplay | Virtual outputs, one backend per compositor plus the Windows driver backend |
crates/host/pf-capture | Capture behind Capturer: PipeWire and direct ext-image-copy-capture-v1 (Linux); driver push, WGC and remote encoders (Windows) |
crates/host/pf-encode-core | The Encoder trait and the policy every backend shares |
crates/host/pf-encode, pf-encode-win | Encoder backends on Linux and Windows |
crates/host/pf-encode-session | Windows pool slot to encoder to access-unit section, shared by the display driver and the capture worker |
crates/host/pf-zerocopy | CUDA, EGL and Vulkan dmabuf import (Linux) |
crates/host/pf-inject | Keyboard, mouse, pen and virtual-gamepad injection |
crates/host/pf-portal | The one tokio runtime every portal call runs on (Linux) |
crates/host/pf-clipboard | Shared clipboard backends and the clipboard plane |
crates/host/pf-audio | Desktop audio capture, the virtual microphone, pad audio and the Windows wiring plan |
crates/host/pf-frame, pf-gpu, pf-win-display | Frame vocabulary; GPU selection; Windows display topology |
crates/host/pf-host-config | The settings registry, its store and the env knobs |
crates/host/pf-paths, pf-paths-win | Config directory and owner-private files; Windows DACL checks |
crates/host/pf-driver-proto | Host↔driver contract |
crates/host/pf-seats, punktfunk-seats | Seat supervisor: inside the service on Windows; the punktfunk-seats root daemon and its pad broker on Linux |
crates/host/pf-seat-keeper, pf-seat-audio | A Windows seat's RDP keeper, its own workspace for IronRDP's lockfile; the seat's audio endpoint DLL |
crates/install/punktfunk-setup, punktfunk-setup-win | Guided Linux installer (install.sh fetches it); Windows installer wizard |
crates/install/punktfunk-tray | Tray status icon |
crates/install/pf-update | Root helper for console-started updates |
clients/session | punktfunk-session, the Vulkan stream every desktop front-end spawns |
clients/linux, clients/windows | GTK4 and WinUI 3 shells (punktfunk-client) |
clients/cli | punktfunk, the headless CLI |
clients/probe | punktfunk-probe, the reference client and measurement tool |
clients/apple | Swift app for macOS, iOS, iPadOS, tvOS and visionOS; native/ is its static library |
clients/android | Kotlin app for phone and TV; native/ is its JNI core |
clients/decky | Steam Deck plugin that launches the CLI |
clients/shared | Test vectors every client checks against |
web/, sdk/, plugin-kit/ | Web console; SDK and runner; plugin authoring kit |
l10n/ | Every message a person reads, and pf-l10n, which compiles and checks them |
api/openapi.json, include/punktfunk_core.h | Generated API spec and C header, checked in |
packaging/ | apt, rpm, Arch, Flatpak, Bazzite, bootc, Nix, Windows installer and drivers, winget |
docs-site/ | This site |
tools/ | gen-headers; probes and measurement tools such as loss-harness, scroll-probe, cursor-probe, wl-scanout-probe, wgc-probe and display-disturb; the latency-probe glass timer page |
ci/, scripts/ | CI builder images; install, packaging and CI scripts |
The browser, LG webOS, Nintendo Switch and PlayStation 5 clients live in their own repositories and pin these crates as git dependencies.
Design invariants
- One core, linked everywhere. Protocol, FEC and crypto live once in
punktfunk-core, behind a versioned C ABI (punktfunk-ffi).punktfunk_abi_version()andPunktfunkConfig.struct_sizelet an embedder detect a mismatch instead of corrupting a struct. - No async on the hot path. The per-frame pipeline runs on native threads.
tokioandquinnsit behind the off-by-defaultquicfeature and serve the control plane only. - The virtual display runs at the client's mode. Each session gets an output at the client's
WxH@Hz; every compositor keeps its own backend behind theVirtualDisplaytrait. When the client names a smaller view (render scale above 1×), the host scales the picture down before it encodes; any other scaling happens in the client's presenter. - FEC sized to the protocol. GF(2⁸) (at most 255 shards a block) for Moonlight; GF(2¹⁶) (up
to 65535 shards, O(n log n)) for
punktfunk/2. - Each version moves alone. The ALPN (
pkf2) versions the wire.ABI_VERSIONtracks the C surface;PROTOCOL_VERSIONinpf-driver-prototracks the driver. Never ride one bump on another.