Commit Graph
8 Commits
Author SHA1 Message Date
enricobuehler 49f5c815ea feat(pf-encode): PyroWave can stream its AU to the wire — and newest-wins was never in the way
PW6 was gated on one question: what happens to the client's newest-wins
draining when a PyroWave AU arrives in pieces, given that
`Session::set_deliver_frame_parts` refuses to combine with an all-intra
stream. The answer is that the doc and the plan conflated two different
axes, and the question never applied to this package.

Host STREAMED_AU chunks change only the WIRE shape. The reassembler
completes such a frame exactly like a whole one (`block_count != 0 &&
blocks_ok == block_count`) and hands up ONE Frame, so the frame channel
still sees one entry per AU and the drain is untouched.

What newest-wins genuinely cannot survive is the client's SEPARATE prefix
delivery, and the mechanism is sharper than "assumes whole AUs" said:
`FrameChannel::pop` counts QUEUE ENTRIES and takes one entry to be one AU.
With parts on, one AU pushes several, so `len > 1` stops meaning "the
consumer is behind" — the drain fires mid-AU, returns a SUFFIX and clears
that same AU's prefixes. For PyroWave that is fatal rather than lossy: the
sequence header lives in window 0 of every AU (`au_dims` reads it there), so
every frame would arrive headerless, and `FramePart`'s own orphan contract
would have a correct consumer abandon essentially all of them. Written into
`pop`, `set_deliver_frame_parts` and the handshake, together with what a fix
would take (skip whole SUPERSEDED AUs, never split one).

That answer shrinks what this package may claim, so the code says so
plainly. `encode_frame` is synchronous: the whole AU exists before the first
chunk can be polled, so `poll_chunk` is not "emit as produced" and there is
no encode/send overlap here (PW6 ⟂ PW5, confirmed). And with the client
still receiving one whole Frame there is no decode-while-arriving either —
the "~7 ms, decouple e2e latency from AU size" framing needs client work
this commit does not do. What IS left is real and host-side: the whole-AU
path FEC-protects, packetizes and seals the entire ~830 KB AU before its
first datagram may leave the socket, while the streamed path seals and paces
each FEC block as it completes.

All of the cutting lives in the shared `pyrowave_wire` helper, which
compiles and unit-tests on every platform, so both backends' `poll_chunk` /
`supports_chunked_poll` are thin delegations — the Windows backend cannot be
compiled from a Linux box, and logic written into it directly would ship
unverified. Chunks are whole numbers of framing windows because `build_au`
gives each window exactly ONE kind; that also makes them shard-aligned for
free, which is what the sealer's sentinel bases require. Dense mode never
streams (no window framing to cut on). `poll()` now errors while a chunk
cursor is live — the trait's one-drain-method-per-AU contract, where
double-emitting would put the same bytes on the wire twice under one frame
index — and `reset()` drops the cursor so a rebuild cannot splice a dead
AU's tail onto a fresh one. No new Encoder trait method, so neither the
TrackedEncoder forwarding trap nor the EncoderCaps default trap is in play.

Shipped OFF: `PUNKTFUNK_PYROWAVE_STREAMED_AU=1` arms it,
`PUNKTFUNK_PYROWAVE_CHUNK_KIB` tunes the 256 KiB target. The pre-registered
partial-delivery trap is real and now has a named cost — an unpinned
streamed frame (final block lost) is excluded from partial delivery, where
the whole-AU path still hands the consumer a usable blur, and PyroWave
clients opt into partials unconditionally. The netem loss-harness leg is the
prerequisite for default-on and has not been run.
2026-08-08 19:23:05 +02:00
enricobuehlerandClaude Fable 5 6d1baa0add fix(pf-encode/pyrowave): the bitrate pin holds on the WIRE, not the raw bitstream
A datagram-aligned PyroWave session inflates the codec bitstream ×1.2–1.3
on its way to the wire — greedy packing of few-hundred-byte atomic block
packets into 1408 B windows zero-pads most window tails, plus the 4-byte
prefixes and FRAG chains. The 2026-07 field report's 1440p60 10-bit
"Automatic" pin of 407 Mb/s put a measured 550 Mb/s on a 1 GbE link;
nothing enforced the pin past the rate controller.

