Files
oak-editor/crates/oak-plugin/src/image.rs
T
Mike-Solar 287a17399b
CI / Build & test (Linux) (push) Successful in 18m49s
CI / Build & test (Windows) (push) Successful in 32m47s
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
2026-08-26 01:31:35 +08:00

489 lines
15 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/>.
//! OFX image: an OFX view of a frame buffer (CPU path).
//!
//! Counterpart of the C++ `OliveImage`. Pixel memory is owned by this
//! crate (this was historically a hotspot of memory bugs: ownership
//! must be single, and lifetime is guaranteed by this type).
//! GL texture views are deferred to phase 2 (`// [P2]`).
//!
//! Property writes mirror the C++ `Image::allocate`
//! (image.cpp:132-172) and the HostSupport image property table
//! (HS: ofxhClip.cpp:458-472): Data/RowBytes/Bounds/
//! RegionOfDefinition/Components/PixelDepth/UniqueIdentifier.
//! This struct does not distinguish bounds from ROD (unified), and
//! does not model field/renderScale/pixelAspect/premultiplication
//! (`// [P2]`).
use std::ffi::{c_void, CString};
use std::sync::atomic::{AtomicU64, Ordering};
use crate::instance::OfxRectD;
use crate::property::{PropertySet, Value};
// ---- OFX property names (macro strings of ofxCore.h / ofxImageEffect.h) ----
/// kOfxImagePropData (ofxCore.h:1275): pixel data pointer.
pub(crate) const K_IMAGE_PROP_DATA: &str = "OfxImagePropData";
/// kOfxImagePropRowBytes (ofxCore.h:1322): row byte count.
pub(crate) const K_IMAGE_PROP_ROW_BYTES: &str = "OfxImagePropRowBytes";
/// kOfxImagePropBounds (ofxCore.h:1291): pixel coordinates, Int x 4.
pub(crate) const K_IMAGE_PROP_BOUNDS: &str = "OfxImagePropBounds";
/// kOfxImagePropRegionOfDefinition (ofxCore.h:1307): pixel coordinates, Int x 4.
pub(crate) const K_IMAGE_PROP_ROD: &str = "OfxImagePropRegionOfDefinition";
/// kOfxImageEffectPropComponents (ofxImageEffect.h:915).
pub(crate) const K_IMAGE_EFFECT_PROP_COMPONENTS: &str = "OfxImageEffectPropComponents";
/// kOfxImageEffectPropPixelDepth (ofxImageEffect.h:901).
pub(crate) const K_IMAGE_EFFECT_PROP_PIXEL_DEPTH: &str = "OfxImageEffectPropPixelDepth";
/// kOfxImagePropUniqueIdentifier (ofxCore.h:927): host-assigned unique id.
pub(crate) const K_IMAGE_PROP_UNIQUE_ID: &str = "OfxImagePropUniqueIdentifier";
/// Process-wide monotonically increasing id (zero-dependency
/// replacement for HostSupport's UUID generation).
static NEXT_IMAGE_ID: AtomicU64 = AtomicU64::new(0);
/// Pixel bit depth (OFX kOfxBitDepth*; the full pipeline only uses
/// Float, the rest are kept for compatibility).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BitDepth {
/// 8-bit integer (compat).
Byte,
/// 16-bit integer (compat).
Short,
/// 16-bit half float (compat).
Half,
/// 32-bit float (main path).
Float,
}
impl BitDepth {
/// Bytes per component.
pub(crate) fn bytes_per_component(self) -> usize {
match self {
BitDepth::Byte => 1,
BitDepth::Short | BitDepth::Half => 2,
BitDepth::Float => 4,
}
}
/// OFX bit depth string (kOfxBitDepth*, ofxCore.h:866-880).
pub(crate) fn to_ofx(self) -> &'static str {
match self {
BitDepth::Byte => "OfxBitDepthByte",
BitDepth::Short => "OfxBitDepthShort",
BitDepth::Half => "OfxBitDepthHalf",
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*).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Components {
/// RGBA.
Rgba,
/// RGB.
Rgb,
/// Single-channel Alpha.
Alpha,
}
impl Components {
/// Channel count.
pub(crate) fn channel_count(self) -> usize {
match self {
Components::Rgba => 4,
Components::Rgb => 3,
Components::Alpha => 1,
}
}
/// OFX component string (kOfxImageComponent*, ofxImageEffect.h:46-55).
pub(crate) fn to_ofx(self) -> &'static str {
match self {
Components::Rgba => "OfxImageComponentRGBA",
Components::Rgb => "OfxImageComponentRGB",
Components::Alpha => "OfxImageComponentAlpha",
}
}
}
/// A single frame. `data` is row-major; the row stride may be padded
/// for alignment; bounds are in pixel coordinates.
/// `#[repr(C)]` with `props` at offset 0 (handle convention,
/// see [`crate::suites::tag`]).
#[repr(C)]
pub struct Image {
/// Image-level properties (bounds, row bytes, depth, components,
/// unique identifier).
pub props: PropertySet,
/// Pixel buffer (length = row_bytes * height).
data: Vec<u8>,
/// Bit depth.
depth: BitDepth,
/// Components.
components: Components,
/// Pixel bounds.
bounds: OfxRectD,
/// Row byte count.
row_bytes: usize,
}
impl Image {
/// Allocate by format (uninitialized pixels).
///
/// Property writes mirror the C++ `Image::allocate`
/// (image.cpp:156-172): Data/RowBytes/Bounds/RegionOfDefinition/
/// Components/PixelDepth; UniqueIdentifier is also written per the
/// image property table (HS: ofxhClip.cpp:470).
/// ROD is unified with bounds (this struct does not distinguish
/// the two; both are pixel coordinates). A zero-size or inverted
/// rectangle yields an empty buffer (mirrors the
/// `buffer_size < 0 -> 0` guard in image.cpp:147-149).
pub fn allocate(depth: BitDepth, components: Components, bounds: OfxRectD) -> Self {
let (w, h) = {
let width = (bounds.x2 - bounds.x1).round();
let height = (bounds.y2 - bounds.y1).round();
if width > 0.0 && height > 0.0 {
(width as usize, height as usize)
} else {
(0, 0)
}
};
let row_bytes = w * components.channel_count() * depth.bytes_per_component();
let mut img = Self {
props: PropertySet::new(),
data: vec![0u8; row_bytes * h],
depth,
components,
bounds,
row_bytes,
};
// `data` is a heap buffer; moving/borrowing the vec does not move
// the buffer, and it is never resized after allocation, so the
// pointer stays valid for the whole Image lifetime (ownership
// discipline: this is the only place that holds the buffer).
img.props.define(
K_IMAGE_PROP_DATA,
vec![Value::Pointer(img.data.as_mut_ptr() as *mut c_void)],
);
img.props
.define(K_IMAGE_PROP_ROW_BYTES, vec![Value::Int(row_bytes as i32)]);
let b = |v: f64| Value::Int(v.round() as i32);
img.props.define(
K_IMAGE_PROP_BOUNDS,
vec![b(bounds.x1), b(bounds.y1), b(bounds.x2), b(bounds.y2)],
);
img.props.define(
K_IMAGE_PROP_ROD,
vec![b(bounds.x1), b(bounds.y1), b(bounds.x2), b(bounds.y2)],
);
img.props.define(
K_IMAGE_EFFECT_PROP_COMPONENTS,
vec![Value::String(CString::new(components.to_ofx()).unwrap())],
);
img.props.define(
K_IMAGE_EFFECT_PROP_PIXEL_DEPTH,
vec![Value::String(CString::new(depth.to_ofx()).unwrap())],
);
img.props.define(
K_IMAGE_PROP_UNIQUE_ID,
vec![Value::String(unique_identifier())],
);
// OfxPropType="OfxTypeImage":图像实例的类型标识(支持库
// validateImageBaseProperties 的必备项,带可校验默认值)。
img.props.define(
"OfxPropType",
vec![Value::String(std::ffi::CString::new("OfxTypeImage").unwrap())],
);
// OFX 必备图像属性(支持库 ImageBase/Image 构造的无默认值强读;
// 缺失即抛 PropertyUnknownToHost → MissingHostFeature 紫帧):
// 方形像素 1.0;预乘声明(本管线按预乘 alpha 处理);无场。
img.props.define(
"OfxImagePropPixelAspectRatio",
vec![Value::Double(1.0)],
);
img.props.define(
"OfxImageEffectPropPreMultiplication",
// kOfxImagePreMultiplied 的真实字符串值是
// "OfxImageAlphaPremultiplied"ofxImageEffect.h),
// ofxs mapStrToPreMultiplicationEnum 只认这三个精确值。
vec![Value::String(
std::ffi::CString::new("OfxImageAlphaPremultiplied").unwrap(),
)],
);
img.props.define(
"OfxImagePropField",
vec![Value::String(std::ffi::CString::new("OfxFieldNone").unwrap())],
);
// OfxImageEffectPropRenderScaleopenfx-misc 的
// checkBadRenderScaleOrField 用它比对渲染参数(1:1)。
img.props.define(
"OfxImageEffectPropRenderScale",
vec![Value::Double(1.0), Value::Double(1.0)],
);
img
}
/// Mutable pixel slice (for writing plugin output). The length is
/// consistent with the format by type construction.
pub fn pixels_mut(&mut self) -> &mut [u8] {
&mut self.data
}
/// Read-only pixel slice.
pub fn pixels(&self) -> &[u8] {
&self.data
}
/// Pixel bounds (canonical coordinates; the Image keeps bounds and
/// ROD unified).
pub fn bounds(&self) -> OfxRectD {
self.bounds
}
/// Bit depth.
pub fn depth(&self) -> BitDepth {
self.depth
}
/// Components.
pub fn components(&self) -> Components {
self.components
}
/// Row byte count.
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,
/// hexadecimal; corresponds to the `uniqueIdentifier` parameter of
/// HS: ofxhClip.cpp:537).
pub(crate) fn unique_identifier() -> CString {
let n = NEXT_IMAGE_ID.fetch_add(1, Ordering::Relaxed);
// 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());
}
}