Files
oak-editor/crates/oak-plugin/src/clip.rs
T
Mike-Solar fdb5caabd5 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).
2026-08-29 00:24:15 +08:00

432 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! clip 实例:clip ↔ oakrender 纹理桥。
//!
//! 对应 C++ 的 `OliveClipInstance`。纹理数据经
//! [`crate::render`]oakrender 值类型:`Texture`/`Frame`)流动;
//! OFX 侧只看到 [`crate::image::Image`]CPU 路径)。
//!
//! `#[repr(C)]` + props 在偏移 0(句柄约定,见 [`crate::suites::tag`]
//! clip handle 即 `&props`)。
//!
//! 单库化后 oakrender 的 ffi 已删除:帧访问走
//! [`oak_render::texture::Texture::to_frame`] 值路径(GPU 纹理经后端
//! 下载、CPU 纹理克隆),帧释放随值 drop 自动发生(原
//! `texture_get_frame`/`frame_free` 句柄调用面随桩删除)。
use crate::instance::{OfxRangeD, OfxRectD, RenderScale};
use crate::property::PropertySet;
/// clip 实例。
#[repr(C)]
pub struct ClipInstance {
/// 实例级 clip 属性(当前分量/位深/像素比,协商结果写入;
/// 偏移 0,句柄约定)。
pub props: PropertySet,
/// clip 名。
pub name: String,
/// 当前输入纹理(oakrender 值;输出 clip 为 None)。
input_texture: std::sync::Mutex<Option<crate::render::Texture>>,
/// 当前输出纹理(C++ `output_textures_` 的 phase 1 单槽;
/// [`store_output_image`](Self::store_output_image) 的回写目标;
/// 输入 clip 为 None)。
output_texture: std::sync::Mutex<Option<crate::render::Texture>>,
}
/// 从 clip 属性读协商分量(getClipPreferences 写入)。
fn components_from_props(props: &PropertySet) -> Option<crate::image::Components> {
use crate::property::Value;
match props.get(crate::image::K_IMAGE_EFFECT_PROP_COMPONENTS, 0)? {
Value::String(s) => match s.to_string_lossy().as_ref() {
"OfxImageComponentRGBA" => Some(crate::image::Components::Rgba),
"OfxImageComponentRGB" => Some(crate::image::Components::Rgb),
"OfxImageComponentAlpha" => Some(crate::image::Components::Alpha),
_ => None,
},
_ => None,
}
}
/// IEEE 754 半精度 → 单精度([`crate::image::f16_to_f32`] 的本地别名,
/// 保持调用点可读)。
fn f16_to_f32(bits: u16) -> f32 {
crate::image::f16_to_f32(bits)
}
impl ClipInstance {
/// 按描述符实例化(createInstance 路径调用;公开:宿主与测试
/// 都需要构造 clip 实例)。实例 props 是描述符 props 的深拷贝
/// HS: ClipBase 的实例构造,ofxhClip.cpp:57-70——插件在实例期
/// 读 supported components 等)。
///
/// ofxColourM11 §4):输入 clip 的 kOfxImageClipPropColourspace
/// 由宿主写为工作空间(ACEScgofxColour.h "Hosts should set this
/// property to the colourspace of the input clip. Typically it will
/// be set to the working colourspace");输出 clip 由
/// GetOutputColourspace action 后写。
pub fn from_descriptor(desc: &crate::descriptor::ClipDescriptor) -> Self {
let props = desc.props.clone();
let name = desc.name.clone();
if name != "Output" {
// The working colorspace follows the pipeline setting (project
// property): ACEScg in the default pipeline, sRGB in the legacy
// pass-through mode — plugins must be told the true space of the
// pixels they receive.
props.set_one(
crate::host::PROP_CLIP_COLOURSPACE,
crate::property::Value::String(
std::ffi::CString::new(oak_render::color::pipeline_working_ofx_name()).unwrap(),
),
);
}
// OfxImageClipPropConnectedofxsImageEffect.cpp:1106 的
// `Clip::isConnected()` 是无默认值强读——缺这个属性时,CImg 这类
// 带可选 mask clip 的插件直接抛
// PropertyUnknownToHost → MissingHostFeature 紫帧)。默认 0
// (未连接),挂接输入纹理时由 set_input_texture 置 1。
props.set_one(
crate::host::PROP_CLIP_CONNECTED,
crate::property::Value::Int(0),
);
Self {
props,
name,
input_texture: std::sync::Mutex::new(None),
output_texture: std::sync::Mutex::new(None),
}
}
/// 写协商后的像素格式(C++ `OliveClipInstance::setParams` 的 props
/// 侧:setPixelDepth/setComponentsoliveclip.cpp:700-709)。
/// `format` 为 olive::PixelFormat::Format0=u8, 2=u16, 3=f16,
/// 4=f32);`channels` 为分量数。
pub fn set_video_params(&self, format: i32, channels: i32) {
let depth = match format {
0 => "OfxBitDepthByte",
2 => "OfxBitDepthShort",
3 => "OfxBitDepthHalf",
_ => "OfxBitDepthFloat",
};
let comps = match channels {
1 => "OfxImageComponentAlpha",
3 => "OfxImageComponentRGB",
_ => "OfxImageComponentRGBA",
};
self.props.set_one(
crate::image::K_IMAGE_EFFECT_PROP_PIXEL_DEPTH,
crate::property::Value::String(std::ffi::CString::new(depth).unwrap()),
);
self.props.set_one(
crate::image::K_IMAGE_EFFECT_PROP_COMPONENTS,
crate::property::Value::String(std::ffi::CString::new(comps).unwrap()),
);
}
/// 写协商 RoDC++ `setRegionOfDefinition` 的单槽版,
/// oliveclip.cpp:674-678——C++ 按 time 存 map,本驱动一帧一槽;
/// 落点与 image effect suite 的 clipGetRegionOfDefinition 读取处
/// 一致)。
pub fn set_region_of_definition(&self, rod: OfxRectD, _time: f64) {
use crate::property::Value;
self.props.define(
"OfxImageEffectPropRegionOfDefinition",
vec![
Value::Double(rod.x1),
Value::Double(rod.y1),
Value::Double(rod.x2),
Value::Double(rod.y2),
],
);
}
/// 挂接输入纹理(oaknode 侧 clip 输入值变化时由 param/render 桥
/// 调用)。`time` 用于多帧纹理选择。None 断开。
pub fn set_input_texture(&self, texture: Option<crate::render::Texture>, _time: f64) {
// The connection state follows the texture hand-off (the render
// driver only feeds clips that have input; optional mask clips
// stay 0, so `Clip::isConnected()` answers false for them).
let connected = i32::from(texture.is_some());
self.props.set_one(
crate::host::PROP_CLIP_CONNECTED,
crate::property::Value::Int(connected),
);
*self.input_texture.lock().unwrap_or_else(|e| e.into_inner()) = texture;
}
/// 挂接输出纹理(render 驱动创建并经值传入;C++
/// `setOutputTexture` 的 phase 1 单槽版)。`time` 用于多帧纹理
/// 选择(`// [P2]`)。None 断开。
pub fn set_output_texture(&self, texture: Option<crate::render::Texture>, _time: f64) {
*self
.output_texture
.lock()
.unwrap_or_else(|e| e.into_inner()) = texture;
}
/// 抓取本 clip 在 `time` 的图像(OFX clipGetImage 的宿主侧)。
/// CPU 路径:把 oakrender 纹理 readback 成 [`crate::image::Image`]
/// (像素格式按协商结果,全链路 F32)。
/// `// [P2]` GL 路径:clipLoadTexture 语义在此扩展。
///
/// 输入帧支持 U8/U16/F16/F32:非 F32 归一化转换为 F32(对齐
/// oliveclip.cpp setInputTexture 的格式转换路径);转换中的
/// NaN/Inf 清洗为 0oliveclip.cpp copy_pixels 的 scrub)。
/// `region` 只支持 None(整帧)——子区域随 renderer 桥落地。
pub fn fetch_image(
&self,
time: f64,
scale: RenderScale,
region: Option<OfxRectD>,
) -> crate::error::Result<crate::image::Image> {
use crate::render::PIXEL_FORMAT_F32;
use crate::error::Error;
let _ = (time, scale);
if region.is_some() {
return Err(Error::Failed("fetch_image 子区域第 1 期不支持".into()));
}
let texture = self
.input_texture
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
.ok_or(Error::NotFound)?;
// 占位纹理(dummy):视作无输入。
if texture.is_dummy() {
return Err(Error::NotFound);
}
// 纹理 → CPU 帧(GPU 纹理后端下载;帧随 drop 释放)。
let frame = crate::render::texture_get_frame(&texture)?;
let params = frame.video_params();
let format = params.format;
if format != PIXEL_FORMAT_F32
&& format != crate::render::PIXEL_FORMAT_U8
&& format != oak_core::PixelFormat::U16 as i32
&& format != oak_core::PixelFormat::F16 as i32
{
return Err(Error::Failed(format!(
"输入帧格式 {format} 不支持(仅 U8/U16/F16/F32"
)));
}
let (w, h) = (params.width as f64, params.height as f64);
// 分量按协商结果(getClipPreferences 已写入 clip.props)。
let components =
components_from_props(&self.props).unwrap_or(crate::image::Components::Rgba);
let mut image = crate::image::Image::allocate(
crate::image::BitDepth::Float,
components,
OfxRectD {
x1: 0.0,
y1: 0.0,
x2: w,
y2: h,
},
);
let src = frame.data();
if src.is_null() {
return Err(Error::Failed("帧无数据".into()));
}
// 行优先 + 格式转换(帧行跨度经 linesize 读取——真实
// oakrender 帧可有行填充;目标 Image 恒紧凑 F32)。
// U8/U16/F16 输入归一化到 [0,1] 浮点(对齐 oliveclip.cpp
// setInputTexture 的 swscale 转换路径:插件侧永远见到协商位
// 深);F32/转换结果中的 NaN/Inf 清洗为 0oliveclip.cpp
// copy_pixels 的 scrub——CImg 对 NaN 未定义行为)。
let channels = components.channel_count();
let samples_per_row = (w as usize) * channels;
let src_bpc = match format {
f if f == crate::render::PIXEL_FORMAT_U8 => 1,
f if f == oak_core::PixelFormat::U16 as i32 => 2,
f if f == oak_core::PixelFormat::F16 as i32 => 2,
_ => 4,
};
let tight_src = samples_per_row * src_bpc;
let row = frame.linesize_bytes();
let row = if row > 0 { row } else { tight_src };
let src_bytes = unsafe { std::slice::from_raw_parts(src, row * h as usize) };
let dst = image.pixels_mut();
let mut scrubbed = false;
for y in 0..h as usize {
let s = y * row;
for i in 0..samples_per_row {
let v = match format {
f if f == crate::render::PIXEL_FORMAT_U8 => {
src_bytes[s + i] as f32 / 255.0
}
f if f == oak_core::PixelFormat::U16 as i32 => {
let off = s + i * 2;
let bits = u16::from_le_bytes([src_bytes[off], src_bytes[off + 1]]);
bits as f32 / 65535.0
}
f if f == oak_core::PixelFormat::F16 as i32 => {
let off = s + i * 2;
let bits = u16::from_le_bytes([src_bytes[off], src_bytes[off + 1]]);
let v = f16_to_f32(bits);
if v.is_nan() || v.is_infinite() {
scrubbed = true;
0.0
} else {
v
}
}
_ => {
let off = s + i * 4;
let v = f32::from_le_bytes([
src_bytes[off],
src_bytes[off + 1],
src_bytes[off + 2],
src_bytes[off + 3],
]);
if v.is_nan() || v.is_infinite() {
scrubbed = true;
0.0
} else {
v
}
}
};
let d = (y * samples_per_row + i) * 4;
dst[d..d + 4].copy_from_slice(&v.to_le_bytes());
}
}
if scrubbed {
eprintln!("[PLUGIN] NaN/Inf scrubbed from input frame data during fetch");
}
// 位深协商(设计约束:管线全链路 ACEScg + F32;插件不支持
// F32 时输入图像转成协商位深——输出端在 render 驱动转回
// F32)。clip props 的 PixelDepth 由协商流程
// set_video_params)写入;缺省 = F32。
let negotiated = self
.props
.get(crate::image::K_IMAGE_EFFECT_PROP_PIXEL_DEPTH, 0)
.and_then(|v| match v {
crate::property::Value::String(s) => {
crate::image::BitDepth::from_ofx(&s.to_string_lossy())
}
_ => None,
})
.unwrap_or(crate::image::BitDepth::Float);
if negotiated != crate::image::BitDepth::Float {
image = image.convert_depth(negotiated);
}
Ok(image)
}
/// 把输出图像回写为 oakrender 纹理(render 完成后由
/// [`crate::instance::Instance::render`] 的调用方使用)。
///
/// 输出纹理由 oakrender 侧创建并经 [`Self::set_output_texture`]
/// 挂入——本函数取该纹理的 CPU 帧(GPU 纹理经后端下载,写回后
/// 对 `Texture::Gpu` 再经
/// [`oak_render::backend::GpuContextLike::upload`] 上传),按帧
/// 参数校验 F32 与尺寸后整帧拷贝图像像素(全链路 F32;C++
/// pluginrenderer 的 `readback/wrap` 路径第 1 期以 CPU 拷贝表达,
/// GL 走 [`crate::render`] 的 `// [P2]`)。未挂输出纹理
/// 或纹理为占位(dummy)→ [`crate::error::Error::NotFound`]。
/// 成功返回纹理值(克隆,随 drop 释放)。
pub fn store_output_image(
&self,
image: &crate::image::Image,
) -> crate::error::Result<crate::render::Texture> {
use crate::render::{texture_get_frame, PIXEL_FORMAT_F32};
use crate::error::Error;
let texture = self
.output_texture
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
.ok_or(Error::NotFound)?;
if texture.is_dummy() {
return Err(Error::NotFound);
}
let mut frame = texture_get_frame(&texture)?;
let params = frame.video_params();
if params.format != PIXEL_FORMAT_F32 {
return Err(Error::Failed(format!(
"输出帧格式 {} 非 F32(第 1 期约束)",
params.format
)));
}
let (w, h) = (params.width as usize, params.height as usize);
// 图像与帧必须同尺寸(全链路 F32;宽高/行宽/总长逐项校验)。
let tight = w * image.components().channel_count() * 4;
if tight != image.row_bytes() || tight * h != image.pixels().len() {
return Err(Error::Failed("图像尺寸与输出帧不一致".into()));
}
let dst = frame.data_mut();
if dst.is_null() {
return Err(Error::Failed("输出帧无数据".into()));
}
// 行优先拷贝(目标帧行跨度经 linesize 读取——真实 oakrender
// 帧可有行填充;M11 §4 修复同 fetch_image)。
let row = frame.linesize_bytes();
let row = if row > 0 { row } else { tight };
let dst_bytes = unsafe { std::slice::from_raw_parts_mut(dst, row * h) };
let pixels = image.pixels();
for y in 0..h {
let d = y * row;
let s = y * tight;
dst_bytes[d..d + tight].copy_from_slice(&pixels[s..s + tight]);
}
// GPU 目标纹理:拷贝只落在下载帧上,经后端 upload 回写
// CPU 纹理无需上传)。
if let crate::render::Texture::Gpu { token, ctx, .. } = &texture {
ctx.upload(*token, &frame)
.map_err(|e| Error::Failed(format!("输出纹理上传失败:{e}")))?;
}
Ok(texture)
}
/// 本 clip 的时间域(clipGetFrameRange)。
///
/// `// TODO(value-model)`:输入范围经 oakrender 帧的时间基推导
/// time_base)——随 clip 迁移到 `oak_render::texture::Texture`
/// 值模型落地。
pub fn frame_range(&self) -> crate::error::Result<OfxRangeD> {
let _ = OfxRangeD::default();
Err(crate::error::Error::Failed(
"frame_range 待 renderer 桥".into(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn f16_to_f32_covers_special_values() {
// 常规值:1.0 = 0x3C00-2.0 = 0xC0000.5 = 0x3800。
assert_eq!(f16_to_f32(0x3C00), 1.0);
assert_eq!(f16_to_f32(0xC000), -2.0);
assert_eq!(f16_to_f32(0x3800), 0.5);
// 零与负零。
assert_eq!(f16_to_f32(0x0000), 0.0);
assert_eq!(f16_to_f32(0x8000).to_bits(), (0.0f32).to_bits() | (1 << 31));
// 非规格数:最小正规格数 2^-14 ≈ 0.000061042^-24 是最小非
// 规格数之一。
assert!((f16_to_f32(0x0400) - 2f32.powi(-14)).abs() < 1e-12);
assert!((f16_to_f32(0x0001) - 2f32.powi(-24)).abs() < 1e-12);
// Inf/NaN。
assert!(f16_to_f32(0x7C00).is_infinite() && f16_to_f32(0x7C00) > 0.0);
assert!(f16_to_f32(0xFC00).is_infinite() && f16_to_f32(0xFC00) < 0.0);
assert!(f16_to_f32(0x7E00).is_nan());
}
}