refactor: drop internal bridge/ffi layers; exporter family lands
Single-lib cleanup: the per-crate src/bridge/ and src/ffi.rs layers are gone (oakundo/oakcommon/oaknode/oaktimeline/oakcodec/oakaudio/ oakrender/oaktask/oakplugin/oakstorage); cross-crate calls are plain Rust, CHandle marshalling shrinks to the oakengine boundary, and tests call the Rust APIs directly (pure C-ABI wrapper tests removed where the domain layer already covers the behavior). exporter.h family implemented: oakengine_export_render (CLI contract), oakengine_export_render_with_params (was a stub), last_error and progress callback; synchronous path reuses task_create_export + start_sync. Fixes on the way: oaktask video ticket self-deadlock, audio params dropped on the export path, codec encoder AAC slicing and H.264 time base. Real-mp4 tests cover both entry points, progress and the illegal-argument matrix. Also: oakstorage session maps null project handles to None (version- info path), configstore test double literal 3.14 -> 3.15 (clippy PI lint), oakaudio output callback scratch buffer + env-aware P1 test, cli media round-trip test uses a generated 16-frame clip (no more minute-long debug runs).
This commit is contained in:
@@ -15,14 +15,7 @@ oakundo = { path = "../oakundo" }
|
||||
oakcodec = { path = "../oakcodec" }
|
||||
thiserror = "2"
|
||||
|
||||
[features]
|
||||
# In-crate stubs for the oakundo C ABI (and other module bridges) so
|
||||
# cargo test can exercise the undoable exports end-to-end without the
|
||||
# module libraries; real builds (feature off) dlsym the actual modules.
|
||||
test-stubs = []
|
||||
|
||||
[dev-dependencies]
|
||||
# oakcodec's oakcore_*/oakrender_* host-mocks (test-stubs feature): the
|
||||
# node test binaries link the real oakcodec (footage probe), whose ffmpeg
|
||||
# unit references those cross-crate symbols that no Rust crate provides.
|
||||
oakcodec = { path = "../oakcodec", features = ["test-stubs"] }
|
||||
# oakcodec is also a plain dependency above; the dev-dependency re-entry
|
||||
# used to enable oakcodec's `test-stubs` host-mocks, which no longer exist
|
||||
# (single-lib unification removed the oakcore_*/oakrender_* C ABI calls).
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! oakcodec C ABI calls (footage probing) — now direct Rust calls into
|
||||
//! the oakcodec crate (single-lib unification, see
|
||||
//! `docs/zh/plans/riir/single-lib.md`).
|
||||
|
||||
use std::ffi::c_char;
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// `oakcodec_decoder_probe` — probe a media file, returning the
|
||||
/// stream-list handle (`oakcodec_decoder_probe(filename)`). The caller
|
||||
/// owns the returned handle.
|
||||
pub fn decoder_probe(path: &str) -> Option<CHandle> {
|
||||
use std::ffi::CString;
|
||||
let c = CString::new(path).ok()?;
|
||||
Some(unsafe { oakcodec::ffi::decoder::oakcodec_decoder_probe(c.as_ptr()) })
|
||||
}
|
||||
@@ -1,424 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! oakcommon C ABI calls — now direct Rust calls into the oakcommon crate
|
||||
//! (single-lib unification, see `docs/zh/plans/riir/single-lib.md`).
|
||||
//! The XML surface mirrors `include/common/xmlutils.h` and backs the
|
||||
//! serializer's reader/writer traits. Function names and signatures are
|
||||
//! unchanged (callers in `src/` and `tests/` are untouched); the
|
||||
//! `test-stubs` feature and its in-crate mocks were removed because the
|
||||
//! real oakcommon rlib is now always linked (the mocks would collide with
|
||||
//! its `#[no_mangle]` exports).
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// `oakcommon_xml_reader_init`.
|
||||
pub fn xml_reader_init(data: *const c_char) -> Option<CHandle> {
|
||||
Some(unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_init(data) })
|
||||
}
|
||||
|
||||
/// `oakcommon_xml_reader_free`.
|
||||
pub fn xml_reader_free(reader: *mut CHandle) {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_free(reader) }
|
||||
}
|
||||
|
||||
/// `oakcommon_xml_reader_read_next_start_element` (advance to the next
|
||||
/// start element; `found` receives 1/0).
|
||||
pub fn xml_reader_next_start_element(reader: CHandle) -> Option<bool> {
|
||||
let mut found = 0;
|
||||
unsafe {
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_reader_read_next_start_element(reader, &mut found);
|
||||
}
|
||||
Some(found != 0)
|
||||
}
|
||||
|
||||
/// `oakcommon_xml_reader_name` (two-stage).
|
||||
pub fn xml_reader_name(reader: CHandle) -> Option<String> {
|
||||
two_stage_string("oakcommon_xml_reader_name", |buf, size| unsafe {
|
||||
Some(oakcommon::ffi::xmlutils::oakcommon_xml_reader_name(
|
||||
reader.clone(),
|
||||
buf,
|
||||
size,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcommon_xml_reader_read_element_text` (two-stage).
|
||||
pub fn xml_reader_read_element_text(reader: CHandle) -> Option<String> {
|
||||
two_stage_string(
|
||||
"oakcommon_xml_reader_read_element_text",
|
||||
|buf, size| unsafe {
|
||||
Some(
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_reader_read_element_text(
|
||||
reader.clone(),
|
||||
buf,
|
||||
size,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// `oakcommon_xml_reader_skip_current_element`.
|
||||
pub fn xml_reader_skip_current_element(reader: CHandle) -> Option<c_int> {
|
||||
Some(unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_skip_current_element(reader) })
|
||||
}
|
||||
|
||||
/// `oakcommon_xml_reader_attribute_count`.
|
||||
pub fn xml_reader_attribute_count(reader: CHandle) -> Option<c_int> {
|
||||
let mut count = 0;
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_count(reader, &mut count) };
|
||||
Some(count)
|
||||
}
|
||||
|
||||
/// `oakcommon_xml_reader_attribute_name` (two-stage).
|
||||
pub fn xml_reader_attribute_name(reader: CHandle, index: c_int) -> Option<String> {
|
||||
two_stage_string("oakcommon_xml_reader_attribute_name", |buf, size| unsafe {
|
||||
Some(
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_name(
|
||||
reader.clone(),
|
||||
index,
|
||||
buf,
|
||||
size,
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcommon_xml_reader_attribute_value` (two-stage).
|
||||
pub fn xml_reader_attribute_value(reader: CHandle, index: c_int) -> Option<String> {
|
||||
two_stage_string("oakcommon_xml_reader_attribute_value", |buf, size| unsafe {
|
||||
Some(
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_value(
|
||||
reader.clone(),
|
||||
index,
|
||||
buf,
|
||||
size,
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcommon_xml_reader_has_error`.
|
||||
pub fn xml_reader_has_error(reader: CHandle) -> Option<bool> {
|
||||
let mut err = 0;
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_has_error(reader, &mut err) };
|
||||
Some(err != 0)
|
||||
}
|
||||
|
||||
/// `oakcommon_xml_writer_init`.
|
||||
pub fn xml_writer_init() -> Option<CHandle> {
|
||||
Some(unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_init() })
|
||||
}
|
||||
|
||||
/// `oakcommon_xml_writer_free`.
|
||||
pub fn xml_writer_free(writer: *mut CHandle) {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_free(writer) }
|
||||
}
|
||||
|
||||
/// `oakcommon_xml_writer_write_start_element`.
|
||||
pub fn xml_writer_start_element(writer: CHandle, name: &str) -> Option<c_int> {
|
||||
use std::ffi::CString;
|
||||
let n = CString::new(name).ok()?;
|
||||
Some(unsafe {
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_start_element(writer, n.as_ptr())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcommon_xml_writer_write_attribute`.
|
||||
pub fn xml_writer_attribute(writer: CHandle, name: &str, value: &str) -> Option<c_int> {
|
||||
use std::ffi::CString;
|
||||
let n = CString::new(name).ok()?;
|
||||
let v = CString::new(value).ok()?;
|
||||
Some(unsafe {
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_attribute(
|
||||
writer,
|
||||
n.as_ptr(),
|
||||
v.as_ptr(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcommon_xml_writer_write_characters`.
|
||||
pub fn xml_writer_characters(writer: CHandle, text: &str) -> Option<c_int> {
|
||||
use std::ffi::CString;
|
||||
let t = CString::new(text).ok()?;
|
||||
Some(unsafe {
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_characters(writer, t.as_ptr())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcommon_xml_writer_write_text_element`.
|
||||
pub fn xml_writer_text_element(writer: CHandle, name: &str, text: &str) -> Option<c_int> {
|
||||
use std::ffi::CString;
|
||||
let n = CString::new(name).ok()?;
|
||||
let t = CString::new(text).ok()?;
|
||||
Some(unsafe {
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_text_element(
|
||||
writer,
|
||||
n.as_ptr(),
|
||||
t.as_ptr(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcommon_xml_writer_write_end_element`.
|
||||
pub fn xml_writer_end_element(writer: CHandle) -> Option<c_int> {
|
||||
Some(unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_end_element(writer) })
|
||||
}
|
||||
|
||||
/// `oakcommon_xml_writer_write_end_document`.
|
||||
pub fn xml_writer_end_document(writer: CHandle) -> Option<c_int> {
|
||||
Some(unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_end_document(writer) })
|
||||
}
|
||||
|
||||
/// `oakcommon_xml_writer_output` (two-stage).
|
||||
pub fn xml_writer_output(writer: CHandle) -> Option<String> {
|
||||
two_stage_string("oakcommon_xml_writer_output", |buf, size| unsafe {
|
||||
Some(oakcommon::ffi::xmlutils::oakcommon_xml_writer_output(
|
||||
writer.clone(),
|
||||
buf,
|
||||
size,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcommon_config_get_int` (config access for node defaults).
|
||||
pub fn config_get_int(group: &str, key: &str, default: c_int) -> Option<c_int> {
|
||||
use std::ffi::CString;
|
||||
let g = CString::new(group).ok()?;
|
||||
let k = CString::new(key).ok()?;
|
||||
Some(unsafe {
|
||||
oakcommon::ffi::config::oakcommon_config_get_int(g.as_ptr(), k.as_ptr(), default)
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// oakcommon videoparams C ABI (sequence/footage stream params)
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// `oakcommon_videoparams_init_basic`: new owned handle (count 1).
|
||||
pub fn videoparams_init_basic(
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
pixel_format: c_int,
|
||||
channels: c_int,
|
||||
par_num: c_int,
|
||||
par_den: c_int,
|
||||
interlacing: c_int,
|
||||
divider: c_int,
|
||||
) -> Option<CHandle> {
|
||||
Some(unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_init_basic(
|
||||
width,
|
||||
height,
|
||||
pixel_format,
|
||||
channels,
|
||||
par_num,
|
||||
par_den,
|
||||
interlacing,
|
||||
divider,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcommon_videoparams_set_frame_rate`.
|
||||
pub fn videoparams_set_frame_rate(params: CHandle, num: c_int, den: c_int) -> Option<c_int> {
|
||||
Some(unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_set_frame_rate(params, num, den)
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcommon_videoparams_free` — releases the handle locally (the
|
||||
/// handle's `release` fn points into the oakcommon box machinery).
|
||||
pub fn videoparams_free(params: *mut CHandle) {
|
||||
if params.is_null() || unsafe { (*params).ctx.is_null() } {
|
||||
return;
|
||||
}
|
||||
let h = unsafe { (*params).clone() };
|
||||
if let Some(f) = h.release {
|
||||
unsafe { f(h.ctx) };
|
||||
}
|
||||
unsafe { (*params).ctx = std::ptr::null_mut() };
|
||||
}
|
||||
|
||||
/// `oakcommon_videoparams_get_width` — the value, or the default on
|
||||
/// error (the caller decides whether the handle is real).
|
||||
pub fn videoparams_get_width(params: CHandle) -> Option<c_int> {
|
||||
let mut v = 0;
|
||||
let rc = unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_width(params.clone(), &mut v)
|
||||
};
|
||||
Some(if rc < 0 { 0 } else { v })
|
||||
}
|
||||
|
||||
/// `oakcommon_videoparams_get_height`.
|
||||
pub fn videoparams_get_height(params: CHandle) -> Option<c_int> {
|
||||
let mut v = 0;
|
||||
let rc = unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_height(params.clone(), &mut v)
|
||||
};
|
||||
Some(if rc < 0 { 0 } else { v })
|
||||
}
|
||||
|
||||
/// `oakcommon_videoparams_get_format`.
|
||||
pub fn videoparams_get_format(params: CHandle) -> Option<c_int> {
|
||||
let mut v = 0;
|
||||
let rc = unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_format(params.clone(), &mut v)
|
||||
};
|
||||
Some(if rc < 0 { 0 } else { v })
|
||||
}
|
||||
|
||||
/// `oakcommon_videoparams_get_channel_count`.
|
||||
pub fn videoparams_get_channel_count(params: CHandle) -> Option<c_int> {
|
||||
let mut v = 0;
|
||||
let rc = unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_channel_count(params.clone(), &mut v)
|
||||
};
|
||||
Some(if rc < 0 { 0 } else { v })
|
||||
}
|
||||
|
||||
/// `oakcommon_videoparams_get_frame_rate` — (num, den).
|
||||
pub fn videoparams_get_frame_rate(params: CHandle) -> Option<(c_int, c_int)> {
|
||||
let mut n = 0;
|
||||
let mut d = 0;
|
||||
let rc = unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_frame_rate(
|
||||
params.clone(),
|
||||
&mut n,
|
||||
&mut d,
|
||||
)
|
||||
};
|
||||
Some(if rc < 0 { (0, 0) } else { (n, d) })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// oakcommon colortransform C ABI (color manager compliance)
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// `oakcommon_colortransform_init_display`: new owned display transform.
|
||||
pub fn colortransform_init_display(display: &str, view: &str, look: &str) -> Option<CHandle> {
|
||||
use std::ffi::CString;
|
||||
let d = CString::new(display).ok()?;
|
||||
let v = CString::new(view).ok()?;
|
||||
let l = CString::new(look).ok()?;
|
||||
Some(unsafe {
|
||||
oakcommon::ffi::colortransform::oakcommon_colortransform_init_display(
|
||||
d.as_ptr(),
|
||||
v.as_ptr(),
|
||||
l.as_ptr(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcommon_colortransform_init_output`: new owned output transform.
|
||||
pub fn colortransform_init_output(output: &str) -> Option<CHandle> {
|
||||
use std::ffi::CString;
|
||||
let o = CString::new(output).ok()?;
|
||||
Some(unsafe {
|
||||
oakcommon::ffi::colortransform::oakcommon_colortransform_init_output(o.as_ptr())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcommon_colortransform_free` — releases the handle locally.
|
||||
pub fn colortransform_free(transform: *mut CHandle) {
|
||||
if transform.is_null() || unsafe { (*transform).ctx.is_null() } {
|
||||
return;
|
||||
}
|
||||
let h = unsafe { (*transform).clone() };
|
||||
if let Some(f) = h.release {
|
||||
unsafe { f(h.ctx) };
|
||||
}
|
||||
unsafe { (*transform).ctx = std::ptr::null_mut() };
|
||||
}
|
||||
|
||||
/// `oakcommon_colortransform_is_display`.
|
||||
pub fn colortransform_is_display(transform: CHandle) -> Option<bool> {
|
||||
Some(unsafe {
|
||||
oakcommon::ffi::colortransform::oakcommon_colortransform_is_display(transform) != 0
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcommon_colortransform_get_display` (two-stage).
|
||||
pub fn colortransform_get_display(transform: CHandle) -> Option<String> {
|
||||
two_stage_string("oakcommon_colortransform_get_display", |buf, size| unsafe {
|
||||
Some(
|
||||
oakcommon::ffi::colortransform::oakcommon_colortransform_get_display(
|
||||
transform.clone(),
|
||||
buf,
|
||||
size,
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcommon_colortransform_get_output` (two-stage).
|
||||
pub fn colortransform_get_output(transform: CHandle) -> Option<String> {
|
||||
two_stage_string("oakcommon_colortransform_get_output", |buf, size| unsafe {
|
||||
Some(
|
||||
oakcommon::ffi::colortransform::oakcommon_colortransform_get_output(
|
||||
transform.clone(),
|
||||
buf,
|
||||
size,
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcommon_colortransform_get_view` (two-stage).
|
||||
pub fn colortransform_get_view(transform: CHandle) -> Option<String> {
|
||||
two_stage_string("oakcommon_colortransform_get_view", |buf, size| unsafe {
|
||||
Some(
|
||||
oakcommon::ffi::colortransform::oakcommon_colortransform_get_view(
|
||||
transform.clone(),
|
||||
buf,
|
||||
size,
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcommon_colortransform_get_look` (two-stage).
|
||||
pub fn colortransform_get_look(transform: CHandle) -> Option<String> {
|
||||
two_stage_string("oakcommon_colortransform_get_look", |buf, size| unsafe {
|
||||
Some(
|
||||
oakcommon::ffi::colortransform::oakcommon_colortransform_get_look(
|
||||
transform.clone(),
|
||||
buf,
|
||||
size,
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Shared two-stage string fetch: query the required size, then read
|
||||
/// into an owned buffer. `None` when the query returns an error.
|
||||
fn two_stage_string<F: Fn(*mut c_char, c_int) -> Option<c_int>>(
|
||||
_sym: &str,
|
||||
call: F,
|
||||
) -> Option<String> {
|
||||
let needed = call(std::ptr::null_mut(), 0)?;
|
||||
if needed <= 0 {
|
||||
return Some(String::new());
|
||||
}
|
||||
let mut buf = vec![0u8; needed as usize];
|
||||
call(buf.as_mut_ptr() as *mut c_char, needed)?;
|
||||
buf.pop(); // trailing NUL
|
||||
String::from_utf8(buf).ok()
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! oakcore C ABI imports (audio stream parameters). dlsym-resolved (see
|
||||
//! [`super`]). The `OakAudioParams` object is an opaque raw pointer owned
|
||||
//! by the caller (`oakcore_audioparams_free`), not a [`crate::handle::CHandle`].
|
||||
|
||||
use std::ffi::{c_int, c_void};
|
||||
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
/// `oakcore_audioparams_create` — new owned params (release with
|
||||
/// [`audioparams_free`]).
|
||||
pub fn audioparams_create(
|
||||
sample_rate: c_int,
|
||||
channel_layout: u64,
|
||||
format: c_int,
|
||||
) -> Option<*mut c_void> {
|
||||
use crate::bridge::dlsym;
|
||||
type F = unsafe extern "C" fn(c_int, u64, c_int) -> *mut c_void;
|
||||
dlsym::call::<F, *mut c_void>("oakcore_audioparams_create", |f| unsafe {
|
||||
f(sample_rate, channel_layout, format)
|
||||
})
|
||||
}
|
||||
|
||||
/// Test-stub path.
|
||||
#[cfg(feature = "test-stubs")]
|
||||
pub fn audioparams_create(
|
||||
sample_rate: c_int,
|
||||
channel_layout: u64,
|
||||
format: c_int,
|
||||
) -> Option<*mut c_void> {
|
||||
Some(unsafe { stub::oakcore_audioparams_create(sample_rate, channel_layout, format) })
|
||||
}
|
||||
|
||||
/// `oakcore_audioparams_free`.
|
||||
pub fn audioparams_free(params: *mut c_void) {
|
||||
if params.is_null() {
|
||||
return;
|
||||
}
|
||||
#[cfg(feature = "test-stubs")]
|
||||
unsafe {
|
||||
stub::oakcore_audioparams_free(params);
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
use crate::bridge::dlsym;
|
||||
type F = unsafe extern "C" fn(*mut c_void);
|
||||
let _ = dlsym::call::<F, ()>("oakcore_audioparams_free", |f| unsafe { f(params) });
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
/// `oakcore_audioparams_sample_rate`.
|
||||
pub fn audioparams_sample_rate(params: *const c_void) -> Option<c_int> {
|
||||
use crate::bridge::dlsym;
|
||||
type F = unsafe extern "C" fn(*const c_void) -> c_int;
|
||||
dlsym::call::<F, c_int>("oakcore_audioparams_sample_rate", |f| unsafe { f(params) })
|
||||
}
|
||||
|
||||
/// Test-stub path.
|
||||
#[cfg(feature = "test-stubs")]
|
||||
pub fn audioparams_sample_rate(params: *const c_void) -> Option<c_int> {
|
||||
Some(unsafe { stub::oakcore_audioparams_sample_rate(params) })
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
/// `oakcore_audioparams_channel_layout`.
|
||||
pub fn audioparams_channel_layout(params: *const c_void) -> Option<u64> {
|
||||
use crate::bridge::dlsym;
|
||||
type F = unsafe extern "C" fn(*const c_void) -> u64;
|
||||
dlsym::call::<F, u64>("oakcore_audioparams_channel_layout", |f| unsafe {
|
||||
f(params)
|
||||
})
|
||||
}
|
||||
|
||||
/// Test-stub path.
|
||||
#[cfg(feature = "test-stubs")]
|
||||
pub fn audioparams_channel_layout(params: *const c_void) -> Option<u64> {
|
||||
Some(unsafe { stub::oakcore_audioparams_channel_layout(params) })
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
/// `oakcore_audioparams_format`.
|
||||
pub fn audioparams_format(params: *const c_void) -> Option<c_int> {
|
||||
use crate::bridge::dlsym;
|
||||
type F = unsafe extern "C" fn(*const c_void) -> c_int;
|
||||
dlsym::call::<F, c_int>("oakcore_audioparams_format", |f| unsafe { f(params) })
|
||||
}
|
||||
|
||||
/// Test-stub path.
|
||||
#[cfg(feature = "test-stubs")]
|
||||
pub fn audioparams_format(params: *const c_void) -> Option<c_int> {
|
||||
Some(unsafe { stub::oakcore_audioparams_format(params) })
|
||||
}
|
||||
|
||||
/// In-crate implementations of the oakcore audioparams C ABI for
|
||||
/// `cargo test` (`--features test-stubs`). Mirrors the real object: a
|
||||
/// plain struct behind the caller-owned pointer.
|
||||
#[cfg(feature = "test-stubs")]
|
||||
pub(crate) mod stub {
|
||||
use super::*;
|
||||
|
||||
/// `oakcore_audioparams_create`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcore_audioparams_create(
|
||||
sample_rate: c_int,
|
||||
channel_layout: u64,
|
||||
format: c_int,
|
||||
) -> *mut c_void {
|
||||
Box::into_raw(Box::new(StubAudioParams {
|
||||
sample_rate,
|
||||
channel_layout,
|
||||
format,
|
||||
})) as *mut c_void
|
||||
}
|
||||
|
||||
/// `oakcore_audioparams_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcore_audioparams_free(params: *mut c_void) {
|
||||
if !params.is_null() {
|
||||
unsafe { drop(Box::from_raw(params as *mut StubAudioParams)) };
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakcore_audioparams_sample_rate`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcore_audioparams_sample_rate(params: *const c_void) -> c_int {
|
||||
if params.is_null() {
|
||||
return 0;
|
||||
}
|
||||
unsafe { (*(params as *const StubAudioParams)).sample_rate }
|
||||
}
|
||||
|
||||
/// `oakcore_audioparams_channel_layout`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcore_audioparams_channel_layout(params: *const c_void) -> u64 {
|
||||
if params.is_null() {
|
||||
return 0;
|
||||
}
|
||||
unsafe { (*(params as *const StubAudioParams)).channel_layout }
|
||||
}
|
||||
|
||||
/// `oakcore_audioparams_format`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcore_audioparams_format(params: *const c_void) -> c_int {
|
||||
if params.is_null() {
|
||||
return 0;
|
||||
}
|
||||
unsafe { (*(params as *const StubAudioParams)).format }
|
||||
}
|
||||
|
||||
/// Boxed audioparams stub payload.
|
||||
pub(crate) struct StubAudioParams {
|
||||
pub sample_rate: c_int,
|
||||
pub channel_layout: u64,
|
||||
pub format: c_int,
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! C ABI imports from other oak modules.
|
||||
//!
|
||||
//! ## Resolution model
|
||||
//!
|
||||
//! Symbols are resolved at runtime with `dlsym(RTLD_DEFAULT)` (the
|
||||
//! module is force-loaded into the host process, so the real module
|
||||
//! libraries' symbols are in the global scope). `cargo test` builds
|
||||
//! without those libraries: a missing symbol surfaces as `None` from
|
||||
//! the wrapper and the caller maps it to a graceful error. This follows
|
||||
//! the oakplugin crate template (`src/plugin/rust/src/bridge/mod.rs`).
|
||||
//!
|
||||
//! Real linkage for the module dylib is provided by the C++ side's
|
||||
//! force_load of liboaknode (the staticlib); nothing here is linked
|
||||
//! directly at compile time.
|
||||
|
||||
pub mod codec;
|
||||
pub mod common;
|
||||
pub mod core;
|
||||
pub mod render;
|
||||
pub mod timeline;
|
||||
pub mod undo;
|
||||
|
||||
/// Shared dlsym runtime resolution (pub for crate tests).
|
||||
pub mod dlsym {
|
||||
use std::ffi::{c_char, c_void};
|
||||
|
||||
/// RTLD_DEFAULT (macOS: -2; Linux: 0).
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) const RTLD_DEFAULT: *mut c_void = -2isize as *mut c_void;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) const RTLD_DEFAULT: *mut c_void = 0isize as *mut c_void;
|
||||
|
||||
extern "C" {
|
||||
fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
|
||||
}
|
||||
|
||||
/// Resolve a global-scope symbol; `None` when missing.
|
||||
pub fn resolve(name: &str) -> Option<*mut c_void> {
|
||||
let c = std::ffi::CString::new(name).ok()?;
|
||||
let p = unsafe { dlsym(RTLD_DEFAULT, c.as_ptr()) };
|
||||
if p.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(p)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve and call by signature; `None` when the symbol is missing.
|
||||
///
|
||||
/// # Safety
|
||||
/// The caller guarantees `T` matches the symbol's real function type.
|
||||
pub(crate) fn call<T, R>(name: &str, f: impl FnOnce(T) -> R) -> Option<R>
|
||||
where
|
||||
T: Copy,
|
||||
{
|
||||
let p = resolve(name)?;
|
||||
let f_ptr: T = unsafe { std::mem::transmute_copy(&p) };
|
||||
Some(f(f_ptr))
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! oakrender C ABI imports (caches, textures, color processors).
|
||||
//!
|
||||
//! Symbols resolved via `dlsym(RTLD_DEFAULT)` (see [`super::dlsym`]);
|
||||
//! every wrapper returns `None`/a neutral value when the symbol is
|
||||
//! absent (cargo test without liboakrender).
|
||||
|
||||
use std::ffi::c_int;
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// oakrender cache handle (value type).
|
||||
pub type CacheHandle = CHandle;
|
||||
/// oakrender texture handle (value type).
|
||||
pub type TextureHandle = CHandle;
|
||||
/// oakrender color processor handle (value type).
|
||||
pub type ColorProcessorHandle = CHandle;
|
||||
|
||||
/// Cache kind constants (oakrender `OAKRENDER_CACHE_*`).
|
||||
pub mod cache_kind {
|
||||
/// `OAKRENDER_CACHE_VIDEO_FRAME`.
|
||||
pub const VIDEO_FRAME: i32 = 0;
|
||||
/// `OAKRENDER_CACHE_THUMBNAIL`.
|
||||
pub const THUMBNAIL: i32 = 1;
|
||||
/// `OAKRENDER_CACHE_AUDIO_PLAYBACK`.
|
||||
pub const AUDIO_PLAYBACK: i32 = 2;
|
||||
/// `OAKRENDER_CACHE_AUDIO_WAVEFORM`.
|
||||
pub const AUDIO_WAVEFORM: i32 = 3;
|
||||
}
|
||||
|
||||
/// `oakrender_cache_create_for_node`.
|
||||
pub fn cache_create_for_node(parent: CHandle, kind: i32) -> Option<CHandle> {
|
||||
use crate::bridge::dlsym;
|
||||
type F = unsafe extern "C" fn(CHandle, i32) -> CHandle;
|
||||
dlsym::call::<F, CHandle>("oakrender_cache_create_for_node", |f| unsafe {
|
||||
f(parent, kind)
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakrender_cache_free`.
|
||||
pub fn cache_free(cache: *mut CHandle) {
|
||||
use crate::bridge::dlsym;
|
||||
type F = unsafe extern "C" fn(*mut CHandle);
|
||||
if let Some(f) = dlsym::call::<F, ()>("oakrender_cache_free", |f| unsafe { f(cache) }) {
|
||||
let _ = f;
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakrender_cache_invalidate_range`.
|
||||
pub fn cache_invalidate_range(
|
||||
cache: CHandle,
|
||||
in_num: i64,
|
||||
in_den: i64,
|
||||
out_num: i64,
|
||||
out_den: i64,
|
||||
) {
|
||||
use crate::bridge::dlsym;
|
||||
type F = unsafe extern "C" fn(CHandle, i64, i64, i64, i64);
|
||||
if let Some(f) = dlsym::call::<F, ()>("oakrender_cache_invalidate_range", |f| unsafe {
|
||||
f(cache, in_num, in_den, out_num, out_den)
|
||||
}) {
|
||||
let _ = f;
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakrender_cache_set_uuid`.
|
||||
pub fn cache_set_uuid(cache: CHandle, uuid: &str) -> Option<i32> {
|
||||
use crate::bridge::dlsym;
|
||||
use std::ffi::CString;
|
||||
type F = unsafe extern "C" fn(CHandle, *const std::ffi::c_char) -> i32;
|
||||
let c = CString::new(uuid).ok()?;
|
||||
dlsym::call::<F, i32>("oakrender_cache_set_uuid", |f| unsafe {
|
||||
f(cache, c.as_ptr())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakrender_cache_get_uuid` (two-stage).
|
||||
pub fn cache_get_uuid(cache: CHandle) -> Option<String> {
|
||||
use crate::bridge::dlsym;
|
||||
use std::ffi::c_char;
|
||||
type F = unsafe extern "C" fn(CHandle, *mut c_char, i32) -> i32;
|
||||
let needed = dlsym::call::<F, i32>("oakrender_cache_get_uuid", |f| unsafe {
|
||||
f(cache.clone(), std::ptr::null_mut(), 0)
|
||||
})?;
|
||||
if needed <= 0 {
|
||||
return None;
|
||||
}
|
||||
let mut buf = vec![0u8; needed as usize];
|
||||
dlsym::call::<F, i32>("oakrender_cache_get_uuid", |f| unsafe {
|
||||
f(cache.clone(), buf.as_mut_ptr() as *mut c_char, needed)
|
||||
})?;
|
||||
buf.pop(); // trailing NUL
|
||||
String::from_utf8(buf).ok()
|
||||
}
|
||||
|
||||
/// `oakrender_disk_cache_path` (two-stage): the default cache directory.
|
||||
pub fn disk_cache_path() -> Option<String> {
|
||||
use crate::bridge::dlsym;
|
||||
use std::ffi::c_char;
|
||||
type F = unsafe extern "C" fn(*mut c_char, i32) -> i32;
|
||||
let needed = dlsym::call::<F, i32>("oakrender_disk_cache_path", |f| unsafe {
|
||||
f(std::ptr::null_mut(), 0)
|
||||
})?;
|
||||
if needed <= 0 {
|
||||
return None;
|
||||
}
|
||||
let mut buf = vec![0u8; needed as usize];
|
||||
dlsym::call::<F, i32>("oakrender_disk_cache_path", |f| unsafe {
|
||||
f(buf.as_mut_ptr() as *mut c_char, needed)
|
||||
})?;
|
||||
buf.pop(); // trailing NUL
|
||||
String::from_utf8(buf).ok()
|
||||
}
|
||||
|
||||
/// `oakrender_color_config_create_default`: load the bundled OCIO
|
||||
/// config. `None` = symbol absent (cargo test); `Some(Ok(())` =
|
||||
/// success; `Some(Err(()))` = OCIO error.
|
||||
pub fn color_config_create_default() -> Option<Result<(), ()>> {
|
||||
use crate::bridge::dlsym;
|
||||
type F = unsafe extern "C" fn() -> i32;
|
||||
let rc = dlsym::call::<F, i32>("oakrender_color_config_create_default", |f| unsafe { f() })?;
|
||||
if rc == 0 {
|
||||
Some(Ok(()))
|
||||
} else {
|
||||
Some(Err(()))
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakrender_color_config_load_from_filename`: load a config file.
|
||||
/// Same tri-state as [`color_config_create_default`].
|
||||
pub fn color_config_load(filename: &str) -> Option<Result<(), ()>> {
|
||||
use crate::bridge::dlsym;
|
||||
use std::ffi::CString;
|
||||
type F = unsafe extern "C" fn(*const std::ffi::c_char) -> i32;
|
||||
let c = CString::new(filename).ok()?;
|
||||
let rc = dlsym::call::<F, i32>("oakrender_color_config_load_from_filename", |f| unsafe {
|
||||
f(c.as_ptr())
|
||||
})?;
|
||||
if rc == 0 {
|
||||
Some(Ok(()))
|
||||
} else {
|
||||
Some(Err(()))
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! oaktimeline C ABI imports (sequence markers/work area, edit
|
||||
//! commands used by sequence setup). dlsym-resolved (see [`super`]).
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// `oaktimeline_marker_list_create`.
|
||||
pub fn marker_list_create() -> Option<CHandle> {
|
||||
use crate::bridge::dlsym;
|
||||
type F = unsafe extern "C" fn() -> CHandle;
|
||||
dlsym::call::<F, CHandle>("oaktimeline_marker_list_create", |f| unsafe { f() })
|
||||
}
|
||||
|
||||
/// `oaktimeline_marker_list_free`.
|
||||
pub fn marker_list_free(list: *mut CHandle) {
|
||||
use crate::bridge::dlsym;
|
||||
type F = unsafe extern "C" fn(*mut CHandle);
|
||||
if let Some(f) = dlsym::call::<F, ()>("oaktimeline_marker_list_free", |f| unsafe { f(list) }) {
|
||||
let _ = f;
|
||||
}
|
||||
}
|
||||
|
||||
/// `oaktimeline_workarea_create`.
|
||||
pub fn workarea_create() -> Option<CHandle> {
|
||||
use crate::bridge::dlsym;
|
||||
type F = unsafe extern "C" fn() -> CHandle;
|
||||
dlsym::call::<F, CHandle>("oaktimeline_workarea_create", |f| unsafe { f() })
|
||||
}
|
||||
|
||||
/// `oaktimeline_workarea_free`.
|
||||
pub fn workarea_free(w: *mut CHandle) {
|
||||
use crate::bridge::dlsym;
|
||||
type F = unsafe extern "C" fn(*mut CHandle);
|
||||
if let Some(f) = dlsym::call::<F, ()>("oaktimeline_workarea_free", |f| unsafe { f(w) }) {
|
||||
let _ = f;
|
||||
}
|
||||
}
|
||||
|
||||
/// `oaktimeline_add_track_command`.
|
||||
pub fn add_track_command(list: CHandle) -> Option<CHandle> {
|
||||
use crate::bridge::dlsym;
|
||||
type F = unsafe extern "C" fn(CHandle) -> CHandle;
|
||||
dlsym::call::<F, CHandle>("oaktimeline_add_track_command", |f| unsafe { f(list) })
|
||||
}
|
||||
@@ -1,408 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! oakundo C ABI calls — now direct Rust calls into the oakundo crate
|
||||
//! (single-lib unification, see `docs/zh/plans/riir/single-lib.md`).
|
||||
//! Undo commands are created through the C ABI vtable
|
||||
//! (`oakundo_command_init` with Rust closures as userdata) — no C++
|
||||
//! UndoCommand subclassing exists on this side.
|
||||
//!
|
||||
//! ## Test stubs (`--features test-stubs`)
|
||||
//!
|
||||
//! With the direct dependency, the real oakundo rlib is linked into
|
||||
//! every build and test; the `test-stubs` feature still compiles
|
||||
//! in-crate `#[no_mangle]` implementations of the undo C ABI (see
|
||||
//! [`stub`]) for environments that link without oakundo. Do not enable
|
||||
//! the feature in a binary that also links oakundo (duplicate symbols).
|
||||
|
||||
use std::ffi::c_int;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// `OakUndoCommandVtable` (include/undo/undocommand.h) — the callback
|
||||
/// table backing a caller-defined undo command. Single-lib unification:
|
||||
/// aliases the oakundo crate's vtable POD (identical layout).
|
||||
pub type Vtable = oakundo::undocommand::OakUndoCommandVtable;
|
||||
|
||||
/// Rust closure state behind a vtable command's `userdata` pointer.
|
||||
///
|
||||
/// The box is handed to [`command_init`] (which takes ownership); the
|
||||
/// vtable trampolines below route `redo`/`undo`/destruction back into
|
||||
/// the closures.
|
||||
pub struct CommandState {
|
||||
/// Whether the command has been executed (redo_now no-ops when done).
|
||||
pub done: AtomicBool,
|
||||
/// Redo closure.
|
||||
pub redo: Box<dyn FnMut() + Send>,
|
||||
/// Undo closure.
|
||||
pub undo: Box<dyn FnMut() + Send>,
|
||||
}
|
||||
|
||||
impl CommandState {
|
||||
/// New state with both directions.
|
||||
pub fn new(
|
||||
redo: impl FnMut() + Send + 'static,
|
||||
undo: impl FnMut() + Send + 'static,
|
||||
) -> CommandState {
|
||||
CommandState {
|
||||
done: AtomicBool::new(false),
|
||||
redo: Box::new(redo),
|
||||
undo: Box::new(undo),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trampoline: run the redo closure behind `userdata`. Panics are
|
||||
/// swallowed at the C boundary (`// CPP-PARITY: undocommand.cpp` — the
|
||||
/// C++ side has no panic concept; a panic here must never unwind across
|
||||
/// the extern "C" frame).
|
||||
unsafe extern "C" fn redo_trampoline(userdata: *mut std::ffi::c_void) {
|
||||
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
if !userdata.is_null() {
|
||||
let state = unsafe { &mut *(userdata as *mut CommandState) };
|
||||
(state.redo)();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// Trampoline: run the undo closure behind `userdata`.
|
||||
unsafe extern "C" fn undo_trampoline(userdata: *mut std::ffi::c_void) {
|
||||
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
if !userdata.is_null() {
|
||||
let state = unsafe { &mut *(userdata as *mut CommandState) };
|
||||
(state.undo)();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// Trampoline: free the `CommandState` box.
|
||||
unsafe extern "C" fn free_trampoline(userdata: *mut std::ffi::c_void) {
|
||||
if !userdata.is_null() {
|
||||
unsafe { drop(Box::from_raw(userdata as *mut CommandState)) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a vtable-backed undo command whose redo/undo run the given
|
||||
/// closures (`oakundo_command_init`). The returned handle is owned by
|
||||
/// the caller; `None` when oakundo is unavailable (or the stub returns
|
||||
/// an empty handle).
|
||||
pub fn command_from_closures(
|
||||
redo: impl FnMut() + Send + 'static,
|
||||
undo: impl FnMut() + Send + 'static,
|
||||
) -> Option<CHandle> {
|
||||
let state = Box::new(CommandState::new(redo, undo));
|
||||
let vtable = Vtable {
|
||||
redo: Some(redo_trampoline),
|
||||
undo: Some(undo_trampoline),
|
||||
free_fn: Some(free_trampoline),
|
||||
};
|
||||
command_init(&vtable, Box::into_raw(state) as *mut std::ffi::c_void)
|
||||
}
|
||||
|
||||
/// `oakundo_command_init` (vtable command).
|
||||
#[cfg(feature = "test-stubs")]
|
||||
pub fn command_init(vtable: &Vtable, userdata: *mut std::ffi::c_void) -> Option<CHandle> {
|
||||
Some(unsafe { stub::oakundo_command_init(vtable as *const Vtable, userdata) })
|
||||
}
|
||||
|
||||
/// `oakundo_command_init` (vtable command).
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
pub fn command_init(vtable: &Vtable, userdata: *mut std::ffi::c_void) -> Option<CHandle> {
|
||||
// Direct call into the oakundo crate (single-lib unification).
|
||||
let h = unsafe { oakundo::ffi::command::oakundo_command_init(vtable, userdata) };
|
||||
if h.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(h)
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakundo_command_init_multi`.
|
||||
#[cfg(feature = "test-stubs")]
|
||||
pub fn command_init_multi() -> Option<CHandle> {
|
||||
Some(unsafe { stub::oakundo_command_init_multi() })
|
||||
}
|
||||
|
||||
/// `oakundo_command_init_multi`.
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
pub fn command_init_multi() -> Option<CHandle> {
|
||||
// Direct call into the oakundo crate (single-lib unification).
|
||||
let h = unsafe { oakundo::ffi::command::oakundo_command_init_multi() };
|
||||
if h.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(h)
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakundo_command_multi_add_child`.
|
||||
#[cfg(feature = "test-stubs")]
|
||||
pub fn command_multi_add_child(multi: CHandle, child: CHandle) -> Option<c_int> {
|
||||
Some(unsafe { stub::oakundo_command_multi_add_child(multi, child) })
|
||||
}
|
||||
|
||||
/// `oakundo_command_multi_add_child`.
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
pub fn command_multi_add_child(multi: CHandle, child: CHandle) -> Option<c_int> {
|
||||
// Direct call into the oakundo crate (single-lib unification).
|
||||
Some(unsafe { oakundo::ffi::command::oakundo_command_multi_add_child(multi, child) })
|
||||
}
|
||||
|
||||
/// `oakundo_command_redo_now`.
|
||||
#[cfg(feature = "test-stubs")]
|
||||
pub fn command_redo_now(command: CHandle) -> Option<c_int> {
|
||||
Some(unsafe { stub::oakundo_command_redo_now(command) })
|
||||
}
|
||||
|
||||
/// `oakundo_command_redo_now`.
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
pub fn command_redo_now(command: CHandle) -> Option<c_int> {
|
||||
// Direct call into the oakundo crate (single-lib unification).
|
||||
Some(unsafe { oakundo::ffi::command::oakundo_command_redo_now(command) })
|
||||
}
|
||||
|
||||
/// `oakundo_command_undo_now`.
|
||||
#[cfg(feature = "test-stubs")]
|
||||
pub fn command_undo_now(command: CHandle) -> Option<c_int> {
|
||||
Some(unsafe { stub::oakundo_command_undo_now(command) })
|
||||
}
|
||||
|
||||
/// `oakundo_command_undo_now`.
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
pub fn command_undo_now(command: CHandle) -> Option<c_int> {
|
||||
// Direct call into the oakundo crate (single-lib unification).
|
||||
Some(unsafe { oakundo::ffi::command::oakundo_command_undo_now(command) })
|
||||
}
|
||||
|
||||
/// `oakundo_command_free`.
|
||||
#[cfg(feature = "test-stubs")]
|
||||
pub fn command_free(command: *mut CHandle) {
|
||||
unsafe { stub::oakundo_command_free(command) };
|
||||
}
|
||||
|
||||
/// `oakundo_command_free`.
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
pub fn command_free(command: *mut CHandle) {
|
||||
// Direct call into the oakundo crate (single-lib unification).
|
||||
unsafe { oakundo::ffi::command::oakundo_command_free(command) }
|
||||
}
|
||||
|
||||
/// `oakundo_stack_push` (facade-owned stack).
|
||||
pub fn stack_push(
|
||||
stack: CHandle,
|
||||
command: CHandle,
|
||||
text: *const std::ffi::c_char,
|
||||
) -> Option<c_int> {
|
||||
// Direct call into the oakundo crate (single-lib unification).
|
||||
Some(unsafe { oakundo::ffi::undostack::oakundo_undostack_push(stack, command, text) })
|
||||
}
|
||||
|
||||
/// In-crate implementations of the undo C ABI for `cargo test`
|
||||
/// (`--features test-stubs`). Mirrors the C++ `CallbackUndoCommand`
|
||||
/// (`src/undo/c_api/undocommand.cpp`) semantics: the command holds the
|
||||
/// vtable + userdata, calls `free_fn` on destruction, and `redo_now`/
|
||||
/// `undo_now` are no-ops when already executed. Multi commands hold one
|
||||
/// reference per child.
|
||||
#[cfg(feature = "test-stubs")]
|
||||
pub(crate) mod stub {
|
||||
use super::*;
|
||||
|
||||
/// A command box behind an OakUndoCommand handle's `ctx`.
|
||||
pub(crate) enum StubCommand {
|
||||
/// Vtable command.
|
||||
Callback {
|
||||
/// Executed state (redo_now no-ops when true).
|
||||
done: AtomicBool,
|
||||
/// Redo callback.
|
||||
redo: Option<unsafe extern "C" fn(*mut std::ffi::c_void)>,
|
||||
/// Undo callback.
|
||||
undo: Option<unsafe extern "C" fn(*mut std::ffi::c_void)>,
|
||||
/// userdata release.
|
||||
free_fn: Option<unsafe extern "C" fn(*mut std::ffi::c_void)>,
|
||||
/// Opaque userdata (owned by the command).
|
||||
userdata: *mut std::ffi::c_void,
|
||||
},
|
||||
/// Multi command.
|
||||
Multi {
|
||||
/// Executed state.
|
||||
done: AtomicBool,
|
||||
/// Child commands (each holds one reference).
|
||||
children: Vec<CHandle>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Drop for StubCommand {
|
||||
fn drop(&mut self) {
|
||||
match self {
|
||||
StubCommand::Callback {
|
||||
free_fn, userdata, ..
|
||||
} => {
|
||||
if let Some(f) = free_fn {
|
||||
if !userdata.is_null() {
|
||||
unsafe { f(*userdata) };
|
||||
}
|
||||
}
|
||||
}
|
||||
StubCommand::Multi { children, .. } => {
|
||||
for child in children {
|
||||
if let Some(f) = child.release {
|
||||
unsafe { f(child.ctx) };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakundo_command_init`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_command_init(
|
||||
vtable: *const Vtable,
|
||||
userdata: *mut std::ffi::c_void,
|
||||
) -> CHandle {
|
||||
if vtable.is_null() {
|
||||
return CHandle::null();
|
||||
}
|
||||
let vt = unsafe { &*vtable };
|
||||
let cmd = StubCommand::Callback {
|
||||
done: AtomicBool::new(false),
|
||||
redo: vt.redo,
|
||||
undo: vt.undo,
|
||||
free_fn: vt.free_fn,
|
||||
userdata,
|
||||
};
|
||||
crate::handle::make_owned(SendStub(cmd))
|
||||
}
|
||||
|
||||
/// `oakundo_command_init_multi`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_command_init_multi() -> CHandle {
|
||||
crate::handle::make_owned(SendStub(StubCommand::Multi {
|
||||
done: AtomicBool::new(false),
|
||||
children: Vec::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// `oakundo_command_multi_add_child`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_command_multi_add_child(
|
||||
multi: CHandle,
|
||||
child: CHandle,
|
||||
) -> c_int {
|
||||
if multi.ctx.is_null() || child.ctx.is_null() {
|
||||
return crate::error::OAKNODE_E_INVALID;
|
||||
}
|
||||
let boxed = multi.ctx as *mut crate::handle::RefBox<SendStub>;
|
||||
// Take one reference for the multi.
|
||||
if let Some(f) = child.addref {
|
||||
unsafe { f(child.ctx) };
|
||||
}
|
||||
let state = unsafe { &mut (*boxed).value };
|
||||
match &mut state.0 {
|
||||
StubCommand::Multi { children, .. } => {
|
||||
children.push(child);
|
||||
crate::error::OAKNODE_OK
|
||||
}
|
||||
_ => crate::error::OAKNODE_E_INVALID,
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakundo_command_redo_now`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_command_redo_now(command: CHandle) -> c_int {
|
||||
if command.ctx.is_null() {
|
||||
return crate::error::OAKNODE_E_INVALID;
|
||||
}
|
||||
let boxed = command.ctx as *mut crate::handle::RefBox<SendStub>;
|
||||
let state = unsafe { &mut (*boxed).value };
|
||||
match &mut state.0 {
|
||||
StubCommand::Callback {
|
||||
done,
|
||||
redo,
|
||||
userdata,
|
||||
..
|
||||
} => {
|
||||
if !done.swap(true, Ordering::AcqRel) {
|
||||
if let Some(f) = redo {
|
||||
unsafe { f(*userdata) };
|
||||
}
|
||||
}
|
||||
crate::error::OAKNODE_OK
|
||||
}
|
||||
StubCommand::Multi { done, children } => {
|
||||
if !done.swap(true, Ordering::AcqRel) {
|
||||
for child in children.iter() {
|
||||
let _ = unsafe { oakundo_command_redo_now(child.clone()) };
|
||||
}
|
||||
}
|
||||
crate::error::OAKNODE_OK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakundo_command_undo_now`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_command_undo_now(command: CHandle) -> c_int {
|
||||
if command.ctx.is_null() {
|
||||
return crate::error::OAKNODE_E_INVALID;
|
||||
}
|
||||
let boxed = command.ctx as *mut crate::handle::RefBox<SendStub>;
|
||||
let state = unsafe { &mut (*boxed).value };
|
||||
match &mut state.0 {
|
||||
StubCommand::Callback {
|
||||
done,
|
||||
undo,
|
||||
userdata,
|
||||
..
|
||||
} => {
|
||||
if done.swap(false, Ordering::AcqRel) {
|
||||
if let Some(f) = undo {
|
||||
unsafe { f(*userdata) };
|
||||
}
|
||||
}
|
||||
crate::error::OAKNODE_OK
|
||||
}
|
||||
StubCommand::Multi { done, children } => {
|
||||
if done.swap(false, Ordering::AcqRel) {
|
||||
for child in children.iter().rev() {
|
||||
let _ = unsafe { oakundo_command_undo_now(child.clone()) };
|
||||
}
|
||||
}
|
||||
crate::error::OAKNODE_OK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakundo_command_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_command_free(command: *mut CHandle) {
|
||||
if command.is_null() || unsafe { (*command).ctx.is_null() } {
|
||||
return;
|
||||
}
|
||||
let h = unsafe { (*command).clone() };
|
||||
if let Some(f) = h.release {
|
||||
unsafe { f(h.ctx) };
|
||||
}
|
||||
unsafe { (*command).ctx = std::ptr::null_mut() };
|
||||
}
|
||||
|
||||
/// Send-marker for the command box (the raw `userdata` pointer is
|
||||
/// only dereferenced on the thread that created the command — the
|
||||
/// test thread — so the box never actually crosses threads).
|
||||
struct SendStub(StubCommand);
|
||||
unsafe impl Send for SendStub {}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,12 +15,14 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Footage nodes (C++ `olive::Footage`): media file references.
|
||||
//! Probing goes through the oakcodec C ABI (`bridge::codec`) — the C++
|
||||
//! transition-stub probe path does not exist here.
|
||||
//! Probing goes through the oakcodec decoder registry (direct Rust calls,
|
||||
//! single-lib unification).
|
||||
//! `// CPP-PARITY: src/node/src/project/footage/footage.{h,cpp}`.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use oakcodec::decoder::Decoder as _;
|
||||
|
||||
use crate::input::Input;
|
||||
use crate::node::{Category, NodeBehavior, NodeCore};
|
||||
use crate::value::{AudioParams, NodeValue, ValueType, VideoParams};
|
||||
@@ -84,33 +86,33 @@ impl FootageBehavior {
|
||||
}
|
||||
}
|
||||
|
||||
/// Probe the file through oakcodec (`oakcodec_decoder_probe`),
|
||||
/// filling `streams`. Error on unreadable/corrupt media or when the
|
||||
/// codec module is unavailable; the prior `streams`/`valid` state is
|
||||
/// preserved on failure (no partial state).
|
||||
/// Probe the file through oakcodec's decoder registry, recording the
|
||||
/// recognized decoder id. Error on unreadable/corrupt media; the
|
||||
/// prior `streams`/`valid` state is preserved on failure (no partial
|
||||
/// state).
|
||||
pub fn probe(&mut self) -> crate::error::Result<()> {
|
||||
use crate::error::Error;
|
||||
// Direct call into the oakcodec crate (single-lib unification):
|
||||
// `oakcodec_decoder_probe(filename)` returns the stream-list
|
||||
// handle (owned by the caller).
|
||||
let out = match crate::bridge::codec::decoder_probe(&self.filename) {
|
||||
Some(out) => out,
|
||||
None => {
|
||||
return Err(Error::Failed(
|
||||
"oakcodec unavailable (not linked)".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
if out.is_null() {
|
||||
return Err(Error::Failed(
|
||||
"oakcodec probe returned no streams".to_string(),
|
||||
));
|
||||
}
|
||||
// The probe result handle is an oakcodec stream-list. Reading
|
||||
// stream entries into `streams` is a Phase-2 follow-up (the
|
||||
// exact accessor symbols are pinned when the codec module C ABI
|
||||
// is finalized).
|
||||
let _ = out;
|
||||
// Direct probe through the oakcodec decoder registry (single-lib
|
||||
// unification; replaces the former `oakcodec_decoder_probe` C ABI
|
||||
// call).
|
||||
let desc = oakcodec::decoder::receive_list_of_all_decoders()
|
||||
.into_iter()
|
||||
.find_map(|d| {
|
||||
let desc = d.probe(&self.filename, None)?;
|
||||
if desc.decoder().is_empty()
|
||||
|| (desc.video_stream_count() == 0
|
||||
&& desc.audio_stream_count() == 0
|
||||
&& desc.subtitle_stream_count() == 0)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(desc)
|
||||
})
|
||||
.ok_or_else(|| Error::Failed("oakcodec probe returned no streams".to_string()))?;
|
||||
self.decoder = desc.decoder().to_string();
|
||||
// Reading stream entries into `streams` is a follow-up (the
|
||||
// stream-access surface is pinned when the codec module is
|
||||
// finalized); the probe result itself is dropped here.
|
||||
self.valid = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -30,11 +30,9 @@
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub mod block;
|
||||
pub mod bridge;
|
||||
pub mod colormanager;
|
||||
pub mod error;
|
||||
pub mod factory;
|
||||
pub mod ffi;
|
||||
pub mod folder;
|
||||
pub mod footage;
|
||||
pub mod graph;
|
||||
|
||||
@@ -73,7 +73,7 @@ pub struct NodeCore {
|
||||
pub inputs: Vec<Input>,
|
||||
/// Keyframe tracks per (input, element).
|
||||
pub keyframes: Vec<(String, i32, KeyframeTrack)>,
|
||||
/// Caches as oakrender handles (created via bridge::render).
|
||||
/// Caches as opaque oakrender handles.
|
||||
pub caches: NodeCaches,
|
||||
/// Node flags bitmask (hidden, dont-show-in-param-view, ...).
|
||||
pub flags: u64,
|
||||
@@ -429,18 +429,18 @@ impl NodeCore {
|
||||
#[derive(Clone)]
|
||||
pub struct NodeCaches {
|
||||
/// Video frame hash cache.
|
||||
pub video: crate::bridge::render::CacheHandle,
|
||||
pub video: crate::handle::CHandle,
|
||||
/// Thumbnail cache.
|
||||
pub thumbnail: crate::bridge::render::CacheHandle,
|
||||
pub thumbnail: crate::handle::CHandle,
|
||||
/// Audio playback cache.
|
||||
pub audio: crate::bridge::render::CacheHandle,
|
||||
pub audio: crate::handle::CHandle,
|
||||
/// Waveform cache.
|
||||
pub waveform: crate::bridge::render::CacheHandle,
|
||||
pub waveform: crate::handle::CHandle,
|
||||
}
|
||||
|
||||
impl Default for NodeCaches {
|
||||
/// All empty handles (caches are created lazily through bridge::render
|
||||
/// when a node enters a project; `// CPP-PARITY: node.cpp:102`).
|
||||
/// All empty handles (caches are created lazily when a node
|
||||
/// enters a project; `// CPP-PARITY: node.cpp:102`).
|
||||
fn default() -> Self {
|
||||
NodeCaches {
|
||||
video: crate::handle::CHandle::null(),
|
||||
@@ -586,7 +586,7 @@ pub trait NodeBehavior: Send {
|
||||
fn generate_frame(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
frame: &mut crate::bridge::render::TextureHandle,
|
||||
frame: &mut crate::handle::CHandle,
|
||||
time: Rational,
|
||||
) {
|
||||
let _ = (core, frame, time);
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
//!
|
||||
//! Note: OpenColorIO itself is never linked here; it is reached through
|
||||
//! the color manager (`crate::colormanager`) and the oakrender bridge
|
||||
//! (`crate::bridge::render`), like the C++ node's
|
||||
//! (oakrender, opaque handles), like the C++ node's
|
||||
//! `oaknode_colormanager_*` / `oakrender_color_processor_*` calls.
|
||||
|
||||
use crate::factory::NodeMeta;
|
||||
|
||||
@@ -93,7 +93,7 @@ impl GeneratorWithMerge {
|
||||
/// and the un-merged case pushes `job` itself.
|
||||
pub fn push_mergable_job(
|
||||
inputs: &crate::value::NodeValueRow,
|
||||
job: crate::bridge::render::TextureHandle,
|
||||
job: crate::handle::CHandle,
|
||||
table: &mut crate::value::NodeValueTable,
|
||||
) {
|
||||
match inputs.get(BASE_INPUT) {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
//! undo commands `NodeGroupAddInputPassthrough` /
|
||||
//! `NodeGroupSetOutputPassthrough`; the C ABI's undoable variants
|
||||
//! (`include/node/group.h`) build equivalent vtable commands through
|
||||
//! `bridge::undo` in the ffi layer.
|
||||
//! oakundo commands in the ops layer.
|
||||
|
||||
use crate::factory::NodeMeta;
|
||||
use crate::graph::Graph;
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
//!
|
||||
//! Note: OpenColorIO itself is never linked here. All OCIO work goes
|
||||
//! through the color manager (`crate::colormanager`) and the oakrender
|
||||
//! bridge (`crate::bridge::render`), mirroring how the C++ node calls
|
||||
//! bridge (opaque oakrender handles), mirroring how the C++ node calls
|
||||
//! `oakrender_color_processor_*` / `oaknode_colormanager_*` C functions
|
||||
//! instead of using OCIO directly.
|
||||
|
||||
@@ -47,13 +47,13 @@ pub const TEXTURE_INPUT: &str = "tex_in";
|
||||
/// bridge), so the field is omitted here: `added_to_graph` /
|
||||
/// `removed_from_graph` document the capture/clear, and the processor
|
||||
/// generation helpers reach the manager through
|
||||
/// `crate::colormanager`/`crate::bridge::render` at call time.
|
||||
/// `crate::colormanager`/oakrender at call time.
|
||||
pub struct OcioBase {
|
||||
/// Owned color processor handle (C++ `processor_`, an
|
||||
/// `OakColorProcessor`); `None`/empty while no valid processor has
|
||||
/// been generated. Released with the node (C++ destructor calls
|
||||
/// `oakrender_color_processor_free`).
|
||||
processor: Option<crate::bridge::render::ColorProcessorHandle>,
|
||||
processor: Option<crate::handle::CHandle>,
|
||||
}
|
||||
|
||||
// The processor handle wraps a refcounted C object that is only
|
||||
@@ -74,7 +74,7 @@ impl OcioBase {
|
||||
|
||||
/// Borrowed view of the owned processor handle (C++
|
||||
/// `OCIOBaseNode::processor()`; callers must NOT free it).
|
||||
pub fn processor(&self) -> Option<&crate::bridge::render::ColorProcessorHandle> {
|
||||
pub fn processor(&self) -> Option<&crate::handle::CHandle> {
|
||||
self.processor.as_ref()
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ impl OcioBase {
|
||||
/// `OakColorProcessor` before storing the new one).
|
||||
pub fn set_processor(
|
||||
&mut self,
|
||||
processor: Option<crate::bridge::render::ColorProcessorHandle>,
|
||||
processor: Option<crate::handle::CHandle>,
|
||||
) {
|
||||
// The C++ frees the previous processor via
|
||||
// `oakrender_color_processor_free`; the Rust handle is a
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
//!
|
||||
//! Note: OpenColorIO itself is never linked here; it is reached through
|
||||
//! the color manager (`crate::colormanager`) and the oakrender bridge
|
||||
//! (`crate::bridge::render`), like the C++ node's
|
||||
//! (oakrender, opaque handles), like the C++ node's
|
||||
//! `oakrender_color_processor_create_grading_primary` call.
|
||||
|
||||
use crate::factory::NodeMeta;
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
//!
|
||||
//! Note: OpenColorIO itself is never linked here; it is reached through
|
||||
//! the color manager (`crate::colormanager`) and the oakrender bridge
|
||||
//! (`crate::bridge::render`), like the C++ node's
|
||||
//! (oakrender, opaque handles), like the C++ node's
|
||||
//! `oakrender_color_processor_create_grading_primary` call.
|
||||
|
||||
use crate::factory::NodeMeta;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
//!
|
||||
//! Note: OpenColorIO itself is never linked here; it is reached through
|
||||
//! the color manager (`crate::colormanager`) and the oakrender bridge
|
||||
//! (`crate::bridge::render`), like the C++ node's
|
||||
//! (oakrender, opaque handles), like the C++ node's
|
||||
//! `oakrender_color_processor_create_lut` / `oakrender_lut_*` calls.
|
||||
|
||||
use std::sync::Mutex;
|
||||
@@ -57,7 +57,7 @@ struct ProcessorState {
|
||||
last_direction: i64,
|
||||
/// Cached processor for change detection (C++ `last_processor_`);
|
||||
/// released with the node.
|
||||
last_processor: Option<crate::bridge::render::ColorProcessorHandle>,
|
||||
last_processor: Option<crate::handle::CHandle>,
|
||||
/// Human-readable reason no LUT processor is active (C++
|
||||
/// `last_error_`); empty when a valid processor is in use or no LUT
|
||||
/// file has been selected yet.
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
//!
|
||||
//! DECLARATION ONLY. This node is a thin wrapper over an OFX plugin
|
||||
//! instance that lives behind the `oakplugin` crate's C ABI bridge
|
||||
//! (`crate::bridge`); no OFX types (`OFX::Host::ImageEffect::Instance`,
|
||||
//! (opaque oakrender handles); no OFX types (`OFX::Host::ImageEffect::Instance`,
|
||||
//! `kOfxParam*`, ...) are declared here. The plugin instance is
|
||||
//! represented as the opaque [`PluginInstanceHandle`] below — the real
|
||||
//! definition belongs to the oakplugin bridge module and this draft
|
||||
@@ -242,14 +242,14 @@ impl NodeBehavior for PluginNode {
|
||||
/// the destination frame if needed and zero-fills it (plugins do
|
||||
/// their real image work in the plugin job, not here).
|
||||
///
|
||||
/// The Rust frame is an opaque [`crate::bridge::render::TextureHandle`]
|
||||
/// The Rust frame is an opaque [`crate::handle::CHandle`]
|
||||
/// whose pixels cannot be read or written from this crate, so the
|
||||
/// body is a documented no-op (`// CPP-PARITY: plugin.cpp`
|
||||
/// `generate_frame`).
|
||||
fn generate_frame(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
frame: &mut crate::bridge::render::TextureHandle,
|
||||
frame: &mut crate::handle::CHandle,
|
||||
time: Rational,
|
||||
) {
|
||||
let _ = (core, frame, time);
|
||||
|
||||
@@ -136,7 +136,7 @@ impl NodeBehavior for PolygonGenerator {
|
||||
/// scaling and center translation; without a backend the frame is
|
||||
/// left empty (warned once).
|
||||
///
|
||||
/// The Rust `frame` is an opaque [`crate::bridge::render::TextureHandle`]
|
||||
/// The Rust `frame` is an opaque [`crate::handle::CHandle`]
|
||||
/// whose bytes cannot be touched, and this crate has no path-fill
|
||||
/// backend — so neither the clear nor the fill is representable here
|
||||
/// (`// CPP-PARITY: polygon.cpp` `generate_frame`). The path building
|
||||
@@ -145,7 +145,7 @@ impl NodeBehavior for PolygonGenerator {
|
||||
fn generate_frame(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
frame: &mut crate::bridge::render::TextureHandle,
|
||||
frame: &mut crate::handle::CHandle,
|
||||
time: oakcore_rs::Rational,
|
||||
) {
|
||||
let _ = (core, frame, time);
|
||||
|
||||
@@ -241,7 +241,7 @@ impl NodeBehavior for TextGeneratorV1 {
|
||||
/// into the float frame multiplied by the color input. With no
|
||||
/// backend installed, warns once and leaves the frame empty.
|
||||
///
|
||||
/// The Rust frame is an opaque [`crate::bridge::render::TextureHandle`]
|
||||
/// The Rust frame is an opaque [`crate::handle::CHandle`]
|
||||
/// whose pixels cannot be read or written from this crate, so the
|
||||
/// body is a documented no-op; the layout/measure/offset control flow
|
||||
/// is ported in [`Self::layout_request`], [`Self::draw_offsets`] and
|
||||
@@ -249,7 +249,7 @@ impl NodeBehavior for TextGeneratorV1 {
|
||||
fn generate_frame(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
frame: &mut crate::bridge::render::TextureHandle,
|
||||
frame: &mut crate::handle::CHandle,
|
||||
time: Rational,
|
||||
) {
|
||||
let _ = (core, frame, time);
|
||||
|
||||
@@ -301,7 +301,7 @@ impl NodeBehavior for TextGeneratorV2 {
|
||||
/// identical). With no backend installed, warns once and leaves
|
||||
/// the frame empty.
|
||||
///
|
||||
/// The Rust frame is an opaque [`crate::bridge::render::TextureHandle`]
|
||||
/// The Rust frame is an opaque [`crate::handle::CHandle`]
|
||||
/// whose pixels cannot be read or written from this crate, so the
|
||||
/// body is a documented no-op; the layout/measure/offset control flow
|
||||
/// is ported in [`Self::layout_request`], [`Self::base_offset`],
|
||||
@@ -310,7 +310,7 @@ impl NodeBehavior for TextGeneratorV2 {
|
||||
fn generate_frame(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
frame: &mut crate::bridge::render::TextureHandle,
|
||||
frame: &mut crate::handle::CHandle,
|
||||
time: Rational,
|
||||
) {
|
||||
let _ = (core, frame, time);
|
||||
|
||||
@@ -342,7 +342,7 @@ impl NodeBehavior for TextGeneratorV3 {
|
||||
/// backend installed, warns once and leaves the cleared frame
|
||||
/// untouched.
|
||||
///
|
||||
/// The Rust frame is an opaque [`crate::bridge::render::TextureHandle`]
|
||||
/// The Rust frame is an opaque [`crate::handle::CHandle`]
|
||||
/// whose pixels cannot be read or written from this crate, so the
|
||||
/// body is a documented no-op; the layout/measure/offset control flow
|
||||
/// is ported in [`Self::layout_request`], [`Self::base_offset`] and
|
||||
@@ -350,7 +350,7 @@ impl NodeBehavior for TextGeneratorV3 {
|
||||
fn generate_frame(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
frame: &mut crate::bridge::render::TextureHandle,
|
||||
frame: &mut crate::handle::CHandle,
|
||||
time: Rational,
|
||||
) {
|
||||
let _ = (core, frame, time);
|
||||
|
||||
+67
-13
@@ -17,7 +17,10 @@
|
||||
//! Free functions replacing the C++ `Node` static methods
|
||||
//! (COVERAGE.md §6/§9).
|
||||
|
||||
use std::ffi::c_void;
|
||||
|
||||
use oakcore_rs::TimeRange;
|
||||
use oakundo::undocommand::{OakUndoCommandVtable, UndoCommand};
|
||||
|
||||
use crate::graph::Graph;
|
||||
use crate::id::NodeId;
|
||||
@@ -111,7 +114,7 @@ pub fn copy_inputs(
|
||||
/// (C++ `copy_dependency_graph` / `copy_node_in_graph` /
|
||||
/// `copy_node_and_dependency_graph_minus_items`). Returns the new
|
||||
/// node ids (source order). Undo packaging happens at the caller via
|
||||
/// bridge::undo.
|
||||
/// oakundo's `UndoCommand`.
|
||||
pub fn copy_subgraph(
|
||||
graph: &mut Graph,
|
||||
nodes: &[NodeId],
|
||||
@@ -147,10 +150,64 @@ fn lock_any<T>(m: &std::sync::Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||
m.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// Userdata payload behind a closure-backed undo command: the boxed
|
||||
/// redo/undo closures.
|
||||
struct ClosureCommand {
|
||||
/// The redo closure.
|
||||
redo: Box<dyn FnMut() + Send>,
|
||||
/// The undo closure.
|
||||
undo: Box<dyn FnMut() + Send>,
|
||||
}
|
||||
|
||||
/// `OakUndoCommandVtable` redo thunk for [`ClosureCommand`] userdata.
|
||||
unsafe extern "C" fn closure_redo(ud: *mut c_void) {
|
||||
// SAFETY: `ud` is the `ClosureCommand` box created by
|
||||
// `command_from_closures` and still owned by the command.
|
||||
let c = unsafe { &mut *(ud as *mut ClosureCommand) };
|
||||
(c.redo)();
|
||||
}
|
||||
|
||||
/// `OakUndoCommandVtable` undo thunk for [`ClosureCommand`] userdata.
|
||||
unsafe extern "C" fn closure_undo(ud: *mut c_void) {
|
||||
// SAFETY: see `closure_redo`.
|
||||
let c = unsafe { &mut *(ud as *mut ClosureCommand) };
|
||||
(c.undo)();
|
||||
}
|
||||
|
||||
/// `OakUndoCommandVtable` free thunk for [`ClosureCommand`] userdata.
|
||||
unsafe extern "C" fn closure_free(ud: *mut c_void) {
|
||||
if !ud.is_null() {
|
||||
// SAFETY: the box is destroyed exactly once, by the command that
|
||||
// owns it.
|
||||
unsafe { drop(Box::from_raw(ud as *mut ClosureCommand)) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an un-executed [`UndoCommand`] from redo/undo closures (the
|
||||
/// direct-Rust replacement of the former oakundo bridge
|
||||
/// `command_from_closures`).
|
||||
fn command_from_closures(
|
||||
redo: impl FnMut() + Send + 'static,
|
||||
undo: impl FnMut() + Send + 'static,
|
||||
) -> UndoCommand {
|
||||
let ud = Box::into_raw(Box::new(ClosureCommand {
|
||||
redo: Box::new(redo),
|
||||
undo: Box::new(undo),
|
||||
}));
|
||||
UndoCommand::from_vtable(
|
||||
OakUndoCommandVtable {
|
||||
redo: Some(closure_redo),
|
||||
undo: Some(closure_undo),
|
||||
free_fn: Some(closure_free),
|
||||
},
|
||||
ud as *mut c_void,
|
||||
)
|
||||
}
|
||||
|
||||
/// Set a keyframed/standard value at a time, returning an un-executed
|
||||
/// undo command (C++ `Node::set_value_at_time` static; command creation
|
||||
/// via bridge::undo). The mutation is chosen from the current state at
|
||||
/// creation time, like the C++ (`// CPP-PARITY: node.cpp:1782`):
|
||||
/// via oakundo's `UndoCommand`). The mutation is chosen from the current
|
||||
/// state at creation time, like the C++ (`// CPP-PARITY: node.cpp:1782`):
|
||||
/// - keyframing input with a key at `time` -> replace the key's value;
|
||||
/// - keyframing input without a key -> insert a key (best type =
|
||||
/// closest key's type, default Linear);
|
||||
@@ -166,7 +223,7 @@ pub fn set_value_at_time_command(
|
||||
element: i32,
|
||||
time: oakcore_rs::Rational,
|
||||
value: &crate::value::NodeValue,
|
||||
) -> crate::error::Result<crate::handle::CHandle> {
|
||||
) -> crate::error::Result<UndoCommand> {
|
||||
use crate::error::Error;
|
||||
use crate::keyframe::{Interpolation, Keyframe};
|
||||
|
||||
@@ -202,7 +259,7 @@ pub fn set_value_at_time_command(
|
||||
// Replace the key's value (preserving type/handles).
|
||||
let project_redo = project.clone();
|
||||
let project_undo = project.clone();
|
||||
Ok(crate::bridge::undo::command_from_closures(
|
||||
Ok(command_from_closures(
|
||||
{
|
||||
let value = value.clone();
|
||||
let input = input.clone();
|
||||
@@ -229,8 +286,7 @@ pub fn set_value_at_time_command(
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.ok_or(Error::NoMem)?)
|
||||
))
|
||||
}
|
||||
None => {
|
||||
let input_redo = input.clone();
|
||||
@@ -250,7 +306,7 @@ pub fn set_value_at_time_command(
|
||||
let project_redo = project.clone();
|
||||
let project_undo = project.clone();
|
||||
let value_redo = value.clone();
|
||||
Ok(crate::bridge::undo::command_from_closures(
|
||||
Ok(command_from_closures(
|
||||
move || {
|
||||
let mut g = lock_any(&project_redo);
|
||||
if let Some(e) = g.graph.get_mut(node) {
|
||||
@@ -272,8 +328,7 @@ pub fn set_value_at_time_command(
|
||||
.remove_key(time);
|
||||
}
|
||||
},
|
||||
)
|
||||
.ok_or(Error::NoMem)?)
|
||||
))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -287,7 +342,7 @@ pub fn set_value_at_time_command(
|
||||
let input_redo = input.clone();
|
||||
let input_undo = input.clone();
|
||||
let value_redo = value.clone();
|
||||
Ok(crate::bridge::undo::command_from_closures(
|
||||
Ok(command_from_closures(
|
||||
move || {
|
||||
let mut g = lock_any(&project_redo);
|
||||
if let Some(e) = g.graph.get_mut(node) {
|
||||
@@ -301,7 +356,6 @@ pub fn set_value_at_time_command(
|
||||
e.core.set_standard_value(&input_undo, element, old.clone());
|
||||
}
|
||||
},
|
||||
)
|
||||
.ok_or(Error::NoMem)?)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ pub struct NodeRef {
|
||||
|
||||
impl NodeRef {
|
||||
/// New reference. `owned` selects whether releasing the last handle
|
||||
/// reference accounts the node in [`crate::ffi::debug_alive_count`].
|
||||
/// reference accounts the node in the crate's debug alive count.
|
||||
pub fn new(project: Arc<Mutex<Project>>, id: NodeId, owned: bool) -> NodeRef {
|
||||
NodeRef {
|
||||
project,
|
||||
|
||||
@@ -117,20 +117,15 @@ impl SequenceBehavior {
|
||||
}
|
||||
|
||||
/// Apply the default video/audio parameters (C++
|
||||
/// `ViewerOutput::set_default_parameters()`; the config lookups use
|
||||
/// oakcommon's defaults when the config module is absent).
|
||||
/// `ViewerOutput::set_default_parameters()`; the config lookups read
|
||||
/// the oakcommon config store directly).
|
||||
pub fn set_default_parameters(&mut self) {
|
||||
let width =
|
||||
crate::bridge::common::config_get_int("DefaultSequenceWidth", "", 1920).unwrap_or(1920);
|
||||
let height = crate::bridge::common::config_get_int("DefaultSequenceHeight", "", 1080)
|
||||
.unwrap_or(1080);
|
||||
let sample_rate =
|
||||
crate::bridge::common::config_get_int("DefaultSequenceAudioFrequency", "", 48000)
|
||||
.unwrap_or(48000);
|
||||
let fps_num = crate::bridge::common::config_get_int("DefaultSequenceFrameRateNum", "", 30)
|
||||
.unwrap_or(30);
|
||||
let fps_den = crate::bridge::common::config_get_int("DefaultSequenceFrameRateDen", "", 1)
|
||||
.unwrap_or(1);
|
||||
let config = oakcommon::configstore::ConfigStore::instance();
|
||||
let width = config.get_int(None, "DefaultSequenceWidth", 1920);
|
||||
let height = config.get_int(None, "DefaultSequenceHeight", 1080);
|
||||
let sample_rate = config.get_int(None, "DefaultSequenceAudioFrequency", 48000);
|
||||
let fps_num = config.get_int(None, "DefaultSequenceFrameRateNum", 30);
|
||||
let fps_den = config.get_int(None, "DefaultSequenceFrameRateDen", 1);
|
||||
|
||||
self.video_params = vec![VideoParams {
|
||||
width,
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
//! Project (de)serialization: the C++ `ProjectSerializer` family.
|
||||
//!
|
||||
//! XML I/O goes through [`crate::bridge::common`] (oakcommon C ABI;
|
||||
//! in-crate stubs under `--features test-stubs`). The XML shape mirrors
|
||||
//! XML I/O goes through oakcommon's [`XmlReader`]/[`XmlWriter`] (direct
|
||||
//! Rust calls, single-lib unification). The XML shape mirrors
|
||||
//! the C++ `Node::save`/`Project::save` writers (`// CPP-PARITY:
|
||||
//! src/node/src/node.cpp:node::save`, `// CPP-PARITY:
|
||||
//! src/node/src/project.cpp:save`). Byte-exact output parity with the C++
|
||||
@@ -32,9 +32,9 @@
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use oakcommon::xmlutils::{XmlReader, XmlWriter};
|
||||
use oakcore_rs::Rational;
|
||||
|
||||
use crate::bridge::common;
|
||||
use crate::graph::Graph;
|
||||
use crate::id::NodeId;
|
||||
use crate::keyframe::{Interpolation, Keyframe};
|
||||
@@ -43,7 +43,7 @@ use crate::project::{NodeRef, Project};
|
||||
use crate::value::{NodeValue, ValueType};
|
||||
|
||||
/// Minimal XML reader surface the serializer needs (implemented over
|
||||
/// the oakcommon xml C ABI in `bridge::common`).
|
||||
/// oakcommon's `xmlutils`).
|
||||
pub trait XmlRead {
|
||||
/// Advance to the next start element; false at end/close.
|
||||
fn next_start_element(&mut self) -> bool;
|
||||
@@ -73,47 +73,38 @@ pub trait XmlWrite {
|
||||
fn characters(&mut self, _text: &str) {}
|
||||
}
|
||||
|
||||
/// Reader over the oakcommon XML C ABI.
|
||||
/// Reader over oakcommon's [`XmlReader`].
|
||||
pub struct XmlReaderBridge {
|
||||
/// The oakcommon reader handle.
|
||||
pub handle: crate::handle::CHandle,
|
||||
/// The oakcommon reader.
|
||||
reader: XmlReader,
|
||||
/// Current element name (cached).
|
||||
name: String,
|
||||
}
|
||||
|
||||
/// Writer over the oakcommon XML C ABI.
|
||||
/// Writer over oakcommon's [`XmlWriter`].
|
||||
pub struct XmlWriterBridge {
|
||||
/// The oakcommon writer handle.
|
||||
pub handle: crate::handle::CHandle,
|
||||
/// The oakcommon writer.
|
||||
writer: XmlWriter,
|
||||
}
|
||||
|
||||
impl XmlReaderBridge {
|
||||
/// Create from XML text; `None` when oakcommon is unavailable.
|
||||
/// Create from XML text; `None` on a parse error.
|
||||
pub fn new(xml: &str) -> Option<XmlReaderBridge> {
|
||||
use std::ffi::CString;
|
||||
let c = CString::new(xml).ok()?;
|
||||
let handle = common::xml_reader_init(c.as_ptr())?;
|
||||
let reader = XmlReader::new(xml).ok()?;
|
||||
Some(XmlReaderBridge {
|
||||
handle,
|
||||
reader,
|
||||
name: String::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for XmlReaderBridge {
|
||||
fn drop(&mut self) {
|
||||
common::xml_reader_free(&mut self.handle);
|
||||
}
|
||||
}
|
||||
|
||||
impl XmlRead for XmlReaderBridge {
|
||||
fn next_start_element(&mut self) -> bool {
|
||||
match common::xml_reader_next_start_element(self.handle.clone()) {
|
||||
Some(true) => {
|
||||
self.name = common::xml_reader_name(self.handle.clone()).unwrap_or_default();
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
if self.reader.read_next_start_element().unwrap_or(false) {
|
||||
self.name = self.reader.name().unwrap_or_default();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,64 +113,58 @@ impl XmlRead for XmlReaderBridge {
|
||||
}
|
||||
|
||||
fn attribute(&self, name: &str) -> Option<String> {
|
||||
let count = common::xml_reader_attribute_count(self.handle.clone()).unwrap_or(0);
|
||||
let count = self.reader.attribute_count().unwrap_or(0);
|
||||
for i in 0..count {
|
||||
let attr_name =
|
||||
common::xml_reader_attribute_name(self.handle.clone(), i).unwrap_or_default();
|
||||
let attr_name = self.reader.attribute_name(i).unwrap_or_default();
|
||||
if attr_name == name {
|
||||
return common::xml_reader_attribute_value(self.handle.clone(), i);
|
||||
return self.reader.attribute_value(i).ok();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn read_element_text(&mut self) -> String {
|
||||
common::xml_reader_read_element_text(self.handle.clone()).unwrap_or_default()
|
||||
self.reader.read_element_text().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn skip_current_element(&mut self) {
|
||||
let _ = common::xml_reader_skip_current_element(self.handle.clone());
|
||||
let _ = self.reader.skip_current_element();
|
||||
}
|
||||
}
|
||||
|
||||
impl XmlWriterBridge {
|
||||
/// Create a writer; `None` when oakcommon is unavailable.
|
||||
/// Create a writer.
|
||||
pub fn new() -> Option<XmlWriterBridge> {
|
||||
let handle = common::xml_writer_init()?;
|
||||
Some(XmlWriterBridge { handle })
|
||||
Some(XmlWriterBridge {
|
||||
writer: XmlWriter::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The serialized output.
|
||||
pub fn output(&self) -> String {
|
||||
common::xml_writer_output(self.handle.clone()).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for XmlWriterBridge {
|
||||
fn drop(&mut self) {
|
||||
common::xml_writer_free(&mut self.handle);
|
||||
self.writer.output().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl XmlWrite for XmlWriterBridge {
|
||||
fn start_element(&mut self, name: &str) {
|
||||
let _ = common::xml_writer_start_element(self.handle.clone(), name);
|
||||
let _ = self.writer.write_start_element(name);
|
||||
}
|
||||
|
||||
fn end_element(&mut self) {
|
||||
let _ = common::xml_writer_end_element(self.handle.clone());
|
||||
let _ = self.writer.write_end_element();
|
||||
}
|
||||
|
||||
fn attribute(&mut self, name: &str, value: &str) {
|
||||
let _ = common::xml_writer_attribute(self.handle.clone(), name, value);
|
||||
let _ = self.writer.write_attribute(name, value);
|
||||
}
|
||||
|
||||
fn text_element(&mut self, name: &str, text: &str) {
|
||||
let _ = common::xml_writer_text_element(self.handle.clone(), name, text);
|
||||
let _ = self.writer.write_text_element(name, text);
|
||||
}
|
||||
|
||||
fn characters(&mut self, text: &str) {
|
||||
let _ = common::xml_writer_characters(self.handle.clone(), text);
|
||||
let _ = self.writer.write_characters(text);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ pub enum NodeValue {
|
||||
/// Boolean.
|
||||
Boolean(bool),
|
||||
/// Texture handle (owned reference).
|
||||
Texture(crate::bridge::render::TextureHandle),
|
||||
Texture(crate::handle::CHandle),
|
||||
/// Interleaved/planar sample payload + format.
|
||||
Samples(SampleBuffer),
|
||||
/// Rational.
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
#[test]
|
||||
fn dbg_dlsym() {
|
||||
let p = oaknode::bridge::dlsym::resolve("oaknode_xml_writer_init");
|
||||
eprintln!("writer init symbol: {:?}", p);
|
||||
let p2 = oaknode::bridge::dlsym::resolve("oakundo_command_init_multi");
|
||||
eprintln!("undo multi symbol: {:?}", p2);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -357,55 +357,77 @@ fn serializer_value_codecs() {
|
||||
);
|
||||
}
|
||||
|
||||
/// bridge::undo: vtable commands + multi commands through the stubs.
|
||||
///
|
||||
/// Needs the `test-stubs` feature: the bridge resolves `oakundo_*`
|
||||
/// symbols via dlsym, which only resolves in the test binary when the
|
||||
/// in-crate stubs are compiled in.
|
||||
#[cfg(feature = "test-stubs")]
|
||||
/// oakundo `UndoCommand`: vtable commands + multi commands (direct Rust
|
||||
/// calls, single-lib unification).
|
||||
#[test]
|
||||
fn undo_command_roundtrip() {
|
||||
use std::ffi::c_void;
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use std::sync::Arc;
|
||||
use oakundo::undocommand::{OakUndoCommandVtable, UndoCommand};
|
||||
|
||||
struct Closures {
|
||||
redo: Box<dyn FnMut() + Send>,
|
||||
undo: Box<dyn FnMut() + Send>,
|
||||
}
|
||||
|
||||
unsafe extern "C" fn redo_thunk(ud: *mut c_void) {
|
||||
let c = unsafe { &mut *(ud as *mut Closures) };
|
||||
(c.redo)();
|
||||
}
|
||||
unsafe extern "C" fn undo_thunk(ud: *mut c_void) {
|
||||
let c = unsafe { &mut *(ud as *mut Closures) };
|
||||
(c.undo)();
|
||||
}
|
||||
unsafe extern "C" fn free_thunk(ud: *mut c_void) {
|
||||
if !ud.is_null() {
|
||||
unsafe { drop(Box::from_raw(ud as *mut Closures)) };
|
||||
}
|
||||
}
|
||||
|
||||
fn from_closures(
|
||||
redo: impl FnMut() + Send + 'static,
|
||||
undo: impl FnMut() + Send + 'static,
|
||||
) -> UndoCommand {
|
||||
let ud = Box::into_raw(Box::new(Closures {
|
||||
redo: Box::new(redo),
|
||||
undo: Box::new(undo),
|
||||
}));
|
||||
UndoCommand::from_vtable(
|
||||
OakUndoCommandVtable {
|
||||
redo: Some(redo_thunk),
|
||||
undo: Some(undo_thunk),
|
||||
free_fn: Some(free_thunk),
|
||||
},
|
||||
ud as *mut c_void,
|
||||
)
|
||||
}
|
||||
|
||||
let value = Arc::new(AtomicI32::new(0));
|
||||
let value_redo = value.clone();
|
||||
let value_undo = value.clone();
|
||||
let value_check = value.clone();
|
||||
let mut cmd = oaknode::bridge::undo::command_from_closures(
|
||||
let mut cmd = from_closures(
|
||||
move || {
|
||||
value_redo.fetch_add(1, Ordering::SeqCst);
|
||||
},
|
||||
move || {
|
||||
value_undo.fetch_sub(1, Ordering::SeqCst);
|
||||
},
|
||||
)
|
||||
.expect("undo stub available");
|
||||
);
|
||||
|
||||
// redo applies, undo reverts; redo_now is idempotent.
|
||||
assert_eq!(
|
||||
oaknode::bridge::undo::command_redo_now(cmd.clone()).unwrap(),
|
||||
0
|
||||
);
|
||||
cmd.redo_now();
|
||||
assert_eq!(value_check.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(
|
||||
oaknode::bridge::undo::command_redo_now(cmd.clone()).unwrap(),
|
||||
0
|
||||
);
|
||||
cmd.redo_now();
|
||||
assert_eq!(
|
||||
value_check.load(Ordering::SeqCst),
|
||||
1,
|
||||
"redo no-ops when done"
|
||||
);
|
||||
assert_eq!(
|
||||
oaknode::bridge::undo::command_undo_now(cmd.clone()).unwrap(),
|
||||
0
|
||||
);
|
||||
cmd.undo_now();
|
||||
assert_eq!(value_check.load(Ordering::SeqCst), 0);
|
||||
assert_eq!(
|
||||
oaknode::bridge::undo::command_undo_now(cmd.clone()).unwrap(),
|
||||
0
|
||||
);
|
||||
cmd.undo_now();
|
||||
assert_eq!(
|
||||
value_check.load(Ordering::SeqCst),
|
||||
0,
|
||||
@@ -413,8 +435,8 @@ fn undo_command_roundtrip() {
|
||||
);
|
||||
|
||||
// Multi command batches children.
|
||||
let mut multi = oaknode::bridge::undo::command_init_multi().unwrap();
|
||||
let mut child = oaknode::bridge::undo::command_from_closures(
|
||||
let mut multi = UndoCommand::multi();
|
||||
let child = from_closures(
|
||||
{
|
||||
let value = value_check.clone();
|
||||
move || {
|
||||
@@ -427,26 +449,12 @@ fn undo_command_roundtrip() {
|
||||
value.fetch_sub(1, Ordering::SeqCst);
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
oaknode::bridge::undo::command_multi_add_child(multi.clone(), child.clone()).unwrap(),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
oaknode::bridge::undo::command_redo_now(multi.clone()).unwrap(),
|
||||
0
|
||||
);
|
||||
multi.multi_add_child(child);
|
||||
multi.redo_now();
|
||||
assert_eq!(value_check.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(
|
||||
oaknode::bridge::undo::command_undo_now(multi.clone()).unwrap(),
|
||||
0
|
||||
);
|
||||
multi.undo_now();
|
||||
assert_eq!(value_check.load(Ordering::SeqCst), 0);
|
||||
|
||||
oaknode::bridge::undo::command_free(&mut cmd);
|
||||
oaknode::bridge::undo::command_free(&mut multi);
|
||||
oaknode::bridge::undo::command_free(&mut child);
|
||||
}
|
||||
|
||||
/// track.rs: branch coverage the ffi contract tests leave open —
|
||||
|
||||
@@ -322,35 +322,6 @@ fn track_block_ordering() {
|
||||
);
|
||||
}
|
||||
|
||||
/// ClipBlock cache passthrough: the C ABI export accepts the call (the
|
||||
/// cache UUID copy is inert until the oakrender bridge creates per-node
|
||||
/// caches).
|
||||
#[test]
|
||||
fn clip_cache_passthrough() {
|
||||
use oaknode::error::OAKNODE_OK;
|
||||
use oaknode::ffi::block::oaknode_block_clip_create;
|
||||
use oaknode::ffi::block::oaknode_block_free;
|
||||
use oaknode::ffi::block::oaknode_clip_add_cache_passthrough_from;
|
||||
use oaknode::ffi::project::oaknode_project_free;
|
||||
use oaknode::ffi::project::oaknode_project_init;
|
||||
use oaknode::handle::CHandle;
|
||||
|
||||
let mut p = unsafe { oaknode_project_init() };
|
||||
let mut clip = unsafe { oaknode_block_clip_create() };
|
||||
let mut other = unsafe { oaknode_block_clip_create() };
|
||||
assert_eq!(
|
||||
unsafe { oaknode_clip_add_cache_passthrough_from(clip.clone(), other.clone()) },
|
||||
OAKNODE_OK
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oaknode_clip_add_cache_passthrough_from(CHandle::null(), other.clone()) },
|
||||
oaknode::error::OAKNODE_E_INVALID
|
||||
);
|
||||
unsafe { oaknode_block_free(&mut clip) };
|
||||
unsafe { oaknode_block_free(&mut other) };
|
||||
unsafe { oaknode_project_free(&mut p) };
|
||||
}
|
||||
|
||||
/// Footage behavior: state without a codec module (probe fails
|
||||
/// gracefully without partial state); proxy fields, counts, duration.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user