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:
@@ -164,6 +164,15 @@ impl ExportTask {
|
||||
params.subtitles_enabled = self.encoding_params.subtitles_enabled as i32;
|
||||
params.export_length_num = self.encoding_params.export_length_num;
|
||||
params.export_length_den = self.encoding_params.export_length_den;
|
||||
// Delivery colorimetry: tag the output container with the project's
|
||||
// output colorspace (H.273 code points → mov `colr` atom / VUI).
|
||||
// Limited range is the video-delivery convention; the encoder's
|
||||
// RGB→YCbCr runs limited.
|
||||
let (_working, spec) = self.delivery_color();
|
||||
params.color_primaries = spec.gamut.av_color_primaries();
|
||||
params.color_trc = spec.transfer.av_color_trc();
|
||||
params.color_space = spec.gamut.av_color_space();
|
||||
params.color_range = 1; // AVCOL_RANGE_MPEG (limited)
|
||||
params
|
||||
}
|
||||
|
||||
@@ -194,6 +203,57 @@ impl ExportTask {
|
||||
TimeRange::new(Rational::new(0, 1), length)
|
||||
}
|
||||
|
||||
/// The project's pipeline color settings (working colorspace + the
|
||||
/// delivery output spec) read off the exported node's project — the
|
||||
/// export renders to the project's delivery target, not to the display.
|
||||
fn delivery_color(&self) -> (
|
||||
oak_common::colormath::WorkingColorSpace,
|
||||
oak_common::colormath::OutputColorSpec,
|
||||
) {
|
||||
let guard = self
|
||||
.viewer_node
|
||||
.0
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
(guard.working_color_space(), guard.output_color_spec())
|
||||
}
|
||||
|
||||
/// Convert an F32 codec frame from the pipeline working space to the
|
||||
/// project's delivery colorspace (row-wise — the codec frame rows are
|
||||
/// 32-byte aligned, so each row is handled through the byte-based
|
||||
/// transform). A no-op for non-F32 frames and in the legacy sRGB
|
||||
/// working space.
|
||||
fn apply_output_node(&self, frame: &mut oak_codec::frame::Frame) {
|
||||
if frame.format() != oak_core::PixelFormat::F32 {
|
||||
return;
|
||||
}
|
||||
let (working, spec) = self.delivery_color();
|
||||
if working == oak_common::colormath::WorkingColorSpace::SrgbLegacy {
|
||||
return;
|
||||
}
|
||||
let w = frame.width().max(0) as usize;
|
||||
let h = frame.height().max(0) as usize;
|
||||
if w == 0 || h == 0 {
|
||||
return;
|
||||
}
|
||||
let row_bytes = w * 16; // F32 RGBA
|
||||
let linesize = frame.linesize_bytes() as usize;
|
||||
let Some(data) = frame.data_mut() else {
|
||||
return;
|
||||
};
|
||||
for y in 0..h {
|
||||
let start = y * linesize;
|
||||
if start + row_bytes > data.len() {
|
||||
break;
|
||||
}
|
||||
oak_common::colormath::acescg_to_output_bytes(
|
||||
&mut data[start..start + row_bytes],
|
||||
w,
|
||||
spec,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy a rendered `oakrender` CPU texture into an `oakcodec` frame
|
||||
/// with the matching video params (row-wise copy — line sizes may
|
||||
/// differ between the render and codec frame layouts).
|
||||
@@ -317,7 +377,12 @@ impl RenderTaskBehavior for ExportTask {
|
||||
let Some(encoder) = &self.encoder else {
|
||||
return Ok(());
|
||||
};
|
||||
let codec_frame = Self::to_codec_frame(frame)?;
|
||||
let mut codec_frame = Self::to_codec_frame(frame)?;
|
||||
// Output node: the rendered frame is in the pipeline working space
|
||||
// (ACEScg linear by default); the export converts it to the
|
||||
// project's delivery colorspace before encoding. (No-op in the
|
||||
// legacy sRGB working space.)
|
||||
self.apply_output_node(&mut codec_frame);
|
||||
if let Err(_) = encoder.write_video(&codec_frame) {
|
||||
let err = encoder.get_error();
|
||||
task.set_error(&err);
|
||||
|
||||
Reference in New Issue
Block a user