The GTK client becomes a relm4 component tree: AppModel owns the window,
trust gate (rules 1-3), and the spawned session child's lifecycle as
typed messages; the hosts page is a child component with a
FactoryVecDeque of host cards — the HostsCallbacks Rc<dyn Fn> bag, the
busy Cell, and the Rc<RefCell<HostsUi>> cross-page pokes are gone.
Trust/settings/library dialogs stay plain GTK, invoked from update with
a ComponentSender.
Deleted with the in-process presenter: ui_stream.rs, video_gl.rs,
ui_gamepad_library.rs, launch.rs, the khronos-egl dep, and the
PUNKTFUNK_LEGACY_PRESENTER hatch. Every stream (and the console library)
now runs in punktfunk-session; the shell spawns it for card connects and
exec's it for --connect/--browse so the Decky wrapper keeps working. The
request-access flow gains --connect-timeout + a CancelHandle that kills
the child. Screenshot scenes re-pointed onto the components (verified:
hosts + library self-capture; the dialog scenes present correctly and
capture under a GL renderer); the gamepad-library scene is gone with the
page.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The run loop grows a browse mode: the console library idles between
streams in ONE window (no gamescope handoff), overlay actions launch
sessions via run_browse's callback, session end returns to the library.
The Overlay contract gains menu routing (MenuEvent → haptic pulse),
action draining, and session-phase edges.
pf-console-ui ports the GTK launcher wholesale: the coverflow's springs,
cursor arithmetic and recede/tilt constants move verbatim with their
tests (plus new projection tests — focused-card centering, the inner-
edge-recedes corridor); paint order is draw order (the gtk::Fixed
restack hack is gone); the aurora renders as an SkSL runtime shader at
full rate on every box (the 30 Hz CPU-upscale path and its frozen-on-
Deck fallback are deleted — the generated SkSL is compile-tested);
titles/scenes shape through textlayout (CJK-safe). Poster art streams
in as encoded bytes through the shared model and decodes renderer-side.
The session binary wires --browse host[:port] [--mgmt PORT]: KnownHosts
lookup (unpaired renders the pair-first scene), library + art fetch on
threads, PUNKTFUNK_FAKE_LIBRARY dev hook, and a session_params helper
shared with --connect. The minimal build refuses --browse cleanly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The §6.1 presenter↔console-UI contract lands: pf-presenter exposes its
device (SharedDevice) and composites at most one premultiplied-alpha
quad per frame (new overlay.frag + LOAD render pass over the swapchain;
zero cost while the overlay returns None). pf-console-ui implements it
with skia-safe on the shared VkDevice: DirectContext via the ash
dispatch chain, a ring of two offscreen render targets (one-frame-in-
flight safe), damage-driven redraws — the OSD re-renders at 1 Hz, the
hint on capture toggles, nothing per-frame. Skia never touches the
swapchain. The session binary carries it behind the default ui feature:
4.9 MB stripped without, 10 MB with (measured).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
punktfunk-session streams one --connect session in an SDL3 window: ash
swapchain with a transfer-only letterboxed blit of the software-decode
path (no graphics pipeline until the phase-2 dmabuf/CSC pass), the
ui_stream input-capture state machine on SDL events (scancode→VK table
cross-checked against the evdev one), gamepads via a new caller-pumped
GamepadService mode (SDL video owns the main thread here), and the
shell↔session stdout contract: {"ready":true}, per-window stats:
lines, JSON error + exit codes 0/2/3/4. Strict trust — no pin, no
connect. Design: punktfunk-planning linux-client-rearchitecture.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Session pump, FFmpeg decode, PipeWire audio, SDL3 gamepads, keymap, trust
store, mDNS discovery, library client and Wake-on-LAN move verbatim from
clients/linux into crates/pf-client-core, shared with the upcoming Vulkan
session binary (punktfunk-planning: linux-client-rearchitecture.md).
The GTK client re-exports them at the crate root so every existing
crate::-path keeps resolving; its manifest drops the moved-only deps.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Release 0.8.2: native AMF encoder reconnect reliability on Windows AMD — a
second connection no longer comes up black (the encoder now Flushes before
Terminate so a reconnect's overlapping teardown can't strand AMD's VCN encode
session, self-heals with a full context rebuild when a reconnect still wedges,
and logs a per-context bring-up number + first-AU line so a silent wedge is
visible); a native data-plane hardening pass that keeps the stream alive across
real Wi-Fi links; Android streaming wake/Wi-Fi locks that actually engage plus a
console-UI polish pass (per-controller glyphs, scrollable dialogs, animated
forms); and gamescope-takeover survival on Bazzite's SDDM session supervisor.
Also a Linux-client crash fix — the FlowBox activation cycle that stack-
overflowed on every host-card click.
The [workspace.package] version (inherited by every crate via version.workspace)
is the release being cut; refresh the 9 workspace entries in Cargo.lock to match
(CI builds --locked). Canary derives from the tag (scripts/ci/pf-version.sh), so
cutting v0.8.2 auto-advances canary to 0.9.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The baseline stream held only WIFI_MODE_FULL_HIGH_PERF, which is deprecated
AND non-functional on recent Android — so with the low-latency toggle off (the
default) Wi-Fi power save stayed fully active: downlink delivery clumped at
beacon intervals (a few hundred ms of latency mush, sawtoothing bitrate) and
the AP's power-save buffer periodically overflowed, killing whole frames every
few seconds (the host log's alternating loss_ppm=0/50000). Now every stream
holds FULL_LOW_LATENCY (API 29+, the only effective power-save disable;
foreground + screen-on, which a stream always is) AND FULL_HIGH_PERF (covers
older releases) — the same pair Moonlight holds. The experimental toggle no
longer selects the lock mode.
Also: declare tracing's "log" feature explicitly in the native crate (core
transport warnings → logcat must not hinge on quinn's default features), and
align the low-latency toggle's copy with its actual scope.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Release 0.8.1: game-mode + dedicated game sessions, zero-copy GPU import
process-isolation (and zero-copy on by default on all backends), user-defined
custom display presets, and a physical-monitor refresh-preservation fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The [workspace.package] version (inherited by every crate via version.workspace)
is the release being cut; refresh the 9 workspace entries in Cargo.lock to match
(CI builds --locked). Canary derives from the tag (scripts/ci/pf-version.sh), so
cutting v0.8.0 auto-advances canary to 0.9.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
VAAPI decode stays; what changes is who touches the YUV. The direct path hands
the NV12 dmabuf (tiled AMD modifier since Mesa 25.1) to GdkDmabufTexture, and
GTK's tiled-NV12 import renders corrupt/gray/washed-out on the Deck. Moonlight
and mpv are clean on the same box because they import the dmabuf into their own
EGL context and convert with their own shader — video_gl.rs is that
architecture for the GTK client: per-plane EGLImages (R8 + GR88, modifier
passed through) → our YUV→RGB shader (matrix/range from the stream's CICP
signaling, unit-tested) → RGBA texture in a GdkGLContext-shared context →
fence-synced GdkGLTexture. GTK composites plain RGBA; no YUV negotiation, no
compositor CSC.
The Deck's decoder default flips back to hardware (the software stopgap is
gone); desktops keep the direct dmabuf path (offload/scan-out eligible).
PUNKTFUNK_PRESENT=direct|gl overrides either way. New failure ladder: GL
converter init failure or a convert-error streak raises a shared flag and the
session pump demotes the decoder to software with a keyframe re-request — the
same mechanism also closes the old silent-black-screen gap where a rejected
dmabuf import had no recovery at all.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a runtime-free Wake-on-LAN sender in punktfunk-core (per-interface subnet-directed broadcast + 255.255.255.255 on ports 9/7, repeated, optional last-known-IP unicast) exposed both as a Rust fn and a punktfunk_wake_on_lan C-ABI (ABI v3), plus a parse_mac helper. The host enumerates its wake-capable NIC MAC(s) and advertises them in a new mDNS `mac` TXT record (routed NIC first), and best-effort detects & warns (never modifies) when the NIC isn't armed for WoL.
MAC delivery is via the unauthenticated mDNS TXT rather than the connection handshake by design: a spoofed MAC only makes a wake fail (the packet is inert; the cert fingerprint still gates the connection), and it avoids threading through the hot connect path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ship the flatpak PipeWire-socket audio fix (d1af630) to the stable channel —
a tag is required (main pushes only publish the canary flatpak branch), and
0.7.1 stable users on the Deck have no client audio until this lands. Bump
[workspace.package] + the 9 Cargo.lock workspace entries (CI builds --locked).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cut 0.7.1 to test the GTK + Decky polish batch (7eea983) on-device: the
host-click / disconnect-chord / gray-screen-recovery / leak fixes on the
Linux client, the Deck launcher perf profile, and the Decky pin/pairing
fixes. The [workspace.package] version (inherited by every crate via
version.workspace) is the release being cut; refresh the 9 workspace
entries in Cargo.lock to match (CI builds --locked). Canary derives from
the tag (scripts/ci/pf-version.sh), so cutting v0.7.1 auto-advances canary.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The [workspace.package] version (inherited by every crate via
version.workspace) is the release being cut. Refresh the 9 workspace entries
in Cargo.lock to match (CI builds --locked). Canary derives from the tag now
(scripts/ci/pf-version.sh), so no canary-base edit is needed — cutting v0.7.0
auto-advances canary to 0.8.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Punktfunk Host" identity work added winresource to the host crate but
didn't update the lock, so every --locked CI job failed to resolve.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New crates/punktfunk-tray — a small per-user companion showing the host service
state at a glance (running / stopped / starting / degraded / failed + the live
session in the tooltip) with one-click actions: open web console, approve a
pending pairing request, start/stop/restart, open logs. No more digging through
logs to learn whether the service came back after a reboot or an update.
Status is service-manager-FIRST (SCM / systemd user unit — a port squatter can
never fake Running), then the new loopback-only unauthenticated
GET /api/v1/local/summary (counts/booleans only; the mgmt token and cert.pem
are SYSTEM/Admins-DACL'd on Windows, so a non-elevated tray cannot bearer-auth).
Windows: windows_subsystem binary (a console exe in the Run key would flash a
terminal at sign-in), Shell_NotifyIcon + hidden window, per-session single
instance, TaskbarCreated re-add, --quit for the uninstaller; service actions
elevate per click via ShellExecuteW "runas" onto the new
`punktfunk-host service restart` (stop → wait Stopped → start).
Linux: ksni/StatusNotifierItem over zbus, systemctl --user actions (no polkit),
/etc/xdg/autostart entry whose --autostart self-gates to actual host users.
Icons: scripts/gen-tray-icons.py (pure stdlib) renders the brand lens + status
dot into committed .ico/hicolor assets; deb/rpm/arch ship binary+autostart+icons.
Live-validated: Linux on the headless KDE session (SNI registration, state
transitions, menu-driven start, dbusmenu layout); Windows on the RTX box
(session-1 launch with no NIM_ADD failure, single instance, --quit, restart
round-trip, summary loopback-200/LAN-401).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
log-crate events arrive through the tracing-log bridge under the shim target
"log" — normalize them back to the real module path (NormalizeEvent) so the
console's target column and the noise gate see mdns_sd::… , and suppress the
bridge's log.* bookkeeping fields like the stderr fmt layer does.
Gate known-chatty third-party DEBUG targets (mdns-sd DEBUG-logs every
unparseable multicast packet — one AirPlay device floods thousands of entries
per hour) to INFO-and-up in the ring, so ambient LAN noise can't evict the
tail the ring exists to preserve. stderr under RUST_LOG is unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bump windows-reactor + windows to a4f7b2cb (from b4129fcc) for the new
PointerEntered/PointerExited events; migration is mechanical renames only
(SymbolGlyph->Symbol, placeholder->placeholder_text, on_changed->
on_text_changed/on_toggled, on_menu_item_clicked->on_item_clicked,
on_ready->on_mounted). New runtime model: reactor lost its build.rs, so the
client build.rs stages the WinAppSDK bootstrap via
windows-reactor-setup::as_framework_dependent() and main calls
windows_reactor::bootstrap() (missing either = 0x80040154 at launch);
staged filenames unchanged, so pack-msix and the MSIX manifest are untouched.
- Host tiles: WinUI pointer-over fill (ControlFillSecondary) via the new
pointer enter/exit events, hover id in root state (backend-wired handlers
bypass the reconciler flush, like the flyout clicks).
- Settings: stock NavigationView sidebar (Windows-Settings pattern) with
Display/Video/Input/Audio/About panes, built-in back arrow, wide content
column, and a per-section content slide-up tween. The section card is
KEYED by section: an in-place diff across sections re-sets a reused
ComboBox's items (clearing WinUI's selection) but skips selected_index
when the values compare equal, rendering a blank selection - the key
forces a remount. Card titles/descriptions dropped; per-control guidance
moved to hover tooltips (ToolTipService).
- New "Show the stats overlay (HUD)" setting (show_hud, default on),
honored mid-stream via the 400 ms HUD re-render.
- Add-host modal: entrance fade + slide-up tween (scrim fades with it).
- Self-initiated disconnect (Ctrl+Alt+Shift+D -> Ended(None)) returns to
the host list silently instead of raising the error banner.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Decode+present rewrite (first real pixels on glass for this client):
- Decode: FFmpeg D3D11VA on NVIDIA/AMD/Intel. get_format now only returns
AV_PIX_FMT_D3D11 and lets libavcodec build the decode pool from
hw_device_ctx (hand-built frames contexts failed three different ways:
NVIDIA rejects DECODER|SHADER_RESOURCE arrays, BindFlags=0 fails texture
creation, Intel rejects non-128-aligned HEVC surfaces at the first
SubmitDecoderBuffers). A DXVA profile probe before the hwdevice commits
hardware-vs-software up front instead of burning the opening IDR;
extra_hw_frames covers the frames the client holds.
- Present: the decoded slice is copied with ONE display-size-boxed
CopySubresourceRegion (a planar slice is a single subresource in D3D11;
the old two-copy D3D12-style code silently no-opped - the black screen)
into a sampleable NV12/P010 texture, per-plane SRVs + YUV->RGB shaders.
- New dedicated render thread (render.rs): presenting is decoupled from the
XAML thread; frame-latency-waitable swapchain + SetMaximumFrameLatency(1),
newest-wins drain after the wait, crossbeam frame channel with pts for a
capture->presented p50 log.
- HiDPI: pixel-sized buffers + SetMatrixTransform(96/dpi) - was blurry at
125/150 % scaling.
- Software fallback now feeds the same shaders (swscale -> NV12/P010 planes
-> two dynamic plane textures); ps_rgba/X2BGR10 path deleted, hw/sw colour
math identical.
- Adapter selection for hybrid boxes: PUNKTFUNK_ADAPTER > the window's
monitor's adapter > default; PUNKTFUNK_D3D_DEBUG=1 debug layer.
- Session pump: request_keyframe at start and on hw->sw demotion (infinite
GOP would otherwise sit on a black screen).
Validated live on the Arc Pro + RTX 3500 Ada laptop against the local
Windows host: 60 fps D3D11VA on both vendors, software path, GUI on glass.
Also: embedded app icon (build.rs winresource + WM_SETICON, MSIX
Square44x44 targetsize assets, pack-msix stages them) and the hosts-page
tile rework (tap-to-connect tiles with sibling overflow menu - fixes
forget-also-connects - in-tile rename editor, add-host modal via root state).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- library.rs + ui_library.rs: the host's unified game library over the
management API (the Apple LibraryClient/LibraryView ported) — mTLS with the
paired identity, host verified by its pinned cert fingerprint (ureq + rustls,
unified with the workspace rustls 0.23); posters load async with monogram
placeholders, and picking a title starts a session that asks the host to
launch it (the library id rides the Hello).
- app.rs (~800 lines lighter) splits into cli.rs (argv/headless
pairing/--connect/screenshot scenes), launch.rs (mode resolve + session
worker + event stream into the UI) and ui_trust.rs (TOFU / SPAKE2 PIN /
delegated-approval dialogs); ui_hosts/ui_stream reworked around the split.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The [workspace.package] version (inherited by every crate via version.workspace)
lagged at 0.4.1 — bump it to 0.4.2, the release being cut, and refresh the 8
workspace entries in Cargo.lock to match (CI builds --locked). This is a patch
release (Windows CI fixes + Apple gamepad UI); the canary base fallbacks stay
at 0.5.0, already one minor ahead of the 0.4.x stable line.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The [workspace.package] version (inherited by every crate via version.workspace)
lagged at 0.3.0 through the 0.4.0 release — bump it to 0.4.1, the release being
cut, and refresh the 8 workspace entries in Cargo.lock to match (CI builds
--locked).
Also advance the CI canary-base fallbacks (deb/rpm/flatpak/android/release
workflows + build-rpm.sh) from 0.3.0 to 0.5.0 so main/canary builds sort one
minor ahead of the latest stable line, per the documented channel convention.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Steam Deck pass-through (design/steam-deck-passthrough-plan.md), code-complete +
all CI checks green on Linux + adversarially reviewed; on-glass validation pending:
- usbip/`vhci_hcd` virtual Deck transport (inject/linux/steam_usbip.rs) for
non-SteamOS hosts (Bazzite/generic) — presents a real interface-2 USB Deck so
Steam Input promotes it. In-process vhci attach (loopback OP_REQ_IMPORT handshake
→ sysfs attach) with a bounded `usbip`-CLI fallback; detach on drop.
- Backed by a vendored, libusb-free trim of the `usbip` crate
(crates/punktfunk-host/vendor/usbip-sim, MIT + NOTICE; host/cdc/hid + rusb/nusb
removed; interrupt-IN paced by bInterval).
- Selection ladder raw_gadget (SteamOS fast-path) → usbip (universal) → UHID,
with PUNKTFUNK_STEAM_USBIP / PUNKTFUNK_USBIP_ATTACH knobs.
- Shared Deck descriptors + the 0x83/0xAE feature contract + a Steam-accepted
serial consolidated into steam_proto.rs; the raw_gadget backend reuses them.
- Linux client leave-shortcuts: Ctrl+Alt+Shift+D + holding the escape chord
(L1+R1+Start+Select) >=1.5s end the session (short press still exits
fullscreen); the chord state resets across sessions.
Also bundles in-progress work already staged in the tree:
- host(kwin): xdg-output logical-geometry mapping so the KWin fake_input backend
places absolute coordinates correctly under display scaling.
- docs: design/README index entries + design/controller-only-mode.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
axum-server was used only for the plain-HTTP nvhttp listener, but we enabled
its tls-rustls feature (HTTPS is hand-rolled over tokio-rustls) — and that
feature was what pulled the unmaintained rustls-pemfile (RUSTSEC-2025-0134).
Drop the feature, bump axum-server to 0.8 (0.8 also no longer pulls it), and
move our own PEM parsing in gamestream/tls.rs to rustls-pki-types' PemObject
(the same path punktfunk-core/quic.rs already uses), removing our direct
rustls-pemfile dep too.
Net: rustls-pemfile fully gone; dependency graph trimmed 547 -> 529 crates
(the tls-rustls feature also dragged in prettyplease + a wasm-tooling chain).
cargo audit now reports only audiopus_sys + paste (transitive, latest, no
successor). 108 host tests + clippy + fmt green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
memmap2 0.9.10 has an unchecked-pointer-offset unsoundness; 0.9.11 is the
patched release (pulled transitively via xkbcommon in the host). cargo audit
now reports only the 3 deliberately-visible `unmaintained` warnings
(audiopus_sys / paste / rustls-pemfile — all latest, transitive, warn-only,
do not fail CI per .cargo/audit.toml).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 0.11.15 bump for S1 (pre-auth out-of-order STREAM reassembly memory
exhaustion on the default QUIC listener) was reverted before the original
fix commit, so Cargo.lock on main still pinned the vulnerable 0.11.14 and
the new cargo-audit CI gate failed. Re-apply and lock it in.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds negotiated 5.1/7.1 surround to the punktfunk/1 protocol and every client
(previously stereo-only):
- core: new shared `audio` layout table (LAYOUT_51/71 + identity multistream
mapping, canonical wire order FL FR FC LFE RL RR SL SR); Hello/Welcome
`audio_channels` negotiation via the trailing-byte back-compat pattern (old
peers fall back to stereo); C-ABI `punktfunk_connect_ex6`,
`punktfunk_connection_audio_channels`, and in-core multistream decode
`punktfunk_connection_next_audio_pcm` for embedders without a multistream
Opus decoder. Real-libopus channel-identity round-trip test.
- host: native audio thread captures + Opus-(multi)stream-encodes at the
negotiated count (with a cross-session cached-capturer channel-mismatch fix);
GameStream surround unified onto the safe `opus::MSEncoder`, dropping
`audiopus_sys` (~4 unsafe blocks) and un-gating Windows GameStream surround;
WASAPI loopback capture relaxed to 2/6/8 with the correct dwChannelMask.
- clients: Linux (PipeWire), Windows (WASAPI), Android (AAudio) decode via
`opus::MSDecoder` + render multichannel; Apple decodes in-core to PCM →
AVAudioEngine with an explicit wire-order channel layout; each gains a
Stereo/5.1/7.1 setting. `punktfunk-probe --audio-channels N` is the headless
validator.
Verified on Linux: core/host/linux/probe test suites + the Android Rust
(cargo-ndk) build, clippy -D warnings, and rustfmt all green. Windows/Apple
builds, all on-glass checks, and the live native loopback are pending (CI / a
free box).
Also lands the concurrent in-tree HEVC 4:4:4 host work (PUNKTFUNK_444): it
shares the same touched files (quic.rs, punktfunk1.rs, encode/*, ...) and so
cannot be committed separately from the surround changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A disk-backed art cache (library-art-cache.json in the canonical host config dir) is the
source of truth read by all_games(), so the library list + launch-resolve never block on
the network. A host-lifetime background warmer (start_art_warmer, started in serve())
fetches uncached art OFF the hot path: GOG via the public no-auth api.gog.com product API,
Xbox via the unofficial no-auth displaycatalog (keyed by StoreId). Both best-effort
(protocol-relative URLs normalized to https; results cached even when empty so they aren't
re-fetched). The GOG + Xbox providers now read cached_art() (title-only until warmed).
Cross-platform (ureq blocking HTTP — no tokio on this path) so the fetch/parse code is
compiled + checked everywhere; a host whose stores all self-provide art (Steam CDN /
Heroic CDN / Lutris data: URLs) does no fetching. Dep: ureq (webpki roots, no system certs).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
XboxProvider scans each fixed drive's <drive>:\XboxGames for GDK games (presence of
Content\MicrosoftGame.config marks a game vs. an ordinary UWP app), parsing title /
Identity name / Executable Id / StoreId via roxmltree. The PackageFamilyName is READ
from the AppRepository\Packages\<PackageFullName> dir name (reduced to Name_Hash) —
never computed from the publisher. Launch via the AUMID (shell:AppsFolder\<PFN>!<AppId>)
through explorer in the interactive user session (UWP activation needs the user token,
which spawn_in_active_session already provides). Cover art (displaycatalog) is deferred
→ title-only. Known v1 gaps: custom .GamingRoot install folders + non-GDK pure-UWP Store
games (under the ACL-locked WindowsApps) aren't enumerated.
New windows_launch_for `aumid` arm; XboxProvider wired into all_games() under cfg(windows).
Dep: roxmltree (Windows). Windows unit tests cover MicrosoftGame.config parsing (incl. the
ms-resource title fallback), the PackageFullName→PFN reduction, and the aumid launch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EpicProvider reads the launcher's local .item manifests under %ProgramData% (no auth,
launcher need not run) with Playnite's exclusion filter (skip UE_* components +
non-launchable addons + dead install dirs); cover art from the base64 catcache.bin
(public Epic CDN, best-effort). Launch via the com.epicgames.launcher:// URI opened
through explorer.exe — the namespace:catalogItemId:appName triple, with a bare-appName
fallback so a launch is never dropped.
GogProvider enumerates HKLM\SOFTWARE\WOW6432Node\GOG.com\Games (winreg) + each
goggame-<id>.info primary FileTask into a direct-exe spawn (no Galaxy, dodges its
cold-start/anti-cheat). GOG cover art (public api.gog.com) is deferred — it needs an
HTTP fetch + cache off the hot all_games() path — so GOG is title-only for now.
windows_launch_for gains epic/gog arms; both providers wired into all_games() under
cfg(windows). Deps: base64 moved to the cross-platform table (Epic catcache decode +
Lutris art encode both need it); winreg added on the Windows target. Windows unit tests
cover the Epic exclusion filter + URI builder and the GOG spawn + play-task parsing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LutrisProvider reads the local pga.db (rusqlite, read-only/immutable so a running
Lutris can't block us) → installed games, launch via `lutris lutris:rungameid/<id>`,
cover art from Lutris's on-disk cache inlined as data: URLs (no public CDN keyed by a
stable id, unlike Steam/Heroic). HeroicProvider parses Heroic's store_cache JSON —
legendary/gog/nile = Epic+GOG+Amazon in one provider — installed-only with an
install-dir existence cross-check (works around Heroic's gog is_installed bug #2691),
free public CDN cover art, launch via `heroic --no-gui heroic://launch?...` (the
single-instance-Electron gamescope-escape caveat is documented; needs live confirm).
New command_for arms (lutris_id digits-guard, heroic runner+appName-guard) + both
providers wired into all_games(); everything Linux-gated (the launchers are
Linux-only), so the Windows/macOS host build is unaffected. Deps rusqlite (bundled
SQLite, no system dep) + base64 added to the Linux target only. Unit tests with
sqlite/json fixtures (installed-only filtering, CDN-art mapping, launch guards); live
`library` enumeration returns [] gracefully on a box without the launchers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The shared host<->driver ABI crate already contains more than the virtual
display: the IDD-push frame ring + control plane AND the gamepad shared-memory
layouts (XusbShm / PadShm). "pf-vdisplay-proto" was a misnomer — the name now
represents all the drivers it serves.
Mechanical rename, no behavior change:
- git mv crates/pf-vdisplay-proto -> crates/pf-driver-proto (package name +
path-deps in the host crate and the driver workspace).
- pf_vdisplay_proto -> pf_driver_proto across host + driver Rust, both Cargo.lock
files, the workspace members, the CI path triggers (windows-drivers.yml), and
the docs/INF comments. The runtime Global\pfvd-* shared-object names are a
SEPARATE contract and are deliberately untouched (host<->driver name matching).
- The pf-vdisplay DRIVER crate + its INF service name (Root\pf_vdisplay,
UmdfService=pf_vdisplay, pf_vdisplay.dll) are unchanged — only the full
`pf_vdisplay_proto` token was replaced, never the `pf_vdisplay` driver name.
Linux-verified: cargo test -p pf-driver-proto (const size-asserts compile) +
cargo clippy -p punktfunk-host -D warnings clean; Cargo.lock regenerated. The
driver-workspace side (path-dep + imports + its Cargo.lock) is Windows-CI-gated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1a86f16 shipped the agent-written pf_vdisplay.rs unformatted (cargo fmt --all --check
gate) and omitted the Cargo.lock edges for the new windows-only deps (pf-vdisplay-proto +
bytemuck). cargo fmt --all is now clean; Cargo.lock records the host dep edges.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First foundation of the Windows-host rewrite (docs/windows-host-rewrite.md): a
self-contained, no_std + bytemuck crate that defines the host<->driver binary
contract ONCE — the control-plane IOCTLs (add/remove/set-render-adapter/ping/
get-info/clear-all) and the IDD-push frame transport (SharedHeader, the
(gen<<40|seq<<8|slot) FrameToken, the Global\pfvd-* name scheme, driver-status
codes). Previously these were hand-duplicated byte-for-byte across
idd_push.rs/frame_transport.rs and sudovda.rs/control.rs with only "must match"
comments; here const size-asserts + bytemuck round-trips make any drift a COMPILE
error.
Clean break from SudoVDA: a freshly-minted interface GUID (not e5bcc234), a
contiguous 0x900 op space (not the gappy 0x800/0x888/0x8FF), a u64 session id (not
the 16-byte GUID + pid-mangling), a single u32 protocol version. Self-contained
(no workspace inheritance, no Windows deps) so the out-of-workspace driver build
graph can path-dep it identically. 7 tests green on Linux; clippy + fmt clean.
Also lands the full rewrite plan in docs/windows-host-rewrite.md (decisions:
greenfield; IDD-push primary incl. secure desktop, WGC+DDA demoted to fallbacks;
unify drivers on windows-drivers-rs + solve /INTEGRITYCHECK; keep GameStream,
default secure).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Discovery: replace the flaky per-OEM NsdManager with the same mdns-sd browse
the Linux/Windows clients use, in the Rust core over JNI and polled by Kotlin
(discovery.rs + nativeDiscovery{Start,Poll,Stop}); Kotlin keeps only the Wi-Fi
MulticastLock + permission UX. IPv4-only (the core can't dial a bare/scoped v6
literal); daemon + fold-thread cleanup on every failure path; field
sanitization so a rogue advert can't corrupt the picker snapshot. Discovery
now starts regardless of NEARBY_WIFI_DEVICES (raw multicast only needs the
MulticastLock) — a denial no longer kills it forever. ParseTxtTest replaced by
ParseRecordTest.
Hosts: hide already-saved hosts from the "Discovered" section (match by
fingerprint, else address:port — mirrors the Apple client); add an optional
Name field to the Add-host sheet and a Rename action on saved cards.
Input: touch -> absolute mouse "direct pointing" like the Apple client — the
host cursor follows the finger (new nativeSendPointerAbs -> MouseMoveAbs). Tap
= left click, two-finger tap = right click, two-finger drag = scroll,
tap-then-drag = left-drag, three-finger tap = HUD toggle.
Settings: revert the dropdowns to the stock ExposedDropdownMenuBox look (a
controller-focus UI will come separately); even out the Add-host field gaps.
Docs updated (CLAUDE.md, client READMEs, docs-site status).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Windows virtual gamepads now have zero external dependencies - ViGEmBus is removed.
- DualShock 4: Windows UMDF backend (inject/dualshock4_windows.rs + dualshock4_proto.rs),
reusing the DualSense SwDeviceCreate game-detection identity fix. The one UMDF driver serves
the DS5 or DS4 identity/descriptor/features/strings per a device_type byte the host stamps into
shared memory. Driver also gains IOCTL_HID_GET_STRING and a 41-byte calibration feature.
- Xbox 360: a new UMDF2 XUSB companion driver (packaging/windows/xusb-driver/) that registers
GUID_DEVINTERFACE_XUSB and answers the buffered XInput IOCTLs from a shared section, so classic
XInputGetState/SetState work with no kernel bus driver. inject/gamepad_windows.rs is rewritten
to drive it and the vigem-client dependency is removed. Xbox One folds to the 360 XInput path.
- Installer: vendor + pnputil-install the three UMDF drivers (packaging/windows/gamepad-drivers/
+ install-gamepad-drivers.ps1, wired into pack-host-installer.ps1 + punktfunk-host.iss).
- Multi-pad: the host stamps each pad index into the device Location (pszDeviceLocation); the
driver reads it via WdfDeviceAllocAndQueryProperty to map its own *-shm-<index>, with
UmdfHostProcessSharing=ProcessSharingDisabled giving each pad its own host (per-pad statics).
Validated live on the Windows host: Cyberpunk native DualSense detection, DS4 identity + descriptor,
XInputGetState + rumble round-trip, two pads -> two distinct XInput slots, and a full installer build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The host hard-linked libcuda.so.1 on Linux (`#[link(name="cuda")]` in
`zerocopy::cuda`), so the binary wouldn't even *start* on a non-NVIDIA box —
the dynamic loader can't resolve the NEEDED libcuda. That blocked running the
new VAAPI (AMD/Intel) path on a machine without the NVIDIA driver.
Resolve the 18 CUDA Driver API symbols at runtime via `libloading` instead.
Same-named wrapper fns forward to the dlopen'd table (call sites unchanged);
when libcuda is absent they return a non-zero CUresult so `context()` fails
cleanly and the capturer falls back to the CPU path. The library handle is
leaked (process-lifetime, like the shared context).
One Linux binary now runs on NVIDIA (CUDA zero-copy -> NVENC) and on AMD/Intel
(VAAPI, no NVIDIA driver). Verified: the NVIDIA dev box still does dmabuf->CUDA
zero-copy; on a Radeon 780M box the host builds with no libcuda present, the
binary has no NEEDED libcuda entry, and VAAPI encode runs with no stub.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stats HUD (mirrors the Apple client): the decode thread accumulates FPS, receive
throughput, and capture->client latency (p50/p95, skew-corrected) in Rust
(clients/android/native/src/stats.rs); nativeVideoStats drains a snapshot ~1 Hz
over JNI as a DoubleArray. StreamScreen renders a Compose overlay
(W*H@Hz / fps / Mb/s / latency, + dropped-under-loss), toggled by a Settings
switch (persisted, default on) or a 3-finger tap.
Performance (decode.rs):
- ANativeWindow_setFrameRate(refresh_hz): align display vsync to the stream rate
(no 60-in-120 judder); safe since minSdk 31 >= API 30.
- Raise the decode thread toward URGENT_DISPLAY (best-effort setpriority) so
background work can't preempt it under load.
- Codec low-latency hints KEY_PRIORITY=0 (realtime) + KEY_OPERATING_RATE.
Verified host-side: cargo build/clippy/fmt --workspace (the ungated stats + JNI
accessor). The android-gated decode.rs (NDK) and the Kotlin build only in CI
(android.yml: gradle + cargo-ndk) -- APIs verified against crate sources.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two bodies of work in one commit (the rename moved files the fixes also touched).
Naming/structure cleanup (pre-launch):
- Host modules m3.rs->punktfunk1.rs, m0.rs->spike.rs; CLI m3-host->punktfunk1-host,
m0->spike; bare `punktfunk-host` now prints help. Types M3Options/M3Source->
Punktfunk1Options/Punktfunk1Source.
- Clients consolidated out of crates/ into clients/: punktfunk-client-rs->
clients/probe (crate punktfunk-probe), client-linux->clients/linux,
client-windows->clients/windows, punktfunk-android->clients/android/native
(crate punktfunk-client-android; kept [lib] name=punktfunk_android so the JNI
contract is unchanged). crates/ now holds only core + host.
- Milestone codes M0-M4 purged from code/CLI/CLAUDE.md/README/docs/docs-site,
kept only in docs/implementation-plan.md. docs/m2-plan.md->
docs/gamestream-host-plan.md. CI/gradle/flatpak paths updated.
Client loss-recovery (video froze and never recovered after a brief drop):
- Export punktfunk_connection_frames_dropped through the C ABI (the core already
tracked it for the client keyframe-recovery loop; it was never reachable from
the ABI clients). Regenerated punktfunk_core.h.
- Apple (StreamPump + Stage2Pipeline) and Android (decode.rs) now poll
frames_dropped and request a keyframe when it climbs -- the same loss-driven
recovery Linux/Windows already had. Under infinite GOP the decoder silently
conceals reference-missing frames, so the decode-error trigger rarely fires.
Apple rumble robustness (worked then went spotty -- DualSense + Xbox):
- Add CHHapticEngine stopped/reset handlers (rebuild on app background / audio
interruption / server reset) and drop the permanent `broken` latch on a
transient drive failure; latch only when the controller truly has no haptics.
- Surface swallowed SDL set_rumble errors on Linux/Windows + diagnostic logging.
Verified: cargo build/clippy/fmt --workspace, C-ABI harness, header drift.
Not runnable on this box (verify in CI): Gitea workflows, gradle/Android,
flatpak, Swift/decky.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Final cleanup after the DDA-parity work, plus an end-user service to replace the
PsExec/VBS/scheduled-task launch chain.
Cleanup (behavior-preserving):
- sudovda.rs: drop the dead legacy GDI isolate_displays/restore_displays (CCD is
the sole isolation path), the always-empty Monitor.isolated field, and the
vestigial reassert_isolation + PUNKTFUNK_ISOLATE_DISPLAYS knob; fix stale comments.
- dxgi.rs: downgrade leftover debug warns/infos (DuplicateOutput1 retry, FALLBACKS,
hook-hits, AcquireNextFrame idle timeout) to debug!; remove the PUNKTFUNK_NO_CURSOR
per-frame test knob.
Windows service (src/service.rs, `punktfunk-host service`):
- SCM supervisor (windows-service crate) that duplicates its LocalSystem token,
retargets it to the active console session, and CreateProcessAsUserW's the host
there (Sunshine/Apollo model) — relaunching on exit and console session switch,
inside a kill-on-close job object so a service crash never orphans the host.
- install/uninstall/start/stop/status subcommands: one elevated `service install`
registers an auto-start LocalSystem service + firewall rules + a default host.env.
- Config moves to %ProgramData%\punktfunk\host.env; config_dir() now resolves to
%ProgramData%\punktfunk on Windows (replacing the APPDATA=C:\Users\Public hack),
with a PUNKTFUNK_CONFIG_DIR override. Logs land in %ProgramData%\punktfunk\logs\.
- merged_env_block (shared with the WGC helper) now also carries RUST_LOG.
- docs/windows-service.md + scripts/windows/host.env.example; windows-host.md updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the winit + raw-HWND-D3D11 shell with a native WinUI 3 UI via windows-reactor (a
declarative React-like framework backed by WinUI). The earlier "Reactor can't host a
swapchain" read was wrong — PR #4499 (merged 2026-06-01) added a SwapChainPanel widget with
`set_swap_chain` over `CreateSwapChainForComposition`. Builds + clippy + fmt green on
x86_64-pc-windows-msvc.
- Cargo: drop winit/raw-window-handle; add windows-reactor + the `windows` crate, both pinned
to the SAME windows-rs commit (b4129fcc) so the `IDXGISwapChain1` handed to `set_swap_chain`
satisfies reactor's `windows_core::Interface`. Reactor's build.rs downloads the Windows App
SDK NuGets + stages the bootstrap DLL/resources.pri — it requires `CARGO_WORKSPACE_DIR` set
(now in the VM build env); /temp + /winmd gitignored.
- present.rs: composition swapchain (B8G8R8A8 FLIP_SEQUENTIAL premultiplied) bound to the
SwapChainPanel; WARP fallback, runtime D3DCompile shaders, dynamic RGBA texture, Contain-fit
letterbox; driven by reactor's per-frame `on_rendering`.
- app.rs: the WinUI 3 shell — host list (live mDNS + saved + manual), settings (resolution/
refresh/mic combos+toggle), in-app SPAKE2 PIN pairing screen, and the stream page. Trust gate
mirrors the GTK client (pinned → silent, pair=optional → TOFU, else PIN); a pinned-fp
mismatch routes to re-pair. The session pump + decoded-frame handoff cross to the UI thread
via a Mutex side-channel + thread-locals (the SwapChainPanel sample's pattern).
- gamepad: `ctl` sender now `Arc<Mutex<…>>` so GamepadService is `Sync` (shared across the UI
and session-pump threads). main.rs: windowed = in-app UI; `--headless`/`--discover` keep the
CLI paths.
Not yet wired: raw stream keyboard/mouse input (next commit — reactor exposes no raw key/
pointer events, so it needs Win32 low-level hooks or Microsoft.UI.Xaml bindings). On-glass
validation pending a display (the dev VM is headless/GPU-less).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>