Files
punktfunk/crates/punktfunk-host/src/hdr.rs
T
enricobuehler 3526517eb1
ci / rust (push) Failing after 45s
apple / swift (push) Successful in 57s
ci / web (push) Successful in 39s
ci / docs-site (push) Successful in 38s
windows-host / package (push) Successful in 3m26s
android / android (push) Successful in 3m40s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m24s
deb / build-publish (push) Successful in 2m10s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m22s
decky / build-publish (push) Successful in 25s
ci / bench (push) Successful in 4m44s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 16s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m4s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m7s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 3m5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m45s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 30s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m37s
flatpak / build-publish (push) Successful in 4m17s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m30s
docker / deploy-docs (push) Successful in 23s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 7m53s
feat: HDR Step-0 colour-metadata transport + security-audit hardening
Two strands, entangled in punktfunk1.rs, committed together (one builds-green tree).

HDR pipeline Step 0 — glass-to-glass colour-metadata transport (docs/hdr-pipeline-plan.md):
- Protocol/ABI: ColorInfo on the Welcome + a 0xCE HdrMeta datagram carry the source colour
  space + HDR10 static mastering metadata (quic.rs, abi.rs connect_ex5 fixing caps=0).
- New platform-independent, unit-tested HDR static-metadata helpers (hdr.rs): chromaticities
  (1/50000), mastering luminance (0.0001 cd/m2), MaxCLL/MaxFALL in HDR10/ST.2086 units.
- Capture/encode hooks (capture.rs, encode.rs set_hdr_meta) + Linux client / probe plumbing.

Security-audit hardening — top 3 from docs/security-review.md, each adversarially verified:
- #1 [HIGH] Secret file permissions. The host key.pem/cert.pem and both trust stores are now
  written owner-only: 0600 + dir 0700 on Unix (mirrors mgmt_token), best-effort
  SYSTEM/Administrators/OWNER-only icacls DACL on Windows (%ProgramData% is Users-readable).
  Closes a local key-disclosure -> host-impersonation gap. New gamestream::{create_private_dir,
  write_secret_file} + a 0600 regression test.
- #2 [HIGH] Native SPAKE2 PIN is single-use. The PIN is consumed the moment the host sends its
  key-confirmation (which lets the client test its one guess), before reading the proof, so any
  completed attempt -- right OR wrong -- disarms the window. A wrong PIN isn't observable
  host-side (the client aborts before sending its proof), so consuming on first attempt is what
  delivers the documented "one online guess" instead of an unbounded brute-force of the static
  4-digit PIN. Test verifies single-use.
- #3 [MEDIUM] RTSP packetSize is bounded ([64,2048] in stream_config) and VideoPacketizer::new
  uses saturating .max(1), killing a PRE-AUTH div-by-zero/underflow panic of the video thread.
  Tests for {0,15,16,17} + out-of-range rejection.

