Files
oak-editor/crates/oakcodec/src/oiioframebridge.rs
T
Mike-Solar 5afa95c80e feat(render,app): footage decode lands (M12 P0) + UI density pass
engine:
- oakrender eval footage hook decodes via oakcodec (JobSpec::Footage
  carries filename/stream); ticket/ffi/manager wiring, real-media
  decode test with programmatically generated MPEG-2
- oakrender bridge/codec.rs + node.rs: direct oakcodec/oaknode calls;
  the crate's dlsym module is gone (project_deep_copy/sync_copy remain
  documented always-fail stubs — never implemented in oaknode)
- oakaudio waveform/decoder path adjustments for the decode hook

app (gpui + gpui_widgets):
- menu bar scrubbing: hovering another top-level title while a menu is
  open switches to it; popup width is content-aware (CJK-aware) instead
  of fixed 160px
- density pass: window rem 16 -> 14px, menu rows 26 -> 22px, dock tabs
  32 -> 26px, viewer transport tightened
- open/import/save-as use the native platform file dialogs
  (prompt_for_paths / prompt_for_new_path; multi-select import);
  MockEngine records imported footage for tests
- project explorer Tree/Icons toggle is localized (explorer.tree /
  explorer.icons widget keys)
2026-08-13 18:16:10 +08:00

426 lines
14 KiB
Rust

