feat(windows): pf-vdisplay IDD-push — HDR + pipelined zero-copy capture
apple / swift (push) Successful in 1m4s
windows-host / package (push) Successful in 6m28s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m14s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m10s
release / apple (push) Successful in 7m53s
android / android (push) Successful in 10m33s
ci / web (push) Successful in 44s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 3m4s
ci / docs-site (push) Successful in 53s
ci / rust (push) Successful in 12m22s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m11s
apple / screenshots (push) Successful in 5m24s
deb / build-publish (push) Successful in 3m16s
decky / build-publish (push) Successful in 21s
ci / bench (push) Successful in 4m42s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 27s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m34s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m42s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m13s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 47s
flatpak / build-publish (push) Successful in 4m24s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m5s
docker / deploy-docs (push) Successful in 25s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 7m44s
apple / swift (push) Successful in 1m4s
windows-host / package (push) Successful in 6m28s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m14s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m10s
release / apple (push) Successful in 7m53s
android / android (push) Successful in 10m33s
ci / web (push) Successful in 44s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 3m4s
ci / docs-site (push) Successful in 53s
ci / rust (push) Successful in 12m22s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m11s
apple / screenshots (push) Successful in 5m24s
deb / build-publish (push) Successful in 3m16s
decky / build-publish (push) Successful in 21s
ci / bench (push) Successful in 4m42s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 27s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m34s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m42s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m13s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 47s
flatpak / build-publish (push) Successful in 4m24s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m5s
docker / deploy-docs (push) Successful in 25s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 7m44s
HDR (display-driven, matching the WGC path): - CTA-861.3 HDR EDID (BT.2020 primaries + HDR Static Metadata block) so Windows offers "Use HDR" on the virtual display. The host FOLLOWS the display's live advanced-color state, recreating the shared ring at the matching format (FP16 in HDR / BGRA in SDR) on a toggle — no freeze. - Always emit Main10/BT.2020-PQ Rgb10a2 while the display is HDR; the client auto-detects PQ from the HEVC VUI (clients under-report VIDEO_CAP_10BIT). Generic HDR10 mastering SEI on every IDR. - Generation-tagged `latest` (gen<<40|seq<<8|slot) + driver `is_stale` re-attach kill the toggle-time garbage frame and any stale-ring read. Perf: - Pipeline the encode loop (Capturer::pipeline_depth; IDD-push = 2): submit N+1 before polling N so the convert/copy on the 3D engine overlaps the NVENC encode of N on the ASIC. PUNKTFUNK_IDD_DEPTH overrides (1 = synchronous). - Rotating host output ring (OUT_RING) so the in-flight encode and the next convert never touch the same texture. - HDR converts directly from the keyed-mutex slot's SRV into the output ring (drops the redundant slot->fp16 scratch copy); SDR copies the BGRA slot in. The slot mutex is held only across the convert/copy, not the encode. RING_LEN 3->6 for publish headroom. - Capture-health diagnostic: new_fps vs repeat_fps under PUNKTFUNK_PERF (a low new_fps at a high send rate means the source isn't compositing, not an encode stall). Validated live on the RTX box: 5120x1440@240 HDR streams; driver composes ~180 new fps, encode 240 fps @ ~4.3 ms p50. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,114 +1,118 @@
|
||||
use std::{array::TryFromSliceError, ops::Deref};
|
||||
//! The 256-byte EDID the pf-vdisplay driver hands IddCx for each virtual monitor: a 128-byte EDID 1.4
|
||||
//! base block + a **CTA-861.3 extension** that advertises HDR — a BT.2020 Colorimetry Data Block and an
|
||||
//! HDR Static Metadata Data Block declaring the SMPTE ST 2084 (PQ) EOTF. Windows reads a display's HDR
|
||||
//! capability from this CTA HDR block; without it the monitor is treated as SDR-only regardless of the
|
||||
//! IddCx adapter's `CAN_PROCESS_FP16` / `HIGH_COLOR_SPACE` / 10-bit mode caps (the missing piece that
|
||||
//! made "Use HDR" never appear for the virtual display). The base block declares EDID 1.4 + 10-bit
|
||||
//! digital so the panel's bit depth is unambiguous.
|
||||
//!
|
||||
//! Identity: manufacturer "PNK" (bytes 8-9), product name "punktfunk" (the 0xFC display descriptor). The
|
||||
//! serial-number field (base offset 0x0C, little-endian) encodes the per-monitor index so
|
||||
//! `parse_monitor_description` can map an EDID the OS hands back to its monitor; [`Edid::generate_with`]
|
||||
//! patches that serial and recomputes BOTH block checksums (base byte 127 + extension byte 255). The
|
||||
//! detailed-timing / range-limit descriptors are placeholders — the modes we actually advertise come
|
||||
//! from the monitor's stored mode list (`monitor.rs` / `callbacks.rs`), not from parsing this EDID.
|
||||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use std::array::TryFromSliceError;
|
||||
|
||||
// A clean, self-contained 128-byte EDID carrying punktfunk's own identity — manufacturer ID "PNK"
|
||||
// (bytes 8-9) and product name "punktfunk" (the 0xFC display-descriptor). Derived from the
|
||||
// virtual-display-rs base block (a standard, widely-deployed virtual EDID); it deliberately carries NO
|
||||
// other driver's bytes or branding. The serial-number field (offset 0x0C) encodes the per-monitor
|
||||
// index, so `parse_monitor_description` can map an EDID the OS hands back to its monitor;
|
||||
// `generate_with` patches that serial and `gen_checksum` recomputes byte 127 before the EDID reaches
|
||||
// IddCx. The detailed-timing / range-limit descriptors are placeholders: the modes we actually
|
||||
// advertise come from the monitor's stored mode list (`monitor.rs` / `callbacks.rs`), not from parsing
|
||||
// this EDID.
|
||||
const _EDID: [u8; 128] = [
|
||||
0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x41, 0xCB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xFF, 0x21, 0x01, 0x03, 0x80, 0x32, 0x1F, 0x78, 0x07, 0xEE, 0x95, 0xA3, 0x54, 0x4C, 0x99, 0x26,
|
||||
0x0F, 0x50, 0x54, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
|
||||
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x3A, 0x80, 0x18, 0x71, 0x38, 0x2D, 0x40, 0x58, 0x2C,
|
||||
0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0xFD, 0x00, 0x17, 0xF0, 0x0F,
|
||||
0xFF, 0x0F, 0x00, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x70,
|
||||
0x75, 0x6E, 0x6B, 0x74, 0x66, 0x75, 0x6E, 0x6B, 0x0A, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
/// Per-monitor serial number, base-block offset 0x0C, little-endian u32.
|
||||
const SERIAL_OFFSET: usize = 0x0C;
|
||||
|
||||
/// EDID 1.4 base block (128 bytes). Differs from a plain SDR virtual EDID only by: revision 1.4 (byte
|
||||
/// 19 = 0x04), 10-bit digital video input (byte 20 = 0xB0), and one extension present (byte 126 = 0x01).
|
||||
/// Byte 127 (checksum) and the serial (0x0C) are filled/patched in [`Edid::generate_with`].
|
||||
#[rustfmt::skip]
|
||||
const BASE: [u8; 128] = [
|
||||
0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, // fixed header
|
||||
0x41, 0xCB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mfr "PNK", product, serial (patched)
|
||||
0xFF, 0x21, 0x01, 0x04, 0xB0, 0x32, 0x1F, 0x78, // week/year, EDID 1.4, 10-bit digital, size, gamma
|
||||
0x03, 0x78, 0xB1, 0xB5, 0x4A, 0x2B, 0xCC, 0x21, // feature (sRGB-default CLEARED), BT.2020 primaries...
|
||||
0x0B, 0x50, 0x54, 0x00, 0x00, 0x00, 0x01, 0x01, // ...BT.2020 primaries, established timings, std timings
|
||||
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
|
||||
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x3A, // std timings, DTD 1 (placeholder preferred timing)
|
||||
0x80, 0x18, 0x71, 0x38, 0x2D, 0x40, 0x58, 0x2C,
|
||||
0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E,
|
||||
0x00, 0x00, 0x00, 0xFD, 0x00, 0x17, 0xF0, 0x0F, // display range-limits descriptor
|
||||
0xFF, 0x0F, 0x00, 0x0A, 0x20, 0x20, 0x20, 0x20,
|
||||
0x20, 0x20, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x70, // name descriptor "punktfunk"
|
||||
0x75, 0x6E, 0x6B, 0x74, 0x66, 0x75, 0x6E, 0x6B,
|
||||
0x0A, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, // empty 4th descriptor...
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, // ...byte 126 = 1 extension, byte 127 = checksum
|
||||
];
|
||||
|
||||
const EDID_LEN: usize = _EDID.len();
|
||||
/// CTA-861.3 extension block (128 bytes), block 1. Header + a Data Block Collection holding the
|
||||
/// Colorimetry and HDR Static Metadata data blocks; the rest is padding up to the checksum (byte 255).
|
||||
/// `D` (byte 130) marks where DTDs would start (= end of the data blocks); we carry none.
|
||||
#[rustfmt::skip]
|
||||
const CTA_HEADER: [u8; 4] = [
|
||||
0x02, // CTA Extension tag
|
||||
0x03, // revision 3 (CTA-861.3 — required for the extended-tag data blocks below)
|
||||
0x0F, // D = 15: the (empty) DTD region starts at block byte 15, i.e. data blocks occupy bytes 4..15
|
||||
0x00, // 0 native DTDs; no basic audio; no YCbCr 4:4:4/4:2:2 (RGB-only, matching the wire format)
|
||||
];
|
||||
|
||||
static EDID: AlignedEdid<EDID_LEN> = AlignedEdid {
|
||||
data: _EDID,
|
||||
_align: [],
|
||||
};
|
||||
/// Colorimetry Data Block (CTA extended tag 0x05): declare BT.2020 RGB (bit 7). YCbCr variants are left
|
||||
/// clear — the IddCx wire format is RGB-only — and the gamut-metadata flags are 0.
|
||||
#[rustfmt::skip]
|
||||
const COLORIMETRY_DB: [u8; 4] = [
|
||||
0xE3, // tag 0b111 (use-extended-tag) | length 3
|
||||
0x05, // extended tag: Colorimetry
|
||||
0x80, // BT2020RGB (bit 7); xvYCC/sYCC/opRGB/BT2020 YCC/cYCC all clear
|
||||
0x00, // gamut metadata profiles MD0..MD3: none
|
||||
];
|
||||
|
||||
#[repr(C)]
|
||||
struct AlignedEdid<const N: usize> {
|
||||
data: [u8; N],
|
||||
// required to make this type aligned to Edid
|
||||
_align: [Edid; 0],
|
||||
}
|
||||
/// HDR Static Metadata Data Block (CTA extended tag 0x06): EOTFs = Traditional SDR (ET_0) + SMPTE ST
|
||||
/// 2084 / PQ (ET_2); Static Metadata Type 1 (SM_0). Plus the optional desired-content luminance hints
|
||||
/// (~993 nit max, ~400 nit max-frame-average, ~0.05 nit min) so the block is complete.
|
||||
#[rustfmt::skip]
|
||||
const HDR_STATIC_METADATA_DB: [u8; 7] = [
|
||||
0xE6, // tag 0b111 (use-extended-tag) | length 6
|
||||
0x06, // extended tag: HDR Static Metadata
|
||||
0x05, // Supported EOTFs: ET_0 (traditional SDR) | ET_2 (SMPTE ST 2084 / PQ)
|
||||
0x01, // Supported Static Metadata Descriptors: SM_0 (Static Metadata Type 1)
|
||||
0x8A, // Desired Content Max Luminance (code 138 ≈ 993 nits)
|
||||
0x60, // Desired Content Max Frame-avg Lum. (code 96 = 400 nits)
|
||||
0x12, // Desired Content Min Luminance (code 18 ≈ 0.05 nits)
|
||||
];
|
||||
|
||||
impl<const N: usize> AlignedEdid<N> {
|
||||
fn new(data: &[u8]) -> Result<Self, TryFromSliceError> {
|
||||
let data: [u8; N] = data.try_into()?;
|
||||
Ok(Self { data, _align: [] })
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Deref for AlignedEdid<N> {
|
||||
type Target = Edid;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
let header = &self.data[..EDID_SIZE];
|
||||
bytemuck::from_bytes(header)
|
||||
}
|
||||
}
|
||||
|
||||
const EDID_SIZE: usize = std::mem::size_of::<Edid>();
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone, Pod, Zeroable)]
|
||||
pub struct Edid {
|
||||
header: [u8; 8],
|
||||
manufacturer_id: [u8; 2],
|
||||
product_code: u16,
|
||||
serial_number: u32,
|
||||
manufacture_week: u8,
|
||||
manufacture_year: u8,
|
||||
version: u8,
|
||||
revision: u8,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Edid;
|
||||
|
||||
impl Edid {
|
||||
/// Build the full 256-byte EDID for monitor `serial`, with both block checksums recomputed.
|
||||
pub fn generate_with(serial: u32) -> Vec<u8> {
|
||||
// change serial number in the header
|
||||
let mut header = *EDID;
|
||||
header.serial_number = serial;
|
||||
|
||||
header.generate()
|
||||
let mut edid = [0u8; 256];
|
||||
// Block 0: base.
|
||||
edid[..128].copy_from_slice(&BASE);
|
||||
edid[SERIAL_OFFSET..SERIAL_OFFSET + 4].copy_from_slice(&serial.to_le_bytes());
|
||||
// Block 1: CTA-861.3 extension (header + colorimetry + HDR static metadata; rest stays 0).
|
||||
edid[128..132].copy_from_slice(&CTA_HEADER);
|
||||
edid[132..136].copy_from_slice(&COLORIMETRY_DB);
|
||||
edid[136..143].copy_from_slice(&HDR_STATIC_METADATA_DB);
|
||||
// Each 128-byte block ends in a checksum byte that makes the block sum ≡ 0 (mod 256).
|
||||
Self::fix_block_checksum(&mut edid, 0);
|
||||
Self::fix_block_checksum(&mut edid, 128);
|
||||
edid.to_vec()
|
||||
}
|
||||
|
||||
/// Read the per-monitor serial (base offset 0x0C, little-endian) from an EDID the OS handed back.
|
||||
/// Works for the full 256-byte EDID or just the 128-byte base block. Errors (rather than panics) on
|
||||
/// a too-short buffer so the caller can reject a malformed descriptor.
|
||||
pub fn get_serial(edid: &[u8]) -> Result<u32, TryFromSliceError> {
|
||||
let edid = AlignedEdid::<EDID_LEN>::new(edid)?;
|
||||
Ok(edid.serial_number)
|
||||
let bytes: [u8; 4] = edid
|
||||
.get(SERIAL_OFFSET..SERIAL_OFFSET + 4)
|
||||
.unwrap_or(&[])
|
||||
.try_into()?;
|
||||
Ok(u32::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
fn generate(&self) -> Vec<u8> {
|
||||
let header = bytemuck::bytes_of(self);
|
||||
|
||||
// slice of monitor edid minus header
|
||||
let data = &EDID.data[EDID_SIZE..];
|
||||
|
||||
// splice together header and the rest of the EDID
|
||||
let mut edid: Vec<u8> = header.iter().chain(data).copied().collect();
|
||||
// regenerate checksum
|
||||
Self::gen_checksum(&mut edid);
|
||||
|
||||
edid
|
||||
}
|
||||
|
||||
fn gen_checksum(data: &mut [u8]) {
|
||||
// important, this is the bare minimum length
|
||||
assert!(data.len() >= 128);
|
||||
|
||||
// slice to the entire data minus the last checksum byte
|
||||
let edid_data = &data[..=126];
|
||||
|
||||
// do checksum calculation
|
||||
let sum: u32 = edid_data.iter().copied().map(u32::from).sum();
|
||||
// this wont ever truncate
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
let checksum = (256 - (sum % 256)) as u8;
|
||||
|
||||
// update last byte with new checksum
|
||||
data[127] = checksum;
|
||||
/// Set the trailing byte of the 128-byte block at `start` so the block's bytes sum to 0 (mod 256) —
|
||||
/// the standard EDID block checksum.
|
||||
fn fix_block_checksum(edid: &mut [u8], start: usize) {
|
||||
let sum = edid[start..start + 127]
|
||||
.iter()
|
||||
.fold(0u8, |acc, &b| acc.wrapping_add(b));
|
||||
edid[start + 127] = 0u8.wrapping_sub(sum);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user