plugin: render non-F32 OFX plugins by converting in and out
The pipeline is ACEScg + F32 end to end by design; a plugin that does not support F32 must not fail the render — its inputs convert down to the negotiated depth and its output converts back to F32 (the previous "Phase 2 F32 only" error path purple-framed those plugins). - render_driver maps getClipPreferences' output bit depth to Byte/Short/Half/Float, sets it on the input/output clips, allocates the output image at the negotiated depth and converts the result back to F32 for frame assembly (both the CPU path and the GL-failure CPU fallback) - the GL path keeps F32 clip params: GL textures are created in the pipeline format and kOfxOpenGLPropPixelDepth is negotiated separately, so clip props must match what the plugin actually sees - fetch_image converts the decoded input down to the clip's negotiated depth (default F32) - new Image::convert_depth ([0,1]-normalized conversion between all depth pairs) plus round-trip and f16 edge tests; f16 helpers moved into image.rs and shared with clip.rs
This commit is contained in:
@@ -61,33 +61,10 @@ fn components_from_props(props: &PropertySet) -> Option<crate::image::Components
|
||||
}
|
||||
}
|
||||
|
||||
/// IEEE 754 半精度 → 单精度(无 half 依赖,手写位转换;非规格数/
|
||||
/// Inf/NaN 均按标准展开)。
|
||||
/// IEEE 754 半精度 → 单精度([`crate::image::f16_to_f32`] 的本地别名,
|
||||
/// 保持调用点可读)。
|
||||
fn f16_to_f32(bits: u16) -> f32 {
|
||||
let sign = ((bits >> 15) & 1) as u32;
|
||||
let exp = ((bits >> 10) & 0x1f) as u32;
|
||||
let mant = (bits & 0x3ff) as u32;
|
||||
let f32_bits = if exp == 0 {
|
||||
if mant == 0 {
|
||||
sign << 31
|
||||
} else {
|
||||
// 非规格数:规格化到 f32 指数域。
|
||||
let mut m = mant;
|
||||
let mut e: i32 = 127 - 15;
|
||||
while m & 0x400 == 0 {
|
||||
m <<= 1;
|
||||
e -= 1;
|
||||
}
|
||||
let m = (m & 0x3ff) << 13;
|
||||
(sign << 31) | (((e + 1) as u32) << 23) | m
|
||||
}
|
||||
} else if exp == 0x1f {
|
||||
// Inf/NaN。
|
||||
(sign << 31) | (0xff << 23) | (mant << 13)
|
||||
} else {
|
||||
(sign << 31) | ((exp + 127 - 15) << 23) | (mant << 13)
|
||||
};
|
||||
f32::from_bits(f32_bits)
|
||||
crate::image::f16_to_f32(bits)
|
||||
}
|
||||
|
||||
impl ClipInstance {
|
||||
@@ -326,6 +303,23 @@ impl ClipInstance {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,83 @@ impl BitDepth {
|
||||
BitDepth::Float => "OfxBitDepthFloat",
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a kOfxBitDepth* string (the clip-preferences negotiation
|
||||
/// value); `None` for unknown depths.
|
||||
pub(crate) fn from_ofx(s: &str) -> Option<BitDepth> {
|
||||
match s {
|
||||
"OfxBitDepthByte" => Some(BitDepth::Byte),
|
||||
"OfxBitDepthShort" => Some(BitDepth::Short),
|
||||
"OfxBitDepthHalf" => Some(BitDepth::Half),
|
||||
"OfxBitDepthFloat" => Some(BitDepth::Float),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// IEEE 754 half → single (no `half` dependency; subnormals/Inf/NaN
|
||||
/// follow the standard expansion).
|
||||
pub(crate) fn f16_to_f32(bits: u16) -> f32 {
|
||||
let sign = ((bits >> 15) & 0x1) as u32;
|
||||
let exp = ((bits >> 10) & 0x1f) as u32;
|
||||
let mant = (bits & 0x3ff) as u32;
|
||||
let f32_bits = if exp == 0 {
|
||||
if mant == 0 {
|
||||
sign << 31
|
||||
} else {
|
||||
// Subnormal: normalize into the f32 exponent domain.
|
||||
let mut m = mant;
|
||||
let mut e = 127 - 15;
|
||||
while m & 0x400 == 0 {
|
||||
m <<= 1;
|
||||
e -= 1;
|
||||
}
|
||||
let m = (m & 0x3ff) << 13;
|
||||
(sign << 31) | (((e + 1) as u32) << 23) | m
|
||||
}
|
||||
} else if exp == 0x1f {
|
||||
(sign << 31) | (0xff << 23) | (mant << 13)
|
||||
} else {
|
||||
(sign << 31) | ((exp + 127 - 15) << 23) | (mant << 13)
|
||||
};
|
||||
f32::from_bits(f32_bits)
|
||||
}
|
||||
|
||||
/// Single → IEEE 754 half (round-to-nearest-even; overflow → Inf,
|
||||
/// NaN/Inf map per the standard).
|
||||
pub(crate) fn f32_to_f16(value: f32) -> u16 {
|
||||
let bits = value.to_bits();
|
||||
let sign = ((bits >> 16) & 0x8000) as u16;
|
||||
let exp = ((bits >> 23) & 0xff) as i32;
|
||||
let mant = bits & 0x7fffff;
|
||||
if exp == 255 {
|
||||
// Inf/NaN.
|
||||
return sign | 0x7c00 | if mant != 0 { 0x200 } else { 0 };
|
||||
}
|
||||
let e = exp - 127 + 15;
|
||||
if e >= 31 {
|
||||
return sign | 0x7c00; // overflow → Inf
|
||||
}
|
||||
if e <= 0 {
|
||||
// Half subnormal or zero.
|
||||
if e < -10 {
|
||||
return sign;
|
||||
}
|
||||
let mant = mant | 0x800000;
|
||||
let shift = (14 - e) as u32;
|
||||
let mut half = (mant >> shift) as u16;
|
||||
let halfway = 1u32 << (shift - 1);
|
||||
if mant & halfway != 0 && ((half & 1) == 1 || mant & (halfway - 1) != 0) {
|
||||
half += 1;
|
||||
}
|
||||
return sign | half;
|
||||
}
|
||||
let mut half = ((e as u16) << 10) | ((mant >> 13) as u16);
|
||||
let rem = mant & 0x1fff;
|
||||
if rem > 0x1000 || (rem == 0x1000 && (half & 1) == 1) {
|
||||
half += 1;
|
||||
}
|
||||
sign | half
|
||||
}
|
||||
|
||||
/// Component layout (OFX kOfxImageComponent*).
|
||||
@@ -274,6 +351,55 @@ impl Image {
|
||||
pub fn row_bytes(&self) -> usize {
|
||||
self.row_bytes
|
||||
}
|
||||
|
||||
/// Convert the pixel buffer to another bit depth. The pipeline's
|
||||
/// working format is ACEScg + F32 end to end; an OFX plugin that
|
||||
/// negotiates Byte/Short/Half gets its inputs converted down before
|
||||
/// the render action and its output converted back afterwards
|
||||
/// (values are [0,1]-normalized across depths; same-depth calls just
|
||||
/// re-allocate and copy).
|
||||
pub(crate) fn convert_depth(&self, depth: BitDepth) -> Image {
|
||||
let mut out = Image::allocate(depth, self.components, self.bounds);
|
||||
if depth == self.depth {
|
||||
out.data.copy_from_slice(&self.data);
|
||||
return out;
|
||||
}
|
||||
let sb = self.depth.bytes_per_component();
|
||||
let n = self.data.len() / sb;
|
||||
let read = |i: usize| -> f32 {
|
||||
let o = i * sb;
|
||||
match self.depth {
|
||||
BitDepth::Byte => self.data[o] as f32 / 255.0,
|
||||
BitDepth::Short | BitDepth::Half => {
|
||||
let bits = u16::from_le_bytes([self.data[o], self.data[o + 1]]);
|
||||
if self.depth == BitDepth::Half {
|
||||
f16_to_f32(bits)
|
||||
} else {
|
||||
bits as f32 / 65535.0
|
||||
}
|
||||
}
|
||||
BitDepth::Float => f32::from_le_bytes(self.data[o..o + 4].try_into().unwrap()),
|
||||
}
|
||||
};
|
||||
let ob = depth.bytes_per_component();
|
||||
let od = &mut out.data;
|
||||
for i in 0..n {
|
||||
let v = read(i);
|
||||
let o = i * ob;
|
||||
match depth {
|
||||
BitDepth::Byte => od[o] = (v.clamp(0.0, 1.0) * 255.0).round() as u8,
|
||||
BitDepth::Short => {
|
||||
let q = (v.clamp(0.0, 1.0) * 65535.0).round() as u16;
|
||||
od[o..o + 2].copy_from_slice(&q.to_le_bytes());
|
||||
}
|
||||
BitDepth::Half => {
|
||||
od[o..o + 2].copy_from_slice(&f32_to_f16(v).to_le_bytes());
|
||||
}
|
||||
BitDepth::Float => od[o..o + 4].copy_from_slice(&v.to_le_bytes()),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// Unique identifier string (monotonically increasing per process,
|
||||
@@ -284,3 +410,79 @@ pub(crate) fn unique_identifier() -> CString {
|
||||
// Hexadecimal ASCII, no NUL; unwrap cannot fail.
|
||||
CString::new(format!("{:x}", n)).unwrap()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn f32_image(values: &[f32]) -> Image {
|
||||
let mut img = Image::allocate(
|
||||
BitDepth::Float,
|
||||
Components::Alpha,
|
||||
crate::instance::OfxRectD {
|
||||
x1: 0.0,
|
||||
y1: 0.0,
|
||||
x2: values.len() as f64,
|
||||
y2: 1.0,
|
||||
},
|
||||
);
|
||||
for (i, v) in values.iter().enumerate() {
|
||||
img.pixels_mut()[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
img
|
||||
}
|
||||
|
||||
fn samples(img: &Image) -> Vec<f32> {
|
||||
let f = img.convert_depth(BitDepth::Float);
|
||||
f.pixels()
|
||||
.chunks_exact(4)
|
||||
.map(|c| f32::from_le_bytes(c.try_into().unwrap()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// F32 → U8/U16/F16 → F32 的往返保持 [0,1] 归一化语义(OFX 低位深
|
||||
/// 协商插件的输入转低、输出转回路径)。
|
||||
#[test]
|
||||
fn convert_depth_roundtrips() {
|
||||
let src = f32_image(&[0.0, 0.25, 0.5, 1.0]);
|
||||
|
||||
let u8img = src.convert_depth(BitDepth::Byte);
|
||||
assert_eq!(u8img.depth(), BitDepth::Byte);
|
||||
assert_eq!(u8img.pixels(), &[0, 64, 128, 255]);
|
||||
let back = samples(&u8img);
|
||||
for (a, b) in back.iter().zip([0.0, 0.25, 0.5, 1.0]) {
|
||||
assert!((a - b).abs() < 0.003, "u8 roundtrip: {a} vs {b}");
|
||||
}
|
||||
|
||||
let u16img = src.convert_depth(BitDepth::Short);
|
||||
assert_eq!(u16img.depth(), BitDepth::Short);
|
||||
let back = samples(&u16img);
|
||||
for (a, b) in back.iter().zip([0.0, 0.25, 0.5, 1.0]) {
|
||||
assert!((a - b).abs() < 0.0001, "u16 roundtrip: {a} vs {b}");
|
||||
}
|
||||
|
||||
let f16img = src.convert_depth(BitDepth::Half);
|
||||
assert_eq!(f16img.depth(), BitDepth::Half);
|
||||
let back = samples(&f16img);
|
||||
for (a, b) in back.iter().zip([0.0, 0.25, 0.5, 1.0]) {
|
||||
assert!((a - b).abs() < 0.001, "f16 roundtrip: {a} vs {b}");
|
||||
}
|
||||
|
||||
// 同深度 = 重新分配 + 拷贝(props 的数据指针必须指向新缓冲)。
|
||||
let same = src.convert_depth(BitDepth::Float);
|
||||
assert_eq!(same.pixels(), src.pixels());
|
||||
assert!(!std::ptr::eq(same.pixels().as_ptr(), src.pixels().as_ptr()));
|
||||
}
|
||||
|
||||
/// f16 转换的边界:零/次规格数/Inf/NaN。
|
||||
#[test]
|
||||
fn f16_edges() {
|
||||
assert_eq!(f32_to_f16(0.0), 0);
|
||||
assert_eq!(f16_to_f32(0), 0.0);
|
||||
assert!(f16_to_f32(f32_to_f16(1.0)) == 1.0);
|
||||
assert!(f16_to_f32(f32_to_f16(f32::INFINITY)).is_infinite());
|
||||
assert!(f16_to_f32(f32_to_f16(f32::NAN)).is_nan());
|
||||
// 超 half 范围 → Inf。
|
||||
assert!(f16_to_f32(f32_to_f16(1e10)).is_infinite());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,9 +222,24 @@ pub fn render_frame(
|
||||
"OfxImageComponentAlpha" => crate::image::Components::Alpha,
|
||||
_ => return Err(Error::Failed("协商分量未知".into())),
|
||||
};
|
||||
if prefs.output_bit_depth != "OfxBitDepthFloat" {
|
||||
return Err(Error::Failed("Phase 2 仅支持 F32 输出".into()));
|
||||
}
|
||||
// 位深协商(设计约束:管线全链路 ACEScg + F32;插件不支持 F32
|
||||
// 时按插件协商的位深渲染——输入图像转低、输出转回 F32,而不是
|
||||
// 直接失败)。GL 路径强制 F32:GL 纹理按管线格式创建,位深由
|
||||
// kOfxOpenGLPropPixelDepth 另行协商,clip props 必须与插件实际
|
||||
// 见到的图像一致。olive 像素格式号:0=u8, 2=u16, 3=f16, 4=f32。
|
||||
let (depth, depth_format) = if use_opengl {
|
||||
(crate::image::BitDepth::Float, 4)
|
||||
} else {
|
||||
let d = crate::image::BitDepth::from_ofx(&prefs.output_bit_depth)
|
||||
.ok_or_else(|| Error::Failed(format!("协商位深未知:{}", prefs.output_bit_depth)))?;
|
||||
let f = match d {
|
||||
crate::image::BitDepth::Byte => 0,
|
||||
crate::image::BitDepth::Short => 2,
|
||||
crate::image::BitDepth::Half => 3,
|
||||
crate::image::BitDepth::Float => 4,
|
||||
};
|
||||
(d, f)
|
||||
};
|
||||
|
||||
// 5. 输出 clip:RoD + 输出纹理挂接(pluginrenderer.cpp:1603-1606)。
|
||||
let output_clip = inst
|
||||
@@ -235,15 +250,15 @@ pub fn render_frame(
|
||||
output_clip.set_region_of_definition(region_of_interest, job.time);
|
||||
output_clip.set_output_texture(Some(dst.clone()), job.time);
|
||||
|
||||
// 6. 输入 clip:RoD 与格式(pluginrenderer.cpp:1627-1665;Phase 2
|
||||
// 全链路 F32 → 格式选择恒等,无转换路径)。
|
||||
// 6. 输入 clip:RoD 与格式(pluginrenderer.cpp:1627-1665;位深按
|
||||
// 协商结果——非 F32 插件的输入图像在 fetch 时转低)。
|
||||
for clip in &inst.clips {
|
||||
if clip.name == "Output" {
|
||||
continue;
|
||||
}
|
||||
if pick_input(&clip.name, job).map_or(false, |t| usable(&t)) {
|
||||
clip.set_region_of_definition(region_of_interest, job.time);
|
||||
clip.set_video_params(render::PIXEL_FORMAT_F32, 4);
|
||||
clip.set_video_params(depth_format, 4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,8 +269,8 @@ pub fn render_frame(
|
||||
.get_regions_of_interest(job.time, RenderScale { x: 1.0, y: 1.0 }, region_of_interest)
|
||||
.unwrap_or_else(|_| inst.clips.iter().map(|_| region_of_interest).collect());
|
||||
|
||||
// 8. 输出 clip 格式(pluginrenderer.cpp:1686-1697)。
|
||||
output_clip.set_video_params(render::PIXEL_FORMAT_F32, components.channel_count() as i32);
|
||||
// 8. 输出 clip 格式(pluginrenderer.cpp:1686-1697;位深按协商)。
|
||||
output_clip.set_video_params(depth_format, components.channel_count() as i32);
|
||||
|
||||
// 渲染窗口(像素坐标;pluginrenderer.cpp:1699-1704)。
|
||||
let render_window = OfxRectD {
|
||||
@@ -278,7 +293,7 @@ pub fn render_frame(
|
||||
// 11. render action。
|
||||
if !use_opengl {
|
||||
let output = std::sync::Arc::new(Image::allocate(
|
||||
crate::image::BitDepth::Float,
|
||||
depth,
|
||||
components,
|
||||
OfxRectD {
|
||||
x1: 0.0,
|
||||
@@ -293,7 +308,7 @@ pub fn render_frame(
|
||||
render_window,
|
||||
output.clone(),
|
||||
)?;
|
||||
if std::env::var_os("OAK_OFX_TRACE").is_some() {
|
||||
if std::env::var_os("OAK_OFX_TRACE").is_some() && depth == crate::image::BitDepth::Float {
|
||||
let p = output.pixels();
|
||||
let dump: Vec<f32> = p
|
||||
.chunks_exact(4)
|
||||
@@ -302,8 +317,16 @@ pub fn render_frame(
|
||||
.collect();
|
||||
eprintln!("[ofx] render_frame: output buffer at {:p}, pixels[0..4] = {dump:?}", p.as_ptr());
|
||||
}
|
||||
// 输出装配(pluginrenderer.cpp:1762-1834 的 CPU 路径)。
|
||||
write_output_frame(&mut dst, &output)?;
|
||||
// 输出装配(pluginrenderer.cpp:1762-1834 的 CPU 路径):插件
|
||||
// 协商了低位深时先转回管线工作格式(全链路 F32/ACEScg)。
|
||||
let f32_output;
|
||||
let output_f32 = if depth == crate::image::BitDepth::Float {
|
||||
&*output
|
||||
} else {
|
||||
f32_output = output.convert_depth(crate::image::BitDepth::Float);
|
||||
&f32_output
|
||||
};
|
||||
write_output_frame(&mut dst, output_f32)?;
|
||||
} else {
|
||||
// GL 路径(方案 B,见 [`crate::gl_bridge`]):宿主自建离屏
|
||||
// 上下文,为输出帧建 GL 纹理 + FBO(插件直接画进附着的输出
|
||||
@@ -324,7 +347,7 @@ pub fn render_frame(
|
||||
Err(gl_err) => {
|
||||
eprintln!("[PLUGIN] GL 渲染失败,回退 CPU:{gl_err}");
|
||||
let output = std::sync::Arc::new(Image::allocate(
|
||||
crate::image::BitDepth::Float,
|
||||
depth,
|
||||
components,
|
||||
OfxRectD {
|
||||
x1: 0.0,
|
||||
@@ -339,7 +362,15 @@ pub fn render_frame(
|
||||
render_window,
|
||||
output.clone(),
|
||||
)?;
|
||||
write_output_frame(&mut dst, &output)?;
|
||||
// 同主 CPU 路径:低位深协商的输出先转回 F32 再装帧。
|
||||
let f32_output;
|
||||
let output_f32 = if depth == crate::image::BitDepth::Float {
|
||||
&*output
|
||||
} else {
|
||||
f32_output = output.convert_depth(crate::image::BitDepth::Float);
|
||||
&f32_output
|
||||
};
|
||||
write_output_frame(&mut dst, output_f32)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user