Files
punktfunk/clients/session
enricobuehler d5e23146c0 feat(client): M8 — the software rung is openh264 and rav1d, and swscale is gone
The ladder's last rung no longer runs FFmpeg. H.264 decodes through openh264,
AV1 through rav1d, and HEVC is refused outright: no permissively licensed
software HEVC decoder exists, so an HEVC session that exhausts its hardware
rungs now tears down and re-dials advertising HEVC-less caps, and the host
picks H.264. The plan calls that a first-class path; it is one.

swscale is deleted, and with it the BT.601 default that its correction code
existed to undo. Colour on the H.264 lane now comes from the same
pf-bitstream planner every hardware rung submits from — openh264 reports no
VUI at all — and AV1's comes per-picture from the sequence header. One colour
source, one CSC: the old default is unrepresentable rather than merely fixed.
Frames reach the presenter as three tightly-packed planes through the planar
CSC pass, which had to be un-gated from the pyrowave feature and its device
probe, since the last rung must exist on devices that failed that probe.

rav1d rather than the dav1d crate, deliberately and against the plan's
literal wording: dav1d-sys is system-deps-only, so it would add a system
library and a .pc file to every client package — in the milestone family
whose excision checklist exists to delete exactly that. rav1d is the same
decoder, same licence, statically linked. The cost is honest: no-asm builds
on both decoders, and software throughput is still unmeasured.

The colour test is the milestone's exit criterion, so it is built to fail.
Three fixtures, and a mutation check: hardcoding the swscale default turns the
red bar to [255,24,0], and swapping Cb/Cr turns red to blue — a silent error
no metadata assertion could catch. Review then disproved the range half of it
numerically: with eight saturated bars, decoding the full-range fixture with
the wrong range gives max error ZERO, because a mismatch only pushes values
outside [0,1] where the shader clamps. A mid-tone was added; the wrong range
now costs 11, well past the tolerance. The exit criterion I set was
satisfiable by a test that proved nothing.

Two blocking defects, both emergent rather than local.

Software AV1 on a 10-bit stream never reached its typed refusal: rav1d is
built 8-bit-only and returns ENOPROTOOPT, which the send loop turned into a
generic error, so the pump's typed downcast missed and every AU failed
identically — a permanent freeze on precisely the shipping case, since AV1 is
advertised only where hardware AV1 exists and hardware AV1 plus HDR is Main
10. The shape is now read from the sequence header before any byte reaches
the decoder, exactly as the H.264 leg reads the active SPS.

And the new Reconnecting phase was the first state that is not streaming, not
connecting, and still holding a live stream — which opened all three guards
that had made a second launch impossible. Pressing A assigned over `stream`
where every other site shuts down first, and StreamState has no Drop, so the
old pump was detached: a second live session still submitting to a Vulkan
device that gets destroyed underneath it. Nothing about the reconnect was
wrong in isolation; the defect lived between a new state and three guards
nobody re-examined. Start is now defensive and the retry raises the
connecting modal, so the UI matches the state and B can cancel.

Also closed: retry_caps was computed, tested and never applied, so a shape
refusal could end a session reporting no codec available while a working
retry existed; the retry inherited force_software sticky-true, landing an
HEVC→H.264 fallback on software H.264 with working hardware H.264; it
re-dialled with a stale mode; the CPU present arm had no survivable-failure
handling where the pyrowave arm — same pass — has it; HEVC is no longer
advertised when the decoder is pinned to software; and the software rung now
feeds the recovery-point SEI it already had in hand to the re-anchor gate.

⚠ Two host-side gaps found while tracing, neither in scope here: Hello::launch
is NOT idempotent (gog:/custom: targets spawn a second copy on a retry; the
field is kept verbatim because dropping it orphans the gamescope display whose
reuse key includes the command), and a reconnected session can never adopt a
game predating its own launch stamp, so it has no game-exit detection.

⚠ OWED: the on-glass software run. ~200 lines of new Vulkan on a path that
only runs because the GPU already failed, and no driver has seen it. The
review's minimum check is sync validation enabled, a non-multiple-of-16 mode,
a mid-session resize and demotion, and both colour matrices.

Gates: container clippy -D warnings over four crates, 236 tests, workspace
check. pf-vkdecode and pf-bitstream are byte-for-byte untouched, so the
hardware rungs' 250/250 stands.
2026-08-07 06:18:23 +02:00
..

punktfunk-session

The Vulkan session binary: one stream per invocation in an SDL3 window — no UI toolkit, no widgets, terminal stats. The power-user / gamescope stream client, and the stage-2 presenter of the Linux client re-architecture (punktfunk-planning: linux-client-rearchitecture.md).

This binary is deliberately dumb: a renderer the front-ends call INTO — the GTK shell (punktfunk-client), the WinUI shell, and the punktfunk CLI all spawn it through the same brain (pf_client_core::orchestrate), which resolves policy (profiles, settings, wake) and hands the result down, normally as a --resolved-spec file. It reads the shared stores only as the compat fallback for a bare hand-launched invocation.

punktfunk-session --connect host[:port] [--fp HEX] [--launch id] [--fullscreen] [--stats]
punktfunk-session --browse host[:port] [--mgmt PORT] [--fullscreen]

