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, 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 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-vkdecode(Vulkan Video),pf-vaapi(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 it isSendInputplus the UMDF gamepad and mouse 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/1 | 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 | UDP, GF(2¹⁶) Leopard FEC, AES-GCM | 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.
Control plane and management API
The management API (crates/punktfunk-host/src/mgmt) is axum over HTTPS on the host's identity
certificate, versioned under /api/v1. It takes two credentials:
- a paired client's certificate, for the read-only LAN surface (status, the game library);
- the bearer token in the host's
mgmt-tokenfile, accepted only from loopback, for everything else.
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
web/messages/en.json and de.json. 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 for the DualSense, DualShock 4, DualSense Edge and Steam Deck pads. |
pf-xusb | Xbox 360 pad visible to XInput, with no kernel bus driver. |
pf-mouse | Resident virtual HID mouse, so Windows draws a cursor on a headless host. |
wdk-iddcx, pf-umdf-util | IddCx bindings and the safe UMDF layer the drivers share. |
crates/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
| Path | What |
|---|---|
crates/punktfunk-core | Wire format, FEC, crypto, pacing, the QUIC control plane (quic feature) and the C ABI |
crates/punktfunk-host | The host: sessions, both protocol planes, management API, game library, CLI |
crates/pf-vdisplay | Virtual outputs, one backend per compositor plus the Windows driver backend |
crates/pf-capture | PipeWire portal capture (Linux) and driver push (Windows) behind Capturer |
crates/pf-encode, pf-encode-win | Encoder backends on Linux and Windows behind one Encoder trait |
crates/punktfunk-encode-worker | Separate binary that runs PyroWave at raised GPU priority |
crates/pf-zerocopy | CUDA, EGL and Vulkan dmabuf import and fence sync (Linux) |
crates/pf-inject | Keyboard, mouse, pen and virtual-gamepad injection |
crates/pf-clipboard | Shared clipboard backends and the clipboard plane |
crates/pf-frame, pf-gpu, pf-win-display | Frame vocabulary; GPU selection; Windows display topology |
crates/pf-host-config | The settings registry, its store and the env knobs |
crates/pf-paths, pf-paths-win | Config directory and owner-private files; Windows DACL checks |
crates/pf-update, pf-update-check | Root helper for console-started updates; signed update manifest |
crates/pf-driver-proto | Host↔driver contract |
crates/pyrowave-sys, libvpl-sys | Vendored PyroWave and Intel VPL, built from source |
crates/punktfunk-tray | Tray status icon |
crates/punktfunk-setup, punktfunk-setup-win | Guided Linux installer (install.sh fetches it); Windows installer wizard |
crates/pf-client-core | Client plumbing: session pump, decode ladder, audio, gamepads, trust, discovery |
crates/pf-bitstream | Access-unit parsing and decode plans for H.264, HEVC and AV1 |
crates/pf-vkdecode, pf-vaapi, pf-dxvadec, pf-libva | Native decode rungs and the dlopen'd libva |
crates/pf-presenter | SDL3 window, Vulkan swapchain, input capture |
crates/pf-console-ui | Skia console: stats overlay, gamepad home, library, settings |
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 and tvOS; 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 |
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/ | Measurement tools: loss-harness, latency-probe, cursor-probe, display-disturb |
ci/, scripts/ | CI builder images; install, packaging and CI scripts |
The browser client and the LG webOS client 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_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 host never scales a virtual display. Each session gets an output at the client's
WxH@Hz; every compositor keeps its own backend behind theVirtualDisplaytrait. 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/1. - Each version moves alone.
WIRE_VERSIONchanges only when the handshake or a plane breaks, and hosts equality-check it.ABI_VERSIONtracks the C surface;PROTOCOL_VERSIONinpf-driver-prototracks the driver. Never ride one bump on another.