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, 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 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-vkdecode (Vulkan Video), pf-vaapi (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 it is SendInput plus the UMDF gamepad and mouse 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/1GameStream
ClientsThe native clientsMoonlight
OnAlwaysserve --gamestream (trusted LAN only)
ControlQUIC: SPAKE2 pairing, mode changes, clock sync, adaptive bitrate, clipboardHTTPS pairing (nvhttp), RTSP, ENet
DataUDP, GF(2¹⁶) Leopard FEC, AES-GCMUDP, 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.

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-token file, 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:

CrateRole
pf-vdisplayIddCx virtual display. Encodes in WUDFHost and publishes access units to the host.
pf-gamepadHID minidriver for the DualSense, DualShock 4, DualSense Edge and Steam Deck pads.
pf-xusbXbox 360 pad visible to XInput, with no kernel bus driver.
pf-mouseResident virtual HID mouse, so Windows draws a cursor on a headless host.
wdk-iddcx, pf-umdf-utilIddCx 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

PathWhat
crates/punktfunk-coreWire format, FEC, crypto, pacing, the QUIC control plane (quic feature) and the C ABI
crates/punktfunk-hostThe host: sessions, both protocol planes, management API, game library, CLI
crates/pf-vdisplayVirtual outputs, one backend per compositor plus the Windows driver backend
crates/pf-capturePipeWire portal capture (Linux) and driver push (Windows) behind Capturer
crates/pf-encode, pf-encode-winEncoder backends on Linux and Windows behind one Encoder trait
crates/punktfunk-encode-workerSeparate binary that runs PyroWave at raised GPU priority
crates/pf-zerocopyCUDA, EGL and Vulkan dmabuf import and fence sync (Linux)
crates/pf-injectKeyboard, mouse, pen and virtual-gamepad injection
crates/pf-clipboardShared clipboard backends and the clipboard plane
crates/pf-frame, pf-gpu, pf-win-displayFrame vocabulary; GPU selection; Windows display topology
crates/pf-host-configThe settings registry, its store and the env knobs
crates/pf-paths, pf-paths-winConfig directory and owner-private files; Windows DACL checks
crates/pf-update, pf-update-checkRoot helper for console-started updates; signed update manifest
crates/pf-driver-protoHost↔driver contract
crates/pyrowave-sys, libvpl-sysVendored PyroWave and Intel VPL, built from source
crates/punktfunk-trayTray status icon
crates/punktfunk-setup, punktfunk-setup-winGuided Linux installer (install.sh fetches it); Windows installer wizard
crates/pf-client-coreClient plumbing: session pump, decode ladder, audio, gamepads, trust, discovery
crates/pf-bitstreamAccess-unit parsing and decode plans for H.264, HEVC and AV1
crates/pf-vkdecode, pf-vaapi, pf-dxvadec, pf-libvaNative decode rungs and the dlopen'd libva
crates/pf-presenterSDL3 window, Vulkan swapchain, input capture
crates/pf-console-uiSkia console: stats overlay, gamepad home, library, settings
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 and tvOS; 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
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/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() 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 host never scales a virtual display. Each session gets an output at the client's WxH@Hz; every compositor keeps its own backend behind the VirtualDisplay trait. 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_VERSION changes only when the handshake or a plane breaks, and hosts equality-check it. ABI_VERSION tracks the C surface; PROTOCOL_VERSION in pf-driver-proto tracks the driver. Never ride one bump on another.

On this page