PunktfunkPunktfunk

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

  1. Mode. The client's Hello names WxH@Hz. The pf-vdisplay crate 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.
  2. Capture. On Linux, pf-capture takes dmabufs straight from the compositor over ext-image-copy-capture-v1 where it can, and otherwise over PipeWire (the ScreenCast portal, or KWin's or niri's own screencast). pf-zerocopy imports them into CUDA or Vulkan without a CPU copy.
  3. Encode. On Linux, pf-encode opens one backend: NVENC, Vulkan Video, VAAPI, PyroWave or software openh264 (PUNKTFUNK_ENCODER pins one). On Windows the driver encodes inside WUDFHost with pf-encode-win (NVENC, AMF, QSV, Media Foundation, PyroWave) and hands access units to the host through a shared section defined in pf-driver-proto.
  4. Send. punktfunk-core splits 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.
  5. 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-vaapi or pf-v4l2dec (Linux) or pf-dxvadec (Windows) over pf-bitstream's access-unit plans, then CPU openh264 or rav1d. PyroWave decodes in Vulkan compute.
  6. Present. pf-presenter draws the picture into an SDL3 window through an ash swapchain, and pf-console-ui composites the Skia overlay on top. Apple decodes with VideoToolbox and draws with Metal; Android uses AMediaCodec. Both link punktfunk-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

  1. The client captures keyboard, mouse, touch, pen and gamepads (SDL3 in pf-presenter on the desktop clients).
  2. punktfunk-core encodes them as input events and sends them as QUIC datagrams.
  3. 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, with SendInput for text and as the fallback, plus the UMDF gamepad drivers.
  4. 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/2GameStream
ClientsThe native clientsMoonlight
OnAlwaysserve --gamestream (trusted LAN only)
ControlQUIC: SPAKE2 pairing, mode changes, clock sync, adaptive bitrate, clipboardHTTPS pairing (nvhttp), RTSP, ENet
DataThe QUIC port's UDP path, GF(2¹⁶) Leopard FEC, keys from the TLS exporterUDP, GF(2⁸) Reed–Solomon FEC
DiscoverymDNS _punktfunk._udpmDNS _nvstream._tcp
Codepunktfunk-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-token file, 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:

CrateRole
pf-vdisplayIddCx virtual display. Encodes in WUDFHost and publishes access units to the host.
pf-gamepadHID minidriver that presents the pad the host names: PlayStation, Xbox, Steam, Switch, 8BitDo and Hori models.
pf-xusbXbox 360 pad visible to XInput, with no kernel bus driver.
pf-mouseResident 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-utilIddCx 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.

PathWhat
crates/core/punktfunk-coreWire format, FEC, crypto, pacing and the QUIC control plane (quic feature)
crates/core/pf-dmabufdmabuf fence wait and read-only mapping, shared by host and clients (Linux)
crates/core/pf-update-checkSigned update manifest, shared by the host, the update helper and the clients
crates/core/pf-fluentWinUI theme colours and motion for the Windows client and installer
crates/codec/pf-bitstreamAccess-unit parsing and decode plans for H.264, HEVC and AV1
crates/codec/pf-vkdecode, pf-vaapi, pf-dxvadecNative decode rungs: Vulkan Video, VAAPI buffers, D3D11VA
crates/codec/pf-libvaThe dlopen'd libva the VAAPI decoder and encoder share
crates/codec/pf-v4l2, pf-v4l2decV4L2 decode: the ioctl half; the uAPI and decoder flows, CPU-testable
crates/codec/pyrowave-sys, libvpl-sysVendored PyroWave and Intel VPL, built from source
crates/client/punktfunk-ffiThe C ABI over the core, and the demo-mode loopback host
crates/client/pf-client-coreClient plumbing: session pump, audio, gamepads, trust, discovery
crates/client/pf-client-videoDecode ladder, Vulkan device handoff, colour maths and PyroWave decode
crates/client/pf-presenterSDL3 window, Vulkan swapchain, input capture
crates/client/pf-console-uiSkia console: stats overlay, gamepad home, library, settings
crates/host/punktfunk-hostThe host: sessions, both protocol planes, management API, game library, CLI
crates/host/punktfunk-encode-workerSeparate binary that runs PyroWave and the NVIDIA frame convert at raised GPU priority
crates/host/punktfunk-capture-workerWindows Graphics Capture and the encoder for a mirror or shared-screen session, as the signed-in user
crates/host/pf-vdisplayVirtual outputs, one backend per compositor plus the Windows driver backend
crates/host/pf-captureCapture behind Capturer: PipeWire and direct ext-image-copy-capture-v1 (Linux); driver push, WGC and remote encoders (Windows)
crates/host/pf-encode-coreThe Encoder trait and the policy every backend shares
crates/host/pf-encode, pf-encode-winEncoder backends on Linux and Windows
crates/host/pf-encode-sessionWindows pool slot to encoder to access-unit section, shared by the display driver and the capture worker
crates/host/pf-zerocopyCUDA, EGL and Vulkan dmabuf import (Linux)
crates/host/pf-injectKeyboard, mouse, pen and virtual-gamepad injection
crates/host/pf-portalThe one tokio runtime every portal call runs on (Linux)
crates/host/pf-clipboardShared clipboard backends and the clipboard plane
crates/host/pf-audioDesktop audio capture, the virtual microphone, pad audio and the Windows wiring plan
crates/host/pf-frame, pf-gpu, pf-win-displayFrame vocabulary; GPU selection; Windows display topology
crates/host/pf-host-configThe settings registry, its store and the env knobs
crates/host/pf-paths, pf-paths-winConfig directory and owner-private files; Windows DACL checks
crates/host/pf-driver-protoHost↔driver contract
crates/host/pf-seats, punktfunk-seatsSeat supervisor: inside the service on Windows; the punktfunk-seats root daemon and its pad broker on Linux
crates/host/pf-seat-keeper, pf-seat-audioA Windows seat's RDP keeper, its own workspace for IronRDP's lockfile; the seat's audio endpoint DLL
crates/install/punktfunk-setup, punktfunk-setup-winGuided Linux installer (install.sh fetches it); Windows installer wizard
crates/install/punktfunk-trayTray status icon
crates/install/pf-updateRoot helper for console-started updates
clients/sessionpunktfunk-session, the Vulkan stream every desktop front-end spawns
clients/linux, clients/windowsGTK4 and WinUI 3 shells (punktfunk-client)
clients/clipunktfunk, the headless CLI
clients/probepunktfunk-probe, the reference client and measurement tool
clients/appleSwift app for macOS, iOS, iPadOS, tvOS and visionOS; native/ is its static library
clients/androidKotlin app for phone and TV; native/ is its JNI core
clients/deckySteam Deck plugin that launches the CLI
clients/sharedTest 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.hGenerated 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() and PunktfunkConfig.struct_size let an embedder detect a mismatch instead of corrupting a struct.
  • No async on the hot path. The per-frame pipeline runs on native threads. tokio and quinn sit behind the off-by-default quic feature 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 the VirtualDisplay trait. 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_VERSION tracks the C surface; PROTOCOL_VERSION in pf-driver-proto tracks the driver. Never ride one bump on another.

On this page