fix(presenter): MAILBOX present; push HDR10 metadata; honest HDR tag
apple / swift (push) Successful in 1m10s
android / android (push) Successful in 4m43s
arch / build-publish (push) Successful in 5m33s
ci / web (push) Successful in 59s
ci / docs-site (push) Successful in 1m9s
windows-host / package (push) Successful in 7m54s
ci / rust (push) Successful in 3m33s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m12s
release / apple (push) Successful in 8m28s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m10s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 45s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 51s
deb / build-publish (push) Successful in 3m59s
decky / build-publish (push) Successful in 14s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
ci / bench (push) Successful in 5m1s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
apple / screenshots (push) Successful in 5m52s
flatpak / build-publish (push) Successful in 4m55s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 10m52s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m50s
apple / swift (push) Successful in 1m10s
android / android (push) Successful in 4m43s
arch / build-publish (push) Successful in 5m33s
ci / web (push) Successful in 59s
ci / docs-site (push) Successful in 1m9s
windows-host / package (push) Successful in 7m54s
ci / rust (push) Successful in 3m33s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m12s
release / apple (push) Successful in 8m28s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m10s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 45s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 51s
deb / build-publish (push) Successful in 3m59s
decky / build-publish (push) Successful in 14s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
ci / bench (push) Successful in 5m1s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
apple / screenshots (push) Successful in 5m52s
flatpak / build-publish (push) Successful in 4m55s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 10m52s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m50s
Three Deck-test findings in the Vulkan presenter: MAILBOX over FIFO when the surface offers it (PUNKTFUNK_PRESENT_MODE= fifo restores the old default): both are tear-free, but an arrival- paced presenter blocking in FIFO's present queue eats most of a vblank whenever gamescope holds images for its composite pass or cadence drifts against refresh — the display stat flipping 1 ms → 11-13 ms. VK_EXT_hdr_metadata wired up: the host's 0xCE mastering metadata (or the Windows presenter's generic HDR10 baseline until it arrives) is pushed via vkSetHdrMetadataEXT whenever an HDR10 swapchain is (re)- created. Compositors key their HDR-app signaling on this — colorspace alone leaves gamescope treating the app as SDR (no SteamOS badge). And the OSD's HDR tag now reports the display path, not the stream: a PQ stream tone-mapped onto an SDR surface shows HDR→SDR, so the next test distinguishes "badge missing" from "HDR never engaged". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -530,7 +530,13 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result<Option<Outcome>
|
||||
}
|
||||
}
|
||||
SessionEvent::Stats(s) => {
|
||||
st.osd_text = stats_text(&st.mode_line, &s, &st.presented, st.hdr);
|
||||
st.osd_text = stats_text(
|
||||
&st.mode_line,
|
||||
&s,
|
||||
&st.presented,
|
||||
st.hdr,
|
||||
presenter.hdr_active(),
|
||||
);
|
||||
if print_stats {
|
||||
println!("stats: {}", st.osd_text.replace('\n', " | "));
|
||||
}
|
||||
@@ -627,6 +633,13 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result<Option<Outcome>
|
||||
// --- Frames: drain to the newest, upload + present -------------------------------
|
||||
let mut presented_video = false;
|
||||
if let Some(st) = &mut stream {
|
||||
// Mastering metadata (0xCE) → the presentation engine, ahead of the frame
|
||||
// that needs it. Low-rate (session start + mastering changes / keyframes).
|
||||
if let Some(c) = &st.connector {
|
||||
while let Ok(m) = c.next_hdr_meta(Duration::ZERO) {
|
||||
presenter.set_hdr_metadata(m);
|
||||
}
|
||||
}
|
||||
let mut newest: Option<DecodedFrame> = None;
|
||||
while let Ok(f) = st.handle.frames.try_recv() {
|
||||
newest = Some(f);
|
||||
@@ -785,13 +798,27 @@ const HINT_WITH_PAD: &str = "Click the stream to capture input · Ctrl+Alt+Shift
|
||||
|
||||
/// The unified stats window (design/stats-unification.md) as OSD text — multi-line for
|
||||
/// the console-UI panel; the stdout `stats:` line joins it with `|`.
|
||||
fn stats_text(mode_line: &str, s: &Stats, p: &PresentedWindow, hdr: bool) -> String {
|
||||
///
|
||||
/// The HDR tag is honest about the display path: `HDR` only when the swapchain actually
|
||||
/// runs HDR10 (`hdr_display`); a PQ stream tone-mapped onto an SDR surface (no HDR10
|
||||
/// format offered, HDR off in the compositor) shows `HDR→SDR` instead.
|
||||
fn stats_text(
|
||||
mode_line: &str,
|
||||
s: &Stats,
|
||||
p: &PresentedWindow,
|
||||
hdr_stream: bool,
|
||||
hdr_display: bool,
|
||||
) -> String {
|
||||
let mut text = format!(
|
||||
"{mode_line} · {:.0} fps · {:.1} Mb/s · {}{}",
|
||||
s.fps,
|
||||
s.mbps,
|
||||
if s.decoder.is_empty() { "-" } else { s.decoder },
|
||||
if hdr { " · HDR" } else { "" },
|
||||
match (hdr_stream, hdr_display) {
|
||||
(true, true) => " · HDR",
|
||||
(true, false) => " · HDR→SDR",
|
||||
_ => "",
|
||||
},
|
||||
);
|
||||
text.push_str(&format!(
|
||||
"\ne2e {:.1}/{:.1} ms (p50/p95)",
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
//! proprietary by design) report `supports_dmabuf() == false` and the caller keeps the
|
||||
//! decoder on software.
|
||||
//!
|
||||
//! Pacing: one frame in flight (the submit fence is waited before each record), FIFO by
|
||||
//! default (`PUNKTFUNK_PRESENT_MODE=mailbox|immediate` if available). Present is
|
||||
//! arrival-paced by the caller: a frame input on each decoded frame,
|
||||
//! `FrameInput::Redraw` re-blits the retained video image (expose/resize redraws).
|
||||
//! Pacing: one frame in flight (the submit fence is waited before each record), MAILBOX
|
||||
//! when available, FIFO otherwise (`PUNKTFUNK_PRESENT_MODE=fifo|mailbox|immediate`
|
||||
//! overrides — see `pick_present_mode` for why an arrival-paced presenter must not
|
||||
//! block in FIFO's present queue). Present is arrival-paced by the caller: a frame
|
||||
//! input on each decoded frame, `FrameInput::Redraw` re-blits the retained video image
|
||||
//! (expose/resize redraws).
|
||||
|
||||
use crate::csc::{build_fullscreen_pipeline, csc_rows, CscPass};
|
||||
use crate::dmabuf::{self, HwFrame};
|
||||
@@ -340,6 +342,12 @@ pub struct Presenter {
|
||||
hdr10_format: Option<vk::SurfaceFormatKHR>,
|
||||
/// PQ frames are on screen and the swapchain is in HDR10 mode.
|
||||
hdr_active: bool,
|
||||
/// `VK_EXT_hdr_metadata` device fns when the driver offers them (gamescope/KDE do).
|
||||
hdr_metadata_d: Option<ash::ext::hdr_metadata::Device>,
|
||||
/// The host's latest ST.2086/CLL metadata (the 0xCE plane) — pushed to the
|
||||
/// swapchain whenever HDR10 mode is live; `None` until the first datagram lands
|
||||
/// (a generic HDR10 baseline is pushed meanwhile).
|
||||
hdr_meta: Option<punktfunk_core::quic::HdrMeta>,
|
||||
/// The video image / CSC attachment format for the current mode.
|
||||
video_format: vk::Format,
|
||||
present_mode: vk::PresentModeKHR,
|
||||
@@ -432,6 +440,15 @@ impl Presenter {
|
||||
unavailable"
|
||||
);
|
||||
}
|
||||
// Static HDR metadata (ST.2086 mastering + CLL) to the presentation engine.
|
||||
// Compositors key their "this app is HDR" signaling on the client pushing
|
||||
// metadata via vkSetHdrMetadataEXT in addition to picking the HDR10 colorspace
|
||||
// (gamescope's SteamOS HDR badge and per-app tone-map targets among them) —
|
||||
// the colorspace alone leaves the app looking SDR to the shell.
|
||||
let has_hdr_metadata = has(ash::ext::hdr_metadata::NAME);
|
||||
if has_hdr_metadata {
|
||||
dev_exts.push(ash::ext::hdr_metadata::NAME.as_ptr());
|
||||
}
|
||||
|
||||
// --- Vulkan Video decode (the FFmpeg-on-our-device path) ---------------------
|
||||
// Probed, never required: a capable stack gets the video extensions, a second
|
||||
@@ -557,6 +574,8 @@ impl Presenter {
|
||||
}
|
||||
.context("vkCreateDevice")?;
|
||||
let swap_d = ash::khr::swapchain::Device::new(&instance, &device);
|
||||
let hdr_metadata_d =
|
||||
has_hdr_metadata.then(|| ash::ext::hdr_metadata::Device::new(&instance, &device));
|
||||
let queue = unsafe { device.get_device_queue(qfi, 0) };
|
||||
let hw = if hw_capable {
|
||||
Some(HwCtx {
|
||||
@@ -578,6 +597,9 @@ impl Presenter {
|
||||
device_extensions
|
||||
.extend(dmabuf::DEVICE_EXTENSIONS.iter().map(|n| CString::from(*n)));
|
||||
}
|
||||
if has_hdr_metadata {
|
||||
device_extensions.push(CString::from(ash::ext::hdr_metadata::NAME));
|
||||
}
|
||||
device_extensions.extend(video_ext_names.iter().map(|n| CString::from(*n)));
|
||||
Some(pf_client_core::video::VulkanDecodeDevice {
|
||||
get_instance_proc_addr: entry.static_fn().get_instance_proc_addr as usize,
|
||||
@@ -603,7 +625,13 @@ impl Presenter {
|
||||
|
||||
let (format, hdr10_format) = pick_formats(&surface_i, pdev, surface, has_colorspace_ext)?;
|
||||
let present_mode = pick_present_mode(&surface_i, pdev, surface)?;
|
||||
tracing::info!(?format, ?hdr10_format, ?present_mode, "swapchain config");
|
||||
tracing::info!(
|
||||
?format,
|
||||
?hdr10_format,
|
||||
?present_mode,
|
||||
hdr_metadata = has_hdr_metadata,
|
||||
"swapchain config"
|
||||
);
|
||||
let overlay_pipe = OverlayPipe::new(&device, format.format)?;
|
||||
|
||||
let cmd_pool = unsafe {
|
||||
@@ -651,6 +679,8 @@ impl Presenter {
|
||||
format,
|
||||
hdr10_format,
|
||||
hdr_active: false,
|
||||
hdr_metadata_d,
|
||||
hdr_meta: None,
|
||||
video_format: vk::Format::R8G8B8A8_UNORM,
|
||||
present_mode,
|
||||
swapchain: vk::SwapchainKHR::null(),
|
||||
@@ -762,9 +792,74 @@ impl Presenter {
|
||||
images = self.images.len(),
|
||||
"swapchain (re)created"
|
||||
);
|
||||
// HDR metadata is per-swapchain state: a rebuilt HDR10 swapchain needs it pushed
|
||||
// again (this also covers set_hdr_mode's entry into HDR10, which lands here).
|
||||
if self.hdr_active {
|
||||
self.apply_hdr_metadata();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether the swapchain is actually in HDR10/PQ mode — as opposed to a PQ stream
|
||||
/// being tone-mapped onto an SDR surface. This, not the stream's own signaling, is
|
||||
/// what user-facing "HDR" indicators should report.
|
||||
pub fn hdr_active(&self) -> bool {
|
||||
self.hdr_active
|
||||
}
|
||||
|
||||
/// Record the host's ST.2086 mastering + content-light metadata (the 0xCE plane),
|
||||
/// pushing it to the swapchain immediately when HDR10 mode is live. Cheap and
|
||||
/// idempotent per distinct value — callers just drain the plane into it.
|
||||
pub fn set_hdr_metadata(&mut self, meta: punktfunk_core::quic::HdrMeta) {
|
||||
if self.hdr_meta == Some(meta) {
|
||||
return;
|
||||
}
|
||||
self.hdr_meta = Some(meta);
|
||||
if self.hdr_active {
|
||||
self.apply_hdr_metadata();
|
||||
}
|
||||
}
|
||||
|
||||
/// Push the current metadata (the host's, or a generic HDR10 baseline until 0xCE
|
||||
/// arrives) to the presentation engine via `vkSetHdrMetadataEXT`. Compositors gate
|
||||
/// their HDR-app signaling on this — picking the HDR10 colorspace alone leaves
|
||||
/// gamescope treating the app as SDR (no SteamOS HDR badge, no per-app tone-map
|
||||
/// target). No-op where the driver lacks the extension.
|
||||
fn apply_hdr_metadata(&self) {
|
||||
let Some(ext) = &self.hdr_metadata_d else {
|
||||
return;
|
||||
};
|
||||
// Same generic baseline as the Windows presenter: BT.2020 primaries + D65
|
||||
// white, 1000-nit mastering display, MaxCLL 1000 / MaxFALL 400.
|
||||
let m = self.hdr_meta.unwrap_or(punktfunk_core::quic::HdrMeta {
|
||||
display_primaries: [[8500, 39850], [6550, 2300], [35400, 14600]],
|
||||
white_point: [15635, 16450],
|
||||
max_display_mastering_luminance: 10_000_000,
|
||||
min_display_mastering_luminance: 1,
|
||||
max_cll: 1000,
|
||||
max_fall: 400,
|
||||
});
|
||||
// Protocol fields are HDR10 SEI fixed-point (chromaticity 1/50000, luminance
|
||||
// 0.0001 cd/m², primaries in ST.2086 G,B,R order); Vulkan wants floats in
|
||||
// 0..1 chromaticity and whole nits, primaries named R/G/B.
|
||||
let xy = |p: [u16; 2]| vk::XYColorEXT {
|
||||
x: p[0] as f32 / 50_000.0,
|
||||
y: p[1] as f32 / 50_000.0,
|
||||
};
|
||||
let [g, b, r] = m.display_primaries;
|
||||
let md = vk::HdrMetadataEXT::default()
|
||||
.display_primary_red(xy(r))
|
||||
.display_primary_green(xy(g))
|
||||
.display_primary_blue(xy(b))
|
||||
.white_point(xy(m.white_point))
|
||||
.max_luminance(m.max_display_mastering_luminance as f32 / 10_000.0)
|
||||
.min_luminance(m.min_display_mastering_luminance as f32 / 10_000.0)
|
||||
.max_content_light_level(m.max_cll as f32)
|
||||
.max_frame_average_light_level(m.max_fall as f32);
|
||||
unsafe { ext.set_hdr_metadata(&[self.swapchain], &[md]) };
|
||||
tracing::debug!(from_host = self.hdr_meta.is_some(), "HDR metadata pushed");
|
||||
}
|
||||
|
||||
/// Whether the hardware (dmabuf) path exists on this device — callers keep the
|
||||
/// decoder on software when it doesn't.
|
||||
pub fn supports_dmabuf(&self) -> bool {
|
||||
@@ -1687,8 +1782,13 @@ fn pick_formats(
|
||||
Ok((sdr, hdr10))
|
||||
}
|
||||
|
||||
/// FIFO unless overridden (`PUNKTFUNK_PRESENT_MODE=mailbox|immediate`) and available —
|
||||
/// a streaming client defaults to tear-free.
|
||||
/// MAILBOX when the surface offers it, FIFO otherwise (`PUNKTFUNK_PRESENT_MODE=
|
||||
/// fifo|mailbox|immediate` overrides). Both are tear-free, but an arrival-paced
|
||||
/// presenter must not block in FIFO's present queue: when the compositor holds images
|
||||
/// for a vblank pass (gamescope's composite path) or arrival cadence drifts against
|
||||
/// refresh, `acquire_next_image` stalls most of a refresh — a standing 11-13 ms added
|
||||
/// to every frame at 60 Hz. MAILBOX never queues more than the newest frame, so the
|
||||
/// pipeline stays at decode latency and a late frame is replaced, not waited for.
|
||||
fn pick_present_mode(
|
||||
surface_i: &ash::khr::surface::Instance,
|
||||
pdev: vk::PhysicalDevice,
|
||||
@@ -1696,9 +1796,9 @@ fn pick_present_mode(
|
||||
) -> Result<vk::PresentModeKHR> {
|
||||
let modes = unsafe { surface_i.get_physical_device_surface_present_modes(pdev, surface) }?;
|
||||
let want = match std::env::var("PUNKTFUNK_PRESENT_MODE").ok().as_deref() {
|
||||
Some("mailbox") => vk::PresentModeKHR::MAILBOX,
|
||||
Some("fifo") => vk::PresentModeKHR::FIFO,
|
||||
Some("immediate") => vk::PresentModeKHR::IMMEDIATE,
|
||||
_ => vk::PresentModeKHR::FIFO,
|
||||
_ => vk::PresentModeKHR::MAILBOX,
|
||||
};
|
||||
Ok(if modes.contains(&want) {
|
||||
want
|
||||
|
||||
Reference in New Issue
Block a user