--browse opens the console game library (the Skia coverflow over the animated aurora) instead of connecting: A launches the focused title as a stream in the same window, session end returns to the library, B quits (Gaming Mode returns). Paired hosts only — pairing is the desktop client / Decky plugin's job. PUNKTFUNK_FAKE_LIBRARY=<file.json> feeds canned entries with no host (portrait paths starting with / load from disk).

Reads the same identity / known-hosts / settings stores as the desktop client (punktfunk-client), so enrolling on either side makes the other work; this binary never connects to a host it has no pinned fingerprint for (--fp HEX overrides the store).

Pairing is punktfunk pair <host> — the CLI, which ships alongside this binary in every package and needs no window and no toolkit either. punktfunk-session --pair still works for one release (someone's provisioning script calls it today) but prints a deprecation notice: pairing is a trust ceremony and belongs to the brain, not a renderer.

Stdout is the machine interface: {"ready":true} after the first presented frame, stats: … once per second while the overlay tier isn't Off (always the full detailed text, whatever the OSD shows; --stats forces the overlay on), one {"error"|"ended": …} JSON line on the way out. Logs go to stderr. Exit codes: 0 clean end, 2 connect failed, 3 trust rejected / pairing required, 4 presenter init failed.

In-stream keys match the desktop client: click captures input (Ctrl+Alt+Shift+Q releases), Ctrl+Alt+Shift+D disconnects, F11 toggles fullscreen; the controller escape chord (L1+R1+Start+Select, hold to disconnect) works the same.

The default build carries the Skia console UI (ui feature): the stats OSD and capture hint render in-window. Ctrl+Alt+Shift+S cycles the OSD tier live — Off → Compact (one line: fps · latency · Mb/s) → Normal (mode + end-to-end percentiles) → Detailed (decoder path + per-stage latency equation); any tier but Off also emits the stdout mirror. --no-default-features is the ~5 MB power-user build — same streaming, stats on stdout only, no Skia anywhere in the dependency tree.

Decode follows the Settings preference (auto is vendor-ordered: hardware Vulkan Video → VAAPI → software on Linux, hardware Vulkan Video → D3D11VA → software on Windows, with VAAPI/D3D11VA first on Intel; on H.264 and HEVC the native pf-vkdecode Vulkan decoder is tried immediately before FFmpeg-Vulkan): the Vulkan decoders run on the presenter's own device where the stack supports it (every vendor, zero copy); VAAPI dmabufs import per-plane elsewhere (D3D11VA textures on Windows); software is the universal fallback. 10-bit Main10 and HDR10 are advertised (VIDEO_CAP_10BIT|HDR): P010 decodes through the native, FFmpeg-Vulkan, VAAPI/D3D11VA and software paths alike, and PQ streams present on an HDR10/ST.2084 swapchain when the desktop offers one (KDE HDR, gamescope) or tone-map in-shader to SDR when it doesn't (PUNKTFUNK_TONEMAP_PEAK tunes the rolloff, default ≈1000 nits). The host still gates the upgrade behind its PUNKTFUNK_10BIT policy.

Debug/bisect knobs: PUNKTFUNK_DECODER=native-vulkan|native-vaapi|native-d3d11va|vulkan|vaapi|d3d11va|software (the three native-* values pin this program's own decoders; native-vaapi also takes PUNKTFUNK_VAAPI_DEVICE=/dev/dri/renderDNNN to choose the GPU), PUNKTFUNK_PRESENT_MODE= mailbox|fifo|immediate|fifo_relaxed (default MAILBOX, FIFO where the surface offers no MAILBOX — AMD on Windows), PUNKTFUNK_VK_DEVICE=<index> (multi-GPU), and PUNKTFUNK_HW_FAULT=import (fault every VAAPI dmabuf import — proves the three-strike demotion to software on healthy hardware).

PUNKTFUNK_AU_FAULT=drop|truncate|flip[:period] deliberately corrupts decoder input on the native Vulkan lane (default period 60 — one AU a second at 60 fps; inert everywhere else, and inert entirely if the value doesn't parse). drop swallows the AU, so the next one references a picture that was never decoded — the bitstream planner catches it immediately. truncate delivers a picture whose slice data stops mid-frame and flip alters one byte deep in the payload: both parse perfectly, so only the driver's per-frame decode-status query can see them, and neither is visible at all on a driver without queryResultStatusSupport or on any FFmpeg lane. Watch the result on the Detailed stats line's integrity: term (damaged = concealment the planner caught, refused = AUs the decoder rejected outright, driver-failed = the hardware's own verdict, run = consecutive frames with no picture, worst run = the longest such stretch of the session — the once-a-second run sample misses the bad moment almost every time — and no driver status = this device cannot answer the driver question at all). A session that lands on any other lane says so in the log rather than faulting silently.

Note that PUNKTFUNK_AU_DUMP records the AU as it arrived from the HOST, while the fault injector runs later, at the native decoder's own entry. On a faulted run the dump is therefore the clean bitstream — reconstruct the damaged bytes from the spec if you need them (the injector is pure and deterministic).