From e5eec51a78679a60fd1fd4a938d59831af1d11e6 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sat, 18 Jul 2026 19:28:28 +0200 Subject: [PATCH] docs(pyrowave): document 4:4:4 + HDR and add an interactive bitrate calculator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New "4:4:4 and HDR" section: what each mode buys, the ~1.6× / +15% bitrate cost, that HDR needs a Windows host today (Linux capture has no HDR source), and that the two combine (~1.9× the 4:2:0 SDR rate). - Interactive (registered in the MDX components map): resolution (presets or custom), frame rate, 4:2:0/4:4:4, SDR/HDR -> the estimated Automatic pin, bits/pixel, per-frame size, and which link tier it needs. Formula mirrors the host's resolve_bitrate_kbps_for exactly. - Expanded the bandwidth table with 120 Hz rows; note the big modes want 5/10 GbE. - Document PUNKTFUNK_PYROWAVE_MAX_MBPS (cap the open-loop pin on a constrained link) in configuration.md. - pyrowave.md -> .mdx so the page can host the component. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs-site/content/docs/configuration.md | 1 + .../docs/{pyrowave.md => pyrowave.mdx} | 45 ++- .../src/components/BitrateCalculator.tsx | 327 ++++++++++++++++++ docs-site/src/components/mdx.tsx | 3 + 4 files changed, 368 insertions(+), 8 deletions(-) rename docs-site/content/docs/{pyrowave.md => pyrowave.mdx} (63%) create mode 100644 docs-site/src/components/BitrateCalculator.tsx diff --git a/docs-site/content/docs/configuration.md b/docs-site/content/docs/configuration.md index f1613275..bc1d3e85 100644 --- a/docs-site/content/docs/configuration.md +++ b/docs-site/content/docs/configuration.md @@ -90,6 +90,7 @@ See your desktop page ([KDE](/docs/kde), [GNOME](/docs/gnome)) for when to set t | `PUNKTFUNK_FEC_PCT` | `N` (percent) | Forward-error-correction redundancy for lossy links (the default is sensible for a normal LAN). Higher = more loss-resilient, more bandwidth. | | `PUNKTFUNK_10BIT` | `1` · `0` *(default on)* | HEVC Main10 / HDR. **On by default** — the host permits 10-bit; a session goes 10-bit only when the client advertises it (behind the client's HDR setting). Set `0` to force 8-bit. **Windows host only** (the Linux host stays 8-bit). | | `PUNKTFUNK_444` | `1` · `0` *(default on)* | Full-chroma HEVC 4:4:4 (Range Extensions) — sharper text/desktop, no chroma loss. **On by default** on the host; the client's own 4:4:4 setting (default off) is the real switch. Set `0` to force 4:2:0. **punktfunk/1 native only** (Moonlight stays 4:2:0), HEVC-only, honored only when the client advertises 4:4:4 **and** the GPU supports it (probed; NVENC is the validated path — VAAPI/AMF/QSV decline). Independent of 10-bit. | +| `PUNKTFUNK_PYROWAVE_MAX_MBPS` | `N` (Mbps) | Cap the [PyroWave](/docs/pyrowave) Automatic bitrate pin, for a host on a link that the open-loop pin can outrun (e.g. 4:4:4 + HDR at 5120×1440@240 pins ~5.3 Gbps, over a 5GbE link). Unset = no cap. Only affects Automatic (bitrate `0`) PyroWave sessions; an explicit client bitrate bypasses it. | | `PUNKTFUNK_DSCP` | `1` | Opt-in DSCP / `SO_PRIORITY` QoS tagging on the media sockets. No-op on the wire on Windows without a qWAVE policy. | | `PUNKTFUNK_OH264_THREADS` / `PUNKTFUNK_OH264_GOP` | `N` | Software (openh264) encoder tuning: encode threads (default 2 — latency over throughput) and GOP length (default 0 = encoder-auto). Only relevant with `PUNKTFUNK_ENCODER=software`. | diff --git a/docs-site/content/docs/pyrowave.md b/docs-site/content/docs/pyrowave.mdx similarity index 63% rename from docs-site/content/docs/pyrowave.md rename to docs-site/content/docs/pyrowave.mdx index b1cadca9..b6f9c2a3 100644 --- a/docs-site/content/docs/pyrowave.md +++ b/docs-site/content/docs/pyrowave.mdx @@ -27,23 +27,52 @@ either side can't, the session silently falls back to the normal codec ladder. ## What it costs -Bandwidth. At the codec's ~1.6 bits-per-pixel operating point (4:2:0, 60 fps): +Bandwidth. At the codec's ~1.6 bits-per-pixel operating point (4:2:0, SDR): | Mode | Bitrate | |---|---| | 1280×800 @ 60 (Deck) | ≈ 100 Mbps | | 1920×1080 @ 60 | ≈ 200 Mbps | +| 1920×1080 @ 120 | ≈ 400 Mbps | | 2560×1440 @ 60 | ≈ 355 Mbps | +| 2560×1440 @ 120 | ≈ 710 Mbps | | 3840×2160 @ 60 | ≈ 800 Mbps | +| 3840×2160 @ 120 | ≈ 1.6 Gbps | -120 Hz doubles these, 4:4:4 multiplies them by ~1.6, and an HDR (10-bit) session adds ~15 %. -Gigabit Ethernet tops out around 940 Mbps of payload, so 4K60 wants 2.5GbE (or a lower -rate). **Do not run this over Wi-Fi** — that's what HEVC/AV1 are for. +Frame rate scales the rate linearly, [4:4:4](#444-and-hdr) multiplies it by ~1.6, and an +[HDR](#444-and-hdr) (10-bit) session adds ~15 %. Estimate any combination: -PyroWave follows the same 4:4:4 and HDR settings as HEVC/AV1: a session negotiates -full-chroma 4:4:4 when your client's 4:4:4 setting is on, and HDR (BT.2020 PQ, carried in -16-bit planes) when the host's display pipeline is HDR — currently the Windows host only -(the Linux host's capture path has no HDR source yet, so Linux-hosted sessions are SDR). + + +Gigabit Ethernet tops out around 940 Mbps of payload, so 4K60 wants 2.5GbE and the big +4:4:4 / HDR / high-refresh modes want 5GbE or 10GbE. **Do not run this over Wi-Fi** — that's +what HEVC/AV1 are for. + +## 4:4:4 and HDR + +PyroWave carries **full-chroma 4:4:4** and **HDR** the same way it carries everything else — +intra-only, every frame a keyframe — so the low-latency and clean-loss properties above hold +in these modes too. Both are negotiated per session from your client's settings, exactly like +HEVC/AV1; nothing PyroWave-specific to turn on beyond picking the codec. + +- **4:4:4 (full chroma).** With your client's **4:4:4** setting on, the session encodes chroma + at full resolution instead of subsampled 4:2:0 — sharp coloured text, thin UI lines, and + red/blue edges that 4:2:0 softens. It costs ~1.6× the bitrate (chroma compresses better than + luma, so it is less than the 2× the extra samples imply). Available on Linux and Windows + hosts. +- **HDR (10-bit, BT.2020 PQ).** With HDR on and an HDR host display pipeline, the session + carries a 10-bit BT.2020 PQ signal in 16-bit planes and adds ~15 % to the bitrate. **HDR + needs a Windows host today** — the Linux host's capture path has no HDR source yet, so + Linux-hosted sessions are SDR (4:4:4 still works). +- The two combine: a 4:4:4 **and** HDR session applies both factors (~1.6 × 1.15 ≈ 1.9× the + 4:2:0 SDR rate). The Apple and Rust clients decode whatever the session negotiated — 4:2:0 + or 4:4:4, SDR or HDR — with no extra setup. + +At the top end this gets demanding: 4:4:4 + HDR at a super-ultrawide 5120×1440@240 pins around +5.3 Gbps, which is more than a 5GbE link carries. On a link that can't keep up, either set an +explicit lower bitrate on the client or cap the host's Automatic pin with +[`PUNKTFUNK_PYROWAVE_MAX_MBPS`](/docs/configuration) — otherwise the overshoot just becomes +dropped packets. ## Turning it on diff --git a/docs-site/src/components/BitrateCalculator.tsx b/docs-site/src/components/BitrateCalculator.tsx new file mode 100644 index 00000000..a0d99dac --- /dev/null +++ b/docs-site/src/components/BitrateCalculator.tsx @@ -0,0 +1,327 @@ +// Interactive PyroWave bitrate estimator for the docs. The formula mirrors the host's +// `resolve_bitrate_kbps_for` (crates/punktfunk-host/src/native.rs): the Automatic pin is +// ~1.6 bits/pixel for 4:2:0, ~2.6 bpp for 4:4:4, +15% for a 10-bit/HDR session, clamped to +// [0.5 Mbps, 8 Gbps]. Self-contained (no design-system imports) so it renders even where the +// full workspace UI packages are unavailable; themed via Fumadocs' `--color-fd-*` variables. +import { useState } from 'react' + +type Preset = { label: string; w: number; h: number } + +const RES_PRESETS: Preset[] = [ + { label: '1280 × 800 — Steam Deck', w: 1280, h: 800 }, + { label: '1920 × 1080 — 1080p', w: 1920, h: 1080 }, + { label: '2560 × 1440 — 1440p', w: 2560, h: 1440 }, + { label: '3440 × 1440 — ultrawide', w: 3440, h: 1440 }, + { label: '3840 × 2160 — 4K', w: 3840, h: 2160 }, + { label: '5120 × 1440 — super-ultrawide', w: 5120, h: 1440 }, +] + +const FPS_PRESETS = [30, 60, 90, 120, 144, 240] + +// Practical payload ceilings (a bit under line rate — headers, FEC, framing). +const LINKS = [ + { label: 'Gigabit', mbps: 940 }, + { label: '2.5 GbE', mbps: 2350 }, + { label: '5 GbE', mbps: 4700 }, + { label: '10 GbE', mbps: 9400 }, +] + +const MIN_MBPS = 0.5 +const MAX_MBPS = 8000 + +function pyrowaveMbps( + w: number, + h: number, + fps: number, + chroma444: boolean, + hdr: boolean, +): number { + if (!(w > 0) || !(h > 0) || !(fps > 0)) return 0 + const bppX10 = chroma444 ? 26 : 16 + let kbps = (w * h * fps * bppX10) / 10 / 1000 + if (hdr) kbps = (kbps * 115) / 100 + kbps = Math.min(Math.max(kbps, MIN_MBPS * 1000), MAX_MBPS * 1000) + return kbps / 1000 +} + +const card: React.CSSProperties = { + border: '1px solid var(--color-fd-border, #e5e7eb)', + borderRadius: '0.75rem', + background: 'var(--color-fd-card, transparent)', + padding: '1.25rem', + margin: '1.5rem 0', +} +const label: React.CSSProperties = { + display: 'block', + fontSize: '0.75rem', + fontWeight: 600, + letterSpacing: '0.02em', + textTransform: 'uppercase', + color: 'var(--color-fd-muted-foreground, #6b7280)', + marginBottom: '0.35rem', +} +const field: React.CSSProperties = { + width: '100%', + padding: '0.45rem 0.6rem', + borderRadius: '0.5rem', + border: '1px solid var(--color-fd-border, #e5e7eb)', + background: 'var(--color-fd-background, transparent)', + color: 'var(--color-fd-foreground, inherit)', + fontSize: '0.9rem', +} + +function Toggle({ + active, + onClick, + children, +}: { + active: boolean + onClick: () => void + children: React.ReactNode +}) { + return ( + + ) +} + +export default function BitrateCalculator() { + const [presetIdx, setPresetIdx] = useState(1) // 1080p + const [custom, setCustom] = useState(false) + const [cw, setCw] = useState(1920) + const [ch, setCh] = useState(1080) + const [fps, setFps] = useState(60) + const [chroma444, setChroma444] = useState(false) + const [hdr, setHdr] = useState(false) + + const preset = RES_PRESETS[presetIdx] ?? RES_PRESETS[1]! + const w = custom ? cw : preset.w + const h = custom ? ch : preset.h + + const mbps = pyrowaveMbps(w, h, fps, chroma444, hdr) + const gbps = mbps / 1000 + const bpp = w > 0 && h > 0 && fps > 0 ? (mbps * 1e6) / (w * h * fps) : 0 + const frameKB = fps > 0 ? (mbps * 1e6) / 8 / fps / 1024 : 0 + const needed = LINKS.find((l) => l.mbps >= mbps) + + const big = + mbps >= 1000 ? `${gbps.toFixed(2)} Gbps` : `${Math.round(mbps)} Mbps` + + return ( +
+
+
+ + +
+ + {custom && ( + <> +
+ + setCw(Math.max(0, Number(e.target.value)))} + /> +
+
+ + setCh(Math.max(0, Number(e.target.value)))} + /> +
+ + )} + +
+ + +
+ +
+ +
+ setChroma444(false)}> + 4:2:0 + + setChroma444(true)}> + 4:4:4 + +
+
+ +
+ +
+ setHdr(false)}> + SDR (8-bit) + + setHdr(true)}> + HDR (10-bit) + +
+
+
+ +
+
+ ≈ {big} +
+
+ {bpp.toFixed(2)} bits/pixel · {Math.round(frameKB)} KB per frame ·{' '} + {needed ? `needs ${needed.label}` : 'over 10 GbE — lower the mode'} +
+
+ +
+ {LINKS.map((l) => { + const fits = l.mbps >= mbps + return ( +
+ + {l.label} + +
+
+
+ + {Math.round((mbps / l.mbps) * 100)}% + +
+ ) + })} +
+ +

+ Estimate of the Automatic bitrate a PyroWave session pins for this mode. Link bars use a + practical payload ceiling (below line rate). The pin is capped at 8 Gbps; on a constrained + link a host can cap it lower with PUNKTFUNK_PYROWAVE_MAX_MBPS. +

+
+ ) +} diff --git a/docs-site/src/components/mdx.tsx b/docs-site/src/components/mdx.tsx index 50495985..002349c4 100644 --- a/docs-site/src/components/mdx.tsx +++ b/docs-site/src/components/mdx.tsx @@ -1,9 +1,12 @@ import defaultMdxComponents from 'fumadocs-ui/mdx' import type { MDXComponents } from 'mdx/types' +import BitrateCalculator from '@/components/BitrateCalculator' export function getMDXComponents(components?: MDXComponents) { return { ...defaultMdxComponents, + // Custom components usable in any .md/.mdx without a per-file import. + BitrateCalculator, ...components, } satisfies MDXComponents }