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
+30 -1
View File
@@ -576,6 +576,10 @@ impl WorkerSession {
};
match oak_node::serializer::load_with_id_map(&content) {
Ok((project, id_map)) => {
let (working, output_spec) = {
let guard = project.lock().unwrap_or_else(|e| e.into_inner());
(guard.working_color_space(), guard.output_color_spec())
};
let project_uuid = project
.lock()
.unwrap_or_else(|e| e.into_inner())
@@ -588,6 +592,10 @@ impl WorkerSession {
id_map,
project_copy: 0,
});
// The project's color pipeline properties drive this
// process's input/output transforms (the oakrender
// process global read by eval + the output node).
oak_render::color::set_pipeline_color_settings(working, output_spec);
// A fresh graph snapshot can change what any viewer
// identity renders; cached pixels from the previous
// graph must not be served (M16 S2 frame cache).
@@ -1098,8 +1106,12 @@ impl WorkerSession {
// BGRA8: the F32 pipeline frame comes from the cache or the
// session scratch, then converts into the slot (the end-of-pipe
// format convert is not an extra frame copy, design §3.1).
match &cached {
// Before quantization the output node runs: working space
// (ACEScg) → the project's output colorspace, so the 8-bit
// pixels carry gamma-encoded display values, not linear light.
match cached.as_mut() {
Some(c) => {
apply_output_node(c, (w * h) as usize);
convert_f32_rgba_to_bgra8(&c[..f32_need], &mut dst[..dst_need]);
}
None => {
@@ -1116,6 +1128,7 @@ impl WorkerSession {
)?;
self.frame_cache
.insert(key, self.f32_scratch[..f32_need].to_vec());
apply_output_node(&mut self.f32_scratch, (w * h) as usize);
convert_f32_rgba_to_bgra8(&self.f32_scratch[..f32_need], &mut dst[..dst_need]);
}
}
@@ -1313,6 +1326,22 @@ fn convert_f32_rgba_to_bgra8(src: &[u8], dst: &mut [u8]) {
}
}
/// The output node for the BGRA8 delivery path: convert the first
/// `pixels` pixels of an F32 RGBA byte buffer from the pipeline working
/// space to the project's output colorspace (in place), so the 8-bit
/// quantization encodes display-referred values instead of linear light.
/// A no-op in the legacy sRGB working space (content already is
/// display-referred sRGB).
fn apply_output_node(bytes: &mut [u8], pixels: usize) {
if oak_render::color::pipeline_working_space()
== oak_common::colormath::WorkingColorSpace::SrgbLegacy
{
return;
}
let spec = oak_render::color::pipeline_output_spec();
oak_common::colormath::acescg_to_output_bytes(bytes, pixels, spec);
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------