feat(windows): parallel virtual displays — proto v3 ring binding, manager slot map, group topology (W0–W3)
windows-drivers / probe-and-proto (push) Successful in 41s
ci / web (push) Successful in 1m2s
ci / docs-site (push) Successful in 1m7s
apple / swift (push) Successful in 1m11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
decky / build-publish (push) Successful in 15s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 38s
windows-drivers / driver-build (push) Successful in 1m46s
ci / bench (push) Successful in 6m35s
docker / deploy-docs (push) Successful in 20s
windows-host / package (push) Successful in 9m37s
deb / build-publish (push) Successful in 13m48s
arch / build-publish (push) Successful in 14m21s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m34s
android / android (push) Successful in 15m45s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m20s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m18s
ci / rust (push) Successful in 18m48s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m41s
release / apple (push) Successful in 19m30s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m49s
flatpak / build-publish (push) Failing after 2m9s
apple / screenshots (push) Successful in 19m3s
windows-drivers / probe-and-proto (push) Successful in 41s
ci / web (push) Successful in 1m2s
ci / docs-site (push) Successful in 1m7s
apple / swift (push) Successful in 1m11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
decky / build-publish (push) Successful in 15s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 38s
windows-drivers / driver-build (push) Successful in 1m46s
ci / bench (push) Successful in 6m35s
docker / deploy-docs (push) Successful in 20s
windows-host / package (push) Successful in 9m37s
deb / build-publish (push) Successful in 13m48s
arch / build-publish (push) Successful in 14m21s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m34s
android / android (push) Successful in 15m45s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m20s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m18s
ci / rust (push) Successful in 18m48s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m41s
release / apple (push) Successful in 19m30s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m49s
flatpak / build-publish (push) Failing after 2m9s
apple / screenshots (push) Successful in 19m3s
design/windows-parallel-virtual-displays.md (display-management Stage 7 / §6.6): N simultaneously-live pf-vdisplay monitors, one sealed ring each, every idd-push-security invariant preserved per-ring. - proto v3: SharedHeader._pad → target_id — the ring NAMES its monitor, host-stamped before the magic; the driver publisher refuses a cross-bound ring via the shared, unit-tested frame::check_attach (new DRV_STATUS_BIND_FAIL — the gamepad pad_index validation applied to frames, invariant #10); the host's wait_for_attach surfaces the refusal loudly and self-checks its own stamp. - manager: the one-monitor MgrState becomes a slot map keyed by the client's identity slot (0 = anonymous/GameStream); per-slot reconnect + dead-WUDFHost preempts, slot-scoped begin_idd_setup (a different identity is an admission question, never a preempt), ONE device-level watchdog pinger, per-slot /display/state + /display/release. - group topology: isolate_displays_ccd takes the managed target SET (a sibling slot is never deactivated); SavedConfig + the DDC/PnP axes move to the group record (first-in captures, last-out restores); desktop layout via CCD source origins from the pure layout::arrange (auto-row default, manual pins win), re-applied on create + reconfigure. - admission: the Windows separate→reject override now sits behind the PUNKTFUNK_WIN_SEPARATE=1 validation hatch (the wedge it guarded is structurally gone — a second identity gets its own monitor + ring; default flips in W5 after soak); max_displays and NVENC session-unit budgets decline an unaffordable display AT admission; kick_dwm_compose is process-globally throttled and per-display — cursor jump + 35 ms dwell (a sub-tick jump composes nothing; DWM reads dirties from current state at the next vsync tick). On-glass on the RTX box: V1/V2/V4/V5/V6/V9 green — two paired clients on two monitors streaming ~60 fps each with zero mismatches and zero bind failures, churn-hammer clean (no 0x80070490), per-ring mode-change recreate leaves the sibling untouched, typed budget rejection, fault-injected cross-bind refused loudly with the sibling undisturbed. V7: WUDFHost-kill shared fate is clean; in-process device recovery is a known follow-up (the retired-never-closed control handles block the adapter cycle — reset-pf-vdisplay.ps1 recovers). DWM composes two IDD monitors concurrently at 60 fps — the plan's load-bearing unknown, answered yes. Also carries the client-HDR EDID forwarding that shared this working tree (Hello::display_hdr → AddRequest luminance tail → the monitor's CTA-861.3 HDR block, PUNKTFUNK_CLIENT_PEAK_NITS hatch) and the Deck client fixes (40 ms rumble keep-alive with 1-LSB jitter, HDR self-diagnosing presenter warn, flatpak HDR env). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1374,6 +1374,10 @@ pub unsafe extern "C" fn punktfunk_connect_ex7(
|
||||
crate::audio::normalize_channels(audio_channels),
|
||||
video_codecs,
|
||||
preferred_codec,
|
||||
// No display-HDR-volume parameter in the C ABI yet: Apple/Android clients tone-map
|
||||
// themselves (EDR / MediaCodec), so the host's EDID defaults are fine there. An `ex8`
|
||||
// variant can carry it if a passthrough embedder ever needs it.
|
||||
None,
|
||||
launch,
|
||||
pin,
|
||||
identity,
|
||||
|
||||
@@ -552,6 +552,11 @@ impl NativeClient {
|
||||
// it emits from these and echoes it in [`NativeClient::codec`].
|
||||
video_codecs: u8,
|
||||
preferred_codec: u8,
|
||||
// The client display's HDR colour volume (primaries/white/luminance), read from the OS
|
||||
// (e.g. DXGI `GetDesc1`) when presenting HDR. The host forwards it into the virtual
|
||||
// display's EDID so host apps tone-map to the client's real panel; `None` = unknown/SDR
|
||||
// (the host keeps its built-in EDID defaults). See [`crate::quic::Hello::display_hdr`].
|
||||
display_hdr: Option<HdrMeta>,
|
||||
launch: Option<String>,
|
||||
pin: Option<[u8; 32]>,
|
||||
identity: Option<(String, String)>,
|
||||
@@ -618,6 +623,7 @@ impl NativeClient {
|
||||
audio_channels,
|
||||
video_codecs,
|
||||
preferred_codec,
|
||||
display_hdr,
|
||||
launch,
|
||||
pin,
|
||||
identity,
|
||||
@@ -1118,6 +1124,33 @@ impl Drop for NativeClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Test/A-B hatch shared by the client shells: `PUNKTFUNK_CLIENT_PEAK_NITS=<nits>` synthesizes a
|
||||
/// display colour volume at that peak (BT.2020 primaries, D65, a 0.005-nit floor, frame-average
|
||||
/// unknown) for [`Hello::display_hdr`](crate::quic::Hello::display_hdr), overriding whatever the
|
||||
/// shell read from the OS — so the host-side tone-map target (the virtual display's EDID volume)
|
||||
/// can be pinned exactly for validation, and shells with no OS display-volume query get a manual
|
||||
/// knob. `None` when unset/unparsable/zero.
|
||||
pub fn display_hdr_env_override() -> Option<HdrMeta> {
|
||||
let nits: u32 = std::env::var("PUNKTFUNK_CLIENT_PEAK_NITS")
|
||||
.ok()?
|
||||
.trim()
|
||||
.parse()
|
||||
.ok()
|
||||
.filter(|&n| n > 0)?;
|
||||
tracing::info!(
|
||||
nits,
|
||||
"PUNKTFUNK_CLIENT_PEAK_NITS: overriding the advertised display volume"
|
||||
);
|
||||
Some(HdrMeta {
|
||||
display_primaries: [[8500, 39850], [6550, 2300], [35400, 14600]], // BT.2020 G, B, R
|
||||
white_point: [15635, 16450], // D65
|
||||
max_display_mastering_luminance: nits.saturating_mul(10_000),
|
||||
min_display_mastering_luminance: 50, // 0.005 nits
|
||||
max_cll: 0,
|
||||
max_fall: 0,
|
||||
})
|
||||
}
|
||||
|
||||
struct WorkerArgs {
|
||||
host: String,
|
||||
port: u16,
|
||||
@@ -1129,6 +1162,7 @@ struct WorkerArgs {
|
||||
audio_channels: u8,
|
||||
video_codecs: u8,
|
||||
preferred_codec: u8,
|
||||
display_hdr: Option<HdrMeta>,
|
||||
launch: Option<String>,
|
||||
pin: Option<[u8; 32]>,
|
||||
identity: Option<(String, String)>,
|
||||
@@ -1171,6 +1205,7 @@ async fn worker_main(args: WorkerArgs) {
|
||||
audio_channels,
|
||||
video_codecs,
|
||||
preferred_codec,
|
||||
display_hdr,
|
||||
launch,
|
||||
pin,
|
||||
identity,
|
||||
@@ -1250,6 +1285,9 @@ async fn worker_main(args: WorkerArgs) {
|
||||
// resolves the emitted codec from these and reports it in `Welcome::codec`.
|
||||
video_codecs,
|
||||
preferred_codec,
|
||||
// The client display's HDR volume → the host's virtual-display EDID (host apps
|
||||
// tone-map to the client's real panel). `None` = unknown/SDR.
|
||||
display_hdr,
|
||||
}
|
||||
.encode(),
|
||||
)
|
||||
|
||||
@@ -330,14 +330,16 @@ pub struct HdrMeta {
|
||||
/// see [`HdrMeta`]). Next tag after [`HIDOUT_MAGIC`].
|
||||
pub const HDR_META_MAGIC: u8 = 0xCE;
|
||||
|
||||
/// Wire length of an [`HDR_META_MAGIC`] datagram: tag + 6×u16 primaries + 2×u16 white + 2×u32
|
||||
/// luminance + 2×u16 CLL/FALL = 29 bytes.
|
||||
const HDR_META_LEN: usize = 1 + 12 + 4 + 8 + 4;
|
||||
/// Wire length of an [`HdrMeta`] body (no tag byte): 6×u16 primaries + 2×u16 white + 2×u32
|
||||
/// luminance + 2×u16 CLL/FALL = 28 bytes. Shared by the [`HDR_META_MAGIC`] datagram (which
|
||||
/// prefixes the tag) and the `Hello::display_hdr` trailing field (which carries the bare body).
|
||||
pub const HDR_META_BODY_LEN: usize = 12 + 4 + 8 + 4;
|
||||
|
||||
/// Encode an [`HdrMeta`] into a [`HDR_META_MAGIC`] datagram.
|
||||
pub fn encode_hdr_meta_datagram(m: &HdrMeta) -> Vec<u8> {
|
||||
let mut b = Vec::with_capacity(HDR_META_LEN);
|
||||
b.push(HDR_META_MAGIC);
|
||||
/// Wire length of an [`HDR_META_MAGIC`] datagram: tag + body = 29 bytes.
|
||||
const HDR_META_LEN: usize = 1 + HDR_META_BODY_LEN;
|
||||
|
||||
/// Append `m`'s [`HDR_META_BODY_LEN`]-byte wire body (LE, no tag byte) to `b`.
|
||||
pub fn write_hdr_meta_body(m: &HdrMeta, b: &mut Vec<u8>) {
|
||||
for p in m.display_primaries.iter() {
|
||||
b.extend_from_slice(&p[0].to_le_bytes());
|
||||
b.extend_from_slice(&p[1].to_le_bytes());
|
||||
@@ -348,6 +350,32 @@ pub fn encode_hdr_meta_datagram(m: &HdrMeta) -> Vec<u8> {
|
||||
b.extend_from_slice(&m.min_display_mastering_luminance.to_le_bytes());
|
||||
b.extend_from_slice(&m.max_cll.to_le_bytes());
|
||||
b.extend_from_slice(&m.max_fall.to_le_bytes());
|
||||
}
|
||||
|
||||
/// Read an [`HdrMeta`] from its wire body (no tag byte). The caller guarantees `b` holds at least
|
||||
/// [`HDR_META_BODY_LEN`] bytes (both callers slice with an exact-length, bounds-checked `get`).
|
||||
pub fn read_hdr_meta_body(b: &[u8]) -> HdrMeta {
|
||||
let u16at = |o: usize| u16::from_le_bytes([b[o], b[o + 1]]);
|
||||
let u32at = |o: usize| u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]);
|
||||
HdrMeta {
|
||||
display_primaries: [
|
||||
[u16at(0), u16at(2)],
|
||||
[u16at(4), u16at(6)],
|
||||
[u16at(8), u16at(10)],
|
||||
],
|
||||
white_point: [u16at(12), u16at(14)],
|
||||
max_display_mastering_luminance: u32at(16),
|
||||
min_display_mastering_luminance: u32at(20),
|
||||
max_cll: u16at(24),
|
||||
max_fall: u16at(26),
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode an [`HdrMeta`] into a [`HDR_META_MAGIC`] datagram.
|
||||
pub fn encode_hdr_meta_datagram(m: &HdrMeta) -> Vec<u8> {
|
||||
let mut b = Vec::with_capacity(HDR_META_LEN);
|
||||
b.push(HDR_META_MAGIC);
|
||||
write_hdr_meta_body(m, &mut b);
|
||||
b
|
||||
}
|
||||
|
||||
@@ -357,20 +385,7 @@ pub fn decode_hdr_meta_datagram(b: &[u8]) -> Option<HdrMeta> {
|
||||
if b.len() < HDR_META_LEN || b[0] != HDR_META_MAGIC {
|
||||
return None;
|
||||
}
|
||||
let u16at = |o: usize| u16::from_le_bytes([b[o], b[o + 1]]);
|
||||
let u32at = |o: usize| u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]);
|
||||
Some(HdrMeta {
|
||||
display_primaries: [
|
||||
[u16at(1), u16at(3)],
|
||||
[u16at(5), u16at(7)],
|
||||
[u16at(9), u16at(11)],
|
||||
],
|
||||
white_point: [u16at(13), u16at(15)],
|
||||
max_display_mastering_luminance: u32at(17),
|
||||
min_display_mastering_luminance: u32at(21),
|
||||
max_cll: u16at(25),
|
||||
max_fall: u16at(27),
|
||||
})
|
||||
Some(read_hdr_meta_body(&b[1..]))
|
||||
}
|
||||
|
||||
/// Per-AU host-timing datagram tag, host → client (see [`HostTiming`]). Next tag after
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! (`CTL_MAGIC` + type byte), including the pairing-ceremony messages. Wire codecs only
|
||||
//! — no transport state.
|
||||
|
||||
use super::datagram::HdrMeta;
|
||||
use super::{CTL_MAGIC, MAGIC};
|
||||
use crate::config::{
|
||||
CompositorPref, Config, FecConfig, FecScheme, GamepadPref, Mode, ProtocolPhase, Role,
|
||||
@@ -73,6 +74,17 @@ pub struct Hello {
|
||||
/// [`Hello::gamepad`] preference pattern; the resolved codec is echoed in [`Welcome::codec`].
|
||||
/// Appended after `video_codecs` as a single trailing byte. Omitted by older clients (→ `0`).
|
||||
pub preferred_codec: u8,
|
||||
/// The client's **display** HDR colour volume — primaries / white point / luminance range in
|
||||
/// the ST.2086 units of [`HdrMeta`] — read from the client OS (e.g. Windows
|
||||
/// `IDXGIOutput6::GetDesc1`) when it advertised [`VIDEO_CAP_HDR`]. The host forwards it into
|
||||
/// the virtual display's EDID (the pf-vdisplay CTA-861.3 HDR static-metadata block), so host
|
||||
/// apps and the OS tone-map to the CLIENT's real panel instead of the driver's built-in
|
||||
/// ~1000-nit placeholder — the client can then present the PQ stream untouched. Also echoed
|
||||
/// back as the session's `0xCE` mastering metadata. Appended after `preferred_codec` as a
|
||||
/// fixed [`super::datagram::HDR_META_BODY_LEN`]-byte block (the [`HdrMeta`] wire body, no tag),
|
||||
/// forcing the earlier placeholders. Omitted by older clients / when the client has no HDR
|
||||
/// display (decodes to `None` — the host keeps its built-in EDID defaults).
|
||||
pub display_hdr: Option<HdrMeta>,
|
||||
}
|
||||
|
||||
/// [`Hello::video_caps`] bit: the client can decode a 10-bit (Main10) HEVC stream.
|
||||
@@ -670,8 +682,9 @@ impl Hello {
|
||||
let ac_present = self.audio_channels != 2;
|
||||
let vcodecs_present = self.video_codecs != 0;
|
||||
let pref_present = self.preferred_codec != 0;
|
||||
let hdr_present = self.display_hdr.is_some();
|
||||
let need_placeholders =
|
||||
self.video_caps != 0 || ac_present || vcodecs_present || pref_present;
|
||||
self.video_caps != 0 || ac_present || vcodecs_present || pref_present || hdr_present;
|
||||
match (&self.name, &self.launch) {
|
||||
(None, None) if !need_placeholders => {}
|
||||
(name, _) => {
|
||||
@@ -688,21 +701,25 @@ impl Hello {
|
||||
}
|
||||
// video_caps: single trailing byte. Emitted when non-zero OR when a later field follows (so
|
||||
// that field lands at a deterministic offset right after it).
|
||||
if self.video_caps != 0 || ac_present || vcodecs_present || pref_present {
|
||||
if need_placeholders {
|
||||
b.push(self.video_caps);
|
||||
}
|
||||
// audio_channels: emitted when non-stereo OR a later field follows.
|
||||
if ac_present || vcodecs_present || pref_present {
|
||||
if ac_present || vcodecs_present || pref_present || hdr_present {
|
||||
b.push(self.audio_channels);
|
||||
}
|
||||
// video_codecs: emitted when non-zero OR preferred_codec follows.
|
||||
if vcodecs_present || pref_present {
|
||||
// video_codecs: emitted when non-zero OR a later field follows.
|
||||
if vcodecs_present || pref_present || hdr_present {
|
||||
b.push(self.video_codecs);
|
||||
}
|
||||
// preferred_codec: single trailing byte. Last field; omitted when `0` (no preference).
|
||||
if pref_present {
|
||||
// preferred_codec: emitted when non-zero OR display_hdr follows.
|
||||
if pref_present || hdr_present {
|
||||
b.push(self.preferred_codec);
|
||||
}
|
||||
// display_hdr: fixed HDR_META_BODY_LEN-byte HdrMeta body. Last field; omitted when `None`.
|
||||
if let Some(m) = &self.display_hdr {
|
||||
super::datagram::write_hdr_meta_body(m, &mut b);
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
@@ -766,6 +783,11 @@ impl Hello {
|
||||
video_codecs: b.get(tail + 2).copied().unwrap_or(0),
|
||||
// `0` = no preference; the host decides by precedence.
|
||||
preferred_codec: b.get(tail + 3).copied().unwrap_or(0),
|
||||
// Optional trailing HdrMeta body (fixed length) — absent on an older client / a
|
||||
// client without an HDR display → `None` (the host keeps its EDID defaults).
|
||||
display_hdr: b
|
||||
.get(tail + 4..tail + 4 + super::datagram::HDR_META_BODY_LEN)
|
||||
.map(super::datagram::read_hdr_meta_body),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ fn codec_negotiation_and_back_compat() {
|
||||
audio_channels: 2, // stereo — forces the video_caps/audio_channels placeholders
|
||||
video_codecs: CODEC_H264 | CODEC_HEVC,
|
||||
preferred_codec: CODEC_H264,
|
||||
display_hdr: None,
|
||||
};
|
||||
let enc = h.encode();
|
||||
let dec = Hello::decode(&enc).unwrap();
|
||||
@@ -234,6 +235,7 @@ fn hello_start_roundtrip() {
|
||||
audio_channels: 2,
|
||||
video_codecs: CODEC_H264 | CODEC_HEVC, // exercise the codec bitfield roundtrip
|
||||
preferred_codec: CODEC_HEVC,
|
||||
display_hdr: None,
|
||||
};
|
||||
assert_eq!(Hello::decode(&h.encode()).unwrap(), h);
|
||||
let s = Start {
|
||||
@@ -316,6 +318,7 @@ fn hello_welcome_compositor_back_compat() {
|
||||
audio_channels: 2,
|
||||
video_codecs: 0,
|
||||
preferred_codec: 0,
|
||||
display_hdr: None,
|
||||
};
|
||||
let enc = h.encode();
|
||||
assert_eq!(enc.len(), 26);
|
||||
@@ -430,6 +433,7 @@ fn hello_name_roundtrip_and_back_compat() {
|
||||
audio_channels: 2,
|
||||
video_codecs: 0,
|
||||
preferred_codec: 0,
|
||||
display_hdr: None,
|
||||
};
|
||||
let enc = base.encode();
|
||||
assert_eq!(
|
||||
@@ -480,6 +484,7 @@ fn hello_launch_roundtrip_and_back_compat() {
|
||||
audio_channels: 2,
|
||||
video_codecs: 0,
|
||||
preferred_codec: 0,
|
||||
display_hdr: None,
|
||||
};
|
||||
// launch alone (no name): a zero-length name placeholder keeps the offset deterministic.
|
||||
let with_launch = Hello {
|
||||
@@ -520,6 +525,70 @@ fn hello_launch_roundtrip_and_back_compat() {
|
||||
assert!(dec.len() <= HELLO_LAUNCH_MAX && dec.starts_with('x'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hello_display_hdr_roundtrip_and_back_compat() {
|
||||
let base = Hello {
|
||||
abi_version: 2,
|
||||
mode: Mode {
|
||||
width: 3840,
|
||||
height: 2160,
|
||||
refresh_hz: 120,
|
||||
},
|
||||
compositor: CompositorPref::Auto,
|
||||
gamepad: GamepadPref::Auto,
|
||||
bitrate_kbps: 0,
|
||||
name: None,
|
||||
launch: None,
|
||||
video_caps: VIDEO_CAP_10BIT | VIDEO_CAP_HDR,
|
||||
audio_channels: 2,
|
||||
video_codecs: 0,
|
||||
preferred_codec: 0,
|
||||
display_hdr: None,
|
||||
};
|
||||
// A real client-panel volume (P3 primaries, 800-nit peak, 0.05-nit floor, 400-nit FALL).
|
||||
let vol = HdrMeta {
|
||||
display_primaries: [[13250, 34500], [7500, 3000], [34000, 16000]], // G, B, R
|
||||
white_point: [15635, 16450], // D65
|
||||
max_display_mastering_luminance: 8_000_000, // 800 nits
|
||||
min_display_mastering_luminance: 500, // 0.05 nits
|
||||
max_cll: 0,
|
||||
max_fall: 400,
|
||||
};
|
||||
let with_hdr = Hello {
|
||||
display_hdr: Some(vol),
|
||||
..base.clone()
|
||||
};
|
||||
// Full roundtrip, including the forced placeholders for the earlier trailing fields.
|
||||
assert_eq!(Hello::decode(&with_hdr.encode()).unwrap(), with_hdr);
|
||||
// display_hdr alone (every earlier optional at its default) still lands at a deterministic
|
||||
// offset — the placeholder discipline holds through the whole tail.
|
||||
let hdr_only = Hello {
|
||||
video_caps: 0,
|
||||
display_hdr: Some(vol),
|
||||
..base.clone()
|
||||
};
|
||||
assert_eq!(Hello::decode(&hdr_only.encode()).unwrap(), hdr_only);
|
||||
// An older host reading a display_hdr-bearing Hello ignores the trailing block (its decode
|
||||
// stops at preferred_codec); a new host reading an older client's Hello gets None.
|
||||
let enc = with_hdr.encode();
|
||||
assert_eq!(
|
||||
Hello::decode(&enc[..enc.len() - HDR_META_BODY_LEN]).unwrap(),
|
||||
Hello {
|
||||
display_hdr: None,
|
||||
..with_hdr.clone()
|
||||
}
|
||||
);
|
||||
assert_eq!(Hello::decode(&base.encode()).unwrap().display_hdr, None);
|
||||
// A TRUNCATED trailing block (mid-datagram cut) degrades to None, never a partial read.
|
||||
assert_eq!(
|
||||
Hello::decode(&enc[..enc.len() - 1]).unwrap().display_hdr,
|
||||
None
|
||||
);
|
||||
// Exact wire length: 26 bitrate-era bytes + the 6 forced single-byte placeholders
|
||||
// (name len, launch len, video_caps, audio_channels, video_codecs, preferred_codec) + the body.
|
||||
assert_eq!(hdr_only.encode().len(), 26 + 6 + HDR_META_BODY_LEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconfigure_roundtrip() {
|
||||
let rq = Reconfigure {
|
||||
@@ -786,6 +855,7 @@ fn control_messages_disjoint_from_hello() {
|
||||
audio_channels: 2,
|
||||
video_codecs: 0,
|
||||
preferred_codec: 0,
|
||||
display_hdr: None,
|
||||
}
|
||||
.encode();
|
||||
assert!(PairRequest::decode(&h).is_err(), "abi {abi} parsed as pair");
|
||||
|
||||
Reference in New Issue
Block a user