fix(client): HDR stops leaking out of the stream and blowing out the console UI
ci / web (push) Successful in 1m3s
ci / rust-arm64 (push) Successful in 1m23s
ci / docs-site (push) Successful in 1m12s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 6s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 8s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 7s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 6s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m29s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 49s
deb / build-publish-client-arm64 (push) Successful in 1m57s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
deb / build-publish-host (push) Successful in 3m40s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m55s
deb / build-publish (push) Successful in 5m59s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m7s
docker / builders-arm64cross (push) Successful in 4s
docker / deploy-docs (push) Successful in 27s
android / android (push) Successful in 8m10s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m12s
arch / build-publish (push) Successful in 9m35s
flatpak / build-publish (push) Successful in 6m57s
ci / rust (push) Successful in 10m44s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 7m54s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 6m17s
apple / swift (push) Successful in 5m10s
apple / screenshots (push) Canceled after 19m1s
ci / web (push) Successful in 1m3s
ci / rust-arm64 (push) Successful in 1m23s
ci / docs-site (push) Successful in 1m12s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 6s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 8s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 7s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 6s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m29s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 49s
deb / build-publish-client-arm64 (push) Successful in 1m57s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
deb / build-publish-host (push) Successful in 3m40s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m55s
deb / build-publish (push) Successful in 5m59s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m7s
docker / builders-arm64cross (push) Successful in 4s
docker / deploy-docs (push) Successful in 27s
android / android (push) Successful in 8m10s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m12s
arch / build-publish (push) Successful in 9m35s
flatpak / build-publish (push) Successful in 6m57s
ci / rust (push) Successful in 10m44s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 7m54s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 6m17s
apple / swift (push) Successful in 5m10s
apple / screenshots (push) Canceled after 19m1s
The gamepad/console UI looked right on launch, and wrong forever after the first HDR session: connect to an HDR host, disconnect, and the UI came back overblown with wrong colours. The UI is not its own renderer. It is a `pf_presenter::overlay::Overlay` composited into the SAME swapchain the stream used, and it draws plain sRGB with no HDR awareness at all — so the swapchain's colorspace decides how its pixels are read. `present` switches SDR↔HDR10 from the FRAME's colour signalling, and a UI-only present is `FrameInput::Redraw`, which carries none: the mode block is skipped entirely and nothing ever hands HDR10 back. The UI's sRGB mid-tones were then emitted as PQ code points, i.e. near-peak nits. `leave_hdr` drops back to SDR, called where the UI-only present already happens and gated on the existing `browse_idle` — Browse mode with no live connector, i.e. the UI owns the screen. That covers every route back to the UI rather than just the Ended/Failed arms, and it is guarded internally so idle iterations stay free. It also bails when minimized, which is load-bearing rather than an optimization: `recreate_swapchain` keeps the old swapchain at a zero extent, but `set_hdr_mode` would by then have rebuilt the CSC and overlay pipes against the SDR format — mismatched against live HDR10 images. `present` early-returns on a zero extent above its HDR block, so this was unreachable until a caller outside `present` existed. Deliberately not applied to the `resize_scrim` arm of the same present: that scrim is a mid-stream gap in a session that is still HDR, and flipping there would rebuild the swapchain twice per resize. Does not address the adjacent case: an overlay drawn DURING a live HDR session (the stats HUD, the resize scrim) is blown out the same way. That needs the overlay to PQ-encode when `hdr_active`; dropping to SDR is only correct here because the console UI shows exactly when no stream is live. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1520,6 +1520,16 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result<Option<Outcome>
|
||||
let browse_idle = matches!(mode, ModeCtl::Browse(_))
|
||||
&& stream.as_ref().is_none_or(|s| s.connector.is_none());
|
||||
if !presented_video && (resize_scrim || browse_idle) {
|
||||
// The UI owns the screen again: hand the swapchain back to SDR before drawing
|
||||
// it. A finished PQ stream leaves HDR10 live, and nothing else would ever turn
|
||||
// it off — `present` re-evaluates the mode only from a frame's colour
|
||||
// signalling, and these UI presents carry no frame. Guarded inside, so this is
|
||||
// free on every ordinary idle iteration. (Deliberately NOT applied to
|
||||
// `resize_scrim`: that scrim is a mid-stream gap in a session that is still
|
||||
// HDR, and flipping there would rebuild the swapchain twice per resize.)
|
||||
if browse_idle {
|
||||
presenter.leave_hdr(&window)?;
|
||||
}
|
||||
presenter.present(&window, FrameInput::Redraw, overlay_frame.as_ref())?;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -171,6 +171,34 @@ impl Presenter {
|
||||
self.hdr_active
|
||||
}
|
||||
|
||||
/// Drop back to the SDR swapchain. A no-op unless HDR10 is actually live, so it is
|
||||
/// cheap to call on every idle iteration (the flip itself rebuilds the CSC pass, the
|
||||
/// video image, the overlay pipe and the swapchain).
|
||||
///
|
||||
/// The console/gamepad UI is SDR content, and it is composited into whatever swapchain
|
||||
/// the last STREAM left behind. [`Presenter::present`] only re-evaluates the mode when a
|
||||
/// frame carries colour signalling, and a UI-only present is `FrameInput::Redraw`, which
|
||||
/// carries none — so once a PQ session ended, the UI kept being written into the HDR10
|
||||
/// swapchain and its sRGB mid-tones were emitted as PQ code points, i.e. near-peak nits.
|
||||
/// That is the "gamepad UI is blown out after disconnecting from an HDR host" report:
|
||||
/// the UI looks right until the first HDR session, and wrong forever after.
|
||||
pub fn leave_hdr(&mut self, window: &sdl3::video::Window) -> Result<()> {
|
||||
if !self.hdr_active {
|
||||
return Ok(());
|
||||
}
|
||||
// Minimized is not just "pointless work": `recreate_swapchain` deliberately keeps
|
||||
// the old swapchain at a zero extent, but `set_hdr_mode` would already have rebuilt
|
||||
// the CSC and overlay pipes against the SDR format — leaving them mismatched against
|
||||
// the live HDR10 swapchain images. [`Presenter::present`] carries the same guard,
|
||||
// which is why the flip could never reach this state before; the mode change just
|
||||
// waits for the window to have a size again.
|
||||
if self.extent.width == 0 || self.extent.height == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
tracing::info!("stream over — leaving HDR10 so the console UI composites as SDR");
|
||||
self.set_hdr_mode(window, false)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
||||
Reference in New Issue
Block a user