// 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/>.
//! `oiio_frame_to_buffer` / `oiio_buffer_to_frame` — codec-internal OIIO
//! frame <-> pixel-buffer conversion.
//!
//! Mirrors `src/codec/src/oiioframebridge.{h,cpp}`. These are internal C++
//! functions that moved into codec from oakcommon (NOTES.md §oakcommon侧修复);
//! oakcommon keeps its OIIO mapping functions; the frame conversion itself
//! lives here.
//!
//! The C++ bridge copies pixels through the live OpenImageIO `ImageBuf`
//! (`oiio_frame_to_buffer`/`oiio_buffer_to_frame`). OIIO is not linked into
//! this build, so the port serializes the frame into a self-describing byte
//! buffer instead. The layout is stable (documented in
//! [`OiioBufferHeader`]); it carries the frame's geometry, pixel format,
//! timestamp and time base alongside the raw pixel rows, so a buffer can be
//! turned back into an equivalent [`Frame`] without any external state.
use crate::bridge::common::{
oakcommon_videoparams_get_time_base, oakcommon_videoparams_init_with_time_base,
oakcommon_videoparams_set_format,
};
use crate::frame::Frame;
use oakcore_rs::Rational;
use std::ffi::c_int;
/// Fixed header size, in bytes, of an OIIO frame buffer.
///
/// The layout is a 64-byte little-endian header followed by the raw pixel
/// data. `OiioBufferHeader::to_bytes` / `from_bytes` are the single writer /
/// reader of this header, so the exact offsets only ever exist in one place.
const HEADER_LEN: usize = 64;
/// Magic bytes identifying an OIIO frame buffer (`"OFMB"`).
const MAGIC: &[u8; 4] = b"OFMB";
/// Current serialization version.
const VERSION: u32 = 1;
/// Decoded OIIO frame-buffer header.
///
/// # Byte layout (little-endian, `HEADER_LEN` = 64 bytes)
///
/// | Offset | Size | Field |
/// |--------|------|-------|
/// | 0 | 4 | Magic bytes `"OFMB"` |
/// | 4 | 4 | Serialization `version` (`u32`, currently 1) |
/// | 8 | 4 | `width` (`i32`) |
/// | 12 | 4 | `height` (`i32`) |
/// | 16 | 4 | `format` (`i32`, an `OakPixelFormat` value) |
/// | 20 | 4 | `linesize_bytes` (`i32`, distance between pixel rows) |
/// | 24 | 8 | `timestamp_num` (`i64`) |
/// | 32 | 8 | `timestamp_den` (`i64`) |
/// | 40 | 8 | `time_base_num` (`i64`) |
/// | 48 | 8 | `time_base_den` (`i64`) |
/// | 56 | 8 | `pixel_len` (`u64`, pixel-data length in bytes) |
/// | 64 | … | raw pixel data (`pixel_len` bytes) |
///
/// `pixel_len` must equal `linesize_bytes * height`; the frame geometry in the
/// header must match the params the frame is reconstructed with.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct OiioBufferHeader {
version: u32,
width: i32,
height: i32,
format: i32,
linesize_bytes: i32,
timestamp_num: i64,
timestamp_den: i64,
time_base_num: i64,
time_base_den: i64,
pixel_len: u64,
}
impl OiioBufferHeader {
/// Serialize the header into exactly `HEADER_LEN` bytes (little-endian).
fn to_bytes(&self) -> [u8; HEADER_LEN] {
let mut b = [0u8; HEADER_LEN];
b[0..4].copy_from_slice(MAGIC);
b[4..8].copy_from_slice(&self.version.to_le_bytes());
b[8..12].copy_from_slice(&self.width.to_le_bytes());
b[12..16].copy_from_slice(&self.height.to_le_bytes());
b[16..20].copy_from_slice(&self.format.to_le_bytes());
b[20..24].copy_from_slice(&self.linesize_bytes.to_le_bytes());
b[24..32].copy_from_slice(&self.timestamp_num.to_le_bytes());
b[32..40].copy_from_slice(&self.timestamp_den.to_le_bytes());
b[40..48].copy_from_slice(&self.time_base_num.to_le_bytes());
b[48..56].copy_from_slice(&self.time_base_den.to_le_bytes());
b[56..64].copy_from_slice(&self.pixel_len.to_le_bytes());
b
}
/// Parse a header from the start of `bytes`, validating the magic and
/// version and requiring at least `HEADER_LEN` bytes. Returns the decoded
/// header and the number of bytes consumed (`HEADER_LEN`).
fn from_bytes(bytes: &[u8]) -> crate::error::Result<(Self, usize)> {
if bytes.len() < HEADER_LEN {
return Err(crate::error::Error::Invalid);
}
if &bytes[0..4] != MAGIC {
return Err(crate::error::Error::Invalid);
}
let version = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
if version != VERSION {
return Err(crate::error::Error::Invalid);
}
let header = OiioBufferHeader {
version,
width: i32::from_le_bytes(bytes[8..12].try_into().unwrap()),
height: i32::from_le_bytes(bytes[12..16].try_into().unwrap()),
format: i32::from_le_bytes(bytes[16..20].try_into().unwrap()),
linesize_bytes: i32::from_le_bytes(bytes[20..24].try_into().unwrap()),
timestamp_num: i64::from_le_bytes(bytes[24..32].try_into().unwrap()),
timestamp_den: i64::from_le_bytes(bytes[32..40].try_into().unwrap()),
time_base_num: i64::from_le_bytes(bytes[40..48].try_into().unwrap()),
time_base_den: i64::from_le_bytes(bytes[48..56].try_into().unwrap()),
pixel_len: u64::from_le_bytes(bytes[56..64].try_into().unwrap()),
};
Ok((header, HEADER_LEN))
}
}
/// Convert an OIIO-backed `Frame` into a raw pixel buffer.
///
/// # CPP-PARITY
/// `src/codec/src/oiioframebridge.cpp` `oiio_frame_to_buffer` —
/// allocates the destination and copies the OIIO pixel data out.
///
/// The buffer is a 64-byte [`OiioBufferHeader`] followed by the frame's pixel
/// rows (see the header docs for the exact layout). The frame must already be
/// allocated ([`Frame::allocate`]); otherwise this returns
/// [`crate::error::Error::State`].
pub fn oiio_frame_to_buffer(frame: &Frame) -> crate::error::Result<Vec<u8>> {
let data = frame.data().ok_or(crate::error::Error::State)?;
let width = frame.width();
let height = frame.height();
let format = frame.format() as i32;
let linesize_bytes = frame.linesize_bytes();
let timestamp = frame.timestamp();
let (time_base_num, time_base_den) = match frame.params() {
Some(p) => {
let mut num = 0i64;
let mut den = 0i64;
// SAFETY: `num`/`den` are live mutable i64s and `p` is a valid
// handle; the C function only writes through the two out pointers.
unsafe {
oakcommon_videoparams_get_time_base(p.clone(), &mut num, &mut den);
}
(num, den)
}
None => (0, 0),
};
let header = OiioBufferHeader {
version: VERSION,
width,
height,
format,
linesize_bytes,
timestamp_num: timestamp.numerator(),
timestamp_den: timestamp.denominator(),
time_base_num,
time_base_den,
pixel_len: data.len() as u64,
};
let mut out = Vec::with_capacity(HEADER_LEN + data.len());
out.extend_from_slice(&header.to_bytes());
out.extend_from_slice(data);
Ok(out)
}
/// Convert a raw pixel buffer back into a `Frame`.
///
/// # CPP-PARITY
/// `src/codec/src/oiioframebridge.cpp` `oiio_buffer_to_frame` —
/// wraps the buffer bytes in a `Frame` for encoder consumption.
///
/// Parses a [`OiioBufferHeader`] from the front of `buffer`, reconstructs the
/// frame's params (geometry, time base, pixel format) and timestamp, then
/// copies the pixel rows into an allocated frame. The buffer is rejected with
/// [`crate::error::Error::Invalid`] when the magic/version is wrong, the
/// header is truncated, or `pixel_len` is inconsistent with the declared
/// `linesize_bytes * height` / the bytes actually present.
pub fn oiio_buffer_to_frame(buffer: &[u8]) -> crate::error::Result<Frame> {
let (header, consumed) = OiioBufferHeader::from_bytes(buffer)?;
let pixels = &buffer[consumed..];
// `pixel_len` must be consistent with the header geometry and the bytes
// actually present (checked, so a corrupt header can't panic later).
if header.linesize_bytes < 0 || header.height < 0 {
return Err(crate::error::Error::Invalid);
}
let expected = (header.linesize_bytes as u64).checked_mul(header.height as u64);
if expected != Some(header.pixel_len) {
return Err(crate::error::Error::Invalid);
}
if pixels.len() != header.pixel_len as usize {
return Err(crate::error::Error::Invalid);
}
// Build the params from the header, then hand ownership to the frame.
// SAFETY: the init returns a live handle; the clone for `set_format` is
// only read, and the original is moved into `Frame::with_params` (which
// takes ownership), so there is no double release.
let params = unsafe {
oakcommon_videoparams_init_with_time_base(
header.width,
header.height,
header.time_base_num as c_int,
header.time_base_den as c_int,
0,
4,
1,
1,
0,
1,
)
};
unsafe {
oakcommon_videoparams_set_format(params.clone(), header.format);
}
let mut frame = Frame::with_params(params);
frame.set_timestamp(Rational::new(header.timestamp_num, header.timestamp_den));
frame.allocate()?;
match frame.data_mut() {
Some(dst) if dst.len() == pixels.len() => {
dst.copy_from_slice(pixels);
Ok(frame)
}
// Reconstructed params should produce exactly `pixel_len` bytes; if
// the geometry in the header disagreed with the format's line size,
// refuse rather than copy a mismatched slice.
_ => Err(crate::error::Error::Invalid),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn header_round_trip() {
let header = OiioBufferHeader {
version: VERSION,
width: 100,
height: 50,
format: 0, // U8
linesize_bytes: 512,
timestamp_num: 5,
timestamp_den: 2,
time_base_num: 1,
time_base_den: 30,
pixel_len: 25600,
};
let bytes = header.to_bytes();
assert_eq!(bytes.len(), HEADER_LEN);
let (decoded, consumed) = OiioBufferHeader::from_bytes(&bytes).unwrap();
assert_eq!(consumed, HEADER_LEN);
assert_eq!(decoded, header);
}
#[test]
fn header_is_little_endian_magic_and_version() {
let header = OiioBufferHeader {
version: VERSION,
width: 1,
height: 1,
format: 0,
linesize_bytes: 4,
timestamp_num: 0,
timestamp_den: 1,
time_base_num: 0,
time_base_den: 0,
pixel_len: 4,
};
let bytes = header.to_bytes();
assert_eq!(&bytes[0..4], b"OFMB");
assert_eq!(u32::from_le_bytes(bytes[4..8].try_into().unwrap()), 1);
// width = 1 -> little-endian 01 00 00 00.
assert_eq!(&bytes[8..12], &[1, 0, 0, 0]);
}
#[test]
fn from_bytes_rejects_truncated() {
let header = OiioBufferHeader {
version: VERSION,
width: 1,
height: 1,
format: 0,
linesize_bytes: 4,
timestamp_num: 0,
timestamp_den: 1,
time_base_num: 0,
time_base_den: 0,
pixel_len: 4,
};
let bytes = header.to_bytes();
assert!(OiioBufferHeader::from_bytes(&bytes[..HEADER_LEN - 1]).is_err());
}
#[test]
fn from_bytes_rejects_bad_magic_and_version() {
let mut bytes = OiioBufferHeader {
version: VERSION,
width: 1,
height: 1,
format: 0,
linesize_bytes: 4,
timestamp_num: 0,
timestamp_den: 1,
time_base_num: 0,
time_base_den: 0,
pixel_len: 4,
}
.to_bytes();
bytes[0] = b'X';
assert!(OiioBufferHeader::from_bytes(&bytes).is_err());
let mut bytes = OiioBufferHeader {
version: VERSION,
width: 1,
height: 1,
format: 0,
linesize_bytes: 4,
timestamp_num: 0,
timestamp_den: 1,
time_base_num: 0,
time_base_den: 0,
pixel_len: 4,
}
.to_bytes();
bytes[4..8].copy_from_slice(&99u32.to_le_bytes());
assert!(OiioBufferHeader::from_bytes(&bytes).is_err());
}
/// A small helper to build a fully allocated, filled frame using the same
/// test-stub params pattern as `frame.rs`.
fn make_frame() -> Frame {
// SAFETY: test-stub videoparams; ownership moves into `with_params`.
let params = unsafe { oakcommon_videoparams_init_with_time_base(100, 50, 1, 30, 0, 4, 1, 1, 0, 1) };
unsafe { oakcommon_videoparams_set_format(params.clone(), 0) }; // U8
let mut frame = Frame::with_params(params);
frame.set_timestamp(Rational::new(5, 2));
frame.allocate().unwrap();
frame
}
#[test]
fn frame_to_buffer_round_trip() {
let mut frame = make_frame();
if let Some(d) = frame.data_mut() {
d.fill(0xAB);
}
let buffer = oiio_frame_to_buffer(&frame).unwrap();
assert_eq!(buffer.len(), HEADER_LEN + frame.allocated_size());
let (header, consumed) = OiioBufferHeader::from_bytes(&buffer).unwrap();
assert_eq!(consumed, HEADER_LEN);
assert_eq!(header.width, 100);
assert_eq!(header.height, 50);
assert_eq!(header.format, 0);
assert_eq!(header.linesize_bytes, 512);
assert_eq!(header.timestamp_num, 5);
assert_eq!(header.timestamp_den, 2);
assert_eq!(header.time_base_num, 1);
assert_eq!(header.time_base_den, 30);
assert_eq!(header.pixel_len, frame.allocated_size() as u64);
}
#[test]
fn buffer_to_frame_round_trip() {
let mut frame = make_frame();
let expected_len = frame.allocated_size();
if let Some(d) = frame.data_mut() {
for (i, b) in d.iter_mut().enumerate() {
*b = (i % 256) as u8;
}
}
let buffer = oiio_frame_to_buffer(&frame).unwrap();
let out = oiio_buffer_to_frame(&buffer).unwrap();
assert_eq!(out.width(), 100);
assert_eq!(out.height(), 50);
assert_eq!(out.format() as i32, 0);
assert_eq!(out.allocated_size(), expected_len);
assert_eq!(out.timestamp().numerator(), 5);
assert_eq!(out.timestamp().denominator(), 2);
assert_eq!(out.data().unwrap(), frame.data().unwrap());
}
#[test]
fn buffer_to_frame_rejects_corrupt() {
let frame = make_frame();
let buffer = oiio_frame_to_buffer(&frame).unwrap();
// Truncated pixel data.
assert!(oiio_buffer_to_frame(&buffer[..HEADER_LEN + 1]).is_err());
// Bad magic.
let mut bad = buffer.clone();
bad[0] = 0;
assert!(oiio_buffer_to_frame(&bad).is_err());
// pixel_len inconsistent with linesize * height.
let mut bad = buffer.clone();
bad[56..64].copy_from_slice(&(999u64).to_le_bytes());
assert!(oiio_buffer_to_frame(&bad).is_err());
}
}