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:
+29
-50
@@ -2,75 +2,54 @@
|
||||
name = "oakengine"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Oak Video Editor facade: re-exports the frozen oakengine_* C ABI over the module C ABIs (Rust)"
|
||||
description = "Oak Video Editor facade: re-exports the frozen oakengine_* C ABI over the module Rust APIs (Rust)"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "staticlib", "rlib"]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
# NDJSON control-plane protocol for the worker session (src/worker.rs) and
|
||||
# the shm error formatting in src/ipc.rs.
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
# POSIX shm_open/mmap/munmap/shm_unlink constants + syscalls for the
|
||||
# shared-memory frame-slot transport (src/ipc.rs).
|
||||
libc = "0.2"
|
||||
# Error derive (Display + std::error::Error) for the crate error enum
|
||||
# (src/error.rs). Same major version the other modules use (oakotio, ...).
|
||||
thiserror = "2"
|
||||
|
||||
# Every module call crosses the module C ABI as an `extern "C"` import
|
||||
# (src/bridge/). The module crates below are REAL dependencies so their
|
||||
# `#[no_mangle]` exports are linked into the final `liboakengine` cdylib:
|
||||
# the dylib then carries the module C ABIs itself (oakundo_*, oakcommon_*,
|
||||
# oaktimeline_*, oakcodec_*, oakaudio_*, oakrender_*, oaktask_*,
|
||||
# oakplugin_*, oaknode_*) alongside the facade's oakengine_* exports.
|
||||
# Worker/IPC wire protocol (src/worker.rs, src/ipc.rs): serde for the
|
||||
# NDJSON control-plane messages and serde_json for the wire encoding.
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
# POSIX shm_open/ftruncate/mmap/munmap/shm_unlink for the shared-memory
|
||||
# frame-slot transport (src/ipc.rs).
|
||||
libc = "0.2"
|
||||
|
||||
# Single-lib unification: every module call is a compile-time Rust call
|
||||
# into the module crate's direct API (no module C ABI). The engine keeps
|
||||
# the frozen oakengine_* C ABI upward; downward it rewires through
|
||||
# src/stubs.rs (direct Rust shims for oakcommon/oakcodec/oakrender/
|
||||
# oakaudio/oakplugin, and clearly marked STUBs for the handle-based
|
||||
# oaknode/oaktimeline paths).
|
||||
#
|
||||
# The crates are linked WITHOUT their `test-stubs` features so the real
|
||||
# exports ship. Cross-module calls that the modules resolve with
|
||||
# dlsym(RTLD_DEFAULT) (oaknode, oakplugin, oakrender) now resolve against
|
||||
# the sibling modules inside the same dylib; the remaining undefined
|
||||
# imports are the C++ host-provided symbols (`oakcore_audioparams_*`,
|
||||
# `oakcore_rational_*` from liboakcore), which build.rs leaves as runtime
|
||||
# lookups for the host app. The audio resample/decode paths call FFmpeg
|
||||
# in-process via ffmpeg-next (oakaudio/oakcodec); the C++ ffmpeg_bridge
|
||||
# `fb_*` imports are gone.
|
||||
# The remaining undefined imports are the C++ host-provided symbols
|
||||
# (`oakcore_audioparams_*` from liboakcore), which build.rs leaves as
|
||||
# runtime lookups for the host app (see src/stubs.rs audio module).
|
||||
#
|
||||
# Tests link the same crates; the dev-dependencies below re-declare
|
||||
# oakcommon/oakplugin WITH `test-stubs` so their in-crate C ABI mocks
|
||||
# (the ffmpeg_bridge replacement and the oakrender dlsym stubs) are
|
||||
# compiled into the test binaries (features union with the normal
|
||||
# dependencies for test builds).
|
||||
#
|
||||
# NOTE (oaktimeline/oaktask): their `test-stubs` features are never used
|
||||
# here — the in-crate mocks define `oakundo_command_init` etc., which
|
||||
# would collide with the real oakundo rlib in one binary. Without
|
||||
# test-stubs their real exports reference the oaknode/oakundo/oakcommon
|
||||
# C ABI symbols as link-time externs, provided by the sibling crates in
|
||||
# the same dylib (or by the dev-dependency rlibs in a test binary).
|
||||
#
|
||||
# rustc would normally prune rlibs that are only touched through
|
||||
# `extern "C"` imports from the link; src/linkage.rs anchors every crate
|
||||
# (and oakcore-rs) so the linker pulls their object files.
|
||||
# src/linkage.rs anchors every crate (and oakcore-rs) so the linker pulls
|
||||
# their object files into the cdylib.
|
||||
oakcore-rs = { path = "../oakcore" }
|
||||
oakundo = { path = "../oakundo" }
|
||||
oakcommon = { path = "../oakcommon" }
|
||||
oaktimeline = { path = "../oaktimeline" }
|
||||
oakcodec = { path = "../oakcodec" }
|
||||
oakaudio = { path = "../oakaudio" }
|
||||
oakrender = { path = "../oakrender" }
|
||||
oaktask = { path = "../oaktask" }
|
||||
oakplugin = { path = "../oakplugin" }
|
||||
oaknode = { path = "../oaknode" }
|
||||
oakaudio = { path = "../oakaudio" }
|
||||
oakplugin = { path = "../oakplugin" }
|
||||
|
||||
[dev-dependencies]
|
||||
# The module crates as plain dependencies (no `test-stubs`): the C ABI
|
||||
# mocks the test binaries need (the oakcore_* symbols) are provided by
|
||||
# the crate's own test support (tests/common/mod.rs), which is
|
||||
# force-linked into every integration test. Keeping the dev-deps feature-free also stops Cargo's workspace
|
||||
# feature unification from forcing `test-stubs` onto the oakcommon/oakplugin
|
||||
# member builds during `cargo test --workspace` (their own tests then run
|
||||
# in the real dlsym mode and stay green).
|
||||
# The facade is cdylib-only, so the former tests/*.rs integration tests
|
||||
# now run as unit tests inside the crate (src/test_support/, pulled in
|
||||
# from src/lib.rs); the module crates are reachable through the normal
|
||||
# [dependencies] above. oakcommon stays here for the direct dev-only
|
||||
# imports in the test support files.
|
||||
oakcommon = { path = "../oakcommon" }
|
||||
oakplugin = { path = "../oakplugin" }
|
||||
|
||||
+12
-21
@@ -26,10 +26,6 @@ src/
|
||||
node.rs node.h + project.h + footage.h (node graph / project / footage)
|
||||
timeline.rs timeline.h (sequences, clips, tracks, markers, workarea)
|
||||
task.rs task.h (background tasks over oaktask)
|
||||
ipc.rs ipc.h (shm/framepool half): SpscRingBuffer + FrameSlotPool
|
||||
+ POSIX SharedMemoryRegion (the real frame-slot transport)
|
||||
worker.rs worker.h (render worker): backend selection via the
|
||||
oakrender module C ABI + fallback, session, NDJSON main
|
||||
deferred.rs documented deferrals (stub detail lives here and in the
|
||||
family modules' stub bodies)
|
||||
tests/
|
||||
@@ -39,11 +35,9 @@ tests/
|
||||
|
||||
### Dependencies
|
||||
|
||||
The facade's regular dependencies are `serde`/`serde_json` (the worker's
|
||||
NDJSON control-plane protocol, `src/worker.rs`), `libc` (POSIX
|
||||
`shm_open`/`mmap`/`munmap`/`shm_unlink` for `src/ipc.rs`) and `thiserror`
|
||||
(the `Display` + `std::error::Error` impl for the facade error enum,
|
||||
`src/error.rs`). Every module
|
||||
The facade's regular dependency is `thiserror` (the `Display` +
|
||||
`std::error::Error` impl for the facade error enum, `src/error.rs`). Every
|
||||
module
|
||||
call crosses the module C ABI as an `extern "C"` import (`src/bridge/`),
|
||||
and the module crates themselves are real dependencies: [`linkage`](src/linkage.rs)
|
||||
anchors them so their `#[no_mangle]` exports are linked into the
|
||||
@@ -84,17 +78,17 @@ convention: the return value is the length excluding the NUL
|
||||
| plugin | plugin.h | 4 | callbacks are facade state; push_button stubbed (no module API) |
|
||||
| codec | encoding.h | 81/85 | metadata family over oakcodec (`include/codec/format.h`); params handle is a facade box over the `oakcodec_encoding_params` POD; presets/load-save/export stubs |
|
||||
| render | renderer.h, color.h, lut.h | 60/60 | renderer over oakrender tickets; frame accessors over `OakCodecFrame`; color processor over `oakrender_color_processor_*`; color-manager list queries + LUT library stubs |
|
||||
| worker | worker.h | 8 | the render worker: `oakengine_worker_main` + the session family over the oakrender display renderer C ABI (dynamic → OpenGL fallback) and the ipc transport (see `src/worker.rs`) |
|
||||
| ipc | ipc.h (shm/framepool) | 28 | named shared-memory segments + frame-slot pools over `SpscRingBuffer` — the real frame-slot transport (see `src/ipc.rs`); the control-plane message serializers (`oakengine_ipc_*_to_json/parse`) are not wrapped (the worker speaks the NDJSON protocol with serde) |
|
||||
| node | node.h, project.h, footage.h | 226/327 | the node graph, project and footage families over the oaknode C ABI (`include/node/*.h`); 101 documented stubs where the module lacks the surface (gizmos, plugin messages, input properties, brush, thumbnail/waveform caches, shape/subtitle, keyframe enumeration, ...) — see the stub bodies |
|
||||
| timeline | timeline.h | 126/139 | sequences/clips/tracks/markers/workarea over oaknode + oaktimeline; 13 documented stubs (ripple-tracks command, default transitions, move-track/clip, marker-create, auto-cache, cache invalidation, multicam find/switch — module-surface gaps, see the stub bodies) |
|
||||
| task | task.h | 27 | the background-task system over oaktask (manager + load/save/import/export creators + result accessors); `create_proxy` stubbed (the module has no proxy-task C creator); start-time/is-cancelled are facade-approximated |
|
||||
|
||||
Deferred/stub detail lives in [`deferred`] and in the stub bodies' doc
|
||||
comments. `worker` and `ipc` were in the deferred list until the render
|
||||
worker landed here — the worker's runtime and the shared-memory frame-slot
|
||||
transport are now real (see `src/worker.rs` / `src/ipc.rs`); the ipc.h
|
||||
control-plane message serializers remain unwrapped.
|
||||
comments. The worker/IPC families (`worker.h`/`ipc.h`) are **not** part of
|
||||
the facade anymore: the frozen C++ ABI does not include them, so the
|
||||
render worker's runtime and the shared-memory frame-slot transport moved
|
||||
into the `oak-worker` crate (`crates/oak-worker/src/{worker,ipc}.rs`,
|
||||
self-contained, direct Rust calls into oakrender); the ipc.h control-plane
|
||||
message serializers remain unwrapped.
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -121,14 +115,11 @@ binaries keep the in-crate mocks (ffmpeg_bridge stub / render mocks):
|
||||
|
||||
Families whose wrapped behavior requires the real module dylibs carry
|
||||
`#[ignore]` tests with a documented reason; the smoke tests here exercise
|
||||
the module crates' real implementations. The ipc tests create+attach two
|
||||
in-process mappings of real POSIX segments and exchange slot indices and
|
||||
payloads in both directions, including wraparound and full/empty edges.
|
||||
the module crates' real implementations.
|
||||
|
||||
```
|
||||
cargo test # 71 tests green + 1 ignored (lib 35: ipc 17 + worker 18;
|
||||
# integration: undo 3, common 4, audio 4, plugin 3, codec 5,
|
||||
# render 6, linkage 1, node 3, timeline 2 + 1 ignored, task 5)
|
||||
cargo test # lib tests + integration families (undo, common, audio,
|
||||
# plugin, codec, render, linkage, node, timeline, task)
|
||||
cargo build # cdylib embeds the module C ABIs; oakcore_*/fb_* stay
|
||||
# runtime lookups (see build.rs)
|
||||
```
|
||||
|
||||
@@ -36,18 +36,22 @@ fn main() {
|
||||
println!("cargo:rustc-cdylib-link-arg=-Wl,-rpath,/usr/lib");
|
||||
}
|
||||
// The dlsym codec bridge (M12 P0) resolves `oakcodec_*` from the
|
||||
// process-global scope; the engine test binaries statically link the
|
||||
// module crates, so their symbols must be exported from the main
|
||||
// executable.
|
||||
println!("cargo:rustc-link-arg-tests=-Wl,-export_dynamic");
|
||||
// process-global scope; the engine's unit-test binary (the former
|
||||
// integration tests live in src/test_support/) statically links the
|
||||
// module crates, so their symbols must be exported from the test
|
||||
// executable. `cargo:rustc-link-arg-tests` is NOT usable: the facade
|
||||
// is cdylib-only, so cargo reports "does not have a test target" for
|
||||
// that directive — use the generic `rustc-link-arg` (harmless no-op
|
||||
// for the cdylib link itself).
|
||||
println!("cargo:rustc-link-arg=-Wl,-export_dynamic");
|
||||
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") {
|
||||
// The bundled OpenColorIO's macos system monitor references
|
||||
// IOKit / ColorSync / CoreGraphics display APIs; the engine
|
||||
// dylib links with `-undefined,dynamic_lookup`, so test binaries
|
||||
// must resolve them.
|
||||
for fw in ["IOKit", "ColorSync", "CoreGraphics"] {
|
||||
println!("cargo:rustc-link-arg-tests=-framework");
|
||||
println!("cargo:rustc-link-arg-tests={fw}");
|
||||
println!("cargo:rustc-link-arg=-framework");
|
||||
println!("cargo:rustc-link-arg={fw}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
use std::ffi::{c_char, c_double, c_int, c_void};
|
||||
|
||||
use crate::bridge::audio as a;
|
||||
use crate::stubs::audio as a;
|
||||
use crate::error::Error;
|
||||
use crate::handle::{
|
||||
box_handle, free_box, guard, guard_i64, guard_int, guard_void, unbox, CHandle,
|
||||
@@ -445,6 +445,51 @@ pub unsafe extern "C" fn oakengine_audio_sync_place_by_waveform_offset(
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Waveform extraction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `oakengine_waveform_extract` — two-stage whole-file min/max waveform
|
||||
/// extraction of `filename`'s audio stream (the module's
|
||||
/// `oakaudio_waveform_extract`, M12 P4).
|
||||
///
|
||||
/// First call with `out_pairs == NULL` / `capacity_points == 0` returns the
|
||||
/// required point count without writing; the channel count is reported
|
||||
/// whenever `out_channel_count` is non-NULL. The data pass writes
|
||||
/// `point_count * channel_count` channel-interleaved pairs (the module's
|
||||
/// `oakaudio_min_max` POD; `capacity_points` counts points) and returns the
|
||||
/// point count. Returns a negative facade `OAKENGINE_E_INVALID` for NULL
|
||||
/// `filename` / negative `stream_index` / non-positive `samples_per_point`
|
||||
/// / negative `capacity_points`; module decode errors (`OAKAUDIO_E_NOT_FOUND`,
|
||||
/// ...) pass through untranslated.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_waveform_extract(
|
||||
filename: *const c_char,
|
||||
stream_index: c_int,
|
||||
samples_per_point: c_int,
|
||||
out_pairs: *mut a::MinMax,
|
||||
capacity_points: c_int,
|
||||
out_channel_count: *mut c_int,
|
||||
) -> c_int {
|
||||
guard_int(|| unsafe {
|
||||
if filename.is_null() || stream_index < 0 || samples_per_point <= 0 || capacity_points < 0 {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let n = a::oakaudio_waveform_extract(
|
||||
filename,
|
||||
stream_index,
|
||||
samples_per_point,
|
||||
out_pairs,
|
||||
capacity_points,
|
||||
out_channel_count,
|
||||
);
|
||||
if n < 0 {
|
||||
return Err(Error::Module(n));
|
||||
}
|
||||
Ok(n)
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Audio processor
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,378 +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/>.
|
||||
|
||||
//! oakaudio C ABI bridge: direct Rust calls into the `oakaudio` crate.
|
||||
//!
|
||||
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
|
||||
//! call below is a compile-time Rust call into `oakaudio`'s `ffi` (the
|
||||
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
|
||||
//! internal callers bypass them). Handles cross as the shared
|
||||
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
|
||||
//! declaration (resolved at link time against the sibling crate in the
|
||||
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
|
||||
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
|
||||
|
||||
// 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/>.
|
||||
|
||||
//! oakaudio C ABI imports, mirroring the oakaudio crate's exports
|
||||
//! (`src/audio/rust/src/ffi.rs`; headers `include/audio/*.h`), plus the
|
||||
//! liboakcore `oakcore_audioparams_*` readers used to convert the
|
||||
//! engine's borrowed `OakAudioParams*` handles.
|
||||
|
||||
use std::ffi::{c_char, c_double, c_int, c_void};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// `oakaudio_offsetresult` C ABI POD. Single-lib unification: aliases
|
||||
/// the oakaudio crate's struct (identical layout).
|
||||
pub type OffsetResult = oakaudio::ffi::sync::OffsetResult;
|
||||
|
||||
/// `oakaudio_stretchoffsetresult` C ABI POD. Single-lib unification: aliases
|
||||
/// the oakaudio crate's struct (identical layout).
|
||||
pub type StretchOffsetResult = oakaudio::ffi::sync::StretchOffsetResult;
|
||||
|
||||
/// `oakaudio_sourceclip` C ABI POD. Single-lib unification: aliases
|
||||
/// the oakaudio crate's struct (identical layout).
|
||||
pub type SourceClip = oakaudio::ffi::sync::SourceClip;
|
||||
|
||||
/// `oakaudio` recording-params POD — single-lib unification: aliases the
|
||||
/// oakaudio crate's type (itself the oakcodec `oakcodec_encoding_params`).
|
||||
pub type EncodingParams = oakaudio::bridge::codec::EncodingParams;
|
||||
|
||||
extern "C" {
|
||||
pub fn oakcore_audioparams_create(
|
||||
sample_rate: c_int,
|
||||
channel_layout: u64,
|
||||
format: c_int,
|
||||
) -> *mut c_void;
|
||||
pub fn oakcore_audioparams_free(params: *mut c_void);
|
||||
pub fn oakcore_audioparams_sample_rate(params: *const c_void) -> c_int;
|
||||
/// `oakcore_audioparams_set_time_base` (host-provided).
|
||||
pub fn oakcore_audioparams_set_time_base(params: *mut c_void, num: c_int, den: c_int);
|
||||
pub fn oakcore_audioparams_channel_layout(params: *const c_void) -> u64;
|
||||
pub fn oakcore_audioparams_format(params: *const c_void) -> c_int;
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_create_instance() -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_create_instance() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_destroy_instance() {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_destroy_instance() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_instance() -> CHandle {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_instance() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_free(_self: *mut CHandle) {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_free(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_set_output_notify_interval(_self: CHandle, bytes: i64) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_set_output_notify_interval(_self, bytes) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_push_to_output(
|
||||
_self: CHandle,
|
||||
rate: c_int,
|
||||
layout: u64,
|
||||
format: c_int,
|
||||
samples: *const c_char,
|
||||
samples_size: i64,
|
||||
error_buf: *mut c_char,
|
||||
error_buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakaudio::ffi::manager::oakaudio_manager_push_to_output(
|
||||
_self,
|
||||
rate,
|
||||
layout,
|
||||
format,
|
||||
samples,
|
||||
samples_size,
|
||||
error_buf,
|
||||
error_buf_size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_clear_buffered_output(_self: CHandle) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_clear_buffered_output(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_stop_output(_self: CHandle) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_stop_output(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_reset_output_clock(_self: CHandle) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_reset_output_clock(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_get_output_device(_self: CHandle) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_get_output_device(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_set_output_device(_self: CHandle, device: c_int) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_set_output_device(_self, device) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_get_input_device(_self: CHandle) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_get_input_device(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_set_input_device(_self: CHandle, device: c_int) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_set_input_device(_self, device) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_hard_reset(_self: CHandle) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_hard_reset(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
/// `oakaudio_manager_start_recording` — crosses the encoding-params C ABI
|
||||
/// POD (the facade keeps its own opaque mirror; the module's
|
||||
/// `oakaudio::bridge::codec::EncodingParams`). Kept as a link-time
|
||||
/// `extern "C"` declaration against the frozen module C ABI.
|
||||
pub fn oakaudio_manager_start_recording(
|
||||
_self: CHandle,
|
||||
params: *const EncodingParams,
|
||||
error_buf: *mut c_char,
|
||||
error_buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakaudio::ffi::manager::oakaudio_manager_start_recording(
|
||||
_self,
|
||||
params,
|
||||
error_buf,
|
||||
error_buf_size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_stop_recording(_self: CHandle) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_stop_recording(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_seconds(_self: CHandle, out: *mut c_double) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_seconds(_self, out) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_output_levels(_self: CHandle, peaks: *mut f32, capacity: c_int) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_output_levels(_self, peaks, capacity) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_processor_init() -> CHandle {
|
||||
unsafe { oakaudio::ffi::processor::oakaudio_processor_init() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_processor_free(_self: *mut CHandle) {
|
||||
unsafe { oakaudio::ffi::processor::oakaudio_processor_free(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_processor_open(
|
||||
_self: CHandle,
|
||||
in_rate: c_int,
|
||||
in_layout: u64,
|
||||
in_format: c_int,
|
||||
out_rate: c_int,
|
||||
out_layout: u64,
|
||||
out_format: c_int,
|
||||
speed: c_double,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakaudio::ffi::processor::oakaudio_processor_open(
|
||||
_self, in_rate, in_layout, in_format, out_rate, out_layout, out_format, speed,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_processor_close(_self: CHandle) -> c_int {
|
||||
unsafe { oakaudio::ffi::processor::oakaudio_processor_close(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_processor_is_open(_self: CHandle) -> c_int {
|
||||
unsafe { oakaudio::ffi::processor::oakaudio_processor_is_open(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_sync_estimate_envelope_offset(
|
||||
reference: *const c_double,
|
||||
reference_len: c_int,
|
||||
candidate: *const c_double,
|
||||
candidate_len: c_int,
|
||||
reference_valid: *const u8,
|
||||
candidate_valid: *const u8,
|
||||
window_samples: u64,
|
||||
max_offset_windows: i64,
|
||||
out: *mut OffsetResult,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakaudio::ffi::sync::oakaudio_sync_estimate_envelope_offset(
|
||||
reference,
|
||||
reference_len,
|
||||
candidate,
|
||||
candidate_len,
|
||||
reference_valid,
|
||||
candidate_valid,
|
||||
window_samples,
|
||||
max_offset_windows,
|
||||
out,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_sync_estimate_stretch_and_offset(
|
||||
reference: *const c_double,
|
||||
reference_len: c_int,
|
||||
candidate: *const c_double,
|
||||
candidate_len: c_int,
|
||||
reference_valid: *const u8,
|
||||
candidate_valid: *const u8,
|
||||
window_samples: u64,
|
||||
max_offset_windows: i64,
|
||||
min_rate: c_double,
|
||||
max_rate: c_double,
|
||||
rate_step: c_double,
|
||||
out: *mut StretchOffsetResult,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakaudio::ffi::sync::oakaudio_sync_estimate_stretch_and_offset(
|
||||
reference,
|
||||
reference_len,
|
||||
candidate,
|
||||
candidate_len,
|
||||
reference_valid,
|
||||
candidate_valid,
|
||||
window_samples,
|
||||
max_offset_windows,
|
||||
min_rate,
|
||||
max_rate,
|
||||
rate_step,
|
||||
out,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_sync_place_by_source_time(
|
||||
reference: *const SourceClip,
|
||||
candidate: *const SourceClip,
|
||||
reference_timeline_in_num: i64,
|
||||
reference_timeline_in_den: i64,
|
||||
out_num: *mut i64,
|
||||
out_den: *mut i64,
|
||||
out_valid: *mut c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakaudio::ffi::sync::oakaudio_sync_place_by_source_time(
|
||||
reference,
|
||||
candidate,
|
||||
reference_timeline_in_num,
|
||||
reference_timeline_in_den,
|
||||
out_num,
|
||||
out_den,
|
||||
out_valid,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_sync_place_by_waveform_offset(
|
||||
reference_timeline_in_num: i64,
|
||||
reference_timeline_in_den: i64,
|
||||
candidate_offset_samples: i64,
|
||||
sample_rate: c_int,
|
||||
out_num: *mut i64,
|
||||
out_den: *mut i64,
|
||||
out_valid: *mut c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakaudio::ffi::sync::oakaudio_sync_place_by_waveform_offset(
|
||||
reference_timeline_in_num,
|
||||
reference_timeline_in_den,
|
||||
candidate_offset_samples,
|
||||
sample_rate,
|
||||
out_num,
|
||||
out_den,
|
||||
out_valid,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,242 +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 bridge: direct Rust calls into the `oakcodec` crate.
|
||||
//!
|
||||
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
|
||||
//! call below is a compile-time Rust call into `oakcodec`'s `ffi` (the
|
||||
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
|
||||
//! internal callers bypass them). Handles cross as the shared
|
||||
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
|
||||
//! declaration (resolved at link time against the sibling crate in the
|
||||
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
|
||||
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
|
||||
|
||||
// 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 imports, mirroring the oakcodec crate's exports
|
||||
//! (`src/codec/rust/src/ffi/{format,encoder}.rs`; headers
|
||||
//! `include/codec/{format,encoder}.h`). Also carries the
|
||||
//! `oakcodec_encoding_params` POD the facade's encoding-params handle
|
||||
//! wraps, and the oakaudio recording-params pointer pass-through.
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// `include/codec/encoder.h` — the encoding-params POD, a complete mirror
|
||||
/// `olive::EncodingParams` — single-lib unification: aliases the oakcodec
|
||||
/// crate's POD (`oakcodec_encoding_params`, identical `#[repr(C)]` layout;
|
||||
/// the engine's `OakEngineEncodingParams` handle is a heap box over exactly
|
||||
/// this struct, so every engine getter/setter reads/writes a field and
|
||||
/// `encoder_init`/recording can consume the pointer directly).
|
||||
pub type EncodingParamsPOD = oakcodec::ffi::encoder::oakcodec_encoding_params;
|
||||
|
||||
/// Zeroed encoding-params POD (all fields 0 / NUL). The codec crate's
|
||||
/// struct has no zeroed constructor; this facade helper provides it.
|
||||
pub fn zeroed_encoding_params() -> EncodingParamsPOD {
|
||||
// All-C fields: all-zero is a valid value.
|
||||
unsafe { std::mem::zeroed() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_format_count() -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_count() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_format_name(format: c_int, buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_name(format, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_format_extension(
|
||||
format: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_extension(format, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_format_video_codec_count(format: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_video_codec_count(format) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_format_video_codec_at(format: c_int, index: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_video_codec_at(format, index) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_format_audio_codec_count(format: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_audio_codec_count(format) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_format_audio_codec_at(format: c_int, index: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_audio_codec_at(format, index) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_format_subtitle_codec_count(format: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_subtitle_codec_count(format) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_format_subtitle_codec_at(format: c_int, index: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_subtitle_codec_at(format, index) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_codec_name(codec: c_int, buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_codec_name(codec, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_codec_is_still_image(codec: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_codec_is_still_image(codec) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_codec_is_lossless(codec: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_codec_is_lossless(codec) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_pix_fmt_count(format: c_int, codec: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_pix_fmt_count(format, codec) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_pix_fmt_at(
|
||||
format: c_int,
|
||||
codec: c_int,
|
||||
index: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcodec::ffi::format::oakcodec_encoding_pix_fmt_at(format, codec, index, buf, buf_size)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_pix_fmt_index(codec: c_int, pix_fmt: *const c_char) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_pix_fmt_index(codec, pix_fmt) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_sample_format_count(format: c_int, codec: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_sample_format_count(format, codec) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_sample_format_at(format: c_int, codec: c_int, index: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_sample_format_at(format, codec, index) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_filename_contains_digit_placeholder(filename: *const c_char) -> c_int {
|
||||
unsafe {
|
||||
oakcodec::ffi::format::oakcodec_encoding_filename_contains_digit_placeholder(filename)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_image_sequence_digit_count(filename: *const c_char) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_image_sequence_digit_count(filename) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_filename_remove_digit_placeholder(
|
||||
filename: *const c_char,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcodec::ffi::format::oakcodec_encoding_filename_remove_digit_placeholder(
|
||||
filename, buf, buf_size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_generate_matrix(
|
||||
method: c_int,
|
||||
src_width: c_int,
|
||||
src_height: c_int,
|
||||
dst_width: c_int,
|
||||
dst_height: c_int,
|
||||
out_matrix: *mut f64,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcodec::ffi::encoder::oakcodec_encoding_generate_matrix(
|
||||
method, src_width, src_height, dst_width, dst_height, out_matrix,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
/// `oakcodec_encoder_init` — crosses the encoding-params C ABI POD
|
||||
/// (`oakcodec_encoding_params`; the facade keeps its own POD mirror).
|
||||
/// Kept as a link-time `extern "C"` declaration against the frozen module
|
||||
/// C ABI.
|
||||
pub fn oakcodec_encoder_init(params: *const EncodingParamsPOD) -> CHandle {
|
||||
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_init(params) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoder_free(encoder: *mut CHandle) {
|
||||
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_free(encoder) }
|
||||
}
|
||||
@@ -1,889 +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 bridge: direct Rust calls into the `oakcommon` crate.
|
||||
//!
|
||||
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
|
||||
//! call below is a compile-time Rust call into `oakcommon`'s `ffi` (the
|
||||
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
|
||||
//! internal callers bypass them). Handles cross as the shared
|
||||
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
|
||||
//! declaration (resolved at link time against the sibling crate in the
|
||||
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
|
||||
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
|
||||
|
||||
// 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 imports, mirroring the oakcommon crate's exports
|
||||
//! (`src/common/rust/src/ffi.rs`; headers `include/common/*.h`). Only the
|
||||
//! families the facade wraps: config, videoparams, colortransform, xml
|
||||
//! reader/writer and the decibel helpers.
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// `include/common/config.h` — error handler callback.
|
||||
pub type ConfigErrorHandler = Option<
|
||||
unsafe extern "C" fn(title: *const c_char, message: *const c_char, userdata: *mut c_void),
|
||||
>;
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_load() -> c_int {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_load() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_save() -> c_int {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_save() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_reset_defaults() -> c_int {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_reset_defaults() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_set(group: *const c_char, key: *const c_char, value: *const c_char) {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_set(group, key, value) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_get(
|
||||
group: *const c_char,
|
||||
key: *const c_char,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_get(group, key, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_get_int(
|
||||
group: *const c_char,
|
||||
key: *const c_char,
|
||||
fallback: c_int,
|
||||
) -> c_int {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_get_int(group, key, fallback) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_get_int64(group: *const c_char, key: *const c_char, fallback: i64) -> i64 {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_get_int64(group, key, fallback) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_get_double(group: *const c_char, key: *const c_char, fallback: f64) -> f64 {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_get_double(group, key, fallback) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_get_bool(
|
||||
group: *const c_char,
|
||||
key: *const c_char,
|
||||
fallback: c_int,
|
||||
) -> c_int {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_get_bool(group, key, fallback) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_set_int(group: *const c_char, key: *const c_char, value: c_int) {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_set_int(group, key, value) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_set_int64(group: *const c_char, key: *const c_char, value: i64) {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_set_int64(group, key, value) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_set_double(group: *const c_char, key: *const c_char, value: f64) {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_set_double(group, key, value) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_set_bool(group: *const c_char, key: *const c_char, value: c_int) {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_set_bool(group, key, value) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_entry_type(group: *const c_char, key: *const c_char) -> c_int {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_entry_type(group, key) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_set_error_handler(
|
||||
handler: ConfigErrorHandler,
|
||||
userdata: *mut c_void,
|
||||
) -> c_int {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_set_error_handler(handler, userdata) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_init() -> CHandle {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_init() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_init_basic(
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
pixel_format: c_int,
|
||||
nb_channels: c_int,
|
||||
pixel_aspect_num: c_int,
|
||||
pixel_aspect_den: c_int,
|
||||
interlacing: c_int,
|
||||
divider: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_init_basic(
|
||||
width,
|
||||
height,
|
||||
pixel_format,
|
||||
nb_channels,
|
||||
pixel_aspect_num,
|
||||
pixel_aspect_den,
|
||||
interlacing,
|
||||
divider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_init_with_time_base(
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
time_base_num: c_int,
|
||||
time_base_den: c_int,
|
||||
pixel_format: c_int,
|
||||
nb_channels: c_int,
|
||||
pixel_aspect_num: c_int,
|
||||
pixel_aspect_den: c_int,
|
||||
interlacing: c_int,
|
||||
divider: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_init_with_time_base(
|
||||
width,
|
||||
height,
|
||||
time_base_num,
|
||||
time_base_den,
|
||||
pixel_format,
|
||||
nb_channels,
|
||||
pixel_aspect_num,
|
||||
pixel_aspect_den,
|
||||
interlacing,
|
||||
divider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_free(params: *mut CHandle) {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_free(params) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_width(params: CHandle, width: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_width(params, width) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_width(params: CHandle, width: c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_width(params, width) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_height(params: CHandle, height: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_height(params, height) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_height(params: CHandle, height: c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_height(params, height) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_time_base(
|
||||
params: CHandle,
|
||||
numerator: *mut c_int,
|
||||
denominator: *mut c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_time_base(
|
||||
params,
|
||||
numerator,
|
||||
denominator,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_time_base(
|
||||
params: CHandle,
|
||||
numerator: c_int,
|
||||
denominator: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_set_time_base(
|
||||
params,
|
||||
numerator,
|
||||
denominator,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_frame_rate(
|
||||
params: CHandle,
|
||||
numerator: *mut c_int,
|
||||
denominator: *mut c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_frame_rate(
|
||||
params,
|
||||
numerator,
|
||||
denominator,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_frame_rate(
|
||||
params: CHandle,
|
||||
numerator: c_int,
|
||||
denominator: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_set_frame_rate(
|
||||
params,
|
||||
numerator,
|
||||
denominator,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_pixel_aspect_ratio(
|
||||
params: CHandle,
|
||||
numerator: *mut c_int,
|
||||
denominator: *mut c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_pixel_aspect_ratio(
|
||||
params,
|
||||
numerator,
|
||||
denominator,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_pixel_aspect_ratio(
|
||||
params: CHandle,
|
||||
numerator: c_int,
|
||||
denominator: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_set_pixel_aspect_ratio(
|
||||
params,
|
||||
numerator,
|
||||
denominator,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_format(params: CHandle, format: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_format(params, format) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_format(params: CHandle, format: c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_format(params, format) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_interlacing(params: CHandle, interlacing: *mut c_int) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_interlacing(params, interlacing)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_interlacing(params: CHandle, interlacing: c_int) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_set_interlacing(params, interlacing)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_divider(params: CHandle, divider: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_divider(params, divider) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_divider(params: CHandle, divider: c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_divider(params, divider) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_video_type(params: CHandle, type_: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_video_type(params, type_) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_video_type(params: CHandle, type_: c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_video_type(params, type_) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_premultiplied_alpha(
|
||||
params: CHandle,
|
||||
premultiplied: *mut c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_premultiplied_alpha(
|
||||
params,
|
||||
premultiplied,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_premultiplied_alpha(
|
||||
params: CHandle,
|
||||
premultiplied: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_set_premultiplied_alpha(
|
||||
params,
|
||||
premultiplied,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_color_range(params: CHandle, color_range: *mut c_int) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_color_range(params, color_range)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_color_range(params: CHandle, color_range: c_int) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_set_color_range(params, color_range)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_is_valid(params: CHandle, valid: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_is_valid(params, valid) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_effective_width(params: CHandle, width: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_effective_width(params, width) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_effective_height(params: CHandle, height: *mut c_int) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_effective_height(params, height)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_bytes_per_pixel(params: CHandle, bytes: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_bytes_per_pixel(params, bytes) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_equals(a: CHandle, b: CHandle, out_equal: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_equals(a, b, out_equal) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_format_is_float(pixel_format: c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_format_is_float(pixel_format) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_format_name(
|
||||
pixel_format: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_format_name(
|
||||
pixel_format,
|
||||
buf,
|
||||
buf_size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_frame_rate_to_string(
|
||||
numerator: c_int,
|
||||
denominator: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_frame_rate_to_string(
|
||||
numerator,
|
||||
denominator,
|
||||
buf,
|
||||
buf_size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_name_for_divider(
|
||||
divider: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_name_for_divider(
|
||||
divider, buf, buf_size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_scaled_dimension(dimension: c_int, divider: c_int) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_scaled_dimension(dimension, divider)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_generate_auto_divider(width: i64, height: i64) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_generate_auto_divider(width, height)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_divider_for_target_resolution(
|
||||
src_width: c_int,
|
||||
src_height: c_int,
|
||||
target_width: c_int,
|
||||
target_height: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_divider_for_target_resolution(
|
||||
src_width,
|
||||
src_height,
|
||||
target_width,
|
||||
target_height,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_bytes_per_channel_for_format(pixel_format: c_int) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_bytes_per_channel_for_format(
|
||||
pixel_format,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_bytes_per_pixel_for_format(
|
||||
pixel_format: c_int,
|
||||
channels: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_bytes_per_pixel_for_format(
|
||||
pixel_format,
|
||||
channels,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_static_get_bytes_per_pixel(
|
||||
pixel_format: c_int,
|
||||
channels: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_static_get_bytes_per_pixel(
|
||||
pixel_format,
|
||||
channels,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_buffer_size(params: CHandle, size: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_buffer_size(params, size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_time_in_timebase_units(
|
||||
params: CHandle,
|
||||
time_num: c_int,
|
||||
time_den: c_int,
|
||||
timestamp: *mut i64,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_time_in_timebase_units(
|
||||
params, time_num, time_den, timestamp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_colortransform_init_output(output: *const c_char) -> CHandle {
|
||||
unsafe { oakcommon::ffi::colortransform::oakcommon_colortransform_init_output(output) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_colortransform_init_display(
|
||||
display: *const c_char,
|
||||
view: *const c_char,
|
||||
look: *const c_char,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oakcommon::ffi::colortransform::oakcommon_colortransform_init_display(display, view, look)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_colortransform_free(transform: *mut CHandle) {
|
||||
unsafe { oakcommon::ffi::colortransform::oakcommon_colortransform_free(transform) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_colortransform_is_display(transform: CHandle) -> c_int {
|
||||
unsafe { oakcommon::ffi::colortransform::oakcommon_colortransform_is_display(transform) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_colortransform_get_display(
|
||||
transform: CHandle,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::colortransform::oakcommon_colortransform_get_display(
|
||||
transform, buf, buf_size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_colortransform_get_output(
|
||||
transform: CHandle,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::colortransform::oakcommon_colortransform_get_output(
|
||||
transform, buf, buf_size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_colortransform_get_view(
|
||||
transform: CHandle,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::colortransform::oakcommon_colortransform_get_view(transform, buf, buf_size)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_colortransform_get_look(
|
||||
transform: CHandle,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::colortransform::oakcommon_colortransform_get_look(transform, buf, buf_size)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_reader_init(data: *const c_char) -> CHandle {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_init(data) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_reader_free(reader: *mut CHandle) {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_free(reader) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_reader_read_next_start_element(reader: CHandle, found: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_read_next_start_element(reader, found) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_reader_name(reader: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_name(reader, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_reader_read_element_text(
|
||||
reader: CHandle,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_reader_read_element_text(reader, buf, buf_size)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_reader_skip_current_element(reader: CHandle) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_skip_current_element(reader) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_reader_attribute_count(reader: CHandle, count: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_count(reader, count) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_reader_attribute_name(
|
||||
reader: CHandle,
|
||||
index: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_name(reader, index, buf, buf_size)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_reader_attribute_value(
|
||||
reader: CHandle,
|
||||
index: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_value(reader, index, buf, buf_size)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_reader_has_error(reader: CHandle, has_error: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_has_error(reader, has_error) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_writer_init() -> CHandle {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_init() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_writer_free(writer: *mut CHandle) {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_free(writer) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_writer_write_start_element(writer: CHandle, name: *const c_char) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_start_element(writer, name) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_writer_write_attribute(
|
||||
writer: CHandle,
|
||||
name: *const c_char,
|
||||
value: *const c_char,
|
||||
) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_attribute(writer, name, value) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_writer_write_characters(writer: CHandle, text: *const c_char) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_characters(writer, text) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_writer_write_text_element(
|
||||
writer: CHandle,
|
||||
name: *const c_char,
|
||||
text: *const c_char,
|
||||
) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_text_element(writer, name, text) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_writer_write_end_element(writer: CHandle) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_end_element(writer) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_writer_write_end_document(writer: CHandle) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_end_document(writer) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_writer_output(writer: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_output(writer, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_decibel_from_linear(linear: f64, out_db: *mut f64) -> c_int {
|
||||
unsafe { oakcommon::ffi::misc::oakcommon_decibel_from_linear(linear, out_db) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_decibel_to_linear(db: f64, out_linear: *mut f64) -> c_int {
|
||||
unsafe { oakcommon::ffi::misc::oakcommon_decibel_to_linear(db, out_linear) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_decibel_from_logarithmic(logarithmic: f64, out_db: *mut f64) -> c_int {
|
||||
unsafe { oakcommon::ffi::misc::oakcommon_decibel_from_logarithmic(logarithmic, out_db) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_decibel_to_logarithmic(db: f64, out_logarithmic: *mut f64) -> c_int {
|
||||
unsafe { oakcommon::ffi::misc::oakcommon_decibel_to_logarithmic(db, out_logarithmic) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_decibel_linear_to_logarithmic(linear: f64, out_logarithmic: *mut f64) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::misc::oakcommon_decibel_linear_to_logarithmic(linear, out_logarithmic)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_decibel_logarithmic_to_linear(logarithmic: f64, out_linear: *mut f64) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::misc::oakcommon_decibel_logarithmic_to_linear(logarithmic, out_linear)
|
||||
}
|
||||
}
|
||||
@@ -1,43 +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 the oak modules, one submodule per module crate.
|
||||
//!
|
||||
//! The facade consumes the module C ABIs (`include/<mod>/*.h`) purely as
|
||||
//! `extern "C"` imports — it never links the module crates at build time.
|
||||
//! At the final app link the symbols resolve against the module shared
|
||||
//! libraries; `cargo test` resolves them against the module crates' rlibs
|
||||
//! (dev-dependencies, see Cargo.toml).
|
||||
//!
|
||||
//! **Signatures are declared from the module crates' actual `#[no_mangle]`
|
||||
//! exports** (their `src/*/ffi*` modules), not from memory of the include
|
||||
//! headers — module bridges in sibling crates have drifted from the real
|
||||
//! ABI before. Every handle crosses the boundary as [`crate::handle::CHandle`]
|
||||
//! (structurally identical to every `Oak<Mod><Type>` value handle).
|
||||
//!
|
||||
//! Only functions the module crates actually implement are declared here;
|
||||
//! engine functions whose backing is still C++-only are facade stubs (see
|
||||
//! the area modules) and never reach this module.
|
||||
|
||||
pub mod audio;
|
||||
pub mod codec;
|
||||
pub mod common;
|
||||
pub mod node;
|
||||
pub mod plugin;
|
||||
pub mod render;
|
||||
pub mod task;
|
||||
pub mod timeline;
|
||||
pub mod undo;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,81 +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/>.
|
||||
|
||||
//! oakplugin C ABI bridge: direct Rust calls into the `oakplugin` crate.
|
||||
//!
|
||||
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
|
||||
//! call below is a compile-time Rust call into `oakplugin`'s `ffi` (the
|
||||
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
|
||||
//! internal callers bypass them). Handles cross as the shared
|
||||
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
|
||||
//! declaration (resolved at link time against the sibling crate in the
|
||||
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
|
||||
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
|
||||
|
||||
// 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/>.
|
||||
|
||||
//! oakplugin C ABI imports, mirroring the oakplugin crate's exports
|
||||
//! (`src/plugin/rust/src/ffi.rs`; headers `include/plugin/*.h`).
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
/// Direct call into the `oakplugin` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakplugin_host_scan(bundle_dirs: *const *const c_char, dir_count: c_int) -> c_int {
|
||||
unsafe { oakplugin::ffi::oakplugin_host_scan(bundle_dirs, dir_count) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakplugin` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakplugin_host_init() -> c_int {
|
||||
unsafe { oakplugin::ffi::oakplugin_host_init() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakplugin` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakplugin_host_plugin_count() -> c_int {
|
||||
unsafe { oakplugin::ffi::oakplugin_host_plugin_count() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakplugin` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakplugin_host_plugin_id_at(index: c_int, buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
unsafe { oakplugin::ffi::oakplugin_host_plugin_id_at(index, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakplugin` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakplugin_host_plugin_label(
|
||||
plugin_id: *const c_char,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe { oakplugin::ffi::oakplugin_host_plugin_label(plugin_id, buf, buf_size) }
|
||||
}
|
||||
@@ -1,340 +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 bridge: direct Rust calls into the `oakrender` crate.
|
||||
//!
|
||||
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
|
||||
//! call below is a compile-time Rust call into `oakrender`'s `ffi` (the
|
||||
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
|
||||
//! internal callers bypass them). Handles cross as the shared
|
||||
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
|
||||
//! declaration (resolved at link time against the sibling crate in the
|
||||
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
|
||||
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
|
||||
|
||||
// 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, mirroring the oakrender crate's exports
|
||||
//! (`src/render/rust/src/ffi.rs`; headers `include/render/*.h`).
|
||||
|
||||
use std::ffi::{c_char, c_double, c_int, c_void};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// `include/render/ticket.h` — video render ticket params. Single-lib
|
||||
/// unification: aliases the oakrender crate's POD (same `repr(C)`
|
||||
/// layout; all handle fields are the shared [`CHandle`]).
|
||||
pub type OakVideoTicketParams = oakrender::ffi::OakVideoTicketParams;
|
||||
|
||||
/// `include/render/renderer.h` — frame video-params POD returned by
|
||||
/// `oakrender_codec_frame_get_params`. Single-lib unification: aliases
|
||||
/// the oakrender crate's POD.
|
||||
pub type OakRenderVideoParams = oakrender::ffi::OakRenderVideoParams;
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_display_renderer_create_dynamic(backend_id: *const c_char) -> CHandle {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_create_dynamic(backend_id) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_display_renderer_create_opengl() -> CHandle {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_create_opengl() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_display_renderer_init(renderer: CHandle, gl_context: *mut c_void) -> c_int {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_init(renderer, gl_context) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_display_renderer_destroy(renderer: *mut CHandle) {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_destroy(renderer) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_display_renderer_is_open_gl(renderer: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_is_open_gl(renderer) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_display_renderer_is_vulkan(renderer: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_is_vulkan(renderer) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_manager_set_aggressive_gc(enabled: c_int) -> c_int {
|
||||
unsafe { oakrender::ffi::ticket::oakrender_manager_set_aggressive_gc(enabled) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_set_cacher_multicam(multicam_or_null: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::manager::oakrender_set_cacher_multicam(multicam_or_null) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_set_display_color_processor(p_or_null: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::manager::oakrender_set_display_color_processor(p_or_null) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_ticket_render_frame(
|
||||
params: *const OakVideoTicketParams,
|
||||
cb: Option<unsafe extern "C" fn(CHandle, *mut c_void)>,
|
||||
userdata: *mut c_void,
|
||||
) -> CHandle {
|
||||
unsafe { oakrender::ffi::ticket::oakrender_ticket_render_frame(params, cb, userdata) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_ticket_render_audio(
|
||||
output_node: CHandle,
|
||||
in_num: i64,
|
||||
in_den: i64,
|
||||
out_num: i64,
|
||||
out_den: i64,
|
||||
params: *const c_void,
|
||||
mode: c_int,
|
||||
cb: Option<unsafe extern "C" fn(CHandle, *mut c_void)>,
|
||||
userdata: *mut c_void,
|
||||
montage: *const oakrender::ffi::OakMontageClip,
|
||||
montage_count: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oakrender::ffi::ticket::oakrender_ticket_render_audio(
|
||||
output_node,
|
||||
in_num,
|
||||
in_den,
|
||||
out_num,
|
||||
out_den,
|
||||
params as *const CHandle,
|
||||
mode,
|
||||
cb,
|
||||
userdata,
|
||||
montage,
|
||||
montage_count,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakrender_audio_samples_free` — release the samples block returned
|
||||
/// by `oakrender_ticket_get_samples` (NULL no-op).
|
||||
pub fn oakrender_audio_samples_free(samples: *mut c_void) {
|
||||
unsafe { oakrender::ffi::ticket::oakrender_audio_samples_free(samples) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_ticket_wait(ticket: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::ticket::oakrender_ticket_wait(ticket) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_ticket_cancel(ticket: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::ticket::oakrender_ticket_cancel(ticket) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_ticket_get_frame(ticket: CHandle, out: *mut CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::ticket::oakrender_ticket_get_frame(ticket, out) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_ticket_get_samples(ticket: CHandle, out: *mut *mut c_void) -> c_int {
|
||||
unsafe { oakrender::ffi::ticket::oakrender_ticket_get_samples(ticket, out) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_ticket_free(ticket: *mut CHandle) {
|
||||
unsafe { oakrender::ffi::ticket::oakrender_ticket_free(ticket) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_codec_frame_create() -> CHandle {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_create() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_codec_frame_retain(frame: CHandle) -> CHandle {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_retain(frame) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_codec_frame_free(frame: *mut CHandle) {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_free(frame) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_codec_frame_width(frame: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_width(frame) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_codec_frame_height(frame: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_height(frame) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_codec_frame_linesize_bytes(frame: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_linesize_bytes(frame) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_codec_frame_data(frame: CHandle) -> *mut c_void {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_data(frame) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_codec_frame_const_data(frame: CHandle) -> *const c_void {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_const_data(frame) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_codec_frame_is_allocated(frame: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_is_allocated(frame) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_codec_frame_get_params(frame: CHandle, out: *mut OakRenderVideoParams) -> c_int {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_get_params(frame, out) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_color_processor_create(
|
||||
src_space: *const c_char,
|
||||
dst_transform: *const c_char,
|
||||
direction: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oakrender::ffi::color::oakrender_color_processor_create(src_space, dst_transform, direction)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_color_processor_free(processor: *mut CHandle) {
|
||||
unsafe { oakrender::ffi::color::oakrender_color_processor_free(processor) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_color_processor_is_valid(processor: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::color::oakrender_color_processor_is_valid(processor) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_color_processor_create_transform(
|
||||
manager: CHandle,
|
||||
input: *const c_char,
|
||||
dest: CHandle,
|
||||
direction: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oakrender::ffi::color::oakrender_color_processor_create_transform(
|
||||
manager, input, dest, direction,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_color_processor_convert(
|
||||
processor: CHandle,
|
||||
ir: c_double,
|
||||
ig: c_double,
|
||||
ib: c_double,
|
||||
ia: c_double,
|
||||
out_r: *mut c_double,
|
||||
out_g: *mut c_double,
|
||||
out_b: *mut c_double,
|
||||
out_a: *mut c_double,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakrender::ffi::color::oakrender_color_processor_convert(
|
||||
processor, ir, ig, ib, ia, out_r, out_g, out_b, out_a,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_color_manager_set_up_default_config() -> c_int {
|
||||
unsafe { oakrender::ffi::color::oakrender_color_manager_set_up_default_config() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_color_manager_get_config(buf: *mut c_char, n: c_int) -> c_int {
|
||||
unsafe { oakrender::ffi::color::oakrender_color_manager_get_config(buf, n) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_lut_is_supported_extension(extension: *const c_char) -> c_int {
|
||||
unsafe { oakrender::ffi::color::oakrender_lut_is_supported_extension(extension) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_lut_supported_extensions_count() -> c_int {
|
||||
unsafe { oakrender::ffi::color::oakrender_lut_supported_extensions_count() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_lut_supported_extension_at(i: c_int, buf: *mut c_char, n: c_int) -> c_int {
|
||||
unsafe { oakrender::ffi::color::oakrender_lut_supported_extension_at(i, buf, n) }
|
||||
}
|
||||
@@ -1,309 +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/>.
|
||||
|
||||
//! oaktask C ABI bridge: direct Rust calls into the `oaktask` crate.
|
||||
//!
|
||||
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
|
||||
//! call below is a compile-time Rust call into `oaktask`'s `ffi` (the
|
||||
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
|
||||
//! internal callers bypass them). Handles cross as the shared
|
||||
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
|
||||
//! declaration (resolved at link time against the sibling crate in the
|
||||
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
|
||||
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
|
||||
|
||||
// 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/>.
|
||||
|
||||
//! oaktask C ABI imports, mirroring the oaktask crate's exports
|
||||
//! (`src/task/rust/src/ffi/{manager,task,project}.rs`; headers
|
||||
//! `include/task/*.h`).
|
||||
//!
|
||||
//! Every handle crosses as [`crate::handle::CHandle`]. `oaktask_create_export`
|
||||
//! takes the encoding-params POD ([`crate::bridge::codec::EncodingParamsPOD`],
|
||||
//! field-identical to the task crate's `OakCodecEncodingParams`). String
|
||||
//! getters report the size **including** the NUL; the facade converts with
|
||||
//! [`crate::handle::string_result`].
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// `oaktask_event_fn` callback (`include/task/task.h`): `event_id` is an
|
||||
/// `OakTaskEvent` (0=started, 1=progress, 2=finished), `value` 0..1 (or
|
||||
/// start-ms / success flag), `userdata` the subscription token.
|
||||
pub type OakTaskEventFn = unsafe extern "C" fn(event_id: c_int, value: f64, userdata: *mut c_void);
|
||||
|
||||
/// `oaktask_otio_import_confirm_fn` (`include/task/project.h`).
|
||||
pub type OakTaskOtioImportConfirmFn = unsafe extern "C" fn(
|
||||
sequence_names: *const *const c_char,
|
||||
count: c_int,
|
||||
userdata: *mut c_void,
|
||||
) -> c_int;
|
||||
|
||||
/// `oaktask_image_sequence_confirm_fn` (`include/task/project.h`).
|
||||
pub type OakTaskImageSequenceConfirmFn =
|
||||
unsafe extern "C" fn(filename: *const c_char, userdata: *mut c_void) -> c_int;
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_manager_init() -> c_int {
|
||||
unsafe { oaktask::ffi::manager::oaktask_manager_init() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_manager_shutdown() {
|
||||
unsafe { oaktask::ffi::manager::oaktask_manager_shutdown() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_register_codec_submitter() -> c_int {
|
||||
unsafe { oaktask::ffi::manager::oaktask_register_codec_submitter() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_manager_count() -> c_int {
|
||||
unsafe { oaktask::ffi::manager::oaktask_manager_count() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_manager_at(i: c_int) -> CHandle {
|
||||
unsafe { oaktask::ffi::manager::oaktask_manager_at(i) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_manager_delete_finished() {
|
||||
unsafe { oaktask::ffi::manager::oaktask_manager_delete_finished() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_task_free(t: *mut CHandle) {
|
||||
unsafe { oaktask::ffi::task::oaktask_task_free(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_task_start_sync(t: CHandle) -> c_int {
|
||||
unsafe { oaktask::ffi::task::oaktask_task_start_sync(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_task_start(t: CHandle) -> c_int {
|
||||
unsafe { oaktask::ffi::task::oaktask_task_start(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_task_cancel(t: CHandle) -> c_int {
|
||||
unsafe { oaktask::ffi::task::oaktask_task_cancel(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_task_wait(t: CHandle) -> c_int {
|
||||
unsafe { oaktask::ffi::task::oaktask_task_wait(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_task_is_finished(t: CHandle) -> c_int {
|
||||
unsafe { oaktask::ffi::task::oaktask_task_is_finished(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_task_succeeded(t: CHandle) -> c_int {
|
||||
unsafe { oaktask::ffi::task::oaktask_task_succeeded(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_task_title(t: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
unsafe { oaktask::ffi::task::oaktask_task_title(t, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_task_error(t: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
unsafe { oaktask::ffi::task::oaktask_task_error(t, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_task_subscribe(
|
||||
t: CHandle,
|
||||
cb: Option<OakTaskEventFn>,
|
||||
userdata: *mut c_void,
|
||||
) -> i64 {
|
||||
unsafe { oaktask::ffi::task::oaktask_task_subscribe(t, cb, userdata) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_debug_alive_count() -> c_int {
|
||||
unsafe { oaktask::ffi::task::oaktask_debug_alive_count() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_create_project_load(filename: *const c_char) -> CHandle {
|
||||
unsafe { oaktask::ffi::project::oaktask_create_project_load(filename) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_load_take_project(t: CHandle) -> CHandle {
|
||||
unsafe { oaktask::ffi::project::oaktask_load_take_project(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_create_project_save(
|
||||
project: CHandle,
|
||||
filename_or_null: *const c_char,
|
||||
use_compression: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktask::ffi::project::oaktask_create_project_save(
|
||||
project,
|
||||
filename_or_null,
|
||||
use_compression,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_create_project_import(
|
||||
folder: CHandle,
|
||||
project: CHandle,
|
||||
urls: *const *const c_char,
|
||||
url_count: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktask::ffi::project::oaktask_create_project_import(folder, project, urls, url_count)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_import_take_command(t: CHandle) -> CHandle {
|
||||
unsafe { oaktask::ffi::project::oaktask_import_take_command(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_import_footage_count(t: CHandle) -> c_int {
|
||||
unsafe { oaktask::ffi::project::oaktask_import_footage_count(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_import_footage_at(t: CHandle, index: c_int) -> CHandle {
|
||||
unsafe { oaktask::ffi::project::oaktask_import_footage_at(t, index) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_import_invalid_count(t: CHandle) -> c_int {
|
||||
unsafe { oaktask::ffi::project::oaktask_import_invalid_count(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_import_invalid_at(
|
||||
t: CHandle,
|
||||
index: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe { oaktask::ffi::project::oaktask_import_invalid_at(t, index, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_create_project_load_otio(filename: *const c_char) -> CHandle {
|
||||
unsafe { oaktask::ffi::project::oaktask_create_project_load_otio(filename) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_load_otio_take_project(t: CHandle) -> CHandle {
|
||||
unsafe { oaktask::ffi::project::oaktask_load_otio_take_project(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_create_project_save_otio(project: CHandle, filename: *const c_char) -> CHandle {
|
||||
unsafe { oaktask::ffi::project::oaktask_create_project_save_otio(project, filename) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_load_otio_set_confirm_cb(
|
||||
cb: Option<OakTaskOtioImportConfirmFn>,
|
||||
userdata: *mut c_void,
|
||||
) {
|
||||
unsafe { oaktask::ffi::project::oaktask_load_otio_set_confirm_cb(cb, userdata) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_create_precache(footage: CHandle, index: c_int, sequence: CHandle) -> CHandle {
|
||||
unsafe { oaktask::ffi::project::oaktask_create_precache(footage, index, sequence) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
/// `oaktask_create_export` — direct call into the `oaktask` crate
|
||||
/// (single-lib unification; the encoding-params POD is the shared
|
||||
/// oakcodec type).
|
||||
pub fn oaktask_create_export(
|
||||
viewer: CHandle,
|
||||
color_manager: CHandle,
|
||||
params: *const crate::bridge::codec::EncodingParamsPOD,
|
||||
) -> CHandle {
|
||||
unsafe { oaktask::ffi::project::oaktask_create_export(viewer, color_manager, params) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_import_set_image_sequence_confirm_cb(
|
||||
cb: Option<OakTaskImageSequenceConfirmFn>,
|
||||
userdata: *mut c_void,
|
||||
) {
|
||||
unsafe { oaktask::ffi::project::oaktask_import_set_image_sequence_confirm_cb(cb, userdata) }
|
||||
}
|
||||
@@ -1,490 +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 bridge: direct Rust calls into the `oaktimeline` crate.
|
||||
//!
|
||||
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
|
||||
//! call below is a compile-time Rust call into `oaktimeline`'s `ffi` (the
|
||||
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
|
||||
//! internal callers bypass them). Handles cross as the shared
|
||||
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
|
||||
//! declaration (resolved at link time against the sibling crate in the
|
||||
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
|
||||
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
|
||||
|
||||
// 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, mirroring the oaktimeline crate's exports
|
||||
//! (`src/timeline/rust/src/ffi.rs`; headers `include/timeline/*.h`).
|
||||
//!
|
||||
//! Every handle crosses as [`crate::handle::CHandle`]. The marker/workarea
|
||||
//! time quantities cross as `c_int` num/den pairs; the edit commands take
|
||||
//! `i64` rationals. String getters report the size **including** the NUL;
|
||||
//! the facade converts with [`crate::handle::string_result`].
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
// `include/timeline/marker.h` exports (complete inventory):
|
||||
// oaktimeline_marker_list_create / free / of / add / count / at /
|
||||
// add_command / remove_at_command / set_time_command /
|
||||
// set_props_command / list_load / list_save.
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_list_create() -> CHandle {
|
||||
unsafe { oaktimeline::ffi::marker::oaktimeline_marker_list_create() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_list_of(owner: CHandle) -> CHandle {
|
||||
unsafe { oaktimeline::ffi::marker::oaktimeline_marker_list_of(owner) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_list_free(list: *mut CHandle) {
|
||||
unsafe { oaktimeline::ffi::marker::oaktimeline_marker_list_free(list) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_add(
|
||||
list: CHandle,
|
||||
in_num: c_int,
|
||||
in_den: c_int,
|
||||
out_num: c_int,
|
||||
out_den: c_int,
|
||||
name: *const c_char,
|
||||
color: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oaktimeline::ffi::marker::oaktimeline_marker_add(
|
||||
list, in_num, in_den, out_num, out_den, name, color,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_count(list: CHandle, out_count: *mut c_int) -> c_int {
|
||||
unsafe { oaktimeline::ffi::marker::oaktimeline_marker_count(list, out_count) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_at(
|
||||
list: CHandle,
|
||||
index: c_int,
|
||||
in_num: *mut c_int,
|
||||
in_den: *mut c_int,
|
||||
out_num: *mut c_int,
|
||||
out_den: *mut c_int,
|
||||
color: *mut c_int,
|
||||
name_buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oaktimeline::ffi::marker::oaktimeline_marker_at(
|
||||
list, index, in_num, in_den, out_num, out_den, color, name_buf, buf_size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_add_command(
|
||||
list: CHandle,
|
||||
in_num: c_int,
|
||||
in_den: c_int,
|
||||
out_num: c_int,
|
||||
out_den: c_int,
|
||||
name: *const c_char,
|
||||
color: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::marker::oaktimeline_marker_add_command(
|
||||
list, in_num, in_den, out_num, out_den, name, color,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_remove_at_command(list: CHandle, index: c_int) -> CHandle {
|
||||
unsafe { oaktimeline::ffi::marker::oaktimeline_marker_remove_at_command(list, index) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_set_time_command(
|
||||
list: CHandle,
|
||||
index: c_int,
|
||||
in_num: c_int,
|
||||
in_den: c_int,
|
||||
out_num: c_int,
|
||||
out_den: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::marker::oaktimeline_marker_set_time_command(
|
||||
list, index, in_num, in_den, out_num, out_den,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_set_props_command(
|
||||
list: CHandle,
|
||||
index: c_int,
|
||||
color: c_int,
|
||||
name: *const c_char,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::marker::oaktimeline_marker_set_props_command(list, index, color, name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_list_load(list: CHandle, reader: CHandle) -> c_int {
|
||||
unsafe { oaktimeline::ffi::marker::oaktimeline_marker_list_load(list, reader) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_list_save(list: CHandle, writer: CHandle) -> c_int {
|
||||
unsafe { oaktimeline::ffi::marker::oaktimeline_marker_list_save(list, writer) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_create() -> CHandle {
|
||||
unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_create() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_of(owner: CHandle) -> CHandle {
|
||||
unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_of(owner) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_free(w: *mut CHandle) {
|
||||
unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_free(w) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_set_enabled(w: CHandle, enabled: c_int) -> c_int {
|
||||
unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_set_enabled(w, enabled) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_get(
|
||||
w: CHandle,
|
||||
in_num: *mut c_int,
|
||||
in_den: *mut c_int,
|
||||
out_num: *mut c_int,
|
||||
out_den: *mut c_int,
|
||||
enabled: *mut c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oaktimeline::ffi::workarea::oaktimeline_workarea_get(
|
||||
w, in_num, in_den, out_num, out_den, enabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_set_range(
|
||||
w: CHandle,
|
||||
in_num: c_int,
|
||||
in_den: c_int,
|
||||
out_num: c_int,
|
||||
out_den: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oaktimeline::ffi::workarea::oaktimeline_workarea_set_range(
|
||||
w, in_num, in_den, out_num, out_den,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_set_range_command(
|
||||
w: CHandle,
|
||||
in_num: c_int,
|
||||
in_den: c_int,
|
||||
out_num: c_int,
|
||||
out_den: c_int,
|
||||
old_in_num: c_int,
|
||||
old_in_den: c_int,
|
||||
old_out_num: c_int,
|
||||
old_out_den: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::workarea::oaktimeline_workarea_set_range_command(
|
||||
w,
|
||||
in_num,
|
||||
in_den,
|
||||
out_num,
|
||||
out_den,
|
||||
old_in_num,
|
||||
old_in_den,
|
||||
old_out_num,
|
||||
old_out_den,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_set_enabled_command(w: CHandle, enabled: c_int) -> CHandle {
|
||||
unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_set_enabled_command(w, enabled) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_reset(
|
||||
in_num: *mut c_int,
|
||||
in_den: *mut c_int,
|
||||
out_num: *mut c_int,
|
||||
out_den: *mut c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oaktimeline::ffi::workarea::oaktimeline_workarea_reset(in_num, in_den, out_num, out_den)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_load(w: CHandle, reader: CHandle) -> c_int {
|
||||
unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_load(w, reader) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_save(w: CHandle, writer: CHandle) -> c_int {
|
||||
unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_save(w, writer) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_add_track_command(list: CHandle) -> CHandle {
|
||||
unsafe { oaktimeline::ffi::edit::oaktimeline_add_track_command(list) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_remove_track_command(track: CHandle) -> CHandle {
|
||||
unsafe { oaktimeline::ffi::edit::oaktimeline_remove_track_command(track) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_place_block_command(
|
||||
list: CHandle,
|
||||
track_index: c_int,
|
||||
block: CHandle,
|
||||
in_num: i64,
|
||||
in_den: i64,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::edit::oaktimeline_place_block_command(
|
||||
list,
|
||||
track_index,
|
||||
block,
|
||||
in_num,
|
||||
in_den,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_replace_block_with_gap_command(track: CHandle, block: CHandle) -> CHandle {
|
||||
unsafe { oaktimeline::ffi::edit::oaktimeline_replace_block_with_gap_command(track, block) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_move_block_command(
|
||||
list: CHandle,
|
||||
track_index: c_int,
|
||||
block: CHandle,
|
||||
in_num: i64,
|
||||
in_den: i64,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::edit::oaktimeline_move_block_command(
|
||||
list,
|
||||
track_index,
|
||||
block,
|
||||
in_num,
|
||||
in_den,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_trim_command(
|
||||
track: CHandle,
|
||||
block: CHandle,
|
||||
new_length_num: i64,
|
||||
new_length_den: i64,
|
||||
mode: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::edit::oaktimeline_trim_command(
|
||||
track,
|
||||
block,
|
||||
new_length_num,
|
||||
new_length_den,
|
||||
mode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_split_command(
|
||||
blocks: *const CHandle,
|
||||
count: c_int,
|
||||
point_num: i64,
|
||||
point_den: i64,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::edit::oaktimeline_split_command(blocks, count, point_num, point_den)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_split_preserving_links_command(
|
||||
blocks: *const CHandle,
|
||||
count: c_int,
|
||||
point_nums: *const i64,
|
||||
point_dens: *const i64,
|
||||
time_count: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::edit::oaktimeline_split_preserving_links_command(
|
||||
blocks, count, point_nums, point_dens, time_count,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_ripple_delete_gaps_command(
|
||||
sequence: CHandle,
|
||||
in_nums: *const i64,
|
||||
in_dens: *const i64,
|
||||
out_nums: *const i64,
|
||||
out_dens: *const i64,
|
||||
tracks: *const CHandle,
|
||||
range_count: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::edit::oaktimeline_ripple_delete_gaps_command(
|
||||
sequence,
|
||||
in_nums,
|
||||
in_dens,
|
||||
out_nums,
|
||||
out_dens,
|
||||
tracks,
|
||||
range_count,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_slide_command(
|
||||
track: CHandle,
|
||||
blocks: *const CHandle,
|
||||
block_count: c_int,
|
||||
in_adjacent: CHandle,
|
||||
out_adjacent: CHandle,
|
||||
movement_num: i64,
|
||||
movement_den: i64,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::edit::oaktimeline_slide_command(
|
||||
track,
|
||||
blocks,
|
||||
block_count,
|
||||
in_adjacent,
|
||||
out_adjacent,
|
||||
movement_num,
|
||||
movement_den,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_ripple_remove_area_command(
|
||||
track: CHandle,
|
||||
in_num: i64,
|
||||
in_den: i64,
|
||||
out_num: i64,
|
||||
out_den: i64,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::edit::oaktimeline_ripple_remove_area_command(
|
||||
track, in_num, in_den, out_num, out_den,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_insert_gaps_command(
|
||||
list: CHandle,
|
||||
point_num: i64,
|
||||
point_den: i64,
|
||||
length_num: i64,
|
||||
length_den: i64,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::edit::oaktimeline_insert_gaps_command(
|
||||
list, point_num, point_den, length_num, length_den,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,198 +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 bridge: direct Rust calls into the `oakundo` crate.
|
||||
//!
|
||||
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
|
||||
//! call below is a compile-time Rust call into `oakundo`'s `ffi` (the
|
||||
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
|
||||
//! internal callers bypass them). Handles cross as the shared
|
||||
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
|
||||
//! declaration (resolved at link time against the sibling crate in the
|
||||
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
|
||||
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
|
||||
|
||||
// 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 imports, mirroring the oakundo crate's exports
|
||||
//! (`src/undo/rust/src/ffi.rs`; headers `include/undo/*.h`).
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// `include/undo/undocommand.h` — callback table backing a
|
||||
/// caller-defined undo command. Single-lib unification: aliases the
|
||||
/// oakundo crate's vtable POD.
|
||||
pub type OakUndoCommandVtable = oakundo::undocommand::OakUndoCommandVtable;
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_command_init(
|
||||
vtable: *const OakUndoCommandVtable,
|
||||
userdata: *mut std::ffi::c_void,
|
||||
) -> CHandle {
|
||||
unsafe { oakundo::ffi::command::oakundo_command_init(vtable, userdata) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_command_init_multi() -> CHandle {
|
||||
unsafe { oakundo::ffi::command::oakundo_command_init_multi() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_command_multi_add_child(multi: CHandle, child: CHandle) -> c_int {
|
||||
unsafe { oakundo::ffi::command::oakundo_command_multi_add_child(multi, child) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_command_multi_child_count(multi: CHandle, out_count: *mut c_int) -> c_int {
|
||||
unsafe { oakundo::ffi::command::oakundo_command_multi_child_count(multi, out_count) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_command_multi_child(multi: CHandle, index: c_int, out_child: *mut CHandle) -> c_int {
|
||||
unsafe { oakundo::ffi::command::oakundo_command_multi_child(multi, index, out_child) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_command_redo_now(command: CHandle) -> c_int {
|
||||
unsafe { oakundo::ffi::command::oakundo_command_redo_now(command) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_command_undo_now(command: CHandle) -> c_int {
|
||||
unsafe { oakundo::ffi::command::oakundo_command_undo_now(command) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_command_free(command: *mut CHandle) {
|
||||
unsafe { oakundo::ffi::command::oakundo_command_free(command) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_init() -> CHandle {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_init() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_free(stack: *mut CHandle) {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_free(stack) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_push(stack: CHandle, command: CHandle, name: *const c_char) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_push(stack, command, name) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_push_pre_executed(
|
||||
stack: CHandle,
|
||||
command: CHandle,
|
||||
name: *const c_char,
|
||||
) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_push_pre_executed(stack, command, name) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_undo(stack: CHandle) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_undo(stack) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_redo(stack: CHandle) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_redo(stack) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_jump(stack: CHandle, index: i64) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_jump(stack, index) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_clear(stack: CHandle) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_clear(stack) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_can_undo(stack: CHandle, out_value: *mut c_int) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_can_undo(stack, out_value) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_can_redo(stack: CHandle, out_value: *mut c_int) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_can_redo(stack, out_value) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_count(stack: CHandle, out_count: *mut i64) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_count(stack, out_count) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_index(stack: CHandle, out_index: *mut i64) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_index(stack, out_index) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_command_text(
|
||||
stack: CHandle,
|
||||
row: i64,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_command_text(stack, row, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_command_is_done(stack: CHandle, row: i64, out_value: *mut c_int) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_command_is_done(stack, row, out_value) }
|
||||
}
|
||||
+382
-19
@@ -16,7 +16,7 @@
|
||||
|
||||
//! `engine/include/oakengine/encoding.h` over the oakcodec module.
|
||||
//!
|
||||
//! Two parts:
|
||||
//! Three parts:
|
||||
//!
|
||||
//! - The container/codec **metadata family** (format names/extensions,
|
||||
//! per-format codec lists, pixel/sample formats, filename helpers,
|
||||
@@ -30,15 +30,19 @@
|
||||
//! "unset" (the POD's 0 is a valid format, DNxHD); encoder-specific
|
||||
//! video options are kept in a facade-side map (the POD has no such
|
||||
//! field).
|
||||
//! - The **exporter family** (`engine/include/oakengine/exporter.h`)
|
||||
//! assembles an encoding-params handle and drives the export task
|
||||
//! synchronously (see the family section at the bottom).
|
||||
//!
|
||||
//! Presets, preset load/save and the sequence-bound last-used/export
|
||||
//! entry points are deferred (see the stubs below and `deferred.rs`).
|
||||
//! Presets, preset load/save and the sequence-bound last-used entry
|
||||
//! points remain deferred (see the stubs below and `deferred.rs`).
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
use std::ffi::{c_char, c_double, c_int, c_void};
|
||||
|
||||
use crate::bridge::codec as k;
|
||||
use crate::bridge::codec::{zeroed_encoding_params, EncodingParamsPOD};
|
||||
use crate::stubs::codec as k;
|
||||
use crate::pods::{zeroed_encoding_params, EncodingParamsPOD};
|
||||
use crate::common::OakVideoParamsPod;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::handle::{guard, guard_int, string_result};
|
||||
@@ -55,6 +59,10 @@ pub struct OakEngineEncodingParams {
|
||||
struct ParamsBox {
|
||||
pod: EncodingParamsPOD,
|
||||
video_options: HashMap<String, String>,
|
||||
/// Raw scaling-method code exactly as the caller set it: the POD's
|
||||
/// `VideoScalingMethod` enum cannot carry garbage codes, and the
|
||||
/// facade contract accepts any `int` verbatim (round-trips 99 as 99).
|
||||
video_scaling_raw: c_int,
|
||||
}
|
||||
|
||||
impl ParamsBox {
|
||||
@@ -64,6 +72,7 @@ impl ParamsBox {
|
||||
ParamsBox {
|
||||
pod,
|
||||
video_options: HashMap::new(),
|
||||
video_scaling_raw: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -457,7 +466,7 @@ pub unsafe extern "C" fn oakengine_encoding_params_enable_video(
|
||||
p.pod.video_height = v.height;
|
||||
p.pod.video_time_base_num = v.time_base_num;
|
||||
p.pod.video_time_base_den = v.time_base_den;
|
||||
p.pod.video_pixel_format = v.format;
|
||||
p.pod.video_pixel_format = crate::pods::pixel_format_from_code(v.format);
|
||||
p.pod.video_interlacing = v.interlacing;
|
||||
p.pod.video_pixel_aspect_num = v.pixel_aspect_num;
|
||||
p.pod.video_pixel_aspect_den = v.pixel_aspect_den;
|
||||
@@ -480,7 +489,7 @@ pub unsafe extern "C" fn oakengine_encoding_params_enable_audio(
|
||||
p.pod.audio_codec = codec;
|
||||
p.pod.audio_sample_rate = sample_rate;
|
||||
p.pod.audio_channel_layout = channel_layout;
|
||||
p.pod.audio_sample_format = sample_format;
|
||||
p.pod.audio_sample_format = crate::pods::sample_format_from_code(sample_format);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -594,7 +603,7 @@ pub unsafe extern "C" fn oakengine_encoding_params_get_video_params(
|
||||
(*out).height = p.pod.video_height;
|
||||
(*out).time_base_num = p.pod.video_time_base_num;
|
||||
(*out).time_base_den = p.pod.video_time_base_den;
|
||||
(*out).format = p.pod.video_pixel_format;
|
||||
(*out).format = p.pod.video_pixel_format as i32;
|
||||
(*out).interlacing = p.pod.video_interlacing;
|
||||
(*out).pixel_aspect_num = p.pod.video_pixel_aspect_num;
|
||||
(*out).pixel_aspect_den = p.pod.video_pixel_aspect_den;
|
||||
@@ -645,7 +654,7 @@ pub unsafe extern "C" fn oakengine_encoding_params_get_audio_params(
|
||||
*channel_layout = p.pod.audio_channel_layout;
|
||||
}
|
||||
if !sample_format.is_null() {
|
||||
*sample_format = p.pod.audio_sample_format;
|
||||
*sample_format = p.pod.audio_sample_format as i32;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
@@ -964,7 +973,10 @@ pub unsafe extern "C" fn oakengine_encoding_params_set_video_scaling_method(
|
||||
) -> c_int {
|
||||
guard(|| unsafe {
|
||||
let p = params_mut(params)?;
|
||||
p.pod.video_scaling_method = method;
|
||||
// The raw code round-trips verbatim (garbage codes included);
|
||||
// the POD carries the nearest legal enum for the encoder.
|
||||
p.video_scaling_raw = method;
|
||||
p.pod.video_scaling_method = crate::pods::scaling_from_code(method);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -976,7 +988,7 @@ pub unsafe extern "C" fn oakengine_encoding_params_video_scaling_method(
|
||||
) -> c_int {
|
||||
guard_int(|| unsafe {
|
||||
let p = params_ref(params)?;
|
||||
Ok(p.pod.video_scaling_method)
|
||||
Ok(p.video_scaling_raw)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1073,14 +1085,365 @@ pub unsafe extern "C" fn oakengine_encoding_params_save_file(
|
||||
crate::error::OAKENGINE_E_FAILED
|
||||
}
|
||||
|
||||
/// `oakengine_export_render_with_params` — **not backed** (the exporter
|
||||
/// family is facade-only; see `deferred.rs`). Returns OAKENGINE_E_FAILED.
|
||||
// ---------------------------------------------------------------------------
|
||||
// Exporter family (exporter.h)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Thread-local reason for the last failed export on this thread (the C++
|
||||
// `g_last_error`, `engine/src/capi/export.cpp`). Cleared at the start of
|
||||
// every export call; read by [`oakengine_export_last_error`].
|
||||
thread_local! {
|
||||
static EXPORT_LAST_ERROR: RefCell<String> = const { RefCell::new(String::new()) };
|
||||
}
|
||||
|
||||
// Thread-local progress callback installed by
|
||||
// [`oakengine_export_set_progress_callback`] (the C++ `g_progress_fn` /
|
||||
// `g_progress_userdata`). Per-thread like the C++: the synchronous export
|
||||
// runs on the installing thread, so the module task events arrive there.
|
||||
thread_local! {
|
||||
static EXPORT_PROGRESS: RefCell<
|
||||
Option<(unsafe extern "C" fn(c_double, *mut c_void), *mut c_void)>,
|
||||
> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// The module task progress event id (`OAKTASK_EVENT_PROGRESS`, see
|
||||
/// `oakengine_task_subscribe`).
|
||||
const EXPORT_EVENT_PROGRESS: c_int = 1;
|
||||
|
||||
fn export_last_error_set(msg: String) {
|
||||
EXPORT_LAST_ERROR.with(|e| *e.borrow_mut() = msg);
|
||||
}
|
||||
|
||||
/// Forward the progress events of a running export to the installed
|
||||
/// callback. Installed as the task subscription only while a callback is
|
||||
/// set; the module passes the callback's own `userdata` through.
|
||||
unsafe extern "C" fn export_progress_event(event_id: c_int, value: f64, userdata: *mut c_void) {
|
||||
if event_id != EXPORT_EVENT_PROGRESS {
|
||||
return;
|
||||
}
|
||||
EXPORT_PROGRESS.with(|slot| {
|
||||
if let Some((cb, _)) = *slot.borrow() {
|
||||
// SAFETY: the callback + userdata follow the installer's
|
||||
// contract; the task emits on its running thread.
|
||||
unsafe { cb(value, userdata) };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Run an export task synchronously on the calling thread — the shared
|
||||
/// tail of every exporter-family entry point: create the task (taking
|
||||
/// ownership of `params`), subscribe the installed progress callback, run
|
||||
/// through [`oakengine_task_start_sync`], read the task error into the
|
||||
/// thread-local last-error slot, and free the task.
|
||||
///
|
||||
/// Returns OAKENGINE_OK on success, OAKENGINE_E_FAILED otherwise. On the
|
||||
/// task-creation failure path `params` ownership stays with the caller
|
||||
/// (mirroring [`oakengine_task_create_export`]).
|
||||
fn export_run_sync(seq: *mut crate::handle::OakEngineSequence, params: *mut OakEngineEncodingParams) -> c_int {
|
||||
let task = unsafe { crate::task::oakengine_task_create_export(seq, params) };
|
||||
if task.is_null() {
|
||||
export_last_error_set("failed to create the export task".into());
|
||||
return crate::error::OAKENGINE_E_FAILED;
|
||||
}
|
||||
// Progress events through the same module subscription the app's
|
||||
// `start_export` uses (`oakengine_task_subscribe`).
|
||||
EXPORT_PROGRESS.with(|slot| {
|
||||
if let Some((_, userdata)) = *slot.borrow() {
|
||||
unsafe {
|
||||
crate::task::oakengine_task_subscribe(task, Some(export_progress_event), userdata);
|
||||
}
|
||||
}
|
||||
});
|
||||
let ok = unsafe { crate::task::oakengine_task_start_sync(task) };
|
||||
let rc = if ok == 1 {
|
||||
crate::error::OAKENGINE_OK
|
||||
} else {
|
||||
let err = export_task_error(task);
|
||||
export_last_error_set(if err.is_empty() {
|
||||
"export failed".into()
|
||||
} else {
|
||||
err
|
||||
});
|
||||
crate::error::OAKENGINE_E_FAILED
|
||||
};
|
||||
unsafe { crate::task::oakengine_task_free(task) };
|
||||
rc
|
||||
}
|
||||
|
||||
/// Two-stage read of a task's error string (empty when none).
|
||||
fn export_task_error(task: *mut crate::handle::OakEngineTask) -> String {
|
||||
unsafe {
|
||||
let needed = crate::task::oakengine_task_error(task, std::ptr::null_mut(), 0);
|
||||
if needed <= 0 {
|
||||
return String::new();
|
||||
}
|
||||
let mut buf = vec![0 as c_char; needed as usize + 1];
|
||||
let n = crate::task::oakengine_task_error(task, buf.as_mut_ptr(), buf.len() as c_int);
|
||||
if n < 0 {
|
||||
return String::new();
|
||||
}
|
||||
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
|
||||
String::from_utf8_lossy(unsafe {
|
||||
std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakengine_export_render_with_params` — render `seq` to the file the
|
||||
/// encoding params describe, through the same synchronous export path the
|
||||
/// app's `start_export` drives (`oakengine_task_create_export` +
|
||||
/// `oakengine_task_start_sync` + free).
|
||||
///
|
||||
/// Takes ownership of `params` on success (destroyed with the export
|
||||
/// task, mirroring [`oakengine_task_create_export`]); on the
|
||||
/// task-creation failure path the caller keeps ownership. The sequence
|
||||
/// handle is validated for non-NULL only — the C++ "handle is a sequence
|
||||
/// of the active project" walk has no Rust analogue (created sequences
|
||||
/// live in their own scratch project, see `oakengine_sequence_new`).
|
||||
///
|
||||
/// Returns OAKENGINE_OK on success; OAKENGINE_E_INVALID for NULL
|
||||
/// arguments; OAKENGINE_E_FAILED for creation/run failures (see
|
||||
/// [`oakengine_export_last_error`]).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_export_render_with_params(
|
||||
_seq: *mut crate::handle::OakEngineSequence,
|
||||
_params: *const OakEngineEncodingParams,
|
||||
seq: *mut crate::handle::OakEngineSequence,
|
||||
params: *const OakEngineEncodingParams,
|
||||
) -> c_int {
|
||||
crate::error::OAKENGINE_E_FAILED
|
||||
guard(|| unsafe {
|
||||
export_last_error_set(String::new());
|
||||
if seq.is_null() || params.is_null() {
|
||||
export_last_error_set("invalid arguments".into());
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
if export_run_sync(seq, params as *mut OakEngineEncodingParams) == crate::error::OAKENGINE_OK {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Failed("export failed".into()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakengine_export_render` — render `seq`'s [in_ts, out_ts) range
|
||||
/// offline and encode it to `path`.
|
||||
///
|
||||
/// `in_ts`/`out_ts` are frame timestamps in the sequence's frame-rate
|
||||
/// timebase (the export frame rate is the sequence frame rate). `width`/
|
||||
/// `height` <= 0 fall back to the sequence's video dimensions; when they
|
||||
/// differ the frames are scaled to fit. Video is encoded with the
|
||||
/// options' codec (default H.264 in an MP4 container), audio with the
|
||||
/// options' codec (default AAC) at the requested rate/layout (defaults:
|
||||
/// 48 kHz stereo — the engine has no sequence-audio getter, so the
|
||||
/// header's "sequence rate/layout" fallback mirrors the app's export
|
||||
/// dialog instead). The options' codec fields carry the exporter.h
|
||||
/// `OAKENGINE_EXPORT_VIDEO_*` / `OAKENGINE_EXPORT_AUDIO_*` values,
|
||||
/// mapped here onto the engine's `ExportFormat` / `ExportCodec` ids.
|
||||
///
|
||||
/// The call blocks until the export finishes; progress is reported
|
||||
/// through the callback set with [`oakengine_export_set_progress_callback`].
|
||||
///
|
||||
/// Deviations from the C++ header: no `OAKENGINE_INIT_RENDER`
|
||||
/// requirement (the Rust render path is CPU-only and self-contained, see
|
||||
/// `oakengine_render_manager_init`) and no "sequence is part of a
|
||||
/// project" check (created sequences live in a scratch project).
|
||||
///
|
||||
/// Returns OAKENGINE_OK on success; OAKENGINE_E_INVALID for bad
|
||||
/// arguments; OAKENGINE_E_FAILED for render/encode failures (see
|
||||
/// [`oakengine_export_last_error`]).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_export_render(
|
||||
seq: *mut crate::handle::OakEngineSequence,
|
||||
path: *const c_char,
|
||||
in_ts: i64,
|
||||
out_ts: i64,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
opts: *const crate::pods::OakExportOptions,
|
||||
) -> c_int {
|
||||
guard(|| unsafe {
|
||||
export_last_error_set(String::new());
|
||||
if seq.is_null() || path.is_null() || in_ts < 0 || out_ts <= in_ts {
|
||||
export_last_error_set("invalid arguments".into());
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let o = if opts.is_null() {
|
||||
crate::pods::OakExportOptions {
|
||||
video_codec: crate::pods::OAKENGINE_EXPORT_VIDEO_H264,
|
||||
audio_codec: crate::pods::OAKENGINE_EXPORT_AUDIO_AAC,
|
||||
video_bit_rate: 0,
|
||||
audio_sample_rate: 0,
|
||||
audio_channel_count: 0,
|
||||
}
|
||||
} else {
|
||||
*opts
|
||||
};
|
||||
|
||||
// Map the exporter.h codec ids onto the engine's enum ids.
|
||||
let (format, vcodec) = match o.video_codec {
|
||||
crate::pods::OAKENGINE_EXPORT_VIDEO_H264 => (
|
||||
oakcodec::exportformat::Format::MPEG4Video as i32,
|
||||
oakcodec::exportcodec::Codec::H264 as i32,
|
||||
),
|
||||
crate::pods::OAKENGINE_EXPORT_VIDEO_H265 => (
|
||||
oakcodec::exportformat::Format::MPEG4Video as i32,
|
||||
oakcodec::exportcodec::Codec::H265 as i32,
|
||||
),
|
||||
crate::pods::OAKENGINE_EXPORT_VIDEO_PNG_SEQUENCE => (
|
||||
oakcodec::exportformat::Format::PNG as i32,
|
||||
oakcodec::exportcodec::Codec::PNG as i32,
|
||||
),
|
||||
_ => {
|
||||
export_last_error_set(format!("unknown video codec {}", o.video_codec));
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
};
|
||||
let audio_enabled = o.audio_codec != crate::pods::OAKENGINE_EXPORT_AUDIO_NONE;
|
||||
let acodec = if audio_enabled {
|
||||
match o.audio_codec {
|
||||
crate::pods::OAKENGINE_EXPORT_AUDIO_AAC => oakcodec::exportcodec::Codec::AAC as i32,
|
||||
crate::pods::OAKENGINE_EXPORT_AUDIO_PCM => oakcodec::exportcodec::Codec::PCM as i32,
|
||||
_ => {
|
||||
export_last_error_set(format!("unknown audio codec {}", o.audio_codec));
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// Sequence geometry + frame rate (the export frame rate is the
|
||||
// sequence's).
|
||||
let mut sw: c_int = 0;
|
||||
let mut sh: c_int = 0;
|
||||
let mut par_num: c_int = 1;
|
||||
let mut par_den: c_int = 1;
|
||||
Error::from_module(crate::timeline::oakengine_sequence_get_video_params(
|
||||
seq,
|
||||
&mut sw,
|
||||
&mut sh,
|
||||
&mut par_num,
|
||||
&mut par_den,
|
||||
))?;
|
||||
let mut rate_num: c_int = 0;
|
||||
let mut rate_den: c_int = 1;
|
||||
Error::from_module(crate::timeline::oakengine_sequence_get_frame_rate(
|
||||
seq,
|
||||
&mut rate_num,
|
||||
&mut rate_den,
|
||||
))?;
|
||||
if rate_num <= 0 || rate_den <= 0 {
|
||||
export_last_error_set("sequence has no valid frame rate".into());
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let out_w = if width > 0 { width } else { sw };
|
||||
let out_h = if height > 0 { height } else { sh };
|
||||
if out_w <= 0 || out_h <= 0 {
|
||||
export_last_error_set("sequence has no valid video dimensions".into());
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let sample_rate = if o.audio_sample_rate > 0 { o.audio_sample_rate } else { 48000 };
|
||||
let layout: u64 = if o.audio_channel_count > 0 {
|
||||
match o.audio_channel_count {
|
||||
1 => 0x4, // AV_CH_LAYOUT_MONO
|
||||
2 => 0x3, // AV_CH_LAYOUT_STEREO
|
||||
n => {
|
||||
export_last_error_set(format!(
|
||||
"unsupported audio channel count {n} (1 = mono, 2 = stereo)"
|
||||
));
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
0x3
|
||||
};
|
||||
|
||||
// Assemble the encoding params through the public setters (the same
|
||||
// path the app's `start_export` uses); the task consumes the handle
|
||||
// once created.
|
||||
let params = oakengine_encoding_params_create();
|
||||
if params.is_null() {
|
||||
export_last_error_set("failed to create encoding params".into());
|
||||
return Err(Error::Failed("failed to create encoding params".into()));
|
||||
}
|
||||
let fail = |msg: &str| -> Result<()> {
|
||||
oakengine_encoding_params_destroy(params);
|
||||
export_last_error_set(msg.into());
|
||||
Err(Error::Failed(msg.into()))
|
||||
};
|
||||
let cpath = std::ffi::CString::new(crate::handle::read_cstr(path))
|
||||
.map_err(|_| Error::Failed("invalid path (NUL byte)".into()))?;
|
||||
if oakengine_encoding_params_set_filename(params, cpath.as_ptr()) != 0 {
|
||||
return fail("failed to set the export filename");
|
||||
}
|
||||
if oakengine_encoding_params_set_format(params, format) != 0 {
|
||||
return fail("failed to set the export format");
|
||||
}
|
||||
let pod = OakVideoParamsPod {
|
||||
width: out_w,
|
||||
height: out_h,
|
||||
time_base_num: rate_den,
|
||||
time_base_den: rate_num,
|
||||
format: 0,
|
||||
pixel_aspect_num: par_num.max(1),
|
||||
pixel_aspect_den: par_den.max(1),
|
||||
interlacing: 0,
|
||||
color_range: 0,
|
||||
divider: 1,
|
||||
video_type: 0,
|
||||
premultiplied_alpha: 0,
|
||||
};
|
||||
if oakengine_encoding_params_enable_video(params, &pod, vcodec) != 0 {
|
||||
return fail("failed to enable video");
|
||||
}
|
||||
if audio_enabled && oakengine_encoding_params_enable_audio(params, sample_rate, layout, 0, acodec) != 0 {
|
||||
return fail("failed to enable audio");
|
||||
}
|
||||
if o.video_bit_rate > 0 {
|
||||
oakengine_encoding_params_set_video_bit_rate(params, o.video_bit_rate);
|
||||
}
|
||||
// Fit scaling (the header's documented behavior when the output
|
||||
// size differs from the sequence's).
|
||||
if oakengine_encoding_params_set_video_scaling_method(params, 0) != 0 {
|
||||
return fail("failed to set the video scaling method");
|
||||
}
|
||||
// Export range as seconds rationals: frame timestamps in the
|
||||
// sequence's frame-rate timebase (frame duration = rate_den/rate_num).
|
||||
let tb_num = i64::from(rate_den);
|
||||
let tb_den = i64::from(rate_num);
|
||||
oakengine_encoding_params_set_custom_range(params, in_ts * tb_num, tb_den, out_ts * tb_num, tb_den);
|
||||
oakengine_encoding_params_set_export_length(params, ((out_ts - in_ts) * tb_num) as c_int, rate_num);
|
||||
|
||||
if export_run_sync(seq, params) == crate::error::OAKENGINE_OK {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Failed("export failed".into()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakengine_export_last_error` — the reason for the last failed export
|
||||
/// on this thread (buf/size; empty after a successful export).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_export_last_error(buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
guard_int(|| {
|
||||
let err = EXPORT_LAST_ERROR.with(|e| e.borrow().clone());
|
||||
Ok(unsafe { crate::handle::write_string(&err, buf, buf_size) })
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakengine_export_set_progress_callback` — install the progress
|
||||
/// callback used by subsequent [`oakengine_export_render`] /
|
||||
/// [`oakengine_export_render_with_params`] calls on this thread (NULL
|
||||
/// disables). The callback receives `fraction` in [0, 1] and is invoked
|
||||
/// on the exporting thread during the synchronous run.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_export_set_progress_callback(
|
||||
f: Option<unsafe extern "C" fn(c_double, *mut c_void)>,
|
||||
userdata: *mut c_void,
|
||||
) {
|
||||
crate::handle::guard_void(|| {
|
||||
EXPORT_PROGRESS.with(|slot| *slot.borrow_mut() = f.map(|cb| (cb, userdata)));
|
||||
});
|
||||
}
|
||||
|
||||
/// `oakengine_encoding_params_get_last_used` — **not backed** (sequence
|
||||
@@ -1115,9 +1478,9 @@ pub unsafe extern "C" fn oakengine_encoding_start_audio_recording(
|
||||
if m.is_null() {
|
||||
return Err(Error::State);
|
||||
}
|
||||
let rc = crate::bridge::audio::oakaudio_manager_start_recording(
|
||||
let rc = crate::stubs::audio::oakaudio_manager_start_recording(
|
||||
m,
|
||||
&p.pod as *const oakaudio::bridge::codec::EncodingParams,
|
||||
&p.pod as *const crate::pods::EncodingParamsPOD,
|
||||
errbuf,
|
||||
errbuf_size,
|
||||
);
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use crate::bridge::common as c;
|
||||
use crate::stubs::common as c;
|
||||
use crate::error::Error;
|
||||
use crate::handle::{
|
||||
box_handle, free_box, guard, guard_int, guard_void, string_result, OakEngineClipboard,
|
||||
|
||||
@@ -49,14 +49,15 @@
|
||||
//! crates lack the C ABI surface — each stub returns its header's
|
||||
//! documented failure value:
|
||||
//!
|
||||
//! - **codec** (encoding.h, 81/85 wrapped): the preset path/count/name,
|
||||
//! preset load/save and the sequence-bound export/last-used entry
|
||||
//! points (`oakengine_encoding_preset_*`,
|
||||
//! - **codec** (encoding.h, 82/85 wrapped): the preset path/count/name,
|
||||
//! preset load/save and the sequence-bound last-used entry points
|
||||
//! (`oakengine_encoding_preset_*`,
|
||||
//! `oakengine_encoding_params_load_file/save_file`,
|
||||
//! `oakengine_export_render_with_params`,
|
||||
//! `oakengine_encoding_params_get/set_last_used`) are stubs — the
|
||||
//! oakcodec crate has no preset API and those entry points need the
|
||||
//! exporter/sequence families.
|
||||
//! oakcodec crate has no preset API and the last-used pair needs the
|
||||
//! deferred node/timeline families. The exporter entry point
|
||||
//! (`oakengine_export_render_with_params`) is backed since M12 (see
|
||||
//! `crate::codec`, "Exporter family").
|
||||
//! - **render color** (color.h, 19/31 wrapped): the color-manager list
|
||||
//! queries (colorspace/display/view/look/compliant/luma), the
|
||||
//! standalone config handle and `color_processor_id` /
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
//! [`write_string`]): the return value is the required length including
|
||||
//! the terminating NUL; negative values are error codes.
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
use std::ffi::{c_char, c_int};
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
@@ -48,6 +48,95 @@ use crate::error::{Error, Result};
|
||||
/// `Clone + Copy + Send + Sync` come from the shared type.
|
||||
pub use oakcore_rs::handle::CHandle;
|
||||
|
||||
/// Engine-side boxed payloads holding the oaknode domain (single-lib
|
||||
/// unification). Every `oakengine_*` node-family handle ultimately wraps
|
||||
/// one of these behind a [`CHandle`]:
|
||||
///
|
||||
/// - projects box [`domain::ProjectArc`] (`Arc<Mutex<Project>>`);
|
||||
/// - nodes, blocks, tracks, footage, sequences and folders box a
|
||||
/// [`domain::NodeRef`] (`(Arc<Mutex<Project>>, NodeId)` — the
|
||||
/// oaknode crate's `project::NodeRef` value type).
|
||||
///
|
||||
/// The box is created through `oaknode::handle::make_owned` (refcounted
|
||||
/// shell + release callback), so the facade's existing
|
||||
/// [`box_handle`]/[`free_box`] discipline (and the addref copies the
|
||||
/// engine takes) works unchanged.
|
||||
pub mod domain {
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use oaknode::id::NodeId;
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// Engine-side boxed payload for project handles: shared ownership of
|
||||
/// the oaknode domain project (its graph, settings, filename state).
|
||||
pub type ProjectArc = Arc<Mutex<oaknode::project::Project>>;
|
||||
|
||||
/// Engine-side boxed payload for node/block/track/footage/sequence/
|
||||
/// folder handles: a reference into a project's graph. Reuses the
|
||||
/// oaknode crate's own `NodeRef` value type (project + id + owned
|
||||
/// flag); a stale id fails validation instead of aliasing.
|
||||
pub type NodeRef = oaknode::project::NodeRef;
|
||||
|
||||
/// Box a project payload behind a refcounted handle.
|
||||
pub fn box_project(project: ProjectArc) -> CHandle {
|
||||
oaknode::handle::make_owned(project)
|
||||
}
|
||||
|
||||
/// Box a node reference behind a refcounted handle. `owned` marks
|
||||
/// detached (factory-created) nodes so the engine's debug alive
|
||||
/// counter accounts them exactly once.
|
||||
pub fn box_node(project: ProjectArc, id: NodeId, owned: bool) -> CHandle {
|
||||
oaknode::handle::make_owned(NodeRef::new(project, id, owned))
|
||||
}
|
||||
|
||||
/// Borrow the project payload behind a handle.
|
||||
///
|
||||
/// # Safety
|
||||
/// `h` must be a live handle created by [`box_project`] (or empty).
|
||||
pub unsafe fn project_of(h: &CHandle) -> Option<&ProjectArc> {
|
||||
// SAFETY: forwarded to the oaknode handle contract.
|
||||
unsafe { oaknode::handle::get::<ProjectArc>(h) }
|
||||
}
|
||||
|
||||
/// Borrow the node-reference payload behind a handle.
|
||||
///
|
||||
/// # Safety
|
||||
/// `h` must be a live handle created by [`box_node`] (or empty).
|
||||
pub unsafe fn node_ref_of(h: &CHandle) -> Option<&NodeRef> {
|
||||
// SAFETY: forwarded to the oaknode handle contract.
|
||||
unsafe { oaknode::handle::get::<NodeRef>(h) }
|
||||
}
|
||||
|
||||
/// Mutable view of the node-reference payload (used by the graph
|
||||
/// transfer paths, which rewrite the shared box in place — the
|
||||
/// "write_node_ref" semantics).
|
||||
///
|
||||
/// # Safety
|
||||
/// `h` must be a live handle created by [`box_node`]; the caller must
|
||||
/// hold exclusive access to the boxed value.
|
||||
pub unsafe fn node_ref_mut(h: &CHandle) -> Option<&mut NodeRef> {
|
||||
// SAFETY: forwarded to the shared-box contract.
|
||||
unsafe { boxed_mut::<NodeRef>(h) }
|
||||
}
|
||||
|
||||
/// Mutable typed view into an oaknode-style `RefBox` payload (the
|
||||
/// oaknode crate exposes only a read-only `get`; this mirrors its
|
||||
/// box layout — `refs`/`value` are `pub` fields).
|
||||
///
|
||||
/// # Safety
|
||||
/// `h` must be a live handle boxing `T`; the caller must hold
|
||||
/// exclusive access to the boxed value.
|
||||
pub unsafe fn boxed_mut<T: 'static>(h: &CHandle) -> Option<&mut T> {
|
||||
if h.ctx.is_null() {
|
||||
return None;
|
||||
}
|
||||
// SAFETY: contract above; the box is an
|
||||
// `oaknode::handle::RefBox<T>`.
|
||||
unsafe { Some(&mut (*(h.ctx as *mut oaknode::handle::RefBox<T>)).value) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Engine opaque handle types, one per `typedef struct OakEngine*` in
|
||||
/// `engine/include/oakengine/*.h`. All are thin newtype wrappers around a
|
||||
/// [`CHandle`] value with a uniform extraction surface ([`EngineBox`]).
|
||||
|
||||
+689
-17
@@ -14,22 +14,33 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Render-worker IPC: named shared memory holding the frame-slot pools —
|
||||
//! the Rust port of `engine/render/ipc/` (`sharedmemoryregion.cpp`,
|
||||
//! `oliveimpl/render/ipc/frameslotpool.cpp`) behind the frozen C ABI in
|
||||
//! `engine/include/oakengine/ipc.h`.
|
||||
//! Render-worker IPC: the control-plane NDJSON protocol and the
|
||||
//! shared-memory frame-slot transport, owned by the oakengine facade and
|
||||
//! exported through the frozen `oakengine_ipc_*` C ABI
|
||||
//! (`engine/include/oakengine/ipc.h`); the `oak-worker` binary consumes it
|
||||
//! purely through the C ABI. The transport is the Rust port of
|
||||
//! `engine/render/ipc/` + `ipcmessage.cpp`.
|
||||
//!
|
||||
//! Two pieces, mirroring the C++ exactly:
|
||||
//! Two halves:
|
||||
//!
|
||||
//! - [`SharedMemoryRegion`]: a named POSIX segment (`shm_open` + `mmap`,
|
||||
//! `munmap` + `shm_unlink` on close). One process creates the segment
|
||||
//! (owner, unlinks on close); the peer attaches to it by key.
|
||||
//! - [`FrameSlotPool`]: a fixed pool of equal-sized frame slots laid out
|
||||
//! inside a region, with lock-free hand-off through two
|
||||
//! - **Control plane.** One compact JSON object per line on the stdio
|
||||
//! pipes (worker.cpp / ipcmessage.cpp `write_message`/`read_message`).
|
||||
//! Every message carries a `"type"` string; the field names below are
|
||||
//! the ones the C++ serializers actually emit
|
||||
//! (`engine/render/ipc/ipcmessage.cpp`): note `ticket` / `node` /
|
||||
//! `channels` / `slot` — the longer names (`ticket_id`, `node_uuid`,
|
||||
//! `channel_count`, `output_slot`) exist only on the C POD structs in
|
||||
//! `ipc.h`. [`write_message`]/[`error_message`] build the wire lines.
|
||||
//! - **Data plane.** Named shared memory holding the frame-slot pools —
|
||||
//! the port of `engine/render/ipc/` (`sharedmemoryregion.cpp`,
|
||||
//! `frameslotpool.cpp`): [`SharedMemoryRegion`] maps a named POSIX
|
||||
//! segment (`shm_open` + `mmap`, `munmap` + `shm_unlink` on close),
|
||||
//! and [`FrameSlotPool`] lays out a fixed pool of equal-sized frame
|
||||
//! slots inside it with lock-free hand-off through two
|
||||
//! [`SpscRingBuffer`]s of slot indices (free + ready). Each ring is a
|
||||
//! single-producer/single-consumer structure; the filler owns
|
||||
//! `free.pop` + `ready.push`, the drainer owns `ready.pop` + `free.push`,
|
||||
//! so no mutex is ever taken.
|
||||
//! `free.pop` + `ready.push`, the drainer owns `ready.pop` +
|
||||
//! `free.push`, so no mutex is ever taken.
|
||||
//!
|
||||
//! **The in-memory layout is the version-1 wire protocol** the app and the
|
||||
//! render worker share, and it never changes: the byte offsets below are
|
||||
@@ -40,15 +51,178 @@
|
||||
//!
|
||||
//! This module is deliberately unsafe-heavy and self-contained: it touches
|
||||
//! raw shared memory and raw POSIX syscalls, and everything else in the
|
||||
//! facade reaches it through the safe wrapper methods and the C ABI exports
|
||||
//! at the bottom.
|
||||
//! crate reaches it through the safe wrapper methods.
|
||||
//!
|
||||
//! Message types (M = main/editor, W = worker):
|
||||
//! handshake M<->W negotiate protocol version + announce shm geometry
|
||||
//! load_graph M ->W path to a temp file holding the serialized graph
|
||||
//! render_frame M ->W request a frame render (ticket, node, time, params)
|
||||
//! frame_ready W ->M a rendered frame is published (slot + ticket)
|
||||
//! cancel M ->W abandon an in-flight ticket
|
||||
//! graph_update M ->W reserved (no payload struct yet)
|
||||
//! shutdown M ->W finish current work and exit cleanly
|
||||
//! error W ->M worker-side failure report ("message" field)
|
||||
//!
|
||||
//! Items the worker does not emit yet (frame_ready, graph_update,
|
||||
//! `FrameReadyMsg`) and message ids it ignores (`cancel`) are kept as the
|
||||
//! documented protocol surface; `dead_code` until the frame-slot transport
|
||||
//! is driven by a real graph (see [`crate::worker`]).
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
use std::io::{self, Write};
|
||||
use std::ptr;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::handle::{guard_int, guard_ptr};
|
||||
|
||||
/// `"handshake"`.
|
||||
pub const TYPE_HANDSHAKE: &str = "handshake";
|
||||
/// `"load_graph"`.
|
||||
pub const TYPE_LOAD_GRAPH: &str = "load_graph";
|
||||
/// `"render_frame"`.
|
||||
pub const TYPE_RENDER_FRAME: &str = "render_frame";
|
||||
/// `"frame_ready"`.
|
||||
pub const TYPE_FRAME_READY: &str = "frame_ready";
|
||||
/// `"cancel"`.
|
||||
pub const TYPE_CANCEL: &str = "cancel";
|
||||
/// `"graph_update"`.
|
||||
pub const TYPE_GRAPH_UPDATE: &str = "graph_update";
|
||||
/// `"shutdown"`.
|
||||
pub const TYPE_SHUTDOWN: &str = "shutdown";
|
||||
/// `"error"`.
|
||||
pub const TYPE_ERROR: &str = "error";
|
||||
|
||||
/// `handshake` — field-for-field equivalent of `oak_ipc_handshake`
|
||||
/// (ipc.h). Wire field names match the C++ serializer.
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct HandshakeMsg {
|
||||
/// Protocol version.
|
||||
pub protocol_version: i32,
|
||||
/// Worker->main output shared-memory segment key.
|
||||
pub shm_key: String,
|
||||
/// Main->worker input shared-memory segment key (optional).
|
||||
pub input_shm_key: String,
|
||||
/// Number of main->worker input frame slots.
|
||||
pub input_slots: i32,
|
||||
/// Number of worker->main output frame slots.
|
||||
pub output_slots: i32,
|
||||
/// Per-output-slot pixel block size.
|
||||
pub slot_data_bytes: i64,
|
||||
/// Per-input-slot pixel block size.
|
||||
pub input_slot_data_bytes: i64,
|
||||
}
|
||||
|
||||
impl HandshakeMsg {
|
||||
/// The worker's startup handshake (`worker.cpp startup_handshake()`).
|
||||
pub fn to_json(&self) -> Value {
|
||||
json!({
|
||||
"type": TYPE_HANDSHAKE,
|
||||
"protocol_version": self.protocol_version,
|
||||
"shm_key": self.shm_key,
|
||||
"input_shm_key": self.input_shm_key,
|
||||
"input_slots": self.input_slots,
|
||||
"output_slots": self.output_slots,
|
||||
"slot_data_bytes": self.slot_data_bytes,
|
||||
"input_slot_data_bytes": self.input_slot_data_bytes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// `render_frame` — request a frame render. Wire names per ipcmessage.cpp:
|
||||
/// `ticket`, `node`, `channels` (not the ipc.h POD names).
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct RenderFrameMsg {
|
||||
/// Correlates with the eventual frame_ready.
|
||||
pub ticket: i64,
|
||||
/// Viewer node stable uuid in the loaded graph.
|
||||
pub node: String,
|
||||
/// Frame timestamp numerator.
|
||||
pub time_num: i64,
|
||||
/// Frame timestamp denominator.
|
||||
pub time_den: i64,
|
||||
/// Forced output size (0 = graph default).
|
||||
pub width: i32,
|
||||
/// Forced output height (0 = graph default).
|
||||
pub height: i32,
|
||||
/// Forced PixelFormat (-1 = default).
|
||||
pub format: i32,
|
||||
/// Channel count (0 = default).
|
||||
pub channels: i32,
|
||||
/// RenderMode.
|
||||
pub mode: i32,
|
||||
/// Optional decoded input slot (-1 = none).
|
||||
pub input_slot: i32,
|
||||
/// Ordered decoded input slots.
|
||||
pub input_slots: Vec<i32>,
|
||||
/// Output color transform present?
|
||||
pub has_color_transform: bool,
|
||||
/// Color transform targets the display space.
|
||||
pub color_is_display: bool,
|
||||
/// Output color space name.
|
||||
pub color_output: String,
|
||||
/// Output color view name.
|
||||
pub color_view: String,
|
||||
/// Output color look name.
|
||||
pub color_look: String,
|
||||
}
|
||||
|
||||
/// `frame_ready` — a rendered frame is published (wire names `ticket`/
|
||||
/// `slot`).
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct FrameReadyMsg {
|
||||
/// Correlates with the render_frame request.
|
||||
pub ticket: i64,
|
||||
/// Index into the worker->main output FrameSlotPool.
|
||||
pub slot: i32,
|
||||
}
|
||||
|
||||
/// `cancel` — abandon an in-flight ticket by id.
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct CancelMsg {
|
||||
/// The in-flight ticket id to abandon.
|
||||
pub ticket: i64,
|
||||
}
|
||||
|
||||
/// `load_graph` — path to a temporary file holding the serialized graph.
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct LoadGraphMsg {
|
||||
/// Path to the temporary file holding the serialized graph.
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// Build a worker-side error report, mirroring `error_message()` in
|
||||
/// worker.cpp: `{"type":"error","message":...}` plus `"ticket"` when
|
||||
/// non-zero.
|
||||
pub fn error_message(message: &str, ticket: Option<i64>) -> Value {
|
||||
match ticket.filter(|t| *t != 0) {
|
||||
Some(t) => json!({ "type": TYPE_ERROR, "message": message, "ticket": t }),
|
||||
None => json!({ "type": TYPE_ERROR, "message": message }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Write one NDJSON message line (compact JSON + `\n`), the Rust port of
|
||||
/// `ipcmessage.cpp write_message()`.
|
||||
pub fn write_message(w: &mut impl Write, msg: &Value) -> io::Result<()> {
|
||||
let line =
|
||||
serde_json::to_string(msg).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
w.write_all(line.as_bytes())?;
|
||||
w.write_all(b"\n")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared-memory frame-slot transport
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `OAK_IPC_SHM_KEY_CAP` — capacity of shm key strings (ipc.h), incl. NUL.
|
||||
pub const OAK_IPC_SHM_KEY_CAP: usize = 128;
|
||||
/// `OAK_IPC_COLORSPACE_CAP` — capacity of `oak_frame_slot_meta::colorspace`.
|
||||
@@ -77,6 +251,8 @@ pub enum ShmMode {
|
||||
}
|
||||
|
||||
impl ShmMode {
|
||||
/// Map the C ABI mode integer (`OAK_IPC_SHM_MODE_CREATE` = 0,
|
||||
/// `OAK_IPC_SHM_MODE_ATTACH` = 1) back to the enum.
|
||||
fn from_c(v: c_int) -> ShmMode {
|
||||
match v {
|
||||
0 => ShmMode::Create,
|
||||
@@ -748,7 +924,7 @@ impl SharedMemoryRegion {
|
||||
/// Unmap and (if owner) unlink the segment. Also called by `Drop`.
|
||||
pub fn close(&mut self) {
|
||||
if !self.data.is_null() {
|
||||
unsafe { libc::munmap(self.data as *mut c_void, self.size) };
|
||||
unsafe { libc::munmap(self.data as *mut std::ffi::c_void, self.size) };
|
||||
self.data = ptr::null_mut();
|
||||
}
|
||||
if self.fd >= 0 {
|
||||
@@ -804,7 +980,503 @@ impl Drop for SharedMemoryRegion {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ---- Control-plane protocol ------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn handshake_wire_format_matches_cpp_field_names() {
|
||||
let hs = HandshakeMsg {
|
||||
protocol_version: 1,
|
||||
shm_key: "olive-rw-1234-0-out".into(),
|
||||
input_shm_key: "".into(),
|
||||
input_slots: 0,
|
||||
output_slots: 6,
|
||||
slot_data_bytes: 4096,
|
||||
input_slot_data_bytes: 0,
|
||||
};
|
||||
let value = hs.to_json();
|
||||
// Key order is not part of the contract (JSON objects; the C++
|
||||
// QJsonObject is hash-ordered too), but the names must match the
|
||||
// C++ serializer exactly.
|
||||
assert_eq!(value["type"], "handshake");
|
||||
assert_eq!(value["protocol_version"], 1);
|
||||
assert_eq!(value["shm_key"], "olive-rw-1234-0-out");
|
||||
assert_eq!(value["input_shm_key"], "");
|
||||
assert_eq!(value["input_slots"], 0);
|
||||
assert_eq!(value["output_slots"], 6);
|
||||
assert_eq!(value["slot_data_bytes"], 4096);
|
||||
assert_eq!(value["input_slot_data_bytes"], 0);
|
||||
// And the serialized line must parse back to the same object.
|
||||
let round: serde_json::Value =
|
||||
serde_json::from_str(&serde_json::to_string(&value).unwrap()).unwrap();
|
||||
assert_eq!(round, value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_frame_parse_accepts_cpp_field_names() {
|
||||
let json = r#"{"type":"render_frame","ticket":42,"node":"abcd","time_num":1,"time_den":24,"width":1920,"height":1080,"format":-1,"channels":0,"mode":0,"input_slot":-1,"input_slots":[],"has_color_transform":false,"color_output":"","color_view":"","color_look":""}"#;
|
||||
let m: RenderFrameMsg = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(m.ticket, 42);
|
||||
assert_eq!(m.node, "abcd");
|
||||
assert_eq!(m.time_num, 1);
|
||||
assert_eq!(m.time_den, 24);
|
||||
assert_eq!(m.width, 1920);
|
||||
assert_eq!(m.input_slot, -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_frame_defaults_on_missing_fields() {
|
||||
// The C++ parser defaults missing fields (QJsonValue defaults);
|
||||
// serde(default) mirrors that.
|
||||
let m: RenderFrameMsg =
|
||||
serde_json::from_str(r#"{"type":"render_frame","ticket":7}"#).unwrap();
|
||||
assert_eq!(m.ticket, 7);
|
||||
assert_eq!(m.time_den, 0);
|
||||
assert!(m.node.is_empty());
|
||||
assert!(!m.has_color_transform);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_message_carries_ticket_only_when_nonzero() {
|
||||
assert_eq!(
|
||||
error_message("boom", None),
|
||||
json!({ "type": "error", "message": "boom" })
|
||||
);
|
||||
assert_eq!(
|
||||
error_message("boom", Some(0)),
|
||||
json!({ "type": "error", "message": "boom" })
|
||||
);
|
||||
assert_eq!(
|
||||
error_message("boom", Some(9)),
|
||||
json!({ "type": "error", "message": "boom", "ticket": 9 })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_message_emits_one_json_line() {
|
||||
let mut buf = Vec::new();
|
||||
write_message(&mut buf, &json!({ "type": "shutdown" })).unwrap();
|
||||
assert_eq!(String::from_utf8(buf).unwrap(), "{\"type\":\"shutdown\"}\n");
|
||||
}
|
||||
|
||||
// ---- Shared-memory transport -----------------------------------------
|
||||
|
||||
/// A unique, temporary POSIX segment key for a test (pid + counter), so
|
||||
/// parallel test runs never collide.
|
||||
fn test_key(name: &str) -> String {
|
||||
static COUNTER: AtomicU32 = AtomicU32::new(0);
|
||||
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
SharedMemoryRegion::make_key(i64::from(std::process::id()), (n & 0x7FFF) as i32)
|
||||
+ &format!("-{name}")
|
||||
}
|
||||
|
||||
/// Create one segment and map it a second time — the in-process
|
||||
/// equivalent of two processes sharing a segment. Returns
|
||||
/// `(owner_region, peer_region)`; both must be kept alive for the
|
||||
/// whole test (the peer is an attach that does not unlink).
|
||||
fn two_mappings(key: &str, size: usize) -> (SharedMemoryRegion, SharedMemoryRegion) {
|
||||
let mut owner = SharedMemoryRegion::new();
|
||||
assert!(
|
||||
owner.open(key, size, ShmMode::Create),
|
||||
"create failed: {}",
|
||||
owner.error()
|
||||
);
|
||||
let mut peer = SharedMemoryRegion::new();
|
||||
assert!(
|
||||
peer.open(key, size, ShmMode::Attach),
|
||||
"attach failed: {}",
|
||||
peer.error()
|
||||
);
|
||||
(owner, peer)
|
||||
}
|
||||
|
||||
// ---- SpscRingBuffer -------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn ring_bytes_needed_matches_cpp_layout() {
|
||||
// 12 header bytes + capacity * 4.
|
||||
assert_eq!(SpscRingBuffer::bytes_needed(4), 12 + 16);
|
||||
assert_eq!(SpscRingBuffer::bytes_needed(5), 12 + 20);
|
||||
assert_eq!(SpscRingBuffer::bytes_needed(0), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ring_empty_full_and_single_entry() {
|
||||
let key = test_key("ring-empty");
|
||||
let size = SpscRingBuffer::bytes_needed(4);
|
||||
let (owner, peer) = two_mappings(&key, size);
|
||||
// SAFETY: both mappings are live and at least `size` bytes.
|
||||
let prod = unsafe { SpscRingBuffer::create(owner.data(), 4) };
|
||||
let cons = unsafe { SpscRingBuffer::attach(peer.data()) };
|
||||
|
||||
assert!(unsafe { cons.is_empty_approx() });
|
||||
let mut v = 99;
|
||||
assert!(!unsafe { cons.pop(&mut v) });
|
||||
assert_eq!(v, 99);
|
||||
|
||||
assert!(unsafe { prod.push(7) });
|
||||
assert!(!unsafe { cons.is_empty_approx() });
|
||||
assert_eq!(unsafe { cons.size_approx() }, 1);
|
||||
assert!(unsafe { cons.pop(&mut v) });
|
||||
assert_eq!(v, 7);
|
||||
assert!(unsafe { cons.is_empty_approx() });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ring_capacity_minus_one_live_entries() {
|
||||
// A ring of capacity N holds at most N-1 entries (one slot is
|
||||
// always left empty to tell full from empty).
|
||||
let key = test_key("ring-cap");
|
||||
let size = SpscRingBuffer::bytes_needed(4);
|
||||
let (owner, peer) = two_mappings(&key, size);
|
||||
// SAFETY: live mappings.
|
||||
let prod = unsafe { SpscRingBuffer::create(owner.data(), 4) };
|
||||
let cons = unsafe { SpscRingBuffer::attach(peer.data()) };
|
||||
|
||||
for i in 0..3 {
|
||||
assert!(unsafe { prod.push(i) });
|
||||
}
|
||||
// The 4th push must fail: head would collide with tail.
|
||||
assert!(!unsafe { prod.push(99) });
|
||||
|
||||
let mut v = 0;
|
||||
for expected in 0..3 {
|
||||
assert!(unsafe { cons.pop(&mut v) });
|
||||
assert_eq!(v, expected);
|
||||
}
|
||||
assert!(!unsafe { cons.pop(&mut v) });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ring_wraparound_preserves_order() {
|
||||
// Fill, drain, then wrap past the end of the slot array: cursors
|
||||
// are modulo-capacity, order must be preserved across the wrap.
|
||||
let key = test_key("ring-wrap");
|
||||
let size = SpscRingBuffer::bytes_needed(4);
|
||||
let (owner, peer) = two_mappings(&key, size);
|
||||
// SAFETY: live mappings.
|
||||
let prod = unsafe { SpscRingBuffer::create(owner.data(), 4) };
|
||||
let cons = unsafe { SpscRingBuffer::attach(peer.data()) };
|
||||
|
||||
for i in 0..3 {
|
||||
assert!(unsafe { prod.push(i) });
|
||||
}
|
||||
let mut v = 0;
|
||||
for _ in 0..3 {
|
||||
assert!(unsafe { cons.pop(&mut v) });
|
||||
}
|
||||
// Ring is empty again; push past the wrap point.
|
||||
for i in 3..6 {
|
||||
assert!(unsafe { prod.push(i) });
|
||||
}
|
||||
for expected in 3..6 {
|
||||
assert!(unsafe { cons.pop(&mut v) });
|
||||
assert_eq!(v, expected);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- FrameSlotPool --------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn framepool_bytes_needed_matches_cpp_offsets() {
|
||||
// Recompute by hand with the C++ layout: header 64, each ring
|
||||
// align_up(12 + 4*(n+1), 64), meta align_up(176*n, 64), data
|
||||
// align_up(slot_bytes, 64) * n.
|
||||
let check = |n: u32, slot: usize| {
|
||||
let ring = align_up(12 + 4 * (n as usize + 1), 64);
|
||||
let expected =
|
||||
64 + ring + ring + align_up(176 * n as usize, 64) + align_up(slot, 64) * n as usize;
|
||||
assert_eq!(FrameSlotPool::bytes_needed(n, slot), expected);
|
||||
};
|
||||
check(4, 4096);
|
||||
check(6, 1_000_000);
|
||||
check(1, 64);
|
||||
check(3, 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn framepool_create_attach_two_processes_both_directions() {
|
||||
// "Two processes": two mappings of the same segment. Owner creates
|
||||
// the pool; the peer attaches. A filler on one side and a drainer
|
||||
// on the other exchange slots in both directions.
|
||||
let key = test_key("pool-bidi");
|
||||
let slots = 4u32;
|
||||
let slot_bytes = 64usize;
|
||||
let size = FrameSlotPool::bytes_needed(slots, slot_bytes);
|
||||
let (owner, peer) = two_mappings(&key, size);
|
||||
|
||||
// SAFETY: both mappings are live and sized by bytes_needed.
|
||||
let filler = unsafe { FrameSlotPool::create(owner.data(), slots, slot_bytes) };
|
||||
let drainer = unsafe { FrameSlotPool::attach(peer.data()) };
|
||||
|
||||
assert!(filler.is_valid());
|
||||
assert!(drainer.is_valid());
|
||||
assert_eq!(drainer.slot_count(), slots);
|
||||
assert_eq!(drainer.slot_data_bytes(), slot_bytes);
|
||||
|
||||
// Filler acquires every slot exactly once (seeded free ring), then
|
||||
// the free ring is empty.
|
||||
let mut got = Vec::new();
|
||||
for _ in 0..slots {
|
||||
let mut s = 0;
|
||||
assert!(unsafe { filler.acquire(&mut s) });
|
||||
got.push(s);
|
||||
}
|
||||
got.sort_unstable();
|
||||
assert_eq!(got, vec![0, 1, 2, 3]);
|
||||
let mut extra = 0;
|
||||
assert!(!unsafe { filler.acquire(&mut extra) });
|
||||
// Drainer sees nothing ready yet.
|
||||
assert!(!unsafe { drainer.consume(&mut extra) });
|
||||
|
||||
// Filler writes pixels + meta into two slots and publishes them.
|
||||
for (i, slot) in [0u32, 2u32].iter().enumerate() {
|
||||
// SAFETY: `slot` was acquired above.
|
||||
let data = unsafe { filler.slot_data(*slot) };
|
||||
unsafe { ptr::write_bytes(data, (i * 40 + 1) as u8, slot_bytes) };
|
||||
// SAFETY: slot in range.
|
||||
let meta = unsafe { &mut *filler.meta(*slot) };
|
||||
meta.id = 100 + *slot as i64;
|
||||
meta.width = 8;
|
||||
meta.height = 8;
|
||||
meta.data_size = slot_bytes as i32;
|
||||
assert!(unsafe { filler.publish(*slot) });
|
||||
}
|
||||
|
||||
// Drainer consumes them through its own mapping and sees the same
|
||||
// payloads and metadata.
|
||||
let mut consumed = Vec::new();
|
||||
for _ in 0..2 {
|
||||
let mut s = 0;
|
||||
assert!(unsafe { drainer.consume(&mut s) });
|
||||
// SAFETY: s was consumed.
|
||||
let data = unsafe { drainer.slot_data_const(s) };
|
||||
let meta = unsafe { &*drainer.meta_const(s) };
|
||||
assert_eq!(meta.id, 100 + s as i64);
|
||||
assert_eq!(meta.width, 8);
|
||||
assert_eq!(meta.data_size, slot_bytes as i32);
|
||||
// SAFETY: slot_bytes readable in the slot block.
|
||||
let first = unsafe { *data };
|
||||
assert_eq!(first, ((s as usize / 2) * 40 + 1) as u8);
|
||||
consumed.push(s);
|
||||
}
|
||||
consumed.sort_unstable();
|
||||
assert_eq!(consumed, vec![0, 2]);
|
||||
assert!(!unsafe { drainer.consume(&mut extra) });
|
||||
|
||||
// Drainer releases the slots back; the filler can acquire them
|
||||
// again — the full round trip through both rings.
|
||||
for s in consumed {
|
||||
assert!(unsafe { drainer.release(s) });
|
||||
}
|
||||
let mut s = 0;
|
||||
assert!(unsafe { filler.acquire(&mut s) });
|
||||
assert_eq!(s, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn framepool_wraparound_and_full_edges() {
|
||||
// Small pool: cycle every slot many times, verifying the rings'
|
||||
// modulo behavior end to end.
|
||||
let key = test_key("pool-wrap");
|
||||
let slots = 3u32;
|
||||
let slot_bytes = 32usize;
|
||||
let size = FrameSlotPool::bytes_needed(slots, slot_bytes);
|
||||
let (owner, peer) = two_mappings(&key, size);
|
||||
|
||||
// SAFETY: live mappings.
|
||||
let filler = unsafe { FrameSlotPool::create(owner.data(), slots, slot_bytes) };
|
||||
let drainer = unsafe { FrameSlotPool::attach(peer.data()) };
|
||||
|
||||
for cycle in 0..4u32 {
|
||||
let mut published = Vec::new();
|
||||
for _ in 0..slots {
|
||||
let mut s = 0;
|
||||
assert!(unsafe { filler.acquire(&mut s) }, "cycle {cycle}");
|
||||
// SAFETY: acquired slot.
|
||||
unsafe { ptr::write_bytes(filler.slot_data(s), cycle as u8, slot_bytes) };
|
||||
// SAFETY: slot in range.
|
||||
let meta = unsafe { &mut *filler.meta(s) };
|
||||
meta.id = i64::from(cycle * 100 + s);
|
||||
assert!(unsafe { filler.publish(s) });
|
||||
published.push(s);
|
||||
}
|
||||
// Pool is full on the filler side.
|
||||
let mut x = 0;
|
||||
assert!(!unsafe { filler.acquire(&mut x) });
|
||||
|
||||
// Drain everything on the drainer side.
|
||||
let mut consumed = Vec::new();
|
||||
for _ in 0..slots {
|
||||
let mut s = 0;
|
||||
assert!(unsafe { drainer.consume(&mut s) });
|
||||
// SAFETY: consumed slot.
|
||||
let meta = unsafe { &*drainer.meta_const(s) };
|
||||
assert_eq!(meta.id, i64::from(cycle * 100 + s));
|
||||
// SAFETY: 1 byte readable.
|
||||
assert_eq!(unsafe { *drainer.slot_data_const(s) }, cycle as u8);
|
||||
consumed.push(s);
|
||||
}
|
||||
assert!(!unsafe { drainer.consume(&mut x) });
|
||||
consumed.sort_unstable();
|
||||
assert_eq!(consumed, vec![0, 1, 2]);
|
||||
|
||||
for s in consumed {
|
||||
assert!(unsafe { drainer.release(s) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn framepool_attach_rejects_wrong_magic() {
|
||||
let key = test_key("pool-badmagic");
|
||||
let size = FrameSlotPool::bytes_needed(2, 16);
|
||||
let (owner, _peer) = two_mappings(&key, size);
|
||||
// Overwrite the header area with garbage — no pool magic.
|
||||
// SAFETY: owner mapping is live.
|
||||
unsafe { ptr::write_bytes(owner.data(), 0xAB, 64) };
|
||||
// SAFETY: buffer is live.
|
||||
let pool = unsafe { FrameSlotPool::attach(owner.data()) };
|
||||
assert!(!pool.is_valid());
|
||||
assert_eq!(pool.slot_count(), 0);
|
||||
assert_eq!(pool.slot_data_bytes(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn framepool_pool_over_reused_segment_is_consistent() {
|
||||
// A pool that has been cycled fully and then attached fresh reports
|
||||
// the same geometry as bytes_needed computed it.
|
||||
let key = test_key("pool-geometry");
|
||||
let slots = 5u32;
|
||||
let slot_bytes = 1000usize;
|
||||
let size = FrameSlotPool::bytes_needed(slots, slot_bytes);
|
||||
let (owner, peer) = two_mappings(&key, size);
|
||||
// SAFETY: live mappings.
|
||||
let _ = unsafe { FrameSlotPool::create(owner.data(), slots, slot_bytes) };
|
||||
let attached = unsafe { FrameSlotPool::attach(peer.data()) };
|
||||
assert!(attached.is_valid());
|
||||
assert_eq!(attached.slot_count(), slots);
|
||||
assert_eq!(attached.slot_data_bytes(), slot_bytes);
|
||||
// Slot stride is 64-aligned (matches the C++ data layout).
|
||||
// SAFETY: valid pool.
|
||||
let s0 = unsafe { attached.slot_data(0) };
|
||||
let s1 = unsafe { attached.slot_data(1) };
|
||||
assert_eq!(s1 as usize - s0 as usize, align_up(slot_bytes, K_ALIGN));
|
||||
}
|
||||
|
||||
// ---- SharedMemoryRegion ---------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn region_create_attach_write_visibility() {
|
||||
let key = test_key("region-vis");
|
||||
let size = 4096usize;
|
||||
let (mut owner, mut peer) = two_mappings(&key, size);
|
||||
assert!(owner.is_valid());
|
||||
assert!(peer.is_valid());
|
||||
assert_eq!(owner.size(), size);
|
||||
assert_eq!(peer.size(), size);
|
||||
assert_eq!(owner.key(), key);
|
||||
assert_eq!(peer.key(), key);
|
||||
|
||||
// Owner writes; peer sees it through its own mapping.
|
||||
// SAFETY: both mappings are live with `size` bytes.
|
||||
unsafe {
|
||||
let dst = owner.data() as *mut u32;
|
||||
*dst = 0xDEADBEEF;
|
||||
}
|
||||
// SAFETY: peer mapping live.
|
||||
let seen = unsafe { *(peer.data() as *const u32) };
|
||||
assert_eq!(seen, 0xDEADBEEF);
|
||||
|
||||
// Peer writes back; owner sees it.
|
||||
// SAFETY: peer mapping live.
|
||||
unsafe {
|
||||
let dst = peer.data() as *mut u32;
|
||||
*dst = 0x12345678;
|
||||
}
|
||||
// SAFETY: owner mapping live.
|
||||
assert_eq!(unsafe { *(owner.data() as *const u32) }, 0x12345678);
|
||||
|
||||
// Closing the ATTACH side does not unlink: while the owner lives,
|
||||
// a third mapping can still open the name.
|
||||
peer.close();
|
||||
assert!(!peer.is_valid());
|
||||
let mut third = SharedMemoryRegion::new();
|
||||
assert!(third.open(&key, size, ShmMode::Attach), "{}", third.error());
|
||||
assert!(third.is_valid());
|
||||
third.close();
|
||||
|
||||
// Closing the OWNER unlinks the segment; further attaches fail.
|
||||
owner.close();
|
||||
assert!(!owner.is_valid());
|
||||
let mut fourth = SharedMemoryRegion::new();
|
||||
assert!(!fourth.open(&key, size, ShmMode::Attach));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn region_create_replaces_stale_segment() {
|
||||
// Mirrors the C++: Create unlinks any stale segment with the same
|
||||
// name first (crash cleanup), so a second Create SUCCEEDS and owns
|
||||
// a fresh, zeroed segment.
|
||||
let key = test_key("region-exists");
|
||||
let size = 128usize;
|
||||
let (mut owner, _peer) = two_mappings(&key, size);
|
||||
assert!(owner.is_valid());
|
||||
// SAFETY: owner mapping live.
|
||||
unsafe { *(owner.data() as *mut u32) = 0xCAFEBABE };
|
||||
|
||||
let mut second = SharedMemoryRegion::new();
|
||||
assert!(
|
||||
second.open(&key, size, ShmMode::Create),
|
||||
"{}",
|
||||
second.error()
|
||||
);
|
||||
assert!(second.is_valid());
|
||||
// The replacement segment is fresh (zeroed by create).
|
||||
// SAFETY: second mapping live.
|
||||
assert_eq!(unsafe { *(second.data() as *const u32) }, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn region_attach_fails_when_segment_too_small() {
|
||||
// macOS rounds shm segment sizes up to a 16 KiB minimum, so use
|
||||
// sizes above that to exercise the size check.
|
||||
let key = test_key("region-small");
|
||||
let (owner, _peer) = two_mappings(&key, 4096);
|
||||
assert!(owner.is_valid());
|
||||
|
||||
// Attaching with a larger size than the segment must fail (the
|
||||
// fstat check, mirroring the C++).
|
||||
let mut big = SharedMemoryRegion::new();
|
||||
assert!(!big.open(&key, 65536, ShmMode::Attach));
|
||||
assert!(!big.is_valid());
|
||||
assert!(!big.error().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn region_make_key_format() {
|
||||
assert_eq!(SharedMemoryRegion::make_key(4242, 3), "olive-rw-4242-3");
|
||||
assert_eq!(SharedMemoryRegion::make_key(1, 0), "olive-rw-1-0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn region_keys_are_isolation_safe() {
|
||||
// Keys with slashes are flattened to a single-slash POSIX name.
|
||||
let key = "a/b/c";
|
||||
let size = 64usize;
|
||||
let (mut owner, mut peer) = two_mappings(key, size);
|
||||
assert!(owner.is_valid());
|
||||
assert!(peer.is_valid());
|
||||
// The actual POSIX name is "/a_b_c".
|
||||
// SAFETY: mapping live.
|
||||
unsafe { *(owner.data() as *mut u32) = 7 };
|
||||
// SAFETY: peer mapping live.
|
||||
assert_eq!(unsafe { *(peer.data() as *const u32) }, 7);
|
||||
}
|
||||
}
|
||||
// C ABI exports (engine/include/oakengine/ipc.h)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1219,7 +1891,7 @@ pub unsafe extern "C" fn oakengine_ipc_framepool_release(
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
mod cabi_tests {
|
||||
use super::*;
|
||||
|
||||
/// A unique, temporary POSIX segment key for a test (pid + counter), so
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
pub use oaknode;
|
||||
|
||||
pub mod audio;
|
||||
pub mod bridge;
|
||||
pub mod codec;
|
||||
pub mod common;
|
||||
pub mod deferred;
|
||||
@@ -77,28 +76,35 @@ pub mod ipc;
|
||||
pub mod linkage;
|
||||
pub mod node;
|
||||
pub mod plugin;
|
||||
pub mod pods;
|
||||
pub mod render;
|
||||
pub mod stubs;
|
||||
pub mod task;
|
||||
pub mod testmedia;
|
||||
pub mod timeline;
|
||||
pub mod undo;
|
||||
pub mod worker;
|
||||
|
||||
/// The former `tests/*.rs` integration tests, now unit tests (the facade
|
||||
/// is cdylib-only, so integration tests cannot link it as an rlib crate;
|
||||
/// see `test_support/mod.rs`).
|
||||
#[cfg(test)]
|
||||
#[path = "test_support/mod.rs"]
|
||||
mod tests;
|
||||
#[cfg(test)]
|
||||
mod test_link {
|
||||
// The lib's own unit-test binary must link the module crates' rlibs to
|
||||
// satisfy the facade's `extern "C"` imports that the unit tests compile
|
||||
// in — e.g. the worker session's oakrender display renderer (src/worker.rs).
|
||||
// satisfy the facade's imports that the unit tests compile in — e.g.
|
||||
// the render family's oakrender display renderer (src/render.rs).
|
||||
// The integration tests do the same through tests/common/mod.rs
|
||||
// `force_link()`; this covers the `cargo test` unit-test binary.
|
||||
#![allow(dead_code)]
|
||||
fn force_link() -> usize {
|
||||
let fns: [usize; 4] = [
|
||||
oakrender::ffi::renderer::oakrender_display_renderer_create_opengl as *const ()
|
||||
as usize,
|
||||
oaknode::ffi::project::oaknode_project_init as *const () as usize,
|
||||
oaktimeline::ffi::marker::oaktimeline_marker_list_create as *const () as usize,
|
||||
oaktask::ffi::manager::oaktask_manager_init as *const () as usize,
|
||||
oakrender::backend::DisplayRenderer::new as *const () as usize,
|
||||
oaknode::project::Project::new as *const () as usize,
|
||||
oaktimeline::marker::TimelineMarkerList::new as *const () as usize,
|
||||
oaktask::manager::TaskManager::init as *const () as usize,
|
||||
];
|
||||
fns.iter().sum()
|
||||
}
|
||||
|
||||
@@ -39,23 +39,24 @@
|
||||
/// runtime via dlsym(RTLD_DEFAULT) and they must be present in the dylib
|
||||
/// for that lookup to succeed.
|
||||
fn force_link() -> usize {
|
||||
let fns: [usize; 13] = [
|
||||
let fns: [usize; 11] = [
|
||||
// oakcore-rs (pure value types; referenced so its rlib is linked).
|
||||
oakcore_rs::Rational::new(1, 2).numerator() as usize,
|
||||
// One exported C ABI symbol per module crate.
|
||||
oakundo::ffi::undostack::oakundo_undostack_init as usize,
|
||||
oakcommon::ffi::config::oakcommon_config_get_int as usize,
|
||||
oaktimeline::ffi::marker::oaktimeline_marker_list_create as usize,
|
||||
oakcodec::ffi::format::oakcodec_encoding_format_count as usize,
|
||||
oakaudio::ffi::waveform::oakaudio_waveform_length as usize,
|
||||
oakrender::ffi::cache::oakrender_cache_indicator_height as usize,
|
||||
oaktask::ffi::manager::oaktask_manager_init as usize,
|
||||
oakplugin::ffi::oakplugin_host_plugin_count as usize,
|
||||
oaknode::ffi::project::oaknode_project_init as usize,
|
||||
// oaknode's dlsym(RTLD_DEFAULT) targets (see tests/common/mod.rs).
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_writer_init as usize,
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_reader_init as usize,
|
||||
oakundo::ffi::command::oakundo_command_init as usize,
|
||||
// One public direct-Rust symbol per module crate. oakundo/oakcommon
|
||||
// no longer export a C ABI; their handle-level Rust API functions
|
||||
// serve as the link anchors.
|
||||
oakundo::undostack::undostack_init as usize,
|
||||
oakcommon::configstore::ConfigStore::instance as usize,
|
||||
oaktimeline::marker::TimelineMarkerList::new as usize,
|
||||
oakcodec::exportformat::Format::get_name as usize,
|
||||
oakrender::manager::RenderManager::init as usize,
|
||||
oaktask::manager::TaskManager::init as usize,
|
||||
oaknode::project::Project::new as usize,
|
||||
// oaknode's serializer resolves oakcommon XML/undo symbols at
|
||||
// runtime; anchors for the dylib.
|
||||
oakcommon::xmlutils::XmlWriter::new as usize,
|
||||
oakcommon::xmlutils::XmlReader::new as usize,
|
||||
oakundo::undocommand::command_init as usize,
|
||||
];
|
||||
fns.iter().sum()
|
||||
}
|
||||
|
||||
+520
-166
@@ -19,8 +19,8 @@
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
|
||||
use crate::bridge::common as c;
|
||||
use crate::bridge::node as n;
|
||||
use crate::stubs::common as c;
|
||||
use crate::stubs::node as n;
|
||||
use crate::common::OakVideoParamsPod;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::handle::{
|
||||
@@ -2518,13 +2518,16 @@ pub unsafe extern "C" fn oakengine_node_input_is_keyframed(
|
||||
self_: *const OakEngineNode,
|
||||
input_id: *const c_char,
|
||||
) -> c_int {
|
||||
// Stub: the oaknode module has no keyframing-enabled query.
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Ok(0);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
Ok(0)
|
||||
let rc = n::oaknode_node_is_input_keyframing(unbox(self_)?, input_id);
|
||||
if rc < 0 {
|
||||
Err(Error::Module(rc))
|
||||
} else {
|
||||
Ok(rc)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2534,13 +2537,16 @@ pub unsafe extern "C" fn oakengine_node_keyframe_count(
|
||||
self_: *const OakEngineNode,
|
||||
input_id: *const c_char,
|
||||
) -> c_int {
|
||||
// Stub: the oaknode module has no keyframe enumeration C ABI.
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Ok(0);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
Ok(0)
|
||||
let rc = n::oaknode_node_keyframe_count(unbox(self_)?, input_id);
|
||||
if rc < 0 {
|
||||
Err(Error::Module(rc))
|
||||
} else {
|
||||
Ok(rc)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2553,16 +2559,20 @@ pub unsafe extern "C" fn oakengine_node_keyframe_at(
|
||||
time_ts: *mut i64,
|
||||
value: *mut OakNodeValue,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
if self_.is_null() || input_id.is_null() || time_ts.is_null() || value.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = index;
|
||||
let _ = time_ts;
|
||||
let _ = value;
|
||||
Err(Error::NotFound)
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let mut num: i64 = 0;
|
||||
let mut den: i64 = 0;
|
||||
let rc = n::oaknode_node_keyframe_at(h, input_id, index, &mut num, &mut den, value);
|
||||
Error::from_module(rc)?;
|
||||
// Rational seconds -> frame timestamp in the project time base.
|
||||
let ts = num as i128 * tb.1 as i128 / den as i128 / tb.0 as i128;
|
||||
*time_ts = ts as i64;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2578,14 +2588,38 @@ pub unsafe extern "C" fn oakengine_node_keyframe_get_easing(
|
||||
y2: *mut f32,
|
||||
type_: *mut c_int,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
if self_.is_null() || input_id.is_null() || type_.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (index, x1, y1, x2, y2, type_);
|
||||
Err(Error::NotFound)
|
||||
let h = unbox(self_)?;
|
||||
// Locate the key's time through the count/at pair.
|
||||
let mut num: i64 = 0;
|
||||
let mut den: i64 = 0;
|
||||
let mut dummy = OakNodeValue::none();
|
||||
Error::from_module(n::oaknode_node_keyframe_at(
|
||||
h, input_id, index, &mut num, &mut den, &mut dummy,
|
||||
))?;
|
||||
Error::from_module(n::oaknode_node_keyframe_type_at(
|
||||
h, input_id, num, den, type_,
|
||||
))?;
|
||||
if !x1.is_null() {
|
||||
let mut bx: f64 = 0.0;
|
||||
let mut by: f64 = 0.0;
|
||||
if n::oaknode_node_keyframe_bezier_at(h, input_id, num, den, 0, &mut bx, &mut by) == 0 {
|
||||
*x1 = bx as f32;
|
||||
*y1 = by as f32;
|
||||
}
|
||||
}
|
||||
if !x2.is_null() {
|
||||
let mut bx: f64 = 0.0;
|
||||
let mut by: f64 = 0.0;
|
||||
if n::oaknode_node_keyframe_bezier_at(h, input_id, num, den, 1, &mut bx, &mut by) == 0 {
|
||||
*x2 = bx as f32;
|
||||
*y2 = by as f32;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2634,14 +2668,14 @@ pub unsafe extern "C" fn oakengine_node_keyframe_remove(
|
||||
input_id: *const c_char,
|
||||
time_ts: i64,
|
||||
) -> c_int {
|
||||
// Stub: the oaknode module has no remove-keyframe C ABI.
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = time_ts;
|
||||
Err(Error::NotFound)
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let (t_num, t_den) = ts_to_time(time_ts, tb);
|
||||
Error::from_module(n::oaknode_node_remove_keyframe(h, input_id, t_num, t_den))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2685,11 +2719,31 @@ pub unsafe extern "C" fn oakengine_node_insert_keyframe_command(
|
||||
pub unsafe extern "C" fn oakengine_node_remove_keyframe_command(
|
||||
keyframe: *mut OakEngineKeyframe,
|
||||
) -> *mut c_void {
|
||||
// Stub: the module has no remove-keyframe command creator (the module
|
||||
// keyframe handles are detached values, not track members).
|
||||
guard_ptr(|| unsafe {
|
||||
let _ = unbox(keyframe);
|
||||
Ok(std::ptr::null_mut())
|
||||
let h = unbox(keyframe)?;
|
||||
let mut num: i64 = 0;
|
||||
let mut den: i64 = 0;
|
||||
Error::from_module(n::oaknode_keyframe_get_time(h, &mut num, &mut den))?;
|
||||
let mut input_buf = [0 as c_char; 256];
|
||||
let rc = n::oaknode_keyframe_get_input(h, input_buf.as_mut_ptr(), 256);
|
||||
if rc < 0 {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
let mut parent = CHandle::null();
|
||||
Error::from_module(n::oaknode_keyframe_get_parent(h, &mut parent))?;
|
||||
// Build the undo command as a closure over the remove path (the
|
||||
// module has no remove-key command creator; the closure carries
|
||||
// the same semantics).
|
||||
let cmd = crate::stubs::node::box_keyframe_remove_command(
|
||||
parent,
|
||||
input_buf.as_ptr(),
|
||||
num,
|
||||
den,
|
||||
);
|
||||
if cmd.ctx.is_null() {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
Ok(command_box(cmd)?.cast())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2746,17 +2800,52 @@ pub unsafe extern "C" fn oakengine_node_keyframe_set_easing(
|
||||
x2: f32,
|
||||
y2: f32,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count` (no easing accessors).
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (time_ts, type_, x1, y1, x2, y2);
|
||||
Err(Error::NotFound)
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let (t_num, t_den) = ts_to_time(time_ts, tb);
|
||||
Error::from_module(n::oaknode_node_keyframe_set_type(
|
||||
h, input_id, t_num, t_den, type_,
|
||||
))?;
|
||||
Error::from_module(n::oaknode_node_keyframe_set_bezier(
|
||||
h, input_id, t_num, t_den, 0, x1 as f64, y1 as f64,
|
||||
))?;
|
||||
Error::from_module(n::oaknode_node_keyframe_set_bezier(
|
||||
h, input_id, t_num, t_den, 1, x2 as f64, y2 as f64,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// Apply a value-at-time write WITHOUT pushing an undo row (the
|
||||
/// `*_many` keyframe editors apply live writes; documented deviation
|
||||
/// from the C++ multi commands).
|
||||
///
|
||||
/// # Safety
|
||||
/// `h` must be a live module node handle; `input_id` a valid
|
||||
/// NUL-terminated string; `v` a live POD.
|
||||
unsafe fn live_set_value_at_time(
|
||||
h: CHandle,
|
||||
input_id: *const c_char,
|
||||
t_num: i64,
|
||||
t_den: i64,
|
||||
v: *const OakNodeValue,
|
||||
) -> Result<()> {
|
||||
unsafe {
|
||||
let mut cmd: CHandle = CHandle::null();
|
||||
Error::from_module(n::oaknode_node_set_input_at_time_undoable(
|
||||
h, input_id, t_num, t_den, v, 0, &mut cmd,
|
||||
))?;
|
||||
let rc = oakundo::undocommand::command_redo_now(cmd);
|
||||
let mut cmd_h = cmd;
|
||||
oakundo::undocommand::command_free(&mut cmd_h);
|
||||
Error::from_module(rc)
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakengine_node_keyframes_set_type_many`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_node_keyframes_set_type_many(
|
||||
@@ -2768,14 +2857,23 @@ pub unsafe extern "C" fn oakengine_node_keyframes_set_type_many(
|
||||
count: c_int,
|
||||
type_: c_int,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
// Live per-key type writes (the C++ grouped them into one command;
|
||||
// documented deviation — no undo row is created).
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() || count < 0 || (count > 0 && times_ts.is_null()) {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (element, times_ts, tracks, count, type_);
|
||||
Err(Error::NotFound)
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let _ = (element, tracks);
|
||||
for i in 0..count as usize {
|
||||
let ts = *times_ts.add(i);
|
||||
let (t_num, t_den) = ts_to_time(ts, tb);
|
||||
Error::from_module(n::oaknode_node_keyframe_set_type(
|
||||
h, input_id, t_num, t_den, type_,
|
||||
))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2790,14 +2888,29 @@ pub unsafe extern "C" fn oakengine_node_keyframes_set_time_many(
|
||||
count: c_int,
|
||||
new_time_ts: i64,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
// Live per-key re-times: each key is removed from its old time and
|
||||
// re-inserted at the new one with its value (no undo row; documented).
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() || count < 0 || (count > 0 && old_times_ts.is_null())
|
||||
{
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (element, old_times_ts, tracks, count, new_time_ts);
|
||||
Err(Error::NotFound)
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let (new_num, new_den) = ts_to_time(new_time_ts, tb);
|
||||
let _ = (element, tracks);
|
||||
for i in 0..count as usize {
|
||||
let old_ts = *old_times_ts.add(i);
|
||||
let (old_num, old_den) = ts_to_time(old_ts, tb);
|
||||
let mut v = OakNodeValue::none();
|
||||
let rc = n::oaknode_node_get_input_at_time(h, input_id, old_num, old_den, &mut v);
|
||||
if rc != 0 {
|
||||
return Err(Error::Module(rc));
|
||||
}
|
||||
Error::from_module(n::oaknode_node_remove_keyframe(h, input_id, old_num, old_den))?;
|
||||
live_set_value_at_time(h, input_id, new_num, new_den, &v)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2813,14 +2926,25 @@ pub unsafe extern "C" fn oakengine_node_keyframes_set_value_many(
|
||||
values: *const OakNodeValue,
|
||||
old_values: *const OakNodeValue,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
// Live per-key value writes (no undo row; the C++ grouped them —
|
||||
// documented deviation).
|
||||
guard(|| unsafe {
|
||||
if self_.is_null()
|
||||
|| input_id.is_null()
|
||||
|| count < 0
|
||||
|| (count > 0 && (times_ts.is_null() || values.is_null()))
|
||||
{
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (element, times_ts, tracks, count, values, old_values);
|
||||
Err(Error::NotFound)
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let _ = (element, tracks, old_values);
|
||||
for i in 0..count as usize {
|
||||
let ts = *times_ts.add(i);
|
||||
let (t_num, t_den) = ts_to_time(ts, tb);
|
||||
live_set_value_at_time(h, input_id, t_num, t_den, values.add(i))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2838,14 +2962,25 @@ pub unsafe extern "C" fn oakengine_node_keyframes_set_bezier_many(
|
||||
out_x: f64,
|
||||
out_y: f64,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
// Live per-key bezier writes (no undo row; documented deviation).
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() || count < 0 || (count > 0 && times_ts.is_null()) {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (element, times_ts, tracks, count, in_x, in_y, out_x, out_y);
|
||||
Err(Error::NotFound)
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let _ = (element, tracks);
|
||||
for i in 0..count as usize {
|
||||
let ts = *times_ts.add(i);
|
||||
let (t_num, t_den) = ts_to_time(ts, tb);
|
||||
Error::from_module(n::oaknode_node_keyframe_set_bezier(
|
||||
h, input_id, t_num, t_den, 0, in_x, in_y,
|
||||
))?;
|
||||
Error::from_module(n::oaknode_node_keyframe_set_bezier(
|
||||
h, input_id, t_num, t_den, 1, out_x, out_y,
|
||||
))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2863,14 +2998,17 @@ pub unsafe extern "C" fn oakengine_node_keyframe_set_bezier_point(
|
||||
old_x: f64,
|
||||
old_y: f64,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
if self_.is_null() || input_id.is_null() || (point_index != 0 && point_index != 1) {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (element, time_ts, track, point_index, x, y, old_x, old_y);
|
||||
Err(Error::NotFound)
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let (t_num, t_den) = ts_to_time(time_ts, tb);
|
||||
let _ = (element, track, old_x, old_y);
|
||||
Error::from_module(n::oaknode_node_keyframe_set_bezier(
|
||||
h, input_id, t_num, t_den, point_index, x, y,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2881,14 +3019,11 @@ pub unsafe extern "C" fn oakengine_node_keyframes_clear(
|
||||
self_: *mut OakEngineNode,
|
||||
input_id: *const c_char,
|
||||
) -> c_int {
|
||||
// Stub: the module has no clear-keyframes command; since it also has
|
||||
// no keyframes, the documented no-op result is returned.
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
Ok(())
|
||||
Error::from_module(n::oaknode_node_clear_keyframes(unbox(self_)?, input_id))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3040,14 +3175,19 @@ pub unsafe extern "C" fn oakengine_node_input_is_keyframed_ex(
|
||||
input_id: *const c_char,
|
||||
element: c_int,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_input_is_keyframed`.
|
||||
// Whole-value tracks: the element selector is recorded but the track
|
||||
// query covers the (input, -1) track (documented deviation).
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Ok(0);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let rc = n::oaknode_node_is_input_keyframing(unbox(self_)?, input_id);
|
||||
let _ = element;
|
||||
Ok(0)
|
||||
if rc < 0 {
|
||||
Err(Error::Module(rc))
|
||||
} else {
|
||||
Ok(rc)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3101,14 +3241,29 @@ pub unsafe extern "C" fn oakengine_node_input_get_default_value(
|
||||
track: c_int,
|
||||
out: *mut OakNodeValue,
|
||||
) -> c_int {
|
||||
// Stub: the oaknode module has no default-value export.
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() || out.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = track;
|
||||
Err(Error::NotFound)
|
||||
// The declared type's default maps back into the POD (same
|
||||
// conversion as the getter family).
|
||||
let mut ty: c_int = 0;
|
||||
Error::from_module(n::oaknode_node_input_get_type(
|
||||
unbox(self_)?,
|
||||
input_id,
|
||||
&mut ty,
|
||||
))?;
|
||||
// Best-effort: the default of the enabled input is Boolean(true);
|
||||
// other inputs report the None POD (documented deviation).
|
||||
let default = OakNodeValue {
|
||||
kind: ty,
|
||||
num: if ty == value_type::BOOL { 1 } else { 0 },
|
||||
den: 0,
|
||||
f: [0.0; 4],
|
||||
};
|
||||
*out = default;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5271,14 +5426,19 @@ pub unsafe extern "C" fn oakengine_node_keyframe_count_on_track(
|
||||
element: c_int,
|
||||
track: c_int,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
// Whole-value tracks: the element/track selectors are recorded but
|
||||
// the count covers the (input, -1) track (documented deviation).
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Ok(0);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let rc = n::oaknode_node_keyframe_count(unbox(self_)?, input_id);
|
||||
let _ = (element, track);
|
||||
Ok(0)
|
||||
if rc < 0 {
|
||||
Err(Error::Module(rc))
|
||||
} else {
|
||||
Ok(rc)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5293,14 +5453,35 @@ pub unsafe extern "C" fn oakengine_node_keyframes_toggle_at_time(
|
||||
on: c_int,
|
||||
undo_name: *const c_char,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (element, time_ts, track, on, undo_name);
|
||||
Err(Error::NotFound)
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let (t_num, t_den) = ts_to_time(time_ts, tb);
|
||||
let has = n::oaknode_node_has_keyframe_at_time(h, input_id, t_num, t_den);
|
||||
if has < 0 {
|
||||
return Err(Error::Module(has));
|
||||
}
|
||||
let _ = (element, track);
|
||||
if on != 0 && has == 0 {
|
||||
// Insert a key at the time carrying the current value.
|
||||
let mut v = OakNodeValue::none();
|
||||
let rc = n::oaknode_node_get_input_at_time(h, input_id, t_num, t_den, &mut v);
|
||||
if rc != 0 {
|
||||
return Err(Error::Module(rc));
|
||||
}
|
||||
let mut cmd: CHandle = CHandle::null();
|
||||
Error::from_module(n::oaknode_node_set_input_at_time_undoable(
|
||||
h, input_id, t_num, t_den, &v, 0, &mut cmd,
|
||||
))?;
|
||||
push_command(cmd, if undo_name.is_null() { "Toggle Keyframe" } else { "" })
|
||||
} else if on == 0 && has == 1 {
|
||||
Error::from_module(n::oaknode_node_remove_keyframe(h, input_id, t_num, t_den))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5313,17 +5494,70 @@ pub unsafe extern "C" fn oakengine_node_has_keyframe_at_time(
|
||||
time_ts: i64,
|
||||
track: c_int,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Ok(0);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (element, time_ts, track);
|
||||
Ok(0)
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let (t_num, t_den) = ts_to_time(time_ts, tb);
|
||||
let _ = (element, track);
|
||||
let rc = n::oaknode_node_has_keyframe_at_time(h, input_id, t_num, t_den);
|
||||
if rc < 0 {
|
||||
Err(Error::Module(rc))
|
||||
} else {
|
||||
Ok(rc)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// First (earliest) or last (latest) keyframe time of an input, written
|
||||
/// as a rational-seconds pair (0/1 when the input has no keys).
|
||||
///
|
||||
/// # Safety
|
||||
/// `self_` must be a live engine node handle; `input_id` a valid
|
||||
/// NUL-terminated string.
|
||||
unsafe fn keyframe_extreme_time(
|
||||
self_: *const OakEngineNode,
|
||||
input_id: *const c_char,
|
||||
earliest: bool,
|
||||
num: *mut i64,
|
||||
den: *mut i64,
|
||||
) -> Result<c_int> {
|
||||
unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Ok(0);
|
||||
}
|
||||
let h = unbox(self_)?;
|
||||
let count = n::oaknode_node_keyframe_count(h, input_id);
|
||||
if count <= 0 {
|
||||
if !num.is_null() {
|
||||
*num = 0;
|
||||
}
|
||||
if !den.is_null() {
|
||||
*den = 1;
|
||||
}
|
||||
return Ok(0);
|
||||
}
|
||||
let index = if earliest { 0 } else { count - 1 };
|
||||
let mut k_num: i64 = 0;
|
||||
let mut k_den: i64 = 0;
|
||||
let mut dummy = OakNodeValue::none();
|
||||
let rc = n::oaknode_node_keyframe_at(h, input_id, index, &mut k_num, &mut k_den, &mut dummy);
|
||||
if rc != 0 {
|
||||
return Err(Error::Module(rc));
|
||||
}
|
||||
if !num.is_null() {
|
||||
*num = k_num;
|
||||
}
|
||||
if !den.is_null() {
|
||||
*den = k_den;
|
||||
}
|
||||
Ok(1)
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakengine_node_keyframe_earliest_time`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_node_keyframe_earliest_time(
|
||||
@@ -5333,21 +5567,9 @@ pub unsafe extern "C" fn oakengine_node_keyframe_earliest_time(
|
||||
num: *mut i64,
|
||||
den: *mut i64,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
set_node_error("invalid arguments");
|
||||
return Ok(0);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = element;
|
||||
if !num.is_null() {
|
||||
*num = 0;
|
||||
}
|
||||
if !den.is_null() {
|
||||
*den = 1;
|
||||
}
|
||||
Ok(0)
|
||||
keyframe_extreme_time(self_, input_id, true, num, den)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5360,22 +5582,85 @@ pub unsafe extern "C" fn oakengine_node_keyframe_latest_time(
|
||||
num: *mut i64,
|
||||
den: *mut i64,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_int(|| unsafe {
|
||||
let _ = element;
|
||||
keyframe_extreme_time(self_, input_id, false, num, den)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// Closest keyframe time strictly before (or at/after) a frame timestamp,
|
||||
/// written as rational seconds (0/1 when none — the documented neutral
|
||||
/// result).
|
||||
///
|
||||
/// # Safety
|
||||
/// `self_` must be a live engine node handle; `input_id` a valid
|
||||
/// NUL-terminated string.
|
||||
unsafe fn closest_keyframe_time(
|
||||
self_: *const OakEngineNode,
|
||||
input_id: *const c_char,
|
||||
element: c_int,
|
||||
time_ts: i64,
|
||||
track: c_int,
|
||||
before: bool,
|
||||
num: *mut i64,
|
||||
den: *mut i64,
|
||||
) -> Result<c_int> {
|
||||
unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
set_node_error("invalid arguments");
|
||||
return Ok(0);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = element;
|
||||
if !num.is_null() {
|
||||
*num = 0;
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let (t_num, t_den) = ts_to_time(time_ts, tb);
|
||||
let target = t_num as f64 / t_den as f64;
|
||||
let _ = (element, track);
|
||||
let count = n::oaknode_node_keyframe_count(h, input_id);
|
||||
let mut best: Option<(i64, i64)> = None;
|
||||
for i in 0..count {
|
||||
let mut k_num: i64 = 0;
|
||||
let mut k_den: i64 = 0;
|
||||
let mut dummy = OakNodeValue::none();
|
||||
if n::oaknode_node_keyframe_at(h, input_id, i, &mut k_num, &mut k_den, &mut dummy) != 0 {
|
||||
continue;
|
||||
}
|
||||
let k = k_num as f64 / k_den as f64;
|
||||
let matches = if before { k < target } else { k >= target };
|
||||
if !matches {
|
||||
continue;
|
||||
}
|
||||
match best {
|
||||
Some((bn, bd)) => {
|
||||
let b = bn as f64 / bd as f64;
|
||||
let closer = if before { k > b } else { k < b };
|
||||
if closer {
|
||||
best = Some((k_num, k_den));
|
||||
}
|
||||
}
|
||||
None => best = Some((k_num, k_den)),
|
||||
}
|
||||
}
|
||||
if !den.is_null() {
|
||||
*den = 1;
|
||||
match best {
|
||||
Some((n_, d_)) => {
|
||||
if !num.is_null() {
|
||||
*num = n_;
|
||||
}
|
||||
if !den.is_null() {
|
||||
*den = d_;
|
||||
}
|
||||
Ok(1)
|
||||
}
|
||||
None => {
|
||||
if !num.is_null() {
|
||||
*num = 0;
|
||||
}
|
||||
if !den.is_null() {
|
||||
*den = 1;
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
Ok(0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakengine_node_keyframe_closest_time_before`.
|
||||
@@ -5389,21 +5674,8 @@ pub unsafe extern "C" fn oakengine_node_keyframe_closest_time_before(
|
||||
num: *mut i64,
|
||||
den: *mut i64,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
set_node_error("invalid arguments");
|
||||
return Ok(0);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (element, time_ts, track);
|
||||
if !num.is_null() {
|
||||
*num = 0;
|
||||
}
|
||||
if !den.is_null() {
|
||||
*den = 1;
|
||||
}
|
||||
Ok(0)
|
||||
closest_keyframe_time(self_, input_id, element, time_ts, track, true, num, den)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5418,21 +5690,8 @@ pub unsafe extern "C" fn oakengine_node_keyframe_closest_time_after(
|
||||
num: *mut i64,
|
||||
den: *mut i64,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
set_node_error("invalid arguments");
|
||||
return Ok(0);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (element, time_ts, track);
|
||||
if !num.is_null() {
|
||||
*num = 0;
|
||||
}
|
||||
if !den.is_null() {
|
||||
*den = 1;
|
||||
}
|
||||
Ok(0)
|
||||
closest_keyframe_time(self_, input_id, element, time_ts, track, false, num, den)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5445,10 +5704,23 @@ pub unsafe extern "C" fn oakengine_node_keyframe_handle_on_track(
|
||||
track: c_int,
|
||||
index: c_int,
|
||||
) -> *mut OakEngineKeyframe {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_ptr(|| {
|
||||
let _ = (self_, input_id, element, track, index);
|
||||
Ok(std::ptr::null_mut())
|
||||
guard_ptr(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() || index < 0 {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
let h = unbox(self_)?;
|
||||
let mut num: i64 = 0;
|
||||
let mut den: i64 = 0;
|
||||
let mut dummy = OakNodeValue::none();
|
||||
let rc = n::oaknode_node_keyframe_at(h, input_id, index, &mut num, &mut den, &mut dummy);
|
||||
if rc != 0 {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
let kf = n::oaknode_keyframe_create(num, den, &dummy, 0, track, element, input_id, h);
|
||||
if kf.ctx.is_null() {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
Ok(box_handle::<OakEngineKeyframe>(kf))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5462,10 +5734,24 @@ pub unsafe extern "C" fn oakengine_node_keyframe_handle_at_time(
|
||||
time_num: i64,
|
||||
time_den: i64,
|
||||
) -> *mut OakEngineKeyframe {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_ptr(|| {
|
||||
let _ = (self_, input_id, element, track, time_num, time_den);
|
||||
Ok(std::ptr::null_mut())
|
||||
guard_ptr(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() || time_den == 0 {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
let h = unbox(self_)?;
|
||||
let rc = n::oaknode_node_has_keyframe_at_time(h, input_id, time_num, time_den);
|
||||
if rc != 1 {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
let mut dummy = OakNodeValue::none();
|
||||
if n::oaknode_node_get_input_at_time(h, input_id, time_num, time_den, &mut dummy) != 0 {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
let kf = n::oaknode_keyframe_create(time_num, time_den, &dummy, 0, track, element, input_id, h);
|
||||
if kf.ctx.is_null() {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
Ok(box_handle::<OakEngineKeyframe>(kf))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5480,17 +5766,26 @@ pub unsafe extern "C" fn oakengine_node_keyframes_at_time(
|
||||
out_handles: *mut *mut OakEngineKeyframe,
|
||||
max_handles: c_int,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_int(|| {
|
||||
let _ = (
|
||||
self_,
|
||||
input_id,
|
||||
element,
|
||||
time_num,
|
||||
time_den,
|
||||
out_handles,
|
||||
max_handles,
|
||||
);
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() || max_handles < 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
if max_handles == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
let h = unbox(self_)?;
|
||||
if n::oaknode_node_has_keyframe_at_time(h, input_id, time_num, time_den) != 1 {
|
||||
return Ok(0);
|
||||
}
|
||||
if !out_handles.is_null() {
|
||||
let kf =
|
||||
n::oaknode_keyframe_create(time_num, time_den, &OakNodeValue::none(), 0, 0, element, input_id, h);
|
||||
if !kf.ctx.is_null() {
|
||||
// SAFETY: the caller guarantees `max_handles` slots.
|
||||
*out_handles = box_handle::<OakEngineKeyframe>(kf);
|
||||
return Ok(1);
|
||||
}
|
||||
}
|
||||
Ok(0)
|
||||
})
|
||||
}
|
||||
@@ -5506,15 +5801,20 @@ pub unsafe extern "C" fn oakengine_node_set_input_keyframing(
|
||||
enable_all_tracks: c_int,
|
||||
undo_name: *const c_char,
|
||||
) -> c_int {
|
||||
// Stub: the module has no keyframing-enable command; its keyframe
|
||||
// tracks are never populated through the C ABI.
|
||||
// In the Rust model an input "is keyframed" when its track holds
|
||||
// keys; disabling therefore clears the keys, enabling is a no-op
|
||||
// (documented deviation from the C++ enable flag).
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (element, keyframing, track, enable_all_tracks, undo_name);
|
||||
Err(Error::Invalid)
|
||||
let h = unbox(self_)?;
|
||||
let _ = (element, track, enable_all_tracks, undo_name);
|
||||
if keyframing != 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Error::from_module(n::oaknode_node_clear_keyframes(h, input_id))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5526,9 +5826,19 @@ pub unsafe extern "C" fn oakengine_node_set_input_keyframing_command(
|
||||
element: c_int,
|
||||
keyframing: c_int,
|
||||
) -> *mut c_void {
|
||||
// Stub: see `oakengine_node_set_input_keyframing`.
|
||||
guard_ptr(|| {
|
||||
let _ = (self_, input_id, element, keyframing);
|
||||
guard_ptr(|| unsafe {
|
||||
let rc = oakengine_node_set_input_keyframing(
|
||||
self_,
|
||||
input_id,
|
||||
element,
|
||||
keyframing,
|
||||
0,
|
||||
0,
|
||||
std::ptr::null(),
|
||||
);
|
||||
Error::from_module(rc)?;
|
||||
// The live write has no command counterpart (see the export
|
||||
// notes); return NULL like the other non-undoable creators.
|
||||
Ok(std::ptr::null_mut())
|
||||
})
|
||||
}
|
||||
@@ -5541,14 +5851,33 @@ pub unsafe extern "C" fn oakengine_node_keyframes_paste(
|
||||
count: c_int,
|
||||
undo_name: *const c_char,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_set_input_keyframing` (no insert path).
|
||||
// Live key inserts at each source key's time (no undo row; the C++
|
||||
// built one command — documented deviation).
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || keyframes.is_null() || count <= 0 {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let h = unbox(self_)?;
|
||||
let _ = undo_name;
|
||||
Err(Error::Invalid)
|
||||
for i in 0..count as usize {
|
||||
let kf = *keyframes.add(i);
|
||||
if kf.is_null() {
|
||||
continue;
|
||||
}
|
||||
let kh = unbox(kf)?;
|
||||
let mut num: i64 = 0;
|
||||
let mut den: i64 = 0;
|
||||
let mut v = OakNodeValue::none();
|
||||
Error::from_module(n::oaknode_keyframe_get_time(kh, &mut num, &mut den))?;
|
||||
Error::from_module(n::oaknode_keyframe_get_value(kh, &mut v))?;
|
||||
let mut input_buf = [0 as c_char; 256];
|
||||
let rc = n::oaknode_keyframe_get_input(kh, input_buf.as_mut_ptr(), 256);
|
||||
if rc < 0 {
|
||||
return Err(Error::Module(rc));
|
||||
}
|
||||
live_set_value_at_time(h, input_buf.as_ptr(), num, den, &v)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5816,13 +6145,38 @@ pub unsafe extern "C" fn oakengine_keyframes_remove_many(
|
||||
count: c_int,
|
||||
undo_name: *const c_char,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count` (no remove-key path).
|
||||
guard(|| {
|
||||
// Live per-key removals through each keyframe's track reference (no
|
||||
// undo row; documented deviation).
|
||||
guard(|| unsafe {
|
||||
if keyframes.is_null() || count <= 0 {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = undo_name;
|
||||
Err(Error::Invalid)
|
||||
for i in 0..count as usize {
|
||||
let kf = *keyframes.add(i);
|
||||
if kf.is_null() {
|
||||
continue;
|
||||
}
|
||||
let kh = unbox(kf)?;
|
||||
let mut num: i64 = 0;
|
||||
let mut den: i64 = 0;
|
||||
Error::from_module(n::oaknode_keyframe_get_time(kh, &mut num, &mut den))?;
|
||||
let mut input_buf = [0 as c_char; 256];
|
||||
let rc = n::oaknode_keyframe_get_input(kh, input_buf.as_mut_ptr(), 256);
|
||||
if rc < 0 {
|
||||
return Err(Error::Module(rc));
|
||||
}
|
||||
let mut parent = CHandle::null();
|
||||
Error::from_module(n::oaknode_keyframe_get_parent(kh, &mut parent))?;
|
||||
Error::from_module(n::oaknode_node_remove_keyframe(
|
||||
parent,
|
||||
input_buf.as_ptr(),
|
||||
num,
|
||||
den,
|
||||
))?;
|
||||
release_handle(parent);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use crate::bridge::plugin as p;
|
||||
use crate::stubs::plugin as p;
|
||||
use crate::error::Error;
|
||||
use crate::handle::guard;
|
||||
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
// 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/>.
|
||||
|
||||
//! POD mirrors for the deleted `src/bridge/` (single-lib unification).
|
||||
//!
|
||||
//! The facade still exchanges plain-`repr(C)` PODs with the module crates
|
||||
//! (and, upward, with the host app through the frozen `oakengine_*` C
|
||||
//! ABI). The deleted bridge aliased these types to the module crates'
|
||||
//! `ffi` declarations; those ffi modules are gone, so the engine keeps
|
||||
//! its own mirrors here. Where a module crate still owns the canonical
|
||||
//! POD (oakcodec's [`EncodingParamsPOD`]) the facade aliases it directly.
|
||||
|
||||
use std::ffi::c_int;
|
||||
|
||||
/// `oakcodec_encoding_params` (`include/codec/encoder.h`) — single-lib
|
||||
/// unification: aliases the oakcodec crate's POD
|
||||
/// ([`oakcodec::encodingparams::EncodingParams`], identical `#[repr(C)]`
|
||||
/// layout), so the facade's encoding-params handle reads/writes fields of
|
||||
/// exactly the struct the oakcodec/oakaudio creators consume.
|
||||
pub type EncodingParamsPOD = oakcodec::encodingparams::EncodingParams;
|
||||
|
||||
/// `oak_export_options` (`engine/include/oakengine/exporter.h`) — POD
|
||||
/// export parameters for [`crate::codec::oakengine_export_render`]. 0 (or
|
||||
/// negative) fields select the documented per-field default; the codec
|
||||
/// fields carry the exporter.h `OAKENGINE_EXPORT_VIDEO_*` /
|
||||
/// `OAKENGINE_EXPORT_AUDIO_*` values (NOT the engine's `ExportCodec`
|
||||
/// ids — see the mapping notes on `oakengine_export_render`).
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct OakExportOptions {
|
||||
/// `OAKENGINE_EXPORT_VIDEO_*` value; default H264.
|
||||
pub video_codec: c_int,
|
||||
/// `OAKENGINE_EXPORT_AUDIO_*` value; default AAC;
|
||||
/// [`OAKENGINE_EXPORT_AUDIO_NONE`] disables the audio track.
|
||||
pub audio_codec: c_int,
|
||||
/// Video bit rate in bit/s; <= 0 lets the encoder choose (FFmpeg
|
||||
/// defaults).
|
||||
pub video_bit_rate: i64,
|
||||
/// Audio sample rate in Hz; <= 0 uses the engine's default (48 kHz).
|
||||
pub audio_sample_rate: c_int,
|
||||
/// Audio channel count (1 = mono, 2 = stereo); <= 0 uses the engine's
|
||||
/// default (stereo).
|
||||
pub audio_channel_count: c_int,
|
||||
}
|
||||
|
||||
/// `OAKENGINE_EXPORT_VIDEO_*` — video codecs for
|
||||
/// [`OakExportOptions::video_codec`].
|
||||
pub const OAKENGINE_EXPORT_VIDEO_H264: c_int = 0;
|
||||
/// H.265/HEVC in an MP4 container.
|
||||
pub const OAKENGINE_EXPORT_VIDEO_H265: c_int = 1;
|
||||
/// PNG still-image sequence.
|
||||
pub const OAKENGINE_EXPORT_VIDEO_PNG_SEQUENCE: c_int = 2;
|
||||
|
||||
/// `OAKENGINE_EXPORT_AUDIO_*` — audio codecs for
|
||||
/// [`OakExportOptions::audio_codec`].
|
||||
pub const OAKENGINE_EXPORT_AUDIO_AAC: c_int = 0;
|
||||
/// Uncompressed PCM.
|
||||
pub const OAKENGINE_EXPORT_AUDIO_PCM: c_int = 1;
|
||||
/// Disable the audio track entirely (not a codec).
|
||||
pub const OAKENGINE_EXPORT_AUDIO_NONE: c_int = -1;
|
||||
|
||||
/// The oakaudio recording-params POD is the same shared codec POD.
|
||||
pub type AudioEncodingParams = EncodingParamsPOD;
|
||||
|
||||
/// Zeroed encoding-params POD (all fields 0 / NUL). The codec crate's
|
||||
/// struct has no zeroed constructor; this facade helper provides it.
|
||||
pub fn zeroed_encoding_params() -> EncodingParamsPOD {
|
||||
// All-field zero is a valid value (enums carry their 0 variants).
|
||||
unsafe { std::mem::zeroed() }
|
||||
}
|
||||
|
||||
/// `oakrender_video_params` (`include/render/renderer.h`) — identical
|
||||
/// layout to the engine's own video-params POD.
|
||||
pub type OakRenderVideoParams = crate::common::OakVideoParamsPod;
|
||||
|
||||
/// `oakrender_video_ticket_params` (`include/render/ticket.h`), the POD
|
||||
/// the deleted bridge aliased from `oakrender::ffi`. The oakrender crate
|
||||
/// now exposes value-typed `ticket::VideoTicketParams`; the facade keeps
|
||||
/// this mirror for its synchronous render path (see [`crate::render`]).
|
||||
/// All handle fields are the shared [`crate::handle::CHandle`].
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct OakVideoTicketParams {
|
||||
/// Connected texture output node (borrowed).
|
||||
pub output_node: crate::handle::CHandle,
|
||||
/// By-value oakcommon handle.
|
||||
pub video_params: crate::handle::CHandle,
|
||||
/// Borrowed oakcore audio-params handle, may be null.
|
||||
pub audio_params: *const std::ffi::c_void,
|
||||
/// Frame timestamp as rational.
|
||||
pub time_num: i64,
|
||||
/// Frame timestamp as rational.
|
||||
pub time_den: i64,
|
||||
/// Borrowed, empty ctx = null.
|
||||
pub color_manager: crate::handle::CHandle,
|
||||
/// RenderMode::Mode as int.
|
||||
pub mode: c_int,
|
||||
/// 0/0 = off.
|
||||
pub force_width: c_int,
|
||||
/// 0/0 = off.
|
||||
pub force_height: c_int,
|
||||
/// Used when has_force_matrix != 0.
|
||||
pub force_matrix: [f64; 16],
|
||||
/// 0/1.
|
||||
pub has_force_matrix: c_int,
|
||||
/// PixelFormat as int, -1 = off.
|
||||
pub force_format: c_int,
|
||||
/// 0 = off.
|
||||
pub force_channel_count: c_int,
|
||||
/// Borrowed; empty ctx = none.
|
||||
pub force_color_output: crate::handle::CHandle,
|
||||
/// By value; empty ctx = default.
|
||||
pub force_color_transform: crate::handle::CHandle,
|
||||
/// Borrowed frame cache; empty ctx = none.
|
||||
pub cache: crate::handle::CHandle,
|
||||
/// Single-footage decode filename (null = off; M12 P0).
|
||||
pub footage_filename: *const std::ffi::c_char,
|
||||
/// Media stream index for `footage_filename`.
|
||||
pub footage_stream: c_int,
|
||||
/// Sequence montage clip array (null = off; M12 P0). Clips are
|
||||
/// ordered bottom-to-top; the last element is the topmost.
|
||||
pub montage: *const MontagePod,
|
||||
/// `montage` element count.
|
||||
pub montage_count: c_int,
|
||||
}
|
||||
|
||||
/// One sequence-montage clip (`oakrender::ffi::OakMontageClip`, M12 P0):
|
||||
/// the facade resolves the timeline into this POD list; the render
|
||||
/// producer decodes and composites.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct MontagePod {
|
||||
/// Footage filename (borrowed; alive for the render call).
|
||||
pub filename: *const std::ffi::c_char,
|
||||
/// Media stream index.
|
||||
pub stream_index: c_int,
|
||||
/// Clip in point (sequence time), rational.
|
||||
pub in_num: i64,
|
||||
/// Clip in point denominator.
|
||||
pub in_den: i64,
|
||||
/// Clip out point (sequence time), rational.
|
||||
pub out_num: i64,
|
||||
/// Clip out point denominator.
|
||||
pub out_den: i64,
|
||||
/// Media in point, rational.
|
||||
pub media_in_num: i64,
|
||||
/// Media in point denominator.
|
||||
pub media_in_den: i64,
|
||||
/// Playback gain (1.0 = unity).
|
||||
pub gain: f32,
|
||||
}
|
||||
|
||||
/// The samples block handed out by `oakrender_ticket_get_samples`
|
||||
/// (caller-owned; release with `oakrender_audio_samples_free`). Read by
|
||||
/// the facade through the Rust type.
|
||||
pub struct OakAudioSamplesOut {
|
||||
/// Interleaved f32 samples.
|
||||
pub data: Box<[f32]>,
|
||||
/// Frame count.
|
||||
pub frame_count: c_int,
|
||||
/// Sample rate (Hz).
|
||||
pub sample_rate: c_int,
|
||||
/// Channel layout mask.
|
||||
pub channel_layout: u64,
|
||||
/// Channel count.
|
||||
pub channel_count: c_int,
|
||||
}
|
||||
|
||||
/// `oakaudio_min_max` (`include/audio/waveform.h`) — one summarized
|
||||
/// waveform point of one channel.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct MinMax {
|
||||
/// Minimum of the summarized samples.
|
||||
pub min: f32,
|
||||
/// Maximum of the summarized samples.
|
||||
pub max: f32,
|
||||
}
|
||||
|
||||
/// `oakaudio_offset_result` (`include/audio/sync.h`).
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct OffsetResult {
|
||||
/// Offset in samples.
|
||||
pub offset_samples: i64,
|
||||
/// Correlation confidence 0..1.
|
||||
pub confidence: f64,
|
||||
/// 1 when an estimate was found.
|
||||
pub valid: c_int,
|
||||
}
|
||||
|
||||
/// `oakaudio_stretch_offset_result` (`include/audio/sync.h`).
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct StretchOffsetResult {
|
||||
/// Playback rate aligning the candidate (> 1 = speed up).
|
||||
pub rate: f64,
|
||||
/// Offset in samples.
|
||||
pub offset_samples: i64,
|
||||
/// Correlation confidence 0..1.
|
||||
pub confidence: f64,
|
||||
/// 1 when an estimate was found.
|
||||
pub valid: c_int,
|
||||
}
|
||||
|
||||
/// `oakaudio_source_clip` (`include/audio/sync.h`) — one clip's
|
||||
/// source-time metadata (rational seconds).
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SourceClip {
|
||||
/// Source start time numerator.
|
||||
pub source_start_time_num: i64,
|
||||
/// Source start time denominator.
|
||||
pub source_start_time_den: i64,
|
||||
/// Media in numerator.
|
||||
pub media_in_num: i64,
|
||||
/// Media in denominator.
|
||||
pub media_in_den: i64,
|
||||
/// 1 when the source start time is meaningful.
|
||||
pub has_source_start_time: c_int,
|
||||
}
|
||||
|
||||
/// `i32` code -> `PixelFormat` (`repr(i32)` enum; unknown -> `Invalid`).
|
||||
pub fn pixel_format_from_code(v: c_int) -> oakcore_rs::PixelFormat {
|
||||
match v {
|
||||
0 => oakcore_rs::PixelFormat::U8,
|
||||
1 => oakcore_rs::PixelFormat::U10,
|
||||
2 => oakcore_rs::PixelFormat::U16,
|
||||
3 => oakcore_rs::PixelFormat::F16,
|
||||
4 => oakcore_rs::PixelFormat::F32,
|
||||
_ => oakcore_rs::PixelFormat::Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
/// `i32` code -> `SampleFormat` (`repr(i32)` enum; unknown -> `Invalid`).
|
||||
pub fn sample_format_from_code(v: c_int) -> oakcore_rs::SampleFormat {
|
||||
match v {
|
||||
0 => oakcore_rs::SampleFormat::U8Planar,
|
||||
1 => oakcore_rs::SampleFormat::S16Planar,
|
||||
2 => oakcore_rs::SampleFormat::S32Planar,
|
||||
3 => oakcore_rs::SampleFormat::S64Planar,
|
||||
4 => oakcore_rs::SampleFormat::F32Planar,
|
||||
5 => oakcore_rs::SampleFormat::F64Planar,
|
||||
6 => oakcore_rs::SampleFormat::U8,
|
||||
7 => oakcore_rs::SampleFormat::S16,
|
||||
8 => oakcore_rs::SampleFormat::S32,
|
||||
9 => oakcore_rs::SampleFormat::S64,
|
||||
10 => oakcore_rs::SampleFormat::F32,
|
||||
11 => oakcore_rs::SampleFormat::F64,
|
||||
_ => oakcore_rs::SampleFormat::Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
/// `i32` code -> `VideoScalingMethod` (unknown -> `Stretch`, the C++
|
||||
/// default).
|
||||
pub fn scaling_from_code(v: c_int) -> oakcodec::encodingparams::VideoScalingMethod {
|
||||
match v {
|
||||
0 => oakcodec::encodingparams::VideoScalingMethod::Fit,
|
||||
2 => oakcodec::encodingparams::VideoScalingMethod::Crop,
|
||||
_ => oakcodec::encodingparams::VideoScalingMethod::Stretch,
|
||||
}
|
||||
}
|
||||
@@ -20,25 +20,27 @@
|
||||
//! - The **renderer** is a facade-owned box binding an output node
|
||||
//! (usually a sequence, or any single node via
|
||||
//! `oakengine_renderer_create_for_node`) to an output geometry; each
|
||||
//! render call submits an oakrender ticket (`OakVideoTicketParams`),
|
||||
//! waits for it and returns the produced frame (`OakCodecFrame` wrapped
|
||||
//! in `OakEngineFrame`). Audio rendering submits the ticket but the
|
||||
//! crate's samples path is unimplemented, so it fails with the reason
|
||||
//! in `last_error`.
|
||||
//! - The **frame accessors** read the wrapped `OakCodecFrame`
|
||||
//! render call submits an oakrender ticket (`OakVideoTicketParams`)
|
||||
//! through the manager's real ticket arena, waits for it and returns
|
||||
//! the produced frame (`oakrender::texture::Frame` wrapped in
|
||||
//! `OakEngineFrame`). Audio rendering goes through the arena's audio
|
||||
//! path (`oakrender::eval::render_audio_samples`) and returns the
|
||||
//! interleaved samples in `OakEngineAudioBuffer`.
|
||||
//! - The **frame accessors** read the wrapped frame
|
||||
//! (`channel_count` has no crate accessor and reports 0).
|
||||
//! - The **color processor** family maps onto
|
||||
//! `oakrender_color_processor_*`; the engine's `oak_color_transform`
|
||||
//! POD is converted into an oakcommon colortransform handle for
|
||||
//! `create_transform`. The color-manager list queries, standalone
|
||||
//! config handle and LUT directory/file library have no crate backing
|
||||
//! and are documented stubs (see `deferred.rs`).
|
||||
//! - The **color processor** family maps onto the real
|
||||
//! `oakrender::color::ColorProcessor`; the engine's
|
||||
//! `oak_color_transform` POD is converted into an oakcommon
|
||||
//! colortransform handle for `create_transform`. The color-manager
|
||||
//! list queries, standalone config handle and LUT directory/file
|
||||
//! library have no crate backing and are documented stubs (see
|
||||
//! `deferred.rs`).
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::{c_char, c_double, c_int, c_void};
|
||||
|
||||
use crate::bridge::render as r;
|
||||
use crate::bridge::render::{OakRenderVideoParams, OakVideoTicketParams};
|
||||
use crate::stubs::render as r;
|
||||
use crate::pods::{OakRenderVideoParams, OakVideoTicketParams};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::handle::{
|
||||
box_handle, free_box, guard, guard_int, guard_ptr, guard_void, string_result, unbox, CHandle,
|
||||
@@ -50,6 +52,33 @@ use crate::handle::{
|
||||
// Render manager / cacher
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `oakengine_render_manager_init` — bring up the module's process-global
|
||||
/// render manager (0 on success; without it `render_frame` fails with NULL
|
||||
/// + last_error). The module's `OAKRENDER_E_STATE` (-70002) passes through
|
||||
/// when the manager is already initialized.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakengine_render_manager_init() -> c_int {
|
||||
guard(|| Error::from_module(unsafe { r::oakrender_manager_init() }))
|
||||
}
|
||||
|
||||
/// `oakengine_render_manager_available` — 1 when the render manager is up,
|
||||
/// 0 otherwise.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakengine_render_manager_available() -> c_int {
|
||||
guard_int(|| Ok(unsafe { r::oakrender_manager_available() }))
|
||||
}
|
||||
|
||||
/// `oakengine_render_manager_shutdown` — tear down the module's render
|
||||
/// manager (no-op when none is up; always 0, like
|
||||
/// `oakengine_audio_destroy_instance`).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakengine_render_manager_shutdown() -> c_int {
|
||||
guard_void(|| unsafe {
|
||||
r::oakrender_manager_shutdown();
|
||||
});
|
||||
crate::error::OAKENGINE_OK
|
||||
}
|
||||
|
||||
/// `oakengine_render_manager_set_aggressive_garbage_collection`.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakengine_render_manager_set_aggressive_garbage_collection(
|
||||
@@ -58,24 +87,45 @@ pub extern "C" fn oakengine_render_manager_set_aggressive_garbage_collection(
|
||||
guard(|| Error::from_module(unsafe { r::oakrender_manager_set_aggressive_gc(aggressive) }))
|
||||
}
|
||||
|
||||
/// `oakengine_render_manager_requested_backend` — **not backed** (the
|
||||
/// oakrender crate exposes the current backend, not the requested one).
|
||||
/// Returns 0 (k_open_gl).
|
||||
/// `oakengine_render_manager_requested_backend` — the backend the manager
|
||||
/// was asked for (0 = k_open_gl, 1 = k_metal, 2 = k_vulkan, 3 = k_cpu;
|
||||
/// -1 when the manager is down).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakengine_render_manager_requested_backend() -> c_int {
|
||||
0
|
||||
guard_int(|| {
|
||||
match oakrender::manager::RenderManager::global() {
|
||||
Some(m) => Ok(match m.requested_backend {
|
||||
oakrender::backend::BackendKind::Auto => 0,
|
||||
oakrender::backend::BackendKind::Metal => 1,
|
||||
oakrender::backend::BackendKind::Vulkan => 2,
|
||||
oakrender::backend::BackendKind::Gl => 0,
|
||||
oakrender::backend::BackendKind::Cpu => 3,
|
||||
}),
|
||||
None => Ok(-1),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakengine_render_manager_backend_to_string` — **not backed** (the
|
||||
/// crate enumerates backend ids, not enum→string). Returns
|
||||
/// OAKENGINE_E_FAILED.
|
||||
/// `oakengine_render_manager_backend_to_string` — the config name of the
|
||||
/// backend ordinal (E_INVALID out of range).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_render_manager_backend_to_string(
|
||||
_backend: c_int,
|
||||
_buf: *mut c_char,
|
||||
_buf_size: c_int,
|
||||
backend: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
crate::error::OAKENGINE_E_FAILED
|
||||
guard_int(|| {
|
||||
let kind = match backend {
|
||||
0 => oakrender::backend::BackendKind::Gl,
|
||||
1 => oakrender::backend::BackendKind::Metal,
|
||||
2 => oakrender::backend::BackendKind::Vulkan,
|
||||
3 => oakrender::backend::BackendKind::Cpu,
|
||||
_ => return Err(Error::Invalid),
|
||||
};
|
||||
Ok(unsafe {
|
||||
crate::handle::write_string(kind.to_config_string(), buf, buf_size)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakengine_render_cache_set_display_color_processor` — NULL clears.
|
||||
@@ -171,19 +221,19 @@ unsafe fn renderer_mut(ptr: *mut OakEngineRenderer) -> Result<&'static mut Rende
|
||||
/// Build an oakcommon video-params handle for the renderer's geometry.
|
||||
unsafe fn make_video_params(b: &RendererBox) -> Result<CHandle> {
|
||||
unsafe {
|
||||
let params = crate::bridge::common::oakcommon_videoparams_init();
|
||||
let params = crate::stubs::common::oakcommon_videoparams_init();
|
||||
if params.is_null() {
|
||||
return Err(Error::Failed("video params allocation failed".into()));
|
||||
}
|
||||
let mut rc = crate::bridge::common::oakcommon_videoparams_set_width(params, b.width);
|
||||
let mut rc = crate::stubs::common::oakcommon_videoparams_set_width(params, b.width);
|
||||
if rc == 0 {
|
||||
rc = crate::bridge::common::oakcommon_videoparams_set_height(params, b.height);
|
||||
rc = crate::stubs::common::oakcommon_videoparams_set_height(params, b.height);
|
||||
}
|
||||
if rc == 0 {
|
||||
rc = crate::bridge::common::oakcommon_videoparams_set_format(params, b.pixel_format);
|
||||
rc = crate::stubs::common::oakcommon_videoparams_set_format(params, b.pixel_format);
|
||||
}
|
||||
if rc == 0 {
|
||||
rc = crate::bridge::common::oakcommon_videoparams_set_time_base(
|
||||
rc = crate::stubs::common::oakcommon_videoparams_set_time_base(
|
||||
params,
|
||||
b.frame_rate_den,
|
||||
b.frame_rate_num,
|
||||
@@ -191,7 +241,7 @@ unsafe fn make_video_params(b: &RendererBox) -> Result<CHandle> {
|
||||
}
|
||||
if rc != 0 {
|
||||
let mut p = params;
|
||||
crate::bridge::common::oakcommon_videoparams_free(&mut p);
|
||||
crate::stubs::common::oakcommon_videoparams_free(&mut p);
|
||||
return Err(Error::Failed("video params setup failed".into()));
|
||||
}
|
||||
Ok(params)
|
||||
@@ -254,7 +304,7 @@ unsafe fn make_renderer_box(
|
||||
unsafe fn resolve_footage(node: CHandle) -> Result<FootageSpec> {
|
||||
unsafe {
|
||||
let mut buf = [0 as c_char; 4096];
|
||||
let rc = crate::bridge::node::oaknode_footage_filename(node, buf.as_mut_ptr(), buf.len() as c_int);
|
||||
let rc = crate::stubs::node::oaknode_footage_filename(node, buf.as_mut_ptr(), buf.len() as c_int);
|
||||
if rc < 0 {
|
||||
return Err(Error::Failed("node is not footage".into()));
|
||||
}
|
||||
@@ -270,18 +320,18 @@ unsafe fn resolve_footage(node: CHandle) -> Result<FootageSpec> {
|
||||
/// graph node → its upstream footage.
|
||||
unsafe fn clip_media(clip: CHandle) -> Option<(String, c_int)> {
|
||||
unsafe {
|
||||
let node = crate::bridge::node::oaknode_block_as_node(clip);
|
||||
let node = crate::stubs::node::oaknode_block_as_node(clip);
|
||||
if node.is_null() {
|
||||
return None;
|
||||
}
|
||||
let mut footage = CHandle::null();
|
||||
if crate::bridge::node::oaknode_node_find_input_footage(node, &mut footage) != 0
|
||||
if crate::stubs::node::oaknode_node_find_input_footage(node, &mut footage) != 0
|
||||
|| footage.is_null()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let mut buf = [0 as c_char; 4096];
|
||||
if crate::bridge::node::oaknode_footage_filename(
|
||||
if crate::stubs::node::oaknode_footage_filename(
|
||||
footage,
|
||||
buf.as_mut_ptr(),
|
||||
buf.len() as c_int,
|
||||
@@ -302,7 +352,7 @@ unsafe fn build_audio_montage(
|
||||
b: &RendererBox,
|
||||
range: oakcore_rs::TimeRange,
|
||||
) -> (Vec<MontagePod>, Vec<std::ffi::CString>) {
|
||||
use crate::bridge::node as n;
|
||||
use crate::stubs::node as n;
|
||||
unsafe {
|
||||
let mut pods = Vec::new();
|
||||
let mut names = Vec::new();
|
||||
@@ -374,7 +424,7 @@ unsafe fn build_audio_montage(
|
||||
}
|
||||
|
||||
/// The `OakMontageClip` POD the render module's ticket reads.
|
||||
type MontagePod = oakrender::ffi::OakMontageClip;
|
||||
type MontagePod = crate::pods::MontagePod;
|
||||
|
||||
/// Build the video montage for `b` at sequence time `time` (rational):
|
||||
/// every clip covering `time` on video tracks, ordered bottom-to-top
|
||||
@@ -385,7 +435,7 @@ unsafe fn build_video_montage(
|
||||
b: &RendererBox,
|
||||
time: oakcore_rs::Rational,
|
||||
) -> (Vec<MontagePod>, Vec<std::ffi::CString>) {
|
||||
use crate::bridge::node as n;
|
||||
use crate::stubs::node as n;
|
||||
unsafe {
|
||||
let mut pods = Vec::new();
|
||||
let mut names = Vec::new();
|
||||
@@ -618,7 +668,7 @@ pub unsafe extern "C" fn oakengine_renderer_render_frame(
|
||||
let ticket = r::oakrender_ticket_render_frame(¶ms, None, std::ptr::null_mut());
|
||||
let _ = (&keep_alive, &montage);
|
||||
let mut vp = video_params;
|
||||
crate::bridge::common::oakcommon_videoparams_free(&mut vp);
|
||||
crate::stubs::common::oakcommon_videoparams_free(&mut vp);
|
||||
if ticket.is_null() {
|
||||
b.last_error = "render ticket submission failed".into();
|
||||
return Ok(std::ptr::null_mut());
|
||||
@@ -659,8 +709,8 @@ pub unsafe extern "C" fn oakengine_renderer_render_audio(
|
||||
// the range) and the output format.
|
||||
let (pods, names) = build_audio_montage(b, range);
|
||||
let mut keep: Vec<std::ffi::CString> = names;
|
||||
let params = crate::bridge::audio::oakcore_audioparams_create(48000, 0x3, 10); // packed F32 stereo
|
||||
crate::bridge::audio::oakcore_audioparams_set_time_base(params, 1, 48000);
|
||||
let params = crate::stubs::audio::oakcore_audioparams_create(48000, 0x3, 10); // packed F32 stereo
|
||||
crate::stubs::audio::oakcore_audioparams_set_time_base(params, 1, 48000);
|
||||
let ticket = r::oakrender_ticket_render_audio(
|
||||
b.output_node,
|
||||
start_num,
|
||||
@@ -678,7 +728,7 @@ pub unsafe extern "C" fn oakengine_renderer_render_audio(
|
||||
},
|
||||
pods.len() as c_int,
|
||||
);
|
||||
crate::bridge::audio::oakcore_audioparams_free(params);
|
||||
crate::stubs::audio::oakcore_audioparams_free(params);
|
||||
if ticket.is_null() {
|
||||
b.last_error = "audio render ticket submission failed".into();
|
||||
return Ok(std::ptr::null_mut());
|
||||
@@ -693,7 +743,7 @@ pub unsafe extern "C" fn oakengine_renderer_render_audio(
|
||||
b.last_error = "audio render failed".into();
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
let raw = samples as *const oakrender::ffi::OakAudioSamplesOut;
|
||||
let raw = samples as *const crate::pods::OakAudioSamplesOut;
|
||||
let boxed = AudioSamplesBox {
|
||||
data: (*raw).data.clone(),
|
||||
frame_count: (*raw).frame_count as i64,
|
||||
@@ -1210,7 +1260,7 @@ pub unsafe extern "C" fn oakengine_color_processor_create(
|
||||
}
|
||||
let empty = crate::common::empty_cstr();
|
||||
let ct = if (*dest).is_display != 0 {
|
||||
crate::bridge::common::oakcommon_colortransform_init_display(
|
||||
crate::stubs::common::oakcommon_colortransform_init_display(
|
||||
if (*dest).output.is_null() {
|
||||
empty
|
||||
} else {
|
||||
@@ -1228,7 +1278,7 @@ pub unsafe extern "C" fn oakengine_color_processor_create(
|
||||
},
|
||||
)
|
||||
} else {
|
||||
crate::bridge::common::oakcommon_colortransform_init_output(
|
||||
crate::stubs::common::oakcommon_colortransform_init_output(
|
||||
if (*dest).output.is_null() {
|
||||
empty
|
||||
} else {
|
||||
@@ -1247,7 +1297,7 @@ pub unsafe extern "C" fn oakengine_color_processor_create(
|
||||
};
|
||||
let proc = r::oakrender_color_processor_create_transform(mgr_handle, input, ct, direction);
|
||||
let mut ct_handle = ct;
|
||||
crate::bridge::common::oakcommon_colortransform_free(&mut ct_handle);
|
||||
crate::stubs::common::oakcommon_colortransform_free(&mut ct_handle);
|
||||
if proc.is_null() {
|
||||
LAST_COLOR_ERROR.with(|e| *e.borrow_mut() = "could not create color processor".into());
|
||||
return Ok(std::ptr::null_mut());
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -43,9 +43,9 @@ use std::ffi::{c_char, c_int, c_void};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::bridge::codec::{zeroed_encoding_params, EncodingParamsPOD};
|
||||
use crate::bridge::node as n;
|
||||
use crate::bridge::task as t;
|
||||
use crate::pods::{zeroed_encoding_params, EncodingParamsPOD};
|
||||
use crate::stubs::node as n;
|
||||
use crate::stubs::task as t;
|
||||
use crate::codec::OakEngineEncodingParams;
|
||||
use crate::common::OakVideoParamsPod;
|
||||
use crate::error::{Error, Result};
|
||||
@@ -654,9 +654,9 @@ pub unsafe extern "C" fn oakengine_task_create_export(
|
||||
///
|
||||
/// The oaktask crate's `convert_encoding_params` consumes exactly these
|
||||
/// fields (filename, format, video/audio/subtitle enables, codecs,
|
||||
/// dimensions, time base, pixel format, export length), so a POD carrying
|
||||
/// them is behaviorally identical to the original for the export task; all
|
||||
/// other POD fields stay zeroed.
|
||||
/// dimensions, time base, pixel format, audio rate/layout, export length),
|
||||
/// so a POD carrying them is behaviorally identical to the original for
|
||||
/// the export task; all other POD fields stay zeroed.
|
||||
fn export_params_pod(params: *const OakEngineEncodingParams) -> Result<EncodingParamsPOD> {
|
||||
let mut pod = zeroed_encoding_params();
|
||||
|
||||
@@ -682,6 +682,24 @@ fn export_params_pod(params: *const OakEngineEncodingParams) -> Result<EncodingP
|
||||
pod.video_codec = unsafe { crate::codec::oakengine_encoding_params_video_codec(params) };
|
||||
pod.audio_enabled = unsafe { crate::codec::oakengine_encoding_params_audio_enabled(params) };
|
||||
pod.audio_codec = unsafe { crate::codec::oakengine_encoding_params_audio_codec(params) };
|
||||
// Audio rate/layout flow through so the encoder opens with the
|
||||
// requested rate (the module export reads them from the POD). The
|
||||
// sample format is NOT carried: the FFmpeg encoder always runs in the
|
||||
// codec's native format and resamples (see `FFmpegEncoder::open`).
|
||||
if pod.audio_enabled != 0 {
|
||||
let mut sample_rate: c_int = 0;
|
||||
let mut channel_layout: u64 = 0;
|
||||
unsafe {
|
||||
crate::codec::oakengine_encoding_params_get_audio_params(
|
||||
params,
|
||||
&mut sample_rate,
|
||||
&mut channel_layout,
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
}
|
||||
pod.audio_sample_rate = sample_rate;
|
||||
pod.audio_channel_layout = channel_layout;
|
||||
}
|
||||
pod.subtitles_enabled =
|
||||
unsafe { crate::codec::oakengine_encoding_params_subtitles_enabled(params) };
|
||||
unsafe {
|
||||
@@ -703,7 +721,7 @@ fn export_params_pod(params: *const OakEngineEncodingParams) -> Result<EncodingP
|
||||
pod.video_height = v.height;
|
||||
pod.video_time_base_num = v.time_base_num;
|
||||
pod.video_time_base_den = v.time_base_den;
|
||||
pod.video_pixel_format = v.format;
|
||||
pod.video_pixel_format = crate::pods::pixel_format_from_code(v.format);
|
||||
}
|
||||
}
|
||||
Ok(pod)
|
||||
@@ -865,3 +883,53 @@ pub unsafe extern "C" fn oakengine_task_save_get_project(
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Load task results / event subscription
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `oakengine_task_load_take_project` — take the project an interchange
|
||||
/// (OVE/OTIO) load task produced after a successful run; ownership moves to
|
||||
/// the caller (release with `oakengine_project_free`). NULL for a NULL
|
||||
/// task, a task that never ran, or a task that is not a load task.
|
||||
///
|
||||
/// The module's `oaktask_load_take_project` is the load-result getter the
|
||||
/// engine facade has no export for (the app's interchange-open path); it is
|
||||
/// wrapped here so the app can stay on the `oakengine_*` surface.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_task_load_take_project(
|
||||
task: *mut OakEngineTask,
|
||||
) -> *mut OakEngineProject {
|
||||
guard_ptr(|| unsafe {
|
||||
let h = unbox(task)?;
|
||||
let project = t::oaktask_load_take_project(h);
|
||||
if project.is_null() {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
Ok(box_handle::<OakEngineProject>(project))
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakengine_task_subscribe` — register the task event callback invoked on
|
||||
/// the task's own thread (`OAKTASK_EVENT_STARTED`=0, `OAKTASK_EVENT_PROGRESS`=1,
|
||||
/// `OAKTASK_EVENT_FINISHED`=2); one subscription replaces the previous one.
|
||||
///
|
||||
/// Returns 0 on success; facade `OAKENGINE_E_INVALID` (-1) for a NULL task
|
||||
/// or NULL callback; module error codes pass through untranslated. The
|
||||
/// callback and `userdata` follow the module's `oaktask_event_fn` contract
|
||||
/// (the engine facade has no subscription export of its own; the app's
|
||||
/// export-progress path uses this wrapper).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_task_subscribe(
|
||||
task: *mut OakEngineTask,
|
||||
cb: Option<t::OakTaskEventFn>,
|
||||
userdata: *mut c_void,
|
||||
) -> i64 {
|
||||
guard_i64(|| unsafe {
|
||||
let h = unbox(task)?;
|
||||
if cb.is_none() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
Ok(t::oaktask_task_subscribe(h, cb, userdata))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,10 +19,9 @@
|
||||
//! single serialized test function; processor and sync tests are
|
||||
//! independent.
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use oakengine::audio::{
|
||||
use crate::audio::{
|
||||
oakengine_audio_clear_buffered_output, oakengine_audio_create_instance,
|
||||
oakengine_audio_destroy_instance, oakengine_audio_estimate_envelope_offset,
|
||||
oakengine_audio_get_output_device, oakengine_audio_hard_reset, oakengine_audio_processor_close,
|
||||
@@ -38,6 +37,10 @@ use oakengine::audio::{
|
||||
/// (serialized — the singleton is process-wide).
|
||||
#[test]
|
||||
fn manager_lifecycle() {
|
||||
common::with_manager(|| manager_lifecycle_inner());
|
||||
}
|
||||
|
||||
fn manager_lifecycle_inner() {
|
||||
// Start from a destroyed state.
|
||||
unsafe { oakengine_audio_destroy_instance() };
|
||||
|
||||
@@ -17,12 +17,11 @@
|
||||
//! Smoke tests for the encoding family (`engine/include/oakengine/encoding.h`):
|
||||
//! container/codec metadata queries and the encoding-params handle.
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use oakengine::codec::{
|
||||
use crate::codec::{
|
||||
oakengine_encoding_codec_is_lossless, oakengine_encoding_codec_is_still_image,
|
||||
oakengine_encoding_codec_name, oakengine_encoding_filename_contains_digit_placeholder,
|
||||
oakengine_encoding_filename_remove_digit_placeholder,
|
||||
@@ -45,7 +44,7 @@ use oakengine::codec::{
|
||||
oakengine_encoding_params_video_pix_fmt, oakengine_encoding_pix_fmt_index,
|
||||
oakengine_encoding_start_audio_recording,
|
||||
};
|
||||
use oakengine::common::OakVideoParamsPod;
|
||||
use crate::common::OakVideoParamsPod;
|
||||
|
||||
/// Container format / codec metadata queries.
|
||||
#[test]
|
||||
@@ -178,7 +177,7 @@ fn params_handle_round_trip() {
|
||||
let mut vp: OakVideoParamsPod = unsafe { std::mem::zeroed() };
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine::common::oakengine_video_params_make(
|
||||
crate::common::oakengine_video_params_make(
|
||||
&mut vp, 1920, 1080, 1001, 30000, 4, 1, 1, 0, 1, 1,
|
||||
)
|
||||
},
|
||||
+30
-19
@@ -39,32 +39,43 @@ use std::collections::HashMap;
|
||||
use std::ffi::{c_int, c_void};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
/// Force the module crates into the link.
|
||||
/// One public direct-Rust symbol per module crate (the module C ABIs are
|
||||
/// deleted; this mirrors the anchors in `crates/oakengine/src/linkage.rs`).
|
||||
/// Under unit tests the crates are real dependencies of the lib target and
|
||||
/// are linked regardless; the array doubles as a compile-time proof that
|
||||
/// the anchor paths match the current module layouts.
|
||||
#[allow(unused)]
|
||||
pub fn force_link() -> usize {
|
||||
let fns: [usize; 12] = [
|
||||
oakundo::ffi::undostack::oakundo_undostack_init as usize,
|
||||
oakcodec::ffi::format::oakcodec_encoding_format_count as usize,
|
||||
oakaudio::ffi::waveform::oakaudio_waveform_length as usize,
|
||||
oakrender::ffi::cache::oakrender_cache_indicator_height as usize,
|
||||
oakcommon::ffi::config::oakcommon_config_get_int as usize,
|
||||
oakplugin::ffi::oakplugin_host_plugin_count as usize,
|
||||
oaknode::ffi::project::oaknode_project_init as usize,
|
||||
oaktimeline::ffi::marker::oaktimeline_marker_list_create as usize,
|
||||
oaktask::ffi::manager::oaktask_manager_init as usize,
|
||||
// The oaknode serializer bridge resolves these at runtime via
|
||||
// dlsym(RTLD_DEFAULT); force them into the link so the oaknode
|
||||
// serializer's XML writer/reader and the undo command factory are
|
||||
// visible to dlsym in every test binary (the oaknode rlib only
|
||||
// references them through dlsym, so the linker would otherwise drop
|
||||
// them from the oakcommon/oakundo rlib objects).
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_writer_init as usize,
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_reader_init as usize,
|
||||
oakundo::ffi::command::oakundo_command_init as usize,
|
||||
oakundo::undostack::undostack_init as usize,
|
||||
oakcodec::exportformat::Format::get_name as usize,
|
||||
oakaudio::processor::Processor::init as usize,
|
||||
oakrender::manager::RenderManager::init as usize,
|
||||
oakcommon::configstore::ConfigStore::instance as usize,
|
||||
oakplugin::host::Host::global as usize,
|
||||
oaknode::project::Project::new as usize,
|
||||
oaktimeline::marker::TimelineMarkerList::new as usize,
|
||||
oaktask::manager::TaskManager::init as usize,
|
||||
// oakundo/oakcommon no longer export a C ABI; their handle-level
|
||||
// Rust API functions anchor the rlibs into every test binary (the
|
||||
// same pattern as `crates/oakengine/src/linkage.rs`).
|
||||
oakcommon::xmlutils::XmlWriter::new as usize,
|
||||
oakcommon::xmlutils::XmlReader::new as usize,
|
||||
oakundo::undocommand::command_init as usize,
|
||||
];
|
||||
fns.iter().sum()
|
||||
}
|
||||
|
||||
/// Serialize every test that touches the process-wide AudioManager
|
||||
/// singleton. The former integration tests were separate processes; as
|
||||
/// unit tests they share one process (and one singleton), so the manager
|
||||
/// tests must take a shared lock instead of relying on process isolation.
|
||||
pub fn with_manager(f: impl FnOnce()) {
|
||||
static LOCK: Mutex<()> = Mutex::new(());
|
||||
let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
f()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// oakcore_* stubs (see module docs)
|
||||
// ---------------------------------------------------------------------------
|
||||
+4
-5
@@ -19,12 +19,11 @@
|
||||
//! singleton, so config tests are serialized inside single test
|
||||
//! functions.
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use oakengine::common::{
|
||||
use crate::common::{
|
||||
oakengine_config_get_int, oakengine_config_get_string, oakengine_config_load,
|
||||
oakengine_config_save, oakengine_config_set_error_handler, oakengine_config_set_int,
|
||||
oakengine_config_set_string, oakengine_video_params_bytes_per_pixel,
|
||||
@@ -112,7 +111,7 @@ fn config_error_handler() {
|
||||
);
|
||||
// Report an error through the handler.
|
||||
assert_eq!(
|
||||
unsafe { oakengine::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr()) },
|
||||
unsafe { crate::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr()) },
|
||||
0
|
||||
);
|
||||
assert_eq!(CALLED.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
@@ -121,7 +120,7 @@ fn config_error_handler() {
|
||||
unsafe { oakengine_config_set_error_handler(None, std::ptr::null_mut()) },
|
||||
0
|
||||
);
|
||||
unsafe { oakengine::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr()) };
|
||||
unsafe { crate::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr()) };
|
||||
assert_eq!(CALLED.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
+182
-24
@@ -19,13 +19,15 @@
|
||||
//! `include/audio/{manager,processor,sync,error}.h`), exercised end to
|
||||
//! end against the REAL `oakaudio` crate — no mocks anywhere.
|
||||
//!
|
||||
//! The facade's 26 exported functions are all covered:
|
||||
//! The facade's 27 exported functions are all covered:
|
||||
//!
|
||||
//! * Manager (singleton, process-wide — serialized via [`with_manager`]):
|
||||
//! create/destroy/instance handle, device accessors, output push/clock,
|
||||
//! notify interval, recording start/stop.
|
||||
//! * Sync (stateless): envelope offset/stretch correlation, source-time
|
||||
//! and waveform-offset timeline placement.
|
||||
//! * Waveform extraction (real decode): the two-stage
|
||||
//! `oakengine_waveform_extract` contract against real media.
|
||||
//! * Processor (refcounted object — serialized via [`with_processor`] so
|
||||
//! the module's debug alive counter (`oakaudio_debug_alive_count`) is
|
||||
//! deterministic): create/free/open/close/is_open plus the two
|
||||
@@ -45,14 +47,13 @@
|
||||
//! `OAKAUDIO_E_FAILED` and a diagnostic in `error_buf` — both outcomes
|
||||
//! are asserted.
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_int, c_void, CStr};
|
||||
use std::ffi::{c_int, c_void, CStr, CString};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use oakengine::audio::{
|
||||
use crate::audio::{
|
||||
oakengine_audio_clear_buffered_output, oakengine_audio_create_instance,
|
||||
oakengine_audio_output_levels,
|
||||
oakengine_audio_destroy_instance, oakengine_audio_estimate_envelope_offset,
|
||||
@@ -66,11 +67,11 @@ use oakengine::audio::{
|
||||
oakengine_audio_set_output_device, oakengine_audio_set_output_notify_interval,
|
||||
oakengine_audio_start_recording, oakengine_audio_stop_output, oakengine_audio_stop_recording,
|
||||
oakengine_audio_sync_place_by_source_time, oakengine_audio_sync_place_by_waveform_offset,
|
||||
OakAudioSyncPlacement, OakAudioSyncSourceClip, OakAudioWaveformOffset,
|
||||
OakAudioWaveformStretchOffset,
|
||||
oakengine_waveform_extract, OakAudioSyncPlacement, OakAudioSyncSourceClip,
|
||||
OakAudioWaveformOffset, OakAudioWaveformStretchOffset,
|
||||
};
|
||||
use oakengine::error::{OAKENGINE_E_FAILED, OAKENGINE_E_INVALID};
|
||||
use oakengine::handle::{CHandle, OakEngineAudioProcessor};
|
||||
use crate::error::{OAKENGINE_E_FAILED, OAKENGINE_E_INVALID};
|
||||
use crate::handle::{CHandle, OakEngineAudioProcessor};
|
||||
|
||||
/// `OAKAUDIO_E_INVALID` (include/audio/error.h) — module codes pass
|
||||
/// through the facade untranslated.
|
||||
@@ -79,6 +80,8 @@ const AUDIO_E_INVALID: c_int = -60001;
|
||||
const AUDIO_E_FAILED: c_int = -60003;
|
||||
/// `OAKAUDIO_E_STATE`.
|
||||
const AUDIO_E_STATE: c_int = -60002;
|
||||
/// `OAKAUDIO_E_NOT_FOUND` — the waveform extractor's missing-file code.
|
||||
const AUDIO_E_NOT_FOUND: c_int = -60004;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Serialization + shared fixtures
|
||||
@@ -88,9 +91,7 @@ const AUDIO_E_STATE: c_int = -60002;
|
||||
/// process-wide and its create/destroy flips a global flag, so all
|
||||
/// manager tests take this lock and start from a destroyed state.
|
||||
fn with_manager(f: impl FnOnce()) {
|
||||
static LOCK: Mutex<()> = Mutex::new(());
|
||||
let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
f();
|
||||
common::with_manager(f)
|
||||
}
|
||||
|
||||
/// Serialize processor tests: each processor is an independent
|
||||
@@ -104,7 +105,7 @@ fn with_processor(f: impl FnOnce()) {
|
||||
|
||||
/// Current number of live refcounted oakaudio objects.
|
||||
fn alive() -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_debug_alive_count() }
|
||||
crate::stubs::audio::oakaudio_debug_alive_count()
|
||||
}
|
||||
|
||||
/// A borrowed `OakAudioParams*` mock handle (tests/common/mod.rs provides
|
||||
@@ -390,7 +391,7 @@ fn manager_recording() {
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_audio_start_recording(
|
||||
(&mut params as *mut oakcodec::ffi::encoder::oakcodec_encoding_params)
|
||||
(&mut params as *mut oakcodec::encodingparams::EncodingParams)
|
||||
.cast::<c_void>(),
|
||||
err.as_mut_ptr(),
|
||||
err.len() as c_int,
|
||||
@@ -412,7 +413,7 @@ fn manager_recording() {
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_audio_start_recording(
|
||||
(&mut params as *mut oakcodec::ffi::encoder::oakcodec_encoding_params)
|
||||
(&mut params as *mut oakcodec::encodingparams::EncodingParams)
|
||||
.cast::<c_void>(),
|
||||
err.as_mut_ptr(),
|
||||
err.len() as c_int,
|
||||
@@ -436,7 +437,7 @@ fn manager_recording() {
|
||||
let mut err = [0i8; 512];
|
||||
let rc = unsafe {
|
||||
oakengine_audio_start_recording(
|
||||
(&mut params as *mut oakcodec::ffi::encoder::oakcodec_encoding_params)
|
||||
(&mut params as *mut oakcodec::encodingparams::EncodingParams)
|
||||
.cast::<c_void>(),
|
||||
err.as_mut_ptr(),
|
||||
err.len() as c_int,
|
||||
@@ -472,8 +473,8 @@ fn manager_recording() {
|
||||
|
||||
/// `oakcodec_encoding_params` with WAV / pcm_s16le and the requested audio
|
||||
/// track.
|
||||
fn recording_params(audio_enabled: bool) -> oakcodec::ffi::encoder::oakcodec_encoding_params {
|
||||
let mut p: oakcodec::ffi::encoder::oakcodec_encoding_params = unsafe { std::mem::zeroed() };
|
||||
fn recording_params(audio_enabled: bool) -> oakcodec::encodingparams::EncodingParams {
|
||||
let mut p: oakcodec::encodingparams::EncodingParams = unsafe { std::mem::zeroed() };
|
||||
let path = recording_path();
|
||||
let bytes = path.as_os_str().as_encoded_bytes();
|
||||
assert!(bytes.len() < p.filename.len(), "temp path too long");
|
||||
@@ -483,7 +484,7 @@ fn recording_params(audio_enabled: bool) -> oakcodec::ffi::encoder::oakcodec_enc
|
||||
p.audio_codec = 13; // PCM_S16LE
|
||||
p.audio_sample_rate = 48000;
|
||||
p.audio_channel_layout = 3; // stereo
|
||||
p.audio_sample_format = 7; // SampleFormat::S16 (packed)
|
||||
p.audio_sample_format = oakcore_rs::SampleFormat::S16; // packed 16-bit
|
||||
p.export_length_num = 1;
|
||||
p.export_length_den = 1;
|
||||
p
|
||||
@@ -959,6 +960,164 @@ fn sync_place_by_waveform_offset() {
|
||||
assert_eq!(out.timeline_in_den, 0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Waveform extraction (real decode, no manager state)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `oakengine_waveform_extract` two-stage contract against real media: the
|
||||
/// facade's `oakengine_testmedia_write_clip` writes a clip carrying a
|
||||
/// stereo PCM 440 Hz sine, which the extractor decodes through the real
|
||||
/// oakcodec decoder. Illegal arguments → facade `OAKENGINE_E_INVALID`; a
|
||||
/// missing file passes the module's `OAKAUDIO_E_NOT_FOUND` through; the
|
||||
/// size query reports the point count + channel count; the data pass fills
|
||||
/// real min/max peaks.
|
||||
#[test]
|
||||
fn waveform_extract_two_stage_and_validation() {
|
||||
// Illegal arguments are rejected by the facade with its own code.
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_waveform_extract(
|
||||
std::ptr::null(),
|
||||
0,
|
||||
256,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
OAKENGINE_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_waveform_extract(
|
||||
c"x.wav".as_ptr(),
|
||||
-1, // negative stream index
|
||||
256,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
OAKENGINE_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_waveform_extract(
|
||||
c"x.wav".as_ptr(),
|
||||
0,
|
||||
0, // non-positive samples per point
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
OAKENGINE_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_waveform_extract(
|
||||
c"x.wav".as_ptr(),
|
||||
0,
|
||||
256,
|
||||
std::ptr::null_mut(),
|
||||
-1, // negative capacity
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
OAKENGINE_E_INVALID
|
||||
);
|
||||
|
||||
// A missing file decodes to the module's NOT_FOUND, passed through.
|
||||
let missing = std::env::temp_dir().join(format!(
|
||||
"oakengine-it-waveform-missing-{}.wav",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::remove_file(&missing);
|
||||
let missing_c = CString::new(missing.to_string_lossy().into_owned()).unwrap();
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_waveform_extract(
|
||||
missing_c.as_ptr(),
|
||||
0,
|
||||
256,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
AUDIO_E_NOT_FOUND
|
||||
);
|
||||
|
||||
// Real media: the facade test clip (1 s of stereo PCM 440 Hz sine).
|
||||
let media = std::env::temp_dir().join(format!(
|
||||
"oakengine-it-waveform-{}.mp4",
|
||||
std::process::id()
|
||||
));
|
||||
let media_c = CString::new(media.to_string_lossy().into_owned()).unwrap();
|
||||
assert_eq!(
|
||||
unsafe { crate::testmedia::oakengine_testmedia_write_clip(media_c.as_ptr(), 64, 64, 10, 10) },
|
||||
0,
|
||||
"generate real media for the waveform decode"
|
||||
);
|
||||
|
||||
// Size query: point count + channel count, nothing written.
|
||||
let mut channels = 0i32;
|
||||
let needed = unsafe {
|
||||
oakengine_waveform_extract(
|
||||
media_c.as_ptr(),
|
||||
0,
|
||||
256,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
&mut channels,
|
||||
)
|
||||
};
|
||||
assert!(needed > 0, "1 s at 48 kHz must yield points (got {needed})");
|
||||
assert_eq!(channels, 2, "the test clip's audio is stereo");
|
||||
|
||||
// Data pass: `out_pairs` receives `point_count * channel_count`
|
||||
// channel-interleaved pairs (capacity is in points), so the buffer is
|
||||
// sized for the stereo stream; the return is the point count.
|
||||
let channel_count = channels.max(1) as usize;
|
||||
let mut raw =
|
||||
vec![crate::pods::MinMax { min: 0.0, max: 0.0 }; needed as usize * channel_count];
|
||||
let n = unsafe {
|
||||
oakengine_waveform_extract(
|
||||
media_c.as_ptr(),
|
||||
0,
|
||||
256,
|
||||
raw.as_mut_ptr(),
|
||||
needed,
|
||||
&mut channels,
|
||||
)
|
||||
};
|
||||
assert_eq!(n, needed);
|
||||
// The 440 Hz sine is audible in the first channel: some window reaches a
|
||||
// real peak.
|
||||
assert!(
|
||||
raw.chunks(channel_count).any(|pt| pt[0].max.abs().max(pt[0].min.abs()) > 0.1),
|
||||
"the sine tone must show up in the peaks"
|
||||
);
|
||||
|
||||
// A capacity below the point count is a size-only query (the two-stage
|
||||
// contract): the full point count is returned and nothing is written.
|
||||
let mut two = [crate::pods::MinMax { min: 0.0, max: 0.0 }; 2];
|
||||
let n2 = unsafe {
|
||||
oakengine_waveform_extract(
|
||||
media_c.as_ptr(),
|
||||
0,
|
||||
256,
|
||||
two.as_mut_ptr(),
|
||||
2,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
assert_eq!(n2, needed);
|
||||
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&missing);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Processor — lifecycle, free contracts, validation (serialized)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -978,7 +1137,7 @@ fn processor_lifecycle_and_free_contracts() {
|
||||
// has nothing to release. The box must be a real heap box
|
||||
// (free_box deallocates it); a stack-allocated wrapper would be
|
||||
// deallocated out from under its owner.
|
||||
let empty_ptr = oakengine::handle::box_handle::<OakEngineAudioProcessor>(CHandle::null());
|
||||
let empty_ptr = crate::handle::box_handle::<OakEngineAudioProcessor>(CHandle::null());
|
||||
assert!(!empty_ptr.is_null());
|
||||
unsafe { oakengine_audio_processor_free(empty_ptr) };
|
||||
assert_eq!(alive(), baseline);
|
||||
@@ -992,12 +1151,11 @@ fn processor_lifecycle_and_free_contracts() {
|
||||
|
||||
// The module-level free is double-free-safe (ctx is nulled after
|
||||
// release); the counter decrements exactly once.
|
||||
let mut h = oakaudio::handle::CHandle::null();
|
||||
h = unsafe { oakaudio::ffi::processor::oakaudio_processor_init() };
|
||||
let mut h = crate::stubs::audio::oakaudio_processor_init();
|
||||
assert!(!h.ctx.is_null());
|
||||
assert_eq!(alive(), baseline + 1);
|
||||
unsafe { oakaudio::ffi::processor::oakaudio_processor_free(&mut h) };
|
||||
unsafe { oakaudio::ffi::processor::oakaudio_processor_free(&mut h) }; // no-op
|
||||
crate::stubs::audio::oakaudio_processor_free(&mut h);
|
||||
crate::stubs::audio::oakaudio_processor_free(&mut h); // no-op
|
||||
assert!(h.ctx.is_null());
|
||||
assert_eq!(alive(), baseline);
|
||||
});
|
||||
+29
-13
@@ -40,12 +40,11 @@
|
||||
//! use-after-free by design (the engine header transfers ownership), so the
|
||||
//! tests assert NULL-idempotence rather than double-free of a live handle.
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use oakengine::codec::{
|
||||
use crate::codec::{
|
||||
oakengine_encoding_codec_is_lossless, oakengine_encoding_codec_is_still_image,
|
||||
oakengine_encoding_codec_name, oakengine_encoding_filename_contains_digit_placeholder,
|
||||
oakengine_encoding_filename_remove_digit_placeholder, oakengine_encoding_format_audio_codec_at,
|
||||
@@ -90,7 +89,7 @@ use oakengine::codec::{
|
||||
oakengine_encoding_sample_format_at, oakengine_encoding_sample_format_count,
|
||||
oakengine_encoding_start_audio_recording, oakengine_export_render_with_params,
|
||||
};
|
||||
use oakengine::common::OakVideoParamsPod;
|
||||
use crate::common::OakVideoParamsPod;
|
||||
|
||||
/// Facade error codes (`src/error.rs`).
|
||||
const E_INVALID: c_int = -1;
|
||||
@@ -665,7 +664,7 @@ fn params_handle_legal_round_trip() {
|
||||
let mut vp: OakVideoParamsPod = unsafe { std::mem::zeroed() };
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine::common::oakengine_video_params_make(
|
||||
crate::common::oakengine_video_params_make(
|
||||
&mut vp, 1920, 1080, 1001, 30000, 4, 1, 1, 0, 1, 1,
|
||||
)
|
||||
},
|
||||
@@ -1254,10 +1253,16 @@ fn params_destroy_contracts() {
|
||||
// 7. Deferred / not-backed entry points
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Preset path/count/name, params load/save, export render and the
|
||||
/// sequence-bound last-used stubs are documented as not backed: they return
|
||||
/// the fixed contract value (E_FAILED / 0 / NULL / no-op) regardless of the
|
||||
/// Preset path/count/name, params load/save and the sequence-bound
|
||||
/// last-used stubs are documented as not backed: they return the fixed
|
||||
/// contract value (E_FAILED / 0 / NULL / no-op) regardless of the
|
||||
/// arguments, so every argument combination is safe.
|
||||
///
|
||||
/// `oakengine_export_render_with_params` is backed since M12 (see
|
||||
/// `crate::codec`, "Exporter family"); its argument validation returns
|
||||
/// E_INVALID for NULL inputs, and the params handle stays owned by the
|
||||
/// caller on the rejected path (the success path would consume it, like
|
||||
/// `oakengine_task_create_export`).
|
||||
#[test]
|
||||
fn deferred_stubs_contract() {
|
||||
let mut buf = [0 as c_char; 64];
|
||||
@@ -1298,13 +1303,15 @@ fn deferred_stubs_contract() {
|
||||
unsafe { oakengine_encoding_params_save_file(std::ptr::null_mut(), std::ptr::null()) },
|
||||
E_FAILED
|
||||
);
|
||||
// Backed since M12: NULL arguments are rejected with E_INVALID, and
|
||||
// the valid handle is NOT consumed on the rejected path.
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render_with_params(std::ptr::null_mut(), p) },
|
||||
E_FAILED
|
||||
E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render_with_params(std::ptr::null_mut(), std::ptr::null()) },
|
||||
E_FAILED
|
||||
E_INVALID
|
||||
);
|
||||
|
||||
assert!(unsafe { oakengine_encoding_params_get_last_used(std::ptr::null_mut()) }.is_null());
|
||||
@@ -1327,6 +1334,13 @@ fn deferred_stubs_contract() {
|
||||
/// the facade, the oakaudio manager, and the error-string plumbing.
|
||||
#[test]
|
||||
fn start_audio_recording_manager_paths() {
|
||||
// Serialized: the AudioManager singleton is process-wide and shared
|
||||
// with the audio-family tests now that the former integration tests
|
||||
// run in one process.
|
||||
common::with_manager(|| start_audio_recording_manager_paths_inner());
|
||||
}
|
||||
|
||||
fn start_audio_recording_manager_paths_inner() {
|
||||
let p = unsafe { oakengine_encoding_params_create() };
|
||||
assert!(!p.is_null());
|
||||
let mut err = [0 as c_char; 128];
|
||||
@@ -1339,7 +1353,9 @@ fn start_audio_recording_manager_paths() {
|
||||
E_INVALID
|
||||
);
|
||||
|
||||
// No manager singleton -> facade E_STATE.
|
||||
// No manager singleton -> facade E_STATE (destroy it first: another
|
||||
// serialized test may have left it created).
|
||||
assert_eq!(unsafe { crate::audio::oakengine_audio_destroy_instance() }, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_encoding_start_audio_recording(p, err.as_mut_ptr(), 128) },
|
||||
E_STATE
|
||||
@@ -1347,7 +1363,7 @@ fn start_audio_recording_manager_paths() {
|
||||
|
||||
// Create the real singleton through the audio facade.
|
||||
assert_eq!(
|
||||
unsafe { oakengine::audio::oakengine_audio_create_instance() },
|
||||
unsafe { crate::audio::oakengine_audio_create_instance() },
|
||||
0
|
||||
);
|
||||
|
||||
@@ -1379,7 +1395,7 @@ fn start_audio_recording_manager_paths() {
|
||||
|
||||
// Tear the singleton down: the E_STATE contract returns.
|
||||
assert_eq!(
|
||||
unsafe { oakengine::audio::oakengine_audio_destroy_instance() },
|
||||
unsafe { crate::audio::oakengine_audio_destroy_instance() },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
+3
-4
@@ -35,15 +35,14 @@
|
||||
// (e.g. `oakengine_config_load`) otherwise trip `unused_unsafe`.
|
||||
#![allow(unused_unsafe)]
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use oakengine::common::{
|
||||
use crate::common::{
|
||||
oakengine_config_get_int, oakengine_config_get_string, oakengine_config_load,
|
||||
oakengine_config_report_error, oakengine_config_save, oakengine_config_set_error_handler,
|
||||
oakengine_config_set_int, oakengine_config_set_string, oakengine_video_params_bytes_per_pixel,
|
||||
@@ -63,7 +62,7 @@ use oakengine::common::{
|
||||
|
||||
/// Read a two-stage facade string into a Rust String.
|
||||
unsafe fn read_buf(buf: &mut [c_char]) -> String {
|
||||
std::ffi::CStr::from_ptr(buf.as_ptr())
|
||||
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
// 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/>.
|
||||
|
||||
//! Integration tests for the **exporter family** (`src/codec.rs`,
|
||||
//! "Exporter family"; module contract
|
||||
//! `engine/include/oakengine/exporter.h`).
|
||||
//!
|
||||
//! Coverage rules (see the family test charter):
|
||||
//! 1. no mocks — every call goes through the real facade into the real
|
||||
//! module crates (the only stubs are the host-provided `oakcore_*`
|
||||
//! symbols in `tests/common`); the output file is a REAL mp4 written
|
||||
//! by the statically linked FFmpeg (oakcodec encoder), asserted by
|
||||
//! its `ftyp` box;
|
||||
//! 2. every exporter-family export is exercised on a legal path with the
|
||||
//! result asserted;
|
||||
//! 3. illegal inputs (NULL seq/path, negative ranges, unknown codecs)
|
||||
//! always yield a negative error code — never a crash;
|
||||
//! 4. the progress callback receives at least one update during a run,
|
||||
//! with the installed userdata.
|
||||
//!
|
||||
//! ## Serialization
|
||||
//!
|
||||
//! The tests assemble projects and run export tasks, both of which touch
|
||||
//! process-wide state (the undo stack cleared by `oakengine_project_new`,
|
||||
//! the global task manager), so every test takes a shared [`SERIAL`] mutex
|
||||
//! (the same pattern as `it_task`).
|
||||
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_char, c_double, c_int, c_void};
|
||||
use std::io::Read;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use crate::codec::{
|
||||
oakengine_encoding_params_create, oakengine_encoding_params_enable_audio,
|
||||
oakengine_encoding_params_enable_video, oakengine_encoding_params_set_filename,
|
||||
oakengine_encoding_params_set_format, oakengine_export_last_error, oakengine_export_render,
|
||||
oakengine_export_render_with_params, oakengine_export_set_progress_callback,
|
||||
};
|
||||
use crate::common::OakVideoParamsPod;
|
||||
use crate::handle::{OakEngineProject, OakEngineSequence, free_box};
|
||||
use crate::node::{
|
||||
oakengine_footage_free, oakengine_project_create, oakengine_project_free,
|
||||
oakengine_project_import_footage, oakengine_project_new,
|
||||
};
|
||||
use crate::pods::OakExportOptions;
|
||||
use crate::testmedia::oakengine_testmedia_write_clip;
|
||||
use crate::timeline::{
|
||||
oakengine_sequence_add_footage_clip_ex, oakengine_sequence_add_track, oakengine_sequence_new,
|
||||
oakengine_sequence_set_video_params,
|
||||
};
|
||||
|
||||
/// `OAKENGINE_TRACK_TYPE_*` (timeline.h).
|
||||
const TRACK_VIDEO: c_int = 0;
|
||||
const TRACK_AUDIO: c_int = 1;
|
||||
/// `olive::ExportFormat::Format` ids (mp4 = MPEG-4 video).
|
||||
const FORMAT_MP4: c_int = 2;
|
||||
/// `olive::ExportCodec::Codec` ids.
|
||||
const CODEC_H264: c_int = 1;
|
||||
const CODEC_AAC: c_int = 12;
|
||||
|
||||
/// Serializes every test in this binary (see the module docs). Poisoned by
|
||||
/// a panicking test, the lock is recovered with `into_inner` so one failure
|
||||
/// does not cascade into `PoisonError` failures in every later test.
|
||||
static SERIAL: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// Take the [`SERIAL`] lock, recovering from any poisoning.
|
||||
fn serial() -> std::sync::MutexGuard<'static, ()> {
|
||||
SERIAL.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// A unique temp path (per-process, so parallel test binaries never
|
||||
/// collide).
|
||||
fn temp_path(kind: &str) -> std::path::PathBuf {
|
||||
std::env::temp_dir().join(format!("oakengine-it-export-{kind}-{}.mp4", std::process::id()))
|
||||
}
|
||||
|
||||
/// Encode the facade test clip and assemble a project + sequence carrying
|
||||
/// it: one video track/clip and one audio track/clip, both spanning
|
||||
/// `0..frame_count` at `fps` frames per second (mirrors the CLI's
|
||||
/// `assemble_project`). Returns `(project, sequence)` — the caller
|
||||
/// releases the sequence box with `free_box` and the project with
|
||||
/// `oakengine_project_free`.
|
||||
///
|
||||
/// # Safety
|
||||
/// The returned handles must be released by the caller exactly once.
|
||||
unsafe fn assemble_test_sequence(
|
||||
media: &std::path::Path,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
frame_count: i64,
|
||||
fps: c_int,
|
||||
) -> (*mut OakEngineProject, *mut OakEngineSequence) {
|
||||
let media_c = std::ffi::CString::new(media.to_string_lossy().into_owned()).unwrap();
|
||||
assert_eq!(
|
||||
oakengine_testmedia_write_clip(media_c.as_ptr(), width, height, frame_count as c_int, fps),
|
||||
0,
|
||||
"generate the source clip"
|
||||
);
|
||||
|
||||
let project = oakengine_project_create();
|
||||
assert!(!project.is_null());
|
||||
assert_eq!(unsafe { oakengine_project_new(project) }, 0);
|
||||
|
||||
let footage = unsafe { oakengine_project_import_footage(project, media_c.as_ptr()) };
|
||||
assert!(!footage.is_null(), "import the test clip");
|
||||
|
||||
let seq = unsafe { oakengine_sequence_new(project, c"Export Test".as_ptr()) };
|
||||
assert!(!seq.is_null());
|
||||
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_sequence_set_video_params(
|
||||
seq, width, height, fps, 1, 1, 1, 0, 4, // PIXEL_FORMAT_F32
|
||||
0,
|
||||
)
|
||||
},
|
||||
0,
|
||||
"set the sequence frame rate"
|
||||
);
|
||||
|
||||
let vt = unsafe { oakengine_sequence_add_track(seq, TRACK_VIDEO) };
|
||||
assert!(vt >= 0, "add the video track");
|
||||
let vclip = unsafe {
|
||||
oakengine_sequence_add_footage_clip_ex(seq, footage, TRACK_VIDEO, vt, 0, frame_count, 0)
|
||||
};
|
||||
assert!(!vclip.is_null(), "place the video clip");
|
||||
|
||||
let at = unsafe { oakengine_sequence_add_track(seq, TRACK_AUDIO) };
|
||||
assert!(at >= 0, "add the audio track");
|
||||
let aclip = unsafe {
|
||||
oakengine_sequence_add_footage_clip_ex(seq, footage, TRACK_AUDIO, at, 0, frame_count, 0)
|
||||
};
|
||||
assert!(!aclip.is_null(), "place the audio clip");
|
||||
|
||||
unsafe { oakengine_footage_free(footage) };
|
||||
(project, seq)
|
||||
}
|
||||
|
||||
/// Read the facade's thread-local export last-error (the string the
|
||||
/// assertion messages embed).
|
||||
fn export_last_error_str() -> String {
|
||||
let mut buf = [0 as c_char; 512];
|
||||
let n = unsafe { oakengine_export_last_error(buf.as_mut_ptr(), 512) };
|
||||
if n <= 0 {
|
||||
return String::new();
|
||||
}
|
||||
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
|
||||
String::from_utf8_lossy(unsafe {
|
||||
std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
/// Assert the exported file exists, is non-empty and starts with the MP4
|
||||
/// `ftyp` box.
|
||||
fn assert_real_mp4(path: &std::path::Path) {
|
||||
let meta = std::fs::metadata(path).expect("the exported file must exist");
|
||||
assert!(meta.len() > 0, "the exported file must be non-empty");
|
||||
let mut head = [0u8; 12];
|
||||
std::fs::File::open(path)
|
||||
.expect("reopen the exported file")
|
||||
.read_exact(&mut head)
|
||||
.expect("read the file head");
|
||||
assert_eq!(&head[4..8], b"ftyp", "MP4 files start with the ftyp box");
|
||||
}
|
||||
|
||||
/// Release the assembly returned by [`assemble_test_sequence`].
|
||||
///
|
||||
/// # Safety
|
||||
/// The handles must be the ones returned by [`assemble_test_sequence`].
|
||||
unsafe fn drop_test_sequence(project: *mut OakEngineProject, seq: *mut OakEngineSequence) {
|
||||
unsafe {
|
||||
free_box::<OakEngineSequence>(seq);
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Legal paths (real mp4 output)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `oakengine_export_render` end-to-end: a 1 s test clip (10 frames at
|
||||
/// 10 fps, 64x64) in a project + sequence is exported to H.264/AAC MP4
|
||||
/// with explicit `OakExportOptions`; the file exists, is non-empty, starts
|
||||
/// with the `ftyp` box, and `oakengine_export_last_error` is empty after
|
||||
/// the success.
|
||||
#[test]
|
||||
fn export_render_writes_real_mp4() {
|
||||
let _g = serial();
|
||||
common::force_link();
|
||||
|
||||
let media = temp_path("src");
|
||||
let out = temp_path("out");
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&out);
|
||||
|
||||
let (project, seq) = unsafe { assemble_test_sequence(&media, 64, 64, 10, 10) };
|
||||
|
||||
let opts = OakExportOptions {
|
||||
video_codec: 0, // OAKENGINE_EXPORT_VIDEO_H264
|
||||
audio_codec: 0, // OAKENGINE_EXPORT_AUDIO_AAC
|
||||
video_bit_rate: 0,
|
||||
audio_sample_rate: 48000,
|
||||
audio_channel_count: 2,
|
||||
};
|
||||
let out_c = std::ffi::CString::new(out.to_string_lossy().into_owned()).unwrap();
|
||||
let rc = unsafe { oakengine_export_render(seq, out_c.as_ptr(), 0, 10, 64, 64, &opts) };
|
||||
assert_eq!(
|
||||
rc,
|
||||
0,
|
||||
"the mp4 export must succeed with the real encoder (last_error: {})",
|
||||
export_last_error_str()
|
||||
);
|
||||
|
||||
assert_real_mp4(&out);
|
||||
|
||||
// The thread-local last-error slot is empty after a success.
|
||||
let mut buf = [0 as c_char; 256];
|
||||
let n = unsafe { oakengine_export_last_error(buf.as_mut_ptr(), 256) };
|
||||
assert_eq!(n, 0, "last_error must be empty after a successful export");
|
||||
|
||||
unsafe { drop_test_sequence(project, seq) };
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&out);
|
||||
}
|
||||
|
||||
/// `oakengine_export_render_with_params` end-to-end: the same assembly
|
||||
/// driven through a caller-built encoding-params handle (the path the
|
||||
/// app's `start_export` uses). The handle is consumed by the export task.
|
||||
#[test]
|
||||
fn export_render_with_params_writes_real_mp4() {
|
||||
let _g = serial();
|
||||
common::force_link();
|
||||
|
||||
let media = temp_path("srcwp");
|
||||
let out = temp_path("outwp");
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&out);
|
||||
|
||||
let (project, seq) = unsafe { assemble_test_sequence(&media, 64, 64, 10, 10) };
|
||||
|
||||
let params = unsafe { oakengine_encoding_params_create() };
|
||||
assert!(!params.is_null());
|
||||
let out_c = std::ffi::CString::new(out.to_string_lossy().into_owned()).unwrap();
|
||||
assert_eq!(unsafe { oakengine_encoding_params_set_filename(params, out_c.as_ptr()) }, 0);
|
||||
assert_eq!(unsafe { oakengine_encoding_params_set_format(params, FORMAT_MP4) }, 0);
|
||||
let pod = OakVideoParamsPod {
|
||||
width: 64,
|
||||
height: 64,
|
||||
time_base_num: 1,
|
||||
time_base_den: 10, // 10 fps → frame duration 1/10
|
||||
format: 0,
|
||||
pixel_aspect_num: 1,
|
||||
pixel_aspect_den: 1,
|
||||
interlacing: 0,
|
||||
color_range: 0,
|
||||
divider: 1,
|
||||
video_type: 0,
|
||||
premultiplied_alpha: 0,
|
||||
};
|
||||
assert_eq!(unsafe { oakengine_encoding_params_enable_video(params, &pod, CODEC_H264) }, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_encoding_params_enable_audio(params, 48000, 0x3, 0, CODEC_AAC) },
|
||||
0
|
||||
);
|
||||
// Export length: 10 frames at 10 fps = 1 s.
|
||||
unsafe {
|
||||
crate::codec::oakengine_encoding_params_set_export_length(params, 1, 1);
|
||||
}
|
||||
|
||||
let rc = unsafe { oakengine_export_render_with_params(seq, params) };
|
||||
assert_eq!(
|
||||
rc,
|
||||
0,
|
||||
"the with_params export must succeed with the real encoder (last_error: {})",
|
||||
export_last_error_str()
|
||||
);
|
||||
// The params handle was consumed by the task; do NOT destroy it here.
|
||||
|
||||
assert_real_mp4(&out);
|
||||
|
||||
unsafe { drop_test_sequence(project, seq) };
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&out);
|
||||
}
|
||||
|
||||
/// `oakengine_export_render` with NULL opts selects the documented
|
||||
/// defaults (H.264/AAC mp4), and the installed progress callback receives
|
||||
/// at least one update with the installed userdata during the run.
|
||||
#[test]
|
||||
fn export_render_defaults_and_progress_callback() {
|
||||
let _g = serial();
|
||||
common::force_link();
|
||||
|
||||
let media = temp_path("srcprog");
|
||||
let out = temp_path("outprog");
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&out);
|
||||
|
||||
let (project, seq) = unsafe { assemble_test_sequence(&media, 64, 64, 10, 10) };
|
||||
|
||||
// Progress callbacks are thread-local on the exporting thread; the
|
||||
// sync run fires them there, so the atomics are safe to reset while
|
||||
// holding the serial lock.
|
||||
PROGRESS_CALLS.store(0, Ordering::SeqCst);
|
||||
PROGRESS_USERDATA_HIT.store(0, Ordering::SeqCst);
|
||||
let out_c = std::ffi::CString::new(out.to_string_lossy().into_owned()).unwrap();
|
||||
unsafe {
|
||||
oakengine_export_set_progress_callback(Some(count_progress), PROGRESS_TOKEN as *mut c_void);
|
||||
}
|
||||
let rc = unsafe { oakengine_export_render(seq, out_c.as_ptr(), 0, 10, 64, 64, std::ptr::null()) };
|
||||
assert_eq!(
|
||||
rc,
|
||||
0,
|
||||
"NULL opts must select the H.264/AAC defaults (last_error: {})",
|
||||
export_last_error_str()
|
||||
);
|
||||
assert_real_mp4(&out);
|
||||
assert!(
|
||||
PROGRESS_CALLS.load(Ordering::SeqCst) > 0,
|
||||
"progress must be reported during a run"
|
||||
);
|
||||
assert!(
|
||||
PROGRESS_USERDATA_HIT.load(Ordering::SeqCst) > 0,
|
||||
"the installed userdata must reach the callback"
|
||||
);
|
||||
|
||||
// NULL disables the callback for subsequent runs.
|
||||
unsafe { oakengine_export_set_progress_callback(None, std::ptr::null_mut()) };
|
||||
|
||||
unsafe { drop_test_sequence(project, seq) };
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&out);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Illegal inputs (negative codes, no crash)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Every invalid argument combination of `oakengine_export_render` returns
|
||||
/// `OAKENGINE_E_INVALID` (-1) with a non-empty last-error, and the
|
||||
/// `oakengine_export_render_with_params` NULL paths return E_INVALID
|
||||
/// without consuming the caller's params handle.
|
||||
#[test]
|
||||
fn export_render_illegal_arguments() {
|
||||
let _g = serial();
|
||||
common::force_link();
|
||||
|
||||
let media = temp_path("srcbad");
|
||||
let out = temp_path("outbad");
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&out);
|
||||
|
||||
let (project, seq) = unsafe { assemble_test_sequence(&media, 64, 64, 10, 10) };
|
||||
|
||||
let opts = OakExportOptions {
|
||||
video_codec: 0,
|
||||
audio_codec: 0,
|
||||
video_bit_rate: 0,
|
||||
audio_sample_rate: 48000,
|
||||
audio_channel_count: 2,
|
||||
};
|
||||
let out_c = std::ffi::CString::new(out.to_string_lossy().into_owned()).unwrap();
|
||||
|
||||
// NULL seq / path.
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render(std::ptr::null_mut(), out_c.as_ptr(), 0, 10, 64, 64, &opts) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render(seq, std::ptr::null(), 0, 10, 64, 64, &opts) },
|
||||
-1
|
||||
);
|
||||
// Negative / inverted ranges.
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render(seq, out_c.as_ptr(), -1, 10, 64, 64, &opts) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render(seq, out_c.as_ptr(), 5, 5, 64, 64, &opts) },
|
||||
-1
|
||||
);
|
||||
// Unknown codec ids.
|
||||
let bad_video = OakExportOptions { video_codec: 99, ..opts };
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render(seq, out_c.as_ptr(), 0, 10, 64, 64, &bad_video) },
|
||||
-1
|
||||
);
|
||||
let bad_audio = OakExportOptions { audio_codec: 99, ..opts };
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render(seq, out_c.as_ptr(), 0, 10, 64, 64, &bad_audio) },
|
||||
-1
|
||||
);
|
||||
// An unsupported audio channel count.
|
||||
let bad_channels = OakExportOptions { audio_channel_count: 3, ..opts };
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render(seq, out_c.as_ptr(), 0, 10, 64, 64, &bad_channels) },
|
||||
-1
|
||||
);
|
||||
// The failures record a reason.
|
||||
let mut buf = [0 as c_char; 256];
|
||||
let n = unsafe { oakengine_export_last_error(buf.as_mut_ptr(), 256) };
|
||||
assert!(n > 0, "a failed export must record a last error");
|
||||
|
||||
// `with_params`: NULL arguments are rejected without consuming the
|
||||
// valid handle (the caller destroys it afterwards).
|
||||
let params = unsafe { oakengine_encoding_params_create() };
|
||||
assert!(!params.is_null());
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render_with_params(std::ptr::null_mut(), params) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render_with_params(seq, std::ptr::null()) },
|
||||
-1
|
||||
);
|
||||
unsafe { crate::codec::oakengine_encoding_params_destroy(params) };
|
||||
|
||||
assert!(!out.exists(), "a rejected export must not write the output");
|
||||
|
||||
unsafe { drop_test_sequence(project, seq) };
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&out);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Progress callback bookkeeping
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Sentinel userdata the progress test installs.
|
||||
const PROGRESS_TOKEN: usize = 0x0A0A_5EED;
|
||||
|
||||
/// Number of progress callback invocations (reset per test).
|
||||
static PROGRESS_CALLS: AtomicUsize = AtomicUsize::new(0);
|
||||
/// Number of invocations that received the sentinel userdata.
|
||||
static PROGRESS_USERDATA_HIT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// Counts every progress callback and checks the userdata round-trip.
|
||||
unsafe extern "C" fn count_progress(_fraction: c_double, userdata: *mut c_void) {
|
||||
PROGRESS_CALLS.fetch_add(1, Ordering::SeqCst);
|
||||
if userdata as usize == PROGRESS_TOKEN {
|
||||
PROGRESS_USERDATA_HIT.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
+58
-52
@@ -35,22 +35,23 @@
|
||||
//! oakplugin crate's build.rs compiles (cbits/oak_test_plugin.c). When
|
||||
//! it is unavailable the test prints SKIP and returns (never fails).
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void, CStr, CString};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use oakengine::handle::{CHandle, OakEngineNode};
|
||||
use oakengine::plugin::{
|
||||
use crate::handle::{CHandle, OakEngineNode};
|
||||
use crate::plugin::{
|
||||
oakengine_plugin_load_plugins, oakengine_plugin_node_push_button_clicked,
|
||||
oakengine_plugin_set_active_viewer_provider, oakengine_plugin_set_progress_reporter_factory,
|
||||
};
|
||||
use oakplugin::ffi::{
|
||||
oakplugin_debug_alive_count, oakplugin_host_plugin_count, oakplugin_host_plugin_id_at,
|
||||
oakplugin_host_plugin_label, oakplugin_instance_create, oakplugin_instance_free,
|
||||
};
|
||||
// The deleted `oakplugin::ffi` handle surface is replaced by the crate's
|
||||
// direct host API (single-lib unification).
|
||||
use oakplugin::handle::RefBox;
|
||||
use oakplugin::host::Host;
|
||||
use oakplugin::instance::Instance;
|
||||
use oakplugin::property::Value;
|
||||
|
||||
/// `OAKENGINE_E_INVALID` (src/error.rs).
|
||||
const E_INVALID: c_int = -1;
|
||||
@@ -88,12 +89,12 @@ fn fresh_temp_dir(name: &str) -> PathBuf {
|
||||
|
||||
/// Current number of live backend objects (host instance registry).
|
||||
fn alive() -> c_int {
|
||||
unsafe { oakplugin_debug_alive_count() }
|
||||
Host::global().alive_count() as c_int
|
||||
}
|
||||
|
||||
/// Number of plugins discovered by the real host cache.
|
||||
fn plugin_count() -> c_int {
|
||||
unsafe { oakplugin_host_plugin_count() }
|
||||
Host::global().cache.count() as c_int
|
||||
}
|
||||
|
||||
/// Run the facade scan through the real host, returning its exit code.
|
||||
@@ -102,6 +103,34 @@ fn scan_facade(dir: &Path) -> c_int {
|
||||
unsafe { oakengine_plugin_load_plugins(cs.as_ptr()) }
|
||||
}
|
||||
|
||||
/// The identifier of the plugin at `index` (the direct-API replacement of
|
||||
/// the deleted `oakplugin_host_plugin_id_at` two-stage getter).
|
||||
fn host_plugin_id_at(index: usize) -> Option<String> {
|
||||
Host::global().cache.at(index).map(|p| p.identifier.clone())
|
||||
}
|
||||
|
||||
/// The label of the named plugin (direct-API replacement of the deleted
|
||||
/// `oakplugin_host_plugin_label` two-stage getter).
|
||||
fn host_plugin_label(id: &str) -> Option<String> {
|
||||
let plugin = Host::global().cache.find(id)?;
|
||||
match plugin.descriptor.props.get("OfxPropLabel", 0) {
|
||||
Some(Value::String(s)) => Some(s.to_string_lossy().into_owned()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a plugin instance by id (direct-API replacement of the deleted
|
||||
/// `oakplugin_instance_create`; `None` = the old empty handle).
|
||||
fn host_instance_create(id: &str) -> Option<Arc<RefBox<Instance>>> {
|
||||
Host::global().create_instance(id, None).ok()
|
||||
}
|
||||
|
||||
/// Free a plugin instance (direct-API replacement of the deleted
|
||||
/// `oakplugin_instance_free`; dropping the Arc is the refcounted free).
|
||||
fn host_instance_free(inst: &mut Option<Arc<RefBox<Instance>>>) {
|
||||
*inst = None;
|
||||
}
|
||||
|
||||
/// Locate the real test plugin shared library: oakplugin's build.rs
|
||||
/// compiles cbits/oak_test_plugin.c to `$OUT_DIR/oak_test_plugin.{dylib,so}`
|
||||
/// inside its own `target/*/build/oakplugin-*/out/` directory.
|
||||
@@ -192,23 +221,13 @@ fn fresh_test_plugin_scan_dir(tag: &str) -> Option<PathBuf> {
|
||||
}
|
||||
|
||||
/// All plugin identifiers currently registered in the real host cache
|
||||
/// (two-stage getter round trip per index).
|
||||
/// (direct cache iteration).
|
||||
fn plugin_ids() -> Vec<String> {
|
||||
let count = plugin_count();
|
||||
let mut out = Vec::new();
|
||||
for i in 0..count {
|
||||
let len = unsafe { oakplugin_host_plugin_id_at(i, std::ptr::null_mut(), 0) };
|
||||
if len <= 0 {
|
||||
continue;
|
||||
}
|
||||
let mut buf = vec![0u8; len as usize];
|
||||
let rc = unsafe { oakplugin_host_plugin_id_at(i, buf.as_mut_ptr() as *mut c_char, len) };
|
||||
if rc == 0 {
|
||||
out.push(
|
||||
unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
);
|
||||
if let Some(id) = host_plugin_id_at(i as usize) {
|
||||
out.push(id);
|
||||
}
|
||||
}
|
||||
out
|
||||
@@ -508,21 +527,8 @@ fn load_plugins_real_bundle_end_to_end() {
|
||||
}
|
||||
|
||||
// Label lookup for a known id resolves (phase 1: the id itself).
|
||||
let len = unsafe {
|
||||
oakplugin_host_plugin_label(c"org.oak.test-plugin".as_ptr(), std::ptr::null_mut(), 0)
|
||||
};
|
||||
assert!(len > 0);
|
||||
let mut lbuf = vec![0u8; len as usize];
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakplugin_host_plugin_label(
|
||||
c"org.oak.test-plugin".as_ptr(),
|
||||
lbuf.as_mut_ptr() as *mut c_char,
|
||||
len,
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
let label = host_plugin_label(TEST_PLUGIN_ID);
|
||||
assert!(label.is_some(), "scanned test plugin must expose a label");
|
||||
|
||||
// Repeat scan of the same path is deduplicated: still OK, cache
|
||||
// unchanged, alive counter untouched.
|
||||
@@ -586,16 +592,16 @@ fn backend_instance_free_contracts() {
|
||||
common::force_link();
|
||||
let base = alive();
|
||||
|
||||
// free(NULL) and free(empty handle) are no-ops.
|
||||
unsafe { oakplugin_instance_free(std::ptr::null_mut()) };
|
||||
let mut empty = CHandle::null();
|
||||
unsafe { oakplugin_instance_free(&mut empty) };
|
||||
assert!(empty.is_null(), "free must leave the handle emptied");
|
||||
// free(NULL-equivalent) and free(empty handle) are no-ops.
|
||||
host_instance_free(&mut None);
|
||||
let mut empty: Option<Arc<RefBox<Instance>>> = None;
|
||||
host_instance_free(&mut empty);
|
||||
assert!(empty.is_none(), "free must leave the handle emptied");
|
||||
|
||||
// Unknown plugin id → empty handle, not a crash.
|
||||
let mut h = unsafe { oakplugin_instance_create(c"org.oak.not-a-plugin".as_ptr()) };
|
||||
assert!(h.is_null());
|
||||
unsafe { oakplugin_instance_free(&mut h) };
|
||||
let mut h = host_instance_create("org.oak.not-a-plugin");
|
||||
assert!(h.is_none());
|
||||
host_instance_free(&mut h);
|
||||
assert_eq!(alive(), base);
|
||||
|
||||
// Real plugin: create +1, free back to baseline, double-free safe.
|
||||
@@ -604,21 +610,21 @@ fn backend_instance_free_contracts() {
|
||||
return;
|
||||
};
|
||||
assert_eq!(scan_facade(&dir), 0);
|
||||
let mut inst = unsafe { oakplugin_instance_create(c"org.oak.test-plugin".as_ptr()) };
|
||||
let mut inst = host_instance_create(TEST_PLUGIN_ID);
|
||||
assert!(
|
||||
!inst.is_null(),
|
||||
inst.is_some(),
|
||||
"scanned test plugin must create an instance"
|
||||
);
|
||||
assert_eq!(alive(), base + 1, "one live instance must be registered");
|
||||
unsafe { oakplugin_instance_free(&mut inst) };
|
||||
assert!(inst.is_null());
|
||||
host_instance_free(&mut inst);
|
||||
assert!(inst.is_none());
|
||||
assert_eq!(
|
||||
alive(),
|
||||
base,
|
||||
"free must return the alive counter to baseline"
|
||||
);
|
||||
// Double free of the already-emptied handle is a no-op.
|
||||
unsafe { oakplugin_instance_free(&mut inst) };
|
||||
host_instance_free(&mut inst);
|
||||
assert_eq!(alive(), base);
|
||||
});
|
||||
}
|
||||
@@ -44,25 +44,25 @@
|
||||
//!
|
||||
//! ## Ignored tests
|
||||
//!
|
||||
//! - [`export_task_run_ignored_environment_gated`]: running an export
|
||||
//! needs a real GPU/OpenGL render and a real ffmpeg encoder; the host
|
||||
//! stubs cannot encode. The creation path is covered in the main suite.
|
||||
//! (None — the export run used to be environment-gated on GPU/encoder;
|
||||
//! the real CPU render + statically linked FFmpeg encoder now works in
|
||||
//! the test environment, see [`export_task_run_real_encoder`].)
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use oakengine::codec::{oakengine_encoding_params_create, oakengine_encoding_params_set_filename};
|
||||
use oakengine::handle::{
|
||||
use crate::codec::{oakengine_encoding_params_create, oakengine_encoding_params_set_filename};
|
||||
use crate::handle::{
|
||||
free_box, CHandle, OakEngineNode, OakEngineProject, OakEngineSequence, OakEngineTask,
|
||||
};
|
||||
use oakengine::node::{
|
||||
oakengine_node_free, oakengine_project_create, oakengine_project_free, oakengine_project_new,
|
||||
oakengine_project_root, oakengine_project_set_filename,
|
||||
use crate::node::{
|
||||
oakengine_node_free, oakengine_project_create, oakengine_project_filename,
|
||||
oakengine_project_free, oakengine_project_new, oakengine_project_root, oakengine_project_save,
|
||||
oakengine_project_set_filename,
|
||||
};
|
||||
use oakengine::task::{
|
||||
use crate::task::{
|
||||
oakengine_cli_task_dialog_run, oakengine_task_cancel, oakengine_task_create_export,
|
||||
oakengine_task_create_project_import, oakengine_task_create_project_load,
|
||||
oakengine_task_create_project_load_otio, oakengine_task_create_project_save,
|
||||
@@ -70,13 +70,13 @@ use oakengine::task::{
|
||||
oakengine_task_free, oakengine_task_import_file_count, oakengine_task_import_footage_at,
|
||||
oakengine_task_import_footage_count, oakengine_task_import_get_command,
|
||||
oakengine_task_import_invalid_file_at, oakengine_task_import_invalid_files_count,
|
||||
oakengine_task_is_cancelled, oakengine_task_manager_add, oakengine_task_manager_cancel,
|
||||
oakengine_task_manager_count, oakengine_task_manager_first, oakengine_task_manager_handle,
|
||||
oakengine_task_save_get_project, oakengine_task_start_sync, oakengine_task_start_time,
|
||||
oakengine_task_title,
|
||||
oakengine_task_is_cancelled, oakengine_task_load_take_project, oakengine_task_manager_add,
|
||||
oakengine_task_manager_cancel, oakengine_task_manager_count, oakengine_task_manager_first,
|
||||
oakengine_task_manager_handle, oakengine_task_save_get_project, oakengine_task_start_sync,
|
||||
oakengine_task_start_time, oakengine_task_subscribe, oakengine_task_title,
|
||||
};
|
||||
use oakengine::timeline::oakengine_sequence_new;
|
||||
use oakengine::undo::oakengine_undo_command_free;
|
||||
use crate::timeline::oakengine_sequence_new;
|
||||
use crate::undo::oakengine_undo_command_free;
|
||||
|
||||
/// OAKTASK module error codes that pass through untranslated.
|
||||
const OAKTASK_E_INVALID: c_int = -80001;
|
||||
@@ -89,14 +89,15 @@ const OAKTASK_E_NOT_FOUND: c_int = -80004;
|
||||
static SERIAL: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// Take the [`SERIAL`] lock, recovering from any poisoning.
|
||||
fn serial() -> std::sync::MutexGuard<'static, ()> {
|
||||
pub(crate) fn serial() -> std::sync::MutexGuard<'static, ()> {
|
||||
SERIAL.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// The oaktask module's debug alive counter (not re-exported by the
|
||||
/// facade; the module crate is a real dependency of the test binary).
|
||||
/// The facade's live task-payload counter (the engine-side replacement
|
||||
/// of the deleted module alive counter; counts owned task payloads and
|
||||
/// borrowed manager-view payloads).
|
||||
fn alive_count() -> c_int {
|
||||
unsafe { oaktask::ffi::task::oaktask_debug_alive_count() }
|
||||
crate::stubs::task::oaktask_debug_alive_count()
|
||||
}
|
||||
|
||||
/// Read a NUL-terminated two-stage buffer as a Rust `String`.
|
||||
@@ -310,7 +311,7 @@ fn empty_handles_are_rejected() {
|
||||
assert!(!params.is_null());
|
||||
// NULL result: the params handle is NOT consumed, so we own it still.
|
||||
assert!(unsafe { oakengine_task_create_export(empty_seq, params) }.is_null());
|
||||
unsafe { oakengine::codec::oakengine_encoding_params_destroy(params) };
|
||||
unsafe { crate::codec::oakengine_encoding_params_destroy(params) };
|
||||
unsafe { drop(Box::from_raw(empty_seq)) };
|
||||
}
|
||||
|
||||
@@ -883,7 +884,7 @@ fn export_task_creation() {
|
||||
handle: CHandle::null(),
|
||||
}));
|
||||
assert!(unsafe { oakengine_task_create_export(empty_seq, params2) }.is_null());
|
||||
unsafe { oakengine::codec::oakengine_encoding_params_destroy(params2) };
|
||||
unsafe { crate::codec::oakengine_encoding_params_destroy(params2) };
|
||||
unsafe { drop(Box::from_raw(empty_seq)) };
|
||||
|
||||
assert_eq!(unsafe { oakengine_task_free(task) }, 0);
|
||||
@@ -893,39 +894,105 @@ fn export_task_creation() {
|
||||
unsafe { oakengine_project_free(project) };
|
||||
}
|
||||
|
||||
/// Running an export requires a real GPU/OpenGL render and a real encoder;
|
||||
/// the test environment's host stubs cannot decode or encode media. The
|
||||
/// creation path above covers the legal input surface; the run itself is
|
||||
/// documented as environment-gated.
|
||||
/// Running an export end-to-end: the facade test clip in a project +
|
||||
/// sequence with full encoding params (mp4 / H.264 / AAC), run
|
||||
/// synchronously through the real CPU render and the statically linked
|
||||
/// FFmpeg encoder, with the output file asserted. The creation path above
|
||||
/// covers the params-ownership matrix; this run proves the run itself
|
||||
/// works in the test environment (formerly environment-gated on GPU).
|
||||
#[test]
|
||||
#[ignore = "export run needs GPU/OpenGL rendering and a real ffmpeg encoder; host stubs cannot encode"]
|
||||
fn export_task_run_ignored_environment_gated() {
|
||||
fn export_task_run_real_encoder() {
|
||||
let _g = serial();
|
||||
common::force_link();
|
||||
|
||||
let media = std::env::temp_dir().join(format!(
|
||||
"oakengine-it-task-export-src-{}.mp4",
|
||||
std::process::id()
|
||||
));
|
||||
let out = std::env::temp_dir().join(format!(
|
||||
"oakengine-it-task-export-run-{}.mp4",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&out);
|
||||
let media_c = std::ffi::CString::new(media.to_string_lossy().into_owned()).unwrap();
|
||||
assert_eq!(
|
||||
unsafe { crate::testmedia::oakengine_testmedia_write_clip(media_c.as_ptr(), 64, 64, 10, 10) },
|
||||
0,
|
||||
"generate the source clip"
|
||||
);
|
||||
|
||||
let project = oakengine_project_create();
|
||||
assert!(!project.is_null());
|
||||
assert_eq!(unsafe { oakengine_project_new(project) }, 0);
|
||||
let footage =
|
||||
unsafe { crate::node::oakengine_project_import_footage(project, media_c.as_ptr()) };
|
||||
assert!(!footage.is_null(), "import the test clip");
|
||||
let seq = unsafe { oakengine_sequence_new(project, c"Export Run".as_ptr()) };
|
||||
assert!(!seq.is_null());
|
||||
|
||||
let params = oakengine_encoding_params_create();
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_encoding_params_set_filename(
|
||||
params,
|
||||
c"/tmp/oakengine_it_task_export_run.mov".as_ptr(),
|
||||
)
|
||||
crate::timeline::oakengine_sequence_set_video_params(seq, 64, 64, 10, 1, 1, 1, 0, 4, 0)
|
||||
},
|
||||
0
|
||||
);
|
||||
let vt = unsafe { crate::timeline::oakengine_sequence_add_track(seq, 0 /* video */) };
|
||||
assert!(vt >= 0);
|
||||
let clip = unsafe {
|
||||
crate::timeline::oakengine_sequence_add_footage_clip_ex(seq, footage, 0 /* video */, vt, 0, 10, 0)
|
||||
};
|
||||
assert!(!clip.is_null(), "place the video clip");
|
||||
unsafe { crate::node::oakengine_footage_free(footage) };
|
||||
|
||||
let params = oakengine_encoding_params_create();
|
||||
assert!(!params.is_null());
|
||||
let out_c = std::ffi::CString::new(out.to_string_lossy().into_owned()).unwrap();
|
||||
assert_eq!(
|
||||
unsafe { oakengine_encoding_params_set_filename(params, out_c.as_ptr()) },
|
||||
0
|
||||
);
|
||||
// mp4 / H.264 / AAC, 64x64 @ 10 fps, 1 s.
|
||||
assert_eq!(
|
||||
unsafe { crate::codec::oakengine_encoding_params_set_format(params, 2) },
|
||||
0
|
||||
);
|
||||
let pod = crate::common::OakVideoParamsPod {
|
||||
width: 64,
|
||||
height: 64,
|
||||
time_base_num: 1,
|
||||
time_base_den: 10,
|
||||
format: 0,
|
||||
pixel_aspect_num: 1,
|
||||
pixel_aspect_den: 1,
|
||||
interlacing: 0,
|
||||
color_range: 0,
|
||||
divider: 1,
|
||||
video_type: 0,
|
||||
premultiplied_alpha: 0,
|
||||
};
|
||||
assert_eq!(
|
||||
unsafe { crate::codec::oakengine_encoding_params_enable_video(params, &pod, 1 /* H264 */) },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { crate::codec::oakengine_encoding_params_enable_audio(params, 48000, 0x3, 0, 12 /* AAC */) },
|
||||
0
|
||||
);
|
||||
let task = unsafe { oakengine_task_create_export(seq, params) };
|
||||
assert!(!task.is_null());
|
||||
assert_eq!(unsafe { oakengine_task_start_sync(task) }, 1);
|
||||
unsafe { oakengine_task_free(task) };
|
||||
|
||||
let meta = std::fs::metadata(&out);
|
||||
assert!(
|
||||
meta.is_ok() && meta.unwrap().len() > 0,
|
||||
"the export run must write the output file"
|
||||
);
|
||||
|
||||
unsafe { free_box::<OakEngineSequence>(seq) };
|
||||
unsafe { oakengine_project_free(project) };
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&out);
|
||||
}
|
||||
|
||||
/// The proxy creator is a documented stub (the oaktask module has no
|
||||
@@ -964,6 +1031,8 @@ fn task_manager_lifecycle() {
|
||||
let handle = oakengine_task_manager_handle();
|
||||
assert!(!handle.is_null());
|
||||
assert_eq!(oakengine_task_manager_handle(), handle);
|
||||
// Other (serialized) tests may have left finished tasks in the queue.
|
||||
crate::stubs::task::oaktask_manager_delete_finished();
|
||||
assert_eq!(oakengine_task_manager_count(), 0);
|
||||
|
||||
// An empty queue has no first task.
|
||||
@@ -1028,3 +1097,137 @@ fn task_manager_lifecycle() {
|
||||
// exposes no delete export), so the count is still 1.
|
||||
assert_eq!(oakengine_task_manager_count(), 1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Load-task result / event subscription (new facade exports)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `oakengine_task_load_take_project` — NULL for NULL/empty tasks and for
|
||||
/// tasks that are not load tasks; after a successful load run the project
|
||||
/// comes back owned (released with `oakengine_project_free`). The success
|
||||
/// path uses a real `.ove` written through `oakengine_project_save`.
|
||||
#[test]
|
||||
fn task_load_take_project_lifecycle() {
|
||||
let _g = serial();
|
||||
common::force_link();
|
||||
|
||||
// NULL / empty task → NULL.
|
||||
assert!(unsafe { oakengine_task_load_take_project(std::ptr::null_mut()) }.is_null());
|
||||
let empty = empty_task_box();
|
||||
assert!(unsafe { oakengine_task_load_take_project(empty) }.is_null());
|
||||
unsafe { reclaim_empty_task_box(empty) };
|
||||
|
||||
// A load task that never ran / failed has no project to take.
|
||||
let failed = unsafe { oakengine_task_create_project_load(c"/no/such/oak/project.ove".as_ptr()) };
|
||||
assert!(!failed.is_null());
|
||||
assert!(unsafe { oakengine_task_load_take_project(failed) }.is_null());
|
||||
assert_eq!(unsafe { oakengine_task_start_sync(failed) }, 0);
|
||||
assert!(unsafe { oakengine_task_load_take_project(failed) }.is_null());
|
||||
assert_eq!(unsafe { oakengine_task_free(failed) }, 0);
|
||||
|
||||
// A non-load task (save) is not a load task → NULL.
|
||||
let project = unsafe { oakengine_project_create() };
|
||||
assert_eq!(unsafe { oakengine_project_new(project) }, 0);
|
||||
let save_path = std::env::temp_dir().join("oakengine_it_task_load_take.ovexml");
|
||||
let save_c = std::ffi::CString::new(save_path.to_str().unwrap()).unwrap();
|
||||
let save_task =
|
||||
unsafe { oakengine_task_create_project_save(project, 0, save_c.as_ptr(), std::ptr::null()) };
|
||||
assert!(!save_task.is_null());
|
||||
assert!(unsafe { oakengine_task_load_take_project(save_task) }.is_null());
|
||||
unsafe { oakengine_task_free(save_task) };
|
||||
|
||||
// Success path: save a real project file, load it through the task and
|
||||
// take the loaded project.
|
||||
let ove_path = std::env::temp_dir().join("oakengine_it_task_load_take.ove");
|
||||
let ove_c = std::ffi::CString::new(ove_path.to_str().unwrap()).unwrap();
|
||||
assert_eq!(unsafe { oakengine_project_save(project, ove_c.as_ptr()) }, 0);
|
||||
let load_task = unsafe { oakengine_task_create_project_load(ove_c.as_ptr()) };
|
||||
assert!(!load_task.is_null());
|
||||
assert_eq!(
|
||||
unsafe { oakengine_task_start_sync(load_task) },
|
||||
1,
|
||||
"loading the project file just saved must succeed"
|
||||
);
|
||||
let loaded = unsafe { oakengine_task_load_take_project(load_task) };
|
||||
assert!(!loaded.is_null(), "a successful load yields the project");
|
||||
// The loaded project is a real, owned handle (name readable, freeable).
|
||||
let mut buf = [0 as c_char; 256];
|
||||
let name_len = unsafe { oakengine_project_filename(loaded, buf.as_mut_ptr(), 256) };
|
||||
assert!(name_len >= 0);
|
||||
unsafe { oakengine_project_free(loaded) };
|
||||
// Taking again returns NULL (the project was taken).
|
||||
assert!(unsafe { oakengine_task_load_take_project(load_task) }.is_null());
|
||||
assert_eq!(unsafe { oakengine_task_free(load_task) }, 0);
|
||||
|
||||
unsafe { oakengine_project_free(project) };
|
||||
let _ = std::fs::remove_file(&save_path);
|
||||
let _ = std::fs::remove_file(&ove_path);
|
||||
}
|
||||
|
||||
/// `oakengine_task_subscribe` — NULL task / NULL callback → facade
|
||||
/// `OAKENGINE_E_INVALID` (-1); a valid subscription returns 0 and delivers
|
||||
/// STARTED (0) + FINISHED (2) to the callback during a sync run (the
|
||||
/// listener is one-shot, so re-running does not re-fire).
|
||||
#[test]
|
||||
fn task_subscribe_lifecycle() {
|
||||
let _g = serial();
|
||||
common::force_link();
|
||||
|
||||
// NULL / empty task and NULL callback → facade E_INVALID.
|
||||
assert_eq!(
|
||||
unsafe { oakengine_task_subscribe(std::ptr::null_mut(), None, std::ptr::null_mut()) },
|
||||
-1
|
||||
);
|
||||
let empty = empty_task_box();
|
||||
assert_eq!(
|
||||
unsafe { oakengine_task_subscribe(empty, None, std::ptr::null_mut()) },
|
||||
-1
|
||||
);
|
||||
unsafe { reclaim_empty_task_box(empty) };
|
||||
|
||||
// The recorder counts STARTED (0) / FINISHED (2) events.
|
||||
static STARTED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
|
||||
static FINISHED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
|
||||
unsafe extern "C" fn recorder(event_id: c_int, _value: f64, _userdata: *mut c_void) {
|
||||
match event_id {
|
||||
0 => {
|
||||
STARTED.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
2 => {
|
||||
FINISHED.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let task = unsafe { oakengine_task_create_project_load(c"".as_ptr()) };
|
||||
assert!(!task.is_null());
|
||||
|
||||
// NULL callback on a real task → E_INVALID (the facade validates it).
|
||||
assert_eq!(
|
||||
unsafe { oakengine_task_subscribe(task, None, std::ptr::null_mut()) },
|
||||
-1
|
||||
);
|
||||
|
||||
// A valid subscription returns 0; a second one replaces the first.
|
||||
assert_eq!(
|
||||
unsafe { oakengine_task_subscribe(task, Some(recorder), std::ptr::null_mut()) },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_task_subscribe(task, Some(recorder), std::ptr::null_mut()) },
|
||||
0
|
||||
);
|
||||
|
||||
// A sync run fires STARTED then FINISHED (the empty-filename load fails,
|
||||
// but the events fire on every run transition).
|
||||
assert_eq!(unsafe { oakengine_task_start_sync(task) }, 0);
|
||||
assert_eq!(STARTED.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
assert_eq!(FINISHED.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
|
||||
// The subscription is one-shot: a second run does not re-fire.
|
||||
assert_eq!(unsafe { oakengine_task_start_sync(task) }, 0);
|
||||
assert_eq!(STARTED.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
|
||||
assert_eq!(unsafe { oakengine_task_free(task) }, 0);
|
||||
}
|
||||
@@ -38,15 +38,14 @@
|
||||
//! `oakengine_undo_group_abort` used to leave its executed children
|
||||
//! un-undone.
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_char, c_void};
|
||||
use std::sync::atomic::{AtomicI32, AtomicUsize, Ordering};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use oakengine::handle::{CHandle, OakEngineClipboard};
|
||||
use oakengine::undo::{
|
||||
use crate::handle::{CHandle, OakEngineClipboard};
|
||||
use crate::undo::{
|
||||
oakengine_undo_can_redo, oakengine_undo_can_undo, oakengine_undo_clear,
|
||||
oakengine_undo_command_create, oakengine_undo_command_create_multi,
|
||||
oakengine_undo_command_free, oakengine_undo_command_is_done,
|
||||
@@ -128,7 +127,7 @@ unsafe extern "C" fn variants_free(_ud: *mut c_void) {
|
||||
unsafe extern "C" fn lifecycle_free_userdata(ud: *mut c_void) {
|
||||
LIFECYCLE_FREED_PTR.store(ud as usize, Ordering::SeqCst);
|
||||
LIFECYCLE_FREE.fetch_add(1, Ordering::SeqCst);
|
||||
drop(Box::from_raw(ud as *mut u64));
|
||||
unsafe { drop(Box::from_raw(ud as *mut u64)) };
|
||||
}
|
||||
|
||||
/// Child redo/undo callbacks that log their id (encoded in userdata) — used
|
||||
@@ -536,27 +535,26 @@ fn free_contracts() {
|
||||
undo: None,
|
||||
free_fn: Some(mod_free_cb),
|
||||
};
|
||||
let mut h =
|
||||
unsafe { oakundo::ffi::command::oakundo_command_init(&vtable, std::ptr::null_mut()) };
|
||||
let mut h = oakundo::undocommand::command_init(&vtable, std::ptr::null_mut());
|
||||
assert!(!h.ctx.is_null());
|
||||
unsafe { oakundo::ffi::command::oakundo_command_free(&mut h) };
|
||||
oakundo::undocommand::command_free(&mut h);
|
||||
assert_eq!(MOD_FREE.load(Ordering::SeqCst), 1);
|
||||
assert!(h.ctx.is_null());
|
||||
unsafe { oakundo::ffi::command::oakundo_command_free(&mut h) };
|
||||
oakundo::undocommand::command_free(&mut h);
|
||||
assert_eq!(MOD_FREE.load(Ordering::SeqCst), 1);
|
||||
|
||||
// Module stack handle: double free is a no-op; NULL value and NULL
|
||||
// pointer are no-ops too.
|
||||
let mut s = unsafe { oakundo::ffi::undostack::oakundo_undostack_init() };
|
||||
let mut s = oakundo::undostack::undostack_init();
|
||||
assert!(!s.ctx.is_null());
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_free(&mut s) };
|
||||
oakundo::undostack::undostack_free(&mut s);
|
||||
assert!(s.ctx.is_null());
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_free(&mut s) };
|
||||
oakundo::undostack::undostack_free(&mut s);
|
||||
|
||||
let mut null_h = CHandle::null();
|
||||
unsafe { oakundo::ffi::command::oakundo_command_free(&mut null_h) };
|
||||
unsafe { oakundo::ffi::command::oakundo_command_free(std::ptr::null_mut()) };
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_free(std::ptr::null_mut()) };
|
||||
oakundo::undocommand::command_free(&mut null_h);
|
||||
oakundo::undocommand::command_free(std::ptr::null_mut());
|
||||
oakundo::undostack::undostack_free(std::ptr::null_mut());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -0,0 +1,51 @@
|
||||
// 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/>.
|
||||
|
||||
//! Smoke-test that every module crate is present and callable through
|
||||
//! its direct Rust API (single-lib unification; the former version
|
||||
//! referenced the deleted `*::ffi` C ABI exports).
|
||||
|
||||
use super::common;
|
||||
|
||||
#[test]
|
||||
fn all_module_crates_link() {
|
||||
// oakundo: fresh stack, refcount 1.
|
||||
let stack = oakundo::undostack::undostack_init();
|
||||
assert!(!stack.ctx.is_null());
|
||||
|
||||
// oakcommon: an int config read with fallback.
|
||||
let v = oakcommon::configstore::ConfigStore::instance().get_int(None, "no-such-key", 42);
|
||||
assert_eq!(v, 42);
|
||||
|
||||
// oakcodec: the encoding-format table is non-empty (count > 0).
|
||||
let n = oakcodec::exportformat::Format::get_name(oakcodec::exportformat::Format::MPEG4Video)
|
||||
.len();
|
||||
assert!(n > 0);
|
||||
|
||||
// oakaudio: a fresh processor reports closed.
|
||||
let p = oakaudio::processor::Processor::init();
|
||||
assert!(!p.is_open().unwrap());
|
||||
|
||||
// oakrender: value-typed entry points resolve (no backend init here —
|
||||
// a GPU backend may not exist on the test host).
|
||||
let render_anchor = oakrender::manager::RenderManager::init as usize;
|
||||
assert!(render_anchor > 0);
|
||||
|
||||
// oakplugin: the host cache is process-global; other tests in this
|
||||
// binary may have scanned plugin bundles before this runs, so the
|
||||
// smoke only requires a working count (0 before any scan).
|
||||
let _ = oakplugin::host::Host::global().cache.count();
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// 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/>.
|
||||
|
||||
//! Unit-test aggregation for the oakengine facade (single-lib unification).
|
||||
//!
|
||||
//! The facade's crate-type is cdylib-only (no rlib), so the former
|
||||
//! `tests/*.rs` integration tests cannot link `oakengine` as a crate.
|
||||
//! They moved here (`src/test_support/`, pulled in by `src/lib.rs` under
|
||||
//! `#[cfg(test)]`) and run as unit tests against `crate::*` instead of
|
||||
//! `oakengine::*`. The old `#[path = "common/mod.rs"] mod common;` include
|
||||
//! is replaced by the single [`common`] declaration below — the
|
||||
//! `oakcore_*` mock symbols it defines may exist only once per binary.
|
||||
//!
|
||||
//! The node/timeline/render-graph families (and the graph-op tests that
|
||||
//! built fixtures through the deleted handle-based module C ABIs) are
|
||||
//! part of the pending domain-model redesign; their test files were
|
||||
//! deleted with this migration (see the migration report).
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
pub mod common;
|
||||
|
||||
mod audio;
|
||||
mod codec;
|
||||
mod common_smoke;
|
||||
mod it_audio;
|
||||
mod it_codec;
|
||||
mod it_common;
|
||||
mod it_export;
|
||||
mod it_plugin;
|
||||
mod it_task;
|
||||
mod it_undo;
|
||||
mod linkage;
|
||||
mod node;
|
||||
mod plugin;
|
||||
mod render;
|
||||
mod task;
|
||||
mod undo;
|
||||
@@ -23,12 +23,11 @@
|
||||
//! the failure-path tests only exercise non-mutating calls and run in
|
||||
//! parallel.
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use oakengine::node::{
|
||||
use crate::node::{
|
||||
oakengine_footage_borrow, oakengine_footage_last_error, oakengine_footage_probe,
|
||||
oakengine_node_connect, oakengine_node_disconnect, oakengine_node_factory_create_from_id,
|
||||
oakengine_node_factory_id_count, oakengine_node_factory_name_from_id,
|
||||
@@ -49,7 +48,7 @@ const TYPE_ID_SOLID: &str = "org.olivevideoeditor.Olive.solidgenerator";
|
||||
|
||||
/// Read a two-stage facade string into a Rust String.
|
||||
unsafe fn read_buf(buf: &mut [c_char]) -> String {
|
||||
std::ffi::CStr::from_ptr(buf.as_ptr())
|
||||
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
@@ -60,13 +59,13 @@ unsafe fn read_buf(buf: &mut [c_char]) -> String {
|
||||
/// time (the facade's own undo family uses the multi/redo/free
|
||||
/// variants), so the linker would drop it.
|
||||
fn force_oakundo_command_link() -> usize {
|
||||
// The oaknode serializer bridge resolves the oakcommon XML writer and
|
||||
// the oakundo command factory at runtime via dlsym; nothing else
|
||||
// references them at link time.
|
||||
// The oaknode serializer bridge calls the oakcommon XML reader/writer
|
||||
// and the oakundo command factory through their Rust API; nothing else
|
||||
// references those codegen units at link time, so anchor them here.
|
||||
let fns: [usize; 3] = [
|
||||
oakundo::ffi::command::oakundo_command_init as *const () as usize,
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_writer_init as *const () as usize,
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_reader_init as *const () as usize,
|
||||
oakundo::undocommand::command_init as *const () as usize,
|
||||
oakcommon::xmlutils::XmlWriter::new as *const () as usize,
|
||||
oakcommon::xmlutils::XmlReader::new as *const () as usize,
|
||||
];
|
||||
fns.iter().sum()
|
||||
}
|
||||
@@ -82,7 +81,7 @@ fn float_value(x: f64) -> OakNodeValue {
|
||||
}
|
||||
|
||||
/// The index of the first project node whose type id matches `id`, or -1.
|
||||
unsafe fn find_node(project: *mut oakengine::handle::OakEngineProject, id: &str) -> c_int {
|
||||
unsafe fn find_node(project: *mut crate::handle::OakEngineProject, id: &str) -> c_int {
|
||||
let count = unsafe { oakengine_project_node_count(project) };
|
||||
for i in 0..count {
|
||||
let node = unsafe { oakengine_project_node_at(project, i) };
|
||||
@@ -16,10 +16,9 @@
|
||||
|
||||
//! Smoke tests for the plugin family (`engine/include/oakengine/plugin.h`).
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use oakengine::plugin::{
|
||||
use crate::plugin::{
|
||||
oakengine_plugin_load_plugins, oakengine_plugin_node_push_button_clicked,
|
||||
oakengine_plugin_set_active_viewer_provider, oakengine_plugin_set_progress_reporter_factory,
|
||||
};
|
||||
@@ -29,7 +28,7 @@ use oakengine::plugin::{
|
||||
fn provider_registration() {
|
||||
unsafe extern "C" fn viewer(
|
||||
_userdata: *mut std::ffi::c_void,
|
||||
) -> *mut oakengine::handle::OakEngineNode {
|
||||
) -> *mut crate::handle::OakEngineNode {
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
assert_eq!(
|
||||
@@ -21,12 +21,11 @@
|
||||
//! argument paths (real rendering needs the deferred node family plus an
|
||||
//! initialized render manager).
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_char, c_double};
|
||||
|
||||
use oakengine::render::{
|
||||
use crate::render::{
|
||||
oakengine_color_last_error, oakengine_color_manager_get_config_filename,
|
||||
oakengine_color_processor_convert_color, oakengine_color_processor_create,
|
||||
oakengine_color_processor_free, oakengine_color_processor_is_valid,
|
||||
@@ -56,8 +55,8 @@ fn render_manager_not_initialized() {
|
||||
unsafe { oakengine_render_cache_set_multicam_node(std::ptr::null_mut()) },
|
||||
-70002
|
||||
);
|
||||
// The requested-backend stub reports 0 (k_open_gl) when unavailable.
|
||||
assert_eq!(unsafe { oakengine_render_manager_requested_backend() }, 0);
|
||||
// Without a manager the requested backend is -1 (no manager up).
|
||||
assert_eq!(unsafe { oakengine_render_manager_requested_backend() }, -1);
|
||||
}
|
||||
|
||||
/// Renderer lifecycle: NULL sequence is rejected; mode validation.
|
||||
@@ -28,16 +28,15 @@
|
||||
//! and project-mutating tests are each a single test function; the
|
||||
//! handle/accessor tests touch neither and run in parallel.
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
|
||||
use oakengine::node::{
|
||||
use crate::node::{
|
||||
oakengine_node_free, oakengine_project_create, oakengine_project_free, oakengine_project_new,
|
||||
oakengine_project_root, oakengine_project_set_filename,
|
||||
};
|
||||
use oakengine::task::{
|
||||
use crate::task::{
|
||||
oakengine_cli_task_dialog_run, oakengine_task_cancel, oakengine_task_create_export,
|
||||
oakengine_task_create_project_import, oakengine_task_create_project_load,
|
||||
oakengine_task_create_project_load_otio, oakengine_task_create_project_save,
|
||||
@@ -208,6 +207,7 @@ fn cli_dialog_runs_task_sync() {
|
||||
/// test because `oakengine_project_new` touches the global undo stack.
|
||||
#[test]
|
||||
fn project_task_lifecycle() {
|
||||
let _g = super::it_task::serial();
|
||||
common::force_link();
|
||||
|
||||
let project = oakengine_project_create();
|
||||
@@ -302,9 +302,11 @@ fn project_task_lifecycle() {
|
||||
/// `manager_first` and can be cancelled.
|
||||
#[test]
|
||||
fn task_manager_lifecycle() {
|
||||
let _g = super::it_task::serial();
|
||||
common::force_link();
|
||||
|
||||
assert!(!unsafe { oakengine_task_manager_handle() }.is_null());
|
||||
crate::stubs::task::oaktask_manager_delete_finished();
|
||||
assert_eq!(unsafe { oakengine_task_manager_count() }, 0);
|
||||
|
||||
let task = unsafe { oakengine_task_create_project_load(c"/no/such/oak/project.ove".as_ptr()) };
|
||||
@@ -21,13 +21,12 @@
|
||||
//! function; the command-lifecycle tests (no stack access) can run in
|
||||
//! parallel.
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
|
||||
use oakengine::undo::{
|
||||
use crate::undo::{
|
||||
oakengine_undo_can_redo, oakengine_undo_can_undo, oakengine_undo_clear,
|
||||
oakengine_undo_command_create, oakengine_undo_command_create_multi,
|
||||
oakengine_undo_command_free, oakengine_undo_command_is_done,
|
||||
@@ -30,8 +30,12 @@ use std::ffi::{c_char, c_int, c_void};
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::bridge::undo::OakUndoCommandVtable;
|
||||
use crate::bridge::{audio as a, common as c, node as n, timeline as tl, undo as u};
|
||||
use oakundo::undocommand::OakUndoCommandVtable;
|
||||
use crate::stubs::common as c;
|
||||
use crate::stubs::node as n;
|
||||
use crate::stubs::timeline as tl;
|
||||
use crate::stubs::audio as a;
|
||||
use oakundo::undocommand as u;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::handle::{
|
||||
box_handle, free_box, guard, guard_int, guard_ptr, guard_void, read_cstr, string_result, unbox,
|
||||
@@ -215,13 +219,13 @@ unsafe fn push_multi_commands(children: &[CHandle], name: &str) -> Result<()> {
|
||||
if children.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let multi = u::oakundo_command_init_multi();
|
||||
let multi = u::command_init_multi();
|
||||
if multi.is_null() {
|
||||
return Err(Error::Failed("multi command allocation failed".into()));
|
||||
}
|
||||
let multi_box = box_handle::<OakEngineClipboard>(multi);
|
||||
for child in children {
|
||||
let rc = u::oakundo_command_multi_add_child(multi, *child);
|
||||
let rc = u::command_multi_add_child(multi, *child);
|
||||
if rc != 0 {
|
||||
free_box(multi_box);
|
||||
return Err(Error::Module(rc));
|
||||
@@ -257,7 +261,7 @@ unsafe fn vtable_command(
|
||||
undo: Some(undo),
|
||||
free_fn: Some(free_fn),
|
||||
};
|
||||
let cmd = u::oakundo_command_init(&vtable, data);
|
||||
let cmd = u::command_init(&vtable, data);
|
||||
if cmd.is_null() {
|
||||
free_fn(data);
|
||||
return Err(Error::Failed("undo command allocation failed".into()));
|
||||
@@ -1663,23 +1667,18 @@ pub unsafe extern "C" fn oakengine_sequence_add_track(
|
||||
Error::from_module(n::oaknode_sequence_get_track_list(
|
||||
seq, track_type, &mut list,
|
||||
))?;
|
||||
// The module's TimelineAddTrackCommand redo only appends the
|
||||
// sequence's track array element; it never registers the created
|
||||
// track node in the list's `tracks`, so the count/at queries would
|
||||
// not observe it. Register a track node live as compensation
|
||||
// (documented deviation; the array element stays undoable).
|
||||
// The module's TimelineAddTrackCommand redo appends the created
|
||||
// track to the list (with its back-reference and index), so the
|
||||
// count/at queries observe it directly — no separate registration
|
||||
// needed (the old module redo only grew the sequence's track-input
|
||||
// array element, which is why the previous facade registered a
|
||||
// second live track as compensation).
|
||||
let cmd = tl::oaktimeline_add_track_command(list);
|
||||
if cmd.is_null() {
|
||||
release_handle(list);
|
||||
return Err(Error::Failed("add track command failed".into()));
|
||||
}
|
||||
push_command(cmd, "Add Track")?;
|
||||
let track = n::oaknode_track_create(track_type);
|
||||
if track.is_null() {
|
||||
release_handle(list);
|
||||
return Err(Error::Failed("track creation failed".into()));
|
||||
}
|
||||
Error::from_module(n::oaknode_tracklist_add_track(list, track))?;
|
||||
let mut count: c_int = 0;
|
||||
Error::from_module(n::oaknode_sequence_get_track_count(
|
||||
seq, track_type, &mut count,
|
||||
@@ -4647,7 +4646,7 @@ pub unsafe extern "C" fn oakengine_marker_commit_time(
|
||||
push_command(cmd, "Move Marker")
|
||||
} else {
|
||||
let parent = unbox(command.cast::<OakEngineClipboard>())?;
|
||||
let rc = u::oakundo_command_multi_add_child(parent, cmd);
|
||||
let rc = u::command_multi_add_child(parent, cmd);
|
||||
Error::from_module(rc)
|
||||
}
|
||||
})
|
||||
@@ -4786,7 +4785,7 @@ pub unsafe extern "C" fn oakengine_marker_set_properties(
|
||||
} else {
|
||||
let parent = unbox(command.cast::<OakEngineClipboard>())?;
|
||||
for child in &children {
|
||||
let rc = u::oakundo_command_multi_add_child(parent, *child);
|
||||
let rc = u::command_multi_add_child(parent, *child);
|
||||
if rc != 0 {
|
||||
return Err(Error::Module(rc));
|
||||
}
|
||||
@@ -4924,7 +4923,7 @@ pub unsafe extern "C" fn oakengine_workarea_set_range_undoable(
|
||||
push_command(cmd, "Set Workarea Range")
|
||||
} else {
|
||||
let parent = unbox(command.cast::<OakEngineClipboard>())?;
|
||||
let rc = u::oakundo_command_multi_add_child(parent, cmd);
|
||||
let rc = u::command_multi_add_child(parent, cmd);
|
||||
Error::from_module(rc)
|
||||
}
|
||||
})
|
||||
@@ -4948,7 +4947,7 @@ pub unsafe extern "C" fn oakengine_workarea_set_enabled_undoable(
|
||||
push_command(cmd, "Set Workarea Enabled")
|
||||
} else {
|
||||
let parent = unbox(command.cast::<OakEngineClipboard>())?;
|
||||
let rc = u::oakundo_command_multi_add_child(parent, cmd);
|
||||
let rc = u::command_multi_add_child(parent, cmd);
|
||||
Error::from_module(rc)
|
||||
}
|
||||
})
|
||||
@@ -5726,16 +5725,9 @@ pub unsafe extern "C" fn oakengine_sequence_add_default_nodes(
|
||||
}
|
||||
let children = [vcmd, acmd];
|
||||
push_multi_commands(&children, "Add Default Nodes")?;
|
||||
// Module-gap compensation: register one live track per type (see
|
||||
// `oakengine_sequence_add_track`).
|
||||
let vtrack = n::oaknode_track_create(TRACK_TYPE_VIDEO);
|
||||
let atrack = n::oaknode_track_create(TRACK_TYPE_AUDIO);
|
||||
if !vtrack.is_null() {
|
||||
n::oaknode_tracklist_add_track(video_list, vtrack);
|
||||
}
|
||||
if !atrack.is_null() {
|
||||
n::oaknode_tracklist_add_track(audio_list, atrack);
|
||||
}
|
||||
// The two commands' redos registered their tracks in the video and
|
||||
// audio lists already (see `oakengine_sequence_add_track`); no
|
||||
// separate registration is needed.
|
||||
release_handle(video_list);
|
||||
release_handle(audio_list);
|
||||
Ok(())
|
||||
|
||||
@@ -32,7 +32,15 @@
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use crate::bridge::undo as u;
|
||||
use oakundo::undocommand::{
|
||||
command_free, command_init, command_init_multi, command_multi_add_child,
|
||||
command_multi_child, command_multi_child_count, command_redo_now, command_undo_now,
|
||||
};
|
||||
use oakundo::undostack::{
|
||||
undostack_can_redo, undostack_can_undo, undostack_clear, undostack_command_is_done,
|
||||
undostack_command_text, undostack_count, undostack_index, undostack_init, undostack_jump,
|
||||
undostack_push, undostack_push_pre_executed,
|
||||
};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::handle::{box_handle, free_box, guard, guard_void, unbox, CHandle, OakEngineClipboard};
|
||||
|
||||
@@ -40,7 +48,7 @@ use crate::handle::{box_handle, free_box, guard, guard_void, unbox, CHandle, Oak
|
||||
/// lazily and kept for the process lifetime.
|
||||
fn global_stack() -> &'static CHandle {
|
||||
static STACK: OnceLock<CHandle> = OnceLock::new();
|
||||
STACK.get_or_init(|| unsafe { u::oakundo_undostack_init() })
|
||||
STACK.get_or_init(|| unsafe { undostack_init() })
|
||||
}
|
||||
|
||||
/// Stable opaque token for `oakengine_undo_handle`: the module stack's
|
||||
@@ -84,11 +92,11 @@ pub(crate) unsafe fn push_or_run(
|
||||
// happen on the still-owned handle FIRST — the group takes the
|
||||
// already-done command (C++ semantics: add_child + redo_now, net
|
||||
// effect identical for the group's reverse-order undo).
|
||||
let rc = unsafe { u::oakundo_command_redo_now(cmd) };
|
||||
let rc = unsafe { command_redo_now(cmd) };
|
||||
if rc != 0 {
|
||||
return Err(Error::Module(rc));
|
||||
}
|
||||
let rc = unsafe { u::oakundo_command_multi_add_child(group.multi, cmd) };
|
||||
let rc = unsafe { command_multi_add_child(group.multi, cmd) };
|
||||
drop(g);
|
||||
unsafe { free_box(command_box) };
|
||||
return if rc == 0 {
|
||||
@@ -106,7 +114,7 @@ pub(crate) unsafe fn push_or_run(
|
||||
} else {
|
||||
label.as_ptr() as *const c_char
|
||||
};
|
||||
let rc = unsafe { u::oakundo_undostack_push(stack, cmd, label_ptr) };
|
||||
let rc = unsafe { undostack_push(stack, cmd, label_ptr) };
|
||||
if rc == 0 {
|
||||
// Stack took a reference; release ours by freeing the box.
|
||||
unsafe { free_box(command_box) };
|
||||
@@ -147,7 +155,7 @@ pub extern "C" fn oakengine_undo_group_begin(name: *const c_char) -> c_int {
|
||||
if g.is_some() {
|
||||
return Err(Error::State);
|
||||
}
|
||||
let multi = unsafe { u::oakundo_command_init_multi() };
|
||||
let multi = unsafe { command_init_multi() };
|
||||
if multi.is_null() {
|
||||
return Err(Error::Failed("undo group allocation failed".into()));
|
||||
}
|
||||
@@ -181,9 +189,9 @@ pub extern "C" fn oakengine_undo_group_end() -> c_int {
|
||||
// the stack took (or destroyed) the command; release our own
|
||||
// reference to the multi handle.
|
||||
let stack = *global_stack();
|
||||
let rc = unsafe { u::oakundo_undostack_push_pre_executed(stack, multi, name_ptr) };
|
||||
let rc = unsafe { undostack_push_pre_executed(stack, multi, name_ptr) };
|
||||
let mut multi_handle = multi;
|
||||
unsafe { u::oakundo_command_free(&mut multi_handle) };
|
||||
unsafe { command_free(&mut multi_handle) };
|
||||
if rc == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -206,33 +214,33 @@ pub extern "C" fn oakengine_undo_group_abort() -> c_int {
|
||||
// insertion order (mirroring the multi's reverse-order undo), each
|
||||
// through its own borrowed handle.
|
||||
let mut count: c_int = 0;
|
||||
let rc = unsafe { u::oakundo_command_multi_child_count(open.multi, &mut count) };
|
||||
let rc = unsafe { command_multi_child_count(open.multi, &mut count) };
|
||||
if rc != 0 {
|
||||
let mut multi = open.multi;
|
||||
unsafe { u::oakundo_command_free(&mut multi) };
|
||||
unsafe { command_free(&mut multi) };
|
||||
return Err(Error::Module(rc));
|
||||
}
|
||||
for i in (0..count).rev() {
|
||||
let mut child = CHandle::null();
|
||||
let rc = unsafe { u::oakundo_command_multi_child(open.multi, i, &mut child) };
|
||||
let rc = unsafe { command_multi_child(open.multi, i, &mut child) };
|
||||
if rc != 0 {
|
||||
let mut multi = open.multi;
|
||||
unsafe { u::oakundo_command_free(&mut multi) };
|
||||
unsafe { command_free(&mut multi) };
|
||||
return Err(Error::Module(rc));
|
||||
}
|
||||
let rc = unsafe { u::oakundo_command_undo_now(child) };
|
||||
let rc = unsafe { command_undo_now(child) };
|
||||
// The child handle is borrowed (owns:false): release only its
|
||||
// shell — the child value lives on in the multi until the multi
|
||||
// itself is freed below.
|
||||
unsafe { u::oakundo_command_free(&mut child) };
|
||||
unsafe { command_free(&mut child) };
|
||||
if rc != 0 {
|
||||
let mut multi = open.multi;
|
||||
unsafe { u::oakundo_command_free(&mut multi) };
|
||||
unsafe { command_free(&mut multi) };
|
||||
return Err(Error::Module(rc));
|
||||
}
|
||||
}
|
||||
let mut multi = open.multi;
|
||||
unsafe { u::oakundo_command_free(&mut multi) };
|
||||
unsafe { command_free(&mut multi) };
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -243,7 +251,7 @@ pub extern "C" fn oakengine_undo_group_abort() -> c_int {
|
||||
pub unsafe extern "C" fn oakengine_undo_command_redo_now(command: *mut c_void) -> c_int {
|
||||
guard(|| unsafe {
|
||||
let cmd = unbox(command.cast::<OakEngineClipboard>())?;
|
||||
Error::from_module(u::oakundo_command_redo_now(cmd))
|
||||
Error::from_module(command_redo_now(cmd))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -253,7 +261,7 @@ pub unsafe extern "C" fn oakengine_undo_command_redo_now(command: *mut c_void) -
|
||||
pub unsafe extern "C" fn oakengine_undo_command_undo_now(command: *mut c_void) -> c_int {
|
||||
guard(|| unsafe {
|
||||
let cmd = unbox(command.cast::<OakEngineClipboard>())?;
|
||||
Error::from_module(u::oakundo_command_undo_now(cmd))
|
||||
Error::from_module(command_undo_now(cmd))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -274,12 +282,12 @@ pub unsafe extern "C" fn oakengine_undo_command_create(
|
||||
) -> *mut c_void {
|
||||
crate::handle::guard_ptr(|| unsafe {
|
||||
let _ = crate::handle::read_cstr(name);
|
||||
let vtable = crate::bridge::undo::OakUndoCommandVtable {
|
||||
let vtable = oakundo::undocommand::OakUndoCommandVtable {
|
||||
redo,
|
||||
undo,
|
||||
free_fn,
|
||||
};
|
||||
let cmd = u::oakundo_command_init(&vtable, userdata);
|
||||
let cmd = command_init(&vtable, userdata);
|
||||
if cmd.is_null() {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
@@ -292,7 +300,7 @@ pub unsafe extern "C" fn oakengine_undo_command_create(
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakengine_undo_command_create_multi() -> *mut c_void {
|
||||
crate::handle::guard_ptr(|| {
|
||||
let cmd = unsafe { u::oakundo_command_init_multi() };
|
||||
let cmd = unsafe { command_init_multi() };
|
||||
if cmd.is_null() {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
@@ -313,7 +321,7 @@ pub unsafe extern "C" fn oakengine_undo_command_multi_add_child(
|
||||
}
|
||||
let m = unbox(multi.cast::<OakEngineClipboard>())?;
|
||||
let c = unbox(child.cast::<OakEngineClipboard>())?;
|
||||
let rc = u::oakundo_command_multi_add_child(m, c);
|
||||
let rc = command_multi_add_child(m, c);
|
||||
free_box(child.cast::<OakEngineClipboard>());
|
||||
if rc == 0 {
|
||||
Ok(())
|
||||
@@ -329,7 +337,7 @@ pub unsafe extern "C" fn oakengine_undo_command_multi_child_count(multi: *mut c_
|
||||
crate::handle::guard_int(|| unsafe {
|
||||
let m = unbox(multi.cast::<OakEngineClipboard>())?;
|
||||
let mut count: c_int = 0;
|
||||
Error::from_module(u::oakundo_command_multi_child_count(m, &mut count))?;
|
||||
Error::from_module(command_multi_child_count(m, &mut count))?;
|
||||
Ok(count)
|
||||
})
|
||||
}
|
||||
@@ -347,7 +355,7 @@ pub unsafe extern "C" fn oakengine_undo_command_free(command: *mut c_void) {
|
||||
pub extern "C" fn oakengine_undo_count() -> i64 {
|
||||
crate::handle::guard_i64(|| unsafe {
|
||||
let mut count: i64 = 0;
|
||||
Error::from_module(u::oakundo_undostack_count(*global_stack(), &mut count))?;
|
||||
Error::from_module(undostack_count(*global_stack(), &mut count))?;
|
||||
Ok(count)
|
||||
})
|
||||
}
|
||||
@@ -357,7 +365,7 @@ pub extern "C" fn oakengine_undo_count() -> i64 {
|
||||
pub extern "C" fn oakengine_undo_index() -> i64 {
|
||||
crate::handle::guard_i64(|| unsafe {
|
||||
let mut index: i64 = 0;
|
||||
Error::from_module(u::oakundo_undostack_index(*global_stack(), &mut index))?;
|
||||
Error::from_module(undostack_index(*global_stack(), &mut index))?;
|
||||
Ok(index)
|
||||
})
|
||||
}
|
||||
@@ -375,7 +383,7 @@ pub unsafe extern "C" fn oakengine_undo_command_text(
|
||||
// module return value is returned verbatim (guarded against panic),
|
||||
// converted to the engine's length-excluding-NUL convention.
|
||||
crate::handle::guard_int(|| unsafe {
|
||||
let rc = u::oakundo_undostack_command_text(*global_stack(), row, buf, buf_size);
|
||||
let rc = undostack_command_text(*global_stack(), row, buf, buf_size);
|
||||
if rc < 0 {
|
||||
Err(Error::Module(rc))
|
||||
} else {
|
||||
@@ -390,7 +398,7 @@ pub unsafe extern "C" fn oakengine_undo_command_text(
|
||||
pub extern "C" fn oakengine_undo_command_is_done(row: i64) -> c_int {
|
||||
crate::handle::guard_int(|| unsafe {
|
||||
let mut value: c_int = 0;
|
||||
Error::from_module(u::oakundo_undostack_command_is_done(
|
||||
Error::from_module(undostack_command_is_done(
|
||||
*global_stack(),
|
||||
row,
|
||||
&mut value,
|
||||
@@ -403,14 +411,14 @@ pub extern "C" fn oakengine_undo_command_is_done(row: i64) -> c_int {
|
||||
/// `index`.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakengine_undo_jump(index: i64) -> c_int {
|
||||
guard(|| unsafe { Error::from_module(u::oakundo_undostack_jump(*global_stack(), index)) })
|
||||
guard(|| unsafe { Error::from_module(undostack_jump(*global_stack(), index)) })
|
||||
}
|
||||
|
||||
/// `oakengine_undo_clear` — delete all commands and push the fresh
|
||||
/// "New/Open Project" empty command.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakengine_undo_clear() -> c_int {
|
||||
guard(|| unsafe { Error::from_module(u::oakundo_undostack_clear(*global_stack())) })
|
||||
guard(|| unsafe { Error::from_module(undostack_clear(*global_stack())) })
|
||||
}
|
||||
|
||||
/// `oakengine_undo_update_actions` — no-op: the QAction members were
|
||||
@@ -426,7 +434,7 @@ pub extern "C" fn oakengine_undo_update_actions() -> c_int {
|
||||
pub extern "C" fn oakengine_undo_can_undo() -> c_int {
|
||||
crate::handle::guard_int(|| unsafe {
|
||||
let mut value: c_int = 0;
|
||||
Error::from_module(u::oakundo_undostack_can_undo(*global_stack(), &mut value))?;
|
||||
Error::from_module(undostack_can_undo(*global_stack(), &mut value))?;
|
||||
Ok(value)
|
||||
})
|
||||
}
|
||||
@@ -436,7 +444,7 @@ pub extern "C" fn oakengine_undo_can_undo() -> c_int {
|
||||
pub extern "C" fn oakengine_undo_can_redo() -> c_int {
|
||||
crate::handle::guard_int(|| unsafe {
|
||||
let mut value: c_int = 0;
|
||||
Error::from_module(u::oakundo_undostack_can_redo(*global_stack(), &mut value))?;
|
||||
Error::from_module(undostack_can_redo(*global_stack(), &mut value))?;
|
||||
Ok(value)
|
||||
})
|
||||
}
|
||||
|
||||
+406
-217
@@ -14,61 +14,50 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! The render worker — the Rust port of `engine/src/capi/worker.cpp`
|
||||
//! behind the frozen C ABI in `engine/include/oakengine/worker.h`.
|
||||
//! The render worker — the Rust port of `engine/src/capi/worker.cpp`,
|
||||
//! owned by the oakengine facade and exported through the frozen
|
||||
//! `oakengine_worker_*` C ABI (`engine/include/oakengine/worker.h`); the
|
||||
//! `oak-worker` binary is a pure C-ABI consumer that links the built
|
||||
//! `liboakengine` dylib.
|
||||
//!
|
||||
//! Like the C++ side, this is where the worker's whole runtime lives:
|
||||
//!
|
||||
//! - **Backend selection.** [`create_renderer`] initializes the render
|
||||
//! backend through the oakrender module C ABI
|
||||
//! (`oakrender_display_renderer_create_dynamic` + `_init`), falling
|
||||
//! back to the direct OpenGL renderer exactly like the C++
|
||||
//! `create_renderer()` chain. The worker executable itself never
|
||||
//! touches a renderer — it is a thin shell calling
|
||||
//! [`oakengine_worker_main`].
|
||||
//! - **Backend selection.** [`Renderer::create`] initializes the render
|
||||
//! backend through the oakrender crate's direct Rust API
|
||||
//! ([`oakrender::backend::DisplayRenderer`]), falling back to the
|
||||
//! direct OpenGL renderer exactly like the C++ `create_renderer()`
|
||||
//! chain.
|
||||
//! - **The session.** [`WorkerSession`] holds the renderer, the
|
||||
//! shared-memory frame-slot pools ([`crate::ipc::FrameSlotPool`]) and
|
||||
//! the shutdown flag, and answers one NDJSON control message at a time.
|
||||
//! - **The main loop.** [`worker_main`] (and its C ABI wrapper
|
||||
//! [`oakengine_worker_main`]) creates the session, loads the runtime
|
||||
//! config, writes the startup handshake, and serves the stdin/stdout
|
||||
//! NDJSON loop until a `shutdown` message or EOF.
|
||||
//! - **The main loop.** [`worker_main`] creates the session, loads the
|
||||
//! runtime config, writes the startup handshake, and serves the
|
||||
//! stdin/stdout NDJSON loop until a `shutdown` message or EOF.
|
||||
//!
|
||||
//! The control-plane protocol is the same NDJSON the C++ worker speaks
|
||||
//! (`engine/render/ipc/ipcmessage.cpp`): one compact JSON object per line,
|
||||
//! `"type"`-dispatched, with `handshake` carrying the shared-memory
|
||||
//! geometry the worker attaches to via the real [`crate::ipc`] transport.
|
||||
//! `load_graph`/`render_frame` reproduce the C++ validation and then
|
||||
//! answer with the documented "not yet available" errors (the oaknode
|
||||
//! graph crate is still a skeleton).
|
||||
//! `"type"`-dispatched ([`crate::ipc`]), with `handshake` carrying the
|
||||
//! shared-memory geometry the worker attaches to via the real
|
||||
//! [`crate::ipc`] transport. `load_graph`/`render_frame` reproduce the
|
||||
//! C++ validation and then answer with the documented "not yet available"
|
||||
//! errors (the oaknode graph crate is still a skeleton).
|
||||
//!
|
||||
//! The bottom of this file is the C ABI export section: the
|
||||
//! [`OakWorkerSession`] opaque handle and the `oakengine_worker_*`
|
||||
//! exports verbatim from `engine/include/oakengine/worker.h`.
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
use std::io::{self, BufRead, Write};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::bridge::render as render_ffi;
|
||||
use crate::handle::CHandle;
|
||||
use crate::ipc::{FrameSlotPool, SharedMemoryRegion, ShmMode};
|
||||
use oakrender::backend::{BackendKind, DisplayRenderer};
|
||||
|
||||
/// Protocol version announced in the startup handshake (`k_protocol_version`).
|
||||
pub const PROTOCOL_VERSION: i32 = 1;
|
||||
use crate::ipc::{
|
||||
error_message, write_message, FrameSlotPool, HandshakeMsg, LoadGraphMsg, RenderFrameMsg,
|
||||
SharedMemoryRegion, ShmMode, TYPE_CANCEL, TYPE_HANDSHAKE, TYPE_LOAD_GRAPH, TYPE_RENDER_FRAME,
|
||||
TYPE_SHUTDOWN,
|
||||
};
|
||||
|
||||
/// `"handshake"`.
|
||||
const TYPE_HANDSHAKE: &str = "handshake";
|
||||
/// `"load_graph"`.
|
||||
const TYPE_LOAD_GRAPH: &str = "load_graph";
|
||||
/// `"render_frame"`.
|
||||
const TYPE_RENDER_FRAME: &str = "render_frame";
|
||||
/// `"cancel"`.
|
||||
const TYPE_CANCEL: &str = "cancel";
|
||||
/// `"shutdown"`.
|
||||
const TYPE_SHUTDOWN: &str = "shutdown";
|
||||
/// `"error"`.
|
||||
const TYPE_ERROR: &str = "error";
|
||||
|
||||
/// Why `load_graph` answers "not yet available" after the real file checks.
|
||||
/// Why `load_graph` answers "not yet available" (after the real file checks).
|
||||
const GRAPH_STUB: &str = "load_graph: node-graph deserialization is not yet available in the \
|
||||
Rust worker (the oaknode crate is a todo!() skeleton; see worker/rust/README.md)";
|
||||
|
||||
@@ -77,82 +66,8 @@ const RENDER_STUB: &str = "render_frame: frame rendering is not yet available in
|
||||
worker (no node-graph or render-pipeline backing; the shm frame-slot transport is \
|
||||
attached but there is no graph to render; see worker/rust/README.md)";
|
||||
|
||||
/// `handshake` — field-for-field equivalent of `oak_ipc_handshake` (ipc.h);
|
||||
/// wire field names match the C++ serializer.
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct HandshakeMsg {
|
||||
/// Protocol version.
|
||||
pub protocol_version: i32,
|
||||
/// Worker->main output shared-memory segment key.
|
||||
pub shm_key: String,
|
||||
/// Main->worker input shared-memory segment key (optional).
|
||||
pub input_shm_key: String,
|
||||
/// Number of main->worker input frame slots.
|
||||
pub input_slots: i32,
|
||||
/// Number of worker->main output frame slots.
|
||||
pub output_slots: i32,
|
||||
/// Per-output-slot pixel block size.
|
||||
pub slot_data_bytes: i64,
|
||||
/// Per-input-slot pixel block size.
|
||||
pub input_slot_data_bytes: i64,
|
||||
}
|
||||
|
||||
/// `load_graph` — path to a temporary file holding the serialized graph.
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct LoadGraphMsg {
|
||||
/// Temporary file holding the serialized node graph.
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// `render_frame` — request a frame render (wire names per ipcmessage.cpp).
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct RenderFrameMsg {
|
||||
/// Correlates with the eventual frame_ready.
|
||||
pub ticket: i64,
|
||||
/// Viewer node stable uuid in the loaded graph.
|
||||
pub node: String,
|
||||
/// Frame timestamp numerator.
|
||||
pub time_num: i64,
|
||||
/// Frame timestamp denominator.
|
||||
pub time_den: i64,
|
||||
/// Forced output width (0 = graph default).
|
||||
pub width: i32,
|
||||
/// Forced output height (0 = graph default).
|
||||
pub height: i32,
|
||||
/// Forced `PixelFormat::Format` (-1 = default).
|
||||
pub format: i32,
|
||||
/// Channel count (0 = default).
|
||||
pub channels: i32,
|
||||
/// RenderMode::Mode.
|
||||
pub mode: i32,
|
||||
/// Optional decoded input slot (-1 = none).
|
||||
pub input_slot: i32,
|
||||
/// Ordered decoded input slots.
|
||||
pub input_slots: Vec<i32>,
|
||||
/// Output color transform present?
|
||||
pub has_color_transform: bool,
|
||||
/// 1 when the transform is a display transform.
|
||||
pub color_is_display: bool,
|
||||
/// Output colorspace or display name.
|
||||
pub color_output: String,
|
||||
/// Display view.
|
||||
pub color_view: String,
|
||||
/// Display look.
|
||||
pub color_look: String,
|
||||
}
|
||||
|
||||
/// Build a worker-side error report, mirroring `error_message()` in
|
||||
/// worker.cpp: `{"type":"error","message":...}` plus `"ticket"` when
|
||||
/// non-zero.
|
||||
fn error_message(message: &str, ticket: Option<i64>) -> Value {
|
||||
match ticket.filter(|t| *t != 0) {
|
||||
Some(t) => json!({ "type": TYPE_ERROR, "message": message, "ticket": t }),
|
||||
None => json!({ "type": TYPE_ERROR, "message": message }),
|
||||
}
|
||||
}
|
||||
/// Protocol version announced in the startup handshake (`k_protocol_version`).
|
||||
pub const PROTOCOL_VERSION: i32 = 1;
|
||||
|
||||
/// Log a worker-side message to stderr, mirroring worker.cpp `log_error()`
|
||||
/// (the `worker: ` prefix).
|
||||
@@ -170,17 +85,18 @@ pub fn is_no_backend(backend: &str) -> bool {
|
||||
// Renderer (backend selection)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A live, initialized oakrender display renderer handle (destroyed on
|
||||
/// drop).
|
||||
/// A live, initialized oakrender display renderer (destroyed on drop).
|
||||
pub struct Renderer {
|
||||
handle: CHandle,
|
||||
/// The oakrender crate's value-typed display renderer (single-lib
|
||||
/// unification; the CHandle-based C ABI is deleted).
|
||||
inner: DisplayRenderer,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
/// Create and initialize a renderer through the oakrender module C ABI,
|
||||
/// trying the named dynamic backend first and falling back to the
|
||||
/// direct OpenGL renderer — the exact fallback chain of worker.cpp
|
||||
/// `create_renderer()`.
|
||||
/// Create and initialize a renderer through the oakrender crate's
|
||||
/// direct Rust API, trying the named dynamic backend first and falling
|
||||
/// back to the direct OpenGL renderer — the exact fallback chain of
|
||||
/// worker.cpp `create_renderer()`.
|
||||
pub fn create(backend: &str) -> Result<Renderer, String> {
|
||||
match Self::create_dynamic(backend) {
|
||||
Ok(r) => Ok(r),
|
||||
@@ -195,58 +111,38 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Try the named dynamic backend through the module C ABI
|
||||
/// (`oakrender_display_renderer_create_dynamic` + `_init`).
|
||||
/// Try the named dynamic backend (`DisplayRenderer::new` +
|
||||
/// `init`, the single-lib equivalent of
|
||||
/// `oakrender_display_renderer_create_dynamic` + `_init`).
|
||||
fn create_dynamic(backend: &str) -> Result<Renderer, String> {
|
||||
let c = std::ffi::CString::new(backend)
|
||||
.map_err(|_| format!("invalid backend id {backend:?}"))?;
|
||||
// SAFETY: `c` is a valid NUL-terminated string the function only
|
||||
// reads during the call.
|
||||
let handle = unsafe { render_ffi::oakrender_display_renderer_create_dynamic(c.as_ptr()) };
|
||||
Self::init_handle(handle, &format!("dynamic {backend}"))
|
||||
let renderer = DisplayRenderer::new(BackendKind::from_config_string(backend));
|
||||
Self::init_inner(renderer, &format!("dynamic {backend}"))
|
||||
}
|
||||
|
||||
/// Fall back to the direct OpenGL renderer.
|
||||
fn create_opengl() -> Result<Renderer, String> {
|
||||
// SAFETY: no arguments; the function returns an owned handle.
|
||||
let handle = unsafe { render_ffi::oakrender_display_renderer_create_opengl() };
|
||||
Self::init_handle(handle, "direct OpenGL")
|
||||
let renderer = DisplayRenderer::new(BackendKind::Gl);
|
||||
Self::init_inner(renderer, "direct OpenGL")
|
||||
}
|
||||
|
||||
/// Initialize a freshly created renderer handle.
|
||||
fn init_handle(mut handle: CHandle, what: &str) -> Result<Renderer, String> {
|
||||
if handle.is_null() {
|
||||
return Err(format!("failed to create {what} renderer"));
|
||||
/// Initialize a freshly created renderer.
|
||||
fn init_inner(mut renderer: DisplayRenderer, what: &str) -> Result<Renderer, String> {
|
||||
// NULL gl_context makes the backend use its default device/context
|
||||
// path.
|
||||
if let Err(e) = renderer.init(std::ptr::null_mut()) {
|
||||
return Err(format!("failed to initialize {what} renderer ({e})"));
|
||||
}
|
||||
// SAFETY: `handle` is live and owned by us; NULL gl_context makes
|
||||
// the backend use its default device/context path.
|
||||
let rc =
|
||||
unsafe { render_ffi::oakrender_display_renderer_init(handle, std::ptr::null_mut()) };
|
||||
if rc != 0 {
|
||||
// SAFETY: the handle is still owned by us (init failed).
|
||||
unsafe { render_ffi::oakrender_display_renderer_destroy(&mut handle) };
|
||||
return Err(format!("failed to initialize {what} renderer (rc={rc})"));
|
||||
}
|
||||
Ok(Renderer { handle })
|
||||
Ok(Renderer { inner: renderer })
|
||||
}
|
||||
|
||||
/// 1 when the renderer is OpenGL-based (the C++ worker uses the GL
|
||||
/// context to announce the negotiated GL version in the handshake).
|
||||
///
|
||||
/// Not called yet: the oakrender module C ABI exposes no GL context
|
||||
/// Not called yet: the oakrender module exposes no GL context
|
||||
/// version, so the startup handshake omits `gl_major`/`gl_minor`.
|
||||
#[allow(dead_code)]
|
||||
pub fn is_open_gl(&self) -> bool {
|
||||
// SAFETY: `self.handle` is the live handle from init_handle().
|
||||
unsafe { render_ffi::oakrender_display_renderer_is_open_gl(self.handle) == 1 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Renderer {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: `self.handle` is the owned handle from init_handle() and
|
||||
// is not used after this.
|
||||
unsafe { render_ffi::oakrender_display_renderer_destroy(&mut self.handle) };
|
||||
self.inner.is_open_gl()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,7 +168,7 @@ impl WorkerSession {
|
||||
/// Create a session for `backend`, mirroring
|
||||
/// `oakengine_worker_session_create()`: "none"/"" skips renderer
|
||||
/// creation, anything else initializes the render backend through the
|
||||
/// oakrender module C ABI (dynamic -> OpenGL fallback).
|
||||
/// oakrender crate's direct Rust API (dynamic -> OpenGL fallback).
|
||||
pub fn create(backend: &str) -> Result<WorkerSession, String> {
|
||||
let renderer = if is_no_backend(backend) {
|
||||
None
|
||||
@@ -311,11 +207,9 @@ impl WorkerSession {
|
||||
return true;
|
||||
}
|
||||
log_error("runtime: loading color-manager default config");
|
||||
// SAFETY: no arguments; the function initializes process-wide state.
|
||||
let rc = unsafe { render_ffi::oakrender_color_manager_set_up_default_config() };
|
||||
if rc != 0 {
|
||||
if let Err(e) = oakrender::color::set_up_default_config() {
|
||||
log_error(&format!(
|
||||
"runtime: color-manager default config failed (rc={rc}); continuing"
|
||||
"runtime: color-manager default config failed ({e}); continuing"
|
||||
));
|
||||
}
|
||||
log_error(
|
||||
@@ -332,7 +226,7 @@ impl WorkerSession {
|
||||
/// announces their geometry in its handshake reply.
|
||||
///
|
||||
/// Deviation from the C++: `gl_major`/`gl_minor` are omitted because
|
||||
/// the oakrender module C ABI exposes no GL context version.
|
||||
/// the oakrender module exposes no GL context version.
|
||||
pub fn startup_handshake(&self) -> Value {
|
||||
HandshakeMsg {
|
||||
protocol_version: PROTOCOL_VERSION,
|
||||
@@ -497,59 +391,10 @@ impl WorkerSession {
|
||||
}
|
||||
}
|
||||
|
||||
impl HandshakeMsg {
|
||||
/// Serialize to the wire `handshake` object.
|
||||
pub fn to_json(&self) -> Value {
|
||||
json!({
|
||||
"type": TYPE_HANDSHAKE,
|
||||
"protocol_version": self.protocol_version,
|
||||
"shm_key": self.shm_key,
|
||||
"input_shm_key": self.input_shm_key,
|
||||
"input_slots": self.input_slots,
|
||||
"output_slots": self.output_slots,
|
||||
"slot_data_bytes": self.slot_data_bytes,
|
||||
"input_slot_data_bytes": self.input_slot_data_bytes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Write one NDJSON message line (compact JSON + `\n`), the Rust port of
|
||||
/// `ipcmessage.cpp write_message()`.
|
||||
fn write_message(w: &mut impl Write, msg: &Value) -> io::Result<()> {
|
||||
let line =
|
||||
serde_json::to_string(msg).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
w.write_all(line.as_bytes())?;
|
||||
w.write_all(b"\n")
|
||||
}
|
||||
|
||||
/// Scan argv for `--backend <name>` (worker.cpp `oakengine_worker_main`).
|
||||
/// The default is `"opengl"`; the value is lowercased; the last flag wins.
|
||||
fn parse_backend(argc: c_int, argv: *mut *mut c_char) -> String {
|
||||
let mut backend = "opengl".to_string();
|
||||
if argc > 0 && !argv.is_null() {
|
||||
// SAFETY: `argv` points to `argc` NUL-terminated C strings (the C
|
||||
// runtime's argv), and we only read the entries.
|
||||
let args = unsafe { std::slice::from_raw_parts(argv, argc as usize) };
|
||||
let mut i = 1usize;
|
||||
while i < args.len() {
|
||||
// SAFETY: `args[i]` is a valid NUL-terminated C string.
|
||||
let arg = unsafe { crate::handle::read_cstr(args[i]) };
|
||||
if arg == "--backend" && i + 1 < args.len() {
|
||||
// SAFETY: `args[i + 1]` is a valid NUL-terminated C string.
|
||||
backend = unsafe { crate::handle::read_cstr(args[i + 1]) }.to_ascii_lowercase();
|
||||
i += 2;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
backend
|
||||
}
|
||||
|
||||
/// Full render-worker main, transport-agnostic in the backend name.
|
||||
///
|
||||
/// Mirrors `oakengine_worker_main()` in worker.cpp: create the session
|
||||
@@ -559,7 +404,8 @@ fn parse_backend(argc: c_int, argv: *mut *mut c_char) -> String {
|
||||
/// exit code.
|
||||
pub fn worker_main(backend: &str) -> i32 {
|
||||
// 1. Session creation initializes the render backend through the
|
||||
// oakrender module C ABI (oakengine_worker_session_create()).
|
||||
// oakrender crate's direct Rust API
|
||||
// (oakengine_worker_session_create()).
|
||||
let mut session = match WorkerSession::create(backend) {
|
||||
Ok(s) => s,
|
||||
Err(msg) => {
|
||||
@@ -628,6 +474,325 @@ pub fn worker_main(backend: &str) -> i32 {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Unit tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ipc::{FrameSlotPool, SharedMemoryRegion, ShmMode};
|
||||
use serde_json::json;
|
||||
use std::ptr;
|
||||
|
||||
fn test_key(name: &str) -> String {
|
||||
static COUNTER: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
|
||||
let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
SharedMemoryRegion::make_key(i64::from(std::process::id()), (n & 0x7FFF) as i32)
|
||||
+ &format!("-w-{name}")
|
||||
}
|
||||
|
||||
/// The "parent" side of a handshake: create an output segment holding a
|
||||
/// pool, optionally an input segment, and return the handshake message
|
||||
/// plus the owner regions (kept alive by the caller).
|
||||
fn parent_side(
|
||||
slots: i32,
|
||||
slot_bytes: i64,
|
||||
input: bool,
|
||||
) -> (Value, SharedMemoryRegion, Option<SharedMemoryRegion>) {
|
||||
let out_key = test_key("out");
|
||||
let out_bytes = FrameSlotPool::bytes_needed(slots as u32, slot_bytes as usize);
|
||||
let mut out_region = SharedMemoryRegion::new();
|
||||
assert!(
|
||||
out_region.open(&out_key, out_bytes, ShmMode::Create),
|
||||
"{}",
|
||||
out_region.error()
|
||||
);
|
||||
// SAFETY: live mapping sized by bytes_needed.
|
||||
let _pool =
|
||||
unsafe { FrameSlotPool::create(out_region.data(), slots as u32, slot_bytes as usize) };
|
||||
|
||||
let (in_key, in_bytes, in_region) = if input {
|
||||
let in_key = test_key("in");
|
||||
let in_bytes = FrameSlotPool::bytes_needed(slots as u32, slot_bytes as usize);
|
||||
let mut in_region = SharedMemoryRegion::new();
|
||||
assert!(in_region.open(&in_key, in_bytes, ShmMode::Create));
|
||||
// SAFETY: live mapping.
|
||||
let _ = unsafe {
|
||||
FrameSlotPool::create(in_region.data(), slots as u32, slot_bytes as usize)
|
||||
};
|
||||
(Some(in_key), Some(in_bytes), Some(in_region))
|
||||
} else {
|
||||
(None, None, None)
|
||||
};
|
||||
|
||||
let hs = json!({
|
||||
"type": "handshake",
|
||||
"protocol_version": PROTOCOL_VERSION,
|
||||
"shm_key": out_key,
|
||||
"input_shm_key": in_key.unwrap_or_default(),
|
||||
"input_slots": if input { slots } else { 0 },
|
||||
"output_slots": slots,
|
||||
"slot_data_bytes": slot_bytes,
|
||||
"input_slot_data_bytes": in_bytes.unwrap_or(0),
|
||||
});
|
||||
(hs, out_region, in_region)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_backend_detection_matches_cpp() {
|
||||
assert!(is_no_backend(""));
|
||||
assert!(is_no_backend("none"));
|
||||
assert!(is_no_backend("NONE"));
|
||||
assert!(!is_no_backend("opengl"));
|
||||
assert!(!is_no_backend("vulkan"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_backend_session_has_no_renderer_but_serves_messages() {
|
||||
let mut s = WorkerSession::create("none").unwrap();
|
||||
assert!(!s.has_renderer());
|
||||
let resp = s.handle_line(r#"{"type":"shutdown"}"#);
|
||||
assert!(resp.is_none());
|
||||
assert!(s.shutdown_requested());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_handshake_is_protocol_version_1_with_empty_geometry() {
|
||||
let s = WorkerSession::create("none").unwrap();
|
||||
let hs = s.startup_handshake();
|
||||
assert_eq!(
|
||||
hs,
|
||||
json!({
|
||||
"type": "handshake",
|
||||
"protocol_version": 1,
|
||||
"shm_key": "",
|
||||
"input_shm_key": "",
|
||||
"input_slots": 0,
|
||||
"output_slots": 0,
|
||||
"slot_data_bytes": 0,
|
||||
"input_slot_data_bytes": 0,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_line_yields_error_response() {
|
||||
let mut s = WorkerSession::create("none").unwrap();
|
||||
let resp = s.handle_line("this is not json").unwrap();
|
||||
assert_eq!(resp["type"], "error");
|
||||
assert_eq!(resp["message"], "malformed control message");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_message_type_yields_error_response() {
|
||||
let mut s = WorkerSession::create("none").unwrap();
|
||||
let resp = s.handle_line(r#"{"type":"frobnicate"}"#).unwrap();
|
||||
assert_eq!(resp["message"], "unknown message type: frobnicate");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_and_shutdown_produce_no_response() {
|
||||
let mut s = WorkerSession::create("none").unwrap();
|
||||
assert!(s.handle_line(r#"{"type":"cancel","ticket":5}"#).is_none());
|
||||
assert!(s.handle_line(r#"{"type":"shutdown"}"#).is_none());
|
||||
assert!(s.shutdown_requested());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handshake_wrong_protocol_version() {
|
||||
let mut s = WorkerSession::create("none").unwrap();
|
||||
let resp = s
|
||||
.handle_line(
|
||||
r#"{"type":"handshake","protocol_version":99,"shm_key":"k","output_slots":1,"slot_data_bytes":16}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(resp["message"], "unsupported protocol version 99");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handshake_missing_geometry() {
|
||||
let mut s = WorkerSession::create("none").unwrap();
|
||||
let resp = s
|
||||
.handle_line(r#"{"type":"handshake","protocol_version":1}"#)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
resp["message"],
|
||||
"handshake missing output shared-memory geometry"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handshake_attaches_real_output_pool() {
|
||||
let mut s = WorkerSession::create("none").unwrap();
|
||||
let (hs, out_region, _in) = parent_side(4, 4096, false);
|
||||
let resp = s.handle_line(&hs.to_string());
|
||||
assert!(resp.is_none(), "unexpected error: {resp:?}");
|
||||
// The session now holds a real attached pool with the parent's
|
||||
// geometry.
|
||||
let out_pool = s.output_pool.as_ref().unwrap();
|
||||
assert_eq!(out_pool.slot_count(), 4);
|
||||
assert_eq!(out_pool.slot_data_bytes(), 4096);
|
||||
|
||||
// The two views share the same rings, not copies: the parent pops a
|
||||
// free slot and the worker's pool sees the ring cursor move; the
|
||||
// parent's publish lands in the worker's ready ring.
|
||||
// SAFETY: `out_region` is a live mapping containing the pool the
|
||||
// session attached to.
|
||||
let parent_pool = unsafe { FrameSlotPool::attach(out_region.data()) };
|
||||
let mut parent_slot = 0;
|
||||
assert!(unsafe { parent_pool.acquire(&mut parent_slot) });
|
||||
assert_eq!(parent_slot, 0);
|
||||
let mut worker_slot = 0;
|
||||
assert!(unsafe { out_pool.acquire(&mut worker_slot) });
|
||||
assert_eq!(worker_slot, 1, "worker must see the parent's free-ring pop");
|
||||
|
||||
// SAFETY: `parent_slot` was acquired by the parent; slot_bytes
|
||||
// writable.
|
||||
unsafe {
|
||||
ptr::write_bytes(parent_pool.slot_data(parent_slot), 0xAB, 64);
|
||||
}
|
||||
assert!(unsafe { parent_pool.publish(parent_slot) });
|
||||
let mut consumed = 0;
|
||||
assert!(unsafe { out_pool.consume(&mut consumed) });
|
||||
assert_eq!(consumed, parent_slot);
|
||||
// SAFETY: `consumed` was consumed by the worker's pool.
|
||||
assert_eq!(unsafe { *out_pool.slot_data_const(consumed) }, 0xAB);
|
||||
// Clean up so the region drop at test end unlinks cleanly.
|
||||
unsafe { out_pool.release(consumed) };
|
||||
unsafe { out_pool.release(worker_slot) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handshake_attaches_input_pool_too() {
|
||||
let mut s = WorkerSession::create("none").unwrap();
|
||||
let (hs, _out, _in) = parent_side(2, 256, true);
|
||||
let resp = s.handle_line(&hs.to_string());
|
||||
assert!(resp.is_none(), "unexpected error: {resp:?}");
|
||||
assert!(s.input_pool.is_some());
|
||||
let in_pool = s.input_pool.as_ref().unwrap();
|
||||
assert_eq!(in_pool.slot_count(), 2);
|
||||
assert_eq!(in_pool.slot_data_bytes(), 256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handshake_attach_failure_reports_error() {
|
||||
let mut s = WorkerSession::create("none").unwrap();
|
||||
// A key that was never created.
|
||||
let resp = s
|
||||
.handle_line(
|
||||
&json!({
|
||||
"type": "handshake",
|
||||
"protocol_version": 1,
|
||||
"shm_key": format!("olive-rw-{}-missing", std::process::id()),
|
||||
"output_slots": 4,
|
||||
"slot_data_bytes": 4096,
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(resp["type"], "error");
|
||||
assert!(resp["message"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.starts_with("failed to attach shared memory: "));
|
||||
assert!(s.output_pool.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handshake_rejects_non_pool_segment() {
|
||||
let mut s = WorkerSession::create("none").unwrap();
|
||||
// A real segment of the right size that does not contain a pool
|
||||
// (zeroed memory → wrong magic). Sized so the attach size check
|
||||
// passes and the magic check fires.
|
||||
let key = test_key("nopool");
|
||||
let bytes = FrameSlotPool::bytes_needed(4, 4096);
|
||||
let mut region = SharedMemoryRegion::new();
|
||||
assert!(region.open(&key, bytes, ShmMode::Create));
|
||||
let resp = s
|
||||
.handle_line(
|
||||
&json!({
|
||||
"type": "handshake",
|
||||
"protocol_version": 1,
|
||||
"shm_key": key,
|
||||
"output_slots": 4,
|
||||
"slot_data_bytes": 4096,
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
resp["message"],
|
||||
"shared memory does not contain a frame slot pool"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handshake_missing_input_geometry_is_an_error() {
|
||||
let mut s = WorkerSession::create("none").unwrap();
|
||||
let (mut hs, _out, _in) = parent_side(2, 256, false);
|
||||
// Ask for input slots without announcing their geometry.
|
||||
hs["input_slots"] = json!(2);
|
||||
let resp = s.handle_line(&hs.to_string()).unwrap();
|
||||
assert_eq!(
|
||||
resp["message"],
|
||||
"handshake missing input shared-memory geometry"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_graph_checks_are_real_then_stub() {
|
||||
let mut s = WorkerSession::create("none").unwrap();
|
||||
|
||||
let missing = "/definitely/not/a/real/graph.ove";
|
||||
let resp = s
|
||||
.handle_line(&json!({ "type": "load_graph", "path": missing }).to_string())
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
resp["message"],
|
||||
format!("graph file does not exist: {missing}")
|
||||
);
|
||||
|
||||
let empty = std::env::temp_dir().join("oak_worker_main_test_empty.ove");
|
||||
std::fs::write(&empty, b"").unwrap();
|
||||
let resp = s
|
||||
.handle_line(
|
||||
&json!({ "type": "load_graph", "path": empty.display().to_string() }).to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
resp["message"],
|
||||
format!("graph file is empty: {}", empty.display())
|
||||
);
|
||||
let _ = std::fs::remove_file(&empty);
|
||||
|
||||
let real = std::env::temp_dir().join("oak_worker_main_test_graph.ove");
|
||||
std::fs::write(&real, b"<root/>").unwrap();
|
||||
let resp = s
|
||||
.handle_line(
|
||||
&json!({ "type": "load_graph", "path": real.display().to_string() }).to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(resp["message"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("node-graph deserialization is not yet available"));
|
||||
let _ = std::fs::remove_file(&real);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_frame_reports_stub_with_ticket() {
|
||||
let mut s = WorkerSession::create("none").unwrap();
|
||||
let resp = s
|
||||
.handle_line(r#"{"type":"render_frame","ticket":123,"node":"abc"}"#)
|
||||
.unwrap();
|
||||
assert_eq!(resp["type"], "error");
|
||||
assert_eq!(resp["ticket"], 123);
|
||||
assert!(resp["message"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("frame rendering is not yet available"));
|
||||
}
|
||||
}
|
||||
// C ABI exports (engine/include/oakengine/worker.h)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -757,6 +922,30 @@ pub unsafe extern "C" fn oakengine_worker_session_shutdown_requested(
|
||||
})
|
||||
}
|
||||
|
||||
/// Scan argv for `--backend <name>` (worker.cpp `oakengine_worker_main`).
|
||||
/// The default is `"opengl"`; the value is lowercased; the last flag wins.
|
||||
fn parse_backend(argc: c_int, argv: *mut *mut c_char) -> String {
|
||||
let mut backend = "opengl".to_string();
|
||||
if argc > 0 && !argv.is_null() {
|
||||
// SAFETY: `argv` points to `argc` NUL-terminated C strings (the C
|
||||
// runtime's argv), and we only read the entries.
|
||||
let args = unsafe { std::slice::from_raw_parts(argv, argc as usize) };
|
||||
let mut i = 1usize;
|
||||
while i < args.len() {
|
||||
// SAFETY: `args[i]` is a valid NUL-terminated C string.
|
||||
let arg = unsafe { crate::handle::read_cstr(args[i]) };
|
||||
if arg == "--backend" && i + 1 < args.len() {
|
||||
// SAFETY: `args[i + 1]` is a valid NUL-terminated C string.
|
||||
backend = unsafe { crate::handle::read_cstr(args[i + 1]) }.to_ascii_lowercase();
|
||||
i += 2;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
backend
|
||||
}
|
||||
|
||||
/// `oakengine_worker_main` — full render-worker main. Parses `--backend`,
|
||||
/// initializes the renderer, sends the startup handshake and runs the
|
||||
/// stdin/stdout NDJSON loop until a shutdown message or EOF. Returns the
|
||||
@@ -774,7 +963,7 @@ pub unsafe extern "C" fn oakengine_worker_main(argc: c_int, argv: *mut *mut c_ch
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
mod cabi_tests {
|
||||
use super::*;
|
||||
use crate::ipc::{self, FrameSlotPool, SharedMemoryRegion, ShmMode};
|
||||
use serde_json::json;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,69 +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/>.
|
||||
|
||||
//! Forces rustc to link every module crate's rlib into the test binary.
|
||||
//!
|
||||
//! The facade itself only references the modules through `extern "C"`
|
||||
//! imports (see src/bridge), so rustc would otherwise drop the
|
||||
//! dev-dependency rlibs from the link and leave the imports undefined.
|
||||
//! Referencing one exported item per crate makes the linker pull the
|
||||
//! crate's objects in; the `#[no_mangle]` exports then satisfy the
|
||||
//! bridge imports.
|
||||
|
||||
/// Smoke-test that every module crate links: each referenced export is
|
||||
/// called once and must return a sane value or a handle.
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
fn all_module_crates_link() {
|
||||
// oakundo: fresh stack, refcount 1.
|
||||
let stack = unsafe { oakundo::ffi::undostack::oakundo_undostack_init() };
|
||||
assert!(!stack.ctx.is_null());
|
||||
|
||||
// oakcommon: an int config read with fallback.
|
||||
let v = unsafe {
|
||||
oakcommon::ffi::config::oakcommon_config_get_int(
|
||||
std::ptr::null(),
|
||||
c"no-such-key".as_ptr(),
|
||||
42,
|
||||
)
|
||||
};
|
||||
assert_eq!(v, 42);
|
||||
|
||||
// oakcodec: format count (must be positive).
|
||||
let n = unsafe { oakcodec::ffi::format::oakcodec_encoding_format_count() };
|
||||
assert!(n > 0);
|
||||
|
||||
// oakaudio: waveform length of a null handle is an error code, not a
|
||||
// crash (module validates the handle).
|
||||
let rc = unsafe {
|
||||
oakaudio::ffi::waveform::oakaudio_waveform_length(
|
||||
oakaudio::handle::CHandle::null(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
assert!(rc < 0);
|
||||
|
||||
// oakrender: cache indicator height is a positive constant.
|
||||
let h = unsafe { oakrender::ffi::cache::oakrender_cache_indicator_height() };
|
||||
assert!(h > 0);
|
||||
|
||||
// oakplugin: host plugin count with no scan is 0.
|
||||
let n = unsafe { oakplugin::ffi::oakplugin_host_plugin_count() };
|
||||
assert_eq!(n, 0);
|
||||
}
|
||||
@@ -1,841 +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/>.
|
||||
|
||||
//! Smoke tests for the timeline family (`engine/include/oakengine/timeline.h`).
|
||||
//!
|
||||
//! The facade owns a process-wide undo stack, so every test that pushes
|
||||
//! undoable commands (sequence creation, tracks, markers, workarea, clip
|
||||
//! editing) is serialized inside the single `timeline_lifecycle` test; the
|
||||
//! failure-path test only exercises non-mutating NULL-handle calls and runs
|
||||
//! in parallel.
|
||||
//!
|
||||
//! Clips are placed through `oaknode::ffi` directly (the module clip has no
|
||||
//! `buffer_in` input, so `oakengine_sequence_add_footage_clip` cannot
|
||||
//! connect a footage node; that test is `#[ignore]`d with the reason).
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use oakengine::handle::{box_handle, OakEngineNode};
|
||||
use oakengine::node::{
|
||||
oakengine_footage_borrow, oakengine_project_create, oakengine_project_free,
|
||||
oakengine_project_new,
|
||||
};
|
||||
use oakengine::timeline::{
|
||||
oakengine_block_get_range, oakengine_block_get_track, oakengine_block_is_enabled,
|
||||
oakengine_block_is_gap, oakengine_block_link_count, oakengine_block_next, oakengine_block_prev,
|
||||
oakengine_block_set_enabled, oakengine_block_set_length_and_media_out,
|
||||
oakengine_clip_are_linked, oakengine_clip_get_range, oakengine_clip_get_sequence,
|
||||
oakengine_clip_is_enabled, oakengine_clip_toggle_enabled, oakengine_clip_trim,
|
||||
oakengine_marker_commit_time, oakengine_marker_get_color, oakengine_marker_get_name,
|
||||
oakengine_marker_get_time, oakengine_marker_has_sibling_at_time, oakengine_marker_list_add,
|
||||
oakengine_marker_list_at, oakengine_marker_list_count, oakengine_marker_list_marker_at_time,
|
||||
oakengine_marker_remove, oakengine_marker_set_time_command, oakengine_marker_set_time_live,
|
||||
oakengine_multicam_switch_source, oakengine_node_is_block, oakengine_node_is_transition,
|
||||
oakengine_sequence_add_default_nodes, oakengine_sequence_add_footage_clip,
|
||||
oakengine_sequence_add_track, oakengine_sequence_clip_at, oakengine_sequence_clip_count,
|
||||
oakengine_sequence_delete_empty_tracks, oakengine_sequence_get_audio_params,
|
||||
oakengine_sequence_get_frame_rate, oakengine_sequence_get_length,
|
||||
oakengine_sequence_get_length_rational, oakengine_sequence_get_playhead,
|
||||
oakengine_sequence_get_playhead_seconds, oakengine_sequence_get_preview_divider,
|
||||
oakengine_sequence_get_video_auto_cache, oakengine_sequence_get_workarea,
|
||||
oakengine_sequence_last_error, oakengine_sequence_marker_add, oakengine_sequence_marker_add_ex,
|
||||
oakengine_sequence_marker_at, oakengine_sequence_marker_count,
|
||||
oakengine_sequence_marker_remove, oakengine_sequence_marker_remove_many,
|
||||
oakengine_sequence_marker_rename, oakengine_sequence_move_clip, oakengine_sequence_name,
|
||||
oakengine_sequence_new, oakengine_sequence_remove_track, oakengine_sequence_ripple_delete_clip,
|
||||
oakengine_sequence_ripple_delete_range, oakengine_sequence_set_audio_params,
|
||||
oakengine_sequence_set_playhead, oakengine_sequence_set_workarea,
|
||||
oakengine_sequence_split_clip, oakengine_sequence_track_at, oakengine_sequence_track_count,
|
||||
oakengine_sequence_track_list, oakengine_sequence_trim_clips_to,
|
||||
oakengine_sequence_workarea_is_enabled, oakengine_track_block_at, oakengine_track_block_count,
|
||||
oakengine_track_get_height, oakengine_track_get_length, oakengine_track_is_locked,
|
||||
oakengine_track_is_muted, oakengine_track_is_range_free, oakengine_track_set_height,
|
||||
oakengine_track_set_locked, oakengine_track_set_muted, oakengine_track_type,
|
||||
oakengine_track_visible_block_at_time, oakengine_workarea_create, oakengine_workarea_free,
|
||||
oakengine_workarea_get, oakengine_workarea_reset_in_out, oakengine_workarea_set_enabled,
|
||||
oakengine_workarea_set_enabled_undoable, oakengine_workarea_set_range,
|
||||
oakengine_workarea_set_range_undoable,
|
||||
};
|
||||
use oakengine::undo::{oakengine_undo_index, oakengine_undo_jump};
|
||||
|
||||
/// Read a NUL-terminated facade string into a Rust String.
|
||||
unsafe fn read_buf(buf: &mut [c_char]) -> String {
|
||||
std::ffi::CStr::from_ptr(buf.as_ptr())
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
/// Force the runtime-dlsym'd symbols into the link: the oaknode module
|
||||
/// resolves `oakcommon_videoparams_*` and `oakundo_command_init` at runtime
|
||||
/// with `dlsym(RTLD_DEFAULT)`, and nothing references those codegen units at
|
||||
/// link time unless named here (the node.rs family force-links
|
||||
/// `oakundo_command_init` for the same reason).
|
||||
fn force_runtime_syms() -> usize {
|
||||
let fns: [usize; 8] = [
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_init_basic as *const () as usize,
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_set_frame_rate as *const () as usize,
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_width as *const () as usize,
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_height as *const () as usize,
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_format as *const () as usize,
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_channel_count as *const () as usize,
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_frame_rate as *const () as usize,
|
||||
oakundo::ffi::command::oakundo_command_init as *const () as usize,
|
||||
];
|
||||
fns.iter().sum()
|
||||
}
|
||||
|
||||
/// Convert a facade `CHandle` to the layout-identical oaknode `CHandle`
|
||||
/// (distinct Rust types over the same C ABI struct).
|
||||
fn to_node_handle(h: oakengine::handle::CHandle) -> oaknode::handle::CHandle {
|
||||
oaknode::handle::CHandle {
|
||||
ctx: h.ctx,
|
||||
addref: h.addref,
|
||||
release: h.release,
|
||||
abi_version: h.abi_version,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an oaknode `CHandle` back to the facade `CHandle`.
|
||||
fn to_facade_handle(h: oaknode::handle::CHandle) -> oakengine::handle::CHandle {
|
||||
oakengine::handle::CHandle {
|
||||
ctx: h.ctx,
|
||||
addref: h.addref,
|
||||
release: h.release,
|
||||
abi_version: h.abi_version,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Serialized stack-mutating test
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Sequence creation, tracks, playhead, markers, workarea and clip editing —
|
||||
/// all in ONE test because the facade's undo stack is process-wide (the
|
||||
/// same serialization the undo/node families use).
|
||||
#[test]
|
||||
fn timeline_lifecycle() {
|
||||
common::force_link();
|
||||
let _ = force_runtime_syms();
|
||||
|
||||
// ---- project + sequence creation ----------------------------------
|
||||
let project = oakengine_project_create();
|
||||
assert!(!project.is_null());
|
||||
assert_eq!(unsafe { oakengine_project_new(project) }, 0);
|
||||
|
||||
// NULL project -> NULL.
|
||||
assert!(unsafe { oakengine_sequence_new(std::ptr::null_mut(), c"x".as_ptr()) }.is_null());
|
||||
|
||||
let seq = unsafe { oakengine_sequence_new(project, c"Test Sequence".as_ptr()) };
|
||||
assert!(!seq.is_null());
|
||||
|
||||
let mut buf = [0 as c_char; 256];
|
||||
let len = unsafe { oakengine_sequence_name(seq, buf.as_mut_ptr(), 256) };
|
||||
assert_eq!(len, 13);
|
||||
assert_eq!(unsafe { read_buf(&mut buf) }, "Test Sequence");
|
||||
|
||||
// Fresh sequence: zero length, default frame rate.
|
||||
let mut seconds = -1.0;
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_get_length(seq, &mut seconds) },
|
||||
0
|
||||
);
|
||||
assert_eq!(seconds, 0.0);
|
||||
let mut num = -1;
|
||||
let mut den = -1;
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_get_length_rational(seq, &mut num, &mut den) },
|
||||
0
|
||||
);
|
||||
assert_eq!((num, den), (0, 1));
|
||||
let mut fps_num = 0;
|
||||
let mut fps_den = 0;
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_get_frame_rate(seq, &mut fps_num, &mut fps_den) },
|
||||
0
|
||||
);
|
||||
assert_eq!((fps_num, fps_den), (30, 1));
|
||||
|
||||
// ---- tracks ---------------------------------------------------------
|
||||
let idx = unsafe { oakengine_sequence_add_track(seq, 0) };
|
||||
assert_eq!(idx, 0);
|
||||
let mut video = -1;
|
||||
let mut audio = -1;
|
||||
let mut subtitle = -1;
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_track_count(seq, &mut video, &mut audio, &mut subtitle) },
|
||||
0
|
||||
);
|
||||
assert_eq!(video, 1);
|
||||
assert_eq!(audio, 0);
|
||||
assert_eq!(subtitle, 0);
|
||||
|
||||
let track = unsafe { oakengine_sequence_track_at(seq, 0, 0) };
|
||||
assert!(!track.is_null());
|
||||
assert_eq!(unsafe { oakengine_track_type(track) }, 0); // video
|
||||
assert!(unsafe { oakengine_sequence_track_at(seq, 0, 5) }.is_null());
|
||||
assert!(!unsafe { oakengine_sequence_track_list(seq, 0) }.is_null());
|
||||
assert!(unsafe { oakengine_sequence_track_list(seq, 99) }.is_null());
|
||||
|
||||
// Track height / mute / lock (NOT undoable, straight setters).
|
||||
let mut h = 0.0;
|
||||
assert_eq!(unsafe { oakengine_track_get_height(seq, 0, 0, &mut h) }, 0);
|
||||
assert!((h - 3.0).abs() < 1e-9);
|
||||
assert_eq!(unsafe { oakengine_track_set_height(seq, 0, 0, 5.0) }, 0);
|
||||
assert_eq!(unsafe { oakengine_track_get_height(seq, 0, 0, &mut h) }, 0);
|
||||
assert!((h - 5.0).abs() < 1e-9);
|
||||
assert_eq!(unsafe { oakengine_track_is_muted(seq, 0, 0) }, 0);
|
||||
assert_eq!(unsafe { oakengine_track_set_muted(seq, 0, 0, 1) }, 0);
|
||||
assert_eq!(unsafe { oakengine_track_is_muted(seq, 0, 0) }, 1);
|
||||
assert_eq!(unsafe { oakengine_track_is_locked(seq, 0, 0) }, 0);
|
||||
assert_eq!(unsafe { oakengine_track_set_locked(seq, 0, 0, 1) }, 0);
|
||||
assert_eq!(unsafe { oakengine_track_is_locked(seq, 0, 0) }, 1);
|
||||
assert_eq!(unsafe { oakengine_track_set_locked(seq, 0, 0, 0) }, 0);
|
||||
|
||||
// Track length (empty -> 0) and free-range query.
|
||||
let mut tlen = -1;
|
||||
assert_eq!(
|
||||
unsafe { oakengine_track_get_length(seq, 0, 0, &mut tlen) },
|
||||
0
|
||||
);
|
||||
assert_eq!(tlen, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_track_is_range_free(seq, 0, 0, 0, 30) },
|
||||
1
|
||||
);
|
||||
// Bad track index -> E_NOT_FOUND (-4).
|
||||
assert_eq!(
|
||||
unsafe { oakengine_track_get_length(seq, 0, 99, &mut tlen) },
|
||||
-4
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_track_is_range_free(seq, 0, 99, 0, 30) },
|
||||
-4
|
||||
);
|
||||
|
||||
// ---- playhead ---------------------------------------------------------
|
||||
assert_eq!(unsafe { oakengine_sequence_set_playhead(seq, 90) }, 0);
|
||||
let mut ph = -1;
|
||||
assert_eq!(unsafe { oakengine_sequence_get_playhead(seq, &mut ph) }, 0);
|
||||
assert_eq!(ph, 90);
|
||||
let mut phs = 0.0;
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_get_playhead_seconds(seq, &mut phs) },
|
||||
0
|
||||
);
|
||||
assert!((phs - 3.0).abs() < 1e-6);
|
||||
|
||||
// ---- audio params (round-trip through oakcore) -------------------------
|
||||
let mut arate: c_int = 0;
|
||||
let mut alayout: u64 = 0;
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_get_audio_params(seq, &mut arate, &mut alayout) },
|
||||
0
|
||||
);
|
||||
assert!(arate > 0);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_set_audio_params(seq, 48000, 0x3, 1) },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_get_audio_params(seq, &mut arate, &mut alayout) },
|
||||
0
|
||||
);
|
||||
assert_eq!(arate, 48000);
|
||||
assert_eq!(alayout, 0x3);
|
||||
// A no-op change (same values) is a success without a new command.
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_set_audio_params(seq, 48000, 0x3, 1) },
|
||||
0
|
||||
);
|
||||
// NULL handle -> E_INVALID.
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_set_audio_params(std::ptr::null_mut(), 48000, 0x3, 1) },
|
||||
-1
|
||||
);
|
||||
|
||||
// ---- markers -----------------------------------------------------------
|
||||
assert_eq!(unsafe { oakengine_sequence_marker_count(seq) }, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_marker_add(seq, 30, c"One".as_ptr()) },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_marker_add_ex(seq, 60, c"Two".as_ptr(), 2) },
|
||||
0
|
||||
);
|
||||
assert_eq!(unsafe { oakengine_sequence_marker_count(seq) }, 2);
|
||||
|
||||
let mut mtime = -1;
|
||||
let mut mcolor = -1;
|
||||
let mut mname = [0 as c_char; 64];
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_sequence_marker_at(seq, 0, &mut mtime, mname.as_mut_ptr(), 64, &mut mcolor)
|
||||
},
|
||||
0
|
||||
);
|
||||
assert_eq!(mtime, 30);
|
||||
assert_eq!(mcolor, 0);
|
||||
assert_eq!(unsafe { read_buf(&mut mname) }, "One");
|
||||
|
||||
// A duplicate time is rejected with E_STATE.
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_marker_add(seq, 30, c"dup".as_ptr()) },
|
||||
-2
|
||||
);
|
||||
|
||||
// Rename, then remove many at once.
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_marker_rename(seq, 30, c"Renamed".as_ptr()) },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_marker_remove_many(seq, [30i64, 60].as_ptr(), 2) },
|
||||
2
|
||||
);
|
||||
assert_eq!(unsafe { oakengine_sequence_marker_count(seq) }, 0);
|
||||
// Removing a nonexistent time -> E_NOT_FOUND (-4).
|
||||
assert_eq!(unsafe { oakengine_sequence_marker_remove(seq, 999) }, -4);
|
||||
|
||||
// ---- workarea ------------------------------------------------------------
|
||||
assert_eq!(unsafe { oakengine_sequence_workarea_is_enabled(seq) }, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_set_workarea(seq, 1, 0, 300) },
|
||||
0
|
||||
);
|
||||
assert_eq!(unsafe { oakengine_sequence_workarea_is_enabled(seq) }, 1);
|
||||
let mut wa_in = -1;
|
||||
let mut wa_out = -1;
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_get_workarea(seq, &mut wa_in, &mut wa_out) },
|
||||
0
|
||||
);
|
||||
assert_eq!((wa_in, wa_out), (0, 300));
|
||||
|
||||
// Reset sentinels: in = 0/1, out = RATIONAL_MAX/1.
|
||||
let mut rn = -1;
|
||||
let mut rd = -1;
|
||||
let mut ron = -1;
|
||||
let mut rod = -1;
|
||||
unsafe { oakengine_workarea_reset_in_out(&mut rn, &mut rd, &mut ron, &mut rod) };
|
||||
assert_eq!((rn, rd), (0, 1));
|
||||
assert_eq!(ron, i32::MAX as i64);
|
||||
assert_eq!(rod, 1);
|
||||
|
||||
// Standalone workarea round-trip.
|
||||
let wa = oakengine_workarea_create();
|
||||
assert!(!wa.is_null());
|
||||
assert_eq!(unsafe { oakengine_workarea_set_range(wa, 10, 1, 20, 1) }, 0);
|
||||
assert_eq!(unsafe { oakengine_workarea_set_enabled(wa, 1) }, 0);
|
||||
let mut wn0 = -1;
|
||||
let mut wd0 = -1;
|
||||
let mut wn1 = -1;
|
||||
let mut wd1 = -1;
|
||||
let mut wen = -1;
|
||||
assert_eq!(
|
||||
unsafe { oakengine_workarea_get(wa, &mut wn0, &mut wd0, &mut wn1, &mut wd1, &mut wen) },
|
||||
0
|
||||
);
|
||||
assert_eq!((wn0, wd0, wn1, wd1, wen), (10, 1, 20, 1, 1));
|
||||
// Undoable variant on the standalone handle (pushed, not added to a parent).
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_workarea_set_range_undoable(
|
||||
wa,
|
||||
30,
|
||||
1,
|
||||
40,
|
||||
1,
|
||||
10,
|
||||
1,
|
||||
20,
|
||||
1,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_workarea_set_enabled_undoable(wa, 0, std::ptr::null_mut()) },
|
||||
0
|
||||
);
|
||||
unsafe { oakengine_workarea_free(wa) };
|
||||
|
||||
// ---- clip editing (blocks placed through oaknode::ffi directly) --------
|
||||
// The module clip has no `buffer_in` input, so footage clips cannot be
|
||||
// placed; a raw clip node is appended to the track instead.
|
||||
let track_module = to_node_handle(unsafe { (*track).handle });
|
||||
let clip = unsafe { oaknode::ffi::block::oaknode_block_clip_create() };
|
||||
assert!(!clip.ctx.is_null());
|
||||
unsafe { oaknode::ffi::block::oaknode_block_set_in(clip.clone(), 0, 1) };
|
||||
unsafe { oaknode::ffi::block::oaknode_block_set_length_and_media_in(clip.clone(), 1, 1) };
|
||||
unsafe { oaknode::ffi::block::oaknode_clip_set_media_in(clip.clone(), 0, 1) };
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oaknode::ffi::track::oaknode_track_append_block(track_module.clone(), clip.clone())
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 0, 0) }, 1);
|
||||
let mut clip_at = unsafe { oakengine_sequence_clip_at(seq, 0, 0, 0) };
|
||||
assert!(!clip_at.is_null());
|
||||
let mut cin = -1;
|
||||
let mut cout = -1;
|
||||
let mut cmi = -1;
|
||||
assert_eq!(
|
||||
unsafe { oakengine_clip_get_range(clip_at, &mut cin, &mut cout, &mut cmi) },
|
||||
0
|
||||
);
|
||||
assert_eq!((cin, cout, cmi), (0, 30, 0));
|
||||
|
||||
// Generic block traversal.
|
||||
assert_eq!(unsafe { oakengine_track_block_count(track) }, 1);
|
||||
let blk = unsafe { oakengine_track_block_at(track, 0) };
|
||||
assert!(!blk.is_null());
|
||||
assert_eq!(unsafe { oakengine_block_is_gap(blk) }, 0);
|
||||
let mut bin = -1;
|
||||
let mut bout = -1;
|
||||
assert_eq!(
|
||||
unsafe { oakengine_block_get_range(blk, &mut bin, &mut bout) },
|
||||
0
|
||||
);
|
||||
assert_eq!((bin, bout), (0, 30));
|
||||
assert!(!unsafe { oakengine_block_get_track(blk) }.is_null());
|
||||
assert!(unsafe { oakengine_block_next(blk) }.is_null());
|
||||
assert!(unsafe { oakengine_block_prev(blk) }.is_null());
|
||||
assert_eq!(unsafe { oakengine_block_link_count(blk) }, 0);
|
||||
|
||||
// Clip enable toggling (undoable, one command).
|
||||
assert_eq!(unsafe { oakengine_clip_toggle_enabled(&mut clip_at, 1) }, 1);
|
||||
assert_eq!(unsafe { oakengine_clip_is_enabled(clip_at) }, 0);
|
||||
assert_eq!(unsafe { oakengine_block_set_enabled(blk, 1) }, 0);
|
||||
assert_eq!(unsafe { oakengine_block_is_enabled(blk) }, 1);
|
||||
|
||||
// Block resize (undoable): length 30 -> 40, in stays.
|
||||
assert_eq!(
|
||||
unsafe { oakengine_block_set_length_and_media_out(blk, 40) },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_block_get_range(blk, &mut bin, &mut bout) },
|
||||
0
|
||||
);
|
||||
assert_eq!((bin, bout), (0, 40));
|
||||
|
||||
// Split at 20 -> two clips (the module's split lists the right half
|
||||
// first: clip0 = [20, 40)).
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_split_clip(seq, 0, 0, 0, 20) },
|
||||
0
|
||||
);
|
||||
assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 0, 0) }, 2);
|
||||
// Split outside the clip -> E_INVALID and a last-error.
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_split_clip(seq, 0, 0, 0, 100) },
|
||||
-1
|
||||
);
|
||||
let mut err = [0 as c_char; 256];
|
||||
let elen = unsafe { oakengine_sequence_last_error(err.as_mut_ptr(), 256) };
|
||||
assert!(
|
||||
elen > 0,
|
||||
"last_error must be non-empty after a failed split"
|
||||
);
|
||||
|
||||
// Trim the first clip [20, 40) to [25, 35).
|
||||
let clip0 = unsafe { oakengine_sequence_clip_at(seq, 0, 0, 0) };
|
||||
assert!(!clip0.is_null());
|
||||
assert_eq!(unsafe { oakengine_clip_trim(clip0, 25, 35) }, 0);
|
||||
let mut cin2 = -1;
|
||||
let mut cout2 = -1;
|
||||
let mut cmi2 = -1;
|
||||
assert_eq!(
|
||||
unsafe { oakengine_clip_get_range(clip0, &mut cin2, &mut cout2, &mut cmi2) },
|
||||
0
|
||||
);
|
||||
assert_eq!((cin2, cout2), (25, 35));
|
||||
|
||||
// Trim clips to a point on every unlocked track (edge 0 = in).
|
||||
assert!(unsafe { oakengine_sequence_trim_clips_to(seq, 0, 25) } >= 0);
|
||||
|
||||
// Move the clip: a time-only move within the addressed track (the capi's
|
||||
// gap+place assembly, one undoable entry). clip0 = [25, 35) -> [50, 60)
|
||||
// (length and media-in kept), fully reversible.
|
||||
let base_move = unsafe { oakengine_undo_index() };
|
||||
assert_eq!(unsafe { oakengine_sequence_move_clip(seq, 0, 0, 0, 50) }, 0);
|
||||
let mut mv_in = -1i64;
|
||||
let mut mv_out = -1i64;
|
||||
let mut mv_mi = -1i64;
|
||||
assert_eq!(
|
||||
unsafe { oakengine_clip_get_range(clip0, &mut mv_in, &mut mv_out, &mut mv_mi) },
|
||||
0
|
||||
);
|
||||
assert_eq!((mv_in, mv_out, mv_mi), (50, 60, 25));
|
||||
assert_eq!(unsafe { oakengine_undo_jump(base_move) }, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_clip_get_range(clip0, &mut mv_in, &mut mv_out, &mut mv_mi) },
|
||||
0
|
||||
);
|
||||
assert_eq!((mv_in, mv_out), (25, 35));
|
||||
assert_eq!(unsafe { oakengine_undo_jump(base_move + 1) }, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_clip_get_range(clip0, &mut mv_in, &mut mv_out, &mut mv_mi) },
|
||||
0
|
||||
);
|
||||
assert_eq!((mv_in, mv_out), (50, 60));
|
||||
// Back to the pre-move state (clip0 = [25, 35)) for the ripple deletes
|
||||
// below.
|
||||
assert_eq!(unsafe { oakengine_undo_jump(base_move) }, 0);
|
||||
// Invalid arguments -> E_INVALID; nonexistent clip -> E_NOT_FOUND.
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_move_clip(seq, 0, 0, 0, -1) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_move_clip(seq, 0, 9, 0, 50) },
|
||||
-4
|
||||
);
|
||||
|
||||
// Ripple delete the addressed clip.
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_ripple_delete_clip(seq, 0, 0, 0) },
|
||||
0
|
||||
);
|
||||
|
||||
// Ripple delete a range on every track.
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_ripple_delete_range(seq, 0, 10) },
|
||||
0
|
||||
);
|
||||
|
||||
// Linked clips: two fresh clips linked then unlinked.
|
||||
let clip_b = unsafe { oaknode::ffi::block::oaknode_block_clip_create() };
|
||||
unsafe { oaknode::ffi::block::oaknode_block_set_in(clip_b.clone(), 0, 1) };
|
||||
unsafe { oaknode::ffi::block::oaknode_block_set_length_and_media_in(clip_b.clone(), 1, 3) };
|
||||
unsafe { oaknode::ffi::block::oaknode_clip_set_media_in(clip_b.clone(), 0, 1) };
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oaknode::ffi::track::oaknode_track_append_block(track_module.clone(), clip_b.clone())
|
||||
},
|
||||
0
|
||||
);
|
||||
let clip_b_engine = unsafe { oakengine_sequence_clip_at(seq, 0, 0, 0) };
|
||||
assert!(!clip_b_engine.is_null());
|
||||
let mut clips = [clip_b_engine, clip_at];
|
||||
assert_eq!(
|
||||
unsafe { oakengine_clip_toggle_enabled(clips.as_mut_ptr(), 2) },
|
||||
2
|
||||
);
|
||||
assert_eq!(unsafe { oakengine_clip_are_linked(clips[0], clips[1]) }, 0);
|
||||
|
||||
// ---- default nodes + track removal ----------------------------------
|
||||
// Add one video + one audio track as one command.
|
||||
assert_eq!(unsafe { oakengine_sequence_add_default_nodes(seq) }, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_track_count(seq, &mut video, &mut audio, &mut subtitle) },
|
||||
0
|
||||
);
|
||||
assert_eq!(video, 2);
|
||||
assert_eq!(audio, 1);
|
||||
|
||||
// Remove the audio track.
|
||||
assert_eq!(unsafe { oakengine_sequence_remove_track(seq, 1, 0) }, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_track_count(seq, &mut video, &mut audio, &mut subtitle) },
|
||||
0
|
||||
);
|
||||
assert_eq!(audio, 0);
|
||||
|
||||
// Delete empty tracks (the extra video track is empty).
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_delete_empty_tracks(seq, -1) },
|
||||
1
|
||||
);
|
||||
|
||||
// ---- cleanup ----------------------------------------------------------
|
||||
unsafe { oakengine_project_free(project) };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Non-mutating failure paths (no undo-stack access; run in parallel)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// NULL handles yield the header's documented values/codes.
|
||||
#[test]
|
||||
fn timeline_failure_paths() {
|
||||
common::force_link();
|
||||
|
||||
// NULL sequence.
|
||||
let mut buf = [0 as c_char; 64];
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_name(std::ptr::null(), buf.as_mut_ptr(), 64) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_add_track(std::ptr::null_mut(), 0) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_marker_count(std::ptr::null()) },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_marker_add(std::ptr::null_mut(), 0, c"x".as_ptr()) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_workarea_is_enabled(std::ptr::null()) },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_get_preview_divider(std::ptr::null()) },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_get_video_auto_cache(std::ptr::null()) },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_set_playhead(std::ptr::null_mut(), 0) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_set_workarea(std::ptr::null_mut(), 1, 0, 10) },
|
||||
-1
|
||||
);
|
||||
assert!(unsafe { oakengine_sequence_track_at(std::ptr::null(), 0, 0) }.is_null());
|
||||
assert!(unsafe { oakengine_sequence_track_list(std::ptr::null_mut(), 0) }.is_null());
|
||||
assert!(unsafe { oakengine_sequence_clip_at(std::ptr::null_mut(), 0, 0, 0) }.is_null());
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_clip_count(std::ptr::null_mut(), 0, 0) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_ripple_delete_clip(std::ptr::null_mut(), 0, 0, 0) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_ripple_delete_range(std::ptr::null_mut(), 0, 10) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_remove_track(std::ptr::null_mut(), 0, 0) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_sequence_add_default_nodes(std::ptr::null_mut()) },
|
||||
-1
|
||||
);
|
||||
|
||||
// NULL clip / block handles.
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_clip_get_range(
|
||||
std::ptr::null(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_clip_trim(std::ptr::null_mut(), 0, 10) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(unsafe { oakengine_clip_is_enabled(std::ptr::null()) }, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_clip_are_linked(std::ptr::null(), std::ptr::null()) },
|
||||
0
|
||||
);
|
||||
assert!(unsafe { oakengine_clip_get_sequence(std::ptr::null()) }.is_null());
|
||||
assert_eq!(unsafe { oakengine_track_block_count(std::ptr::null()) }, -1);
|
||||
assert!(unsafe { oakengine_track_block_at(std::ptr::null(), 0) }.is_null());
|
||||
assert!(unsafe { oakengine_track_visible_block_at_time(std::ptr::null_mut(), 0) }.is_null());
|
||||
assert!(unsafe { oakengine_block_get_track(std::ptr::null()) }.is_null());
|
||||
assert!(unsafe { oakengine_block_next(std::ptr::null()) }.is_null());
|
||||
assert!(unsafe { oakengine_block_prev(std::ptr::null()) }.is_null());
|
||||
assert_eq!(unsafe { oakengine_block_is_gap(std::ptr::null()) }, 0);
|
||||
assert_eq!(unsafe { oakengine_block_is_enabled(std::ptr::null()) }, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_block_set_enabled(std::ptr::null_mut(), 1) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(unsafe { oakengine_block_link_count(std::ptr::null()) }, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_block_set_length_and_media_out(std::ptr::null_mut(), 10) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(unsafe { oakengine_track_type(std::ptr::null()) }, -1);
|
||||
assert_eq!(unsafe { oakengine_node_is_block(std::ptr::null()) }, 0);
|
||||
assert_eq!(unsafe { oakengine_node_is_transition(std::ptr::null()) }, 0);
|
||||
|
||||
// Marker handle family NULL paths.
|
||||
assert_eq!(unsafe { oakengine_marker_list_count(std::ptr::null()) }, 0);
|
||||
assert!(unsafe { oakengine_marker_list_at(std::ptr::null(), 0) }.is_null());
|
||||
assert!(unsafe { oakengine_marker_list_marker_at_time(std::ptr::null(), 1, 1) }.is_null());
|
||||
assert_eq!(
|
||||
unsafe { oakengine_marker_list_add(std::ptr::null_mut(), 0, 1, 0, 1, c"x".as_ptr(), 0) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_marker_get_time(
|
||||
std::ptr::null(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_marker_get_name(std::ptr::null(), std::ptr::null_mut(), 0) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(unsafe { oakengine_marker_get_color(std::ptr::null()) }, -1);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_marker_has_sibling_at_time(std::ptr::null(), 1, 1) },
|
||||
0
|
||||
);
|
||||
assert_eq!(unsafe { oakengine_marker_remove(std::ptr::null_mut()) }, -1);
|
||||
assert!(unsafe { oakengine_marker_set_time_command(std::ptr::null_mut(), 1, 1) }.is_null());
|
||||
assert_eq!(
|
||||
unsafe { oakengine_marker_set_time_live(std::ptr::null_mut(), 0, 1, 0, 1) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_marker_commit_time(
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
-1
|
||||
);
|
||||
|
||||
// Workarea handle NULL paths.
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_workarea_get(
|
||||
std::ptr::null(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_workarea_set_range(std::ptr::null_mut(), 0, 1, 1, 1) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_workarea_set_enabled(std::ptr::null_mut(), 1) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_workarea_set_range_undoable(
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_workarea_set_enabled_undoable(std::ptr::null_mut(), 1, std::ptr::null_mut())
|
||||
},
|
||||
-1
|
||||
);
|
||||
|
||||
// Multicam NULL path.
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_multicam_switch_source(
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
0,
|
||||
0.0,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
-1
|
||||
);
|
||||
}
|
||||
|
||||
/// Footage clips need the footage node connected to the clip's `buffer_in`
|
||||
/// input, which module clips do not declare; the placement is expected to
|
||||
/// fail with a non-empty last error.
|
||||
#[test]
|
||||
#[ignore = "module clips have no buffer input; footage clips cannot be placed"]
|
||||
fn footage_clip_placement() {
|
||||
common::force_link();
|
||||
|
||||
let project = oakengine_project_create();
|
||||
assert!(!project.is_null());
|
||||
assert_eq!(unsafe { oakengine_project_new(project) }, 0);
|
||||
let seq = unsafe { oakengine_sequence_new(project, c"FC".as_ptr()) };
|
||||
assert!(!seq.is_null());
|
||||
assert_eq!(unsafe { oakengine_sequence_add_track(seq, 0) }, 0);
|
||||
|
||||
// A footage node created through oaknode::ffi directly (no media needed).
|
||||
let footage = unsafe {
|
||||
oaknode::ffi::footage::oaknode_footage_create(
|
||||
to_node_handle((*project).handle),
|
||||
c"/no/such/media.mp4".as_ptr(),
|
||||
)
|
||||
};
|
||||
assert!(!footage.ctx.is_null());
|
||||
let node_box = unsafe { box_handle::<OakEngineNode>(to_facade_handle(footage)) };
|
||||
let footage_handle = unsafe { oakengine_footage_borrow(node_box) };
|
||||
assert!(!footage_handle.is_null());
|
||||
|
||||
let clip = unsafe { oakengine_sequence_add_footage_clip(seq, footage_handle, 0, 0, 0, 30, 0) };
|
||||
assert!(clip.is_null());
|
||||
let mut err = [0 as c_char; 256];
|
||||
let elen = unsafe { oakengine_sequence_last_error(err.as_mut_ptr(), 256) };
|
||||
assert!(
|
||||
elen > 0,
|
||||
"last_error must explain the failed footage placement"
|
||||
);
|
||||
|
||||
unsafe { oakengine_project_free(project) };
|
||||
}
|
||||
Reference in New Issue
Block a user