diff --git a/crates/oak-plugin/src/clip.rs b/crates/oak-plugin/src/clip.rs index 69d09f483..e995bbdd5 100644 --- a/crates/oak-plugin/src/clip.rs +++ b/crates/oak-plugin/src/clip.rs @@ -61,33 +61,10 @@ fn components_from_props(props: &PropertySet) -> Option 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) } diff --git a/crates/oak-plugin/src/image.rs b/crates/oak-plugin/src/image.rs index d23921021..ad0546fd8 100644 --- a/crates/oak-plugin/src/image.rs +++ b/crates/oak-plugin/src/image.rs @@ -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 { + 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 { + 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()); + } +} diff --git a/crates/oak-plugin/src/render_driver.rs b/crates/oak-plugin/src/render_driver.rs index 8b9a616f3..1baae1c1a 100644 --- a/crates/oak-plugin/src/render_driver.rs +++ b/crates/oak-plugin/src/render_driver.rs @@ -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 = 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)?; } } }