A PyroWave session on an NVIDIA-auto host was forced onto CPU-RGB capture
(session_plan flipped gpu=false): Mutter blits tiled->LINEAR, we mmap +
de-pad ~30 MB, the encoder re-uploads it - three full-frame CPU touches
per frame at 5120x1440 while an HEVC session on the same box rides the
tiled EGL/CUDA zero-copy. The dmabuf passthrough + Vulkan tiled import
were already validated (8dc5d672) but only reachable via the global
PUNKTFUNK_ENCODER=pyrowave lab policy.
ZeroCopyPolicy gains pyrowave_session (from OutputFormat.pyrowave, i.e.
the negotiated codec): the capturer skips the NVENC-only EGL->CUDA
importer, takes the raw-dmabuf passthrough, and advertises the wavelet
encoder's Vulkan-importable modifiers so Mutter+NVIDIA negotiates tiled
zero-copy. The forced-CPU flip in session_plan is gone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The decode surface is DXVA-aligned (height rounded up to the profile's
alignment, 128 for HEVC/AV1), so it is taller than the frame: a 2400-line
stream decodes into a 2432-line texture. VideoProcessorBlt was called with
no stream source rect, so it blitted the WHOLE surface — the uninitialized
padding rows (NV12 Y=0,U=V=0, which converts to vivid green) landed at the
bottom of the output and the picture was squashed to fit. Set the source
rect to the real frame.
Pre-existing on this backend, but 40030e90 made D3D11VA auto's first choice
on Intel, so it went from a corner case to what every Intel user sees — a
~32 px green bar at 3840x2400, reported on glass within the hour.
Also dedupe the Settings GPU picker by description: that string IS the
identity downstream (persisted as Settings::adapter, matched by name as
PUNKTFUNK_VK_ADAPTER), so repeated enumerations of one adapter offered the
user the same choice twice — live on an Intel Arc laptop, whose Vulkan ICD
also enumerates the single physical iGPU twice. Both paths now log their
raw enumeration (decode texture dims; DXGI name/LUID/ids) for the next
report.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A PQ stream on the D3D11VA backend was always tone-mapped to sRGB by the
VideoProcessor — with D3D11VA now auto's first choice on Intel (40030e90),
Intel Windows users would have lost HDR entirely. When the presenter can
import an RGB10A2 D3D11 texture AND offers an HDR10 swapchain (the new
VulkanDecodeDevice::d3d11_hdr10 probe), the hand-off ring switches to
RGB10A2 and the VideoProcessor does a pure colorspace conversion (YCbCr
G2084 -> RGB G2084, no tone mapping); the emitted frame carries PQ/BT.2020
color, so the presenter flips its HDR10 swapchain and video image exactly
as it does for Vulkan Video PQ frames, and the blit passes the PQ values
through untouched. SDR-only paths keep the tonemap-to-sRGB BGRA8 ring;
in-band PQ flips rebuild the ring like a resize (generation-bumped).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Intel's Windows driver advertises Vulkan Video (Arc drivers since 2023), so
the capability gate alone no longer keeps Intel off FFmpeg-Vulkan — and that
combination is broken in the field (B580 report: strobing + 7.2 ms p50
decodes at 4K120) and on glass (Arc Pro iGPU: 29-33 ms p50 decodes at 4K60
where D3D11VA does 2.5 ms on the SAME GPU). Mirror the Linux vendor order on
Windows: NVIDIA/AMD keep Vulkan Video first, Intel/unknown take D3D11VA
first; vulkan stays reachable by explicit preference and as auto's fallback.
Also:
- Vulkan→D3D11VA mid-session demotion rung (the Windows analog of Linux's
Vulkan→VAAPI rung) — a failing Vulkan backend lands on hardware, not on
software, which cannot survive 4K120.
- The demotion streak now needs 1 s of age as well as 3 consecutive errors:
a startup loss burst produced 3 errors in 20 ms and stranded the session
on software decode (one-way) before the IDR requested on the first error
could possibly arrive — live-hit on the Intel iGPU leg.
- Stale "Intel's Windows driver has no Vulkan Video" comments corrected,
docs updated to the per-vendor order, vendor-order test extended with
discrete/iGPU Arc cases.
Verified on an Arc Pro iGPU + RTX 3500 Ada laptop against a CachyOS/NVENC
host: auto now picks d3d11va on Intel (60 fps, 2.5-2.8 ms decode, e2e 26 ms
vs 50 ms before) and still picks vulkan on NVIDIA. 27/27 pf-client-core
tests, clippy clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The GNOME 50 HDR format offer took the PQ transfer id straight from
pw::spa::sys, which only exists on libspa new enough to carry the
BT2020_10/SMPTE2084/ARIB_STD_B67 block. Ubuntu 24.04 (noble) — the .deb host
builder — ships older headers, so bindgen emitted no such constant and the
host failed to compile there:
error[E0425]: cannot find value `SPA_VIDEO_TRANSFER_SMPTE2084`
in crate `pw::spa::sys`
This never showed up locally or on the Linux CI: both run a current PipeWire,
where the binding is present. It broke deb.yml's build-publish-host job, so
v0.14.0 published its client .debs but no host .deb.
Spell the id out (14) instead. It's wire ABI, not a private detail — SPA
mirrors GStreamer's GstVideoTransferFunction and that block was added as a
unit, so the value is the same on every libspa that has the symbol. On one
that doesn't, PipeWire fails to intersect the offer and the session
negotiates SDR, which is what an HDR-incapable host should do anyway (the
path needs GNOME 50+ regardless).
A test pins our value against pw::spa::sys wherever the symbol exists, so a
renumbered enum fails loudly instead of silently mis-tagging the transfer
function. It only builds where tests are compiled — the .deb/.rpm builders
run plain `cargo build`, so it can't reintroduce the failure it guards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`let mut hdr` tripped -D unused_mut on Windows: the only reassignment is the
GNOME colour-mode probe below it, which is #[cfg(target_os = "linux")]. Dropping
`mut` would break the Linux build, so allow unused_mut on non-Linux only — the
lint still fires on Linux if that probe is ever removed.
Second Windows-only breakage in this release that the Linux CI cannot see (see
also the ffmpeg_win swscale match): `cargo clippy --workspace` on the Linux
runner never compiles cfg(windows) code, and the Windows job builds only
punktfunk-host + punktfunk-tray.
Verified: cargo clippy --release -p punktfunk-host --features nvenc,amf-qsv,qsv
-- -D warnings on 192.168.1.173.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The GNOME 50 HDR work added PixelFormat::X2Rgb10 / X2Bgr10 but only taught the
Linux encoders about them. `sws_src` in the Windows-gated ffmpeg_win.rs matches
PixelFormat exhaustively, so the Windows host stopped compiling:
error[E0004]: non-exhaustive patterns: `X2Rgb10` and `X2Bgr10` not covered
--> crates\pf-encode\src\enc\windows\ffmpeg_win.rs:132:14
Linux CI never caught it — the file is cfg(windows), so `cargo clippy
--workspace` on the Linux runner never compiles it.
Both are Linux-only screencast formats (the Windows HDR path stays
Rgb10a2/P010, per the PixelFormat docs), so they join the existing bail arm.
Spelled out rather than folded into a `_` catch-all so the next PixelFormat
addition breaks this match again on purpose.
Verified: cargo check --workspace --all-targets --features nvenc,amf-qsv on the
Windows box (192.168.1.173).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MINOR, not patch: 31 commits since v0.13.0 carry 10 features, including a new
/api/v1/plugins surface with a regenerated OpenAPI spec + SDK client. Since
sdk-publish pushes this version to consumers, a patch bump would understate a
new API surface for anyone pinning ~0.13.
Headline work: GNOME 50 HDR screencast capture + Linux Main10 encode, PyroWave
4:4:4 + HDR end to end (Linux/Windows/Apple), the console-hosted plugin UI
surface, and the pyrowave perf work that lifted the 2.5 Gbps wall. Notable
fixes: the RSA host-identity keygen that broke every fresh Windows install on
0.13.0, and SDR negotiation on the Windows IDD-push path.
pf-clipboard / pf-inject / pf-vdisplay were still hardcoding 0.12.0 — the three
the 22b352c1 sweep missed. Switched to version.workspace = true so the next
bump stays one line. (fec-rs, pf-driver-proto and usbip-sim are deliberately
versioned independently and stay put.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`cargo clippy --workspace --all-targets --locked -- -D warnings` was red on
main — three lints landed with the GNOME 50 HDR + PyroWave 4:4:4 work:
* pyrowave_wire.rs: `aw / 2 >> level` tripped clippy::precedence. Rust already
binds `/` tighter than `>>`, so this always parsed as `(aw / 2) >> level`
(subband dim at half res, then one halving per DWT level) — the parens are
purely explicit, no change in behaviour.
* linux/mod.rs: `probe_can_encode_10bit` sat after `mod hdr_tests`
(clippy::items_after_test_module) — moved above the test module, unchanged.
Lint-only; no functional change. fmt/clippy/test all green afterwards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`feat(hdr): GNOME 50 HDR screencast capture` (0e977817) landed with rustfmt
drift — six files were not clean under the pinned 1.96.0 toolchain, so
`cargo fmt --all --check` (ci.yml "Format") is red on main. Pure whitespace/
wrapping from `cargo fmt --all`; no semantic change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A Windows client with HDR off (video_caps=0) negotiates bit_depth=8, but the
H.26x IDD-push capture composition FOLLOWED the virtual display's live "Use
HDR" state rather than the negotiated depth. On a display whose advanced color
was on — leftover from a prior 10-bit session on a reused monitor, the driver
default, or the host's global toggle — the capturer emitted P010 and NVENC
stamped HEVC Main10 + BT.2020 PQ from the pixel format alone (the in-band HDR
upgrade). That 10-bit PQ stream reached a client that advertised SDR-only; on a
client whose monitor is HDR-capable but has "Use HDR" off, the PQ landed on an
SDR desktop and blew out.
Make client_10bit authoritative for the composition depth on BOTH codecs,
mirroring the PyroWave branch that already did this:
* setup: force advanced color OFF for any !client_10bit session (was
pyrowave-only), settling before the ring is sized;
* display_hdr gates on client_10bit alone, so an SDR-negotiated session
composes SDR even if a physical display forces HDR;
* the descriptor poller re-asserts SDR against a mid-session "Use HDR" flip for
any SDR-negotiated session (an HDR-negotiated H.26x session still follows
flips both ways — its encoder re-inits on the depth change).
Only Punktfunk's virtual display is touched, never the host's real desktop.
Not yet on-glass; the client-side robustness fix (presenter should tone-map PQ
on an SDR-mode display instead of taking an HDR10 swapchain) is a follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GNOME 50 (Mutter MR 4928, PipeWire >= 1.6) added HDR screen sharing for
monitor streams: 10-bit PQ formats (xRGB_210LE/xBGR_210LE) with MANDATORY
BT.2020 + SMPTE-2084 colorimetry props, advertised while the mirrored
monitor is in BT.2100 colour mode. Wire the Linux host into it end-to-end
on the GameStream desktop-mirror path (PUNKTFUNK_VIDEO_SOURCE=portal):
* pf-frame: PixelFormat::X2Rgb10/X2Bgr10 (DRM XR30/XB30; X2Bgr10 is the
Windows Rgb10a2 layout) + fourccs.
* pf-capture: want_hdr portal offer — HDR-only LINEAR-dmabuf pods with
MANDATORY PQ/BT.2020 props (SHM excluded: Mutter's SHM record path
paints 8-bit ARGB32 regardless of format; tiled excluded: the EGL
de-tile blit is 8-bit RGBA8), negotiated-colorimetry parse, generic
HDR10 hdr_meta(), packed-10-bit CPU cursor blend, a process-wide SDR
downgrade latch on negotiation timeout, and a DisplayConfig BT.2100
colour-mode probe (gnome_hdr_monitor_active).
* pf-encode: libav NVENC X2RGB10->P010 swscale (BT.2020 limited) ->
HEVC Main10 / 10-bit AV1 with PQ VUI; VAAPI 10-bit on both paths (CPU
P010 upload + dmabuf XR30 scale_vaapi p010/bt2020); can_encode_10bit
now probes for real on Linux; 10-bit sessions route around the
8-bit-only Vulkan-video/direct-NVENC backends.
* GameStream: host_hdr_capable() Linux arm, live monitor-HDR check at
RTSP honor time, capturer-pool reuse keyed on HDR-ness, gs_bit_depth
covers the new formats. New `punktfunk-host hdr-probe` diagnostic and
a PUNKTFUNK_SPIKE_HDR spike lever.
* Native plane stays honestly 8-bit via capturer_supports_hdr(): Mutter
RecordVirtual streams are SDR-only upstream (GNOME 50 and 51-dev), so
virtual-display sources cannot deliver HDR yet.
Validated on the RTX 5070 Ti (GNOME 50.3 / PipeWire 1.6.8): the Main10
probes pass and the ignored nvenc_hdr10_smoke GPU test emits an IDR that
ffprobe reads as Main 10 / yuv420p10le / bt2020nc / smpte2084 / limited.
Live HDR capture negotiation still needs an HDR monitor on glass; VAAPI
10-bit needs the AMD box.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The vendored rate controller packs its wavelet block index into 16 bits
(RDOperation.block_offset_saving), so a mode whose 32x32-block count exceeds
u16::MAX wraps inside the controller and corrupts the bitstream — ~8K-class
4:4:4 territory. Compute the exact count (`block_count_32x32`, the counting walk
of upstream init_block_meta, pinned against the validated Apple WaveletLayout)
and expose `pyrowave_mode_fits_rdo`; the negotiator downgrades such a session to
4:2:0 before the Welcome (the honest-downgrade channel), and both encoders
refuse outright if one slips through rather than emit a wrapped stream.
Vendor patches: 0002-rdo-saving-clamp (analyze_rate_control.comp clamps the
saving accumulation to the target, same overrun class as 0001; slangmosh.hpp
regenerated), 0003-devel-encode-16bit-read (devel tool y4m 16-bit plane reads;
tool-only, kept so the vendored source stays honest).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PyroWave's wavelet encode runs on the GPU's compute/shader cores, so a GPU-bound
game starves it: submit spikes from ~2 ms to ~15 ms under a 95%+ game load and
the stream fps collapses. NVENC is immune (separate encoder ASIC). Two levers to
let the encode get scheduled ahead of the game's rendering:
- Windows process GPU scheduling: D3DKMTSetProcessSchedulingPriorityClass, env
PUNKTFUNK_GPU_PRIORITY = off|above-normal|high (default)|realtime. Best-effort,
once per process, non-fatal on refusal (enc/windows/pyrowave.rs).
- Global-priority Vulkan encode queue (Granite patch 0005): request a
VK_KHR_global_priority queue (PYROWAVE_QUEUE_PRIORITY = off|high|realtime,
default realtime), downgrading REALTIME→HIGH→none on NOT_PERMITTED so a refused
class never regresses the encoder to HEVC.
HONEST STATUS: on an RTX 4090 / Windows / WDDM neither moved the ~15 ms spikes —
the graphics-vs-compute preemption granularity is the wall, not the priority
level. Kept because both are correct, harmless (graceful fallback), and may help
other GPUs/drivers. For a GPU-saturated game the working levers are reducing the
encode's GPU cost (4:2:0/8-bit) or H.265; PyroWave holds full rate on the desktop
and in games that leave the GPU headroom.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The frame-driven capture loop set its rate-limit floor (`earliest`) relative to
`next`, which is re-based to the instant AFTER submit(). For an async encoder
(NVENC) submit() returns in ~0, so that anchor is ~frame-arrival and the loop
correctly waits for the next vsync — full framerate. But PyroWave's encode is
SYNCHRONOUS (~2 ms inline in submit()), so the anchor lands ~2 ms late every
frame: the loop misses the next arrival and samples one interval behind, making
the period `interval + encode`. That capped a 240 Hz source at ~158 fps (and a
360 Hz request at ~200) with the link and the encoder both idle — no drops.
Anchor the floor to this frame's arrival (`t_cap`) instead. The synchronous
encode now overlaps the interval rather than stacking onto it; the ≥0.9×interval
spacing from the last grab still caps the rate at ~1.11× target. No-op for async
NVENC (t_cap ≈ post-submit there), which is why H.26x already held full rate.
Measured on-glass (5120x1440@240, RTX 4090 host, macOS client): desktop now
holds 240 fps. Also reduces latency (samples fresher frames).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A provider plugin that runs on the host (e.g. the Playnite sync plugin) can
now reconcile cover art as an on-host FILE PATH instead of an inlined data
URL. `GET /library` rewrites such local-file art into `/library/art/<id>/
<kind>` proxy URLs (the same relative-proxy shape Steam art already uses),
and the art proxy serves the bytes from the stored path. Moonlight's
/appasset proxy reads the local file too.
This is what lets a large Playnite library sync with covers: the reconcile
payload carries paths, not bytes, so it no longer blows past the mgmt API's
request-body limit and scales to thousands of titles.
Cross-platform; local-path detection is Windows-shaped (Playnite is
Windows-only). Unit-tested (compiles + 5/5 art tests on Linux).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Real-world PyroWave streaming maxed ~2.5 Gbps with sagging fps while raw
transport does 4.8. Root-caused to serial per-frame paths at BOTH ends
(the transport was never the limit); this fixes the two dominant ones.
Host (vendored shim, patch 0004): pyrowave_encoder_encode_gpu_synchronous
allocated four Vulkan buffers (meta + bitstream, Device + CachedHost) on
EVERY frame. At 240 fps with MB-scale bitstreams that per-frame allocator
churn stalled the encode itself. Pool them on the encoder and reuse across
frames (recreate only on a size grow); the sizes are session-fixed, so it
is pure reuse after frame 1. On an RTX 4090 the 5120x1440 submit+fence-wait
drops ~15 ms -> ~1 ms, i.e. the host serial ceiling goes 64 -> 1025 fps
(444+HDR 44 -> 614). Safe under the synchronous encode model; re-validated
by pyrowave_win_smoke (Windows) and pyrowave_smoke/_444 (Linux). Applies to
both host encoder paths (they share the shim).
Client (Apple Metal decoder): WaveletBitstream.parse reserved the payload
buffer per packet (reserveCapacity(count + words), an exact realloc each of
~3000 packets/frame => O(n²)) and copied word-by-word. Reserve once up
front and memcpy each packet's coefficients in one shot (all Apple
platforms are little-endian, so the wire's LE u32s land verbatim; memcpy is
alignment-free). 5.44 ms -> 0.055 ms per 1.44 MB frame (25x); byte-identical
(parser unit tests + golden-frame PSNR unchanged).
Also:
- native.rs: PUNKTFUNK_PYROWAVE_MAX_MBPS caps PyroWave's open-loop Automatic
bitrate pin for hosts on a constrained link (unset => no cap; an explicit
client rate bypasses it). The pin is all-intra + ABR-off, so at a high
pixel rate it can outrun the fabric (4:4:4+HDR 5120x1440@240 pins ~5.3
Gbps, over a 5 GbE link) and the overshoot just becomes loss.
- pf-encode caps(): report the real opened chroma instead of a hardcoded
4:2:0 default, so a genuine 4:4:4 session no longer trips the spurious
"encoder chroma disagrees with the negotiated Welcome" warn. Also fix a
latent Windows reset() that rebuilt at 4:2:0 for a 4:4:4 session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dedicated single-game sessions now end cleanly when the launched game
quits, and Steam launches get gamescope's Steam integration without an
operator env knob.
End-on-exit: the existing APP_EXITED close only fired when gamescope's
PipeWire node vanished, which never happens for Steam — the nested
`steam` is the resident singleton client and stays up after a game
quits, so gamescope (and its node) never die and the stream sat on a
hidden Steam session forever. Add a Steam AppId reaper watcher
(pf-vdisplay .../gamescope/discovery.rs): steam_appid_from_launch()
parses steam://rungameid/<id>; wait_for_steam_game_exit() waits for the
game to start (<=300s grace) then exit (3s confirm); steam_game_running()
scans same-uid /proc for Steam's launch reaper matching both the
`SteamLaunch` and `AppId=<id>` argv tokens (exact-match; reaper lifetime
== game lifetime, so shader precompile can't false-trigger). The host
spawns a pf1-gamewatch thread for nested Steam launches that closes the
connection with APP_EXITED (launcher clients return to their library)
and sets quit/stop; cancelled via stop if the session ends first.
Non-Steam nested launches keep the node-death path (gamescope's child
IS the game).
Flags: auto-enable `--steam` whenever the launch is a Steam launch (was
only the global PUNKTFUNK_GAMESCOPE_STEAM knob, default-off) — in-game
overlay / Steam+X / gamepad-UI nav for Steam titles with no operator
config. New opt-in PUNKTFUNK_GAMESCOPE_GRAB_CURSOR adds
`--force-grab-cursor` for a real game launch (FPS mouselook); default
OFF because it forces relative mode, which breaks absolute-pointer
games/menus.
Verified: fmt clean; clippy -D warnings clean on the three crates;
pf-vdisplay 64/0 (incl. new steam-appid parse test); punktfunk-host
builds + 186/0; reaper /proc detection smoke-tested (detect, exact
non-match, gone-after-exit).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 3 of design/pyrowave-444-hdr.md. A PyroWave session now negotiates HDR
(10-bit) and 4:4:4 on a Windows host exactly like HEVC/AV1, and the Linux
client presents it through the real HDR10 path.
Host (Windows): BgraToYuvPlanes becomes mode-aware — SDR/BGRA and HDR/scRGB
variants at half- or full-res chroma. The HDR passes reuse HdrP010Converter's
exact colour math (scRGB -> PQ BT.2020 limited studio codes, verified by
hdr_p010_selftest) but write P010-style MSB-packed codes into two separate
shareable R16_UNORM/R16G16_UNORM textures; chroma keeps the pyrowave family's
centre-sited 2x2 box. idd_push pins the composition to the NEGOTIATED depth
(SDR sessions force advanced color off as before; 10-bit sessions enable it
and ride the FP16 ring), and the descriptor poller re-asserts that state
instead of following display flips the fixed-format encoder can't. The
encoder imports 8/16-bit planes per session and stamps the sequence header's
BT.2020/PQ/matrix bits on HDR (stamp_color_bits, extending 574e3e4e's range
stamp); supports_10bit/can_encode_10bit/can_encode_444 gates open (HDR
Windows-only — Linux capture has no HDR source).
Client: the plane ring becomes R16_UNORM for 10-bit sessions (with a
STORAGE_IMAGE format probe), the planar CSC pass joins the HDR10 swapchain
rebuild (set_hdr_mode previously destroyed it without rebuilding — latent),
st.hdr follows frame.color.is_pq(), and the planar push constants carry
depth-10 MSB-packed rows + the PQ tonemap mode, identical to the NV12 arm.
Verified: .173 (RTX 4090) deploy-config clippy + fmt + wire tests + the
extended pyrowave_win_smoke (10-case {SDR,HDR}x{420,444} matrix incl. R16
imports and header stamps); .21 (RTX 5070 Ti) clippy across 4 crates, host
186 tests, client/presenter/encode tests, both Linux GPU smokes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 2 of design/pyrowave-444-hdr.md. can_encode_444(PyroWave) now returns
true on Linux, so a client that advertises VIDEO_CAP_444 (the 4:4:4 setting)
and prefers PyroWave negotiates a full-chroma wavelet session end to end
(the client decoder side landed in 5eb930e7).
- rgb2yuv444.comp: the 4:4:4 twin of rgb2yuv.comp — one invocation per pixel,
full-res interleaved RG8 CbCr, no box filter/siting, byte-identical BT.709
limited coefficients; compiled .spv committed (glslangValidator -V, matches
the existing shader's toolchain).
- Encoder: chroma-conditional pyrowave create (open + reset), full-res chroma
plane + views, per-pixel dispatch, 4:2:0-only even-dims check.
- Tests: decode oracle grows a 4:4:4 mode (YUV444P CPU readback);
pyrowave_smoke_444 round-trips plane means AND drives the busy test card at
the ~2.6 bpp operating point asserting in-budget + run-to-run deterministic
AU sizes — the exact regime that silently corrupted before the vendored
payload_data fix (patches/0001), so this doubles as its regression test.
Verified on .21 (RTX 5070 Ti): clippy -D warnings, host tests, and both GPU
smokes (pyrowave_smoke + pyrowave_smoke_444) green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 1 of design/pyrowave-444-hdr.md. No behavior change yet: the handshake's
4:4:4 gate now admits PyroWave (probe = can_encode_444(codec), capture gate
inherently satisfied — the wavelet path always ingests an RGB source and does
its own CSC), but can_encode_444 stays false for PyroWave until the per-OS
full-res-chroma CSC variants land (Phase 2 Linux, Phase 3 Windows), so every
session still resolves 4:2:0/8-bit.
- Both host encoders take the negotiated ChromaFormat (bail on 444 for now);
the PUNKTFUNK_ENCODER=pyrowave lab override pins 4:2:0.
- Bitrate: the automatic ~1.6 bpp pin resolves AFTER depth+chroma and scales
x1.625 for 4:4:4 / x1.15 for 10-bit (factors from the Phase-0 fixture
matrix); the mid-stream mode-switch re-resolve threads the session's values.
- Client: PyroWaveDecoder builds its plane ring (full-res chroma when 444) and
creates the upstream decoder from the negotiated chroma, keeps chroma fixed
across mid-stream resizes, drops the even-dims requirement for 444, and
returns the negotiated Welcome ColorInfo as the frame colour contract
instead of hardcoded BT.709 (the wavelet bitstream has no VUI).
Verified on .21 (RTX 5070 Ti): clippy -D warnings (host+client+encode), host
186 tests, client + pf-encode tests, fmt, and the pyrowave_smoke GPU
round-trip through the patched vendored lib (97cf15e3).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Upstream sizes the raw quantized-payload worst-case buffer at
aligned_width*aligned_height*2 bytes: real headroom for 4:2:0's 1.5
samples/px, but half of what 4:4:4's 3 samples/px can produce. Busy 4:4:4
content overruns the buffer on the GPU and corrupts the adjacent meta/bucket
suballocations — nondeterministic corrupt bitstreams and encoder crashes at
ANY target bitrate (smooth content never trips it, which is why 4:2:0 and
simple 4:4:4 both look fine). Found by the Phase-0 measurement matrix for
design/pyrowave-444-hdr.md; fix validated alone via upstream's own devel
tools on the RTX 5070 Ti: deterministic byte-identical outputs across runs,
1080p + 4K, 8- and 16-bit, PSNR at the expected operating points.
Patch lives in crates/pyrowave-sys/patches/ and vendor-pyrowave.sh now
re-applies patches on re-vendor; PUNKTFUNK-VENDOR.txt records it. Upstream
report to follow. No wire/ABI change; 4:2:0 sizing unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pyrowave's encoder fills the BitstreamSequenceHeader with `= {}` and its C API
offers no way to set colour/range, so it signals ycbcr_range=0=FULL — but both
host CSCs (rgb2yuv.comp on Linux, BgraToYuvPlanes on Windows) always emit BT.709
LIMITED Y'CbCr (black = Y'16). A client that honours the VUI (the Apple wavelet
decoder reads bit 30 of word1) then skips the limited→full expansion and shows
washed-out, raised blacks — reported on both Linux and Windows hosts.
Patch the range bit HONEST (mark_limited_range in the shared pyrowave_wire, called
by both encoders after packetize). Clients that hardcode limited (the Vulkan
video_pyrowave path) are unaffected, and pyrowave's own decode ignores the flag
(raw Y'CbCr reconstruction). No client rebuild needed. Unit-tested + asserted in
pyrowave_win_smoke; the smoke decode still round-trips 100/180/60.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire PyroWave into the Windows host (design/pyrowave-windows-host-zerocopy.md).
Before this a macOS client + Windows host that both selected PyroWave silently ran
HEVC: the host never advertised CODEC_PYROWAVE and open_video_backend bailed.
Approach (zero-copy, no GPU→CPU→GPU): pyrowave owns its own Vulkan device
(create_device_by_compat, by render-GPU vendor/device-id — NOT LUID, invalid in
Session 0). The capturer runs a BGRA→YUV BT.709-limited CSC (matching rgb2yuv.comp)
into TWO SEPARATE shareable plane textures — full-res R8 Y + half-res R8G8 CbCr —
which the encoder imports into pyrowave's device. Separate single/two-component
textures import reliably on NVIDIA at any size; a single planar NV12 import does NOT
(the vendored interop test: "only very specific resource sizes" — confirmed on-glass:
1024² fine, 720p/1080p/1440p garbage). A shared D3D11 fence, signalled after the CSC,
is imported as a Vulkan timeline semaphore so the wavelet read is ordered after it.
- pf-encode: enc/windows/pyrowave.rs (Encoder impl, two-plane import + Linux-style
plane views); host_wire_caps advertises CODEC_PYROWAVE on Windows when the backend
isn't Software; open_video_backend routes a negotiated PyroWave session first;
pyrowave-sys on the Windows target; interop confirmed at open → clean HEVC fallback.
- pf-encode: shared, unit-tested enc/pyrowave_wire.rs (single source of truth for the
client-facing AU framing); Linux encoder uses it too.
- pf-capture: dxgi.rs BgraToYuvPlanes CSC; idd_push.rs pyrowave mode — forces the
virtual display SDR (the VideoProcessor can't ingest the FP16 HDR ring), a
two-plane shareable out-ring, a shared fence passed every frame (so a rebuilt
encoder re-imports it). Threaded via OutputFormat::pyrowave.
- pf-frame: D3d11Frame::pyro carries the CbCr plane + fence; OutputFormat::pyrowave.
Verified on .173 (RTX 4090): full-host build + clippy -D warnings (nvenc,amf-qsv) +
fmt --all --check; pyrowave_wire unit tests; pyrowave_win_smoke GPU test round-trips
distinct Y/Cb/Cr (100/180/60) exactly at 1024²/720p/1080p/1440p; Stage-0 interop
validated in the real Session-0 service context on-glass. Deployed to the box.
Owed: final on-glass picture/latency confirmation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The workspace went ring-only in 0.13.0 (aws-lc-sys breaks Windows CI), and
`ring` can sign with an existing RSA key but cannot *generate* one —
rcgen's ring backend returns `KeyGenerationUnavailable` for
`generate_for(&PKCS_RSA_SHA256)`. So `ServerIdentity::{load_or_create,
ephemeral}` panics with "rcgen RSA keygen / There is no support for
generating keys for the given algorithm" on any host without an existing
`cert.pem`. This is the shared trust root for both the GameStream TLS cert
and the QUIC identity clients pin, so a *fresh* 0.13.0 install can't start
the host at all (existing dev boxes survive only because they already have
a cert on disk); in CI every test that builds a `ServerIdentity` (all of
`mgmt::tests`, `gamestream::nvhttp`, and the `native::tests` synthetic host
via `serve()`) failed — 30 failures from this one cause.
Moonlight requires an RSA-2048 identity, so generate the key with the
pure-Rust `rsa` crate (already a dependency for the pairing signer) and
hand its PKCS#8 PEM to rcgen, whose ring backend *can* load and self-sign
with an externally supplied RSA key (`from_pkcs8_pem_and_sign_algo` →
`RsaKeyPair::from_pkcs8`). Return that same PEM so it stays byte-identical
to what `from_pems` re-parses.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`is_transient_io` only recognized `ENOBUFS` on unix; on Windows the
`#[cfg(not(unix))]` arm returned `false`, so `WSAENOBUFS` (10055) — which
Rust maps to `ErrorKind::Uncategorized`, missing the `WouldBlock` arm —
propagated out of the USO `send_gso` path through `Session::send_sealed`
and killed the session with `native::stream` "send failed — stopping
stream". A high-bitrate keyframe burst (one `WSASendMsg` USO super-buffer
is up to ~512 segments) momentarily exhausts the socket send buffer / AFD
non-paged pool; it's a lossy drop that FEC + the next frame recover,
exactly like the unix `ath11k` `ENOBUFS` case the classifier already
handles. Fires independently of client platform (Windows/macOS clients
both saw the crash) because it's the Windows *host's* send socket.
Add a `#[cfg(windows)]` arm matching `WSAENOBUFS` plus the
`WSAENET*`/`WSAEHOST*` network-path family (the Windows counterparts of
the droppable unix set), and extend the classifier test with per-platform
raw-errno coverage so the Windows CI runner exercises the 10055 path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docs/automation.md promises 'changes apply immediately, no restart' for BOTH
config paths, but the store loaded hooks.json once (OnceLock) — only PUT
/api/v1/hooks applied live, and a hand-edited file silently did nothing until
the next host start. Since the file is the accessible path most users will
take, make it real: get() re-stats the file per event (mtime + length) and
re-reads it on change, with the same lenient contract as startup (missing =
no hooks, invalid = disabled loudly). set() records the identity it wrote so
the API path doesn't trigger a spurious reload.
Validated live on the 0.13.0 host (.21): full connect/disconnect lifecycle
fires exec hooks (client.*/session.*/stream.*) — the reload gap was found
setting that test up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Companion to the host ring fix: pf-client-core declared rustls with
features=["ring"] but WITHOUT default-features=false, so rustls's default
aws-lc-rs backend came along too (feature tree: aws-lc-rs <- rustls default <-
pf-client-core default) — pulling aws-lc-sys 0.41.0 into the Windows client +
session + presenter builds, the same C dep that fails on the CI runner. Pin
default-features=false + [ring, logging, std, tls12] (matching the tray/host/core
pattern). aws-lc-sys now gone from EVERY windows-built crate (host, client-windows,
client-session, tray — all 'cargo tree -i aws-lc-sys' => no match). Code already
used rustls::crypto::ring::default_provider (library.rs); no behaviour change.
Validated: cargo tree (all 4 windows crates aws-lc-free) + .21 clippy -D warnings
(pf-client-core, pf-presenter, punktfunk-client-session) clean.
The Windows host clippy CI broke building aws-lc-sys 0.41.0 (a heavy C dep that
cl.exe fails to compile on the runner). The host was the ONLY thing pulling it:
its rcgen used features=["aws_lc_rs"], and bare `rustls`/`tokio-rustls` take
the default (aws-lc-rs) backend, and gamestream/{mod,tls}.rs installed the
aws_lc_rs crypto provider explicitly — a pre-existing deviation from the
project's documented ring-only design (punktfunk-core + the client are ring).
Align the host to ring: rcgen/rustls/tokio-rustls pinned to the `ring` feature
(keeping tls12 for Moonlight/GameStream TLS-1.2 clients) and the two GameStream
provider installs switched to `rustls::crypto::ring::default_provider()`. Both
providers offer the same standard TLS 1.2/1.3 suites, so it's transparent to the
TLS paths; aws-lc-sys/aws-lc-rs leave the tree entirely (`cargo tree -i
aws-lc-sys` => no match). Not caused by the preceding log fix (that touched no
crypto deps / the lock).
Validated: cargo tree (aws-lc-sys gone, windows target, amf-qsv,qsv features) +
.21 host clippy -D warnings (--features nvenc) clean. Owed: GameStream on-glass
re-check with a real Moonlight client (low risk — same TLS suites, client is
already ring).
The `wasapi` audio crate polls the default output device ~1×/s and
`log::debug!`s it (`wasapi::api default device Ok(...)`), flooding host.log
even at RUST_LOG=info: those `log`-crate records reach tracing through the
tracing-log bridge, and at *filter* time they carry the bridge's "log" shim
target, so neither a bare-info EnvFilter nor a `wasapi=warn` directive on the
file layer matches them (the ring already drops them post-hoc via
normalized_metadata + NOISY_DEBUG_TARGETS — the fmt layer filters pre-event and
can't).
Fix at the source: a shared `log_capture::install_global` replaces
SubscriberInitExt::init in both init paths (service file log + interactive
stderr) and inits the tracing-log bridge with `ignore_crate("wasapi")`, so the
poll records are dropped before they ever become tracing events — both sinks go
quiet, while every other log-crate dependency still reaches the console ring
(bridge max-level stays DEBUG). `wasapi` also added to NOISY_DEBUG_TARGETS as
belt-and-suspenders, and `log` moved dev-dep -> dep for LevelFilter.
Validated: .21 clippy (host --features nvenc) clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
From the 2026-07-17 on-glass finding (.173 RTX 4090): the IDD-push capturer's
depth-2 pipeline measured ~13ms of glass-to-glass latency over depth-1 at
60fps (17ms → 4ms) — the AU is ready in µs but depth-2 holds it a whole frame
interval unpolled while N+1 is submitted. Depth-2 only earns its keep under
GPU contention (it overlaps the convert of N+1 with the encode of N, avoiding
the depth-1 ~50fps collapse). So the native encode loop now runs depth-1 by
default and escalates to the capturer's max ONLY when it can't hold cadence at
depth-1 (a leaky-bucket over 'the frame's work overran its pacing deadline',
with a startup warmup skip), then holds there for the session (no oscillation;
de-escalation is a v2 item). PUNKTFUNK_IDD_ADAPTIVE=0 pins the capturer's full
depth (pre-adaptive behaviour); no effect where the capturer's max is already
1 (every non-IDD backend). GameStream plane untouched.
Pairs with the shipped REALTIME auto-gate (1bc156aa): uncontended sessions get
the ~13ms latency cut, genuinely GPU-bound ones keep the depth-2 overlap AND
the REALTIME priority. Validated: .21 clippy -D warnings (host --features nvenc).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
design/latency-reduction-2026-07.md T2.5's Linux half: the LINEAR dmabuf path
(gamescope's only offer) fed NVENC RGB, paying its internal RGB->YUV CSC on
the SM the game is saturating — the exact contention §5.A removed everywhere
else. The Vulkan bridge now carries a buffer-to-buffer RGB->NV12 compute
shader (rgb2nv12_buf.comp, BT.709 limited, coefficient-identical to
pf-encode's rgb2yuv.comp; whole-word writes so no 8-bit-storage feature is
needed): import dmabuf -> dispatch CSC into the exportable buffer -> CUDA
de-strides both planes into a pooled two-plane NV12 buffer. PUNKTFUNK_NV12
(default-on) now covers LINEAR; a CSC failure latches RGB for the stream
(mid-frame fallback, no dropped frame); 4:4:4 LINEAR sessions stay RGB (never
silently subsample). New ImportKind::LinearNv12 rides the existing worker IPC
(appended last per the wire-tag rule); cursor stays downstream (blend_nv12).
Validated: .21 clippy -D warnings (pf-zerocopy/pf-capture/host+nvenc) + 17
zero-copy tests. Owed: on-glass gamescope session (visual + dmon sm% check).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Surfaced by giving windows-host CI its missing pf-encode/pf-frame path triggers:
the T2.3 auto-gate (dxgi.rs) and the clipboard host (windows.rs) landed with
Windows clippy owed — three undocumented unsafe blocks, one manual checked
division, one comment orphaned off its statement, and a latent E0432 in
standalone pf-clipboard builds (WNDCLASSW needs Win32_Graphics_Gdi; the host
graph only compiled via feature unification).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
design/latency-reduction-2026-07.md tier 2, the two code-side halves:
- T2.2: the Linux direct-NVENC backend gains the two-thread retrieve
(PUNKTFUNK_NVENC_ASYNC, the same opt-in knob as Windows): the session stays
sync-mode (async events are Windows-only) but the blocking lock_bitstream
moves to a dedicated pf-nvenc-out thread — the NVENC guide's sanctioned
submit-thread/output-thread split. poll() drains completions non-blocking,
submit() backpressures at PUNKTFUNK_NVENC_ASYNC_DEPTH (default 4) in-flight;
map/unmap and every other session call stay on the encode thread; teardown
joins the thread before destroying the session. Under a GPU-saturating game
completed frames queue instead of serializing capture on the encode wait.
- T2.3: PUNKTFUNK_GPU_PRIORITY_CLASS gains 'auto' AND IT IS THE NEW DEFAULT
(gpu-contention §5.C): HIGH immediately, then REALTIME where the documented
NVIDIA+HAGS+near-full-VRAM NVENC hang cannot bite — HAGS probed once via
D3DKMT WDDM_2_7_CAPS (off => REALTIME outright); HAGS on => a pf-gpu-prio
monitor flips REALTIME<->HIGH on LOCAL-segment VRAM headroom (downgrade
>92% of budget, restore <=85% for 3x2s polls). 'high' restores the old
static default; 'realtime' pins it (operator owns the hazard).
Validated: .21 clippy -D warnings (punktfunk-host --features nvenc) against
the QSV-merged main; .133 Windows cargo check of pf-frame + punktfunk-host.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8/8 live tests green on the Arc Pro laptop (VPL 2.16): H264/HEVC SDR, HEVC
Main10+HDR metadata, AV1 10-bit, LTR-RFI clean recovery (no IDR; driver honors
mfxExtRefListCtrl on H264/HEVC/AV1), no-IDR bitrate retarget. Zero-copy
GetNativeHandle path confirmed (single textures, no array-slice hazard).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reconciles the qsv line (libvpl-sys + qsv.rs + dispatch/probe wiring, verified
on the .133 Windows box: check + clippy + dispatcher link test + unit tests)
with the concurrent clipboard JPEG/GIF pass-through commits; Cargo.lock union
re-adds the libvpl-sys entry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Transcoding every image to PNG was the Phase-1 floor, but it bloats lossy
originals (a copied JPEG re-encoded lossless is 5-10x the bytes for zero
quality gain — feeding the render-timeout on constrained links) and flattens
GIFs to one frame. The wire already carries kind LISTS, so offer the original
format beside the portable fallback and let the destination pick:
- New wire kinds image/jpeg + image/gif (§3.5 extension; strings only, no
protocol change). image/png stays the universal floor every peer accepts.
- macOS: public.jpeg / com.compuserve.gif rows pass through verbatim both
directions; the PNG floor is announced whenever any image is present.
- Windows: registered JFIF/GIF formats read + promised verbatim; every image
offer still promises CF_DIB, whose delayed render now fetches the richest
offered kind (png > jpeg > gif) through the generalized image_to_dib.
- Linux maps gain the matching rows (native MIME pass-through).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
design/latency-reduction-2026-07.md tier 1, remaining halves:
- T1.1: the native encode loop wakes on the capture's ACTUAL arrival instead of
sampling at a free-running tick — deletes the sample-and-hold (~half a frame
interval on average, a full one worst-case: ~8ms avg @60fps). New
Capturer::supports_arrival_wait/wait_arrival pair (IDD-push waits its
frame-ready event against the shared-header token; the PipeWire portal blocks
its channel with a pending stash); backends without an arrival signal — and
PUNKTFUNK_FRAME_DRIVEN=0 — keep the legacy tick bit-identically. A
0.9x-interval rate floor caps encode at ~1.11x target when the compositor
outruns the session; a +0.5x-interval keepalive keeps static desktops
re-encoding at 1.5x-interval cadence. Pacing deadlines re-anchor to the
actual submit so they can't drift against the arrival clock. GameStream
plane untouched.
- T1.4: the jump-to-live detectors run on WALL-CLOCK now (STANDING_TIME /
FLUSH_AFTER = 250ms) instead of 30-frame counts whose meaning scaled with
fps (500ms @60 but 125ms @240 — and stretching further under T1.1's slower
static-scene repeats). The queue trip also requires depth still >= high, so
a hysteresis-band hover can't fire on elapsed time alone.
Validated: .21 Linux 185 core + 177 host + pf-capture tests, clippy
-D warnings; .133 Windows cargo check of pf-capture + punktfunk-host green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
design/latency-reduction-2026-07.md T0.1/T0.2/T1.2/T1.3:
- T1.2 rate-capped front-loaded pacing: the paced overflow's budget is now
min(0.9x slack, overflow wire time at ~3x the live encoder bitrate)
(PUNKTFUNK_PACE_FACTOR, 0 = legacy deadline-only spread). A 300 KB-1 MB
frame's tail leaves in ~2-5 ms instead of smearing across ~15 ms at 60 fps;
GameStream schedule byte-identical (pins unchanged).
- T1.3 data-first wire order: packetize emits every block's data shards before
any parity (per-block parity pools keep all blocks' parity alive for the
second pass), so lossless completion stops waiting behind the parity tail.
EOF = last emitted packet; receiver already order-agnostic.
- T0.1 staged 0xCF: HostTiming gains an append-extensible per-stage tail
(queue/encode/pace us; seal+channel-wait derived as residual) - no cap bit
needed, old peers read the 13-byte prefix. Joined client-side into
Stats::host_{queue,encode,xfer,pace}_ms, the OSD detailed tier, and the
probe's report.
- T0.2 true on-glass present timing: VK_KHR_present_id/present_wait enabled
when supported; a PresentTimer waiter thread resolves each present id to
real visibility, replacing the submit-time display stamp (which undercounts
by up to a refresh and hides a silent-FIFO standing queue).
Validated on .21: core 185 + host 185 tests, pf-presenter 19, clippy
-D warnings across all five touched crates; loss-harness recovery curve
unchanged; C ABI harness round-trips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
macOS image copies rarely carry public.png — screenshots/Preview put TIFF on
the pasteboard, browsers add WebP/AVIF/GIF (observed live: TIFF+RTFD+WebP+AVIF+
8BPS+GIF, no PNG) — so the literal .png announce never fired and images
silently didn't sync. Announce image/png whenever a convertible image is
present (TIFF/HEIC alongside the native PNG check) and convert at serve time
via NSImage -> NSBitmapImageRep PNG (lazy, per design §3.5 — bytes still cross
only on a host paste).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Brings origin's release prep (widgets App Store profile mapping + workspace
version 0.13.0) into the local line that landed the Linux stream sink, the
Windows client-only audio rework, the July security-audit fixes, cross-client
render-scale, and the widgets scheme. release.yml was byte-identical on both
sides; no other file overlap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>