feat: HDR Step-0 colour-metadata transport + security-audit hardening
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
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
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>
This commit is contained in:
@@ -41,7 +41,7 @@ use windows::Win32::Graphics::Dxgi::Common::{
|
||||
};
|
||||
use windows::Win32::Graphics::Dxgi::{
|
||||
CreateDXGIFactory1, IDXGIAdapter1, IDXGIDevice, IDXGIDevice1, IDXGIFactory1, IDXGIOutput1,
|
||||
IDXGIOutput5, IDXGIOutputDuplication, IDXGIResource, DXGI_ERROR_ACCESS_LOST,
|
||||
IDXGIOutput5, IDXGIOutput6, IDXGIOutputDuplication, IDXGIResource, DXGI_ERROR_ACCESS_LOST,
|
||||
DXGI_ERROR_DEVICE_REMOVED, DXGI_ERROR_DEVICE_RESET, DXGI_ERROR_INVALID_CALL,
|
||||
DXGI_ERROR_MODE_CHANGE_IN_PROGRESS, DXGI_ERROR_WAIT_TIMEOUT, DXGI_OUTDUPL_DESC,
|
||||
DXGI_OUTDUPL_FRAME_INFO, DXGI_OUTDUPL_POINTER_SHAPE_INFO,
|
||||
@@ -129,6 +129,33 @@ pub(crate) unsafe fn find_output(gdi_name: &str) -> Result<(IDXGIAdapter1, IDXGI
|
||||
bail!("no DXGI output named {gdi_name} (gone after ACCESS_LOST?)")
|
||||
}
|
||||
|
||||
/// Read the source display's static HDR mastering metadata via `IDXGIOutput6::GetDesc1` (the
|
||||
/// monitor IS the "mastering display" for a desktop capture, exactly as Sunshine/Apollo treat it).
|
||||
/// GetDesc1 exposes the colour primaries, white point, and min/max mastering luminance but NOT a
|
||||
/// content light level, so MaxCLL/MaxFALL are left `0` (unknown — the display tone-maps from the
|
||||
/// mastering luminance). `None` if the output can't be cast to `IDXGIOutput6` or the call fails.
|
||||
unsafe fn read_output_hdr_meta(output: &IDXGIOutput1) -> Option<punktfunk_core::quic::HdrMeta> {
|
||||
let out6: IDXGIOutput6 = output.cast().ok()?;
|
||||
let d = out6.GetDesc1().ok()?;
|
||||
let m = crate::hdr::hdr_meta_from_display(
|
||||
(d.RedPrimary[0], d.RedPrimary[1]),
|
||||
(d.GreenPrimary[0], d.GreenPrimary[1]),
|
||||
(d.BluePrimary[0], d.BluePrimary[1]),
|
||||
(d.WhitePoint[0], d.WhitePoint[1]),
|
||||
d.MaxLuminance,
|
||||
d.MinLuminance,
|
||||
0, // MaxCLL: GetDesc1 has no content light level (Apollo zeroes it)
|
||||
0, // MaxFALL
|
||||
);
|
||||
tracing::info!(
|
||||
max_nits = d.MaxLuminance,
|
||||
min_nits = d.MinLuminance,
|
||||
max_full_frame_nits = d.MaxFullFrameLuminance,
|
||||
"read source display HDR mastering metadata (GetDesc1)"
|
||||
);
|
||||
Some(m)
|
||||
}
|
||||
|
||||
/// Create a fresh D3D11 device + context on a specific adapter (driver_type UNKNOWN with an explicit
|
||||
/// adapter). Used at open and on every ACCESS_LOST: a device created on one desktop cannot sustain a
|
||||
/// duplication on a *different* desktop (perpetual ACCESS_LOST), so the secure-desktop switch needs a
|
||||
@@ -1900,6 +1927,10 @@ pub struct DuplCapturer {
|
||||
/// produce a BT.2020 PQ 10-bit (`R10G10B10A2`) frame for NVENC. Toggling HDR fires ACCESS_LOST →
|
||||
/// `recreate_dupl` re-detects the format, so this tracks the *current* duplication.
|
||||
hdr_fp16: bool,
|
||||
/// The source display's static HDR mastering metadata (ST.2086 + content light level), read from
|
||||
/// `IDXGIOutput6::GetDesc1` whenever the duplication is HDR (`hdr_fp16`). The stream loop forwards
|
||||
/// it to the encoder (in-band SEI) and the client (0xCE). `None` when SDR or the read failed.
|
||||
hdr_meta: Option<punktfunk_core::quic::HdrMeta>,
|
||||
/// FP16 copy of the duplication surface (RT|SRV): the cursor composites onto it and the converter
|
||||
/// samples it. Reallocated on device/size change.
|
||||
fp16_src: Option<ID3D11Texture2D>,
|
||||
@@ -2129,6 +2160,14 @@ impl DuplCapturer {
|
||||
let gpu_mode = std::env::var("PUNKTFUNK_ENCODER")
|
||||
.map(|v| matches!(v.to_ascii_lowercase().as_str(), "nvenc" | "hw" | "nvidia"))
|
||||
.unwrap_or(false);
|
||||
// Read the source display's HDR mastering metadata while we still hold `output` (it is
|
||||
// moved into the struct below). Only meaningful for an HDR (FP16) duplication.
|
||||
let is_hdr_init = dd.ModeDesc.Format == DXGI_FORMAT_R16G16B16A16_FLOAT;
|
||||
let hdr_meta_init = if is_hdr_init {
|
||||
read_output_hdr_meta(&output)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
tracing::info!(
|
||||
"DXGI duplication: {}x{}@{} on {} ({}) dxgi_format={} (87=BGRA8 24=R10G10B10A2 10=R16G16B16A16_FLOAT)",
|
||||
width,
|
||||
@@ -2165,7 +2204,8 @@ impl DuplCapturer {
|
||||
gpu_copy: None,
|
||||
last_present: None,
|
||||
want_hdr,
|
||||
hdr_fp16: dd.ModeDesc.Format == DXGI_FORMAT_R16G16B16A16_FLOAT,
|
||||
hdr_fp16: is_hdr_init,
|
||||
hdr_meta: hdr_meta_init,
|
||||
fp16_src: None,
|
||||
fp16_srv: None,
|
||||
hdr10_out: None,
|
||||
@@ -2661,6 +2701,12 @@ impl DuplCapturer {
|
||||
// Re-detect HDR and drop the HDR textures/converter (old device). Toggling HDR on or
|
||||
// off is exactly this path: the duplication comes back as FP16 (HDR) or BGRA8.
|
||||
self.hdr_fp16 = dd.ModeDesc.Format == DXGI_FORMAT_R16G16B16A16_FLOAT;
|
||||
// Re-read the source mastering metadata for the (possibly new) HDR output, or clear it on SDR.
|
||||
self.hdr_meta = if self.hdr_fp16 {
|
||||
read_output_hdr_meta(&self.output)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.fp16_src = None;
|
||||
self.fp16_srv = None;
|
||||
self.hdr10_out = None;
|
||||
@@ -3084,6 +3130,15 @@ fn now_ns() -> u64 {
|
||||
}
|
||||
|
||||
impl Capturer for DuplCapturer {
|
||||
fn hdr_meta(&self) -> Option<punktfunk_core::quic::HdrMeta> {
|
||||
// Only when the duplication is actually HDR (FP16); cleared to None on an SDR rebuild.
|
||||
if self.hdr_fp16 {
|
||||
self.hdr_meta
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn next_frame(&mut self) -> Result<CapturedFrame> {
|
||||
// Generous: a secure-desktop switch can take several seconds to settle (re-resolve + recreate
|
||||
// the duplication up to 12 s). Better a few seconds of frozen-last-frame than dropping the stream.
|
||||
|
||||
@@ -127,6 +127,11 @@ pub struct WgcCapturer {
|
||||
first_frame: bool,
|
||||
|
||||
hdr: bool,
|
||||
/// The source display's static HDR mastering metadata (ST.2086 + content light level), read from
|
||||
/// `IDXGIOutput6::GetDesc1` at open when the output is HDR. Forwarded to the encoder (in-band SEI)
|
||||
/// and the client (0xCE) by the stream loop. `None` when SDR. (The helper relay path also encodes,
|
||||
/// so this is what gives the secure/normal-desktop HDR stream its mastering SEI.)
|
||||
hdr_meta: Option<punktfunk_core::quic::HdrMeta>,
|
||||
hdr_conv: Option<HdrConverter>,
|
||||
fp16_src: Option<ID3D11Texture2D>,
|
||||
fp16_srv: Option<ID3D11ShaderResourceView>,
|
||||
@@ -213,12 +218,31 @@ impl WgcCapturer {
|
||||
let hmonitor = od.Monitor;
|
||||
|
||||
// HDR iff the output's colour space is BT.2020 PQ (G2084) — matches the DDA FP16 detection.
|
||||
let hdr = output
|
||||
// From the same desc, read the source display's mastering metadata (ST.2086) when HDR.
|
||||
let desc1 = output
|
||||
.cast::<IDXGIOutput6>()
|
||||
.ok()
|
||||
.and_then(|o6| o6.GetDesc1().ok())
|
||||
.and_then(|o6| o6.GetDesc1().ok());
|
||||
let hdr = desc1
|
||||
.as_ref()
|
||||
.map(|d1| d1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020)
|
||||
.unwrap_or(false);
|
||||
let hdr_meta = if hdr {
|
||||
desc1.as_ref().map(|d| {
|
||||
crate::hdr::hdr_meta_from_display(
|
||||
(d.RedPrimary[0], d.RedPrimary[1]),
|
||||
(d.GreenPrimary[0], d.GreenPrimary[1]),
|
||||
(d.BluePrimary[0], d.BluePrimary[1]),
|
||||
(d.WhitePoint[0], d.WhitePoint[1]),
|
||||
d.MaxLuminance,
|
||||
d.MinLuminance,
|
||||
0, // MaxCLL: GetDesc1 has no content light level (Apollo zeroes it)
|
||||
0, // MaxFALL
|
||||
)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Wrap our D3D11 device as a WinRT IDirect3DDevice so the frame pool allocates on it (the
|
||||
// pool textures land on our device → CopyResource + NVENC are same-device, no readback).
|
||||
@@ -326,6 +350,7 @@ impl WgcCapturer {
|
||||
timeout_ms,
|
||||
first_frame: true,
|
||||
hdr,
|
||||
hdr_meta,
|
||||
hdr_conv: None,
|
||||
fp16_src: None,
|
||||
fp16_srv: None,
|
||||
@@ -680,6 +705,10 @@ impl WgcCapturer {
|
||||
}
|
||||
|
||||
impl Capturer for WgcCapturer {
|
||||
fn hdr_meta(&self) -> Option<punktfunk_core::quic::HdrMeta> {
|
||||
self.hdr_meta
|
||||
}
|
||||
|
||||
fn next_frame(&mut self) -> Result<CapturedFrame> {
|
||||
let overall = Instant::now() + Duration::from_secs(20);
|
||||
loop {
|
||||
|
||||
Reference in New Issue
Block a user