color: non-sRGB preview, per-monitor display ICC, pipeline hardening

Preview now follows the project output colorspace end to end: the
display chain derives its content space from the project's OutputColorSpec
instead of a hardcoded sRGB name, self-managed ICC transforms go through
an XYZ D65 interchange stage (OCIO cie_xyz_d65_interchange) for non-sRGB
targets, and the platform layer declares the content colorspace (gpui
submodule bump). macOS defaults to OS-managed (fixes wide-gamut UI
oversaturation); Windows ACM warns once on non-sRGB targets.

Multi-monitor: the display ICC is looked up per the window's current
screen (macOS display id, Windows per-monitor DC, X11 RandR output
profile) with a throttled poll that invalidates frame caches on moves.

Pipeline precision: 10-bit+ sources fall back to YUV444P16LE + a Rust
matrix conversion when swscale lacks F32 output (no more 8-bit
truncation); BT.709/2020 SDR decodes with BT.1886 gamma 2.4 instead of
the sRGB EOTF; working-space compositing no longer clamps RGB to [0,1]
(alpha still clamped); the output node clamps to the target gamut;
frames without colorimetry metadata convert with BT.709 defaults
(warned once) instead of passing through; scopes read the
output-colorspace signal on both F32 paths.

Also: only emit rerun-if-changed for .env when it exists (a missing file
made every build fully dirty).
This commit is contained in:
2026-08-29 00:24:15 +08:00
parent 879ff8da5b
commit fdb5caabd5
42 changed files with 4423 additions and 146 deletions
+14
View File
@@ -43,10 +43,23 @@ fn clip_path(tag: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!("oakrender_graph_{tag}_{}.mp4", std::process::id()))
}
/// These tests verify graph/decode/composite MECHANICS (stacking, scaling,
/// effects), not the color pipeline. Pin the working space to the legacy
/// sRGB pass-through so the decoded pixels stay display-referred and the
/// pixel-value assertions hold regardless of the ACEScg default. All tests
/// in this binary set the same value, so the shared global is race-free.
fn pin_legacy_working_space() {
oak_render::color::set_pipeline_color_settings(
oak_common::colormath::WorkingColorSpace::SrgbLegacy,
oak_common::colormath::OutputColorSpec::default(),
);
}
/// One sequence + one video track list with one track per clip
/// `(filename, [in, out))`. The LAST entry's track composites on top
/// (NLE stacking: the highest-numbered track is topmost).
fn build_project(clips: &[(&str, Rational, Rational)]) -> (Arc<Mutex<Project>>, NodeId) {
pin_legacy_working_space();
let project = Project::new();
let seq;
{
@@ -269,6 +282,7 @@ fn build_effect_project(
clip: (&str, Rational, Rational),
insert_effect: impl FnOnce(&mut Project, NodeId, NodeId) -> NodeId,
) -> (Arc<Mutex<Project>>, NodeId) {
pin_legacy_working_space();
let project = Project::new();
let seq;
{