fmt + clippy -D warnings clean; full workspace test suite green (93 host tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 09:07:59 +00:00

169 lines
6.4 KiB
Rust

//! Pure HDR static-metadata helpers shared by the capture (source mastering metadata) and encode
//! (in-band SEI) paths — kept platform-independent and unit-tested here so the byte-level logic is
//! verified on every target, even though the only *callers* of the SEI builders are the Windows
//! NVENC path (`encode/nvenc.rs`) and of the display conversion the Windows DXGI/WGC capturers.
//!
//! Units follow the HDR10 standards so the values pass straight through:
//! - chromaticities in 1/50000 increments (SMPTE ST.2086 / DXGI `DXGI_HDR_METADATA_HDR10`),
//! - mastering luminance in 0.0001 cd/m²,
//! - content light level (MaxCLL/MaxFALL) in cd/m² (nits).
use punktfunk_core::quic::HdrMeta;
/// HEVC/H.264 SEI payload type for `mastering_display_colour_volume` (SMPTE ST.2086). Same code
/// point in AVC and HEVC.
pub const SEI_TYPE_MASTERING_DISPLAY_COLOUR_VOLUME: u32 = 137;
/// HEVC/H.264 SEI payload type for `content_light_level_info` (CEA-861.3 MaxCLL/MaxFALL).
pub const SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO: u32 = 144;
/// Quantize a CIE xy chromaticity coordinate (0.0..=1.0) to ST.2086 1/50000 units.
fn xy_to_2086(v: f32) -> u16 {
(v * 50000.0).round().clamp(0.0, 65535.0) as u16
}
/// Build an [`HdrMeta`] from a source display's measured colour volume — the chromaticities (CIE xy)
/// and luminances (cd/m²) reported by e.g. Windows `IDXGIOutput6::GetDesc1`. `max_cll`/`max_fall`
/// are content light levels in nits; pass `0` when unknown (GetDesc1 doesn't expose them — Apollo
/// zeroes them too, and a `0` lets the display fall back to the mastering luminance).
#[allow(clippy::too_many_arguments)]
pub fn hdr_meta_from_display(
red: (f32, f32),
green: (f32, f32),
blue: (f32, f32),
white: (f32, f32),
max_mastering_nits: f32,
min_mastering_nits: f32,
max_cll: u16,
max_fall: u16,
) -> HdrMeta {
HdrMeta {
// ST.2086 stores primaries in G, B, R order.
display_primaries: [
[xy_to_2086(green.0), xy_to_2086(green.1)],
[xy_to_2086(blue.0), xy_to_2086(blue.1)],
[xy_to_2086(red.0), xy_to_2086(red.1)],
],
white_point: [xy_to_2086(white.0), xy_to_2086(white.1)],
max_display_mastering_luminance: (max_mastering_nits.max(0.0) * 10_000.0).round() as u32,
min_display_mastering_luminance: (min_mastering_nits.max(0.0) * 10_000.0).round() as u32,
max_cll,
max_fall,
}
}
/// A generic HDR10 default (BT.2020 primaries, D65 white, 1000-nit mastering, MaxCLL 1000 /
/// MaxFALL 400) — the baseline a host sends until it reads the source display's real mastering
/// metadata, and the values clients used to hardcode.
pub fn generic_hdr10() -> HdrMeta {
HdrMeta {
display_primaries: [[8500, 39850], [6550, 2300], [35400, 14600]], // BT.2020 G, B, R
white_point: [15635, 16450], // D65
max_display_mastering_luminance: 10_000_000, // 1000 nits
min_display_mastering_luminance: 1, // 0.0001 nits
max_cll: 1000,
max_fall: 400,
}
}
/// The `mastering_display_colour_volume` SEI payload (HEVC/H.264 type
/// [`SEI_TYPE_MASTERING_DISPLAY_COLOUR_VOLUME`]) — 24 bytes, big-endian (SEI RBSP order), in G/B/R
/// primary order per ST.2086. Pass this raw payload to NVENC's `NV_ENC_SEI_PAYLOAD` (NVENC wraps it
/// in the SEI NAL).
pub fn hevc_mastering_display_sei(m: &HdrMeta) -> [u8; 24] {
let mut b = [0u8; 24];
let mut o = 0;
let mut put16 = |v: u16| {
b[o..o + 2].copy_from_slice(&v.to_be_bytes());
o += 2;
};
for p in m.display_primaries.iter() {
put16(p[0]);
put16(p[1]);
}
put16(m.white_point[0]);
put16(m.white_point[1]);
let mut put32 = |v: u32| {
b[o..o + 4].copy_from_slice(&v.to_be_bytes());
o += 4;
};
put32(m.max_display_mastering_luminance);
put32(m.min_display_mastering_luminance);
debug_assert_eq!(o, 24);
b
}
/// The `content_light_level_info` SEI payload (HEVC/H.264 type
/// [`SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO`]) — 4 bytes, big-endian: MaxCLL then MaxFALL.
pub fn hevc_content_light_level_sei(m: &HdrMeta) -> [u8; 4] {
let mut b = [0u8; 4];
b[0..2].copy_from_slice(&m.max_cll.to_be_bytes());
b[2..4].copy_from_slice(&m.max_fall.to_be_bytes());
b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_conversion_bt2020_1000nit() {
// BT.2020 primaries + D65 white, a 1000-nit / 0.0001-nit mastering display.
let m = hdr_meta_from_display(
(0.708, 0.292), // red
(0.170, 0.797), // green
(0.131, 0.046), // blue
(0.3127, 0.3290), // D65
1000.0,
0.0001,
0,
0,
);
// ST.2086 G, B, R order, 1/50000 units.
assert_eq!(
m.display_primaries,
[[8500, 39850], [6550, 2300], [35400, 14600]]
);
assert_eq!(m.white_point, [15635, 16450]);
assert_eq!(m.max_display_mastering_luminance, 10_000_000); // 1000 * 10000
assert_eq!(m.min_display_mastering_luminance, 1); // 0.0001 * 10000
assert_eq!((m.max_cll, m.max_fall), (0, 0));
}
#[test]
fn mastering_sei_is_24_bytes_big_endian_gbr() {
let m = generic_hdr10();
let p = hevc_mastering_display_sei(&m);
assert_eq!(p.len(), 24);
// First field = green.x = 8500 = 0x2134, big-endian.
assert_eq!(&p[0..2], &8500u16.to_be_bytes());
assert_eq!(&p[2..4], &39850u16.to_be_bytes()); // green.y
assert_eq!(&p[4..6], &6550u16.to_be_bytes()); // blue.x
assert_eq!(&p[12..14], &15635u16.to_be_bytes()); // white.x
assert_eq!(&p[16..20], &10_000_000u32.to_be_bytes()); // max lum
assert_eq!(&p[20..24], &1u32.to_be_bytes()); // min lum
}
#[test]
fn cll_sei_is_4_bytes_big_endian() {
let m = generic_hdr10();
let p = hevc_content_light_level_sei(&m);
assert_eq!(p, [0x03, 0xE8, 0x01, 0x90]); // 1000, 400 big-endian
}
#[test]
fn clamps_out_of_range() {
let m = hdr_meta_from_display(
(2.0, 2.0),
(0.0, 0.0),
(0.0, 0.0),
(0.5, 0.5),
-5.0,
0.0,
0,
0,
);
assert_eq!(m.display_primaries[2], [65535, 65535]); // red clamped
assert_eq!(m.max_display_mastering_luminance, 0); // negative → 0
}
}