New shared WireBudget (pyrowave_wire.rs, both backends): tracks the real
per-frame AU/bitstream ratio as a ×1024 fixed-point EMA (prior ×1.25,
weight 1/8, clamped ×1.0–×2.0) and deflates the budget handed to
pyrowave's rate control by it, so the windowed AU lands on the configured
rate. Sealed-datagram framing (+4.5%) and FEC parity stay uncompensated —
H.26x sessions carry those on top of the configured bitrate too, and the
pin must mean the same thing for every codec.

Dense (non-chunked) sessions are untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 12:51:11 +02:00
enricobuehlerandClaude Opus 5 6c97c00add fix(encode/pyrowave): the RDO block-index cap must cover 4:2:0, not just 4:4:4
The vendored rate controller packs the 32x32 block index into the low 16 bits of
`RDOperation::block_offset_saving` (pyrowave-sys patches/0002). Past u16::MAX the
index collides with the `saving` field, the resolve over-credits, and the emitted
payload can overshoot the buffer `pyrowave_encoder_packetize` writes into — whose
only bounds check is an `assert` that the Release (NDEBUG) vendored build compiles
out. There is no Rust-side guard behind it.

All three callers of `pyrowave_mode_fits_rdo` hardcoded 4:4:4, leaving 4:2:0
unchecked — but 4:2:0 overflows too, just later: 8192x6144 is 73728 blocks and
8192x8192 is 98304, while `Codec::PyroWave.max_dimension()` permits 8192 per axis,
so both are reachable from a client-requested `mode=WxHxFPS`. Worse, the host's
only use of the helper (native/handshake.rs) is a 4:4:4 -> 4:2:0 downgrade, so an
oversized mode was actively routed INTO the unguarded branch.

The cap now lives in `validate_dimensions`, checked against 4:2:0 — the most
permissive chroma, so a mode that cannot fit there fits no PyroWave session at any
chroma. That is the single chokepoint both the negotiator (handshake.rs:180) and
`open_video_backend` already run. Both encoder opens additionally check the chroma
actually being opened: the 4:4:4 half, plus defence in depth for the
`PUNKTFUNK_ENCODER=pyrowave` lab override.

Not dormant: punktfunk-host has `default = ["pyrowave"]` and no build passes
`--no-default-features`, so these backends ship in every host binary.

Tests pin the arithmetic (8192x6144 = 73728, 8192x8192 = 98304, 7680x4320 still
fits) and assert the cap does not leak to H.265/AV1, which have no such controller.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 00:32:37 +02:00
enricobuehlerandClaude Opus 4.8 00f759ec72 style(pf-encode): clear the clippy gate on the HDR/PyroWave additions
`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>
2026-07-19 10:23:47 +02:00
enricobuehlerandClaude Fable 5 75474dcc90 fix(pyrowave): guard 4:4:4 modes that overflow the rate controller's block index
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>
2026-07-19 01:16:51 +02:00
enricobuehlerandClaude Fable 5 188edde2b3 feat(pyrowave): Windows host HDR + 4:4:4, Rust client HDR present
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>
2026-07-18 13:21:06 +02:00
enricobuehlerandClaude Opus 4.8 574e3e4e3f fix(pyrowave): signal ycbcr_range=LIMITED in the sequence header
docker / deploy-docs (push) Successful in 23s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 22m2s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 22m8s
apple / swift (push) Successful in 1m23s
apple / screenshots (push) Successful in 6m20s
ci / docs-site (push) Successful in 1m10s
ci / web (push) Successful in 1m35s
android / android (push) Successful in 13m8s
windows-host / package (push) Successful in 16m16s
ci / bench (push) Successful in 5m21s
decky / build-publish (push) Successful in 20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 11s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 5m28s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
arch / build-publish (push) Successful in 18m52s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 12s
deb / build-publish-host (push) Successful in 9m38s
deb / build-publish (push) Successful in 12m35s
ci / rust (push) Successful in 19m12s
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>
2026-07-18 11:39:44 +02:00
enricobuehlerandClaude Opus 4.8 ebd9967547 feat(pyrowave): Windows host encoder — separate-plane zero-copy D3D11→Vulkan
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>
2026-07-18 11:39:44 +02:00