diff --git a/Cargo.lock b/Cargo.lock index d995dc82d..8ce8f813f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4823,30 +4823,6 @@ dependencies = [ name = "oakcore-rs" version = "0.1.0" -[[package]] -name = "oakengine" -version = "0.1.0" -dependencies = [ - "libc", - "oakaudio", - "oakcodec", - "oakcommon", - "oakcore-rs", - "oaknode", - "oakplugin", - "oakrender", - "oakstorage", - "oaktask", - "oaktimeline", - "oakundo", - "parking_lot", - "sea-orm", - "serde", - "serde_json", - "thiserror 2.0.20", - "tokio", -] - [[package]] name = "oakffmpeg-link" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 4441867ac..b89d30196 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,20 +26,17 @@ # workspace root, exactly as before the monorepo workspace existed. [workspace] members = ["crates/*"] -exclude = ["gpui"] +exclude = ["gpui", "crates/oakengine.bk"] # NOTE: oakstorage (crates/oakstorage) is a workspace member but NOT a # default member (it stays out of the default-members test matrix to keep # `cargo test` at the root fast; the app links it as a normal path # dependency, so it builds with the app). Build/test it explicitly with # `cargo test -p oakstorage`. -# NOTE: `crates/oakengine` is deliberately NOT a default member (it stays a -# workspace member, so `cargo test -p oakengine` / `cargo test --workspace` -# still run its tests): it is the plugin/external-consumer cdylib (M14 R4) — -# no crate in the workspace links it (app/cli/worker call the module rlibs -# directly), so default builds skip it. Its in-crate unit tests -# (src/test_support/, the former tests/*.rs) share temp files and -# process-global facade state, which makes the parallel default-members run -# flaky. +# NOTE: `crates/oakengine` (the frozen C-ABI facade cdylib) is retired: +# every consumer (app/cli/worker/plugins) links the module rlibs +# directly, so nothing in the workspace referenced it. The sources are +# kept at crates/oakengine.bk (excluded from the workspace) as a +# reference snapshot; git history is the authoritative backup. default-members = [".", "crates/oak-cli", "crates/oak-worker"] resolver = "2" diff --git a/crates/oakcommon/src/lib.rs b/crates/oakcommon/src/lib.rs index d152d75d9..5466dc4bc 100644 --- a/crates/oakcommon/src/lib.rs +++ b/crates/oakcommon/src/lib.rs @@ -29,7 +29,6 @@ pub mod debug; pub mod error; pub mod ffmpegutils; pub mod filefunctions; -pub mod handle; pub mod miscutils; pub mod ocioutils; pub mod oiioutils; diff --git a/crates/oakcommon/tests/contract.rs b/crates/oakcommon/tests/contract.rs index 713cd831c..b32712658 100644 --- a/crates/oakcommon/tests/contract.rs +++ b/crates/oakcommon/tests/contract.rs @@ -27,7 +27,6 @@ use oakcommon::error::{ OAKCOMMON_E_STATE, OAKCOMMON_OK, }; use oakcommon::ffmpegutils::{RGBA_CHANNEL_COUNT, RGB_CHANNEL_COUNT}; -use oakcommon::handle::{CHandle, OAKCOMMON_ABI_VERSION}; use oakcommon::miscutils::{DropWorkflowBehavior, LoopMode, DECIBEL_MINIMUM}; use oakcommon::ocioutils::PixelFormat; use oakcommon::videoparams::{ColorRange, Interlacing, VideoType}; @@ -44,21 +43,9 @@ fn error_codes_match_header() { } /// Handle ABI version must match `include/common/handle.h`. -#[test] -fn handle_abi_version() { - assert_eq!(OAKCOMMON_ABI_VERSION, 1); -} /// The handle struct must be a plain `{ctx, addref, release, abi_version}` /// `#[repr(C)]` record: 3 pointers + a u32, padded to pointer alignment. -#[test] -fn handle_layout() { - let ptr = size_of::<*const ()>(); - let align = align_of::<*const ()>(); - let expected = (3 * ptr + size_of::()).div_ceil(align) * align; - assert_eq!(size_of::(), expected); - assert_eq!(align_of::(), align); -} /// Pixel-format codes must match `olive::core::PixelFormat`. #[test] @@ -124,28 +111,3 @@ fn color_range_discriminants() { assert_eq!(ColorRange::Limited as i32, 0); assert_eq!(ColorRange::Full as i32, 1); } - -/// The public type names must exist and be usable at their intended ABI -/// shape (compile-time contract). -#[test] -fn public_types_exist() { - // Enums are plain C-like int enums. - let _ = PixelFormat::U8; - let _ = Interlacing::TopFirst; - let _ = VideoType::Still; - let _ = ColorRange::Full; - let _ = LoopMode::Loop; - let _ = DropWorkflowBehavior::Ask; - - // The handle is a plain struct constructible without a panic. - let h = CHandle { - ctx: std::ptr::null_mut(), - addref: None, - release: None, - abi_version: 0, - }; - assert!(h.ctx.is_null()); - assert!(h.addref.is_none()); - assert!(h.release.is_none()); - assert_eq!(h.abi_version, 0); -} diff --git a/crates/oakengine.bk/Cargo.lock b/crates/oakengine.bk/Cargo.lock deleted file mode 100644 index d8ea00f60..000000000 --- a/crates/oakengine.bk/Cargo.lock +++ /dev/null @@ -1,1745 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" -dependencies = [ - "memchr", -] - -[[package]] -name = "android_system_properties" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" -dependencies = [ - "libc", -] - -[[package]] -name = "arrayvec" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" - -[[package]] -name = "ash" -version = "0.38.0+1.3.281" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" -dependencies = [ - "libloading", -] - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.13.1", - "cexpr", - "clang-sys", - "itertools", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.3", - "shlex 1.3.0", - "syn 2.0.119", -] - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" -dependencies = [ - "serde_core", -] - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "bytemuck" -version = "1.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - -[[package]] -name = "cc" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" -dependencies = [ - "find-msvc-tools", - "shlex 2.0.1", -] - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" - -[[package]] -name = "clang-sys" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - -[[package]] -name = "codespan-reporting" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" -dependencies = [ - "serde", - "termcolor", - "unicode-width", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics-types" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "document-features" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] - -[[package]] -name = "either" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "fax" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" - -[[package]] -name = "ffmpeg-next" -version = "9.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6380599799e175191eb7ffe82c97f36a2a90a36cbc54c738a903e5287d7f516a" -dependencies = [ - "bitflags 2.13.1", - "ffmpeg-sys-next", - "libc", -] - -[[package]] -name = "ffmpeg-sys-next" -version = "9.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b939bf79dd5949412a4b81cfe21a07f48ea21b47fcbb5f57816c8c2de5ae30b" -dependencies = [ - "bindgen", - "cc", - "libc", - "num_cpus", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "futures-core" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" - -[[package]] -name = "futures-task" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" - -[[package]] -name = "futures-util" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "gl_generator" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" -dependencies = [ - "khronos_api", - "log", - "xml-rs", -] - -[[package]] -name = "glob" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" - -[[package]] -name = "glow" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" -dependencies = [ - "js-sys", - "slotmap", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "glutin_wgl_sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" -dependencies = [ - "gl_generator", -] - -[[package]] -name = "gpu-alloc" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45cf04b2726f02df5508c6de726acdc90cdf97ac771a9a0ffd8ba10a6e696bf9" -dependencies = [ - "bitflags 2.13.1", - "gpu-alloc-types", -] - -[[package]] -name = "gpu-alloc-types" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2bbed164dd10ed526c2e4fe3e721ca4a71c61730e5aafac6844b417b3227058" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "gpu-allocator" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" -dependencies = [ - "log", - "presser", - "thiserror 1.0.69", - "windows", -] - -[[package]] -name = "gpu-descriptor" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" -dependencies = [ - "bitflags 2.13.1", - "gpu-descriptor-types", - "hashbrown 0.15.5", -] - -[[package]] -name = "gpu-descriptor-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "num-traits", - "zerocopy", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hexf-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" - -[[package]] -name = "image" -version = "0.25.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" -dependencies = [ - "bytemuck", - "byteorder-lite", - "moxcms", - "num-traits", - "tiff", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.119", -] - -[[package]] -name = "js-sys" -version = "0.3.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "khronos-egl" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" -dependencies = [ - "libc", - "libloading", - "pkg-config", -] - -[[package]] -name = "khronos_api" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" - -[[package]] -name = "libc" -version = "0.2.189" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "metal" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" -dependencies = [ - "bitflags 2.13.1", - "block", - "core-graphics-types", - "foreign-types", - "log", - "objc", - "paste", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "moxcms" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" -dependencies = [ - "num-traits", - "pxfm", -] - -[[package]] -name = "naga" -version = "25.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632" -dependencies = [ - "arrayvec", - "bit-set", - "bitflags 2.13.1", - "cfg_aliases", - "codespan-reporting", - "half", - "hashbrown 0.15.5", - "hexf-parse", - "indexmap", - "log", - "num-traits", - "once_cell", - "rustc-hash 1.1.0", - "spirv", - "strum", - "thiserror 2.0.20", - "unicode-ident", -] - -[[package]] -name = "ndk-sys" -version = "0.5.0+25.2.9519653" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" -dependencies = [ - "jni-sys 0.3.1", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "oakaudio" -version = "0.1.0" -dependencies = [ - "oakcodec", - "oakcommon", - "oakcore-rs", -] - -[[package]] -name = "oakcodec" -version = "0.1.0" -dependencies = [ - "ffmpeg-next", - "oakcore-rs", -] - -[[package]] -name = "oakcommon" -version = "0.1.0" -dependencies = [ - "image", - "log", - "oakcore-rs", - "ocio-rs", - "quick-xml", -] - -[[package]] -name = "oakcore-rs" -version = "0.1.0" - -[[package]] -name = "oakengine" -version = "0.1.0" -dependencies = [ - "libc", - "oakaudio", - "oakcodec", - "oakcommon", - "oakcore-rs", - "oaknode", - "oakplugin", - "oakrender", - "oaktask", - "oaktimeline", - "oakundo", - "serde", - "serde_json", -] - -[[package]] -name = "oaknode" -version = "0.1.0" -dependencies = [ - "oakcodec", - "oakcommon", - "oakcore-rs", - "oakundo", -] - -[[package]] -name = "oakotio" -version = "0.1.0" -dependencies = [ - "oakcore-rs", - "quick-xml", - "serde", - "serde_json", -] - -[[package]] -name = "oakplugin" -version = "0.1.0" -dependencies = [ - "cc", - "oakcore-rs", - "oaknode", - "oakrender", - "oakundo", -] - -[[package]] -name = "oakrender" -version = "0.1.0" -dependencies = [ - "oakcommon", - "oakcore-rs", - "ocio-rs", - "wgpu", -] - -[[package]] -name = "oaktask" -version = "0.1.0" -dependencies = [ - "oakcodec", - "oakcommon", - "oakcore-rs", - "oaknode", - "oakotio", - "oakrender", - "oaktimeline", - "oakundo", -] - -[[package]] -name = "oaktimeline" -version = "0.1.0" -dependencies = [ - "oakcommon", - "oakcore-rs", - "oaknode", - "oakundo", -] - -[[package]] -name = "oakundo" -version = "0.1.0" -dependencies = [ - "oakcore-rs", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - -[[package]] -name = "ocio-rs" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3492534019b59e29dba06014f907dd12824537ed4d293d4108c4bfc669de7fd" -dependencies = [ - "ocio-sys", - "thiserror 1.0.69", -] - -[[package]] -name = "ocio-sys" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63251d72d848de5eda39d59cd6490260cf031738ebd518ea37d76b5aae614ec" -dependencies = [ - "cc", - "cmake", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "ordered-float" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" -dependencies = [ - "num-traits", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "portable-atomic" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" - -[[package]] -name = "presser" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "profiling" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" - -[[package]] -name = "pxfm" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" - -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - -[[package]] -name = "quick-xml" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "range-alloc" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "regex" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "renderdoc-sys" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "indexmap", - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "simd-adler32" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "slotmap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" - -[[package]] -name = "spirv" -version = "0.3.0+sdk-1.3.268.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.119", -] - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" -dependencies = [ - "thiserror-impl 2.0.20", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "tiff" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" -dependencies = [ - "fax", - "flate2", - "half", - "quick-error", - "weezl", - "zune-jpeg", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasm-bindgen" -version = "0.2.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.119", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "web-sys" -version = "0.3.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "weezl" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" - -[[package]] -name = "wgpu" -version = "25.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8fb398f119472be4d80bc3647339f56eb63b2a331f6a3d16e25d8144197dd9" -dependencies = [ - "arrayvec", - "bitflags 2.13.1", - "cfg_aliases", - "document-features", - "hashbrown 0.15.5", - "js-sys", - "log", - "naga", - "parking_lot", - "portable-atomic", - "profiling", - "raw-window-handle", - "smallvec", - "static_assertions", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-core" -version = "25.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7b882196f8368511d613c6aeec80655160db6646aebddf8328879a88d54e500" -dependencies = [ - "arrayvec", - "bit-set", - "bit-vec", - "bitflags 2.13.1", - "cfg_aliases", - "document-features", - "hashbrown 0.15.5", - "indexmap", - "log", - "naga", - "once_cell", - "parking_lot", - "portable-atomic", - "profiling", - "raw-window-handle", - "rustc-hash 1.1.0", - "smallvec", - "thiserror 2.0.20", - "wgpu-core-deps-apple", - "wgpu-core-deps-emscripten", - "wgpu-core-deps-windows-linux-android", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-core-deps-apple" -version = "25.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd488b3239b6b7b185c3b045c39ca6bf8af34467a4c5de4e0b1a564135d093d" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-core-deps-emscripten" -version = "25.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09ad7aceb3818e52539acc679f049d3475775586f3f4e311c30165cf2c00445" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-core-deps-windows-linux-android" -version = "25.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cba5fb5f7f9c98baa7c889d444f63ace25574833df56f5b817985f641af58e46" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-hal" -version = "25.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f968767fe4d3d33747bbd1473ccd55bf0f6451f55d733b5597e67b5deab4ad17" -dependencies = [ - "android_system_properties", - "arrayvec", - "ash", - "bit-set", - "bitflags 2.13.1", - "block", - "bytemuck", - "cfg-if", - "cfg_aliases", - "core-graphics-types", - "glow", - "glutin_wgl_sys", - "gpu-alloc", - "gpu-allocator", - "gpu-descriptor", - "hashbrown 0.15.5", - "js-sys", - "khronos-egl", - "libc", - "libloading", - "log", - "metal", - "naga", - "ndk-sys", - "objc", - "ordered-float", - "parking_lot", - "portable-atomic", - "profiling", - "range-alloc", - "raw-window-handle", - "renderdoc-sys", - "smallvec", - "thiserror 2.0.20", - "wasm-bindgen", - "web-sys", - "wgpu-types", - "windows", - "windows-core", -] - -[[package]] -name = "wgpu-types" -version = "25.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aa49460c2a8ee8edba3fca54325540d904dd85b2e086ada762767e17d06e8bc" -dependencies = [ - "bitflags 2.13.1", - "bytemuck", - "js-sys", - "log", - "thiserror 2.0.20", - "web-sys", -] - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "windows" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" -dependencies = [ - "windows-core", - "windows-targets", -] - -[[package]] -name = "windows-core" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-result", - "windows-strings", - "windows-targets", -] - -[[package]] -name = "windows-implement" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-interface" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result", - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "xml-rs" -version = "0.8.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" - -[[package]] -name = "zerocopy" -version = "0.8.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" - -[[package]] -name = "zune-core" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" - -[[package]] -name = "zune-jpeg" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" -dependencies = [ - "zune-core", -] diff --git a/crates/oakengine.bk/Cargo.toml b/crates/oakengine.bk/Cargo.toml deleted file mode 100644 index 3440011df..000000000 --- a/crates/oakengine.bk/Cargo.toml +++ /dev/null @@ -1,70 +0,0 @@ -[package] -name = "oakengine" -version = "0.1.0" -edition = "2021" -description = "Oak Video Editor plugin/external-C-ABI layer: the frozen oakengine_* C ABI as a pure cdylib over the module crates' direct Rust APIs" -license = "GPL-3.0-or-later" - -# Pure cdylib (M14 R4): the only consumers are OFX plugins and external -# C-ABI embedders. No Rust crate depends on this crate, so no rlib/staticlib -# artifact is produced and it stays out of the workspace default-members. -[lib] -crate-type = ["cdylib"] - -[dependencies] -# Error derive (Display + std::error::Error) for the crate error enum -# (src/error.rs). Same major version the other modules use (oakotio, ...). -thiserror = "2" - -# 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 `oakcore_audioparams_*` accessors the audio paths read through are -# implemented inside the dylib too (src/stubs.rs, module `audio`, M12 P5): -# they used to be C++ liboakcore host-provided symbols left as runtime -# lookups, which blocked Windows DLL linking. -# -# 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" } -oakrender = { path = "../oakrender" } -oaktask = { path = "../oaktask" } -oaknode = { path = "../oaknode" } -oakaudio = { path = "../oakaudio" } -oakplugin = { path = "../oakplugin" } -# Live write-through to the project library (plan M13 D2): the facade's -# storage session manager (src/storage.rs) drives the oakstorage database -# backend (DatabaseBackend::save/snapshot) straight from the undo path. -oakstorage = { path = "../oakstorage" } - -[dev-dependencies] -parking_lot = "0.12" -# 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" } -# The write-through tests (src/test_support/it_storage.rs) inspect the -# journal/snapshot rows of the temp libraries directly; sea-orm + tokio -# unify with oakstorage's own versions (same features), so the entities -# and the current-thread runtime pattern match the oakstorage tests. -sea-orm = { version = "2", features = ["sqlx-postgres", "sqlx-sqlite", "runtime-tokio"] } -tokio = { version = "1", features = ["rt", "macros"] } diff --git a/crates/oakengine.bk/README.md b/crates/oakengine.bk/README.md deleted file mode 100644 index 42cd5eca2..000000000 --- a/crates/oakengine.bk/README.md +++ /dev/null @@ -1,140 +0,0 @@ -# oakengine — the `liboakengine` cdylib (plugin / external C ABI) - -The frozen `oakengine_*` C ABI (`engine/include/oakengine/*.h`) as a -**pure cdylib**. This is the plugin / external-consumer layer: OFX plugins -and third-party embedders link `liboakengine` and call the C ABI. The app, -oak-cli and oak-worker do not use it anymore (M14 R4) — they link the -module crates directly as Rust rlibs. The C ABI itself is frozen: only -additive changes, plus major version bumps. - -Downward, every export is a direct Rust call into the module crates -(oakundo/oaknode/oaktimeline/oakcodec/oakaudio/oakrender/oaktask/ -oakcommon/oakplugin/oakstorage/oakcore-rs) through `src/stubs.rs` (the -rewired replacement for the deleted `bridge/`). The facade itself owns -only the engine's box/unbox, buf/size and error-code conventions; -cross-cutting state that used to live here (the process-wide undo stack, -the open undo group) has sunk into the modules (M14 R1: -`oakundo::global`). - -## Architecture - -``` -src/ - lib.rs crate docs, module list, test-only test_link - error.rs OAKENGINE error codes (module 00 → -1..-6) - handle.rs CHandle mirror, OakEngine* opaque wrappers, box/unbox, - catch_unwind guards, buf/size string helpers - stubs.rs direct-Rust shims replacing the deleted bridge/ (the - engine's only downward path to the modules) - linkage.rs #[used] anchors pulling every module rlib into the cdylib - undo.rs engine/include/oakengine/undo.h (thin forward to - oakundo::global) - common.rs config.h + videoparams.h (facade-static tables + POD↔handle) - audio.rs audio.h (manager + sync + processor) - codec.rs encoding.h + exporter.h (metadata, params POD, exporter) - render.rs renderer.h + color.h + lut.h (renderer/frame/color processor) - plugin.rs plugin.h - 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) - storage.rs write-through session (project ↔ oakstorage backend) - deferred.rs documented deferrals (stub detail lives here and in the - family modules' stub bodies) - test_support/ the former tests/*.rs, now in-crate unit tests -``` - -### Dependencies - -The facade's regular dependency is `thiserror` (the `Display` + -`std::error::Error` impl for the facade error enum, `src/error.rs`). Every -module call is a compile-time Rust call into the module crate's direct API -(`src/stubs.rs`); the module crates are real dependencies, and -[`linkage`](src/linkage.rs) anchors them so their rlibs are embedded in -the `liboakengine` cdylib next to the facade's `oakengine_*` exports. The -`oakcore_audioparams_*` accessors the audio paths read through are -implemented in the dylib too (src/stubs.rs, module `audio`, M12 P5) — -they used to be C++ host symbols left as runtime lookups (macOS -`-undefined dynamic_lookup`); the cdylib now carries no undefined imports. - -### Handle mapping - -The engine headers' opaque pointers (`OakEngineNode*`, `OakEngineTrack*`, -...) are thin newtype wrappers around the module layer's `CHandle` -(`{ctx, addref, release, abi_version}`) values. A box is created by -`handle::box_handle` and freed by `handle::free_box` (release + dealloc); -consuming exports (`oakengine_*_free`, `oakengine_undo_push`, ...) free -their box, borrowed results never are. The process-wide undo stack and -open undo group live in the oakundo module (`oakundo::global`); the -undo.h exports are thin forwards over it (module 00 analogues of -`EngineCore::undo_stack()` and the C++ capi's `g_undo_group`). - -### Error codes - -Facade-local codes are -1..-6 (`OAKENGINE_E_*`); module codes pass -through **untranslated** (the -MMCCCC prefix preserves provenance, e.g. --20004 is oakundo's NOT_FOUND). String getters follow the engine buf/size -convention: the return value is the length excluding the NUL -(`handle::string_result` converts the modules' size-including-NUL). - -## Scope - -| Family | Header | Wrapped | Notes | -|---|---|---|---| -| undo | undo.h | 37 | stack/group/command lifecycle over oakundo (`oakundo::global` + undocommand) + Qt leftovers (update_actions/actions → no-op/NULL) | -| common | config.h, videoparams.h | 34 | config over oakcommon; videoparams static tables ported from `engine/render/videoparams.cpp` | -| audio | audio.h | 26 | manager + sync; processor convert/output_params stubbed (interface mismatch) | -| plugin | plugin.h | 4 | callbacks are facade state; push_button stubbed (no module API) | -| codec | encoding.h, exporter.h | 81/85 | metadata family over oakcodec (`include/codec/format.h`); params handle is a facade box over the `oakcodec_encoding_params` POD; the exporter family drives the export task synchronously (integration-tested, real mp4 via FFmpeg); presets/load-save deferred | -| 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 | -| node | node.h, project.h, footage.h | 226/327 | the node graph, project and footage families over the oaknode crate; 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; 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`](src/deferred.rs) and in the -stub bodies' doc 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 - -The crate is cdylib-only (no rlib artifact), so integration tests cannot -link it as a crate; the former `tests/*.rs` moved into `src/test_support/` -and run as in-crate unit tests (pulled in from `src/lib.rs` under -`#[cfg(test)]`), addressing the modules through `crate::*`. The module -crates are real dependencies, so the test binary statically links the same -rlibs the cdylib embeds: - -- `src/linkage.rs` (always-on) anchors the module rlibs into the cdylib - for `cargo build`; the test-only `test_link` module in `src/lib.rs` - forces the oakrender/oaknode/oaktimeline/oaktask rlibs into the lib - unit-test binary, and `src/test_support/common/mod.rs::force_link` - covers the test-support module (same symbol list, so the anchor paths - stay proven against the current module layouts). -- Tests that touch process-wide state (the audio manager, the undo stack, - the task manager) take a shared serialization lock instead of relying - on the process isolation the old integration tests had - (`test_support/common::with_manager`, the per-family `SERIAL` mutexes). - -The smoke tests exercise the module crates' real implementations. Where a -wrapped family needs module behavior the crates do not implement yet, the -engine function is a documented stub with its reason (see `deferred.rs`). - -``` -cargo test # in-crate unit tests (undo, common, audio, codec, - # exporter, plugin, render, node, timeline, task, - # library/storage families) -cargo build # liboakengine cdylib embeds the module rlibs + the - # folded-in oakcore_audioparams_* accessors (no - # undefined imports) -``` - -## FFI discipline - -Every export goes through a `catch_unwind` guard -(`handle::guard*`); `*_free` is a NULL no-op; strings use the two-stage -buf/size convention; module error codes pass through untranslated; -handles are refcounted module values wrapped in opaque boxes. diff --git a/crates/oakengine.bk/build.rs b/crates/oakengine.bk/build.rs deleted file mode 100644 index db5423d8a..000000000 --- a/crates/oakengine.bk/build.rs +++ /dev/null @@ -1,60 +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 . - -//! Build-time link configuration for the `liboakengine` cdylib. -//! -//! The dylib carries the module crates themselves (their direct-Rust -//! code, kept in the link by `src/linkage.rs` — see Cargo.toml). The -//! `oakcore_audioparams_*` accessors the audio -//! paths read through used to be host-provided C++ liboakcore symbols, -//! left as runtime lookups via `-undefined,dynamic_lookup`; M12 P5 -//! implemented them inside the dylib (src/stubs.rs, module `audio`), so -//! no undefined imports remain except system frameworks/libc++, and the -//! cdylib links on every platform (Windows DLLs reject undefined symbols, -//! which was the blocker). - -fn main() { - let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); - if os == "macos" { - // The static FFmpeg's transitive system deps (libz etc.) are - // recorded as `@rpath/libz.1.dylib`; the dylib itself carries the - // rpath so standalone binaries (and the packaged app) resolve - // them without extra host rpaths. - println!("cargo:rustc-cdylib-link-arg=-Wl,-rpath,/usr/lib"); - } - if os == "macos" || os == "linux" { - // The dlsym codec bridge (M12 P0) resolves `oakcodec_*` from the - // 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` (a no-op for the - // cdylib link itself, and not emitted on Windows where the flag is - // meaningless and would break the DLL link). - println!("cargo:rustc-link-arg=-Wl,-export_dynamic"); - } - if os == "macos" { - // The bundled OpenColorIO's macOS system monitor references - // IOKit / ColorSync / CoreGraphics display APIs; link them for - // the cdylib link and the unit-test binary (which statically - // pulls the same OCIO rlib). - for fw in ["IOKit", "ColorSync", "CoreGraphics"] { - println!("cargo:rustc-link-arg=-framework"); - println!("cargo:rustc-link-arg={fw}"); - } - } -} diff --git a/crates/oakengine.bk/include/audio/error.h b/crates/oakengine.bk/include/audio/error.h deleted file mode 100644 index 890908298..000000000 --- a/crates/oakengine.bk/include/audio/error.h +++ /dev/null @@ -1,59 +0,0 @@ -/*** - - Oak Video Editor - Non-Linear Video Editor - Copyright (C) 2026 Oak Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#ifndef OAK_EDITOR_AUDIO_ERROR_H -#define OAK_EDITOR_AUDIO_ERROR_H - -/** - * @brief Current ABI version stamped into every oakaudio handle. - * - * Bump whenever a handle layout or the semantics of any exported function - * change incompatibly. Consumers should compare a handle's abi_version - * field against the value they were compiled with before dereferencing - * ctx. - */ -#define OAKAUDIO_ABI_VERSION 1 - -#if defined(_WIN32) -#if defined(OAKAUDIO_BUILD) -#define OAKAUDIO_API __declspec(dllexport) -#else -#define OAKAUDIO_API __declspec(dllimport) -#endif -#else -#define OAKAUDIO_API __attribute__((visibility("default"))) -#endif - -/** - * @brief Status and error codes shared by all oakaudio C API families. - * - * Return-code convention (mirrors engine/include/oakengine/init.h): - * 0 (OAKAUDIO_OK) on success, a negative OAKAUDIO_E_* error code on - * failure. String getters return the required buffer size in bytes - * (including the terminating NUL) as a non-negative value instead. - */ -#define OAKAUDIO_OK 0 /**< Success. */ -#define OAKAUDIO_E_INVALID (-60001) /**< NULL handle or invalid argument. */ -#define OAKAUDIO_E_STATE (-60002) /**< Call not valid in the current state. */ -#define OAKAUDIO_E_FAILED (-60003) /**< The underlying operation failed. */ -#define OAKAUDIO_E_NOT_FOUND (-60004) /**< Index out of range / entry not found. */ -#define OAKAUDIO_E_NOMEM (-60005) /**< Allocation failed. */ - -#endif //OAK_EDITOR_AUDIO_ERROR_H diff --git a/crates/oakengine.bk/include/audio/levelmeter.h b/crates/oakengine.bk/include/audio/levelmeter.h deleted file mode 100644 index 928a155db..000000000 --- a/crates/oakengine.bk/include/audio/levelmeter.h +++ /dev/null @@ -1,73 +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 . - -***/ - -#ifndef OAK_EDITOR_AUDIO_LEVELMETER_H -#define OAK_EDITOR_AUDIO_LEVELMETER_H - -#include "error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file levelmeter.h - * @brief C ABI for the oakaudio level meter (olive::AudioLevelMeter): - * stateless peak/RMS/VU/LUFS analysis of planar float audio. - */ - -/** Per-channel analysis results. dB fields floor at -200. */ -typedef struct oakaudio_channel_stats { - double peak_linear; - double peak_db; - double rms_linear; - double rms_db; - double vu_db; -} oakaudio_channel_stats; - -/** Buffer-wide summary. */ -typedef struct oakaudio_meter_stats { - double max_peak_linear; - double integrated_lufs; /**< BS.1770-compatible unit (no K-weighting). */ - int silence; /**< 1 when the buffer is (near-)silent. */ -} oakaudio_meter_stats; - -/** - * @brief Analyze a planar float buffer. - * - * @param planar Per-channel float planes. - * @param channel_count Number of channels (> 0). - * @param frame_count Frames per channel (>= 0). - * @param channels Receives per-channel stats; may be NULL. - * @param channels_capacity Capacity of `channels` (must be >= - * channel_count when channels is non-NULL). - * @param summary Receives the buffer-wide summary; may be NULL. - * @return OAKAUDIO_OK or OAKAUDIO_E_INVALID. - */ -OAKAUDIO_API int oakaudio_levelmeter_analyze(const float *const *planar, - int channel_count, int frame_count, - oakaudio_channel_stats *channels, int channels_capacity, - oakaudio_meter_stats *summary); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_AUDIO_LEVELMETER_H diff --git a/crates/oakengine.bk/include/audio/manager.h b/crates/oakengine.bk/include/audio/manager.h deleted file mode 100644 index 9861511fc..000000000 --- a/crates/oakengine.bk/include/audio/manager.h +++ /dev/null @@ -1,176 +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 . - -***/ - -#ifndef OAK_EDITOR_AUDIO_MANAGER_H -#define OAK_EDITOR_AUDIO_MANAGER_H - -#include - -#include "codec/encoder.h" -#include "error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file manager.h - * @brief C ABI for the oakaudio PortAudio output/input manager - * (olive::AudioManager singleton). - * - * OakAudioManager uses the standard handle layout (see oakcommon's - * common/handle.h) but with singleton semantics: ctx points to the - * process-wide instance created by oakaudio_manager_create_instance(), so - * addref() and release() are intentionally no-ops and never destroy - * anything (mirrors oakcommon's OakCurrent). abi_version is always - * OAKAUDIO_ABI_VERSION. - * - * Device indices are PortAudio PaDeviceIndex values (-1 = paNoDevice). - * Sample formats are olive::core::SampleFormat::Format values. - */ -typedef struct OakAudioManager { - void *ctx; /**< Opaque pointer to the singleton object. */ - void (*addref)(void *ctx); /**< No-op (singleton). */ - void (*release)(void *ctx); /**< No-op (singleton). */ - uint32_t abi_version; /**< OAKAUDIO_ABI_VERSION. */ -} OakAudioManager; - -/** - * @brief Create the process-wide AudioManager (no-op when it exists). - * - * Initializes PortAudio and picks the configured/default devices. - * - * @return OAKAUDIO_OK or OAKAUDIO_E_NOMEM. - */ -OAKAUDIO_API int oakaudio_manager_create_instance(void); - -/** - * @brief Destroy the process-wide AudioManager (no-op when absent). - */ -OAKAUDIO_API void oakaudio_manager_destroy_instance(void); - -/** - * @brief Return a handle to the process-wide AudioManager. - * - * The returned handle is borrowed; addref/release are no-ops. When no - * instance exists the handle is empty (ctx == NULL) and all functions - * report OAKAUDIO_E_STATE. - */ -OAKAUDIO_API OakAudioManager oakaudio_manager_instance(void); - -/** - * @brief Release a manager handle. No-op (singleton), safe on NULL/empty. - */ -OAKAUDIO_API void oakaudio_manager_free(OakAudioManager *self); - -/** - * @brief Bytes between output-notify pulses (0 disables). - */ -OAKAUDIO_API int oakaudio_manager_set_output_notify_interval( - OakAudioManager self, int64_t bytes); - -/** - * @brief Push a block of samples to the output device, opening/restarting - * the stream when the params changed. - * - * @param rate/layout/format Stream params (ffmpeg-style layout mask, - * SampleFormat::Format int). - * @param samples Packed samples in the given format. - * @param samples_size Byte count of `samples`. - * @param error_buf/error_buf_size Optional human-readable failure detail. - * @return OAKAUDIO_OK, OAKAUDIO_E_INVALID, OAKAUDIO_E_STATE (no output - * device), or OAKAUDIO_E_FAILED (PortAudio error, see error_buf). - */ -OAKAUDIO_API int oakaudio_manager_push_to_output(OakAudioManager self, - int rate, uint64_t layout, int format, - const char *samples, int64_t samples_size, - char *error_buf, int error_buf_size); - -OAKAUDIO_API int oakaudio_manager_clear_buffered_output(OakAudioManager self); -OAKAUDIO_API int oakaudio_manager_stop_output(OakAudioManager self); - -/** - * @brief Seconds of audio consumed by the output device since the last - * reset, compensated for output latency; negative when no stream - * is running. - */ -OAKAUDIO_API int oakaudio_manager_seconds(OakAudioManager self, double *out); - -OAKAUDIO_API int oakaudio_manager_reset_output_clock(OakAudioManager self); - -/** - * @brief Current output device index, paNoDevice (-1), or a negative - * OAKAUDIO_E_* code. - */ -OAKAUDIO_API int oakaudio_manager_get_output_device(OakAudioManager self); -OAKAUDIO_API int oakaudio_manager_set_output_device(OakAudioManager self, - int device); -OAKAUDIO_API int oakaudio_manager_get_input_device(OakAudioManager self); -OAKAUDIO_API int oakaudio_manager_set_input_device(OakAudioManager self, - int device); - -/** - * @brief Close the output stream and re-initialize PortAudio. - */ -OAKAUDIO_API int oakaudio_manager_hard_reset(OakAudioManager self); - -/** - * @brief Start recording the input device to a file via the oakcodec - * encoder C ABI. - * - * `params` must describe an audio-enabled encoding; the input stream is - * always captured as interleaved 32-bit float (the only format the - * oakcodec encoder write path accepts). - * - * @return OAKAUDIO_OK, OAKAUDIO_E_STATE (no input device), or - * OAKAUDIO_E_FAILED (see error_buf). - */ -OAKAUDIO_API int oakaudio_manager_start_recording(OakAudioManager self, - const oakcodec_encoding_params *params, - char *error_buf, int error_buf_size); - -OAKAUDIO_API int oakaudio_manager_stop_recording(OakAudioManager self); - -/** - * @brief Device index named by the configuration ("AudioOutput" / - * "AudioInput"), or the default device when unset/unmatched. - * Static: valid without an instance (PortAudio must be initialized - * by an instance first; returns paNoDevice otherwise). - */ -OAKAUDIO_API int oakaudio_manager_find_config_device_by_name_s( - int is_output_device); - -/** - * @brief Device index whose name matches `name` exactly (empty name - * matches nothing, falls through to the default device). - */ -OAKAUDIO_API int oakaudio_manager_find_device_by_name_s(const char *name, - int is_output_device); - -/** - * @brief Number of live oakaudio reference-counted objects (leak check). - */ -OAKAUDIO_API int oakaudio_debug_alive_count(void); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_AUDIO_MANAGER_H diff --git a/crates/oakengine.bk/include/audio/processor.h b/crates/oakengine.bk/include/audio/processor.h deleted file mode 100644 index 547d89232..000000000 --- a/crates/oakengine.bk/include/audio/processor.h +++ /dev/null @@ -1,131 +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 . - -***/ - -#ifndef OAK_EDITOR_AUDIO_PROCESSOR_H -#define OAK_EDITOR_AUDIO_PROCESSOR_H - -#include - -#include "error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file processor.h - * @brief C ABI for the oakaudio real-time resampler/format converter - * (olive::AudioProcessor). - * - * OakAudioProcessor follows the neutral by-value handle convention (see - * oakcommon's common/handle.h): oakaudio_processor_init() returns a handle - * whose underlying object has reference count 1, the addref and release - * function pointers adjust that count atomically (release destroys the - * object at zero), and abi_version is always OAKAUDIO_ABI_VERSION. - * Functions that only use a handle take it BY VALUE; an empty handle - * (ctx == NULL) is reported as OAKAUDIO_E_INVALID. - * - * Sample formats are passed as ints matching the - * olive::core::SampleFormat::Format enum values (invalid = -1, u8_p = 0, - * s16_p, s32_p, s64_p, f32_p, f64_p, u8, s16, s32, s64, f32, f64, - * count). Channel layouts are ffmpeg-style channel masks. - */ -typedef struct OakAudioProcessor { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKAUDIO_ABI_VERSION. */ -} OakAudioProcessor; - -/** oakaudio_processor_convert() delivers planar 32-bit float output. */ -#define OAKAUDIO_PROCESSOR_OUTPUT_FORMAT 4 /**< SampleFormat::f32_p. */ - -/** - * @brief Create a closed audio processor (count 1). - * - * @return Handle with reference count 1; ctx is NULL on allocation - * failure. - */ -OAKAUDIO_API OakAudioProcessor oakaudio_processor_init(void); - -/** - * @brief Release one reference to a processor. - * - * Convenience wrapper around self->release(self->ctx); nulls self->ctx. - * No-op when self is NULL or self->ctx is NULL. - */ -OAKAUDIO_API void oakaudio_processor_free(OakAudioProcessor *self); - -/** - * @brief Open the resampling/format-conversion graph. - * - * out_format is accepted for interface completeness but the conversion - * output is always planar 32-bit float (see - * OAKAUDIO_PROCESSOR_OUTPUT_FORMAT); passing any other format returns - * OAKAUDIO_E_INVALID. A channel layout mask of 0 falls back to the - * default layout for the channel count (stereo when unknown), matching - * the C++ implementation. - * - * @param speed Tempo factor (1.0 = unchanged). - * @return OAKAUDIO_OK, OAKAUDIO_E_STATE when already open, - * OAKAUDIO_E_INVALID for bad arguments, or OAKAUDIO_E_FAILED when - * the filter graph could not be created. - */ -OAKAUDIO_API int oakaudio_processor_open(OakAudioProcessor self, - int in_rate, uint64_t in_layout, int in_format, - int out_rate, uint64_t out_layout, int out_format, double speed); - -/** - * @brief Close the graph (safe when closed; self must be non-empty). - */ -OAKAUDIO_API int oakaudio_processor_close(OakAudioProcessor self); - -/** - * @brief 1 when open, 0 when closed, OAKAUDIO_E_INVALID for empty handle. - */ -OAKAUDIO_API int oakaudio_processor_is_open(OakAudioProcessor self); - -/** - * @brief Push planar float input and pull converted output. - * - * @param in_planar Per-channel float input planes (in channel count); - * NULL with in_frame_count == 0 only pulls pending output. - * @param in_frame_count Frames per input channel. - * @param out_planar Per-channel float output planes (out channel count); - * NULL to discard/pull nothing (returns 0). - * @param out_capacity_frames Capacity of each output plane in frames. - * @return Number of output frames written (>= 0), or a negative - * OAKAUDIO_E_* code. Output is clamped to out_capacity_frames; - * remaining frames stay queued in the graph. - */ -OAKAUDIO_API int oakaudio_processor_convert(OakAudioProcessor self, - const float *const *in_planar, int in_frame_count, - float *const *out_planar, int out_capacity_frames); - -/** - * @brief Signal end-of-input to the graph (flushes internal delay). - */ -OAKAUDIO_API int oakaudio_processor_flush(OakAudioProcessor self); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_AUDIO_PROCESSOR_H diff --git a/crates/oakengine.bk/include/audio/sync.h b/crates/oakengine.bk/include/audio/sync.h deleted file mode 100644 index fdd9a630f..000000000 --- a/crates/oakengine.bk/include/audio/sync.h +++ /dev/null @@ -1,132 +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 . - -***/ - -#ifndef OAK_EDITOR_AUDIO_SYNC_H -#define OAK_EDITOR_AUDIO_SYNC_H - -#include - -#include "error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file sync.h - * @brief C ABI for the oakaudio synchronization helpers - * (olive::AudioSynchronizer and olive::AudioWaveformSync): - * stateless source-time placement and envelope-correlation offset - * estimation. - */ - -/** Result of an offset estimation. */ -typedef struct oakaudio_offset_result { - int64_t offset_samples; - double confidence; /**< 0..1 correlation score. */ - int valid; /**< 1 when an estimate was found. */ -} oakaudio_offset_result; - -/** Result of a stretch-plus-offset estimation. */ -typedef struct oakaudio_stretch_offset_result { - double rate; /**< Playback rate aligning the candidate (> 1 = speed up). */ - int64_t offset_samples; - double confidence; - int valid; -} oakaudio_stretch_offset_result; - -/** - * @brief Per-window RMS envelope of a planar float buffer (static). - * - * @return Number of envelope windows (>= 0) or a negative OAKAUDIO_E_* - * code. When out is NULL or too small, the required window count - * is returned and nothing is written. - */ -OAKAUDIO_API int oakaudio_sync_extract_rms_envelope( - const float *const *planar, int channel_count, int frame_count, - uint64_t window_samples, double *out, int capacity); - -/** - * @brief Estimate the candidate's offset against the reference by - * normalized cross-correlation of RMS envelopes. - * - * @param reference_valid/candidate_valid Optional per-window validity - * masks (NULL = all windows valid; when non-NULL the length must - * match the corresponding envelope length). - */ -OAKAUDIO_API int oakaudio_sync_estimate_envelope_offset( - const double *reference, int reference_len, - const double *candidate, int candidate_len, - const uint8_t *reference_valid, const uint8_t *candidate_valid, - uint64_t window_samples, int64_t max_offset_windows, - oakaudio_offset_result *out); - -/** - * @brief Estimate a playback-rate change plus offset aligning the - * candidate to the reference. - * - * The candidate envelope is resampled at each rate in - * [min_rate, max_rate] (step rate_step) and correlated against the - * reference. O(rates * lags * overlap); bound max_offset_windows. - */ -OAKAUDIO_API int oakaudio_sync_estimate_stretch_and_offset( - const double *reference, int reference_len, - const double *candidate, int candidate_len, - const uint8_t *reference_valid, const uint8_t *candidate_valid, - uint64_t window_samples, int64_t max_offset_windows, - double min_rate, double max_rate, double rate_step, - oakaudio_stretch_offset_result *out); - -/** One clip's source-time metadata (rational seconds). */ -typedef struct oakaudio_source_clip { - int64_t source_start_time_num; - int64_t source_start_time_den; - int64_t media_in_num; - int64_t media_in_den; - int has_source_start_time; -} oakaudio_source_clip; - -/** - * @brief Place the candidate on the timeline so its source time aligns - * with the reference clip. - * - * @param reference_timeline_in_num/den Reference clip's timeline in point. - * @param out_num/out_den Receive the candidate's timeline in point. - * @param out_valid Receives 1 when placement succeeded. - */ -OAKAUDIO_API int oakaudio_sync_place_by_source_time( - const oakaudio_source_clip *reference, - const oakaudio_source_clip *candidate, - int64_t reference_timeline_in_num, int64_t reference_timeline_in_den, - int64_t *out_num, int64_t *out_den, int *out_valid); - -/** - * @brief Timeline placement from a measured waveform offset. - */ -OAKAUDIO_API int oakaudio_sync_place_by_waveform_offset( - int64_t reference_timeline_in_num, int64_t reference_timeline_in_den, - int64_t candidate_offset_samples, int sample_rate, - int64_t *out_num, int64_t *out_den, int *out_valid); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_AUDIO_SYNC_H diff --git a/crates/oakengine.bk/include/audio/waveform.h b/crates/oakengine.bk/include/audio/waveform.h deleted file mode 100644 index 6dbb173c2..000000000 --- a/crates/oakengine.bk/include/audio/waveform.h +++ /dev/null @@ -1,179 +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 . - -***/ - -#ifndef OAK_EDITOR_AUDIO_WAVEFORM_H -#define OAK_EDITOR_AUDIO_WAVEFORM_H - -#include - -#include "error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file waveform.h - * @brief C ABI for the oakaudio visual waveform store - * (olive::AudioVisualWaveform) and whole-file waveform extraction. - * - * OakAudioWaveform follows the neutral by-value handle convention (see - * oakcommon's common/handle.h). Times are rationals as (num, den) pairs - * of int64_t in seconds; den must be non-zero. - * - * Summaries are stored as channel-interleaved min/max pairs: point p of - * channel c lives at pairs[p * channel_count + c]. This matches the - * on-disk/cache layout of the engine's waveform data (min/max float - * pairs), so the extraction output is drop-in compatible. - */ - -/** One summarized waveform point of one channel. */ -typedef struct oakaudio_min_max { - float min; - float max; -} oakaudio_min_max; - -typedef struct OakAudioWaveform { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKAUDIO_ABI_VERSION. */ -} OakAudioWaveform; - -/** - * @brief Create an empty waveform (count 1, channel count 0). - */ -OAKAUDIO_API OakAudioWaveform oakaudio_waveform_init(void); - -/** - * @brief Release one reference. No-op on NULL/empty handle. - */ -OAKAUDIO_API void oakaudio_waveform_free(OakAudioWaveform *self); - -/** - * @brief Channel count, or a negative OAKAUDIO_E_* code. - */ -OAKAUDIO_API int oakaudio_waveform_get_channel_count(OakAudioWaveform self); -OAKAUDIO_API int oakaudio_waveform_set_channel_count(OakAudioWaveform self, - int channels); - -/** - * @brief Waveform length in seconds as a rational pair. - */ -OAKAUDIO_API int oakaudio_waveform_length(OakAudioWaveform self, - int64_t *num, int64_t *den); - -/** - * @brief Write planar float samples into the waveform at `start` seconds, - * expanding it if necessary. - * - * @param planar Per-channel float planes; channel count is taken from the - * waveform (set it first with oakaudio_waveform_set_channel_count). - */ -OAKAUDIO_API int oakaudio_waveform_overwrite_samples(OakAudioWaveform self, - const float *const *planar, int frame_count, int sample_rate, - int64_t start_num, int64_t start_den); - -/** - * @brief Copy summarized data from another waveform over this one. - * - * @param dest_num/dest_den Where in `self` the sums start being written. - * @param offset_num/offset_den Where in `src` reading starts. - * @param length_num/length_den Maximum amount to copy; 0/1 = all of src. - */ -OAKAUDIO_API int oakaudio_waveform_overwrite_sums(OakAudioWaveform self, - OakAudioWaveform src, - int64_t dest_num, int64_t dest_den, - int64_t offset_num, int64_t offset_den, - int64_t length_num, int64_t length_den); - -OAKAUDIO_API int oakaudio_waveform_overwrite_silence(OakAudioWaveform self, - int64_t start_num, int64_t start_den, - int64_t length_num, int64_t length_den); - -/** - * @brief Drop `length` seconds from the front (negative prepends silence). - */ -OAKAUDIO_API int oakaudio_waveform_trim_in(OakAudioWaveform self, - int64_t length_num, int64_t length_den); - -OAKAUDIO_API int oakaudio_waveform_resize(OakAudioWaveform self, - int64_t length_num, int64_t length_den); - -OAKAUDIO_API int oakaudio_waveform_trim_range(OakAudioWaveform self, - int64_t in_num, int64_t in_den, - int64_t length_num, int64_t length_den); - -/** - * @brief Summarized min/max pairs covering [start, start+length). - * - * @param out_pairs Receives points * channel_count channel-interleaved - * pairs; may be NULL to query the point count. - * @param capacity_points Capacity of out_pairs in points. - * @return Number of points (>= 0), or a negative OAKAUDIO_E_* code. - * When out_pairs is NULL or too small the required count is - * returned and nothing is written. - */ -OAKAUDIO_API int oakaudio_waveform_get_summary(OakAudioWaveform self, - int64_t start_num, int64_t start_den, - int64_t length_num, int64_t length_den, - oakaudio_min_max *out_pairs, int capacity_points); - -/** - * @brief Min/max of `length` samples starting at `start_index` for every - * channel (static, no handle). - */ -OAKAUDIO_API int oakaudio_waveform_sum_samples_s(const float *const *planar, - int channel_count, int start_index, int length, - oakaudio_min_max *out); - -/** - * @brief Re-summarize channel-interleaved pairs into one point per - * channel (static, no handle). - */ -OAKAUDIO_API int oakaudio_waveform_re_sum_s(const oakaudio_min_max *in, - int nb_entries, int nb_channels, oakaudio_min_max *out); - -/** - * @brief Extract a whole-file waveform summary from a media file through - * the oakcodec decoder C ABI. - * - * Decodes `filename`'s audio stream `stream_index` (index within the - * file's audio stream list) and reduces it to channel-interleaved - * min/max pairs, one point per `samples_per_point` source samples. - * - * @param out_pairs Receives the pairs; may be NULL to query the size. - * @param capacity_points Capacity of out_pairs in points. - * @param out_channel_count Receives the channel count (may be NULL). - * @return Number of points (>= 0); when out_pairs is NULL or too small, - * the required count is returned and nothing is written. - * Negative OAKAUDIO_E_* code on failure - * (OAKAUDIO_E_NOT_FOUND when the file/stream does not exist). - */ -OAKAUDIO_API int oakaudio_waveform_extract(const char *filename, - int stream_index, int samples_per_point, - oakaudio_min_max *out_pairs, int capacity_points, - int *out_channel_count); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_AUDIO_WAVEFORM_H diff --git a/crates/oakengine.bk/include/codec/conform.h b/crates/oakengine.bk/include/codec/conform.h deleted file mode 100644 index d02afed11..000000000 --- a/crates/oakengine.bk/include/codec/conform.h +++ /dev/null @@ -1,106 +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 . - -***/ - -#ifndef OAK_EDITOR_CODEC_CONFORM_H -#define OAK_EDITOR_CODEC_CONFORM_H - -#include - -#include "error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file conform.h - * @brief C ABI for the oakcodec audio conform manager - * (olive::ConformManager): pcm waveform cache files used for fast - * audio scrubbing. - * - * Interim state (pre-M8): actual conform work is delegated to the global - * task submit callback (see task.h). While no callback is registered, - * state queries report OAKCODEC_CONFORM_UNAVAILABLE. - */ - -#define OAKCODEC_CONFORM_EXISTS 0 -#define OAKCODEC_CONFORM_GENERATING 1 -#define OAKCODEC_CONFORM_UNAVAILABLE 2 - -/** - * @brief Create the ConformManager singleton (no-op when it exists). - */ -OAKCODEC_API int oakcodec_conform_create_instance(void); - -/** - * @brief Destroy the ConformManager singleton (no-op when absent). - */ -OAKCODEC_API int oakcodec_conform_destroy_instance(void); - -/** - * @brief Query the conform state of one audio stream, starting the - * conform when needed and possible. - * - * Addresses the source by filename/stream_index and the target audio - * format by sample_rate/channel_layout/sample_format - * (olive::core::SampleFormat::Format as int). - * - * When the conform files do not exist and a task submit callback is - * registered (task.h), the conform is submitted synchronously and the - * filesystem is re-checked; `wait` only controls whether a post-submit - * miss is reported as OAKCODEC_CONFORM_UNAVAILABLE (wait != 0) or - * OAKCODEC_CONFORM_GENERATING (wait == 0). Without a registrar the - * result is always OAKCODEC_CONFORM_UNAVAILABLE. - * - * @return One of OAKCODEC_CONFORM_* (non-negative), or a negative - * OAKCODEC_E_* code for invalid arguments. - */ -OAKCODEC_API int oakcodec_conform_get_state(const char *cache_path, - const char *source_filename, int stream_index, - int sample_rate, uint64_t channel_layout, - int sample_format, int wait); - -/** - * @brief Number of conform (pcm) files for the given stream/params — one - * per channel; 0 on invalid arguments. - */ -OAKCODEC_API int oakcodec_conform_filename_count(const char *cache_path, - const char *source_filename, int stream_index, - int sample_rate, uint64_t channel_layout, - int sample_format); - -/** - * @brief The `index`-th conform filename (buf/size getter). - * - * @return Required buffer size including NUL (non-negative), or a - * negative OAKCODEC_E_* code (OAKCODEC_E_NOT_FOUND when index is - * out of range). - */ -OAKCODEC_API int oakcodec_conform_filename_at(const char *cache_path, - const char *source_filename, - int stream_index, int sample_rate, - uint64_t channel_layout, int sample_format, - int index, char *buf, int buf_size); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_CODEC_CONFORM_H diff --git a/crates/oakengine.bk/include/codec/decoder.h b/crates/oakengine.bk/include/codec/decoder.h deleted file mode 100644 index 81b10a105..000000000 --- a/crates/oakengine.bk/include/codec/decoder.h +++ /dev/null @@ -1,245 +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 . - -***/ - -#ifndef OAK_EDITOR_CODEC_DECODER_H -#define OAK_EDITOR_CODEC_DECODER_H - -#include - -#include "error.h" -#include "frame.h" -#include "render/cancelatom.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file decoder.h - * @brief C ABI for oakcodec media decoders (olive::Decoder and its - * FFmpeg/OIIO implementations): probing, stream enumeration and - * CPU-frame decoding. - * - * Handles follow the neutral by-value convention documented in frame.h - * (and oakcommon's common/handle.h). Two usage patterns share the - * OakDecoder handle: - * - * - Probe: oakcodec_decoder_probe() inspects a file WITHOUT opening a - * decode session; the stream getters describe what was found. - * - Decode: oakcodec_decoder_init() + oakcodec_decoder_open() attach a - * decoder instance to one (filename, stream) pair; the decode - * functions then produce frames/audio. - */ - -typedef struct OakDecoder { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKCODEC_ABI_VERSION. */ -} OakDecoder; - -/** - * @brief POD description of one probed video stream. - * - * duration_ts counts units of the stream's time base; - * time_base_num/den is seconds per time-base unit. color_primaries and - * color_trc carry the ISO/IEC 23001-8 code points the decoder reports - * (0 = unknown). interlaced is 1 when the stream is interlaced. - * format is an OakPixelFormat value (the decoder's native delivery - * format), channel_count its plane channel count. - */ -typedef struct oakcodec_video_stream_info { - int stream_index; - int width; - int height; - int frame_rate_num; - int frame_rate_den; - int64_t duration_ts; - int time_base_num; - int time_base_den; - int format; - int channel_count; - int color_primaries; - int color_trc; - int interlaced; -} oakcodec_video_stream_info; - -/** - * @brief POD description of one probed audio stream. - * - * channel_layout is the ffmpeg-style channel mask (e.g. 0x3 = stereo). - */ -typedef struct oakcodec_audio_stream_info { - int stream_index; - int sample_rate; - uint64_t channel_layout; - int channel_count; - int64_t duration_ts; - int time_base_num; - int time_base_den; -} oakcodec_audio_stream_info; - -/* ---- Probe (stateless inspection) ---------------------------------------- */ - -/** - * @brief Probe a media file: decoder name plus stream inventory. - * - * Tries each available decoder implementation (FFmpeg, then OIIO) and - * wraps the first one that recognizes the file. The returned handle only - * carries probe results; it cannot decode (use init + open for that). - * - * @return Handle with reference count 1, or an empty handle (ctx == NULL) - * when no decoder recognizes the file (oakcodec_probe_last_error() - * carries the reason). - */ -OAKCODEC_API OakDecoder oakcodec_decoder_probe(const char *filename); - -/** - * @brief Thread-local error detail of the last failed probe on this - * thread (buf/size string getter convention). - */ -OAKCODEC_API int oakcodec_probe_last_error(char *buf, int buf_size); - -/** @brief Probed decoder id ("ffmpeg"/"oiio", buf/size getter). */ -OAKCODEC_API int oakcodec_decoder_probe_decoder_name(OakDecoder probe, char *buf, - int buf_size); - -OAKCODEC_API int oakcodec_decoder_probe_video_stream_count(OakDecoder probe); -OAKCODEC_API int oakcodec_decoder_probe_audio_stream_count(OakDecoder probe); -OAKCODEC_API int oakcodec_decoder_probe_subtitle_stream_count(OakDecoder probe); - -/** - * @brief Fill `out` with the video stream at `index` (0-based within the - * video stream list). - * - * @return OAKCODEC_OK, OAKCODEC_E_INVALID, or OAKCODEC_E_NOT_FOUND when - * index is out of range. - */ -OAKCODEC_API int oakcodec_decoder_probe_get_video_stream(OakDecoder probe, int index, - oakcodec_video_stream_info *out); -OAKCODEC_API int oakcodec_decoder_probe_get_audio_stream(OakDecoder probe, int index, - oakcodec_audio_stream_info *out); - -/* ---- Decode session ------------------------------------------------------- */ - -/** - * @brief Create a closed decoder handle (count 1). - */ -OAKCODEC_API OakDecoder oakcodec_decoder_init(void); - -/** - * @brief Release one reference to a decoder. No-op on NULL/empty. - */ -OAKCODEC_API void oakcodec_decoder_free(OakDecoder *decoder); - -/** - * @brief Open `filename`'s stream `stream_index` for decoding. - * - * The decoder implementation is chosen automatically from the probe - * results. Opening an already-open decoder on the same stream is a - * successful no-op. - * - * @return OAKCODEC_OK on success, OAKCODEC_E_NOT_FOUND when the file - * does not exist, OAKCODEC_E_FAILED otherwise (see - * oakcodec_decoder_last_error()). - */ -OAKCODEC_API int oakcodec_decoder_open(OakDecoder decoder, const char *filename, - int stream_index); - -/** @brief Close the current stream (safe when closed). */ -OAKCODEC_API int oakcodec_decoder_close(OakDecoder decoder); - -/** @brief 1 when a stream is open, 0 otherwise. */ -OAKCODEC_API int oakcodec_decoder_is_open(OakDecoder decoder); - -/** - * @brief Decode the video frame at `numerator/denominator` seconds. - * - * Before the start of the footage the first frame is returned, after the - * end the last frame. - * - * @return A frame handle with reference count 1 (caller releases), or an - * empty handle (ctx == NULL) on error/EOF — check - * oakcodec_decoder_last_error(). - */ -OAKCODEC_API OakFrame oakcodec_decoder_decode_video(OakDecoder decoder, int numerator, - int denominator); - -/** - * @brief Decode audio into a float buffer. - * - * Decodes the interleaved audio covering [in, out) seconds (rational - * pairs), resampled/laid out to `sample_rate`/`channel_layout`. - * `buf` must hold at least `buf_frames` frames worth of interleaved - * floats. - * - * @return The number of frames written (>= 0), or a negative - * OAKCODEC_E_* code. Conform generation is NOT triggered by this - * family in the current intermediate state (no task registrar); - * media requiring a conform yields OAKCODEC_E_STATE. - */ -OAKCODEC_API int oakcodec_decoder_decode_audio(OakDecoder decoder, int in_num, int in_den, - int out_num, int out_den, int sample_rate, - uint64_t channel_layout, float *buf, - int buf_frames); - -/** - * @brief Conform the open stream's audio into per-channel pcm cache files - * (Decoder::conform_audio()). - * - * `output_filenames` is an array of `filename_count` final per-channel - * paths. `sample_format` is olive::core::SampleFormat::Format as int. - * `cancelled` may be an empty OakCancelAtom (ctx == NULL). - * - * @return OAKCODEC_OK on success, OAKCODEC_E_STATE when no stream is - * open, OAKCODEC_E_CANCELLED when cancelled, OAKCODEC_E_FAILED - * otherwise. - */ -OAKCODEC_API int oakcodec_decoder_conform_audio(OakDecoder decoder, - const char *const *output_filenames, int filename_count, - int sample_rate, uint64_t channel_layout, int sample_format, - OakCancelAtom cancelled); - -/** - * @brief Image-sequence filename heuristics (Decoder::get_image_sequence_*). - * - * digit_count: number of trailing digits in the filename stem (0 = not an - * image sequence filename). index: the numeric value of those digits (-1 - * when none). transform: substitute `number` into the digit field, - * two-stage string getter. - */ -OAKCODEC_API int oakcodec_decoder_get_image_sequence_digit_count( - const char *filename); -OAKCODEC_API int64_t oakcodec_decoder_get_image_sequence_index( - const char *filename); -OAKCODEC_API int oakcodec_decoder_transform_image_sequence_file_name( - const char *filename, int64_t number, char *buf, int buf_size); - -/** - * @brief Human-readable detail of the last error on this decoder - * (buf/size string getter convention). - */ -OAKCODEC_API int oakcodec_decoder_last_error(OakDecoder decoder, char *buf, int buf_size); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_CODEC_DECODER_H diff --git a/crates/oakengine.bk/include/codec/encoder.h b/crates/oakengine.bk/include/codec/encoder.h deleted file mode 100644 index 8606541c1..000000000 --- a/crates/oakengine.bk/include/codec/encoder.h +++ /dev/null @@ -1,223 +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 . - -***/ - -#ifndef OAK_EDITOR_CODEC_ENCODER_H -#define OAK_EDITOR_CODEC_ENCODER_H - -#include - -#include "error.h" -#include "frame.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file encoder.h - * @brief C ABI for oakcodec media encoders (olive::Encoder and its - * FFmpeg/OIIO implementations). - * - * Handles follow the neutral by-value convention documented in frame.h. - * The workflow is: fill an oakcodec_encoding_params POD (all fields, - * zeroed = disabled) -> oakcodec_encoder_init() -> - * oakcodec_encoder_open() -> oakcodec_encoder_write_*() -> - * oakcodec_encoder_flush(). Encoder-specific options - * (e.g. "crf" = "18") go through oakcodec_encoder_set_video_option() - * between init and open. - * - * Enum int fields carry the engine's own enum values - * (olive::ExportFormat::Format, olive::ExportCodec::Codec, - * OakPixelFormat, olive::VideoParams::Interlacing, - * olive::core::SampleFormat::Format) — the same values - * oakengine/encoding.h documents. - */ - -typedef struct OakEncoder { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKCODEC_ABI_VERSION. */ -} OakEncoder; - -/** @brief olive::VideoParams::Interlacing values. */ -#define OAKCODEC_INTERLACE_NONE 0 -#define OAKCODEC_INTERLACE_TOP_FIRST 1 -#define OAKCODEC_INTERLACE_BOTTOM_FIRST 2 - -/** @brief EncodingParams::VideoScalingMethod values. */ -#define OAKCODEC_ENCODING_SCALING_FIT 0 -#define OAKCODEC_ENCODING_SCALING_STRETCH 1 -#define OAKCODEC_ENCODING_SCALING_CROP 2 - -/** - * @brief Flattened encoding parameters (olive::EncodingParams). - * - * A zeroed struct describes an all-tracks-disabled configuration. The - * filename (and image-sequence "[#####]" template when - * video_is_image_sequence is set) lives in `filename`. - * video_time_base_* is the frame duration (frame rate flipped), matching - * oak_video_params' convention. - */ -typedef struct oakcodec_encoding_params { - char filename[1024]; - int format; /**< olive::ExportFormat::Format. */ - - int video_enabled; /**< 1/0. */ - int video_codec; /**< olive::ExportCodec::Codec. */ - int video_width; - int video_height; - int video_time_base_num; /**< Frame duration numerator. */ - int video_time_base_den; - int video_pixel_format; /**< OakPixelFormat (delivery format). */ - int video_interlacing; /**< OAKCODEC_INTERLACE_*. */ - int video_pixel_aspect_num; - int video_pixel_aspect_den; - int64_t video_bit_rate; /**< bit/s, 0 = codec default. */ - int64_t video_min_bit_rate; - int64_t video_max_bit_rate; - int64_t video_buffer_size; /**< bytes. */ - int video_threads; /**< 0 = auto. */ - char video_pix_fmt[64]; /**< Encoded pixel format name ("yuv420p"). */ - int video_is_image_sequence; /**< 1/0. */ - int video_scaling_method; /**< OAKCODEC_ENCODING_SCALING_*. */ - - int audio_enabled; /**< 1/0. */ - int audio_codec; /**< olive::ExportCodec::Codec. */ - int audio_sample_rate; - uint64_t audio_channel_layout; /**< ffmpeg-style channel mask. */ - int audio_sample_format; /**< olive::core::SampleFormat::Format. */ - int64_t audio_bit_rate; /**< bit/s. */ - - int subtitles_enabled; /**< 1/0. */ - int subtitles_codec; /**< olive::ExportCodec::Codec. */ - int subtitles_are_sidecar; /**< 1/0. */ - int subtitles_sidecar_format; /**< olive::ExportFormat::Format. */ - - /** Output OCIO colorspace name; empty = reference space (no transform). */ - char color_transform_output[256]; - - int export_length_num; /**< Export length in seconds (rational). */ - int export_length_den; - - /** Custom export range (seconds, rational pairs); used when - * has_custom_range != 0. */ - int has_custom_range; - int64_t custom_range_in_num; - int64_t custom_range_in_den; - int64_t custom_range_out_num; - int64_t custom_range_out_den; -} oakcodec_encoding_params; - -/** - * @brief Create an encoder for `params` (count 1). - * - * The implementation (FFmpeg/OIIO) is chosen from params.format and the - * enabled tracks. The file is NOT opened yet. Returns an empty handle - * (ctx == NULL) when the configuration is invalid. - */ -OAKCODEC_API OakEncoder oakcodec_encoder_init(const oakcodec_encoding_params *params); - -/** @brief Release one reference to an encoder. No-op on NULL/empty. */ -OAKCODEC_API void oakcodec_encoder_free(OakEncoder *encoder); - -/** - * @brief Set an encoder-specific video option (e.g. "crf" = "18"). - * - * Only valid between init and open. - * - * @return OAKCODEC_OK, or OAKCODEC_E_STATE when already open. - */ -OAKCODEC_API int oakcodec_encoder_set_video_option(OakEncoder encoder, const char *key, - const char *value); - -/** - * @brief Open the output file and write stream headers. - * - * @return OAKCODEC_OK, OAKCODEC_E_STATE (already open), or - * OAKCODEC_E_FAILED (see oakcodec_encoder_last_error()). - */ -OAKCODEC_API int oakcodec_encoder_open(OakEncoder encoder); - -/** - * @brief Encode one video frame. - * - * The frame's parameters must match the encoding parameters (the encoder - * converts the delivery pixel format to the encoded one internally). - */ -OAKCODEC_API int oakcodec_encoder_write_video(OakEncoder encoder, OakFrame frame); - -/** - * @brief Encode interleaved float audio samples. - * - * @param samples frame_count * channel_count interleaved floats. - * @return OAKCODEC_OK or a negative OAKCODEC_E_* code. - */ -OAKCODEC_API int oakcodec_encoder_write_audio(OakEncoder encoder, const float *samples, - int frame_count); - -/** - * @brief Encode one subtitle entry (times in seconds). - */ -OAKCODEC_API int oakcodec_encoder_write_subtitle(OakEncoder encoder, const char *text, - double in_seconds, double out_seconds); - -/** - * @brief Flush the encoders, write the trailer and close the file. - * - * Idempotent; after a successful flush the encoder cannot be written to - * (write calls return OAKCODEC_E_STATE). - */ -OAKCODEC_API int oakcodec_encoder_flush(OakEncoder encoder); - -/** - * @brief Human-readable detail of the last error on this encoder - * (buf/size string getter convention). - */ -OAKCODEC_API int oakcodec_encoder_last_error(OakEncoder encoder, char *buf, int buf_size); - -/** - * @brief The pixel format the encoder wants frames in - * (Encoder::get_desired_pixel_format()), as int; -1 when - * unknown/invalid encoder. - */ -OAKCODEC_API int oakcodec_encoder_get_desired_pixel_format(OakEncoder encoder); - -/** - * @brief File extension for an export format - * (ExportFormat::get_extension()), two-stage string getter. - */ -OAKCODEC_API int oakcodec_export_format_get_extension(int format, char *buf, - int buf_size); - -/** - * @brief Scaling matrix for a scaling method - * (EncodingParams::generate_matrix()), row-major 4x4 into - * out_matrix[16]. - */ -OAKCODEC_API int oakcodec_encoding_generate_matrix(int method, int src_width, - int src_height, int dst_width, - int dst_height, double *out_matrix); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_CODEC_ENCODER_H diff --git a/crates/oakengine.bk/include/codec/error.h b/crates/oakengine.bk/include/codec/error.h deleted file mode 100644 index a4c0744fa..000000000 --- a/crates/oakengine.bk/include/codec/error.h +++ /dev/null @@ -1,61 +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 . - -***/ - -#ifndef OAK_EDITOR_CODEC_ERROR_H -#define OAK_EDITOR_CODEC_ERROR_H - -/** - * @brief Status and error codes shared by all oakcodec C API families. - * - * Return-code convention (mirrors the other split modules): - * 0 (OAKCODEC_OK) on success, a negative OAKCODEC_E_* error code on - * failure. String getters return the required buffer size in bytes - * (including the terminating NUL) as a non-negative value instead. - */ -#define OAKCODEC_OK 0 /**< Success. */ -#define OAKCODEC_E_INVALID (-50001) /**< NULL handle or invalid argument. */ -#define OAKCODEC_E_STATE (-50002) /**< Call not valid in the current state. */ -#define OAKCODEC_E_FAILED (-50003) /**< The underlying operation failed. */ -#define OAKCODEC_E_NOT_FOUND (-50004) /**< Index out of range / entry not found. */ -#define OAKCODEC_E_NOMEM (-50005) /**< Allocation failed. */ -#define OAKCODEC_E_CANCELLED (-50006) /**< The operation was cancelled. */ - -/** - * @brief Current ABI version stamped into every oakcodec handle. - * - * Bump whenever the handle layout or the semantics of any exported - * function change incompatibly. Consumers should compare a handle's - * abi_version field against the value they were compiled with before - * dereferencing ctx. - */ -#define OAKCODEC_ABI_VERSION 1 - -/** - * @brief Export macro for the oakcodec C ABI. - * - * oakcodec is built with -fvisibility=hidden (01 §1 rule 5): only the - * oakcodec_* functions marked with this macro leave the shared library. - * This also keeps codec-internal C++ classes (whose olive::* names may - * collide with transition stubs inside other modules) from participating - * in cross-library weak-symbol coalescing. - */ -#define OAKCODEC_API __attribute__((visibility("default"))) - -#endif //OAK_EDITOR_CODEC_ERROR_H diff --git a/crates/oakengine.bk/include/codec/format.h b/crates/oakengine.bk/include/codec/format.h deleted file mode 100644 index 68e62e185..000000000 --- a/crates/oakengine.bk/include/codec/format.h +++ /dev/null @@ -1,217 +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 . - -***/ - -#ifndef OAK_EDITOR_CODEC_FORMAT_H -#define OAK_EDITOR_CODEC_FORMAT_H - -#include "error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file format.h - * @brief C ABI for the oakcodec container-format / codec metadata queries - * (olive::ExportFormat / olive::ExportCodec / olive::Encoder statics). - * - * This family is the module-side mirror of the facade's - * oakengine_encoding_format_* / codec_* surface (oakengine/encoding.h): - * the export dialog queries it to populate its format/codec combo boxes and - * to enable/disable the bit-rate controls. The functions are stateless — - * no handles involved. - * - * Enum int fields carry the engine's own enum values - * (olive::ExportFormat::Format, olive::ExportCodec::Codec, - * olive::core::SampleFormat::Format) — the same values oakengine/encoding.h - * documents. Return-code convention follows include/codec/error.h: 0 - * (OAKCODEC_OK) on success, a negative OAKCODEC_E_* code on failure, and - * string getters return the required buffer size in bytes INCLUDING the - * terminating NUL as a non-negative value (two-stage convention). Note this - * differs from oakcodec_export_format_get_extension() (encoder.h), which - * predates this family and reports unknown formats as the empty string. - */ - -/** - * @brief Container formats (olive::ExportFormat::Format) referenced by name - * in UI code. Only append; the values are serialized in project/preset - * files. The complete list lives in src/codec/src/exportformat.h. - */ -#define OAKCODEC_ENCODING_FORMAT_MATROSKA 1 -#define OAKCODEC_ENCODING_FORMAT_MPEG4_VIDEO 2 -#define OAKCODEC_ENCODING_FORMAT_QUICKTIME 4 -#define OAKCODEC_ENCODING_FORMAT_PNG 5 -#define OAKCODEC_ENCODING_FORMAT_WAV 7 -#define OAKCODEC_ENCODING_FORMAT_SRT 13 - -/** - * @brief Codecs (olive::ExportCodec::Codec) referenced by name in UI code. - * Only append; the values are serialized. The complete list lives in - * src/codec/src/exportcodec.h. - */ -#define OAKCODEC_ENCODING_CODEC_H264 1 -#define OAKCODEC_ENCODING_CODEC_H264RGB 2 -#define OAKCODEC_ENCODING_CODEC_H265 3 -#define OAKCODEC_ENCODING_CODEC_CINEFORM 7 -#define OAKCODEC_ENCODING_CODEC_AAC 12 -#define OAKCODEC_ENCODING_CODEC_PCM 13 -#define OAKCODEC_ENCODING_CODEC_SRT 17 -#define OAKCODEC_ENCODING_CODEC_AV1 18 - -/* ---- Container format / codec metadata ---------------------------------- */ - -/** - * @brief Number of container formats (olive::ExportFormat::k_format_count). - */ -OAKCODEC_API int oakcodec_encoding_format_count(void); - -/** - * @brief Display name of a container format (buf/size, two-stage). - * - * @return The required buffer size (including the NUL), or - * OAKCODEC_E_INVALID when `format` is out of range. - */ -OAKCODEC_API int oakcodec_encoding_format_name(int format, char *buf, - int buf_size); - -/** - * @brief File extension (no dot) of a container format (buf/size, - * two-stage); same return convention as - * oakcodec_encoding_format_name(). - */ -OAKCODEC_API int oakcodec_encoding_format_extension(int format, char *buf, - int buf_size); - -/** - * @brief Number of video codecs a container format supports, or - * OAKCODEC_E_INVALID when the format is invalid. - */ -OAKCODEC_API int oakcodec_encoding_format_video_codec_count(int format); - -/** - * @brief The `index`-th video codec of `format` as an - * olive::ExportCodec::Codec value. - * - * @return OAKCODEC_E_INVALID when the format is invalid, or - * OAKCODEC_E_NOT_FOUND when the index is out of range. - */ -OAKCODEC_API int oakcodec_encoding_format_video_codec_at(int format, - int index); - -/** @brief Audio-codec variant of the two functions above. */ -OAKCODEC_API int oakcodec_encoding_format_audio_codec_count(int format); -OAKCODEC_API int oakcodec_encoding_format_audio_codec_at(int format, - int index); - -/** @brief Subtitle-codec variant of the two functions above. */ -OAKCODEC_API int oakcodec_encoding_format_subtitle_codec_count(int format); -OAKCODEC_API int oakcodec_encoding_format_subtitle_codec_at(int format, - int index); - -/** - * @brief Display name of a codec (buf/size, two-stage). - * - * @return The required buffer size (including the NUL), or - * OAKCODEC_E_INVALID when `codec` is out of range. - */ -OAKCODEC_API int oakcodec_encoding_codec_name(int codec, char *buf, - int buf_size); - -/** @brief 1 when `codec` encodes still images (PNG/TIFF/OpenEXR), else 0 - * (0 also for an invalid codec). */ -OAKCODEC_API int oakcodec_encoding_codec_is_still_image(int codec); - -/** @brief 1 when `codec` is lossless (no bit-rate setting applies), else 0 - * (0 also for an invalid codec). */ -OAKCODEC_API int oakcodec_encoding_codec_is_lossless(int codec); - -/** - * @brief Number of encoded pixel formats (e.g. "yuv420p") usable with - * `codec` inside `format`, or OAKCODEC_E_INVALID when either - * argument is out of range. The list is queried from the format's - * encoder (FFmpeg/OIIO), so codecs without an encoder report 0. - */ -OAKCODEC_API int oakcodec_encoding_pix_fmt_count(int format, int codec); - -/** - * @brief The `index`-th encoded pixel format name (buf/size, two-stage). - * - * @return The required buffer size (including the NUL), or - * OAKCODEC_E_INVALID for bad format/codec, or - * OAKCODEC_E_NOT_FOUND when the index is out of range. - */ -OAKCODEC_API int oakcodec_encoding_pix_fmt_at(int format, int codec, - int index, char *buf, - int buf_size); - -/** - * @brief Index of `pix_fmt` (e.g. "yuv420p") in `codec`'s supported pixel - * format list; 0 (the codec's preferred format) when absent or - * `pix_fmt` is NULL/empty or `codec` is invalid. - */ -OAKCODEC_API int oakcodec_encoding_pix_fmt_index(int codec, - const char *pix_fmt); - -/** - * @brief Number of sample formats usable with `codec` inside `format`, or - * OAKCODEC_E_INVALID when either argument is out of range. - */ -OAKCODEC_API int oakcodec_encoding_sample_format_count(int format, - int codec); - -/** - * @brief The `index`-th sample format as an olive::core::SampleFormat::Format - * value. - * - * @return OAKCODEC_E_INVALID for bad format/codec, or - * OAKCODEC_E_NOT_FOUND when the index is out of range. - */ -OAKCODEC_API int oakcodec_encoding_sample_format_at(int format, int codec, - int index); - -/* ---- Image-sequence filename helpers (olive::Encoder statics) ----------- */ - -/** @brief 1 when `filename` contains a "[#####]" digit placeholder, else 0 - * (0 for NULL). */ -OAKCODEC_API int -oakcodec_encoding_filename_contains_digit_placeholder(const char *filename); - -/** @brief Digit count of the filename's "[#####]" placeholder; 0 when none - * (0 for NULL). */ -OAKCODEC_API int -oakcodec_encoding_image_sequence_digit_count(const char *filename); - -/** - * @brief `filename` with the digit placeholder removed (buf/size, two-stage; - * a leading separator like "_"/"-"/"."/" " before the placeholder is - * removed along with it). - * - * @return The required buffer size (including the NUL), or - * OAKCODEC_E_INVALID when `filename` is NULL. - */ -OAKCODEC_API int -oakcodec_encoding_filename_remove_digit_placeholder(const char *filename, - char *buf, int buf_size); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_CODEC_FORMAT_H diff --git a/crates/oakengine.bk/include/codec/frame.h b/crates/oakengine.bk/include/codec/frame.h deleted file mode 100644 index 74456d5a4..000000000 --- a/crates/oakengine.bk/include/codec/frame.h +++ /dev/null @@ -1,162 +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 . - -***/ - -#ifndef OAK_EDITOR_CODEC_FRAME_H -#define OAK_EDITOR_CODEC_FRAME_H - -#include - -#include "common/videoparams.h" -#include "error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file frame.h - * @brief C ABI for the oakcodec frame object (olive::Frame), a CPU pixel - * buffer plus an OakVideoParams parameter set. - * - * Handle convention (all oakcodec families): neutral by-value handles with - * the same four fields as oakcommon (see oakcommon's common/handle.h): - * - * typedef struct OakFrame { - * void *ctx; // opaque, points to the impl - * void (*addref)(void *ctx); // atomic +1, owner-DLL code - * void (*release)(void *ctx); // atomic -1, destroys at 0 - * uint32_t abi_version; // OAKCODEC_ABI_VERSION - * } OakFrame; - * - * oakcodec_frame_init*() returns a handle whose underlying object has - * reference count 1. Copying the struct copies the pointer, not the - * count: call handle.addref(handle.ctx) for every additional long-lived - * copy and handle.release(handle.ctx) (or oakcodec_frame_free()) when - * done with each copy. Functions that only use a handle take it BY - * VALUE; an empty handle (ctx == NULL) is reported as - * OAKCODEC_E_INVALID. oakcodec_frame_free() takes a pointer so it can - * null out the caller's ctx; NULL and ctx == NULL are no-ops. - */ -typedef struct OakFrame { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKCODEC_ABI_VERSION. */ -} OakFrame; - -/** - * @brief Create an empty frame with default (invalid) video parameters. - * - * @return Handle with reference count 1; ctx is NULL on allocation - * failure. - */ -OAKCODEC_API OakFrame oakcodec_frame_init(void); - -/** - * @brief Create a frame with a copy of the given parameter set. - * - * The params handle is addref'd internally; the caller keeps its own - * reference. The frame is not allocated; call oakcodec_frame_allocate(). - * - * @return Handle with reference count 1; ctx is NULL on failure. - */ -OAKCODEC_API OakFrame oakcodec_frame_init_with_params(OakVideoParams params); - -/** - * @brief Release one reference to a frame. - * - * Convenience wrapper around handle.release(handle.ctx); nulls ctx - * afterwards. No-op when frame is NULL or frame->ctx is NULL. - */ -OAKCODEC_API void oakcodec_frame_free(OakFrame *frame); - -/** - * @brief Get a copy of the frame's parameter set. - * - * @param out Receives an addref'd OakVideoParams; the caller must release - * it with oakcommon_videoparams_free(). - * @return OAKCODEC_OK, or OAKCODEC_E_INVALID for bad arguments. - */ -OAKCODEC_API int oakcodec_frame_get_params(OakFrame frame, OakVideoParams *out); - -/** - * @brief Replace the frame's parameter set (the handle is addref'd - * internally). Recomputes the line sizes; does not reallocate the - * buffer. - */ -OAKCODEC_API int oakcodec_frame_set_params(OakFrame frame, OakVideoParams params); - -/** - * @brief Allocate the pixel buffer from the current parameters. - * - * @return OAKCODEC_OK on success (including already-allocated), - * OAKCODEC_E_STATE when the parameters are invalid, - * OAKCODEC_E_INVALID for an empty handle. - */ -OAKCODEC_API int oakcodec_frame_allocate(OakFrame frame); - -/** @brief 1 when the pixel buffer is allocated, 0 otherwise. */ -OAKCODEC_API int oakcodec_frame_is_allocated(OakFrame frame); - -/** @brief Writable pixel buffer, or NULL when unallocated/empty. */ -OAKCODEC_API void *oakcodec_frame_data(OakFrame frame); - -/** @brief Const variant of oakcodec_frame_data(). */ -OAKCODEC_API const void *oakcodec_frame_const_data(OakFrame frame); - -/** @brief Size of the pixel buffer in bytes (0 when unallocated). */ -OAKCODEC_API int oakcodec_frame_allocated_size(OakFrame frame); - -/** @brief Distance between two rows in bytes (0 when params are unset). */ -OAKCODEC_API int oakcodec_frame_linesize_bytes(OakFrame frame); - -/** @brief Distance between two rows in pixels. */ -OAKCODEC_API int oakcodec_frame_linesize_pixels(OakFrame frame); - -/* Query helpers; all return 0 / OAKCOMMON_PIXEL_FORMAT_INVALID on an - * empty handle. */ -OAKCODEC_API int oakcodec_frame_width(OakFrame frame); -OAKCODEC_API int oakcodec_frame_height(OakFrame frame); -OAKCODEC_API int oakcodec_frame_format(OakFrame frame); /**< OakPixelFormat value. */ -OAKCODEC_API int oakcodec_frame_channel_count(OakFrame frame); - -/** - * @brief Frame timestamp as a rational number of seconds. - * - * @return OAKCODEC_OK, or OAKCODEC_E_INVALID for bad arguments. - */ -OAKCODEC_API int oakcodec_frame_get_timestamp(OakFrame frame, int *numerator, - int *denominator); -OAKCODEC_API int oakcodec_frame_set_timestamp(OakFrame frame, int numerator, - int denominator); - -/** - * @brief Number of live oakcodec handle objects (debug/leak checking). - * - * Counts every boxed object created by oakcodec_*_init*() that has not - * been released yet, across all families (frame/decoder/encoder/...). - */ -OAKCODEC_API int oakcodec_debug_alive_count(void); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_CODEC_FRAME_H diff --git a/crates/oakengine.bk/include/codec/proxy.h b/crates/oakengine.bk/include/codec/proxy.h deleted file mode 100644 index e6514ed7b..000000000 --- a/crates/oakengine.bk/include/codec/proxy.h +++ /dev/null @@ -1,140 +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 . - -***/ - -#ifndef OAK_EDITOR_CODEC_PROXY_H -#define OAK_EDITOR_CODEC_PROXY_H - -#include - -#include "error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file proxy.h - * @brief C ABI for the oakcodec proxy generation singleton - * (olive::ProxyManager). - * - * Interim state (pre-M8): actual transcodes are delegated to the global - * task submit callback (see task.h). While no callback is registered, - * oakcodec_proxy_get_or_start() reports the proxy as missing instead of - * starting background work. - */ - -#define OAKCODEC_PROXY_STATE_MISSING 0 -#define OAKCODEC_PROXY_STATE_GENERATING 1 -#define OAKCODEC_PROXY_STATE_READY 2 -#define OAKCODEC_PROXY_STATE_FAILED 3 - -/** - * @brief POD proxy generation parameters (olive::ProxyManager::ProxyParams). - * - * divider: source resolution divider (1 = use absolute width/height, - * 2/4/8 = fraction of the source resolution). extension/preset are the - * ffmpeg output container and encoder preset (e.g. "mp4"/"veryfast"). - */ -typedef struct oakcodec_proxy_params { - int width; - int height; - int divider; - int version; - int crf; - int include_audio; /**< 1/0. */ - char extension[32]; - char preset[32]; -} oakcodec_proxy_params; - -typedef struct oakcodec_proxy_result { - int state; /**< OAKCODEC_PROXY_STATE_* */ - char filename[1024]; -} oakcodec_proxy_result; - -/** - * @brief Create the ProxyManager singleton (no-op when it exists). - */ -OAKCODEC_API int oakcodec_proxy_create_instance(void); - -/** - * @brief Destroy the ProxyManager singleton (no-op when absent). - */ -OAKCODEC_API int oakcodec_proxy_destroy_instance(void); - -/** - * @brief Compiled-in default proxy parameters (1280x720, divider 1, mp4, - * crf 23, "veryfast", audio included). Interim state: until the config - * milestone wires a real store these do not reflect user settings. - */ -OAKCODEC_API int oakcodec_proxy_params_default(oakcodec_proxy_params *out); - -/** - * @brief State of a proxy file on disk (OAKCODEC_PROXY_STATE_*; - * OAKCODEC_PROXY_STATE_MISSING for NULL/empty/absent). - */ -OAKCODEC_API int oakcodec_proxy_get_state(const char *proxy_filename); - -/** @brief Human-readable string for a proxy state (buf/size getter). */ -OAKCODEC_API int oakcodec_proxy_state_to_string(int state, char *buf, int buf_size); - -/** @brief Proxy directory for a project cache path (buf/size getter). */ -OAKCODEC_API int oakcodec_proxy_get_proxy_directory(const char *cache_path, char *buf, - int buf_size); - -/** - * @brief Deterministic proxy filename for a source stream (buf/size - * getter). - */ -OAKCODEC_API int oakcodec_proxy_get_proxy_filename(const char *cache_path, - const char *source_filename, - int stream_index, - const oakcodec_proxy_params *params, - char *buf, int buf_size); - -/** @brief Working (in-progress) filename of a proxy (buf/size getter). */ -OAKCODEC_API int oakcodec_proxy_get_working_filename(const char *proxy_filename, - char *buf, int buf_size); - -/** - * @brief Get or start generating a proxy for `source_filename`. - * - * `cache_path` is the project cache directory. On return `out->state` - * and `out->filename` describe the proxy. When a task submit callback is - * registered (task.h) and no proxy exists, generation is submitted - * synchronously before the state is re-derived; without a registrar the - * state stays OAKCODEC_PROXY_STATE_MISSING. - */ -OAKCODEC_API int oakcodec_proxy_get_or_start(const char *cache_path, - const char *source_filename, int stream_index, - const oakcodec_proxy_params *params, - oakcodec_proxy_result *out); - -/** - * @brief Locate an ffmpeg executable for proxy generation (buf/size - * getter; empty string when none is found). - */ -OAKCODEC_API int oakcodec_proxy_find_ffmpeg(const char *configured_path, char *buf, - int buf_size); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_CODEC_PROXY_H diff --git a/crates/oakengine.bk/include/codec/task.h b/crates/oakengine.bk/include/codec/task.h deleted file mode 100644 index aba9bcb4c..000000000 --- a/crates/oakengine.bk/include/codec/task.h +++ /dev/null @@ -1,113 +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 . - -***/ - -#ifndef OAK_EDITOR_CODEC_TASK_H -#define OAK_EDITOR_CODEC_TASK_H - -#include - -#include "error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Background task submission hook for oakcodec (interim state). - * - * The codec module occasionally needs background work (audio conforms, - * proxy transcodes). The task system itself is split out at milestone M8; - * until then oakcodec exposes a single global submit callback. A host - * (M8: oaktask) registers a callback with oakcodec_set_task_submit_cb(); - * the conform/proxy managers call it whenever they need a task. - * - * While no callback is registered, managers report the work as - * unavailable (they never crash and never block). - */ - -/** - * @brief Kinds of background tasks oakcodec can request. - */ -enum OakCodecTaskKind { - OAKCODEC_TASK_CONFORM = 0, /**< Audio conform to pcm cache files. */ - OAKCODEC_TASK_PROXY = 1 /**< Video proxy transcode. */ -}; - -/** - * @brief Description of one background task request. - * - * All strings are borrowed and only valid for the duration of the - * submit call; the callback must copy anything it retains. - * - * Field usage by kind: - * - OAKCODEC_TASK_CONFORM: input_filename (source media), stream_index - * (audio stream), output_filename (final path of the FIRST channel's - * pcm file; the task derives the sibling per-channel paths and the - * ".working" temporary names from the deterministic naming rule), - * sample_rate / channel_layout / sample_format (target audio params, - * sample_format is olive::core::SampleFormat::Format as int). - * - OAKCODEC_TASK_PROXY: input_filename (source media), stream_index - * (video stream), output_filename (final proxy path; the task owns - * the ".working.mp4" temporary name and the rename on success), - * proxy_width / proxy_height (absolute target size, both 0 when the - * request is divider-based). - */ -typedef struct OakCodecTaskRequest { - int kind; /**< OakCodecTaskKind. */ - const char *input_filename; /**< Source media filename. */ - const char *output_filename; /**< Final destination path (see above). */ - int stream_index; /**< Stream inside the source media. */ - int sample_rate; /**< conform: target sample rate. */ - uint64_t channel_layout; /**< conform: target channel layout mask. */ - int sample_format; /**< conform: target sample format (enum as int). */ - int proxy_width; /**< proxy: target width, 0 = unspecified/divider. */ - int proxy_height; /**< proxy: target height, 0 = unspecified/divider. */ -} OakCodecTaskRequest; - -/** - * @brief Task submit callback. - * - * @return 0 (OAKCODEC_OK) if the task was accepted - either completed - * synchronously or queued; a negative OAKCODEC_E_* code if the request - * was rejected. - */ -typedef int (*oakcodec_task_submit_fn)(const OakCodecTaskRequest *req, - void *userdata); - -/** - * @brief Registers (or replaces) the global task submit callback. - * - * Thread-safe. Pass cb == NULL to unregister. Interim state (pre-M8): - * nobody registers and all task-dependent work reports unavailable. - */ -OAKCODEC_API void oakcodec_set_task_submit_cb(oakcodec_task_submit_fn cb, void *userdata); - -/** - * @brief Returns 1 if a submit callback is currently registered, else 0. - * - * Thread-safe. - */ -OAKCODEC_API int oakcodec_task_submit_is_registered(void); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_CODEC_TASK_H diff --git a/crates/oakengine.bk/include/common/colortransform.h b/crates/oakengine.bk/include/common/colortransform.h deleted file mode 100644 index fa3a80be5..000000000 --- a/crates/oakengine.bk/include/common/colortransform.h +++ /dev/null @@ -1,156 +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 . - -***/ - -#ifndef OAK_EDITOR_COLORTRANSFORM_H -#define OAK_EDITOR_COLORTRANSFORM_H - -#include "common/error.h" -#include "common/handle.h" - -#ifdef __cplusplus -namespace olive -{ -class ColorTransform; -} -extern "C" { -#endif - -/** - * @brief Neutral by-value handle to a color transform description - * (olive::ColorTransform). - * - * Ownership/count semantics follow the convention in common/handle.h: - * init functions return a handle whose object has reference count 1, - * addref(ctx)/release(ctx) adjust it atomically, and release destroys - * the object at zero. abi_version is always OAKCOMMON_ABI_VERSION. - */ -typedef struct OakColorTransform { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKCOMMON_ABI_VERSION. */ -} OakColorTransform; - -/** - * @brief Create a plain output-colorspace transform. - * - * @param output Output colorspace name. Must not be NULL. - * @return Handle with reference count 1; ctx is NULL on failure. - */ -OakColorTransform oakcommon_colortransform_init_output( - const char *output); - -/** - * @brief Create a display/view/look transform. - * - * All three strings must not be NULL. - * - * @return Handle with reference count 1; ctx is NULL on failure. - */ -OakColorTransform oakcommon_colortransform_init_display( - const char *display, const char *view, const char *look); - -#ifdef __cplusplus -/** - * @brief Copy a native olive::ColorTransform into a new handle. - * - * The source object is deep-copied; the handle does not keep any - * reference to @p src, which may be destroyed immediately afterwards. - * Only visible to C++ consumers. - * - * @return Handle with reference count 1; ctx is NULL if src is NULL or - * on allocation failure. - */ -OakColorTransform oakcommon_colortransform_init_from_native( - const olive::ColorTransform *src); - -/** - * @brief Borrow the native object behind a handle. - * - * The returned pointer is borrowed: it stays valid while the caller - * holds a reference to the handle (i.e. until the matching release). - * Only visible to C++ consumers. - * - * @return Borrowed pointer, or NULL if transform is NULL or - * transform->ctx is NULL. - */ -const olive::ColorTransform *oakcommon_colortransform_get_native( - OakColorTransform transform); -#endif - -/** - * @brief Release one reference to a transform. - * - * Convenience wrapper around handle.release(handle.ctx): decrements the - * atomic reference count and destroys the object when it reaches zero. - * No-op when transform is NULL or transform->ctx is NULL. - */ -void oakcommon_colortransform_free(OakColorTransform *transform); - -/** - * @brief Query whether this is a display/view/look transform. - * - * @param is_display Receives the result. Must not be NULL. - * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_colortransform_is_display(OakColorTransform transform, - int *is_display); - -/** - * @brief Get the display name (two-stage string getter). - * - * @return Required buffer size in bytes including the terminating NUL - * (non-negative), or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_colortransform_get_display(OakColorTransform transform, - char *buf, int buf_size); - -/** - * @brief Get the output colorspace name (two-stage string getter). - * - * @return Required buffer size in bytes including the terminating NUL - * (non-negative), or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_colortransform_get_output(OakColorTransform transform, - char *buf, int buf_size); - -/** - * @brief Get the view name (two-stage string getter). - * - * @return Required buffer size in bytes including the terminating NUL - * (non-negative), or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_colortransform_get_view(OakColorTransform transform, - char *buf, int buf_size); - -/** - * @brief Get the look name (two-stage string getter). - * - * @return Required buffer size in bytes including the terminating NUL - * (non-negative), or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_colortransform_get_look(OakColorTransform transform, - char *buf, int buf_size); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_COLORTRANSFORM_H diff --git a/crates/oakengine.bk/include/common/commandlineparser.h b/crates/oakengine.bk/include/common/commandlineparser.h deleted file mode 100644 index 4d67078be..000000000 --- a/crates/oakengine.bk/include/common/commandlineparser.h +++ /dev/null @@ -1,225 +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 . - -***/ - -#ifndef OAK_EDITOR_COMMANDLINEPARSER_H -#define OAK_EDITOR_COMMANDLINEPARSER_H - -#ifndef __cplusplus -#include -#endif - -#include "common/error.h" -#include "common/handle.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Neutral by-value handle to a command-line parser instance. - * - * Ownership/count semantics follow the convention in common/handle.h: - * init returns a handle whose object has reference count 1, - * addref(ctx)/release(ctx) adjust it atomically, and release destroys - * the object at zero. abi_version is always OAKCOMMON_ABI_VERSION. - */ -typedef struct OakCommandLineParser { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKCOMMON_ABI_VERSION. */ -} OakCommandLineParser; - -/** - * @brief Neutral by-value handle to a registered command-line option. - * - * The handle is released with oakcommon_commandlineoption_free() (or - * handle.release(handle.ctx)); the underlying option is owned by the - * parser and stays valid until the parser is destroyed. abi_version is - * always OAKCOMMON_ABI_VERSION. - */ -typedef struct OakCommandLineOption { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKCOMMON_ABI_VERSION. */ -} OakCommandLineOption; - -/** - * @brief Neutral by-value handle to a registered positional argument. - * - * The handle is released with - * oakcommon_commandlinepositionalargument_free() (or - * handle.release(handle.ctx)); the underlying argument is owned by the - * parser and stays valid until the parser is destroyed. abi_version is - * always OAKCOMMON_ABI_VERSION. - */ -typedef struct OakCommandLinePositionalArgument { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKCOMMON_ABI_VERSION. */ -} OakCommandLinePositionalArgument; - -/** - * @brief Create a command-line parser. - * - * @return Handle with reference count 1; ctx is NULL on allocation - * failure. - */ -OakCommandLineParser oakcommon_commandlineparser_init(void); - -/** - * @brief Release one reference to a command-line parser. - * - * Convenience wrapper around handle.release(handle.ctx): decrements the - * atomic reference count and destroys the parser (invalidating all - * option and positional-argument handles created from it) when the - * count reaches zero. No-op when parser is NULL or parser->ctx is NULL. - */ -void oakcommon_commandlineparser_free(OakCommandLineParser *parser); - -/** - * @brief Set the application name/version shown by print_help. - * - * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_commandlineparser_set_app_info(OakCommandLineParser parser, - const char *name, - const char *version); - -/** - * @brief Register an option with one or more name strings. - * - * @param names Array of option name strings (without leading dash). - * @param name_count Number of entries in names. Must be > 0. - * @param description Help text, may be NULL. - * @param takes_arg Non-zero if the option consumes the following argument. - * @param arg_placeholder Placeholder shown in help, may be NULL. - * @param hidden Non-zero to omit from help output. - * @param out_option Receives the option handle (reference count 1). - * May be NULL if unused. - * - * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_commandlineparser_add_option( - OakCommandLineParser parser, const char *const *names, int name_count, - const char *description, int takes_arg, const char *arg_placeholder, - int hidden, OakCommandLineOption *out_option); - -/** - * @brief Register a positional argument. - * - * @param out_argument Receives the argument handle (reference count 1). - * May be NULL if unused. - * - * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_commandlineparser_add_positional_argument( - OakCommandLineParser parser, const char *name, - const char *description, int required, - OakCommandLinePositionalArgument *out_argument); - -/** - * @brief Parse an argv-style argument list. - * - * argv[0] is skipped as the program name, matching C main() convention. - * - * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_commandlineparser_process(OakCommandLineParser parser, - const char *const *argv, int argc); - -/** - * @brief Print usage/help text to stdout. - * - * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_commandlineparser_print_help(OakCommandLineParser parser, - const char *filename); - -/** - * @brief Query whether an option was present on the command line. - * - * @param is_set Receives the result. Must not be NULL. - * - * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_commandlineoption_is_set(OakCommandLineOption option, - bool *is_set); - -/** - * @brief Release one reference to an option handle. - * - * Convenience wrapper around handle.release(handle.ctx). Does not - * unregister the option from the parser. No-op when option is NULL or - * option->ctx is NULL. - */ -void oakcommon_commandlineoption_free(OakCommandLineOption *option); - -/** - * @brief Get an option's argument value (two-stage string getter). - * - * @return Required buffer size in bytes including the terminating NUL - * (non-negative), or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_commandlineoption_get_setting(OakCommandLineOption option, - char *buf, int buf_size); - -/** - * @brief Set an option's argument value. - * - * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_commandlineoption_set_setting(OakCommandLineOption option, - const char *value); - -/** - * @brief Get a positional argument's value (two-stage string getter). - * - * @return Required buffer size in bytes including the terminating NUL - * (non-negative), or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_commandlinepositionalargument_get_setting( - OakCommandLinePositionalArgument argument, char *buf, int buf_size); - -/** - * @brief Set a positional argument's value. - * - * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_commandlinepositionalargument_set_setting( - OakCommandLinePositionalArgument argument, const char *value); - -/** - * @brief Release one reference to a positional argument handle. - * - * Convenience wrapper around handle.release(handle.ctx). Does not - * unregister the argument from the parser. No-op when argument is NULL - * or argument->ctx is NULL. - */ -void oakcommon_commandlinepositionalargument_free( - OakCommandLinePositionalArgument *argument); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_COMMANDLINEPARSER_H diff --git a/crates/oakengine.bk/include/common/config.h b/crates/oakengine.bk/include/common/config.h deleted file mode 100644 index a6f731785..000000000 --- a/crates/oakengine.bk/include/common/config.h +++ /dev/null @@ -1,189 +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 . - -***/ - -#ifndef OAK_EDITOR_COMMON_CONFIG_H -#define OAK_EDITOR_COMMON_CONFIG_H - -#include - -#include "common/error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief De-Qt application configuration store, C ABI - * (M1-oakcommon.md §2.1, extended for the real consumer surface) - * - * oakcommon_config is a process-wide singleton key/value store (the de-Qt - * replacement for engine/config/config.h's QSettings/QVariant wrapper). - * Per the config-wave ruling it is NOT wrapped in the refcounted-handle - * convention of common/handle.h: there is exactly one store per process, - * so the family is a plain set of functions over that singleton (same - * singleton precedent as OakCurrent). - * - * Keys follow the frozen (group, key) convention of §2.1 and keep the - * QSettings INI shape: pass group == NULL (or "") for a top-level key, - * otherwise the entry is stored under an INI [group] section and - * addressed as "group/key" internally. - * - * Values are typed (string / int64 / double / bool). Rational settings - * are stored as strings in the "num/den" form used by - * oakcore_rational_to_string(). Typed getters take a fallback which is - * returned when the key is absent or has a different type (§2.1 special - * convention: they return values, not error codes). - * - * Persistence is an INI file at - * /config.ini. The store - * starts up with compiled-in defaults; oakcommon_config_load() re-reads - * the file (a missing file is not an error) and oakcommon_config_save() - * writes it. The OAK_CONFIG_DIR environment override honored by - * get_configuration_location() also redirects this file (tests/tooling). - * - * NOTE (behavior change): the old Qt implementation persisted to - * config.xml (engine XML) — and on macOS QSettings used a plist — so - * previously saved settings do NOT carry over; the first run starts from - * the compiled-in defaults. - */ - -typedef enum OakCommonConfigEntryType { - OAKCOMMON_CONFIG_ENTRY_NONE = 0, /**< No entry / null type. */ - OAKCOMMON_CONFIG_ENTRY_STRING = 1, - OAKCOMMON_CONFIG_ENTRY_INT = 2, - OAKCOMMON_CONFIG_ENTRY_DOUBLE = 3, - OAKCOMMON_CONFIG_ENTRY_BOOL = 4 -} OakCommonConfigEntryType; - -/** - * @brief Handler for configuration errors that should be shown to the user - * - * The engine layer cannot show dialogs itself. The UI registers a handler - * (e.g. QMessageBox-based) at startup; without one, errors go to stderr. - * Same injection pattern as the codec task-submit callback. - */ -typedef void (*OakCommonConfigErrorHandler)(const char *title, - const char *message, - void *userdata); - -/** - * @brief Resets the store to compiled-in defaults and loads config.ini - * - * A missing file leaves the defaults in place and returns OAKCOMMON_OK. - * Malformed lines are skipped. An unreadable existing file is reported - * through the error handler and returns OAKCOMMON_E_FAILED. - */ -int oakcommon_config_load(void); - -/** - * @brief Writes the current store to config.ini (via a temp file + rename) - * - * On failure the error handler is invoked and OAKCOMMON_E_FAILED is - * returned. - */ -int oakcommon_config_save(void); - -/** - * @brief Resets the store to compiled-in defaults (drops custom keys) - */ -int oakcommon_config_reset_defaults(void); - -/** - * @brief Sets a string entry (§2.1) - * - * A new key is created as OAKCOMMON_CONFIG_ENTRY_STRING. Setting an - * existing typed (INT/DOUBLE/BOOL) entry parses the string into its - * declared type; an unparseable value returns OAKCOMMON_E_STATE and - * leaves the entry unchanged. - */ -void oakcommon_config_set(const char *group, const char *key, - const char *value_utf8); - -/** - * @brief Reads an entry as a string, two-stage buffer (§2.1) - * - * Numeric/bool entries are formatted (bools as "true"/"false", doubles - * with %g). - * - * @return Required buffer size in bytes (including the terminating NUL), - * or a negative OAKCOMMON_E_* error code (OAKCOMMON_E_NOT_FOUND when the - * key is absent). - */ -int oakcommon_config_get(const char *group, const char *key, char *buf, - int buf_size); - -/** - * @brief Reads an INT entry as int (§2.1) - * - * @return The stored value, or `fallback` when the key is absent or has - * a different type. - */ -int oakcommon_config_get_int(const char *group, const char *key, - int fallback); - -/** - * @brief Reads a DOUBLE entry (§2.1), fallback semantics as get_int - */ -double oakcommon_config_get_double(const char *group, const char *key, - double fallback); - -/** - * @brief Sets an INT entry (32-bit, §2.1) - */ -void oakcommon_config_set_int(const char *group, const char *key, int v); - -/** - * @brief INT entry as int64 (extension for channel-layout style values) - */ -int64_t oakcommon_config_get_int64(const char *group, const char *key, - int64_t fallback); -void oakcommon_config_set_int64(const char *group, const char *key, - int64_t v); - -/** - * @brief BOOL entry as int 0/1 (extension), fallback semantics as get_int - */ -int oakcommon_config_get_bool(const char *group, const char *key, - int fallback); -void oakcommon_config_set_bool(const char *group, const char *key, int v); - -/** - * @brief Sets a DOUBLE entry (extension) - */ -void oakcommon_config_set_double(const char *group, const char *key, - double v); - -/** - * @brief Returns the OakCommonConfigEntryType of a key, or a negative - * OAKCOMMON_E_* error (OAKCOMMON_E_NOT_FOUND when the key is absent) - */ -int oakcommon_config_entry_type(const char *group, const char *key); - -/** - * @brief Registers (or clears, with NULL) the error handler - */ -int oakcommon_config_set_error_handler(OakCommonConfigErrorHandler handler, - void *userdata); - -#ifdef __cplusplus -} -#endif - -#endif // OAK_EDITOR_COMMON_CONFIG_H diff --git a/crates/oakengine.bk/include/common/current.h b/crates/oakengine.bk/include/common/current.h deleted file mode 100644 index 867fafd54..000000000 --- a/crates/oakengine.bk/include/common/current.h +++ /dev/null @@ -1,123 +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 . - -***/ - -#ifndef OAK_EDITOR_CURRENT_H -#define OAK_EDITOR_CURRENT_H - -#include "common/error.h" -#include "common/handle.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Neutral by-value handle to the process-wide Current singleton. - * - * Uses the standard handle layout (see common/handle.h) but with - * singleton semantics: ctx points to a statically allocated object that - * lives until process exit, so addref() and release() are intentionally - * no-ops and never destroy anything. abi_version is always - * OAKCOMMON_ABI_VERSION. - */ -typedef struct OakCurrent { - void *ctx; /**< Opaque pointer to the singleton object. */ - void (*addref)(void *ctx); /**< No-op (singleton). */ - void (*release)(void *ctx); /**< No-op (singleton). */ - uint32_t abi_version; /**< OAKCOMMON_ABI_VERSION. */ -} OakCurrent; - -/** - * @brief Destructor callback for objects handed to Current slots. - * - * Called when the slot is overwritten or cleared. May be NULL if the - * caller keeps ownership of the object. - */ -typedef void (*OakDestroyFn)(void *obj); - -/** - * @brief Return a handle to the process-wide Current singleton. - * - * The returned handle is borrowed: its ctx is valid for the lifetime of - * the process. addref/release on it are no-ops; calling - * oakcommon_current_free() is allowed for symmetry and does nothing. - */ -OakCurrent oakcommon_current_instance(void); - -/** - * @brief Release a Current handle. - * - * No-op: the underlying object is a singleton whose release() never - * destroys anything. Safe to call with NULL or a ctx == NULL handle. - */ -void oakcommon_current_free(OakCurrent *self); - -/** - * @brief Store a pointer in a Current slot, taking over destruction. - * - * Passing NULL for obj clears the slot (destroy is ignored). If a - * previous object with a destroy callback was stored, it is destroyed. - * - * @param self Handle from oakcommon_current_instance(). - * @param obj Opaque pointer to the external object (e.g. a - * VideoParams), or NULL to clear. - * @param destroy Optional destructor invoked when the slot is replaced. - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if self.ctx is NULL. - */ -int oakcommon_current_set_video_params(OakCurrent self, void *obj, - OakDestroyFn destroy); -int oakcommon_current_set_audio_params(OakCurrent self, void *obj, - OakDestroyFn destroy); -int oakcommon_current_set_plugin_host(OakCurrent self, void *obj, - OakDestroyFn destroy); -int oakcommon_current_set_plugin_cache(OakCurrent self, void *obj, - OakDestroyFn destroy); - -/** - * @brief Fetch the raw pointer currently stored in a slot. - * - * The returned pointer is borrowed and remains valid until the slot is - * overwritten or cleared. *out is set to NULL when the slot is empty. - * - * @param self Handle from oakcommon_current_instance(). - * @param out Receives the stored pointer. - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if self.ctx or - * out is NULL. - */ -int oakcommon_current_get_video_params(OakCurrent self, void **out); -int oakcommon_current_get_audio_params(OakCurrent self, void **out); -int oakcommon_current_get_plugin_host(OakCurrent self, void **out); -int oakcommon_current_get_plugin_cache(OakCurrent self, void **out); - -/** - * @brief Query whether the session is interactive. - * - * @param self Handle from oakcommon_current_instance(). - * @param out Receives 1 for interactive, 0 otherwise. - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if self.ctx or - * out is NULL. - */ -int oakcommon_current_is_interactive(OakCurrent self, int *out); - -#ifdef __cplusplus -} -#endif - -#endif // OAK_EDITOR_CURRENT_H diff --git a/crates/oakengine.bk/include/common/debug.h b/crates/oakengine.bk/include/common/debug.h deleted file mode 100644 index 6b89470a2..000000000 --- a/crates/oakengine.bk/include/common/debug.h +++ /dev/null @@ -1,113 +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 . - -***/ - -#ifndef OAK_EDITOR_DEBUG_H -#define OAK_EDITOR_DEBUG_H - -#include "common/error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Severity levels for oakcommon debug output. - * - * Mirrors olive::DebugLevel in src/common/src/debug.h. - */ -enum OakDebugLevel { - OAKCOMMON_DEBUG_DEBUG = 0, /**< Verbose debug message. */ - OAKCOMMON_DEBUG_INFO = 1, /**< Informational message. */ - OAKCOMMON_DEBUG_WARNING = 2, /**< Warning message. */ - OAKCOMMON_DEBUG_ERROR = 3, /**< Error message. */ - OAKCOMMON_DEBUG_FATAL = 4 /**< Fatal error message. */ -}; - -/** - * @brief Print a debug message to stderr, prefixed with its level. - * - * De-Qt replacement for the old Qt message handler. The line is - * flushed immediately. - * - * @param level One of OakDebugLevel; out-of-range values print - * as "UNKNOWN". - * @param msg NUL-terminated message; NULL is treated as an empty - * string. - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if msg is NULL. - */ -int oakcommon_debug_log(int level, const char *msg); - -/** - * @brief Copy the printable name of a debug level into buf. - * - * Two-segment string getter: if buf is NULL or buf_size is too small, - * nothing is written. - * - * @param level One of OakDebugLevel. - * @param buf Destination buffer, may be NULL to query the size. - * @param buf_size Size of buf in bytes. - * @return Required buffer size in bytes including the terminating NUL - * (non-negative). - */ -int oakcommon_debug_level_name(int level, char *buf, int buf_size); - -/** - * @brief printf-style filtered log, replacing qDebug()/qInfo()/ - * qWarning()/qCritical() call sites. - * - * The message is formatted with vsnprintf into a dynamically sized - * buffer (arbitrary length, no truncation, no fixed stack buffer) and - * emitted as "[LEVEL] message\n" unless @p level is below the current - * filter level (see oakcommon_log_set_level()). - * - * @param level One of OakDebugLevel; out-of-range values print - * as "UNKNOWN" and are never filtered out below FATAL. - * @param fmt printf-style format string. Must not be NULL. - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if fmt is NULL, - * OAKCOMMON_E_FAILED if formatting failed. - */ -int oakcommon_log(int level, const char *fmt, ...); - -/** - * @brief Set the minimum level emitted by oakcommon_log(). - * - * Messages with a lower level are dropped. The default is - * OAKCOMMON_DEBUG_INFO. - * - * @param level One of OakDebugLevel. - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if level is - * outside the OakDebugLevel range. - */ -int oakcommon_log_set_level(int level); - -/** - * @brief Query the current minimum level emitted by oakcommon_log(). - * - * @param out_level Receives one of OakDebugLevel. Must not be NULL. - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out_level is - * NULL. - */ -int oakcommon_log_get_level(int *out_level); - -#ifdef __cplusplus -} -#endif - -#endif // OAK_EDITOR_DEBUG_H diff --git a/crates/oakengine.bk/include/common/dropworkflowbehavior.h b/crates/oakengine.bk/include/common/dropworkflowbehavior.h deleted file mode 100644 index 1fe7bc0b6..000000000 --- a/crates/oakengine.bk/include/common/dropworkflowbehavior.h +++ /dev/null @@ -1,73 +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 . - -***/ - -#ifndef OAK_EDITOR_DROPWORKFLOWBEHAVIOR_H -#define OAK_EDITOR_DROPWORKFLOWBEHAVIOR_H - -#include "common/error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Behavior when media is dropped onto a timeline without a - * sequence. - * - * Mirrors olive::DropWithoutSequenceBehavior in - * src/common/src/dropworkflowbehavior.h; enumerator order and values - * must stay identical because the config layer persists them as ints. - */ -enum OakDropWorkflowBehavior { - OAKCOMMON_DWS_ASK = 0, /**< Ask the user every time. */ - OAKCOMMON_DWS_AUTO = 1, /**< Automatically create a sequence. */ - OAKCOMMON_DWS_MANUAL = 2, /**< Never create; import manually. */ - OAKCOMMON_DWS_DISABLE = 3 /**< Disable dropping entirely. */ -}; - -/** - * @brief Check whether value is a valid OakDropWorkflowBehavior. - * - * @param value Integer behavior value (e.g. read from config). - * @return 1 if valid, 0 otherwise (this is a predicate, not a status - * code). - */ -int oakcommon_drop_workflow_behavior_is_valid(int value); - -/** - * @brief Copy the printable name of a behavior into buf. - * - * Two-segment string getter: if buf is NULL or buf_size is too small, - * nothing is written. Invalid values yield "UNKNOWN". - * - * @param value One of OakDropWorkflowBehavior. - * @param buf Destination buffer, may be NULL to query the size. - * @param buf_size Size of buf in bytes. - * @return Required buffer size in bytes including the terminating NUL - * (non-negative). - */ -int oakcommon_drop_workflow_behavior_name(int value, char *buf, - int buf_size); - -#ifdef __cplusplus -} -#endif - -#endif // OAK_EDITOR_DROPWORKFLOWBEHAVIOR_H diff --git a/crates/oakengine.bk/include/common/error.h b/crates/oakengine.bk/include/common/error.h deleted file mode 100644 index 13bb7e3ed..000000000 --- a/crates/oakengine.bk/include/common/error.h +++ /dev/null @@ -1,66 +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 . - -***/ - -#ifndef OAK_EDITOR_ERROR_H -#define OAK_EDITOR_ERROR_H - -/** - * @brief Status and error codes shared by all oakcommon C API families. - * - * Return-code convention (mirrors engine/include/oakengine/init.h): - * 0 (OAKCOMMON_OK) on success, a negative OAKCOMMON_E_* error code on - * failure. String getters return the required buffer size in bytes - * (including the terminating NUL) as a non-negative value instead. - * - * Project-wide error code scheme (-MMCCCC, 2026-08): - * every module's error codes are negative integers of the form - * -(MM * 10000 + CCCC), where MM is the module number from the registry - * below and CCCC is a module-local code. The first module-local codes - * are reserved and identical across modules: 0001 INVALID, 0002 STATE, - * 0003 FAILED, 0004 NOT_FOUND, 0005 NOMEM, 0006 CANCELLED. - * - * An error code crossing a module boundary is passed through - * UNTRANSLATED — the numeric module prefix preserves provenance - * (e.g. -30004 is oaknode's NOT_FOUND no matter which module reports it - * to the caller). - * - * Module number registry (only ever appended to; numbers are frozen): - */ -#define OAK_ERROR_MODULE_COMMON 1 /**< oakcommon */ -#define OAK_ERROR_MODULE_UNDO 2 /**< oakundo */ -#define OAK_ERROR_MODULE_NODE 3 /**< oaknode */ -#define OAK_ERROR_MODULE_TIMELINE 4 /**< oaktimeline */ -#define OAK_ERROR_MODULE_CODEC 5 /**< oakcodec */ -#define OAK_ERROR_MODULE_AUDIO 6 /**< oakaudio */ -#define OAK_ERROR_MODULE_RENDER 7 /**< oakrender */ -#define OAK_ERROR_MODULE_TASK 8 /**< oaktask */ -#define OAK_ERROR_MODULE_PLUGIN 9 /**< oakplugin */ -#define OAK_ERROR_MODULE_STORAGE 10 /**< oakstorage (reserved) */ - -#define OAKCOMMON_OK 0 /**< Success. */ -#define OAKCOMMON_E_INVALID (-10001) /**< Empty handle (ctx == NULL) or invalid argument. */ -#define OAKCOMMON_E_STATE (-10002) /**< Call not valid in the current state. */ -#define OAKCOMMON_E_FAILED (-10003) /**< The underlying operation failed. */ -#define OAKCOMMON_E_NOT_FOUND (-10004) /**< Index out of range / entry not found. */ -#define OAKCOMMON_E_NOMEM (-10005) /**< Allocation failed. */ - -#define SUCCESS OAKCOMMON_OK /**< @deprecated Use OAKCOMMON_OK. */ - -#endif //OAK_EDITOR_ERROR_H diff --git a/crates/oakengine.bk/include/common/ffmpegutils.h b/crates/oakengine.bk/include/common/ffmpegutils.h deleted file mode 100644 index ee589b567..000000000 --- a/crates/oakengine.bk/include/common/ffmpegutils.h +++ /dev/null @@ -1,116 +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 . - -***/ - -#ifndef OAK_EDITOR_FFMPEGUTILS_H -#define OAK_EDITOR_FFMPEGUTILS_H - -#include "common/error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Stateless mappings between native pixel/sample formats and the - * opaque FBPixelFormat / FBSampleFormat constants of ffmpeg_bridge - * - * All functions are pure format conversions; there is no handle to create - * or free. Native pixel/sample formats are passed as plain ints matching - * the olive::core::PixelFormat::Format / SampleFormat::Format enum values - * (invalid = -1). Bridge formats are the fb_pix_fmt_* / fb_sample_fmt_* - * constants from ffmpeg_bridge/ffmpeg_bridge.h. - */ - -/** - * @brief RGB / RGBA channel counts (flattened from VideoParams) - */ -#define OAKCOMMON_RGB_CHANNEL_COUNT 3 -#define OAKCOMMON_RGBA_CHANNEL_COUNT 4 - -/** - * @brief Returns a bridge pixel format that a frame can be converted to - * with minimal data loss, clamped to a maximum native precision - * - * @param pix_fmt bridge pixel format to find a compatible conversion for - * @param maximum_pix_fmt maximum native pixel format, or -1 for no limit - * @param out receives the chosen bridge pixel format - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out is NULL - */ -int oakcommon_ffmpegutils_get_compatible_bridge_pixel_format( - int pix_fmt, int maximum_pix_fmt, int *out); - -/** - * @brief Returns a native pixel format usable to convert from a native - * frame to a bridge frame with minimal data loss - * - * @param pix_fmt native pixel format - * @param out receives the compatible native pixel format (-1 if none) - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out is NULL - */ -int oakcommon_ffmpegutils_get_compatible_pixel_format(int pix_fmt, - int *out); - -/** - * @brief Returns a bridge pixel format for a given native pixel format - * - * @param pix_fmt native pixel format - * @param channel_count OAKCOMMON_RGB_CHANNEL_COUNT or - * OAKCOMMON_RGBA_CHANNEL_COUNT - * @param out receives the bridge pixel format (fb_pix_fmt_none if none) - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out is NULL - */ -int oakcommon_ffmpegutils_get_ffmpeg_pixel_format(int pix_fmt, - int channel_count, - int *out); - -/** - * @brief Returns a native sample format for a given bridge sample format - * - * @param smp_fmt bridge sample format - * @param out receives the native sample format (-1 if unknown) - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out is NULL - */ -int oakcommon_ffmpegutils_get_native_sample_format(int smp_fmt, int *out); - -/** - * @brief Returns a bridge sample format for a given native sample format - * - * @param smp_fmt native sample format - * @param out receives the bridge sample format (fb_sample_fmt_none if none) - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out is NULL - */ -int oakcommon_ffmpegutils_get_ffmpeg_sample_format(int smp_fmt, int *out); - -/** - * @brief Converts a "JPEG" full-range bridge pixel format to its regular - * counterpart - * - * @param pix_fmt bridge pixel format - * @param out receives the regular-range format (unchanged if not JPEG) - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out is NULL - */ -int oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(int pix_fmt, - int *out); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_FFMPEGUTILS_H diff --git a/crates/oakengine.bk/include/common/filefunctions.h b/crates/oakengine.bk/include/common/filefunctions.h deleted file mode 100644 index ff26a1ede..000000000 --- a/crates/oakengine.bk/include/common/filefunctions.h +++ /dev/null @@ -1,167 +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 . - -***/ - -#ifndef OAK_EDITOR_FILEFUNCTIONS_H -#define OAK_EDITOR_FILEFUNCTIONS_H - -#include "common/error.h" -#include "common/handle.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Neutral by-value handle for the filefunctions family - * - * File functions are stateless; the handle only exists to keep the C API - * shape uniform across oakcommon families. Ownership/count semantics - * follow the convention in common/handle.h: init returns a handle whose - * (empty) object has reference count 1, addref(ctx)/release(ctx) adjust - * it atomically, and release destroys it at zero. abi_version is always - * OAKCOMMON_ABI_VERSION. - */ -typedef struct OakFileFunctions { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKCOMMON_ABI_VERSION. */ -} OakFileFunctions; - -/** - * @brief Creates a filefunctions handle - * - * @return Handle with reference count 1; ctx is NULL on failure. - */ -OakFileFunctions oakcommon_filefunctions_init(void); - -/** - * @brief Releases one reference to a filefunctions handle - * - * Convenience wrapper around handle.release(handle.ctx): decrements the - * atomic reference count and destroys the object when it reaches zero. - * No-op when self is NULL or self->ctx is NULL. - */ -void oakcommon_filefunctions_free(OakFileFunctions *self); - -/** - * @brief Returns a deterministic identifier string for a file - * - * @return Required buffer size in bytes (including NUL), or a negative - * OAKCOMMON_E_* error code. Returns an empty string (required size 1) if - * the file does not exist. - */ -int oakcommon_filefunctions_get_unique_file_identifier( - OakFileFunctions self, const char *filename, char *buf, - int buf_size); - -int oakcommon_filefunctions_get_configuration_location( - OakFileFunctions self, char *buf, int buf_size); - -int oakcommon_filefunctions_get_application_path( - OakFileFunctions self, char *buf, int buf_size); - -int oakcommon_filefunctions_get_temp_file_path( - OakFileFunctions self, char *buf, int buf_size); - -int oakcommon_filefunctions_get_auto_recovery_root( - OakFileFunctions self, char *buf, int buf_size); - -/** - * @brief Checks whether `source` can be copied to `dest` without - * overwriting anything - * - * @param out Receives 1 (safe) or 0 (would overwrite). - */ -int oakcommon_filefunctions_can_copy_directory_without_overwriting( - OakFileFunctions self, const char *source, const char *dest, - int *out); - -/** - * @brief Recursively copies a directory - */ -int oakcommon_filefunctions_copy_directory(OakFileFunctions self, - const char *source, - const char *dest, int overwrite); - -/** - * @brief Checks whether a directory exists, optionally creating it - * - * @param out Receives 1 (valid) or 0 (invalid). - */ -int oakcommon_filefunctions_directory_is_valid( - OakFileFunctions self, const char *dir, - int try_to_create_if_not_exists, int *out); - -/** - * @brief Ensures a filename ends with the given extension (no dot) - * - * @return Required buffer size in bytes (including NUL), or a negative - * OAKCOMMON_E_* error code. - */ -int oakcommon_filefunctions_ensure_filename_extension( - OakFileFunctions self, const char *filename, - const char *extension, char *buf, int buf_size); - -/** - * @brief Reads an entire file into a string - * - * @return Required buffer size in bytes (including NUL), or a negative - * OAKCOMMON_E_* error code. Returns an empty string (required size 1) if - * the file cannot be read. - */ -int oakcommon_filefunctions_read_file_as_string( - OakFileFunctions self, const char *filename, char *buf, - int buf_size); - -/** - * @brief Returns a non-existing temporary variant of `original` - * - * @return Required buffer size in bytes (including NUL), or a negative - * OAKCOMMON_E_* error code. - */ -int oakcommon_filefunctions_get_safe_temporary_filename( - OakFileFunctions self, const char *original, char *buf, - int buf_size); - -/** - * @brief Renames `from` to `to`, deleting `to` first if it exists - * - * @param out Receives 1 (renamed) or 0 (failed). - */ -int oakcommon_filefunctions_rename_file_allow_overwrite( - OakFileFunctions self, const char *from, const char *to, - int *out); - -/** - * @brief Appends the platform executable suffix (".exe" on Windows) - * - * @return Required buffer size in bytes (including NUL), or a negative - * OAKCOMMON_E_* error code. - */ -int oakcommon_filefunctions_get_formatted_executable_for_platform( - OakFileFunctions self, const char *unformatted, char *buf, - int buf_size); - -#ifdef __cplusplus -} -#endif - -#endif // OAK_EDITOR_FILEFUNCTIONS_H diff --git a/crates/oakengine.bk/include/common/handle.h b/crates/oakengine.bk/include/common/handle.h deleted file mode 100644 index 8a6be7900..000000000 --- a/crates/oakengine.bk/include/common/handle.h +++ /dev/null @@ -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 . - -***/ - -#ifndef OAK_EDITOR_HANDLE_H -#define OAK_EDITOR_HANDLE_H - -#include - -/** - * @brief Current ABI version stamped into every oakcommon handle. - * - * Bump whenever the handle layout or the semantics of any exported - * function change incompatibly. Consumers should compare a handle's - * abi_version field against the value they were compiled with before - * dereferencing ctx. - */ -#define OAKCOMMON_ABI_VERSION 1 - -/** - * @brief Neutral handle convention shared by all oakcommon wrappers. - * - * Every wrapper type is a by-value struct with the same four fields: - * - * typedef struct OakXxx { - * void *ctx; // opaque, points to the impl - * void (*addref)(void *ctx); // atomic +1, owner-DLL code - * void (*release)(void *ctx); // atomic -1, destroys at 0 - * uint32_t abi_version; // OAKCOMMON_ABI_VERSION - * } OakXxx; - * - * Rules: - * - oakcommon__init*() returns a handle whose underlying object - * has reference count 1. - * - Copying the struct copies the pointer, not the count: call - * handle.addref(handle.ctx) for every additional long-lived copy and - * handle.release(handle.ctx) (or the oakcommon__free() - * convenience wrapper) when done with each copy. - * - release() decrements the atomic count and destroys the underlying - * object when it reaches zero; the destructor runs in the DLL that - * created the object, so cross-DLL handing is safe. - * - The struct itself carries no ownership: it is never heap-allocated - * by the API, so it needs no destruction of its own. - * - Functions that only read a handle take it BY VALUE (OakXxx self); - * an empty handle (ctx == NULL) is reported as OAKCOMMON_E_INVALID. - * oakcommon__free() deliberately stays a pointer API - * (OakXxx *h, like av_frame_unref()/av_buffer_unref()) so it can - * null out the caller's ctx after the final release; NULL and - * ctx == NULL are no-ops. Out parameters that produce a handle - * (e.g. option/positional-argument registration) also stay pointers. - */ - -#endif //OAK_EDITOR_HANDLE_H diff --git a/crates/oakengine.bk/include/common/loopmode.h b/crates/oakengine.bk/include/common/loopmode.h deleted file mode 100644 index d78bb9738..000000000 --- a/crates/oakengine.bk/include/common/loopmode.h +++ /dev/null @@ -1,44 +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 . - -***/ - -#ifndef OAK_EDITOR_LOOPMODE_H -#define OAK_EDITOR_LOOPMODE_H - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Playback loop mode, mirroring olive::LoopMode. - * - * The numeric values must stay in sync with src/common/src/loopmode.h. - * Pure enum: no functions are needed. - */ -enum OakLoopMode { - OAKCOMMON_LOOP_MODE_OFF = 0, /**< Looping disabled. */ - OAKCOMMON_LOOP_MODE_LOOP = 1, /**< Loop playback. */ - OAKCOMMON_LOOP_MODE_CLAMP = 2 /**< Clamp at the end. */ -}; - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_LOOPMODE_H diff --git a/crates/oakengine.bk/include/common/miscutils.h b/crates/oakengine.bk/include/common/miscutils.h deleted file mode 100644 index 35df776b9..000000000 --- a/crates/oakengine.bk/include/common/miscutils.h +++ /dev/null @@ -1,117 +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 . - -***/ - -#ifndef OAK_EDITOR_MISCUTILS_H -#define OAK_EDITOR_MISCUTILS_H - -#include "common/error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Minimum decibel value used by the editor (-200.0 dB). - * - * In basically all circumstances, this calculates to 0.0 linear. - */ -#define OAKCOMMON_DECIBEL_MINIMUM (-200.0) - -/** - * @brief Convert a linear amplitude to decibels. - * - * A linear value of 0.0 (or anything yielding an infinite result) returns - * OAKCOMMON_DECIBEL_MINIMUM. - * - * @param linear Linear amplitude. - * @param out_db Receives the decibel value. Must not be NULL. - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out_db is NULL. - */ -int oakcommon_decibel_from_linear(double linear, double *out_db); - -/** - * @brief Convert decibels to a linear amplitude. - * - * Results below 1e-6 are clamped to 0.0. - * - * @param db Decibel value. - * @param out_linear Receives the linear amplitude. Must not be NULL. - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out_linear is NULL. - */ -int oakcommon_decibel_to_linear(double db, double *out_linear); - -/** - * @brief Convert a logarithmic slider position (0..1) to decibels. - * - * @param logarithmic Logarithmic position. - * @param out_db Receives the decibel value. Must not be NULL. - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out_db is NULL. - */ -int oakcommon_decibel_from_logarithmic(double logarithmic, double *out_db); - -/** - * @brief Convert decibels to a logarithmic slider position (0..1). - * - * @param db Decibel value. - * @param out_logarithmic Receives the logarithmic position. Must not be NULL. - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out_logarithmic - * is NULL. - */ -int oakcommon_decibel_to_logarithmic(double db, double *out_logarithmic); - -/** - * @brief Convert a linear amplitude directly to a logarithmic position. - * - * @param linear Linear amplitude. - * @param out_logarithmic Receives the logarithmic position. Must not be NULL. - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out_logarithmic - * is NULL. - */ -int oakcommon_decibel_linear_to_logarithmic(double linear, - double *out_logarithmic); - -/** - * @brief Convert a logarithmic position directly to a linear amplitude. - * - * @param logarithmic Logarithmic position. - * @param out_linear Receives the linear amplitude. Must not be NULL. - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out_linear is NULL. - */ -int oakcommon_decibel_logarithmic_to_linear(double logarithmic, - double *out_linear); - -/** - * @brief Linearly interpolate between a and b using t. - * - * t should be between 0.0 and 1.0: 0.0 returns a, 1.0 returns b. - * - * @param a Start value. - * @param b End value. - * @param t Interpolation factor. - * @param out_value Receives the interpolated value. Must not be NULL. - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out_value is NULL. - */ -int oakcommon_lerp(double a, double b, double t, double *out_value); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_MISCUTILS_H diff --git a/crates/oakengine.bk/include/common/ocioutils.h b/crates/oakengine.bk/include/common/ocioutils.h deleted file mode 100644 index f3be72246..000000000 --- a/crates/oakengine.bk/include/common/ocioutils.h +++ /dev/null @@ -1,105 +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 . - -***/ - -#ifndef OAK_EDITOR_OCIOUTILS_H -#define OAK_EDITOR_OCIOUTILS_H - -#include "common/error.h" -#include "common/handle.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Native pixel format codes, mirroring olive::core::PixelFormat - * - * The numeric values must stay in sync with - * olive/core/render/pixelformat.h (Format enum). - */ -enum OakPixelFormat { - OAKCOMMON_PIXEL_FORMAT_INVALID = -1, /**< Invalid/unknown format. */ - OAKCOMMON_PIXEL_FORMAT_U8 = 0, /**< 8-bit unsigned integer. */ - OAKCOMMON_PIXEL_FORMAT_U10 = 1, /**< 10-bit unsigned integer. */ - OAKCOMMON_PIXEL_FORMAT_U16 = 2, /**< 16-bit unsigned integer. */ - OAKCOMMON_PIXEL_FORMAT_F16 = 3, /**< 16-bit float (half). */ - OAKCOMMON_PIXEL_FORMAT_F32 = 4, /**< 32-bit float. */ - OAKCOMMON_PIXEL_FORMAT_COUNT = 5 /**< Sentinel, not a valid format. */ -}; - -/** - * @brief OpenColorIO bit depth codes, matching OCIO::BitDepth - * - * Returned through the out parameter of - * oakcommon_ocioutils_get_ocio_bit_depth_from_pixel_format() as a plain - * int so that callers never see OCIO types. Values match the OCIO - * BitDepth enum: 0 = unknown, 1 = uint8, 2 = uint10, 3 = uint12, - * 4 = uint14, 5 = uint16, 6 = uint32, 7 = f16, 8 = f32 (OCIO v2). - */ -/** - * @brief Neutral by-value handle for the OCIO utils family - * - * The object is stateless; the handle exists only to satisfy the C API - * lifetime contract. Ownership/count semantics follow common/handle.h: - * init returns a handle whose (empty) object has reference count 1 and - * release destroys it at zero. abi_version is always - * OAKCOMMON_ABI_VERSION. - */ -typedef struct OakOCIOUtils { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKCOMMON_ABI_VERSION. */ -} OakOCIOUtils; - -/** - * @brief Creates an OCIOUtils handle - * - * @return Handle with reference count 1; ctx is NULL on failure. - */ -OakOCIOUtils oakcommon_ocioutils_init(void); - -/** - * @brief Releases one reference to an OCIOUtils handle - * - * Convenience wrapper around handle.release(handle.ctx); no-op when - * self is NULL or self->ctx is NULL. - */ -void oakcommon_ocioutils_free(OakOCIOUtils *self); - -/** - * @brief Maps a native pixel format to an OCIO bit depth - * - * @param self handle from oakcommon_ocioutils_init() - * @param pixel_format one of the OakPixelFormat values - * @param out_bit_depth receives the OCIO bit depth as an int (see the - * OakOCIOUtils typedef documentation); set to 0 - * (BIT_DEPTH_UNKNOWN) for invalid formats - * @return OAKCOMMON_OK, or OAKCOMMON_E_INVALID if self.ctx or - * out_bit_depth is NULL or pixel_format is not a known code - */ -int oakcommon_ocioutils_get_ocio_bit_depth_from_pixel_format( - OakOCIOUtils self, int pixel_format, int *out_bit_depth); - -#ifdef __cplusplus -} -#endif - -#endif // OAK_EDITOR_OCIOUTILS_H diff --git a/crates/oakengine.bk/include/common/oiioutils.h b/crates/oakengine.bk/include/common/oiioutils.h deleted file mode 100644 index ee48dfab8..000000000 --- a/crates/oakengine.bk/include/common/oiioutils.h +++ /dev/null @@ -1,125 +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 . - -***/ - -#ifndef OAK_EDITOR_OIIOUTILS_H -#define OAK_EDITOR_OIIOUTILS_H - -#include "common/error.h" - -/* Reuses the OakPixelFormat enum (mirroring - * olive::core::PixelFormat) rather than redefining it here. */ -#include "common/ocioutils.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief OIIO base type codes, matching OIIO::TypeDesc::BASETYPE - * - * Passed through the C API as plain ints so callers never see OIIO - * types. Values match the OIIO TypeDesc::BASETYPE enum: 0 = UNKNOWN, - * 1 = NONE, 2 = UINT8, 3 = INT8, 4 = UINT16, 5 = INT16, 6 = UINT32, - * 7 = INT32, 8 = UINT64, 9 = INT64, 10 = HALF, 11 = FLOAT, 12 = DOUBLE, - * 13 = STRING, 14 = PTR. OIIO >= 2.5 adds 15 = USTRINGHASH and shifts - * LASTBASE, so the exact LASTBASE value is version-dependent. - */ -/** - * @brief Neutral by-value handle for the OIIO utils family - * - * The object is stateless; the handle exists only to satisfy the C API - * lifetime contract. Ownership/count semantics follow common/handle.h: - * init returns a handle whose (empty) object has reference count 1 and - * release destroys it at zero. abi_version is always - * OAKCOMMON_ABI_VERSION. - */ -typedef struct OakOIIOUtils { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKCOMMON_ABI_VERSION. */ -} OakOIIOUtils; - -/** - * @brief Creates an OIIOUtils handle - * - * @return Handle with reference count 1; ctx is NULL on failure. - */ -OakOIIOUtils oakcommon_oiioutils_init(void); - -/** - * @brief Releases one reference to an OIIOUtils handle - * - * Convenience wrapper around handle.release(handle.ctx); no-op when - * self is NULL or self->ctx is NULL. - */ -void oakcommon_oiioutils_free(OakOIIOUtils *self); - -/** - * @brief Maps a native pixel format to an OIIO base type - * - * @param self handle from oakcommon_oiioutils_init() - * @param pixel_format one of the OakPixelFormat values - * @param out_base_type receives the OIIO base type as an int (see the - * OakOIIOUtils typedef documentation); set to 0 - * (TypeDesc::UNKNOWN) for invalid or unmappable formats - * @return OAKCOMMON_OK, or OAKCOMMON_E_INVALID if self.ctx or - * out_base_type is NULL or pixel_format is not a known code - */ -int oakcommon_oiioutils_get_oiio_base_type_from_format( - OakOIIOUtils self, int pixel_format, int *out_base_type); - -/** - * @brief Maps an OIIO base type to a native pixel format - * - * @param self handle from oakcommon_oiioutils_init() - * @param base_type an OIIO TypeDesc::BASETYPE value as an int - * @param out_pixel_format receives one of the OakPixelFormat - * values; set to OAKCOMMON_PIXEL_FORMAT_INVALID for unknown or - * unmappable base types - * @return OAKCOMMON_OK, or OAKCOMMON_E_INVALID if self.ctx or - * out_pixel_format is NULL or base_type is negative - */ -int oakcommon_oiioutils_get_format_from_oiio_basetype( - OakOIIOUtils self, int base_type, int *out_pixel_format); - -/** - * @brief Converts a PixelAspectRatio attribute value to a rational - * - * Flattened form of the former ImageSpec-based helper: the caller reads - * the "PixelAspectRatio" float attribute from the OIIO::ImageSpec - * (defaulting to 1.0 when absent) and passes it here. - * - * @param self handle from oakcommon_oiioutils_init() - * @param pixel_aspect_ratio the PixelAspectRatio attribute value - * @param out_numerator receives the rational numerator - * @param out_denominator receives the rational denominator - * @return OAKCOMMON_OK, or OAKCOMMON_E_INVALID if self.ctx, - * out_numerator or out_denominator is NULL - */ -int oakcommon_oiioutils_get_pixel_aspect_ratio( - OakOIIOUtils self, double pixel_aspect_ratio, int *out_numerator, - int *out_denominator); - -#ifdef __cplusplus -} -#endif - -#endif // OAK_EDITOR_OIIOUTILS_H diff --git a/crates/oakengine.bk/include/common/power.h b/crates/oakengine.bk/include/common/power.h deleted file mode 100644 index ccf54abba..000000000 --- a/crates/oakengine.bk/include/common/power.h +++ /dev/null @@ -1,58 +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 . - -***/ - -#ifndef OAK_EDITOR_POWER_H -#define OAK_EDITOR_POWER_H - -#include - -#include "error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Round `value` up to the next power of two - * - * Stateless pure function, no handle required. Writes the result to `out`. - * - * @param value Input value. - * @param out Receives the rounded value. Must not be NULL. - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if `out` is NULL. - */ -int oakcommon_power_ceil_to_power_of_2(uint32_t value, uint32_t *out); - -/** - * @brief Round `value` down to the nearest power of two - * - * Stateless pure function, no handle required. Writes the result to `out`. - * - * @param value Input value. - * @param out Receives the rounded value. Must not be NULL. - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if `out` is NULL. - */ -int oakcommon_power_floor_to_power_of_2(uint32_t value, uint32_t *out); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_POWER_H diff --git a/crates/oakengine.bk/include/common/qtutils.h b/crates/oakengine.bk/include/common/qtutils.h deleted file mode 100644 index c370e8a29..000000000 --- a/crates/oakengine.bk/include/common/qtutils.h +++ /dev/null @@ -1,67 +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 . - -***/ - -#ifndef OAK_COMMON_QTUTILS_H -#define OAK_COMMON_QTUTILS_H - -#include - -#include "common/error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Convert a pointer to an integer value - * - * @param ptr Pointer to convert (may be NULL, yielding 0). - * @param out_value Receives the integer representation of ptr. - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out_value is NULL. - */ -int oakcommon_qtutils_ptr_to_value(void *ptr, uint64_t *out_value); - -/** - * @brief Convert an integer produced by oakcommon_qtutils_ptr_to_value() back to a pointer - * - * @param value Integer representation of a pointer. - * @param out_ptr Receives the decoded pointer. - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out_ptr is NULL. - */ -int oakcommon_qtutils_value_to_ptr(uint64_t value, void **out_ptr); - -/** - * @brief Get the creation (birth) time of a file as seconds since the Unix epoch - * - * Falls back to the last metadata change time when the filesystem does not - * record birth times. - * - * @param path NUL-terminated filesystem path. - * @param out_secs Receives the creation time in seconds since the epoch. - * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID for NULL arguments, - * OAKCOMMON_E_NOT_FOUND if the file does not exist or cannot be stat'ed. - */ -int oakcommon_qtutils_get_creation_date(const char *path, int64_t *out_secs); - -#ifdef __cplusplus -} -#endif - -#endif // OAK_COMMON_QTUTILS_H diff --git a/crates/oakengine.bk/include/common/subtitleparams.h b/crates/oakengine.bk/include/common/subtitleparams.h deleted file mode 100644 index b25af15bc..000000000 --- a/crates/oakengine.bk/include/common/subtitleparams.h +++ /dev/null @@ -1,176 +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 . - -***/ - -#ifndef OAK_EDITOR_SUBTITLEPARAMS_H -#define OAK_EDITOR_SUBTITLEPARAMS_H - -#include "common/error.h" -#include "common/handle.h" - -#ifdef __cplusplus -namespace olive -{ -class SubtitleParams; -} -extern "C" { -#endif - -/** - * @brief Neutral by-value handle to a subtitle parameter set - * (olive::SubtitleParams). - * - * Ownership/count semantics follow the convention in common/handle.h: - * init functions return a handle whose object has reference count 1, - * addref(ctx)/release(ctx) adjust it atomically, and release destroys - * the object at zero. abi_version is always OAKCOMMON_ABI_VERSION. - */ -typedef struct OakSubtitleParams { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKCOMMON_ABI_VERSION. */ -} OakSubtitleParams; - -/** - * @brief Create an empty subtitle parameter set. - * - * @return Handle with reference count 1; ctx is NULL on allocation - * failure. - */ -OakSubtitleParams oakcommon_subtitleparams_init(void); - -#ifdef __cplusplus -/** - * @brief Copy a native olive::SubtitleParams into a new handle. - * - * The source object is deep-copied; the handle does not keep any - * reference to @p src, which may be destroyed immediately afterwards. - * Only visible to C++ consumers. - * - * @return Handle with reference count 1; ctx is NULL if src is NULL or - * on allocation failure. - */ -OakSubtitleParams oakcommon_subtitleparams_init_from_native( - const olive::SubtitleParams *src); -#endif - -/** - * @brief Release one reference to a subtitle parameter set. - * - * Convenience wrapper around handle.release(handle.ctx): decrements the - * atomic reference count and destroys the object when it reaches zero. - * No-op when params is NULL or params->ctx is NULL. - */ -void oakcommon_subtitleparams_free(OakSubtitleParams *params); - -int oakcommon_subtitleparams_get_stream_index( - OakSubtitleParams params, int *index); -int oakcommon_subtitleparams_set_stream_index( - OakSubtitleParams params, int index); -int oakcommon_subtitleparams_get_enabled(OakSubtitleParams params, - int *enabled); -int oakcommon_subtitleparams_set_enabled(OakSubtitleParams params, - int enabled); - -/** - * @brief Query whether the set contains at least one subtitle. - */ -int oakcommon_subtitleparams_is_valid(OakSubtitleParams params, - int *is_valid); - -/** - * @brief Number of subtitle entries. - */ -int oakcommon_subtitleparams_count(OakSubtitleParams params, - int *count); - -/** - * @brief Out time of the last subtitle (0/1 when empty). - */ -int oakcommon_subtitleparams_duration(OakSubtitleParams params, - int *numerator, int *denominator); - -/** - * @brief Append a subtitle entry. - * - * @param text Subtitle text. Must not be NULL. - * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_subtitleparams_add_subtitle(OakSubtitleParams params, - int in_num, int in_den, int out_num, - int out_den, const char *text); - -/** - * @brief Remove all subtitle entries. - */ -int oakcommon_subtitleparams_clear(OakSubtitleParams params); - -/** - * @brief Get the time range of the subtitle at @p index. - * - * @return OAKCOMMON_OK, OAKCOMMON_E_NOT_FOUND if @p index is out of range, - * or another negative OAKCOMMON_E_* error code. - */ -int oakcommon_subtitleparams_get_subtitle(OakSubtitleParams params, - int index, int *in_num, int *in_den, - int *out_num, int *out_den); - -/** - * @brief Get the text of the subtitle at @p index (two-stage string getter). - * - * @return Required buffer size in bytes including the terminating NUL - * (non-negative), OAKCOMMON_E_NOT_FOUND if @p index is out of - * range, or another negative OAKCOMMON_E_* error code. - */ -int oakcommon_subtitleparams_get_subtitle_text(OakSubtitleParams params, - int index, char *buf, - int buf_size); - -/** - * @brief Generate a default ASS header (static, no handle required). - * - * @return Required buffer size in bytes including the terminating NUL - * (non-negative), or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_subtitleparams_generate_ass_header(char *buf, int buf_size); - -/** - * @brief Load subtitles from an XML fragment. - * - * @param xml NUL-terminated XML text. Must not be NULL. - * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_subtitleparams_load_xml(OakSubtitleParams params, - const char *xml); - -/** - * @brief Save subtitles to an XML fragment (two-stage string getter). - * - * @return Required buffer size in bytes including the terminating NUL - * (non-negative), or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_subtitleparams_save_xml(OakSubtitleParams params, - char *buf, int buf_size); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_SUBTITLEPARAMS_H diff --git a/crates/oakengine.bk/include/common/videoparams.h b/crates/oakengine.bk/include/common/videoparams.h deleted file mode 100644 index 2dfb143e2..000000000 --- a/crates/oakengine.bk/include/common/videoparams.h +++ /dev/null @@ -1,367 +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 . - -***/ - -#ifndef OAK_EDITOR_VIDEOPARAMS_H -#define OAK_EDITOR_VIDEOPARAMS_H - -#ifndef __cplusplus -#include -#endif - -#include - -#include "common/error.h" -#include "common/handle.h" -#include "common/ocioutils.h" - -#ifdef __cplusplus -namespace olive -{ -class VideoParams; -} -extern "C" { -#endif - -/** - * @brief Neutral by-value handle to a video parameter set - * (olive::VideoParams). - * - * Ownership/count semantics follow the convention in common/handle.h: - * init functions return a handle whose object has reference count 1, - * addref(ctx)/release(ctx) adjust it atomically, and release destroys - * the object at zero. abi_version is always OAKCOMMON_ABI_VERSION. - */ -typedef struct OakVideoParams { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKCOMMON_ABI_VERSION. */ -} OakVideoParams; - -/** - * @brief Interlacing modes, mirroring olive::VideoParams::Interlacing. - */ -enum OakVideoInterlacing { - OAKCOMMON_VIDEO_INTERLACE_NONE = 0, - OAKCOMMON_VIDEO_INTERLACED_TOP_FIRST = 1, - OAKCOMMON_VIDEO_INTERLACED_BOTTOM_FIRST = 2 -}; - -/** - * @brief Video stream types, mirroring olive::VideoParams::Type. - */ -enum OakVideoType { - OAKCOMMON_VIDEO_TYPE_VIDEO = 0, - OAKCOMMON_VIDEO_TYPE_STILL = 1, - OAKCOMMON_VIDEO_TYPE_IMAGE_SEQUENCE = 2 -}; - -/** - * @brief Color range codes, mirroring olive::VideoParams::ColorRange. - */ -enum OakVideoColorRange { - OAKCOMMON_COLOR_RANGE_LIMITED = 0, /**< 16-235 */ - OAKCOMMON_COLOR_RANGE_FULL = 1 /**< 0-255 */ -}; - -/** - * @brief Create a default (invalid) video parameter set. - * - * @return Handle with reference count 1; ctx is NULL on allocation - * failure. - */ -OakVideoParams oakcommon_videoparams_init(void); - -/** - * @brief Create a video parameter set without a time base. - * - * @param pixel_format One of the OakPixelFormat values. - * @return Handle with reference count 1; ctx is NULL on allocation - * failure. - */ -OakVideoParams oakcommon_videoparams_init_basic( - int width, int height, int pixel_format, int nb_channels, - int pixel_aspect_num, int pixel_aspect_den, int interlacing, int divider); - -/** - * @brief Create a video parameter set with a time base. - * - * The frame rate is derived as the flipped time base. - * - * @param pixel_format One of the OakPixelFormat values. - * @return Handle with reference count 1; ctx is NULL on allocation - * failure. - */ -OakVideoParams oakcommon_videoparams_init_with_time_base( - int width, int height, int time_base_num, int time_base_den, - int pixel_format, int nb_channels, int pixel_aspect_num, - int pixel_aspect_den, int interlacing, int divider); - -#ifdef __cplusplus -/** - * @brief Copy a native olive::VideoParams into a new handle. - * - * The source object is deep-copied; the handle does not keep any - * reference to @p src, which may be destroyed immediately afterwards. - * Only visible to C++ consumers. - * - * @return Handle with reference count 1; ctx is NULL if src is NULL or - * on allocation failure. - */ -OakVideoParams oakcommon_videoparams_init_from_native( - const olive::VideoParams *src); - -/** - * @brief Borrow the native object behind a handle. - * - * The returned pointer is borrowed: it stays valid while the caller - * holds a reference to the handle (i.e. until the matching release). - * Only visible to C++ consumers. - * - * @return Borrowed pointer, or NULL if params is NULL or params->ctx is - * NULL. - */ -const olive::VideoParams *oakcommon_videoparams_get_native( - OakVideoParams params); -#endif - -/** - * @brief Release one reference to a video parameter set. - * - * Convenience wrapper around handle.release(handle.ctx): decrements the - * atomic reference count and destroys the object when it reaches zero. - * No-op when params is NULL or params->ctx is NULL. - */ -void oakcommon_videoparams_free(OakVideoParams *params); - -int oakcommon_videoparams_get_width(OakVideoParams params, int *width); -int oakcommon_videoparams_set_width(OakVideoParams params, int width); -int oakcommon_videoparams_get_height(OakVideoParams params, int *height); -int oakcommon_videoparams_set_height(OakVideoParams params, int height); -int oakcommon_videoparams_get_depth(OakVideoParams params, int *depth); -int oakcommon_videoparams_set_depth(OakVideoParams params, int depth); -int oakcommon_videoparams_get_is_3d(OakVideoParams params, int *is_3d); - -/** - * @brief Rational getters return the value as a numerator/denominator pair. - */ -int oakcommon_videoparams_get_time_base(OakVideoParams params, - int *numerator, int *denominator); -int oakcommon_videoparams_set_time_base(OakVideoParams params, - int numerator, int denominator); -int oakcommon_videoparams_get_frame_rate(OakVideoParams params, - int *numerator, int *denominator); -int oakcommon_videoparams_set_frame_rate(OakVideoParams params, - int numerator, int denominator); -int oakcommon_videoparams_frame_rate_as_time_base(OakVideoParams params, - int *numerator, - int *denominator); -int oakcommon_videoparams_get_pixel_aspect_ratio(OakVideoParams params, - int *numerator, - int *denominator); -int oakcommon_videoparams_set_pixel_aspect_ratio(OakVideoParams params, - int numerator, int denominator); - -/** - * @brief Format getters/setters use the OakPixelFormat codes. - */ -int oakcommon_videoparams_get_format(OakVideoParams params, int *format); -int oakcommon_videoparams_set_format(OakVideoParams params, int format); -int oakcommon_videoparams_get_channel_count(OakVideoParams params, - int *count); -int oakcommon_videoparams_set_channel_count(OakVideoParams params, - int count); -int oakcommon_videoparams_get_interlacing(OakVideoParams params, - int *interlacing); -int oakcommon_videoparams_set_interlacing(OakVideoParams params, - int interlacing); -int oakcommon_videoparams_get_divider(OakVideoParams params, - int *divider); -int oakcommon_videoparams_set_divider(OakVideoParams params, - int divider); -int oakcommon_videoparams_get_enabled(OakVideoParams params, - int *enabled); -int oakcommon_videoparams_set_enabled(OakVideoParams params, - int enabled); -int oakcommon_videoparams_get_x(OakVideoParams params, float *x); -int oakcommon_videoparams_set_x(OakVideoParams params, float x); -int oakcommon_videoparams_get_y(OakVideoParams params, float *y); -int oakcommon_videoparams_set_y(OakVideoParams params, float y); -int oakcommon_videoparams_get_stream_index(OakVideoParams params, - int *index); -int oakcommon_videoparams_set_stream_index(OakVideoParams params, - int index); -int oakcommon_videoparams_get_video_type(OakVideoParams params, - int *type); -int oakcommon_videoparams_set_video_type(OakVideoParams params, - int type); -int oakcommon_videoparams_get_start_time(OakVideoParams params, - int64_t *start_time); -int oakcommon_videoparams_set_start_time(OakVideoParams params, - int64_t start_time); -int oakcommon_videoparams_get_duration(OakVideoParams params, - int64_t *duration); -int oakcommon_videoparams_set_duration(OakVideoParams params, - int64_t duration); -int oakcommon_videoparams_get_premultiplied_alpha(OakVideoParams params, - int *premultiplied); -int oakcommon_videoparams_set_premultiplied_alpha(OakVideoParams params, - int premultiplied); -int oakcommon_videoparams_get_color_range(OakVideoParams params, - int *color_range); -int oakcommon_videoparams_set_color_range(OakVideoParams params, - int color_range); -int oakcommon_videoparams_get_color_primaries(OakVideoParams params, - int *primaries); -int oakcommon_videoparams_set_color_primaries(OakVideoParams params, - int primaries); -int oakcommon_videoparams_get_color_transfer(OakVideoParams params, - int *transfer); -int oakcommon_videoparams_set_color_transfer(OakVideoParams params, - int transfer); - -/** - * @brief Get the colorspace name (two-stage string getter). - * - * @return Required buffer size in bytes including the terminating NUL - * (non-negative), or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_videoparams_get_colorspace(OakVideoParams params, - char *buf, int buf_size); -int oakcommon_videoparams_set_colorspace(OakVideoParams params, - const char *colorspace); - -/** - * @brief Width multiplied by the pixel aspect ratio. - */ -int oakcommon_videoparams_get_square_pixel_width(OakVideoParams params, - int *width); -int oakcommon_videoparams_get_effective_width(OakVideoParams params, - int *width); -int oakcommon_videoparams_get_effective_height(OakVideoParams params, - int *height); -int oakcommon_videoparams_get_effective_depth(OakVideoParams params, - int *depth); -int oakcommon_videoparams_get_is_valid(OakVideoParams params, - int *is_valid); -int oakcommon_videoparams_get_bytes_per_channel(OakVideoParams params, - int *bytes); -int oakcommon_videoparams_get_bytes_per_pixel(OakVideoParams params, - int *bytes); -int oakcommon_videoparams_get_buffer_size(OakVideoParams params, - int *size); - -/** - * @brief Convert a time (in seconds, as a rational) to time base units. - * - * Returns INT64_MIN (AV_NOPTS_VALUE) in @p timestamp when no time base is - * set. - */ -int oakcommon_videoparams_get_time_in_timebase_units( - OakVideoParams params, int time_num, int time_den, - int64_t *timestamp); - -/** - * @brief Compare two parameter sets for equality. - */ -int oakcommon_videoparams_equals(OakVideoParams params, - OakVideoParams other, int *equal); - -/** - * @brief Load parameters from an XML fragment. - * - * @param xml NUL-terminated XML text. Must not be NULL. - * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_videoparams_load_xml(OakVideoParams params, - const char *xml); - -/** - * @brief Save parameters to an XML fragment (two-stage string getter). - * - * @return Required buffer size in bytes including the terminating NUL - * (non-negative), or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_videoparams_save_xml(OakVideoParams params, char *buf, - int buf_size); - -/* Static helpers (no handle required). */ - -int oakcommon_videoparams_get_bytes_per_channel_for_format(int pixel_format); -int oakcommon_videoparams_get_bytes_per_pixel_for_format(int pixel_format, - int channels); -int oakcommon_videoparams_calculate_buffer_size(int width, int height, - int pixel_format, - int channels); -int oakcommon_videoparams_format_is_float(int pixel_format); -int oakcommon_videoparams_generate_auto_divider(int64_t width, int64_t height); -int oakcommon_videoparams_get_scaled_dimension(int dimension, int divider); -int oakcommon_videoparams_get_divider_for_target_resolution(int src_width, - int src_height, - int dst_width, - int dst_height); - -/** - * @brief Human-readable name for a divider ("Full", "1/2", ...). - * - * @return Required buffer size in bytes including the terminating NUL - * (non-negative), or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_videoparams_get_name_for_divider(int divider, char *buf, - int buf_size); - -/** - * @brief Human-readable name for a pixel format. - * - * @return Required buffer size in bytes including the terminating NUL - * (non-negative), or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_videoparams_get_format_name(int pixel_format, char *buf, - int buf_size); - -/** - * @brief Human-readable frame rate string ("23.976 FPS"). - * - * @return Required buffer size in bytes including the terminating NUL - * (non-negative), or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_videoparams_frame_rate_to_string(int numerator, int denominator, - char *buf, int buf_size); - -/** - * @brief Get bytes per channel. - * - * @return Bytes per channel. - */ -int oakcommon_videoparams_static_get_bytes_per_channel(OakPixelFormat format); - -/** - * @brief Get bytes per pixel. - * - * @return Bytes per pixel. - */ -int oakcommon_videoparams_static_get_bytes_per_pixel(OakPixelFormat format, - int channels); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_VIDEOPARAMS_H diff --git a/crates/oakengine.bk/include/common/xmlutils.h b/crates/oakengine.bk/include/common/xmlutils.h deleted file mode 100644 index fba5f3885..000000000 --- a/crates/oakengine.bk/include/common/xmlutils.h +++ /dev/null @@ -1,215 +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 . - -***/ - -#ifndef OAK_EDITOR_XMLUTILS_H -#define OAK_EDITOR_XMLUTILS_H - -#include "common/error.h" -#include "common/handle.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Neutral by-value handle to a streaming XML reader. - * - * Ownership/count semantics follow the convention in common/handle.h: - * init returns a handle whose object has reference count 1, - * addref(ctx)/release(ctx) adjust it atomically, and release destroys - * the object at zero. abi_version is always OAKCOMMON_ABI_VERSION. - */ -typedef struct OakXmlReader { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKCOMMON_ABI_VERSION. */ -} OakXmlReader; - -/** - * @brief Neutral by-value handle to a streaming XML writer. - * - * Same ownership/count semantics as OakXmlReader. - */ -typedef struct OakXmlWriter { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKCOMMON_ABI_VERSION. */ -} OakXmlWriter; - -/** - * @brief Create a streaming XML reader over a complete document. - * - * @param data NUL-terminated XML text. Must not be NULL. - * @return Handle with reference count 1; ctx is NULL on failure - * (NULL data, out of memory). - */ -OakXmlReader oakcommon_xml_reader_init(const char *data); - -#ifdef __cplusplus -} /* extern "C" */ - -namespace olive { class XmlStreamReader; class XmlStreamWriter; } - -extern "C" { -#endif - -/** - * @brief Borrowed access to the underlying C++ reader/writer (C++ only, - * for adapter layers). Valid while the handle is held. NULL-safe. - */ -olive::XmlStreamReader *oakcommon_xml_reader_get_native(OakXmlReader reader); -olive::XmlStreamWriter *oakcommon_xml_writer_get_native(OakXmlWriter writer); - -/** - * @brief Wrap an existing C++ reader/writer in a borrowed handle (C++ - * only, for adapter layers). The box never owns the object; the caller - * must keep it alive and release the box with - * oakcommon_xml_reader_free()/oakcommon_xml_writer_free(). Empty handle - * for a NULL object or on allocation failure. - */ -OakXmlReader oakcommon_xml_reader_wrap_native(olive::XmlStreamReader *reader); -OakXmlWriter oakcommon_xml_writer_wrap_native(olive::XmlStreamWriter *writer); - -/** - * @brief Release one reference to a reader. - * - * Convenience wrapper around handle.release(handle.ctx): decrements the - * atomic reference count and destroys the object when it reaches zero. - * No-op when reader is NULL or reader->ctx is NULL. - */ -void oakcommon_xml_reader_free(OakXmlReader *reader); - -/** - * @brief Advance until the next start element, an end element, or the end - * of the document. - * - * @param reader Reader handle. - * @param found Out: 1 if positioned on a start element, 0 otherwise. - * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_xml_reader_read_next_start_element(OakXmlReader reader, - int *found); - -/** - * @brief Name of the current element token. - * - * @return Required buffer size in bytes (including NUL), or a negative - * OAKCOMMON_E_* error code. - */ -int oakcommon_xml_reader_name(OakXmlReader reader, char *buf, - int buf_size); - -/** - * @brief Read the concatenated character data of the current element. - * - * Must be called on a start element; consumes up to the matching end - * element. - * - * @return Required buffer size in bytes (including NUL), or a negative - * OAKCOMMON_E_* error code. - */ -int oakcommon_xml_reader_read_element_text(OakXmlReader reader, - char *buf, int buf_size); - -/** - * @brief Skip the current element and all of its children. - * - * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_xml_reader_skip_current_element(OakXmlReader reader); - -/** - * @brief Number of attributes on the current start element. - * - * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_xml_reader_attribute_count(OakXmlReader reader, - int *count); - -/** - * @brief Name of the attribute at @p index on the current start element. - * - * @return Required buffer size in bytes (including NUL), OAKCOMMON_E_NOT_FOUND - * if @p index is out of range, or another negative OAKCOMMON_E_* code. - */ -int oakcommon_xml_reader_attribute_name(OakXmlReader reader, int index, - char *buf, int buf_size); - -/** - * @brief Value of the attribute at @p index on the current start element. - * - * @return Required buffer size in bytes (including NUL), OAKCOMMON_E_NOT_FOUND - * if @p index is out of range, or another negative OAKCOMMON_E_* code. - */ -int oakcommon_xml_reader_attribute_value(OakXmlReader reader, - int index, char *buf, int buf_size); - -/** - * @brief Whether the document failed to parse. - * - * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. - */ -int oakcommon_xml_reader_has_error(OakXmlReader reader, - int *has_error); - -/** - * @brief Create a streaming XML writer. - * - * @return Handle with reference count 1; ctx is NULL on failure. - */ -OakXmlWriter oakcommon_xml_writer_init(void); - -/** - * @brief Release one reference to a writer. - * - * Convenience wrapper around handle.release(handle.ctx): decrements the - * atomic reference count and destroys the object when it reaches zero. - * No-op when writer is NULL or writer->ctx is NULL. - */ -void oakcommon_xml_writer_free(OakXmlWriter *writer); - -int oakcommon_xml_writer_write_start_element(OakXmlWriter writer, - const char *name); -int oakcommon_xml_writer_write_attribute(OakXmlWriter writer, - const char *name, const char *value); -int oakcommon_xml_writer_write_characters(OakXmlWriter writer, - const char *text); -int oakcommon_xml_writer_write_text_element(OakXmlWriter writer, - const char *name, - const char *text); -int oakcommon_xml_writer_write_end_element(OakXmlWriter writer); -int oakcommon_xml_writer_write_end_document(OakXmlWriter writer); - -/** - * @brief The document written so far. - * - * @return Required buffer size in bytes (including NUL), or a negative - * OAKCOMMON_E_* error code. - */ -int oakcommon_xml_writer_output(OakXmlWriter writer, char *buf, - int buf_size); - -#ifdef __cplusplus -} -#endif - -#endif // OAK_EDITOR_XMLUTILS_H diff --git a/crates/oakengine.bk/include/node/block.h b/crates/oakengine.bk/include/node/block.h deleted file mode 100644 index d5f084260..000000000 --- a/crates/oakengine.bk/include/node/block.h +++ /dev/null @@ -1,313 +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 . - -***/ - -#ifndef OAK_EDITOR_NODE_BLOCK_H -#define OAK_EDITOR_NODE_BLOCK_H - -#ifndef __cplusplus -#include -#endif - -#include - -#include "node/error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Reference-counted handle to a timeline block (olive::Block). - * - * Covers the whole Block family: ClipBlock, GapBlock and the concrete - * TransitionBlock subclasses. The object never leaves the library that - * created it; every external reference is one of these handles. - * Semantics are shared_ptr-like: the oaknode_block_*_create() factories - * below return a handle with count 1, addref(ctx) takes another - * reference, release(ctx) drops one and the library destroys the object - * when the count reaches zero. Callers never touch C++ subclasses - * directly. - * - * Placing a block on a track (the oaknode_track_*_block() primitives) - * transfers ownership to the track; handles obtained from accessors - * (neighbours, lookups) are borrowed and never destroy the underlying - * object. - */ -typedef struct OakNodeBlock { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKNODE_ABI_VERSION. */ -} OakNodeBlock; - -/** - * @brief Reference-counted handle to a track (olive::Track), see - * node/track.h. - * - * Re-declared here so block.h is self-contained; the typedef is identical. - */ -typedef struct OakNodeTrack OakNodeTrack; - -/** - * @brief Reference-counted handle to a node (olive::Node), see - * node/node.h. - * - * Re-declared here so block.h is self-contained; the typedef is identical. - */ -typedef struct OakNodeNode OakNodeNode; - -/** - * @brief Concrete transition kinds for oaknode_block_transition_create(). - */ -enum OakNodeTransitionKind { - OAKNODE_TRANSITION_CROSS_DISSOLVE = 0, /**< CrossDissolveTransition. */ - OAKNODE_TRANSITION_DIP_TO_COLOR = 1 /**< DipToColorTransition. */ -}; - -/** - * @brief Input ids of a TransitionBlock's block connections - * (TransitionBlock::k_out_block_input / k_in_block_input). Pinned by - * test; pass to oaknode_node_connect()/oaknode_node_disconnect(). - */ -#define OAKNODE_TRANSITION_OUT_BLOCK_INPUT "out_block_in" -#define OAKNODE_TRANSITION_IN_BLOCK_INPUT "in_block_in" - -/** - * @brief Create a ClipBlock. - * - * The caller owns the block until it is placed on a track that belongs to - * a project; a block that was never placed must be released with - * oaknode_block_free(). - * - * @return Block handle with reference count 1; ctx is NULL on allocation - * failure. - */ -OakNodeBlock oaknode_block_clip_create(void); - -/** - * @brief Create a GapBlock. Ownership as oaknode_block_clip_create(). - * - * @return Block handle with reference count 1; ctx is NULL on allocation - * failure. - */ -OakNodeBlock oaknode_block_gap_create(void); - -/** - * @brief Create a concrete TransitionBlock. - * - * @param kind One of the OakNodeTransitionKind values. - * @return Block handle with reference count 1; ctx is NULL on invalid - * kind / allocation failure. - */ -OakNodeBlock oaknode_block_transition_create(int kind); - -/** - * @brief Release one reference to a block handle. - * - * Destroys the block when the reference count reaches zero. NULL handle - * or NULL ctx is a no-op; clears `block->ctx` after releasing. - * - * The block must not be placed on a track or linked to other nodes; the - * caller is responsible for detaching it first. - */ -void oaknode_block_free(OakNodeBlock *block); - -enum OakNodeBlockKind { - OAKNODE_BLOCK_OTHER = 0, - OAKNODE_BLOCK_CLIP = 1, - OAKNODE_BLOCK_GAP = 2, - OAKNODE_BLOCK_TRANSITION = 3 -}; - -/** - * @brief Concrete kind of a block (dynamic_cast query). - */ -int oaknode_block_get_kind(OakNodeBlock block, int *out_kind); - -/** - * @brief Borrowed cast from a block handle to its node handle. - * - * Every Block is a Node; releasing the result never destroys the block. - * Empty handle for an empty handle. - */ -OakNodeNode oaknode_block_as_node(OakNodeBlock block); - -/** - * @brief Borrowed cast from a node handle to a block handle. - * - * Returns an empty handle if the node is not a Block (or is empty). - */ -OakNodeBlock oaknode_block_from_node(OakNodeNode node); - -/** - * @brief Rational getters/setters use numerator/denominator out pairs. - * - * @return OAKNODE_OK or OAKNODE_E_INVALID. - */ -int oaknode_block_get_in(OakNodeBlock block, int *numerator, int *denominator); -int oaknode_block_set_in(OakNodeBlock block, int numerator, int denominator); -int oaknode_block_get_out(OakNodeBlock block, int *numerator, int *denominator); -int oaknode_block_set_out(OakNodeBlock block, int numerator, int denominator); -int oaknode_block_get_length(OakNodeBlock block, int *numerator, - int *denominator); - -/** - * @brief Set the block length, keeping the media out/in point anchored - * (olive::Block::set_length_and_media_out / _media_in). - * - * @return OAKNODE_OK or OAKNODE_E_INVALID. - */ -int oaknode_block_set_length_and_media_out(OakNodeBlock block, int numerator, - int denominator); -int oaknode_block_set_length_and_media_in(OakNodeBlock block, int numerator, - int denominator); - -/** - * @brief Enabled flag (olive::Block::is_enabled/set_enabled). - * - * @return OAKNODE_OK or OAKNODE_E_INVALID. - */ -int oaknode_block_get_enabled(OakNodeBlock block, int *enabled); -int oaknode_block_set_enabled(OakNodeBlock block, int enabled); - -/** - * @brief Adjacency accessors. `out` receives a borrowed handle (empty when - * there is no neighbour / the block is not on a track). - * - * @return OAKNODE_OK or OAKNODE_E_INVALID. - */ -int oaknode_block_get_previous(OakNodeBlock block, OakNodeBlock *out); -int oaknode_block_get_next(OakNodeBlock block, OakNodeBlock *out); -int oaknode_block_get_track(OakNodeBlock block, OakNodeTrack *out); - -/** - * @brief Link two blocks (olive::Node::link/unlink/are_linked). - * - * Linked blocks move together in timeline edits. - * - * @return OAKNODE_OK, OAKNODE_E_INVALID or OAKNODE_E_FAILED (already - * linked / not linked). - */ -int oaknode_block_link(OakNodeBlock a, OakNodeBlock b); -int oaknode_block_unlink(OakNodeBlock a, OakNodeBlock b); -int oaknode_block_are_linked(OakNodeBlock a, OakNodeBlock b, int *linked); - -/** - * @brief Number of blocks linked to `block` (olive::Node::links()). - * - * @return OAKNODE_OK or OAKNODE_E_INVALID. - */ -int oaknode_block_get_link_count(OakNodeBlock block, int *count); - -/** - * @brief Borrowed handle to the linked block at `index`. - * - * @return OAKNODE_OK, OAKNODE_E_INVALID or OAKNODE_E_NOT_FOUND. - */ -int oaknode_block_get_link_at(OakNodeBlock block, int index, - OakNodeBlock *out); - -/* ---------------------------------------------------------------- Clip */ - -/** - * @brief Media in/out accessors (olive::ClipBlock). Non-clip blocks return - * OAKNODE_E_INVALID. - */ -int oaknode_clip_get_media_in(OakNodeBlock clip, int *numerator, - int *denominator); -int oaknode_clip_set_media_in(OakNodeBlock clip, int numerator, - int denominator); - -/** - * @brief Playback speed factor, 1.0 = normal (olive::ClipBlock speed input). - */ -int oaknode_clip_get_speed(OakNodeBlock clip, double *speed); -int oaknode_clip_set_speed(OakNodeBlock clip, double speed); - -/** - * @brief Reverse playback flag. - */ -int oaknode_clip_get_reverse(OakNodeBlock clip, int *reverse); -int oaknode_clip_set_reverse(OakNodeBlock clip, int reverse); - -/** - * @brief Maintain-audio-pitch flag. - */ -int oaknode_clip_get_maintain_audio_pitch(OakNodeBlock clip, int *maintain); -int oaknode_clip_set_maintain_audio_pitch(OakNodeBlock clip, int maintain); - -/** - * @brief Loop mode, one of the OakLoopMode values - * (olive::ClipBlock::loop_mode/set_loop_mode). - */ -int oaknode_clip_get_loop_mode(OakNodeBlock clip, int *loop_mode); -int oaknode_clip_set_loop_mode(OakNodeBlock clip, int loop_mode); - -/** - * @brief Type of the track the clip sits on (OakNodeTrackType values, - * OAKNODE_TRACK_TYPE_NONE when trackless). - */ -int oaknode_clip_get_track_type(OakNodeBlock clip, int *type); - -/* ----------------------------------------------------------- Transition */ - -/** - * @brief Transition offsets (olive::TransitionBlock). Non-transition blocks - * return OAKNODE_E_INVALID. - */ -int oaknode_transition_get_in_offset(OakNodeBlock transition, int *numerator, - int *denominator); -int oaknode_transition_get_out_offset(OakNodeBlock transition, int *numerator, - int *denominator); -int oaknode_transition_get_offset_center(OakNodeBlock transition, - int *numerator, int *denominator); -int oaknode_transition_set_offset_center(OakNodeBlock transition, - int numerator, int denominator); -int oaknode_transition_set_offsets_and_length(OakNodeBlock transition, - int in_num, int in_den, - int out_num, int out_den); - -/** - * @brief Whether both sides of the transition are connected to clips. - */ -int oaknode_transition_is_dual(OakNodeBlock transition, int *dual); - -/** - * @brief Borrowed handles to the connected out/in side blocks (empty when - * unconnected). - */ -int oaknode_transition_get_connected_out_block(OakNodeBlock transition, - OakNodeBlock *out); -int oaknode_transition_get_connected_in_block(OakNodeBlock transition, - OakNodeBlock *out); - -/** - * @brief Forward cache passthroughs from another clip - * (ClipBlock::add_cache_passthrough_from()). Used after splitting a - * clip so the new part shares the render caches. - */ -int oaknode_clip_add_cache_passthrough_from(OakNodeBlock clip, - OakNodeBlock other); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_NODE_BLOCK_H diff --git a/crates/oakengine.bk/include/node/colormanager.h b/crates/oakengine.bk/include/node/colormanager.h deleted file mode 100644 index f43b3910d..000000000 --- a/crates/oakengine.bk/include/node/colormanager.h +++ /dev/null @@ -1,221 +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 . - -***/ - -#ifndef OAK_EDITOR_NODE_COLORMANAGER_H -#define OAK_EDITOR_NODE_COLORMANAGER_H - -#ifndef __cplusplus -#include -#endif - -#include - -#include "common/colortransform.h" -#include "node/error.h" -#include "node/project.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Reference-counted handle to a color manager - * (olive::ColorManager). - * - * Semantics are shared_ptr-like: oaknode_colormanager_init() returns a - * handle whose object has reference count 1, addref(ctx) takes another - * reference, and release(ctx) (or oaknode_colormanager_free()) drops - * one; the library destroys the object when the count reaches zero. - */ -typedef struct OakNodeColorManager { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKNODE_ABI_VERSION. */ -} OakNodeColorManager; - -/** - * @brief Create a color manager bound to `project` (borrowed). - * - * The manager is created without a config; call - * oaknode_colormanager_initialize() (or set a config filename and - * oaknode_colormanager_update_config_from_filename()) before using the - * config-dependent queries. - * - * @return Manager handle with reference count 1 (release with - * oaknode_colormanager_free()); ctx is NULL on an empty project - * handle or allocation failure. - */ -OakNodeColorManager oaknode_colormanager_init(OakNodeProject project); - -/** - * @brief Release the caller's reference to the color manager and null - * out the handle. No-op on NULL or an empty handle; the object is - * destroyed when its reference count reaches zero. - */ -void oaknode_colormanager_free(OakNodeColorManager *manager); - -/** - * @brief Borrowed handle wrapping a native manager pointer held by a - * node (olive::OCIOBaseNode::manager()). - * - * The manager stays owned by its project: release() on this handle - * only frees the box. Empty handle (ctx == NULL) for a NULL native - * pointer. - */ -OakNodeColorManager oaknode_colormanager_wrap_borrowed(void *native_manager); - -/** - * @brief Load the built-in default OCIO config and set the default input - * colorspace (olive::ColorManager::init()). - * - * @return OAKNODE_OK, OAKNODE_E_INVALID or OAKNODE_E_FAILED (the OCIO - * config could not be created). - */ -int oaknode_colormanager_initialize(OakNodeColorManager manager); - -/** - * @brief (Re)build the process-wide default OCIO config - * (olive::ColorManager::set_up_default_config()). - * - * @return OAKNODE_OK or OAKNODE_E_FAILED. - */ -int oaknode_colormanager_set_up_default_config(void); - -/** - * @brief Config filename stored on the project. Two-stage string getter: - * returns the required buffer size in bytes including NUL; pass - * buf == NULL or a too-small buffer to query the size. - */ -int oaknode_colormanager_get_config_filename(OakNodeColorManager manager, - char *buf, int buf_size); -int oaknode_colormanager_set_config_filename(OakNodeColorManager manager, - const char *filename); - -/** - * @brief Reload the OCIO config from the stored filename. Missing/invalid - * files are tolerated (the previous config is kept), matching - * olive::ColorManager::update_config_from_filename(). - */ -int oaknode_colormanager_update_config_from_filename( - OakNodeColorManager manager); - -/** - * @brief Default input colorspace. Two-stage string accessor. - */ -int oaknode_colormanager_get_default_input_color_space( - OakNodeColorManager manager, char *buf, int buf_size); -int oaknode_colormanager_set_default_input_color_space( - OakNodeColorManager manager, const char *colorspace); - -/** - * @brief Reference (working) colorspace. Two-stage string getter. - */ -int oaknode_colormanager_get_reference_color_space( - OakNodeColorManager manager, char *buf, int buf_size); - -/** - * @brief Return `colorspace` when the active config lists it, otherwise the - * default input colorspace. Two-stage string getter. Requires a config - * (OAKNODE_E_STATE when none is loaded). - */ -int oaknode_colormanager_get_compliant_color_space( - OakNodeColorManager manager, const char *colorspace, char *buf, - int buf_size); - -/** - * @brief Map FFmpeg color primaries/transfer codes to a colorspace of the - * active config. Two-stage string getter; an empty result (required size - * 1) means "unknown tags, use the default". Requires a config - * (OAKNODE_E_STATE when none is loaded). - */ -int oaknode_colormanager_get_colorspace_for_ffmpeg_tags( - OakNodeColorManager manager, int primaries, int trc, char *buf, - int buf_size); - -/** - * @brief Config listings. Count + per-index two-stage string getters. - * All require a loaded config (OAKNODE_E_STATE otherwise); index out of - * range yields OAKNODE_E_NOT_FOUND. - */ -int oaknode_colormanager_get_display_count(OakNodeColorManager manager, - int *count); -int oaknode_colormanager_get_display_at(OakNodeColorManager manager, - int index, char *buf, int buf_size); -int oaknode_colormanager_get_default_display(OakNodeColorManager manager, - char *buf, int buf_size); -int oaknode_colormanager_get_view_count(OakNodeColorManager manager, - const char *display, int *count); -int oaknode_colormanager_get_view_at(OakNodeColorManager manager, - const char *display, int index, char *buf, - int buf_size); -int oaknode_colormanager_get_default_view(OakNodeColorManager manager, - const char *display, char *buf, - int buf_size); -int oaknode_colormanager_get_look_count(OakNodeColorManager manager, - int *count); -int oaknode_colormanager_get_look_at(OakNodeColorManager manager, int index, - char *buf, int buf_size); -int oaknode_colormanager_get_colorspace_count(OakNodeColorManager manager, - int *count); -int oaknode_colormanager_get_colorspace_at(OakNodeColorManager manager, - int index, char *buf, - int buf_size); - -/** - * @brief Default luma coefficients of the active config into rgb[3]. - * Requires a loaded config (OAKNODE_E_STATE otherwise). - */ -int oaknode_colormanager_get_default_luma_coefs(OakNodeColorManager manager, - double rgb[3]); - -/** - * @brief Return a copy of `transform` whose display/view/look (or output - * colorspace) is clamped to what the active config offers - * (olive::ColorManager::get_compliant_color_space(ColorTransform, bool)). - * - * `out` receives a NEW by-value handle owned by the caller (reference - * count 1, release with oakcommon_colortransform_free()). Requires a - * loaded config (OAKNODE_E_STATE otherwise). - */ -int oaknode_colormanager_get_compliant_color_transform( - OakNodeColorManager manager, OakColorTransform transform, - int force_display, OakColorTransform *out); - -#ifdef __cplusplus -} /* extern "C" */ - -namespace olive { class ColorManager; } - -extern "C" { -#endif - -/** - * @brief Borrowed access to the underlying C++ manager (C++ only, for - * adapter layers). Valid while the handle is held. NULL-safe. - */ -olive::ColorManager *oaknode_colormanager_get_native( - OakNodeColorManager manager); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_NODE_COLORMANAGER_H diff --git a/crates/oakengine.bk/include/node/dragger.h b/crates/oakengine.bk/include/node/dragger.h deleted file mode 100644 index e3f8180c6..000000000 --- a/crates/oakengine.bk/include/node/dragger.h +++ /dev/null @@ -1,137 +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 . - -***/ - -#ifndef OAK_EDITOR_NODE_DRAGGER_H -#define OAK_EDITOR_NODE_DRAGGER_H - -#include - -#include "node/error.h" -#include "node/node.h" -#include "undo/undocommand.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file dragger.h - * @brief C ABI for olive::NodeInputDragger (src/node/src/inputdragger.h): - * live drag of an input's value with a single commit command. - * - * A dragger wraps the engine's NodeInputDragger state machine - * (start -> drag* -> end). start() records the drag anchor and, when the - * input is keyframing, creates one keyframe at the drag time (on every - * track when requested); drag() live-sets the dragged component (clamped - * by the input's min/max properties when present); end() returns ONE - * undoable command that commits the whole drag -- undo removes the - * created keyframe(s) (restoring the pre-drag keyframe count), redo - * re-creates them with the final value. - * - * A dragger must be ended before it is freed; freeing a started dragger - * leaks the created keyframe(s) (the same ownership rule as the C++ - * class). - */ - -/** - * @brief Reference-counted handle to an input dragger - * (olive::NodeInputDragger). - * - * The object never leaves the library that created it; every external - * reference is one of these handles. Semantics are shared_ptr-like: - * oaknode_dragger_create() returns a handle with count 1, addref(ctx) - * takes another reference, release(ctx) drops one and the library - * destroys the object when the count reaches zero. - */ -typedef struct OakNodeDragger { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKNODE_ABI_VERSION. */ -} OakNodeDragger; - -/** - * @brief Create an input dragger for live-drag of an input's value. - * - * `input_id` must name an existing input of `node`; `element` addresses - * an array input's element (-1 for non-array inputs). `track` is the - * create-time default; the track passed to oaknode_dragger_start() - * establishes the actual drag track. - * - * @return Dragger handle with count 1; ctx is NULL on invalid arguments - * or allocation failure. - */ -OakNodeDragger oaknode_dragger_create(OakNodeNode node, const char *input_id, - int element, int track); - -/** - * @brief Start the drag at the given rational time (creates a keyframe - * when the input is keyframing). - * - * `insert_on_all_tracks` != 0 also creates sibling keyframes on every - * other track of the input. OAKNODE_E_STATE when the dragger was already - * started. - */ -int oaknode_dragger_start(OakNodeDragger dragger, int64_t time_num, - int64_t time_den, int track, - int insert_on_all_tracks); - -/** - * @brief Drag to a new per-track component value (live; no undo). - * - * `value` carries the dragged component of the input's declared type: - * scalar types in f[0]/num; for split-track types (VEC2/3/4/COLOR) the - * POD type must match the input's declared type and the dragged - * component sits in f[0] (the facade's dragger convention). The value is - * clamped to the input's min/max properties when present. - * OAKNODE_E_STATE when the dragger was not started. - */ -int oaknode_dragger_drag(OakNodeDragger dragger, const oaknode_value *value); - -/** - * @brief End the drag, returning ONE undoable command for the whole drag. - * - * `*out_command` receives an owned command handle (execute it with - * oakundo_command_redo_now(), push it onto an OakUndoStack, or release - * it with oakundo_command_free()). OAKNODE_E_STATE when the dragger was - * not started. - */ -int oaknode_dragger_end(OakNodeDragger dragger, OakUndoCommand *out_command); - -/** - * @brief 1 if the dragger has been started and not yet ended. - */ -int oaknode_dragger_is_started(OakNodeDragger dragger, int *out_started); - -/** - * @brief Release one reference to a dragger handle. - * - * Convenience wrapper around handle.release(handle.ctx): destroys the - * dragger when the count reaches zero. NULL handle or NULL ctx is a - * no-op; clears `dragger->ctx` after releasing. The dragger must have - * been ended (see the file comment). - */ -void oaknode_dragger_free(OakNodeDragger *dragger); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_NODE_DRAGGER_H diff --git a/crates/oakengine.bk/include/node/error.h b/crates/oakengine.bk/include/node/error.h deleted file mode 100644 index 1eeffa339..000000000 --- a/crates/oakengine.bk/include/node/error.h +++ /dev/null @@ -1,49 +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 . - -***/ - -#ifndef OAK_EDITOR_NODE_ERROR_H -#define OAK_EDITOR_NODE_ERROR_H - -/** - * @brief Status and error codes shared by all oaknode C API families. - * - * Return-code convention (mirrors engine/include/oakengine/init.h): - * 0 (OAKNODE_OK) on success, a negative OAKNODE_E_* error code on - * failure. String getters return the required buffer size in bytes - * (including the terminating NUL) as a non-negative value instead. - */ -/** - * @brief Current ABI version stamped into every oaknode handle. - * - * Bump whenever a handle layout or the semantics of any exported - * function change incompatibly. Consumers should compare a handle's - * abi_version field against the value they were compiled with before - * dereferencing ctx. - */ -#define OAKNODE_ABI_VERSION 1 - -#define OAKNODE_OK 0 /**< Success. */ -#define OAKNODE_E_INVALID (-30001) /**< NULL handle or invalid argument. */ -#define OAKNODE_E_STATE (-30002) /**< Call not valid in the current state. */ -#define OAKNODE_E_FAILED (-30003) /**< The underlying operation failed. */ -#define OAKNODE_E_NOT_FOUND (-30004) /**< Index out of range / entry not found. */ -#define OAKNODE_E_NOMEM (-30005) /**< Allocation failed. */ - -#endif //OAK_EDITOR_NODE_ERROR_H diff --git a/crates/oakengine.bk/include/node/factory.h b/crates/oakengine.bk/include/node/factory.h deleted file mode 100644 index 5b6c6c35b..000000000 --- a/crates/oakengine.bk/include/node/factory.h +++ /dev/null @@ -1,100 +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 . - -***/ - -#ifndef OAK_EDITOR_NODE_FACTORY_H -#define OAK_EDITOR_NODE_FACTORY_H - -#include "node/error.h" -#include "node/node.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file factory.h - * @brief C ABI for olive::NodeFactory (src/node/src/factory.h): the - * internal node-type library. - * - * The library must be populated with oaknode_factory_initialize() before - * any other call; oaknode_factory_destroy() releases it. The factory is - * a process-wide singleton (static olive::NodeFactory), so there is no - * OakNodeFactory handle type. Prototype nodes from - * oaknode_factory_node_at() are owned by the library: read-only metadata - * queries only, never add them to a graph. - */ - -/** - * @brief Populate the internal node library (NodeFactory::initialize()). - * Idempotent: calling twice is a no-op. - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_factory_initialize(void); - -/** - * @brief Release the internal node library (NodeFactory::destroy()). - * Safe when not initialized. - */ -void oaknode_factory_destroy(void); - -/** - * @brief Number of registered node types (the library size). - * OAKNODE_E_STATE when not initialized. - */ -int oaknode_factory_id_count(int *out_count); - -/** - * @brief The type id of the registered node at `index`. Two-stage - * getter; OAKNODE_E_NOT_FOUND for an out-of-range index, - * OAKNODE_E_STATE when not initialized. - */ -int oaknode_factory_id_at(int index, char *buf, int buf_size); - -/** - * @brief The display name of the node type `type_id` - * (NodeFactory::get_name_from_id()). Two-stage getter; an unknown id - * yields an empty string (required size 1). - */ -int oaknode_factory_name_from_id(const char *type_id, char *buf, - int buf_size); - -/** - * @brief Create a node of `type_id` WITHOUT adding it to any graph - * (NodeFactory::create_from_id()). The caller owns the returned node - * (reference count 1) and must release it with oaknode_node_free() while - * it is still orphaned. ctx is NULL when the id is unknown or not - * initialized. - */ -OakNodeNode oaknode_factory_create_from_id(const char *type_id); - -/** - * @brief Borrow the prototype node at `index` in the library (non-owning - * handle written to `out_node`; release it with oaknode_node_free()). - * OAKNODE_E_NOT_FOUND for an out-of-range index, OAKNODE_E_STATE when - * not initialized. - */ -int oaknode_factory_node_at(int index, OakNodeNode *out_node); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_NODE_FACTORY_H diff --git a/crates/oakengine.bk/include/node/folder.h b/crates/oakengine.bk/include/node/folder.h deleted file mode 100644 index a2387c30a..000000000 --- a/crates/oakengine.bk/include/node/folder.h +++ /dev/null @@ -1,170 +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 . - -***/ - -#ifndef OAK_EDITOR_NODE_FOLDER_H -#define OAK_EDITOR_NODE_FOLDER_H - -#include - -#include "node/error.h" -#include "undo/undocommand.h" -#include "node/project.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file folder.h - * @brief C ABI for olive::Folder (oaknode) - * - * A folder is a project node that organizes item children (footage, - * sequences, subfolders). Folder handles are borrowed from the owning - * project; they become invalid when the project is freed or cleared. - * - * Child add/remove/move operations execute the underlying undo commands - * live (redo_now); wiring them onto an undo stack is the oakundo / - * facade layer's job, not this layer's. - */ - -/** - * @brief Reference-counted handle to a folder node (olive::Folder). - * - * Semantics are shared_ptr-like (see OakNodeProject): addref(ctx) takes a - * reference, release(ctx) drops one. Folder handles handed out by this API - * are borrowed views into the owning project's graph: releasing them only - * releases the handle itself, never the folder. - */ -typedef struct OakNodeFolder { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKNODE_ABI_VERSION. */ -} OakNodeFolder; - -/** - * @brief Create a folder node owned by `project`. - * - * The folder is added to the project's graph (Project::add_node()) but is - * NOT attached under any parent folder; use oaknode_folder_add_child() to - * place it. The returned handle is borrowed: the project owns the folder, - * so releasing the handle only releases the handle itself. - * - * @return Folder handle; ctx is NULL on failure. - */ -OakNodeFolder oaknode_folder_create(OakNodeProject project); - -/** - * @brief Number of direct item children (Folder::item_child_count()). - * Negative OAKNODE_E_* code on an empty handle. - */ -int oaknode_folder_child_count(OakNodeFolder folder); - -/** - * @brief Borrowed node handle of the item child at `index` - * (Folder::item_child()). - * - * The returned handle only releases the handle itself. Empty handle - * (ctx == NULL) when out of range. - */ -OakNodeNode oaknode_folder_child_at(OakNodeFolder folder, int index); - -/** - * @brief Add `child` as a direct item child of `folder` (live, non-undoable; - * executes FolderAddChild::redo()). - * - * After a successful call the graph owns `child`: releasing the child - * handle only releases the handle itself. - * - * @return OAKNODE_OK, OAKNODE_E_STATE if `child` already belongs to a - * folder, or another negative OAKNODE_E_* error code. - */ -int oaknode_folder_add_child(OakNodeFolder folder, OakNodeNode child); - -/** - * @brief Borrowed cast from a folder handle to its node handle. - * - * The returned handle only releases the handle itself. Empty handle for an - * empty handle. - */ -OakNodeNode oaknode_folder_as_node(OakNodeFolder folder); - -/** - * @brief Create an undoable FolderAddChild command. - * - * @return Command handle with reference count 1 (release with - * oakundo_command_free()); ctx is NULL on failure. - */ -OakUndoCommand oaknode_command_create_folder_add_child( - OakNodeFolder folder, OakNodeNode child); - -/** - * @brief Remove `child` from `folder` without deleting it (live, - * non-undoable; executes Folder::RemoveElementCommand::redo()). - * - * @return OAKNODE_OK, OAKNODE_E_NOT_FOUND if `child` is not a direct child, - * or another negative OAKNODE_E_* error code. - */ -int oaknode_folder_remove_child(OakNodeFolder folder, OakNodeNode child); - -/** - * @brief Move several nodes into `dest_folder` (live, non-undoable). - * - * Each node is removed from its current folder (if any) and appended to - * `dest_folder`; the graph assumes the lifetime of every moved node. Nodes - * already directly inside `dest_folder` are skipped. - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_folder_move_children(const OakNodeNode *nodes, int count, - OakNodeFolder dest_folder); - -/** - * @brief 1 if `folder` recursively contains `child`, 0 otherwise - * (Folder::has_child_recursive()). Negative OAKNODE_E_* code on empty - * handles. - */ -int oaknode_folder_has_child_recursive(OakNodeFolder folder, - OakNodeNode child); - -/** - * @brief Index of `child` in `folder`'s direct children - * (Folder::index_of_child()). - * - * @return The index, OAKNODE_E_NOT_FOUND if not a direct child, or - * OAKNODE_E_INVALID on empty handles. - */ -int oaknode_folder_index_of_child(OakNodeFolder folder, - OakNodeNode child); - -/** - * @brief Borrowed handle of the folder a node currently belongs to - * (Node::folder()). - * - * The returned handle only releases the handle itself. Empty handle - * (ctx == NULL) if the node is not in any folder. - */ -OakNodeFolder oaknode_folder_parent_of(OakNodeNode node); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_NODE_FOLDER_H diff --git a/crates/oakengine.bk/include/node/footage.h b/crates/oakengine.bk/include/node/footage.h deleted file mode 100644 index 939728110..000000000 --- a/crates/oakengine.bk/include/node/footage.h +++ /dev/null @@ -1,256 +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 . - -***/ - -#ifndef OAK_EDITOR_NODE_FOOTAGE_H -#define OAK_EDITOR_NODE_FOOTAGE_H - -#include - -#include "common/videoparams.h" -#include "node/error.h" -// NOTE: quoted-relative to bypass the "render/cancelatom.h" transition -// bridge (oakrender's C++ olive::CancelAtom) that shadows the C ABI -// header on oaknode's include path. -#include "../../include/render/cancelatom.h" -#include "node/project.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file footage.h - * @brief C ABI for olive::Footage (oaknode) - * - * A footage node references an external media file and caches its stream - * metadata. Footage handles are borrowed from the owning project; they - * become invalid when the project is freed or cleared. - * - * NOTE: setting a filename whose file exists on disk triggers a probe, - * which requires the codec/render modules (outside oaknode). Tests and - * pure-graph consumers should use nonexistent paths; probing is the - * facade layer's job. - */ - -/** - * @brief Reference-counted handle to a footage node (olive::Footage). - * - * Semantics are shared_ptr-like (see OakNodeProject): addref(ctx) takes a - * reference, release(ctx) drops one. Footage handles handed out by this - * API are borrowed views into the owning project's graph: releasing them - * only releases the handle itself, never the footage. - */ -typedef struct OakNodeFootage { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKNODE_ABI_VERSION. */ -} OakNodeFootage; - -/** - * @brief Create a footage node owned by `project` (added to the project's - * graph, not attached to any folder). - * - * The returned handle is borrowed: the project owns the footage, so - * releasing the handle only releases the handle itself. - * - * @param filename Initial media path, may be NULL/empty. - * - * @return Footage handle; ctx is NULL on failure. - */ -OakNodeFootage oaknode_footage_create(OakNodeProject project, - const char *filename); - -/** - * @brief Borrowed cast from a footage handle to its node handle. - * - * The returned handle only releases the handle itself. Empty handle for an - * empty handle. - */ -OakNodeNode oaknode_footage_as_node(OakNodeFootage footage); - -/** - * @brief Current media path (Footage::filename()). Two-stage string getter. - * - * @return Required buffer size in bytes including the NUL, or a negative - * OAKNODE_E_* error code. - */ -int oaknode_footage_filename(OakNodeFootage footage, char *buf, - int buf_size); - -/** - * @brief Set the media path (Footage::set_filename()). Does not re-probe - * unless the file exists (see the file comment above). - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_footage_set_filename(OakNodeFootage footage, const char *filename); - -/** - * @brief 1 if the footage was successfully probed and is ready for use - * (Footage::is_valid()), 0 otherwise. Negative OAKNODE_E_* code on an - * empty handle. - */ -int oaknode_footage_is_valid(OakNodeFootage footage); - -/** - * @brief Last-modified timestamp of the media file in milliseconds since the - * epoch (Footage::timestamp()). - * - * @param out_timestamp Receives the timestamp. Must not be NULL. - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_footage_timestamp(OakNodeFootage footage, - int64_t *out_timestamp); - -/** - * @brief Set the last-modified timestamp (Footage::set_timestamp()). - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_footage_set_timestamp(OakNodeFootage footage, int64_t timestamp); - -/** - * @brief Decoder ID recorded when the footage was probed - * (Footage::decoder()). Two-stage string getter. - */ -int oaknode_footage_decoder(OakNodeFootage footage, char *buf, - int buf_size); - -/** - * @brief Total number of streams (Footage::get_total_stream_count()). - * Negative OAKNODE_E_* code on an empty handle. - */ -int oaknode_footage_total_stream_count(OakNodeFootage footage); - -/** - * @brief Number of video streams (ViewerOutput::get_video_stream_count()). - * Negative OAKNODE_E_* code on an empty handle. - */ -int oaknode_footage_video_stream_count(OakNodeFootage footage); - -/** - * @brief Number of audio streams (ViewerOutput::get_audio_stream_count()). - * Negative OAKNODE_E_* code on an empty handle. - */ -int oaknode_footage_audio_stream_count(OakNodeFootage footage); - -/** - * @brief Number of subtitle streams (ViewerOutput::get_subtitle_stream_count()). - * Negative OAKNODE_E_* code on an empty handle. - */ -int oaknode_footage_subtitle_stream_count(OakNodeFootage footage); - -/** - * @brief Footage duration as a rational number of seconds - * (ViewerOutput::get_length()). - * - * @param out_numerator Receives the numerator. Must not be NULL. - * @param out_denominator Receives the denominator. Must not be NULL. - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_footage_duration(OakNodeFootage footage, int *out_numerator, - int *out_denominator); - -/** - * @brief 1 if proxy playback is enabled (Footage::proxy_enabled()). - * Negative OAKNODE_E_* code on an empty handle. - */ -int oaknode_footage_proxy_enabled(OakNodeFootage footage); - -/** - * @brief Enable/disable proxy playback (Footage::set_proxy_enabled()). - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_footage_set_proxy_enabled(OakNodeFootage footage, int enabled); - -/** - * @brief Proxy file path, or "" when none (Footage::proxy_path()). - * Two-stage string getter. - */ -int oaknode_footage_proxy_path(OakNodeFootage footage, char *buf, - int buf_size); - -/** - * @brief Proxy state enum value (Footage::proxy_state(): - * ProxyManager::ProxyState). Negative OAKNODE_E_* code on an empty handle. - */ -int oaknode_footage_proxy_state(OakNodeFootage footage); - -/** - * @brief Set all proxy fields at once (Footage::set_proxy()). - * - * @param path Proxy file path, may be NULL/empty. - * @param state ProxyManager::ProxyState enum value. - * @param video_stream_index Proxy's video stream index (-1 when none). - * @param preset_version Proxy preset version. - * @param enabled Non-zero to enable proxy playback. - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_footage_set_proxy(OakNodeFootage footage, const char *path, - int state, int video_stream_index, - int preset_version, int enabled); - -/** - * @brief Clear all proxy fields (Footage::clear_proxy()). - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_footage_clear_proxy(OakNodeFootage footage); - -/** - * @brief Video stream parameters as an oakcommon video-params handle - * (ViewerOutput::get_video_params()). `out` receives a handle with - * reference count 1 (release with oakcommon_videoparams_free()). - * OAKNODE_E_NOT_FOUND for an out-of-range index. - */ -int oaknode_footage_get_video_params(OakNodeFootage footage, int index, - OakVideoParams *out); - -/** - * @brief Set a video stream's parameters from an oakcommon handle - * (ViewerOutput::set_video_params()). - */ -int oaknode_footage_set_video_params(OakNodeFootage footage, int index, - const OakVideoParams *params); - -/** - * @brief Video length as a rational pair (ViewerOutput::get_video_length()). - */ -int oaknode_footage_get_video_length(OakNodeFootage footage, - int64_t *out_num, int64_t *out_den); - -/** - * @brief Set the footage's cancellation atom used during probing - * (Footage::set_cancel_pointer()). `atom` may be an empty OakCancelAtom - * (ctx == NULL) to clear. - */ -int oaknode_footage_set_cancel_atom(OakNodeFootage footage, - OakCancelAtom atom); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_NODE_FOOTAGE_H diff --git a/crates/oakengine.bk/include/node/group.h b/crates/oakengine.bk/include/node/group.h deleted file mode 100644 index eb9bcc527..000000000 --- a/crates/oakengine.bk/include/node/group.h +++ /dev/null @@ -1,186 +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 . - -***/ - -#ifndef OAK_EDITOR_NODE_GROUP_H -#define OAK_EDITOR_NODE_GROUP_H - -#include - -#include "node/error.h" -#include "node/node.h" -#include "undo/undocommand.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file group.h - * @brief C ABI for olive::NodeGroup (src/node/src/group/group.h): - * input passthrough management and input resolution. - * - * An OakNodeGroup wraps an olive::NodeGroup (a Node subclass); group - * handles share the reference-counted lifetime rules of OakNodeNode. - */ - -/** - * @brief Reference-counted handle to a node group (olive::NodeGroup). - * - * The object never leaves the library that created it; every external - * reference is one of these handles. Semantics are shared_ptr-like: - * oaknode_group_create() returns a handle with count 1, addref(ctx) - * takes another reference, release(ctx) drops one and the library - * destroys the object when the count reaches zero. Handles returned by - * oaknode_group_cast() are borrowed views of a node: releasing them - * never destroys the underlying group. - */ -typedef struct OakNodeGroup { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKNODE_ABI_VERSION. */ -} OakNodeGroup; - -/** - * @brief Create a standalone NodeGroup (owned; release with - * oaknode_group_free() while still orphaned). - * - * @return Group handle with count 1; ctx is NULL on allocation failure. - */ -OakNodeGroup oaknode_group_create(void); - -/** - * @brief Borrow a group view of a node (dynamic_cast). The returned - * handle is non-owning; release it with oaknode_group_free(). - * - * @return Borrowed group handle; ctx is NULL when the node is not a - * NodeGroup. - */ -OakNodeGroup oaknode_group_cast(OakNodeNode node); - -/** - * @brief Release one reference to a group handle. - * - * Convenience wrapper around handle.release(handle.ctx): destroys the - * group when the count reaches zero and the handle owns it. NULL handle - * or NULL ctx is a no-op; clears `group->ctx` after releasing. - */ -void oaknode_group_free(OakNodeGroup *group); - -/** - * @brief Add an input passthrough for (`node`, `input_id`, `element`) - * (live, NodeGroup::add_input_passthrough()). The generated passthrough - * id is returned through the two-stage string convention. - * - * @return Required buffer size in bytes including the terminating NUL - * (non-negative), or a negative OAKNODE_E_* error code. - */ -int oaknode_group_add_input_passthrough(OakNodeGroup group, - OakNodeNode node, - const char *input_id, int element, - char *buf, int buf_size); - -/** - * @brief Create an add-passthrough command - * (olive::NodeGroupAddInputPassthrough). The generated id is NOT - * retrievable through this call (the command computes it on redo). - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_group_add_input_passthrough_undoable(OakNodeGroup group, - OakNodeNode node, - const char *input_id, - int element, - OakUndoCommand *out_command); - -/** - * @brief Remove the passthrough for (`node`, `input_id`, `element`) - * (live). OAKNODE_E_NOT_FOUND when no such passthrough exists. - */ -int oaknode_group_remove_input_passthrough(OakNodeGroup group, - OakNodeNode node, - const char *input_id, int element); - -/** - * @brief Number of registered input passthroughs. - */ -int oaknode_group_passthrough_count(OakNodeGroup group, int *out_count); - -/** - * @brief The passthrough id at `index`. Two-stage getter; - * OAKNODE_E_NOT_FOUND for an out-of-range index. - */ -int oaknode_group_passthrough_id_at(OakNodeGroup group, int index, - char *buf, int buf_size); - -/** - * @brief The inner input behind passthrough `index`: node (borrowed - * handle written to `out_node` when non-NULL; release it with - * oaknode_node_free()), input id (two-stage string) and element. - * OAKNODE_E_NOT_FOUND for an out-of-range index. - */ -int oaknode_group_passthrough_input_at(OakNodeGroup group, int index, - OakNodeNode *out_node, char *buf, - int buf_size, int *out_element); - -/** - * @brief The output passthrough node (borrowed handle written to - * `out_node`; release it with oaknode_node_free()), an empty handle when - * unset. OAKNODE_OK is returned either way. - */ -int oaknode_group_get_output_passthrough(OakNodeGroup group, - OakNodeNode *out_node); - -/** - * @brief Set the output passthrough node directly (live). `node` may be - * an empty handle to clear the passthrough. - */ -int oaknode_group_set_output_passthrough(OakNodeGroup group, - OakNodeNode node); - -/** - * @brief Create a set-output-passthrough command - * (olive::NodeGroupSetOutputPassthrough). - */ -int oaknode_group_set_output_passthrough_undoable( - OakNodeGroup group, OakNodeNode node, OakUndoCommand *out_command); - -/** - * @brief Resolve an input through group passthroughs - * (NodeGroup::resolve_input()): follows a group's passthrough id to the - * inner node input. Non-group inputs resolve to themselves. - * - * `out_node` (may be NULL) receives a borrowed handle (release it with - * oaknode_node_free()); the resolved input id uses the two-stage string - * convention; `out_element` (may be NULL) receives the element. - * OAKNODE_E_NOT_FOUND when the input does not resolve to a valid target. - * - * @return Required buffer size in bytes including the terminating NUL - * (non-negative), or a negative OAKNODE_E_* error code. - */ -int oaknode_group_resolve_input(OakNodeNode node, const char *input_id, - int element, OakNodeNode *out_node, - char *buf, int buf_size, int *out_element); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_NODE_GROUP_H diff --git a/crates/oakengine.bk/include/node/keyframe.h b/crates/oakengine.bk/include/node/keyframe.h deleted file mode 100644 index 6b28b9cb0..000000000 --- a/crates/oakengine.bk/include/node/keyframe.h +++ /dev/null @@ -1,295 +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 . - -***/ - -#ifndef OAK_EDITOR_NODE_KEYFRAME_H -#define OAK_EDITOR_NODE_KEYFRAME_H - -#include - -#include "node/error.h" -#include "node/node.h" -#include "undo/undocommand.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file keyframe.h - * @brief C ABI for olive::NodeKeyframe (src/node/src/keyframe.h). - * - * An OakNodeKeyframe wraps an olive::NodeKeyframe. Handles created by - * oaknode_keyframe_create() are owned and must be released with - * oaknode_keyframe_free(); keyframes attached to a node input's track - * are owned by the node. - * - * Every setter comes in a live variant and an undoable variant (suffix - * _undoable) returning an owned, un-executed OakUndoCommand. - */ - -/** - * @brief Interpolation type of a keyframe (olive::NodeKeyframe::Type). - */ -typedef enum oaknode_keyframe_type { - OAKNODE_KEYFRAME_INVALID = -1, - OAKNODE_KEYFRAME_LINEAR = 0, - OAKNODE_KEYFRAME_HOLD = 1, - OAKNODE_KEYFRAME_BEZIER = 2 -} oaknode_keyframe_type; - -/** - * @brief Bezier handle selector (olive::NodeKeyframe::BezierType). - */ -typedef enum oaknode_keyframe_bezier { - OAKNODE_KEYFRAME_IN_HANDLE = 0, - OAKNODE_KEYFRAME_OUT_HANDLE = 1 -} oaknode_keyframe_bezier; - -/** - * @brief Reference-counted handle to a keyframe (olive::NodeKeyframe). - * - * The object never leaves the library that created it; every external - * reference is one of these handles. Semantics are shared_ptr-like: - * oaknode_keyframe_create() returns a handle with count 1, addref(ctx) - * takes another reference, release(ctx) drops one and the library - * destroys the object when the count reaches zero. - */ -typedef struct OakNodeKeyframe { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKNODE_ABI_VERSION. */ -} OakNodeKeyframe; - -/** - * @brief Create a standalone keyframe (owned; release with - * oaknode_keyframe_free()). - * - * `value` may be NULL (null variant); OAKNODE_VALUE_STRING is rejected - * (use oaknode_keyframe_set_value_string() after creation). `type` is an - * oaknode_keyframe_type. `parent_or_null` may be an empty handle. - * - * @return Keyframe handle with count 1; ctx is NULL on invalid argument - * or allocation failure. - */ -OakNodeKeyframe oaknode_keyframe_create(int64_t time_num, int64_t time_den, - const oaknode_value *value, int type, - int track, int element, - const char *input_id, - OakNodeNode parent_or_null); - -/** - * @brief Release one reference to a keyframe handle. - * - * Convenience wrapper around handle.release(handle.ctx): destroys the - * keyframe when the count reaches zero and the handle owns it. NULL - * handle or NULL ctx is a no-op; clears `keyframe->ctx` after releasing. - * Never free a keyframe that is attached to a node's track. - */ -void oaknode_keyframe_free(OakNodeKeyframe *keyframe); - -/** - * @brief The keyframe's time as a rational (numerator/denominator). - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_keyframe_get_time(OakNodeKeyframe keyframe, - int64_t *out_num, int64_t *out_den); - -/** - * @brief Set the keyframe's time directly (live). - */ -int oaknode_keyframe_set_time(OakNodeKeyframe keyframe, int64_t time_num, - int64_t time_den); - -/** - * @brief Create a set-time command (olive::NodeParamSetKeyframeTimeCommand). - */ -int oaknode_keyframe_set_time_undoable(OakNodeKeyframe keyframe, - int64_t time_num, int64_t time_den, - OakUndoCommand *out_command); - -/** - * @brief Read the keyframe's value mapped into `out`. Values without a - * POD representation fail with OAKNODE_E_FAILED. - */ -int oaknode_keyframe_get_value(OakNodeKeyframe keyframe, - oaknode_value *out); - -/** - * @brief Set the keyframe's value directly (live). - * OAKNODE_VALUE_STRING is rejected (use - * oaknode_keyframe_set_value_string()). - */ -int oaknode_keyframe_set_value(OakNodeKeyframe keyframe, - const oaknode_value *v); - -/** - * @brief Create a set-value command - * (olive::NodeParamSetKeyframeValueCommand). - */ -int oaknode_keyframe_set_value_undoable(OakNodeKeyframe keyframe, - const oaknode_value *v, - OakUndoCommand *out_command); - -/** - * @brief Read a string value. Two-stage getter. - * - * @return Required buffer size in bytes including the terminating NUL - * (non-negative), or a negative OAKNODE_E_* error code. - */ -int oaknode_keyframe_get_value_string(OakNodeKeyframe keyframe, - char *buf, int buf_size); - -/** - * @brief Set a string value directly (live). - */ -int oaknode_keyframe_set_value_string(OakNodeKeyframe keyframe, - const char *value); - -/** - * @brief Create a set-string-value command. - */ -int oaknode_keyframe_set_value_string_undoable(OakNodeKeyframe keyframe, - const char *value, - OakUndoCommand *out_command); - -/** - * @brief The keyframe's interpolation type (oaknode_keyframe_type). - */ -int oaknode_keyframe_get_type(OakNodeKeyframe keyframe, int *out_type); - -/** - * @brief Set the interpolation type directly (live, - * NodeKeyframe::set_type(), which adjusts neighbouring bezier handles). - */ -int oaknode_keyframe_set_type(OakNodeKeyframe keyframe, int type); - -/** - * @brief Create a set-type command (same semantics as the live variant). - */ -int oaknode_keyframe_set_type_undoable(OakNodeKeyframe keyframe, int type, - OakUndoCommand *out_command); - -/** - * @brief A bezier control point (`handle` is an - * oaknode_keyframe_bezier). - */ -int oaknode_keyframe_get_bezier_control(OakNodeKeyframe keyframe, - int handle, double *out_x, - double *out_y); - -/** - * @brief Set a bezier control point directly (live). - */ -int oaknode_keyframe_set_bezier_control(OakNodeKeyframe keyframe, int handle, - double x, double y); - -/** - * @brief Create a set-bezier-control command. - */ -int oaknode_keyframe_set_bezier_control_undoable(OakNodeKeyframe keyframe, - int handle, double x, double y, - OakUndoCommand *out_command); - -/** - * @brief The keyframe's track index. - */ -int oaknode_keyframe_get_track(OakNodeKeyframe keyframe, - int *out_track); - -/** - * @brief The keyframe's element index. - */ -int oaknode_keyframe_get_element(OakNodeKeyframe keyframe, - int *out_element); - -/** - * @brief The id of the input this keyframe belongs to. Two-stage getter. - */ -int oaknode_keyframe_get_input(OakNodeKeyframe keyframe, char *buf, - int buf_size); - -/** - * @brief The node this keyframe belongs to (borrowed handle written to - * `out_node`; release it with oaknode_node_free()), an empty handle when - * orphaned. OAKNODE_OK either way. - */ -int oaknode_keyframe_get_parent(OakNodeKeyframe keyframe, - OakNodeNode *out_node); - -/** - * @brief A bezier control point guaranteed valid for animation - * (NodeKeyframe::valid_bezier_control_in()/out()). - * - * Unlike oaknode_keyframe_get_bezier_control(), the returned point is - * clamped so the curve never overlaps: the in-handle's x cannot pass the - * previous keyframe's time and the out-handle's x cannot pass the next - * keyframe's time. `handle` is an oaknode_keyframe_bezier. - */ -int oaknode_keyframe_get_valid_bezier_control(OakNodeKeyframe keyframe, - int handle, double *out_x, - double *out_y); - -/** - * @brief The opposing bezier handle type - * (NodeKeyframe::get_opposing_bezier_type): OAKNODE_KEYFRAME_IN_HANDLE - * (0) <-> OAKNODE_KEYFRAME_OUT_HANDLE (1). - * - * @return The opposing handle type, or OAKNODE_E_INVALID for a type - * outside the two handle values. - */ -int oaknode_keyframe_opposing_bezier_type(int type); - -/** - * @brief Compute the combined node value to use when inserting - * `keyframe` onto `target_node` (the keyframe paste path). - * - * Takes the target node's split value at the keyframe's time, replaces - * the keyframe's own track with the keyframe's value, and combines the - * per-track components into a single normal value (mirrors the facade's - * oakengine_keyframe_compute_paste_value). OAKNODE_E_NOT_FOUND when the - * keyframe's input id does not exist on `target_node`; OAKNODE_E_FAILED - * for input types without a POD representation. - */ -int oaknode_keyframe_compute_paste_value(OakNodeNode target_node, - OakNodeKeyframe keyframe, - oaknode_value *out); - -/** - * @brief 1 if a sibling keyframe exists at the given rational time on - * this keyframe's own track (NodeKeyframe::has_sibling_at_time(): the - * track's key at `time` that is not this keyframe — the move-collision - * check). Unlike the facade, the time is an exact rational rather than a - * whole-second frame timestamp, and no track argument is needed (the - * lookup is relative to this keyframe's track). - * - * An orphaned keyframe (no parent node) has no siblings: `*out_value` - * is set to 0 and OAKNODE_OK is returned. - */ -int oaknode_keyframe_has_sibling_at_time(OakNodeKeyframe keyframe, - int64_t time_num, int64_t time_den, - int *out_value); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_NODE_KEYFRAME_H diff --git a/crates/oakengine.bk/include/node/multicam.h b/crates/oakengine.bk/include/node/multicam.h deleted file mode 100644 index 5d5699395..000000000 --- a/crates/oakengine.bk/include/node/multicam.h +++ /dev/null @@ -1,119 +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 . - -***/ - -#ifndef OAK_EDITOR_NODE_MULTICAM_H -#define OAK_EDITOR_NODE_MULTICAM_H - -#include - -#include "node/error.h" -#include "node/node.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file multicam.h - * @brief C ABI for olive::MultiCamNode (src/node/src/input/multicam/ - * multicamnode.h): multi-camera source switching and the source-grid - * math used by the multicam viewer. - * - * The input-id getters return static strings (never freed) naming the - * multicam node's inputs: current source (combo), sources (array), - * sequence and sequence type. A node that is not a MultiCamNode (or a - * NULL handle) fails the per-node queries with OAKNODE_E_INVALID. - * - * The grid helpers are static and pure: they only depend on their - * arguments, not on a node. - */ - -/** - * @brief The input id string for the current camera ("current_in"). - */ -const char *oaknode_multicam_input_current(void); - -/** - * @brief The input id string for the sources array ("sources_in"). - */ -const char *oaknode_multicam_input_sources(void); - -/** - * @brief The input id string for the sequence ("sequence_in"). - */ -const char *oaknode_multicam_input_sequence(void); - -/** - * @brief The input id string for the sequence type ("sequence_type_in"). - */ -const char *oaknode_multicam_input_sequence_type(void); - -/** - * @brief Number of connected source cameras (MultiCamNode:: - * get_source_count(); the connected sequence's track count, or the - * sources array size when no sequence is connected). - * - * OAKNODE_E_INVALID when `node` is not a multicam. - */ -int oaknode_multicam_get_source_count(OakNodeNode node, int *out_count); - -/** - * @brief Compute the grid (rows, cols) that holds `source_count` cells. - * - * Mirrors MultiCamNode::get_rows_and_columns(): the grid grows from - * 1x1, widening the smaller dimension, until rows * cols >= source_count - * (0 sources yields 1x1). OAKNODE_E_INVALID for a negative count or - * NULL out pointers. - */ -int oaknode_multicam_get_rows_and_columns(int source_count, int *rows, - int *cols); - -/** - * @brief Convert a flat source index to (row, col) in a rows x cols grid - * (row-major: col = index % cols, row = index / cols). - * - * OAKNODE_E_INVALID for a negative index, degenerate grid or NULL out - * pointers. - */ -int oaknode_multicam_index_to_row_cols(int index, int rows, int cols, - int *out_row, int *out_col); - -/** - * @brief Convert (row, col) to a flat source index (col + row * cols). - * - * @return The flat index (>= 0), or OAKNODE_E_INVALID when the cell is - * out of range or the grid is degenerate. - */ -int oaknode_multicam_rows_cols_to_index(int row, int col, int rows, - int cols); - -/** - * @brief The current source index (MultiCamNode::get_current_source(), - * the "current_in" combo value). - * - * OAKNODE_E_INVALID when `node` is not a multicam. - */ -int oaknode_multicam_get_current_source(OakNodeNode node, int *out_source); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_NODE_MULTICAM_H diff --git a/crates/oakengine.bk/include/node/node.h b/crates/oakengine.bk/include/node/node.h deleted file mode 100644 index cfb03b304..000000000 --- a/crates/oakengine.bk/include/node/node.h +++ /dev/null @@ -1,679 +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 . - -***/ - -#ifndef OAK_EDITOR_NODE_NODE_H -#define OAK_EDITOR_NODE_NODE_H - -#include - -#include "common/videoparams.h" -#include "node/error.h" -#include "undo/undocommand.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file node.h - * @brief C ABI for olive::Node (src/node/src/node.h). - * - * Handles are by-value reference-counted structs (see - * include/common/handle.h): every OakNodeNode carries ctx/addref/release/ - * abi_version and behaves like a shared_ptr at the ABI level. Factory - * functions return a handle with reference count 1; release it with - * oaknode_node_free(). Handles borrowed from a graph only release the - * handle itself when freed; once a node lives in a project graph its - * lifetime belongs to the graph (the implementation flips ownership - * internally), and borrowed handles become invalid when the owning project - * or node is destroyed. - * - * Parameter values cross the boundary as the POD oaknode_value; the - * meaningful fields depend on its type (oaknode_value_type). String-typed - * inputs (NodeValue::k_file/k_text/k_font/k_str_combo) do not fit the POD - * and use the dedicated *_input_string() pair (two-stage buf/size getters - * return the required size including the terminating NUL). - * - * Every mutating function comes in a live variant (applies immediately) - * and an undoable variant (suffix _undoable) that creates an - * olive::UndoCommand without executing it and returns it as an owned - * OakUndoCommand handle. Execute it with oakundo_command_redo_now(), - * push it onto an OakUndoStack, or release it with - * oakundo_command_free(). - */ - -/** - * @brief Value type of an oaknode_value / a node input. - * - * Pinned mapping to olive::NodeValue::Type (src/node/src/value.h): - * NONE -> k_none, INT -> k_int, FLOAT -> k_float, BOOL -> k_boolean, - * RATIONAL -> k_rational, COLOR -> k_color, VEC2 -> k_vec2, - * VEC3 -> k_vec3, VEC4 -> k_vec4, COMBO -> k_combo, - * STRING -> k_file (string-family inputs: k_file/k_text/k_font/ - * k_str_combo, handled by the dedicated string functions). Types without - * a POD representation (texture, samples, matrix, params, bezier, binary, - * ...) report as OAKNODE_VALUE_NONE. - */ -typedef enum oaknode_value_type { - OAKNODE_VALUE_NONE = 0, - OAKNODE_VALUE_INT, /**< num (olive k_int, int64_t) */ - OAKNODE_VALUE_FLOAT, /**< f[0] (olive k_float, double) */ - OAKNODE_VALUE_BOOL, /**< num 0/1 (olive k_boolean) */ - OAKNODE_VALUE_RATIONAL, /**< num/den (olive k_rational) */ - OAKNODE_VALUE_COLOR, /**< f[0..3] = r,g,b,a (olive k_color) */ - OAKNODE_VALUE_VEC2, /**< f[0..1] (olive k_vec2) */ - OAKNODE_VALUE_VEC3, /**< f[0..2] (olive k_vec3) */ - OAKNODE_VALUE_VEC4, /**< f[0..3] (olive k_vec4) */ - OAKNODE_VALUE_COMBO, /**< num = selected index (olive k_combo) */ - OAKNODE_VALUE_STRING, /**< k_file string family; string APIs only */ - OAKNODE_VALUE_COUNT -} oaknode_value_type; - -/** - * @brief POD parameter value. Only the fields documented for the value's - * `type` are meaningful. - */ -typedef struct oaknode_value { - int type; /**< oaknode_value_type. */ - int64_t num; /**< INT/COMBO value, BOOL 0/1, RATIONAL numerator. */ - int64_t den; /**< RATIONAL denominator. */ - double f[4]; /**< FLOAT f[0]; VEC2/3/4 f[0..n-1]; COLOR r,g,b,a. */ -} oaknode_value; - -/** - * @brief Reference-counted handle to a node (olive::Node). - * - * The object never leaves the library that created it; every external - * reference is one of these handles. Semantics are shared_ptr-like: - * init/factory functions return a handle with reference count 1, - * addref(ctx) takes another reference, release(ctx) drops one; release a - * handle with oaknode_node_free(). Borrowed handles into graph-owned - * objects only release the handle itself. - */ -typedef struct OakNodeNode { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKNODE_ABI_VERSION. */ -} OakNodeNode; - -/* Re-declared here so node.h is self-contained; see node/project.h. */ -typedef struct OakNodeProject OakNodeProject; - -/* Re-declared here so node.h is self-contained; see node/footage.h. */ -typedef struct OakNodeFootage OakNodeFootage; - -/** - * @brief Timeline data owned by viewer nodes (TimelineMarkerList / - * TimelineWorkArea in oaktimeline) cross the boundary as oaktimeline - * value handles. Forward-declared here so node.h stays self-contained; - * include timeline/marker.h / timeline/workarea.h for the definitions. - */ -struct OakTimelineMarkerList; -struct OakTimelineWorkArea; - -/** - * @brief Opaque borrowed handle to a node's video frame cache - * (olive::FrameHashCache in oakrender). oakrender reinterprets this into - * its own handle types. - */ -struct OakRenderCache; - -/* oakcore handles used by the viewer setters. */ -typedef struct OakAudioParams OakAudioParams; - -/** - * @brief Number of live owned objects created through this API - * (nodes from oaknode_factory_create_from_id()/oaknode_node_create_copy(), - * keyframes, groups, traversers, traverser databases). Debug aid for - * leak checking; thread-unsafe, test/diagnostic use only. - */ -int oaknode_debug_alive_count(void); - -/* ---- Metadata --------------------------------------------------------- */ - -/** - * @brief The node's unique type id (Node::id(), e.g. - * "org.olivevideoeditor.Olive.solidgenerator"). Two-stage getter. - * - * @return Required buffer size in bytes including the terminating NUL - * (non-negative), or a negative OAKNODE_E_* error code. - */ -int oaknode_node_get_id(OakNodeNode node, char *buf, int buf_size); - -/** - * @brief The node's display name (Node::name()). Two-stage getter, - * same return convention as oaknode_node_get_id(). - */ -int oaknode_node_get_name(OakNodeNode node, char *buf, int buf_size); - -/** - * @brief The node's user label (Node::get_label()). Two-stage getter, - * same return convention as oaknode_node_get_id(). - */ -int oaknode_node_get_label(OakNodeNode node, char *buf, int buf_size); - -/** - * @brief Set the node's user label directly (Node::set_label(), live). - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_node_set_label(OakNodeNode node, const char *label); - -/** - * @brief Create a label-change command (olive::NodeRenameCommand). - * - * The command is NOT executed; `out_command` receives an owned command - * handle. - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_node_set_label_undoable(OakNodeNode node, const char *label, - OakUndoCommand *out_command); - -/** - * @brief The node's override color index (Node::get_override_color(); - * -1 = none). - * - * @param out_value Receives the result. Must not be NULL. - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_node_get_override_color(OakNodeNode node, int *out_value); - -/** - * @brief Set the override color index directly (-1 = none; live). - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_node_set_override_color(OakNodeNode node, int index); - -/** - * @brief Create an override-color command (olive::NodeOverrideColorCommand). - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_node_set_override_color_undoable(OakNodeNode node, int index, - OakUndoCommand *out_command); - -/** - * @brief 1 if the node is enabled (the boolean "enabled_in" input's - * standard value). - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_node_is_enabled(OakNodeNode node, int *out_value); - -/** - * @brief Set the node's enabled state directly (live). - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_node_set_enabled(OakNodeNode node, int enabled); - -/** - * @brief Create an enabled-state command - * (olive::NodeParamSetStandardValueCommand on "enabled_in"). - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_node_set_enabled_undoable(OakNodeNode node, int enabled, - OakUndoCommand *out_command); - -/* ---- Input introspection ------------------------------------------------ */ - -/** - * @brief Number of declared inputs (Node::inputs(); array elements are not - * counted separately). - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_node_input_count(OakNodeNode node, int *out_count); - -/** - * @brief The input id at `index` (Node::inputs()). Two-stage getter; - * returns OAKNODE_E_NOT_FOUND for an out-of-range index. - */ -int oaknode_node_input_id(OakNodeNode node, int index, char *buf, - int buf_size); - -/** - * @brief The input's value type mapped to oaknode_value_type (see the - * pinned mapping on oaknode_value_type). OAKNODE_E_NOT_FOUND for an - * unknown input id. - */ -int oaknode_node_input_get_type(OakNodeNode node, const char *input_id, - int *out_type); - -/** - * @brief 1 if the input currently has a connected edge - * (Node::is_input_connected()). OAKNODE_E_NOT_FOUND for an unknown id. - */ -int oaknode_node_input_is_connected(OakNodeNode node, const char *input_id, - int *out_value); - -/** - * @brief 1 if the input accepts connections (Node::is_input_connectable()). - * OAKNODE_E_NOT_FOUND for an unknown id. - */ -int oaknode_node_input_is_connectable(OakNodeNode node, const char *input_id, - int *out_value); - -/** - * @brief The human-readable name of the input (Node::get_input_name()). - * Two-stage getter; OAKNODE_E_NOT_FOUND for an unknown id. - */ -int oaknode_node_get_input_name(OakNodeNode node, const char *input_id, - char *buf, int buf_size); - -/** - * @brief The node feeding this input (Node::get_connected_output(), - * element -1). `out_node` receives a borrowed handle (empty, ctx == NULL, - * when not connected; releasing it only releases the handle). - * OAKNODE_E_NOT_FOUND for an unknown input id. - */ -int oaknode_node_input_get_connected_node(OakNodeNode node, - const char *input_id, - OakNodeNode *out_node); - -/* ---- Parameter access ----------------------------------------------------- */ - -/** - * @brief Read an input's standard value (Node::get_standard_value()) - * mapped into `out`. - * - * String-family inputs fail with OAKNODE_E_INVALID (use - * oaknode_node_get_input_string()); types without a POD representation - * fail with OAKNODE_E_FAILED; an unknown input id fails with - * OAKNODE_E_NOT_FOUND. - */ -int oaknode_node_get_input(OakNodeNode node, const char *input_id, - oaknode_value *out); - -/** - * @brief Write an input's standard value directly (live, - * Node::set_standard_value()). - * - * `v->type` must match the input's declared type; OAKNODE_VALUE_STRING is - * rejected (use oaknode_node_set_input_string()). - */ -int oaknode_node_set_input(OakNodeNode node, const char *input_id, - const oaknode_value *v); - -/** - * @brief Create a set-standard-value command - * (olive::NodeParamSetStandardValueCommand, track -1 semantics via the - * whole-value reference on track 0). - * - * Same type rules as oaknode_node_set_input(). - */ -int oaknode_node_set_input_undoable(OakNodeNode node, const char *input_id, - const oaknode_value *v, - OakUndoCommand *out_command); - -/** - * @brief Read a string-family input's standard value. Two-stage getter. - */ -int oaknode_node_get_input_string(OakNodeNode node, const char *input_id, - char *buf, int buf_size); - -/** - * @brief Write a string-family input's standard value directly (live). - */ -int oaknode_node_set_input_string(OakNodeNode node, const char *input_id, - const char *value); - -/** - * @brief Create a set-standard-value command for a string-family input. - */ -int oaknode_node_set_input_string_undoable(OakNodeNode node, - const char *input_id, - const char *value, - OakUndoCommand *out_command); - -/* ---- Graph editing -------------------------------------------------------- */ - -/** - * @brief Connect `output_node`'s output into `input_node`'s `input_id` - * directly (live, Node::connect_edge(), element -1). - * - * Fails with OAKNODE_E_NOT_FOUND for an unknown input id, - * OAKNODE_E_INVALID when the input is not connectable, and - * OAKNODE_E_STATE when the input is already connected or the nodes belong - * to different graphs. - */ -int oaknode_node_connect(OakNodeNode output_node, OakNodeNode input_node, - const char *input_id); - -/** - * @brief Create an edge-add command (olive::NodeEdgeAddCommand, - * element -1). Same validation as oaknode_node_connect() except the - * different-graph check (the command may legitimately be redone after - * graph changes). - */ -int oaknode_node_connect_undoable(OakNodeNode output_node, - OakNodeNode input_node, - const char *input_id, - OakUndoCommand *out_command); - -/** - * @brief Remove the edge feeding `input_node`'s `input_id` directly - * (live, Node::disconnect_edge(), element -1). OAKNODE_E_NOT_FOUND when - * the input is unknown or not connected. - */ -int oaknode_node_disconnect(OakNodeNode input_node, const char *input_id); - -/** - * @brief Create an edge-remove command (olive::NodeEdgeRemoveCommand, - * element -1). OAKNODE_E_NOT_FOUND when not connected. - */ -int oaknode_node_disconnect_undoable(OakNodeNode input_node, - const char *input_id, - OakUndoCommand *out_command); - -/** - * @brief Number of outgoing edges (Node::output_connections()). - */ -int oaknode_node_output_connection_count(OakNodeNode node, int *out_count); - -/** - * @brief The node at the input end of outgoing edge `index` (borrowed - * handle; releasing it only releases the handle). OAKNODE_E_NOT_FOUND for - * an out-of-range index. - */ -int oaknode_node_output_connection_node_at(OakNodeNode node, int index, - OakNodeNode *out_node); - -/** - * @brief The input id at the input end of outgoing edge `index`. - * Two-stage getter; OAKNODE_E_NOT_FOUND for an out-of-range index. - */ -int oaknode_node_output_connection_input_id_at(OakNodeNode node, int index, - char *buf, int buf_size); - -/** - * @brief The input element at the input end of outgoing edge `index` - * (-1 for non-array inputs). OAKNODE_E_NOT_FOUND for an out-of-range - * index. - */ -int oaknode_node_output_connection_element_at(OakNodeNode node, int index, - int *out_element); - -/* ---- Links --------------------------------------------------------------- */ - -/** - * @brief Link two nodes directly (live, Node::link()). `out_linked` - * receives 1 on success, 0 when the link was rejected (e.g. either node - * rejects links). `out_linked` may be NULL. - */ -int oaknode_node_link(OakNodeNode a, OakNodeNode b, int *out_linked); - -/** - * @brief Unlink two nodes directly (live, Node::unlink()). - * `out_unlinked` receives 1 on success, 0 otherwise; may be NULL. - */ -int oaknode_node_unlink(OakNodeNode a, OakNodeNode b, int *out_unlinked); - -/** - * @brief Create a link/unlink command (olive::NodeLinkCommand; - * `link` != 0 links, 0 unlinks). - */ -int oaknode_node_link_undoable(OakNodeNode a, OakNodeNode b, int link, - OakUndoCommand *out_command); - -/** - * @brief 1 if the two nodes are linked (Node::are_linked()). - */ -int oaknode_node_are_linked(OakNodeNode a, OakNodeNode b, int *out_value); - -/** - * @brief Number of linked nodes (Node::links()). - */ -int oaknode_node_link_count(OakNodeNode node, int *out_count); - -/** - * @brief The linked node at `index` (borrowed handle; releasing it only - * releases the handle). OAKNODE_E_NOT_FOUND for an out-of-range index. - */ -int oaknode_node_link_at(OakNodeNode node, int index, - OakNodeNode *out_node); - -/* ---- Context positions ---------------------------------------------------- */ - -/** - * @brief Number of context entries (Node::get_context_positions()). - */ -int oaknode_node_context_count(OakNodeNode node, int *out_count); - -/** - * @brief The context node at `index` (borrowed handle; releasing it only - * releases the handle). OAKNODE_E_NOT_FOUND for an out-of-range index. - */ -int oaknode_node_context_node_at(OakNodeNode node, int index, - OakNodeNode *out_node); - -/** - * @brief The node's position in `context` (any out pointer may be NULL). - * OAKNODE_E_NOT_FOUND when the context does not contain this node. - */ -int oaknode_node_get_context_position(OakNodeNode node, OakNodeNode context, - double *out_x, double *out_y, - int *out_expanded); - -/** - * @brief Set the node's position in `context` directly (live, - * Node::set_node_position_in_context() + set_node_expanded_in_context()). - */ -int oaknode_node_set_context_position(OakNodeNode node, OakNodeNode context, - double x, double y, int expanded); - -/** - * @brief Create a set-position command (olive::NodeSetPositionCommand). - */ -int oaknode_node_set_context_position_undoable(OakNodeNode node, - OakNodeNode context, double x, - double y, int expanded, - OakUndoCommand *out_command); - -/** - * @brief Remove the node from `context` directly (live). - * OAKNODE_E_NOT_FOUND when not contained. - */ -int oaknode_node_remove_from_context(OakNodeNode node, OakNodeNode context); - -/* ---- Lifetime --------------------------------------------------------------- */ - -/** - * @brief Create a standalone copy of the node (Node::copy()). The copy is - * NOT added to any graph; the returned handle has reference count 1 and - * must be released with oaknode_node_free() while it is still orphaned. - * Returns an empty handle (ctx == NULL) for an empty handle or on failure. - */ -OakNodeNode oaknode_node_create_copy(OakNodeNode node); - -/** - * @brief Copy a node inside its graph (Node::copy_node_in_graph()), - * recording the reconnect operations in a new MultiUndoCommand. - * - * `*out_command` receives an owned undo command handle (free with - * oakundo_command_free()). The copy is inserted into the graph only when - * the returned command is redone; treat it as owned (oaknode_node_free()) - * until then. Returns an empty handle (ctx == NULL) on failure. - */ -OakNodeNode oaknode_node_copy_in_graph(OakNodeNode node, - OakUndoCommand *out_command); - -/** - * @brief Get the project this node belongs to. `out` receives a borrowed - * handle (empty, ctx == NULL, if the node is orphaned; releasing it only - * releases the handle). - */ -int oaknode_node_get_project(OakNodeNode node, OakNodeProject *out); - -/** - * @brief Insert/remove an element in an input array (live, - * Node::input_array_insert/remove()). OAKNODE_E_NOT_FOUND for an - * unknown input id. - */ -int oaknode_node_input_array_insert(OakNodeNode node, const char *input_id, - int index); -int oaknode_node_input_array_remove(OakNodeNode node, const char *input_id, - int index); - -/** - * @brief Element-aware variants of oaknode_node_connect()/disconnect() - * (NodeInput element != -1, e.g. Sequence's track_in_N array inputs). - */ -int oaknode_node_connect_element(OakNodeNode output_node, - OakNodeNode input_node, - const char *input_id, int element); -int oaknode_node_disconnect_element(OakNodeNode input_node, - const char *input_id, int element); - -/** - * @brief Create a command that adds a node to a project's graph - * (olive::NodeAddCommand). Owned; free with oakundo_command_free(). - */ -OakUndoCommand oaknode_command_create_add_node(OakNodeProject graph, - OakNodeNode node); - -/** - * @brief Create a command that sets a node's position in a context and - * repositions its dependencies recursively - * (olive::NodeSetPositionAndDependenciesRecursivelyCommand). Owned. - */ -OakUndoCommand oaknode_command_create_set_position_recursive( - OakNodeNode node, OakNodeNode context, double x, double y); - -/** - * @brief Marker list / work area of a viewer node, as addref'd - * oaktimeline value handles (release with - * oaktimeline_marker_list_free()/oaktimeline_workarea_free()). *out is - * an empty handle (ctx == NULL) when the node is not a viewer or for - * an empty node handle. - */ -int oaknode_node_get_markers(OakNodeNode node, - struct OakTimelineMarkerList *out); -int oaknode_node_get_work_area(OakNodeNode node, - struct OakTimelineWorkArea *out); - -/** - * @brief Video frame cache of a node as an addref'd oakrender value - * handle (release with oakrender_cache_free()). *out is an - * empty handle (ctx == NULL) when the node has none or for an - * empty node handle. struct OakRenderCache is forward-declared - * here; include render/cache.h for the definition. - */ -int oaknode_node_get_video_frame_cache(OakNodeNode node, - struct OakRenderCache *out); - -/** - * @brief Copy input values/connections from one node to another - * (Node::copy_inputs()). include_connections != 0 also copies - * input connections. - */ -int oaknode_node_copy_inputs(OakNodeNode dst, OakNodeNode src, - int include_connections); - -/** - * @brief Set a track-routing value hint on an input - * (Node::set_value_hint_for_input() with a single texture type - * and a Track::Reference string). - */ -int oaknode_node_set_value_hint_track(OakNodeNode node, const char *input_id, - int track_type, int track_index); - -/** - * @brief Set a viewer node's video/audio params (ViewerOutput:: - * set_video_params/set_audio_params, stream index 0). `params` is an - * oakcommon handle (video) or borrowed oakcore handle (audio). - */ -int oaknode_viewer_set_video_params(OakNodeNode viewer, - const OakVideoParams *params); -int oaknode_viewer_set_audio_params(OakNodeNode viewer, - const OakAudioParams *params); - -/** - * @brief Find a footage node upstream of this node's inputs - * (Node::find_input_nodes(), first match). `out` receives - * a borrowed handle (empty, ctx == NULL, when none; releasing it - * only releases the handle). - */ -int oaknode_node_find_input_footage(OakNodeNode node, OakNodeFootage *out); - -/** - * @brief Value of an input at a specific time (Node::get_value_at_time(), - * element -1). Same POD rules as oaknode_node_get_input(). - */ -int oaknode_node_get_input_at_time(OakNodeNode node, - const char *input_id, int64_t time_num, - int64_t time_den, oaknode_value *out); - -/** - * @brief Set an input's value at a specific time with keyframe logic - * (Node::set_value_at_time(), element -1, track 0, - * insert_on_all_tracks_if_no_key = true). `*out_command` receives - * an owned undo command handle. - */ -int oaknode_node_set_input_at_time_undoable(OakNodeNode node, - const char *input_id, int64_t time_num, int64_t time_den, - const oaknode_value *v, int track, OakUndoCommand *out_command); - -/** - * @brief Identity of the underlying node object as an opaque integer - * (address-cast; for registry keys only, never dereference). - */ -uintptr_t oaknode_node_identity(OakNodeNode node); - -/** - * @brief Append a value-at-time set into an existing multi command - * (same semantics as oaknode_node_set_input_at_time_undoable but - * batches into `multi_command` from oakundo_command_init_multi()). - */ -int oaknode_node_set_input_at_time_into(OakNodeNode node, - const char *input_id, int64_t time_num, int64_t time_den, - const oaknode_value *v, int track, OakUndoCommand multi_command); - -/** - * @brief Create a command that removes a node from its graph together - * with its exclusive dependencies and disconnects its edges - * (NodeRemoveWithExclusiveDependenciesAndDisconnect). - * - * Owned command handle; free with oakundo_command_free(). Returns an - * empty handle (ctx == NULL) on failure. - */ -OakUndoCommand oaknode_command_create_remove_node(OakNodeNode node); - -/** - * @brief Release one reference to a node handle. - * - * Convenience wrapper around handle.release(handle.ctx): the underlying - * node is destroyed only when the last reference of an OWNED handle is - * released; releasing a borrowed handle into a graph-owned object only - * destroys the handle itself. NULL handle or NULL ctx is a no-op; clears - * `node->ctx` after releasing. - */ -void oaknode_node_free(OakNodeNode *node); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_NODE_NODE_H diff --git a/crates/oakengine.bk/include/node/project.h b/crates/oakengine.bk/include/node/project.h deleted file mode 100644 index 42795a59b..000000000 --- a/crates/oakengine.bk/include/node/project.h +++ /dev/null @@ -1,267 +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 . - -***/ - -#ifndef OAK_EDITOR_NODE_PROJECT_H -#define OAK_EDITOR_NODE_PROJECT_H - -#include - -#include "node/error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file project.h - * @brief C ABI for olive::Project (oaknode) - * - * An OakNodeProject owns its whole node graph: nodes added with - * oaknode_project_add_node() (directly, or indirectly through the folder and - * footage families) are deleted when the project's last reference is - * released. Handles to nodes, folders and footage obtained from a project - * are borrowed views: releasing them only releases the handle itself. - * - * Conventions (shared by all oaknode C API families): - * - Return codes: 0 (OAKNODE_OK) on success, a negative OAKNODE_E_* code on - * failure. - * - String getters are two-stage: pass buf == NULL (or a short buffer) to - * query the required size; the return value is the required buffer size in - * bytes INCLUDING the terminating NUL. The output is NUL-terminated - * whenever buf_size > 0. - * - Empty handles (ctx == NULL) yield OAKNODE_E_INVALID (or a no-op for - * free()). - * - Disk save/load of project files is NOT part of this layer; it belongs to - * oakstorage (milestone M10). - */ - -/** - * @brief Reference-counted handle to a project (olive::Project). - * - * Semantics are shared_ptr-like: oaknode_project_init() returns a handle - * whose underlying object has reference count 1, addref(ctx) takes another - * reference, and release(ctx) (or oaknode_project_free()) drops one; the - * project and every node it owns are destroyed when the count reaches zero. - */ -typedef struct OakNodeProject { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKNODE_ABI_VERSION. */ -} OakNodeProject; - -/** - * @brief Node handle (defined by the node family; forward-declared - * here so the headers can be included in any order). - */ -typedef struct OakNodeNode OakNodeNode; - -/** - * @brief Folder handle (defined in node/folder.h; forward-declared - * here so the headers can be included in any order). Handles obtained from - * a project are borrowed from it. - */ -typedef struct OakNodeFolder OakNodeFolder; - -/** - * @brief Create an empty project shell. - * - * The project has no root folder until oaknode_project_initialize() is - * called (mirrors Project::initialize()). - * - * @return Project handle with reference count 1 (release with - * oaknode_project_free()); ctx is NULL on allocation failure. - */ -OakNodeProject oaknode_project_init(void); - -/** - * @brief Release one reference to a project handle. - * - * Destroys the project and every node it owns when the count reaches zero. - * NULL handle or NULL ctx is a no-op; clears `project->ctx` after releasing. - */ -void oaknode_project_free(OakNodeProject *project); - -/** - * @brief Initialize the project: create the root folder (Project::initialize()). - * - * @return OAKNODE_OK, or OAKNODE_E_STATE if already initialized. - */ -int oaknode_project_initialize(OakNodeProject project); - -/** - * @brief Destructively destroy all nodes in the graph (Project::clear()). - * - * The project shell stays usable; oaknode_project_initialize() may be called - * again afterwards. - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_project_clear(OakNodeProject project); - -/** - * @brief Borrowed handle of the project's root folder (Project::root()). - * - * The returned handle only releases the handle itself; the project owns the - * folder. Empty handle (ctx == NULL) if the project has not been - * initialized. - */ -OakNodeFolder oaknode_project_root(OakNodeProject project); - -/** - * @brief Project display name (Project::name(): the filename's base name, or - * "(untitled)"). Two-stage string getter. - * - * @return Required buffer size in bytes including the NUL, or a negative - * OAKNODE_E_* error code. - */ -int oaknode_project_name(OakNodeProject project, char *buf, int buf_size); - -/** - * @brief Full path the project was saved as, or "" if untitled - * (Project::filename()). Two-stage string getter. - */ -int oaknode_project_filename(OakNodeProject project, char *buf, - int buf_size); - -/** - * @brief Display name safe for window titles (Project::pretty_filename()). - * Two-stage string getter. - */ -int oaknode_project_pretty_filename(OakNodeProject project, char *buf, - int buf_size); - -/** - * @brief Set the project's filename (Project::set_filename()). - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_project_set_filename(OakNodeProject project, const char *filename); - -/** - * @brief 1 if the project has unsaved changes, 0 otherwise - * (Project::is_modified()). Negative OAKNODE_E_* code on an empty handle. - */ -int oaknode_project_is_modified(OakNodeProject project); - -/** - * @brief Set the modified flag (Project::set_modified()). - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_project_set_modified(OakNodeProject project, int modified); - -/** - * @brief 1 if the project is new (untitled and unmodified, Project::is_new()). - * Negative OAKNODE_E_* code on an empty handle. - */ -int oaknode_project_is_new(OakNodeProject project); - -/** - * @brief Effective cache directory (Project::cache_path(), honoring the cache - * location setting). Two-stage string getter. - */ -int oaknode_project_cache_path(OakNodeProject project, char *buf, - int buf_size); - -/** - * @brief Copy all project settings (Project::copy_settings()). - */ -int oaknode_project_copy_settings(OakNodeProject dst, - OakNodeProject src); - -/** - * @brief Cache location setting enum value - * (Project::get_cache_location_setting(): 0 = default location, - * 1 = alongside project, 2 = custom path). Negative OAKNODE_E_* code on an - * empty handle. - */ -int oaknode_project_get_cache_location_setting(OakNodeProject project); - -/** - * @brief Set the cache location setting (0/1/2, see - * oaknode_project_get_cache_location_setting()). - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_project_set_cache_location_setting(OakNodeProject project, - int setting); - -/** - * @brief Custom cache directory, or "" when none is set - * (Project::get_custom_cache_path()). Two-stage string getter. - */ -int oaknode_project_get_custom_cache_path(OakNodeProject project, - char *buf, int buf_size); - -/** - * @brief Set a custom cache directory (Project::set_custom_cache_path()). - * NULL clears it. - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_project_set_custom_cache_path(OakNodeProject project, - const char *path); - -/** - * @brief Project UUID string (Project::get_uuid()). Two-stage string getter. - */ -int oaknode_project_get_uuid(OakNodeProject project, char *buf, - int buf_size); - -/** - * @brief Add a node to the graph; the graph assumes the node's lifetime - * (Project::add_node()). - * - * After a successful call the graph owns the node: releasing `node` only - * releases the handle itself. - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_project_add_node(OakNodeProject project, OakNodeNode node); - -/** - * @brief Detach a node from the graph without deleting it - * (Project::remove_node()). - * - * @return OAKNODE_OK, OAKNODE_E_NOT_FOUND if the node is not in the graph, or - * another negative OAKNODE_E_* error code. - */ -int oaknode_project_remove_node(OakNodeProject project, OakNodeNode node); - -/** - * @brief Number of nodes belonging to the graph (Project::nodes().size()). - * Negative OAKNODE_E_* code on an empty handle. - */ -int oaknode_project_node_count(OakNodeProject project); - -/** - * @brief Borrowed handle of the graph node at `index`. - * - * The returned handle only releases the handle itself. Empty handle - * (ctx == NULL) when out of range. - */ -OakNodeNode oaknode_project_node_at(OakNodeProject project, int index); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_NODE_PROJECT_H diff --git a/crates/oakengine.bk/include/node/sequence.h b/crates/oakengine.bk/include/node/sequence.h deleted file mode 100644 index e040a785c..000000000 --- a/crates/oakengine.bk/include/node/sequence.h +++ /dev/null @@ -1,219 +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 . - -***/ - -#ifndef OAK_EDITOR_NODE_SEQUENCE_H -#define OAK_EDITOR_NODE_SEQUENCE_H - -#ifndef __cplusplus -#include -#endif - -#include - -#include "common/videoparams.h" -#include "node/error.h" -#include "olive/core/oakcore/audioparams.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Sequence texture/samples input ids (ViewerOutput::k_texture_input - * / k_samples_input) and the track input id format - * (Sequence::k_track_input_format). Pinned by test. - */ -#define OAKNODE_SEQUENCE_TEXTURE_INPUT "tex_in" -#define OAKNODE_SEQUENCE_SAMPLES_INPUT "samples_in" -#define OAKNODE_SEQUENCE_TRACK_INPUT_FORMAT "track_in_%1" - -/* Re-declared here so sequence.h is self-contained; see node/node.h. */ -typedef struct OakNodeNode OakNodeNode; - -/** - * @brief Reference-counted handle to a sequence (olive::Sequence). - * - * The object never leaves the library that created it; every external - * reference is one of these handles. Semantics are shared_ptr-like: - * oaknode_sequence_create() returns a handle with count 1, addref(ctx) - * takes another reference, release(ctx) drops one and the library - * destroys the object when the count reaches zero. - * - * Handles obtained from accessors (track lists, tracks) are borrowed: - * releasing them does not destroy the underlying object, which stays - * owned by the sequence graph. - */ -typedef struct OakNodeSequence { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKNODE_ABI_VERSION. */ -} OakNodeSequence; - -/** - * @brief Reference-counted handle to a track list (olive::TrackList), - * see node/track.h. - */ -typedef struct OakNodeTrackList OakNodeTrackList; - -/** - * @brief Reference-counted handle to a track (olive::Track), see - * node/track.h. - */ -typedef struct OakNodeTrack OakNodeTrack; - -/** - * @brief Create an empty sequence with zero tracks. - * - * @return Sequence handle with reference count 1 (release with - * oaknode_sequence_free()); ctx is NULL on allocation failure. - */ -OakNodeSequence oaknode_sequence_create(void); - -/** - * @brief Release one reference to a sequence handle. - * - * Destroys the sequence (and its owned track lists) when the reference - * count reaches zero. NULL handle or NULL ctx is a no-op; clears - * `sequence->ctx` after releasing. - * - * Tracks and blocks connected to the sequence are owned by the graph and - * are not deleted here; the caller must have torn them down first. - */ -void oaknode_sequence_free(OakNodeSequence *sequence); - -/** - * @brief Apply the default video/audio parameters - * (ViewerOutput::set_default_parameters()). - */ -int oaknode_sequence_set_default_parameters(OakNodeSequence sequence); - -/** - * @brief Borrowed cast from a sequence handle to its node handle. - * Empty handle for an empty handle. - */ -OakNodeNode oaknode_sequence_as_node(OakNodeSequence sequence); - -/** - * @brief Non-owning cast from a node handle to a sequence handle (empty - * ctx when the node is not a Sequence). - */ -OakNodeSequence oaknode_sequence_from_node(OakNodeNode node); - -/** - * @brief Borrowed handle to the per-type track list. - * - * @param type One of OAKNODE_TRACK_TYPE_VIDEO / _AUDIO / _SUBTITLE. - * @return OAKNODE_OK, OAKNODE_E_INVALID or OAKNODE_E_NOT_FOUND (bad type). - */ -int oaknode_sequence_get_track_list(OakNodeSequence sequence, int type, - OakNodeTrackList *out); - -/** - * @brief Number of connected tracks of the given type. - */ -int oaknode_sequence_get_track_count(OakNodeSequence sequence, int type, - int *count); - -/** - * @brief Borrowed handle to the track of `type` at `index`. - */ -int oaknode_sequence_get_track_at(OakNodeSequence sequence, int type, - int index, OakNodeTrack *out); - -/** - * @brief Flat track cache across all types (olive::Sequence::get_tracks()). - */ -int oaknode_sequence_get_all_track_count(OakNodeSequence sequence, int *count); -int oaknode_sequence_get_all_track_at(OakNodeSequence sequence, int index, - OakNodeTrack *out); - -/** - * @brief Playhead position in sequence time. - */ -int oaknode_sequence_get_playhead(OakNodeSequence sequence, int *numerator, - int *denominator); -int oaknode_sequence_set_playhead(OakNodeSequence sequence, int numerator, - int denominator); - -/** - * @brief Cached overall/video/audio lengths (olive::ViewerOutput). - */ -int oaknode_sequence_get_length(OakNodeSequence sequence, int *numerator, - int *denominator); -int oaknode_sequence_get_video_length(OakNodeSequence sequence, - int *numerator, int *denominator); -int oaknode_sequence_get_audio_length(OakNodeSequence sequence, - int *numerator, int *denominator); - -/** - * @brief Recompute the cached lengths from the track lists - * (olive::ViewerOutput::verify_length()). - */ -int oaknode_sequence_verify_length(OakNodeSequence sequence); - -/* --------------------------------------------------- Video/audio params */ - -/** - * @brief Number of video/audio parameter slots. - */ -int oaknode_sequence_get_video_stream_count(OakNodeSequence sequence, - int *count); -int oaknode_sequence_get_audio_stream_count(OakNodeSequence sequence, - int *count); - -/** - * @brief Video parameters at `index` as a NEW by-value handle owned by - * the caller (reference count 1, release with - * oakcommon_videoparams_free()). - * - * @return OAKNODE_OK, OAKNODE_E_INVALID, OAKNODE_E_NOT_FOUND or - * OAKNODE_E_NOMEM. - */ -int oaknode_sequence_get_video_params(OakNodeSequence sequence, int index, - OakVideoParams *out); - -/** - * @brief Replace the video parameters at `index` with a copy of `params`. - * - * @return OAKNODE_E_INVALID if the sequence handle is empty, params.ctx is - * NULL, or index is negative. - */ -int oaknode_sequence_set_video_params(OakNodeSequence sequence, int index, - OakVideoParams params); - -/** - * @brief Audio parameters at `index` as a NEW handle owned by the caller - * (release with oakcore_audioparams_free()). - */ -int oaknode_sequence_get_audio_params(OakNodeSequence sequence, int index, - OakAudioParams **out); - -/** - * @brief Replace the audio parameters at `index` with a copy of `params`. - */ -int oaknode_sequence_set_audio_params(OakNodeSequence sequence, int index, - const OakAudioParams *params); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_NODE_SEQUENCE_H diff --git a/crates/oakengine.bk/include/node/serializer.h b/crates/oakengine.bk/include/node/serializer.h deleted file mode 100644 index 4b5782094..000000000 --- a/crates/oakengine.bk/include/node/serializer.h +++ /dev/null @@ -1,310 +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 . - -***/ - -#ifndef OAK_EDITOR_NODE_SERIALIZER_H -#define OAK_EDITOR_NODE_SERIALIZER_H - -#include - -#include "node/error.h" -#include "node/node.h" -#include "node/project.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file serializer.h - * @brief C ABI for olive::ProjectSerializer (oaknode), in-memory form - * - * Clipboard copy/paste and node-graph XML round trips without touching the - * filesystem: "copy" is oaknode_serializer_save_to_xml() (serialize a - * SaveData to an XML string), "paste" is oaknode_serializer_load_from_xml() - * (parse an XML string into a project, exposing the resulting LoadData). - * System-clipboard integration and on-disk .ove save/load live in the - * facade / oakstorage layers (M9/M10), not here. - * - * oaknode_serializer_initialize() must be called before any save/load; it - * registers the versioned serializers and the node factory the loaders use - * to instantiate nodes by id. - */ - -/** @brief Load type: a whole project. */ -#define OAKNODE_SERIALIZER_LOAD_PROJECT 0 -/** @brief Load type: only nodes (clipboard node-graph paste). */ -#define OAKNODE_SERIALIZER_LOAD_ONLY_NODES 1 -/** @brief Load type: only clips (timeline family). */ -#define OAKNODE_SERIALIZER_LOAD_ONLY_CLIPS 2 -/** @brief Load type: only markers (timeline family). */ -#define OAKNODE_SERIALIZER_LOAD_ONLY_MARKERS 3 -/** @brief Load type: only keyframes (keyframe family). */ -#define OAKNODE_SERIALIZER_LOAD_ONLY_KEYFRAMES 4 - -/** @brief Serializer result code: success. */ -#define OAKNODE_SERIALIZER_OK 0 -/** @brief Serializer result code: data written by a too-old format. */ -#define OAKNODE_SERIALIZER_TOO_OLD 1 -/** @brief Serializer result code: data written by a too-new format. */ -#define OAKNODE_SERIALIZER_TOO_NEW 2 -/** @brief Serializer result code: unrecognizable format version. */ -#define OAKNODE_SERIALIZER_UNKNOWN_VERSION 3 -/** @brief Serializer result code: file I/O error (unused in-memory). */ -#define OAKNODE_SERIALIZER_FILE_ERROR 4 -/** @brief Serializer result code: XML parse error. */ -#define OAKNODE_SERIALIZER_XML_ERROR 5 -/** @brief Serializer result code: overwrite error (unused in-memory). */ -#define OAKNODE_SERIALIZER_OVERWRITE_ERROR 6 -/** @brief Serializer result code: no data to load. */ -#define OAKNODE_SERIALIZER_NO_DATA 7 - -/** - * @brief Reference-counted save descriptor (wraps - * olive::ProjectSerializer::SaveData). - * - * oaknode_serializer_savedata_create() returns a handle whose object has - * reference count 1; release it with oaknode_serializer_savedata_free(). - */ -typedef struct OakNodeSerializerSaveData { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKNODE_ABI_VERSION. */ -} OakNodeSerializerSaveData; - -/** - * @brief Reference-counted load result (wraps - * olive::ProjectSerializer::LoadData). - * - * The handle returned through oaknode_serializer_load_from_xml() has - * reference count 1; release it with oaknode_serializer_loaddata_free(). - * Node handles obtained from it are borrowed from the target project. - */ -typedef struct OakNodeSerializerLoadData { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKNODE_ABI_VERSION. */ -} OakNodeSerializerLoadData; - -/** - * @brief Register the versioned serializers and initialize the node factory. - * Idempotent. Must be called before any save/load. - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_serializer_initialize(void); - -/** - * @brief Tear down the serializers and the node factory registered by - * oaknode_serializer_initialize(). Safe to call when not initialized. - */ -void oaknode_serializer_shutdown(void); - -/** - * @brief Create a save descriptor. - * - * @param load_type One of OAKNODE_SERIALIZER_LOAD_*; use - * OAKNODE_SERIALIZER_LOAD_ONLY_NODES for clipboard-style node copies. - * @param project Context project (borrowed), may be an empty handle for - * load types that do not require it. - * - * @return Save-data handle with reference count 1 (release with - * oaknode_serializer_savedata_free()); ctx is NULL on failure. - */ -OakNodeSerializerSaveData oaknode_serializer_savedata_create( - int load_type, OakNodeProject project); - -/** - * @brief Release the caller's reference to the save descriptor and null - * out the handle. NULL and empty handles are a no-op; the object is - * destroyed when its reference count reaches zero. - */ -void oaknode_serializer_savedata_free(OakNodeSerializerSaveData *save_data); - -/** - * @brief Restrict serialization to the given nodes - * (SaveData::set_only_serialize_nodes()). `nodes` is an array of `count` - * borrowed node handles. - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_serializer_savedata_set_nodes( - OakNodeSerializerSaveData save_data, const OakNodeNode *nodes, int count); - -/** - * @brief Attach a free-form (key, value) property to a node in the - * serialized output (SaveData::set_properties()); used for graph positions - * and clip metadata. Replaces the value if the (node, key) pair exists. - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_serializer_savedata_set_property( - OakNodeSerializerSaveData save_data, OakNodeNode node, const char *key, - const char *value); - -/** - * @brief Serialize to an in-memory XML document ("copy"). Two-stage string - * getter: pass buf == NULL to query the size. - * - * @return Required buffer size in bytes including the NUL, or a negative - * OAKNODE_E_* error code (OAKNODE_E_STATE if the serializers have - * not been initialized). - */ -int oaknode_serializer_save_to_xml(OakNodeSerializerSaveData save_data, - char *buf, int buf_size); - -/** - * @brief Parse an in-memory XML document into `project` ("paste"). - * - * @param project Target project (borrowed), may be an empty handle for - * load types that do not attach nodes to a project. - * @param xml Complete XML document text. Must not be NULL. - * @param load_type One of OAKNODE_SERIALIZER_LOAD_*. - * @param out_result Receives one of the OAKNODE_SERIALIZER_* result codes. - * Must not be NULL. - * @param out_load_data Receives the load result on OAKNODE_SERIALIZER_OK - * (reference count 1, release with oaknode_serializer_loaddata_free(); - * may be NULL if the caller does not need it; receives an empty - * handle on failure). - * @param details_buf Optional human-readable error detail buffer - * (two-stage convention is NOT used; truncation is silent). May be - * NULL. - * @param details_buf_size Size of details_buf. - * - * @return OAKNODE_OK if the call itself succeeded (inspect *out_result for - * the serializer outcome), or a negative OAKNODE_E_* error code. - */ -int oaknode_serializer_load_from_xml(OakNodeProject project, const char *xml, - int load_type, int *out_result, - OakNodeSerializerLoadData *out_load_data, - char *details_buf, int details_buf_size); - -/** - * @brief Release the caller's reference to the load result and null out - * the handle. NULL and empty handles are a no-op. - * - * Does not delete the loaded nodes: they are newly created objects owned by - * the CALLER until adopted into a project with oaknode_project_add_node() - * (or attached under a folder); otherwise they leak. - */ -void oaknode_serializer_loaddata_free(OakNodeSerializerLoadData *load_data); - -/** - * @brief Number of nodes created by the load. Negative OAKNODE_E_* code on - * an empty handle. - */ -int oaknode_serializer_loaddata_node_count( - OakNodeSerializerLoadData load_data); - -/** - * @brief Borrowed handle of the loaded node at `index`, or an empty handle - * when out of range. - */ -OakNodeNode oaknode_serializer_loaddata_node_at( - OakNodeSerializerLoadData load_data, int index); - -/** - * @brief Look up a serialized property attached to a loaded node. - * Two-stage string getter. - * - * @return Required buffer size in bytes including the NUL, - * OAKNODE_E_NOT_FOUND if the (node, key) pair is absent, or another - * negative OAKNODE_E_* error code. - */ -int oaknode_serializer_loaddata_get_property( - OakNodeSerializerLoadData load_data, OakNodeNode node, const char *key, - char *buf, int buf_size); - -/** - * @brief Number of promised (deferred) connections in the load result. - * Negative OAKNODE_E_* code on an empty handle. - */ -int oaknode_serializer_loaddata_connection_count( - OakNodeSerializerLoadData load_data); - -/** - * @brief Read the promised connection at `index`. - * - * All output parameters except the input-id buffer are required; - * `input_id_buf` follows the two-stage string convention inside a - * fixed call: pass NULL/0 to skip copying the id. - * - * @param out_output_node Receives the output (source) node (borrowed). - * @param out_input_node Receives the input (destination) node (borrowed). - * @param input_id_buf Receives the input id string, may be NULL. - * @param input_id_buf_size Size of input_id_buf. - * @param out_element Receives the input element index. - * - * @return OAKNODE_OK, OAKNODE_E_NOT_FOUND when out of range, or another - * negative OAKNODE_E_* error code. - */ -int oaknode_serializer_loaddata_connection_at( - OakNodeSerializerLoadData load_data, int index, - OakNodeNode *out_output_node, OakNodeNode *out_input_node, - char *input_id_buf, int input_id_buf_size, int *out_element); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_NODE_SERIALIZER_H - -/** - * @brief Result codes for file-level save/load (mirror - * ProjectSerializer::ResultCode; pinned by test). - */ -enum OakNodeSerializerResultCode { - OAKNODE_SERIALIZER_RESULT_SUCCESS = 0, - OAKNODE_SERIALIZER_RESULT_PROJECT_TOO_OLD = 1, - OAKNODE_SERIALIZER_RESULT_PROJECT_TOO_NEW = 2, - OAKNODE_SERIALIZER_RESULT_UNKNOWN_VERSION = 3, - OAKNODE_SERIALIZER_RESULT_FILE_ERROR = 4, - OAKNODE_SERIALIZER_RESULT_XML_ERROR = 5, - OAKNODE_SERIALIZER_RESULT_OVERWRITE_ERROR = 6, - OAKNODE_SERIALIZER_RESULT_NO_DATA = 7 -}; - -/** - * @brief Save a project to a file (ProjectSerializer::save(), project - * type, optional OVEC compression). Layout data is not serialized - * through this API (app-layer concern, see oakstorage/M10). - * - * @param out_code Receives an OakNodeSerializerResultCode (may be NULL). - * @param details Optional two-stage buffer for the result details - * string (e.g. the fallback filename on overwrite errors). - * @return OAKNODE_OK when the result code is - * OAKNODE_SERIALIZER_RESULT_SUCCESS, OAKNODE_E_FAILED otherwise - * (details in out_code/details), OAKNODE_E_INVALID for empty - * handles/NULL args. - */ -int oaknode_serializer_save_to_file(OakNodeProject project, - const char *filename, int use_compression, int *out_code, - char *details, int details_size); - -/** - * @brief Load a project from a file into `project` - * (ProjectSerializer::load(), project type). - * - * Same return/out-param convention as oaknode_serializer_save_to_file(). - */ -int oaknode_serializer_load_from_file(OakNodeProject project, - const char *filename, int *out_code, char *details, - int details_size); diff --git a/crates/oakengine.bk/include/node/track.h b/crates/oakengine.bk/include/node/track.h deleted file mode 100644 index 7e0de834d..000000000 --- a/crates/oakengine.bk/include/node/track.h +++ /dev/null @@ -1,355 +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 . - -***/ - -#ifndef OAK_EDITOR_NODE_TRACK_H -#define OAK_EDITOR_NODE_TRACK_H - -#ifndef __cplusplus -#include -#endif - -#include - -#include "node/error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Reference-counted handle to a track (olive::Track). - * - * The object never leaves the library that created it; every external - * reference is one of these handles. Semantics are shared_ptr-like: - * oaknode_track_create() returns a handle with count 1, addref(ctx) - * takes another reference, release(ctx) drops one and the library - * destroys the object when the count reaches zero. - * - * Adding a track to a track list (oaknode_tracklist_add_track()) - * transfers ownership to the graph; handles obtained from accessors - * (sequence/track-list lookups) are borrowed and never destroy the - * underlying object. - */ -typedef struct OakNodeTrack { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKNODE_ABI_VERSION. */ -} OakNodeTrack; - -/** - * @brief Reference-counted handle to a per-type track container - * (olive::TrackList). - * - * Always borrowed from oaknode_sequence_get_track_list(); releasing the - * handle never destroys the list, which stays owned by its sequence. - */ -typedef struct OakNodeTrackList { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKNODE_ABI_VERSION. */ -} OakNodeTrackList; - -/** - * @brief Reference-counted handle to a block (olive::Block), see - * node/block.h. - */ -typedef struct OakNodeBlock OakNodeBlock; - -/** - * @brief Reference-counted handle to a sequence (olive::Sequence), see - * node/sequence.h. - */ -typedef struct OakNodeSequence OakNodeSequence; - -/** - * @brief Track types, matching olive::Track::Type. - */ -enum OakNodeTrackType { - OAKNODE_TRACK_TYPE_NONE = -1, - OAKNODE_TRACK_TYPE_VIDEO = 0, - OAKNODE_TRACK_TYPE_AUDIO = 1, - OAKNODE_TRACK_TYPE_SUBTITLE = 2, - OAKNODE_TRACK_TYPE_COUNT = 3 -}; - -/* Re-declared here so track.h is self-contained; see node/node.h. */ -typedef struct OakNodeNode OakNodeNode; - -/** - * @brief Borrowed cast from a track handle to its node handle. - * Empty handle for an empty handle. - */ -OakNodeNode oaknode_track_as_node(OakNodeTrack track); - -/* ---------------------------------------------------------------- Track */ - -/** - * @brief Create a track of the given type (OakNodeTrackType value). - * - * The caller owns the track until it is added to a track list; a track - * that was never added must be released with oaknode_track_free(). - * - * @return Track handle with reference count 1; ctx is NULL on invalid - * type / allocation failure. - */ -OakNodeTrack oaknode_track_create(int type); - -/** - * @brief Release one reference to a track handle. - * - * Destroys the track when the reference count reaches zero. NULL handle - * or NULL ctx is a no-op; clears `track->ctx` after releasing. - * - * The track must have been removed from its track list first. - */ -void oaknode_track_free(OakNodeTrack *track); - -/** - * @brief Track type (OakNodeTrackType values). - * - * @return OAKNODE_OK or OAKNODE_E_INVALID. - */ -int oaknode_track_get_type(OakNodeTrack track, int *type); -int oaknode_track_set_type(OakNodeTrack track, int type); - -/** - * @brief Track height in internal units (olive::Track::get/set_track_height). - */ -int oaknode_track_get_height(OakNodeTrack track, double *height); -int oaknode_track_set_height(OakNodeTrack track, double height); - -/** - * @brief Track height in pixels (converted through the default font height). - */ -int oaknode_track_get_height_in_pixels(OakNodeTrack track, int *height); -int oaknode_track_set_height_in_pixels(OakNodeTrack track, int height); - -/** - * @brief Default / minimum track heights in pixels (static). - */ -int oaknode_track_get_default_height_in_pixels(void); -int oaknode_track_get_minimum_height_in_pixels(void); - -/** - * @brief Index of the track inside its track list. - */ -int oaknode_track_get_index(OakNodeTrack track, int *index); -int oaknode_track_set_index(OakNodeTrack track, int index); - -/** - * @brief Mute / lock flags. - */ -int oaknode_track_get_muted(OakNodeTrack track, int *muted); -int oaknode_track_set_muted(OakNodeTrack track, int muted); -int oaknode_track_get_locked(OakNodeTrack track, int *locked); -int oaknode_track_set_locked(OakNodeTrack track, int locked); - -/** - * @brief Track reference as a (type, index) pair (olive::Track::Reference). - */ -int oaknode_track_get_reference(OakNodeTrack track, int *type, int *index); - -/** - * @brief Total length of the track (end of the last block). - */ -int oaknode_track_get_length(OakNodeTrack track, int *numerator, - int *denominator); - -/** - * @brief Owning sequence as a borrowed handle (empty when trackless). - */ -int oaknode_track_get_sequence(OakNodeTrack track, OakNodeSequence *out); - -/* ------------------------------------------------------- Track blocks */ - -/** - * @brief Number of blocks on the track. - */ -int oaknode_track_get_block_count(OakNodeTrack track, int *count); - -/** - * @brief Borrowed handle to the block at `index`. - * - * @return OAKNODE_OK, OAKNODE_E_INVALID or OAKNODE_E_NOT_FOUND. - */ -int oaknode_track_get_block_at(OakNodeTrack track, int index, - OakNodeBlock *out); - -/** - * @brief Append/prepend/insert primitives (olive::Track::*_block). - * - * The track takes over graph membership of the block; the block must have - * a valid length before insertion. - * - * @return OAKNODE_OK or OAKNODE_E_INVALID. - */ -int oaknode_track_append_block(OakNodeTrack track, OakNodeBlock block); -int oaknode_track_prepend_block(OakNodeTrack track, OakNodeBlock block); -int oaknode_track_insert_block_at_index(OakNodeTrack track, - OakNodeBlock block, int index); -int oaknode_track_insert_block_after(OakNodeTrack track, OakNodeBlock block, - OakNodeBlock before); -int oaknode_track_insert_block_before(OakNodeTrack track, OakNodeBlock block, - OakNodeBlock after); - -/** - * @brief Remove `block` and shift all subsequent blocks earlier - * (olive::Track::ripple_remove_block). The block is NOT deleted; ownership - * returns to the caller. - */ -int oaknode_track_ripple_remove_block(OakNodeTrack track, OakNodeBlock block); - -/** - * @brief Replace `old_block` with `new_block`; both must have equal lengths. - */ -int oaknode_track_replace_block(OakNodeTrack track, OakNodeBlock old_block, - OakNodeBlock new_block); - -/** - * @brief Index of `block` in the track's block array, or OAKNODE_E_NOT_FOUND. - */ -int oaknode_track_get_block_index(OakNodeTrack track, OakNodeBlock block, - int *index); - -/** - * @brief Block strictly containing `time` (in < time < out), or - * OAKNODE_E_NOT_FOUND. - */ -int oaknode_track_get_block_containing_time(OakNodeTrack track, int numerator, - int denominator, - OakNodeBlock *out); - -/** - * @brief Block visible at `time` (in <= time < out), or OAKNODE_E_NOT_FOUND. - */ -int oaknode_track_get_visible_block_at_time(OakNodeTrack track, int numerator, - int denominator, - OakNodeBlock *out); - -/** - * @brief Whether the [in, out) range holds no block or only a gap - * (olive::Track::is_range_free). `is_free` receives 1/0. - */ -int oaknode_track_is_range_free(OakNodeTrack track, int in_num, int in_den, - int out_num, int out_den, int *is_free); - -/* ------------------------------------------------------------ TrackList */ - -/** - * @brief Track list type (OakNodeTrackType values). - */ -/** - * @brief Nearest block lookups (Track::nearest_block_before_or_at / - * nearest_block_after_or_at). *out is a borrowed handle (empty when none). - */ -int oaknode_track_get_nearest_block_before_or_at(OakNodeTrack track, - int numerator, int denominator, OakNodeBlock *out); -int oaknode_track_get_nearest_block_after_or_at(OakNodeTrack track, - int numerator, int denominator, OakNodeBlock *out); - -/** - * @brief Borrowed sequence owning this track list. - */ -int oaknode_tracklist_get_sequence(OakNodeTrackList list, - OakNodeSequence *out); - -/** - * @brief The list's track input id on the parent sequence - * (e.g. "track_in_0"). Two-stage string getter. - */ -int oaknode_tracklist_get_track_input_id(OakNodeTrackList list, - char *buf, int buf_size); - -/** - * @brief Live input-array append/remove on the parent sequence for this - * list's track input (TrackList::array_append/array_remove_last()). - */ -int oaknode_tracklist_array_append(OakNodeTrackList list); -int oaknode_tracklist_array_remove_last(OakNodeTrackList list); - -/** - * @brief Map a cached track index to the input-array element index - * (TrackList::get_array_index_from_cache_index()). - */ -int oaknode_tracklist_get_array_index_from_cache_index( - OakNodeTrackList list, int cache_index, int *out_index); - -int oaknode_tracklist_get_type(OakNodeTrackList list, int *type); - -/** - * @brief Number of connected tracks. - */ -int oaknode_tracklist_get_track_count(OakNodeTrackList list, int *count); - -/** - * @brief Borrowed handle to the track at `index`. - * - * @return OAKNODE_OK, OAKNODE_E_INVALID or OAKNODE_E_NOT_FOUND. - */ -int oaknode_tracklist_get_track_at(OakNodeTrackList list, int index, - OakNodeTrack *out); - -/** - * @brief Combined length of the longest track in the list. - */ -int oaknode_tracklist_get_total_length(OakNodeTrackList list, int *numerator, - int *denominator); - -/** - * @brief Size of the underlying input array (>= track count; may contain - * disconnected slots). - */ -int oaknode_tracklist_get_array_size(OakNodeTrackList list, int *size); - -/** - * @brief Add `track` to the list (non-undoable primitive). - * - * Mirrors the graph steps of TimelineAddTrackCommand::redo() minus the - * auto-merge: the track is parented to the list's graph (when any), - * inherits the previous track's height, a new array slot is appended and - * the track is connected to it. The sequence's flat track cache and - * lengths are refreshed before returning. - * - * The list takes ownership of the track on success; the caller's handle - * becomes a non-owning reference. - * - * @return OAKNODE_OK or OAKNODE_E_INVALID. - */ -int oaknode_tracklist_add_track(OakNodeTrackList list, OakNodeTrack track); - -/** - * @brief Remove `track` from the list (non-undoable primitive). - * - * Disconnects the track from its array slot and removes the slot - * (Node::input_array_remove). The track is NOT deleted; ownership returns - * to the caller. - * - * @return OAKNODE_OK, OAKNODE_E_INVALID or OAKNODE_E_NOT_FOUND. - */ -int oaknode_tracklist_remove_track(OakNodeTrackList list, - OakNodeTrack track); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_NODE_TRACK_H diff --git a/crates/oakengine.bk/include/node/traverser.h b/crates/oakengine.bk/include/node/traverser.h deleted file mode 100644 index 53f9a7624..000000000 --- a/crates/oakengine.bk/include/node/traverser.h +++ /dev/null @@ -1,154 +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 . - -***/ - -#ifndef OAK_EDITOR_NODE_TRAVERSER_H -#define OAK_EDITOR_NODE_TRAVERSER_H - -#include - -#include "node/error.h" -#include "node/node.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file traverser.h - * @brief C ABI for olive::NodeTraverser (src/node/src/traverser.h), - * limited to database generation: generating the value database of a node - * over a time range and enumerating its rows. - * - * The base NodeTraverser resolves no render jobs (textures/samples stay - * dummy); only value-producing nodes are meaningful here. - */ - -/** - * @brief Reference-counted handle to a traverser (olive::NodeTraverser). - * - * Semantics are shared_ptr-like: oaknode_traverser_init() returns a - * handle with count 1, addref(ctx) takes another reference, release(ctx) - * drops one and the library destroys the object when the count reaches - * zero. - */ -typedef struct OakNodeTraverser { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKNODE_ABI_VERSION. */ -} OakNodeTraverser; - -/** - * @brief Reference-counted handle to an owned copy of an - * olive::NodeValueDatabase. Same reference-counting rules as - * OakNodeTraverser; release with oaknode_traverser_database_free(). - */ -typedef struct OakNodeValueDatabase { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKNODE_ABI_VERSION. */ -} OakNodeValueDatabase; - -/** - * @brief Create a traverser. - * - * @return Traverser handle with count 1; ctx is NULL on allocation - * failure. - */ -OakNodeTraverser oaknode_traverser_init(void); - -/** - * @brief Release one reference to a traverser handle. - * - * Convenience wrapper around handle.release(handle.ctx): destroys the - * traverser when the count reaches zero. NULL handle or NULL ctx is a - * no-op; clears `traverser->ctx` after releasing. - */ -void oaknode_traverser_free(OakNodeTraverser *traverser); - -/** - * @brief Generate the value database of `node` over the time range - * [`in_num`/`in_den`, `out_num`/`out_den`) seconds - * (NodeTraverser::generate_database()). - * - * `out_db` receives an owned database handle with count 1. - * - * @return OAKNODE_OK or a negative OAKNODE_E_* error code. - */ -int oaknode_traverser_generate_database(OakNodeTraverser traverser, - OakNodeNode node, int64_t in_num, - int64_t in_den, int64_t out_num, - int64_t out_den, - OakNodeValueDatabase *out_db); - -/** - * @brief Release one reference to a database handle. - * - * Convenience wrapper around handle.release(handle.ctx): destroys the - * database when the count reaches zero. NULL handle or NULL ctx is a - * no-op; clears `db->ctx` after releasing. - */ -void oaknode_traverser_database_free(OakNodeValueDatabase *db); - -/** - * @brief Number of rows (input tables) in the database. - */ -int oaknode_traverser_database_row_count(OakNodeValueDatabase db, - int *out_count); - -/** - * @brief The input id (key) of the row at `index`. Two-stage getter; - * OAKNODE_E_NOT_FOUND for an out-of-range index. - */ -int oaknode_traverser_database_row_key_at(OakNodeValueDatabase db, - int index, char *buf, int buf_size); - -/** - * @brief Number of values in the row named `key`. - * OAKNODE_E_NOT_FOUND for an unknown key. - */ -int oaknode_traverser_database_row_value_count(OakNodeValueDatabase db, - const char *key, - int *out_count); - -/** - * @brief Read the value at `index` of row `key` mapped into `out`. - * Values without a POD representation fail with OAKNODE_E_FAILED; - * OAKNODE_E_NOT_FOUND for an unknown key or out-of-range index. - */ -int oaknode_traverser_database_value_at(OakNodeValueDatabase db, - const char *key, int index, - oaknode_value *out); - -/** - * @brief Read the value at `index` of row `key` as a string - * (NodeValue::value_to_string()). Two-stage getter; - * OAKNODE_E_NOT_FOUND for an unknown key or out-of-range index. - */ -int oaknode_traverser_database_value_string_at(OakNodeValueDatabase db, - const char *key, int index, - char *buf, int buf_size); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_NODE_TRAVERSER_H diff --git a/crates/oakengine.bk/include/plugin/error.h b/crates/oakengine.bk/include/plugin/error.h deleted file mode 100644 index 6fe36358f..000000000 --- a/crates/oakengine.bk/include/plugin/error.h +++ /dev/null @@ -1,40 +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 . - -***/ - -#ifndef OAK_EDITOR_PLUGIN_ERROR_H -#define OAK_EDITOR_PLUGIN_ERROR_H - -#include - -/** - * @brief Status and error codes shared by all oakplugin C API families. - */ -#define OAKPLUGIN_OK 0 /**< Success. */ -#define OAKPLUGIN_E_INVALID (-90001) /**< NULL handle or invalid argument. */ -#define OAKPLUGIN_E_STATE (-90002) /**< Call not valid in the current state. */ -#define OAKPLUGIN_E_FAILED (-90003) /**< The underlying operation failed. */ -#define OAKPLUGIN_E_NOT_FOUND (-90004) /**< Entry not found. */ -#define OAKPLUGIN_E_NOMEM (-90005) /**< Allocation failed. */ -#define OAKPLUGIN_E_CANCELLED (-90006) /**< The operation was cancelled. */ - -/** @brief ABI version stamped into every oakplugin handle. */ -#define OAKPLUGIN_ABI_VERSION 1 - -#endif //OAK_EDITOR_PLUGIN_ERROR_H diff --git a/crates/oakengine.bk/include/plugin/host.h b/crates/oakengine.bk/include/plugin/host.h deleted file mode 100644 index 644fc166f..000000000 --- a/crates/oakengine.bk/include/plugin/host.h +++ /dev/null @@ -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 . - -***/ - -#ifndef OAK_EDITOR_PLUGIN_HOST_H -#define OAK_EDITOR_PLUGIN_HOST_H - -#include "plugin/error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Initialize the OFX host (olive::plugin::load_plugins() with the - * default search paths). Idempotent. - */ -int oakplugin_host_init(void); - -/** @brief Shut the host down (persistent messages cleared). */ -void oakplugin_host_shutdown(void); - -/** @brief Scan additional bundle directories. */ -int oakplugin_host_scan(const char *const *bundle_dirs, int dir_count); - -/** @brief Number of discovered plugins (>= 0), or a negative error. */ -int oakplugin_host_plugin_count(void); - -/** @brief Plugin identifier at index (two-stage string getter). */ -int oakplugin_host_plugin_id_at(int index, char *buf, int buf_size); - -/** @brief Plugin label for an identifier (two-stage; currently the - * identifier itself). OAKPLUGIN_E_NOT_FOUND for unknown ids. */ -int oakplugin_host_plugin_label(const char *plugin_id, char *buf, - int buf_size); - -/** - * @brief UI message handler for OFX host messages (question replies use - * OAKPLUGIN_MESSAGE_ANSWER_YES/NO). Without a handler, messages - * are logged and questions get "no". - */ -#define OAKPLUGIN_MESSAGE_ANSWER_NO 0 -#define OAKPLUGIN_MESSAGE_ANSWER_YES 1 -typedef int (*oakplugin_message_fn)(const char *type, const char *message, - void *userdata); -void oakplugin_host_set_message_handler(oakplugin_message_fn fn, - void *userdata); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_PLUGIN_HOST_H diff --git a/crates/oakengine.bk/include/plugin/instance.h b/crates/oakengine.bk/include/plugin/instance.h deleted file mode 100644 index 5a25edd63..000000000 --- a/crates/oakengine.bk/include/plugin/instance.h +++ /dev/null @@ -1,171 +0,0 @@ -/*** - - Oak Video Editor - Non-Linear Video Editor - Copyright (C) 2026 Oak Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#ifndef OAK_EDITOR_PLUGIN_INSTANCE_H -#define OAK_EDITOR_PLUGIN_INSTANCE_H - -#include - -#include "node/node.h" -#include "plugin/error.h" -#include "render/renderer.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Reference-counted handle to an OFX plugin instance - * (olive::plugin::OlivePluginInstance). - * - * Ownership/count semantics follow include/common/handle.h: create - * returns count 1, addref/release adjust it, release destroys at zero. - */ -typedef struct OakPluginInstance { - void *ctx; - void (*addref)(void *ctx); - void (*release)(void *ctx); - uint32_t abi_version; /**< OAKPLUGIN_ABI_VERSION. */ -} OakPluginInstance; - -/** - * @brief Create an instance of a discovered plugin (filter context). - * Returns an empty handle (ctx == NULL) for unknown ids/failure. - */ -OakPluginInstance oakplugin_instance_create(const char *plugin_id); - -/** @brief Release one reference. NULL/empty no-op; clears ctx. */ -void oakplugin_instance_free(OakPluginInstance *instance); - -/** - * @brief Set/get a parameter as an oaknode_value POD (type rules from - * node/node.h). String-typed params use - * oakplugin_instance_set_param_string()/get_param_string(). - */ -int oakplugin_instance_set_param(OakPluginInstance instance, - const char *param_id, - const oaknode_value *value); -int oakplugin_instance_get_param(OakPluginInstance instance, - const char *param_id, oaknode_value *out); -int oakplugin_instance_set_param_string(OakPluginInstance instance, - const char *param_id, - const char *value); -int oakplugin_instance_get_param_string(OakPluginInstance instance, - const char *param_id, char *buf, - int buf_size); - -/** - * @brief Render one frame through the instance (renderAction). - * - * `src` may be an empty handle for generator plugins. Textures stay - * owned by the caller (borrowed for the call). - */ -int oakplugin_instance_render(OakPluginInstance instance, - OakRenderTexture dst, OakRenderTexture src, - double time_seconds); - -/** - * @brief Progress callback for long renders (async return channel, - * 01 §4 exception). Return non-zero to abort processing. - */ -typedef int (*oakplugin_progress_fn)(double progress, void *userdata); -int oakplugin_instance_set_progress_cb(OakPluginInstance instance, - oakplugin_progress_fn fn, - void *userdata); - -/** @brief Cancel any in-progress render/progress reporting. */ -int oakplugin_instance_cancel(OakPluginInstance instance); - -/** @brief Alive-count for leak assertions in tests. */ -int oakplugin_debug_alive_count(void); - -/* - * M11 §4(GL 路径 + render 驱动收编)新增声明。既有签名不变。 - * - * oakrender 的 PluginJob 经本组入口把整帧渲染流程(RoI/RoD、 - * 多输入收集、isIdentity 短路、参数覆盖、CPU/GL 渲染与输出装配) - * 委托给 oakplugin 的 render 驱动(Rust 侧 render_driver 模块, - * 语义对照 src/render/src/plugin/pluginrenderer.cpp)。 - */ - -/** @brief 一帧渲染任务的参数覆盖条目(参数名 → oaknode_value POD; - * 字符串参数走 oakplugin_instance_set_param_string)。 */ -typedef struct oakplugin_job_value { - const char *key; - oaknode_value value; -} oakplugin_job_value; - -/** @brief 一帧渲染任务的输入 clip 纹理条目。纹理为借用句柄 - * (job 内有效)。 */ -typedef struct oakplugin_job_texture { - const char *clip; - OakRenderTexture texture; -} oakplugin_job_texture; - -/** - * @brief beginSequenceRender 括号。oakrender 对同一实例的一批帧先 - * begin 后 end,中间逐帧 oakplugin_instance_render_job - * (OFX:render action 由 begin/end sequence render 括号包围)。 - * `interactive` 为信息性标记(Phase 2 不传入 action)。 - */ -int oakplugin_instance_render_begin_sequence(OakPluginInstance instance, - double start_time, - double end_time, - int interactive); - -/** @brief endSequenceRender 括号(与 render_begin_sequence 配对)。 */ -int oakplugin_instance_render_end_sequence(OakPluginInstance instance, - double start_time, - double end_time, - int interactive); - -/** - * @brief 一帧渲染的单一 C ABI 调用(PluginJob 的载体)。 - * - * @param dst 目标纹理(oakrender 创建)。GL 模式下调用方须先把 - * dst 附着为渲染器输出目标并保持 GL 上下文 current - * (OFX "OpenGL Current Context" 规则;等价 C++ - * PluginRenderer::attach_output_texture)。 - * @param src 主输入纹理(effect_input_id / SimpleSource;可空句柄)。 - * @param effect_input_id job.src 落点的 clip 名(可 NULL)。 - * @param inputs / input_count 其余输入 clip 的纹理表。 - * @param values / value_count 参数覆盖表。 - * @param renderer GL 渲染器(空句柄 → CPU 路径)。 - * @param clear_destination / interactive 信息性标记(Phase 2, - * render 驱动暂不处理;上层渲染器负责目标清空)。 - */ -int oakplugin_instance_render_job(OakPluginInstance instance, - OakRenderTexture dst, - double time_seconds, - int clear_destination, - int interactive, - const char *effect_input_id, - OakRenderTexture src, - const oakplugin_job_texture *inputs, - int input_count, - const oakplugin_job_value *values, - int value_count, - OakRenderRenderer renderer); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_PLUGIN_INSTANCE_H diff --git a/crates/oakengine.bk/include/render/cache.h b/crates/oakengine.bk/include/render/cache.h deleted file mode 100644 index 402ce317c..000000000 --- a/crates/oakengine.bk/include/render/cache.h +++ /dev/null @@ -1,315 +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 . - -***/ - -#ifndef OAK_EDITOR_RENDER_CACHE_H -#define OAK_EDITOR_RENDER_CACHE_H - -#include - -// Same-dir quoted includes: inside this build the engine-style spelling -// "render/renderer.h" resolves to the transition bridge headers, so the -// public headers reference each other relative to their own directory. -#include "error.h" -#include "renderer.h" /* OakCodecFrame */ -#include "node/node.h" /* OakNodeNode */ - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file cache.h - * @brief C ABI for the oakrender playback/frame-hash caches - * (olive::PlaybackCache / olive::FrameHashCache), M7 §2.2. - * - * An OakRenderCache is a by-value reference-counted handle (shared_ptr - * semantics, see oakcommon's common/handle.h) boxing an - * olive::FrameHashCache (created without a parent node). Handles from - * oakrender_cache_create() are owned by the caller (reference count 1) - * and must be released with oakrender_cache_free(); handles from - * oakrender_cache_wrap_borrowed() are borrowed (release only frees the - * box). - * - * All timestamps are int64 frame numbers in the cache's timebase (see - * oakrender_cache_set_timebase()); a cache without a valid timebase - * treats timestamps as whole seconds. - * - * No cache events cross the boundary (M7 §2.2, 2026-08 revision): - * invalidate/validate are triggered by and known to the caller; the - * facade re-emits notifications after the triggering command. - */ -typedef struct OakRenderCache { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKRENDER_ABI_VERSION. */ -} OakRenderCache; - -/** - * @brief Create a detached frame hash cache (no parent node, no - * timebase). Owned by the caller. - * - * @return Cache handle with reference count 1; ctx is NULL on - * allocation failure. - */ -OakRenderCache oakrender_cache_create(void); - -/** - * @brief Release one reference to a cache created by - * oakrender_cache_create(). Convenience wrapper around - * cache->release(cache->ctx). NULL / empty-handle no-op; clears - * cache->ctx after releasing. - */ -void oakrender_cache_free(OakRenderCache *cache); - -/** - * @brief Borrowed handle wrapping a native frame cache pointer obtained - * through oaknode (oaknode_node_get_video_frame_cache()). - * - * The cache itself stays owned by its node: release() on this handle - * only frees the box. Empty handle (ctx == NULL) for a NULL native - * pointer. - */ -OakRenderCache oakrender_cache_wrap_borrowed(void *native_cache); - -/** - * @brief Cache flavours owned by a node - * (olive::Node's video/thumbnail/audio/waveform caches). - */ -enum OakRenderCacheKind { - OAKRENDER_CACHE_VIDEO_FRAME = 0, /**< olive::FrameHashCache */ - OAKRENDER_CACHE_THUMBNAIL = 1, /**< olive::ThumbnailCache */ - OAKRENDER_CACHE_AUDIO_PLAYBACK = 2, /**< olive::AudioPlaybackCache */ - OAKRENDER_CACHE_AUDIO_WAVEFORM = 3 /**< olive::AudioWaveformCache */ -}; - -/** - * @brief Create a cache of the given kind with a parent node (the - * native back-pointer stays inside oakrender; it is used for - * project cache-path resolution and job bookkeeping only). - * - * Owned by the caller (reference count 1); release with - * oakrender_cache_free(). Empty handle for an empty parent handle, an - * unknown kind, or on allocation failure. - */ -OakRenderCache oakrender_cache_create_for_node(OakNodeNode parent, - int kind); - -/** - * @brief Cache UUID as canonical text, two-stage - * (PlaybackCache::get_uuid()). - * - * @return Required buffer size in bytes (including NUL), or a negative - * OAKRENDER_E_* code for an empty cache. - */ -int oakrender_cache_get_uuid(OakRenderCache cache, char *buf, - int buf_size); - -/** - * @brief Request caching of a time range on behalf of a viewer - * (PlaybackCache::request()). - * - * @return OAKRENDER_OK, or OAKRENDER_E_INVALID for an empty cache / - * context handle or a context that is not a viewer. - */ -int oakrender_cache_request(OakRenderCache cache, OakNodeNode context, - int64_t in_num, int64_t in_den, - int64_t out_num, int64_t out_den); - -/** - * @brief Load/save the cache's on-disk state (PlaybackCache::load_state() - * / save_state()). OAKRENDER_E_INVALID for an empty cache. - */ -int oakrender_cache_load_state(OakRenderCache cache); -int oakrender_cache_save_state(OakRenderCache cache); - -/** - * @brief Enable/disable persisting this cache - * (PlaybackCache::set_saving_enabled()). - */ -int oakrender_cache_set_saving_enabled(OakRenderCache cache, int enabled); - -/** - * @brief Pass this cache's ranges through to another cache - * (PlaybackCache::set_passthrough()). OAKRENDER_E_INVALID for an - * empty cache or an empty `other`. - */ -int oakrender_cache_set_passthrough(OakRenderCache cache, - OakRenderCache other); - -/** - * @brief The on-disk filename for the frame at a time - * (FrameHashCache::get_valid_cache_filename()), two-stage. - * - * @return Required buffer size in bytes (including NUL), or a negative - * OAKRENDER_E_* code (OAKRENDER_E_INVALID when the cache is not - * a frame hash cache). - */ -int oakrender_cache_get_valid_cache_filename(OakRenderCache cache, - int64_t time_num, - int64_t time_den, char *buf, - int buf_size); - -/** - * @brief The passthrough ranges as flat {in_n, in_d, out_n, out_d} - * quadruples (PlaybackCache::get_passthroughs(); only the ranges - * cross the boundary, the per-range cache UUID text stays - * internal). - * - * Two-stage: call with ranges == NULL (or max_ranges == 0) to get the - * count; then call with a buffer of max_ranges * 4 int64_t values. - * - * @return Range count (>= 0), or a negative OAKRENDER_E_* code. - */ -int oakrender_cache_get_passthroughs(OakRenderCache cache, int64_t *ranges, - int max_ranges); - -/** - * @brief The cache's frame timebase (FrameHashCache::get_timebase()). - * Out params may individually be NULL. OAKRENDER_E_INVALID for - * an empty cache or a non-frame-hash cache. - */ -int oakrender_cache_get_timebase(OakRenderCache cache, int *num, - int *den); - -/** - * @brief Lock/unlock the cache's internal mutex (PlaybackCache::mutex()). - * Empty cache is a no-op. Always pair the calls. - */ -void oakrender_cache_lock(OakRenderCache cache); -void oakrender_cache_unlock(OakRenderCache cache); - -#ifdef __cplusplus -} /* extern "C" */ - -namespace olive { class PlaybackCache; } - -extern "C" { -#endif - -/** - * @brief Borrowed access to the underlying C++ cache (C++ only, for - * oakrender-internal adapters such as PreviewAutoCacher). Valid - * while the handle is held. NULL-safe. - */ -olive::PlaybackCache *oakrender_cache_get_native(OakRenderCache cache); - -/** - * @brief Set the frame timebase used to interpret all timestamps of this - * cache (FrameHashCache::set_timebase()). - * - * @return OAKRENDER_OK, or OAKRENDER_E_INVALID for an empty cache or - * non-positive num/den. - */ -int oakrender_cache_set_timebase(OakRenderCache cache, int num, int den); - -/** - * @brief Set the cache UUID used in on-disk frame cache filenames - * (PlaybackCache::set_uuid()). - * - * @return OAKRENDER_OK or OAKRENDER_E_INVALID. - */ -int oakrender_cache_set_uuid(OakRenderCache cache, const char *uuid); - -/** - * @brief Mark the timestamp range [in_ts, out_ts) invalidated - * (PlaybackCache::invalidate()). Empty cache is a no-op. - */ -void oakrender_cache_invalidate(OakRenderCache cache, int64_t in_ts, - int64_t out_ts); - -/** - * @brief Mark a rational time range invalidated - * (PlaybackCache::invalidate(TimeRange)). Empty cache is a no-op. - */ -void oakrender_cache_invalidate_range(OakRenderCache cache, - int64_t in_num, int64_t in_den, - int64_t out_num, int64_t out_den); - -/** - * @brief Mark the timestamp range [in_ts, out_ts) validated - * (PlaybackCache::validate()). Empty cache is a no-op. - */ -void oakrender_cache_validate(OakRenderCache cache, int64_t in_ts, - int64_t out_ts); - -/** - * @brief 1 when the cache holds any validated range - * (PlaybackCache::has_validated_ranges()), 0 otherwise / empty. - */ -int oakrender_cache_has_validated_ranges(OakRenderCache cache); - -/** - * @brief Timeline cache indicator height in pixels - * (PlaybackCache::get_cache_indicator_height()). Constant query. - */ -int oakrender_cache_indicator_height(void); - -/** - * @brief The invalidated sub-ranges of [in, out) as flat - * {in_n, in_d, out_n, out_d} quadruples - * (PlaybackCache::get_invalidated_ranges()). - * - * Two-stage: call with ranges == NULL (or max_ranges == 0) to get the - * count; then call with a buffer of max_ranges * 4 int64_t values. - * - * @return Range count (>= 0), or a negative OAKRENDER_E_* code. - */ -int oakrender_cache_get_invalidated_ranges(OakRenderCache c, - int64_t in_num, int64_t in_den, int64_t out_num, int64_t out_den, - int64_t *ranges, int max_ranges); - -/** - * @brief Load a cached frame from disk - * (FrameHashCache::load_cache_frame(cache_path, uuid, ts)). - * - * @param path Cache directory (e.g. oakrender_disk_cache_path()). - * @param uuid Cache UUID of the producing node. - * @param out_frame Receives an owned frame handle (release with - * oakrender_codec_frame_free()). - * - * @return OAKRENDER_OK, OAKRENDER_E_INVALID (empty/NULL argument), or - * OAKRENDER_E_NOT_FOUND (no cached frame at `ts` / undecodable). - */ -int oakrender_frame_cache_load(OakRenderCache cache, const char *path, - const char *uuid, int64_t ts, - OakCodecFrame *out_frame); - -/** - * @brief Save a frame to the disk cache under the cache's timebase and - * the frame's own timestamp (FrameHashCache::save_cache_frame()). - * Empty/NULL arguments are a no-op. - */ -void oakrender_frame_cache_save(OakRenderCache cache, const char *path, - const char *uuid, OakCodecFrame frame); - -/* ---- Debug --------------------------------------------------------------- */ - -/** - * @brief Number of live oakrender-owned objects (caches, textures, - * frames, color processors) for leak assertions in tests. - */ -int oakrender_debug_alive_count(void); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_RENDER_CACHE_H diff --git a/crates/oakengine.bk/include/render/cancelatom.h b/crates/oakengine.bk/include/render/cancelatom.h deleted file mode 100644 index df06ed8f1..000000000 --- a/crates/oakengine.bk/include/render/cancelatom.h +++ /dev/null @@ -1,120 +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 . - -***/ - -#ifndef OAK_EDITOR_RENDER_CANCELATOM_H -#define OAK_EDITOR_RENDER_CANCELATOM_H - -#include - -#include "error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#ifdef __cplusplus -} /* extern "C" */ - -namespace olive { class CancelAtom; } - -extern "C" { -#endif - -/** - * @file cancelatom.h - * @brief C ABI for the oakrender cancellation primitive - * (olive::CancelAtom), a thread-safe cancel flag shared between a - * render/encode caller and its worker. - * - * OakCancelAtom follows the neutral by-value handle convention (see - * oakcommon's common/handle.h): oakrender_cancelatom_init() returns a - * handle whose underlying object has reference count 1, the addref and - * release function pointers adjust that count atomically (release - * destroys the object at zero), and abi_version is always - * OAKRENDER_ABI_VERSION. Copying the struct copies the pointer, not the - * count: call addref for every additional long-lived copy and release (or - * oakrender_cancelatom_free()) when done with each copy. Functions that - * only use a handle take it BY VALUE; an empty handle (ctx == NULL) is - * reported as OAKRENDER_E_INVALID. - */ -typedef struct OakCancelAtom { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKRENDER_ABI_VERSION. */ -} OakCancelAtom; - -/** - * @brief Create a cancellation atom in the not-cancelled state. - * - * @return Handle with reference count 1; ctx is NULL on allocation - * failure. - */ -OakCancelAtom oakrender_cancelatom_init(void); - -/** - * @brief Release one reference to a cancellation atom. - * - * Convenience wrapper around atom->release(atom->ctx): decrements the - * atomic reference count and destroys the object when it reaches zero, - * then nulls atom->ctx. No-op when atom is NULL or atom->ctx is NULL. - */ -void oakrender_cancelatom_free(OakCancelAtom *atom); - -/** - * @brief Set the cancel flag (CancelAtom::cancel()). Thread-safe. - * - * @return OAKRENDER_OK, or OAKRENDER_E_INVALID for an empty handle. - */ -int oakrender_cancelatom_cancel(OakCancelAtom atom); - -/** - * @brief Read the cancel flag (CancelAtom::is_cancelled()). - * - * Reading a set flag also records that a consumer heard the - * cancellation; see oakrender_cancelatom_heard_cancel(). - * - * @param cancelled Receives 1 when cancelled, 0 otherwise. - * @return OAKRENDER_OK, or OAKRENDER_E_INVALID for an empty handle or a - * NULL out parameter. - */ -int oakrender_cancelatom_is_cancelled(OakCancelAtom atom, int *cancelled); - -/** - * @brief Whether any consumer has observed the cancel flag through - * oakrender_cancelatom_is_cancelled() (CancelAtom::heard_cancel()). - * - * @param heard Receives 1 when the cancellation was heard, 0 otherwise. - * @return OAKRENDER_OK, or OAKRENDER_E_INVALID for an empty handle or a - * NULL out parameter. - */ -int oakrender_cancelatom_heard_cancel(OakCancelAtom atom, int *heard); -#ifdef __cplusplus - -/** - * @brief Borrowed access to the underlying C++ atom (C++ only, for - * adapter layers). Valid while the handle is held. NULL-safe. - */ -olive::CancelAtom *oakrender_cancelatom_get_native(OakCancelAtom atom); - -} -#endif - -#endif //OAK_EDITOR_RENDER_CANCELATOM_H diff --git a/crates/oakengine.bk/include/render/color.h b/crates/oakengine.bk/include/render/color.h deleted file mode 100644 index d805c2440..000000000 --- a/crates/oakengine.bk/include/render/color.h +++ /dev/null @@ -1,255 +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 . - -***/ - -#ifndef OAK_EDITOR_RENDER_COLOR_H -#define OAK_EDITOR_RENDER_COLOR_H - -#include "error.h" -#include "renderer.h" -#include "common/colortransform.h" /* OakColorTransform */ -#include "node/colormanager.h" /* OakNodeColorManager */ - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file color.h - * @brief C ABI for oakrender color processing (olive::ColorProcessor) and - * the process-wide default OCIO config (olive::ColorManager - * statics), M7 §2.3. - * - * An OakColorProcessor is a by-value reference-counted handle (shared_ptr - * semantics, see oakcommon's common/handle.h) boxing a ColorProcessorPtr - * (ColorProcessor is shared_ptr-managed); release with - * oakrender_color_processor_free(). Empty handles (ctx == NULL) are - * accepted by every function and yield a no-op / OAKRENDER_E_INVALID. - * - * Processors are built against the process-wide default OCIO config - * (olive::ColorManager::get_default_config()): the $OCIO config when the - * environment variable is set, otherwise the config extracted to the - * user configuration location. oakrender_color_manager_set_up_default_config() - * (re)builds it. - */ - -/** Direction values for oakrender_color_processor_create(). */ -enum { - OAKRENDER_COLOR_DIRECTION_NORMAL = 0, - OAKRENDER_COLOR_DIRECTION_INVERSE = 1 -}; - -typedef struct OakColorProcessor { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKRENDER_ABI_VERSION. */ -} OakColorProcessor; - -/** - * @brief Create a colorspace-to-colorspace processor on the default - * OCIO config. - * - * @param src_space Source colorspace name (role names are resolved). - * @param dst_transform Destination colorspace / output transform name. - * @param direction OAKRENDER_COLOR_DIRECTION_NORMAL (src -> dst) or - * OAKRENDER_COLOR_DIRECTION_INVERSE (dst -> src). - * - * OCIO failures are non-fatal (matching the C++ behavior): the handle is - * still returned but oakrender_color_processor_is_valid() reports 0 and - * conversions are pass-through. - * - * @return Processor handle with reference count 1; ctx is NULL for - * NULL/empty strings, an unknown direction, no default config, - * or allocation failure. - */ -OakColorProcessor oakrender_color_processor_create(const char *src_space, - const char *dst_transform, - int direction); - -/** - * @brief Release one reference to a processor handle. Convenience - * wrapper around processor->release(processor->ctx). NULL / - * empty-handle no-op; clears processor->ctx after releasing. - */ -void oakrender_color_processor_free(OakColorProcessor *processor); - -/** - * @brief 1 when the processor holds a valid OCIO processor - * (ColorProcessor::get_processor() != null), 0 otherwise / empty. - */ -int oakrender_color_processor_is_valid(OakColorProcessor processor); - -/** - * @brief Create a processor from an input colorspace and a destination - * transform on a node's color manager - * (ColorProcessor::create(ColorManager*, input, dest, dir)). - * - * @param manager Borrowed manager handle (e.g. - * oaknode_colormanager_wrap_borrowed()). - * @param direction OAKRENDER_COLOR_DIRECTION_NORMAL / _INVERSE. - * @return Processor handle with reference count 1; ctx is NULL for - * empty/invalid arguments or allocation failure. - */ -OakColorProcessor oakrender_color_processor_create_transform( - OakNodeColorManager manager, const char *input, - OakColorTransform dest, int direction); - -/** - * @brief Create a processor from a LUT file on a node's color manager - * (OCIO FileTransform with linear interpolation; direction - * selects forward/inverse). - * - * @return Processor handle with reference count 1; ctx is NULL for - * empty/invalid arguments, an unreadable LUT, or allocation - * failure. - */ -OakColorProcessor oakrender_color_processor_create_lut( - OakNodeColorManager manager, const char *path, int direction); - -/** - * @brief Grading-primary transform styles for - * oakrender_color_processor_create_grading_primary(). - */ -enum OakRenderGradingPrimaryStyle { - OAKRENDER_GRADING_PRIMARY_LIN = 0, /**< OCIO GRADING_LIN */ - OAKRENDER_GRADING_PRIMARY_LOG = 1 /**< OCIO GRADING_LOG */ -}; - -/** - * @brief Create a dynamic grading-primary processor on a node's color - * manager (OCIO GradingPrimaryTransform, forward direction). - * - * @return Processor handle with reference count 1; ctx is NULL for - * invalid arguments or allocation failure. - */ -OakColorProcessor oakrender_color_processor_create_grading_primary( - OakNodeColorManager manager, int style); - -/* ---- LUT library ---------------------------------------------------- */ - -/** - * @brief 1 when `extension` (without dot, case-insensitive) is a - * supported LUT extension (LUTLibrary::is_supported_extension()). - */ -int oakrender_lut_is_supported_extension(const char *extension); - -/** - * @brief Number of supported LUT extensions - * (LUTLibrary::supported_extensions()). - */ -int oakrender_lut_supported_extensions_count(void); - -/** - * @brief Supported LUT extension at `index`, two-stage string. - * - * @return Required buffer size in bytes (including NUL), or a negative - * OAKRENDER_E_* code for an out-of-range index. - */ -int oakrender_lut_supported_extension_at(int index, char *buf, - int buf_size); - -/** - * @brief Convert a single RGBA color (ColorProcessor::convert_color()). - * On an invalid processor the input is copied through. - * - * @return OAKRENDER_OK, or OAKRENDER_E_INVALID for empty/NULL arguments. - */ -int oakrender_color_processor_convert(OakColorProcessor processor, - double ir, double ig, double ib, - double ia, double *out_r, double *out_g, - double *out_b, double *out_a); - -/** - * @brief Convert a CPU frame's pixels through the processor, in place - * (olive::ColorProcessor::convert_frame()). - * - * The frame's data buffer is rewritten through an OCIO PackedImageDesc - * view; nothing is allocated and the frame handle stays owned by the - * caller. A processor whose underlying OCIO processor is null - * (oakrender_color_processor_create() treats lookup failure as - * non-fatal) is a pass-through and returns OAKRENDER_OK, mirroring the - * C++ API. - * - * @return OAKRENDER_OK, OAKRENDER_E_INVALID for empty/uninitialized - * arguments, or OAKRENDER_E_FAILED on an internal exception. - */ -int oakrender_color_processor_convert_frame(OakColorProcessor processor, - OakCodecFrame frame); - -/* ---- ColorManager statics ------------------------------------------------- */ - -/** - * @brief (Re)build the process-wide default OCIO config - * (ColorManager::set_up_default_config()). - * - * @return OAKRENDER_OK, or OAKRENDER_E_FAILED when no config could be - * created. - */ -int oakrender_color_manager_set_up_default_config(void); - -/** - * @brief Describe the active default config: the $OCIO path when set, - * otherwise the extracted default config's path. Two-stage string - * getter: returns the required buffer size including NUL; pass - * buf == NULL or too small a buffer to query the size. - * - * @return Required size (non-negative), or OAKRENDER_E_STATE when no - * default config exists. - */ -int oakrender_color_manager_get_config(char *buf, int n); - -/** - * @brief OCIO cache id of the display/view transform of the active - * default config, computed from the config's reference colorspace - * (a stable identifier usable as a conversion cache key). - * - * Two-stage string getter (same convention as - * oakrender_color_manager_get_config()). - * - * @return Required size (non-negative), OAKRENDER_E_INVALID (NULL/empty - * display or view), OAKRENDER_E_STATE (no default config), or - * OAKRENDER_E_NOT_FOUND (unknown display/view). - */ -int oakrender_color_manager_display_transform(const char *display, - const char *view, char *buf, - int n); - -#ifdef __cplusplus -} /* extern "C" */ - -#include -namespace olive { class ColorProcessor; } - -extern "C" { -#endif - -/** - * @brief Borrowed access to the underlying C++ processor (C++ only, for - * adapter layers; a shared_ptr copy keeps the object alive). - * Empty shared_ptr for an empty handle. - */ -std::shared_ptr oakrender_color_processor_get_native( - OakColorProcessor processor); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_RENDER_COLOR_H diff --git a/crates/oakengine.bk/include/render/copier.h b/crates/oakengine.bk/include/render/copier.h deleted file mode 100644 index 91987ded6..000000000 --- a/crates/oakengine.bk/include/render/copier.h +++ /dev/null @@ -1,84 +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 . - -***/ - -#ifndef OAK_EDITOR_RENDER_COPIER_H -#define OAK_EDITOR_RENDER_COPIER_H - -#include "node/node.h" -#include "node/project.h" -#include "render/error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Reference-counted handle to a project copier - * (olive::ProjectCopier): deep-copies a project graph for - * background processing (export/precache). - * - * By-value handle (shared_ptr semantics, see oakcommon's - * common/handle.h): oakrender_project_copier_create() returns a handle - * with reference count 1; release it with - * oakrender_project_copier_free(). - */ -typedef struct OakRenderProjectCopier { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKRENDER_ABI_VERSION. */ -} OakRenderProjectCopier; - -/** - * @brief Create a copier. The copy is built by - * oakrender_project_copier_set_project(). - * - * @return Copier handle with reference count 1; ctx is NULL on - * allocation failure. - */ -OakRenderProjectCopier oakrender_project_copier_create(void); - -/** - * @brief Release one reference to a copier; the final release frees the - * copier AND its copied project. NULL / empty-handle no-op; clears - * copier->ctx after releasing. - */ -void oakrender_project_copier_free(OakRenderProjectCopier *copier); - -/** @brief (Re)build the copy from `project` (borrowed handle). */ -int oakrender_project_copier_set_project(OakRenderProjectCopier copier, - OakNodeProject project); - -/** @brief The copied counterpart of an original node (borrowed handle; - * freeing it only releases the handle box), empty handle when the - * node is not in the copied project. */ -OakNodeNode oakrender_project_copier_get_copy( - OakRenderProjectCopier copier, OakNodeNode original); - -/** @brief The copied project (borrowed handle; freeing it only releases - * the handle box). */ -OakNodeProject oakrender_project_copier_get_copied_project( - OakRenderProjectCopier copier); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_RENDER_COPIER_H diff --git a/crates/oakengine.bk/include/render/error.h b/crates/oakengine.bk/include/render/error.h deleted file mode 100644 index e57c568e1..000000000 --- a/crates/oakengine.bk/include/render/error.h +++ /dev/null @@ -1,49 +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 . - -***/ - -#ifndef OAK_EDITOR_RENDER_ERROR_H -#define OAK_EDITOR_RENDER_ERROR_H - -/** - * @brief Status and error codes shared by all oakrender C API families. - * - * Return-code convention (mirrors include/node/error.h): - * 0 (OAKRENDER_OK) on success, a negative OAKRENDER_E_* error code on - * failure. String getters return the required buffer size in bytes - * (including the terminating NUL) as a non-negative value instead. - */ -/** - * @brief Current ABI version stamped into every oakrender handle. - * - * Bump whenever a handle layout or the semantics of any exported function - * change incompatibly. Consumers should compare a handle's abi_version - * field against the value they were compiled with before dereferencing - * ctx. - */ -#define OAKRENDER_ABI_VERSION 1 - -#define OAKRENDER_OK 0 /**< Success. */ -#define OAKRENDER_E_INVALID (-70001) /**< NULL handle or invalid argument. */ -#define OAKRENDER_E_STATE (-70002) /**< Call not valid in the current state. */ -#define OAKRENDER_E_FAILED (-70003) /**< The underlying operation failed. */ -#define OAKRENDER_E_NOT_FOUND (-70004) /**< Index out of range / entry not found. */ -#define OAKRENDER_E_NOMEM (-70005) /**< Allocation failed. */ - -#endif //OAK_EDITOR_RENDER_ERROR_H diff --git a/crates/oakengine.bk/include/render/manager.h b/crates/oakengine.bk/include/render/manager.h deleted file mode 100644 index 9a99233b7..000000000 --- a/crates/oakengine.bk/include/render/manager.h +++ /dev/null @@ -1,167 +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 . - -***/ - -#ifndef OAK_EDITOR_RENDER_MANAGER_H -#define OAK_EDITOR_RENDER_MANAGER_H - -#include - -// See cache.h for why these are same-dir relative includes. -#include "node/node.h" /* OakNodeNode (by-value handle) */ -#include "cache.h" /* OakCodecFrame */ -#include "color.h" /* OakColorProcessor */ -#include "error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file manager.h - * @brief C ABI for the oakrender render manager / preview auto-cacher / - * disk cache singletons (olive::RenderManager, - * olive::PreviewAutoCacher, olive::DiskManager), M7 §2.4. - * - * The render manager is a process-wide singleton gated by - * oakrender_manager_init() / oakrender_manager_shutdown(). Functions - * that need it return OAKRENDER_E_STATE when it is not up. - * - * The frame request callback is the asynchronous command return channel - * (M7 §2.2 note): it fires on a render worker thread, possibly after - * cancellation. The delivered OakCodecFrame is owned by the callback - * recipient (release with oakrender_codec_frame_free()); an empty frame - * (ctx == NULL) signals "no result" (cancelled or failed). Beyond this - * callback there are no event subscription interfaces. - */ - -/** - * @brief Create the RenderManager singleton (spawns render/audio - * threads, loads the configured backend). - * - * @return OAKRENDER_OK, OAKRENDER_E_STATE (already initialized), or - * OAKRENDER_E_FAILED. - */ -int oakrender_manager_init(void); - -/** - * @brief Destroy the RenderManager singleton. No-op when not - * initialized. - */ -void oakrender_manager_shutdown(void); - -/** - * @brief Completion callback of an asynchronous frame request. - * - * @param frame Owned frame handle, or an empty handle (ctx == NULL) - * when the request finished without a result (cancelled/failed). - * @param ts The request's timestamp, passed back verbatim. - */ -typedef void (*oakrender_frame_ready_fn)(OakCodecFrame frame, int64_t ts, - void *userdata); - -/** - * @brief Asynchronously render one frame of `viewer` at `ts` - * (PreviewAutoCacher::get_single_frame()). - * - * `ts` is a frame number in the viewer node's video timebase (a whole - * second count when the viewer carries no valid timebase). The - * completion is delivered through `cb`; until then the request can be - * cancelled with oakrender_cancel_request(). - * - * @return A positive request id, or a negative OAKRENDER_E_* code - * (OAKRENDER_E_INVALID for an empty viewer handle or NULL - * callback, OAKRENDER_E_STATE when the manager is not - * initialized, OAKRENDER_E_FAILED when no ticket could be - * created). - */ -int64_t oakrender_request_frame(OakNodeNode viewer, int64_t ts, - oakrender_frame_ready_fn cb, void *userdata); - -/** - * @brief Cancel a pending frame request. The callback still fires with a - * NULL frame. - * - * @return OAKRENDER_OK, or OAKRENDER_E_NOT_FOUND for an unknown id. - */ -int oakrender_cancel_request(int64_t request_id); - -/** - * @brief Set the multicam node on the manager's auto-cacher - * (PreviewAutoCacher::set_multicam_node()). `multicam_or_NULL` is a - * borrowed oaknode handle to a MultiCamNode (empty handle to clear). - * - * @return OAKRENDER_OK or OAKRENDER_E_STATE. - */ -int oakrender_set_cacher_multicam(OakNodeNode multicam_or_NULL); - -/** - * @brief Set the display color processor on the manager's auto-cacher - * (PreviewAutoCacher::set_display_color_processor()). Borrowed handle, - * empty ctx to clear. - * - * @return OAKRENDER_OK or OAKRENDER_E_STATE. - */ -int oakrender_set_display_color_processor(OakColorProcessor p_or_NULL); - -/** - * @brief 1 when the process-wide RenderManager singleton exists - * (RenderManager::instance() != nullptr; only the main GUI - * process creates one), 0 otherwise. - */ -int oakrender_manager_available(void); - -/** - * @brief Cancel in-flight video cache tasks on the manager's - * auto-cacher (PreviewAutoCacher::cancel_video_tasks()). No-op - * when no manager/auto-cacher exists (e.g. a worker process). - */ -void oakrender_cancel_video_tasks(int wait_for_done); - -/* ---- Disk cache (olive::DiskManager) -------------------------------------- */ - -/** - * @brief The default disk cache directory - * (DiskManager::get_default_disk_cache_path()). Two-stage string getter: - * returns the required buffer size including NUL; pass buf == NULL or - * too small a buffer to query the size. Does not require the manager. - */ -int oakrender_disk_cache_path(char *buf, int n); - -/** - * @brief Bytes currently consumed by the default disk cache folder. - * Lazily creates the DiskManager singleton on first use. - * - * @return Consumption in bytes (>= 0), or OAKRENDER_E_FAILED. - */ -int64_t oakrender_disk_cache_size(void); - -/** - * @brief Clear the default disk cache folder - * (DiskManager::clear_disk_cache()). - * - * @return OAKRENDER_OK or OAKRENDER_E_FAILED. - */ -int oakrender_disk_cache_clear(void); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_RENDER_MANAGER_H diff --git a/crates/oakengine.bk/include/render/renderer.h b/crates/oakengine.bk/include/render/renderer.h deleted file mode 100644 index 7ab78f647..000000000 --- a/crates/oakengine.bk/include/render/renderer.h +++ /dev/null @@ -1,374 +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 . - -***/ - -#ifndef OAK_EDITOR_RENDER_RENDERER_H -#define OAK_EDITOR_RENDER_RENDERER_H - -#include - -#ifdef __cplusplus -#include -#endif - -#include "error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @file renderer.h - * @brief C ABI for the oakrender display renderer (olive::Renderer) — - * renderer/texture/frame/blit families plus backend management. - * - * Signatures follow the R7-A display.h rewrite - * (docs/zh/plans/completed/r7-pure-abi-plan.md §A.2) with the - * oakrender_ prefix (M7 §2.1). - * - * Ownership protocol: every public handle is a by-value - * reference-counted struct (see oakcommon's common/handle.h; shared_ptr - * semantics). init/create functions return a handle with reference - * count 1, handle.addref(handle.ctx) takes another reference, and - * handle.release(handle.ctx) (or the oakrender_*_free() convenience - * wrappers, which also null the caller's ctx) drops one; the object is - * destroyed in this library when the count reaches zero. Empty handles - * (ctx == NULL) are accepted by every function and yield a no-op / zero - * result / OAKRENDER_E_INVALID. - * - * Cross-thread handoff (§A.3): the producing side addrefs before - * publishing a handle into a shared slot; the consuming side releases - * the handle it replaced. The side holding the slot when it is torn - * down releases the remaining handle. - * - * Handles: - * - OakRenderRenderer wraps a native olive::Renderer. - * - OakRenderTexture / OakCodecFrame box shared_ptr-managed engine - * objects. - * - `gl_context` is an opaque borrowed olive::OpenGLContext* (or NULL - * to let the backend create its own offscreen surface). - */ - -/** - * @brief POD mirror of olive::VideoParams' user-facing fields. - * - * Same layout and field semantics as oak_video_params - * (engine/include/oakengine/videoparams.h): `time_base_*` is the frame - * duration (frame rate flipped), `format` an olive::PixelFormat::Format - * value, `interlacing` an olive::VideoParams::Interlacing value, - * `color_range` an olive::VideoParams::ColorRange value. The video - * channel count is an engine-internal constant and not exposed. - */ -typedef struct oakrender_video_params { - int width; - int height; - int time_base_num; /**< Frame duration numerator (e.g. 1001/30000 s). */ - int time_base_den; - int format; /**< olive::PixelFormat::Format. */ - int pixel_aspect_num; - int pixel_aspect_den; - int interlacing; /**< olive::VideoParams::Interlacing. */ - int color_range; /**< olive::VideoParams::ColorRange. */ - int divider; /**< Preview resolution divider (1 = full). */ - int video_type; /**< olive::VideoParams::Type (0 = video). */ - int premultiplied_alpha; /**< 0/1. */ -} oakrender_video_params; - -/** - * @brief Reference-counted handle to a display renderer - * (olive::Renderer). See the file-level ownership protocol. - */ -typedef struct OakRenderRenderer { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKRENDER_ABI_VERSION. */ -} OakRenderRenderer; - -/** - * @brief Reference-counted handle to a GPU texture (olive::Texture). - * See the file-level ownership protocol. - */ -typedef struct OakRenderTexture { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKRENDER_ABI_VERSION. */ -} OakRenderTexture; - -/** - * @brief Reference-counted handle to a CPU frame (an olive::FramePtr - * boxed in a control block). Declared here so the cache family - * (render/cache.h) can use the same type; the frame functions live in - * this header. - * Named OakCodecFrame per the M7 §2.2 contract; the oakcodec wave (M5) - * adopts the same handle. - */ -typedef struct OakCodecFrame { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKRENDER_ABI_VERSION. */ -} OakCodecFrame; - -/** - * @brief Flattened POD of olive::ColorTransformJob for the display blit - * path. `matrix`/`crop_matrix` are column-major 4x4; an all-zero matrix - * means identity. - */ -typedef struct oakrender_color_transform_job { - const void *processor; /**< OakColorProcessor ctx (borrowed), may be NULL. */ - void *input_texture; /**< OakRenderTexture ctx (borrowed, not retained). */ - int input_alpha_association; /**< 0=none, 1=associated. */ - int clear_destination; /**< 0/1. */ - int force_opaque; /**< 0/1. */ - float matrix[16]; - float crop_matrix[16]; -} oakrender_color_transform_job; - -/* ---- Renderer lifecycle -------------------------------------------------- */ - -/** - * @brief Create a renderer on the named dynamic backend ("opengl", - * "vulkan"; olive::DynamicRenderer). Loads the backend shared library; - * falls back per DynamicRenderer rules. - * - * @return Renderer handle with reference count 1; ctx is NULL on - * NULL/empty backend id, load failure, or allocation failure. - */ -OakRenderRenderer oakrender_display_renderer_create_dynamic( - const char *backend_id); - -/** - * @brief Create an OpenGL renderer (olive::OpenGLRenderer). The renderer - * is not initialized; call oakrender_display_renderer_init() before use. - * - * @return Renderer handle with reference count 1; ctx is NULL on - * allocation failure. - */ -OakRenderRenderer oakrender_display_renderer_create_opengl(void); - -/** - * @brief Initialize a renderer. `gl_context` is a borrowed opaque - * olive::OpenGLContext*, or NULL to use the backend's default - * device/context path (Renderer::init()). - * - * @return OAKRENDER_OK, OAKRENDER_E_INVALID (empty renderer), or - * OAKRENDER_E_FAILED (backend init failed). - */ -int oakrender_display_renderer_init(OakRenderRenderer renderer, - void *gl_context); - -/** - * @brief Release one reference to a renderer (the final release runs - * Renderer::destroy() + delete). Convenience wrapper around - * renderer->release(renderer->ctx): NULL / empty-handle no-op; clears - * renderer->ctx after releasing. - */ -void oakrender_display_renderer_destroy(OakRenderRenderer *renderer); - -/* ---- Renderer queries ---------------------------------------------------- */ - -/** @brief 1 when the renderer is OpenGL-based, 0 otherwise / empty. */ -int oakrender_display_renderer_is_open_gl(OakRenderRenderer renderer); - -/** @brief 1 when the renderer is Vulkan-based, 0 otherwise / empty. */ -int oakrender_display_renderer_is_vulkan(OakRenderRenderer renderer); - -/* ---- Texture handle ------------------------------------------------------ */ - -/** - * @brief Create a GPU texture on `renderer`. - * - * @param pixels Initial pixel data, or NULL for an uninitialized texture. - * @param linesize Stride of `pixels` in bytes (0 when pixels is NULL). - * @return New texture handle (reference count 1); ctx is NULL on invalid - * arguments / allocation failure. - */ -OakRenderTexture oakrender_display_texture_create( - OakRenderRenderer renderer, const oakrender_video_params *params, - const void *pixels, int linesize); - -/** - * @brief Take another reference to a texture and return the same handle. - * - * Convenience wrapper around handle.addref(handle.ctx). An empty handle - * in yields an empty handle out. Every retain must be paired with - * exactly one free/release. - */ -OakRenderTexture oakrender_display_texture_retain(OakRenderTexture texture); - -/** - * @brief Release one reference to a texture. Convenience wrapper around - * texture->release(texture->ctx): frees the texture when the count - * reaches zero. NULL / empty-handle no-op; clears texture->ctx after - * releasing. - */ -void oakrender_display_texture_free(OakRenderTexture *texture); - -int oakrender_display_texture_upload(OakRenderTexture texture, - const void *pixels, int linesize); - -int oakrender_display_texture_download(OakRenderTexture texture, void *pixels, - int linesize); - -/* ---- Texture queries ----------------------------------------------------- */ - -int oakrender_display_texture_get_params(OakRenderTexture texture, - oakrender_video_params *out); - -/** @brief Frame width/height in pixels (0 on empty). */ -int oakrender_codec_frame_width(OakCodecFrame frame); -int oakrender_codec_frame_height(OakCodecFrame frame); - -/** @brief ffmpeg_bridge pixel format when the frame wraps a texture's - * CPU copy (an AVFramePtr); -1 otherwise. */ -int oakrender_codec_frame_fb_format(OakCodecFrame frame); - -/** @brief Native texture id (0 on empty or a dummy/id-less texture). */ -int oakrender_display_texture_id(OakRenderTexture texture); - -/** @brief 1 when the texture is a placeholder dummy (Texture::is_dummy()). */ -int oakrender_display_texture_is_dummy(OakRenderTexture texture); - -/** - * @brief The CPU frame stored in the texture, if any (Texture::frame()). - * *out receives a retained frame handle (empty when none). - */ -int oakrender_display_texture_get_frame(OakRenderTexture texture, - OakCodecFrame *out); - -#ifdef __cplusplus -} /* extern "C" */ - -namespace olive { class Texture; using TexturePtr = std::shared_ptr; } - -/** - * @brief Wrap a native TexturePtr in a retained handle (C++ only; used - * by oakrender internals when handing textures across the C ABI). - */ -OakRenderTexture oakrender_display_texture_wrap_native( - const olive::TexturePtr &texture); - -extern "C" { -#endif - -/* ---- Frame handle -------------------------------------------------------- */ - -/** @brief Create an empty CPU frame. Returns a handle with count 1. */ -OakCodecFrame oakrender_codec_frame_create(void); - -/** - * @brief Take another reference to a frame and return the same handle. - * Empty in yields empty out (see oakrender_display_texture_retain()). - */ -OakCodecFrame oakrender_codec_frame_retain(OakCodecFrame frame); - -/** - * @brief Release one reference to a frame. Convenience wrapper around - * frame->release(frame->ctx). NULL / empty-handle no-op; clears - * frame->ctx after releasing. - */ -void oakrender_codec_frame_free(OakCodecFrame *frame); - -int oakrender_codec_frame_set_video_params( - OakCodecFrame frame, const oakrender_video_params *params); - -int oakrender_codec_frame_get_params(OakCodecFrame frame, - oakrender_video_params *out); - -/** - * @brief Allocate the pixel buffer per the frame's video params - * (Frame::allocate()). - * - * @return OAKRENDER_OK, OAKRENDER_E_INVALID (empty frame), or - * OAKRENDER_E_FAILED (invalid params / allocation failed). - */ -int oakrender_codec_frame_allocate(OakCodecFrame frame); - -/** @brief Borrowed pixel data pointer (valid until the final release). */ -void *oakrender_codec_frame_data(OakCodecFrame frame); - -/** @brief Borrowed const pixel data pointer. */ -const void *oakrender_codec_frame_const_data(OakCodecFrame frame); - -/** @brief Line stride in bytes. */ -int oakrender_codec_frame_linesize_bytes(OakCodecFrame frame); - -/** @brief 1 when the pixel buffer is allocated, 0 otherwise / empty. */ -int oakrender_codec_frame_is_allocated(OakCodecFrame frame); - -/* ---- Color-managed blit -------------------------------------------------- */ - -/** - * @brief Blit a color-managed image through the OCIO pipeline - * (Renderer::blit_color_managed()). - * - * @param dst_texture Destination texture handle, or an empty handle for - * the current output target. - * @param params Destination video params, or NULL to use dst_texture's. - */ -int oakrender_display_renderer_blit_color_managed( - OakRenderRenderer renderer, const oakrender_color_transform_job *job, - OakRenderTexture dst_texture, const oakrender_video_params *params); - -/* ---- Cross-backend texture download -------------------------------------- */ - -int oakrender_display_renderer_download_from_texture( - OakRenderRenderer renderer, int texture_id, - const oakrender_video_params *params, void *dst_pixels, int linesize); - -/* ---- Backend management (M7 §2.1) ---------------------------------------- */ - -/** - * @brief Number of known render backends (olive::RenderManager::Backend: - * opengl, vulkan, multiprocess, dummy). - */ -int oakrender_backend_count(void); - -/** - * @brief Id string of the `i`-th backend ("opengl", ...). Two-stage - * string getter: returns the required buffer size including NUL; pass - * buf == NULL or too small a buffer to query the size. - * - * @return Required size (non-negative), or OAKRENDER_E_NOT_FOUND when - * `i` is out of range. - */ -int oakrender_backend_id_at(int i, char *buf, int n); - -/** - * @brief Record the requested backend id (applied to the RenderManager - * instance when one exists). - * - * @return OAKRENDER_OK, or OAKRENDER_E_INVALID for a NULL/unknown id. - */ -int oakrender_set_backend(const char *backend_id); - -/** - * @brief The effective backend: the RenderManager instance's backend when - * an instance exists, otherwise the requested backend. Two-stage string - * getter (same convention as oakrender_backend_id_at()). - */ -int oakrender_current_backend(char *buf, int n); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_RENDER_RENDERER_H diff --git a/crates/oakengine.bk/include/render/ticket.h b/crates/oakengine.bk/include/render/ticket.h deleted file mode 100644 index eb24e18e5..000000000 --- a/crates/oakengine.bk/include/render/ticket.h +++ /dev/null @@ -1,171 +0,0 @@ -/*** - - Oak Video Editor - Non-Linear Video Editor - Copyright (C) 2026 Oak Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#ifndef OAK_EDITOR_RENDER_TICKET_H -#define OAK_EDITOR_RENDER_TICKET_H - -#include - -#include "common/colortransform.h" -#include "common/videoparams.h" -#include "node/colormanager.h" -#include "node/node.h" -#include "olive/core/oakcore/audioparams.h" -#include "olive/core/oakcore/samplebuffer.h" -#include "render/error.h" -#include "render/cache.h" -#include "render/color.h" -#include "render/renderer.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Reference-counted handle to a render ticket - * (olive::RenderTicketWatcher). - * - * By-value handle (shared_ptr semantics, see oakcommon's - * common/handle.h). Created by oakrender_ticket_render_frame() / - * oakrender_ticket_render_audio() with reference count 1; release with - * oakrender_ticket_free(). - */ -typedef struct OakRenderTicket { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKRENDER_ABI_VERSION. */ -} OakRenderTicket; - -/** - * @brief Finished callback (async command return channel, 01 §4 - * exception). Fires on the ticket's finishing thread, exactly - * once (cancelled tickets fire with a NULL result). The ticket - * handle is a borrowed copy of the submitter's handle; the - * submitter keeps ownership and releases it. - */ -typedef void (*oakrender_ticket_finished_fn)(OakRenderTicket ticket, - void *userdata); - -/** @brief Ticket types (RenderManager::TicketType). */ -enum OakRenderTicketType { - OAKRENDER_TICKET_VIDEO = 0, - OAKRENDER_TICKET_AUDIO = 1 -}; - -/** - * @brief Parameters for a video frame ticket - * (RenderManager::RenderVideoParams). - */ -typedef struct oakrender_video_ticket_params { - OakNodeNode output_node; /**< Connected texture output node (borrowed). */ - OakVideoParams video_params; /**< By value (oakcommon handle). */ - OakAudioParams *audio_params; /**< Borrowed oakcore handle, may be NULL. */ - int64_t time_num; /**< Frame timestamp as rational. */ - int64_t time_den; - OakNodeColorManager color_manager; /**< Borrowed, empty ctx = NULL. */ - int mode; /**< olive::RenderMode::Mode as int. */ - int force_width; /**< 0/0 = off. */ - int force_height; - double force_matrix[16]; /**< Used when has_force_matrix != 0. */ - int has_force_matrix; - int force_format; /**< PixelFormat as int, -1 = off. */ - int force_channel_count; /**< 0 = off. */ - OakColorProcessor force_color_output; /**< Borrowed; empty ctx = none. */ - OakColorTransform force_color_transform; /**< By value; empty ctx = default. */ - OakRenderCache cache; /**< Borrowed frame cache; empty ctx = none. */ -} oakrender_video_ticket_params; - -/** - * @brief Submit a video frame render ticket. - * - * @return Ticket handle with reference count 1 (caller releases); ctx is - * NULL on failure. The finished callback fires exactly once; - * NULL `cb` is allowed (poll with - * oakrender_ticket_wait()/oakrender_ticket_is_finished()). - */ -OakRenderTicket oakrender_ticket_render_frame( - const oakrender_video_ticket_params *params, - oakrender_ticket_finished_fn cb, void *userdata); - -/** - * @brief Submit an audio render ticket (RenderManager::render_audio()). - * - * @param output_node Connected sample output node. - * @param params Audio params (borrowed oakcore handle). - */ -OakRenderTicket oakrender_ticket_render_audio( - OakNodeNode output_node, int64_t in_num, int64_t in_den, - int64_t out_num, int64_t out_den, const OakAudioParams *params, - int mode, oakrender_ticket_finished_fn cb, void *userdata); - -int oakrender_ticket_is_finished(OakRenderTicket ticket); - -/** @brief Block until the ticket finishes. */ -int oakrender_ticket_wait(OakRenderTicket ticket); - -int oakrender_ticket_cancel(OakRenderTicket ticket); - -/** @brief OAKRENDER_TICKET_* or negative error. */ -int oakrender_ticket_get_type(OakRenderTicket ticket); - -/** @brief Ticket timestamp (video tickets). */ -int oakrender_ticket_get_time(OakRenderTicket ticket, int64_t *out_num, - int64_t *out_den); - -/** @brief Ticket time range (audio tickets). */ -int oakrender_ticket_get_range(OakRenderTicket ticket, int64_t *in_num, - int64_t *in_den, int64_t *out_num, - int64_t *out_den); - -/** - * @brief The resulting frame (video tickets). *out receives an owned - * OakCodecFrame (release with oakrender_codec_frame_free()). - * OAKRENDER_E_STATE when unfinished, OAKRENDER_E_FAILED when the - * ticket has no frame result. - */ -int oakrender_ticket_get_frame(OakRenderTicket ticket, OakCodecFrame *out); - -/** - * @brief The resulting samples (audio tickets). *out receives a copy - * (release with oakcore_samplebuffer_free()). - */ -int oakrender_ticket_get_samples(OakRenderTicket ticket, - OakSampleBuffer **out); - -/** - * @brief Release one reference to a ticket (the final release is safe on - * finished tickets; cancels and waits on running ones). Convenience - * wrapper around ticket->release(ticket->ctx). NULL / empty-handle - * no-op; clears ticket->ctx after releasing. - */ -void oakrender_ticket_free(OakRenderTicket *ticket); - -/** - * @brief Toggle aggressive garbage collection on the render manager - * (RenderManager::set_aggressive_garbage_collection()). - */ -int oakrender_manager_set_aggressive_gc(int enabled); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_RENDER_TICKET_H diff --git a/crates/oakengine.bk/include/task/error.h b/crates/oakengine.bk/include/task/error.h deleted file mode 100644 index 9f93913d0..000000000 --- a/crates/oakengine.bk/include/task/error.h +++ /dev/null @@ -1,42 +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 . - -***/ - -#ifndef OAK_EDITOR_TASK_ERROR_H -#define OAK_EDITOR_TASK_ERROR_H - -/** - * @brief Status and error codes shared by all oaktask C API families. - * - * Return-code convention (mirrors engine/include/oakengine/init.h): - * 0 (OAKTASK_OK) on success, a negative OAKTASK_E_* error code on - * failure. String getters return the required buffer size in bytes - * (including the terminating NUL) as a non-negative value instead. - */ -#define OAKTASK_ABI_VERSION 1 - -#define OAKTASK_OK 0 /**< Success. */ -#define OAKTASK_E_INVALID (-80001) /**< NULL handle or invalid argument. */ -#define OAKTASK_E_STATE (-80002) /**< Call not valid in the current state. */ -#define OAKTASK_E_FAILED (-80003) /**< The underlying operation failed. */ -#define OAKTASK_E_NOT_FOUND (-80004) /**< Index out of range / entry not found. */ -#define OAKTASK_E_NOMEM (-80005) /**< Allocation failed. */ -#define OAKTASK_E_CANCELLED (-80006) /**< The operation was cancelled. */ - -#endif //OAK_EDITOR_TASK_ERROR_H diff --git a/crates/oakengine.bk/include/task/manager.h b/crates/oakengine.bk/include/task/manager.h deleted file mode 100644 index acbebcec5..000000000 --- a/crates/oakengine.bk/include/task/manager.h +++ /dev/null @@ -1,55 +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 . - -***/ - -#ifndef OAK_EDITOR_TASK_MANAGER_H -#define OAK_EDITOR_TASK_MANAGER_H - -#include "task/task.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Task manager singleton lifecycle. - */ -int oaktask_manager_init(void); -void oaktask_manager_shutdown(void); - -/** - * @brief Register oaktask as oakcodec's background task submitter - * (olive::register_codec_task_submitter()). Called by - * oaktask_manager_init(); exposed for manual control. - */ -int oaktask_register_codec_submitter(void); - -int oaktask_manager_count(void); - -/** @brief Borrowed task at index (release only frees the box), empty - * handle when out of range or no manager. */ -OakTaskTask oaktask_manager_at(int i); - -void oaktask_manager_delete_finished(void); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_TASK_MANAGER_H diff --git a/crates/oakengine.bk/include/task/project.h b/crates/oakengine.bk/include/task/project.h deleted file mode 100644 index ee2467b2d..000000000 --- a/crates/oakengine.bk/include/task/project.h +++ /dev/null @@ -1,126 +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 . - -***/ - -#ifndef OAK_EDITOR_TASK_PROJECT_H -#define OAK_EDITOR_TASK_PROJECT_H - -#include "codec/encoder.h" -#include "node/colormanager.h" -#include "node/footage.h" -#include "node/node.h" -#include "node/project.h" -#include "node/sequence.h" -#include "task/task.h" -#include "undo/undocommand.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Project task factories and result accessors (M8 §2.2). - */ - -/** @brief olive::ProjectLoadTask. Empty handle (ctx == NULL) on - * failure. */ -OakTaskTask oaktask_create_project_load(const char *filename); - -/** @brief Take the loaded project (ownership transfer). Empty handle - * (ctx == NULL) when the task has not succeeded or the project was - * already taken. */ -OakNodeProject oaktask_load_take_project(OakTaskTask t); - -/** @brief olive::ProjectSaveTask. `filename_or_NULL` overrides the - * project's own filename. `project` is borrowed by the task. */ -OakTaskTask oaktask_create_project_save(OakNodeProject project, - const char *filename_or_NULL, - int use_compression); - -/** @brief olive::ProjectImportTask. `folder`/`project` are borrowed by - * the task. */ -OakTaskTask oaktask_create_project_import(OakNodeFolder folder, - OakNodeProject project, - const char *const *urls, - int url_count); - -/** @brief Take the import's undo command (ownership transfer). */ -OakUndoCommand oaktask_import_take_command(OakTaskTask t); - -int oaktask_import_footage_count(OakTaskTask t); - -/** @brief Footage handle at index (addref'd; release with - * handle.release(handle.ctx) - box only, the project owns the - * footage). Empty handle when out of range. */ -OakNodeFootage oaktask_import_footage_at(OakTaskTask t, int index); - -int oaktask_import_invalid_count(OakTaskTask t); - -/** @brief Invalid filename at index (two-stage). */ -int oaktask_import_invalid_at(OakTaskTask t, int index, char *buf, - int buf_size); - -/** @brief olive::LoadOTIOTask. Empty handle (ctx == NULL) on failure. */ -OakTaskTask oaktask_create_project_load_otio(const char *filename); - -/** @brief Take the loaded project (ownership transfer). Empty handle - * (ctx == NULL) when the task has not succeeded or the project was - * already taken. */ -OakNodeProject oaktask_load_otio_take_project(OakTaskTask t); - -/** @brief olive::SaveOTIOTask. `project` is borrowed by the task. */ -OakTaskTask oaktask_create_project_save_otio(OakNodeProject project, - const char *filename); - -/** - * @brief OTIO import confirmation callback (facade concern; default - * accepts everything). Return non-zero to accept. - */ -typedef int (*oaktask_otio_import_confirm_fn)( - const char *const *sequence_names, int count, void *userdata); -void oaktask_load_otio_set_confirm_cb(oaktask_otio_import_confirm_fn fn, - void *userdata); - -/** @brief olive::PreCacheTask. `footage`/`sequence` are borrowed by the - * task. */ -OakTaskTask oaktask_create_precache(OakNodeFootage footage, int index, - OakNodeSequence sequence); - -/** @brief olive::ExportTask (params POD from codec/encoder.h). - * `viewer`/`color_manager` are borrowed by the task. */ -OakTaskTask oaktask_create_export(OakNodeNode viewer, - OakNodeColorManager color_manager, - const oakcodec_encoding_params *params); - -/** - * @brief Image-sequence confirmation callback (facade/UI concern; - * olive::ProjectImportTask::set_image_sequence_confirm_callback). - * Return non-zero to treat numbered stills as a sequence. - * Default (no callback): not a sequence. - */ -typedef int (*oaktask_image_sequence_confirm_fn)(const char *filename, - void *userdata); -void oaktask_import_set_image_sequence_confirm_cb( - oaktask_image_sequence_confirm_fn fn, void *userdata); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_TASK_PROJECT_H diff --git a/crates/oakengine.bk/include/task/task.h b/crates/oakengine.bk/include/task/task.h deleted file mode 100644 index a8c1e4ca6..000000000 --- a/crates/oakengine.bk/include/task/task.h +++ /dev/null @@ -1,108 +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 . - -***/ - -#ifndef OAK_EDITOR_TASK_TASK_H -#define OAK_EDITOR_TASK_TASK_H - -#include - -#include "task/error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Reference-counted handle to a background task (olive::Task). - * - * By-value handle (shared_ptr semantics, see oakcommon's - * common/handle.h). Tasks are created through the factories in - * task/project.h (and future family headers) with reference count 1 and - * must be released with oaktask_task_free(). oaktask_task_start() - * transfers the task's lifetime to the task manager: releasing the - * handle afterwards only frees the box. - */ -typedef struct OakTaskTask { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKTASK_ABI_VERSION. */ -} OakTaskTask; - -/** @brief Lifecycle event ids for oaktask_task_subscribe(). */ -enum OakTaskEvent { - OAKTASK_EVENT_STARTED = 0, - OAKTASK_EVENT_PROGRESS = 1, - OAKTASK_EVENT_FINISHED = 2 -}; - -/** - * @brief Event callback (async command return channel, 01 §4 exception). - * - * For OAKTASK_EVENT_FINISHED, `value` is 1.0 on success / 0.0 on failure; - * for OAKTASK_EVENT_PROGRESS it is 0..1; for OAKTASK_EVENT_STARTED it is - * the start time in milliseconds. - */ -typedef void (*oaktask_event_fn)(int event_id, double value, - void *userdata); - -/** - * @brief Release one reference to a task. Convenience wrapper around - * t->release(t->ctx): NULL / empty-handle no-op; clears t->ctx - * after releasing. The task must not be running on the manager - * (oaktask_task_cancel + wait first if it is). - */ -void oaktask_task_free(OakTaskTask *t); - -/** @brief Run synchronously in the calling thread. 1 = succeeded. */ -int oaktask_task_start_sync(OakTaskTask t); - -/** @brief Run asynchronously on the task manager. */ -int oaktask_task_start(OakTaskTask t); - -int oaktask_task_cancel(OakTaskTask t); - -/** @brief Wait for an asynchronously started task. */ -int oaktask_task_wait(OakTaskTask t); - -int oaktask_task_is_finished(OakTaskTask t); - -int oaktask_task_succeeded(OakTaskTask t); - -/** @brief Two-stage string getters. */ -int oaktask_task_title(OakTaskTask t, char *buf, int buf_size); -int oaktask_task_error(OakTaskTask t, char *buf, int buf_size); - -/** - * @brief Subscribe to lifecycle events (returns a subscription id >= 0, - * or a negative error code). One-shot per event stream: the - * subscription is dropped after OAKTASK_EVENT_FINISHED. - */ -int64_t oaktask_task_subscribe(OakTaskTask t, oaktask_event_fn fn, - void *userdata); - -/** @brief Alive-count for leak assertions in tests. */ -int oaktask_debug_alive_count(void); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_TASK_TASK_H diff --git a/crates/oakengine.bk/include/timeline/displaymode.h b/crates/oakengine.bk/include/timeline/displaymode.h deleted file mode 100644 index 4c831036d..000000000 --- a/crates/oakengine.bk/include/timeline/displaymode.h +++ /dev/null @@ -1,51 +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 . - -***/ - -#ifndef OAK_EDITOR_TIMELINE_DISPLAYMODE_H -#define OAK_EDITOR_TIMELINE_DISPLAYMODE_H - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Shared timeline display-mode constants. - * - * Neutral home for the enum values behind the TimelineThumbnailMode / - * TimelineWaveformMode config keys; mirrors olive::Timeline::ThumbnailMode - * / WaveformMode (src/timeline/src/timelinecommon.h) and must stay - * value-compatible with them. - */ -enum OakTimelineThumbnailMode { - OAK_TIMELINE_THUMBNAIL_OFF = 0, - OAK_TIMELINE_THUMBNAIL_IN_OUT = 1, - OAK_TIMELINE_THUMBNAIL_ON = 2 -}; - -enum OakTimelineWaveformMode { - OAK_TIMELINE_WAVEFORMS_DISABLED = 0, - OAK_TIMELINE_WAVEFORMS_ENABLED = 1 -}; - -#ifdef __cplusplus -} -#endif - -#endif // OAK_EDITOR_TIMELINE_DISPLAYMODE_H diff --git a/crates/oakengine.bk/include/timeline/edit.h b/crates/oakengine.bk/include/timeline/edit.h deleted file mode 100644 index 4f6a5c101..000000000 --- a/crates/oakengine.bk/include/timeline/edit.h +++ /dev/null @@ -1,131 +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 . - -***/ - -#ifndef OAK_EDITOR_TIMELINE_EDIT_H -#define OAK_EDITOR_TIMELINE_EDIT_H - -#include "node/block.h" -#include "node/sequence.h" -#include "node/track.h" -#include "timeline/error.h" -#include "undo/undocommand.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Timeline edit primitives (M4 §2.3). - * - * The timeline undo command classes stay inside oaktimeline (01 §5); - * consumers create commands through these factories, receiving base - * OakUndoCommand handles (owned; free with oakundo_command_free()). - * Redo a command directly or push it on an undo stack. - * - * OakNode* handles are passed by value per the oaknode handle - * convention; an empty handle (ctx == NULL) yields an empty - * OakUndoCommand result. - */ - -/** @brief olive::TimelineAddTrackCommand. */ -OakUndoCommand oaktimeline_add_track_command(OakNodeTrackList list); - -/** @brief olive::TimelineRemoveTrackCommand. */ -OakUndoCommand oaktimeline_remove_track_command(OakNodeTrack track); - -/** @brief olive::TrackPlaceBlockCommand. */ -OakUndoCommand oaktimeline_place_block_command(OakNodeTrackList list, - int track_index, - OakNodeBlock block, - int64_t in_num, - int64_t in_den); - -/** @brief olive::TrackReplaceBlockWithGapCommand. */ -OakUndoCommand oaktimeline_replace_block_with_gap_command( - OakNodeTrack track, OakNodeBlock block); - -/** - * @brief The capi's move-clip assembly: gap the block's old spot and place - * it at `in` on `track_index` of `list` as ONE undoable entry - * (olive::TrackReplaceBlockWithGapCommand + olive::TrackPlaceBlockCommand - * inside a MultiUndoCommand). - */ -OakUndoCommand oaktimeline_move_block_command(OakNodeTrackList list, - int track_index, - OakNodeBlock block, - int64_t in_num, - int64_t in_den); - -/** - * @brief olive::BlockTrimCommand. `mode` is an OakTimelineMovementMode - * value (k_trim_in / k_trim_out). - */ -OakUndoCommand oaktimeline_trim_command(OakNodeTrack track, - OakNodeBlock block, - int64_t new_length_num, - int64_t new_length_den, int mode); - -/** @brief olive::BlockSplitCommand on a set of blocks at one point. */ -OakUndoCommand oaktimeline_split_command(const OakNodeBlock *blocks, - int count, int64_t point_num, - int64_t point_den); - -/** @brief olive::BlockSplitPreservingLinksCommand. */ -OakUndoCommand oaktimeline_split_preserving_links_command( - const OakNodeBlock *blocks, int count, const int64_t *point_nums, - const int64_t *point_dens, int time_count); - -/** @brief olive::TimelineRippleDeleteGapsAtRegionsCommand. */ -OakUndoCommand oaktimeline_ripple_delete_gaps_command( - OakNodeSequence sequence, const int64_t *in_nums, - const int64_t *in_dens, const int64_t *out_nums, - const int64_t *out_dens, const OakNodeTrack *tracks, int range_count); - -/** @brief olive::TrackSlideCommand. */ -OakUndoCommand oaktimeline_slide_command( - OakNodeTrack track, const OakNodeBlock *blocks, int block_count, - OakNodeBlock in_adjacent, OakNodeBlock out_adjacent, - int64_t movement_num, int64_t movement_den); - -/** @brief olive::TrackRippleRemoveAreaCommand. */ -OakUndoCommand oaktimeline_ripple_remove_area_command( - OakNodeTrack track, int64_t in_num, int64_t in_den, int64_t out_num, - int64_t out_den); - -/** @brief olive::TrackListInsertGaps. */ -OakUndoCommand oaktimeline_insert_gaps_command(OakNodeTrackList list, - int64_t point_num, - int64_t point_den, - int64_t length_num, - int64_t length_den); - -/** @brief Movement modes (olive::Timeline::MovementMode). */ -enum OakTimelineMovementMode { - OAKTIMELINE_MOVEMENT_NONE = 0, - OAKTIMELINE_MOVEMENT_MOVE = 1, - OAKTIMELINE_MOVEMENT_TRIM_IN = 2, - OAKTIMELINE_MOVEMENT_TRIM_OUT = 3 -}; - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_TIMELINE_EDIT_H diff --git a/crates/oakengine.bk/include/timeline/error.h b/crates/oakengine.bk/include/timeline/error.h deleted file mode 100644 index 39e0b1047..000000000 --- a/crates/oakengine.bk/include/timeline/error.h +++ /dev/null @@ -1,41 +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 . - -***/ - -#ifndef OAK_EDITOR_TIMELINE_ERROR_H -#define OAK_EDITOR_TIMELINE_ERROR_H - -/** - * @brief Status and error codes shared by all oaktimeline C API families. - * - * Return-code convention (mirrors engine/include/oakengine/init.h): - * 0 (OAKTIMELINE_OK) on success, a negative OAKTIMELINE_E_* error code on - * failure. String getters return the required buffer size in bytes - * (including the terminating NUL) as a non-negative value instead. - */ -#define OAKTIMELINE_ABI_VERSION 1 - -#define OAKTIMELINE_OK 0 /**< Success. */ -#define OAKTIMELINE_E_INVALID (-40001) /**< NULL handle or invalid argument. */ -#define OAKTIMELINE_E_STATE (-40002) /**< Call not valid in the current state. */ -#define OAKTIMELINE_E_FAILED (-40003) /**< The underlying operation failed. */ -#define OAKTIMELINE_E_NOT_FOUND (-40004) /**< Index out of range / entry not found. */ -#define OAKTIMELINE_E_NOMEM (-40005) /**< Allocation failed. */ - -#endif //OAK_EDITOR_TIMELINE_ERROR_H diff --git a/crates/oakengine.bk/include/timeline/marker.h b/crates/oakengine.bk/include/timeline/marker.h deleted file mode 100644 index f0b6d1049..000000000 --- a/crates/oakengine.bk/include/timeline/marker.h +++ /dev/null @@ -1,138 +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 . - -***/ - -#ifndef OAK_EDITOR_TIMELINE_MARKER_H -#define OAK_EDITOR_TIMELINE_MARKER_H - -#include "common/xmlutils.h" -#include "node/node.h" -#include "timeline/error.h" -#include "undo/undocommand.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief By-value handle to a timeline marker list - * (olive::TimelineMarkerList). - * - * Borrowed handles are obtained via oaktimeline_marker_list_of() and box - * a reference into the owning node; owning handles are created by - * oaktimeline_marker_list_create(). Either way, release with - * oaktimeline_marker_list_free() (or handle.release(handle.ctx)) when - * done — release destroys the list only for owning handles. - */ -typedef struct OakTimelineMarkerList { - void *ctx; /**< Opaque pointer to the object's box. */ - void (*addref)(void *ctx); /**< Atomically increments the box count. */ - void (*release)(void *ctx); /**< Decrements the count, frees the box. */ - uint32_t abi_version; /**< OAKTIMELINE_ABI_VERSION. */ -} OakTimelineMarkerList; - -/** - * @brief Create an owning handle to a new, empty marker list. Empty - * handle (ctx == NULL) on allocation failure. - */ -OakTimelineMarkerList oaktimeline_marker_list_create(void); - -/** - * @brief Borrowed marker list of a viewer node (sequence). Empty handle - * (ctx == NULL) for an empty node handle or when the node is not a - * viewer. - */ -OakTimelineMarkerList oaktimeline_marker_list_of(OakNodeNode owner); - -/** - * @brief Release a marker list handle (destroys the list itself only - * for owning handles). NULL / empty-handle no-op; clears - * list->ctx after releasing. - */ -void oaktimeline_marker_list_free(OakTimelineMarkerList *list); - -/** - * @brief Append a marker directly (no undo command). name may be NULL - * for an empty name. - */ -int oaktimeline_marker_add(OakTimelineMarkerList list, int in_num, - int in_den, int out_num, int out_den, - const char *name, int color); - -/** - * @brief Number of markers. Out-param convention; OAKTIMELINE_E_INVALID - * for empty/NULL arguments. - */ -int oaktimeline_marker_count(OakTimelineMarkerList list, int *out_count); - -/** - * @brief Marker at index: time as num/den pairs, color and name - * (two-stage string). OAKTIMELINE_E_NOT_FOUND when out of range. - */ -int oaktimeline_marker_at(OakTimelineMarkerList list, int index, - int *in_num, int *in_den, int *out_num, int *out_den, - int *color, char *name_buf, int buf_size); - -/** - * @brief Create a command that adds a marker (olive::MarkerAddCommand). - * - * Owned command; free with oakundo_command_free(). Redo it directly or - * push it on an undo stack. Empty handle on failure. - */ -OakUndoCommand oaktimeline_marker_add_command( - OakTimelineMarkerList list, int in_num, int in_den, int out_num, - int out_den, const char *name, int color); - -/** - * @brief Create a command that removes the marker at `index`. - * OAKTIMELINE_E_NOT_FOUND (as an empty result documented by error) is - * reported by returning an empty handle. - */ -OakUndoCommand oaktimeline_marker_remove_at_command( - OakTimelineMarkerList list, int index); - -/** - * @brief Create a command that sets a marker's time range. - */ -OakUndoCommand oaktimeline_marker_set_time_command( - OakTimelineMarkerList list, int index, int in_num, int in_den, - int out_num, int out_den); - -/** - * @brief Create a command that sets a marker's color and/or name. - * `name` may be NULL to leave the name unchanged (color still applies - * when >= 0; both NULL-name and color < 0 is a no-op error). - */ -OakUndoCommand oaktimeline_marker_set_props_command( - OakTimelineMarkerList list, int index, int color, const char *name); - -/** - * @brief Load/save the list through oakcommon XML handles. The reader - * must be positioned on the wrapping element (e.g. "markers"). - */ -int oaktimeline_marker_list_load(OakTimelineMarkerList list, - OakXmlReader reader); -int oaktimeline_marker_list_save(OakTimelineMarkerList list, - OakXmlWriter writer); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_TIMELINE_MARKER_H diff --git a/crates/oakengine.bk/include/timeline/workarea.h b/crates/oakengine.bk/include/timeline/workarea.h deleted file mode 100644 index 57058ae77..000000000 --- a/crates/oakengine.bk/include/timeline/workarea.h +++ /dev/null @@ -1,122 +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 . - -***/ - -#ifndef OAK_EDITOR_TIMELINE_WORKAREA_H -#define OAK_EDITOR_TIMELINE_WORKAREA_H - -#include "common/xmlutils.h" -#include "node/node.h" -#include "timeline/error.h" -#include "undo/undocommand.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief By-value handle to a timeline work area - * (olive::TimelineWorkArea). - * - * Borrowed handles are obtained via oaktimeline_workarea_of() and box a - * reference into the owning node; owning handles are created by - * oaktimeline_workarea_create(). Either way, release with - * oaktimeline_workarea_free() (or handle.release(handle.ctx)) when - * done — release destroys the work area only for owning handles. - */ -typedef struct OakTimelineWorkArea { - void *ctx; /**< Opaque pointer to the object's box. */ - void (*addref)(void *ctx); /**< Atomically increments the box count. */ - void (*release)(void *ctx); /**< Decrements the count, frees the box. */ - uint32_t abi_version; /**< OAKTIMELINE_ABI_VERSION. */ -} OakTimelineWorkArea; - -/** - * @brief Create an owning handle to a new, default-constructed work - * area. Empty handle (ctx == NULL) on allocation failure. - */ -OakTimelineWorkArea oaktimeline_workarea_create(void); - -/** - * @brief Borrowed work area of a viewer node (sequence). Empty handle - * (ctx == NULL) for an empty node handle or when the node is not a - * viewer. - */ -OakTimelineWorkArea oaktimeline_workarea_of(OakNodeNode owner); - -/** - * @brief Release a work area handle (destroys the work area itself only - * for owning handles). NULL / empty-handle no-op; clears w->ctx - * after releasing. - */ -void oaktimeline_workarea_free(OakTimelineWorkArea *w); - -/** - * @brief Set enabled directly (live). - */ -int oaktimeline_workarea_set_enabled(OakTimelineWorkArea w, int enabled); - -/** - * @brief Read the work area state. Out params may individually be NULL. - */ -int oaktimeline_workarea_get(OakTimelineWorkArea w, int *in_num, - int *in_den, int *out_num, int *out_den, - int *enabled); - -/** - * @brief Set the range directly (live). - */ -int oaktimeline_workarea_set_range(OakTimelineWorkArea w, int in_num, - int in_den, int out_num, int out_den); - -/** - * @brief Create a set-range command (olive::WorkareaSetRangeCommand). - * The old range must be supplied by the caller (facade knows what it - * changed from). Owned; free with oakundo_command_free(). - */ -OakUndoCommand oaktimeline_workarea_set_range_command( - OakTimelineWorkArea w, int in_num, int in_den, int out_num, - int out_den, int old_in_num, int old_in_den, int old_out_num, - int old_out_den); - -/** - * @brief Create a set-enabled command (olive::WorkareaSetEnabledCommand). - */ -OakUndoCommand oaktimeline_workarea_set_enabled_command( - OakTimelineWorkArea w, int enabled); - -/** - * @brief The reset sentinel range (TimelineWorkArea::k_reset_in/out). - */ -int oaktimeline_workarea_reset(int *in_num, int *in_den, int *out_num, - int *out_den); - -/** - * @brief Load/save through oakcommon XML handles. The reader must be - * positioned on the "workarea" element. - */ -int oaktimeline_workarea_load(OakTimelineWorkArea w, OakXmlReader reader); -int oaktimeline_workarea_save(OakTimelineWorkArea w, - OakXmlWriter writer); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_TIMELINE_WORKAREA_H diff --git a/crates/oakengine.bk/include/undo/error.h b/crates/oakengine.bk/include/undo/error.h deleted file mode 100644 index fb7075d7a..000000000 --- a/crates/oakengine.bk/include/undo/error.h +++ /dev/null @@ -1,39 +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 . - -***/ - -#ifndef OAK_EDITOR_UNDO_ERROR_H -#define OAK_EDITOR_UNDO_ERROR_H - -/** - * @brief Status and error codes shared by all oakundo C API families. - * - * Return-code convention (mirrors engine/include/oakengine/init.h): - * 0 (OAKUNDO_OK) on success, a negative OAKUNDO_E_* error code on - * failure. String getters return the required buffer size in bytes - * (including the terminating NUL) as a non-negative value instead. - */ -#define OAKUNDO_OK 0 /**< Success. */ -#define OAKUNDO_E_INVALID (-20001) /**< NULL handle or invalid argument. */ -#define OAKUNDO_E_STATE (-20002) /**< Call not valid in the current state. */ -#define OAKUNDO_E_FAILED (-20003) /**< The underlying operation failed. */ -#define OAKUNDO_E_NOT_FOUND (-20004) /**< Index out of range / entry not found. */ -#define OAKUNDO_E_NOMEM (-20005) /**< Allocation failed. */ - -#endif //OAK_EDITOR_UNDO_ERROR_H diff --git a/crates/oakengine.bk/include/undo/undocommand.h b/crates/oakengine.bk/include/undo/undocommand.h deleted file mode 100644 index 902c219d5..000000000 --- a/crates/oakengine.bk/include/undo/undocommand.h +++ /dev/null @@ -1,149 +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 . - -***/ - -#ifndef OAK_EDITOR_UNDO_UNDOCOMMAND_H -#define OAK_EDITOR_UNDO_UNDOCOMMAND_H - -#include - -#include "undo/error.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define OAKUNDO_ABI_VERSION 1 - -/** - * @brief Reference-counted handle to an undo command - * (olive::UndoCommand). - * - * The object never leaves the library that created it; every external - * reference is one of these handles. Semantics are shared_ptr-like: - * init/factory functions return a handle with count 1, addref(ctx) - * takes another reference, release(ctx) drops one and the library - * destroys the object when the count reaches zero. - * - * Pushing a command onto an OakUndoStack transfers one reference to the - * stack (the stack releases it when the command is discarded); callers - * may keep their own reference or release it right after the push. - */ -typedef struct OakUndoCommand { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKUNDO_ABI_VERSION. */ -} OakUndoCommand; - -/** - * @brief Callback table backing a caller-defined undo command. - * - * Any callback may be NULL; a NULL redo/undo makes that direction a - * no-op. free_fn is invoked when the command is destroyed (whether held - * by a stack or released directly) and releases userdata. - */ -typedef struct OakUndoCommandVtable { - void (*redo)(void *userdata); - void (*undo)(void *userdata); - void (*free_fn)(void *userdata); -} OakUndoCommandVtable; - -/** - * @brief Create an undo command backed by C callbacks. - * - * The command takes ownership of `userdata`; `vtable` is copied. - * - * @return Command handle with count 1; ctx is NULL on invalid argument - * or allocation failure. - */ -OakUndoCommand oakundo_command_init(const OakUndoCommandVtable *vtable, - void *userdata); - -/** - * @brief Create an empty multi command (olive::MultiUndoCommand). - * - * @return Command handle with count 1; ctx is NULL on allocation - * failure. - */ -OakUndoCommand oakundo_command_init_multi(void); - -/** - * @brief Add `child` to the multi command `multi`. - * - * The multi command takes one reference to the child; the caller keeps - * its own reference and may release it after the call. - * - * @return OAKUNDO_OK or a negative OAKUNDO_E_* error code. - */ -int oakundo_command_multi_add_child(OakUndoCommand multi, - OakUndoCommand child); - -/** - * @brief Query the number of children in a multi command. - * - * @param out_count Receives the result. Must not be NULL. - * - * @return OAKUNDO_OK or a negative OAKUNDO_E_* error code. - */ -int oakundo_command_multi_child_count(OakUndoCommand multi, - int *out_count); - -/** - * @brief Reference to the child at `index` of a multi command. - * - * The returned handle carries its own reference; release it with - * oakundo_command_free(). - * - * @return OAKUNDO_OK, OAKUNDO_E_NOT_FOUND for an out-of-range index, or - * another negative OAKUNDO_E_* error code. - */ -int oakundo_command_multi_child(OakUndoCommand multi, int index, - OakUndoCommand *out_child); - -/** - * @brief Execute the command's redo without a stack - * (olive::UndoCommand::redo_now semantics; a no-op if already done). - * - * @return OAKUNDO_OK or a negative OAKUNDO_E_* error code. - */ -int oakundo_command_redo_now(OakUndoCommand command); - -/** - * @brief Execute the command's undo without a stack - * (olive::UndoCommand::undo_now semantics; a no-op if not done). - * - * @return OAKUNDO_OK or a negative OAKUNDO_E_* error code. - */ -int oakundo_command_undo_now(OakUndoCommand command); - -/** - * @brief Release one reference to a command handle. - * - * Convenience wrapper around handle.release(handle.ctx): destroys the - * command when the count reaches zero. NULL handle or NULL ctx is a - * no-op; clears `command->ctx` after releasing. - */ -void oakundo_command_free(OakUndoCommand *command); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_UNDO_UNDOCOMMAND_H diff --git a/crates/oakengine.bk/include/undo/undostack.h b/crates/oakengine.bk/include/undo/undostack.h deleted file mode 100644 index 494297fae..000000000 --- a/crates/oakengine.bk/include/undo/undostack.h +++ /dev/null @@ -1,173 +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 . - -***/ - -#ifndef OAK_EDITOR_UNDO_UNDOSTACK_H -#define OAK_EDITOR_UNDO_UNDOSTACK_H - -#include - -#include "undo/error.h" -#include "undo/undocommand.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Reference-counted handle to an undo stack (olive::UndoStack). - * - * Same ownership/count semantics as OakUndoCommand (see - * undo/undocommand.h). - */ -typedef struct OakUndoStack { - void *ctx; /**< Opaque pointer to the reference-counted object. */ - void (*addref)(void *ctx); /**< Atomically increments the count. */ - void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */ - uint32_t abi_version; /**< OAKUNDO_ABI_VERSION. */ -} OakUndoStack; - -/** - * @brief Create an undo stack (count 1). - * - * A fresh stack contains a single "New/Open Project" empty command, - * matching olive::UndoStack::clear(). - * - * @return Stack handle; ctx is NULL on allocation failure. - */ -OakUndoStack oakundo_undostack_init(void); - -/** - * @brief Release one reference to an undo stack. - * - * NULL handle or NULL ctx is a no-op; clears `stack->ctx` after - * releasing. - */ -void oakundo_undostack_free(OakUndoStack *stack); - -/** - * @brief Push `command` onto the stack and execute its redo. - * - * The stack takes one reference to the command; the caller keeps its - * own reference and may release it after the call. An empty multi - * command is deleted immediately (not pushed), matching - * olive::UndoStack::push. `name` is the user-visible label (NULL - * behaves like an empty label). - * - * @return OAKUNDO_OK or a negative OAKUNDO_E_* error code. - */ -int oakundo_undostack_push(OakUndoStack stack, OakUndoCommand command, - const char *name); - -/** - * @brief Push a command that has already been executed (redo skipped). - * - * Reference rules match oakundo_undostack_push(). - * - * @return OAKUNDO_OK or a negative OAKUNDO_E_* error code. - */ -int oakundo_undostack_push_pre_executed(OakUndoStack stack, - OakUndoCommand command, - const char *name); - -/** - * @brief Undo the most recently done command, if any. - * - * @return OAKUNDO_OK or a negative OAKUNDO_E_* error code. - */ -int oakundo_undostack_undo(OakUndoStack stack); - -/** - * @brief Redo the most recently undone command, if any. - * - * @return OAKUNDO_OK or a negative OAKUNDO_E_* error code. - */ -int oakundo_undostack_redo(OakUndoStack stack); - -/** - * @brief Undo/redo until the done-command count equals `index` - * (olive::UndoStack::jump semantics). Negative values are clamped to 0. - * - * @return OAKUNDO_OK or a negative OAKUNDO_E_* error code. - */ -int oakundo_undostack_jump(OakUndoStack stack, int64_t index); - -/** - * @brief Delete all commands and push the fresh "New/Open Project" empty - * command (olive::UndoStack::clear). - * - * @return OAKUNDO_OK or a negative OAKUNDO_E_* error code. - */ -int oakundo_undostack_clear(OakUndoStack stack); - -/** - * @brief Query whether undo (redo) is currently possible. - * - * @param out_value Receives 1/0. Must not be NULL. - * - * @return OAKUNDO_OK or a negative OAKUNDO_E_* error code. - */ -int oakundo_undostack_can_undo(OakUndoStack stack, int *out_value); -int oakundo_undostack_can_redo(OakUndoStack stack, int *out_value); - -/** - * @brief Total number of history rows (done + undone commands). - * - * @param out_count Receives the result. Must not be NULL. - * - * @return OAKUNDO_OK or a negative OAKUNDO_E_* error code. - */ -int oakundo_undostack_count(OakUndoStack stack, int64_t *out_count); - -/** - * @brief Current position in the history: the number of done commands - * (rows at or above this index are undone). - * - * @param out_index Receives the result. Must not be NULL. - * - * @return OAKUNDO_OK or a negative OAKUNDO_E_* error code. - */ -int oakundo_undostack_index(OakUndoStack stack, int64_t *out_index); - -/** - * @brief Label of the history row at `row` (0-based, two-stage getter). - * - * @return Required buffer size in bytes including the terminating NUL - * (non-negative), OAKUNDO_E_NOT_FOUND for an invalid row, or - * another negative OAKUNDO_E_* error code. - */ -int oakundo_undostack_command_text(OakUndoStack stack, int64_t row, - char *buf, int buf_size); - -/** - * @brief Query whether the row at `row` is currently done (not undone). - * - * @param out_value Receives 1 (done) / 0 (undone). Must not be NULL. - * - * @return OAKUNDO_OK, OAKUNDO_E_NOT_FOUND for an invalid row, or another - * negative OAKUNDO_E_* error code. - */ -int oakundo_undostack_command_is_done(OakUndoStack stack, int64_t row, - int *out_value); - -#ifdef __cplusplus -} -#endif - -#endif //OAK_EDITOR_UNDO_UNDOSTACK_H diff --git a/crates/oakengine.bk/src/audio.rs b/crates/oakengine.bk/src/audio.rs deleted file mode 100644 index fb1c9e9ed..000000000 --- a/crates/oakengine.bk/src/audio.rs +++ /dev/null @@ -1,700 +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 . - -//! `engine/include/oakengine/audio.h` over the oakaudio module. -//! -//! The engine API is static (the singleton is implicit); the oakaudio C -//! ABI passes the manager handle explicitly, so every family call goes -//! through [`manager()`] (a borrowed handle; empty when no instance -//! exists — engine semantics then report `paNoDevice`/error as -//! documented). The borrowed `OakAudioParams*` handles are read through -//! the `oakcore_audioparams_*` accessors the facade provides itself -//! (crate::stubs::audio, folded in M12 P5 — they used to be host-provided -//! liboakcore symbols). - -use std::ffi::{c_char, c_double, c_int, c_void}; - -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, - OakEngineAudioProcessor, -}; - -/// paNoDevice — no audio device selected. -const PA_NO_DEVICE: i64 = -1; - -/// Borrowed handle of the AudioManager singleton (empty when none). -fn manager() -> CHandle { - unsafe { a::oakaudio_manager_instance() } -} - -/// Borrowed handle of the AudioManager singleton for other facade -/// families (empty ctx == NULL when none). -pub(crate) fn audio_manager_handle_raw() -> CHandle { - manager() -} - -/// `oakengine_audio_create_instance` — create the singleton (no-op when -/// it already exists). -#[no_mangle] -pub extern "C" fn oakengine_audio_create_instance() -> c_int { - guard(|| Error::from_module(unsafe { a::oakaudio_manager_create_instance() })) -} - -/// `oakengine_audio_destroy_instance` — destroy the singleton (no-op when -/// none exists). -#[no_mangle] -pub extern "C" fn oakengine_audio_destroy_instance() -> c_int { - guard_void(|| unsafe { - a::oakaudio_manager_destroy_instance(); - }); - crate::error::OAKENGINE_OK -} -/// `oakengine_audio_manager_handle` — borrowed token of the singleton -/// (NULL when none); only for event-subscription use, never freed. -#[no_mangle] -pub extern "C" fn oakengine_audio_manager_handle() -> *mut c_void { - let m = manager(); - if m.is_null() { - std::ptr::null_mut() - } else { - m.ctx - } -} - -/// `oakengine_audio_get_output_device` — paNoDevice when none/no instance. -#[no_mangle] -pub extern "C" fn oakengine_audio_get_output_device() -> i64 { - guard_i64(|| { - let m = manager(); - if m.is_null() { - return Ok(PA_NO_DEVICE); - } - Ok(i64::from(unsafe { - a::oakaudio_manager_get_output_device(m) - })) - }) -} - -/// `oakengine_audio_set_output_device`. -#[no_mangle] -pub extern "C" fn oakengine_audio_set_output_device(device: i64) -> c_int { - guard(|| { - let m = manager(); - if m.is_null() { - return Err(Error::Failed("no AudioManager instance".into())); - } - Error::from_module(unsafe { a::oakaudio_manager_set_output_device(m, device as c_int) }) - }) -} - -/// `oakengine_audio_get_input_device` — paNoDevice when none/no instance. -#[no_mangle] -pub extern "C" fn oakengine_audio_get_input_device() -> i64 { - guard_i64(|| { - let m = manager(); - if m.is_null() { - return Ok(PA_NO_DEVICE); - } - Ok(i64::from(unsafe { - a::oakaudio_manager_get_input_device(m) - })) - }) -} - -/// `oakengine_audio_set_input_device`. -#[no_mangle] -pub extern "C" fn oakengine_audio_set_input_device(device: i64) -> c_int { - guard(|| { - let m = manager(); - if m.is_null() { - return Err(Error::Failed("no AudioManager instance".into())); - } - Error::from_module(unsafe { a::oakaudio_manager_set_input_device(m, device as c_int) }) - }) -} - -/// `oakengine_audio_output_device_count` — the number of host output -/// devices; the list index is the device index -/// `oakengine_audio_set_output_device` takes. Needs no AudioManager -/// instance. -#[no_mangle] -pub extern "C" fn oakengine_audio_output_device_count() -> c_int { - guard_int(|| Ok(unsafe { a::oakaudio_output_device_count() })) -} - -/// `oakengine_audio_input_device_count` — the input side of -/// [`oakengine_audio_output_device_count`]. -#[no_mangle] -pub extern "C" fn oakengine_audio_input_device_count() -> c_int { - guard_int(|| Ok(unsafe { a::oakaudio_input_device_count() })) -} - -/// `oakengine_audio_output_device_name` — the name of output device -/// `index` (buf/size; the length excludes the NUL). -#[no_mangle] -pub unsafe extern "C" fn oakengine_audio_output_device_name( - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let rc = a::oakaudio_output_device_name(index, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(crate::handle::string_result(rc)) - } - }) -} - -/// `oakengine_audio_input_device_name` — the input side of -/// [`oakengine_audio_output_device_name`]. -#[no_mangle] -pub unsafe extern "C" fn oakengine_audio_input_device_name( - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let rc = a::oakaudio_input_device_name(index, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(crate::handle::string_result(rc)) - } - }) -} - -/// `oakengine_audio_hard_reset` — re-initialize PortAudio and refresh the -/// device lists. -#[no_mangle] -pub extern "C" fn oakengine_audio_hard_reset() -> c_int { - guard(|| { - let m = manager(); - if m.is_null() { - return Err(Error::Failed("no AudioManager instance".into())); - } - Error::from_module(unsafe { a::oakaudio_manager_hard_reset(m) }) - }) -} - -/// `oakengine_audio_clear_buffered_output`. -#[no_mangle] -pub extern "C" fn oakengine_audio_clear_buffered_output() -> c_int { - guard(|| { - let m = manager(); - if m.is_null() { - return Err(Error::Failed("no AudioManager instance".into())); - } - Error::from_module(unsafe { a::oakaudio_manager_clear_buffered_output(m) }) - }) -} - -/// `oakengine_audio_output_levels` — per-channel linear peaks of the -/// buffered, not-yet-consumed output into `peaks` (up to `capacity` -/// entries). Returns the channel count (0 when nothing is buffered). -/// The UI's audio meter reads this; there is no C++ counterpart (the Qt -/// side metered inside the output callback, which is not bridged). -#[no_mangle] -pub unsafe extern "C" fn oakengine_audio_output_levels(peaks: *mut f32, capacity: c_int) -> c_int { - guard_int(|| { - if peaks.is_null() || capacity <= 0 { - return Err(Error::Invalid); - } - let m = manager(); - if m.is_null() { - return Err(Error::Failed("no AudioManager instance".into())); - } - // The module returns the channel count (>= 0) or a negative - // OAKAUDIO_E_* code, which passes through untouched. - let n = a::oakaudio_manager_output_levels(m, peaks, capacity); - if n < 0 { - return Err(Error::Module(n)); - } - Ok(n) - }) -} - -/// `oakengine_audio_push_to_output` — queue interleaved samples described -/// by the borrowed `OakAudioParams*` handle. -#[no_mangle] -pub unsafe extern "C" fn oakengine_audio_push_to_output( - params: *const c_void, - samples: *const c_char, - samples_size: i64, - error_buf: *mut c_char, - error_buf_size: c_int, -) -> c_int { - guard(|| unsafe { - let m = manager(); - if m.is_null() || params.is_null() { - return Err(Error::Failed("no AudioManager instance".into())); - } - let rate = a::oakcore_audioparams_sample_rate(params); - let layout = a::oakcore_audioparams_channel_layout(params); - let format = a::oakcore_audioparams_format(params); - Error::from_module(a::oakaudio_manager_push_to_output( - m, - rate, - layout, - format, - samples, - samples_size, - error_buf, - error_buf_size, - )) - }) -} - -/// `oakengine_audio_stop_recording`. -#[no_mangle] -pub extern "C" fn oakengine_audio_stop_recording() -> c_int { - guard(|| { - let m = manager(); - if m.is_null() { - return Err(Error::Failed("no AudioManager instance".into())); - } - Error::from_module(unsafe { a::oakaudio_manager_stop_recording(m) }) - }) -} - -/// `oakengine_audio_stop_output`. -#[no_mangle] -pub extern "C" fn oakengine_audio_stop_output() -> c_int { - guard(|| { - let m = manager(); - if m.is_null() { - return Err(Error::Failed("no AudioManager instance".into())); - } - Error::from_module(unsafe { a::oakaudio_manager_stop_output(m) }) - }) -} - -/// `oakengine_audio_reset_output_clock`. -#[no_mangle] -pub extern "C" fn oakengine_audio_reset_output_clock() -> c_int { - guard(|| { - let m = manager(); - if m.is_null() { - return Err(Error::Failed("no AudioManager instance".into())); - } - Error::from_module(unsafe { a::oakaudio_manager_reset_output_clock(m) }) - }) -} - -/// `oakengine_audio_set_output_notify_interval`. -#[no_mangle] -pub extern "C" fn oakengine_audio_set_output_notify_interval(bytes: i64) -> c_int { - guard(|| { - let m = manager(); - if m.is_null() { - return Err(Error::Failed("no AudioManager instance".into())); - } - Error::from_module(unsafe { a::oakaudio_manager_set_output_notify_interval(m, bytes) }) - }) -} - -/// `oakengine_audio_start_recording` — takes ownership of `params` -/// (the handle is destroyed when the recording ends). -#[no_mangle] -pub unsafe extern "C" fn oakengine_audio_start_recording( - params: *mut c_void, - error_buf: *mut c_char, - error_buf_size: c_int, -) -> c_int { - guard(|| unsafe { - let m = manager(); - if m.is_null() { - return Err(Error::Failed("no AudioManager instance".into())); - } - if params.is_null() { - return Err(Error::Invalid); - } - let rc = a::oakaudio_manager_start_recording( - m, - params.cast::(), - error_buf, - error_buf_size, - ); - Error::from_module(rc) - }) -} - -// --------------------------------------------------------------------------- -// Audio synchronization -// --------------------------------------------------------------------------- - -/// `oakengine_audio_estimate_envelope_offset` — estimate the sample offset -/// between two RMS envelopes. -#[no_mangle] -pub unsafe extern "C" fn oakengine_audio_estimate_envelope_offset( - reference: *const c_double, - reference_len: c_int, - candidate: *const c_double, - candidate_len: c_int, - reference_valid: *const u8, - _reference_valid_len: c_int, - candidate_valid: *const u8, - _candidate_valid_len: c_int, - window_samples: u64, - max_offset_windows: i64, - out: *mut OakAudioWaveformOffset, -) -> c_int { - guard(|| unsafe { - if reference.is_null() || candidate.is_null() || out.is_null() { - return Err(Error::Invalid); - } - let mut result = a::OffsetResult { - offset_samples: 0, - confidence: 0.0, - valid: 0, - }; - Error::from_module(a::oakaudio_sync_estimate_envelope_offset( - reference, - reference_len, - candidate, - candidate_len, - reference_valid, - candidate_valid, - window_samples, - max_offset_windows, - &mut result, - ))?; - (*out).offset_samples = result.offset_samples; - (*out).confidence = result.confidence; - (*out).valid = result.valid; - Ok(()) - }) -} - -/// `oakengine_audio_estimate_stretch_and_offset` — rate + offset -/// correlation. -#[no_mangle] -pub unsafe extern "C" fn oakengine_audio_estimate_stretch_and_offset( - reference: *const c_double, - reference_len: c_int, - candidate: *const c_double, - candidate_len: c_int, - reference_valid: *const u8, - _reference_valid_len: c_int, - candidate_valid: *const u8, - _candidate_valid_len: c_int, - window_samples: u64, - max_offset_windows: i64, - min_rate: c_double, - max_rate: c_double, - rate_step: c_double, - out: *mut OakAudioWaveformStretchOffset, -) -> c_int { - guard(|| unsafe { - if reference.is_null() || candidate.is_null() || out.is_null() { - return Err(Error::Invalid); - } - let mut result = a::StretchOffsetResult { - rate: 0.0, - offset_samples: 0, - confidence: 0.0, - valid: 0, - }; - Error::from_module(a::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, - &mut result, - ))?; - (*out).rate = result.rate; - (*out).offset_samples = result.offset_samples; - (*out).confidence = result.confidence; - (*out).valid = result.valid; - Ok(()) - }) -} - -/// `oakengine_audio_sync_place_by_source_time` — timeline placement from -/// source timecodes. -#[no_mangle] -pub unsafe extern "C" fn oakengine_audio_sync_place_by_source_time( - reference: *const OakAudioSyncSourceClip, - candidate: *const OakAudioSyncSourceClip, - reference_timeline_in_num: i64, - reference_timeline_in_den: i64, - out: *mut OakAudioSyncPlacement, -) -> c_int { - guard(|| unsafe { - if reference.is_null() || candidate.is_null() || out.is_null() { - return Err(Error::Invalid); - } - let mut num: i64 = 0; - let mut den: i64 = 0; - let mut valid: c_int = 0; - Error::from_module(a::oakaudio_sync_place_by_source_time( - reference.cast::(), - candidate.cast::(), - reference_timeline_in_num, - reference_timeline_in_den, - &mut num, - &mut den, - &mut valid, - ))?; - (*out).timeline_in_num = num; - (*out).timeline_in_den = den; - (*out).valid = valid; - Ok(()) - }) -} - -/// `oakengine_audio_sync_place_by_waveform_offset` — timeline placement -/// from a waveform offset. -#[no_mangle] -pub unsafe extern "C" fn oakengine_audio_sync_place_by_waveform_offset( - reference_timeline_in_num: i64, - reference_timeline_in_den: i64, - candidate_offset_samples: i64, - sample_rate: c_int, - out: *mut OakAudioSyncPlacement, -) -> c_int { - guard(|| unsafe { - if out.is_null() { - return Err(Error::Invalid); - } - let mut num: i64 = 0; - let mut den: i64 = 0; - let mut valid: c_int = 0; - Error::from_module(a::oakaudio_sync_place_by_waveform_offset( - reference_timeline_in_num, - reference_timeline_in_den, - candidate_offset_samples, - sample_rate, - &mut num, - &mut den, - &mut valid, - ))?; - (*out).timeline_in_num = num; - (*out).timeline_in_den = den; - (*out).valid = valid; - Ok(()) - }) -} - -// --------------------------------------------------------------------------- -// 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 -// --------------------------------------------------------------------------- - -/// `oakengine_audio_processor_create`. -#[no_mangle] -pub extern "C" fn oakengine_audio_processor_create() -> *mut OakEngineAudioProcessor { - crate::handle::guard_ptr(|| { - let p = unsafe { a::oakaudio_processor_init() }; - if p.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(p)) - }) -} - -/// `oakengine_audio_processor_free` — NULL no-op. -#[no_mangle] -pub unsafe extern "C" fn oakengine_audio_processor_free(p: *mut OakEngineAudioProcessor) { - guard_void(|| unsafe { - free_box(p); - }) -} - -/// `oakengine_audio_processor_open` — open the conversion graph; `from`/ -/// `to` are borrowed `OakAudioParams*` handles (read via oakcore). -#[no_mangle] -pub unsafe extern "C" fn oakengine_audio_processor_open( - p: *mut OakEngineAudioProcessor, - from: *const c_void, - to: *const c_void, - tempo: c_double, -) -> c_int { - guard(|| unsafe { - let handle = unbox(p)?; - if from.is_null() || to.is_null() { - return Err(Error::Invalid); - } - let in_rate = a::oakcore_audioparams_sample_rate(from); - let in_layout = a::oakcore_audioparams_channel_layout(from); - let in_format = a::oakcore_audioparams_format(from); - let out_rate = a::oakcore_audioparams_sample_rate(to); - let out_layout = a::oakcore_audioparams_channel_layout(to); - let out_format = a::oakcore_audioparams_format(to); - Error::from_module(a::oakaudio_processor_open( - handle, in_rate, in_layout, in_format, out_rate, out_layout, out_format, tempo, - )) - }) -} - -/// `oakengine_audio_processor_close` — NULL/not-open no-op. -#[no_mangle] -pub unsafe extern "C" fn oakengine_audio_processor_close(p: *mut OakEngineAudioProcessor) -> c_int { - guard(|| unsafe { - if p.is_null() { - return Ok(()); - } - let handle = unbox(p)?; - Error::from_module(a::oakaudio_processor_close(handle)) - }) -} - -/// `oakengine_audio_processor_is_open` — 1 when open, 0 when NULL. -#[no_mangle] -pub unsafe extern "C" fn oakengine_audio_processor_is_open( - p: *mut OakEngineAudioProcessor, -) -> c_int { - crate::handle::guard_int(|| unsafe { - if p.is_null() { - return Ok(0); - } - let handle = unbox(p)?; - Ok(a::oakaudio_processor_is_open(handle)) - }) -} - -/// `oakengine_audio_processor_convert` — **not backed**: the oakaudio -/// module's processor converts planar→planar, while the engine contract -/// is planar→packed with an owned output buffer. Returns -/// `OAKENGINE_E_FAILED` until the module exposes a packed-output -/// converter. -#[no_mangle] -pub unsafe extern "C" fn oakengine_audio_processor_convert( - _p: *mut OakEngineAudioProcessor, - _in: *mut *mut f32, - _nb_in_samples: c_int, - _out_data: *mut *const c_void, - _out_size: *mut c_int, -) -> c_int { - crate::error::OAKENGINE_E_FAILED -} - -/// `oakengine_audio_processor_output_params` — **not backed**: the -/// oakaudio module has no output-params getter. Returns NULL. -#[no_mangle] -pub unsafe extern "C" fn oakengine_audio_processor_output_params( - _p: *mut OakEngineAudioProcessor, -) -> *mut c_void { - std::ptr::null_mut() -} - -/// `engine/include/oakengine/audio.h` — envelope-offset result. -#[repr(C)] -#[derive(Clone, Copy)] -pub struct OakAudioWaveformOffset { - /// Offset in samples. - pub offset_samples: i64, - /// Correlation confidence. - pub confidence: c_double, - /// 1 when usable. - pub valid: c_int, -} - -/// `engine/include/oakengine/audio.h` — rate+offset result. -#[repr(C)] -#[derive(Clone, Copy)] -pub struct OakAudioWaveformStretchOffset { - /// Playback rate. - pub rate: c_double, - /// Offset in samples. - pub offset_samples: i64, - /// Correlation confidence. - pub confidence: c_double, - /// 1 when usable. - pub valid: c_int, -} - -/// `engine/include/oakengine/audio.h` — source-clip description. -#[repr(C)] -#[derive(Clone, Copy)] -pub struct OakAudioSyncSourceClip { - /// Source start time num. - pub source_start_time_num: i64, - /// Source start time den. - pub source_start_time_den: i64, - /// Media in num. - pub media_in_num: i64, - /// Media in den. - pub media_in_den: i64, - /// 1 when source start time is meaningful. - pub has_source_start_time: c_int, -} - -/// `engine/include/oakengine/audio.h` — timeline placement result. -#[repr(C)] -#[derive(Clone, Copy)] -pub struct OakAudioSyncPlacement { - /// Timeline in-point num. - pub timeline_in_num: i64, - /// Timeline in-point den. - pub timeline_in_den: i64, - /// 1 when usable. - pub valid: c_int, -} diff --git a/crates/oakengine.bk/src/codec.rs b/crates/oakengine.bk/src/codec.rs deleted file mode 100644 index c5e677824..000000000 --- a/crates/oakengine.bk/src/codec.rs +++ /dev/null @@ -1,1489 +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 . - -//! `engine/include/oakengine/encoding.h` over the oakcodec module. -//! -//! Three parts: -//! -//! - The container/codec **metadata family** (format names/extensions, -//! per-format codec lists, pixel/sample formats, filename helpers, -//! transform matrix) maps directly onto the oakcodec crate's -//! `oakcodec_encoding_*` exports (`include/codec/format.h`). -//! - The **encoding-params handle** is a facade-owned heap box over the -//! `oakcodec_encoding_params` POD (`include/codec/encoder.h`): every -//! engine getter/setter reads/writes a POD field, so the handle can be -//! handed straight to `oakcodec_encoder_init` / -//! `oakaudio_manager_start_recording`. The `format` field uses -1 as -//! "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 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_double, c_int, c_void}; - -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}; - -/// `engine/include/oakengine/encoding.h` — the opaque encoding-params -/// handle. The facade boxes a [`ParamsBox`] here. -pub struct OakEngineEncodingParams { - _opaque: [u8; 0], -} - -/// Facade-side box behind `OakEngineEncodingParams*`: the codec POD plus -/// the encoder-specific video options (key → value), which the POD cannot -/// carry. -struct ParamsBox { - pod: EncodingParamsPOD, - video_options: HashMap, - /// 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 { - fn new() -> Self { - let mut pod = zeroed_encoding_params(); - pod.format = -1; // unset (POD 0 is a valid format, DNxHD) - ParamsBox { - pod, - video_options: HashMap::new(), - video_scaling_raw: 0, - } - } -} - -/// Borrow the params box behind an engine handle (NULL → Invalid). -unsafe fn params_ref(ptr: *const OakEngineEncodingParams) -> Result<&'static ParamsBox> { - unsafe { - if ptr.is_null() { - return Err(Error::Invalid); - } - Ok(&*(ptr as *const ParamsBox)) - } -} - -/// Borrow the params box mutably. -unsafe fn params_mut(ptr: *mut OakEngineEncodingParams) -> Result<&'static mut ParamsBox> { - unsafe { - if ptr.is_null() { - return Err(Error::Invalid); - } - Ok(&mut *(ptr as *mut ParamsBox)) - } -} - -/// Read a NUL-terminated fixed array field as a `String`. -fn field_str(field: &[u8]) -> String { - let bytes: Vec = field.iter().take_while(|c| **c != 0).copied().collect(); - String::from_utf8_lossy(&bytes).into_owned() -} - -/// Write a string into a fixed array field (truncated, NUL-terminated). -fn write_field(field: &mut [u8], value: &str) { - for (i, slot) in field.iter_mut().enumerate() { - *slot = if i < value.len() { - value.as_bytes()[i] - } else { - 0 - }; - } -} - -// --------------------------------------------------------------------------- -// Container format / codec metadata -// --------------------------------------------------------------------------- - -/// `oakengine_encoding_format_count`. -#[no_mangle] -pub extern "C" fn oakengine_encoding_format_count() -> c_int { - guard_int(|| Ok(unsafe { k::oakcodec_encoding_format_count() })) -} - -/// `oakengine_encoding_format_name` (buf/size; -1 invalid). -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_format_name( - format: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let rc = k::oakcodec_encoding_format_name(format, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_encoding_format_extension` (buf/size). -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_format_extension( - format: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let rc = k::oakcodec_encoding_format_extension(format, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_encoding_format_video_codec_count` (-1 invalid format). -#[no_mangle] -pub extern "C" fn oakengine_encoding_format_video_codec_count(format: c_int) -> c_int { - guard_int(|| Ok(unsafe { k::oakcodec_encoding_format_video_codec_count(format) })) -} - -/// `oakengine_encoding_format_video_codec_at` (-1 out of range). -#[no_mangle] -pub extern "C" fn oakengine_encoding_format_video_codec_at(format: c_int, index: c_int) -> c_int { - guard_int(|| Ok(unsafe { k::oakcodec_encoding_format_video_codec_at(format, index) })) -} - -/// `oakengine_encoding_format_audio_codec_count`. -#[no_mangle] -pub extern "C" fn oakengine_encoding_format_audio_codec_count(format: c_int) -> c_int { - guard_int(|| Ok(unsafe { k::oakcodec_encoding_format_audio_codec_count(format) })) -} - -/// `oakengine_encoding_format_audio_codec_at`. -#[no_mangle] -pub extern "C" fn oakengine_encoding_format_audio_codec_at(format: c_int, index: c_int) -> c_int { - guard_int(|| Ok(unsafe { k::oakcodec_encoding_format_audio_codec_at(format, index) })) -} - -/// `oakengine_encoding_format_subtitle_codec_count`. -#[no_mangle] -pub extern "C" fn oakengine_encoding_format_subtitle_codec_count(format: c_int) -> c_int { - guard_int(|| Ok(unsafe { k::oakcodec_encoding_format_subtitle_codec_count(format) })) -} - -/// `oakengine_encoding_format_subtitle_codec_at`. -#[no_mangle] -pub extern "C" fn oakengine_encoding_format_subtitle_codec_at( - format: c_int, - index: c_int, -) -> c_int { - guard_int(|| Ok(unsafe { k::oakcodec_encoding_format_subtitle_codec_at(format, index) })) -} - -/// `oakengine_encoding_codec_name` (buf/size; -1 invalid). -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_codec_name( - codec: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let rc = k::oakcodec_encoding_codec_name(codec, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_encoding_codec_is_still_image` (1/0). -#[no_mangle] -pub extern "C" fn oakengine_encoding_codec_is_still_image(codec: c_int) -> c_int { - guard_int(|| Ok(unsafe { k::oakcodec_encoding_codec_is_still_image(codec) })) -} - -/// `oakengine_encoding_codec_is_lossless` (1/0). -#[no_mangle] -pub extern "C" fn oakengine_encoding_codec_is_lossless(codec: c_int) -> c_int { - guard_int(|| Ok(unsafe { k::oakcodec_encoding_codec_is_lossless(codec) })) -} - -/// `oakengine_encoding_pix_fmt_count` (-1 invalid). -#[no_mangle] -pub extern "C" fn oakengine_encoding_pix_fmt_count(format: c_int, codec: c_int) -> c_int { - guard_int(|| Ok(unsafe { k::oakcodec_encoding_pix_fmt_count(format, codec) })) -} - -/// `oakengine_encoding_pix_fmt_at` (buf/size). -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_pix_fmt_at( - format: c_int, - codec: c_int, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let rc = k::oakcodec_encoding_pix_fmt_at(format, codec, index, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_encoding_pix_fmt_index` (0 = preferred when absent). -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_pix_fmt_index( - codec: c_int, - pix_fmt: *const c_char, -) -> c_int { - guard_int(|| Ok(unsafe { k::oakcodec_encoding_pix_fmt_index(codec, pix_fmt) })) -} - -/// `oakengine_encoding_sample_format_count` (-1 invalid). -#[no_mangle] -pub extern "C" fn oakengine_encoding_sample_format_count(format: c_int, codec: c_int) -> c_int { - guard_int(|| Ok(unsafe { k::oakcodec_encoding_sample_format_count(format, codec) })) -} - -/// `oakengine_encoding_sample_format_at` (-1 out of range). -#[no_mangle] -pub extern "C" fn oakengine_encoding_sample_format_at( - format: c_int, - codec: c_int, - index: c_int, -) -> c_int { - guard_int(|| Ok(unsafe { k::oakcodec_encoding_sample_format_at(format, codec, index) })) -} - -/// `oakengine_encoding_filename_contains_digit_placeholder` (1/0). -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_filename_contains_digit_placeholder( - filename: *const c_char, -) -> c_int { - guard_int(|| Ok(unsafe { k::oakcodec_encoding_filename_contains_digit_placeholder(filename) })) -} - -/// `oakengine_encoding_image_sequence_digit_count` (0 when none). -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_image_sequence_digit_count( - filename: *const c_char, -) -> c_int { - guard_int(|| Ok(unsafe { k::oakcodec_encoding_image_sequence_digit_count(filename) })) -} - -/// `oakengine_encoding_filename_remove_digit_placeholder` (buf/size). -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_filename_remove_digit_placeholder( - filename: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let rc = k::oakcodec_encoding_filename_remove_digit_placeholder(filename, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_encoding_generate_matrix` — fit/stretch/crop matrix into -/// `out16` (16 `f32`, QMatrix4x4 layout). The module computes in `f64`; -/// the facade narrows. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_generate_matrix( - method: c_int, - src_width: c_int, - src_height: c_int, - dest_width: c_int, - dest_height: c_int, - out16: *mut f32, -) -> c_int { - guard(|| unsafe { - if out16.is_null() { - return Err(Error::Invalid); - } - let mut m = [0.0_f64; 16]; - Error::from_module(k::oakcodec_encoding_generate_matrix( - method, - src_width, - src_height, - dest_width, - dest_height, - m.as_mut_ptr(), - ))?; - let m16 = out16 as *mut f32; - for i in 0..16 { - *m16.add(i) = m[i] as f32; - } - Ok(()) - }) -} - -// --------------------------------------------------------------------------- -// Encoding parameters handle -// --------------------------------------------------------------------------- - -/// `oakengine_encoding_params_create` — empty handle (all tracks disabled, -/// format unset). -#[no_mangle] -pub extern "C" fn oakengine_encoding_params_create() -> *mut OakEngineEncodingParams { - crate::handle::guard_ptr(|| { - Ok(Box::into_raw(Box::new(ParamsBox::new())) as *mut OakEngineEncodingParams) - }) -} - -/// `oakengine_encoding_params_destroy` — NULL no-op. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_destroy(params: *mut OakEngineEncodingParams) { - crate::handle::guard_void(|| unsafe { - if params.is_null() { - return; - } - drop(Box::from_raw(params as *mut ParamsBox)); - }) -} - -/// `oakengine_encoding_params_is_valid` — 1 when any track is enabled. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_is_valid( - params: *const OakEngineEncodingParams, -) -> c_int { - guard_int(|| unsafe { - let p = params_ref(params)?; - Ok( - if p.pod.video_enabled != 0 || p.pod.audio_enabled != 0 || p.pod.subtitles_enabled != 0 - { - 1 - } else { - 0 - }, - ) - }) -} - -/// `oakengine_encoding_params_set_filename`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_set_filename( - params: *mut OakEngineEncodingParams, - filename: *const c_char, -) -> c_int { - guard(|| unsafe { - let p = params_mut(params)?; - if filename.is_null() { - return Err(Error::Invalid); - } - write_field(&mut p.pod.filename, &crate::handle::read_cstr(filename)); - Ok(()) - }) -} - -/// `oakengine_encoding_params_filename` (buf/size). -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_filename( - params: *const OakEngineEncodingParams, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let p = params_ref(params)?; - Ok(crate::handle::write_string( - &field_str(&p.pod.filename), - buf, - buf_size, - )) - }) -} - -/// `oakengine_encoding_params_set_format` — rejects out-of-range values. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_set_format( - params: *mut OakEngineEncodingParams, - format: c_int, -) -> c_int { - guard(|| unsafe { - let p = params_mut(params)?; - let count = k::oakcodec_encoding_format_count(); - if format < 0 || format >= count { - return Err(Error::Invalid); - } - p.pod.format = format; - Ok(()) - }) -} - -/// `oakengine_encoding_params_format` — -1 when unset. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_format( - params: *const OakEngineEncodingParams, -) -> c_int { - guard_int(|| unsafe { - let p = params_ref(params)?; - Ok(p.pod.format) - }) -} - -/// `oakengine_encoding_params_enable_video` — copy the POD-carryable -/// fields of `video` and enable the video track. (The POD has no divider -/// field; documented deviation.) -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_enable_video( - params: *mut OakEngineEncodingParams, - video: *const OakVideoParamsPod, - codec: c_int, -) -> c_int { - guard(|| unsafe { - let p = params_mut(params)?; - if video.is_null() { - return Err(Error::Invalid); - } - let v = &*video; - p.pod.video_enabled = 1; - p.pod.video_codec = codec; - p.pod.video_width = v.width; - 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 = 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; - Ok(()) - }) -} - -/// `oakengine_encoding_params_enable_audio`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_enable_audio( - params: *mut OakEngineEncodingParams, - sample_rate: c_int, - channel_layout: u64, - sample_format: c_int, - codec: c_int, -) -> c_int { - guard(|| unsafe { - let p = params_mut(params)?; - p.pod.audio_enabled = 1; - p.pod.audio_codec = codec; - p.pod.audio_sample_rate = sample_rate; - p.pod.audio_channel_layout = channel_layout; - p.pod.audio_sample_format = crate::pods::sample_format_from_code(sample_format); - Ok(()) - }) -} - -/// `oakengine_encoding_params_enable_subtitles`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_enable_subtitles( - params: *mut OakEngineEncodingParams, - codec: c_int, -) -> c_int { - guard(|| unsafe { - let p = params_mut(params)?; - p.pod.subtitles_enabled = 1; - p.pod.subtitles_codec = codec; - p.pod.subtitles_are_sidecar = 0; - Ok(()) - }) -} - -/// `oakengine_encoding_params_enable_sidecar_subtitles`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_enable_sidecar_subtitles( - params: *mut OakEngineEncodingParams, - format: c_int, - codec: c_int, -) -> c_int { - guard(|| unsafe { - let p = params_mut(params)?; - p.pod.subtitles_enabled = 1; - p.pod.subtitles_codec = codec; - p.pod.subtitles_are_sidecar = 1; - p.pod.subtitles_sidecar_format = format; - Ok(()) - }) -} - -/// `oakengine_encoding_params_disable_video`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_disable_video( - params: *mut OakEngineEncodingParams, -) { - crate::handle::guard_void(|| unsafe { - if let Ok(p) = params_mut(params) { - p.pod.video_enabled = 0; - } - }) -} - -/// `oakengine_encoding_params_disable_audio`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_disable_audio( - params: *mut OakEngineEncodingParams, -) { - crate::handle::guard_void(|| unsafe { - if let Ok(p) = params_mut(params) { - p.pod.audio_enabled = 0; - } - }) -} - -/// `oakengine_encoding_params_disable_subtitles`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_disable_subtitles( - params: *mut OakEngineEncodingParams, -) { - crate::handle::guard_void(|| unsafe { - if let Ok(p) = params_mut(params) { - p.pod.subtitles_enabled = 0; - } - }) -} - -/// `oakengine_encoding_params_video_enabled` (1/0). -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_video_enabled( - params: *const OakEngineEncodingParams, -) -> c_int { - guard_int(|| unsafe { - let p = params_ref(params)?; - Ok(p.pod.video_enabled) - }) -} - -/// `oakengine_encoding_params_video_codec`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_video_codec( - params: *const OakEngineEncodingParams, -) -> c_int { - guard_int(|| unsafe { - let p = params_ref(params)?; - Ok(p.pod.video_codec) - }) -} - -/// `oakengine_encoding_params_get_video_params` — OAKENGINE_E_STATE when -/// video is disabled. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_get_video_params( - params: *const OakEngineEncodingParams, - out: *mut OakVideoParamsPod, -) -> c_int { - guard(|| unsafe { - let p = params_ref(params)?; - if p.pod.video_enabled == 0 { - return Err(Error::State); - } - if out.is_null() { - return Err(Error::Invalid); - } - (*out).width = p.pod.video_width; - (*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 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; - Ok(()) - }) -} - -/// `oakengine_encoding_params_audio_enabled` (1/0). -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_audio_enabled( - params: *const OakEngineEncodingParams, -) -> c_int { - guard_int(|| unsafe { - let p = params_ref(params)?; - Ok(p.pod.audio_enabled) - }) -} - -/// `oakengine_encoding_params_audio_codec`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_audio_codec( - params: *const OakEngineEncodingParams, -) -> c_int { - guard_int(|| unsafe { - let p = params_ref(params)?; - Ok(p.pod.audio_codec) - }) -} - -/// `oakengine_encoding_params_get_audio_params` — OAKENGINE_E_STATE when -/// audio is disabled. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_get_audio_params( - params: *const OakEngineEncodingParams, - sample_rate: *mut c_int, - channel_layout: *mut u64, - sample_format: *mut c_int, -) -> c_int { - guard(|| unsafe { - let p = params_ref(params)?; - if p.pod.audio_enabled == 0 { - return Err(Error::State); - } - if !sample_rate.is_null() { - *sample_rate = p.pod.audio_sample_rate; - } - if !channel_layout.is_null() { - *channel_layout = p.pod.audio_channel_layout; - } - if !sample_format.is_null() { - *sample_format = p.pod.audio_sample_format as i32; - } - Ok(()) - }) -} - -/// `oakengine_encoding_params_subtitles_enabled` (1/0). -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_subtitles_enabled( - params: *const OakEngineEncodingParams, -) -> c_int { - guard_int(|| unsafe { - let p = params_ref(params)?; - Ok(p.pod.subtitles_enabled) - }) -} - -/// `oakengine_encoding_params_subtitles_are_sidecar` (1/0). -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_subtitles_are_sidecar( - params: *const OakEngineEncodingParams, -) -> c_int { - guard_int(|| unsafe { - let p = params_ref(params)?; - Ok(p.pod.subtitles_are_sidecar) - }) -} - -/// `oakengine_encoding_params_subtitles_sidecar_format`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_subtitles_sidecar_format( - params: *const OakEngineEncodingParams, -) -> c_int { - guard_int(|| unsafe { - let p = params_ref(params)?; - Ok(p.pod.subtitles_sidecar_format) - }) -} - -/// `oakengine_encoding_params_subtitles_codec`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_subtitles_codec( - params: *const OakEngineEncodingParams, -) -> c_int { - guard_int(|| unsafe { - let p = params_ref(params)?; - Ok(p.pod.subtitles_codec) - }) -} - -macro_rules! params_i64_field { - ($set:ident, $get:ident, $field:ident) => { - /// Setter for an int64 params field (see the engine header). - #[no_mangle] - pub unsafe extern "C" fn $set(params: *mut OakEngineEncodingParams, value: i64) { - crate::handle::guard_void(|| unsafe { - if let Ok(p) = params_mut(params) { - p.pod.$field = value; - } - }) - } - - /// Getter for an int64 params field (see the engine header). - #[no_mangle] - pub unsafe extern "C" fn $get(params: *const OakEngineEncodingParams) -> i64 { - crate::handle::guard_i64(|| unsafe { - let p = params_ref(params)?; - Ok(p.pod.$field) - }) - } - }; -} - -params_i64_field!( - oakengine_encoding_params_set_video_bit_rate, - oakengine_encoding_params_video_bit_rate, - video_bit_rate -); -params_i64_field!( - oakengine_encoding_params_set_video_min_bit_rate, - oakengine_encoding_params_video_min_bit_rate, - video_min_bit_rate -); -params_i64_field!( - oakengine_encoding_params_set_video_max_bit_rate, - oakengine_encoding_params_video_max_bit_rate, - video_max_bit_rate -); -params_i64_field!( - oakengine_encoding_params_set_video_buffer_size, - oakengine_encoding_params_video_buffer_size, - video_buffer_size -); -params_i64_field!( - oakengine_encoding_params_set_audio_bit_rate, - oakengine_encoding_params_audio_bit_rate, - audio_bit_rate -); - -/// `oakengine_encoding_params_set_video_threads`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_set_video_threads( - params: *mut OakEngineEncodingParams, - threads: c_int, -) { - crate::handle::guard_void(|| unsafe { - if let Ok(p) = params_mut(params) { - p.pod.video_threads = threads; - } - }) -} - -/// `oakengine_encoding_params_video_threads`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_video_threads( - params: *const OakEngineEncodingParams, -) -> c_int { - guard_int(|| unsafe { - let p = params_ref(params)?; - Ok(p.pod.video_threads) - }) -} - -/// `oakengine_encoding_params_set_video_pix_fmt`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_set_video_pix_fmt( - params: *mut OakEngineEncodingParams, - pix_fmt: *const c_char, -) -> c_int { - guard(|| unsafe { - let p = params_mut(params)?; - if pix_fmt.is_null() { - return Err(Error::Invalid); - } - write_field(&mut p.pod.video_pix_fmt, &crate::handle::read_cstr(pix_fmt)); - Ok(()) - }) -} - -/// `oakengine_encoding_params_video_pix_fmt` (buf/size). -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_video_pix_fmt( - params: *const OakEngineEncodingParams, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let p = params_ref(params)?; - Ok(crate::handle::write_string( - &field_str(&p.pod.video_pix_fmt), - buf, - buf_size, - )) - }) -} - -/// `oakengine_encoding_params_set_video_is_image_sequence`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_set_video_is_image_sequence( - params: *mut OakEngineEncodingParams, - is_image_sequence: c_int, -) { - crate::handle::guard_void(|| unsafe { - if let Ok(p) = params_mut(params) { - p.pod.video_is_image_sequence = is_image_sequence; - } - }) -} - -/// `oakengine_encoding_params_video_is_image_sequence` (1/0). -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_video_is_image_sequence( - params: *const OakEngineEncodingParams, -) -> c_int { - guard_int(|| unsafe { - let p = params_ref(params)?; - Ok(p.pod.video_is_image_sequence) - }) -} - -/// `oakengine_encoding_params_set_color_transform`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_set_color_transform( - params: *mut OakEngineEncodingParams, - output_name: *const c_char, -) -> c_int { - guard(|| unsafe { - let p = params_mut(params)?; - let name = if output_name.is_null() { - String::new() - } else { - crate::handle::read_cstr(output_name) - }; - write_field(&mut p.pod.color_transform_output, &name); - Ok(()) - }) -} - -/// `oakengine_encoding_params_color_transform_output` (buf/size). -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_color_transform_output( - params: *const OakEngineEncodingParams, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let p = params_ref(params)?; - Ok(crate::handle::write_string( - &field_str(&p.pod.color_transform_output), - buf, - buf_size, - )) - }) -} - -/// `oakengine_encoding_params_set_export_length`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_set_export_length( - params: *mut OakEngineEncodingParams, - num: c_int, - den: c_int, -) { - crate::handle::guard_void(|| unsafe { - if let Ok(p) = params_mut(params) { - p.pod.export_length_num = num; - p.pod.export_length_den = den; - } - }) -} - -/// `oakengine_encoding_params_get_export_length`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_get_export_length( - params: *const OakEngineEncodingParams, - num: *mut c_int, - den: *mut c_int, -) -> c_int { - guard(|| unsafe { - let p = params_ref(params)?; - if !num.is_null() { - *num = p.pod.export_length_num; - } - if !den.is_null() { - *den = p.pod.export_length_den; - } - Ok(()) - }) -} - -/// `oakengine_encoding_params_set_custom_range`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_set_custom_range( - params: *mut OakEngineEncodingParams, - in_num: i64, - in_den: i64, - out_num: i64, - out_den: i64, -) { - crate::handle::guard_void(|| unsafe { - if let Ok(p) = params_mut(params) { - p.pod.has_custom_range = 1; - p.pod.custom_range_in_num = in_num; - p.pod.custom_range_in_den = in_den; - p.pod.custom_range_out_num = out_num; - p.pod.custom_range_out_den = out_den; - } - }) -} - -/// `oakengine_encoding_params_has_custom_range` (1/0). -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_has_custom_range( - params: *const OakEngineEncodingParams, -) -> c_int { - guard_int(|| unsafe { - let p = params_ref(params)?; - Ok(p.pod.has_custom_range) - }) -} - -/// `oakengine_encoding_params_get_custom_range` — E_NOT_FOUND when no -/// custom range is set. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_get_custom_range( - params: *const OakEngineEncodingParams, - in_num: *mut i64, - in_den: *mut i64, - out_num: *mut i64, - out_den: *mut i64, -) -> c_int { - guard(|| unsafe { - let p = params_ref(params)?; - if p.pod.has_custom_range == 0 { - return Err(Error::NotFound); - } - if !in_num.is_null() { - *in_num = p.pod.custom_range_in_num; - } - if !in_den.is_null() { - *in_den = p.pod.custom_range_in_den; - } - if !out_num.is_null() { - *out_num = p.pod.custom_range_out_num; - } - if !out_den.is_null() { - *out_den = p.pod.custom_range_out_den; - } - Ok(()) - }) -} - -/// `oakengine_encoding_params_set_video_scaling_method`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_set_video_scaling_method( - params: *mut OakEngineEncodingParams, - method: c_int, -) -> c_int { - guard(|| unsafe { - let p = params_mut(params)?; - // 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(()) - }) -} - -/// `oakengine_encoding_params_video_scaling_method`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_video_scaling_method( - params: *const OakEngineEncodingParams, -) -> c_int { - guard_int(|| unsafe { - let p = params_ref(params)?; - Ok(p.video_scaling_raw) - }) -} - -/// `oakengine_encoding_params_set_video_option` — stored in the -/// facade-side options map (the POD has no option fields). -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_set_video_option( - params: *mut OakEngineEncodingParams, - key: *const c_char, - value: *const c_char, -) -> c_int { - guard(|| unsafe { - let p = params_mut(params)?; - if key.is_null() || value.is_null() { - return Err(Error::Invalid); - } - p.video_options.insert( - crate::handle::read_cstr(key), - crate::handle::read_cstr(value), - ); - Ok(()) - }) -} - -/// `oakengine_encoding_params_video_option` — would-be length, or -/// OAKENGINE_E_NOT_FOUND when the key is unset. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_video_option( - params: *const OakEngineEncodingParams, - key: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let p = params_ref(params)?; - if key.is_null() { - return Err(Error::Invalid); - } - match p.video_options.get(&crate::handle::read_cstr(key)) { - Some(v) => Ok(crate::handle::write_string(v, buf, buf_size)), - None => Err(Error::NotFound), - } - }) -} - -// --------------------------------------------------------------------------- -// Presets / load-save / sequence-bound entry points (deferred) -// --------------------------------------------------------------------------- - -/// `oakengine_encoding_preset_path` — **not backed** (no preset API in the -/// oakcodec crate). Returns OAKENGINE_E_FAILED. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_preset_path( - _buf: *mut c_char, - _buf_size: c_int, -) -> c_int { - crate::error::OAKENGINE_E_FAILED -} - -/// `oakengine_encoding_preset_count` — **not backed**. Returns 0. -#[no_mangle] -pub extern "C" fn oakengine_encoding_preset_count() -> c_int { - 0 -} - -/// `oakengine_encoding_preset_name` — **not backed**. Returns -/// OAKENGINE_E_FAILED. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_preset_name( - _index: c_int, - _buf: *mut c_char, - _buf_size: c_int, -) -> c_int { - crate::error::OAKENGINE_E_FAILED -} - -/// `oakengine_encoding_params_load_file` — **not backed** (no params -/// load/save in the oakcodec crate). Returns OAKENGINE_E_FAILED. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_load_file( - _params: *mut OakEngineEncodingParams, - _path: *const c_char, -) -> c_int { - crate::error::OAKENGINE_E_FAILED -} - -/// `oakengine_encoding_params_save_file` — **not backed**. Returns -/// OAKENGINE_E_FAILED. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_save_file( - _params: *const OakEngineEncodingParams, - _path: *const c_char, -) -> c_int { - crate::error::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 = 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, -) -> c_int { - 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, - 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 -/// binding needs the deferred node/timeline families). Returns NULL. -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_get_last_used( - _seq: *mut crate::handle::OakEngineSequence, -) -> *mut OakEngineEncodingParams { - std::ptr::null_mut() -} - -/// `oakengine_encoding_params_set_last_used` — **not backed** (NULL -/// no-op; non-NULL is ignored). -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_params_set_last_used( - _seq: *mut crate::handle::OakEngineSequence, - _params: *const OakEngineEncodingParams, -) { -} - -/// `oakengine_encoding_start_audio_recording` — hand the params POD to -/// the audio manager's recording entry point (oakaudio). -#[no_mangle] -pub unsafe extern "C" fn oakengine_encoding_start_audio_recording( - params: *const OakEngineEncodingParams, - errbuf: *mut c_char, - errbuf_size: c_int, -) -> c_int { - guard(|| unsafe { - let p = params_ref(params)?; - let m = crate::audio::audio_manager_handle_raw(); - if m.is_null() { - return Err(Error::State); - } - let rc = crate::stubs::audio::oakaudio_manager_start_recording( - m, - &p.pod as *const crate::pods::EncodingParamsPOD, - errbuf, - errbuf_size, - ); - Error::from_module(rc) - }) -} diff --git a/crates/oakengine.bk/src/common.rs b/crates/oakengine.bk/src/common.rs deleted file mode 100644 index 390a53a8d..000000000 --- a/crates/oakengine.bk/src/common.rs +++ /dev/null @@ -1,628 +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 . - -//! `engine/include/oakengine/config.h` and -//! `engine/include/oakengine/videoparams.h` over the oakcommon module. -//! -//! The engine config family uses flat keys; the oakcommon store is -//! `(group, key)` — the facade passes group = NULL. Engine semantics that -//! differ from the module are honored here (a missing key reads as an -//! empty string / 0, not a module error). -//! -//! The engine videoparams family is mostly **facade-local static data** -//! (the standard frame-rate / pixel-aspect / divider tables from -//! `engine/render/videoparams.cpp`) plus POD↔handle conversion over the -//! oakcommon `OakVideoParams` handle; see the mapping notes per function. - -use std::ffi::{c_char, c_int, c_void}; -use std::sync::{Mutex, OnceLock}; - -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, -}; - -// --------------------------------------------------------------------------- -// config.h -// --------------------------------------------------------------------------- - -/// Facade copy of the registered config error handler (the module keeps -/// its own copy for load/save errors; this one backs -/// `oakengine_config_report_error`). The userdata pointer is stored as -/// `usize` so the static stays Send/Sync. -static ERROR_FN: OnceLock, usize)>>> = OnceLock::new(); - -/// `engine/include/oakengine/config.h` error callback. -pub type ConfigErrorFn = - unsafe extern "C" fn(title: *const c_char, message: *const c_char, userdata: *mut c_void); - -fn error_fn_slot() -> &'static Mutex, usize)>> { - ERROR_FN.get_or_init(|| Mutex::new(None)) -} - -/// `oakengine_config_load` — load configuration from disk. -#[no_mangle] -pub extern "C" fn oakengine_config_load() -> c_int { - guard(|| Error::from_module(unsafe { c::oakcommon_config_load() })) -} - -/// `oakengine_config_save` — save configuration to disk. -#[no_mangle] -pub extern "C" fn oakengine_config_save() -> c_int { - guard(|| Error::from_module(unsafe { c::oakcommon_config_save() })) -} - -/// `oakengine_config_get_string` — read a string value (buf/size). -/// Returns the string length, 0 when the key is missing or empty. -#[no_mangle] -pub unsafe extern "C" fn oakengine_config_get_string( - key: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - if key.is_null() { - return Err(Error::Invalid); - } - let rc = c::oakcommon_config_get(std::ptr::null(), key, buf, buf_size); - // Engine contract: a missing key reads as an empty string. - if rc == -10004 { - Ok(0) - } else if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_config_set_string` — write a string value. -#[no_mangle] -pub unsafe extern "C" fn oakengine_config_set_string( - key: *const c_char, - value: *const c_char, -) -> c_int { - guard(|| unsafe { - if key.is_null() { - return Err(Error::Invalid); - } - let value = if value.is_null() { empty_cstr() } else { value }; - c::oakcommon_config_set(std::ptr::null(), key, value); - Ok(()) - }) -} - -/// `oakengine_config_get_int` — read an integer value (fallback when the -/// key is missing or not convertible). -#[no_mangle] -pub extern "C" fn oakengine_config_get_int(key: *const c_char, default_value: i64) -> i64 { - crate::handle::guard_i64(|| unsafe { - if key.is_null() { - return Ok(default_value); - } - Ok(c::oakcommon_config_get_int64( - std::ptr::null(), - key, - default_value, - )) - }) -} - -/// `oakengine_config_set_int` — write an integer value. -#[no_mangle] -pub unsafe extern "C" fn oakengine_config_set_int(key: *const c_char, value: i64) -> c_int { - guard(|| unsafe { - if key.is_null() { - return Err(Error::Invalid); - } - c::oakcommon_config_set_int64(std::ptr::null(), key, value); - Ok(()) - }) -} - -/// `oakengine_config_set_error_handler` — register the error callback -/// (NULL clears it). Forwards to oakcommon and keeps a facade copy for -/// `oakengine_config_report_error`. -#[no_mangle] -pub extern "C" fn oakengine_config_set_error_handler( - fn_: Option, - userdata: *mut c_void, -) -> c_int { - guard(|| { - let mut slot = error_fn_slot().lock().unwrap_or_else(|e| e.into_inner()); - *slot = Some((fn_, userdata as usize)); - let rc = unsafe { c::oakcommon_config_set_error_handler(fn_, userdata) }; - if rc != 0 { - return Err(Error::Module(rc)); - } - Ok(()) - }) -} - -/// `oakengine_config_report_error` — report an error through the -/// registered handler (logged and discarded when none is set). -#[no_mangle] -pub unsafe extern "C" fn oakengine_config_report_error( - title: *const c_char, - message: *const c_char, -) -> c_int { - guard(|| unsafe { - let slot = error_fn_slot().lock().unwrap_or_else(|e| e.into_inner()); - if let Some((Some(fn_), userdata)) = *slot { - let title = if title.is_null() { empty_cstr() } else { title }; - let message = if message.is_null() { - empty_cstr() - } else { - message - }; - fn_(title, message, userdata as *mut c_void); - } - Ok(()) - }) -} - -/// Static empty C string used where the engine treats NULL as "". -static EMPTY_CSTR: std::ffi::c_char = 0; - -/// Pointer to the static empty C string. -pub(crate) fn empty_cstr() -> *const c_char { - &EMPTY_CSTR as *const c_char -} - -// --------------------------------------------------------------------------- -// videoparams.h — static tables (ported from engine/render/videoparams.cpp) -// --------------------------------------------------------------------------- - -/// Standard frame rates as num/den -/// (`VideoParams::k_supported_frame_rates`). -const SUPPORTED_FRAME_RATES: &[(c_int, c_int)] = &[ - (10, 1), - (15, 1), - (24000, 1001), - (24, 1), - (25, 1), - (30000, 1001), - (30, 1), - (48000, 1001), - (48, 1), - (50, 1), - (60000, 1001), - (60, 1), -]; - -/// Standard pixel aspect ratios as num/den -/// (`VideoParams::k_standard_pixel_aspects`). -const STANDARD_PIXEL_ASPECTS: &[(c_int, c_int)] = - &[(1, 1), (8, 9), (32, 27), (16, 15), (64, 45), (4, 3)]; - -/// Supported preview dividers (`VideoParams::k_supported_dividers`). -const SUPPORTED_DIVIDERS: &[c_int] = &[1, 2, 3, 4, 6, 8, 12, 16]; - -/// Engine-internal video channel count (RGBA). -const INTERNAL_CHANNEL_COUNT: c_int = 4; - -/// `oakengine_video_params_supported_frame_rate_count`. -#[no_mangle] -pub extern "C" fn oakengine_video_params_supported_frame_rate_count() -> c_int { - guard_int(|| Ok(SUPPORTED_FRAME_RATES.len() as c_int)) -} - -/// `oakengine_video_params_supported_frame_rate_at` — num/den at `index`. -#[no_mangle] -pub extern "C" fn oakengine_video_params_supported_frame_rate_at( - index: c_int, - num: *mut c_int, - den: *mut c_int, -) -> c_int { - guard(|| unsafe { - if num.is_null() || den.is_null() { - return Err(Error::Invalid); - } - match SUPPORTED_FRAME_RATES.get(index as usize) { - Some((n, d)) => { - *num = *n; - *den = *d; - Ok(()) - } - None => Err(Error::Invalid), - } - }) -} - -/// `oakengine_video_params_frame_rate_to_string` — label of a frame rate. -#[no_mangle] -pub unsafe extern "C" fn oakengine_video_params_frame_rate_to_string( - num: c_int, - den: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let rc = c::oakcommon_videoparams_frame_rate_to_string(num, den, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_video_params_standard_pixel_aspect_count`. -#[no_mangle] -pub extern "C" fn oakengine_video_params_standard_pixel_aspect_count() -> c_int { - guard_int(|| Ok(STANDARD_PIXEL_ASPECTS.len() as c_int)) -} - -/// `oakengine_video_params_standard_pixel_aspect_at` — num/den at `index`. -#[no_mangle] -pub extern "C" fn oakengine_video_params_standard_pixel_aspect_at( - index: c_int, - num: *mut c_int, - den: *mut c_int, -) -> c_int { - guard(|| unsafe { - if num.is_null() || den.is_null() { - return Err(Error::Invalid); - } - match STANDARD_PIXEL_ASPECTS.get(index as usize) { - Some((n, d)) => { - *num = *n; - *den = *d; - Ok(()) - } - None => Err(Error::Invalid), - } - }) -} - -/// `oakengine_video_params_standard_pixel_aspect_name` — display name of -/// the `index`-th standard pixel aspect. Built from the table the way the -/// C++ `VideoParams::standard_pixel_aspect_list()` populates the combo: -/// square = "Square", others = "num:den". -#[no_mangle] -pub unsafe extern "C" fn oakengine_video_params_standard_pixel_aspect_name( - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - match STANDARD_PIXEL_ASPECTS.get(index as usize) { - Some((1, 1)) => Ok(crate::handle::write_string("Square", buf, buf_size)), - Some((n, d)) => Ok(crate::handle::write_string( - &format!("{n}:{d}"), - buf, - buf_size, - )), - None => Err(Error::Invalid), - } - }) -} - -/// `oakengine_video_params_format_pixel_aspect_ratio_string` — format a -/// printf-style template with the pixel aspect ratio. -#[no_mangle] -pub unsafe extern "C" fn oakengine_video_params_format_pixel_aspect_ratio_string( - format: *const c_char, - num: c_int, - den: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - if format.is_null() { - return Err(Error::Invalid); - } - let template = crate::handle::read_cstr(format); - // The engine formats a single "%1" placeholder with num/den. - let rendered = if template.contains("%1") { - template.replace("%1", &format!("{num}:{den}")) - } else { - template - }; - Ok(crate::handle::write_string(&rendered, buf, buf_size)) - }) -} - -/// `oakengine_video_params_supported_divider_count`. -#[no_mangle] -pub extern "C" fn oakengine_video_params_supported_divider_count() -> c_int { - SUPPORTED_DIVIDERS.len() as c_int -} - -/// `oakengine_video_params_supported_divider_at` — divider at `index` -/// (-1 when out of range). -#[no_mangle] -pub extern "C" fn oakengine_video_params_supported_divider_at(index: c_int) -> c_int { - guard_int(|| { - Ok(match SUPPORTED_DIVIDERS.get(index as usize) { - Some(d) => *d, - None => -1, - }) - }) -} - -/// `oakengine_video_params_divider_name` — display name of a divider -/// (`VideoParams::get_name_for_divider`, ported). -#[no_mangle] -pub unsafe extern "C" fn oakengine_video_params_divider_name( - divider: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - if divider <= 0 { - return Err(Error::Invalid); - } - let rc = c::oakcommon_videoparams_get_name_for_divider(divider, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_video_params_format_is_float` — 1 when the format is float. -#[no_mangle] -pub extern "C" fn oakengine_video_params_format_is_float(format: c_int) -> c_int { - guard_int(|| Ok(unsafe { c::oakcommon_videoparams_format_is_float(format) })) -} - -/// `oakengine_video_params_pixel_format_name` — display name of a format. -#[no_mangle] -pub unsafe extern "C" fn oakengine_video_params_pixel_format_name( - format: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let rc = c::oakcommon_videoparams_get_format_name(format, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_video_params_effective_size` — divider-scaled dimensions. -#[no_mangle] -pub extern "C" fn oakengine_video_params_effective_size( - width: c_int, - height: c_int, - divider: c_int, - out_width: *mut c_int, - out_height: *mut c_int, -) -> c_int { - guard(|| unsafe { - if width <= 0 || height <= 0 || divider <= 0 { - return Err(Error::Invalid); - } - if !out_width.is_null() { - *out_width = c::oakcommon_videoparams_get_scaled_dimension(width, divider); - } - if !out_height.is_null() { - *out_height = c::oakcommon_videoparams_get_scaled_dimension(height, divider); - } - Ok(()) - }) -} - -/// `oakengine_video_params_make` — fill an `oak_video_params` POD. -#[no_mangle] -pub unsafe extern "C" fn oakengine_video_params_make( - p: *mut OakVideoParamsPod, - width: c_int, - height: c_int, - time_base_num: c_int, - time_base_den: c_int, - format: c_int, - pixel_aspect_num: c_int, - pixel_aspect_den: c_int, - interlacing: c_int, - color_range: c_int, - divider: c_int, -) -> c_int { - guard(|| unsafe { - if p.is_null() { - return Err(Error::Invalid); - } - (*p).width = width; - (*p).height = height; - (*p).time_base_num = time_base_num; - (*p).time_base_den = time_base_den; - (*p).format = format; - (*p).pixel_aspect_num = pixel_aspect_num; - (*p).pixel_aspect_den = pixel_aspect_den; - (*p).interlacing = interlacing; - (*p).color_range = color_range; - (*p).divider = divider; - (*p).video_type = 0; - (*p).premultiplied_alpha = 0; - Ok(()) - }) -} - -/// `oakengine_video_params_create` — create an engine-side VideoParams -/// from a POD (returns an opaque engine pointer; free with -/// `oakengine_video_params_free`). -#[no_mangle] -pub unsafe extern "C" fn oakengine_video_params_create( - pod: *const OakVideoParamsPod, -) -> *mut c_void { - crate::handle::guard_ptr(|| unsafe { - if pod.is_null() { - return Ok(std::ptr::null_mut()); - } - let params = c::oakcommon_videoparams_init(); - if params.is_null() { - return Ok(std::ptr::null_mut()); - } - let mut rc = c::oakcommon_videoparams_set_width(params, (*pod).width); - if rc == 0 { - rc = c::oakcommon_videoparams_set_height(params, (*pod).height); - } - if rc == 0 { - rc = c::oakcommon_videoparams_set_time_base( - params, - (*pod).time_base_num, - (*pod).time_base_den, - ); - } - if rc == 0 { - rc = c::oakcommon_videoparams_set_format(params, (*pod).format); - } - if rc == 0 { - rc = c::oakcommon_videoparams_set_pixel_aspect_ratio( - params, - (*pod).pixel_aspect_num, - (*pod).pixel_aspect_den, - ); - } - if rc == 0 { - rc = c::oakcommon_videoparams_set_interlacing(params, (*pod).interlacing); - } - if rc == 0 { - rc = c::oakcommon_videoparams_set_color_range(params, (*pod).color_range); - } - if rc == 0 { - rc = c::oakcommon_videoparams_set_divider(params, (*pod).divider); - } - if rc == 0 { - rc = c::oakcommon_videoparams_set_video_type(params, (*pod).video_type); - } - if rc == 0 { - rc = c::oakcommon_videoparams_set_premultiplied_alpha( - params, - (*pod).premultiplied_alpha, - ); - } - if rc != 0 { - let mut p = params; - c::oakcommon_videoparams_free(&mut p); - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(params).cast()) - }) -} - -/// `oakengine_video_params_free` — free a params object. -#[no_mangle] -pub unsafe extern "C" fn oakengine_video_params_free(params: *mut c_void) { - guard_void(|| unsafe { - free_box(params.cast::()); - }) -} - -/// `oakengine_video_params_equal` — 1 when all user-facing fields match. -#[no_mangle] -pub unsafe extern "C" fn oakengine_video_params_equal( - a: *const OakVideoParamsPod, - b: *const OakVideoParamsPod, -) -> c_int { - crate::handle::guard_int(|| unsafe { - if a.is_null() || b.is_null() { - return Ok(0); - } - Ok(compare_pod(&*a, &*b)) - }) -} - -fn compare_pod(a: &OakVideoParamsPod, b: &OakVideoParamsPod) -> c_int { - let same = a.width == b.width - && a.height == b.height - && a.time_base_num == b.time_base_num - && a.time_base_den == b.time_base_den - && a.format == b.format - && a.pixel_aspect_num == b.pixel_aspect_num - && a.pixel_aspect_den == b.pixel_aspect_den - && a.interlacing == b.interlacing - && a.color_range == b.color_range - && a.divider == b.divider - && a.video_type == b.video_type - && a.premultiplied_alpha == b.premultiplied_alpha; - if same { - 1 - } else { - 0 - } -} - -/// `oakengine_video_params_is_valid` — 1 when the POD describes a usable -/// video stream. -#[no_mangle] -pub unsafe extern "C" fn oakengine_video_params_is_valid(p: *const OakVideoParamsPod) -> c_int { - crate::handle::guard_int(|| unsafe { - if p.is_null() { - return Ok(0); - } - let pod = &*p; - let valid = pod.width > 0 - && pod.height > 0 - && pod.pixel_aspect_num > 0 - && pod.pixel_aspect_den > 0 - && pod.format >= 0 - && pod.time_base_den > 0; - Ok(if valid { 1 } else { 0 }) - }) -} - -/// `oakengine_video_params_bytes_per_pixel` — bytes per pixel of -/// `format` with `channels` channels. -#[no_mangle] -pub extern "C" fn oakengine_video_params_bytes_per_pixel(format: c_int, channels: c_int) -> c_int { - guard_int(|| { - Ok(unsafe { c::oakcommon_videoparams_static_get_bytes_per_pixel(format, channels) }) - }) -} - -/// `oakengine_video_params_internal_channel_count` — RGBA. -#[no_mangle] -pub extern "C" fn oakengine_video_params_internal_channel_count() -> c_int { - guard_int(|| Ok(INTERNAL_CHANNEL_COUNT)) -} - -/// `engine/include/oakengine/videoparams.h` — POD mirror of VideoParams' -/// user-facing fields. Rust mirror of `oak_video_params`. -#[repr(C)] -#[derive(Clone, Copy)] -pub struct OakVideoParamsPod { - /// Width. - pub width: c_int, - /// Height. - pub height: c_int, - /// Frame duration numerator (e.g. 1001/30000 s). - pub time_base_num: c_int, - /// Frame duration denominator. - pub time_base_den: c_int, - /// PixelFormat::Format value. - pub format: c_int, - /// Pixel aspect numerator. - pub pixel_aspect_num: c_int, - /// Pixel aspect denominator. - pub pixel_aspect_den: c_int, - /// Interlacing value. - pub interlacing: c_int, - /// ColorRange value. - pub color_range: c_int, - /// Preview resolution divider (1 = full). - pub divider: c_int, - /// VideoParams::Type value. - pub video_type: c_int, - /// 0/1 premultiplied alpha. - pub premultiplied_alpha: c_int, -} diff --git a/crates/oakengine.bk/src/deferred.rs b/crates/oakengine.bk/src/deferred.rs deleted file mode 100644 index 35fcfced2..000000000 --- a/crates/oakengine.bk/src/deferred.rs +++ /dev/null @@ -1,92 +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 . - -//! Deferred `oakengine_*` families and the reasons. -//! -//! This module exists purely as documentation: the areas below are in the -//! facade's scope (module-backed or assembly-layer) but are **not wrapped -//! yet**. Nothing here is exported. -//! -//! ## Genuinely facade-only areas (out of scope, per M9 §4) -//! -//! viewer/playback/preview/display/gizmo/app/events/exporter/disk/proxy/ -//! serializer — the liboakengine assembly layer. No files for them in this -//! crate. -//! -//! worker and ipc were in this list too until the render-worker port -//! landed: [`worker`] (`engine/include/oakengine/worker.h`) and the -//! shared-memory frame-slot transport (`engine/include/oakengine/ipc.h`, -//! the shm/framepool half) now live in this crate — see `src/worker.rs` -//! and `src/ipc.rs`. -//! -//! The node/timeline/task families were deferred while the oaknode crate -//! was a `todo!()` skeleton and oaktimeline's test-stub mocks collided -//! with the real oakundo crate in one test binary. Both blockers are -//! cleared: oaknode now implements the module C ABI, and the facade links -//! oaknode/oaktimeline/oaktask WITHOUT their `test-stubs` features (see -//! README.md "Testing"), so the real exports resolve against the -//! dev-dependency rlibs. The families now live in [`node`] -//! (`engine/include/oakengine/{node,project,footage}.h`), [`timeline`] -//! (`engine/include/oakengine/timeline.h`) and [`task`] -//! (`engine/include/oakengine/task.h`). -//! -//! ## Partial coverage within wrapped families (documented stubs) -//! -//! The wrapped families still carry documented stubs where the module -//! crates lack the C ABI surface — each stub returns its header's -//! documented failure value: -//! -//! - **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_encoding_params_get/set_last_used`) are stubs — the -//! 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` / -//! `transform_job_set_processor` are stubs — the oakrender crate -//! exposes only `color_manager_get_config`/`set_up_default_config` and -//! the processor create/convert surface. -//! - **render lut** (lut.h, 0/5 wrapped): the directory/file library is -//! facade-level over FileFunctions; the crate only enumerates supported -//! LUT extensions. -//! - **render audio buffer** (renderer.h): the buffer accessors are -//! stubs because the crate's `ticket_get_samples` path is -//! unimplemented. -//! - **node** (node.h+project.h+footage.h, 226/327 wrapped): gizmo -//! accessors, plugin messages, the QBrush getter, input properties, -//! thumbnail/waveform caches, shape/subtitle blocks, keyframe -//! enumeration (count/at/easing/remove/batch/handles-on-track — the -//! oaknode keyframe C ABI is handle-only), input flags/array/data-type -//! introspection, category/flags metadata, effect-input lookup, -//! exclusive dependencies, `node_get_data`, transform-time, dependency -//! copy, project color reference space / alongside cache path, footage -//! audio-stream info, colorspace candidates, custom proxy params, -//! source start time, stream-enabled, proxy generate. Each stub body -//! carries the one-line reason. -//! - **timeline** (timeline.h, 126/139 wrapped): the ripple-tracks -//! command, default transitions, move-track/move-clip, standalone -//! marker creation, auto-cache accessors, clip cache invalidation and -//! the multicam find/switch helpers are stubs — the oaktimeline/oaknode -//! module surfaces for them do not exist (see the stub bodies). -//! - **task** (task.h, 27/27 wrapped): `oakengine_task_create_proxy` is -//! stubbed (the oaktask crate exposes no proxy-task C creator); -//! `oakengine_task_start_time`/`is_cancelled` are facade-approximated -//! (the module has no getters). diff --git a/crates/oakengine.bk/src/error.rs b/crates/oakengine.bk/src/error.rs deleted file mode 100644 index c321c42a0..000000000 --- a/crates/oakengine.bk/src/error.rs +++ /dev/null @@ -1,155 +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 . - -//! Facade error codes, mirroring `engine/include/oakengine/init.h`. -//! -//! The facade is module 00 of the project-wide -MMCCCC scheme -//! (see `include/common/error.h`): its own codes are `-(0*10000 + CCCC)`, -//! i.e. -1..-6. Codes returned by a wrapped module call pass through -//! **untranslated** — the numeric module prefix preserves provenance -//! (e.g. -20004 is oakundo's NOT_FOUND, -30001 oaknode's INVALID) and the -//! facade never rewrites them. - -use thiserror::Error; - -/// Success. -pub const OAKENGINE_OK: i32 = 0; -/// Empty handle or invalid argument. -pub const OAKENGINE_E_INVALID: i32 = -1; -/// Call not valid in the current state. -pub const OAKENGINE_E_STATE: i32 = -2; -/// The underlying operation failed. -pub const OAKENGINE_E_FAILED: i32 = -3; -/// Index out of range / entry not found. -pub const OAKENGINE_E_NOT_FOUND: i32 = -4; -/// Allocation failed (reserved; mirrors the -MMCCCC reserved list). -pub const OAKENGINE_E_NOMEM: i32 = -5; -/// The operation was cancelled (reserved; mirrors the -MMCCCC reserved -/// list). -pub const OAKENGINE_E_CANCELLED: i32 = -6; - -/// Crate-internal result type. -pub type Result = std::result::Result; - -/// Crate-internal error. -#[derive(Debug, Error)] -pub enum Error { - /// Empty handle or invalid argument. - #[error("engine: invalid argument")] - Invalid, - /// Wrong state. - #[error("engine: call not valid in the current state")] - State, - /// The underlying operation failed (context string is log-only). - #[error("engine: operation failed: {0}")] - Failed(String), - /// Not found. - #[error("engine: not found")] - NotFound, - /// Out of memory. - #[error("engine: out of memory")] - NoMem, - /// Cancelled. - #[error("engine: cancelled")] - Cancelled, - /// A module error code that must pass through untranslated. - #[error("engine: module error code {0}")] - Module(i32), -} - -impl Error { - /// Map to the public error code. Module codes pass through verbatim. - pub fn code(&self) -> i32 { - match self { - Error::Invalid => OAKENGINE_E_INVALID, - Error::State => OAKENGINE_E_STATE, - Error::Failed(_) => OAKENGINE_E_FAILED, - Error::NotFound => OAKENGINE_E_NOT_FOUND, - Error::NoMem => OAKENGINE_E_NOMEM, - Error::Cancelled => OAKENGINE_E_CANCELLED, - Error::Module(code) => *code, - } - } - - /// Wrap a module return code. `0` (OK) never becomes an error; any - /// negative code is kept as a pass-through [`Error::Module`]. - pub fn from_module(code: i32) -> Result<()> { - if code == 0 { - Ok(()) - } else { - Err(Error::Module(code)) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// One instance of every variant (data-carrying ones get a sample - /// payload). - fn all_errors() -> Vec { - vec![ - Error::Invalid, - Error::State, - Error::Failed("boom".to_string()), - Error::NotFound, - Error::NoMem, - Error::Cancelled, - Error::Module(-20004), - ] - } - - #[test] - fn display_is_non_empty_for_every_variant() { - for e in all_errors() { - let s = e.to_string(); - assert!(!s.is_empty(), "Display produced an empty message for {e:?}"); - } - } - - #[test] - fn error_is_object_safe() { - // `Box` must be constructible for every - // variant; `source()` stays None (no wrapped downstream error). - let errors: Vec> = all_errors() - .into_iter() - .map(|e| Box::new(e) as Box) - .collect(); - for e in &errors { - assert!(!e.to_string().is_empty()); - assert!(e.source().is_none()); - } - } - - #[test] - fn code_is_unaffected_by_trait_impl() { - assert_eq!(Error::Invalid.code(), OAKENGINE_E_INVALID); - assert_eq!(Error::State.code(), OAKENGINE_E_STATE); - assert_eq!(Error::Failed("boom".to_string()).code(), OAKENGINE_E_FAILED); - assert_eq!(Error::NotFound.code(), OAKENGINE_E_NOT_FOUND); - assert_eq!(Error::NoMem.code(), OAKENGINE_E_NOMEM); - assert_eq!(Error::Cancelled.code(), OAKENGINE_E_CANCELLED); - // Module codes pass through verbatim, untranslated. - assert_eq!(Error::Module(-20004).code(), -20004); - } - - #[test] - fn from_module_wraps_negative_and_accepts_ok() { - assert!(Error::from_module(0).is_ok()); - assert_eq!(Error::from_module(-20004).unwrap_err().code(), -20004); - } -} diff --git a/crates/oakengine.bk/src/handle.rs b/crates/oakengine.bk/src/handle.rs deleted file mode 100644 index fc0aa4e6e..000000000 --- a/crates/oakengine.bk/src/handle.rs +++ /dev/null @@ -1,344 +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 . - -//! Facade scaffolding: engine opaque pointers as thin newtype wrappers -//! around module [`CHandle`] values. -//! -//! Every `OakEngine*` opaque type from `engine/include/oakengine/*.h` -//! is a `#[repr(C)]` struct holding one [`CHandle`] (the module C ABI's -//! `{ctx, addref, release, abi_version}` value handle, see -//! `include/common/handle.h`). The C caller only ever sees an opaque -//! pointer, so the field layout is ours to choose; the wrappers exist so -//! the exported `oakengine_*` signatures match the frozen headers -//! verbatim. -//! -//! A box is created by [`box_handle`] and freed by [`free_box`]: freeing -//! calls the handle's `release` (for a module-borrowed handle that only -//! releases the handle shell, never the graph-owned object) and then -//! deallocates the box. Consuming exports (`oakengine_*_free`, -//! `oakengine_undo_push`, ...) call [`free_box`]. -//! -//! String output follows the engine's buf/size convention (see -//! [`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}; -use std::panic::{catch_unwind, AssertUnwindSafe}; - -use crate::error::{Error, Result}; - -/// The shared ABI value-handle type (single-lib unification, see -/// `docs/zh/plans/riir/single-lib.md`): one canonical -/// `{ctx, addref, release, abi_version}` type in `oakcore-rs`, re-exported -/// by every module crate, so the facade can pass a handle straight into a -/// module's `pub` Rust functions without an `extern "C"` declaration. -/// `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>`); -/// - nodes, blocks, tracks, footage, sequences and folders box a -/// [`domain::NodeRef`] (`(Arc>, 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>; - - /// 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::(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::(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::(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(h: &CHandle) -> Option<&mut T> { - if h.ctx.is_null() { - return None; - } - // SAFETY: contract above; the box is an - // `oaknode::handle::RefBox`. - unsafe { Some(&mut (*(h.ctx as *mut oaknode::handle::RefBox)).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`]). -macro_rules! engine_handle { - ($($name:ident),* $(,)?) => { - $( - /// Opaque engine handle: thin newtype wrapper around a module - /// [`CHandle`] value. - #[repr(C)] - #[derive(Clone, Copy)] - pub struct $name { - /// The wrapped module handle. - pub handle: CHandle, - } - - impl EngineBox for $name { - fn boxed_new(handle: CHandle) -> Self { - $name { handle } - } - fn handle(&self) -> CHandle { - self.handle - } - } - )* - }; -} - -engine_handle! { - OakEngineAudioBuffer, - OakEngineAudioProcessor, - OakEngineBlock, - OakEngineClip, - OakEngineClipboard, - OakEngineColorConfig, - OakEngineColorManager, - OakEngineColorProcessor, - OakEngineEncodingParams, - OakEngineFootage, - OakEngineFrame, - OakEngineFrameCache, - OakEngineKeyframe, - OakEngineMarker, - OakEngineMarkerList, - OakEngineNode, - OakEngineNodeDragger, - OakEnginePlayback, - OakEnginePlaybackCache, - OakEnginePreviewRequest, - OakEngineProject, - OakEngineRenderer, - OakEngineSequence, - OakEngineTask, - OakEngineThumbnailCache, - OakEngineTrack, - OakEngineTrackList, - OakEngineTraverseDb, - OakEngineWaveformCache, - OakEngineWorkarea, -} - -/// Uniform construction/extraction surface of the engine opaque types. -pub trait EngineBox: Sized { - /// Build the wrapper from a module handle. - fn boxed_new(handle: CHandle) -> Self; - /// Extract the wrapped module handle (copy). - fn handle(&self) -> CHandle; -} - -/// Allocate a heap box for a module handle and return its raw pointer. -/// The box must later be released with [`free_box`]. -pub fn box_handle(handle: CHandle) -> *mut T { - Box::into_raw(Box::new(T::boxed_new(handle))) -} - -/// Dereference an engine opaque pointer and copy out its module handle. -/// Returns [`Error::Invalid`] for a NULL pointer or an empty handle. -/// -/// # Safety -/// `ptr` must point to a live box created by [`box_handle`] (or be -/// NULL). -pub unsafe fn unbox(ptr: *const T) -> Result { - unsafe { - if ptr.is_null() { - return Err(Error::Invalid); - } - let h = (*ptr).handle(); - if h.is_null() { - return Err(Error::Invalid); - } - Ok(h) - } -} - -/// Free a box created by [`box_handle`]: release the module handle (via -/// its `release` function pointer) and deallocate the box. NULL and -/// empty handles are no-ops. After the call `ptr` is dangling; the -/// caller must not use it again. -/// -/// # Safety -/// `ptr` must be a pointer previously returned by [`box_handle`] (or -/// NULL) and must not be freed twice. -pub unsafe fn free_box(ptr: *mut T) { - unsafe { - if ptr.is_null() { - return; - } - let handle = (*ptr).handle(); - if let Some(release) = handle.release { - release(handle.ctx); - } - drop(Box::from_raw(ptr)); - } -} - -/// Panic-catching FFI wrapper for `i32`-returning exports. -pub fn guard Result<()>>(f: F) -> c_int { - match catch_unwind(AssertUnwindSafe(f)) { - Ok(Ok(())) => crate::error::OAKENGINE_OK, - Ok(Err(e)) => e.code(), - Err(_) => crate::error::OAKENGINE_E_FAILED, - } -} - -/// Panic-catching FFI wrapper for pointer-returning exports. -pub fn guard_ptr Result<*mut T>>(f: F) -> *mut T { - match catch_unwind(AssertUnwindSafe(f)) { - Ok(Ok(p)) => p, - _ => std::ptr::null_mut(), - } -} - -/// Panic-catching FFI wrapper for `int64_t`-returning exports -/// (`OAKENGINE_E_INVALID` sentinel on error, matching the engine's -/// "no application core exists" convention). -pub fn guard_i64 Result>(f: F) -> i64 { - match catch_unwind(AssertUnwindSafe(f)) { - Ok(Ok(v)) => v, - Ok(Err(_)) => crate::error::OAKENGINE_E_INVALID as i64, - Err(_) => crate::error::OAKENGINE_E_FAILED as i64, - } -} - -/// Panic-catching FFI wrapper for void exports. -pub fn guard_void(f: F) { - let _ = catch_unwind(AssertUnwindSafe(f)); -} - -/// Panic-catching FFI wrapper for exports whose return value IS the -/// result (a count, a 1/0 flag, a required string length): the closure -/// returns the positive payload, errors are returned as negative codes. -pub fn guard_int Result>(f: F) -> c_int { - match catch_unwind(AssertUnwindSafe(f)) { - Ok(Ok(v)) => v, - Ok(Err(e)) => e.code(), - Err(_) => crate::error::OAKENGINE_E_FAILED, - } -} - -/// Write `s` into `buf` following the engine buf/size convention and -/// return the string length **excluding** the terminating NUL (the engine -/// headers' "would-be length"; module getters report len+1 and are -/// converted with [`string_result`]). A NULL `buf` or `buf_size <= 0` -/// only reports the length. `s` is truncated to `buf_size - 1` bytes when -/// it does not fit. -/// -/// # Safety -/// `buf` must point to `buf_size` writable bytes when non-NULL and -/// `buf_size > 0`. -pub unsafe fn write_string(s: &str, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { - if !buf.is_null() && buf_size > 0 { - let copy_len = s.len().min((buf_size as usize).saturating_sub(1)); - std::ptr::copy_nonoverlapping(s.as_ptr(), buf as *mut u8, copy_len); - *buf.add(copy_len) = 0; - } - } - s.len() as c_int -} - -/// Read a NUL-terminated C string; NULL yields an empty string. -/// -/// # Safety -/// `s` must be a valid NUL-terminated string, or NULL. -pub unsafe fn read_cstr(s: *const c_char) -> String { - unsafe { - if s.is_null() { - String::new() - } else { - std::ffi::CStr::from_ptr(s).to_string_lossy().into_owned() - } - } -} - -/// Convert a module two-stage getter result to the engine convention. -/// Module getters report the required buffer size **including** the -/// terminating NUL; the engine headers' buf/size convention reports the -/// string **length** (excluding the NUL, mirroring the C++ capi -/// `write_string`). Negative codes pass through untranslated. -pub fn string_result(module_ret: c_int) -> c_int { - if module_ret > 0 { - module_ret - 1 - } else { - module_ret - } -} diff --git a/crates/oakengine.bk/src/ipc.rs b/crates/oakengine.bk/src/ipc.rs deleted file mode 100644 index cb5dfac16..000000000 --- a/crates/oakengine.bk/src/ipc.rs +++ /dev/null @@ -1,2432 +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 . - -//! 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 halves: -//! -//! - **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. -//! -//! **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 -//! copied field-for-field from the C++ implementation (64-byte cache-line -//! alignment, the `Header`/`SpscRingBuffer`/`oak_frame_slot_meta` POD -//! structs). A segment written by the C++ side attaches here and vice -//! versa. -//! -//! This module is deliberately unsafe-heavy and self-contained: it touches -//! raw shared memory and raw POSIX syscalls, and everything else in the -//! 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, - /// 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) -> 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`. -pub const OAK_IPC_COLORSPACE_CAP: usize = 128; - -/// Byte alignment of every sub-region of a frame slot pool (the C++ -/// `k_align = 64`; cache-line alignment). -const K_ALIGN: usize = 64; - -/// `k_magic = 0x4F4B5350` ("OKSP") — the frame slot pool header magic. -pub const FRAMEPOOL_MAGIC: u32 = 0x4F4B5350; - -/// Round `value` up to the next multiple of `align` (power of two). -const fn align_up(value: usize, align: usize) -> usize { - (value + (align - 1)) & !(align - 1) -} - -/// `OAK_IPC_SHM_MODE_CREATE` / `OAK_IPC_SHM_MODE_ATTACH` (ipc.h). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ShmMode { - /// Create (and own) the segment. Fails if it already exists; the owner - /// unlinks it on close. - Create, - /// Attach to a segment created by the peer. Does not unlink on close. - Attach, -} - -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, - _ => ShmMode::Attach, - } - } -} - -/// Per-slot metadata describing the frame currently occupying a slot — -/// field-for-field `oak_frame_slot_meta` from `engine/include/oakengine/ipc.h`. -/// -/// This POD lives in shared memory alongside the pixel data and is part of -/// the version-1 wire protocol; `#[repr(C)]` keeps the C ABI layout. -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct FrameSlotMeta { - /// Caller-defined tag (ticket id, or footage stream hash). - pub id: i64, - /// Frame timestamp numerator. - pub time_num: i64, - /// Frame timestamp denominator. - pub time_den: i64, - /// Frame width. - pub width: i32, - /// Frame height. - pub height: i32, - /// `PixelFormat::Format` value. - pub format: i32, - /// Channel count. - pub channel_count: i32, - /// Bytes per scanline (stride). - pub linesize: i32, - /// Valid bytes written into the slot's data block. - pub data_size: i32, - /// Input colorspace name. - pub colorspace: [c_char; OAK_IPC_COLORSPACE_CAP], -} - -impl Default for FrameSlotMeta { - fn default() -> Self { - FrameSlotMeta { - id: 0, - time_num: 0, - time_den: 0, - width: 0, - height: 0, - format: 0, - channel_count: 0, - linesize: 0, - data_size: 0, - colorspace: [0; OAK_IPC_COLORSPACE_CAP], - } - } -} - -/// `sizeof(oak_frame_slot_meta)` (8+8+8 + 4*6 + 128). -const FRAME_SLOT_META_SIZE: usize = 176; - -// --------------------------------------------------------------------------- -// SpscRingBuffer -// --------------------------------------------------------------------------- - -/// A lock-free single-producer / single-consumer ring buffer of `u32` -/// indices, living in shared memory — the port of -/// `engine/include/oakengine/spscringbuffer.h`. -/// -/// Layout (offsets from the buffer base, matching the C++ class): -/// -/// ```text -/// 0 head_ u32 producer cursor (relaxed read, release write) -/// 4 tail_ u32 consumer cursor (relaxed read, release write) -/// 8 capacity_ u32 slot count (written once by create()) -/// 12 slots u32[capacity] -/// ``` -/// -/// One slot is always left empty to disambiguate full and empty, so a -/// buffer with `capacity` slots holds at most `capacity - 1` live entries. -/// The payload is a `u32` slot index — never a pointer. -/// -/// `SpscRingBuffer` is a thin view over a raw pointer; it is `Copy` and -/// owns nothing. All methods are `unsafe` because they read and write the -/// shared segment concurrently with a peer process. -#[derive(Clone, Copy)] -pub struct SpscRingBuffer { - /// Base of the ring header (`head_` at offset 0). - base: *mut u8, -} - -// The shared memory the ring lives in is usable from any thread of the -// local process; synchronization with the peer is the ring's own atomics. -unsafe impl Send for SpscRingBuffer {} -unsafe impl Sync for SpscRingBuffer {} - -impl SpscRingBuffer { - /// `sizeof(SpscRingBuffer)` — header bytes before the slot array. - pub const HEADER_BYTES: usize = 12; - - /// Total bytes required for the header plus `capacity` index slots - /// (`SpscRingBuffer::bytes_needed`). - pub fn bytes_needed(capacity: u32) -> usize { - Self::HEADER_BYTES + capacity as usize * 4 - } - - /// In-place construct a ring header at `mem` with `capacity` index - /// slots. `mem` must provide at least [`Self::bytes_needed`] bytes and - /// be suitably aligned (mmap-backed segments are). Done exactly once by - /// whichever process owns the segment's creation; the peer uses - /// [`Self::attach`] instead. - /// - /// # Safety - /// `mem` must be a valid, writable, aligned buffer of at least - /// [`Self::bytes_needed`] bytes, and must not be concurrently written - /// during this call. - pub unsafe fn create(mem: *mut u8, capacity: u32) -> SpscRingBuffer { - let ring = SpscRingBuffer { base: mem }; - unsafe { - ring.store_capacity(capacity); - ring.head().store(0, Ordering::Relaxed); - ring.tail().store(0, Ordering::Relaxed); - for i in 0..capacity as usize { - *ring.slot_ptr(i) = 0; - } - } - ring - } - - /// Re-interpret already-initialized shared memory as a ring buffer - /// (peer-process side). No writes are performed. - /// - /// # Safety - /// `mem` must point to a buffer previously initialized by - /// [`Self::create`] (or an ABI-identical C++ side) that stays mapped - /// for as long as this view is used. - pub unsafe fn attach(mem: *mut u8) -> SpscRingBuffer { - SpscRingBuffer { base: mem } - } - - /// The ring's capacity (slot count). - /// - /// # Safety - /// `self` must point at a live ring (created or attached). - pub unsafe fn capacity(&self) -> u32 { - unsafe { (self.base.add(8) as *const u32).read() } - } - - /// Producer side: enqueue an index. Returns false if the buffer is full. - /// - /// # Safety - /// Exactly one producer may call this concurrently with exactly one - /// consumer calling [`Self::pop`]; the ring must be live. - pub unsafe fn push(&self, value: u32) -> bool { - unsafe { - let head = self.head().load(Ordering::Relaxed); - let next = self.increment(head); - if next == self.tail().load(Ordering::Acquire) { - return false; - } - *self.slot_ptr(head as usize) = value; - self.head().store(next, Ordering::Release); - } - true - } - - /// Consumer side: dequeue an index into `out`. Returns false if the - /// buffer is empty. - /// - /// # Safety - /// Exactly one consumer may call this concurrently with exactly one - /// producer calling [`Self::push`]; the ring must be live. - pub unsafe fn pop(&self, out: &mut u32) -> bool { - unsafe { - let tail = self.tail().load(Ordering::Relaxed); - if tail == self.head().load(Ordering::Acquire) { - return false; - } - *out = *self.slot_ptr(tail as usize); - self.tail().store(self.increment(tail), Ordering::Release); - } - true - } - - /// Approximate number of entries currently queued; may be stale the - /// instant it returns. For metrics/backpressure, not correctness. - /// - /// # Safety - /// The ring must be live. - pub unsafe fn size_approx(&self) -> u32 { - unsafe { - let head = self.head().load(Ordering::Acquire); - let tail = self.tail().load(Ordering::Acquire); - let cap = self.capacity(); - (head + cap - tail) % cap - } - } - - /// Approximate empty check (see [`Self::size_approx`]). - /// - /// # Safety - /// The ring must be live. - pub unsafe fn is_empty_approx(&self) -> bool { - unsafe { self.head().load(Ordering::Acquire) == self.tail().load(Ordering::Acquire) } - } - - #[inline] - fn increment(&self, index: u32) -> u32 { - // `capacity_` is small; this avoids requiring a power-of-two capacity. - unsafe { (index + 1) % self.capacity() } - } - - #[inline] - unsafe fn head(&self) -> &AtomicU32 { - unsafe { &*(self.base as *const AtomicU32) } - } - - #[inline] - unsafe fn tail(&self) -> &AtomicU32 { - unsafe { &*(self.base.add(4) as *const AtomicU32) } - } - - #[inline] - unsafe fn store_capacity(&self, capacity: u32) { - unsafe { *(self.base.add(8) as *mut u32) = capacity }; - } - - #[inline] - unsafe fn slot_ptr(&self, index: usize) -> *mut u32 { - unsafe { self.base.add(Self::HEADER_BYTES + index * 4) as *mut u32 } - } -} - -// --------------------------------------------------------------------------- -// FrameSlotPool -// --------------------------------------------------------------------------- - -/// Pool header written by create() and read back by attach(). Field-for- -/// field the C++ `FrameSlotPool::Header` (offsets: 0,4,8,16,24,32,40; -/// 48 bytes total). -#[repr(C)] -struct PoolHeader { - magic: u32, - slot_count: u32, - slot_data_bytes: u64, - free_ring_offset: u64, - ready_ring_offset: u64, - meta_offset: u64, - data_offset: u64, -} - -const POOL_HEADER_SIZE: usize = 48; - -/// A fixed-size pool of equal-sized frame slots in shared memory with -/// lock-free hand-off — the port of the C++ `FrameSlotPool` -/// (`engine/src/oliveimpl/render/ipc/frameslotpool.{h,cpp}`). -/// -/// One pool models a single direction of frame flow. It does NOT own the -/// memory; it is a view over a mapped [`SharedMemoryRegion`] (or any -/// ABI-identical segment). Lifecycle: the filler `acquire`s a free slot, -/// writes meta + pixels, then `publish`es it; the drainer `consume`s the -/// next ready slot, reads it, and `release`s it back to the free ring. -/// -/// [`FrameSlotPool`] is `Clone` — the clone is another view of the same -/// segment (the C++ `copy()`), useful to hand both sides a handle without -/// owning the mapping twice. -pub struct FrameSlotPool { - /// Segment base. - base: *mut u8, - /// The pool header at `base + 0`. - header: *mut PoolHeader, - /// Free-ring view (filler pops, drainer pushes). - free_ring: SpscRingBuffer, - /// Ready-ring view (filler pushes, drainer pops). - ready_ring: SpscRingBuffer, - /// Metadata array at `base + meta_offset`. - meta: *mut FrameSlotMeta, - /// Pixel data blocks at `base + data_offset`. - data: *mut u8, -} - -// Views into shared memory are safe to share within the process; the rings -// carry their own synchronization. -unsafe impl Send for FrameSlotPool {} -unsafe impl Sync for FrameSlotPool {} - -impl Clone for FrameSlotPool { - fn clone(&self) -> FrameSlotPool { - FrameSlotPool { - base: self.base, - header: self.header, - free_ring: self.free_ring, - ready_ring: self.ready_ring, - meta: self.meta, - data: self.data, - } - } -} - -impl FrameSlotPool { - /// Total bytes a region must provide to back a pool of - /// `slot_count` x `slot_data_bytes` - /// (`FrameSlotPool::bytes_needed`). - pub fn bytes_needed(slot_count: u32, slot_data_bytes: usize) -> usize { - let ring_cap = slot_count + 1; - let mut total = align_up(POOL_HEADER_SIZE, K_ALIGN); - let ring_bytes = align_up(SpscRingBuffer::bytes_needed(ring_cap), K_ALIGN); - total += ring_bytes; // free ring - total += ring_bytes; // ready ring - total += align_up(FRAME_SLOT_META_SIZE * slot_count as usize, K_ALIGN); // metadata - total += align_up(slot_data_bytes, K_ALIGN) * slot_count as usize; // pixel data - total - } - - /// Lay out and initialize a brand-new pool over `mem` (owner side, once). - /// - /// Writes the header, initializes both rings, seeds the free ring with - /// every slot index and zeroes the metadata. `mem` must provide at - /// least [`Self::bytes_needed`] bytes of writable, aligned memory (an - /// mmap-backed segment) and must outlive the returned pool. - /// - /// # Safety - /// `mem` must be a valid, writable, aligned buffer of at least - /// [`Self::bytes_needed`] bytes, not concurrently written during this - /// call. - pub unsafe fn create(mem: *mut u8, slot_count: u32, slot_data_bytes: usize) -> FrameSlotPool { - let ring_cap = slot_count + 1; - let free_off = align_up(POOL_HEADER_SIZE, K_ALIGN); - let ready_off = free_off + align_up(SpscRingBuffer::bytes_needed(ring_cap), K_ALIGN); - let meta_off = ready_off + align_up(SpscRingBuffer::bytes_needed(ring_cap), K_ALIGN); - let data_off = meta_off + align_up(FRAME_SLOT_META_SIZE * slot_count as usize, K_ALIGN); - - let pool = unsafe { - FrameSlotPool { - base: mem, - header: mem as *mut PoolHeader, - free_ring: SpscRingBuffer::create(mem.add(free_off), ring_cap), - ready_ring: SpscRingBuffer::create(mem.add(ready_off), ring_cap), - meta: mem.add(meta_off) as *mut FrameSlotMeta, - data: mem.add(data_off), - } - }; - unsafe { - (*pool.header).magic = FRAMEPOOL_MAGIC; - (*pool.header).slot_count = slot_count; - (*pool.header).slot_data_bytes = slot_data_bytes as u64; - (*pool.header).free_ring_offset = free_off as u64; - (*pool.header).ready_ring_offset = ready_off as u64; - (*pool.header).meta_offset = meta_off as u64; - (*pool.header).data_offset = data_off as u64; - } - // `ptr::write_bytes` counts in elements of T, so cast to bytes. - unsafe { - ptr::write_bytes( - pool.meta as *mut u8, - 0, - slot_count as usize * std::mem::size_of::(), - ); - } - // Seed the free ring with every slot index so the filler can - // acquire() immediately. - for i in 0..slot_count { - unsafe { pool.free_ring.push(i) }; - } - pool - } - - /// Map an existing, already-initialized pool (peer side). - /// - /// Reads the geometry from the in-memory header written by - /// [`Self::create`]; the returned pool reports `is_valid() == false` - /// when the magic does not match. - /// - /// # Safety - /// `mem` must point to a mapped segment that either contains a pool - /// initialized by [`Self::create`] (or an ABI-identical C++ side) or is - /// an arbitrary buffer whose first 4 bytes we must be able to read. - pub unsafe fn attach(mem: *mut u8) -> FrameSlotPool { - if mem.is_null() { - return FrameSlotPool::invalid(); - } - let header = mem as *mut PoolHeader; - // SAFETY: `mem` is a live mapping of at least the header size. - if unsafe { (*header).magic } != FRAMEPOOL_MAGIC { - return FrameSlotPool::invalid(); - } - let pool = unsafe { - FrameSlotPool { - base: mem, - header, - free_ring: SpscRingBuffer::attach(mem.add((*header).free_ring_offset as usize)), - ready_ring: SpscRingBuffer::attach(mem.add((*header).ready_ring_offset as usize)), - meta: mem.add((*header).meta_offset as usize) as *mut FrameSlotMeta, - data: mem.add((*header).data_offset as usize), - } - }; - pool - } - - /// An invalid pool (attach on a non-pool segment). - fn invalid() -> FrameSlotPool { - FrameSlotPool { - base: ptr::null_mut(), - header: ptr::null_mut(), - free_ring: SpscRingBuffer { - base: ptr::null_mut(), - }, - ready_ring: SpscRingBuffer { - base: ptr::null_mut(), - }, - meta: ptr::null_mut(), - data: ptr::null_mut(), - } - } - - /// True when the pool was attached to a segment containing a valid pool - /// header. - pub fn is_valid(&self) -> bool { - !self.header.is_null() - } - - /// Number of slots in the pool (0 for an invalid pool). - pub fn slot_count(&self) -> u32 { - if self.is_valid() { - unsafe { (*self.header).slot_count } - } else { - 0 - } - } - - /// Bytes available in every slot's pixel-data block (0 for invalid). - pub fn slot_data_bytes(&self) -> usize { - if self.is_valid() { - unsafe { (*self.header).slot_data_bytes as usize } - } else { - 0 - } - } - - /// Byte stride between consecutive slot data blocks. - fn slot_stride(&self) -> usize { - align_up(self.slot_data_bytes(), K_ALIGN) - } - - // ---- Filler side ---- - - /// Take ownership of a free slot. Returns false (leaving `index` - /// untouched) if none is free. - /// - /// # Safety - /// The pool must be a valid view of a live segment. - pub unsafe fn acquire(&self, index: &mut u32) -> bool { - unsafe { self.free_ring.pop(index) } - } - - /// Pointer to a slot's pixel data block (`slot_data_bytes` available). - /// - /// # Safety - /// `index` must be in `0..slot_count`; the pool must be a valid view of - /// a live segment. - pub unsafe fn slot_data(&self, index: u32) -> *mut u8 { - unsafe { self.data.add(index as usize * self.slot_stride()) } - } - - /// Mutable metadata for a slot. The filler writes this before - /// [`Self::publish`]. The returned pointer addresses shared memory; it - /// is borrowed, not owned. - /// - /// # Safety - /// `index` must be in `0..slot_count`; the pool must be a valid view of - /// a live segment. - pub unsafe fn meta(&self, index: u32) -> *mut FrameSlotMeta { - unsafe { self.meta.add(index as usize) } - } - - /// Publish a filled slot to the drainer. Must follow a successful - /// [`Self::acquire`] of `index`. Returns false if the ready ring is - /// full (the filler must then release the slot and retry later). - /// - /// # Safety - /// `index` must be a slot previously acquired and not yet released. - pub unsafe fn publish(&self, index: u32) -> bool { - unsafe { self.ready_ring.push(index) } - } - - // ---- Drainer side ---- - - /// Take the next published slot. Returns false if nothing is ready. - /// - /// # Safety - /// The pool must be a valid view of a live segment. - pub unsafe fn consume(&self, index: &mut u32) -> bool { - unsafe { self.ready_ring.pop(index) } - } - - /// Return a consumed slot to the free pool for reuse. Must follow a - /// successful [`Self::consume`] of `index`. Returns false if the free - /// ring is full (the drainer must not release the slot yet). - /// - /// # Safety - /// `index` must be a slot previously consumed and not yet re-acquired. - pub unsafe fn release(&self, index: u32) -> bool { - unsafe { self.free_ring.push(index) } - } - - /// Immutable metadata for a slot (drainer side). - /// - /// # Safety - /// `index` must be in `0..slot_count`; the pool must be a valid view of - /// a live segment. - pub unsafe fn meta_const(&self, index: u32) -> *const FrameSlotMeta { - unsafe { self.meta.add(index as usize) } - } - - /// Immutable pixel data for a slot. - /// - /// # Safety - /// `index` must be in `0..slot_count`; the pool must be a valid view of - /// a live segment. - pub unsafe fn slot_data_const(&self, index: u32) -> *const u8 { - unsafe { self.data.add(index as usize * self.slot_stride()) } - } -} - -// --------------------------------------------------------------------------- -// SharedMemoryRegion -// --------------------------------------------------------------------------- - -/// A named, fixed-size POSIX shared-memory segment mapped into the process -/// address space — the port of the C++ `SharedMemoryRegion` -/// (`engine/render/ipc/sharedmemoryregion.cpp`). -/// -/// One process opens the segment in [`ShmMode::Create`] (owner: fails if -/// the name already exists, zeroes the mapping, unlinks on close); the -/// peer opens the same key in [`ShmMode::Attach`]. The mapping is a raw -/// contiguous byte range; the ring buffers and frame slot pools are laid -/// out inside it. Nothing here is locked — synchronization is entirely the -/// caller's responsibility via the lock-free structures placed in the -/// mapping. -pub struct SharedMemoryRegion { - /// The key the region was opened with (no leading slash). - key: String, - /// Requested mapping size in bytes. - size: usize, - /// The mmap'd data pointer; null when invalid. - data: *mut u8, - /// File descriptor from `shm_open` (-1 when invalid). - fd: i32, - /// Open mode. - mode: ShmMode, - /// Human-readable reason of the last failed open. - error: String, - /// The platform-prefixed name actually passed to `shm_open`. - shm_name: String, -} - -impl SharedMemoryRegion { - /// An empty (invalid) region. - pub fn new() -> SharedMemoryRegion { - SharedMemoryRegion { - key: String::new(), - size: 0, - data: ptr::null_mut(), - fd: -1, - mode: ShmMode::Attach, - error: String::new(), - shm_name: String::new(), - } - } - - /// Build a unique segment key for a worker, e.g. - /// "olive-rw--" (`SharedMemoryRegion::make_key`). - /// Centralized so the owner and the spawned worker agree on the same - /// name. - pub fn make_key(owner_pid: i64, worker_index: i32) -> String { - format!("olive-rw-{owner_pid}-{worker_index}") - } - - /// Open the segment identified by `key` with the given `size` in bytes. - /// - /// `key` is a short identifier (no leading slash needed; the platform - /// prefix is added internally). Returns true on success; on failure - /// [`Self::error`] carries a human-readable reason. An existing region - /// is closed first. - pub fn open(&mut self, key: &str, size: usize, mode: ShmMode) -> bool { - self.close(); - self.key = key.to_string(); - self.size = size; - self.mode = mode; - - // POSIX shared-memory names must start with a single slash and - // contain no others. - let shm_name = format!("/{}", key.replace('/', "_")); - let name_c = match std::ffi::CString::new(shm_name.clone()) { - Ok(c) => c, - Err(_) => { - self.error = format!("invalid shm key {key:?} (contains NUL)"); - return false; - } - }; - self.shm_name = shm_name; - - let mut oflag = libc::O_RDWR; - if mode == ShmMode::Create { - oflag |= libc::O_CREAT | libc::O_EXCL; - // Clear any stale segment left by a crashed previous run with - // the same name. - unsafe { libc::shm_unlink(name_c.as_ptr()) }; - } - - let fd = unsafe { libc::shm_open(name_c.as_ptr(), oflag, 0o600) }; - if fd < 0 { - self.error = format!( - "shm_open({}) failed: {}", - self.shm_name, - std::io::Error::last_os_error() - ); - return false; - } - self.fd = fd; - - if mode == ShmMode::Create { - if unsafe { libc::ftruncate(fd, size as libc::off_t) } != 0 { - self.error = format!("ftruncate failed: {}", std::io::Error::last_os_error()); - self.close(); - return false; - } - } else { - // mmap() succeeds even beyond the real segment size and only - // faults (SIGBUS) on access, so verify the segment is large - // enough up front. - let mut st: libc::stat = unsafe { std::mem::zeroed() }; - if unsafe { libc::fstat(fd, &mut st) } != 0 { - self.error = format!("fstat failed: {}", std::io::Error::last_os_error()); - self.close(); - return false; - } - if (st.st_size as usize) < size { - self.error = format!( - "shared memory segment is {} bytes, smaller than the requested {}", - st.st_size, size - ); - self.close(); - return false; - } - } - - let data = unsafe { - libc::mmap( - ptr::null_mut(), - size, - libc::PROT_READ | libc::PROT_WRITE, - libc::MAP_SHARED, - fd, - 0, - ) - }; - if data == libc::MAP_FAILED { - self.error = format!("mmap failed: {}", std::io::Error::last_os_error()); - self.close(); - return false; - } - self.data = data as *mut u8; - self.error.clear(); - - if mode == ShmMode::Create { - unsafe { ptr::write_bytes(self.data, 0, size) }; - } - true - } - - /// 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 std::ffi::c_void, self.size) }; - self.data = ptr::null_mut(); - } - if self.fd >= 0 { - unsafe { libc::close(self.fd) }; - self.fd = -1; - } - if self.mode == ShmMode::Create && !self.shm_name.is_empty() { - // Only the owner unlinks, so the name is freed once both sides - // have unmapped. - if let Ok(c) = std::ffi::CString::new(self.shm_name.clone()) { - unsafe { libc::shm_unlink(c.as_ptr()) }; - } - self.shm_name.clear(); - } - self.size = 0; - } - - /// True when the region holds a live mapping. - pub fn is_valid(&self) -> bool { - !self.data.is_null() - } - - /// The mapped data pointer (null when invalid). - pub fn data(&self) -> *mut u8 { - self.data - } - - /// The mapping size in bytes. - pub fn size(&self) -> usize { - self.size - } - - /// The key the region was opened with. - pub fn key(&self) -> &str { - &self.key - } - - /// Human-readable reason of the last failed open. - pub fn error(&self) -> &str { - &self.error - } -} - -impl Default for SharedMemoryRegion { - fn default() -> Self { - SharedMemoryRegion::new() - } -} - -impl Drop for SharedMemoryRegion { - fn drop(&mut self) { - self.close(); - } -} - -#[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) -// --------------------------------------------------------------------------- - -/// Opaque `OakSharedMemoryRegion` handle (ipc.h). A facade-owned box around -/// a [`SharedMemoryRegion`]; the C caller only ever sees the pointer. -#[repr(C)] -pub struct OakSharedMemoryRegion { - _opaque: [u8; 0], -} - -/// Opaque `OakFrameSlotPool` handle (ipc.h). A facade-owned box around a -/// [`FrameSlotPool`] view. -#[repr(C)] -pub struct OakFrameSlotPool { - _opaque: [u8; 0], -} - -/// `oakengine_ipc_shm_create` — allocate an empty (invalid) region object. -#[no_mangle] -pub extern "C" fn oakengine_ipc_shm_create() -> *mut OakSharedMemoryRegion { - guard_ptr(|| { - let region = Box::new(SharedMemoryRegion::new()); - Ok(Box::into_raw(region) as *mut OakSharedMemoryRegion) - }) -} - -/// `oakengine_ipc_shm_free` — NULL no-op. -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_shm_free(self_: *mut OakSharedMemoryRegion) { - if self_.is_null() { - return; - } - // SAFETY: `self_` was produced by `oakengine_ipc_shm_create` and is - // not used after this. - unsafe { drop(Box::from_raw(self_ as *mut SharedMemoryRegion)) }; -} - -/// `oakengine_ipc_shm_open` — open the segment; 1 on success, 0 on failure -/// (`oakengine_ipc_shm_error` carries the reason). -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_shm_open( - self_: *mut OakSharedMemoryRegion, - key: *const c_char, - size: usize, - mode: c_int, -) -> c_int { - crate::handle::guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let key = crate::handle::read_cstr(key); - let region = &mut *(self_ as *mut SharedMemoryRegion); - Ok(region.open(&key, size, ShmMode::from_c(mode)) as c_int) - }) -} - -/// `oakengine_ipc_shm_close` — unmap and (if owner) unlink. -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_shm_close(self_: *mut OakSharedMemoryRegion) { - if self_.is_null() { - return; - } - // SAFETY: `self_` is a live region handle. - unsafe { (&mut *(self_ as *mut SharedMemoryRegion)).close() }; -} - -/// `oakengine_ipc_shm_is_valid` — 1/0. -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_shm_is_valid(self_: *const OakSharedMemoryRegion) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - Ok((&*(self_ as *const SharedMemoryRegion)).is_valid() as c_int) - }) -} - -/// `oakengine_ipc_shm_data` — the mapped data pointer (NULL when invalid). -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_shm_data(self_: *mut OakSharedMemoryRegion) -> *mut c_void { - guard_ptr(|| unsafe { - if self_.is_null() { - return Ok(std::ptr::null_mut()); - } - let region = &mut *(self_ as *mut SharedMemoryRegion); - Ok(region.data() as *mut c_void) - }) -} - -/// `oakengine_ipc_shm_size` — mapping size in bytes. -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_shm_size(self_: *const OakSharedMemoryRegion) -> usize { - if self_.is_null() { - return 0; - } - // SAFETY: `self_` is a live region handle. - unsafe { (&*(self_ as *const SharedMemoryRegion)).size() } -} - -/// `oakengine_ipc_shm_key` — the key the region was opened with -/// (buf/size convention). -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_shm_key( - self_: *const OakSharedMemoryRegion, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let region = &*(self_ as *const SharedMemoryRegion); - Ok(crate::handle::write_string(region.key(), buf, buf_size)) - }) -} - -/// `oakengine_ipc_shm_error` — reason of the last failed open -/// (buf/size convention). -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_shm_error( - self_: *const OakSharedMemoryRegion, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let region = &*(self_ as *const SharedMemoryRegion); - Ok(crate::handle::write_string(region.error(), buf, buf_size)) - }) -} - -/// `oakengine_ipc_shm_make_key` — build a unique segment key -/// ("olive-rw--", buf/size convention). -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_shm_make_key( - owner_pid: i64, - worker_index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let key = SharedMemoryRegion::make_key(owner_pid, worker_index); - Ok(crate::handle::write_string(&key, buf, buf_size)) - }) -} - -/// `oakengine_ipc_framepool_bytes_needed`. -#[no_mangle] -pub extern "C" fn oakengine_ipc_framepool_bytes_needed( - slot_count: u32, - slot_data_bytes: usize, -) -> usize { - FrameSlotPool::bytes_needed(slot_count, slot_data_bytes) -} - -/// `oakengine_ipc_framepool_create` — lay out and initialize a brand-new -/// pool over `mem` (owner side, once). The handle is owned by the caller -/// but does not own `mem`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_framepool_create( - mem: *mut c_void, - slot_count: u32, - slot_data_bytes: usize, -) -> *mut OakFrameSlotPool { - guard_ptr(|| unsafe { - if mem.is_null() { - return Ok(std::ptr::null_mut()); - } - let pool = FrameSlotPool::create(mem as *mut u8, slot_count, slot_data_bytes); - Ok(Box::into_raw(Box::new(pool)) as *mut OakFrameSlotPool) - }) -} - -/// `oakengine_ipc_framepool_attach` — map an existing pool (peer side). -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_framepool_attach(mem: *mut c_void) -> *mut OakFrameSlotPool { - guard_ptr(|| unsafe { - if mem.is_null() { - return Ok(std::ptr::null_mut()); - } - let pool = FrameSlotPool::attach(mem as *mut u8); - if !pool.is_valid() { - return Ok(std::ptr::null_mut()); - } - Ok(Box::into_raw(Box::new(pool)) as *mut OakFrameSlotPool) - }) -} - -/// `oakengine_ipc_framepool_copy` — copy the view (same shared memory, -/// independent handle). NULL yields NULL. -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_framepool_copy( - self_: *const OakFrameSlotPool, -) -> *mut OakFrameSlotPool { - guard_ptr(|| unsafe { - if self_.is_null() { - return Ok(std::ptr::null_mut()); - } - let pool = (*(self_ as *const FrameSlotPool)).clone(); - Ok(Box::into_raw(Box::new(pool)) as *mut OakFrameSlotPool) - }) -} - -/// `oakengine_ipc_framepool_free` — NULL no-op. -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_framepool_free(self_: *mut OakFrameSlotPool) { - if self_.is_null() { - return; - } - // SAFETY: `self_` was produced by one of the create/attach/copy - // exports and is not used after this. - unsafe { drop(Box::from_raw(self_ as *mut FrameSlotPool)) }; -} - -/// `oakengine_ipc_framepool_is_valid` — 1/0. -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_framepool_is_valid(self_: *const OakFrameSlotPool) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - Ok((&*(self_ as *const FrameSlotPool)).is_valid() as c_int) - }) -} - -/// `oakengine_ipc_framepool_slot_count`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_framepool_slot_count(self_: *const OakFrameSlotPool) -> u32 { - if self_.is_null() { - return 0; - } - // SAFETY: `self_` is a live pool handle. - unsafe { (&*(self_ as *const FrameSlotPool)).slot_count() } -} - -/// `oakengine_ipc_framepool_slot_data_bytes`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_framepool_slot_data_bytes( - self_: *const OakFrameSlotPool, -) -> usize { - if self_.is_null() { - return 0; - } - // SAFETY: `self_` is a live pool handle. - unsafe { (&*(self_ as *const FrameSlotPool)).slot_data_bytes() } -} - -/// `oakengine_ipc_framepool_acquire` — take a free slot; 1 on success -/// (`*index` set), 0 if none is free or the pool is invalid. -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_framepool_acquire( - self_: *mut OakFrameSlotPool, - index: *mut u32, -) -> c_int { - crate::handle::guard_int(|| unsafe { - if self_.is_null() || index.is_null() { - return Ok(0); - } - let pool = &*(self_ as *const FrameSlotPool); - if !pool.is_valid() { - return Ok(0); - } - let mut out = 0u32; - if pool.acquire(&mut out) { - *index = out; - Ok(1) - } else { - Ok(0) - } - }) -} - -/// `oakengine_ipc_framepool_slot_data` — pointer to a slot's pixel block. -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_framepool_slot_data( - self_: *mut OakFrameSlotPool, - index: u32, -) -> *mut c_void { - guard_ptr(|| unsafe { - if self_.is_null() { - return Ok(std::ptr::null_mut()); - } - let pool = &*(self_ as *const FrameSlotPool); - if !pool.is_valid() || index >= pool.slot_count() { - return Ok(std::ptr::null_mut()); - } - Ok(pool.slot_data(index) as *mut c_void) - }) -} - -/// `oakengine_ipc_framepool_slot_data_const`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_framepool_slot_data_const( - self_: *const OakFrameSlotPool, - index: u32, -) -> *const c_void { - guard_ptr(|| unsafe { - if self_.is_null() { - return Ok(std::ptr::null_mut()); - } - let pool = &*(self_ as *const FrameSlotPool); - if !pool.is_valid() || index >= pool.slot_count() { - return Ok(std::ptr::null_mut()); - } - Ok(pool.slot_data_const(index) as *mut c_void) - }) -} - -/// `oakengine_ipc_framepool_meta` — mutable per-slot metadata (borrowed). -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_framepool_meta( - self_: *mut OakFrameSlotPool, - index: u32, -) -> *mut crate::ipc::FrameSlotMeta { - guard_ptr(|| unsafe { - if self_.is_null() { - return Ok(std::ptr::null_mut()); - } - let pool = &*(self_ as *const FrameSlotPool); - if !pool.is_valid() || index >= pool.slot_count() { - return Ok(std::ptr::null_mut()); - } - Ok(pool.meta(index)) - }) -} - -/// `oakengine_ipc_framepool_meta_const`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_framepool_meta_const( - self_: *const OakFrameSlotPool, - index: u32, -) -> *const crate::ipc::FrameSlotMeta { - guard_ptr(|| unsafe { - if self_.is_null() { - return Ok(std::ptr::null_mut()); - } - let pool = &*(self_ as *const FrameSlotPool); - if !pool.is_valid() || index >= pool.slot_count() { - return Ok(std::ptr::null_mut()); - } - Ok(pool.meta_const(index) as *mut FrameSlotMeta) - }) -} - -/// `oakengine_ipc_framepool_publish` — publish a filled slot; 1 on success, -/// 0 if the ready ring is full or the pool is invalid. -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_framepool_publish( - self_: *mut OakFrameSlotPool, - index: u32, -) -> c_int { - crate::handle::guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let pool = &*(self_ as *const FrameSlotPool); - if !pool.is_valid() || index >= pool.slot_count() { - return Ok(0); - } - Ok(pool.publish(index) as c_int) - }) -} - -/// `oakengine_ipc_framepool_consume` — take the next published slot; 1 on -/// success (`*index` set), 0 if nothing is ready or the pool is invalid. -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_framepool_consume( - self_: *mut OakFrameSlotPool, - index: *mut u32, -) -> c_int { - crate::handle::guard_int(|| unsafe { - if self_.is_null() || index.is_null() { - return Ok(0); - } - let pool = &*(self_ as *const FrameSlotPool); - if !pool.is_valid() { - return Ok(0); - } - let mut out = 0u32; - if pool.consume(&mut out) { - *index = out; - Ok(1) - } else { - Ok(0) - } - }) -} - -/// `oakengine_ipc_framepool_release` — return a consumed slot to the free -/// pool; 1 on success, 0 if the free ring is full or the pool is invalid. -#[no_mangle] -pub unsafe extern "C" fn oakengine_ipc_framepool_release( - self_: *mut OakFrameSlotPool, - index: u32, -) -> c_int { - crate::handle::guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let pool = &*(self_ as *const FrameSlotPool); - if !pool.is_valid() || index >= pool.slot_count() { - return Ok(0); - } - Ok(pool.release(index) as c_int) - }) -} - -// --------------------------------------------------------------------------- -// Unit tests -// --------------------------------------------------------------------------- - -#[cfg(test)] -mod cabi_tests { - use super::*; - - /// 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 wrappers --------------------------------------------------- - - #[test] - fn c_abi_shm_make_key_buf_size() { - let mut buf = [0 as c_char; 64]; - let n = unsafe { oakengine_ipc_shm_make_key(7, 2, buf.as_mut_ptr(), 64) }; - assert_eq!(n, "olive-rw-7-2".len() as c_int); - let s = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } - .to_string_lossy() - .into_owned(); - assert_eq!(s, "olive-rw-7-2"); - // NULL/0 buffer only queries the size. - assert_eq!( - unsafe { oakengine_ipc_shm_make_key(7, 2, std::ptr::null_mut(), 0) }, - n - ); - } - - #[test] - fn c_abi_framepool_create_attach_publish_consume() { - let key = test_key("cabi-pool"); - let slots = 2u32; - let slot_bytes = 32usize; - let size = FrameSlotPool::bytes_needed(slots, slot_bytes); - let (mut owner, mut peer) = two_mappings(&key, size); - - // SAFETY: both regions live. - let owner_ptr = unsafe { owner.data() as *mut c_void }; - let peer_ptr = unsafe { peer.data() as *mut c_void }; - // SAFETY: buffers sized by bytes_needed. - let pool = unsafe { oakengine_ipc_framepool_create(owner_ptr, slots, slot_bytes) }; - assert!(!pool.is_null()); - assert_eq!(unsafe { oakengine_ipc_framepool_is_valid(pool) }, 1); - assert_eq!(unsafe { oakengine_ipc_framepool_slot_count(pool) }, slots); - assert_eq!( - unsafe { oakengine_ipc_framepool_slot_data_bytes(pool) }, - slot_bytes - ); - - // SAFETY: peer buffer is a live mapping of the same segment. - let attached = unsafe { oakengine_ipc_framepool_attach(peer_ptr) }; - assert!(!attached.is_null()); - assert_eq!(unsafe { oakengine_ipc_framepool_is_valid(attached) }, 1); - - let mut slot = 0u32; - assert_eq!( - unsafe { oakengine_ipc_framepool_acquire(pool, &mut slot) }, - 1 - ); - // SAFETY: acquired slot; slot_bytes writable (count in bytes). - unsafe { - ptr::write_bytes( - oakengine_ipc_framepool_slot_data(pool, slot) as *mut u8, - 0x5A, - slot_bytes, - ); - } - // SAFETY: slot in range. - let meta = unsafe { &mut *oakengine_ipc_framepool_meta(pool, slot) }; - meta.id = 77; - assert_eq!(unsafe { oakengine_ipc_framepool_publish(pool, slot) }, 1); - - let mut got = 99u32; - assert_eq!( - unsafe { oakengine_ipc_framepool_consume(attached, &mut got) }, - 1 - ); - assert_eq!(got, slot); - // SAFETY: consumed slot. - let meta = unsafe { &*oakengine_ipc_framepool_meta_const(attached, got) }; - assert_eq!(meta.id, 77); - // SAFETY: slot_bytes readable. - assert_eq!( - unsafe { *(oakengine_ipc_framepool_slot_data_const(attached, got) as *const u8) }, - 0x5A - ); - assert_eq!(unsafe { oakengine_ipc_framepool_release(attached, got) }, 1); - - unsafe { oakengine_ipc_framepool_free(pool) }; - unsafe { oakengine_ipc_framepool_free(attached) }; - } - - #[test] - fn c_abi_shm_open_data_size_error() { - let key = test_key("cabi-shm"); - let size = 256usize; - // SAFETY: shm_create returns an owned handle. - let region = unsafe { oakengine_ipc_shm_create() }; - assert!(!region.is_null()); - let key_c = std::ffi::CString::new(key.clone()).unwrap(); - // SAFETY: valid C string + owned handle. - let rc = unsafe { oakengine_ipc_shm_open(region, key_c.as_ptr(), size, 0) }; - assert_eq!(rc, 1); - assert_eq!(unsafe { oakengine_ipc_shm_is_valid(region) }, 1); - assert_eq!(unsafe { oakengine_ipc_shm_size(region) }, size); - // SAFETY: mapping live. - assert!(!unsafe { oakengine_ipc_shm_data(region) }.is_null()); - - let mut kb = [0 as c_char; 128]; - let n = unsafe { oakengine_ipc_shm_key(region, kb.as_mut_ptr(), 128) }; - assert_eq!(n, key.len() as c_int); - assert_eq!(unsafe { oakengine_ipc_shm_is_valid(region) }, 1); - - unsafe { oakengine_ipc_shm_close(region) }; - assert_eq!(unsafe { oakengine_ipc_shm_is_valid(region) }, 0); - unsafe { oakengine_ipc_shm_free(region) }; - - // A failed open records a human-readable error. - // SAFETY: shm_create returns an owned handle. - let region2 = unsafe { oakengine_ipc_shm_create() }; - let bad = std::ffi::CString::new("olive-rw-no-such-segment-for-test").unwrap(); - // SAFETY: valid C string + owned handle. - assert_eq!( - unsafe { oakengine_ipc_shm_open(region2, bad.as_ptr(), size, 1) }, - 0 - ); - let mut eb = [0 as c_char; 256]; - let n = unsafe { oakengine_ipc_shm_error(region2, eb.as_mut_ptr(), 256) }; - assert!(n > 0); - assert_eq!(unsafe { oakengine_ipc_shm_is_valid(region2) }, 0); - unsafe { oakengine_ipc_shm_free(region2) }; - } -} diff --git a/crates/oakengine.bk/src/lib.rs b/crates/oakengine.bk/src/lib.rs deleted file mode 100644 index e5e838f56..000000000 --- a/crates/oakengine.bk/src/lib.rs +++ /dev/null @@ -1,111 +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 . - -//! # oakengine — the `liboakengine` cdylib (plugin / external C ABI) -//! -//! The frozen `oakengine_*` C ABI (`engine/include/oakengine/*.h`) as a -//! **pure cdylib** (M14 R4). This is the plugin / external-consumer layer: -//! OFX plugins and third-party embedders link `liboakengine` and call the -//! C ABI; the app, oak-cli and oak-worker do not use it anymore — they -//! link the module crates directly as Rust rlibs. -//! -//! Downward, every `oakengine_*` export is a direct Rust call into the -//! module crates (oakundo/oaknode/oaktimeline/oakcodec/oakaudio/oakrender/ -//! oaktask/oakcommon/oakplugin/oakstorage/oakcore-rs) through [`stubs`] -//! (the rewired replacement for the deleted `bridge/`); the C ABI itself -//! stays frozen (only additive changes + major version bumps). -//! Cross-cutting state that used to live here (the process-wide undo stack, -//! the open undo group) has sunk into the modules (M14 R1: -//! [`oakundo::global`]); [`undo`] is a thin forward that adds the engine's -//! box/unbox, buf/size and error-code conventions. -//! -//! ## Handle mapping -//! -//! The engine headers' opaque pointers (`OakEngineNode*`, `OakEngineTrack*`, -//! ...) become thin newtype wrappers around module [`handle::CHandle`] -//! values (see [`handle`]). Each exported function keeps the exact -//! signature from the engine header; inside, it unboxes the module value, -//! calls the module's direct Rust API and boxes the result. -//! -//! ## FFI discipline -//! -//! Every export goes through a `catch_unwind` guard ([`handle::guard*`]), -//! `free` functions are NULL no-ops, strings use the two-stage buf/size -//! convention ([`handle::write_string`]), and module error codes pass -//! through untranslated ([`error`], facade module 00 → -1..-6). -//! -//! ## Testing -//! -//! The crate is cdylib-only, so the former `tests/*.rs` integration tests -//! run as in-crate unit tests under `src/test_support/` (pulled in from -//! here under `#[cfg(test)]`; they address the crate's modules through -//! `crate::*`). The module crates are real dependencies (see Cargo.toml), -//! so the test binary statically links the same rlibs the cdylib embeds; -//! [`linkage`] anchors every crate into the cdylib link, and the test-only -//! [`test_link`] module does the same for the unit-test binary. Where a -//! wrapped family needs module behavior the crates do not implement yet, -//! the engine function is a documented stub with its reason (see -//! `deferred.rs` and README.md). - -#![deny(unsafe_op_in_unsafe_fn)] -#![warn(missing_docs)] - -pub mod audio; -pub mod codec; -pub mod common; -pub mod deferred; -pub mod error; -pub mod handle; -pub mod ipc; -#[cfg(not(test))] -pub mod linkage; -pub mod library; -pub mod node; -pub mod plugin; -pub mod pods; -pub mod render; -pub mod stubs; -pub mod storage; -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 imports that the unit tests compile in — e.g. - // the render family's oakrender display renderer (src/render.rs). - // The in-crate tests do the same through test_support/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::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() - } -} diff --git a/crates/oakengine.bk/src/library.rs b/crates/oakengine.bk/src/library.rs deleted file mode 100644 index e134ef4e1..000000000 --- a/crates/oakengine.bk/src/library.rs +++ /dev/null @@ -1,368 +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 . - -//! The project-library manager C ABI (plan M13 §4): list / create / open / -//! rename / duplicate / delete / import / export over the oakstorage -//! database backend the write-through binds to ([`crate::storage`]). -//! -//! These exports are additive (D4): the app talks to the engine dylib only -//! through the frozen `oakengine_*` surface, so the manager's data source -//! crosses the boundary here instead of linking oakstorage directly (which -//! would give the app a second copy of the handle/serializer types). -//! -//! All operations address the configured default library (the same -//! `Storage/Backend` + `Storage/SqlitePath` configuration the write-through -//! uses); with storage disabled every call fails with `OAKENGINE_E_STATE` -//! except [`oakengine_library_list`], which reports an empty library -//! (`"[]"`) so the manager window can still open. - -use std::ffi::{c_char, c_int}; - -use oakstorage::backend::StorageBackend; -use oakstorage::uri::StorageUri; - -use crate::error::{Error, Result}; -use crate::handle::{guard, guard_int, read_cstr, write_string, OakEngineProject}; -use crate::stubs::node as n; - -/// One library row as the project manager shows it: the project metadata -/// plus the stats derived from the head state (plan §4). -#[derive(serde::Serialize)] -struct LibraryRow { - /// Library row uuid (the open/duplicate/export selector). - uuid: String, - /// Display name. - name: String, - /// Row creation time (unix seconds, UTC). - created_at: i64, - /// Last-write time (unix seconds, UTC; the manager sort key). - modified_at: i64, - /// Longest sequence duration, milliseconds. - duration_ms: i64, - /// Total tracks across all sequences. - track_count: i32, - /// Total clip blocks. - clip_count: i32, - /// Total footage nodes. - footage_count: i32, -} - -/// Map an oakstorage error onto the facade error space (the context string -/// is log-only per the error contract). -fn map_err(e: oakstorage::error::Error) -> Error { - use oakstorage::error::Error as E; - match e { - E::Invalid => Error::Invalid, - E::State => Error::State, - E::NotFound => Error::NotFound, - E::NoMem => Error::NoMem, - other => Error::Failed(other.to_string()), - } -} - -/// The configured default library as a parsed URI; [`Error::State`] when -/// the write-through backend is disabled or the path does not resolve. -fn library() -> Result { - if !crate::storage::storage_enabled() { - return Err(Error::State); - } - let uri = crate::storage::library_uri().ok_or(Error::State)?; - StorageUri::parse(&uri).map_err(map_err) -} - -/// The library URI selecting one row (`…?project=`). -fn project_uri(uuid: &str) -> Result { - let uri = library()?; - StorageUri::parse(&format!("{}?project={uuid}", uri.to_uri_string())).map_err(map_err) -} - -/// Load one library row as an owned project handle (refcount 1). -fn load_handle(uuid: &str) -> Result { - let uri = project_uri(uuid)?; - let result = crate::storage::backend().load(&uri).map_err(map_err)?; - if result.project.is_null() { - return Err(Error::Failed(format!( - "library load of {uuid} returned no project (info code {})", - result.version_info - ))); - } - Ok(result.project) -} - -/// Release an owned handle (refcount 1). -fn release(h: crate::handle::CHandle) { - if let Some(release) = h.release { - unsafe { release(h.ctx) }; - } -} - -/// `oakengine_library_list` — the library rows as a JSON array (buf/size -/// convention), most recently modified first. Each row carries the manager -/// stats derived from the head state; a row whose stats fail to replay -/// degrades to zeros instead of failing the whole list. With storage -/// disabled the result is the empty array (`"[]"`), not an error. -#[no_mangle] -pub unsafe extern "C" fn oakengine_library_list(buf: *mut c_char, buf_size: c_int) -> c_int { - guard_int(|| unsafe { - if !crate::storage::storage_enabled() { - return Ok(write_string("[]", buf, buf_size)); - } - let uri = library()?; - let infos = crate::storage::backend() - .list_projects(&uri) - .map_err(map_err)?; - let mut rows = Vec::with_capacity(infos.len()); - for info in infos { - let stats = crate::storage::backend() - .project_stats(&uri, &info.uuid) - .unwrap_or_default(); - rows.push(LibraryRow { - uuid: info.uuid, - name: info.name, - created_at: info.created_at.and_utc().timestamp(), - modified_at: info.modified_at.and_utc().timestamp(), - duration_ms: stats.duration_ms, - track_count: stats.track_count, - clip_count: stats.clip_count, - footage_count: stats.footage_count, - }); - } - let json = serde_json::to_string(&rows) - .map_err(|e| Error::Failed(format!("library list encode: {e}")))?; - Ok(write_string(&json, buf, buf_size)) - }) -} - -/// `oakengine_library_create` — create a blank project named `name` as a -/// new library row and report its uuid (buf/size convention on -/// `out_uuid`; the return value is the uuid length, negative on error). -/// The row lands immediately (one `kind='import'` command), so the -/// manager list shows it before the first edit. -#[no_mangle] -pub unsafe extern "C" fn oakengine_library_create( - name: *const c_char, - out_uuid: *mut c_char, - out_size: c_int, -) -> c_int { - guard_int(|| unsafe { - if name.is_null() { - return Err(Error::Invalid); - } - let name = read_cstr(name); - if name.trim().is_empty() { - return Err(Error::Invalid); - } - let uri = library()?; - - let mut h = n::oaknode_project_init(); - if h.is_null() { - return Err(Error::NoMem); - } - let outcome = (|| -> Result { - Error::from_module(n::oaknode_project_initialize(h))?; - let uuid = { - let arc = crate::handle::domain::project_of(&h).ok_or(Error::Invalid)?; - let mut guard = arc.lock().unwrap_or_else(|e| e.into_inner()); - guard.settings.insert("projectname".to_string(), name); - guard.uuid.clone() - }; - crate::storage::backend().save(h, &uri, 0).map_err(map_err)?; - Ok(uuid) - })(); - n::oaknode_project_free(&mut h); - let uuid = outcome?; - Ok(write_string(&uuid, out_uuid, out_size)) - }) -} - -/// `oakengine_library_delete` — delete the library row `uuid` (cascades -/// settings / snapshots / journal; `OAKENGINE_E_NOT_FOUND` when absent). -/// The manager confirms with the user before calling. -#[no_mangle] -pub unsafe extern "C" fn oakengine_library_delete(uuid: *const c_char) -> c_int { - guard(|| unsafe { - if uuid.is_null() { - return Err(Error::Invalid); - } - let uuid = read_cstr(uuid); - if uuid.is_empty() { - return Err(Error::Invalid); - } - crate::storage::backend() - .delete_project(&library()?, &uuid) - .map_err(map_err) - }) -} - -/// `oakengine_library_rename` — rename the library row `uuid` (the -/// manager's list name; the in-project `projectname` setting is -/// untouched). -#[no_mangle] -pub unsafe extern "C" fn oakengine_library_rename( - uuid: *const c_char, - name: *const c_char, -) -> c_int { - guard(|| unsafe { - if uuid.is_null() || name.is_null() { - return Err(Error::Invalid); - } - let (uuid, name) = (read_cstr(uuid), read_cstr(name)); - if uuid.is_empty() || name.trim().is_empty() { - return Err(Error::Invalid); - } - crate::storage::backend() - .rename_project(&library()?, &uuid, name.trim()) - .map_err(map_err) - }) -} - -/// `oakengine_library_duplicate` — copy the library row `uuid` (settings, -/// snapshots and the full journal history included) under a fresh uuid, -/// reporting the new row's uuid (buf/size convention on `out_uuid`; the -/// return value is the uuid length, negative on error). -/// `name` is the copy's display name; NULL/empty defaults to -/// ` (copy)`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_library_duplicate( - uuid: *const c_char, - name: *const c_char, - out_uuid: *mut c_char, - out_size: c_int, -) -> c_int { - guard_int(|| unsafe { - if uuid.is_null() { - return Err(Error::Invalid); - } - let uuid = read_cstr(uuid); - if uuid.is_empty() { - return Err(Error::Invalid); - } - let name = read_cstr(name); - let name = match name.trim() { - "" => None, - trimmed => Some(trimmed), - }; - let info = crate::storage::backend() - .duplicate_project(&library()?, &uuid, name) - .map_err(map_err)?; - Ok(write_string(&info.uuid, out_uuid, out_size)) - }) -} - -/// `oakengine_library_import` — import a `.ove` / `.otio` / `.fcpxml` -/// project file as a new library row (the file backend parses it, a fresh -/// uuid is assigned, and the first save journals the whole project as one -/// `kind='import'` command). Reports the new row's uuid (buf/size -/// convention on `out_uuid`; the return value is the uuid length, -/// negative on error). -#[no_mangle] -pub unsafe extern "C" fn oakengine_library_import( - path: *const c_char, - out_uuid: *mut c_char, - out_size: c_int, -) -> c_int { - guard_int(|| unsafe { - if path.is_null() { - return Err(Error::Invalid); - } - let path = read_cstr(path); - if path.is_empty() { - return Err(Error::Invalid); - } - let file_uri = StorageUri::parse(&path).map_err(map_err)?; - let uuid = crate::storage::backend() - .import_from_file(&library()?, &file_uri) - .map_err(map_err)?; - Ok(write_string(&uuid, out_uuid, out_size)) - }) -} - -/// `oakengine_library_export` — export the library row `uuid` to the file -/// `path`; the format is dispatched by extension through the oakstorage -/// registry (`.ove` / `.ovexml` → ove-xml, `.otio` / `.fcpxml` → the -/// interchange backend). Nothing is written back to the library. -#[no_mangle] -pub unsafe extern "C" fn oakengine_library_export( - uuid: *const c_char, - path: *const c_char, -) -> c_int { - guard(|| unsafe { - if uuid.is_null() || path.is_null() { - return Err(Error::Invalid); - } - let (uuid, path) = (read_cstr(uuid), read_cstr(path)); - if uuid.is_empty() || path.is_empty() { - return Err(Error::Invalid); - } - let file_uri = StorageUri::parse(&path).map_err(map_err)?; - if file_uri.scheme != "file" { - return Err(Error::Invalid); - } - let handle = load_handle(&uuid)?; - let backend = oakstorage::registry::Registry::global() - .resolve(&file_uri) - .map_err(map_err)?; - let result = backend.save(handle, &file_uri, 0).map_err(map_err); - release(handle); - result - }) -} - -/// `oakengine_project_load_library` — load the library row `uuid` into a -/// fresh project shell (same contract as `oakengine_project_load`: the -/// shell must carry no content). On success the undo stack is cleared, the -/// modified flag is reset, and the project is bound to the library session -/// (the write-through continues the row's journal from its head seq). -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_load_library( - self_: *mut OakEngineProject, - uuid: *const c_char, - err: *mut c_char, - err_size: c_int, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || uuid.is_null() { - return Err(Error::Invalid); - } - let h = crate::handle::unbox(self_)?; - if !n::oaknode_project_root(h).is_null() { - return Err(Error::State); - } - let uuid = read_cstr(uuid); - if uuid.is_empty() { - return Err(Error::Invalid); - } - let loaded = match load_handle(&uuid) { - Ok(handle) => handle, - Err(e) => { - write_string(&e.to_string(), err, err_size); - return Err(e); - } - }; - // Swap the loaded content into the caller's shell box, releasing - // the empty shell handle the box was created with. - let mut old = (*self_).handle; - (*self_).handle = loaded; - n::oaknode_project_free(&mut old); - crate::undo::oakengine_undo_clear(); - Error::from_module(n::oaknode_project_set_modified(loaded, 0))?; - crate::storage::bind_project(loaded); - if !err.is_null() && err_size > 0 { - *err = 0; - } - Ok(()) - }) -} diff --git a/crates/oakengine.bk/src/linkage.rs b/crates/oakengine.bk/src/linkage.rs deleted file mode 100644 index 0784559b2..000000000 --- a/crates/oakengine.bk/src/linkage.rs +++ /dev/null @@ -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 . - -//! Linkage anchors — force the module crates' rlibs into every link. -//! -//! The facade calls the module crates' direct Rust APIs (single-lib; the -//! deleted `src/bridge/` no longer exists), and most modules reach the -//! linker through those normal references. The anchors below reference one -//! direct-Rust symbol of every module crate (and `oakcore-rs`) from a -//! `#[used]` static, which (a) marks each crate as used so its rlib -//! reaches the linker even when the facade only touches it indirectly and -//! (b) keeps the anchor alive so the referenced object files are pulled. -//! For the `liboakengine` cdylib this is what embeds the module crates -//! next to the facade's own `oakengine_*` exports — including the -//! oakcommon XML/undo symbols oaknode's serializer resolves at runtime -//! via dlsym(RTLD_DEFAULT) (see the per-anchor comments in `force_link`). -//! -//! The per-crate symbol mirrors the test-force-link in -//! test_support/common/mod.rs (same paths, same `as usize` cast idiom), -//! so the crate/module paths are proven against the current module -//! layouts. - -#![allow(dead_code)] - -/// Pull every module crate into the link. Mirrors -/// `tests/common/mod.rs::force_link`; the oakcommon XML/undo anchors are -/// repeated because oaknode's serializer resolves those C ABI symbols at -/// 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; 11] = [ - // oakcore-rs (pure value types; referenced so its rlib is linked). - oakcore_rs::Rational::new(1, 2).numerator() 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() -} - -/// Keeps [`force_link`] (and through it every referenced export) alive in -/// the cdylib/staticlib even though nothing calls it directly. -#[used] -static FORCE_LINK_ANCHOR: fn() -> usize = force_link; diff --git a/crates/oakengine.bk/src/node.rs b/crates/oakengine.bk/src/node.rs deleted file mode 100644 index 64b83be30..000000000 --- a/crates/oakengine.bk/src/node.rs +++ /dev/null @@ -1,8637 +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 . - -//! `engine/include/oakengine/{node,project,footage}.h` — the node graph, -//! project and footage families over the oaknode module. - -use std::ffi::{c_char, c_int, c_void}; - -use crate::stubs::common as c; -use crate::stubs::node as n; -use crate::common::OakVideoParamsPod; -use crate::error::{Error, Result}; -use crate::handle::{ - box_handle, free_box, guard, guard_int, guard_ptr, guard_void, read_cstr, string_result, unbox, - write_string, CHandle, OakEngineClipboard, OakEngineFootage, OakEngineFrameCache, - OakEngineKeyframe, OakEngineNode, OakEngineNodeDragger, OakEngineProject, OakEngineSequence, - OakEngineThumbnailCache, OakEngineWaveformCache, -}; -use crate::undo::push_or_run; - -/// `engine/include/oakengine/node.h` — POD mirror of `oak_node_value`. -/// -/// Single-lib unification: aliases the oaknode crate's POD (the module -/// C ABI struct `oak_node_value`), so the facade can pass it straight -/// into `oaknode::ffi` functions without an `extern "C"` declaration. -/// The field is named `kind` on the shared type (was `type_`). -pub type OakNodeValue = oaknode::value::OakNodeValue; - -/// `engine/include/oakengine/footage.h` — POD mirror of -/// `oak_footage_video_info` (olive::VideoParams stream description). -#[repr(C)] -#[derive(Clone, Copy)] -pub struct OakFootageVideoInfo { - /// Stream index within the video streams. - pub stream_index: c_int, - /// Width in pixels. - pub width: c_int, - /// Height in pixels. - pub height: c_int, - /// Frame rate numerator. - pub frame_rate_num: c_int, - /// Frame rate denominator. - pub frame_rate_den: c_int, - /// Duration in time-base units. - pub duration_ts: i64, - /// Seconds per time-base unit (numerator). - pub time_base_num: c_int, - /// Seconds per time-base unit (denominator). - pub time_base_den: c_int, - /// ISO/IEC 23001-8 color primaries code point (0 when unknown). - pub color_primaries: c_int, - /// ISO/IEC 23001-8 transfer characteristics code point. - pub color_trc: c_int, - /// 1 when the stream is interlaced. - pub interlaced: c_int, -} - -/// `engine/include/oakengine/footage.h` — POD mirror of -/// `oak_footage_audio_info` (olive::AudioParams stream description). -#[repr(C)] -#[derive(Clone, Copy)] -pub struct OakFootageAudioInfo { - /// Stream index within the audio streams. - pub stream_index: c_int, - /// Sample rate in Hz. - pub sample_rate: c_int, - /// ffmpeg-style channel mask (e.g. 0x3 = stereo). - pub channel_layout: u64, - /// Channel count. - pub channel_count: c_int, - /// Duration in time-base units. - pub duration_ts: i64, - /// Seconds per time-base unit (numerator). - pub time_base_num: c_int, - /// Seconds per time-base unit (denominator). - pub time_base_den: c_int, -} - -/// `engine/include/oakengine/footage.h` — POD mirror of `oak_proxy_params` -/// (olive::ProxyManager::ProxyParams). -#[repr(C)] -#[derive(Clone, Copy)] -pub struct OakProxyParams { - /// Source resolution width (absolute when `divider` == 1). - pub width: c_int, - /// Source resolution height. - pub height: c_int, - /// Resolution divider (1/2/4/8 fraction of the source). - pub divider: c_int, - /// Preset version. - pub version: c_int, - /// CRF. - pub crf: c_int, - /// Whether the proxy includes audio (1/0). - pub include_audio: c_int, - /// ffmpeg output container (NUL-terminated). - pub extension: [c_char; 32], - /// ffmpeg encoder preset (NUL-terminated). - pub preset: [c_char; 32], -} - -/// `oak_node_value_type` values (`engine/include/oakengine/node.h`). -#[allow(missing_docs)] // mirror of the engine's oak_node_value_type enum -pub mod value_type { - use std::ffi::c_int; - - pub const NONE: c_int = 0; - pub const INT: c_int = 1; - pub const FLOAT: c_int = 2; - pub const BOOL: c_int = 3; - pub const RATIONAL: c_int = 4; - pub const COLOR: c_int = 5; - pub const VEC2: c_int = 6; - pub const VEC3: c_int = 7; - pub const VEC4: c_int = 8; - pub const COMBO: c_int = 9; - pub const STRING: c_int = 10; - pub const TEXT: c_int = 11; - pub const FONT: c_int = 12; - pub const STR_COMBO: c_int = 13; - pub const BINARY: c_int = 14; - pub const BEZIER: c_int = 15; - pub const TEXTURE: c_int = 16; - pub const SAMPLES: c_int = 17; - pub const VIDEO_PARAMS: c_int = 18; - pub const AUDIO_PARAMS: c_int = 19; -} - -// --------------------------------------------------------------------------- -// Shared helpers -// --------------------------------------------------------------------------- - -// Node-family thread-local last error (mirrors the C++ capi's per-family -// `thread_local QString`; engine/src/capi/node.cpp:67). -thread_local! { - static LAST_NODE_ERROR: std::cell::RefCell = std::cell::RefCell::new(String::new()); -} - -// Footage-family thread-local last error (engine/src/capi/footage.cpp:62). -thread_local! { - static LAST_FOOTAGE_ERROR: std::cell::RefCell = std::cell::RefCell::new(String::new()); -} - -/// Record the node-family last error. -fn set_node_error(msg: &str) { - LAST_NODE_ERROR.with(|c| *c.borrow_mut() = msg.to_string()); -} - -/// Record the footage-family last error. -fn set_footage_error(msg: &str) { - LAST_FOOTAGE_ERROR.with(|c| *c.borrow_mut() = msg.to_string()); -} - -/// oaknode type ids (verified against src/node/rust/src/). -const TYPE_ID_SEQUENCE: &str = "org.olivevideoeditor.Olive.sequence"; -const TYPE_ID_FOLDER: &str = "org.olivevideoeditor.Olive.folder"; -const TYPE_ID_TRACK: &str = "org.olivevideoeditor.Olive.track"; -const TYPE_ID_FOOTAGE: &str = "org.olivevideoeditor.Olive.footage"; -const TYPE_ID_CLIP_BLOCK: &str = "org.olivevideoeditor.Olive.clipblock"; -const TYPE_ID_GAP_BLOCK: &str = "org.olivevideoeditor.Olive.gapblock"; -const TYPE_ID_TRANSITION_BLOCK: &str = "org.olivevideoeditor.Olive.transitionblock"; -const TYPE_ID_GROUP: &str = "org.olivevideoeditor.Olive.group"; -const TYPE_ID_MULTICAM: &str = "org.olivevideoeditor.Olive.multicam"; - -/// Run a module two-stage string getter to completion. -/// -/// # Safety -/// `f` must call a module two-stage getter (NULL `buf`/0 size queries the -/// required size including NUL; negative returns are module codes). -unsafe fn module_string c_int>(mut f: F) -> Result { - unsafe { - let len = f(std::ptr::null_mut(), 0); - if len < 0 { - return Err(Error::Module(len)); - } - if len == 0 { - return Ok(String::new()); - } - let mut buf = vec![0 as c_char; (len + 1) as usize]; - let rc = f(buf.as_mut_ptr(), (len + 1) as c_int); - if rc < 0 { - return Err(Error::Module(rc)); - } - Ok(read_cstr(buf.as_ptr())) - } -} - -/// The node's type id (oaknode `oaknode_node_get_id`). -fn node_type_id(node: CHandle) -> Result { - unsafe { module_string(|buf, size| n::oaknode_node_get_id(node, buf, size)) } -} - -/// Whether `node` has type id `id`. -fn is_node_type(node: CHandle, id: &str) -> bool { - node_type_id(node).map(|t| t == id).unwrap_or(false) -} - -/// Unwrap an engine node pointer to the module handle. -/// -/// # Safety -/// `node` must be NULL or a live box created by [`box_handle`]. -unsafe fn node_handle(node: *const OakEngineNode) -> Result { - unsafe { unbox(node) } -} - -/// Unwrap an engine project pointer to the module handle. -/// -/// # Safety -/// `project` must be NULL or a live box created by [`box_handle`]. -unsafe fn project_handle(project: *const OakEngineProject) -> Result { - unsafe { unbox(project) } -} - -/// The project that owns `node` (module handle). -fn project_of(node: CHandle) -> Result { - let mut out = CHandle::null(); - Error::from_module(unsafe { n::oaknode_node_get_project(node, &mut out) })?; - if out.is_null() { - return Err(Error::NotFound); - } - Ok(out) -} - -/// Count project nodes whose type id equals `id`. -fn project_count_of_type(project: CHandle, id: &str) -> c_int { - let total = unsafe { n::oaknode_project_node_count(project) }; - let mut count = 0; - for i in 0..total { - let node = unsafe { n::oaknode_project_node_at(project, i) }; - if !node.is_null() && is_node_type(node, id) { - count += 1; - } - } - count -} - -/// The `index`-th project node of type id `id`, or NULL. -fn project_node_at_of_type(project: CHandle, id: &str, index: c_int) -> CHandle { - if index < 0 { - return CHandle::null(); - } - let total = unsafe { n::oaknode_project_node_count(project) }; - let mut seen = 0; - for i in 0..total { - let node = unsafe { n::oaknode_project_node_at(project, i) }; - if node.is_null() { - continue; - } - if is_node_type(node, id) { - if seen == index { - return node; - } - seen += 1; - } - } - CHandle::null() -} - -/// Whether `node` lives in `project`'s graph (identity comparison: handle -/// copies carry distinct `ctx` pointers to the same graph). -fn project_contains_node(project: CHandle, node: CHandle) -> bool { - let id = unsafe { n::oaknode_node_identity(node) }; - if id == 0 { - return false; - } - let total = unsafe { n::oaknode_project_node_count(project) }; - for i in 0..total { - let other = unsafe { n::oaknode_project_node_at(project, i) }; - if !other.is_null() && unsafe { n::oaknode_node_identity(other) } == id { - return true; - } - } - false -} - -/// `oakengine_node_frame_time_base` semantics: the frame rate of the -/// project's first sequence flipped (seconds per frame), or the engine -/// default 1001/30000. -fn time_base_for(node: CHandle) -> (i64, i64) { - if let Ok(project) = project_of(node) { - let total = unsafe { n::oaknode_project_node_count(project) }; - for i in 0..total { - let seq = unsafe { n::oaknode_project_node_at(project, i) }; - if seq.is_null() || !is_node_type(seq, TYPE_ID_SEQUENCE) { - continue; - } - let mut params: CHandle = CHandle::null(); - let rc = unsafe { n::oaknode_sequence_get_video_params(seq, 0, &mut params) }; - if rc == 0 && !params.is_null() { - let mut num: c_int = 0; - let mut den: c_int = 0; - let fr = - unsafe { c::oakcommon_videoparams_get_frame_rate(params, &mut num, &mut den) }; - let mut h = params; - unsafe { c::oakcommon_videoparams_free(&mut h) }; - if fr == 0 && num > 0 && den > 0 { - // Frame rate flipped → seconds per frame. - return (den as i64, num as i64); - } - } - } - } - (1001, 30000) -} - -/// Greatest common divisor (1 when both are zero). -fn gcd(a: i64, b: i64) -> i64 { - let (mut a, mut b) = (a.abs(), b.abs()); - while b != 0 { - let t = b; - b = a % b; - a = t; - } - if a == 0 { - 1 - } else { - a - } -} - -/// Convert a frame timestamp to rational seconds in the project's time -/// base: `time = ts * tb_num / tb_den` (reduced). -fn ts_to_time(ts: i64, tb: (i64, i64)) -> (i64, i64) { - let num = ts as i128 * tb.0 as i128; - let den = tb.1 as i128; - let g = gcd(num as i64, den as i64); - ((num / g as i128) as i64, (den / g as i128) as i64) -} - -/// Box a module command handle into the engine command shell. -fn command_box(cmd: CHandle) -> Result<*mut OakEngineClipboard> { - if cmd.ctx.is_null() { - return Err(Error::Failed("command creation failed".into())); - } - Ok(box_handle::(cmd).cast()) -} - -/// Push a module command produced by a `*out_command` creator. -/// -/// # Safety -/// `cmd` must be a live module command handle (or empty). -unsafe fn push_command(cmd: CHandle, name: &str) -> Result<()> { - unsafe { - let boxed = command_box(cmd)?; - let name_c = - std::ffi::CString::new(name).map_err(|_| Error::Failed("invalid undo name".into()))?; - push_or_run(boxed, name_c.as_ptr()) - } -} - -/// Assemble several module command handles into ONE multi command and push -/// it (or add it to `parent` when non-NULL). -/// -/// # Safety -/// `children` must hold live module command handles. -unsafe fn push_multi_commands(children: &[CHandle], parent: *mut c_void, name: &str) -> Result<()> { - unsafe { - let multi = crate::undo::oakengine_undo_command_create_multi(); - if multi.is_null() { - return Err(Error::Failed("multi command allocation failed".into())); - } - let multi = multi as *mut OakEngineClipboard; - for child in children { - let rc = crate::undo::oakengine_undo_command_multi_add_child( - multi as *mut c_void, - command_box(*child)?.cast(), - ); - if rc != 0 { - free_box(multi); - return Err(Error::Module(rc)); - } - } - if parent.is_null() { - let name_c = std::ffi::CString::new(name) - .map_err(|_| Error::Failed("invalid undo name".into()))?; - push_or_run(multi, name_c.as_ptr()) - } else { - // The caller owns the parent; `multi` is consumed by add_child. - let rc = crate::undo::oakengine_undo_command_multi_add_child(parent, multi.cast()); - if rc == 0 { - Ok(()) - } else { - Err(Error::Module(rc)) - } - } - } -} - -// --------------------------------------------------------------------------- -// project.h -// --------------------------------------------------------------------------- - -/// `oakengine_project_create` — allocate an empty project shell. -#[no_mangle] -pub extern "C" fn oakengine_project_create() -> *mut OakEngineProject { - guard_ptr(|| { - let h = unsafe { n::oaknode_project_init() }; - if h.ctx.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(h)) - }) -} - -/// `oakengine_project_free` — destroy a project and everything it owns. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_free(self_: *mut OakEngineProject) { - guard_void(|| unsafe { - if self_.is_null() { - return; - } - // The module's project_free releases the handle and clears `ctx`; - // the box shell is then deallocated (no double release). Flush the - // write-through binding first (save + snapshot of pending writes). - let mut h = (*self_).handle; - crate::storage::unbind_project(h); - n::oaknode_project_free(&mut h); - drop(Box::from_raw(self_)); - }) -} - -/// `oakengine_project_new` — initialize `self` as a new blank project. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_new(self_: *mut OakEngineProject) -> c_int { - guard(|| unsafe { - let h = project_handle(self_)?; - if !n::oaknode_project_root(h).is_null() { - return Err(Error::State); - } - Error::from_module(n::oaknode_project_initialize(h))?; - // Clearing the global undo stack mirrors the app's new-project - // behavior (see undo.rs `oakengine_undo_clear`). - crate::undo::oakengine_undo_clear(); - // Bind the fresh project to the default library (plan M13 D2): its - // first undoable edit lands in the journal as the import command. - crate::storage::bind_project(h); - Ok(()) - }) -} - -/// `oakengine_project_load` — load project content from a .ove file. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_load( - self_: *mut OakEngineProject, - path: *const c_char, - err: *mut c_char, - err_size: c_int, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || path.is_null() { - return Err(Error::Invalid); - } - let h = project_handle(self_)?; - if !n::oaknode_project_root(h).is_null() { - return Err(Error::State); - } - - // Normalize to an absolute path so the stored filename matches the - // file's saved_url (mirrors capi project.cpp). - let path_str = read_cstr(path); - let p = std::path::Path::new(&path_str); - let abs = if p.is_absolute() { - p.to_path_buf() - } else { - std::env::current_dir() - .map(|d| d.join(p)) - .unwrap_or_else(|_| p.to_path_buf()) - }; - let filename = abs.to_string_lossy().into_owned(); - let filename_c = std::ffi::CString::new(filename.as_str()) - .map_err(|_| Error::Failed("invalid path".into()))?; - Error::from_module(n::oaknode_project_set_filename(h, filename_c.as_ptr()))?; - - let mut code: c_int = -1; - let mut details = [0 as c_char; 4096]; - let rc = n::oaknode_serializer_load_from_file( - h, - filename_c.as_ptr(), - &mut code, - details.as_mut_ptr(), - details.len() as c_int, - ); - if rc != 0 || code != 0 { - let msg = read_cstr(details.as_ptr()); - let full = if msg.is_empty() { - format!("Failed to load project file \"{}\".", filename) - } else { - msg - }; - write_string(&full, err, err_size); - return Err(Error::Failed(full)); - } - - // Success: clear the undo stack and the modified flag. - crate::undo::oakengine_undo_clear(); - Error::from_module(n::oaknode_project_set_modified(h, 0))?; - // Bind the opened project to the default library (plan M13 D2): the - // row is keyed by the loaded uuid — an existing library row - // continues its journal, otherwise the first edit imports it. - crate::storage::bind_project(h); - if !err.is_null() && err_size > 0 { - *err = 0; - } - Ok(()) - }) -} - -/// `oakengine_project_save` — write the project to `path` (or its own -/// filename when `path` is NULL). -/// -/// Legacy manual-save ABI (frozen, keep exporting): since M13 D5 the -/// write-through library is the primary persistence, and this entry is used -/// only as the .ove export path — the app's 导出工程文件… always passes an -/// explicit `path`, and `project_load_library` / `storage` cover the rest. -/// The NULL branch (save to the recorded filename) survives for older -/// callers. Behavior unchanged: on success the target filename is recorded -/// and the modified flag cleared. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_save( - self_: *mut OakEngineProject, - path: *const c_char, -) -> c_int { - guard(|| unsafe { - let h = project_handle(self_)?; - let filename = if path.is_null() { - let mut buf = [0 as c_char; 4096]; - let rc = n::oaknode_project_filename(h, buf.as_mut_ptr(), buf.len() as c_int); - if rc < 0 { - return Err(Error::Module(rc)); - } - let s = read_cstr(buf.as_ptr()); - if s.is_empty() { - return Err(Error::Invalid); - } - s - } else { - read_cstr(path) - }; - let filename_c = std::ffi::CString::new(filename.as_str()) - .map_err(|_| Error::Failed("invalid path".into()))?; - let compress = !filename.to_lowercase().ends_with(".ovexml"); - let mut code: c_int = -1; - let mut details = [0 as c_char; 4096]; - let rc = n::oaknode_serializer_save_to_file( - h, - filename_c.as_ptr(), - compress as c_int, - &mut code, - details.as_mut_ptr(), - details.len() as c_int, - ); - if rc != 0 || code != 0 { - return Err(Error::Failed(read_cstr(details.as_ptr()))); - } - // Success: record the target filename and clear the modified flag. - Error::from_module(n::oaknode_project_set_filename(h, filename_c.as_ptr()))?; - Error::from_module(n::oaknode_project_set_modified(h, 0))?; - Ok(()) - }) -} - -/// `oakengine_project_is_modified` — the legacy dirty flag. -/// -/// Frozen ABI, kept for older callers and the undo/save machinery: the app -/// (M13 D5) no longer reads it — write-through means a bound project is -/// always persisted — so nothing on the UI side consults this anymore. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_is_modified(self_: *const OakEngineProject) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let rc = n::oaknode_project_is_modified(unbox(self_)?); - Ok(if rc != 0 { 1 } else { 0 }) - }) -} - -/// `oakengine_project_set_modified`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_set_modified( - self_: *mut OakEngineProject, - modified: c_int, -) -> c_int { - guard(|| unsafe { - let h = project_handle(self_)?; - Error::from_module(n::oaknode_project_set_modified( - h, - if modified != 0 { 1 } else { 0 }, - )) - }) -} - -/// `oakengine_project_name` — display name (buf/size). -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_name( - self_: *const OakEngineProject, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let h = unbox(self_)?; - let rc = n::oaknode_project_name(h, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_project_filename` — full path (buf/size). -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_filename( - self_: *const OakEngineProject, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let h = unbox(self_)?; - let rc = n::oaknode_project_filename(h, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_project_footage_count`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_footage_count(self_: *const OakEngineProject) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - Ok(project_count_of_type(unbox(self_)?, TYPE_ID_FOOTAGE)) - }) -} - -/// `oakengine_project_footage_filename` — stored filename at `index`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_footage_filename( - self_: *const OakEngineProject, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let h = unbox(self_)?; - let footage = project_node_at_of_type(h, TYPE_ID_FOOTAGE, index); - if footage.is_null() { - return Err(Error::NotFound); - } - let rc = n::oaknode_footage_filename(footage, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_project_footage_is_online` — 1 when the footage file exists -/// (as stored, or resolved relative to the project file's directory). -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_footage_is_online( - self_: *const OakEngineProject, - index: c_int, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let footage = project_node_at_of_type(h, TYPE_ID_FOOTAGE, index); - if footage.is_null() { - return Err(Error::NotFound); - } - let filename = module_string(|buf, size| n::oaknode_footage_filename(footage, buf, size))?; - let path = std::path::Path::new(&filename); - if path.exists() { - return Ok(1); - } - // Footage that moved together with the project file. - if path.is_relative() { - let mut fbuf = [0 as c_char; 4096]; - let rc = n::oaknode_project_filename(h, fbuf.as_mut_ptr(), fbuf.len() as c_int); - if rc >= 0 { - let project_file = read_cstr(fbuf.as_ptr()); - let resolved = std::path::Path::new(&project_file) - .parent() - .map(|d| d.join(&filename)) - .unwrap_or_else(|| std::path::PathBuf::from(&filename)); - if resolved.exists() { - return Ok(1); - } - } - } - Ok(0) - }) -} - -/// `oakengine_project_footage_at` — the footage node at `index`, boxed -/// (freed with `oakengine_node_free`); NULL for an invalid index or -/// project. The node is borrowed from the project graph, so it stays -/// valid until the project is freed. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_footage_at( - self_: *const OakEngineProject, - index: c_int, -) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - let h = unbox(self_)?; - let footage = project_node_at_of_type(h, TYPE_ID_FOOTAGE, index); - if footage.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(footage)) - }) -} - -/// `oakengine_project_can_undo`. -#[no_mangle] -pub extern "C" fn oakengine_project_can_undo(self_: *const OakEngineProject) -> c_int { - if self_.is_null() { - 0 - } else { - crate::undo::oakengine_undo_can_undo() - } -} - -/// `oakengine_project_can_redo`. -#[no_mangle] -pub extern "C" fn oakengine_project_can_redo(self_: *const OakEngineProject) -> c_int { - if self_.is_null() { - 0 - } else { - crate::undo::oakengine_undo_can_redo() - } -} - -/// `oakengine_project_undo` — step the global undo stack back. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_undo(self_: *mut OakEngineProject) -> c_int { - guard(|| { - if self_.is_null() { - return Err(Error::Invalid); - } - let index = crate::undo::oakengine_undo_index(); - crate::undo::oakengine_undo_jump(index - 1); - Ok(()) - }) -} - -/// `oakengine_project_redo` — step the global undo stack forward. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_redo(self_: *mut OakEngineProject) -> c_int { - guard(|| { - if self_.is_null() { - return Err(Error::Invalid); - } - let index = crate::undo::oakengine_undo_index(); - crate::undo::oakengine_undo_jump(index + 1); - Ok(()) - }) -} - -/// `oakengine_project_sequence_count`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_sequence_count(self_: *const OakEngineProject) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - Ok(project_count_of_type(unbox(self_)?, TYPE_ID_SEQUENCE)) - }) -} - -/// `oakengine_project_sequence_at` — borrowed sequence at `index`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_sequence_at( - self_: *const OakEngineProject, - index: c_int, -) -> *mut OakEngineSequence { - guard_ptr(|| unsafe { - if self_.is_null() { - return Ok(std::ptr::null_mut()); - } - let seq = project_node_at_of_type(unbox(self_)?, TYPE_ID_SEQUENCE, index); - if seq.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(seq)) - }) -} - -/* ---- Folder operations ---------------------------------------------------- */ - -/// `oakengine_folder_create` — create a folder node under `parent`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_folder_create( - project: *mut OakEngineProject, - parent: *mut OakEngineNode, - name: *const c_char, -) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - let ph = project_handle(project)?; - let parent_h = node_handle(parent)?; - if !is_node_type(parent_h, TYPE_ID_FOLDER) { - return Ok(std::ptr::null_mut()); - } - let child = n::oaknode_folder_create(ph); - if child.is_null() { - return Ok(std::ptr::null_mut()); - } - let label = if name.is_null() { - String::new() - } else { - read_cstr(name) - }; - let label_c = std::ffi::CString::new(label.as_str()) - .map_err(|_| Error::Failed("invalid name".into()))?; - // The module's folder_create already registers the node in the - // project's graph, so only the FolderAddChild command is needed - // (the C++ additionally pushes a NodeAddCommand; that surface does - // not exist here). The label is applied directly like the capi. - Error::from_module(n::oaknode_node_set_label(child, label_c.as_ptr()))?; - let add_child = n::oaknode_command_create_folder_add_child(parent_h, child); - if add_child.ctx.is_null() { - return Ok(std::ptr::null_mut()); - } - push_command(add_child, "Create Folder")?; - Ok(box_handle::(child)) - }) -} - -/// `oakengine_folder_has_child_recursive`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_folder_has_child_recursive( - folder: *const OakEngineNode, - child: *const OakEngineNode, -) -> c_int { - guard_int(|| unsafe { - if folder.is_null() || child.is_null() { - return Ok(0); - } - let f = unbox(folder)?; - if !is_node_type(f, TYPE_ID_FOLDER) { - return Ok(0); - } - Ok(n::oaknode_folder_has_child_recursive(f, unbox(child)?)) - }) -} - -/// `oakengine_folder_index_of_child` — index or OAKENGINE_E_NOT_FOUND. -#[no_mangle] -pub unsafe extern "C" fn oakengine_folder_index_of_child( - folder: *const OakEngineNode, - child: *const OakEngineNode, -) -> c_int { - guard_int(|| unsafe { - if folder.is_null() || child.is_null() { - return Err(Error::Invalid); - } - let f = unbox(folder)?; - if !is_node_type(f, TYPE_ID_FOLDER) { - return Err(Error::Invalid); - } - let idx = n::oaknode_folder_index_of_child(f, unbox(child)?); - if idx >= 0 { - Ok(idx) - } else { - Err(Error::NotFound) - } - }) -} - -/// `oakengine_folder_item_child_count`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_folder_item_child_count(folder: *const OakEngineNode) -> c_int { - guard_int(|| unsafe { - if folder.is_null() { - return Ok(0); - } - let f = unbox(folder)?; - if !is_node_type(f, TYPE_ID_FOLDER) { - return Ok(0); - } - let rc = n::oaknode_folder_child_count(f); - Ok(if rc < 0 { 0 } else { rc }) - }) -} - -/// `oakengine_folder_item_child` — borrowed child at `index`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_folder_item_child( - folder: *const OakEngineNode, - index: c_int, -) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - if folder.is_null() { - return Ok(std::ptr::null_mut()); - } - let f = unbox(folder)?; - if !is_node_type(f, TYPE_ID_FOLDER) { - return Ok(std::ptr::null_mut()); - } - let child = n::oaknode_folder_child_at(f, index); - if child.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(child)) - }) -} - -/// `oakengine_folder_child_input_key` — static input key string. -#[no_mangle] -pub extern "C" fn oakengine_folder_child_input_key() -> *const c_char { - // Folder::k_child_input (engine/node/project/folder/folder.cpp:34). - static S: &[u8] = b"child_in\0"; - S.as_ptr() as *const c_char -} - -/// `oakengine_folder_add_child` — undoable add. -#[no_mangle] -pub unsafe extern "C" fn oakengine_folder_add_child( - folder: *mut OakEngineNode, - child: *mut OakEngineNode, -) -> c_int { - guard(|| unsafe { - if folder.is_null() || child.is_null() { - return Err(Error::Invalid); - } - let f = unbox(folder)?; - if !is_node_type(f, TYPE_ID_FOLDER) { - return Err(Error::Invalid); - } - let c = unbox(child)?; - // Mirror the module's live one-folder-per-node check (its UNDOABLE - // FolderAddChild command creator skips it): a node already in - // another folder is rejected with the module STATE error. - let parent = n::oaknode_folder_parent_of(c); - if !parent.ctx.is_null() { - let already_here = n::oaknode_node_identity(parent) != 0 - && n::oaknode_node_identity(parent) == n::oaknode_node_identity(f); - // The borrowed parent handle's shell is released here. - if let Some(release) = parent.release { - unsafe { release(parent.ctx) }; - } - if !already_here { - return Err(Error::Module(oaknode::error::OAKNODE_E_STATE)); - } - } - let cmd = n::oaknode_command_create_folder_add_child(f, c); - if cmd.ctx.is_null() { - return Err(Error::Failed("folder add child command failed".into())); - } - push_command(cmd, "Add Child to Folder") - }) -} - -/// `oakengine_folder_remove_element_command` — opaque command pointer. -#[no_mangle] -pub unsafe extern "C" fn oakengine_folder_remove_element_command( - folder: *mut OakEngineNode, - child: *mut OakEngineNode, -) -> *mut c_void { - guard_ptr(|| unsafe { - if folder.is_null() || child.is_null() { - return Ok(std::ptr::null_mut()); - } - let f = unbox(folder)?; - if !is_node_type(f, TYPE_ID_FOLDER) { - return Ok(std::ptr::null_mut()); - } - // Stub: the module has no Folder::RemoveElementCommand creator - // (its only folder-child command is the ADD). - let _ = unbox(child)?; - Ok(std::ptr::null_mut()) - }) -} - -/// `oakengine_folder_move_child` — move one node into `new_folder`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_folder_move_child( - node: *mut OakEngineNode, - new_folder: *mut OakEngineNode, -) -> c_int { - guard(|| unsafe { - let mut nodes = [node]; - let rc = - oakengine_folder_move_children(nodes.as_mut_ptr(), 1, new_folder, std::ptr::null()); - Error::from_module(rc) - }) -} - -/// `oakengine_folder_move_children` — move several nodes as ONE command. -#[no_mangle] -pub unsafe extern "C" fn oakengine_folder_move_children( - nodes: *mut *mut OakEngineNode, - count: c_int, - dest_folder: *mut OakEngineNode, - undo_name: *const c_char, -) -> c_int { - guard(|| unsafe { - if nodes.is_null() || count <= 0 || dest_folder.is_null() { - return Err(Error::Invalid); - } - let dest = unbox(dest_folder)?; - if !is_node_type(dest, TYPE_ID_FOLDER) { - return Err(Error::Invalid); - } - let mut handles = Vec::with_capacity(count as usize); - for i in 0..count as usize { - let node = *nodes.add(i); - if node.is_null() { - return Err(Error::Invalid); - } - handles.push(unbox(node)?); - } - // The module's folder_move_children is a live (direct) move; the - // capi's move is undoable but the module has no move command, so - // this applies the move directly. Documented deviation. - let rc = n::oaknode_folder_move_children(handles.as_ptr(), count, dest); - if rc != 0 { - return Err(Error::Module(rc)); - } - let _ = undo_name; - Ok(()) - }) -} - -/* ---- Project extras ------------------------------------------------------- */ - -/// `oakengine_project_root`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_root( - self_: *mut OakEngineProject, -) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - if self_.is_null() { - return Ok(std::ptr::null_mut()); - } - let root = n::oaknode_project_root(unbox(self_)?); - if root.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(root)) - }) -} - -/// `oakengine_project_pretty_filename`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_pretty_filename( - self_: *const OakEngineProject, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let h = unbox(self_)?; - let rc = n::oaknode_project_pretty_filename(h, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_project_set_filename`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_set_filename( - self_: *mut OakEngineProject, - path: *const c_char, -) -> c_int { - guard(|| unsafe { - if path.is_null() { - return Err(Error::Invalid); - } - let h = project_handle(self_)?; - Error::from_module(n::oaknode_project_set_filename(h, path)) - }) -} - -/// `oakengine_project_cache_path`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_cache_path( - self_: *const OakEngineProject, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let h = unbox(self_)?; - let rc = n::oaknode_project_cache_path(h, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_project_cache_alongside_path`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_cache_alongside_path( - self_: *const OakEngineProject, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - // Stub: the oaknode project has no alongside-cache-path export (the - // C++ derives it from the project file location). - guard_int(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - Ok(write_string("", buf, buf_size)) - }) -} - -/// `oakengine_project_set_custom_cache_path`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_set_custom_cache_path( - self_: *mut OakEngineProject, - path: *const c_char, -) -> c_int { - guard(|| unsafe { - let h = project_handle(self_)?; - let path = if path.is_null() { - crate::common::empty_cstr() - } else { - path - }; - Error::from_module(n::oaknode_project_set_custom_cache_path(h, path)) - }) -} - -/// `oakengine_project_get_custom_cache_path` (empty → 0). -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_get_custom_cache_path( - self_: *const OakEngineProject, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let h = unbox(self_)?; - let rc = n::oaknode_project_get_custom_cache_path(h, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else if rc == 0 { - Ok(0) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_project_get_cache_location_setting`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_get_cache_location_setting( - self_: *const OakEngineProject, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(-1); - } - Ok(n::oaknode_project_get_cache_location_setting(unbox(self_)?)) - }) -} - -/// `oakengine_project_item_mime_type` — static MIME string. -#[no_mangle] -pub extern "C" fn oakengine_project_item_mime_type() -> *const c_char { - // Project::k_item_mime_type (engine/node/project.cpp:55). - static S: &[u8] = b"application/x-oliveprojectitemdata\0"; - S.as_ptr() as *const c_char -} - -/// `oakengine_project_from_object` — owning project of a node. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_from_object( - node: *const OakEngineNode, -) -> *mut OakEngineProject { - guard_ptr(|| unsafe { - if node.is_null() { - return Ok(std::ptr::null_mut()); - } - let mut out = CHandle::null(); - Error::from_module(n::oaknode_node_get_project(unbox(node)?, &mut out))?; - if out.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(out)) - }) -} - -/// `oakengine_project_get_color_reference_space`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_get_color_reference_space( - self_: *const OakEngineProject, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - // Stub: the oaknode project stores no color reference space (the C++ - // reads Project::k_color_reference_space through the color manager). - guard_int(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - Ok(write_string("", buf, buf_size)) - }) -} - -/// `oakengine_project_set_color_reference_space`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_set_color_reference_space( - self_: *mut OakEngineProject, - colorspace: *const c_char, -) -> c_int { - // Stub: see `oakengine_project_get_color_reference_space`; accepted as - // a no-op so callers do not break. - guard(|| unsafe { - if self_.is_null() || colorspace.is_null() { - return Err(Error::Invalid); - } - let _ = project_handle(self_)?; - Ok(()) - }) -} - -// --------------------------------------------------------------------------- -// node.h — enumeration -// --------------------------------------------------------------------------- - -/// `oakengine_node_last_error` — last node-family failure reason. -#[no_mangle] -pub extern "C" fn oakengine_node_last_error(buf: *mut c_char, buf_size: c_int) -> c_int { - crate::handle::guard_int(|| unsafe { - Ok(write_string( - &LAST_NODE_ERROR.with(|c| c.borrow().clone()), - buf, - buf_size, - )) - }) -} - -/// `oakengine_project_node_count` — number of nodes in the graph. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_node_count(self_: *const OakEngineProject) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - Ok(n::oaknode_project_node_count(unbox(self_)?)) - }) -} - -/// `oakengine_project_node_at` — borrowed node at `index`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_node_at( - self_: *const OakEngineProject, - index: c_int, -) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - if self_.is_null() || index < 0 { - return Ok(std::ptr::null_mut()); - } - let node = n::oaknode_project_node_at(unbox(self_)?, index); - if node.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(node)) - }) -} - -// --------------------------------------------------------------------------- -// node.h — node factory -// --------------------------------------------------------------------------- - -/// `oakengine_node_factory_id_count`. -#[no_mangle] -pub extern "C" fn oakengine_node_factory_id_count() -> c_int { - guard_int(|| { - let mut count: c_int = 0; - Error::from_module(unsafe { n::oaknode_factory_id_count(&mut count) })?; - Ok(count) - }) -} - -/// `oakengine_node_factory_create_from_id` — owned node, not added. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_factory_create_from_id( - type_id: *const c_char, -) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - if type_id.is_null() { - return Ok(std::ptr::null_mut()); - } - let node = n::oaknode_factory_create_from_id(type_id); - if node.ctx.is_null() { - set_node_error(&format!("unknown node type id \"{}\"", read_cstr(type_id))); - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(node)) - }) -} - -/// `oakengine_node_factory_name_from_id` — display name (buf/size). -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_factory_name_from_id( - type_id: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - if type_id.is_null() { - if !buf.is_null() && buf_size > 0 { - *buf = 0; - } - return Ok(0); - } - let rc = n::oaknode_factory_name_from_id(type_id, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_node_factory_node_at` — borrowed prototype node. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_factory_node_at(index: c_int) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - let mut out = CHandle::null(); - let rc = n::oaknode_factory_node_at(index, &mut out); - if rc != 0 || out.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(out)) - }) -} - -/// `oakengine_node_factory_id_at` — type id at `index` (two-stage). -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_factory_id_at( - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let rc = n::oaknode_factory_id_at(index, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_node_category_count`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_category_count(self_: *const OakEngineNode) -> c_int { - // Stub: the oaknode factory metadata exposes no category enumeration - // through the C ABI. - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let _ = unbox(self_)?; - Ok(0) - }) -} - -/// `oakengine_node_category_at`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_category_at( - self_: *const OakEngineNode, - index: c_int, -) -> c_int { - // Stub: see `oakengine_node_category_count`. - guard_int(|| unsafe { - if self_.is_null() { - return Ok(-1); - } - let _ = unbox(self_)?; - let _ = index; - Ok(-1) - }) -} - -/// `oakengine_node_get_flags`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_flags(self_: *const OakEngineNode) -> u64 { - // NULL and empty (null-ctx) handle boxes both report 0 — the - // `guard_i64` error sentinel would otherwise surface as u64::MAX to C - // callers. - crate::handle::guard_i64(|| unsafe { - if self_.is_null() { - return Ok(0); - } - if (*self_).handle.is_null() { - return Ok(0); - } - Ok(n::oaknode_node_get_flags(unbox(self_)?) as i64) - }) as u64 -} - -/// `oakengine_node_flag_dont_show_in_create_menu`. -#[no_mangle] -pub extern "C" fn oakengine_node_flag_dont_show_in_create_menu() -> u64 { - // Node::k_dont_show_in_create_menu (engine/node/node.h:118). - 0x8 -} - -/// `oakengine_node_flag_dont_show_in_param_view`. -#[no_mangle] -pub extern "C" fn oakengine_node_flag_dont_show_in_param_view() -> u64 { - // Node::k_dont_show_in_param_view (engine/node/node.h:115). - 0x1 -} - -/// `oakengine_node_flag_video_effect`. -#[no_mangle] -pub extern "C" fn oakengine_node_flag_video_effect() -> u64 { - // Node::k_video_effect (engine/node/node.h:116). - 0x2 -} - -/// `oakengine_node_flag_audio_effect`. -#[no_mangle] -pub extern "C" fn oakengine_node_flag_audio_effect() -> u64 { - // Node::k_audio_effect (engine/node/node.h:117). - 0x4 -} - -/// `oakengine_node_retranslate` — no-op: the module has no translation -/// pass (the engine re-reads translated strings on language change). -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_retranslate(self_: *mut OakEngineNode) { - guard_void(|| unsafe { - let _ = unbox(self_); - }) -} - -/// `oakengine_node_get_sub_category`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_sub_category( - self_: *const OakEngineNode, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - // Stub: the oaknode module has no sub-category export. - guard_int(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - Ok(write_string("", buf, buf_size)) - }) -} - -/// `oakengine_node_get_description`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_description( - self_: *const OakEngineNode, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - // Stub: the oaknode module has no description export. - guard_int(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - Ok(write_string("", buf, buf_size)) - }) -} - -/// `oakengine_node_create_copy` — standalone copy (caller owns). -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_create_copy( - self_: *const OakEngineNode, -) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - if self_.is_null() { - return Ok(std::ptr::null_mut()); - } - let copy = n::oaknode_node_create_copy(unbox(self_)?); - if copy.ctx.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(copy)) - }) -} - -// --------------------------------------------------------------------------- -// node.h — metadata -// --------------------------------------------------------------------------- - -/// `oakengine_node_get_type_id`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_type_id( - self_: *const OakEngineNode, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let h = unbox(self_)?; - let rc = n::oaknode_node_get_id(h, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_node_get_name`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_name( - self_: *const OakEngineNode, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let h = unbox(self_)?; - let rc = n::oaknode_node_get_name(h, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_node_get_short_name`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_short_name( - self_: *const OakEngineNode, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - // The oaknode module has no short-name export; the engine's default - // `short_name()` falls back to `name()`, so the name is returned. - guard_int(|| unsafe { - let h = unbox(self_)?; - let rc = n::oaknode_node_get_name(h, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_node_get_label`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_label( - self_: *const OakEngineNode, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let h = unbox(self_)?; - let rc = n::oaknode_node_get_label(h, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_node_set_label` — undoable rename (like set_label_ex(1)). -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_set_label( - self_: *mut OakEngineNode, - label: *const c_char, -) -> c_int { - guard(|| unsafe { - let h = node_handle(self_)?; - let mut cmd: CHandle = CHandle::null(); - let label = if label.is_null() { - crate::common::empty_cstr() - } else { - label - }; - let rc = n::oaknode_node_set_label_undoable(h, label, &mut cmd); - if rc != 0 { - return Err(Error::Module(rc)); - } - push_command(cmd, "Rename Node") - }) -} - -/// `oakengine_node_set_label_ex` — explicit undoable flag. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_set_label_ex( - self_: *mut OakEngineNode, - label: *const c_char, - undoable: c_int, -) -> c_int { - guard(|| unsafe { - if self_.is_null() { - set_node_error("invalid node"); - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let label = if label.is_null() { - crate::common::empty_cstr() - } else { - label - }; - if undoable != 0 { - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_node_set_label_undoable(h, label, &mut cmd); - if rc != 0 { - return Err(Error::Module(rc)); - } - push_command(cmd, "Rename Node") - } else { - Error::from_module(n::oaknode_node_set_label(h, label)) - } - }) -} - -/// `oakengine_node_set_label_many` — one command for several nodes. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_set_label_many( - nodes: *mut *mut OakEngineNode, - count: c_int, - label: *const c_char, -) -> c_int { - guard(|| unsafe { - let rc = oakengine_node_rename_many(nodes, count, label, std::ptr::null_mut()); - Error::from_module(rc) - }) -} - -/// `oakengine_node_rename_many` — with optional parent multi command. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_rename_many( - nodes: *mut *mut OakEngineNode, - count: c_int, - label: *const c_char, - parent_multi_or_null: *mut c_void, -) -> c_int { - guard(|| unsafe { - if count < 0 || (count > 0 && nodes.is_null()) { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - if count == 0 { - return Ok(()); - } - let label = if label.is_null() { - crate::common::empty_cstr() - } else { - label - }; - let mut cmds = Vec::with_capacity(count as usize); - for i in 0..count as usize { - let node = *nodes.add(i); - if node.is_null() { - set_node_error(&format!("invalid node at index {}", i)); - return Err(Error::Invalid); - } - let h = unbox(node)?; - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_node_set_label_undoable(h, label, &mut cmd); - if rc != 0 { - return Err(Error::Module(rc)); - } - cmds.push(cmd); - } - push_multi_commands(&cmds, parent_multi_or_null, "Rename Nodes") - }) -} - -/// `oakengine_node_rename_command` — opaque command pointer. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_rename_command( - node: *mut OakEngineNode, - label: *const c_char, -) -> *mut c_void { - guard_ptr(|| unsafe { - if node.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(node)?; - let label = if label.is_null() { - crate::common::empty_cstr() - } else { - label - }; - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_node_set_label_undoable(h, label, &mut cmd); - if rc != 0 { - return Ok(std::ptr::null_mut()); - } - Ok(command_box(cmd)?.cast()) - }) -} - -/// `oakengine_node_set_color_label` — one command for several nodes. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_set_color_label( - nodes: *mut *mut OakEngineNode, - count: c_int, - color_index: c_int, -) -> c_int { - guard(|| unsafe { - if count < 0 || (count > 0 && nodes.is_null()) { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - if count == 0 { - return Ok(()); - } - let mut cmds = Vec::with_capacity(count as usize); - for i in 0..count as usize { - let node = *nodes.add(i); - if node.is_null() { - set_node_error(&format!("invalid node at index {}", i)); - return Err(Error::Invalid); - } - let h = unbox(node)?; - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_node_set_override_color_undoable(h, color_index, &mut cmd); - if rc != 0 { - return Err(Error::Module(rc)); - } - cmds.push(cmd); - } - push_multi_commands(&cmds, std::ptr::null_mut(), "Set Node Color Labels") - }) -} - -/// `oakengine_node_set_color_label_command` — opaque command pointer. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_set_color_label_command( - node: *mut OakEngineNode, - color_index: c_int, -) -> *mut c_void { - guard_ptr(|| unsafe { - if node.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(node)?; - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_node_set_override_color_undoable(h, color_index, &mut cmd); - if rc != 0 { - return Ok(std::ptr::null_mut()); - } - Ok(command_box(cmd)?.cast()) - }) -} - -/// `oakengine_node_get_color_label`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_color_label(self_: *const OakEngineNode) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(-1); - } - let mut value: c_int = -1; - Error::from_module(n::oaknode_node_get_override_color( - unbox(self_)?, - &mut value, - ))?; - Ok(value) - }) -} - -/// `oakengine_node_get_effective_color_label`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_effective_color_label( - self_: *const OakEngineNode, -) -> c_int { - // The C++ falls back to the config "CatColor" value of the node's - // first category; the module has no category export, so the override - // color is returned as-is (documented divergence). - guard_int(|| unsafe { - if self_.is_null() { - return Ok(-1); - } - let mut value: c_int = -1; - Error::from_module(n::oaknode_node_get_override_color( - unbox(self_)?, - &mut value, - ))?; - Ok(value) - }) -} - -/// `oakengine_node_get_brush`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_brush( - self_: *const OakEngineNode, - _top: f64, - _bottom: f64, - _out_qbrush: *mut c_void, -) { - // Stub: oaknode has no QBrush surface (the C++ writes the node's - // title-bar brush into a caller QBrush; Qt-only). - guard_void(|| unsafe { - let _ = unbox(self_); - }) -} - -// --------------------------------------------------------------------------- -// node.h — input introspection -// --------------------------------------------------------------------------- - -/// `oakengine_node_input_count`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_count(self_: *const OakEngineNode) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let mut count: c_int = 0; - Error::from_module(n::oaknode_node_input_count(unbox(self_)?, &mut count))?; - Ok(count) - }) -} - -/// `oakengine_node_input_id` — input id at `index`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_id( - self_: *const OakEngineNode, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let h = unbox(self_)?; - let mut count: c_int = 0; - Error::from_module(n::oaknode_node_input_count(h, &mut count))?; - if index < 0 || index >= count { - return Err(Error::NotFound); - } - let rc = n::oaknode_node_input_id(h, index, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_node_input_get_type` — value type as oak_node_value_type. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_get_type( - self_: *const OakEngineNode, - input_id: *const c_char, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Ok(0); - } - let mut ty: c_int = 0; - Error::from_module(n::oaknode_node_input_get_type( - unbox(self_)?, - input_id, - &mut ty, - ))?; - Ok(ty) - }) -} - -/// `oakengine_node_input_is_connected`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_is_connected( - self_: *const OakEngineNode, - input_id: *const c_char, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Ok(0); - } - let mut value: c_int = 0; - Error::from_module(n::oaknode_node_input_is_connected( - unbox(self_)?, - input_id, - &mut value, - ))?; - Ok(value) - }) -} - -// --------------------------------------------------------------------------- -// node.h — parameter access -// --------------------------------------------------------------------------- - -/// `oakengine_node_get_input` — standard value mapped into the POD. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_input( - self_: *const OakEngineNode, - input_id: *const c_char, - out: *mut OakNodeValue, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || input_id.is_null() || out.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let rc = n::oaknode_node_get_input(unbox(self_)?, input_id, out); - Error::from_module(rc) - }) -} - -/// `oakengine_node_set_input` — undoable standard-value write. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_set_input( - self_: *mut OakEngineNode, - input_id: *const c_char, - v: *const OakNodeValue, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || input_id.is_null() || v.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_node_set_input_undoable(h, input_id, v, &mut cmd); - if rc != 0 { - return Err(Error::Module(rc)); - } - push_command(cmd, "Set Node Value") - }) -} - -/// `oakengine_node_get_input_string` — string input read (buf/size). -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_input_string( - self_: *const OakEngineNode, - input_id: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let rc = n::oaknode_node_get_input_string(unbox(self_)?, input_id, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_node_set_input_string` — undoable string write. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_set_input_string( - self_: *mut OakEngineNode, - input_id: *const c_char, - s: *const c_char, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || input_id.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let s = if s.is_null() { - crate::common::empty_cstr() - } else { - s - }; - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_node_set_input_string_undoable(h, input_id, s, &mut cmd); - if rc != 0 { - return Err(Error::Module(rc)); - } - push_command(cmd, "Set Node Value") - }) -} - -/// `oakengine_node_set_standard_value_command` — opaque command pointer. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_set_standard_value_command( - self_: *mut OakEngineNode, - input_id: *const c_char, - _element: c_int, - _track: c_int, - v: *const OakNodeValue, -) -> *mut c_void { - guard_ptr(|| unsafe { - if self_.is_null() || input_id.is_null() || v.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(self_)?; - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_node_set_input_undoable(h, input_id, v, &mut cmd); - if rc != 0 { - return Ok(std::ptr::null_mut()); - } - Ok(command_box(cmd)?.cast()) - }) -} - -/// `oakengine_node_set_input_video_params_command`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_set_input_video_params_command( - self_: *mut OakEngineNode, - input_id: *const c_char, - _params: *const OakVideoParamsPod, -) -> *mut c_void { - // Stub: the oaknode module has no k_video_params command creator. - guard_ptr(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Ok(std::ptr::null_mut()); - } - let _ = unbox(self_)?; - Ok(std::ptr::null_mut()) - }) -} - -/// `oakengine_node_set_value_at_time_command` — opaque command pointer. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_set_value_at_time_command( - node: *mut c_void, - input: *const c_char, - _element: c_int, - time_num: i64, - time_den: i64, - value: *const OakNodeValue, - track: c_int, - insert_on_all_tracks_if_no_key: c_int, -) -> *mut c_void { - guard_ptr(|| unsafe { - if node.is_null() || input.is_null() || value.is_null() || time_den == 0 { - return Ok(std::ptr::null_mut()); - } - let h = unbox(node.cast::())?; - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_node_set_input_at_time_undoable( - h, input, time_num, time_den, value, track, &mut cmd, - ); - if rc != 0 { - return Ok(std::ptr::null_mut()); - } - let _ = insert_on_all_tracks_if_no_key; - Ok(command_box(cmd)?.cast()) - }) -} - -/// `oakengine_node_frame_time_base` — seconds per frame of the project's -/// first sequence (flipped), or the engine default 1001/30000. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_frame_time_base( - self_: *const OakEngineNode, - num: *mut c_int, - den: *mut c_int, -) -> c_int { - guard(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let (n_, d_) = time_base_for(h); - if !num.is_null() { - *num = n_ as c_int; - } - if !den.is_null() { - *den = d_ as c_int; - } - Ok(()) - }) -} - -/// `oakengine_node_set_input_at_time` — undoable value at a frame -/// timestamp (set_value_at_time semantics). -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_set_input_at_time( - self_: *mut OakEngineNode, - input_id: *const c_char, - element: c_int, - time_ts: i64, - track: c_int, - v: *const OakNodeValue, - insert_on_all_tracks: c_int, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || input_id.is_null() || v.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let tb = time_base_for(h); - let (t_num, t_den) = ts_to_time(time_ts, tb); - let mut cmd: CHandle = CHandle::null(); - // The module's at-time setter is whole-value and ignores the track - // selector; track -1 (all components) and per-component tracks - // collapse to the whole value (documented deviation). - let rc = n::oaknode_node_set_input_at_time_undoable( - h, - input_id, - t_num, - t_den, - v, - if track < 0 { 0 } else { track }, - &mut cmd, - ); - if rc != 0 { - return Err(Error::Module(rc)); - } - let _ = element; - let _ = insert_on_all_tracks; - push_command(cmd, "Set Input Value") - }) -} - -/// `oakengine_node_set_input_string_at_time`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_set_input_string_at_time( - self_: *mut OakEngineNode, - input_id: *const c_char, - _element: c_int, - _time_ts: i64, - _value: *const c_char, -) -> c_int { - // Stub: the module's at-time setter is POD-only; string inputs have no - // value-at-time path. - guard(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - Err(Error::Invalid) - }) -} - -// --------------------------------------------------------------------------- -// node.h — array inputs -// --------------------------------------------------------------------------- - -/// `oakengine_node_array_insert_at` — insert an array element (undoable). -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_array_insert_at( - self_: *mut OakEngineNode, - input_id: *const c_char, - index: c_int, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || input_id.is_null() || index < 0 { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let mut ty: c_int = 0; - let rc = n::oaknode_node_input_get_type(h, input_id, &mut ty); - if rc != 0 { - return Err(Error::Module(rc)); - } - // The module has no undoable array-insert command; the live insert - // is applied directly (documented deviation). - let rc = n::oaknode_node_input_array_insert(h, input_id, index); - Error::from_module(rc) - }) -} - -/// `oakengine_node_array_remove_at` — remove an array element (undoable). -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_array_remove_at( - self_: *mut OakEngineNode, - input_id: *const c_char, - index: c_int, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || input_id.is_null() || index < 0 { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let h = unbox(self_)?; - // The module has no undoable array-remove command; the live remove - // is applied directly (documented deviation). - let rc = n::oaknode_node_input_array_remove(h, input_id, index); - Error::from_module(rc) - }) -} - -// --------------------------------------------------------------------------- -// node.h — graph editing -// --------------------------------------------------------------------------- - -/// `oakengine_project_add_node` — create a node of `type_id` in the -/// project (undoable). -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_add_node( - project: *mut OakEngineProject, - type_id: *const c_char, -) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - if project.is_null() || type_id.is_null() { - set_node_error("invalid project or type id"); - return Ok(std::ptr::null_mut()); - } - let ph = unbox(project)?; - // The identities of the nodes already in the project; the newly - // added node is the one whose identity is new. - let existing: Vec = (0..n::oaknode_project_node_count(ph)) - .filter_map(|i| { - let other = n::oaknode_project_node_at(ph, i); - if other.is_null() { - None - } else { - let id = n::oaknode_node_identity(other); - if id == 0 { - None - } else { - Some(id) - } - } - }) - .collect(); - let node = n::oaknode_factory_create_from_id(type_id); - if node.ctx.is_null() { - set_node_error(&format!("unknown node type id \"{}\"", read_cstr(type_id))); - return Ok(std::ptr::null_mut()); - } - let cmd = n::oaknode_command_create_add_node(ph, node); - if cmd.ctx.is_null() { - return Ok(std::ptr::null_mut()); - } - push_command(cmd, "Add Node")?; - // The module's command-based add moves the node without rewriting - // the caller's handle, so a fresh borrowed view is derived from - // the project graph (the capi's `wrap(node)` has no module - // equivalent for the command path). - let type_id_str = read_cstr(type_id); - let total = n::oaknode_project_node_count(ph); - for i in 0..total { - let other = n::oaknode_project_node_at(ph, i); - if other.is_null() { - continue; - } - let id = n::oaknode_node_identity(other); - if existing.contains(&id) { - continue; - } - if is_node_type(other, &type_id_str) { - // The AddNode command MOVED the node into the project graph, - // so the factory's owned handle is now just a stale view: - // release it (the node itself stays graph-owned) so the - // debug alive counter returns to baseline. - let mut owned = node; - n::oaknode_node_free(&mut owned); - return Ok(box_handle::(other)); - } - } - // Fallback: return the (stale) factory handle; read-only metadata - // queries still work while the node lived in its source project. - Ok(box_handle::(node)) - }) -} - -/// `oakengine_project_remove_node` — remove a node, disconnecting edges -/// (undoable). -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_remove_node( - project: *mut OakEngineProject, - node: *mut OakEngineNode, -) -> c_int { - guard(|| unsafe { - if project.is_null() || node.is_null() { - set_node_error("invalid project or node"); - return Err(Error::Invalid); - } - let ph = unbox(project)?; - let nh = unbox(node)?; - // Verify the node belongs to this project. - if !project_contains_node(ph, nh) { - set_node_error("node does not belong to this project"); - return Err(Error::Invalid); - } - let cmd = n::oaknode_command_create_remove_node(nh); - if cmd.ctx.is_null() { - return Err(Error::Failed("remove node command failed".into())); - } - push_command(cmd, "Remove Node") - }) -} - -/// `oakengine_node_delete_later` — deferred deletion (Qt `deleteLater`). -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_delete_later(node: *mut OakEngineNode) { - // Stub: there is no event-loop based deferred deletion in the module - // world; orphaned nodes are freed synchronously with - // `oakengine_node_free` instead. - guard_void(|| unsafe { - let _ = unbox(node); - }) -} - -/// `oakengine_node_free` — destroy an OWNED, orphaned node immediately. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_free(node: *mut OakEngineNode) { - guard_void(|| unsafe { - if node.is_null() { - return; - } - let mut h = (*node).handle; - n::oaknode_node_free(&mut h); - drop(Box::from_raw(node)); - }) -} - -/// `oakengine_node_connect` — undoable edge add. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_connect( - output_node: *mut OakEngineNode, - input_node: *mut OakEngineNode, - input_id: *const c_char, -) -> c_int { - guard(|| unsafe { - if output_node.is_null() || input_node.is_null() || input_id.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let out_h = unbox(output_node)?; - let in_h = unbox(input_node)?; - // Mirror the module's live connect rejection: an already-connected - // input is a STATE error. The UNDOABLE creator validates existence - // and connectability but not "already connected" (its redo swallows - // the state error), so the facade pre-checks like the live variant. - let mut connected: c_int = 0; - Error::from_module(n::oaknode_node_input_is_connected( - in_h, - input_id, - &mut connected, - ))?; - if connected != 0 { - set_node_error("input is already connected"); - return Err(Error::Module(oaknode::error::OAKNODE_E_STATE)); - } - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_node_connect_undoable(out_h, in_h, input_id, &mut cmd); - if rc != 0 { - // Distinguish the module's "already connected" state from other - // failures so the caller gets the engine's E_STATE. - return Err(Error::Module(rc)); - } - push_command(cmd, "Connect Nodes") - }) -} - -/// `oakengine_node_disconnect` — remove the edge (element -1). -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_disconnect( - input_node: *mut OakEngineNode, - input_id: *const c_char, -) -> c_int { - guard(|| unsafe { - let rc = oakengine_node_disconnect_ex(input_node, input_id, -1); - Error::from_module(rc) - }) -} - -/// `oakengine_node_disconnect_ex` — remove the edge at `element`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_disconnect_ex( - input_node: *mut OakEngineNode, - input_id: *const c_char, - element: c_int, -) -> c_int { - guard(|| unsafe { - if input_node.is_null() || input_id.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let in_h = unbox(input_node)?; - // The module's disconnect command ignores the element; a - // whole-input disconnect is the only variant. - let _ = element; - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_node_disconnect_undoable(in_h, input_id, &mut cmd); - if rc != 0 { - return Err(Error::Module(rc)); - } - push_command(cmd, "Disconnect Nodes") - }) -} - -/// `oakengine_node_connect_command` — opaque NodeEdgeAddCommand pointer. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_connect_command( - output_node: *mut OakEngineNode, - input_node: *mut OakEngineNode, - input_id: *const c_char, - element: c_int, -) -> *mut c_void { - guard_ptr(|| unsafe { - if output_node.is_null() || input_node.is_null() || input_id.is_null() { - return Ok(std::ptr::null_mut()); - } - let out_h = unbox(output_node)?; - let in_h = unbox(input_node)?; - let _ = element; - // Same duplicate-connect rejection as `oakengine_node_connect`. - let mut connected: c_int = 0; - Error::from_module(n::oaknode_node_input_is_connected( - in_h, - input_id, - &mut connected, - ))?; - if connected != 0 { - return Ok(std::ptr::null_mut()); - } - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_node_connect_undoable(out_h, in_h, input_id, &mut cmd); - if rc != 0 { - return Ok(std::ptr::null_mut()); - } - Ok(command_box(cmd)?.cast()) - }) -} - -/// `oakengine_node_disconnect_command` — opaque NodeEdgeRemoveCommand. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_disconnect_command( - input_node: *mut OakEngineNode, - input_id: *const c_char, - element: c_int, -) -> *mut c_void { - guard_ptr(|| unsafe { - if input_node.is_null() || input_id.is_null() { - return Ok(std::ptr::null_mut()); - } - let in_h = unbox(input_node)?; - let _ = element; - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_node_disconnect_undoable(in_h, input_id, &mut cmd); - if rc != 0 { - return Ok(std::ptr::null_mut()); - } - Ok(command_box(cmd)?.cast()) - }) -} - -/// `oakengine_block_link` — link/unlink two nodes directly. -#[no_mangle] -pub unsafe extern "C" fn oakengine_block_link( - a: *mut c_void, - b: *mut c_void, - linked: c_int, -) -> c_int { - guard_int(|| unsafe { - if a.is_null() || b.is_null() { - return Err(Error::Invalid); - } - let ah = unbox(a.cast::())?; - let bh = unbox(b.cast::())?; - let mut value: c_int = 0; - let rc = if linked != 0 { - n::oaknode_node_link(ah, bh, &mut value) - } else { - n::oaknode_node_unlink(ah, bh, &mut value) - }; - if rc != 0 { - return Err(Error::Module(rc)); - } - Ok(value) - }) -} - -/// `oakengine_node_add_to_project_command` — opaque NodeAddCommand. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_add_to_project_command( - project: *mut OakEngineProject, - node: *mut OakEngineNode, -) -> *mut c_void { - guard_ptr(|| unsafe { - if project.is_null() || node.is_null() { - return Ok(std::ptr::null_mut()); - } - let ph = unbox(project)?; - let nh = unbox(node)?; - let cmd = n::oaknode_command_create_add_node(ph, nh); - if cmd.ctx.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(command_box(cmd)?.cast()) - }) -} - -/// `oakengine_node_set_value_hint` — traverse value hint on an input. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_set_value_hint( - self_: *mut OakEngineNode, - input_id: *const c_char, - element: c_int, - type_: c_int, - index: c_int, - tag: *const c_char, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || input_id.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let h = unbox(self_)?; - // The module's hint setter is a simplified single-type variant; the - // element and tag are not representable (documented deviation). - let _ = element; - let _ = tag; - let rc = n::oaknode_node_set_value_hint_track(h, input_id, type_, index); - Error::from_module(rc) - }) -} - -// --------------------------------------------------------------------------- -// node.h — parameter animation (keyframes) -// --------------------------------------------------------------------------- - -/// `oakengine_node_input_is_keyframed`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_is_keyframed( - self_: *const OakEngineNode, - input_id: *const c_char, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Ok(0); - } - let rc = n::oaknode_node_is_input_keyframing(unbox(self_)?, input_id); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(rc) - } - }) -} - -/// `oakengine_node_keyframe_count` — keyframes on track 0. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframe_count( - self_: *const OakEngineNode, - input_id: *const c_char, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Ok(0); - } - let rc = n::oaknode_node_keyframe_count(unbox(self_)?, input_id); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(rc) - } - }) -} - -/// `oakengine_node_keyframe_at` — read keyframe at `index`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframe_at( - self_: *const OakEngineNode, - input_id: *const c_char, - index: c_int, - time_ts: *mut i64, - value: *mut OakNodeValue, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || input_id.is_null() || time_ts.is_null() || value.is_null() { - return Err(Error::Invalid); - } - 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(()) - }) -} - -/// `oakengine_node_keyframe_get_easing`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframe_get_easing( - self_: *const OakEngineNode, - input_id: *const c_char, - index: c_int, - x1: *mut f32, - y1: *mut f32, - x2: *mut f32, - y2: *mut f32, - type_: *mut c_int, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || input_id.is_null() || type_.is_null() { - return Err(Error::Invalid); - } - 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(()) - }) -} - -/// `oakengine_node_keyframe_add` — add a keyframe at `time_ts`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframe_add( - self_: *mut OakEngineNode, - input_id: *const c_char, - time_ts: i64, - value: *const OakNodeValue, - type_: c_int, - x1: f32, - y1: f32, - x2: f32, - y2: f32, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || input_id.is_null() || value.is_null() || type_ < 0 || type_ > 2 { - set_node_error("invalid arguments or easing type"); - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let tb = time_base_for(h); - let (t_num, t_den) = ts_to_time(time_ts, tb); - // The module's set_value_at_time is the closest analogue of the - // engine's insert path: on a keyframed input it inserts/updates the - // key, otherwise it writes the standard value (the module cannot - // enable keyframing, so the "already exists" E_STATE check and the - // keyframing enablement of the capi are not reachable). - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_node_set_input_at_time_undoable( - h, input_id, t_num, t_den, value, 0, &mut cmd, - ); - if rc != 0 { - return Err(Error::Module(rc)); - } - let _ = (x1, y1, x2, y2); - push_command(cmd, "Add Keyframe") - }) -} - -/// `oakengine_node_keyframe_remove`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframe_remove( - self_: *mut OakEngineNode, - input_id: *const c_char, - time_ts: i64, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Err(Error::Invalid); - } - 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)) - }) -} - -/// `oakengine_node_insert_keyframe_command` — opaque command pointer. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_insert_keyframe_command( - self_: *mut OakEngineNode, - input_id: *const c_char, - element: c_int, - track: c_int, - time_ts: i64, - value: *const OakNodeValue, - type_: c_int, - x1: f32, - y1: f32, - x2: f32, - y2: f32, -) -> *mut c_void { - guard_ptr(|| unsafe { - if self_.is_null() || input_id.is_null() || value.is_null() || type_ < 0 || type_ > 2 { - return Ok(std::ptr::null_mut()); - } - let h = unbox(self_)?; - let tb = time_base_for(h); - let (t_num, t_den) = ts_to_time(time_ts, tb); - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_node_set_input_at_time_undoable( - h, input_id, t_num, t_den, value, track, &mut cmd, - ); - if rc != 0 { - return Ok(std::ptr::null_mut()); - } - let _ = element; - let _ = (x1, y1, x2, y2); - Ok(command_box(cmd)?.cast()) - }) -} - -/// `oakengine_node_remove_keyframe_command`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_remove_keyframe_command( - keyframe: *mut OakEngineKeyframe, -) -> *mut c_void { - guard_ptr(|| unsafe { - 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()) - }) -} - -/// `oakengine_keyframe_set_time_command`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_keyframe_set_time_command( - keyframe: *mut OakEngineKeyframe, - new_time_ts: i64, -) -> *mut c_void { - guard_ptr(|| unsafe { - let h = unbox(keyframe)?; - // The keyframe handle's own project is not reachable from the - // module keyframe box, so the default time base applies. - let tb = (1001, 30000); - let (t_num, t_den) = ts_to_time(new_time_ts, tb); - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_keyframe_set_time_undoable(h, t_num, t_den, &mut cmd); - if rc != 0 { - return Ok(std::ptr::null_mut()); - } - Ok(command_box(cmd)?.cast()) - }) -} - -/// `oakengine_keyframe_set_value_command`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_keyframe_set_value_command( - keyframe: *mut OakEngineKeyframe, - value: *const OakNodeValue, -) -> *mut c_void { - guard_ptr(|| unsafe { - let h = unbox(keyframe)?; - if value.is_null() { - return Ok(std::ptr::null_mut()); - } - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_keyframe_set_value_undoable(h, value, &mut cmd); - if rc != 0 { - return Ok(std::ptr::null_mut()); - } - Ok(command_box(cmd)?.cast()) - }) -} - -/// `oakengine_node_keyframe_set_easing`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframe_set_easing( - self_: *mut OakEngineNode, - input_id: *const c_char, - time_ts: i64, - type_: c_int, - x1: f32, - y1: f32, - x2: f32, - y2: f32, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Err(Error::Invalid); - } - 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( - self_: *mut OakEngineNode, - input_id: *const c_char, - element: c_int, - times_ts: *const i64, - tracks: *const c_int, - count: c_int, - type_: c_int, -) -> c_int { - // 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 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(()) - }) -} - -/// `oakengine_node_keyframes_set_time_many`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframes_set_time_many( - self_: *mut OakEngineNode, - input_id: *const c_char, - element: c_int, - old_times_ts: *const i64, - tracks: *const c_int, - count: c_int, - new_time_ts: i64, -) -> c_int { - // 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 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(()) - }) -} - -/// `oakengine_node_keyframes_set_value_many`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframes_set_value_many( - self_: *mut OakEngineNode, - input_id: *const c_char, - element: c_int, - times_ts: *const i64, - tracks: *const c_int, - count: c_int, - values: *const OakNodeValue, - old_values: *const OakNodeValue, -) -> c_int { - // 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 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(()) - }) -} - -/// `oakengine_node_keyframes_set_bezier_many`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframes_set_bezier_many( - self_: *mut OakEngineNode, - input_id: *const c_char, - element: c_int, - times_ts: *const i64, - tracks: *const c_int, - count: c_int, - in_x: f64, - in_y: f64, - out_x: f64, - out_y: f64, -) -> c_int { - // 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 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(()) - }) -} - -/// `oakengine_node_keyframe_set_bezier_point`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframe_set_bezier_point( - self_: *mut OakEngineNode, - input_id: *const c_char, - element: c_int, - time_ts: i64, - track: c_int, - point_index: c_int, - x: f64, - y: f64, - old_x: f64, - old_y: f64, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || input_id.is_null() || (point_index != 0 && point_index != 1) { - return Err(Error::Invalid); - } - 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, - )) - }) -} - -/// `oakengine_node_keyframes_clear` — remove all keyframes (no-op when -/// none). -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframes_clear( - self_: *mut OakEngineNode, - input_id: *const c_char, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Err(Error::Invalid); - } - Error::from_module(n::oaknode_node_clear_keyframes(unbox(self_)?, input_id)) - }) -} - -// --------------------------------------------------------------------------- -// node.h — extended input introspection -// --------------------------------------------------------------------------- - -/// `oakengine_node_input_is_array`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_is_array( - self_: *const OakEngineNode, - input_id: *const c_char, -) -> c_int { - // Stub: the oaknode module has no array-flag query. - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Ok(0); - } - let _ = unbox(self_)?; - Ok(0) - }) -} - -/// `oakengine_node_input_array_size`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_array_size( - self_: *const OakEngineNode, - input_id: *const c_char, -) -> c_int { - // Stub: see `oakengine_node_input_is_array`. - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Ok(0); - } - let _ = unbox(self_)?; - Ok(0) - }) -} - -/// `oakengine_node_input_get_flags`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_get_flags( - self_: *const OakEngineNode, - input_id: *const c_char, -) -> c_int { - // Stub: the oaknode module has no input-flags export. - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Ok(0); - } - let _ = unbox(self_)?; - Ok(0) - }) -} - -/// `oakengine_node_input_get_data_type` — NodeValue::Type ordinal. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_get_data_type( - self_: *const OakEngineNode, - input_id: *const c_char, -) -> c_int { - // Stub: the oaknode module reports the oak value type (which differs - // from the olive::NodeValue::Type ordinal); the C++ ordinal is not - // reachable. - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Ok(-1); - } - let _ = unbox(self_)?; - Ok(-1) - }) -} - -/// `oakengine_node_input_is_connectable`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_is_connectable( - self_: *const OakEngineNode, - input_id: *const c_char, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Ok(0); - } - let mut value: c_int = 0; - Error::from_module(n::oaknode_node_input_is_connectable( - unbox(self_)?, - input_id, - &mut value, - ))?; - Ok(value) - }) -} - -/// `oakengine_node_input_is_keyframable`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_is_keyframable( - self_: *const OakEngineNode, - input_id: *const c_char, -) -> c_int { - // Derived from the input's value type: the POD types (int..combo) are - // keyframable, everything else is not (the module has no keyframable - // query of its own). - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Ok(0); - } - let mut ty: c_int = 0; - Error::from_module(n::oaknode_node_input_get_type( - unbox(self_)?, - input_id, - &mut ty, - ))?; - let keyframable = matches!( - ty, - value_type::INT - | value_type::FLOAT - | value_type::BOOL - | value_type::RATIONAL - | value_type::COLOR - | value_type::VEC2 - | value_type::VEC3 - | value_type::VEC4 - | value_type::COMBO - ); - Ok(if keyframable { 1 } else { 0 }) - }) -} - -/// `oakengine_node_input_is_hidden`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_is_hidden( - self_: *const OakEngineNode, - input_id: *const c_char, -) -> c_int { - // Stub: the oaknode module has no input-flags export. - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Ok(0); - } - let _ = unbox(self_)?; - Ok(0) - }) -} - -/// `oakengine_node_input_is_keyframed_ex`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_is_keyframed_ex( - self_: *const OakEngineNode, - input_id: *const c_char, - element: c_int, -) -> c_int { - // 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 rc = n::oaknode_node_is_input_keyframing(unbox(self_)?, input_id); - let _ = element; - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(rc) - } - }) -} - -/// `oakengine_node_get_label_and_name`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_label_and_name( - self_: *const OakEngineNode, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let h = unbox(self_)?; - let label = module_string(|b, s| n::oaknode_node_get_label(h, b, s))?; - let name = module_string(|b, s| n::oaknode_node_get_name(h, b, s))?; - // The engine's default `get_label_and_name()`: label when set, - // otherwise the name. - Ok(write_string( - if label.is_empty() { &name } else { &label }, - buf, - buf_size, - )) - }) -} - -/// `oakengine_node_get_input_name`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_input_name( - self_: *const OakEngineNode, - input_id: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Err(Error::Invalid); - } - let rc = n::oaknode_node_get_input_name(unbox(self_)?, input_id, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_node_input_get_default_value`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_get_default_value( - self_: *const OakEngineNode, - input_id: *const c_char, - track: c_int, - out: *mut OakNodeValue, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || input_id.is_null() || out.is_null() { - return Err(Error::Invalid); - } - let _ = track; - // 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(()) - }) -} - -/// `oakengine_node_get_project`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_project( - self_: *const OakEngineNode, -) -> *mut OakEngineProject { - guard_ptr(|| unsafe { - if self_.is_null() { - return Ok(std::ptr::null_mut()); - } - let mut out = CHandle::null(); - Error::from_module(n::oaknode_node_get_project(unbox(self_)?, &mut out))?; - if out.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(out)) - }) -} - -/// `oakengine_node_parent`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_parent( - self_: *const OakEngineNode, -) -> *mut OakEngineProject { - // The module has no graph-parent concept; the owning project is the - // same value the C++ reports for both accessors. - guard_ptr(|| unsafe { - if self_.is_null() { - return Ok(std::ptr::null_mut()); - } - let mut out = CHandle::null(); - Error::from_module(n::oaknode_node_get_project(unbox(self_)?, &mut out))?; - if out.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(out)) - }) -} - -/// `oakengine_node_is_item`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_is_item(self_: *const OakEngineNode) -> c_int { - // Item = folder | footage | sequence | group (the project tree types). - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let h = unbox(self_)?; - Ok( - if is_node_type(h, TYPE_ID_FOLDER) - || is_node_type(h, TYPE_ID_FOOTAGE) - || is_node_type(h, TYPE_ID_SEQUENCE) - || is_node_type(h, TYPE_ID_GROUP) - { - 1 - } else { - 0 - }, - ) - }) -} - -/// `oakengine_node_folder` — the folder owning this item node. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_folder(self_: *const OakEngineNode) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - if self_.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(self_)?; - // Walk the project's folders for a recursive parent relationship. - if let Ok(project) = project_of(h) { - let total = n::oaknode_project_node_count(project); - for i in 0..total { - let folder = n::oaknode_project_node_at(project, i); - if folder.is_null() || !is_node_type(folder, TYPE_ID_FOLDER) { - continue; - } - if n::oaknode_folder_has_child_recursive(folder, h) != 0 { - return Ok(box_handle::(folder)); - } - } - } - Ok(std::ptr::null_mut()) - }) -} - -/// `oakengine_node_input_get_connected_node`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_get_connected_node( - self_: *const OakEngineNode, - input_id: *const c_char, - element: c_int, -) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(self_)?; - let _ = element; - let mut out = CHandle::null(); - let rc = n::oaknode_node_input_get_connected_node(h, input_id, &mut out); - if rc != 0 || out.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(out)) - }) -} - -/// `oakengine_node_copy_inputs` — copy values from `src` to `dest`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_copy_inputs( - dest: *mut OakEngineNode, - src: *const OakEngineNode, -) -> c_int { - guard(|| unsafe { - if dest.is_null() || src.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let rc = n::oaknode_node_copy_inputs(unbox(dest)?, unbox(src)?, 0); - Error::from_module(rc) - }) -} - -/// `oakengine_node_get_input_at_time` — value at a frame timestamp. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_input_at_time( - self_: *const OakEngineNode, - input_id: *const c_char, - element: c_int, - track: c_int, - time_ts: i64, - track_for_time: c_int, - out: *mut OakNodeValue, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || input_id.is_null() || out.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let tb = time_base_for(h); - let (t_num, t_den) = ts_to_time(time_ts, tb); - let _ = (element, track, track_for_time); - let rc = n::oaknode_node_get_input_at_time(h, input_id, t_num, t_den, out); - Error::from_module(rc) - }) -} - -/// `oakengine_node_get_input_string_at_time`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_input_string_at_time( - self_: *const OakEngineNode, - input_id: *const c_char, - element: c_int, - time_ts: i64, - track: c_int, - _buf: *mut c_char, - _buf_size: c_int, -) -> c_int { - // Stub: the module's at-time reader is POD-only. - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - let _ = (element, time_ts, track); - Err(Error::Invalid) - }) -} - -/// `oakengine_node_get_input_bezier_at_time`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_input_bezier_at_time( - self_: *const OakEngineNode, - input_id: *const c_char, - element: c_int, - time_ts: i64, - track: c_int, - out_6: *mut f64, -) -> c_int { - // Stub: the module's at-time reader is POD-only (no bezier). - guard(|| unsafe { - if self_.is_null() || input_id.is_null() || out_6.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - let _ = (element, time_ts, track); - Err(Error::Invalid) - }) -} - -/// `oakengine_node_get_input_binary_at_time`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_input_binary_at_time( - self_: *const OakEngineNode, - input_id: *const c_char, - element: c_int, - time_ts: i64, - track: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - // Stub: the module's at-time reader is POD-only (no binary). - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - let _ = (element, time_ts, track, buf, buf_size); - Err(Error::Invalid) - }) -} - -// --------------------------------------------------------------------------- -// node.h — input properties -// --------------------------------------------------------------------------- - -/// `oakengine_node_input_has_property`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_has_property( - self_: *const OakEngineNode, - input_id: *const c_char, - key: *const c_char, -) -> c_int { - // Stub: the oaknode module has no input-property C ABI. - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() || key.is_null() { - return Ok(0); - } - let _ = unbox(self_)?; - Ok(0) - }) -} - -/// `oakengine_node_set_input_property_string`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_set_input_property_string( - self_: *mut OakEngineNode, - input_id: *const c_char, - key: *const c_char, - value: *const c_char, - notify: c_int, -) -> c_int { - // Stub: see `oakengine_node_input_has_property`. - guard(|| unsafe { - if self_.is_null() || input_id.is_null() || key.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - let _ = (value, notify); - Err(Error::NotFound) - }) -} - -/// `oakengine_node_input_get_property_string`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_get_property_string( - self_: *const OakEngineNode, - input_id: *const c_char, - key: *const c_char, - _buf: *mut c_char, - _buf_size: c_int, -) -> c_int { - // Stub: see `oakengine_node_input_has_property`. - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() || key.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - Err(Error::NotFound) - }) -} - -/// `oakengine_node_input_get_property_number`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_get_property_number( - self_: *const OakEngineNode, - input_id: *const c_char, - key: *const c_char, - track: c_int, - out: *mut f64, -) -> c_int { - // Stub: see `oakengine_node_input_has_property`. - guard(|| unsafe { - if self_.is_null() || input_id.is_null() || key.is_null() || out.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - let _ = track; - Err(Error::NotFound) - }) -} - -/// `oakengine_node_input_get_property_int`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_get_property_int( - self_: *const OakEngineNode, - input_id: *const c_char, - key: *const c_char, - out: *mut i64, -) -> c_int { - // Stub: see `oakengine_node_input_has_property`. - guard(|| unsafe { - if self_.is_null() || input_id.is_null() || key.is_null() || out.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - Err(Error::NotFound) - }) -} - -/// `oakengine_node_input_get_property_rational`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_get_property_rational( - self_: *const OakEngineNode, - input_id: *const c_char, - key: *const c_char, - num: *mut c_int, - den: *mut c_int, -) -> c_int { - // Stub: see `oakengine_node_input_has_property`. - guard(|| unsafe { - if self_.is_null() || input_id.is_null() || key.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - let _ = (num, den); - Err(Error::NotFound) - }) -} - -/// `oakengine_node_input_get_property_track_number`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_get_property_track_number( - self_: *const OakEngineNode, - input_id: *const c_char, - key: *const c_char, - track: c_int, - out: *mut f64, -) -> c_int { - // Stub: see `oakengine_node_input_has_property`. - guard(|| unsafe { - if self_.is_null() || input_id.is_null() || key.is_null() || out.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - let _ = track; - Err(Error::NotFound) - }) -} - -/// `oakengine_node_input_get_property_count`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_get_property_count( - self_: *const OakEngineNode, - input_id: *const c_char, -) -> c_int { - // Stub: see `oakengine_node_input_has_property`. - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Ok(0); - } - let _ = unbox(self_)?; - Ok(0) - }) -} - -/// `oakengine_node_input_get_property_key`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_get_property_key( - self_: *const OakEngineNode, - input_id: *const c_char, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - // Stub: see `oakengine_node_input_has_property`. - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - let _ = (index, buf, buf_size); - Err(Error::NotFound) - }) -} - -/// `oakengine_node_input_get_property_string_list_count`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_get_property_string_list_count( - self_: *const OakEngineNode, - input_id: *const c_char, - key: *const c_char, -) -> c_int { - // Stub: see `oakengine_node_input_has_property`. - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() || key.is_null() { - return Ok(0); - } - let _ = unbox(self_)?; - Ok(0) - }) -} - -/// `oakengine_node_input_get_property_string_list`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_get_property_string_list( - self_: *const OakEngineNode, - input_id: *const c_char, - key: *const c_char, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - // Stub: see `oakengine_node_input_has_property`. - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() || key.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - let _ = (index, buf, buf_size); - Err(Error::NotFound) - }) -} - -// --------------------------------------------------------------------------- -// node.h — node type queries -// --------------------------------------------------------------------------- - -/// `oakengine_node_is_group`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_is_group(self_: *const OakEngineNode) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - Ok(if is_node_type(unbox(self_)?, TYPE_ID_GROUP) { - 1 - } else { - 0 - }) - }) -} - -/// `oakengine_node_is_multicam`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_is_multicam(self_: *const OakEngineNode) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - Ok(if is_node_type(unbox(self_)?, TYPE_ID_MULTICAM) { - 1 - } else { - 0 - }) - }) -} - -// --------------------------------------------------------------------------- -// node.h — context positions -// --------------------------------------------------------------------------- - -/// `oakengine_node_context_node_count`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_context_node_count(context: *const OakEngineNode) -> c_int { - guard_int(|| unsafe { - if context.is_null() { - return Err(Error::Invalid); - } - let mut count: c_int = 0; - Error::from_module(n::oaknode_node_context_count(unbox(context)?, &mut count))?; - Ok(count) - }) -} - -/// `oakengine_node_context_contains_node`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_context_contains_node( - context: *const OakEngineNode, - node: *const OakEngineNode, -) -> c_int { - guard_int(|| unsafe { - if context.is_null() || node.is_null() { - return Err(Error::Invalid); - } - let ch = unbox(context)?; - let nh = unbox(node)?; - let mut count: c_int = 0; - Error::from_module(n::oaknode_node_context_count(ch, &mut count))?; - for i in 0..count { - let mut other = CHandle::null(); - if n::oaknode_node_context_node_at(ch, i, &mut other) == 0 - && !other.is_null() - && n::oaknode_node_identity(other) == n::oaknode_node_identity(nh) - { - return Ok(1); - } - } - Ok(0) - }) -} - -/// `oakengine_node_context_node_at`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_context_node_at( - context: *mut OakEngineNode, - index: c_int, - x: *mut f64, - y: *mut f64, - expanded: *mut c_int, -) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - if context.is_null() || index < 0 { - return Ok(std::ptr::null_mut()); - } - let ch = unbox(context)?; - let mut out = CHandle::null(); - let rc = n::oaknode_node_context_node_at(ch, index, &mut out); - if rc != 0 || out.is_null() { - return Ok(std::ptr::null_mut()); - } - // Positions are read from the context's position map; the module - // has no per-node position fetch, so x/y/expanded stay untouched. - let _ = (x, y, expanded); - Ok(box_handle::(out)) - }) -} - -/// `oakengine_node_set_context_position` — undoable position set. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_set_context_position( - context: *mut OakEngineNode, - node: *mut OakEngineNode, - x: f64, - y: f64, -) -> c_int { - guard(|| unsafe { - if context.is_null() || node.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let ch = unbox(context)?; - let nh = unbox(node)?; - // The module's undoable setter requires a pre-existing - // context_positions entry (else NOT_FOUND); create the first entry - // with the live setter so positions can be ESTABLISHED through the - // facade (bug fix: they were previously impossible to create). - let mut x0: f64 = 0.0; - let mut y0: f64 = 0.0; - let mut e0: c_int = 0; - if n::oaknode_node_get_context_position(nh, ch, &mut x0, &mut y0, &mut e0) != 0 { - Error::from_module(n::oaknode_node_set_context_position(nh, ch, 0.0, 0.0, 0))?; - } - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_node_set_context_position_undoable(nh, ch, x, y, 0, &mut cmd); - if rc != 0 { - return Err(Error::Module(rc)); - } - push_command(cmd, "Set Position") - }) -} - -/// `oakengine_node_get_context_position`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_context_position( - context: *const OakEngineNode, - node: *const OakEngineNode, - x: *mut f64, - y: *mut f64, - expanded: *mut c_int, -) -> c_int { - guard(|| unsafe { - if context.is_null() || node.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let rc = - n::oaknode_node_get_context_position(unbox(node)?, unbox(context)?, x, y, expanded); - Error::from_module(rc) - }) -} - -/// `oakengine_node_set_context_expanded`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_set_context_expanded( - context: *mut OakEngineNode, - node: *mut OakEngineNode, - expanded: c_int, -) -> c_int { - guard(|| unsafe { - if context.is_null() || node.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let ch = unbox(context)?; - let nh = unbox(node)?; - // Preserve the current position and flip only the expanded flag. - let mut x: f64 = 0.0; - let mut y: f64 = 0.0; - let mut was: c_int = 0; - let rc = n::oaknode_node_get_context_position(nh, ch, &mut x, &mut y, &mut was); - if rc != 0 { - // No entry yet: establish one (the module's undoable setter - // below demands a pre-existing context_positions entry). - Error::from_module(n::oaknode_node_set_context_position(nh, ch, 0.0, 0.0, 0))?; - } - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_node_set_context_position_undoable( - nh, - ch, - x, - y, - if expanded != 0 { 1 } else { 0 }, - &mut cmd, - ); - if rc != 0 { - return Err(Error::Module(rc)); - } - push_command(cmd, "Set Position") - }) -} - -// --------------------------------------------------------------------------- -// node.h — effect input / effect chain -// --------------------------------------------------------------------------- - -/// Owns a set of borrowed module node handles, releasing each shell on -/// drop (error paths never leak). Extracted handles are replaced with -/// NULL (a no-op release). -struct HandleGuard(Vec); - -impl HandleGuard { - /// Wrap a handle vector. - fn new(v: Vec) -> Self { - Self(v) - } - - /// The number of held handles. - fn len(&self) -> usize { - self.0.len() - } - - /// A borrowed copy of the `i`-th handle. - fn get(&self, i: usize) -> CHandle { - self.0[i] - } - - /// Take the `i`-th handle out of the guard (the caller owns it now). - fn take(&mut self, i: usize) -> CHandle { - let h = self.0[i]; - self.0[i] = CHandle::null(); - h - } - - /// Release ownership of every held handle (into the caller's hands). - fn into_inner(mut self) -> Vec { - let out = std::mem::take(&mut self.0); - std::mem::forget(self); - out - } -} - -impl Drop for HandleGuard { - fn drop(&mut self) { - for h in &self.0 { - release_handle(*h); - } - } -} - -/// Release a module handle shell (NULL and empty handles are no-ops). -fn release_handle(h: CHandle) { - if let Some(release) = h.release { - unsafe { release(h.ctx) }; - } -} - -/// `oakengine_node_get_effect_input` — the id of the input the effect -/// chain attaches to (C++ `Node::GetEffectInputID`). Empty when the node -/// cannot host effects; the facade reports that as `E_NOT_FOUND`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_effect_input( - self_: *const OakEngineNode, - input_id: *mut c_char, - input_id_size: c_int, - element: *mut c_int, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - if !element.is_null() { - *element = -1; - } - let id = effect_input_of(h)?; - if id.is_empty() { - return Err(Error::NotFound); - } - Ok(write_string(&id, input_id, input_id_size)) - }) -} - -/// The effect-input id of `node`, or `""` when the node cannot host -/// effects (C++ `GetEffectInputID`). -fn effect_input_of(node: CHandle) -> Result { - unsafe { module_string(|buf, size| n::oaknode_node_get_effect_input(node, buf, size)) } -} - -/// The node feeding `node`'s input `input_id` (borrowed handle; caller -/// releases), or `None` when the input is unconnected. -fn connected_node(node: CHandle, input_id: &str) -> Result> { - unsafe { - let cid = std::ffi::CString::new(input_id) - .map_err(|_| Error::Failed("invalid input id".into()))?; - let mut out = CHandle::null(); - let rc = n::oaknode_node_input_get_connected_node(node, cid.as_ptr(), &mut out); - if rc != 0 { - // An input that does not exist on the node is a chain-structure - // error; an unconnected input returns OK with a NULL handle. - return Err(Error::Module(rc)); - } - Ok(if out.is_null() { None } else { Some(out) }) - } -} - -/// The effect chain of `node`, **closest-to-source first** (signal order: -/// the first element feeds the media side, the last feeds `node`'s effect -/// input). Every returned handle is a borrowed shell the caller must -/// release. The walk follows each node's effect input upstream until an -/// unconnected input or a node without an effect input; a `seen` guard -/// protects against malformed cycles. -fn effect_chain(node: CHandle) -> Result> { - let mut chain: HandleGuard = HandleGuard::new(Vec::new()); - let mut cur = node; - let mut seen: Vec = Vec::new(); - loop { - let id = unsafe { n::oaknode_node_identity(cur) }; - if seen.contains(&id) { - break; - } - seen.push(id); - let input = effect_input_of(cur)?; - if input.is_empty() { - break; - } - let Some(up) = connected_node(cur, &input)? else { - break; - }; - chain.0.push(up); - cur = up; - } - // The walk collects host-upstream (last effect first); reverse to get - // signal order. - chain.0.reverse(); - Ok(chain.into_inner()) -} - -/// `oakengine_node_effect_count` — the length of `self_`'s effect chain -/// (0 when the node cannot host effects or nothing is attached). -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_effect_count(self_: *const OakEngineNode) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let h = unbox(self_)?; - Ok(effect_chain(h)?.len() as c_int) - }) -} - -/// `oakengine_node_effect_at` — the `index`-th effect of `self_`'s chain -/// (borrowed handle; index 0 = closest to the source). NULL when out of -/// range or the node hosts no effects. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_effect_at( - self_: *const OakEngineNode, - index: c_int, -) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - if self_.is_null() || index < 0 { - return Ok(std::ptr::null_mut()); - } - let h = unbox(self_)?; - let mut chain = HandleGuard::new(effect_chain(h)?); - if (index as usize) >= chain.len() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(chain.take(index as usize))) - }) -} - -/// `oakengine_node_is_enabled` — 1/0 (the node's `enabled_in` flag; the -/// effect stack's enable toggle). -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_is_enabled(self_: *const OakEngineNode) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let mut value: c_int = 0; - Error::from_module(n::oaknode_node_is_enabled(unbox(self_)?, &mut value))?; - Ok(value) - }) -} - -/// `oakengine_node_identity` — the node's stable identity (the effect -/// stack uses it as the card id across frames). 0 for NULL/invalid. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_identity(self_: *const OakEngineNode) -> u64 { - crate::handle::guard_i64(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let h = unbox(self_)?; - Ok(n::oaknode_node_identity(h) as i64) - }) as u64 -} - -/// `oakengine_node_effect_set_enabled` — undoable enable toggle of an -/// effect node (the stack's enable switch; `enabled_in` flag). -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_effect_set_enabled( - self_: *mut OakEngineNode, - enabled: c_int, -) -> c_int { - guard(|| unsafe { - if self_.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_node_set_enabled_undoable(h, enabled, &mut cmd); - if rc != 0 { - return Err(Error::Module(rc)); - } - push_command(cmd, "Toggle Effect") - }) -} - -/// `oakengine_node_effect_insert` — undoable insertion of a new effect of -/// `type_id` at chain position `index` (0 = closest to the source, `len` -/// = closest to the host; out-of-range indices clamp to the ends). The -/// node is created from the factory, added to the host's project, and -/// wired into the chain — one undo row for the whole edit. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_effect_insert( - self_: *mut OakEngineNode, - index: c_int, - type_id: *const c_char, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || type_id.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let host = unbox(self_)?; - node_effect_insert_impl(host, index, type_id) - }) -} - -/// The insert implementation (see `oakengine_node_effect_insert`). -/// -/// # Safety -/// `host` must be a live module node handle; `type_id` a NUL-terminated -/// C string. -unsafe fn node_effect_insert_impl( - host: CHandle, - index: c_int, - type_id: *const c_char, -) -> Result<()> { - unsafe { - // The host must be able to host effects. - let host_input = effect_input_of(host)?; - if host_input.is_empty() { - set_node_error("node cannot host effects (no effect input)"); - return Err(Error::NotFound); - } - // The new effect node (owned scratch handle; freed below). - let mut new_node = n::oaknode_factory_create_from_id(type_id); - if new_node.ctx.is_null() { - set_node_error(&format!("unknown node type id \"{}\"", read_cstr(type_id))); - return Err(Error::Failed("unknown node type".into())); - } - let new_input = match effect_input_of(new_node) { - Ok(id) if !id.is_empty() => id, - _ => { - // A node without an effect input cannot sit in the chain. - set_node_error("node type has no effect input; cannot be chained"); - n::oaknode_node_free(&mut new_node); - return Err(Error::Invalid); - } - }; - - // The chain (closest-to-source first) and the neighbors of the - // insertion point. - let chain = HandleGuard::new(effect_chain(host)?); - let len = chain.len(); - let pos = (index.max(0) as usize).min(len); - let (upstream, downstream) = if pos == 0 { - if len == 0 { - (None, host) - } else { - let d = chain.get(0); - (connected_node(d, &effect_input_of(d)?)?, d) - } - } else if pos == len { - (Some(chain.get(len - 1)), host) - } else { - (Some(chain.get(pos - 1)), chain.get(pos)) - }; - let downstream_input = effect_input_of(downstream)?; - let upstream_input = upstream - .map(|u| effect_input_of(u)) - .transpose()? - .unwrap_or_default(); - - let project = project_of(host)?; - // The identities present before the add; the moved node is the one - // whose identity is new afterwards (its id may be reallocated on a - // slot collision — see the module's `Graph::add_entry`). - let existing: Vec = (0..n::oaknode_project_node_count(project)) - .filter_map(|i| { - let other = n::oaknode_project_node_at(project, i); - if other.is_null() { - None - } else { - let id = n::oaknode_node_identity(other); - if id == 0 { - None - } else { - Some(id) - } - } - }) - .collect(); - - // One undo row for the whole edit: open a group, push the add-node - // command and the rewiring commands into it, then close it. On any - // failure the group is aborted (executed children are undone). - let name = std::ffi::CString::new("Add Effect") - .map_err(|_| Error::Failed("invalid undo name".into()))?; - let begin = crate::undo::oakengine_undo_group_begin(name.as_ptr()); - if begin != 0 { - set_node_error("failed to open an undo group"); - n::oaknode_node_free(&mut new_node); - return Err(Error::Module(begin)); - } - macro_rules! step { - ($e:expr) => { - match $e { - Ok(()) => {} - Err(e) => { - crate::undo::oakengine_undo_group_abort(); - n::oaknode_node_free(&mut new_node); - return Err(e); - } - } - }; - } - // 1. Add the node to the project (the group child executes eagerly). - let add_cmd = n::oaknode_command_create_add_node(project, new_node); - if add_cmd.ctx.is_null() { - set_node_error("add-node command failed"); - step!(Err(Error::Failed("add-node command failed".into()))); - } - step!(push_command(add_cmd, "Add Effect")); - // 2. A fresh project-borrowed view of the moved node (the one whose - // identity was not present before the add). - let total = n::oaknode_project_node_count(project); - let mut fresh = CHandle::null(); - for i in 0..total { - let other = n::oaknode_project_node_at(project, i); - if other.is_null() { - continue; - } - let id = n::oaknode_node_identity(other); - if id != 0 && !existing.contains(&id) { - fresh = other; - break; - } - } - if fresh.ctx.is_null() { - set_node_error("could not resolve the added node"); - step!(Err(Error::Failed("added node resolution failed".into()))); - } - // 3. Unhook the downstream input from its current upstream (when - // there is one). - if upstream.is_some() { - let mut cmd = CHandle::null(); - let cid = std::ffi::CString::new(downstream_input.as_str()) - .map_err(|_| Error::Failed("invalid input id".into()))?; - let rc = n::oaknode_node_disconnect_undoable(downstream, cid.as_ptr(), &mut cmd); - step!(Error::from_module(rc).and_then(|()| push_command(cmd, "Add Effect"))); - } - // 4. Wire the new effect between upstream and downstream. - let dcid = std::ffi::CString::new(downstream_input.as_str()) - .map_err(|_| Error::Failed("invalid input id".into()))?; - let mut cmd = CHandle::null(); - let rc = n::oaknode_node_connect_undoable(fresh, downstream, dcid.as_ptr(), &mut cmd); - step!(Error::from_module(rc).and_then(|()| push_command(cmd, "Add Effect"))); - if let Some(upstream) = upstream { - let ucid = std::ffi::CString::new(upstream_input.as_str()) - .map_err(|_| Error::Failed("invalid input id".into()))?; - let mut cmd = CHandle::null(); - let rc = n::oaknode_node_connect_undoable(upstream, fresh, ucid.as_ptr(), &mut cmd); - step!(Error::from_module(rc).and_then(|()| push_command(cmd, "Add Effect"))); - } - let end = crate::undo::oakengine_undo_group_end(); - // Cleanup: the factory shell is a stale view (the node now lives in - // the project graph); the fresh view is a borrowed shell. The chain - // guard releases the rest. - n::oaknode_node_free(&mut new_node); - release_handle(fresh); - if end != 0 { - return Err(Error::Module(end)); - } - Ok(()) - } -} - -/// `oakengine_node_effect_remove` — undoable removal of `effect` (a node -/// in `self_`'s chain): unhook both edges and bridge the gap, one undo -/// row. The node itself is left orphaned in the project graph: the module -/// node-transfer commands are one-way (undo discards the entry), so a -/// reversible detach does not exist there yet — documented limitation -/// (a future module command can take the node out of the project while -/// keeping its entry restorable). -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_effect_remove( - self_: *mut OakEngineNode, - effect: *mut OakEngineNode, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || effect.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let host = unbox(self_)?; - let eff = unbox(effect)?; - let eff_identity = n::oaknode_node_identity(eff); - - let chain = HandleGuard::new(effect_chain(host)?); - let len = chain.len(); - let Some(pos) = chain - .0 - .iter() - .position(|c| n::oaknode_node_identity(*c) == eff_identity) - else { - return Err(Error::NotFound); - }; - let upstream = if pos == 0 { - connected_node(chain.get(0), &effect_input_of(chain.get(0))?)? - } else { - Some(chain.get(pos - 1)) - }; - let downstream = if pos + 1 == len { - host - } else { - chain.get(pos + 1) - }; - let downstream_input = effect_input_of(downstream)?; - - // All nodes are already in the project: a plain multi command. - let mut children: Vec = Vec::new(); - // 1. Unhook the effect from its upstream. - let eff_input = effect_input_of(eff)?; - if upstream.is_some() { - let mut cmd = CHandle::null(); - let cid = std::ffi::CString::new(eff_input.as_str()) - .map_err(|_| Error::Failed("invalid input id".into()))?; - Error::from_module(n::oaknode_node_disconnect_undoable( - eff, - cid.as_ptr(), - &mut cmd, - ))?; - children.push(cmd); - } - // 2. Unhook the downstream from the effect, then bridge it back to - // the upstream. - let mut cmd = CHandle::null(); - let cid = std::ffi::CString::new(downstream_input.as_str()) - .map_err(|_| Error::Failed("invalid input id".into()))?; - Error::from_module(n::oaknode_node_disconnect_undoable( - downstream, - cid.as_ptr(), - &mut cmd, - ))?; - children.push(cmd); - if let Some(upstream) = upstream { - let mut cmd = CHandle::null(); - let cid = std::ffi::CString::new(downstream_input.as_str()) - .map_err(|_| Error::Failed("invalid input id".into()))?; - Error::from_module(n::oaknode_node_connect_undoable( - upstream, - downstream, - cid.as_ptr(), - &mut cmd, - ))?; - children.push(cmd); - } - // The guard releases every command handle on error paths; on success - // `push_multi_commands` consumes them all. - let children = HandleGuard::new(children); - push_multi_commands( - &children.into_inner(), - std::ptr::null_mut(), - "Remove Effect", - ) - }) -} - -/// `oakengine_node_effect_move` — undoable reorder of `effect` to chain -/// position `new_index` (an insertion index **after** removal, matching -/// the effect stack's `ReorderRequested`; `0..=len-1` where `len` is the -/// post-removal chain length). One undo row. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_effect_move( - self_: *mut OakEngineNode, - effect: *mut OakEngineNode, - new_index: c_int, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || effect.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let host = unbox(self_)?; - let eff = unbox(effect)?; - let eff_identity = n::oaknode_node_identity(eff); - - let chain = HandleGuard::new(effect_chain(host)?); - let len = chain.len(); - let Some(from) = chain - .0 - .iter() - .position(|c| n::oaknode_node_identity(*c) == eff_identity) - else { - return Err(Error::NotFound); - }; - // The post-removal insertion index (0..=len-1; clamp for safety). - let to = if new_index < 0 { - 0 - } else { - (new_index as usize).min(len - 1) - }; - - // The guard releases every command handle on error paths; on success - // `push_multi_commands` consumes them all. - let mut children: HandleGuard = HandleGuard::new(Vec::new()); - // ---- remove the effect from position `from` --------------------- - let upstream = if from == 0 { - connected_node(chain.get(0), &effect_input_of(chain.get(0))?)? - } else { - Some(chain.get(from - 1)) - }; - let downstream = if from + 1 == len { - host - } else { - chain.get(from + 1) - }; - let downstream_input = effect_input_of(downstream)?; - let eff_input = effect_input_of(eff)?; - if upstream.is_some() { - let mut cmd = CHandle::null(); - let cid = std::ffi::CString::new(eff_input.as_str()) - .map_err(|_| Error::Failed("invalid input id".into()))?; - Error::from_module(n::oaknode_node_disconnect_undoable( - eff, - cid.as_ptr(), - &mut cmd, - ))?; - children.0.push(cmd); - } - let mut cmd = CHandle::null(); - let cid = std::ffi::CString::new(downstream_input.as_str()) - .map_err(|_| Error::Failed("invalid input id".into()))?; - Error::from_module(n::oaknode_node_disconnect_undoable( - downstream, - cid.as_ptr(), - &mut cmd, - ))?; - children.0.push(cmd); - if let Some(upstream) = upstream { - let mut cmd = CHandle::null(); - let cid = std::ffi::CString::new(downstream_input.as_str()) - .map_err(|_| Error::Failed("invalid input id".into()))?; - Error::from_module(n::oaknode_node_connect_undoable( - upstream, - downstream, - cid.as_ptr(), - &mut cmd, - ))?; - children.0.push(cmd); - } - // ---- reinsert at position `to` (the post-removal chain) --------- - // After removal the chain has `len - 1` effects; position `to` - // (0..=len-1) sits between index `to - 1` and `to` of the remaining - // list, where index `-1` is the chain source and index `len-1` is - // the host. - let remaining: Vec = chain - .0 - .iter() - .enumerate() - .filter(|(i, _)| *i != from) - .map(|(_, c)| *c) - .collect(); - let (up2, down2) = if to == 0 { - if remaining.is_empty() { - (None, host) - } else { - let d = remaining[0]; - (connected_node(d, &effect_input_of(d)?)?, d) - } - } else if to == len - 1 { - (Some(remaining[len - 2]), host) - } else { - (Some(remaining[to - 1]), remaining[to]) - }; - let down2_input = effect_input_of(down2)?; - let up2_input = up2 - .map(|u| effect_input_of(u)) - .transpose()? - .unwrap_or_default(); - if up2.is_some() { - let mut cmd = CHandle::null(); - let cid = std::ffi::CString::new(down2_input.as_str()) - .map_err(|_| Error::Failed("invalid input id".into()))?; - Error::from_module(n::oaknode_node_disconnect_undoable( - down2, - cid.as_ptr(), - &mut cmd, - ))?; - children.0.push(cmd); - } - let mut cmd = CHandle::null(); - let cid = std::ffi::CString::new(down2_input.as_str()) - .map_err(|_| Error::Failed("invalid input id".into()))?; - Error::from_module(n::oaknode_node_connect_undoable( - eff, - down2, - cid.as_ptr(), - &mut cmd, - ))?; - children.0.push(cmd); - if let Some(up2) = up2 { - let mut cmd = CHandle::null(); - let cid = std::ffi::CString::new(up2_input.as_str()) - .map_err(|_| Error::Failed("invalid input id".into()))?; - Error::from_module(n::oaknode_node_connect_undoable( - up2, - eff, - cid.as_ptr(), - &mut cmd, - ))?; - children.0.push(cmd); - } - - push_multi_commands( - &children.into_inner(), - std::ptr::null_mut(), - "Reorder Effect", - ) - }) -} - -// --------------------------------------------------------------------------- -// node.h — group passthrough -// --------------------------------------------------------------------------- - -/// `oakengine_node_group_create` — detached group node. -#[no_mangle] -pub extern "C" fn oakengine_node_group_create() -> *mut OakEngineNode { - guard_ptr(|| { - let g = unsafe { n::oaknode_group_create() }; - if g.ctx.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(g)) - }) -} - -/// `oakengine_node_group_get_inner` — walk one group-passthrough level. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_group_get_inner( - inout_node: *mut *mut OakEngineNode, - inout_input: *mut c_char, - inout_input_size: c_int, - inout_element: *mut c_int, -) -> c_int { - guard_int(|| unsafe { - if inout_node.is_null() - || (*inout_node).is_null() - || inout_input.is_null() - || inout_input_size <= 0 - || inout_element.is_null() - { - return Ok(0); - } - let node = unbox(*inout_node)?; - if !is_node_type(node, TYPE_ID_GROUP) { - return Ok(0); - } - let id = read_cstr(inout_input); - let mut out_node = CHandle::null(); - let mut out_input = [0 as c_char; 256]; - let mut out_element: c_int = *inout_element; - let rc = n::oaknode_group_resolve_input( - node, - std::ffi::CString::new(id.as_str()).unwrap().as_ptr(), - out_element, - &mut out_node, - out_input.as_mut_ptr(), - out_input.len() as c_int, - &mut out_element, - ); - // The module getter returns the copied string length (>= 0) on - // success; only negative codes are failures. - if rc < 0 || out_node.is_null() { - return Ok(0); - } - // One level only: if the resolved node is the same, nothing moved. - if n::oaknode_node_identity(out_node) == n::oaknode_node_identity(node) { - return Ok(0); - } - *inout_node = box_handle::(out_node); - let s = read_cstr(out_input.as_ptr()); - write_string(&s, inout_input, inout_input_size); - *inout_element = out_element; - Ok(1) - }) -} - -/// `oakengine_group_input_passthrough_count`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_group_input_passthrough_count( - self_: *const OakEngineNode, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - if !is_node_type(h, TYPE_ID_GROUP) { - return Err(Error::Invalid); - } - let mut count: c_int = 0; - Error::from_module(n::oaknode_group_passthrough_count(h, &mut count))?; - Ok(count) - }) -} - -/// `oakengine_group_add_input_passthrough` — direct add. -#[no_mangle] -pub unsafe extern "C" fn oakengine_group_add_input_passthrough( - self_: *mut OakEngineNode, - inner_node: *mut OakEngineNode, - inner_input: *const c_char, - inner_element: c_int, - preferred_id: *const c_char, - out_id: *mut c_char, - out_id_size: c_int, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() || inner_node.is_null() || inner_input.is_null() { - set_node_error("invalid arguments or not a group"); - return Err(Error::Invalid); - } - let gh = unbox(self_)?; - let ih = unbox(inner_node)?; - let rc = n::oaknode_group_add_input_passthrough( - gh, - ih, - inner_input, - inner_element, - out_id, - out_id_size, - ); - if rc < 0 { - return Err(Error::Module(rc)); - } - let _ = preferred_id; - Ok(string_result(rc)) - }) -} - -/// `oakengine_group_input_passthrough_at`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_group_input_passthrough_at( - self_: *const OakEngineNode, - index: c_int, - id: *mut c_char, - id_size: c_int, - node: *mut *mut OakEngineNode, - input_id: *mut c_char, - input_id_size: c_int, - element: *mut c_int, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - set_node_error("not a group"); - return Err(Error::Invalid); - } - let gh = unbox(self_)?; - if !is_node_type(gh, TYPE_ID_GROUP) { - set_node_error("not a group"); - return Err(Error::Invalid); - } - let mut out_node = CHandle::null(); - let rc = n::oaknode_group_passthrough_input_at( - gh, - index, - &mut out_node, - input_id, - input_id_size, - element, - ); - if rc < 0 { - return Err(Error::Module(rc)); - } - let mut id_buf = [0 as c_char; 256]; - let id_rc = n::oaknode_group_passthrough_id_at(gh, index, id_buf.as_mut_ptr(), 256); - if id_rc < 0 { - return Err(Error::Module(id_rc)); - } - let id_len = write_string(&read_cstr(id_buf.as_ptr()), id, id_size); - if !out_node.is_null() { - if node.is_null() { - // The caller owns nothing; the borrowed node must not leak. - free_box(box_handle::(out_node)); - } else { - *node = box_handle::(out_node); - } - } - Ok(id_len) - }) -} - -/// `oakengine_group_get_id_of_passthrough`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_group_get_id_of_passthrough( - self_: *const OakEngineNode, - inner_node: *mut OakEngineNode, - inner_input: *const c_char, - inner_element: c_int, - id: *mut c_char, - id_size: c_int, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() || inner_node.is_null() || inner_input.is_null() { - set_node_error("invalid arguments or not a group"); - return Err(Error::Invalid); - } - let gh = unbox(self_)?; - let ih = unbox(inner_node)?; - let mut count: c_int = 0; - Error::from_module(n::oaknode_group_passthrough_count(gh, &mut count))?; - for i in 0..count { - let mut out_node = CHandle::null(); - let mut out_input = [0 as c_char; 256]; - let mut out_element: c_int = 0; - let rc = n::oaknode_group_passthrough_input_at( - gh, - i, - &mut out_node, - out_input.as_mut_ptr(), - out_input.len() as c_int, - &mut out_element, - ); - // The module getter returns the copied string length (>= 0) on - // success; only negative codes are failures. - if rc < 0 || out_node.is_null() { - continue; - } - if n::oaknode_node_identity(out_node) == n::oaknode_node_identity(ih) - && read_cstr(out_input.as_ptr()) == read_cstr(inner_input) - && out_element == inner_element - { - let mut id_buf = [0 as c_char; 256]; - let id_rc = n::oaknode_group_passthrough_id_at(gh, i, id_buf.as_mut_ptr(), 256); - if id_rc < 0 { - return Err(Error::Module(id_rc)); - } - return Ok(write_string(&read_cstr(id_buf.as_ptr()), id, id_size)); - } - } - set_node_error("no passthrough for that node/input"); - Err(Error::NotFound) - }) -} - -/// `oakengine_group_get_passthrough_from_id`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_group_get_passthrough_from_id( - self_: *const OakEngineNode, - id: *const c_char, - out_node: *mut *mut OakEngineNode, - out_input: *mut c_char, - out_input_size: c_int, - out_element: *mut c_int, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || id.is_null() { - set_node_error("invalid arguments or not a group"); - return Err(Error::Invalid); - } - let gh = unbox(self_)?; - let want = read_cstr(id); - let mut count: c_int = 0; - Error::from_module(n::oaknode_group_passthrough_count(gh, &mut count))?; - for i in 0..count { - let mut id_buf = [0 as c_char; 256]; - let id_rc = n::oaknode_group_passthrough_id_at(gh, i, id_buf.as_mut_ptr(), 256); - if id_rc < 0 || read_cstr(id_buf.as_ptr()) != want { - continue; - } - let mut node = CHandle::null(); - let rc = n::oaknode_group_passthrough_input_at( - gh, - i, - &mut node, - out_input, - out_input_size, - out_element, - ); - // The module getter returns the copied string length (>= 0) on - // success; only negative codes are failures. - if rc < 0 { - return Err(Error::Module(rc)); - } - if node.is_null() { - continue; - } - if !out_node.is_null() { - *out_node = box_handle::(node); - } - return Ok(()); - } - set_node_error(&format!("no passthrough with id \"{}\"", want)); - Err(Error::NotFound) - }) -} - -/// `oakengine_group_get_output_passthrough`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_group_get_output_passthrough( - self_: *const OakEngineNode, -) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - if self_.is_null() { - return Ok(std::ptr::null_mut()); - } - let gh = unbox(self_)?; - if !is_node_type(gh, TYPE_ID_GROUP) { - return Ok(std::ptr::null_mut()); - } - let mut out = CHandle::null(); - let rc = n::oaknode_group_get_output_passthrough(gh, &mut out); - if rc != 0 || out.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(out)) - }) -} - -/// `oakengine_group_set_output_passthrough` — direct set. -#[no_mangle] -pub unsafe extern "C" fn oakengine_group_set_output_passthrough( - self_: *mut OakEngineNode, - inner_node: *mut OakEngineNode, -) -> c_int { - guard(|| unsafe { - if self_.is_null() { - set_node_error("not a group"); - return Err(Error::Invalid); - } - let gh = unbox(self_)?; - let ih = unbox(inner_node)?; - let rc = n::oaknode_group_set_output_passthrough(gh, ih); - Error::from_module(rc) - }) -} - -/// `oakengine_group_resolve_input`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_group_resolve_input( - self_: *const OakEngineNode, - id: *const c_char, - element: c_int, - out_node: *mut *mut OakEngineNode, - out_input: *mut c_char, - out_input_size: c_int, - out_element: *mut c_int, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || id.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let gh = unbox(self_)?; - let mut node = CHandle::null(); - let rc = n::oaknode_group_resolve_input( - gh, - id, - element, - &mut node, - out_input, - out_input_size, - out_element, - ); - // The module getter returns the copied string length (>= 0) on - // success; only negative codes are failures. - if rc < 0 { - return Err(Error::Module(rc)); - } - if !out_node.is_null() { - if node.is_null() { - // Pass through: the group itself is the resolved node. - *out_node = box_handle::(gh); - } else { - *out_node = box_handle::(node); - } - } - Ok(()) - }) -} - -/// `oakengine_group_remove_input_passthrough` — direct remove. -#[no_mangle] -pub unsafe extern "C" fn oakengine_group_remove_input_passthrough( - self_: *mut OakEngineNode, - inner_node: *mut OakEngineNode, - inner_input: *const c_char, - inner_element: c_int, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || inner_node.is_null() || inner_input.is_null() { - set_node_error("invalid arguments or not a group"); - return Err(Error::Invalid); - } - let rc = n::oaknode_group_remove_input_passthrough( - unbox(self_)?, - unbox(inner_node)?, - inner_input, - inner_element, - ); - Error::from_module(rc) - }) -} - -/// `oakengine_group_add_input_passthrough_command` — opaque command. -#[no_mangle] -pub unsafe extern "C" fn oakengine_group_add_input_passthrough_command( - self_: *mut OakEngineNode, - inner_node: *mut OakEngineNode, - inner_input: *const c_char, - inner_element: c_int, - preferred_id: *const c_char, -) -> *mut c_void { - guard_ptr(|| unsafe { - if self_.is_null() || inner_node.is_null() || inner_input.is_null() { - return Ok(std::ptr::null_mut()); - } - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_group_add_input_passthrough_undoable( - unbox(self_)?, - unbox(inner_node)?, - inner_input, - inner_element, - &mut cmd, - ); - if rc != 0 { - return Ok(std::ptr::null_mut()); - } - let _ = preferred_id; - Ok(command_box(cmd)?.cast()) - }) -} - -/// `oakengine_group_set_output_passthrough_command` — opaque command. -#[no_mangle] -pub unsafe extern "C" fn oakengine_group_set_output_passthrough_command( - self_: *mut OakEngineNode, - inner_node: *mut OakEngineNode, -) -> *mut c_void { - guard_ptr(|| unsafe { - if self_.is_null() || inner_node.is_null() { - return Ok(std::ptr::null_mut()); - } - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_group_set_output_passthrough_undoable( - unbox(self_)?, - unbox(inner_node)?, - &mut cmd, - ); - if rc != 0 { - return Ok(std::ptr::null_mut()); - } - Ok(command_box(cmd)?.cast()) - }) -} - -/// `oakengine_group_add_input_passthrough_undoable`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_group_add_input_passthrough_undoable( - self_: *mut OakEngineNode, - inner_node: *mut OakEngineNode, - inner_input: *const c_char, - inner_element: c_int, - preferred_id: *const c_char, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || inner_node.is_null() || inner_input.is_null() { - set_node_error("invalid arguments or not a group"); - return Err(Error::Invalid); - } - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_group_add_input_passthrough_undoable( - unbox(self_)?, - unbox(inner_node)?, - inner_input, - inner_element, - &mut cmd, - ); - if rc != 0 { - return Err(Error::Module(rc)); - } - let _ = preferred_id; - push_command(cmd, "Add Input Passthrough") - }) -} - -/// `oakengine_group_set_output_passthrough_undoable`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_group_set_output_passthrough_undoable( - self_: *mut OakEngineNode, - inner_node: *mut OakEngineNode, -) -> c_int { - guard(|| unsafe { - if self_.is_null() { - set_node_error("not a group"); - return Err(Error::Invalid); - } - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_group_set_output_passthrough_undoable( - unbox(self_)?, - unbox(inner_node)?, - &mut cmd, - ); - if rc != 0 { - return Err(Error::Module(rc)); - } - push_command(cmd, "Set Output Passthrough") - }) -} - -// --------------------------------------------------------------------------- -// node.h — multi-camera -// --------------------------------------------------------------------------- - -/// `oakengine_multicam_input_current`. -#[no_mangle] -pub extern "C" fn oakengine_multicam_input_current() -> *const c_char { - unsafe { n::oaknode_multicam_input_current() } -} - -/// `oakengine_multicam_input_sources`. -#[no_mangle] -pub extern "C" fn oakengine_multicam_input_sources() -> *const c_char { - unsafe { n::oaknode_multicam_input_sources() } -} - -/// `oakengine_multicam_input_sequence`. -#[no_mangle] -pub extern "C" fn oakengine_multicam_input_sequence() -> *const c_char { - unsafe { n::oaknode_multicam_input_sequence() } -} - -/// `oakengine_multicam_input_sequence_type`. -#[no_mangle] -pub extern "C" fn oakengine_multicam_input_sequence_type() -> *const c_char { - unsafe { n::oaknode_multicam_input_sequence_type() } -} - -/// `oakengine_multicam_get_source_count`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_multicam_get_source_count(self_: *const OakEngineNode) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - if !is_node_type(h, TYPE_ID_MULTICAM) { - return Err(Error::Invalid); - } - let mut count: c_int = 0; - Error::from_module(n::oaknode_multicam_get_source_count(h, &mut count))?; - Ok(count) - }) -} - -/// `oakengine_multicam_get_rows_and_columns`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_multicam_get_rows_and_columns( - source_count: c_int, - rows: *mut c_int, - cols: *mut c_int, -) -> c_int { - guard(|| unsafe { - if source_count < 0 || rows.is_null() || cols.is_null() { - return Err(Error::Invalid); - } - Error::from_module(n::oaknode_multicam_get_rows_and_columns( - source_count, - rows, - cols, - )) - }) -} - -/// `oakengine_multicam_index_to_row_cols`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_multicam_index_to_row_cols( - index: c_int, - rows: c_int, - cols: c_int, - out_row: *mut c_int, - out_col: *mut c_int, -) -> c_int { - guard(|| unsafe { - if index < 0 || rows < 1 || cols < 1 || out_row.is_null() || out_col.is_null() { - return Err(Error::Invalid); - } - Error::from_module(n::oaknode_multicam_index_to_row_cols( - index, rows, cols, out_row, out_col, - )) - }) -} - -/// `oakengine_multicam_rows_cols_to_index`. -#[no_mangle] -pub extern "C" fn oakengine_multicam_rows_cols_to_index( - row: c_int, - col: c_int, - rows: c_int, - cols: c_int, -) -> c_int { - crate::handle::guard_int(|| { - if row < 0 || col < 0 || rows < 1 || cols < 1 || row >= rows || col >= cols { - return Err(Error::Invalid); - } - Ok(unsafe { n::oaknode_multicam_rows_cols_to_index(row, col, rows, cols) }) - }) -} - -/// `oakengine_multicam_get_current_source`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_multicam_get_current_source( - self_: *const OakEngineNode, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - if !is_node_type(h, TYPE_ID_MULTICAM) { - return Err(Error::Invalid); - } - let mut source: c_int = 0; - Error::from_module(n::oaknode_multicam_get_current_source(h, &mut source))?; - Ok(source) - }) -} - -// --------------------------------------------------------------------------- -// node.h — shape node -// --------------------------------------------------------------------------- - -/// `oakengine_shape_set_rect_undoable`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_shape_set_rect_undoable( - node: *mut OakEngineNode, - _x: f64, - _y: f64, - _w: f64, - _h: f64, - _video_params: *const OakVideoParamsPod, - _command: *mut c_void, -) -> c_int { - // Stub: the oaknode module has no ShapeNodeBase::set_rect surface. - guard(|| unsafe { - if node.is_null() || _video_params.is_null() || _command.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(node)?; - Err(Error::Invalid) - }) -} - -// --------------------------------------------------------------------------- -// node.h — subtitle block -// --------------------------------------------------------------------------- - -/// `oakengine_subtitle_text_input_id`. -#[no_mangle] -pub extern "C" fn oakengine_subtitle_text_input_id() -> *const c_char { - // SubtitleBlock::k_text_in (engine/node/block/subtitle/subtitle.cpp:29). - static S: &[u8] = b"text_in\0"; - S.as_ptr() as *const c_char -} - -/// `oakengine_subtitle_get_text`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_subtitle_get_text( - node: *mut OakEngineNode, - _buf: *mut c_char, - _buf_size: c_int, -) -> c_int { - // Stub: the oaknode module has no subtitle block type. - guard_int(|| unsafe { - if node.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(node)?; - Err(Error::Invalid) - }) -} - -/// `oakengine_subtitle_set_text`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_subtitle_set_text( - node: *mut OakEngineNode, - _text: *const c_char, -) -> c_int { - // Stub: see `oakengine_subtitle_get_text`. - guard(|| unsafe { - if node.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(node)?; - Err(Error::Invalid) - }) -} - -// --------------------------------------------------------------------------- -// node.h — bulk graph deletion -// --------------------------------------------------------------------------- - -/// `oakengine_nodes_delete_many` — delete nodes and edges in one command. -#[no_mangle] -pub unsafe extern "C" fn oakengine_nodes_delete_many( - nodes: *mut *mut OakEngineNode, - contexts: *mut *mut OakEngineNode, - node_count: c_int, - edge_outputs: *mut *mut OakEngineNode, - edge_input_nodes: *mut *mut OakEngineNode, - edge_input_ids: *mut *const c_char, - edge_input_elements: *const c_int, - edge_count: c_int, -) -> c_int { - guard(|| unsafe { - let rc = oakengine_nodes_delete_many_ex( - nodes, - contexts, - node_count, - edge_outputs, - edge_input_nodes, - edge_input_ids, - edge_input_elements, - edge_count, - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null(), - 0, - ); - Error::from_module(rc) - }) -} - -/// `oakengine_nodes_delete_many_ex` — delete plus reconnects in one -/// command. -#[no_mangle] -pub unsafe extern "C" fn oakengine_nodes_delete_many_ex( - nodes: *mut *mut OakEngineNode, - contexts: *mut *mut OakEngineNode, - node_count: c_int, - edge_outputs: *mut *mut OakEngineNode, - edge_input_nodes: *mut *mut OakEngineNode, - edge_input_ids: *mut *const c_char, - edge_input_elements: *const c_int, - edge_count: c_int, - reconnect_outputs: *mut *mut OakEngineNode, - reconnect_input_nodes: *mut *mut OakEngineNode, - reconnect_input_ids: *mut *const c_char, - reconnect_input_elements: *const c_int, - reconnect_count: c_int, -) -> c_int { - guard(|| unsafe { - if node_count <= 0 && edge_count <= 0 { - set_node_error("nothing to delete"); - return Err(Error::Invalid); - } - if node_count > 0 && nodes.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let mut children: Vec = Vec::new(); - // Node removals (disconnecting their edges), with context removal. - for i in 0..node_count as usize { - let node = *nodes.add(i); - if node.is_null() { - set_node_error(&format!("null node at index {}", i)); - return Err(Error::Invalid); - } - let nh = unbox(node)?; - if !contexts.is_null() { - let ctx = *contexts.add(i); - if !ctx.is_null() { - let ch = unbox(ctx)?; - let rc = n::oaknode_node_remove_from_context(nh, ch); - if rc != 0 { - return Err(Error::Module(rc)); - } - } - } - let cmd = n::oaknode_command_create_remove_node(nh); - if cmd.ctx.is_null() { - return Err(Error::Failed("remove node command failed".into())); - } - children.push(cmd); - } - // Edge removals. - for i in 0..edge_count as usize { - if edge_outputs.is_null() || edge_input_nodes.is_null() || edge_input_ids.is_null() { - set_node_error(&format!("invalid edge at index {}", i)); - return Err(Error::Invalid); - } - let input = *edge_input_nodes.add(i); - let input_id = *edge_input_ids.add(i); - if input.is_null() || input_id.is_null() { - set_node_error(&format!("invalid edge at index {}", i)); - return Err(Error::Invalid); - } - let _ = *edge_outputs.add(i); - let _ = if edge_input_elements.is_null() { - -1 - } else { - *edge_input_elements.add(i) - }; - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_node_disconnect_undoable(unbox(input)?, input_id, &mut cmd); - if rc != 0 { - return Err(Error::Module(rc)); - } - children.push(cmd); - } - // Reconnect edges run AFTER the deletion inside the same command. - for i in 0..reconnect_count as usize { - if reconnect_outputs.is_null() - || reconnect_input_nodes.is_null() - || reconnect_input_ids.is_null() - { - set_node_error(&format!("invalid reconnect edge at index {}", i)); - return Err(Error::Invalid); - } - let out = *reconnect_outputs.add(i); - let input = *reconnect_input_nodes.add(i); - let input_id = *reconnect_input_ids.add(i); - if out.is_null() || input.is_null() || input_id.is_null() { - set_node_error(&format!("invalid reconnect edge at index {}", i)); - return Err(Error::Invalid); - } - let _ = if reconnect_input_elements.is_null() { - -1 - } else { - *reconnect_input_elements.add(i) - }; - let mut cmd: CHandle = CHandle::null(); - let rc = - n::oaknode_node_connect_undoable(unbox(out)?, unbox(input)?, input_id, &mut cmd); - if rc != 0 { - return Err(Error::Module(rc)); - } - children.push(cmd); - } - push_multi_commands(&children, std::ptr::null_mut(), "Delete Nodes") - }) -} - -// --------------------------------------------------------------------------- -// node.h — keyframe best type at time -// --------------------------------------------------------------------------- - -/// `oakengine_node_keyframe_best_type_at_time`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframe_best_type_at_time( - self_: *const OakEngineNode, - input_id: *const c_char, - element: c_int, - time_ts: i64, - track: c_int, - default_type: c_int, -) -> c_int { - // Stub: the module has no keyframe type inspection; the caller's - // default is returned unchanged. - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Ok(default_type); - } - let _ = unbox(self_)?; - let _ = (element, time_ts, track); - Ok(default_type) - }) -} - -// --------------------------------------------------------------------------- -// node.h — handle-based keyframe API -// --------------------------------------------------------------------------- - -/// `oakengine_node_keyframe_track_count` — keyframe tracks of the input. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframe_track_count( - self_: *const OakEngineNode, - input_id: *const c_char, - element: c_int, -) -> c_int { - // Derived from the input's value type (C++ NodeValue:: - // get_number_of_keyframe_tracks): 1 scalar, 2/3/4 VEC2/3/4, - // 4 COLOR, 6 BEZIER. - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Ok(0); - } - let h = unbox(self_)?; - let mut ty: c_int = 0; - Error::from_module(n::oaknode_node_input_get_type(h, input_id, &mut ty))?; - let _ = element; - Ok(match ty { - value_type::VEC2 => 2, - value_type::VEC3 => 3, - value_type::VEC4 | value_type::COLOR => 4, - value_type::BEZIER => 6, - _ => 1, - }) - }) -} - -/// `oakengine_node_keyframe_count_on_track`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframe_count_on_track( - self_: *const OakEngineNode, - input_id: *const c_char, - element: c_int, - track: c_int, -) -> c_int { - // 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 rc = n::oaknode_node_keyframe_count(unbox(self_)?, input_id); - let _ = (element, track); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(rc) - } - }) -} - -/// `oakengine_node_keyframes_toggle_at_time`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframes_toggle_at_time( - self_: *mut OakEngineNode, - input_id: *const c_char, - element: c_int, - time_ts: i64, - track: c_int, - on: c_int, - undo_name: *const c_char, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Err(Error::Invalid); - } - 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(()) - } - }) -} - -/// `oakengine_node_has_keyframe_at_time`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_has_keyframe_at_time( - self_: *const OakEngineNode, - input_id: *const c_char, - element: c_int, - time_ts: i64, - track: c_int, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - return 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 { - 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( - self_: *const OakEngineNode, - input_id: *const c_char, - element: c_int, - num: *mut i64, - den: *mut i64, -) -> c_int { - guard_int(|| unsafe { - let _ = element; - keyframe_extreme_time(self_, input_id, true, num, den) - }) -} - -/// `oakengine_node_keyframe_latest_time`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframe_latest_time( - self_: *const OakEngineNode, - input_id: *const c_char, - element: c_int, - num: *mut i64, - den: *mut i64, -) -> c_int { - 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 { - unsafe { - if self_.is_null() || input_id.is_null() { - return Ok(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)), - } - } - 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) - } - } - } -} - -/// `oakengine_node_keyframe_closest_time_before`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframe_closest_time_before( - self_: *const OakEngineNode, - input_id: *const c_char, - element: c_int, - time_ts: i64, - track: c_int, - num: *mut i64, - den: *mut i64, -) -> c_int { - guard_int(|| unsafe { - closest_keyframe_time(self_, input_id, element, time_ts, track, true, num, den) - }) -} - -/// `oakengine_node_keyframe_closest_time_after`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframe_closest_time_after( - self_: *const OakEngineNode, - input_id: *const c_char, - element: c_int, - time_ts: i64, - track: c_int, - num: *mut i64, - den: *mut i64, -) -> c_int { - guard_int(|| unsafe { - closest_keyframe_time(self_, input_id, element, time_ts, track, false, num, den) - }) -} - -/// `oakengine_node_keyframe_handle_on_track`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframe_handle_on_track( - self_: *const OakEngineNode, - input_id: *const c_char, - element: c_int, - track: c_int, - index: c_int, -) -> *mut OakEngineKeyframe { - 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::(kf)) - }) -} - -/// `oakengine_node_keyframe_handle_at_time`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframe_handle_at_time( - self_: *const OakEngineNode, - input_id: *const c_char, - element: c_int, - track: c_int, - time_num: i64, - time_den: i64, -) -> *mut OakEngineKeyframe { - 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::(kf)) - }) -} - -/// `oakengine_node_keyframes_at_time`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframes_at_time( - self_: *const OakEngineNode, - input_id: *const c_char, - element: c_int, - time_num: i64, - time_den: i64, - out_handles: *mut *mut OakEngineKeyframe, - max_handles: c_int, -) -> c_int { - 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::(kf); - return Ok(1); - } - } - Ok(0) - }) -} - -/// `oakengine_node_set_input_keyframing`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_set_input_keyframing( - self_: *mut OakEngineNode, - input_id: *const c_char, - element: c_int, - keyframing: c_int, - track: c_int, - enable_all_tracks: c_int, - undo_name: *const c_char, -) -> c_int { - // 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 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)) - } - }) -} - -/// `oakengine_node_set_input_keyframing_command`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_set_input_keyframing_command( - self_: *mut OakEngineNode, - input_id: *const c_char, - element: c_int, - keyframing: c_int, -) -> *mut c_void { - 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()) - }) -} - -/// `oakengine_node_keyframes_paste`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_keyframes_paste( - self_: *mut OakEngineNode, - keyframes: *mut *mut OakEngineKeyframe, - count: c_int, - undo_name: *const c_char, -) -> c_int { - // 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 h = unbox(self_)?; - let _ = undo_name; - 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(()) - }) -} - -// --------------------------------------------------------------------------- -// node.h — OakEngineKeyframe accessors -// --------------------------------------------------------------------------- - -/// `oakengine_keyframe_get_time`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_keyframe_get_time( - self_: *const OakEngineKeyframe, - num: *mut i64, - den: *mut i64, -) -> c_int { - guard(|| unsafe { - let h = unbox(self_)?; - Error::from_module(n::oaknode_keyframe_get_time(h, num, den)) - }) -} - -/// `oakengine_keyframe_get_input_id`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_keyframe_get_input_id( - self_: *const OakEngineKeyframe, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let h = unbox(self_)?; - let rc = n::oaknode_keyframe_get_input(h, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_keyframe_get_track`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_keyframe_get_track(self_: *const OakEngineKeyframe) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(-1); - } - let mut track: c_int = -1; - Error::from_module(n::oaknode_keyframe_get_track(unbox(self_)?, &mut track))?; - Ok(track) - }) -} - -/// `oakengine_keyframe_get_element`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_keyframe_get_element(self_: *const OakEngineKeyframe) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(-1); - } - let mut element: c_int = -1; - Error::from_module(n::oaknode_keyframe_get_element(unbox(self_)?, &mut element))?; - Ok(element) - }) -} - -/// `oakengine_keyframe_get_node`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_keyframe_get_node( - self_: *const OakEngineKeyframe, -) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - if self_.is_null() { - return Ok(std::ptr::null_mut()); - } - let mut out = CHandle::null(); - let rc = n::oaknode_keyframe_get_parent(unbox(self_)?, &mut out); - if rc != 0 || out.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(out)) - }) -} - -/// `oakengine_keyframe_get_type` — facade easing type. -#[no_mangle] -pub unsafe extern "C" fn oakengine_keyframe_get_type(self_: *const OakEngineKeyframe) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(-1); - } - let mut ty: c_int = 0; - Error::from_module(n::oaknode_keyframe_get_type(unbox(self_)?, &mut ty))?; - Ok(ty) - }) -} - -/// `oakengine_keyframe_default_type`. -#[no_mangle] -pub extern "C" fn oakengine_keyframe_default_type() -> c_int { - // NodeKeyframe::k_default_type is linear → facade type 0. - 0 -} - -/// `oakengine_keyframe_opposing_bezier_type`. -#[no_mangle] -pub extern "C" fn oakengine_keyframe_opposing_bezier_type(type_: c_int) -> c_int { - unsafe { n::oaknode_keyframe_opposing_bezier_type(type_) } -} - -/// `oakengine_keyframe_get_value`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_keyframe_get_value( - self_: *const OakEngineKeyframe, - out: *mut OakNodeValue, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || out.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - Error::from_module(n::oaknode_keyframe_get_value(unbox(self_)?, out)) - }) -} - -/// `oakengine_keyframe_compute_paste_value`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_keyframe_compute_paste_value( - target_node: *mut OakEngineNode, - keyframe: *mut OakEngineKeyframe, - out: *mut OakNodeValue, -) -> c_int { - guard(|| unsafe { - if target_node.is_null() || keyframe.is_null() || out.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - Error::from_module(n::oaknode_keyframe_compute_paste_value( - unbox(target_node)?, - unbox(keyframe)?, - out, - )) - }) -} - -/// `oakengine_keyframe_has_sibling_at_time`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_keyframe_has_sibling_at_time( - self_: *const OakEngineKeyframe, - time_ts: i64, - track: c_int, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let mut value: c_int = 0; - // The capi passes the timestamp as seconds with denominator 1. - let rc = n::oaknode_keyframe_has_sibling_at_time(unbox(self_)?, time_ts, 1, &mut value); - if rc != 0 { - return Err(Error::Module(rc)); - } - let _ = track; - Ok(value) - }) -} - -/// `oakengine_keyframe_set_bezier_point_live`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_keyframe_set_bezier_point_live( - self_: *mut OakEngineKeyframe, - point_index: c_int, - x: f64, - y: f64, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || point_index < 0 || point_index > 1 { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let rc = n::oaknode_keyframe_set_bezier_control(unbox(self_)?, point_index, x, y); - Error::from_module(rc) - }) -} - -/// `oakengine_keyframe_get_bezier_point`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_keyframe_get_bezier_point( - self_: *const OakEngineKeyframe, - point_index: c_int, - x: *mut f64, - y: *mut f64, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || x.is_null() || y.is_null() || point_index < 0 || point_index > 1 { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - Error::from_module(n::oaknode_keyframe_get_bezier_control( - unbox(self_)?, - point_index, - x, - y, - )) - }) -} - -/// `oakengine_keyframe_get_valid_bezier_point`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_keyframe_get_valid_bezier_point( - self_: *const OakEngineKeyframe, - point_index: c_int, - x: *mut f64, - y: *mut f64, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || x.is_null() || y.is_null() || point_index < 0 || point_index > 1 { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - Error::from_module(n::oaknode_keyframe_get_valid_bezier_control( - unbox(self_)?, - point_index, - x, - y, - )) - }) -} - -/// `oakengine_keyframe_set_value_live`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_keyframe_set_value_live( - self_: *mut OakEngineKeyframe, - value: *const OakNodeValue, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || value.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let rc = n::oaknode_keyframe_set_value(unbox(self_)?, value); - Error::from_module(rc) - }) -} - -/// `oakengine_keyframe_set_time_live`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_keyframe_set_time_live( - self_: *mut OakEngineKeyframe, - num: i64, - den: i64, -) -> c_int { - guard(|| unsafe { - if self_.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - let rc = n::oaknode_keyframe_set_time(unbox(self_)?, num, den); - Error::from_module(rc) - }) -} - -/// `oakengine_keyframes_remove_many`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_keyframes_remove_many( - keyframes: *mut *mut OakEngineKeyframe, - count: c_int, - undo_name: *const c_char, -) -> c_int { - // 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; - 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(()) - }) -} - -/// `oakengine_keyframe_create` — detached keyframe handle. -#[no_mangle] -pub unsafe extern "C" fn oakengine_keyframe_create( - node: *mut OakEngineNode, - input_id: *const c_char, - element: c_int, - track: c_int, - time_ts: i64, - type_: c_int, - value: *const OakNodeValue, - duration_ts: i64, -) -> *mut OakEngineKeyframe { - guard_ptr(|| unsafe { - if node.is_null() || input_id.is_null() || value.is_null() { - set_node_error("invalid arguments"); - return Ok(std::ptr::null_mut()); - } - let h = unbox(node)?; - let tb = time_base_for(h); - let (t_num, t_den) = ts_to_time(time_ts, tb); - let kf = - n::oaknode_keyframe_create(t_num, t_den, value, type_, track, element, input_id, h); - if kf.ctx.is_null() { - return Ok(std::ptr::null_mut()); - } - let _ = duration_ts; - Ok(box_handle::(kf)) - }) -} - -/// `oakengine_keyframe_dispose` — destroy a detached keyframe. -#[no_mangle] -pub unsafe extern "C" fn oakengine_keyframe_dispose(keyframe: *mut OakEngineKeyframe) { - guard_void(|| unsafe { - if keyframe.is_null() { - return; - } - let mut h = (*keyframe).handle; - n::oaknode_keyframe_free(&mut h); - drop(Box::from_raw(keyframe)); - }) -} - -// --------------------------------------------------------------------------- -// node.h — input dragger -// --------------------------------------------------------------------------- - -/// `oakengine_dragger_create`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_dragger_create( - node: *mut OakEngineNode, - input_id: *const c_char, - element: c_int, - track: c_int, -) -> *mut OakEngineNodeDragger { - guard_ptr(|| unsafe { - if node.is_null() || input_id.is_null() { - set_node_error("invalid arguments"); - return Ok(std::ptr::null_mut()); - } - let d = n::oaknode_dragger_create(unbox(node)?, input_id, element, track); - if d.ctx.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(d)) - }) -} - -/// `oakengine_dragger_start` — start the drag at a frame timestamp. -#[no_mangle] -pub unsafe extern "C" fn oakengine_dragger_start( - self_: *mut OakEngineNodeDragger, - time_ts: i64, - track: c_int, - insert_on_all_tracks: c_int, -) -> c_int { - guard(|| unsafe { - if self_.is_null() { - set_node_error("invalid dragger"); - return Err(Error::Invalid); - } - let h = unbox(self_)?; - // The engine's dragger contract is 1-based tracks; the module is - // 0-based. The dragger box does not carry its node, so the - // engine-default time base (1001/30000) applies to the frame - // timestamp conversion (documented deviation from the capi, which - // uses the project's first-sequence time base). - let tb = (1001, 30000); - let (t_num, t_den) = ts_to_time(time_ts, tb); - let rc = n::oaknode_dragger_start(h, t_num, t_den, track - 1, insert_on_all_tracks); - Error::from_module(rc) - }) -} - -/// `oakengine_dragger_drag` — live drag. -#[no_mangle] -pub unsafe extern "C" fn oakengine_dragger_drag( - self_: *mut OakEngineNodeDragger, - value: *const OakNodeValue, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || value.is_null() { - set_node_error("invalid arguments"); - return Err(Error::Invalid); - } - Error::from_module(n::oaknode_dragger_drag(unbox(self_)?, value)) - }) -} - -/// `oakengine_dragger_end` — end the drag, pushing ONE undoable command. -#[no_mangle] -pub unsafe extern "C" fn oakengine_dragger_end( - self_: *mut OakEngineNodeDragger, - undo_name: *const c_char, -) -> c_int { - guard(|| unsafe { - if self_.is_null() { - set_node_error("invalid dragger"); - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_dragger_end(h, &mut cmd); - if rc != 0 { - return Err(Error::Module(rc)); - } - let name = if undo_name.is_null() { - "Drag Input".to_string() - } else { - let s = read_cstr(undo_name); - if s.is_empty() { - "Drag Input".to_string() - } else { - s - } - }; - let name_c = std::ffi::CString::new(name.as_str()) - .map_err(|_| Error::Failed("invalid undo name".into()))?; - push_or_run(command_box(cmd)?, name_c.as_ptr()) - }) -} - -/// `oakengine_dragger_is_started`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_dragger_is_started(self_: *const OakEngineNodeDragger) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let mut started: c_int = 0; - Error::from_module(n::oaknode_dragger_is_started(unbox(self_)?, &mut started))?; - Ok(started) - }) -} - -/// `oakengine_dragger_free`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_dragger_free(self_: *mut OakEngineNodeDragger) { - guard_void(|| unsafe { - if self_.is_null() { - return; - } - let mut h = (*self_).handle; - n::oaknode_dragger_free(&mut h); - drop(Box::from_raw(self_)); - }) -} - -// --------------------------------------------------------------------------- -// node.h — node static data and helpers -// --------------------------------------------------------------------------- - -/// `oakengine_node_enabled_input_id`. -#[no_mangle] -pub extern "C" fn oakengine_node_enabled_input_id() -> *const c_char { - // Node::k_enabled_input (engine/node/node.cpp:47). - static S: &[u8] = b"enabled_in\0"; - S.as_ptr() as *const c_char -} - -/// `oakengine_volume_samples_input_id`. -#[no_mangle] -pub extern "C" fn oakengine_volume_samples_input_id() -> *const c_char { - // VolumeNode::k_samples_input (engine/node/audio/volume/volume.cpp:29). - static S: &[u8] = b"samples_in\0"; - S.as_ptr() as *const c_char -} - -/// `oakengine_transform_texture_input_id`. -#[no_mangle] -pub extern "C" fn oakengine_transform_texture_input_id() -> *const c_char { - // TransformDistortNode::k_texture_input - // (engine/node/distort/transform/transformdistortnode.cpp:30). - static S: &[u8] = b"tex_in\0"; - S.as_ptr() as *const c_char -} - -/// `oakengine_transition_in_block_input_id`. -#[no_mangle] -pub extern "C" fn oakengine_transition_in_block_input_id() -> *const c_char { - // TransitionBlock::k_in_block_input - // (engine/node/block/transition/transition.cpp:34). - static S: &[u8] = b"in_block_in\0"; - S.as_ptr() as *const c_char -} - -/// `oakengine_transition_out_block_input_id`. -#[no_mangle] -pub extern "C" fn oakengine_transition_out_block_input_id() -> *const c_char { - // TransitionBlock::k_out_block_input - // (engine/node/block/transition/transition.cpp:33). - static S: &[u8] = b"out_block_in\0"; - S.as_ptr() as *const c_char -} - -/// `oakengine_audio_waveform_max_sample_rate`. -#[no_mangle] -pub extern "C" fn oakengine_audio_waveform_max_sample_rate() -> f64 { - // AudioVisualWaveform::k_maximum_sample_rate (engine/audio/ - // audiovisualwaveform.cpp:33) is the Rational 1024. - 1024.0 -} - -/// `oakengine_node_category_name` — name of a CategoryID ordinal. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_category_name( - category_id: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| { - // Node::get_category_name (engine/node/node.cpp:2348). - let name = match category_id { - 0 => "Output", - 1 => "Generator", - 2 => "Math", - 3 => "Keying", - 4 => "Filter", - 5 => "Color", - 6 => "Time", - 7 => "Timeline", - 8 => "Transition", - 9 => "Distort", - 10 => "Project", - 11 => "OpenFX", - _ => "Uncategorized", - }; - Ok(unsafe { write_string(name, buf, buf_size) }) - }) -} - -/// `oakengine_node_link_command` — opaque NodeLinkCommand. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_link_command( - a: *mut OakEngineNode, - b: *mut OakEngineNode, - link: c_int, -) -> *mut c_void { - guard_ptr(|| unsafe { - if a.is_null() || b.is_null() { - return Ok(std::ptr::null_mut()); - } - let mut cmd: CHandle = CHandle::null(); - let rc = n::oaknode_node_link_undoable( - unbox(a)?, - unbox(b)?, - if link != 0 { 1 } else { 0 }, - &mut cmd, - ); - if rc != 0 { - return Ok(std::ptr::null_mut()); - } - Ok(command_box(cmd)?.cast()) - }) -} - -/// `oakengine_node_copy_in_graph` — copy with an optional parent multi. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_copy_in_graph( - node: *mut OakEngineNode, - command: *mut c_void, -) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - if node.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(node)?; - let mut cmd: CHandle = CHandle::null(); - let copy = n::oaknode_node_copy_in_graph(h, &mut cmd); - if copy.is_null() { - return Ok(std::ptr::null_mut()); - } - if cmd.ctx.is_null() { - return Ok(std::ptr::null_mut()); - } - if command.is_null() { - push_command(cmd, "Copy Node")?; - } else { - let rc = crate::undo::oakengine_undo_command_multi_add_child( - command, - command_box(cmd)?.cast(), - ); - if rc != 0 { - return Ok(std::ptr::null_mut()); - } - } - Ok(box_handle::(copy)) - }) -} - -/// `oakengine_node_copy_dependency_graph`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_copy_dependency_graph( - nodes: *mut *mut OakEngineNode, - copies: *mut *mut OakEngineNode, - count: c_int, - command: *mut c_void, -) -> c_int { - // Stub: the oaknode module has no dependency-graph copy. - guard(|| { - if nodes.is_null() || copies.is_null() || count <= 0 { - return Err(Error::Invalid); - } - let _ = command; - Err(Error::Invalid) - }) -} - -/// `oakengine_node_connect_command_string`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_connect_command_string( - output: *mut OakEngineNode, - input_node: *mut OakEngineNode, - input_id: *const c_char, - element: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - // Stub: the module has no connect-command string generator; an empty - // description is returned. - guard_int(|| unsafe { - if output.is_null() || input_node.is_null() || input_id.is_null() { - return Err(Error::Invalid); - } - let _ = (unbox(output)?, unbox(input_node)?); - let _ = element; - Ok(write_string("", buf, buf_size)) - }) -} - -/// `oakengine_node_transform_time_to`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_transform_time_to( - from: *mut OakEngineNode, - to: *mut OakEngineNode, - _direction: c_int, - _path_index: c_int, - in_num: i64, - in_den: i64, - out_num: i64, - out_den: i64, - result_in_num: *mut i64, - result_in_den: *mut i64, - result_out_num: *mut i64, - result_out_den: *mut i64, -) -> c_int { - // Stub: the oaknode module has no transform_time_to; the identity - // range is returned. - guard(|| unsafe { - if from.is_null() || to.is_null() { - return Err(Error::Invalid); - } - let _ = (unbox(from)?, unbox(to)?); - if !result_in_num.is_null() { - *result_in_num = in_num; - } - if !result_in_den.is_null() { - *result_in_den = in_den; - } - if !result_out_num.is_null() { - *result_out_num = out_num; - } - if !result_out_den.is_null() { - *result_out_den = out_den; - } - Ok(()) - }) -} - -// --------------------------------------------------------------------------- -// node.h — NodeValue static methods -// --------------------------------------------------------------------------- - -/// `oakengine_node_value_keyframe_track_count` — tracks for a value type. -#[no_mangle] -pub extern "C" fn oakengine_node_value_keyframe_track_count(c_type: c_int) -> c_int { - // NodeValue::get_number_of_keyframe_tracks (engine/node/value.cpp:181). - match c_type { - value_type::VEC2 => 2, - value_type::VEC3 => 3, - value_type::VEC4 | value_type::COLOR => 4, - value_type::BEZIER => 6, - _ => 1, - } -} - -/// `oakengine_node_value_pretty_type_name` — display name (buf/size). -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_value_pretty_type_name( - c_type: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - // NodeValue::get_pretty_data_type_name (engine/node/value.cpp:256). - guard_int(|| { - if c_type <= value_type::NONE || c_type > value_type::AUDIO_PARAMS { - return Err(Error::Invalid); - } - let name = match c_type { - value_type::INT | value_type::COMBO => "Integer", - value_type::STR_COMBO => "String Combo", - value_type::FLOAT => "Float", - value_type::RATIONAL => "Rational", - value_type::BOOL => "Boolean", - value_type::COLOR => "Color", - value_type::TEXT => "Text", - value_type::FONT => "Font", - value_type::STRING => "File", - value_type::TEXTURE => "Texture", - value_type::SAMPLES => "Samples", - value_type::VEC2 => "Vector 2D", - value_type::VEC3 => "Vector 3D", - value_type::VEC4 => "Vector 4D", - value_type::BINARY => "Binary", - value_type::BEZIER => "Bezier", - value_type::VIDEO_PARAMS => "Video Params", - value_type::AUDIO_PARAMS => "Audio Params", - _ => "None", - }; - Ok(unsafe { write_string(name, buf, buf_size) }) - }) -} - -/// `oakengine_node_value_split_to_tracks` — split a normal value into -/// per-component track values. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_value_split_to_tracks( - c_type: c_int, - normal: *const OakNodeValue, - tracks_out: *mut OakNodeValue, - track_count: c_int, -) -> c_int { - guard(|| unsafe { - if normal.is_null() || tracks_out.is_null() || track_count <= 0 { - return Err(Error::Invalid); - } - let n = *normal; - let count = oakengine_node_value_keyframe_track_count(c_type).min(track_count); - for i in 0..count as usize { - let mut t = OakNodeValue { - kind: c_type, - num: 0, - den: 0, - f: [0.0; 4], - }; - match c_type { - value_type::INT | value_type::COMBO | value_type::BOOL => { - t.num = n.num; - } - value_type::RATIONAL => { - t.num = n.num; - t.den = n.den; - } - value_type::COLOR | value_type::VEC2 | value_type::VEC3 | value_type::VEC4 => { - // Per-component split: track `i` carries component `i` - // (combine_tracks reassembles from each track's f[0]). - t.f = [n.f[i], 0.0, 0.0, 0.0]; - } - value_type::FLOAT | value_type::BEZIER => { - t.f[0] = n.f[0]; - } - _ => { - t.kind = c_type; - t.f = n.f; - } - } - *tracks_out.add(i) = t; - } - Ok(()) - }) -} - -/// `oakengine_node_value_combine_tracks` — combine track values into one. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_value_combine_tracks( - c_type: c_int, - tracks: *const OakNodeValue, - track_count: c_int, - normal_out: *mut OakNodeValue, -) -> c_int { - guard(|| unsafe { - if tracks.is_null() || normal_out.is_null() || track_count <= 0 { - return Err(Error::Invalid); - } - let mut out = OakNodeValue { - kind: c_type, - num: 0, - den: 0, - f: [0.0; 4], - }; - let first = *tracks; - match c_type { - value_type::INT | value_type::COMBO | value_type::BOOL => { - out.num = first.num; - } - value_type::RATIONAL => { - out.num = first.num; - out.den = first.den; - } - value_type::FLOAT | value_type::BEZIER => { - out.f[0] = first.f[0]; - } - value_type::COLOR | value_type::VEC2 | value_type::VEC3 | value_type::VEC4 => { - for i in 0..(track_count as usize).min(4) { - out.f[i] = (*tracks.add(i)).f[0]; - } - } - _ => { - out.f = first.f; - } - } - *normal_out = out; - Ok(()) - }) -} - -// --------------------------------------------------------------------------- -// node.h — node type queries (dynamic_cast replacements) -// --------------------------------------------------------------------------- - -/// `oakengine_node_is_clip`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_is_clip(self_: *const OakEngineNode) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - Ok(if is_node_type(unbox(self_)?, TYPE_ID_CLIP_BLOCK) { - 1 - } else { - 0 - }) - }) -} - -/// `oakengine_node_is_track`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_is_track(self_: *const OakEngineNode) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - Ok(if is_node_type(unbox(self_)?, TYPE_ID_TRACK) { - 1 - } else { - 0 - }) - }) -} - -/// `oakengine_node_is_viewer_output` — Sequence or Footage. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_is_viewer_output(self_: *const OakEngineNode) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let h = unbox(self_)?; - Ok( - if is_node_type(h, TYPE_ID_SEQUENCE) || is_node_type(h, TYPE_ID_FOOTAGE) { - 1 - } else { - 0 - }, - ) - }) -} - -/// `oakengine_node_is_footage`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_is_footage(self_: *const OakEngineNode) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - Ok(if is_node_type(unbox(self_)?, TYPE_ID_FOOTAGE) { - 1 - } else { - 0 - }) - }) -} - -/// `oakengine_node_is_sequence`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_is_sequence(self_: *const OakEngineNode) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - Ok(if is_node_type(unbox(self_)?, TYPE_ID_SEQUENCE) { - 1 - } else { - 0 - }) - }) -} - -/// `oakengine_node_is_folder`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_is_folder(self_: *const OakEngineNode) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - Ok(if is_node_type(unbox(self_)?, TYPE_ID_FOLDER) { - 1 - } else { - 0 - }) - }) -} - -// --------------------------------------------------------------------------- -// node.h — clip / track specific -// --------------------------------------------------------------------------- - -/// `oakengine_clip_get_track`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_get_track( - clip: *const OakEngineNode, -) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - if clip.is_null() { - return Ok(std::ptr::null_mut()); - } - let ch = unbox(clip)?; - if !is_node_type(ch, TYPE_ID_CLIP_BLOCK) { - return Ok(std::ptr::null_mut()); - } - let block = n::oaknode_block_from_node(ch); - if block.is_null() { - return Ok(std::ptr::null_mut()); - } - let mut out = CHandle::null(); - let rc = n::oaknode_block_get_track(block, &mut out); - if rc != 0 || out.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(out)) - }) -} - -/// `oakengine_track_get_type`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_track_get_type(track: *const OakEngineNode) -> c_int { - guard_int(|| unsafe { - if track.is_null() { - return Ok(-1); - } - let th = unbox(track)?; - if !is_node_type(th, TYPE_ID_TRACK) { - return Ok(-1); - } - let mut ty: c_int = -1; - Error::from_module(n::oaknode_track_get_type(th, &mut ty))?; - Ok(ty) - }) -} - -/// `oakengine_track_get_index`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_track_get_index(track: *const OakEngineNode) -> c_int { - guard_int(|| unsafe { - if track.is_null() { - return Ok(-1); - } - let th = unbox(track)?; - if !is_node_type(th, TYPE_ID_TRACK) { - return Ok(-1); - } - let mut index: c_int = -1; - Error::from_module(n::oaknode_track_get_index(th, &mut index))?; - Ok(index) - }) -} - -/// `oakengine_track_get_sequence`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_track_get_sequence( - track: *const OakEngineNode, -) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - if track.is_null() { - return Ok(std::ptr::null_mut()); - } - let th = unbox(track)?; - if !is_node_type(th, TYPE_ID_TRACK) { - return Ok(std::ptr::null_mut()); - } - let mut out = CHandle::null(); - let rc = n::oaknode_track_get_sequence(th, &mut out); - if rc != 0 || out.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(out)) - }) -} - -/// `oakengine_block_get_length_rational`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_block_get_length_rational( - block: *const OakEngineNode, - num: *mut c_int, - den: *mut c_int, -) -> c_int { - guard(|| unsafe { block_rational(block, "length", num, den) }) -} - -/// `oakengine_block_get_in_rational`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_block_get_in_rational( - block: *const OakEngineNode, - num: *mut c_int, - den: *mut c_int, -) -> c_int { - guard(|| unsafe { block_rational(block, "in", num, den) }) -} - -/// `oakengine_block_get_out_rational`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_block_get_out_rational( - block: *const OakEngineNode, - num: *mut c_int, - den: *mut c_int, -) -> c_int { - guard(|| unsafe { block_rational(block, "out", num, den) }) -} - -/// Shared body of the block rational getters. -unsafe fn block_rational( - block: *const OakEngineNode, - which: &str, - num: *mut c_int, - den: *mut c_int, -) -> Result<()> { - unsafe { - if block.is_null() { - return Err(Error::Invalid); - } - let bh = unbox(block)?; - if !is_node_type(bh, TYPE_ID_CLIP_BLOCK) - && !is_node_type(bh, TYPE_ID_GAP_BLOCK) - && !is_node_type(bh, TYPE_ID_TRANSITION_BLOCK) - { - return Err(Error::Invalid); - } - let b = n::oaknode_block_from_node(bh); - if b.is_null() { - return Err(Error::Invalid); - } - let (mut n_, mut d) = (0 as c_int, 0 as c_int); - let rc = match which { - "in" => n::oaknode_block_get_in(b, &mut n_, &mut d), - "out" => n::oaknode_block_get_out(b, &mut n_, &mut d), - _ => n::oaknode_block_get_length(b, &mut n_, &mut d), - }; - if rc != 0 { - return Err(Error::Module(rc)); - } - if !num.is_null() { - *num = n_; - } - if !den.is_null() { - *den = d; - } - Ok(()) - } -} - -// --------------------------------------------------------------------------- -// node.h — viewer output specific -// --------------------------------------------------------------------------- - -/// `oakengine_viewer_output_get_connected_texture`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_viewer_output_get_connected_texture( - self_: *const OakEngineNode, -) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - if self_.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(self_)?; - if !is_node_type(h, TYPE_ID_SEQUENCE) && !is_node_type(h, TYPE_ID_FOOTAGE) { - return Ok(std::ptr::null_mut()); - } - let mut out = CHandle::null(); - // The viewer's texture input ("tex_in") — the engine default. - let rc = n::oaknode_node_input_get_connected_node(h, c"tex_in".as_ptr(), &mut out); - if rc != 0 || out.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(out)) - }) -} - -// --------------------------------------------------------------------------- -// node.h — gizmo access -// --------------------------------------------------------------------------- - -/// `oakengine_node_has_gizmos`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_has_gizmos(self_: *const OakEngineNode) -> c_int { - // Stub: the oaknode module has no gizmo C ABI. - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let _ = unbox(self_)?; - Ok(0) - }) -} - -/// `oakengine_node_gizmo_count`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_gizmo_count(self_: *const OakEngineNode) -> c_int { - // Stub: see `oakengine_node_has_gizmos`. - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let _ = unbox(self_)?; - Ok(0) - }) -} - -/// `oakengine_node_gizmo_at`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_gizmo_at( - self_: *const OakEngineNode, - index: c_int, -) -> *mut c_void { - // Stub: see `oakengine_node_has_gizmos`. - guard_ptr(|| { - let _ = (self_, index); - Ok(std::ptr::null_mut()) - }) -} - -/// `oakengine_node_update_gizmo_positions`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_update_gizmo_positions( - self_: *mut OakEngineNode, - _node_value_row: *mut c_void, - _video_width: c_int, - _video_height: c_int, - _time_num: i64, - _time_den: i64, -) -> c_int { - // Stub: see `oakengine_node_has_gizmos` (no gizmos → the capi's - // documented no-op result). - guard(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - Ok(()) - }) -} - -// --------------------------------------------------------------------------- -// node.h — graph topology -// --------------------------------------------------------------------------- - -/// `oakengine_node_inputs_from` — whether this node receives from `other`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_inputs_from( - self_: *const OakEngineNode, - other: *const OakEngineNode, - recursive: c_int, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() || other.is_null() { - return Ok(0); - } - let sh = unbox(self_)?; - let oh = unbox(other)?; - // BFS over the module's output connections starting at `other` - // (inputs_from: is `other` reachable feeding into `self`?). Every - // discovered neighbor is checked against the target, so a DIRECT - // feeder is found while expanding the depth-0 frontier; recursion - // merely widens the search beyond it. - let target = n::oaknode_node_identity(sh); - let mut frontier = vec![oh]; - let mut visited: Vec = Vec::new(); - let mut depth = 0; - while !frontier.is_empty() && (recursive != 0 || depth == 0) { - let mut next = Vec::new(); - for cur in frontier { - let id = n::oaknode_node_identity(cur); - if id == target { - return Ok(1); - } - if visited.contains(&id) { - continue; - } - visited.push(id); - let mut count: c_int = 0; - if n::oaknode_node_output_connection_count(cur, &mut count) != 0 { - continue; - } - for i in 0..count { - let mut out = CHandle::null(); - if n::oaknode_node_output_connection_node_at(cur, i, &mut out) == 0 - && !out.is_null() - { - // Direct feeders are identified at discovery, before - // the depth counter advances (recursive == 0 must - // still inspect `other`'s own outputs). - if n::oaknode_node_identity(out) == target { - return Ok(1); - } - next.push(out); - } - } - } - frontier = next; - depth += 1; - } - Ok(0) - }) -} - -/// `oakengine_node_output_connection_count`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_output_connection_count( - self_: *const OakEngineNode, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let mut count: c_int = 0; - Error::from_module(n::oaknode_node_output_connection_count( - unbox(self_)?, - &mut count, - ))?; - Ok(count) - }) -} - -/// `oakengine_node_output_connection_at`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_output_connection_at( - self_: *const OakEngineNode, - index: c_int, - input_node: *mut *mut OakEngineNode, - input_id_buf: *mut c_char, - input_id_size: c_int, - element: *mut c_int, -) -> c_int { - guard(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let mut count: c_int = 0; - Error::from_module(n::oaknode_node_output_connection_count(h, &mut count))?; - if index < 0 || index >= count { - return Err(Error::NotFound); - } - let mut out = CHandle::null(); - let rc = n::oaknode_node_output_connection_node_at(h, index, &mut out); - if rc != 0 { - return Err(Error::Module(rc)); - } - if !out.is_null() { - if !input_node.is_null() { - *input_node = box_handle::(out); - } - } - let rc = - n::oaknode_node_output_connection_input_id_at(h, index, input_id_buf, input_id_size); - if rc < 0 { - return Err(Error::Module(rc)); - } - if !element.is_null() { - let mut e: c_int = -1; - let rc = n::oaknode_node_output_connection_element_at(h, index, &mut e); - if rc != 0 { - return Err(Error::Module(rc)); - } - *element = e; - } - Ok(()) - }) -} - -/// `oakengine_node_output_connection_at_ex`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_output_connection_at_ex( - self_: *const OakEngineNode, - index: c_int, - input_node: *mut *mut OakEngineNode, - input_id_buf: *mut c_char, - input_id_size: c_int, - element: *mut c_int, - hidden: *mut c_int, -) -> c_int { - guard(|| unsafe { - let rc = oakengine_node_output_connection_at( - self_, - index, - input_node, - input_id_buf, - input_id_size, - element, - ); - Error::from_module(rc)?; - // The module has no per-input hidden flag; reported as 0. - if !hidden.is_null() { - *hidden = 0; - } - Ok(()) - }) -} - -/// `oakengine_node_input_connection_count_all`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_connection_count_all( - self_: *const OakEngineNode, -) -> c_int { - // Stub: the oaknode module exposes output connections only. - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let _ = unbox(self_)?; - Ok(0) - }) -} - -/// `oakengine_node_input_connection_at_all`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_connection_at_all( - self_: *const OakEngineNode, - index: c_int, - input_node: *mut *mut OakEngineNode, - input_id_buf: *mut c_char, - input_id_size: c_int, - element: *mut c_int, - source_node: *mut *mut OakEngineNode, - hidden: *mut c_int, -) -> c_int { - // Stub: see `oakengine_node_input_connection_count_all`. - guard(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - let _ = ( - index, - input_node, - input_id_buf, - input_id_size, - element, - source_node, - hidden, - ); - Err(Error::NotFound) - }) -} - -/// `oakengine_node_input_connection_count`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_connection_count( - self_: *const OakEngineNode, - input_id: *const c_char, - element: c_int, -) -> c_int { - // Stub: see `oakengine_node_input_connection_count_all`. - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - return Ok(0); - } - let _ = unbox(self_)?; - let _ = element; - Ok(0) - }) -} - -/// `oakengine_node_input_connection_at`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_input_connection_at( - self_: *const OakEngineNode, - input_id: *const c_char, - element: c_int, - index: c_int, -) -> *mut OakEngineNode { - // Stub: see `oakengine_node_input_connection_count_all`. - guard_ptr(|| { - let _ = (self_, input_id, element, index); - Ok(std::ptr::null_mut()) - }) -} - -// --------------------------------------------------------------------------- -// node.h — node data (project tree columns) -// --------------------------------------------------------------------------- - -/// `oakengine_node_get_data`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_data( - self_: *const OakEngineNode, - role: c_int, - out_type: *mut c_int, - out_int: *mut i64, - out_str: *mut c_char, - out_str_size: c_int, -) -> c_int { - // Stub: the oaknode module has no Node::data() table; every role - // reports "no data" (out_type 0) like a node without data. - guard(|| unsafe { - if !out_type.is_null() { - *out_type = 0; - } - if !out_int.is_null() { - *out_int = 0; - } - if !out_str.is_null() && out_str_size > 0 { - *out_str = 0; - } - if self_.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - if role < 0 || role > 5 { - return Err(Error::Invalid); - } - Ok(()) - }) -} - -/// `oakengine_node_get_exclusive_dependency_count`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_exclusive_dependency_count( - self_: *const OakEngineNode, -) -> c_int { - // Stub: the oaknode module has no exclusive-dependency export. - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let _ = unbox(self_)?; - Ok(0) - }) -} - -/// `oakengine_node_get_exclusive_dependency_at`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_exclusive_dependency_at( - self_: *const OakEngineNode, - index: c_int, -) -> *mut OakEngineNode { - // Stub: see `oakengine_node_get_exclusive_dependency_count`. - guard_ptr(|| { - let _ = (self_, index); - Ok(std::ptr::null_mut()) - }) -} - -// --------------------------------------------------------------------------- -// node.h — plugin messages -// --------------------------------------------------------------------------- - -/// `oakengine_node_has_plugin`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_has_plugin(self_: *const OakEngineNode) -> c_int { - // Stub: the oaknode module has no plugin-instance surface. - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let _ = unbox(self_)?; - Ok(0) - }) -} - -/// `oakengine_node_plugin_message_count`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_plugin_message_count(self_: *const OakEngineNode) -> c_int { - // Stub: see `oakengine_node_has_plugin`. - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let _ = unbox(self_)?; - Ok(0) - }) -} - -/// `oakengine_node_plugin_message_at`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_plugin_message_at( - self_: *const OakEngineNode, - index: c_int, - type_: *mut c_int, - msg_buf: *mut c_char, - msg_buf_size: c_int, -) -> c_int { - // Stub: see `oakengine_node_has_plugin`. - guard(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - let _ = (index, type_, msg_buf, msg_buf_size); - Err(Error::NotFound) - }) -} - -/// `oakengine_node_plugin_clear_messages`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_plugin_clear_messages(self_: *mut OakEngineNode) -> c_int { - // Stub: see `oakengine_node_has_plugin`. - guard(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - Err(Error::NotFound) - }) -} - -// --------------------------------------------------------------------------- -// node.h — node cache objects -// --------------------------------------------------------------------------- - -/// `oakengine_node_get_thumbnail_cache`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_thumbnail_cache( - self_: *const OakEngineNode, -) -> *mut OakEngineThumbnailCache { - // Stub: the oaknode module has no thumbnail-cache export (only the - // video frame cache is exposed). - guard_ptr(|| unsafe { - if self_.is_null() { - return Ok(std::ptr::null_mut()); - } - let _ = unbox(self_)?; - Ok(std::ptr::null_mut()) - }) -} - -/// `oakengine_node_get_waveform_cache`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_waveform_cache( - self_: *const OakEngineNode, -) -> *mut OakEngineWaveformCache { - // Stub: the oaknode module has no waveform-cache export. - guard_ptr(|| unsafe { - if self_.is_null() { - return Ok(std::ptr::null_mut()); - } - let _ = unbox(self_)?; - Ok(std::ptr::null_mut()) - }) -} - -/// `oakengine_node_get_video_frame_cache`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_get_video_frame_cache( - self_: *const OakEngineNode, -) -> *mut OakEngineFrameCache { - guard_ptr(|| unsafe { - if self_.is_null() { - return Ok(std::ptr::null_mut()); - } - let mut out = CHandle::null(); - let rc = n::oaknode_node_get_video_frame_cache(unbox(self_)?, &mut out); - if rc != 0 || out.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(out)) - }) -} - -// --------------------------------------------------------------------------- -// footage.h -// --------------------------------------------------------------------------- - -/// The hidden process-wide project that holds probe nodes: the module has -/// no detached footage create (only `oaknode_footage_create(project, ...)`), -/// so probe handles are backed by nodes in this never-exposed project -/// (leaked like the C++ EngineCore shell). -fn probe_project() -> CHandle { - static PROBE: std::sync::OnceLock = std::sync::OnceLock::new(); - *PROBE.get_or_init(|| unsafe { - let p = n::oaknode_project_init(); - if !p.ctx.is_null() { - n::oaknode_project_initialize(p); - } - p - }) -} - -/// `oakengine_footage_probe` — probe a media file without a project. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_probe(path: *const c_char) -> *mut OakEngineFootage { - guard_ptr(|| unsafe { - set_footage_error(""); - if path.is_null() { - set_footage_error("file does not exist: (null)"); - return Ok(std::ptr::null_mut()); - } - let path_str = read_cstr(path); - if !std::path::Path::new(&path_str).exists() { - set_footage_error(&format!("file does not exist: {}", path_str)); - return Ok(std::ptr::null_mut()); - } - let filename = path_str; - let footage = n::oaknode_footage_create( - probe_project(), - std::ffi::CString::new(filename.as_str()).unwrap().as_ptr(), - ); - if footage.ctx.is_null() { - set_footage_error(&format!("failed to probe \"{}\"", filename)); - return Ok(std::ptr::null_mut()); - } - // The module's footage has no decoder probe cascade (it records - // the filename only); the returned handle is a real node view, so - // the stream/count accessors work and `oakengine_footage_free` - // releases the wrapper. The node itself stays in the hidden probe - // arena (documented deviation: the module cannot delete a - // project-graph node on demand). - Ok(box_handle::(footage)) - }) -} - -/// `oakengine_footage_free` — release a footage handle. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_free(self_: *mut OakEngineFootage) { - guard_void(|| unsafe { - free_box(self_); - }) -} - -/// `oakengine_footage_last_error` — last probe/import error on this thread. -#[no_mangle] -pub extern "C" fn oakengine_footage_last_error(buf: *mut c_char, buf_size: c_int) -> c_int { - crate::handle::guard_int(|| unsafe { - Ok(write_string( - &LAST_FOOTAGE_ERROR.with(|c| c.borrow().clone()), - buf, - buf_size, - )) - }) -} - -/// `oakengine_footage_get_decoder_name`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_get_decoder_name( - self_: *mut OakEngineFootage, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let h = unbox(self_)?; - let rc = n::oaknode_footage_decoder(h, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_footage_get_video_stream_count`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_get_video_stream_count( - self_: *const OakEngineFootage, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let rc = n::oaknode_footage_video_stream_count(unbox(self_)?); - Ok(if rc < 0 { 0 } else { rc }) - }) -} - -/// `oakengine_footage_get_audio_stream_count`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_get_audio_stream_count( - self_: *const OakEngineFootage, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let rc = n::oaknode_footage_audio_stream_count(unbox(self_)?); - Ok(if rc < 0 { 0 } else { rc }) - }) -} - -/// `oakengine_footage_get_subtitle_stream_count`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_get_subtitle_stream_count( - self_: *const OakEngineFootage, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let rc = n::oaknode_footage_subtitle_stream_count(unbox(self_)?); - Ok(if rc < 0 { 0 } else { rc }) - }) -} - -/// `oakengine_footage_get_video_stream_info`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_get_video_stream_info( - self_: *mut OakEngineFootage, - index: c_int, - out: *mut OakFootageVideoInfo, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || out.is_null() { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let mut params: CHandle = CHandle::null(); - let rc = n::oaknode_footage_get_video_params(h, index, &mut params); - if rc != 0 || params.ctx.is_null() { - return Err(Error::NotFound); - } - let mut width: c_int = 0; - let mut height: c_int = 0; - let mut fr_num: c_int = 0; - let mut fr_den: c_int = 0; - let mut tb_num: c_int = 0; - let mut tb_den: c_int = 0; - let mut interlacing: c_int = 0; - c::oakcommon_videoparams_get_width(params, &mut width); - c::oakcommon_videoparams_get_height(params, &mut height); - c::oakcommon_videoparams_get_frame_rate(params, &mut fr_num, &mut fr_den); - c::oakcommon_videoparams_get_time_base(params, &mut tb_num, &mut tb_den); - c::oakcommon_videoparams_get_interlacing(params, &mut interlacing); - (*out) = OakFootageVideoInfo { - stream_index: index, - width, - height, - frame_rate_num: fr_num, - frame_rate_den: fr_den, - // The oakcommon videoparams handle has no duration accessor; - // the module cannot provide it (documented). - duration_ts: 0, - time_base_num: tb_num, - time_base_den: tb_den, - // The module's videoparams has no color tag accessors. - color_primaries: 0, - color_trc: 0, - interlaced: if interlacing != 0 { 1 } else { 0 }, - }; - let mut h2 = params; - c::oakcommon_videoparams_free(&mut h2); - Ok(()) - }) -} - -/// `oakengine_footage_get_audio_stream_info`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_get_audio_stream_info( - self_: *mut OakEngineFootage, - index: c_int, - out: *mut OakFootageAudioInfo, -) -> c_int { - // Stub: the oaknode module exposes video params only; audio stream - // descriptions are not reachable. - guard(|| unsafe { - if self_.is_null() || out.is_null() { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let count = n::oaknode_footage_audio_stream_count(h); - if index < 0 || index >= count { - return Err(Error::NotFound); - } - Err(Error::NotFound) - }) -} - -/// `oakengine_footage_get_duration` — media duration in seconds. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_get_duration( - self_: *mut OakEngineFootage, - seconds: *mut f64, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || seconds.is_null() { - return Err(Error::Invalid); - } - let mut num: c_int = 0; - let mut den: c_int = 0; - let rc = n::oaknode_footage_duration(unbox(self_)?, &mut num, &mut den); - if rc != 0 { - return Err(Error::Module(rc)); - } - *seconds = if den != 0 { - num as f64 / den as f64 - } else { - 0.0 - }; - Ok(()) - }) -} - -/// `oakengine_footage_is_online` — 1 when the media file exists. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_is_online(self_: *mut OakEngineFootage) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let filename = module_string(|buf, size| n::oaknode_footage_filename(h, buf, size))?; - Ok(if std::path::Path::new(&filename).exists() { - 1 - } else { - 0 - }) - }) -} - -/// `oakengine_footage_get_source_start_time`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_get_source_start_time( - self_: *mut OakEngineFootage, - num: *mut c_int, - den: *mut c_int, -) -> c_int { - // Stub: the module's footage has no source-start-time state; the - // "media carries none" result is returned. - guard_int(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - if !num.is_null() { - *num = 0; - } - if !den.is_null() { - *den = 1; - } - Ok(0) - }) -} - -/// `oakengine_project_import_footage` — probe and add to the root folder. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_import_footage( - project: *mut OakEngineProject, - path: *const c_char, -) -> *mut OakEngineFootage { - guard_ptr(|| unsafe { - set_footage_error(""); - if project.is_null() || path.is_null() { - set_footage_error("invalid project or path"); - return Ok(std::ptr::null_mut()); - } - let path_str = read_cstr(path); - if !std::path::Path::new(&path_str).exists() { - set_footage_error(&format!("file does not exist: {}", path_str)); - return Ok(std::ptr::null_mut()); - } - let ph = unbox(project)?; - let root = n::oaknode_project_root(ph); - if root.is_null() { - set_footage_error("invalid project or path"); - return Ok(std::ptr::null_mut()); - } - // The module's footage_create registers the node in the project's - // graph; only the FolderAddChild command is pushed (the capi also - // pushes a NodeAddCommand, which has no module equivalent here). - // The module cannot probe media, so the capi's validity rejection - // is skipped (documented deviation). - let path_c = std::ffi::CString::new(path_str.as_str()).unwrap(); - let footage = n::oaknode_footage_create(ph, path_c.as_ptr()); - if footage.ctx.is_null() { - set_footage_error(&format!("failed to probe \"{}\"", path_str)); - return Ok(std::ptr::null_mut()); - } - let label = std::path::Path::new(&path_str) - .file_name() - .map(|f| f.to_string_lossy().into_owned()) - .unwrap_or_else(|| path_str.clone()); - let label_c = std::ffi::CString::new(label.as_str()).unwrap(); - let rc = n::oaknode_node_set_label(footage, label_c.as_ptr()); - if rc != 0 { - return Ok(std::ptr::null_mut()); - } - let cmd = n::oaknode_command_create_folder_add_child(root, footage); - if cmd.ctx.is_null() { - return Ok(std::ptr::null_mut()); - } - push_command(cmd, "Import Footage")?; - Ok(box_handle::(footage)) - }) -} - -/// `oakengine_footage_borrow` — wrap a footage node in a borrowed handle. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_borrow( - node: *mut OakEngineNode, -) -> *mut OakEngineFootage { - guard_ptr(|| unsafe { - set_footage_error(""); - if node.is_null() { - return Ok(std::ptr::null_mut()); - } - let mut h = unbox(node)?; - if !is_node_type(h, TYPE_ID_FOOTAGE) { - set_footage_error("node is not a footage node"); - return Ok(std::ptr::null_mut()); - } - // The borrow takes its OWN reference (addref) so freeing both the - // borrow and the source node shell later is double-free-safe. - if let Some(addref) = h.addref { - unsafe { addref(h.ctx) }; - } - Ok(box_handle::(h)) - }) -} - -/// `oakengine_footage_is_valid` — 1 when the footage node is valid. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_is_valid(node: *const OakEngineNode) -> c_int { - guard_int(|| unsafe { - if node.is_null() { - return Ok(0); - } - let h = unbox(node)?; - if !is_node_type(h, TYPE_ID_FOOTAGE) { - return Ok(0); - } - let rc = n::oaknode_footage_is_valid(h); - Ok(if rc > 0 { 1 } else { 0 }) - }) -} - -/// `oakengine_footage_relink` — point the footage at a new file. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_relink( - footage: *mut OakEngineFootage, - new_path: *const c_char, -) -> c_int { - guard(|| unsafe { - set_footage_error(""); - if new_path.is_null() { - set_footage_error("invalid path"); - return Err(Error::Invalid); - } - let h = unbox(footage)?; - let path = read_cstr(new_path); - if !std::path::Path::new(&path).exists() { - set_footage_error(&format!("file does not exist: {}", path)); - return Err(Error::NotFound); - } - // The module's set_filename records the new path (no reprobe - // cascade exists in the module; documented deviation). - let rc = n::oaknode_footage_set_filename(h, new_path); - Error::from_module(rc) - }) -} - -/// `oakengine_project_find_offline_footage`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_project_find_offline_footage( - project: *mut OakEngineProject, - search_dir: *const c_char, -) -> c_int { - guard_int(|| unsafe { - set_footage_error(""); - if project.is_null() || search_dir.is_null() { - set_footage_error("invalid arguments"); - return Err(Error::Invalid); - } - let search = read_cstr(search_dir); - let dir = std::path::Path::new(&search); - if !dir.exists() { - set_footage_error(&format!("directory does not exist: {}", search)); - return Err(Error::Invalid); - } - let ph = unbox(project)?; - let mut relinked: c_int = 0; - let total = n::oaknode_project_node_count(ph); - for i in 0..total { - let node = n::oaknode_project_node_at(ph, i); - if node.is_null() || !is_node_type(node, TYPE_ID_FOOTAGE) { - continue; - } - let filename = module_string(|buf, size| n::oaknode_footage_filename(node, buf, size))?; - if filename.is_empty() || std::path::Path::new(&filename).exists() { - continue; - } - let base = std::path::Path::new(&filename) - .file_name() - .map(|f| f.to_string_lossy().into_owned()) - .unwrap_or_default(); - let candidate = dir.join(&base); - if candidate.exists() { - let cand = candidate.to_string_lossy().into_owned(); - let cand_c = std::ffi::CString::new(cand.as_str()).unwrap(); - let rc = n::oaknode_footage_set_filename(node, cand_c.as_ptr()); - if rc == 0 { - relinked += 1; - } - } - } - Ok(relinked) - }) -} - -/// `oakengine_footage_proxy_get_state`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_proxy_get_state(self_: *mut OakEngineFootage) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - Ok(n::oaknode_footage_proxy_state(unbox(self_)?)) - }) -} - -/// `oakengine_footage_proxy_generate`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_proxy_generate(self_: *mut OakEngineFootage) -> c_int { - // Stub: the oaknode module has no ProxyManager / proxy task surface. - guard(|| unsafe { - set_footage_error(""); - if self_.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - set_footage_error("proxy generation is not available through the module"); - Err(Error::State) - }) -} - -/// `oakengine_footage_proxy_delete`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_proxy_delete(self_: *mut OakEngineFootage) -> c_int { - guard(|| unsafe { - set_footage_error(""); - let h = unbox(self_)?; - // The module clears the proxy state; the on-disk proxy files are - // not removed (no file surface in the module). - Error::from_module(n::oaknode_footage_clear_proxy(h)) - }) -} - -/// `oakengine_footage_proxy_is_enabled`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_proxy_is_enabled(self_: *mut OakEngineFootage) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - Ok(n::oaknode_footage_proxy_enabled(unbox(self_)?)) - }) -} - -/// `oakengine_footage_proxy_set_enabled`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_proxy_set_enabled( - self_: *mut OakEngineFootage, - enabled: c_int, -) -> c_int { - guard(|| unsafe { - set_footage_error(""); - let h = unbox(self_)?; - Error::from_module(n::oaknode_footage_set_proxy_enabled( - h, - if enabled != 0 { 1 } else { 0 }, - )) - }) -} - -/// `oakengine_footage_proxy_get_path`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_proxy_get_path( - self_: *mut OakEngineFootage, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let h = unbox(self_)?; - let rc = n::oaknode_footage_proxy_path(h, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/* ---- Stream parameter overrides --------------------------------------------- */ - -/// `oakengine_footage_get_video_stream_overrides`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_get_video_stream_overrides( - self_: *mut OakEngineFootage, - stream_index: c_int, - colorspace_buf: *mut c_char, - colorspace_size: c_int, - color_range: *mut c_int, - interlacing: *mut c_int, - premultiplied: *mut c_int, -) -> c_int { - guard(|| unsafe { - set_footage_error(""); - let h = unbox(self_)?; - let mut params: CHandle = CHandle::null(); - let rc = n::oaknode_footage_get_video_params(h, stream_index, &mut params); - if rc != 0 || params.ctx.is_null() { - set_footage_error(&format!("no video stream at index {}", stream_index)); - return Err(Error::NotFound); - } - // The oakcommon params carry no colorspace name; written empty. - if !colorspace_buf.is_null() { - write_string("", colorspace_buf, colorspace_size); - } - if !color_range.is_null() { - c::oakcommon_videoparams_get_color_range(params, color_range); - } - if !interlacing.is_null() { - c::oakcommon_videoparams_get_interlacing(params, interlacing); - } - if !premultiplied.is_null() { - c::oakcommon_videoparams_get_premultiplied_alpha(params, premultiplied); - } - let mut h2 = params; - c::oakcommon_videoparams_free(&mut h2); - Ok(()) - }) -} - -/// `oakengine_footage_set_video_stream_overrides`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_set_video_stream_overrides( - self_: *mut OakEngineFootage, - stream_index: c_int, - colorspace: *const c_char, - color_range: c_int, - interlacing: c_int, - premultiplied: c_int, -) -> c_int { - guard(|| unsafe { - set_footage_error(""); - let h = unbox(self_)?; - let mut params: CHandle = CHandle::null(); - let rc = n::oaknode_footage_get_video_params(h, stream_index, &mut params); - if rc != 0 || params.ctx.is_null() { - set_footage_error(&format!("no video stream at index {}", stream_index)); - return Err(Error::NotFound); - } - // Colorspace is not representable in the module's params; the - // numeric overrides are applied. The module has no undoable - // params command, so this is applied directly (documented). - let _ = colorspace; - if color_range >= 0 { - c::oakcommon_videoparams_set_color_range(params, color_range); - } - if interlacing >= 0 { - c::oakcommon_videoparams_set_interlacing(params, interlacing); - } - if premultiplied >= 0 { - c::oakcommon_videoparams_set_premultiplied_alpha( - params, - if premultiplied != 0 { 1 } else { 0 }, - ); - } - let rc = n::oaknode_footage_set_video_params(h, stream_index, ¶ms); - let mut h2 = params; - c::oakcommon_videoparams_free(&mut h2); - Error::from_module(rc) - }) -} - -/// `oakengine_footage_get_pixel_aspect`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_get_pixel_aspect( - self_: *mut OakEngineFootage, - stream_index: c_int, - num: *mut c_int, - den: *mut c_int, -) -> c_int { - guard(|| unsafe { - set_footage_error(""); - let h = unbox(self_)?; - let mut params: CHandle = CHandle::null(); - let rc = n::oaknode_footage_get_video_params(h, stream_index, &mut params); - if rc != 0 || params.ctx.is_null() { - set_footage_error(&format!("no video stream at index {}", stream_index)); - return Err(Error::NotFound); - } - let mut n_: c_int = 1; - let mut d: c_int = 1; - c::oakcommon_videoparams_get_pixel_aspect_ratio(params, &mut n_, &mut d); - if !num.is_null() { - *num = n_; - } - if !den.is_null() { - *den = d; - } - let mut h2 = params; - c::oakcommon_videoparams_free(&mut h2); - Ok(()) - }) -} - -/// `oakengine_footage_set_pixel_aspect`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_set_pixel_aspect( - self_: *mut OakEngineFootage, - stream_index: c_int, - num: c_int, - den: c_int, -) -> c_int { - guard(|| unsafe { - set_footage_error(""); - let h = unbox(self_)?; - if num <= 0 || den <= 0 { - set_footage_error(&format!("invalid pixel aspect ratio {}/{}", num, den)); - return Err(Error::Invalid); - } - let mut params: CHandle = CHandle::null(); - let rc = n::oaknode_footage_get_video_params(h, stream_index, &mut params); - if rc != 0 || params.ctx.is_null() { - set_footage_error(&format!("no video stream at index {}", stream_index)); - return Err(Error::NotFound); - } - c::oakcommon_videoparams_set_pixel_aspect_ratio(params, num, den); - let rc = n::oaknode_footage_set_video_params(h, stream_index, ¶ms); - let mut h2 = params; - c::oakcommon_videoparams_free(&mut h2); - Error::from_module(rc) - }) -} - -/// `oakengine_footage_get_image_sequence_params`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_get_image_sequence_params( - self_: *mut OakEngineFootage, - stream_index: c_int, - start_index: *mut i64, - duration: *mut i64, - frame_rate_num: *mut c_int, - frame_rate_den: *mut c_int, -) -> c_int { - guard(|| unsafe { - set_footage_error(""); - let h = unbox(self_)?; - let mut params: CHandle = CHandle::null(); - let rc = n::oaknode_footage_get_video_params(h, stream_index, &mut params); - if rc != 0 || params.ctx.is_null() { - set_footage_error(&format!("no video stream at index {}", stream_index)); - return Err(Error::NotFound); - } - // start/duration are not accessible on the module's params; - // the frame rate is. - if !start_index.is_null() { - *start_index = 0; - } - if !duration.is_null() { - *duration = 0; - } - if !frame_rate_num.is_null() || !frame_rate_den.is_null() { - let mut n_: c_int = 0; - let mut d: c_int = 0; - c::oakcommon_videoparams_get_frame_rate(params, &mut n_, &mut d); - if !frame_rate_num.is_null() { - *frame_rate_num = n_; - } - if !frame_rate_den.is_null() { - *frame_rate_den = d; - } - } - let mut h2 = params; - c::oakcommon_videoparams_free(&mut h2); - Ok(()) - }) -} - -/// `oakengine_footage_set_image_sequence_params`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_set_image_sequence_params( - self_: *mut OakEngineFootage, - stream_index: c_int, - start_index: i64, - duration: i64, - frame_rate_num: c_int, - frame_rate_den: c_int, -) -> c_int { - guard(|| unsafe { - set_footage_error(""); - let h = unbox(self_)?; - if start_index < 0 || duration <= 0 || frame_rate_num <= 0 || frame_rate_den <= 0 { - set_footage_error("invalid image sequence parameters"); - return Err(Error::Invalid); - } - let mut params: CHandle = CHandle::null(); - let rc = n::oaknode_footage_get_video_params(h, stream_index, &mut params); - if rc != 0 || params.ctx.is_null() { - set_footage_error(&format!("no video stream at index {}", stream_index)); - return Err(Error::NotFound); - } - // The module's params have no start/duration setters; the frame - // rate and its flipped time base are applied (documented). - c::oakcommon_videoparams_set_frame_rate(params, frame_rate_num, frame_rate_den); - c::oakcommon_videoparams_set_time_base(params, frame_rate_den, frame_rate_num); - let rc = n::oaknode_footage_set_video_params(h, stream_index, ¶ms); - let mut h2 = params; - c::oakcommon_videoparams_free(&mut h2); - Error::from_module(rc) - }) -} - -/// `oakengine_footage_get_stream_enabled`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_get_stream_enabled( - self_: *mut OakEngineFootage, - track_type: c_int, - index: c_int, -) -> c_int { - // Stub: the module's stream params carry no enabled flag. - guard_int(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let count = match track_type { - 0 => n::oaknode_footage_video_stream_count(h), - 1 => n::oaknode_footage_audio_stream_count(h), - 2 => n::oaknode_footage_subtitle_stream_count(h), - _ => -1, - }; - if index < 0 || index >= count { - return Err(Error::NotFound); - } - Err(Error::NotFound) - }) -} - -/// `oakengine_footage_set_stream_enabled`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_set_stream_enabled( - self_: *mut OakEngineFootage, - track_type: c_int, - index: c_int, - enabled: c_int, -) -> c_int { - // Stub: see `oakengine_footage_get_stream_enabled`. - guard(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - let _ = (track_type, index, enabled); - Err(Error::NotFound) - }) -} - -/// `oakengine_footage_set_source_start_time`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_set_source_start_time( - self_: *mut OakEngineFootage, - enabled: c_int, - num: i64, - den: i64, -) -> c_int { - // Stub: the module's footage has no source-start-time state. - guard(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - let _ = (enabled, num, den); - Err(Error::Invalid) - }) -} - -/// `oakengine_footage_get_source_start_time_source`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_get_source_start_time_source( - self_: *mut OakEngineFootage, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - // Stub: see `oakengine_footage_set_source_start_time`. - guard_int(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - Ok(write_string("", buf, buf_size)) - }) -} - -/* ---- Colorspace candidates --------------------------------------------------- */ - -/// `oakengine_footage_colorspace_count`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_colorspace_count(self_: *mut OakEngineFootage) -> c_int { - // Stub: the module has no color-config surface. - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let _ = unbox(self_)?; - Ok(0) - }) -} - -/// `oakengine_footage_colorspace_at`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_colorspace_at( - self_: *mut OakEngineFootage, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - // Stub: see `oakengine_footage_colorspace_count`. - guard_int(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - let _ = (index, buf, buf_size); - Err(Error::NotFound) - }) -} - -/* ---- Footage extras ------------------------------------------------------- */ - -/// `oakengine_footage_get_filename`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_get_filename( - self_: *mut OakEngineFootage, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let h = unbox(self_)?; - let rc = n::oaknode_footage_filename(h, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_footage_get_stream_reference`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_get_stream_reference( - self_: *mut OakEngineFootage, - stream_index_in_footage: c_int, - out_track_type: *mut c_int, - out_stream_index: *mut c_int, -) -> c_int { - guard(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let vc = n::oaknode_footage_video_stream_count(h); - let ac = n::oaknode_footage_audio_stream_count(h); - let sc = n::oaknode_footage_subtitle_stream_count(h); - let mut flat = stream_index_in_footage; - if flat >= 0 && flat < vc { - if !out_track_type.is_null() { - *out_track_type = 0; // OAKENGINE_TRACK_TYPE_VIDEO - } - if !out_stream_index.is_null() { - *out_stream_index = flat; - } - return Ok(()); - } - flat -= vc; - if flat >= 0 && flat < ac { - if !out_track_type.is_null() { - *out_track_type = 1; // OAKENGINE_TRACK_TYPE_AUDIO - } - if !out_stream_index.is_null() { - *out_stream_index = flat; - } - return Ok(()); - } - flat -= ac; - if flat >= 0 && flat < sc { - if !out_track_type.is_null() { - *out_track_type = 2; // OAKENGINE_TRACK_TYPE_SUBTITLE - } - if !out_stream_index.is_null() { - *out_stream_index = flat; - } - return Ok(()); - } - Err(Error::NotFound) - }) -} - -/// `oakengine_footage_describe_video_stream`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_describe_video_stream( - self_: *mut OakEngineFootage, - video_stream_index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let mut params: CHandle = CHandle::null(); - let rc = n::oaknode_footage_get_video_params(h, video_stream_index, &mut params); - if rc != 0 || params.ctx.is_null() { - return Err(Error::NotFound); - } - let mut width: c_int = 0; - let mut height: c_int = 0; - let mut fr_num: c_int = 0; - let mut fr_den: c_int = 0; - c::oakcommon_videoparams_get_width(params, &mut width); - c::oakcommon_videoparams_get_height(params, &mut height); - c::oakcommon_videoparams_get_frame_rate(params, &mut fr_num, &mut fr_den); - let desc = if fr_den != 0 { - format!( - "{}x{}, {:.3} fps", - width, - height, - fr_num as f64 / fr_den as f64 - ) - } else { - format!("{}x{}", width, height) - }; - let mut h2 = params; - c::oakcommon_videoparams_free(&mut h2); - Ok(write_string(&desc, buf, buf_size)) - }) -} - -/// `oakengine_footage_describe_audio_stream`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_describe_audio_stream( - self_: *mut OakEngineFootage, - audio_stream_index: c_int, - _buf: *mut c_char, - _buf_size: c_int, -) -> c_int { - // Stub: see `oakengine_footage_get_audio_stream_info` (no audio - // params surface). - guard_int(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let count = n::oaknode_footage_audio_stream_count(h); - if audio_stream_index < 0 || audio_stream_index >= count { - return Err(Error::NotFound); - } - Err(Error::NotFound) - }) -} - -/// `oakengine_footage_stream_type_name`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_stream_type_name( - track_type: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| { - let name = match track_type { - 0 => "Video", - 1 => "Audio", - 2 => "Subtitle", - _ => "Unknown", - }; - Ok(unsafe { write_string(name, buf, buf_size) }) - }) -} - -/// `oakengine_footage_has_custom_proxy_params`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_has_custom_proxy_params( - self_: *mut OakEngineFootage, -) -> c_int { - // Stub: the module's footage has no custom-proxy-params state. - guard_int(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - Ok(0) - }) -} - -/// `oakengine_footage_get_effective_proxy_params`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_get_effective_proxy_params( - self_: *mut OakEngineFootage, - out: *mut OakProxyParams, -) -> c_int { - // Stub: the module has no proxy-params surface; a zeroed struct is - // returned. - guard(|| unsafe { - if self_.is_null() || out.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - *out = OakProxyParams { - width: 0, - height: 0, - divider: 0, - version: 0, - crf: 0, - include_audio: 0, - extension: [0; 32], - preset: [0; 32], - }; - Ok(()) - }) -} - -/// `oakengine_footage_set_custom_proxy_params`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_set_custom_proxy_params( - self_: *mut OakEngineFootage, - _params: *const OakProxyParams, -) -> c_int { - // Stub: see `oakengine_footage_has_custom_proxy_params`. - guard(|| unsafe { - if self_.is_null() || _params.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - Err(Error::Invalid) - }) -} - -/// `oakengine_footage_clear_custom_proxy_params`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_clear_custom_proxy_params( - self_: *mut OakEngineFootage, -) -> c_int { - // Stub: see `oakengine_footage_has_custom_proxy_params`. - guard(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - Err(Error::Invalid) - }) -} - -/// `oakengine_footage_set_proxy`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_set_proxy( - self_: *mut OakEngineFootage, - path: *const c_char, - state: c_int, - stream_index: c_int, - enabled: c_int, - version: c_int, -) -> c_int { - guard(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let path = if path.is_null() { - crate::common::empty_cstr() - } else { - path - }; - Error::from_module(n::oaknode_footage_set_proxy( - h, - path, - state, - stream_index, - version, - if enabled != 0 { 1 } else { 0 }, - )) - }) -} - -/// `oakengine_footage_clear_proxy`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_clear_proxy(self_: *mut OakEngineFootage) -> c_int { - guard(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - Error::from_module(n::oaknode_footage_clear_proxy(unbox(self_)?)) - }) -} - -/// `oakengine_footage_invalidate` — force a re-probe on next use. -#[no_mangle] -pub unsafe extern "C" fn oakengine_footage_invalidate(self_: *mut OakEngineFootage) -> c_int { - // Stub: the module's footage has no clear/reprobe cascade; accepted - // as a no-op. - guard(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let _ = unbox(self_)?; - Ok(()) - }) -} diff --git a/crates/oakengine.bk/src/plugin.rs b/crates/oakengine.bk/src/plugin.rs deleted file mode 100644 index 702c47013..000000000 --- a/crates/oakengine.bk/src/plugin.rs +++ /dev/null @@ -1,132 +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 . - -//! `engine/include/oakengine/plugin.h` over the oakplugin module. -//! -//! The active-viewer provider and progress-reporter factory are pure -//! facade state (module 00 analogues of the C++ capi's statics): the UI -//! registers C callbacks here, and the plugin host consumes them once the -//! module exposes the corresponding registration points -//! (`oakplugin_*_set_*_provider`). Until then the callbacks are stored -//! and reported as registered. - -use std::ffi::{c_char, c_int, c_void}; -use std::sync::{Mutex, OnceLock}; - -use crate::stubs::plugin as p; -use crate::error::Error; -use crate::handle::guard; - -/// `oakengine_plugin_active_viewer_fn` — returns the active viewer node. -pub type ActiveViewerFn = - unsafe extern "C" fn(userdata: *mut c_void) -> *mut crate::handle::OakEngineNode; - -/// `oakengine_plugin_reporter_create_fn` — creates a UI progress reporter. -pub type ReporterCreateFn = unsafe extern "C" fn( - message: *const c_char, - title: *const c_char, - userdata: *mut c_void, -) -> *mut c_void; -/// `oakengine_plugin_reporter_destroy_fn` — destroys a reporter. -pub type ReporterDestroyFn = unsafe extern "C" fn(reporter: *mut c_void, userdata: *mut c_void); -/// `oakengine_plugin_reporter_is_cancelled_fn` — 1 when cancelled. -pub type ReporterIsCancelledFn = - unsafe extern "C" fn(reporter: *mut c_void, userdata: *mut c_void) -> c_int; -/// `oakengine_plugin_reporter_set_progress_fn` — progress update. -pub type ReporterSetProgressFn = - unsafe extern "C" fn(reporter: *mut c_void, progress: f64, userdata: *mut c_void); - -struct ProviderState { - active_viewer: Option<(Option, usize)>, - reporter: Option<( - Option, - Option, - Option, - Option, - usize, - )>, -} - -fn state() -> &'static Mutex { - static STATE: OnceLock> = OnceLock::new(); - STATE.get_or_init(|| { - Mutex::new(ProviderState { - active_viewer: None, - reporter: None, - }) - }) -} - -/// `oakengine_plugin_set_active_viewer_provider` — register the active -/// viewer callback (NULL clears it). -#[no_mangle] -pub extern "C" fn oakengine_plugin_set_active_viewer_provider( - fn_: Option, - userdata: *mut c_void, -) -> c_int { - guard(|| { - let mut s = state().lock().unwrap_or_else(|e| e.into_inner()); - s.active_viewer = Some((fn_, userdata as usize)); - Ok(()) - }) -} - -/// `oakengine_plugin_set_progress_reporter_factory` — register the -/// progress-reporter factory callbacks (NULL clears them). -#[no_mangle] -pub extern "C" fn oakengine_plugin_set_progress_reporter_factory( - create: Option, - destroy: Option, - is_cancelled: Option, - set_progress: Option, - userdata: *mut c_void, -) -> c_int { - guard(|| { - let mut s = state().lock().unwrap_or_else(|e| e.into_inner()); - s.reporter = Some(( - create, - destroy, - is_cancelled, - set_progress, - userdata as usize, - )); - Ok(()) - }) -} - -/// `oakengine_plugin_load_plugins` — scan the plugin bundle directory -/// `path` (oakplugin_host_scan). -#[no_mangle] -pub unsafe extern "C" fn oakengine_plugin_load_plugins(path: *const c_char) -> c_int { - guard(|| unsafe { - if path.is_null() { - return Err(Error::Invalid); - } - let dirs: [*const c_char; 1] = [path]; - Error::from_module(p::oakplugin_host_scan(dirs.as_ptr(), 1)) - }) -} - -/// `oakengine_plugin_node_push_button_clicked` — not yet backed: the -/// oakplugin crate exposes no push-button API (the OFX button-param -/// trigger is C++-only). Returns `OAKENGINE_E_FAILED`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_plugin_node_push_button_clicked( - _node: *mut crate::handle::OakEngineNode, - _button_id: *const c_char, -) -> c_int { - crate::error::OAKENGINE_E_FAILED -} diff --git a/crates/oakengine.bk/src/pods.rs b/crates/oakengine.bk/src/pods.rs deleted file mode 100644 index 197d0b7d1..000000000 --- a/crates/oakengine.bk/src/pods.rs +++ /dev/null @@ -1,275 +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 . - -//! 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, - } -} diff --git a/crates/oakengine.bk/src/render.rs b/crates/oakengine.bk/src/render.rs deleted file mode 100644 index b309596a3..000000000 --- a/crates/oakengine.bk/src/render.rs +++ /dev/null @@ -1,1429 +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 . - -//! `engine/include/oakengine/{renderer,color,lut}.h` over the oakrender -//! module. -//! -//! - 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`) -//! 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 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::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, - EngineBox, OakEngineAudioBuffer, OakEngineColorProcessor, OakEngineFrame, OakEngineNode, - OakEngineRenderer, OakEngineSequence, -}; - -// --------------------------------------------------------------------------- -// 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( - aggressive: c_int, -) -> c_int { - guard(|| Error::from_module(unsafe { r::oakrender_manager_set_aggressive_gc(aggressive) })) -} - -/// `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 { - 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` — 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, -) -> c_int { - 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. -#[no_mangle] -pub unsafe extern "C" fn oakengine_render_cache_set_display_color_processor( - processor: *mut c_void, -) -> c_int { - guard(|| unsafe { - let proc = if processor.is_null() { - CHandle::null() - } else { - unbox(processor.cast::())? - }; - Error::from_module(r::oakrender_set_display_color_processor(proc)) - }) -} - -/// `oakengine_render_cache_set_multicam_node` — NULL clears. -#[no_mangle] -pub unsafe extern "C" fn oakengine_render_cache_set_multicam_node( - node: *mut OakEngineNode, -) -> c_int { - guard(|| unsafe { - let n = if node.is_null() { - CHandle::null() - } else { - unbox(node)? - }; - Error::from_module(r::oakrender_set_cacher_multicam(n)) - }) -} - -// --------------------------------------------------------------------------- -// Renderer -// --------------------------------------------------------------------------- - -/// Facade-side renderer box: the bound output node + output geometry. -struct RendererBox { - /// Unboxed output node handle (borrowed): a sequence, or any node the - /// module can evaluate (footage, generator, ...). - output_node: CHandle, - /// 1 when `output_node` is a sequence (its montage is built at render - /// time); 0 when it is a single node. - is_sequence: bool, - /// Footage spec resolved at create time (M12 P0): the filename + - /// video stream when `output_node` is a footage node. The - /// single-footage render path feeds these to the render ticket. - footage: Option, - /// Output width. - width: c_int, - /// Output height. - height: c_int, - /// Output pixel format (`PixelFormat::Format`). - pixel_format: c_int, - /// Frame-rate numerator. - frame_rate_num: c_int, - /// Frame-rate denominator. - frame_rate_den: c_int, - /// Render mode (0 offline / 1 online). - mode: c_int, - /// Last failure reason. - last_error: String, -} - -/// Resolved single-footage spec (filename + media stream). -struct FootageSpec { - /// Footage file path. - filename: String, - /// Media stream index (0 = first video stream). - stream_index: c_int, -} - -/// Borrow the renderer box (NULL → Invalid). -unsafe fn renderer(ptr: *const OakEngineRenderer) -> Result<&'static RendererBox> { - unsafe { - if ptr.is_null() { - return Err(Error::Invalid); - } - Ok(&*(ptr as *const RendererBox)) - } -} - -/// Borrow the renderer box mutably. -unsafe fn renderer_mut(ptr: *mut OakEngineRenderer) -> Result<&'static mut RendererBox> { - unsafe { - if ptr.is_null() { - return Err(Error::Invalid); - } - Ok(&mut *(ptr as *mut RendererBox)) - } -} - -/// Build an oakcommon video-params handle for the renderer's geometry. -unsafe fn make_video_params(b: &RendererBox) -> Result { - unsafe { - 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::stubs::common::oakcommon_videoparams_set_width(params, b.width); - if rc == 0 { - rc = crate::stubs::common::oakcommon_videoparams_set_height(params, b.height); - } - if rc == 0 { - rc = crate::stubs::common::oakcommon_videoparams_set_format(params, b.pixel_format); - } - if rc == 0 { - rc = crate::stubs::common::oakcommon_videoparams_set_time_base( - params, - b.frame_rate_den, - b.frame_rate_num, - ); - } - if rc != 0 { - let mut p = params; - crate::stubs::common::oakcommon_videoparams_free(&mut p); - return Err(Error::Failed("video params setup failed".into())); - } - Ok(params) - } -} - -/// Build a renderer box for `output_node` (a sequence or any node the -/// module can evaluate) with the given geometry. Returns NULL for -/// non-positive geometry/rate or a pixel format outside the oakcore enum. -unsafe fn make_renderer_box( - output_node: CHandle, - is_sequence: bool, - width: c_int, - height: c_int, - pixel_format: c_int, - frame_rate_num: c_int, - frame_rate_den: c_int, - output_colorspace: *const c_char, -) -> Result<*mut OakEngineRenderer> { - unsafe { - if width <= 0 || height <= 0 || frame_rate_num <= 0 || frame_rate_den <= 0 { - return Ok(std::ptr::null_mut()); - } - // Validate the pixel format against the oakcore enum. The - // oakcommon format_name lookup succeeds for ANY code (unknowns - // format as "Unknown (0x…)"), so only the real formats (U8..F32) - // are accepted; Invalid (-1), the Count sentinel (5) and garbage - // codes are rejected. - if pixel_format < oakcore_rs::PixelFormat::U8 as c_int - || pixel_format > oakcore_rs::PixelFormat::F32 as c_int - { - return Ok(std::ptr::null_mut()); - } - let _ = crate::handle::read_cstr(output_colorspace); // resolved by the module at render time - // M12 P0: resolve the footage spec when the bound node is a - // footage node (single-node renderers only). - let footage = if !is_sequence { - resolve_footage(output_node).ok() - } else { - None - }; - let boxed = Box::new(RendererBox { - output_node, - is_sequence, - footage, - width, - height, - pixel_format, - frame_rate_num, - frame_rate_den, - mode: 0, - last_error: String::new(), - }); - Ok(Box::into_raw(boxed) as *mut OakEngineRenderer) - } -} - -/// Resolve `node` as a footage node: `(filename, stream_index)` when it -/// carries footage, an error otherwise. -unsafe fn resolve_footage(node: CHandle) -> Result { - unsafe { - let mut buf = [0 as c_char; 4096]; - 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())); - } - let filename = crate::handle::read_cstr(buf.as_ptr()); - Ok(FootageSpec { - filename, - stream_index: 0, - }) - } -} - -/// Resolve a clip's media: `(filename, stream_index)` via the clip's -/// graph node → its upstream footage. -unsafe fn clip_media(clip: CHandle) -> Option<(String, c_int)> { - unsafe { - let node = crate::stubs::node::oaknode_block_as_node(clip); - if node.is_null() { - return None; - } - let mut footage = CHandle::null(); - 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::stubs::node::oaknode_footage_filename( - footage, - buf.as_mut_ptr(), - buf.len() as c_int, - ) < 0 - { - return None; - } - let filename = crate::handle::read_cstr(buf.as_ptr()); - Some((filename, 0)) - } -} - -/// Build the audio montage for `b` over `range` (M12 P1): every audio -/// clip overlapping the range, media times resolved from the clip -/// ranges, audio stream index 1. Returns the POD array plus the filename -/// CStrings that must outlive the render call. -unsafe fn build_audio_montage( - b: &RendererBox, - range: oakcore_rs::TimeRange, -) -> (Vec, Vec) { - use crate::stubs::node as n; - unsafe { - let mut pods = Vec::new(); - let mut names = Vec::new(); - let seq = b.output_node; - let mut audio: c_int = 0; - let _ = n::oaknode_sequence_get_track_count(seq, 1, &mut audio); - let is_clip = |block: CHandle| { - let mut kind: c_int = 0; - n::oaknode_block_get_kind(block, &mut kind) == 0 && kind == 1 - }; - for track_index in 0..audio { - let mut track = CHandle::null(); - if n::oaknode_sequence_get_track_at(seq, 1, track_index, &mut track) != 0 - || track.is_null() - { - continue; - } - let mut block_count: c_int = 0; - if n::oaknode_track_get_block_count(track, &mut block_count) != 0 { - continue; - } - for block_index in 0..block_count { - let mut block = CHandle::null(); - if n::oaknode_track_get_block_at(track, block_index, &mut block) != 0 - || block.is_null() - || !is_clip(block) - { - continue; - } - let mut in_num: c_int = 0; - let mut in_den: c_int = 0; - let mut out_num: c_int = 0; - let mut out_den: c_int = 0; - let mut mi_num: c_int = 0; - let mut mi_den: c_int = 0; - if n::oaknode_block_get_in(block, &mut in_num, &mut in_den) != 0 - || n::oaknode_block_get_out(block, &mut out_num, &mut out_den) != 0 - || n::oaknode_clip_get_media_in(block, &mut mi_num, &mut mi_den) != 0 - { - continue; - } - let in_time = oakcore_rs::Rational::new(in_num as i64, in_den as i64); - let out_time = oakcore_rs::Rational::new(out_num as i64, out_den as i64); - if out_time <= range.in_() || in_time >= range.out() { - continue; - } - let Some((filename, _)) = clip_media(block) else { - continue; - }; - let media_in = oakcore_rs::Rational::new(mi_num as i64, mi_den as i64); - let name = std::ffi::CString::new(filename).unwrap_or_default(); - let ptr = name.as_ptr(); - names.push(name); - pods.push(MontagePod { - filename: ptr, - stream_index: 1, // audio stream - in_num: in_time.numerator(), - in_den: in_time.denominator(), - out_num: out_time.numerator(), - out_den: out_time.denominator(), - media_in_num: media_in.numerator(), - media_in_den: media_in.denominator(), - gain: 1.0, - }); - } - } - (pods, names) - } -} - -/// The `OakMontageClip` POD the render module's ticket reads. -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 -/// (track index 0 is topmost → pushed last), media times resolved from -/// the clip ranges. Returns the POD array plus the filename CStrings -/// that must outlive the render call. -unsafe fn build_video_montage( - b: &RendererBox, - time: oakcore_rs::Rational, -) -> (Vec, Vec) { - use crate::stubs::node as n; - unsafe { - let mut pods = Vec::new(); - let mut names = Vec::new(); - let seq = b.output_node; - let mut video: c_int = 0; - let _ = n::oaknode_sequence_get_track_count(seq, 0, &mut video); - // Clip-blocks only (gaps skipped), track index 0 = topmost. - let is_clip = |block: CHandle| { - let mut kind: c_int = 0; - n::oaknode_block_get_kind(block, &mut kind) == 0 && kind == 1 - }; - for track_index in (0..video).rev() { - let mut track = CHandle::null(); - if n::oaknode_sequence_get_track_at(seq, 0, track_index, &mut track) != 0 - || track.is_null() - { - continue; - } - let mut block_count: c_int = 0; - if n::oaknode_track_get_block_count(track, &mut block_count) != 0 { - continue; - } - for block_index in 0..block_count { - let mut block = CHandle::null(); - if n::oaknode_track_get_block_at(track, block_index, &mut block) != 0 - || block.is_null() - || !is_clip(block) - { - continue; - } - let mut in_num: c_int = 0; - let mut in_den: c_int = 0; - let mut out_num: c_int = 0; - let mut out_den: c_int = 0; - let mut mi_num: c_int = 0; - let mut mi_den: c_int = 0; - if n::oaknode_block_get_in(block, &mut in_num, &mut in_den) != 0 - || n::oaknode_block_get_out(block, &mut out_num, &mut out_den) != 0 - || n::oaknode_clip_get_media_in(block, &mut mi_num, &mut mi_den) != 0 - { - continue; - } - let in_time = oakcore_rs::Rational::new(in_num as i64, in_den as i64); - let out_time = oakcore_rs::Rational::new(out_num as i64, out_den as i64); - if time < in_time || time >= out_time { - continue; - } - let Some((filename, stream_index)) = clip_media(block) else { - continue; - }; - let media_in = oakcore_rs::Rational::new(mi_num as i64, mi_den as i64); - let name = std::ffi::CString::new(filename).unwrap_or_default(); - let ptr = name.as_ptr(); - names.push(name); - pods.push(MontagePod { - filename: ptr, - stream_index, - in_num: in_time.numerator(), - in_den: in_time.denominator(), - out_num: out_time.numerator(), - out_den: out_time.denominator(), - media_in_num: media_in.numerator(), - media_in_den: media_in.denominator(), - gain: 1.0, - }); - } - } - (pods, names) - } -} - -/// `oakengine_renderer_create` — NULL for invalid arguments. -#[no_mangle] -pub unsafe extern "C" fn oakengine_renderer_create( - seq: *mut OakEngineSequence, - width: c_int, - height: c_int, - pixel_format: c_int, - frame_rate_num: c_int, - frame_rate_den: c_int, - output_colorspace: *const c_char, -) -> *mut OakEngineRenderer { - guard_ptr(|| unsafe { - if seq.is_null() { - return Ok(std::ptr::null_mut()); - } - let seq_handle = unbox(seq)?; - make_renderer_box( - seq_handle, - true, - width, - height, - pixel_format, - frame_rate_num, - frame_rate_den, - output_colorspace, - ) - }) -} - -/// `oakengine_renderer_create_for_node` — like `oakengine_renderer_create`, -/// but binds any node instead of a sequence: the surface for rendering a -/// single footage/generator node (the source monitor). The renderer is -/// freed with `oakengine_renderer_free` and renders with -/// `oakengine_renderer_render_frame`, exactly like the sequence renderer. -/// NULL for invalid arguments. -#[no_mangle] -pub unsafe extern "C" fn oakengine_renderer_create_for_node( - node: *mut OakEngineNode, - width: c_int, - height: c_int, - pixel_format: c_int, - frame_rate_num: c_int, - frame_rate_den: c_int, - output_colorspace: *const c_char, -) -> *mut OakEngineRenderer { - guard_ptr(|| unsafe { - if node.is_null() { - return Ok(std::ptr::null_mut()); - } - let node_handle = unbox(node)?; - make_renderer_box( - node_handle, - false, - width, - height, - pixel_format, - frame_rate_num, - frame_rate_den, - output_colorspace, - ) - }) -} - -/// `oakengine_renderer_free` — NULL no-op. -#[no_mangle] -pub unsafe extern "C" fn oakengine_renderer_free(self_: *mut OakEngineRenderer) { - guard_void(|| unsafe { - if self_.is_null() { - return; - } - drop(Box::from_raw(self_ as *mut RendererBox)); - }) -} - -/// `oakengine_renderer_set_mode` — 0/1 only. -#[no_mangle] -pub unsafe extern "C" fn oakengine_renderer_set_mode( - self_: *mut OakEngineRenderer, - mode: c_int, -) -> c_int { - guard(|| unsafe { - let b = renderer_mut(self_)?; - if mode != 0 && mode != 1 { - return Err(Error::Invalid); - } - b.mode = mode; - Ok(()) - }) -} - -/// `oakengine_renderer_last_error` (buf/size). -#[no_mangle] -pub unsafe extern "C" fn oakengine_renderer_last_error( - self_: *const OakEngineRenderer, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let b = renderer(self_)?; - Ok(crate::handle::write_string(&b.last_error, buf, buf_size)) - }) -} - -/// `oakengine_renderer_render_frame` — synchronous frame render. -/// -/// M12 P0: a sequence renderer resolves the timeline into a montage -/// (video clips covering the timestamp, composited track 0 on top); a -/// footage renderer decodes its bound footage. Both run inside the -/// render ticket's producer. -#[no_mangle] -pub unsafe extern "C" fn oakengine_renderer_render_frame( - self_: *mut OakEngineRenderer, - timestamp: i64, -) -> *mut OakEngineFrame { - guard_ptr(|| unsafe { - let b = renderer_mut(self_)?; - let video_params = make_video_params(b)?; - let time = - oakcore_rs::Rational::new(timestamp * i64::from(b.frame_rate_den), i64::from(b.frame_rate_num)); - let mut params = OakVideoTicketParams { - output_node: b.output_node, - video_params, - audio_params: std::ptr::null(), - time_num: time.numerator(), - time_den: time.denominator(), - color_manager: CHandle::null(), - mode: b.mode, - force_width: b.width, - force_height: b.height, - force_matrix: [0.0; 16], - has_force_matrix: 0, - force_format: -1, - force_channel_count: 0, - force_color_output: CHandle::null(), - force_color_transform: CHandle::null(), - cache: CHandle::null(), - footage_filename: std::ptr::null(), - footage_stream: 0, - montage: std::ptr::null(), - montage_count: 0, - }; - // M12 P0: sequence renderers resolve the montage; footage - // renderers pass the resolved spec. Both stay alive until the - // synchronous ticket call copies them. - let mut keep_alive: Vec = Vec::new(); - let mut montage: Vec = Vec::new(); - if b.is_sequence { - (montage, keep_alive) = build_video_montage(b, time); - if !montage.is_empty() { - params.montage = montage.as_ptr(); - params.montage_count = montage.len() as c_int; - } - } else if let Some(footage) = &b.footage { - let name = std::ffi::CString::new(footage.filename.clone()).unwrap_or_default(); - params.footage_filename = name.as_ptr(); - params.footage_stream = footage.stream_index; - keep_alive.push(name); - } - let ticket = r::oakrender_ticket_render_frame(¶ms, None, std::ptr::null_mut()); - let _ = (&keep_alive, &montage); - let mut vp = video_params; - 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()); - } - let wait_rc = r::oakrender_ticket_wait(ticket); - let mut frame = CHandle::null(); - let get_rc = r::oakrender_ticket_get_frame(ticket, &mut frame); - let mut t = ticket; - r::oakrender_ticket_free(&mut t); - if wait_rc != 0 || get_rc != 0 || frame.is_null() { - b.last_error = "render failed or timed out".into(); - return Ok(std::ptr::null_mut()); - } - b.last_error.clear(); - Ok(box_handle::(frame)) - }) -} - -/// `oakengine_renderer_render_audio` — synchronous audio render. The -/// oakrender crate's samples path is unimplemented, so this submits the -/// ticket and reports the failure reason. -#[no_mangle] -pub unsafe extern "C" fn oakengine_renderer_render_audio( - self_: *mut OakEngineRenderer, - start_timestamp: i64, - length_timestamp: i64, -) -> *mut OakEngineAudioBuffer { - guard_ptr(|| unsafe { - let b = renderer_mut(self_)?; - let start_num = start_timestamp * i64::from(b.frame_rate_den); - let end_num = (start_timestamp + length_timestamp) * i64::from(b.frame_rate_den); - let den = i64::from(b.frame_rate_num); - let range = oakcore_rs::TimeRange::new( - oakcore_rs::Rational::new(start_num, den), - oakcore_rs::Rational::new(end_num, den), - ); - // M12 P1: resolve the audio montage (audio-track clips covering - // the range) and the output format. - let (pods, names) = build_audio_montage(b, range); - let mut keep: Vec = names; - 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, - den, - end_num, - den, - params as *const c_void, - b.mode, - None, - std::ptr::null_mut(), - if pods.is_empty() { - std::ptr::null() - } else { - pods.as_ptr() - }, - pods.len() as c_int, - ); - 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()); - } - let wait_rc = r::oakrender_ticket_wait(ticket); - let mut samples: *mut c_void = std::ptr::null_mut(); - let get_rc = r::oakrender_ticket_get_samples(ticket, &mut samples); - let mut t = ticket; - r::oakrender_ticket_free(&mut t); - let _ = &keep; - if wait_rc != 0 || get_rc != 0 || samples.is_null() { - b.last_error = "audio render failed".into(); - return Ok(std::ptr::null_mut()); - } - let raw = samples as *const crate::pods::OakAudioSamplesOut; - let boxed = AudioSamplesBox { - data: (*raw).data.clone(), - frame_count: (*raw).frame_count as i64, - sample_rate: (*raw).sample_rate, - channel_layout: (*raw).channel_layout, - channel_count: (*raw).channel_count, - }; - r::oakrender_audio_samples_free(samples); - b.last_error.clear(); - Ok(Box::into_raw(Box::new(boxed)) as *mut OakEngineAudioBuffer) - }) -} - -/// `oakengine_renderer_cancel` — cancel the in-flight render call. -#[no_mangle] -pub unsafe extern "C" fn oakengine_renderer_cancel(self_: *mut OakEngineRenderer) { - guard_void(|| unsafe { - if let Ok(b) = renderer_mut(self_) { - let _ = b; // the crate tracks in-flight tickets internally - } - }) -} - -// --------------------------------------------------------------------------- -// OakEngineFrame accessors (wraps the module's OakCodecFrame) -// --------------------------------------------------------------------------- - -/// Borrow the frame's module handle; `None` for NULL/empty (the engine -/// contract: NULL is a no-op yielding zero results). -unsafe fn frame_handle(ptr: *const OakEngineFrame) -> Option { - unsafe { - if ptr.is_null() { - return None; - } - let h = (*ptr).handle(); - if h.is_null() { - None - } else { - Some(h) - } - } -} - -/// `oakengine_frame_width`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_frame_width(self_: *const OakEngineFrame) -> c_int { - guard_int(|| unsafe { - Ok(match frame_handle(self_) { - Some(f) => r::oakrender_codec_frame_width(f), - None => 0, - }) - }) -} - -/// `oakengine_frame_height`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_frame_height(self_: *const OakEngineFrame) -> c_int { - guard_int(|| unsafe { - Ok(match frame_handle(self_) { - Some(f) => r::oakrender_codec_frame_height(f), - None => 0, - }) - }) -} - -/// `oakengine_frame_format` — the frame's params POD format -/// (`PixelFormat::Format`). -#[no_mangle] -pub unsafe extern "C" fn oakengine_frame_format(self_: *const OakEngineFrame) -> c_int { - guard_int(|| unsafe { - let Some(f) = frame_handle(self_) else { - return Ok(0); - }; - let mut params = OakRenderVideoParams { - width: 0, - height: 0, - time_base_num: 0, - time_base_den: 0, - format: 0, - pixel_aspect_num: 0, - pixel_aspect_den: 0, - interlacing: 0, - color_range: 0, - divider: 0, - video_type: 0, - premultiplied_alpha: 0, - }; - Error::from_module(r::oakrender_codec_frame_get_params(f, &mut params))?; - Ok(params.format) - }) -} - -/// `oakengine_frame_channel_count` — **not backed** (the oakrender crate -/// exposes no frame channel count). Returns 0. -#[no_mangle] -pub unsafe extern "C" fn oakengine_frame_channel_count(self_: *const OakEngineFrame) -> c_int { - let _ = self_; - 0 -} - -/// `oakengine_frame_linesize_bytes`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_frame_linesize_bytes(self_: *const OakEngineFrame) -> c_int { - guard_int(|| unsafe { - Ok(match frame_handle(self_) { - Some(f) => r::oakrender_codec_frame_linesize_bytes(f), - None => 0, - }) - }) -} - -/// `oakengine_frame_data` — borrowed pixel data. -#[no_mangle] -pub unsafe extern "C" fn oakengine_frame_data(self_: *const OakEngineFrame) -> *const c_void { - guard_ptr(|| unsafe { - Ok(match frame_handle(self_) { - Some(f) => r::oakrender_codec_frame_const_data(f) as *mut c_void, - None => std::ptr::null_mut(), - }) - }) -} - -/// `oakengine_frame_free` — NULL no-op. -#[no_mangle] -pub unsafe extern "C" fn oakengine_frame_free(self_: *mut OakEngineFrame) { - guard_void(|| unsafe { - if self_.is_null() { - return; - } - let handle = (*self_).handle(); - let mut h = handle; - r::oakrender_codec_frame_free(&mut h); - drop(Box::from_raw(self_)); - }) -} - -// --------------------------------------------------------------------------- -// OakEngineAudioBuffer accessors (M12 P1: rendered audio samples) -// --------------------------------------------------------------------------- - -/// The buffer box: the rendered interleaved f32 samples plus format. -struct AudioSamplesBox { - /// Interleaved samples. - data: Box<[f32]>, - /// Frame count. - frame_count: i64, - /// Sample rate (Hz). - sample_rate: c_int, - /// Channel layout mask. - channel_layout: u64, - /// Channel count. - channel_count: c_int, -} - -/// `oakengine_audio_sample_rate`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_audio_sample_rate(self_: *const OakEngineAudioBuffer) -> c_int { - unsafe { - if self_.is_null() { - return 0; - } - let b = self_ as *const AudioSamplesBox; - (*b).sample_rate - } -} - -/// `oakengine_audio_channel_count`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_audio_channel_count( - self_: *const OakEngineAudioBuffer, -) -> c_int { - unsafe { - if self_.is_null() { - return 0; - } - let b = self_ as *const AudioSamplesBox; - (*b).channel_count - } -} - -/// `oakengine_audio_sample_count` — interleaved frame count. -#[no_mangle] -pub unsafe extern "C" fn oakengine_audio_sample_count(self_: *const OakEngineAudioBuffer) -> i64 { - unsafe { - if self_.is_null() { - return 0; - } - let b = self_ as *const AudioSamplesBox; - (*b).frame_count - } -} - -/// `oakengine_audio_data` — pointer to the interleaved samples (channel -/// index ignored: interleaved layout). -#[no_mangle] -pub unsafe extern "C" fn oakengine_audio_data( - self_: *const OakEngineAudioBuffer, - _channel: c_int, -) -> *const f32 { - unsafe { - if self_.is_null() { - return std::ptr::null(); - } - let b = &*(self_ as *const AudioSamplesBox); - b.data.as_ptr() - } -} - -/// `oakengine_audio_free` — release the buffer box. -#[no_mangle] -pub unsafe extern "C" fn oakengine_audio_free(self_: *mut OakEngineAudioBuffer) { - unsafe { - if self_.is_null() { - return; - } - drop(Box::from_raw(self_ as *mut AudioSamplesBox)); - } -} - -// --------------------------------------------------------------------------- -// Color management -// --------------------------------------------------------------------------- - -// Thread-local reason of the last failed color call. -thread_local! { - static LAST_COLOR_ERROR: RefCell = const { RefCell::new(String::new()) }; -} - -/// `oakengine_color_last_error` (buf/size). -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_last_error(buf: *mut c_char, buf_size: c_int) -> c_int { - crate::handle::guard_int(|| { - Ok(LAST_COLOR_ERROR.with(|e| { - // SAFETY: `buf` is the caller's buf/size buffer. - unsafe { crate::handle::write_string(&e.borrow(), buf, buf_size) } - })) - }) -} - -/// `oakengine_color_manager_from_project` — **not backed** (needs the -/// deferred oaknode project family). Returns NULL. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_manager_from_project( - _project: *mut crate::handle::OakEngineProject, -) -> *mut crate::handle::OakEngineColorManager { - std::ptr::null_mut() -} - -/// `oakengine_color_manager_get_config_filename` (buf/size). -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_manager_get_config_filename( - _mgr: *const crate::handle::OakEngineColorManager, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let rc = r::oakrender_color_manager_get_config(buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_color_manager_set_config_filename` — **not backed** (the -/// crate only reads the config). Returns OAKENGINE_E_FAILED. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_manager_set_config_filename( - _mgr: *mut crate::handle::OakEngineColorManager, - _filename: *const c_char, -) -> c_int { - crate::error::OAKENGINE_E_FAILED -} - -macro_rules! color_manager_stub { - ($($name:ident),* $(,)?) => { - $( - /// Documented stub: the oakrender crate does not implement the - /// color-manager list queries (see `deferred.rs`). - #[no_mangle] - pub unsafe extern "C" fn $name() -> c_int { - crate::error::OAKENGINE_E_FAILED - } - )* - }; -} - -color_manager_stub! { - oakengine_color_manager_colorspace_count, - oakengine_color_manager_display_count, - oakengine_color_manager_look_count, -} - -macro_rules! color_manager_stub_arg { - ($($name:ident),* $(,)?) => { - $( - /// Documented stub: the oakrender crate does not implement the - /// color-manager list queries (see `deferred.rs`). - #[no_mangle] - pub unsafe extern "C" fn $name( - _mgr: *const crate::handle::OakEngineColorManager, - _index: c_int, - _buf: *mut c_char, - _buf_size: c_int, - ) -> c_int { - crate::error::OAKENGINE_E_FAILED - } - )* - }; -} - -color_manager_stub_arg! { - oakengine_color_manager_colorspace_at, - oakengine_color_manager_display_at, -} - -/// `oakengine_color_manager_view_count` — **not backed**. -1. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_manager_view_count( - _mgr: *const crate::handle::OakEngineColorManager, - _display: *const c_char, -) -> c_int { - -1 -} - -/// `oakengine_color_manager_view_at` — **not backed**. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_manager_view_at( - _mgr: *const crate::handle::OakEngineColorManager, - _display: *const c_char, - _index: c_int, - _buf: *mut c_char, - _buf_size: c_int, -) -> c_int { - crate::error::OAKENGINE_E_FAILED -} - -/// `oakengine_color_manager_look_at` — **not backed**. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_manager_look_at( - _mgr: *const crate::handle::OakEngineColorManager, - _index: c_int, - _buf: *mut c_char, - _buf_size: c_int, -) -> c_int { - crate::error::OAKENGINE_E_FAILED -} - -/// `oakengine_color_manager_default_display` — **not backed**. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_manager_default_display( - _mgr: *const crate::handle::OakEngineColorManager, - _buf: *mut c_char, - _buf_size: c_int, -) -> c_int { - crate::error::OAKENGINE_E_FAILED -} - -/// `oakengine_color_manager_default_view` — **not backed**. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_manager_default_view( - _mgr: *const crate::handle::OakEngineColorManager, - _display: *const c_char, - _buf: *mut c_char, - _buf_size: c_int, -) -> c_int { - crate::error::OAKENGINE_E_FAILED -} - -/// `oakengine_color_manager_default_input_color_space` — **not backed**. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_manager_default_input_color_space( - _mgr: *const crate::handle::OakEngineColorManager, - _buf: *mut c_char, - _buf_size: c_int, -) -> c_int { - crate::error::OAKENGINE_E_FAILED -} - -/// `oakengine_color_manager_set_default_input_color_space` — **not -/// backed**. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_manager_set_default_input_color_space( - _mgr: *mut crate::handle::OakEngineColorManager, - _colorspace: *const c_char, -) -> c_int { - crate::error::OAKENGINE_E_FAILED -} - -/// `oakengine_color_manager_reference_color_space` — **not backed**. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_manager_reference_color_space( - _mgr: *const crate::handle::OakEngineColorManager, - _buf: *mut c_char, - _buf_size: c_int, -) -> c_int { - crate::error::OAKENGINE_E_FAILED -} - -/// `oakengine_color_manager_default_luma_coefs` — **not backed**. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_manager_default_luma_coefs( - _mgr: *const crate::handle::OakEngineColorManager, - _rgb: *mut c_double, -) -> c_int { - crate::error::OAKENGINE_E_FAILED -} - -/// `oakengine_color_manager_compliant_color_space` — **not backed**. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_manager_compliant_color_space( - _mgr: *const crate::handle::OakEngineColorManager, - _name: *const c_char, - _buf: *mut c_char, - _buf_size: c_int, -) -> c_int { - crate::error::OAKENGINE_E_FAILED -} - -/// `oakengine_color_manager_compliant_transform` — **not backed**. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_manager_compliant_transform( - _mgr: *const crate::handle::OakEngineColorManager, - _in: *const OakColorTransformPod, - _force_display: c_int, - _out_is_display: *mut c_int, - _out_output: *mut c_char, - _output_size: c_int, - _out_view: *mut c_char, - _view_size: c_int, - _out_look: *mut c_char, - _look_size: c_int, -) -> c_int { - crate::error::OAKENGINE_E_FAILED -} - -// --------------------------------------------------------------------------- -// Color config handle (not backed) -// --------------------------------------------------------------------------- - -/// `oakengine_color_config_load_default` — **not backed**. NULL. -#[no_mangle] -pub extern "C" fn oakengine_color_config_load_default() -> *mut crate::handle::OakEngineColorConfig -{ - std::ptr::null_mut() -} - -/// `oakengine_color_config_load_file` — **not backed**. NULL. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_config_load_file( - _filename: *const c_char, -) -> *mut crate::handle::OakEngineColorConfig { - std::ptr::null_mut() -} - -/// `oakengine_color_config_free` — NULL no-op. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_config_free( - _config: *mut crate::handle::OakEngineColorConfig, -) { -} - -/// `oakengine_color_config_colorspace_count` — **not backed**. 0. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_config_colorspace_count( - _config: *const crate::handle::OakEngineColorConfig, -) -> c_int { - 0 -} - -/// `oakengine_color_config_colorspace_at` — **not backed**. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_config_colorspace_at( - _config: *const crate::handle::OakEngineColorConfig, - _index: c_int, - _buf: *mut c_char, - _buf_size: c_int, -) -> c_int { - crate::error::OAKENGINE_E_FAILED -} - -// --------------------------------------------------------------------------- -// Color processor -// --------------------------------------------------------------------------- - -/// `engine/include/oakengine/color.h` — `oak_color_transform` POD mirror. -#[repr(C)] -#[derive(Clone, Copy)] -pub struct OakColorTransformPod { - /// 0: `output` is a colorspace; 1: display/view/look. - pub is_display: c_int, - /// Colorspace name, or display device when is_display. - pub output: *const c_char, - /// Display view (is_display only). - pub view: *const c_char, - /// Display look (is_display only). - pub look: *const c_char, -} - -/// `oakengine_color_processor_create` — convert the `oak_color_transform` -/// POD into an oakcommon colortransform handle and hand it to -/// `oakrender_color_processor_create_transform`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_processor_create( - mgr: *const crate::handle::OakEngineColorManager, - input: *const c_char, - dest: *const OakColorTransformPod, - direction: c_int, -) -> *mut OakEngineColorProcessor { - guard_ptr(|| unsafe { - if input.is_null() || dest.is_null() { - return Ok(std::ptr::null_mut()); - } - let empty = crate::common::empty_cstr(); - let ct = if (*dest).is_display != 0 { - crate::stubs::common::oakcommon_colortransform_init_display( - if (*dest).output.is_null() { - empty - } else { - (*dest).output - }, - if (*dest).view.is_null() { - empty - } else { - (*dest).view - }, - if (*dest).look.is_null() { - empty - } else { - (*dest).look - }, - ) - } else { - crate::stubs::common::oakcommon_colortransform_init_output( - if (*dest).output.is_null() { - empty - } else { - (*dest).output - }, - ) - }; - if ct.is_null() { - LAST_COLOR_ERROR.with(|e| *e.borrow_mut() = "invalid color transform".into()); - return Ok(std::ptr::null_mut()); - } - let mgr_handle = if mgr.is_null() { - CHandle::null() - } else { - unbox(mgr.cast::())? - }; - let proc = r::oakrender_color_processor_create_transform(mgr_handle, input, ct, direction); - let mut ct_handle = ct; - 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()); - } - LAST_COLOR_ERROR.with(|e| e.borrow_mut().clear()); - Ok(box_handle::(proc)) - }) -} - -/// `oakengine_color_processor_free` — NULL no-op. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_processor_free(proc: *mut OakEngineColorProcessor) { - guard_void(|| unsafe { - free_box(proc); - }) -} - -/// `oakengine_color_processor_is_valid` (1/0). -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_processor_is_valid( - proc: *const OakEngineColorProcessor, -) -> c_int { - guard_int(|| unsafe { - if proc.is_null() { - return Ok(0); - } - let p = unbox(proc)?; - Ok(r::oakrender_color_processor_is_valid(p)) - }) -} - -/// `oakengine_color_processor_convert_color` — single RGBA color. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_processor_convert_color( - proc: *const OakEngineColorProcessor, - in_rgba: *const c_double, - out_rgba: *mut c_double, -) -> c_int { - guard(|| unsafe { - if in_rgba.is_null() || out_rgba.is_null() { - return Err(Error::Invalid); - } - let p = unbox(proc)?; - let rc = r::oakrender_color_processor_convert( - p, - *in_rgba, - *in_rgba.add(1), - *in_rgba.add(2), - *in_rgba.add(3), - out_rgba, - out_rgba.add(1), - out_rgba.add(2), - out_rgba.add(3), - ); - Error::from_module(rc) - }) -} - -/// `oakengine_color_processor_id` — **not backed** (the crate exposes no -/// processor cache id). Returns OAKENGINE_E_FAILED. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_processor_id( - _proc: *const OakEngineColorProcessor, - _buf: *mut c_char, - _buf_size: c_int, -) -> c_int { - crate::error::OAKENGINE_E_FAILED -} - -/// `oakengine_color_transform_job_set_processor` — **not backed** (the -/// job is a C++ type). Returns OAKENGINE_E_INVALID for a NULL job. -#[no_mangle] -pub unsafe extern "C" fn oakengine_color_transform_job_set_processor( - job: *mut c_void, - _proc: *const OakEngineColorProcessor, -) -> c_int { - if job.is_null() { - crate::error::OAKENGINE_E_INVALID - } else { - crate::error::OAKENGINE_E_FAILED - } -} - -// --------------------------------------------------------------------------- -// LUT library (not backed) -// --------------------------------------------------------------------------- - -/// `oakengine_lut_directory_count` — **not backed** (the LUT directory/ -/// file library is facade-level over FileFunctions; the crate only -/// enumerates supported extensions). Returns 0. -#[no_mangle] -pub extern "C" fn oakengine_lut_directory_count() -> c_int { - 0 -} - -/// `oakengine_lut_directory_at` — **not backed**. -#[no_mangle] -pub unsafe extern "C" fn oakengine_lut_directory_at( - _index: c_int, - _buf: *mut c_char, - _buf_size: c_int, -) -> c_int { - crate::error::OAKENGINE_E_FAILED -} - -/// `oakengine_lut_file_count` — **not backed**. Returns 0. -#[no_mangle] -pub extern "C" fn oakengine_lut_file_count() -> c_int { - 0 -} - -/// `oakengine_lut_file_at` — **not backed**. -#[no_mangle] -pub unsafe extern "C" fn oakengine_lut_file_at( - _index: c_int, - _buf: *mut c_char, - _buf_size: c_int, -) -> c_int { - crate::error::OAKENGINE_E_FAILED -} - -/// `oakengine_lut_set_directories` — **not backed**. -#[no_mangle] -pub unsafe extern "C" fn oakengine_lut_set_directories( - _dirs: *const *const c_char, - _count: c_int, -) -> c_int { - crate::error::OAKENGINE_E_FAILED -} diff --git a/crates/oakengine.bk/src/storage.rs b/crates/oakengine.bk/src/storage.rs deleted file mode 100644 index 9bfa0598e..000000000 --- a/crates/oakengine.bk/src/storage.rs +++ /dev/null @@ -1,217 +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 . - -//! Live write-through to the oakstorage project library — a thin -//! forward to the module's session manager (plan M13 §2/§3; M14 R1: -//! the binding map, snapshot thread and exit flush moved into -//! [`oakstorage::writethrough`]). -//! -//! The write-through itself is subscribed directly to the oakundo -//! process-wide stack's command-success observers (see -//! [`oakstorage::writethrough`]); this module only keeps the frozen -//! `oakengine_storage_*` C ABI exports and the box/buf-size glue, and -//! re-exports the manager entry points the rest of the facade (the -//! library manager, the project family) calls. -//! -//! See [`oakstorage::writethrough`] for the binding model, the config -//! keys (`Storage/Backend`, `Storage/SqlitePath`, `Storage/PgUrl`, -//! `Storage/SnapshotIntervalSec`) and the graceful-degradation rules. - -use std::ffi::{c_char, c_int}; - -use oakstorage::backends::database::DatabaseBackend; - -use crate::handle::{guard_int, CHandle, OakEngineProject}; - -/// Bind `project` to the configured default library (no-op when the -/// backend is disabled, the project has no uuid, or it is already bound). -pub fn bind_project(project: CHandle) { - oakstorage::writethrough::bind_project(project); -} - -/// Flush `project`'s pending writes and drop its binding (closing the -/// project). -pub fn unbind_project(project: CHandle) { - oakstorage::writethrough::unbind_project(project); -} - -/// Whether `project` currently has a binding (status-bar / D5 surface). -pub fn is_bound(project: CHandle) -> bool { - oakstorage::writethrough::is_bound(project) -} - -/// The last write-through / snapshot error of `project` (empty when none -/// or not bound). -pub fn last_error(project: CHandle) -> Option { - oakstorage::writethrough::last_error(project) -} - -/// Persist every bound project after a successful undo-path operation. -/// No-op when nothing is bound (also called by the module's command -/// observer; kept here for the facade's own call sites). -pub fn note_command() { - oakstorage::writethrough::note_command(); -} - -/// Whether the write-through backend is enabled (config-driven). -pub(crate) fn storage_enabled() -> bool { - oakstorage::writethrough::storage_enabled() -} - -/// The `oakdb+…` uri of the configured library (None when it cannot be -/// resolved). Shared with the library manager exports. -pub(crate) fn library_uri() -> Option { - oakstorage::writethrough::library_uri() -} - -/// The default library file path (config-driven data directory). -pub(crate) fn default_library_path() -> String { - oakstorage::writethrough::default_library_path() -} - -/// The process-wide oakstorage backend (shared with the library manager -/// exports in [`crate::library`]). -pub(crate) fn backend() -> &'static DatabaseBackend { - oakstorage::writethrough::backend() -} - -/// The exit path: stop the snapshot thread and drain every still-bound -/// project (save + snapshot). -pub fn flush_all() { - oakstorage::writethrough::flush_all(); -} - -// --------------------------------------------------------------------------- -// Facade exports -// --------------------------------------------------------------------------- - -/// `oakengine_storage_flush` — flush every bound project (write-through + -/// snapshot) and stop the snapshot thread. The app calls this on exit -/// (the facade's shutdown path; the write-through is already per-command, -/// so this only drains the periodic snapshot backlog). -#[no_mangle] -pub extern "C" fn oakengine_storage_flush() -> c_int { - flush_all(); - crate::error::OAKENGINE_OK -} - -/// `oakengine_storage_is_bound` — 1 when `project` is bound to a library -/// session, 0 otherwise (NULL project -> 0). -#[no_mangle] -pub unsafe extern "C" fn oakengine_storage_is_bound( - project: *mut OakEngineProject, -) -> c_int { - guard_int(|| unsafe { - let h = crate::handle::unbox(project)?; - Ok(is_bound(h) as c_int) - }) -} - -/// `oakengine_storage_last_error` — the last write-through / snapshot -/// error of `project` (buf/size convention; empty when none or not -/// bound). -#[no_mangle] -pub unsafe extern "C" fn oakengine_storage_last_error( - project: *mut OakEngineProject, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let h = crate::handle::unbox(project)?; - let msg = last_error(h).unwrap_or_default(); - Ok(crate::handle::write_string(&msg, buf, buf_size)) - }) -} - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn library_uri_resolves_configured_path() { - use oakcommon::configstore::ConfigStore; - let store = ConfigStore::instance(); - let _g = crate::tests::common::STORAGE_CONFIG_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); - - // A configured path wins and yields an absolute oakdb+sqlite uri. - let dir = - std::env::temp_dir().join(format!("oakengine_storage_uri_{}", std::process::id())); - let _ = std::fs::create_dir_all(&dir); - let lib = dir.join("lib.db"); - store.set(Some("Storage"), "Backend", "sqlite"); - store.set(Some("Storage"), "SqlitePath", &lib.to_string_lossy()); - let uri = library_uri().expect("configured path resolves"); - assert!(uri.starts_with("oakdb+sqlite://"), "{uri}"); - assert!(uri.ends_with("lib.db"), "{uri}"); - assert!(storage_enabled()); - - // Leave the store in a safe state: backend off (the config store - // has no remove API, so later unguarded tests see "off" and never - // bind a project to a library). - store.set(Some("Storage"), "Backend", "off"); - assert!(!storage_enabled()); - let _ = std::fs::remove_dir_all(&dir); - } - - #[test] - fn library_uri_resolves_pg_config() { - use oakcommon::configstore::ConfigStore; - let store = ConfigStore::instance(); - let _g = crate::tests::common::STORAGE_CONFIG_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); - - // Backend = "pg" yields an oakdb+pg uri from Storage/PgUrl; a - // postgres:// scheme on the config value is stripped. - store.set(Some("Storage"), "Backend", "pg"); - store.set( - Some("Storage"), - "PgUrl", - "postgres://user:pass@host:5432/oak", - ); - assert!(storage_enabled()); - assert_eq!( - library_uri().as_deref(), - Some("oakdb+pg://user:pass@host:5432/oak") - ); - - // postgresql:// is accepted too. - store.set( - Some("Storage"), - "PgUrl", - "postgresql://u@h/db?sslmode=disable", - ); - assert_eq!( - library_uri().as_deref(), - Some("oakdb+pg://u@h/db?sslmode=disable") - ); - - // Backend = "pg" with no PgUrl = no library (graceful - // degradation, same as an absent sqlite path). - store.set(Some("Storage"), "PgUrl", ""); - assert_eq!(library_uri(), None); - - // Leave the store in a safe state. - store.set(Some("Storage"), "Backend", "off"); - assert!(!storage_enabled()); - } -} diff --git a/crates/oakengine.bk/src/stubs.rs b/crates/oakengine.bk/src/stubs.rs deleted file mode 100644 index 97614899e..000000000 --- a/crates/oakengine.bk/src/stubs.rs +++ /dev/null @@ -1,13913 +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 . - -//! Local replacements for the deleted `src/bridge/` (single-lib -//! unification). -//! -//! The engine's `src/*.rs` files used to call the module crates through -//! the deleted `src/bridge/` wrappers over their C ABIs. Those ABIs are -//! deleted; every module crate now exposes direct Rust APIs only. This -//! module carries the replacement surface: -//! -//! - **Direct-Rust shims** ([`common`], [`codec`], [`audio`], [`plugin`]): -//! the old bridge names and signatures are kept, implemented over the -//! module crates' value types (oakcommon `ConfigStore`/`VideoParams`/ -//! `ColorTransform`, oakcodec's export tables, oakaudio's singleton -//! manager/processor/sync/waveform APIs, oakplugin's OFX host). -//! - **Domain implementations** ([`node`], [`timeline`], [`render`], -//! [`task`]): the handle-based paths are implemented over the module -//! crates' direct Rust domains. The engine's handle layer boxes the -//! oaknode domain behind the upward CHandles -//! (`crate::handle::domain`): projects box -//! `Arc>`, nodes/blocks/tracks/ -//! footage/sequences/folders box `oaknode::project::NodeRef` -//! (project + `NodeId`). Undoable creators return handles boxing -//! `oakundo::undocommand::UndoCommand` values; the oaktimeline -//! commands are the crate's real `NodeRef`-based constructors and the -//! oaktask creators wrap the real `Task` types. The few genuinely -//! unwireable parts (no Rust equivalent) remain clearly marked STUBs -//! with their reasons. -//! -//! The engine's `oakengine_*` C ABI exports (upward) are untouched; only -//! the downward internals were rewired. - -pub mod common { - use std::ffi::{c_char, c_int, c_void}; - - use oakcommon::colortransform::ColorTransform; - use oakcommon::configstore::ConfigStore; - use oakcommon::ocioutils::PixelFormat; - use oakcommon::videoparams::{ColorRange, Interlacing, VideoParams, VideoType}; - use oakcommon::error::{OAKCOMMON_E_INVALID, OAKCOMMON_OK}; - - use crate::handle::{read_cstr, CHandle}; - - /// `include/common/config.h` — error handler callback (same shape as - /// the oakcommon crate's [`oakcommon::configstore::ErrorHandler`]). - pub type ConfigErrorHandler = oakcommon::configstore::ErrorHandler; - - /// Standard two-stage getter copy: copy only when the buffer is large - /// enough (never truncates); always return the required size incl. NUL. - fn copy_string(value: &str, buf: *mut c_char, buf_size: c_int) -> c_int { - let required = (value.len() + 1) as c_int; - if !buf.is_null() && buf_size >= required { - // SAFETY: the caller guarantees `buf` holds `buf_size` bytes. - unsafe { - std::ptr::copy_nonoverlapping(value.as_ptr() as *const c_char, buf, value.len()); - *buf.add(value.len()) = 0; - } - } - required - } - - /// Whether `(buf, buf_size)` is a valid two-stage getter output. - fn is_valid_string_out(buf: *mut c_char, buf_size: c_int) -> bool { - buf_size >= 0 && (buf_size == 0 || !buf.is_null()) - } - - /// group pointer -> `Option<&str>` (null -> `None`). - fn group_opt(group: *const c_char) -> Option<&'static str> { - if group.is_null() { - None - } else { - // SAFETY: the caller guarantees a valid NUL-terminated string - // that lives for the call. - unsafe { Some(std::ffi::CStr::from_ptr(group).to_str().unwrap_or("")) } - } - } - - /// Release a `CHandle` in place: call its release callback, then write - /// null back. - fn free_handle(h: *mut CHandle) { - if h.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - let handle = unsafe { &mut *h }; - if handle.ctx.is_null() { - return; - } - if let Some(release) = handle.release { - // SAFETY: `release` targets the box behind `ctx`. - unsafe { release(handle.ctx) }; - } - handle.ctx = std::ptr::null_mut(); - handle.addref = None; - handle.release = None; - handle.abi_version = 0; - } - - /// `oakcommon_config_load` — direct call into `ConfigStore::load`. - pub fn oakcommon_config_load() -> c_int { - match ConfigStore::instance().load() { - Ok(()) => OAKCOMMON_OK, - Err(e) => e.code(), - } - } - - /// `oakcommon_config_save` — direct call into `ConfigStore::save`. - pub fn oakcommon_config_save() -> c_int { - match ConfigStore::instance().save() { - Ok(()) => OAKCOMMON_OK, - Err(e) => e.code(), - } - } - - /// `oakcommon_config_reset_defaults` — direct call. - pub fn oakcommon_config_reset_defaults() -> c_int { - match ConfigStore::instance().reset_defaults() { - Ok(()) => OAKCOMMON_OK, - Err(e) => e.code(), - } - } - - /// `oakcommon_config_set` — direct call into `ConfigStore::set`. - pub unsafe fn oakcommon_config_set( - group: *const c_char, - key: *const c_char, - value: *const c_char, - ) { - // SAFETY: the caller guarantees valid NUL-terminated strings. - unsafe { - if key.is_null() || value.is_null() { - return; - } - ConfigStore::instance().set(group_opt(group), &read_cstr(key), &read_cstr(value)); - } - } - - /// `oakcommon_config_get` (two-stage string getter). - pub unsafe fn oakcommon_config_get( - group: *const c_char, - key: *const c_char, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - // SAFETY: the caller guarantees valid NUL-terminated strings. - unsafe { - if key.is_null() || !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - match ConfigStore::instance().get(group_opt(group), &read_cstr(key)) { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } - } - - /// `oakcommon_config_get_int` — direct call. - pub unsafe fn oakcommon_config_get_int( - group: *const c_char, - key: *const c_char, - fallback: c_int, - ) -> c_int { - // SAFETY: `key` is a valid NUL-terminated string or NULL. - unsafe { - if key.is_null() { - return fallback; - } - ConfigStore::instance().get_int(group_opt(group), &read_cstr(key), fallback) - } - } - - /// `oakcommon_config_get_int64` — direct call. - pub unsafe fn oakcommon_config_get_int64( - group: *const c_char, - key: *const c_char, - fallback: i64, - ) -> i64 { - // SAFETY: `key` is a valid NUL-terminated string or NULL. - unsafe { - if key.is_null() { - return fallback; - } - ConfigStore::instance().get_int64(group_opt(group), &read_cstr(key), fallback) - } - } - - /// `oakcommon_config_get_double` — direct call. - pub unsafe fn oakcommon_config_get_double( - group: *const c_char, - key: *const c_char, - fallback: f64, - ) -> f64 { - // SAFETY: `key` is a valid NUL-terminated string or NULL. - unsafe { - if key.is_null() { - return fallback; - } - ConfigStore::instance().get_double(group_opt(group), &read_cstr(key), fallback) - } - } - - /// `oakcommon_config_get_bool` — direct call. - pub unsafe fn oakcommon_config_get_bool( - group: *const c_char, - key: *const c_char, - fallback: c_int, - ) -> c_int { - // SAFETY: `key` is a valid NUL-terminated string or NULL. - unsafe { - if key.is_null() { - return fallback; - } - ConfigStore::instance().get_bool(group_opt(group), &read_cstr(key), fallback) - } - } - - /// `oakcommon_config_set_int` — direct call. - pub unsafe fn oakcommon_config_set_int(group: *const c_char, key: *const c_char, value: c_int) { - // SAFETY: `key` is a valid NUL-terminated string or NULL. - unsafe { - if key.is_null() { - return; - } - ConfigStore::instance().set_int(group_opt(group), &read_cstr(key), value); - } - } - - /// `oakcommon_config_set_int64` — direct call. - pub unsafe fn oakcommon_config_set_int64( - group: *const c_char, - key: *const c_char, - value: i64, - ) { - // SAFETY: `key` is a valid NUL-terminated string or NULL. - unsafe { - if key.is_null() { - return; - } - ConfigStore::instance().set_int64(group_opt(group), &read_cstr(key), value); - } - } - - /// `oakcommon_config_set_double` — direct call. - pub unsafe fn oakcommon_config_set_double( - group: *const c_char, - key: *const c_char, - value: f64, - ) { - // SAFETY: `key` is a valid NUL-terminated string or NULL. - unsafe { - if key.is_null() { - return; - } - ConfigStore::instance().set_double(group_opt(group), &read_cstr(key), value); - } - } - - /// `oakcommon_config_set_bool` — direct call. - pub unsafe fn oakcommon_config_set_bool(group: *const c_char, key: *const c_char, value: c_int) { - // SAFETY: `key` is a valid NUL-terminated string or NULL. - unsafe { - if key.is_null() { - return; - } - ConfigStore::instance().set_bool(group_opt(group), &read_cstr(key), value); - } - } - - /// `oakcommon_config_entry_type` — direct call. - pub unsafe fn oakcommon_config_entry_type(group: *const c_char, key: *const c_char) -> c_int { - // SAFETY: `key` is a valid NUL-terminated string or NULL. - unsafe { - if key.is_null() { - return OAKCOMMON_E_INVALID; - } - match ConfigStore::instance().entry_type(group_opt(group), &read_cstr(key)) { - Ok(oakcommon::configstore::EntryType::None) => 0, - Ok(oakcommon::configstore::EntryType::String) => 1, - Ok(oakcommon::configstore::EntryType::Int) => 2, - Ok(oakcommon::configstore::EntryType::Double) => 3, - Ok(oakcommon::configstore::EntryType::Bool) => 4, - Err(e) => e.code(), - } - } - } - - /// `oakcommon_config_set_error_handler` — direct call. - pub fn oakcommon_config_set_error_handler( - handler: ConfigErrorHandler, - userdata: *mut c_void, - ) -> c_int { - match ConfigStore::instance().set_error_handler(handler, userdata) { - Ok(()) => OAKCOMMON_OK, - Err(e) => e.code(), - } - } - - // ---- videoparams (value-typed VideoParams boxed as CHandles) ---------- - - fn interlacing_from_i32(v: c_int) -> Interlacing { - match v { - 1 => Interlacing::TopFirst, - 2 => Interlacing::BottomFirst, - _ => Interlacing::None, - } - } - - fn video_type_from_i32(v: c_int) -> VideoType { - match v { - 1 => VideoType::Still, - 2 => VideoType::ImageSequence, - _ => VideoType::Video, - } - } - - fn color_range_from_i32(v: c_int) -> ColorRange { - if v == 1 { - ColorRange::Full - } else { - ColorRange::Limited - } - } - - /// `oakcommon_videoparams_init` — boxes `VideoParams::new()`. - pub fn oakcommon_videoparams_init() -> CHandle { - make_owned(VideoParams::new()) - } - - /// `oakcommon_videoparams_init_basic` — boxes `VideoParams::new_basic`. - 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 { - make_owned(VideoParams::new_basic( - width, - height, - PixelFormat::from_code(pixel_format), - nb_channels, - pixel_aspect_num, - pixel_aspect_den, - interlacing, - divider, - )) - } - - /// `oakcommon_videoparams_init_with_time_base` — boxes - /// `VideoParams::new_with_time_base`. - #[allow(clippy::too_many_arguments)] - 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 { - make_owned(VideoParams::new_with_time_base( - width, - height, - time_base_num, - time_base_den, - PixelFormat::from_code(pixel_format), - nb_channels, - pixel_aspect_num, - pixel_aspect_den, - interlacing, - divider, - )) - } - - /// `oakcommon_videoparams_free` — release one reference. - pub fn oakcommon_videoparams_free(params: *mut CHandle) { - free_handle(params); - } - - macro_rules! vp_getter { - ($name:ident, $method:ident, $ty:ty) => { - #[doc = concat!("`oakcommon_videoparams_", stringify!($name), "` — direct call into `VideoParams::", stringify!($method), "`.")] - pub fn $name(params: CHandle, out: *mut $ty) -> c_int { - if params.is_null() || out.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get::(¶ms) } { - Some(p) => { - // SAFETY: `out` is a valid out pointer. - unsafe { - *out = p.$method() as $ty; - } - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - }; - } - - macro_rules! vp_setter { - ($name:ident, $method:ident, $ty:ty) => { - #[doc = concat!("`oakcommon_videoparams_", stringify!($name), "` — direct call into `VideoParams::", stringify!($method), "`.")] - pub fn $name(params: CHandle, value: $ty) -> c_int { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get_mut::(¶ms) } { - Some(p) => { - p.$method(value); - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - }; - } - - vp_getter!(oakcommon_videoparams_get_width, width, i32); - vp_setter!(oakcommon_videoparams_set_width, set_width, i32); - vp_getter!(oakcommon_videoparams_get_height, height, i32); - vp_setter!(oakcommon_videoparams_set_height, set_height, i32); - vp_getter!(oakcommon_videoparams_get_depth, depth, i32); - vp_setter!(oakcommon_videoparams_set_depth, set_depth, i32); - vp_getter!(oakcommon_videoparams_get_is_3d, is_3d, i32); - vp_getter!(oakcommon_videoparams_get_channel_count, channel_count, i32); - vp_setter!(oakcommon_videoparams_set_channel_count, set_channel_count, i32); - vp_getter!(oakcommon_videoparams_get_enabled, enabled, i32); - /// `oakcommon_videoparams_set_enabled` — bool setter. - pub fn oakcommon_videoparams_set_enabled(params: CHandle, value: c_int) -> c_int { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get_mut::(¶ms) } { - Some(p) => { - p.set_enabled(value != 0); - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - vp_getter!(oakcommon_videoparams_get_x, x, f32); - vp_setter!(oakcommon_videoparams_set_x, set_x, f32); - vp_getter!(oakcommon_videoparams_get_y, y, f32); - vp_setter!(oakcommon_videoparams_set_y, set_y, f32); - vp_getter!(oakcommon_videoparams_get_stream_index, stream_index, i32); - vp_setter!(oakcommon_videoparams_set_stream_index, set_stream_index, i32); - vp_getter!(oakcommon_videoparams_get_start_time, start_time, i64); - vp_setter!(oakcommon_videoparams_set_start_time, set_start_time, i64); - vp_getter!(oakcommon_videoparams_get_duration, duration, i64); - vp_setter!(oakcommon_videoparams_set_duration, set_duration, i64); - vp_getter!(oakcommon_videoparams_get_color_primaries, color_primaries, i32); - vp_setter!(oakcommon_videoparams_set_color_primaries, set_color_primaries, i32); - vp_getter!(oakcommon_videoparams_get_color_transfer, color_transfer, i32); - vp_setter!(oakcommon_videoparams_set_color_transfer, set_color_transfer, i32); - vp_getter!(oakcommon_videoparams_get_square_pixel_width, square_pixel_width, i32); - vp_getter!(oakcommon_videoparams_get_effective_width, effective_width, i32); - vp_getter!(oakcommon_videoparams_get_effective_height, effective_height, i32); - vp_getter!(oakcommon_videoparams_get_effective_depth, effective_depth, i32); - vp_getter!(oakcommon_videoparams_get_is_valid, is_valid, i32); - vp_getter!(oakcommon_videoparams_get_bytes_per_channel, bytes_per_channel, i32); - vp_getter!(oakcommon_videoparams_get_bytes_per_pixel, bytes_per_pixel, i32); - vp_getter!(oakcommon_videoparams_get_buffer_size, buffer_size, i32); - - /// `oakcommon_videoparams_get_time_base` — num/den pair getter. - pub fn oakcommon_videoparams_get_time_base( - params: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - if params.is_null() || numerator.is_null() || denominator.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get::(¶ms) } { - Some(p) => { - let (n, d) = p.time_base(); - // SAFETY: valid out pointers. - unsafe { - *numerator = n; - *denominator = d; - } - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_set_time_base` — num/den pair setter. - pub fn oakcommon_videoparams_set_time_base( - params: CHandle, - numerator: c_int, - denominator: c_int, - ) -> c_int { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get_mut::(¶ms) } { - Some(p) => { - p.set_time_base(numerator, denominator); - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_get_frame_rate` — num/den pair getter. - pub fn oakcommon_videoparams_get_frame_rate( - params: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - if params.is_null() || numerator.is_null() || denominator.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get::(¶ms) } { - Some(p) => { - let (n, d) = p.frame_rate(); - // SAFETY: valid out pointers. - unsafe { - *numerator = n; - *denominator = d; - } - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_set_frame_rate` — num/den pair setter. - pub fn oakcommon_videoparams_set_frame_rate( - params: CHandle, - numerator: c_int, - denominator: c_int, - ) -> c_int { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get_mut::(¶ms) } { - Some(p) => { - p.set_frame_rate(numerator, denominator); - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_frame_rate_as_time_base` — num/den getter. - pub fn oakcommon_videoparams_frame_rate_as_time_base( - params: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - if params.is_null() || numerator.is_null() || denominator.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get::(¶ms) } { - Some(p) => { - let (n, d) = p.frame_rate_as_time_base(); - // SAFETY: valid out pointers. - unsafe { - *numerator = n; - *denominator = d; - } - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_get_pixel_aspect_ratio` — num/den getter. - pub fn oakcommon_videoparams_get_pixel_aspect_ratio( - params: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - if params.is_null() || numerator.is_null() || denominator.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get::(¶ms) } { - Some(p) => { - let (n, d) = p.pixel_aspect_ratio(); - // SAFETY: valid out pointers. - unsafe { - *numerator = n; - *denominator = d; - } - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_set_pixel_aspect_ratio` — num/den setter. - pub fn oakcommon_videoparams_set_pixel_aspect_ratio( - params: CHandle, - numerator: c_int, - denominator: c_int, - ) -> c_int { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get_mut::(¶ms) } { - Some(p) => { - p.set_pixel_aspect_ratio(numerator, denominator); - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_get_format` — `PixelFormat::Format` code. - pub fn oakcommon_videoparams_get_format(params: CHandle, format: *mut c_int) -> c_int { - if params.is_null() || format.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get::(¶ms) } { - Some(p) => { - // SAFETY: valid out pointer. - unsafe { - *format = p.format().code(); - } - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_set_format` — `PixelFormat::Format` code. - pub fn oakcommon_videoparams_set_format(params: CHandle, format: c_int) -> c_int { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get_mut::(¶ms) } { - Some(p) => { - p.set_format(PixelFormat::from_code(format)); - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_get_interlacing` — `Interlacing` code. - pub fn oakcommon_videoparams_get_interlacing( - params: CHandle, - interlacing: *mut c_int, - ) -> c_int { - if params.is_null() || interlacing.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get::(¶ms) } { - Some(p) => { - // SAFETY: valid out pointer. - unsafe { - *interlacing = p.interlacing() as c_int; - } - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_set_interlacing` — `Interlacing` code. - pub fn oakcommon_videoparams_set_interlacing(params: CHandle, interlacing: c_int) -> c_int { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get_mut::(¶ms) } { - Some(p) => { - p.set_interlacing(interlacing_from_i32(interlacing)); - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_get_divider` — divider. - pub fn oakcommon_videoparams_get_divider(params: CHandle, divider: *mut c_int) -> c_int { - if params.is_null() || divider.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get::(¶ms) } { - Some(p) => { - // SAFETY: valid out pointer. - unsafe { - *divider = p.divider(); - } - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_set_divider` — divider. - pub fn oakcommon_videoparams_set_divider(params: CHandle, divider: c_int) -> c_int { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get_mut::(¶ms) } { - Some(p) => { - p.set_divider(divider); - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_get_video_type` — `VideoType` code. - pub fn oakcommon_videoparams_get_video_type(params: CHandle, type_: *mut c_int) -> c_int { - if params.is_null() || type_.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get::(¶ms) } { - Some(p) => { - // SAFETY: valid out pointer. - unsafe { - *type_ = p.video_type() as c_int; - } - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_set_video_type` — `VideoType` code. - pub fn oakcommon_videoparams_set_video_type(params: CHandle, type_: c_int) -> c_int { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get_mut::(¶ms) } { - Some(p) => { - p.set_video_type(video_type_from_i32(type_)); - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_get_premultiplied_alpha` — 0/1. - pub fn oakcommon_videoparams_get_premultiplied_alpha( - params: CHandle, - premultiplied: *mut c_int, - ) -> c_int { - if params.is_null() || premultiplied.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get::(¶ms) } { - Some(p) => { - // SAFETY: valid out pointer. - unsafe { - *premultiplied = p.premultiplied_alpha() as c_int; - } - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_set_premultiplied_alpha` — 0/1. - pub fn oakcommon_videoparams_set_premultiplied_alpha( - params: CHandle, - premultiplied: c_int, - ) -> c_int { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get_mut::(¶ms) } { - Some(p) => { - p.set_premultiplied_alpha(premultiplied != 0); - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_get_color_range` — `ColorRange` code. - pub fn oakcommon_videoparams_get_color_range( - params: CHandle, - color_range: *mut c_int, - ) -> c_int { - if params.is_null() || color_range.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get::(¶ms) } { - Some(p) => { - // SAFETY: valid out pointer. - unsafe { - *color_range = p.color_range() as c_int; - } - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_set_color_range` — `ColorRange` code. - pub fn oakcommon_videoparams_set_color_range(params: CHandle, color_range: c_int) -> c_int { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get_mut::(¶ms) } { - Some(p) => { - p.set_color_range(color_range_from_i32(color_range)); - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_get_colorspace` (two-stage string getter). - pub fn oakcommon_videoparams_get_colorspace( - params: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - if params.is_null() || !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get::(¶ms) } { - Some(p) => copy_string(p.colorspace(), buf, buf_size), - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_set_colorspace`. - pub unsafe fn oakcommon_videoparams_set_colorspace( - params: CHandle, - colorspace: *const c_char, - ) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - unsafe { - if params.is_null() || colorspace.is_null() { - return OAKCOMMON_E_INVALID; - } - match get_mut::(¶ms) { - Some(p) => { - p.set_colorspace(&read_cstr(colorspace)); - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - } - - /// `oakcommon_videoparams_get_time_in_timebase_units`. - pub fn oakcommon_videoparams_get_time_in_timebase_units( - params: CHandle, - time_num: c_int, - time_den: c_int, - timestamp: *mut i64, - ) -> c_int { - if params.is_null() || timestamp.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `params` must be a live `make_owned(VideoParams)` handle. - match unsafe { get::(¶ms) } { - Some(p) => { - // CPP-PARITY: the C++ returns INT64_MIN (AV_NOPTS_VALUE) when - // no time base is set; the Rust domain returns None. - // SAFETY: valid out pointer. - unsafe { - *timestamp = p.time_in_timebase_units(time_num, time_den).unwrap_or(i64::MIN); - } - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_equals` — 1 when the sets match. - pub fn oakcommon_videoparams_equals(a: CHandle, b: CHandle, out_equal: *mut c_int) -> c_int { - if a.is_null() || b.is_null() || out_equal.is_null() { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `a`/`b` must be live `make_owned(VideoParams)` handles. - match (unsafe { get::(&a) }, unsafe { get::(&b) }) { - (Some(pa), Some(pb)) => { - // SAFETY: valid out pointer. - unsafe { - *out_equal = pa.equals(pb) as c_int; - } - OAKCOMMON_OK - } - _ => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_videoparams_format_is_float` (static). - pub fn oakcommon_videoparams_format_is_float(pixel_format: c_int) -> c_int { - VideoParams::format_is_float(PixelFormat::from_code(pixel_format)) as c_int - } - - /// `oakcommon_videoparams_get_format_name` (two-stage string getter). - pub fn oakcommon_videoparams_get_format_name( - pixel_format: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - if !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - match VideoParams::format_name(PixelFormat::from_code(pixel_format)) { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// `oakcommon_videoparams_frame_rate_to_string` (two-stage). - pub fn oakcommon_videoparams_frame_rate_to_string( - numerator: c_int, - denominator: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - if !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - match VideoParams::frame_rate_to_string(numerator, denominator) { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// `oakcommon_videoparams_get_name_for_divider` (two-stage). - pub fn oakcommon_videoparams_get_name_for_divider( - divider: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - if !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - match VideoParams::name_for_divider(divider) { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// `oakcommon_videoparams_get_scaled_dimension` (static). - pub fn oakcommon_videoparams_get_scaled_dimension(dimension: c_int, divider: c_int) -> c_int { - VideoParams::get_scaled_dimension(dimension, divider) - } - - /// `oakcommon_videoparams_generate_auto_divider` (static). - pub fn oakcommon_videoparams_generate_auto_divider(width: i64, height: i64) -> c_int { - VideoParams::generate_auto_divider(width, height) - } - - /// `oakcommon_videoparams_get_divider_for_target_resolution` (static). - 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 { - VideoParams::get_divider_for_target_resolution(src_width, src_height, target_width, target_height) - } - - /// `oakcommon_videoparams_get_bytes_per_channel_for_format` (static). - pub fn oakcommon_videoparams_get_bytes_per_channel_for_format(pixel_format: c_int) -> c_int { - VideoParams::bytes_per_channel_for_format(PixelFormat::from_code(pixel_format)) - } - - /// `oakcommon_videoparams_get_bytes_per_pixel_for_format` (static). - pub fn oakcommon_videoparams_get_bytes_per_pixel_for_format( - pixel_format: c_int, - channels: c_int, - ) -> c_int { - VideoParams::bytes_per_pixel_for_format(PixelFormat::from_code(pixel_format), channels) - } - - /// `oakcommon_videoparams_calculate_buffer_size` (static). - pub fn oakcommon_videoparams_calculate_buffer_size( - width: c_int, - height: c_int, - pixel_format: c_int, - channels: c_int, - ) -> c_int { - VideoParams::calculate_buffer_size(width, height, PixelFormat::from_code(pixel_format), channels) - } - - /// `oakcommon_videoparams_static_get_bytes_per_pixel` (static). - pub fn oakcommon_videoparams_static_get_bytes_per_pixel( - pixel_format: c_int, - channels: c_int, - ) -> c_int { - VideoParams::bytes_per_pixel_for_format(PixelFormat::from_code(pixel_format), channels) - } - - // ---- colortransform (value-typed ColorTransform boxed as CHandle) ---- - - /// `oakcommon_colortransform_init_output` — boxes - /// `ColorTransform::new_output`. - pub unsafe fn oakcommon_colortransform_init_output(output: *const c_char) -> CHandle { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let output = unsafe { read_cstr(output) }; - make_owned(ColorTransform::new_output(&output)) - } - - /// `oakcommon_colortransform_init_display` — boxes - /// `ColorTransform::new_display`. - pub unsafe fn oakcommon_colortransform_init_display( - display: *const c_char, - view: *const c_char, - look: *const c_char, - ) -> CHandle { - // SAFETY: the caller guarantees valid NUL-terminated strings. - unsafe { - let display = read_cstr(display); - let view = read_cstr(view); - let look = read_cstr(look); - make_owned(ColorTransform::new_display(&display, &view, &look)) - } - } - - /// `oakcommon_colortransform_free` — release one reference. - pub fn oakcommon_colortransform_free(transform: *mut CHandle) { - free_handle(transform); - } - - /// `oakcommon_colortransform_is_display` — 1/0. - pub fn oakcommon_colortransform_is_display(transform: CHandle) -> c_int { - if transform.is_null() { - return 0; - } - // SAFETY: `transform` must be a live `make_owned(ColorTransform)` handle. - match unsafe { get::(&transform) } { - Some(t) => t.is_display() as c_int, - None => 0, - } - } - - /// `oakcommon_colortransform_get_display` (two-stage string getter). - pub fn oakcommon_colortransform_get_display( - transform: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - if transform.is_null() || !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `transform` must be a live `make_owned(ColorTransform)` handle. - match unsafe { get::(&transform) } { - Some(t) => copy_string(t.display(), buf, buf_size), - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_colortransform_get_output` (two-stage string getter). - pub fn oakcommon_colortransform_get_output( - transform: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - if transform.is_null() || !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `transform` must be a live `make_owned(ColorTransform)` handle. - match unsafe { get::(&transform) } { - Some(t) => copy_string(t.output(), buf, buf_size), - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_colortransform_get_view` (two-stage string getter). - pub fn oakcommon_colortransform_get_view( - transform: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - if transform.is_null() || !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `transform` must be a live `make_owned(ColorTransform)` handle. - match unsafe { get::(&transform) } { - Some(t) => copy_string(t.view(), buf, buf_size), - None => OAKCOMMON_E_INVALID, - } - } - - /// `oakcommon_colortransform_get_look` (two-stage string getter). - pub fn oakcommon_colortransform_get_look( - transform: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - if transform.is_null() || !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - // SAFETY: `transform` must be a live `make_owned(ColorTransform)` handle. - match unsafe { get::(&transform) } { - Some(t) => copy_string(t.look(), buf, buf_size), - None => OAKCOMMON_E_INVALID, - } - } -} - -// =========================================================================== -// codec — direct Rust shims over the oakcodec crate -// =========================================================================== - -/// oakcodec bridge replacements: direct Rust calls into the `oakcodec` -/// crate's export tables (single-lib unification). Ported from the -/// deleted `oakcodec/src/ffi/format.rs` and `ffi/encoder.rs`. -pub mod codec { - use std::ffi::{c_char, c_int}; - - use oakcodec::error::{OAKCODEC_E_INVALID, OAKCODEC_E_NOT_FOUND}; - use oakcodec::exportcodec::Codec; - use oakcodec::exportformat::Format; - - /// Standard two-stage getter copy: copy only when the buffer is large - /// enough (never truncates); always return the required size incl. NUL. - fn string_out(s: &str, buf: *mut c_char, buf_size: c_int) -> c_int { - let need = s.len() as c_int + 1; - if !buf.is_null() && buf_size > 0 { - let n = (s.len() as c_int).min(buf_size - 1); - // SAFETY: the caller guarantees `buf` holds `buf_size` bytes. - unsafe { - std::ptr::copy_nonoverlapping(s.as_ptr() as *const c_char, buf, n as usize); - *buf.add(n as usize) = 0; - } - } - need - } - - /// Read a NUL-terminated C string; `None` on NULL pointers. - unsafe fn c_str(ptr: *const c_char) -> Option { - // SAFETY: `ptr` must be a valid NUL-terminated C string, or NULL. - if ptr.is_null() { - return None; - } - unsafe { Some(std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned()) } - } - - /// `oakcodec_encoding_format_count` — `ExportFormat::k_format_count`. - pub fn oakcodec_encoding_format_count() -> c_int { - Format::Count as c_int - } - - /// `oakcodec_encoding_format_name` (two-stage). - pub unsafe fn oakcodec_encoding_format_name( - format: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match Format::from_i32(format) { - Some(f) => string_out(&Format::get_name(f), buf, buf_size), - None => OAKCODEC_E_INVALID, - } - } - - /// `oakcodec_encoding_format_extension` (two-stage). - pub unsafe fn oakcodec_encoding_format_extension( - format: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match Format::from_i32(format) { - Some(f) => string_out(&Format::get_extension(f), buf, buf_size), - None => OAKCODEC_E_INVALID, - } - } - - /// `oakcodec_encoding_format_video_codec_count`. - pub fn oakcodec_encoding_format_video_codec_count(format: c_int) -> c_int { - match Format::from_i32(format) { - Some(f) => Format::get_video_codecs(f).len() as c_int, - None => OAKCODEC_E_INVALID, - } - } - - /// `oakcodec_encoding_format_video_codec_at`. - pub fn oakcodec_encoding_format_video_codec_at(format: c_int, index: c_int) -> c_int { - let f = match Format::from_i32(format) { - Some(f) => f, - None => return OAKCODEC_E_INVALID, - }; - let list = Format::get_video_codecs(f); - if index < 0 || index as usize >= list.len() { - return OAKCODEC_E_NOT_FOUND; - } - list[index as usize] as c_int - } - - /// `oakcodec_encoding_format_audio_codec_count`. - pub fn oakcodec_encoding_format_audio_codec_count(format: c_int) -> c_int { - match Format::from_i32(format) { - Some(f) => Format::get_audio_codecs(f).len() as c_int, - None => OAKCODEC_E_INVALID, - } - } - - /// `oakcodec_encoding_format_audio_codec_at`. - pub fn oakcodec_encoding_format_audio_codec_at(format: c_int, index: c_int) -> c_int { - let f = match Format::from_i32(format) { - Some(f) => f, - None => return OAKCODEC_E_INVALID, - }; - let list = Format::get_audio_codecs(f); - if index < 0 || index as usize >= list.len() { - return OAKCODEC_E_NOT_FOUND; - } - list[index as usize] as c_int - } - - /// `oakcodec_encoding_format_subtitle_codec_count`. - pub fn oakcodec_encoding_format_subtitle_codec_count(format: c_int) -> c_int { - match Format::from_i32(format) { - Some(f) => Format::get_subtitle_codecs(f).len() as c_int, - None => OAKCODEC_E_INVALID, - } - } - - /// `oakcodec_encoding_format_subtitle_codec_at`. - pub fn oakcodec_encoding_format_subtitle_codec_at(format: c_int, index: c_int) -> c_int { - let f = match Format::from_i32(format) { - Some(f) => f, - None => return OAKCODEC_E_INVALID, - }; - let list = Format::get_subtitle_codecs(f); - if index < 0 || index as usize >= list.len() { - return OAKCODEC_E_NOT_FOUND; - } - list[index as usize] as c_int - } - - /// `oakcodec_encoding_codec_name` (two-stage). - pub unsafe fn oakcodec_encoding_codec_name( - codec: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match Codec::from_i32(codec) { - Some(c) => string_out(&Codec::get_codec_name(c), buf, buf_size), - None => OAKCODEC_E_INVALID, - } - } - - /// `oakcodec_encoding_codec_is_still_image` (0 for an invalid codec). - pub fn oakcodec_encoding_codec_is_still_image(codec: c_int) -> c_int { - match Codec::from_i32(codec) { - Some(c) => Codec::is_codec_a_still_image(c) as c_int, - None => 0, - } - } - - /// `oakcodec_encoding_codec_is_lossless` (0 for an invalid codec). - pub fn oakcodec_encoding_codec_is_lossless(codec: c_int) -> c_int { - match Codec::from_i32(codec) { - Some(c) => Codec::is_codec_lossless(c) as c_int, - None => 0, - } - } - - /// `oakcodec_encoding_pix_fmt_count`. - /// - /// # CPP-PARITY - /// The Rust table is empty (see - /// `Format::get_pixel_formats_for_codec`), so the count is 0 — the same - /// as the C++ base `Encoder` default. - pub fn oakcodec_encoding_pix_fmt_count(format: c_int, codec: c_int) -> c_int { - let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) { - (Some(f), Some(c)) => (f, c), - _ => return OAKCODEC_E_INVALID, - }; - Format::get_pixel_formats_for_codec(f, c).len() as c_int - } - - /// `oakcodec_encoding_pix_fmt_at` (two-stage). - pub unsafe 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 { - let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) { - (Some(f), Some(c)) => (f, c), - _ => return OAKCODEC_E_INVALID, - }; - let list = Format::get_pixel_formats_for_codec(f, c); - if index < 0 || index as usize >= list.len() { - return OAKCODEC_E_NOT_FOUND; - } - // Interim: the Rust list carries no names yet, so this arm is - // unreachable while the list is empty (C++ queries the FFmpeg bridge). - string_out(&list[index as usize].to_string(), buf, buf_size) - } - - /// `oakcodec_encoding_pix_fmt_index` — 0 (the preferred format) for an - /// invalid codec, a NULL/empty `pix_fmt`, or when not found. - pub unsafe fn oakcodec_encoding_pix_fmt_index(codec: c_int, pix_fmt: *const c_char) -> c_int { - // SAFETY: `pix_fmt` is a valid NUL-terminated C string or NULL. - unsafe { - if Codec::from_i32(codec).is_none() { - return 0; - } - match c_str(pix_fmt) { - Some(s) if !s.is_empty() => { - // Interim: empty table (see module doc) -> preferred index 0. - let _ = s; - 0 - } - _ => 0, - } - } - } - - /// `oakcodec_encoding_sample_format_count`. - pub fn oakcodec_encoding_sample_format_count(format: c_int, codec: c_int) -> c_int { - let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) { - (Some(f), Some(c)) => (f, c), - _ => return OAKCODEC_E_INVALID, - }; - Format::get_sample_formats_for_codec(f, c).len() as c_int - } - - /// `oakcodec_encoding_sample_format_at` — an - /// `olive::core::SampleFormat::Format` value. - pub fn oakcodec_encoding_sample_format_at( - format: c_int, - codec: c_int, - index: c_int, - ) -> c_int { - let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) { - (Some(f), Some(c)) => (f, c), - _ => return OAKCODEC_E_INVALID, - }; - let list = Format::get_sample_formats_for_codec(f, c); - if index < 0 || index as usize >= list.len() { - return OAKCODEC_E_NOT_FOUND; - } - list[index as usize] as c_int - } - - /// `oakcodec_encoding_filename_contains_digit_placeholder` (0 for NULL). - pub unsafe fn oakcodec_encoding_filename_contains_digit_placeholder( - filename: *const c_char, - ) -> c_int { - // SAFETY: `filename` is a valid NUL-terminated C string or NULL. - match unsafe { c_str(filename) } { - Some(f) => oakcodec::encoder::filename_contains_digit_placeholder(&f) as c_int, - None => 0, - } - } - - /// `oakcodec_encoding_image_sequence_digit_count` (0 for NULL). - pub unsafe fn oakcodec_encoding_image_sequence_digit_count(filename: *const c_char) -> c_int { - // SAFETY: `filename` is a valid NUL-terminated C string or NULL. - match unsafe { c_str(filename) } { - Some(f) => oakcodec::encoder::image_sequence_placeholder_digit_count(&f), - None => 0, - } - } - - /// `oakcodec_encoding_filename_remove_digit_placeholder` (two-stage). - pub unsafe fn oakcodec_encoding_filename_remove_digit_placeholder( - filename: *const c_char, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - // SAFETY: `filename` is a valid NUL-terminated C string or NULL - // (NULL is the documented E_INVALID path, matching the deleted - // module ffi). - match unsafe { c_str(filename) } { - Some(f) => { - string_out(&oakcodec::encoder::filename_remove_digit_placeholder(&f), buf, buf_size) - } - None => OAKCODEC_E_INVALID, - } - } - - /// `oakcodec_encoding_generate_matrix`: scaling matrix for a scaling - /// method, row-major 4x4 `double` into `out_matrix[16]`. - 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 { - if out_matrix.is_null() { - return OAKCODEC_E_INVALID; - } - let method = match method { - 0 => oakcodec::encodingparams::VideoScalingMethod::Fit, - 2 => oakcodec::encodingparams::VideoScalingMethod::Crop, - _ => oakcodec::encodingparams::VideoScalingMethod::Stretch, - }; - let mut m = [0.0f64; 16]; - oakcodec::encodingparams::EncodingParams::generate_matrix( - method, - src_width, - src_height, - dst_width, - dst_height, - &mut m, - ); - // SAFETY: the caller guarantees `out_matrix` holds 16 doubles. - unsafe { std::ptr::copy_nonoverlapping(m.as_ptr(), out_matrix, 16) }; - 0 - } -} - -// =========================================================================== -// node — engine-side oaknode domain implementation (single-lib unification) -// =========================================================================== -// -// The deleted oaknode C ABI is replaced by direct calls into the oaknode -// crate's Rust domain. The engine's handle layer boxes the domain behind -// the upward CHandles (see `crate::handle::domain`): -// -// - project handles box `Arc>`; -// - node/block/track/footage/sequence/folder handles box -// `oaknode::project::NodeRef` (project + NodeId). -// -// Undoable creators return handles boxing -// `oakundo::undocommand::UndoCommand` values (built from redo/undo -// closures over the real graph). Return-code convention: 0 on success, -// negative `oaknode::error::OAKNODE_*` codes on failure; two-stage string -// getters report the required length **including** the NUL. -pub mod node { - use std::collections::HashMap; - use std::ffi::{c_char, c_int, c_void}; - use std::sync::atomic::{AtomicI64, Ordering}; - use std::sync::{Arc, Mutex, OnceLock, Weak}; - - use oakcore_rs::{Rational, TimeRange}; - use oaknode::error::{ - OAKNODE_E_FAILED, OAKNODE_E_INVALID, OAKNODE_E_NOMEM, OAKNODE_E_NOT_FOUND, OAKNODE_E_STATE, - OAKNODE_OK, - }; - use oaknode::factory::Factory; - use oaknode::graph::Graph; - use oaknode::id::NodeId; - use oakundo::undocommand::{command_from_owned, OakUndoCommandVtable, UndoCommand}; - - use crate::handle::domain::{box_node, node_ref_mut, node_ref_of, project_of, ProjectArc}; - use crate::handle::CHandle; - - // ------------------------------------------------------------------- - // Shared helpers - // ------------------------------------------------------------------- - - fn lock(m: &Mutex) -> std::sync::MutexGuard<'_, T> { - m.lock().unwrap_or_else(|e| e.into_inner()) - } - - /// Standard two-stage getter copy: copy only when the buffer is large - /// enough (never truncates); always return the required size incl. NUL. - fn string_out(s: &str, buf: *mut c_char, buf_size: c_int) -> c_int { - let required = (s.len() + 1) as c_int; - if !buf.is_null() && buf_size >= required { - // SAFETY: the caller guarantees `buf` holds `buf_size` bytes. - unsafe { - std::ptr::copy_nonoverlapping(s.as_ptr() as *const c_char, buf, s.len()); - *buf.add(s.len()) = 0; - } - } - required - } - - /// Read a NUL-terminated C string; NULL -> empty. - unsafe fn cstr(ptr: *const c_char) -> String { - // SAFETY: the caller guarantees a valid NUL-terminated string. - unsafe { crate::handle::read_cstr(ptr) } - } - - /// A handle's `addref` copy (borrowed-view semantics: every facade - /// "borrowed" handle is an owned copy with refcount 1 that the caller - /// releases). - fn addref_copy(h: CHandle) -> CHandle { - if h.is_null() { - return CHandle::null(); - } - if let Some(addref) = h.addref { - // SAFETY: `h` is a live handle. - unsafe { addref(h.ctx) }; - } - h - } - - /// Node identity (the graph slot identity; 0 for invalid handles). - fn node_identity(h: CHandle) -> usize { - // SAFETY: node handles box NodeRef payloads. - match unsafe { node_ref_of(&h) } { - Some(nr) => nr.id.identity() as usize, - None => 0, - } - } - - /// Read-only project access. - fn with_project(h: CHandle, f: impl FnOnce(&oaknode::project::Project) -> R) -> Option { - // SAFETY: project handles box ProjectArc payloads. - let p = unsafe { project_of(&h) }?; - Some(f(&lock(p))) - } - - /// Mutable project access. - fn with_project_mut( - h: CHandle, - f: impl FnOnce(&mut oaknode::project::Project) -> R, - ) -> Option { - // SAFETY: project handles box ProjectArc payloads. - let p = unsafe { project_of(&h) }?; - Some(f(&mut lock(p))) - } - - /// Read-only graph access for a node handle. - fn with_node(h: CHandle, f: impl FnOnce(&Graph, NodeId) -> R) -> Option { - // SAFETY: node handles box NodeRef payloads. - let nr = unsafe { node_ref_of(&h) }?; - let p = lock(&nr.project); - Some(f(&p.graph, nr.id)) - } - - /// Mutable graph access for a node handle. - fn with_node_mut(h: CHandle, f: impl FnOnce(&mut Graph, NodeId) -> R) -> Option { - // SAFETY: node handles box NodeRef payloads. - let nr = unsafe { node_ref_of(&h) }?; - let mut p = lock(&nr.project); - Some(f(&mut p.graph, nr.id)) - } - - /// Typed behavior view of a graph node. - fn behavior_of<'a, T: 'static>(g: &'a Graph, id: NodeId) -> Option<&'a T> { - g.get(id) - .and_then(|e| e.behavior.as_any()) - .and_then(|a| a.downcast_ref::()) - } - - /// Typed mutable behavior view of a graph node. - fn behavior_of_mut<'a, T: 'static>(g: &'a mut Graph, id: NodeId) -> Option<&'a mut T> { - g.get_mut(id) - .and_then(|e| e.behavior.as_any_mut()) - .and_then(|a| a.downcast_mut::()) - } - - /// The node's type id (empty when invalid). - fn type_id_of(h: CHandle) -> String { - match with_node(h, |g, id| { - g.get(id).map(|e| e.behavior.type_id().to_string()) - }) { - Some(Some(s)) => s, - _ => String::new(), - } - } - - fn is_type(h: CHandle, id: &str) -> bool { - type_id_of(h) == id - } - - // ---- Debug alive counter (the deleted `oaknode_debug_alive_count`) ---- - - /// Live detached nodes + live projects (factory-created nodes that - /// were not adopted by a project graph, plus projects themselves). - static ALIVE: AtomicI64 = AtomicI64::new(0); - - fn alive_inc() { - ALIVE.fetch_add(1, Ordering::SeqCst); - } - - fn alive_dec() { - ALIVE.fetch_sub(1, Ordering::SeqCst); - } - - /// Weak references to every alive-counted project (for the release - /// bookkeeping below). - static ALIVE_REGISTRY: OnceLock< - Mutex>>>, - > = OnceLock::new(); - - fn registry() -> std::sync::MutexGuard< - 'static, - HashMap>>, - > { - ALIVE_REGISTRY - .get_or_init(|| Mutex::new(HashMap::new())) - .lock() - .unwrap_or_else(|e| e.into_inner()) - } - - /// Release callback for project payload boxes (refcounted shell + - /// alive-counter bookkeeping). - unsafe extern "C" fn release_project_box(ctx: *mut c_void) { - // SAFETY: `ctx` was produced by `make_project_handle` and this - // callback runs once per shell reference. - unsafe { - if ctx.is_null() { - return; - } - let rb = ctx as *mut oaknode::handle::RefBox; - if (*rb).refs.fetch_sub(1, Ordering::AcqRel) == 1 { - let value = Box::from_raw(rb).value; - let key = Arc::as_ptr(&value) as usize; - drop(value); - // Last strong reference: retire the alive-count entry. - let dead = match registry().get(&key) { - Some(weak) => weak.upgrade().is_none(), - None => false, - }; - if dead { - registry().remove(&key); - alive_dec(); - } - } - } - } - - /// Box a project payload, registering it in the alive registry. - fn make_project_handle(project: ProjectArc) -> CHandle { - let key = Arc::as_ptr(&project) as usize; - registry().entry(key).or_insert_with(|| { - alive_inc(); - Arc::downgrade(&project) - }); - // SAFETY: custom release callback for the payload box. - unsafe { oaknode::handle::make_owned_with(project, release_project_box) } - } - - /// Box a node reference behind a refcounted handle (detached-node - /// accounting rides on the payload's `owned` flag). - fn make_node_handle(project: ProjectArc, id: NodeId, owned: bool) -> CHandle { - box_node(project, id, owned) - } - - /// Box a project payload for the engine's other stub families (the - /// task module's result paths). - pub(crate) fn box_project_handle(project: ProjectArc) -> CHandle { - make_project_handle(project) - } - - /// Box a node reference for the engine's other stub families (the - /// task module's footage result paths). - pub(crate) fn box_node_handle(project: ProjectArc, id: NodeId, owned: bool) -> CHandle { - make_node_handle(project, id, owned) - } - - /// The hidden scratch project holding detached (factory-created) - /// nodes (the C++ `memory_manager_` analogue; leaked for the process). - fn scratch_project() -> ProjectArc { - static SCRATCH: OnceLock = OnceLock::new(); - SCRATCH - .get_or_init(|| oaknode::project::Project::new()) - .clone() - } - - /// Create a detached node in the scratch project (owned = true, - /// alive-counted). - fn make_detached( - (core, behavior): (oaknode::node::NodeCore, Box), - ) -> CHandle { - let project = scratch_project(); - let id = { - let mut p = lock(&project); - p.graph.add_node(core, behavior) - }; - alive_inc(); - make_node_handle(project, id, true) - } - - /// Release a handle shell reference. - fn release_handle(h: CHandle) { - if let Some(release) = h.release { - // SAFETY: the handle is live and this is the caller's - // reference. - unsafe { release(h.ctx) }; - } - } - - /// Free a node handle shell; a still-owned (detached) node is removed - /// from its scratch graph and un-counted. Adopted nodes live in their - /// project graph (the graph owns them). - fn free_node_handle(h: CHandle) { - // SAFETY: node handles box NodeRef payloads. - let owned = unsafe { node_ref_of(&h) } - .map(|nr| nr.owned.load(Ordering::SeqCst)) - .unwrap_or(false); - if owned { - let removed = unsafe { node_ref_of(&h) }.and_then(|nr| { - let mut p = lock(&nr.project); - p.graph.remove_node(nr.id).map(|_| ()) - }); - if removed.is_some() { - alive_dec(); - } - } - release_handle(h); - } - - // ---- Undo-command scaffolding -------------------------------------- - - /// Userdata payload behind a closure-backed undo command. - struct ClosureCommand { - redo: Box, - undo: Box, - } - - unsafe extern "C" fn closure_redo(ud: *mut c_void) { - // SAFETY: `ud` is the `ClosureCommand` box owned by the command. - let c = unsafe { &mut *(ud as *mut ClosureCommand) }; - (c.redo)(); - } - - unsafe extern "C" fn closure_undo(ud: *mut c_void) { - // SAFETY: see `closure_redo`. - let c = unsafe { &mut *(ud as *mut ClosureCommand) }; - (c.undo)(); - } - - unsafe extern "C" fn closure_free(ud: *mut c_void) { - if !ud.is_null() { - // SAFETY: the box is destroyed exactly once, by the command. - unsafe { drop(Box::from_raw(ud as *mut ClosureCommand)) }; - } - } - - /// Build an un-executed [`UndoCommand`] from redo/undo closures. - fn closure_command( - redo: impl FnMut() + Send + 'static, - undo: impl FnMut() + Send + 'static, - ) -> UndoCommand { - let ud = Box::into_raw(Box::new(ClosureCommand { - redo: Box::new(redo), - undo: Box::new(undo), - })); - UndoCommand::from_vtable( - OakUndoCommandVtable { - redo: Some(closure_redo), - undo: Some(closure_undo), - free_fn: Some(closure_free), - }, - ud as *mut c_void, - ) - } - - /// Box an [`UndoCommand`] value behind a handle. - fn box_command(cmd: UndoCommand) -> CHandle { - // SAFETY: `command_from_owned` owns the command value. - unsafe { command_from_owned(cmd) } - } - - /// Create a multi command handle from children. - fn box_multi(children: Vec) -> CHandle { - let mut multi = UndoCommand::multi(); - for c in children { - multi.multi_add_child(c); - } - box_command(multi) - } - - // ---- Value conversions ---------------------------------------------- - - /// Map an oaknode domain `VideoParams` into an oakcommon params - /// handle (the engine's `stubs::common` videoparams surface). - fn vp_handle(v: &oaknode::value::VideoParams) -> CHandle { - let h = crate::stubs::common::oakcommon_videoparams_init(); - if h.is_null() { - return CHandle::null(); - } - crate::stubs::common::oakcommon_videoparams_set_width(h, v.width); - crate::stubs::common::oakcommon_videoparams_set_height(h, v.height); - crate::stubs::common::oakcommon_videoparams_set_format(h, v.pixel_format); - crate::stubs::common::oakcommon_videoparams_set_channel_count(h, v.channels); - let (n, d) = ( - v.frame_rate.numerator() as c_int, - v.frame_rate.denominator() as c_int, - ); - crate::stubs::common::oakcommon_videoparams_set_frame_rate(h, n, d); - if n > 0 { - crate::stubs::common::oakcommon_videoparams_set_time_base(h, d, n); - } - h - } - - /// Read an oakcommon params handle back into the domain type. - unsafe fn vp_from_handle(h: CHandle) -> Option { - let mut width: c_int = 0; - let mut height: c_int = 0; - let mut format: c_int = 0; - let mut channels: c_int = 0; - let mut fr_num: c_int = 0; - let mut fr_den: c_int = 0; - if crate::stubs::common::oakcommon_videoparams_get_width(h, &mut width) != 0 - || crate::stubs::common::oakcommon_videoparams_get_height(h, &mut height) != 0 - || crate::stubs::common::oakcommon_videoparams_get_format(h, &mut format) != 0 - || crate::stubs::common::oakcommon_videoparams_get_channel_count(h, &mut channels) != 0 - || crate::stubs::common::oakcommon_videoparams_get_frame_rate( - h, - &mut fr_num, - &mut fr_den, - ) != 0 - { - return None; - } - Some(oaknode::value::VideoParams { - width, - height, - frame_rate: Rational::new(fr_num as i64, fr_den as i64), - pixel_format: format, - channels, - }) - } - - /// POD -> domain value (using the input's declared type). - fn pod_to_value( - declared: oaknode::value::ValueType, - v: crate::node::OakNodeValue, - ) -> Option { - v.to_node_value(declared).ok() - } - - /// Domain value -> POD (using the input's declared type). - fn value_to_pod( - declared: oaknode::value::ValueType, - v: &oaknode::value::NodeValue, - ) -> Option { - crate::node::OakNodeValue::from_node_value(declared, v).ok() - } - // ------------------------------------------------------------------- - // Project family - // ------------------------------------------------------------------- - - /// `oaknode_project_init` — fresh uninitialized project box. - pub fn oaknode_project_init() -> CHandle { - make_project_handle(oaknode::project::Project::new()) - } - - /// `oaknode_project_free` — release one project-handle reference. - pub fn oaknode_project_free(project: *mut CHandle) { - if project.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - let h = unsafe { *project }; - release_handle(h); - // SAFETY: the caller passes a valid handle pointer. - unsafe { *project = CHandle::null() }; - } - - /// `oaknode_project_initialize` — create the root folder. - pub fn oaknode_project_initialize(project: CHandle) -> c_int { - match with_project_mut(project, |p| p.initialize()) { - Some(Ok(())) => OAKNODE_OK, - Some(Err(e)) => e.code(), - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_project_clear` — remove all nodes, reset to blank. - pub fn oaknode_project_clear(project: CHandle) -> c_int { - match with_project_mut(project, |p| p.clear()) { - Some(Ok(())) => OAKNODE_OK, - Some(Err(e)) => e.code(), - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_project_root` — borrowed root folder handle (null until - /// initialized). - pub fn oaknode_project_root(project: CHandle) -> CHandle { - let p = match unsafe { project_of(&project) }.cloned() { - Some(p) => p, - None => return CHandle::null(), - }; - let root = { - let g = lock(&p); - g.root - }; - if root.valid() { - make_node_handle(p, root, false) - } else { - CHandle::null() - } - } - - /// `oaknode_project_name` (two-stage). - pub fn oaknode_project_name(project: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - match with_project(project, |p| p.name()) { - Some(name) => string_out(&name, buf, buf_size), - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_project_filename` (two-stage). - pub fn oaknode_project_filename(project: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - match with_project(project, |p| p.filename().to_string()) { - Some(name) => string_out(&name, buf, buf_size), - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_project_pretty_filename` (two-stage). - pub fn oaknode_project_pretty_filename( - project: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match with_project(project, |p| p.pretty_filename().to_string()) { - Some(name) => string_out(&name, buf, buf_size), - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_project_set_filename`. - pub fn oaknode_project_set_filename(project: CHandle, filename: *const c_char) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let filename = unsafe { cstr(filename) }; - match with_project_mut(project, |p| p.set_filename(&filename)) { - Some(()) => OAKNODE_OK, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_project_is_modified` — 1/0. - pub fn oaknode_project_is_modified(project: CHandle) -> c_int { - match with_project(project, |p| p.is_modified()) { - Some(true) => 1, - Some(false) => 0, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_project_set_modified`. - pub fn oaknode_project_set_modified(project: CHandle, modified: c_int) -> c_int { - match with_project_mut(project, |p| p.set_modified(modified != 0)) { - Some(()) => OAKNODE_OK, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_project_is_new` — 1/0. - pub fn oaknode_project_is_new(project: CHandle) -> c_int { - match with_project(project, |p| p.is_new()) { - Some(true) => 1, - Some(false) => 0, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_project_cache_path` (two-stage). - pub fn oaknode_project_cache_path( - project: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match with_project(project, |p| p.cache_path()) { - Some(path) => string_out(&path, buf, buf_size), - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_project_copy_settings`. - pub fn oaknode_project_copy_settings(dst: CHandle, src: CHandle) -> c_int { - let src_proj = unsafe { project_of(&src) }.cloned(); - match src_proj { - Some(src_proj) => { - let (settings, location, custom) = { - let s = lock(&src_proj); - ( - s.settings.clone(), - s.cache_location_setting, - s.custom_cache_path.clone(), - ) - }; - match with_project_mut(dst, |d| { - d.settings = settings; - d.cache_location_setting = location; - d.custom_cache_path = custom; - }) { - Some(()) => OAKNODE_OK, - None => OAKNODE_E_INVALID, - } - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_project_get_cache_location_setting`. - pub fn oaknode_project_get_cache_location_setting(project: CHandle) -> c_int { - match with_project(project, |p| p.cache_location_setting) { - Some(v) => v, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_project_set_cache_location_setting`. - pub fn oaknode_project_set_cache_location_setting(project: CHandle, setting: c_int) -> c_int { - match with_project_mut(project, |p| p.cache_location_setting = setting) { - Some(()) => OAKNODE_OK, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_project_get_custom_cache_path` (two-stage). - pub fn oaknode_project_get_custom_cache_path( - project: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match with_project(project, |p| p.custom_cache_path.clone()) { - Some(path) => string_out(&path, buf, buf_size), - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_project_set_custom_cache_path`. - pub fn oaknode_project_set_custom_cache_path(project: CHandle, path: *const c_char) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let path = unsafe { cstr(path) }; - match with_project_mut(project, |p| p.custom_cache_path = path) { - Some(()) => OAKNODE_OK, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_project_get_uuid` (two-stage). - pub fn oaknode_project_get_uuid(project: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - match with_project(project, |p| p.uuid.clone()) { - Some(uuid) => string_out(&uuid, buf, buf_size), - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_project_add_node` — move `node` into `project`'s graph - /// (live; the shared node box is rewritten in place so every handle - /// copy sees the new home — the C++ `write_node_ref` semantics). - pub fn oaknode_project_add_node(project: CHandle, node: CHandle) -> c_int { - let target = match unsafe { project_of(&project) }.cloned() { - Some(t) => t, - None => return OAKNODE_E_INVALID, - }; - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => nr, - None => return OAKNODE_E_INVALID, - }; - let src_project = nr.project.clone(); - let src_id = nr.id; - // Already in the target graph: nothing to do. - if Arc::ptr_eq(&src_project, &target) && { lock(&target).graph.is_valid(src_id) } { - return OAKNODE_OK; - } - let entry = { - let mut s = lock(&src_project); - s.graph.take_node(src_id) - }; - let Some(entry) = entry else { - return OAKNODE_E_NOT_FOUND; - }; - let new_id = { - let mut t = lock(&target); - t.graph.add_entry(entry, src_id) - }; - // SAFETY: node handles box NodeRef payloads; the box is shared by - // every handle copy. - if let Some(boxed) = unsafe { node_ref_mut(&node) } { - boxed.project = target; - boxed.id = new_id; - if boxed.owned.swap(false, Ordering::SeqCst) { - alive_dec(); - } - } - OAKNODE_OK - } - - /// `oaknode_project_remove_node` — live removal from the graph. - pub fn oaknode_project_remove_node(project: CHandle, node: CHandle) -> c_int { - let target = match unsafe { project_of(&project) }.cloned() { - Some(t) => t, - None => return OAKNODE_E_INVALID, - }; - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => nr, - None => return OAKNODE_E_INVALID, - }; - if !Arc::ptr_eq(&nr.project, &target) { - return OAKNODE_E_NOT_FOUND; - } - let mut t = lock(&target); - if t.graph.remove_node(nr.id).is_some() { - OAKNODE_OK - } else { - OAKNODE_E_NOT_FOUND - } - } - - /// `oaknode_project_node_count`. - pub fn oaknode_project_node_count(project: CHandle) -> c_int { - match with_project(project, |p| p.graph.node_count()) { - Some(n) => n as c_int, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_project_node_at` — borrowed node view at `index` (null - /// out of range). - pub fn oaknode_project_node_at(project: CHandle, index: c_int) -> CHandle { - if index < 0 { - return CHandle::null(); - } - let p = match unsafe { project_of(&project) }.cloned() { - Some(p) => p, - None => return CHandle::null(), - }; - let id = { - let g = lock(&p); - g.graph.node_ids().get(index as usize).copied() - }; - match id { - Some(id) => make_node_handle(p, id, false), - None => CHandle::null(), - } - } - - /// `oaknode_debug_alive_count` — live detached nodes + projects. - pub fn oaknode_debug_alive_count() -> c_int { - ALIVE.load(Ordering::SeqCst) as c_int - } - - // ------------------------------------------------------------------- - // Node family — metadata - // ------------------------------------------------------------------- - - /// `oaknode_node_get_id` (two-stage): the type id. - pub fn oaknode_node_get_id(node: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - match with_node(node, |g, id| { - g.get(id).map(|e| e.behavior.type_id().to_string()) - }) { - Some(Some(s)) => string_out(&s, buf, buf_size), - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_get_name` (two-stage). - pub fn oaknode_node_get_name(node: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - match with_node(node, |g, id| { - g.get(id).map(|e| e.behavior.name().to_string()) - }) { - Some(Some(s)) => string_out(&s, buf, buf_size), - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_get_label` (two-stage). - pub fn oaknode_node_get_label(node: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - match with_node(node, |g, id| g.get(id).map(|e| e.core.label.clone())) { - Some(Some(s)) => string_out(&s, buf, buf_size), - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_set_label`. - pub fn oaknode_node_set_label(node: CHandle, label: *const c_char) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let label = unsafe { cstr(label) }; - match with_node_mut(node, |g, id| { - g.get_mut(id).map(|e| e.core.label = label.clone()) - }) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_set_label_undoable` — closure-backed rename. - pub fn oaknode_node_set_label_undoable( - node: CHandle, - label: *const c_char, - out_command: *mut CHandle, - ) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let label = unsafe { cstr(label) }; - if out_command.is_null() { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let old = match { - let g = lock(&project); - g.graph.get(id).map(|e| e.core.label.clone()) - } { - Some(old) => old, - None => return OAKNODE_E_NOT_FOUND, - }; - let p1 = project.clone(); - let p2 = project; - let label1 = label.clone(); - let old1 = old.clone(); - let cmd = closure_command( - move || { - let mut g = lock(&p1); - if let Some(e) = g.graph.get_mut(id) { - e.core.label = label1.clone(); - } - }, - move || { - let mut g = lock(&p2); - if let Some(e) = g.graph.get_mut(id) { - e.core.label = old1.clone(); - } - }, - ); - // SAFETY: `out_command` is a valid out pointer. - unsafe { *out_command = box_command(cmd) }; - OAKNODE_OK - } - - /// `oaknode_node_get_override_color`. - pub fn oaknode_node_get_override_color(node: CHandle, out_value: *mut c_int) -> c_int { - if out_value.is_null() { - return OAKNODE_E_INVALID; - } - match with_node(node, |g, id| g.get(id).map(|e| e.core.override_color)) { - Some(Some(v)) => { - // SAFETY: valid out pointer. - unsafe { *out_value = v }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_set_override_color`. - pub fn oaknode_node_set_override_color(node: CHandle, index: c_int) -> c_int { - match with_node_mut(node, |g, id| { - g.get_mut(id).map(|e| e.core.override_color = index) - }) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_set_override_color_undoable`. - pub fn oaknode_node_set_override_color_undoable( - node: CHandle, - index: c_int, - out_command: *mut CHandle, - ) -> c_int { - if out_command.is_null() { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let old = match { - let g = lock(&project); - g.graph.get(id).map(|e| e.core.override_color) - } { - Some(old) => old, - None => return OAKNODE_E_NOT_FOUND, - }; - let p1 = project.clone(); - let p2 = project; - let cmd = closure_command( - move || { - let mut g = lock(&p1); - if let Some(e) = g.graph.get_mut(id) { - e.core.override_color = index; - } - }, - move || { - let mut g = lock(&p2); - if let Some(e) = g.graph.get_mut(id) { - e.core.override_color = old; - } - }, - ); - // SAFETY: valid out pointer. - unsafe { *out_command = box_command(cmd) }; - OAKNODE_OK - } - - /// The `enabled_in` boolean (1/0; default true). - fn node_enabled(g: &Graph, id: NodeId) -> Option { - let e = g.get(id)?; - match e.core.standard_value(oaknode::node::ENABLED_INPUT, -1) { - oaknode::value::NodeValue::Boolean(b) => Some(b), - _ => Some(true), - } - } - - /// `oaknode_node_is_enabled`. - pub fn oaknode_node_is_enabled(node: CHandle, out_value: *mut c_int) -> c_int { - if out_value.is_null() { - return OAKNODE_E_INVALID; - } - match with_node(node, |g, id| node_enabled(g, id)) { - Some(Some(enabled)) => { - // SAFETY: valid out pointer. - unsafe { *out_value = if enabled { 1 } else { 0 } }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_set_enabled`. - pub fn oaknode_node_set_enabled(node: CHandle, enabled: c_int) -> c_int { - match with_node_mut(node, |g, id| { - g.get_mut(id).map(|e| { - e.core.set_standard_value( - oaknode::node::ENABLED_INPUT, - -1, - oaknode::value::NodeValue::Boolean(enabled != 0), - ) - }) - }) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_set_enabled_undoable`. - pub fn oaknode_node_set_enabled_undoable( - node: CHandle, - enabled: c_int, - out_command: *mut CHandle, - ) -> c_int { - if out_command.is_null() { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let old = match { - let g = lock(&project); - node_enabled(&g.graph, id) - } { - Some(old) => old, - None => return OAKNODE_E_NOT_FOUND, - }; - let p1 = project.clone(); - let p2 = project; - let cmd = closure_command( - move || { - let mut g = lock(&p1); - if let Some(e) = g.graph.get_mut(id) { - e.core.set_standard_value( - oaknode::node::ENABLED_INPUT, - -1, - oaknode::value::NodeValue::Boolean(enabled != 0), - ); - } - }, - move || { - let mut g = lock(&p2); - if let Some(e) = g.graph.get_mut(id) { - e.core.set_standard_value( - oaknode::node::ENABLED_INPUT, - -1, - oaknode::value::NodeValue::Boolean(old), - ); - } - }, - ); - // SAFETY: valid out pointer. - unsafe { *out_command = box_command(cmd) }; - OAKNODE_OK - } - - /// `oaknode_node_get_effect_input` (two-stage). - pub fn oaknode_node_get_effect_input( - node: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match with_node(node, |g, id| g.get(id).map(|e| e.core.effect_input.clone())) { - Some(Some(s)) => string_out(&s, buf, buf_size), - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_get_flags`. - pub fn oaknode_node_get_flags(node: CHandle) -> u64 { - match with_node(node, |g, id| g.get(id).map(|e| e.core.flags)) { - Some(Some(f)) => f, - _ => 0, - } - } - - /// `oaknode_node_input_count`. - pub fn oaknode_node_input_count(node: CHandle, out_count: *mut c_int) -> c_int { - if out_count.is_null() { - return OAKNODE_E_INVALID; - } - match with_node(node, |g, id| g.get(id).map(|e| e.core.inputs.len())) { - Some(Some(n)) => { - // SAFETY: valid out pointer. - unsafe { *out_count = n as c_int }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_input_id` (two-stage). - pub fn oaknode_node_input_id( - node: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - if index < 0 { - return OAKNODE_E_NOT_FOUND; - } - match with_node(node, |g, id| { - g.get(id) - .and_then(|e| e.core.inputs.get(index as usize)) - .map(|i| i.id.clone()) - }) { - Some(Some(s)) => string_out(&s, buf, buf_size), - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_input_get_type` — `oak_node_value_type` code. - pub fn oaknode_node_input_get_type( - node: CHandle, - input_id: *const c_char, - out_type: *mut c_int, - ) -> c_int { - if out_type.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - match with_node(node, |g, id| { - g.get(id) - .and_then(|e| e.core.input_data_type(&input_id)) - .map(|t| t.to_oak()) - }) { - Some(Some(t)) => { - // SAFETY: valid out pointer. - unsafe { *out_type = t }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_input_is_connected`. - pub fn oaknode_node_input_is_connected( - node: CHandle, - input_id: *const c_char, - out_value: *mut c_int, - ) -> c_int { - if out_value.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - match with_node(node, |g, id| { - g.get(id) - .map(|e| e.core.has_input(&input_id)) - .unwrap_or(false) - && g.is_input_connected(id, &input_id, -1) - }) { - Some(v) => { - // SAFETY: valid out pointer. - unsafe { *out_value = if v { 1 } else { 0 } }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_input_is_connectable`. - pub fn oaknode_node_input_is_connectable( - node: CHandle, - input_id: *const c_char, - out_value: *mut c_int, - ) -> c_int { - if out_value.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - match with_node(node, |g, id| { - g.get(id) - .and_then(|e| e.core.get_input(&input_id)) - .map(|i| i.is_connectable()) - }) { - Some(Some(v)) => { - // SAFETY: valid out pointer. - unsafe { *out_value = if v { 1 } else { 0 } }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_get_input_name` (two-stage). - pub fn oaknode_node_get_input_name( - node: CHandle, - input_id: *const c_char, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - match with_node(node, |g, id| { - g.get(id).map(|e| e.behavior.input_name(&input_id).to_string()) - }) { - Some(Some(s)) => string_out(&s, buf, buf_size), - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_input_get_connected_node` — null when unconnected. - pub fn oaknode_node_input_get_connected_node( - node: CHandle, - input_id: *const c_char, - out_node: *mut CHandle, - ) -> c_int { - if out_node.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let from = { - let g = lock(&project); - if !g - .graph - .get(id) - .map(|e| e.core.has_input(&input_id)) - .unwrap_or(false) - { - return OAKNODE_E_NOT_FOUND; - } - g.graph.connected_output(id, &input_id, -1) - }; - // SAFETY: valid out pointer. - unsafe { - *out_node = match from { - Some(f) => make_node_handle(project, f, false), - None => CHandle::null(), - }; - } - OAKNODE_OK - } - /// `oaknode_node_get_input` — standard value into the POD. - pub fn oaknode_node_get_input( - node: CHandle, - input_id: *const c_char, - out: *mut crate::node::OakNodeValue, - ) -> c_int { - if out.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - match with_node(node, |g, id| { - let e = g.get(id)?; - let declared = e.core.input_data_type(&input_id)?; - let v = e.core.standard_value(&input_id, -1); - value_to_pod(declared, &v) - }) { - Some(Some(pod)) => { - // SAFETY: valid out pointer. - unsafe { *out = pod }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_set_input`. - pub fn oaknode_node_set_input( - node: CHandle, - input_id: *const c_char, - v: *const crate::node::OakNodeValue, - ) -> c_int { - if v.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string and - // a live POD. - let (input_id, v) = unsafe { (cstr(input_id), *v) }; - match with_node_mut(node, |g, id| { - let e = g.get_mut(id)?; - let declared = e.core.input_data_type(&input_id)?; - let value = pod_to_value(declared, v)?; - e.core.set_standard_value(&input_id, -1, value); - Some(()) - }) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_set_input_undoable`. - pub fn oaknode_node_set_input_undoable( - node: CHandle, - input_id: *const c_char, - v: *const crate::node::OakNodeValue, - out_command: *mut CHandle, - ) -> c_int { - if v.is_null() || out_command.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string and - // a live POD. - let (input_id, v) = unsafe { (cstr(input_id), *v) }; - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let (new_value, old_value) = { - let g = lock(&project); - let e = match g.graph.get(id) { - Some(e) => e, - None => return OAKNODE_E_NOT_FOUND, - }; - let declared = match e.core.input_data_type(&input_id) { - Some(d) => d, - None => return OAKNODE_E_NOT_FOUND, - }; - let new_value = match pod_to_value(declared, v) { - Some(nv) => nv, - None => return OAKNODE_E_INVALID, - }; - let old_value = e.core.standard_value(&input_id, -1); - (new_value, old_value) - }; - let p1 = project.clone(); - let p2 = project; - let input1 = input_id.clone(); - let input2 = input_id; - let cmd = closure_command( - move || { - let mut g = lock(&p1); - if let Some(e) = g.graph.get_mut(id) { - e.core.set_standard_value(&input1, -1, new_value.clone()); - } - }, - move || { - let mut g = lock(&p2); - if let Some(e) = g.graph.get_mut(id) { - e.core.set_standard_value(&input2, -1, old_value.clone()); - } - }, - ); - // SAFETY: valid out pointer. - unsafe { *out_command = box_command(cmd) }; - OAKNODE_OK - } - - /// `oaknode_node_get_input_string` (two-stage). - pub fn oaknode_node_get_input_string( - node: CHandle, - input_id: *const c_char, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - match with_node(node, |g, id| { - let e = g.get(id)?; - if !e.core.has_input(&input_id) { - return None; - } - let v = e.core.standard_value(&input_id, -1); - match &v { - oaknode::value::NodeValue::Text(s) => Some(s.clone()), - _ => None, - } - }) { - Some(Some(s)) => string_out(&s, buf, buf_size), - Some(None) => OAKNODE_E_FAILED, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_set_input_string`. - pub fn oaknode_node_set_input_string( - node: CHandle, - input_id: *const c_char, - value: *const c_char, - ) -> c_int { - // SAFETY: the caller guarantees valid NUL-terminated strings. - let (input_id, value) = unsafe { (cstr(input_id), cstr(value)) }; - match with_node_mut(node, |g, id| { - let e = g.get_mut(id)?; - if !e.core.has_input(&input_id) { - return None; - } - e.core - .set_standard_value(&input_id, -1, oaknode::value::NodeValue::Text(value)); - Some(()) - }) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_set_input_string_undoable`. - pub fn oaknode_node_set_input_string_undoable( - node: CHandle, - input_id: *const c_char, - value: *const c_char, - out_command: *mut CHandle, - ) -> c_int { - if out_command.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees valid NUL-terminated strings. - let (input_id, value) = unsafe { (cstr(input_id), cstr(value)) }; - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let old = { - let g = lock(&project); - let e = match g.graph.get(id) { - Some(e) => e, - None => return OAKNODE_E_NOT_FOUND, - }; - if !e.core.has_input(&input_id) { - return OAKNODE_E_NOT_FOUND; - } - let v = e.core.standard_value(&input_id, -1); - match &v { - oaknode::value::NodeValue::Text(s) => s.clone(), - _ => String::new(), - } - }; - let p1 = project.clone(); - let p2 = project; - let input1 = input_id.clone(); - let input2 = input_id; - let value1 = value.clone(); - let cmd = closure_command( - move || { - let mut g = lock(&p1); - if let Some(e) = g.graph.get_mut(id) { - e.core.set_standard_value( - &input1, - -1, - oaknode::value::NodeValue::Text(value1.clone()), - ); - } - }, - move || { - let mut g = lock(&p2); - if let Some(e) = g.graph.get_mut(id) { - e.core.set_standard_value( - &input2, - -1, - oaknode::value::NodeValue::Text(old.clone()), - ); - } - }, - ); - // SAFETY: valid out pointer. - unsafe { *out_command = box_command(cmd) }; - OAKNODE_OK - } - - /// `oaknode_node_connect` — live edge add. - pub fn oaknode_node_connect( - output_node: CHandle, - input_node: CHandle, - input_id: *const c_char, - ) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - let out_nr = match unsafe { node_ref_of(&output_node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let in_nr = match unsafe { node_ref_of(&input_node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - if !Arc::ptr_eq(&out_nr.0, &in_nr.0) { - // Edges live in the input node's graph; both endpoints must - // share it (cross-project connects are rejected). - return OAKNODE_E_NOT_FOUND; - } - let mut g = lock(&out_nr.0); - match g.graph.connect(out_nr.1, in_nr.1, &input_id, -1) { - Ok(()) => OAKNODE_OK, - Err(e) => e.code(), - } - } - - /// `oaknode_node_connect_undoable`. - pub fn oaknode_node_connect_undoable( - output_node: CHandle, - input_node: CHandle, - input_id: *const c_char, - out_command: *mut CHandle, - ) -> c_int { - if out_command.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - let out_nr = match unsafe { node_ref_of(&output_node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let in_nr = match unsafe { node_ref_of(&input_node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - if !Arc::ptr_eq(&out_nr.0, &in_nr.0) { - return OAKNODE_E_NOT_FOUND; - } - let (project, from, to) = (out_nr.0, out_nr.1, in_nr.1); - // Pre-validate like the live connect (existence, connectability, - // already-connected -> STATE). - { - let g = lock(&project); - if !g.graph.is_valid(from) || !g.graph.is_valid(to) { - return OAKNODE_E_NOT_FOUND; - } - let e = match g.graph.get(to) { - Some(e) => e, - None => return OAKNODE_E_NOT_FOUND, - }; - let input = match e.core.get_input(&input_id) { - Some(i) => i, - None => return OAKNODE_E_NOT_FOUND, - }; - if !input.is_connectable() { - return OAKNODE_E_INVALID; - } - if g.graph.connected_output(to, &input_id, -1).is_some() { - return OAKNODE_E_STATE; - } - } - let p1 = project.clone(); - let p2 = project; - let input1 = input_id.clone(); - let input2 = input_id; - let cmd = closure_command( - move || { - let mut g = lock(&p1); - let _ = g.graph.connect(from, to, &input1, -1); - }, - move || { - let mut g = lock(&p2); - g.graph.disconnect_input(to, &input2, -1); - }, - ); - // SAFETY: valid out pointer. - unsafe { *out_command = box_command(cmd) }; - OAKNODE_OK - } - - /// `oaknode_node_disconnect` — live edge remove. - pub fn oaknode_node_disconnect(input_node: CHandle, input_id: *const c_char) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - let nr = match unsafe { node_ref_of(&input_node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let mut g = lock(&nr.0); - if g - .graph - .get(nr.1) - .map(|e| e.core.has_input(&input_id)) - .unwrap_or(false) - { - g.graph.disconnect_input(nr.1, &input_id, -1); - OAKNODE_OK - } else { - OAKNODE_E_NOT_FOUND - } - } - - /// `oaknode_node_disconnect_undoable` — succeeds even when nothing is - /// connected (the redo is then a no-op, mirroring the C++ command's - /// redo swallowing). The undo re-connect is not modelled (the source - /// node id is not retained) — documented deviation. - pub fn oaknode_node_disconnect_undoable( - input_node: CHandle, - input_id: *const c_char, - out_command: *mut CHandle, - ) -> c_int { - if out_command.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - let nr = match unsafe { node_ref_of(&input_node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, to) = nr; - { - let g = lock(&project); - if !g - .graph - .get(to) - .map(|e| e.core.has_input(&input_id)) - .unwrap_or(false) - { - return OAKNODE_E_NOT_FOUND; - } - } - let p1 = project.clone(); - let input1 = input_id.clone(); - let cmd = closure_command( - move || { - let mut g = lock(&p1); - g.graph.disconnect_input(to, &input1, -1); - }, - || {}, - ); - // SAFETY: valid out pointer. - unsafe { *out_command = box_command(cmd) }; - OAKNODE_OK - } - - /// `oaknode_node_output_connection_count`. - pub fn oaknode_node_output_connection_count(node: CHandle, out_count: *mut c_int) -> c_int { - if out_count.is_null() { - return OAKNODE_E_INVALID; - } - match with_node(node, |g, id| { - if !g.is_valid(id) { - return None; - } - Some(g.output_connections(id).len()) - }) { - Some(Some(n)) => { - // SAFETY: valid out pointer. - unsafe { *out_count = n as c_int }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_output_connection_node_at`. - pub fn oaknode_node_output_connection_node_at( - node: CHandle, - index: c_int, - out_node: *mut CHandle, - ) -> c_int { - if out_node.is_null() { - return OAKNODE_E_INVALID; - } - if index < 0 { - return OAKNODE_E_NOT_FOUND; - } - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let target = { - let g = lock(&project); - if !g.graph.is_valid(id) { - return OAKNODE_E_NOT_FOUND; - } - g.graph - .output_connections(id) - .get(index as usize) - .map(|(t, _, _)| *t) - }; - // SAFETY: valid out pointer. - unsafe { - *out_node = match target { - Some(t) => make_node_handle(project, t, false), - None => CHandle::null(), - }; - } - if target.is_some() { - OAKNODE_OK - } else { - OAKNODE_E_NOT_FOUND - } - } - - /// `oaknode_node_output_connection_input_id_at` (two-stage). - pub fn oaknode_node_output_connection_input_id_at( - node: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - if index < 0 { - return OAKNODE_E_NOT_FOUND; - } - match with_node(node, |g, id| { - if !g.is_valid(id) { - return None; - } - g.output_connections(id) - .get(index as usize) - .map(|(_, input, _)| input.clone()) - }) { - Some(Some(s)) => string_out(&s, buf, buf_size), - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_output_connection_element_at`. - pub fn oaknode_node_output_connection_element_at( - node: CHandle, - index: c_int, - out_element: *mut c_int, - ) -> c_int { - if out_element.is_null() || index < 0 { - return OAKNODE_E_INVALID; - } - match with_node(node, |g, id| { - if !g.is_valid(id) { - return None; - } - g.output_connections(id) - .get(index as usize) - .map(|(_, _, e)| *e) - }) { - Some(Some(e)) => { - // SAFETY: valid out pointer. - unsafe { *out_element = e }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_link` — live link. - pub fn oaknode_node_link(a: CHandle, b: CHandle, out_linked: *mut c_int) -> c_int { - if out_linked.is_null() { - return OAKNODE_E_INVALID; - } - let a_nr = match unsafe { node_ref_of(&a) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let b_nr = match unsafe { node_ref_of(&b) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - if !Arc::ptr_eq(&a_nr.0, &b_nr.0) { - return OAKNODE_E_NOT_FOUND; - } - let mut g = lock(&a_nr.0); - let linked = g.graph.link(a_nr.1, b_nr.1); - // SAFETY: valid out pointer. - unsafe { *out_linked = if linked { 1 } else { 0 } }; - OAKNODE_OK - } - - /// `oaknode_node_unlink` — live unlink. - pub fn oaknode_node_unlink(a: CHandle, b: CHandle, out_unlinked: *mut c_int) -> c_int { - if out_unlinked.is_null() { - return OAKNODE_E_INVALID; - } - let a_nr = match unsafe { node_ref_of(&a) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let b_nr = match unsafe { node_ref_of(&b) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - if !Arc::ptr_eq(&a_nr.0, &b_nr.0) { - return OAKNODE_E_NOT_FOUND; - } - let mut g = lock(&a_nr.0); - let unlinked = g.graph.unlink(a_nr.1, b_nr.1); - // SAFETY: valid out pointer. - unsafe { *out_unlinked = if unlinked { 1 } else { 0 } }; - OAKNODE_OK - } - - /// `oaknode_node_link_undoable`. - pub fn oaknode_node_link_undoable( - a: CHandle, - b: CHandle, - link: c_int, - out_command: *mut CHandle, - ) -> c_int { - if out_command.is_null() { - return OAKNODE_E_INVALID; - } - let a_nr = match unsafe { node_ref_of(&a) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let b_nr = match unsafe { node_ref_of(&b) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - if !Arc::ptr_eq(&a_nr.0, &b_nr.0) { - return OAKNODE_E_NOT_FOUND; - } - let (project, id_a, id_b) = (a_nr.0, a_nr.1, b_nr.1); - let p1 = project.clone(); - let p2 = project; - let cmd = closure_command( - move || { - let mut g = lock(&p1); - if link != 0 { - g.graph.link(id_a, id_b); - } else { - g.graph.unlink(id_a, id_b); - } - }, - move || { - let mut g = lock(&p2); - if link != 0 { - g.graph.unlink(id_a, id_b); - } else { - g.graph.link(id_a, id_b); - } - }, - ); - // SAFETY: valid out pointer. - unsafe { *out_command = box_command(cmd) }; - OAKNODE_OK - } - - /// `oaknode_node_are_linked`. - pub fn oaknode_node_are_linked(a: CHandle, b: CHandle, out_value: *mut c_int) -> c_int { - if out_value.is_null() { - return OAKNODE_E_INVALID; - } - let a_nr = match unsafe { node_ref_of(&a) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let b_nr = match unsafe { node_ref_of(&b) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let value = if Arc::ptr_eq(&a_nr.0, &b_nr.0) { - let g = lock(&a_nr.0); - g.graph.are_linked(a_nr.1, b_nr.1) - } else { - false - }; - // SAFETY: valid out pointer. - unsafe { *out_value = if value { 1 } else { 0 } }; - OAKNODE_OK - } - - /// `oaknode_node_link_count`. - pub fn oaknode_node_link_count(node: CHandle, out_count: *mut c_int) -> c_int { - if out_count.is_null() { - return OAKNODE_E_INVALID; - } - match with_node(node, |g, id| { - if !g.is_valid(id) { - return None; - } - Some(g.links_of(id).len()) - }) { - Some(Some(n)) => { - // SAFETY: valid out pointer. - unsafe { *out_count = n as c_int }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_link_at`. - pub fn oaknode_node_link_at(node: CHandle, index: c_int, out_node: *mut CHandle) -> c_int { - if out_node.is_null() || index < 0 { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let target = { - let g = lock(&project); - if !g.graph.is_valid(id) { - return OAKNODE_E_NOT_FOUND; - } - g.graph.links_of(id).get(index as usize).copied() - }; - // SAFETY: valid out pointer. - unsafe { - *out_node = match target { - Some(t) => make_node_handle(project, t, false), - None => CHandle::null(), - }; - } - if target.is_some() { - OAKNODE_OK - } else { - OAKNODE_E_NOT_FOUND - } - } - - /// `oaknode_node_context_count`. - pub fn oaknode_node_context_count(node: CHandle, out_count: *mut c_int) -> c_int { - if out_count.is_null() { - return OAKNODE_E_INVALID; - } - match with_node(node, |g, id| { - g.get(id).map(|e| e.core.context_positions.len()) - }) { - Some(Some(n)) => { - // SAFETY: valid out pointer. - unsafe { *out_count = n as c_int }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_context_node_at`. - pub fn oaknode_node_context_node_at( - node: CHandle, - index: c_int, - out_node: *mut CHandle, - ) -> c_int { - if out_node.is_null() || index < 0 { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let target = { - let g = lock(&project); - let e = match g.graph.get(id) { - Some(e) => e, - None => return OAKNODE_E_NOT_FOUND, - }; - e.core.context_positions.get(index as usize).map(|(c, _, _)| *c) - }; - // SAFETY: valid out pointer. - unsafe { - *out_node = match target { - Some(t) => make_node_handle(project, t, false), - None => CHandle::null(), - }; - } - if target.is_some() { - OAKNODE_OK - } else { - OAKNODE_E_NOT_FOUND - } - } - - /// `oaknode_node_get_context_position`. - pub fn oaknode_node_get_context_position( - node: CHandle, - context: CHandle, - out_x: *mut f64, - out_y: *mut f64, - out_expanded: *mut c_int, - ) -> c_int { - if out_x.is_null() || out_y.is_null() || out_expanded.is_null() { - return OAKNODE_E_INVALID; - } - let ctx_id = match unsafe { node_ref_of(&context) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - match with_node(node, |g, id| { - g.get(id).and_then(|e| { - e.core - .context_positions - .iter() - .find(|(c, _, _)| *c == ctx_id) - .map(|(_, pos, expanded)| (*pos, *expanded)) - }) - }) { - Some(Some(((x, y), expanded))) => { - // SAFETY: valid out pointers. - unsafe { - *out_x = x; - *out_y = y; - *out_expanded = if expanded { 1 } else { 0 }; - } - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_set_context_position`. - pub fn oaknode_node_set_context_position( - node: CHandle, - context: CHandle, - x: f64, - y: f64, - expanded: c_int, - ) -> c_int { - let ctx_id = match unsafe { node_ref_of(&context) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - match with_node_mut(node, |g, id| { - g.get_mut(id) - .map(|e| e.core.set_context_position(ctx_id, x, y, expanded != 0)) - }) { - Some(Some(_)) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_set_context_position_undoable`. - pub fn oaknode_node_set_context_position_undoable( - node: CHandle, - context: CHandle, - x: f64, - y: f64, - expanded: c_int, - out_command: *mut CHandle, - ) -> c_int { - if out_command.is_null() { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let ctx_id = match unsafe { node_ref_of(&context) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let old = { - let g = lock(&project); - let e = match g.graph.get(id) { - Some(e) => e, - None => return OAKNODE_E_NOT_FOUND, - }; - e.core - .context_positions - .iter() - .find(|(c, _, _)| *c == ctx_id) - .map(|(_, pos, expanded)| (*pos, *expanded)) - }; - let p1 = project.clone(); - let p2 = project; - let cmd = closure_command( - move || { - let mut g = lock(&p1); - if let Some(e) = g.graph.get_mut(id) { - e.core.set_context_position(ctx_id, x, y, expanded != 0); - } - }, - move || { - let mut g = lock(&p2); - if let Some(e) = g.graph.get_mut(id) { - match old { - Some(((ox, oy), oe)) => { - e.core.set_context_position(ctx_id, ox, oy, oe); - } - None => { - e.core.remove_from_context(ctx_id); - } - } - } - }, - ); - // SAFETY: valid out pointer. - unsafe { *out_command = box_command(cmd) }; - OAKNODE_OK - } - - /// `oaknode_node_remove_from_context`. - pub fn oaknode_node_remove_from_context(node: CHandle, context: CHandle) -> c_int { - let ctx_id = match unsafe { node_ref_of(&context) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - match with_node_mut(node, |g, id| { - g.get_mut(id).map(|e| e.core.remove_from_context(ctx_id)) - }) { - Some(Some(_)) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - /// `oaknode_node_create_copy` — standalone duplicate (owned). - pub fn oaknode_node_create_copy(node: CHandle) -> CHandle { - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return CHandle::null(), - }; - let (project, id) = nr; - let (core, behavior) = { - let g = lock(&project); - let e = match g.graph.get(id) { - Some(e) => e, - None => return CHandle::null(), - }; - let core = e.core.clone(); - let behavior = match e.behavior.duplicate(&core) { - Some(b) => b, - None => return CHandle::null(), - }; - (core, behavior) - }; - make_detached((core, behavior)) - } - - /// `oaknode_node_copy_in_graph` — duplicate into the source project - /// plus an undo command removing the copy. - pub fn oaknode_node_copy_in_graph(node: CHandle, out_command: *mut CHandle) -> CHandle { - if out_command.is_null() { - return CHandle::null(); - } - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return CHandle::null(), - }; - let (project, id) = nr; - let (core, behavior) = { - let g = lock(&project); - let e = match g.graph.get(id) { - Some(e) => e, - None => return CHandle::null(), - }; - let core = e.core.clone(); - let behavior = match e.behavior.duplicate(&core) { - Some(b) => b, - None => return CHandle::null(), - }; - (core, behavior) - }; - let new_id = { - let mut g = lock(&project); - g.graph.add_node(core, behavior) - }; - let p1 = project.clone(); - let p2 = project.clone(); - let cmd = closure_command( - move || { - // The copy already lives in the graph (created above). - let _ = &mut lock(&p1).graph; - }, - move || { - let mut g = lock(&p2); - g.graph.remove_node(new_id); - }, - ); - // SAFETY: valid out pointer. - unsafe { *out_command = box_command(cmd) }; - make_node_handle(project, new_id, false) - } - - /// `oaknode_node_get_project` — borrowed project handle. - pub fn oaknode_node_get_project(node: CHandle, out: *mut CHandle) -> c_int { - if out.is_null() { - return OAKNODE_E_INVALID; - } - let project = unsafe { node_ref_of(&node) }.map(|nr| nr.project.clone()); - match project { - Some(p) => { - // SAFETY: valid out pointer. - unsafe { *out = make_project_handle(p) }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_input_array_insert`. - pub fn oaknode_node_input_array_insert( - node: CHandle, - input_id: *const c_char, - index: c_int, - ) -> c_int { - if index < 0 { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - match with_node_mut(node, |g, id| { - let e = g.get_mut(id)?; - if !e.core.has_input(&input_id) { - return None; - } - e.core.input_array_insert(&input_id, index as usize); - Some(()) - }) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_input_array_remove`. - pub fn oaknode_node_input_array_remove( - node: CHandle, - input_id: *const c_char, - index: c_int, - ) -> c_int { - if index < 0 { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - match with_node_mut(node, |g, id| { - let e = g.get_mut(id)?; - if !e.core.has_input(&input_id) { - return None; - } - e.core.input_array_remove(&input_id, index as usize); - Some(()) - }) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_connect_element`. - pub fn oaknode_node_connect_element( - output_node: CHandle, - input_node: CHandle, - input_id: *const c_char, - element: c_int, - ) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - let out_nr = match unsafe { node_ref_of(&output_node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let in_nr = match unsafe { node_ref_of(&input_node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - if !Arc::ptr_eq(&out_nr.0, &in_nr.0) { - return OAKNODE_E_NOT_FOUND; - } - let mut g = lock(&out_nr.0); - match g.graph.connect(out_nr.1, in_nr.1, &input_id, element) { - Ok(()) => OAKNODE_OK, - Err(e) => e.code(), - } - } - - /// `oaknode_node_disconnect_element`. - pub fn oaknode_node_disconnect_element( - input_node: CHandle, - input_id: *const c_char, - element: c_int, - ) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - let nr = match unsafe { node_ref_of(&input_node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let mut g = lock(&nr.0); - if g - .graph - .get(nr.1) - .map(|e| e.core.has_input(&input_id)) - .unwrap_or(false) - { - g.graph.disconnect_input(nr.1, &input_id, element); - OAKNODE_OK - } else { - OAKNODE_E_NOT_FOUND - } - } - - /// `oaknode_command_create_add_node` — move `node` into `graph`'s - /// project (undo moves it back). - pub fn oaknode_command_create_add_node(graph: CHandle, node: CHandle) -> CHandle { - let target = match unsafe { project_of(&graph) }.cloned() { - Some(t) => t, - None => return CHandle::null(), - }; - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => nr, - None => return CHandle::null(), - }; - let src_project = nr.project.clone(); - let src_id = nr.id; - if Arc::ptr_eq(&src_project, &target) && { lock(&target).graph.is_valid(src_id) } { - return CHandle::null(); - } - let entry = { - let mut s = lock(&src_project); - match s.graph.take_node(src_id) { - Some(e) => e, - None => return CHandle::null(), - } - }; - let mut entry = Some(entry); - let target_redo = target.clone(); - let src_undo = src_project; - let target_undo = target; - let cmd = closure_command( - move || { - let e = match entry.take() { - Some(e) => e, - None => return, - }; - let new_id = { - let mut t = lock(&target_redo); - t.graph.add_entry(e, src_id) - }; - // SAFETY: the shared node box is rewritten in place. - if let Some(boxed) = unsafe { node_ref_mut(&node) } { - boxed.project = target_redo.clone(); - boxed.id = new_id; - if boxed.owned.swap(false, Ordering::SeqCst) { - alive_dec(); - } - } - }, - move || { - let current_id = unsafe { node_ref_of(&node) } - .map(|n| n.id) - .unwrap_or(src_id); - let e = { - let mut t = lock(&target_undo); - match t.graph.take_node(current_id) { - Some(e) => e, - None => return, - } - }; - { - let mut s = lock(&src_undo); - s.graph.add_entry(e, src_id); - } - // SAFETY: the shared node box is rewritten back. - if let Some(boxed) = unsafe { node_ref_mut(&node) } { - boxed.project = src_undo.clone(); - boxed.id = src_id; - if !boxed.owned.swap(true, Ordering::SeqCst) { - alive_inc(); - } - } - }, - ); - box_command(cmd) - } - - /// `oaknode_command_create_set_position_recursive` — best-effort: set - /// the node's own context position (undo restores the old value). - pub fn oaknode_command_create_set_position_recursive( - node: CHandle, - context: CHandle, - x: f64, - y: f64, - ) -> CHandle { - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return CHandle::null(), - }; - let ctx_id = match unsafe { node_ref_of(&context) } { - Some(nr) => nr.id, - None => return CHandle::null(), - }; - let (project, id) = nr; - let old = { - let g = lock(&project); - match g.graph.get(id) { - Some(e) => e - .core - .context_positions - .iter() - .find(|(c, _, _)| *c == ctx_id) - .map(|(_, pos, expanded)| (*pos, *expanded)), - None => return CHandle::null(), - } - }; - let p1 = project.clone(); - let p2 = project; - let cmd = closure_command( - move || { - let mut g = lock(&p1); - if let Some(e) = g.graph.get_mut(id) { - e.core.set_context_position(ctx_id, x, y, false); - } - }, - move || { - let mut g = lock(&p2); - if let Some(e) = g.graph.get_mut(id) { - match old { - Some(((ox, oy), oe)) => { - e.core.set_context_position(ctx_id, ox, oy, oe); - } - None => { - e.core.remove_from_context(ctx_id); - } - } - } - }, - ); - box_command(cmd) - } - - /// `oaknode_node_get_markers` — the sequence's marker list (created - /// lazily; addref'd copy). - pub fn oaknode_node_get_markers(node: CHandle, out: *mut CHandle) -> c_int { - if out.is_null() { - return OAKNODE_E_INVALID; - } - let markers = with_node_mut(node, |g, id| { - let seq = behavior_of_mut::(g, id)?; - if seq.markers.is_null() { - seq.markers = oaktimeline::handle::make_owned( - oaktimeline::marker::TimelineMarkerList::new(), - ); - } - Some(seq.markers) - }); - match markers { - Some(Some(h)) => { - // SAFETY: valid out pointer. - unsafe { *out = addref_copy(h) }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_get_work_area` — the sequence's work area (created - /// lazily; addref'd copy). - pub fn oaknode_node_get_work_area(node: CHandle, out: *mut CHandle) -> c_int { - if out.is_null() { - return OAKNODE_E_INVALID; - } - let workarea = with_node_mut(node, |g, id| { - let seq = behavior_of_mut::(g, id)?; - if seq.workarea.is_null() { - seq.workarea = - oaktimeline::handle::make_owned(oaktimeline::workarea::TimelineWorkArea::new()); - } - Some(seq.workarea) - }); - match workarea { - Some(Some(h)) => { - // SAFETY: valid out pointer. - unsafe { *out = addref_copy(h) }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_get_video_frame_cache` — the node's video cache - /// handle (usually empty: caches are created lazily by the render - /// module; documented). - pub fn oaknode_node_get_video_frame_cache(node: CHandle, out: *mut CHandle) -> c_int { - if out.is_null() { - return OAKNODE_E_INVALID; - } - match with_node(node, |g, id| g.get(id).map(|e| e.core.caches.video)) { - Some(Some(h)) => { - // SAFETY: valid out pointer. - unsafe { *out = addref_copy(h) }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_copy_inputs`. - pub fn oaknode_node_copy_inputs( - dst: CHandle, - src: CHandle, - include_connections: c_int, - ) -> c_int { - let dst_nr = match unsafe { node_ref_of(&dst) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let src_nr = match unsafe { node_ref_of(&src) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - if !Arc::ptr_eq(&dst_nr.0, &src_nr.0) { - return OAKNODE_E_NOT_FOUND; - } - let mut g = lock(&dst_nr.0); - match oaknode::ops::copy_inputs( - &mut g.graph, - src_nr.1, - dst_nr.1, - include_connections != 0, - ) { - Ok(()) => OAKNODE_OK, - Err(e) => e.code(), - } - } - - /// `oaknode_node_set_value_hint_track` — best-effort value hint from a - /// track reference (video -> Texture, audio -> Samples). - pub fn oaknode_node_set_value_hint_track( - node: CHandle, - input_id: *const c_char, - track_type: c_int, - track_index: c_int, - ) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - let types = match track_type { - 0 => vec![oaknode::value::ValueType::Texture], - 1 => vec![oaknode::value::ValueType::Samples], - _ => Vec::new(), - }; - match with_node_mut(node, |g, id| { - let e = g.get_mut(id)?; - if !e.core.has_input(&input_id) { - return None; - } - e.core.set_value_hint( - &input_id, - -1, - oaknode::input::ValueHint { - types, - index: track_index, - tag: String::new(), - }, - ); - Some(()) - }) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_viewer_set_video_params` — set the sequence's video - /// parameter stream 0. - pub fn oaknode_viewer_set_video_params(viewer: CHandle, params: *const CHandle) -> c_int { - if params.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller passes a live oakcommon videoparams handle. - let converted = unsafe { vp_from_handle(*params) }; - let Some(converted) = converted else { - return OAKNODE_E_INVALID; - }; - match with_node_mut(viewer, |g, id| { - let seq = behavior_of_mut::(g, id)?; - if seq.video_params.is_empty() { - seq.video_params.push(converted); - } else { - seq.video_params[0] = converted; - } - Some(()) - }) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_viewer_set_audio_params` — set the sequence's audio - /// parameter stream 0 (oakcore audioparams pointer). - pub fn oaknode_viewer_set_audio_params(viewer: CHandle, params: *const c_void) -> c_int { - if params.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the oakcore audioparams contract. - let (sample_rate, channel_layout, format) = unsafe { - ( - crate::stubs::audio::oakcore_audioparams_sample_rate(params), - crate::stubs::audio::oakcore_audioparams_channel_layout(params), - crate::stubs::audio::oakcore_audioparams_format(params), - ) - }; - let converted = oaknode::value::AudioParams { - sample_rate, - channel_layout, - format, - }; - match with_node_mut(viewer, |g, id| { - let seq = behavior_of_mut::(g, id)?; - if seq.audio_params.is_empty() { - seq.audio_params.push(converted); - } else { - seq.audio_params[0] = converted; - } - Some(()) - }) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_find_input_footage` — the first footage node feeding - /// this node (upstream walk). - pub fn oaknode_node_find_input_footage(node: CHandle, out: *mut CHandle) -> c_int { - if out.is_null() { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let found = { - let g = lock(&project); - if !g.graph.is_valid(id) { - return OAKNODE_E_NOT_FOUND; - } - let mut frontier = vec![id]; - let mut visited: Vec = Vec::new(); - let mut found = None; - while !frontier.is_empty() && found.is_none() { - let mut next = Vec::new(); - for cur in frontier { - if visited.contains(&cur) { - continue; - } - visited.push(cur); - let Some(e) = g.graph.get(cur) else { continue }; - if e.behavior.type_id() == "org.olivevideoeditor.Olive.footage" && cur != id { - found = Some(cur); - break; - } - for (src, _, _) in g.graph.input_connections(cur) { - next.push(src); - } - } - frontier = next; - } - found - }; - // SAFETY: valid out pointer. - unsafe { - *out = match found { - Some(f) => make_node_handle(project, f, false), - None => CHandle::null(), - }; - } - OAKNODE_OK - } - - /// `oaknode_node_get_input_at_time` — value at a rational time. - pub fn oaknode_node_get_input_at_time( - node: CHandle, - input_id: *const c_char, - time_num: i64, - time_den: i64, - out: *mut crate::node::OakNodeValue, - ) -> c_int { - if out.is_null() || time_den == 0 { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - match with_node(node, |g, id| { - let e = g.get(id)?; - let declared = e.core.input_data_type(&input_id)?; - let v = e - .core - .value_at_time(&input_id, -1, Rational::new(time_num, time_den)); - value_to_pod(declared, &v) - }) { - Some(Some(pod)) => { - // SAFETY: valid out pointer. - unsafe { *out = pod }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_set_input_at_time_undoable` — the real - /// `set_value_at_time` domain op (keyframed tracks get a key at the - /// time; static inputs get the standard value). - pub fn oaknode_node_set_input_at_time_undoable( - node: CHandle, - input_id: *const c_char, - time_num: i64, - time_den: i64, - v: *const crate::node::OakNodeValue, - track: c_int, - out_command: *mut CHandle, - ) -> c_int { - if v.is_null() || out_command.is_null() || time_den == 0 { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string and - // a live POD. - let (input_id, v) = unsafe { (cstr(input_id), *v) }; - let _ = track; - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let declared = { - let g = lock(&project); - let e = match g.graph.get(id) { - Some(e) => e, - None => return OAKNODE_E_NOT_FOUND, - }; - match e.core.input_data_type(&input_id) { - Some(d) => d, - None => return OAKNODE_E_NOT_FOUND, - } - }; - let value = match pod_to_value(declared, v) { - Some(nv) => nv, - None => return OAKNODE_E_INVALID, - }; - let guard = lock(&project); - let cmd = match oaknode::ops::set_value_at_time_command( - &project, - &guard.graph, - id, - &input_id, - -1, - Rational::new(time_num, time_den), - &value, - ) { - Ok(c) => c, - Err(e) => return e.code(), - }; - // SAFETY: valid out pointer. - unsafe { *out_command = box_command(cmd) }; - OAKNODE_OK - } - - /// `oaknode_node_identity` — the graph slot identity (0 for invalid). - pub fn oaknode_node_identity(node: CHandle) -> usize { - node_identity(node) - } - - /// `oaknode_node_set_input_at_time_into` — create the command and add - /// it to a multi command (the multi takes ownership). - pub fn oaknode_node_set_input_at_time_into( - node: CHandle, - input_id: *const c_char, - time_num: i64, - time_den: i64, - v: *const crate::node::OakNodeValue, - track: c_int, - multi_command: CHandle, - ) -> c_int { - let mut cmd = CHandle::null(); - let rc = oaknode_node_set_input_at_time_undoable( - node, input_id, time_num, time_den, v, track, &mut cmd, - ); - if rc != OAKNODE_OK { - return rc; - } - oakundo::undocommand::command_multi_add_child(multi_command, cmd) - } - - /// `oaknode_command_create_remove_node` — remove the node from its - /// graph (the entry is not retained for undo — documented deviation - /// from the C++ shared-ptr retention). - pub fn oaknode_command_create_remove_node(node: CHandle) -> CHandle { - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return CHandle::null(), - }; - let (project, id) = nr; - { - let g = lock(&project); - if !g.graph.is_valid(id) { - return CHandle::null(); - } - } - let p1 = project.clone(); - let cmd = closure_command( - move || { - let mut g = lock(&p1); - g.graph.remove_node(id); - }, - || {}, - ); - box_command(cmd) - } - - /// `oaknode_node_free` — release a node handle shell; a still-owned - /// (detached) node is dropped from its scratch graph. - pub fn oaknode_node_free(node: *mut CHandle) { - if node.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - let h = unsafe { *node }; - free_node_handle(h); - // SAFETY: the caller passes a valid handle pointer. - unsafe { *node = CHandle::null() }; - } - - // ------------------------------------------------------------------- - // Factory family - // ------------------------------------------------------------------- - - /// `oaknode_factory_initialize` — the registry builds lazily; nothing - /// to do. - pub fn oaknode_factory_initialize() -> c_int { - let _ = Factory::global(); - OAKNODE_OK - } - - /// `oaknode_factory_destroy` — process-lifetime registry; nothing to - /// do. - pub fn oaknode_factory_destroy() {} - - /// `oaknode_factory_id_count`. - pub fn oaknode_factory_id_count(out_count: *mut c_int) -> c_int { - if out_count.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: valid out pointer. - unsafe { *out_count = Factory::global().entries().len() as c_int }; - OAKNODE_OK - } - - /// `oaknode_factory_id_at` (two-stage). - pub fn oaknode_factory_id_at(index: c_int, buf: *mut c_char, buf_size: c_int) -> c_int { - if index < 0 { - return OAKNODE_E_NOT_FOUND; - } - match Factory::global().entries().get(index as usize) { - Some(meta) => string_out(meta.type_id, buf, buf_size), - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_factory_name_from_id` (two-stage). - pub fn oaknode_factory_name_from_id( - type_id: *const c_char, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let type_id = unsafe { cstr(type_id) }; - match Factory::global().find(&type_id) { - Some(meta) => string_out(meta.name, buf, buf_size), - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_factory_create_from_id` — detached node (owned). - pub fn oaknode_factory_create_from_id(type_id: *const c_char) -> CHandle { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let type_id = unsafe { cstr(type_id) }; - match Factory::global().find(&type_id) { - Some(meta) => make_detached((meta.create)()), - None => CHandle::null(), - } - } - - /// Cached factory prototype nodes (one per registry entry; created - /// lazily in the scratch project). - static PROTOTYPES: OnceLock>> = OnceLock::new(); - - /// `oaknode_factory_node_at` — borrowed prototype node. - pub fn oaknode_factory_node_at(index: c_int, out_node: *mut CHandle) -> c_int { - if out_node.is_null() { - return OAKNODE_E_INVALID; - } - if index < 0 { - return OAKNODE_E_NOT_FOUND; - } - let entries = Factory::global().entries(); - if index as usize >= entries.len() { - return OAKNODE_E_NOT_FOUND; - } - let mut protos = PROTOTYPES - .get_or_init(|| Mutex::new(Vec::new())) - .lock() - .unwrap_or_else(|e| e.into_inner()); - if protos.len() != entries.len() { - // Fill lazily: only the entries up to (and including) the - // requested index are materialized. - for i in protos.len()..=index as usize { - let (core, behavior) = (entries[i].create)(); - let project = scratch_project(); - let id = { - let mut p = lock(&project); - p.graph.add_node(core, behavior) - }; - protos.push(make_node_handle(project, id, false)); - } - } - // SAFETY: valid out pointer; the prototype is addref'd for the - // caller (owned copy semantics). - unsafe { *out_node = addref_copy(protos[index as usize]) }; - OAKNODE_OK - } - // ------------------------------------------------------------------- - // Folder family - // ------------------------------------------------------------------- - - /// `oaknode_folder_create` — new folder node in the project graph. - pub fn oaknode_folder_create(project: CHandle) -> CHandle { - let p = match unsafe { project_of(&project) }.cloned() { - Some(p) => p, - None => return CHandle::null(), - }; - let (core, behavior) = oaknode::folder::create("Folder"); - let id = { - let mut g = lock(&p); - g.graph.add_node(core, behavior) - }; - make_node_handle(p, id, false) - } - - /// Folder children of a folder handle. - fn folder_children(h: CHandle) -> Option> { - with_node(h, |g, id| { - behavior_of::(g, id) - .map(|f| f.children.clone()) - })? - } - - /// `oaknode_folder_child_count`. - pub fn oaknode_folder_child_count(folder: CHandle) -> c_int { - match folder_children(folder) { - Some(children) => children.len() as c_int, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_folder_child_at` — borrowed child view. - pub fn oaknode_folder_child_at(folder: CHandle, index: c_int) -> CHandle { - if index < 0 { - return CHandle::null(); - } - let nr = match unsafe { node_ref_of(&folder) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return CHandle::null(), - }; - let (project, id) = nr; - let child = { - let g = lock(&project); - behavior_of::(&g.graph, id) - .and_then(|f| f.children.get(index as usize).copied()) - }; - match child { - Some(c) => make_node_handle(project, c, false), - None => CHandle::null(), - } - } - - /// `oaknode_folder_add_child` — live add (+ bin membership). - pub fn oaknode_folder_add_child(folder: CHandle, child: CHandle) -> c_int { - let f_nr = match unsafe { node_ref_of(&folder) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let c_id = match unsafe { node_ref_of(&child) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - let mut g = lock(&f_nr.0); - let folder_b = - match behavior_of_mut::(&mut g.graph, f_nr.1) { - Some(f) => f, - None => return OAKNODE_E_NOT_FOUND, - }; - folder_b.add_child(c_id); - if let Some(e) = g.graph.get_mut(c_id) { - e.core.bin_folder = Some(f_nr.1); - } - OAKNODE_OK - } - - /// `oaknode_folder_as_node` — identity cast (every handle shares the - /// same payload layout); an addref'd copy keeps the borrow discipline. - pub fn oaknode_folder_as_node(folder: CHandle) -> CHandle { - addref_copy(folder) - } - - /// `oaknode_command_create_folder_add_child` — undoable add. - pub fn oaknode_command_create_folder_add_child(folder: CHandle, child: CHandle) -> CHandle { - let f_nr = match unsafe { node_ref_of(&folder) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return CHandle::null(), - }; - let c_id = match unsafe { node_ref_of(&child) } { - Some(nr) => nr.id, - None => return CHandle::null(), - }; - let (project, f_id) = f_nr; - { - let g = lock(&project); - if behavior_of::(&g.graph, f_id).is_none() { - return CHandle::null(); - } - } - let p1 = project.clone(); - let p2 = project; - let cmd = closure_command( - move || { - let mut g = lock(&p1); - if let Some(f) = - behavior_of_mut::(&mut g.graph, f_id) - { - f.add_child(c_id); - } - if let Some(e) = g.graph.get_mut(c_id) { - e.core.bin_folder = Some(f_id); - } - }, - move || { - let mut g = lock(&p2); - if let Some(f) = - behavior_of_mut::(&mut g.graph, f_id) - { - f.remove_child(c_id); - } - if let Some(e) = g.graph.get_mut(c_id) { - e.core.bin_folder = None; - } - }, - ); - box_command(cmd) - } - - /// `oaknode_folder_remove_child` — live remove. - pub fn oaknode_folder_remove_child(folder: CHandle, child: CHandle) -> c_int { - let f_nr = match unsafe { node_ref_of(&folder) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let c_id = match unsafe { node_ref_of(&child) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - let mut g = lock(&f_nr.0); - let folder_b = - match behavior_of_mut::(&mut g.graph, f_nr.1) { - Some(f) => f, - None => return OAKNODE_E_NOT_FOUND, - }; - folder_b.remove_child(c_id); - if let Some(e) = g.graph.get_mut(c_id) { - e.core.bin_folder = None; - } - OAKNODE_OK - } - - /// `oaknode_folder_move_children` — move nodes into `dest_folder`. - pub fn oaknode_folder_move_children( - nodes: *const CHandle, - count: c_int, - dest_folder: CHandle, - ) -> c_int { - if nodes.is_null() || count < 0 { - return OAKNODE_E_INVALID; - } - for i in 0..count as usize { - // SAFETY: the caller guarantees `count` valid handles. - let child = unsafe { *nodes.add(i) }; - let rc = oaknode_folder_add_child(dest_folder, child); - if rc != OAKNODE_OK { - return rc; - } - } - OAKNODE_OK - } - - /// `oaknode_folder_has_child_recursive`. - pub fn oaknode_folder_has_child_recursive(folder: CHandle, child: CHandle) -> c_int { - let f_nr = match unsafe { node_ref_of(&folder) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let c_id = match unsafe { node_ref_of(&child) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - let g = lock(&f_nr.0); - match behavior_of::(&g.graph, f_nr.1) { - Some(f) => { - if f.has_child_recursive(c_id, &g.graph) { - 1 - } else { - 0 - } - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_folder_index_of_child`. - pub fn oaknode_folder_index_of_child(folder: CHandle, child: CHandle) -> c_int { - let f_nr = match unsafe { node_ref_of(&folder) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let c_id = match unsafe { node_ref_of(&child) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - let g = lock(&f_nr.0); - match behavior_of::(&g.graph, f_nr.1) { - Some(f) => match f.index_of_child(c_id) { - Some(i) => i as c_int, - None => OAKNODE_E_NOT_FOUND, - }, - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_folder_parent_of` — the folder owning this item (null when - /// none). - pub fn oaknode_folder_parent_of(node: CHandle) -> CHandle { - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return CHandle::null(), - }; - let (project, id) = nr; - let parent = { - let g = lock(&project); - let entry = match g.graph.get(id) { - Some(e) => e, - None => return CHandle::null(), - }; - match entry.core.bin_folder { - Some(bin) - if behavior_of::(&g.graph, bin).is_some() => - { - Some(bin) - } - _ => { - // Fall back to a recursive walk over folder children. - g.graph.node_ids().into_iter().find(|fid| { - behavior_of::(&g.graph, *fid) - .map(|f| f.has_child_recursive(id, &g.graph)) - .unwrap_or(false) - }) - } - } - }; - match parent { - Some(p) => make_node_handle(project, p, false), - None => CHandle::null(), - } - } - - // ------------------------------------------------------------------- - // Footage family - // ------------------------------------------------------------------- - - /// Borrow the footage behavior behind a footage handle. - fn footage_behavior(h: CHandle) -> Option<&'static oaknode::footage::FootageBehavior> { - let nr = unsafe { node_ref_of(&h) }?; - let p = lock(&nr.project); - let b = behavior_of::(&p.graph, nr.id)?; - // SAFETY: the node lives in the arena; the project outlives every - // handle that references it (handles hold an Arc clone). - unsafe { Some(&*(b as *const _)) } - } - - /// Mutable footage-behavior view. - fn footage_behavior_mut(h: CHandle) -> Option<&'static mut oaknode::footage::FootageBehavior> { - let nr = unsafe { node_ref_of(&h) }?; - let mut p = lock(&nr.project); - let b = behavior_of_mut::(&mut p.graph, nr.id)?; - // SAFETY: see `footage_behavior`. - unsafe { Some(&mut *(b as *mut _)) } - } - - /// `oaknode_footage_create` — footage node registered in `project`. - pub fn oaknode_footage_create(project: CHandle, filename: *const c_char) -> CHandle { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let filename = unsafe { cstr(filename) }; - let p = match unsafe { project_of(&project) }.cloned() { - Some(p) => p, - None => return CHandle::null(), - }; - let (mut core, behavior) = oaknode::footage::FootageBehavior::create(); - core.set_standard_value( - "file_in", - -1, - oaknode::value::NodeValue::Text(filename.clone()), - ); - let id = { - let mut g = lock(&p); - g.graph.add_node(core, behavior) - }; - { - let mut g = lock(&p); - if let Some(f) = - behavior_of_mut::(&mut g.graph, id) - { - f.filename = filename; - // Best-effort probe: the stream metadata is filled when a - // decoder recognizes the file; failures keep the node - // usable (valid stays false). - let _ = f.probe(); - } - } - make_node_handle(p, id, false) - } - - /// `oaknode_footage_as_node` — identity cast (addref'd copy). - pub fn oaknode_footage_as_node(footage: CHandle) -> CHandle { - addref_copy(footage) - } - - /// `oaknode_footage_filename` (two-stage). - pub fn oaknode_footage_filename(footage: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - match footage_behavior(footage) { - Some(f) => string_out(&f.filename, buf, buf_size), - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_set_filename`. - pub fn oaknode_footage_set_filename(footage: CHandle, filename: *const c_char) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let filename = unsafe { cstr(filename) }; - match footage_behavior_mut(footage) { - Some(f) => { - f.filename = filename; - f.valid = false; - let _ = f.probe(); - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_is_valid` — 1/0. - pub fn oaknode_footage_is_valid(footage: CHandle) -> c_int { - match footage_behavior(footage) { - Some(f) => { - if f.valid { - 1 - } else { - 0 - } - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_timestamp`. - pub fn oaknode_footage_timestamp(footage: CHandle, out_timestamp: *mut i64) -> c_int { - if out_timestamp.is_null() { - return OAKNODE_E_INVALID; - } - match footage_behavior(footage) { - Some(f) => { - // SAFETY: valid out pointer. - unsafe { *out_timestamp = f.timestamp }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_set_timestamp`. - pub fn oaknode_footage_set_timestamp(footage: CHandle, timestamp: i64) -> c_int { - match footage_behavior_mut(footage) { - Some(f) => { - f.timestamp = timestamp; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_decoder` (two-stage). - pub fn oaknode_footage_decoder(footage: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - match footage_behavior(footage) { - Some(f) => string_out(&f.decoder, buf, buf_size), - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_total_stream_count`. - pub fn oaknode_footage_total_stream_count(footage: CHandle) -> c_int { - match footage_behavior(footage) { - Some(f) => f.total_stream_count() as c_int, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_video_stream_count`. - pub fn oaknode_footage_video_stream_count(footage: CHandle) -> c_int { - match footage_behavior(footage) { - Some(f) => f.video_stream_count() as c_int, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_audio_stream_count`. - pub fn oaknode_footage_audio_stream_count(footage: CHandle) -> c_int { - match footage_behavior(footage) { - Some(f) => f.audio_stream_count() as c_int, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_subtitle_stream_count`. - pub fn oaknode_footage_subtitle_stream_count(footage: CHandle) -> c_int { - match footage_behavior(footage) { - Some(f) => f.subtitle_stream_count() as c_int, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_duration` — (num, den) of the longest stream. - pub fn oaknode_footage_duration( - footage: CHandle, - out_numerator: *mut c_int, - out_denominator: *mut c_int, - ) -> c_int { - if out_numerator.is_null() || out_denominator.is_null() { - return OAKNODE_E_INVALID; - } - match footage_behavior(footage) { - Some(f) => { - let d = f.duration(); - // SAFETY: valid out pointers. - unsafe { - *out_numerator = d.numerator() as c_int; - *out_denominator = d.denominator() as c_int; - } - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_proxy_enabled`. - pub fn oaknode_footage_proxy_enabled(footage: CHandle) -> c_int { - match footage_behavior(footage) { - Some(f) => { - if f.proxy_enabled { - 1 - } else { - 0 - } - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_set_proxy_enabled`. - pub fn oaknode_footage_set_proxy_enabled(footage: CHandle, enabled: c_int) -> c_int { - match footage_behavior_mut(footage) { - Some(f) => { - f.proxy_enabled = enabled != 0; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_proxy_path` (two-stage). - pub fn oaknode_footage_proxy_path(footage: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - match footage_behavior(footage) { - Some(f) => string_out(&f.proxy, buf, buf_size), - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_proxy_state`. - pub fn oaknode_footage_proxy_state(footage: CHandle) -> c_int { - match footage_behavior(footage) { - Some(f) => f.proxy_state, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_set_proxy`. - pub fn oaknode_footage_set_proxy( - footage: CHandle, - path: *const c_char, - state: c_int, - video_stream_index: c_int, - preset_version: c_int, - enabled: c_int, - ) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let path = unsafe { cstr(path) }; - match footage_behavior_mut(footage) { - Some(f) => { - f.set_proxy( - &path, - state, - video_stream_index, - preset_version, - enabled != 0, - ); - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_clear_proxy`. - pub fn oaknode_footage_clear_proxy(footage: CHandle) -> c_int { - match footage_behavior_mut(footage) { - Some(f) => { - f.clear_proxy(); - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_get_video_params` — stream `index` as an oakcommon - /// params handle. - pub fn oaknode_footage_get_video_params( - footage: CHandle, - index: c_int, - out: *mut CHandle, - ) -> c_int { - if out.is_null() || index < 0 { - return OAKNODE_E_INVALID; - } - match footage_behavior(footage).and_then(|f| f.video_params(index as usize)) { - Some(v) => { - // SAFETY: valid out pointer. - unsafe { *out = vp_handle(&v) }; - OAKNODE_OK - } - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_footage_set_video_params`. - pub fn oaknode_footage_set_video_params( - footage: CHandle, - index: c_int, - params: *const CHandle, - ) -> c_int { - if params.is_null() || index < 0 { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller passes a live oakcommon videoparams handle. - let converted = unsafe { vp_from_handle(*params) }; - let Some(converted) = converted else { - return OAKNODE_E_INVALID; - }; - let nr = match unsafe { node_ref_of(&footage) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let mut g = lock(&nr.0); - let f = match behavior_of_mut::(&mut g.graph, nr.1) { - Some(f) => f, - None => return OAKNODE_E_NOT_FOUND, - }; - let mut seen = 0usize; - for s in f.streams.iter_mut().filter(|s| s.is_video) { - if seen == index as usize { - s.video = Some(converted); - return OAKNODE_OK; - } - seen += 1; - } - OAKNODE_E_NOT_FOUND - } - - /// `oaknode_footage_get_video_length` — (num, den) of the longest video - /// stream. - pub fn oaknode_footage_get_video_length( - footage: CHandle, - out_num: *mut i64, - out_den: *mut i64, - ) -> c_int { - if out_num.is_null() || out_den.is_null() { - return OAKNODE_E_INVALID; - } - match footage_behavior(footage) { - Some(f) => { - let d = f.video_length(); - // SAFETY: valid out pointers. - unsafe { - *out_num = d.numerator(); - *out_den = d.denominator(); - } - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_set_cancel_atom` — record the footage's cancel - /// state from a shared oakcommon cancel atom handle. - pub fn oaknode_footage_set_cancel_atom(footage: CHandle, atom: CHandle) -> c_int { - let cancelled = if atom.is_null() { - false - } else { - // SAFETY: the atom handle boxes an oakcommon CancelAtom. - unsafe { oakcommon::handle::get::(&atom) } - .map(|a| a.is_cancelled()) - .unwrap_or(false) - }; - match footage_behavior_mut(footage) { - Some(f) => { - f.set_cancel(cancelled); - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - // ------------------------------------------------------------------- - // Group family - // ------------------------------------------------------------------- - - /// `oaknode_group_create` — detached group node. - pub fn oaknode_group_create() -> CHandle { - make_detached(oaknode::nodes::group::create()) - } - - /// `oaknode_group_cast` — the same node when it is a group, else null. - pub fn oaknode_group_cast(node: CHandle) -> CHandle { - if is_type(node, "org.olivevideoeditor.Olive.group") { - addref_copy(node) - } else { - CHandle::null() - } - } - - /// `oaknode_group_free` — same shell release as the node family. - pub fn oaknode_group_free(group: *mut CHandle) { - if group.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - let h = unsafe { *group }; - free_node_handle(h); - // SAFETY: the caller passes a valid handle pointer. - unsafe { *group = CHandle::null() }; - } - - /// `oaknode_group_add_input_passthrough` — mint a passthrough input on - /// the group mirroring `node`'s input; writes the minted id (two-stage - /// getter convention). - pub fn oaknode_group_add_input_passthrough( - group: CHandle, - node: CHandle, - input_id: *const c_char, - element: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - let g_nr = match unsafe { node_ref_of(&group) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let n_id = match unsafe { node_ref_of(&node) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - let mut g = lock(&g_nr.0); - let descriptor = match g - .graph - .get(n_id) - .and_then(|e| e.core.get_input(&input_id)) - .cloned() - { - Some(d) => d, - None => return OAKNODE_E_NOT_FOUND, - }; - let entry = match g.graph.get_mut(g_nr.1) { - Some(e) => e, - None => return OAKNODE_E_NOT_FOUND, - }; - let (core, behavior) = (&mut entry.core, &mut entry.behavior); - let group_b = match behavior.as_any_mut().and_then(|a| a.downcast_mut::()) { - Some(b) => b, - None => return OAKNODE_E_NOT_FOUND, - }; - let id = group_b.add_input_passthrough( - core, - oaknode::nodes::group::InnerInput { - node: n_id, - input: input_id, - element, - }, - "", - &descriptor, - ); - string_out(&id, buf, buf_size) - } - - /// `oaknode_group_add_input_passthrough_undoable`. - pub fn oaknode_group_add_input_passthrough_undoable( - group: CHandle, - node: CHandle, - input_id: *const c_char, - element: c_int, - out_command: *mut CHandle, - ) -> c_int { - if out_command.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - let g_nr = match unsafe { node_ref_of(&group) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let n_id = match unsafe { node_ref_of(&node) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - let (project, g_id) = g_nr; - let descriptor = { - let g = lock(&project); - match g - .graph - .get(n_id) - .and_then(|e| e.core.get_input(&input_id)) - .cloned() - { - Some(d) => d, - None => return OAKNODE_E_NOT_FOUND, - } - }; - let p1 = project.clone(); - let p2 = project; - let input1 = input_id.clone(); - let input2 = input_id; - let cmd = closure_command( - move || { - let mut g = lock(&p1); - if let Some(entry) = g.graph.get_mut(g_id) { - let (core, behavior) = (&mut entry.core, &mut entry.behavior); - if let Some(b) = behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - { - b.add_input_passthrough( - core, - oaknode::nodes::group::InnerInput { - node: n_id, - input: input1.clone(), - element, - }, - "", - &descriptor, - ); - } - } - }, - move || { - let mut g = lock(&p2); - if let Some(entry) = g.graph.get_mut(g_id) { - let (core, behavior) = (&mut entry.core, &mut entry.behavior); - if let Some(b) = behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - { - b.remove_input_passthrough( - core, - &oaknode::nodes::group::InnerInput { - node: n_id, - input: input2.clone(), - element, - }, - ); - } - } - }, - ); - // SAFETY: valid out pointer. - unsafe { *out_command = box_command(cmd) }; - OAKNODE_OK - } - - /// `oaknode_group_remove_input_passthrough`. - pub fn oaknode_group_remove_input_passthrough( - group: CHandle, - node: CHandle, - input_id: *const c_char, - element: c_int, - ) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - let g_nr = match unsafe { node_ref_of(&group) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let n_id = match unsafe { node_ref_of(&node) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - let mut g = lock(&g_nr.0); - let entry = match g.graph.get_mut(g_nr.1) { - Some(e) => e, - None => return OAKNODE_E_NOT_FOUND, - }; - let (core, behavior) = (&mut entry.core, &mut entry.behavior); - let group_b = match behavior.as_any_mut().and_then(|a| a.downcast_mut::()) { - Some(b) => b, - None => return OAKNODE_E_NOT_FOUND, - }; - group_b.remove_input_passthrough( - core, - &oaknode::nodes::group::InnerInput { - node: n_id, - input: input_id, - element, - }, - ); - OAKNODE_OK - } - - /// `oaknode_group_passthrough_count`. - pub fn oaknode_group_passthrough_count(group: CHandle, out_count: *mut c_int) -> c_int { - if out_count.is_null() { - return OAKNODE_E_INVALID; - } - match with_node(group, |g, id| { - behavior_of::(g, id) - .map(|b| b.passthroughs().len()) - }) { - Some(Some(n)) => { - // SAFETY: valid out pointer. - unsafe { *out_count = n as c_int }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_group_passthrough_id_at` (two-stage). - pub fn oaknode_group_passthrough_id_at( - group: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - if index < 0 { - return OAKNODE_E_NOT_FOUND; - } - match with_node(group, |g, id| { - behavior_of::(g, id).and_then(|b| { - b.passthroughs() - .get(index as usize) - .map(|(pid, _)| pid.clone()) - }) - }) { - Some(Some(s)) => string_out(&s, buf, buf_size), - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_group_passthrough_input_at` — inner node + input id + - /// element (string via the two-stage convention). - pub fn oaknode_group_passthrough_input_at( - group: CHandle, - index: c_int, - out_node: *mut CHandle, - buf: *mut c_char, - buf_size: c_int, - out_element: *mut c_int, - ) -> c_int { - if out_node.is_null() || out_element.is_null() || index < 0 { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&group) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let inner = { - let g = lock(&project); - behavior_of::(&g.graph, id) - .and_then(|b| b.passthroughs().get(index as usize)) - .map(|(_, inner)| inner.clone()) - }; - match inner { - Some(inner) => { - // SAFETY: valid out pointers. - unsafe { - *out_node = make_node_handle(project, inner.node, false); - *out_element = inner.element; - } - string_out(&inner.input, buf, buf_size) - } - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_group_get_output_passthrough` — null when unset. - pub fn oaknode_group_get_output_passthrough(group: CHandle, out_node: *mut CHandle) -> c_int { - if out_node.is_null() { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&group) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let target = { - let g = lock(&project); - behavior_of::(&g.graph, id) - .and_then(|b| b.output_passthrough()) - }; - // SAFETY: valid out pointer. - unsafe { - *out_node = match target { - Some(t) => make_node_handle(project, t, false), - None => CHandle::null(), - }; - } - OAKNODE_OK - } - - /// `oaknode_group_set_output_passthrough`. - pub fn oaknode_group_set_output_passthrough(group: CHandle, node: CHandle) -> c_int { - let g_nr = match unsafe { node_ref_of(&group) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let n_id = match unsafe { node_ref_of(&node) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - let mut g = lock(&g_nr.0); - match behavior_of_mut::(&mut g.graph, g_nr.1) { - Some(b) => { - b.set_output_passthrough(Some(n_id)); - OAKNODE_OK - } - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_group_set_output_passthrough_undoable`. - pub fn oaknode_group_set_output_passthrough_undoable( - group: CHandle, - node: CHandle, - out_command: *mut CHandle, - ) -> c_int { - if out_command.is_null() { - return OAKNODE_E_INVALID; - } - let g_nr = match unsafe { node_ref_of(&group) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let n_id = match unsafe { node_ref_of(&node) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - let (project, g_id) = g_nr; - let old = { - let g = lock(&project); - behavior_of::(&g.graph, g_id) - .and_then(|b| b.output_passthrough()) - }; - let p1 = project.clone(); - let p2 = project; - let cmd = closure_command( - move || { - let mut g = lock(&p1); - if let Some(b) = - behavior_of_mut::(&mut g.graph, g_id) - { - b.set_output_passthrough(Some(n_id)); - } - }, - move || { - let mut g = lock(&p2); - if let Some(b) = - behavior_of_mut::(&mut g.graph, g_id) - { - b.set_output_passthrough(old); - } - }, - ); - // SAFETY: valid out pointer. - unsafe { *out_command = box_command(cmd) }; - OAKNODE_OK - } - - /// `oaknode_group_resolve_input` — resolve a group input through the - /// passthrough table (string via the two-stage convention; node null - /// when the input does not map to a passthrough). - pub fn oaknode_group_resolve_input( - node: CHandle, - input_id: *const c_char, - element: c_int, - out_node: *mut CHandle, - buf: *mut c_char, - buf_size: c_int, - out_element: *mut c_int, - ) -> c_int { - if out_node.is_null() || out_element.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let resolved = { - let g = lock(&project); - let group_b = match behavior_of::(&g.graph, id) { - Some(b) => b, - None => return OAKNODE_E_NOT_FOUND, - }; - match group_b.input_from_id(&input_id) { - Some(inner) => { - Some(oaknode::nodes::group::NodeGroup::resolve_input(&g.graph, inner.clone())) - } - None => None, - } - }; - match resolved { - Some(inner) => { - // SAFETY: valid out pointers. - unsafe { - *out_node = make_node_handle(project, inner.node, false); - *out_element = inner.element; - } - string_out(&inner.input, buf, buf_size) - } - None => { - // SAFETY: valid out pointers; the engine substitutes the - // group itself for a null resolved node. - unsafe { - *out_node = CHandle::null(); - *out_element = element; - } - string_out(&input_id, buf, buf_size) - } - } - } - - // ------------------------------------------------------------------- - // Multicam family (static input ids + math over the multicam node) - // ------------------------------------------------------------------- - - /// `oaknode_multicam_input_current`. - pub fn oaknode_multicam_input_current() -> *const c_char { - static S: &[u8] = b"current_in\0"; - S.as_ptr() as *const c_char - } - - /// `oaknode_multicam_input_sources`. - pub fn oaknode_multicam_input_sources() -> *const c_char { - static S: &[u8] = b"sources_in\0"; - S.as_ptr() as *const c_char - } - - /// `oaknode_multicam_input_sequence`. - pub fn oaknode_multicam_input_sequence() -> *const c_char { - static S: &[u8] = b"sequence_in\0"; - S.as_ptr() as *const c_char - } - - /// `oaknode_multicam_input_sequence_type`. - pub fn oaknode_multicam_input_sequence_type() -> *const c_char { - static S: &[u8] = b"sequence_type_in\0"; - S.as_ptr() as *const c_char - } - - /// `oaknode_multicam_get_source_count` — the `sources_in` array size - /// (the domain query is core-only and not re-exported, so it is read - /// directly). - pub fn oaknode_multicam_get_source_count(node: CHandle, out_count: *mut c_int) -> c_int { - if out_count.is_null() { - return OAKNODE_E_INVALID; - } - match with_node(node, |g, id| { - g.get(id) - .map(|e| e.core.input_array_size("sources_in") as i32) - }) { - Some(Some(n)) => { - // SAFETY: valid out pointer. - unsafe { *out_count = n }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_multicam_get_rows_and_columns` — the C++ grid math (the - /// multicam node module is private, so the statics are re-implemented - /// here). - pub fn oaknode_multicam_get_rows_and_columns( - source_count: c_int, - rows: *mut c_int, - cols: *mut c_int, - ) -> c_int { - if rows.is_null() || cols.is_null() || source_count < 0 { - return OAKNODE_E_INVALID; - } - let (mut r, mut c) = (1, 1); - while r * c < source_count { - if r < c { - r += 1; - } else { - c += 1; - } - } - // SAFETY: valid out pointers. - unsafe { - *rows = r; - *cols = c; - } - OAKNODE_OK - } - - /// `oaknode_multicam_index_to_row_cols` — `row = index / cols`, - /// `col = index % cols` (C++ parity; `rows` is unused there too). - pub fn oaknode_multicam_index_to_row_cols( - index: c_int, - rows: c_int, - cols: c_int, - out_row: *mut c_int, - out_col: *mut c_int, - ) -> c_int { - if out_row.is_null() || out_col.is_null() || index < 0 || rows < 1 || cols < 1 { - return OAKNODE_E_INVALID; - } - // SAFETY: valid out pointers. - unsafe { - *out_row = index / cols; - *out_col = index % cols; - } - OAKNODE_OK - } - - /// `oaknode_multicam_rows_cols_to_index` — `col + row * cols` (C++ - /// parity). - pub fn oaknode_multicam_rows_cols_to_index( - row: c_int, - col: c_int, - rows: c_int, - cols: c_int, - ) -> c_int { - if row < 0 || col < 0 || rows < 1 || cols < 1 { - return OAKNODE_E_INVALID; - } - col + row * cols - } - - /// `oaknode_multicam_get_current_source` — the `current_in` standard - /// value as int (the domain query is core-only and not re-exported). - pub fn oaknode_multicam_get_current_source(node: CHandle, out_source: *mut c_int) -> c_int { - if out_source.is_null() { - return OAKNODE_E_INVALID; - } - match with_node(node, |g, id| { - g.get(id) - .map(|e| e.core.standard_value("current_in", -1).to_double() as i32) - }) { - Some(Some(s)) => { - // SAFETY: valid out pointer. - unsafe { *out_source = s }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - // ------------------------------------------------------------------- - // Sequence family - // ------------------------------------------------------------------- - - fn seq_behavior(h: CHandle) -> Option<&'static oaknode::sequence::SequenceBehavior> { - let nr = unsafe { node_ref_of(&h) }?; - let p = lock(&nr.project); - let b = behavior_of::(&p.graph, nr.id)?; - // SAFETY: the node lives in the arena; the project outlives every - // handle that references it. - unsafe { Some(&*(b as *const _)) } - } - - fn seq_behavior_mut(h: CHandle) -> Option<&'static mut oaknode::sequence::SequenceBehavior> { - let nr = unsafe { node_ref_of(&h) }?; - let mut p = lock(&nr.project); - let b = behavior_of_mut::(&mut p.graph, nr.id)?; - // SAFETY: see `seq_behavior`. - unsafe { Some(&mut *(b as *mut _)) } - } - - /// Block-range accessor backed by the graph arena. - struct GraphBlockRange<'a>(&'a Graph); - - impl<'a> oaknode::track::BlockRange for GraphBlockRange<'a> { - fn in_(&self, block: NodeId) -> Rational { - block_core(self.0, block) - .map(|c| c.in_()) - .unwrap_or_else(|| Rational::new(0, 1)) - } - - fn out(&self, block: NodeId) -> Rational { - block_core(self.0, block) - .map(|c| c.out()) - .unwrap_or_else(|| Rational::new(0, 1)) - } - } - - /// Track-length accessor backed by the graph arena. - struct GraphTrackRange<'a>(&'a Graph); - - impl<'a> oaknode::track::TrackRange for GraphTrackRange<'a> { - fn length(&self, track: NodeId) -> Rational { - let blocks = GraphBlockRange(self.0); - behavior_of::(self.0, track) - .map(|t| t.length(&blocks)) - .unwrap_or_else(|| Rational::new(0, 1)) - } - } - - /// The block core of a block node (clip/gap/transition). - fn block_core(g: &Graph, id: NodeId) -> Option<&oaknode::block::BlockCore> { - let e = g.get(id)?; - if let Some(clip) = e.behavior.as_any().and_then(|a| a.downcast_ref::()) - { - return Some(&clip.core); - } - if let Some(gap) = e.behavior.as_any().and_then(|a| a.downcast_ref::()) - { - return Some(&gap.core); - } - if let Some(tr) = e - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - { - return Some(&tr.core); - } - None - } - - /// Mutable block core. - fn block_core_mut<'a>(g: &'a mut Graph, id: NodeId) -> Option<&'a mut oaknode::block::BlockCore> { - let e = g.get_mut(id)?; - let a = e.behavior.as_any_mut()?; - if a.is::() { - return Some(&mut a - .downcast_mut::() - .expect("is-checked") - .core); - } - if a.is::() { - return Some(&mut a - .downcast_mut::() - .expect("is-checked") - .core); - } - if a.is::() { - return Some(&mut a - .downcast_mut::() - .expect("is-checked") - .core); - } - None - } - - /// `oaknode_sequence_create` — detached sequence node. - pub fn oaknode_sequence_create() -> CHandle { - make_detached(oaknode::sequence::SequenceBehavior::create()) - } - - /// `oaknode_sequence_free` — same shell release as the node family. - pub fn oaknode_sequence_free(sequence: *mut CHandle) { - if sequence.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - let h = unsafe { *sequence }; - free_node_handle(h); - // SAFETY: the caller passes a valid handle pointer. - unsafe { *sequence = CHandle::null() }; - } - - /// `oaknode_sequence_set_default_parameters`. - pub fn oaknode_sequence_set_default_parameters(sequence: CHandle) -> c_int { - match seq_behavior_mut(sequence) { - Some(s) => { - s.set_default_parameters(); - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_sequence_as_node` — identity cast (addref'd copy). - pub fn oaknode_sequence_as_node(sequence: CHandle) -> CHandle { - addref_copy(sequence) - } - - /// `oaknode_sequence_from_node` — the same node when it is a sequence, - /// else null. - pub fn oaknode_sequence_from_node(node: CHandle) -> CHandle { - if is_type(node, "org.olivevideoeditor.Olive.sequence") { - addref_copy(node) - } else { - CHandle::null() - } - } - - /// Find (or create) the track list of `kind` on the sequence. - fn sequence_track_list_id(project: &ProjectArc, seq_id: NodeId, kind: oaknode::track::TrackType) -> Option { - // Find an existing list of the kind. - { - let g = lock(project); - let seq = behavior_of::(&g.graph, seq_id)?; - for tl in &seq.track_lists { - if let Some(list) = behavior_of::(&g.graph, *tl) - { - if list.kind == kind { - return Some(*tl); - } - } - } - } - // Create it: the list is a graph node owned by the sequence. - let mut g = lock(project); - let (core, mut behavior) = oaknode::track::TrackListBehavior::create(); - if let Some(a) = behavior.as_any_mut() { - if let Some(list) = a.downcast_mut::() { - list.kind = kind; - let base = match behavior_of::(&g.graph, seq_id) - { - Some(s) => s.track_lists.len() as i32, - None => return None, - }; - list.array_base = base; - } - } - let list_id = g.graph.add_node(core, behavior); - if let Some(seq) = behavior_of_mut::(&mut g.graph, seq_id) - { - seq.track_lists.push(list_id); - } - if let Some(list) = behavior_of_mut::(&mut g.graph, list_id) - { - list.sequence = Some(seq_id); - } - Some(list_id) - } - - /// `oaknode_sequence_get_track_list` — find-or-create the list of the - /// given type (borrowed handle). - pub fn oaknode_sequence_get_track_list( - sequence: CHandle, - type_: c_int, - out: *mut CHandle, - ) -> c_int { - if out.is_null() { - return OAKNODE_E_INVALID; - } - let kind = match oaknode::track::TrackType::from_c(type_) { - Some(k) => k, - None => return OAKNODE_E_INVALID, - }; - let nr = match unsafe { node_ref_of(&sequence) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - match sequence_track_list_id(&project, id, kind) { - Some(list_id) => { - // SAFETY: valid out pointer. - unsafe { *out = make_node_handle(project, list_id, false) }; - OAKNODE_OK - } - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_sequence_get_track_count` — tracks of `type_`. - pub fn oaknode_sequence_get_track_count( - sequence: CHandle, - type_: c_int, - count: *mut c_int, - ) -> c_int { - if count.is_null() { - return OAKNODE_E_INVALID; - } - let kind = match oaknode::track::TrackType::from_c(type_) { - Some(k) => k, - None => return OAKNODE_E_INVALID, - }; - let nr = match unsafe { node_ref_of(&sequence) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let n = match sequence_track_list_id(&project, id, kind) { - Some(list_id) => { - let g = lock(&project); - behavior_of::(&g.graph, list_id) - .map(|l| l.tracks.len()) - .unwrap_or(0) - } - None => 0, - }; - // SAFETY: valid out pointer. - unsafe { *count = n as c_int }; - OAKNODE_OK - } - - /// `oaknode_sequence_get_track_at`. - pub fn oaknode_sequence_get_track_at( - sequence: CHandle, - type_: c_int, - index: c_int, - out: *mut CHandle, - ) -> c_int { - if out.is_null() || index < 0 { - return OAKNODE_E_INVALID; - } - let kind = match oaknode::track::TrackType::from_c(type_) { - Some(k) => k, - None => return OAKNODE_E_INVALID, - }; - let nr = match unsafe { node_ref_of(&sequence) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let target = match sequence_track_list_id(&project, id, kind) { - Some(list_id) => { - let g = lock(&project); - behavior_of::(&g.graph, list_id) - .and_then(|l| l.tracks.get(index as usize).copied()) - } - None => None, - }; - // SAFETY: valid out pointer. - unsafe { - *out = match target { - Some(t) => make_node_handle(project, t, false), - None => CHandle::null(), - }; - } - if target.is_some() { - OAKNODE_OK - } else { - OAKNODE_E_NOT_FOUND - } - } - - /// All track lists of the sequence (list ids). - fn sequence_track_lists(h: CHandle) -> Option> { - with_node(h, |g, id| { - behavior_of::(g, id) - .map(|s| s.track_lists.clone()) - })? - } - - /// `oaknode_sequence_get_all_track_count`. - pub fn oaknode_sequence_get_all_track_count(sequence: CHandle, count: *mut c_int) -> c_int { - if count.is_null() { - return OAKNODE_E_INVALID; - } - let lists = match sequence_track_lists(sequence) { - Some(l) => l, - None => return OAKNODE_E_INVALID, - }; - let nr = unsafe { node_ref_of(&sequence) }.map(|n| n.project.clone()); - let Some(project) = nr else { - return OAKNODE_E_INVALID; - }; - let g = lock(&project); - let mut total = 0usize; - for list_id in &lists { - if let Some(l) = behavior_of::(&g.graph, *list_id) { - total += l.tracks.len(); - } - } - // SAFETY: valid out pointer. - unsafe { *count = total as c_int }; - OAKNODE_OK - } - - /// `oaknode_sequence_get_all_track_at` — flat track index across all - /// lists. - pub fn oaknode_sequence_get_all_track_at( - sequence: CHandle, - index: c_int, - out: *mut CHandle, - ) -> c_int { - if out.is_null() || index < 0 { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&sequence) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let target = { - let g = lock(&project); - let seq = behavior_of::(&g.graph, id); - let mut flat = index as usize; - let mut target = None; - if let Some(seq) = seq { - for list_id in &seq.track_lists { - if let Some(l) = - behavior_of::(&g.graph, *list_id) - { - if flat < l.tracks.len() { - target = l.tracks.get(flat).copied(); - break; - } - flat -= l.tracks.len(); - } - } - } - target - }; - // SAFETY: valid out pointer. - unsafe { - *out = match target { - Some(t) => make_node_handle(project, t, false), - None => CHandle::null(), - }; - } - if target.is_some() { - OAKNODE_OK - } else { - OAKNODE_E_NOT_FOUND - } - } - - /// `oaknode_sequence_get_playhead`. - pub fn oaknode_sequence_get_playhead( - sequence: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - if numerator.is_null() || denominator.is_null() { - return OAKNODE_E_INVALID; - } - match seq_behavior(sequence) { - Some(s) => { - // SAFETY: valid out pointers. - unsafe { - *numerator = s.playhead.numerator() as c_int; - *denominator = s.playhead.denominator() as c_int; - } - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_sequence_set_playhead`. - pub fn oaknode_sequence_set_playhead( - sequence: CHandle, - numerator: c_int, - denominator: c_int, - ) -> c_int { - if denominator == 0 { - return OAKNODE_E_INVALID; - } - match seq_behavior_mut(sequence) { - Some(s) => { - s.playhead = Rational::new(numerator as i64, denominator as i64); - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// The sequence's overall content length (max track length). - fn sequence_length(h: CHandle) -> Option { - let nr = unsafe { node_ref_of(&h) }?; - let g = lock(&nr.project); - let seq = behavior_of::(&g.graph, nr.id)?; - let tracks = GraphTrackRange(&g.graph); - let blocks = GraphBlockRange(&g.graph); - let mut longest = Rational::new(0, 1); - for list_id in &seq.track_lists { - if let Some(list) = behavior_of::(&g.graph, *list_id) { - let len = list.total_length(&tracks); - if len > longest { - longest = len; - } - let _ = &blocks; - } - } - Some(longest) - } - - /// `oaknode_sequence_get_length`. - pub fn oaknode_sequence_get_length( - sequence: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - if numerator.is_null() || denominator.is_null() { - return OAKNODE_E_INVALID; - } - match sequence_length(sequence) { - Some(l) => { - // SAFETY: valid out pointers. - unsafe { - *numerator = l.numerator() as c_int; - *denominator = l.denominator() as c_int; - } - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// Length of the video track list. - fn sequence_length_of_type(h: CHandle, kind: oaknode::track::TrackType) -> Option { - let nr = unsafe { node_ref_of(&h) }?; - let g = lock(&nr.project); - let seq = behavior_of::(&g.graph, nr.id)?; - let tracks = GraphTrackRange(&g.graph); - for list_id in &seq.track_lists { - if let Some(list) = behavior_of::(&g.graph, *list_id) { - if list.kind == kind { - return Some(list.total_length(&tracks)); - } - } - } - Some(Rational::new(0, 1)) - } - - /// `oaknode_sequence_get_video_length`. - pub fn oaknode_sequence_get_video_length( - sequence: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - if numerator.is_null() || denominator.is_null() { - return OAKNODE_E_INVALID; - } - match sequence_length_of_type(sequence, oaknode::track::TrackType::Video) { - Some(l) => { - // SAFETY: valid out pointers. - unsafe { - *numerator = l.numerator() as c_int; - *denominator = l.denominator() as c_int; - } - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_sequence_get_audio_length`. - pub fn oaknode_sequence_get_audio_length( - sequence: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - if numerator.is_null() || denominator.is_null() { - return OAKNODE_E_INVALID; - } - match sequence_length_of_type(sequence, oaknode::track::TrackType::Audio) { - Some(l) => { - // SAFETY: valid out pointers. - unsafe { - *numerator = l.numerator() as c_int; - *denominator = l.denominator() as c_int; - } - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_sequence_verify_length` — recompute and cache the lengths. - pub fn oaknode_sequence_verify_length(sequence: CHandle) -> c_int { - let overall = match sequence_length(sequence) { - Some(l) => l, - None => return OAKNODE_E_INVALID, - }; - let video = sequence_length_of_type(sequence, oaknode::track::TrackType::Video) - .unwrap_or_else(|| Rational::new(0, 1)); - let audio = sequence_length_of_type(sequence, oaknode::track::TrackType::Audio) - .unwrap_or_else(|| Rational::new(0, 1)); - match seq_behavior_mut(sequence) { - Some(s) => { - s.verify_length((video, audio, overall)); - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_sequence_get_video_stream_count`. - pub fn oaknode_sequence_get_video_stream_count(sequence: CHandle, count: *mut c_int) -> c_int { - if count.is_null() { - return OAKNODE_E_INVALID; - } - match seq_behavior(sequence) { - Some(s) => { - // SAFETY: valid out pointer. - unsafe { *count = s.video_stream_count() as c_int }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_sequence_get_audio_stream_count`. - pub fn oaknode_sequence_get_audio_stream_count(sequence: CHandle, count: *mut c_int) -> c_int { - if count.is_null() { - return OAKNODE_E_INVALID; - } - match seq_behavior(sequence) { - Some(s) => { - // SAFETY: valid out pointer. - unsafe { *count = s.audio_stream_count() as c_int }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_sequence_get_video_params` — stream `index` as an - /// oakcommon params handle. - pub fn oaknode_sequence_get_video_params( - sequence: CHandle, - index: c_int, - out: *mut CHandle, - ) -> c_int { - if out.is_null() || index < 0 { - return OAKNODE_E_INVALID; - } - match seq_behavior(sequence).and_then(|s| s.video_params.get(index as usize)) { - Some(v) => { - // SAFETY: valid out pointer. - unsafe { *out = vp_handle(v) }; - OAKNODE_OK - } - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_sequence_set_video_params`. - pub fn oaknode_sequence_set_video_params( - sequence: CHandle, - index: c_int, - params: CHandle, - ) -> c_int { - if index < 0 { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller passes a live oakcommon videoparams handle. - let converted = unsafe { vp_from_handle(params) }; - let Some(converted) = converted else { - return OAKNODE_E_INVALID; - }; - match seq_behavior_mut(sequence) { - Some(s) => { - if index as usize >= s.video_params.len() { - s.video_params.resize(index as usize + 1, converted.clone()); - } - s.video_params[index as usize] = converted; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_sequence_get_audio_params` — host oakcore audioparams - /// pointer. - pub fn oaknode_sequence_get_audio_params( - sequence: CHandle, - index: c_int, - out: *mut *mut c_void, - ) -> c_int { - if out.is_null() || index < 0 { - return OAKNODE_E_INVALID; - } - let params = match seq_behavior(sequence).and_then(|s| s.audio_params.get(index as usize)) { - Some(p) => *p, - None => return OAKNODE_E_NOT_FOUND, - }; - // SAFETY: the oakcore audioparams contract. - let ptr = unsafe { - crate::stubs::audio::oakcore_audioparams_create( - params.sample_rate, - params.channel_layout, - params.format, - ) - }; - if ptr.is_null() { - return OAKNODE_E_NOMEM; - } - // SAFETY: valid out pointer. - unsafe { *out = ptr }; - OAKNODE_OK - } - - /// `oaknode_sequence_set_audio_params`. - pub fn oaknode_sequence_set_audio_params( - sequence: CHandle, - index: c_int, - params: *const c_void, - ) -> c_int { - if params.is_null() || index < 0 { - return OAKNODE_E_INVALID; - } - // SAFETY: the oakcore audioparams contract. - let converted = unsafe { - oaknode::value::AudioParams { - sample_rate: crate::stubs::audio::oakcore_audioparams_sample_rate(params), - channel_layout: crate::stubs::audio::oakcore_audioparams_channel_layout(params), - format: crate::stubs::audio::oakcore_audioparams_format(params), - } - }; - match seq_behavior_mut(sequence) { - Some(s) => { - if index as usize >= s.audio_params.len() { - s.audio_params.resize(index as usize + 1, converted.clone()); - } - s.audio_params[index as usize] = converted; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - // ------------------------------------------------------------------- - // Track family - // ------------------------------------------------------------------- - - fn track_behavior(h: CHandle) -> Option<&'static oaknode::track::TrackBehavior> { - let nr = unsafe { node_ref_of(&h) }?; - let p = lock(&nr.project); - let b = behavior_of::(&p.graph, nr.id)?; - // SAFETY: the node lives in the arena; the project outlives every - // handle that references it. - unsafe { Some(&*(b as *const _)) } - } - - fn track_behavior_mut(h: CHandle) -> Option<&'static mut oaknode::track::TrackBehavior> { - let nr = unsafe { node_ref_of(&h) }?; - let mut p = lock(&nr.project); - let b = behavior_of_mut::(&mut p.graph, nr.id)?; - // SAFETY: see `track_behavior`. - unsafe { Some(&mut *(b as *mut _)) } - } - - fn tracklist_behavior(h: CHandle) -> Option<&'static oaknode::track::TrackListBehavior> { - let nr = unsafe { node_ref_of(&h) }?; - let p = lock(&nr.project); - let b = behavior_of::(&p.graph, nr.id)?; - // SAFETY: see `track_behavior`. - unsafe { Some(&*(b as *const _)) } - } - - fn tracklist_behavior_mut(h: CHandle) -> Option<&'static mut oaknode::track::TrackListBehavior> { - let nr = unsafe { node_ref_of(&h) }?; - let mut p = lock(&nr.project); - let b = behavior_of_mut::(&mut p.graph, nr.id)?; - // SAFETY: see `track_behavior`. - unsafe { Some(&mut *(b as *mut _)) } - } - - /// `oaknode_track_as_node` — the same node when it is a track, else - /// null. - pub fn oaknode_track_as_node(track: CHandle) -> CHandle { - if is_type(track, "org.olivevideoeditor.Olive.track") { - addref_copy(track) - } else { - CHandle::null() - } - } - - /// `oaknode_track_create` — detached track of the given type. - pub fn oaknode_track_create(type_: c_int) -> CHandle { - let kind = match oaknode::track::TrackType::from_c(type_) { - Some(k) => k, - None => return CHandle::null(), - }; - let (core, behavior) = oaknode::track::TrackBehavior::create(); - // `create()` always yields a video track; specialize the kind. - let mut b = behavior; - if let Some(a) = b.as_any_mut() { - if let Some(t) = a.downcast_mut::() { - t.kind = kind; - } - } - make_detached((core, b)) - } - - /// `oaknode_track_free` — same shell release as the node family. - pub fn oaknode_track_free(track: *mut CHandle) { - if track.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - let h = unsafe { *track }; - free_node_handle(h); - // SAFETY: the caller passes a valid handle pointer. - unsafe { *track = CHandle::null() }; - } - - /// `oaknode_track_get_type`. - pub fn oaknode_track_get_type(track: CHandle, type_: *mut c_int) -> c_int { - if type_.is_null() { - return OAKNODE_E_INVALID; - } - match track_behavior(track) { - Some(t) => { - // SAFETY: valid out pointer. - unsafe { *type_ = t.kind.to_c() }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_track_set_type`. - pub fn oaknode_track_set_type(track: CHandle, type_: c_int) -> c_int { - let kind = match oaknode::track::TrackType::from_c(type_) { - Some(k) => k, - None => return OAKNODE_E_INVALID, - }; - match track_behavior_mut(track) { - Some(t) => { - t.kind = kind; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_track_get_height`. - pub fn oaknode_track_get_height(track: CHandle, height: *mut f64) -> c_int { - if height.is_null() { - return OAKNODE_E_INVALID; - } - match track_behavior(track) { - Some(t) => { - // SAFETY: valid out pointer. - unsafe { *height = t.height }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_track_set_height`. - pub fn oaknode_track_set_height(track: CHandle, height: f64) -> c_int { - match track_behavior_mut(track) { - Some(t) => { - t.height = height; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_track_get_height_in_pixels`. - pub fn oaknode_track_get_height_in_pixels(track: CHandle, height: *mut c_int) -> c_int { - if height.is_null() { - return OAKNODE_E_INVALID; - } - match track_behavior(track) { - Some(t) => { - // SAFETY: valid out pointer. - unsafe { - *height = oaknode::track::internal_height_to_pixel_height(t.height); - } - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_track_set_height_in_pixels`. - pub fn oaknode_track_set_height_in_pixels(track: CHandle, height: c_int) -> c_int { - match track_behavior_mut(track) { - Some(t) => { - t.height = oaknode::track::pixel_height_to_internal_height(height); - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_track_get_default_height_in_pixels`. - pub fn oaknode_track_get_default_height_in_pixels() -> c_int { - oaknode::track::internal_height_to_pixel_height( - oaknode::track::DEFAULT_HEIGHT_INTERNAL, - ) - } - - /// `oaknode_track_get_minimum_height_in_pixels`. - pub fn oaknode_track_get_minimum_height_in_pixels() -> c_int { - oaknode::track::internal_height_to_pixel_height( - oaknode::track::MINIMUM_HEIGHT_INTERNAL, - ) - } - - /// `oaknode_track_get_index`. - pub fn oaknode_track_get_index(track: CHandle, index: *mut c_int) -> c_int { - if index.is_null() { - return OAKNODE_E_INVALID; - } - match track_behavior(track) { - Some(t) => { - // SAFETY: valid out pointer. - unsafe { *index = t.index }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_track_set_index`. - pub fn oaknode_track_set_index(track: CHandle, index: c_int) -> c_int { - match track_behavior_mut(track) { - Some(t) => { - t.index = index; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_track_get_muted`. - pub fn oaknode_track_get_muted(track: CHandle, muted: *mut c_int) -> c_int { - if muted.is_null() { - return OAKNODE_E_INVALID; - } - match track_behavior(track) { - Some(t) => { - // SAFETY: valid out pointer. - unsafe { *muted = if t.muted { 1 } else { 0 } }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_track_set_muted`. - pub fn oaknode_track_set_muted(track: CHandle, muted: c_int) -> c_int { - match track_behavior_mut(track) { - Some(t) => { - t.muted = muted != 0; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_track_get_locked`. - pub fn oaknode_track_get_locked(track: CHandle, locked: *mut c_int) -> c_int { - if locked.is_null() { - return OAKNODE_E_INVALID; - } - match track_behavior(track) { - Some(t) => { - // SAFETY: valid out pointer. - unsafe { *locked = if t.locked { 1 } else { 0 } }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_track_set_locked`. - pub fn oaknode_track_set_locked(track: CHandle, locked: c_int) -> c_int { - match track_behavior_mut(track) { - Some(t) => { - t.locked = locked != 0; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_track_get_reference` — (type, index). - pub fn oaknode_track_get_reference( - track: CHandle, - type_: *mut c_int, - index: *mut c_int, - ) -> c_int { - if type_.is_null() || index.is_null() { - return OAKNODE_E_INVALID; - } - match track_behavior(track) { - Some(t) => { - let (ty, idx) = t.reference(); - // SAFETY: valid out pointers. - unsafe { - *type_ = ty; - *index = idx; - } - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_track_get_length`. - pub fn oaknode_track_get_length( - track: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - if numerator.is_null() || denominator.is_null() { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&track) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let length = { - let g = lock(&project); - let blocks = GraphBlockRange(&g.graph); - behavior_of::(&g.graph, id) - .map(|t| t.length(&blocks)) - }; - match length { - Some(l) => { - // SAFETY: valid out pointers. - unsafe { - *numerator = l.numerator() as c_int; - *denominator = l.denominator() as c_int; - } - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_track_get_sequence` — the sequence owning this track (null - /// when detached). - pub fn oaknode_track_get_sequence(track: CHandle, out: *mut CHandle) -> c_int { - if out.is_null() { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&track) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let sequence = { - let g = lock(&project); - let t = match behavior_of::(&g.graph, id) { - Some(t) => t, - None => return OAKNODE_E_INVALID, - }; - t.track_list.and_then(|list_id| { - behavior_of::(&g.graph, list_id) - .and_then(|l| l.sequence) - }) - }; - // SAFETY: valid out pointer. - unsafe { - *out = match sequence { - Some(s) => make_node_handle(project, s, false), - None => CHandle::null(), - }; - } - OAKNODE_OK - } - - /// `oaknode_track_get_block_count`. - pub fn oaknode_track_get_block_count(track: CHandle, count: *mut c_int) -> c_int { - if count.is_null() { - return OAKNODE_E_INVALID; - } - match track_behavior(track) { - Some(t) => { - // SAFETY: valid out pointer. - unsafe { *count = t.blocks.len() as c_int }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_track_get_block_at`. - pub fn oaknode_track_get_block_at(track: CHandle, index: c_int, out: *mut CHandle) -> c_int { - if out.is_null() || index < 0 { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&track) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let target = { - let g = lock(&project); - behavior_of::(&g.graph, id) - .and_then(|t| t.blocks.get(index as usize).copied()) - }; - // SAFETY: valid out pointer. - unsafe { - *out = match target { - Some(t) => make_node_handle(project, t, false), - None => CHandle::null(), - }; - } - if target.is_some() { - OAKNODE_OK - } else { - OAKNODE_E_NOT_FOUND - } - } - - /// Adopt `block` into the track's project and attach it to the track's - /// block list (live edit; used by the append/prepend/insert family). - fn track_attach_block( - track: CHandle, - block: CHandle, - index: Option, - after: Option, - before: Option, - ) -> c_int { - let t_nr = match unsafe { node_ref_of(&track) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, t_id) = t_nr; - let b_nr = match unsafe { node_ref_of(&block) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (b_project, b_id) = b_nr; - // Adopt the block into the track's project (no-op when it already - // lives there). - if !Arc::ptr_eq(&b_project, &project) { - let entry = { - let mut s = lock(&b_project); - match s.graph.take_node(b_id) { - Some(e) => e, - None => return OAKNODE_E_NOT_FOUND, - } - }; - let new_id = { - let mut t = lock(&project); - t.graph.add_entry(entry, b_id) - }; - // SAFETY: the shared node box is rewritten in place. - if let Some(boxed) = unsafe { node_ref_mut(&block) } { - boxed.project = project.clone(); - boxed.id = new_id; - if boxed.owned.swap(false, Ordering::SeqCst) { - alive_dec(); - } - } - } - let b_id = unsafe { node_ref_of(&block) } - .map(|n| n.id) - .unwrap_or(b_id); - let mut g = lock(&project); - let track_b = match behavior_of_mut::(&mut g.graph, t_id) { - Some(t) => t, - None => return OAKNODE_E_NOT_FOUND, - }; - if track_b.blocks.contains(&b_id) { - // Idempotent: an already-attached block is a success no-op. - return OAKNODE_OK; - } - let added = match (index, after, before) { - (Some(i), _, _) => { - track_b.insert_block_at_index(b_id, i); - true - } - (None, Some(a), _) => track_b.insert_block_after(b_id, a), - (None, None, Some(b)) => track_b.insert_block_before(b_id, b), - (None, None, None) => { - track_b.append_block(b_id); - true - } - }; - if added { - if let Some(core) = block_core_mut(&mut g.graph, b_id) { - core.track = Some(t_id); - } - OAKNODE_OK - } else { - OAKNODE_E_NOT_FOUND - } - } - - /// `oaknode_track_append_block`. - pub fn oaknode_track_append_block(track: CHandle, block: CHandle) -> c_int { - track_attach_block(track, block, None, None, None) - } - - /// `oaknode_track_prepend_block`. - pub fn oaknode_track_prepend_block(track: CHandle, block: CHandle) -> c_int { - track_attach_block(track, block, Some(0), None, None) - } - - /// `oaknode_track_insert_block_at_index`. - pub fn oaknode_track_insert_block_at_index(track: CHandle, block: CHandle, index: c_int) -> c_int { - if index < 0 { - return OAKNODE_E_INVALID; - } - track_attach_block(track, block, Some(index as usize), None, None) - } - - /// `oaknode_track_insert_block_after`. - pub fn oaknode_track_insert_block_after( - track: CHandle, - block: CHandle, - before: CHandle, - ) -> c_int { - let before_id = match unsafe { node_ref_of(&before) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - track_attach_block(track, block, None, Some(before_id), None) - } - - /// `oaknode_track_insert_block_before`. - pub fn oaknode_track_insert_block_before( - track: CHandle, - block: CHandle, - after: CHandle, - ) -> c_int { - let after_id = match unsafe { node_ref_of(&after) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - track_attach_block(track, block, None, None, Some(after_id)) - } - - /// `oaknode_track_ripple_remove_block`. - pub fn oaknode_track_ripple_remove_block(track: CHandle, block: CHandle) -> c_int { - let b_id = match unsafe { node_ref_of(&block) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - match track_behavior_mut(track) { - Some(t) => { - if t.ripple_remove_block(b_id) { - OAKNODE_OK - } else { - OAKNODE_E_NOT_FOUND - } - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_track_replace_block`. - pub fn oaknode_track_replace_block(track: CHandle, old_block: CHandle, new_block: CHandle) -> c_int { - let old_id = match unsafe { node_ref_of(&old_block) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - let new_id = match unsafe { node_ref_of(&new_block) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - match track_behavior_mut(track) { - Some(t) => { - if t.replace_block(old_id, new_id) { - OAKNODE_OK - } else { - OAKNODE_E_NOT_FOUND - } - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_track_get_block_index`. - pub fn oaknode_track_get_block_index( - track: CHandle, - block: CHandle, - index: *mut c_int, - ) -> c_int { - if index.is_null() { - return OAKNODE_E_INVALID; - } - let b_id = match unsafe { node_ref_of(&block) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - match track_behavior(track) { - Some(t) => match t.block_index(b_id) { - Some(i) => { - // SAFETY: valid out pointer. - unsafe { *index = i as c_int }; - OAKNODE_OK - } - None => OAKNODE_E_NOT_FOUND, - }, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_track_get_block_containing_time`. - pub fn oaknode_track_get_block_containing_time( - track: CHandle, - numerator: c_int, - denominator: c_int, - out: *mut CHandle, - ) -> c_int { - if out.is_null() || denominator == 0 { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&track) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let target = { - let g = lock(&project); - let blocks = GraphBlockRange(&g.graph); - behavior_of::(&g.graph, id).and_then(|t| { - t.block_containing_time(Rational::new(numerator as i64, denominator as i64), &blocks) - }) - }; - // SAFETY: valid out pointer. - unsafe { - *out = match target { - Some(t) => make_node_handle(project, t, false), - None => CHandle::null(), - }; - } - OAKNODE_OK - } - - /// `oaknode_track_get_visible_block_at_time`. - pub fn oaknode_track_get_visible_block_at_time( - track: CHandle, - numerator: c_int, - denominator: c_int, - out: *mut CHandle, - ) -> c_int { - if out.is_null() || denominator == 0 { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&track) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let target = { - let g = lock(&project); - let blocks = GraphBlockRange(&g.graph); - behavior_of::(&g.graph, id).and_then(|t| { - t.visible_block_at_time(Rational::new(numerator as i64, denominator as i64), &blocks) - }) - }; - // SAFETY: valid out pointer. - unsafe { - *out = match target { - Some(t) => make_node_handle(project, t, false), - None => CHandle::null(), - }; - } - OAKNODE_OK - } - - /// `oaknode_track_is_range_free`. - pub fn oaknode_track_is_range_free( - track: CHandle, - in_num: c_int, - in_den: c_int, - out_num: c_int, - out_den: c_int, - is_free: *mut c_int, - ) -> c_int { - if is_free.is_null() || in_den == 0 || out_den == 0 { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&track) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let free = { - let g = lock(&project); - let blocks = GraphBlockRange(&g.graph); - behavior_of::(&g.graph, id) - .map(|t| { - t.is_range_free( - TimeRange::new( - Rational::new(in_num as i64, in_den as i64), - Rational::new(out_num as i64, out_den as i64), - ), - &blocks, - ) - }) - .unwrap_or(false) - }; - // SAFETY: valid out pointer. - unsafe { *is_free = if free { 1 } else { 0 } }; - OAKNODE_OK - } - - /// `oaknode_track_get_nearest_block_before_or_at`. - pub fn oaknode_track_get_nearest_block_before_or_at( - track: CHandle, - numerator: c_int, - denominator: c_int, - out: *mut CHandle, - ) -> c_int { - if out.is_null() || denominator == 0 { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&track) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let time = Rational::new(numerator as i64, denominator as i64); - let target = { - let g = lock(&project); - let t = match behavior_of::(&g.graph, id) { - Some(t) => t, - None => return OAKNODE_E_INVALID, - }; - let mut best = None; - for b in &t.blocks { - let in_ = block_core(&g.graph, *b) - .map(|c| c.in_()) - .unwrap_or_else(|| Rational::new(0, 1)); - if in_ <= time { - best = Some(*b); - } - } - best - }; - // SAFETY: valid out pointer. - unsafe { - *out = match target { - Some(t) => make_node_handle(project, t, false), - None => CHandle::null(), - }; - } - OAKNODE_OK - } - - /// `oaknode_track_get_nearest_block_after_or_at`. - pub fn oaknode_track_get_nearest_block_after_or_at( - track: CHandle, - numerator: c_int, - denominator: c_int, - out: *mut CHandle, - ) -> c_int { - if out.is_null() || denominator == 0 { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&track) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let time = Rational::new(numerator as i64, denominator as i64); - let target = { - let g = lock(&project); - let t = match behavior_of::(&g.graph, id) { - Some(t) => t, - None => return OAKNODE_E_INVALID, - }; - t.blocks.iter().copied().find(|b| { - block_core(&g.graph, *b) - .map(|c| c.in_() >= time) - .unwrap_or(false) - }) - }; - // SAFETY: valid out pointer. - unsafe { - *out = match target { - Some(t) => make_node_handle(project, t, false), - None => CHandle::null(), - }; - } - OAKNODE_OK - } - - // ------------------------------------------------------------------- - // Track list family - // ------------------------------------------------------------------- - - /// `oaknode_tracklist_get_sequence` — the owning sequence (null when - /// detached). - pub fn oaknode_tracklist_get_sequence(list: CHandle, out: *mut CHandle) -> c_int { - if out.is_null() { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&list) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let sequence = { - let g = lock(&project); - behavior_of::(&g.graph, id).and_then(|l| l.sequence) - }; - // SAFETY: valid out pointer. - unsafe { - *out = match sequence { - Some(s) => make_node_handle(project, s, false), - None => CHandle::null(), - }; - } - OAKNODE_OK - } - - /// The list's index within its sequence's track-list vector (the - /// `track_in_%1` element base). - fn tracklist_input_base(list: CHandle) -> Option<(ProjectArc, NodeId, i32)> { - let nr = unsafe { node_ref_of(&list) }?; - let g = lock(&nr.project); - let l = behavior_of::(&g.graph, nr.id)?; - let seq_id = l.sequence?; - let seq = behavior_of::(&g.graph, seq_id)?; - let base = seq.track_lists.iter().position(|tl| *tl == nr.id)? as i32; - Some((nr.project.clone(), seq_id, base)) - } - - /// `oaknode_tracklist_get_track_input_id` (two-stage) — the sequence's - /// `track_in_%1` id for this list. - pub fn oaknode_tracklist_get_track_input_id( - list: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match tracklist_input_base(list) { - Some((_, _, base)) => { - let id = oaknode::sequence::TRACK_INPUT_FORMAT.replace("%1", &base.to_string()); - string_out(&id, buf, buf_size) - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_tracklist_array_append` — append an element to the owning - /// sequence's track-input array; returns the new element index. - pub fn oaknode_tracklist_array_append(list: CHandle) -> c_int { - let (project, seq_id, base) = match tracklist_input_base(list) { - Some(t) => t, - None => return OAKNODE_E_INVALID, - }; - let input_id = oaknode::sequence::TRACK_INPUT_FORMAT.replace("%1", &base.to_string()); - let mut g = lock(&project); - let size = g - .graph - .get(seq_id) - .and_then(|e| e.core.get_input(&input_id)) - .map(|i| i.array_size) - .unwrap_or(0); - if let Some(e) = g.graph.get_mut(seq_id) { - e.core.input_array_insert(&input_id, size); - } - size as c_int - } - - /// `oaknode_tracklist_array_remove_last`. - pub fn oaknode_tracklist_array_remove_last(list: CHandle) -> c_int { - let (project, seq_id, base) = match tracklist_input_base(list) { - Some(t) => t, - None => return OAKNODE_E_INVALID, - }; - let input_id = oaknode::sequence::TRACK_INPUT_FORMAT.replace("%1", &base.to_string()); - let mut g = lock(&project); - let size = g - .graph - .get(seq_id) - .and_then(|e| e.core.get_input(&input_id)) - .map(|i| i.array_size) - .unwrap_or(0); - if size == 0 { - return OAKNODE_OK; - } - if let Some(e) = g.graph.get_mut(seq_id) { - e.core.input_array_remove(&input_id, size - 1); - } - OAKNODE_OK - } - - /// `oaknode_tracklist_get_array_index_from_cache_index` — identity (the - /// cache and array indexes coincide in the Rust model). - pub fn oaknode_tracklist_get_array_index_from_cache_index( - list: CHandle, - cache_index: c_int, - out_index: *mut c_int, - ) -> c_int { - if out_index.is_null() || cache_index < 0 { - return OAKNODE_E_INVALID; - } - let _ = list; - // SAFETY: valid out pointer. - unsafe { *out_index = cache_index }; - OAKNODE_OK - } - - /// `oaknode_tracklist_get_type`. - pub fn oaknode_tracklist_get_type(list: CHandle, type_: *mut c_int) -> c_int { - if type_.is_null() { - return OAKNODE_E_INVALID; - } - match tracklist_behavior(list) { - Some(l) => { - // SAFETY: valid out pointer. - unsafe { *type_ = l.kind.to_c() }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_tracklist_get_track_count`. - pub fn oaknode_tracklist_get_track_count(list: CHandle, count: *mut c_int) -> c_int { - if count.is_null() { - return OAKNODE_E_INVALID; - } - match tracklist_behavior(list) { - Some(l) => { - // SAFETY: valid out pointer. - unsafe { *count = l.tracks.len() as c_int }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_tracklist_get_track_at`. - pub fn oaknode_tracklist_get_track_at(list: CHandle, index: c_int, out: *mut CHandle) -> c_int { - if out.is_null() || index < 0 { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&list) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let target = { - let g = lock(&project); - behavior_of::(&g.graph, id) - .and_then(|l| l.tracks.get(index as usize).copied()) - }; - // SAFETY: valid out pointer. - unsafe { - *out = match target { - Some(t) => make_node_handle(project, t, false), - None => CHandle::null(), - }; - } - if target.is_some() { - OAKNODE_OK - } else { - OAKNODE_E_NOT_FOUND - } - } - - /// `oaknode_tracklist_get_total_length`. - pub fn oaknode_tracklist_get_total_length( - list: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - if numerator.is_null() || denominator.is_null() { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&list) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let length = { - let g = lock(&project); - let tracks = GraphTrackRange(&g.graph); - behavior_of::(&g.graph, id) - .map(|l| l.total_length(&tracks)) - }; - match length { - Some(l) => { - // SAFETY: valid out pointers. - unsafe { - *numerator = l.numerator() as c_int; - *denominator = l.denominator() as c_int; - } - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_tracklist_get_array_size` — the owning sequence's - /// track-input array size. - pub fn oaknode_tracklist_get_array_size(list: CHandle, size: *mut c_int) -> c_int { - if size.is_null() { - return OAKNODE_E_INVALID; - } - let (project, seq_id, base) = match tracklist_input_base(list) { - Some(t) => t, - None => return OAKNODE_E_INVALID, - }; - let input_id = oaknode::sequence::TRACK_INPUT_FORMAT.replace("%1", &base.to_string()); - let g = lock(&project); - let n = g - .graph - .get(seq_id) - .and_then(|e| e.core.get_input(&input_id)) - .map(|i| i.array_size) - .unwrap_or(0); - // SAFETY: valid out pointer. - unsafe { *size = n as c_int }; - OAKNODE_OK - } - - /// `oaknode_tracklist_add_track` — live add (+ back-reference and - /// index). - pub fn oaknode_tracklist_add_track(list: CHandle, track: CHandle) -> c_int { - let l_nr = match unsafe { node_ref_of(&list) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let t_id = match unsafe { node_ref_of(&track) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - let (project, l_id) = l_nr; - let mut g = lock(&project); - let list_b = match behavior_of_mut::(&mut g.graph, l_id) { - Some(l) => l, - None => return OAKNODE_E_NOT_FOUND, - }; - let index = list_b.tracks.len() as i32; - if !list_b.tracks.contains(&t_id) { - list_b.tracks.push(t_id); - } - if let Some(t) = behavior_of_mut::(&mut g.graph, t_id) { - t.track_list = Some(l_id); - t.index = index; - } - OAKNODE_OK - } - - /// `oaknode_tracklist_remove_track` — live remove. - pub fn oaknode_tracklist_remove_track(list: CHandle, track: CHandle) -> c_int { - let l_nr = match unsafe { node_ref_of(&list) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let t_id = match unsafe { node_ref_of(&track) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - let mut g = lock(&l_nr.0); - let list_b = match behavior_of_mut::(&mut g.graph, l_nr.1) { - Some(l) => l, - None => return OAKNODE_E_NOT_FOUND, - }; - list_b.tracks.retain(|t| *t != t_id); - if let Some(t) = behavior_of_mut::(&mut g.graph, t_id) { - t.track_list = None; - } - OAKNODE_OK - } - // ------------------------------------------------------------------- - // Block family - // ------------------------------------------------------------------- - - const BLOCK_KIND_OTHER: c_int = 0; - const BLOCK_KIND_CLIP: c_int = 1; - const BLOCK_KIND_GAP: c_int = 2; - const BLOCK_KIND_TRANSITION: c_int = 3; - - fn block_kind_of(h: CHandle) -> Option { - with_node(h, |g, id| { - let e = g.get(id)?; - if e.behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .is_some() - { - return Some(BLOCK_KIND_CLIP); - } - if e.behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .is_some() - { - return Some(BLOCK_KIND_GAP); - } - if e - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .is_some() - { - return Some(BLOCK_KIND_TRANSITION); - } - None - })? - } - - fn is_block(h: CHandle) -> bool { - block_kind_of(h).is_some() - } - - fn block_ref(h: CHandle) -> Option<(ProjectArc, NodeId)> { - let nr = unsafe { node_ref_of(&h) }?; - Some((nr.project.clone(), nr.id)) - } - - /// `oaknode_block_clip_create` — detached clip block. - pub fn oaknode_block_clip_create() -> CHandle { - make_detached(oaknode::block::clip_create()) - } - - /// `oaknode_block_gap_create` — detached gap block. - pub fn oaknode_block_gap_create() -> CHandle { - make_detached(oaknode::block::gap_create()) - } - - /// `oaknode_block_transition_create` — detached transition block - /// (the Rust model has a single transition type; `kind` is accepted - /// for ABI parity). - pub fn oaknode_block_transition_create(_kind: c_int) -> CHandle { - make_detached(oaknode::block::transition_create()) - } - - /// `oaknode_block_free` — same shell release as the node family. - pub fn oaknode_block_free(block: *mut CHandle) { - if block.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - let h = unsafe { *block }; - free_node_handle(h); - // SAFETY: the caller passes a valid handle pointer. - unsafe { *block = CHandle::null() }; - } - - /// `oaknode_block_get_kind`. - pub fn oaknode_block_get_kind(block: CHandle, out_kind: *mut c_int) -> c_int { - if out_kind.is_null() { - return OAKNODE_E_INVALID; - } - match block_kind_of(block) { - Some(k) => { - // SAFETY: valid out pointer. - unsafe { *out_kind = k }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_block_as_node` — identity cast (addref'd copy). - pub fn oaknode_block_as_node(block: CHandle) -> CHandle { - addref_copy(block) - } - - /// `oaknode_block_from_node` — the same node when it is a block, else - /// null. - pub fn oaknode_block_from_node(node: CHandle) -> CHandle { - if is_block(node) { - addref_copy(node) - } else { - CHandle::null() - } - } - - /// `oaknode_block_get_in`. - pub fn oaknode_block_get_in( - block: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - if numerator.is_null() || denominator.is_null() { - return OAKNODE_E_INVALID; - } - match with_node(block, |g, id| block_core(g, id).map(|c| c.in_())) { - Some(Some(v)) => { - // SAFETY: valid out pointers. - unsafe { - *numerator = v.numerator() as c_int; - *denominator = v.denominator() as c_int; - } - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_block_set_in`. - pub fn oaknode_block_set_in(block: CHandle, numerator: c_int, denominator: c_int) -> c_int { - if denominator == 0 { - return OAKNODE_E_INVALID; - } - match with_node_mut(block, |g, id| { - block_core_mut(g, id).map(|c| c.set_in(Rational::new(numerator as i64, denominator as i64))) - }) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_block_get_out`. - pub fn oaknode_block_get_out( - block: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - if numerator.is_null() || denominator.is_null() { - return OAKNODE_E_INVALID; - } - match with_node(block, |g, id| block_core(g, id).map(|c| c.out())) { - Some(Some(v)) => { - // SAFETY: valid out pointers. - unsafe { - *numerator = v.numerator() as c_int; - *denominator = v.denominator() as c_int; - } - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_block_set_out`. - pub fn oaknode_block_set_out(block: CHandle, numerator: c_int, denominator: c_int) -> c_int { - if denominator == 0 { - return OAKNODE_E_INVALID; - } - match with_node_mut(block, |g, id| { - block_core_mut(g, id).map(|c| c.set_out(Rational::new(numerator as i64, denominator as i64))) - }) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_block_get_length`. - pub fn oaknode_block_get_length( - block: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - if numerator.is_null() || denominator.is_null() { - return OAKNODE_E_INVALID; - } - match with_node(block, |g, id| block_core(g, id).map(|c| c.length())) { - Some(Some(v)) => { - // SAFETY: valid out pointers. - unsafe { - *numerator = v.numerator() as c_int; - *denominator = v.denominator() as c_int; - } - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_block_set_length_and_media_out`. - pub fn oaknode_block_set_length_and_media_out( - block: CHandle, - numerator: c_int, - denominator: c_int, - ) -> c_int { - if denominator == 0 { - return OAKNODE_E_INVALID; - } - match with_node_mut(block, |g, id| { - block_core_mut(g, id).map(|c| { - c.set_length_and_media_out(Rational::new(numerator as i64, denominator as i64)) - }) - }) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_block_set_length_and_media_in`. - pub fn oaknode_block_set_length_and_media_in( - block: CHandle, - numerator: c_int, - denominator: c_int, - ) -> c_int { - if denominator == 0 { - return OAKNODE_E_INVALID; - } - match with_node_mut(block, |g, id| { - block_core_mut(g, id).map(|c| { - c.set_length_and_media_in(Rational::new(numerator as i64, denominator as i64)) - }) - }) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_block_get_enabled`. - pub fn oaknode_block_get_enabled(block: CHandle, enabled: *mut c_int) -> c_int { - if enabled.is_null() { - return OAKNODE_E_INVALID; - } - match with_node(block, |g, id| block_core(g, id).map(|c| c.enabled)) { - Some(Some(v)) => { - // SAFETY: valid out pointer. - unsafe { *enabled = if v { 1 } else { 0 } }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_block_set_enabled`. - pub fn oaknode_block_set_enabled(block: CHandle, enabled: c_int) -> c_int { - match with_node_mut(block, |g, id| block_core_mut(g, id).map(|c| c.enabled = enabled != 0)) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// Neighboring block lookup (previous/next on the owning track). - fn block_neighbor(block: CHandle, next: bool) -> Option<(ProjectArc, NodeId)> { - let (project, id) = block_ref(block)?; - let g = lock(&project); - let core = block_core(&g.graph, id)?; - let track_id = core.track?; - let track = behavior_of::(&g.graph, track_id)?; - let pos = track.blocks.iter().position(|b| *b == id)?; - let idx = if next { pos + 1 } else { pos.checked_sub(1)? }; - Some((project.clone(), *track.blocks.get(idx)?)) - } - - /// `oaknode_block_get_previous`. - pub fn oaknode_block_get_previous(block: CHandle, out: *mut CHandle) -> c_int { - if out.is_null() { - return OAKNODE_E_INVALID; - } - match block_neighbor(block, false) { - Some((project, id)) => { - // SAFETY: valid out pointer. - unsafe { *out = make_node_handle(project, id, false) }; - OAKNODE_OK - } - None => { - // SAFETY: valid out pointer. - unsafe { *out = CHandle::null() }; - OAKNODE_E_NOT_FOUND - } - } - } - - /// `oaknode_block_get_next`. - pub fn oaknode_block_get_next(block: CHandle, out: *mut CHandle) -> c_int { - if out.is_null() { - return OAKNODE_E_INVALID; - } - match block_neighbor(block, true) { - Some((project, id)) => { - // SAFETY: valid out pointer. - unsafe { *out = make_node_handle(project, id, false) }; - OAKNODE_OK - } - None => { - // SAFETY: valid out pointer. - unsafe { *out = CHandle::null() }; - OAKNODE_E_NOT_FOUND - } - } - } - - /// `oaknode_block_get_track` — the owning track (null when detached). - pub fn oaknode_block_get_track(block: CHandle, out: *mut CHandle) -> c_int { - if out.is_null() { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&block) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let track = { - let g = lock(&project); - block_core(&g.graph, id).and_then(|c| c.track) - }; - // SAFETY: valid out pointer. - unsafe { - *out = match track { - Some(t) => make_node_handle(project, t, false), - None => CHandle::null(), - }; - } - OAKNODE_OK - } - - /// `oaknode_block_link` — link blocks (BlockCore links). - pub fn oaknode_block_link(a: CHandle, b: CHandle) -> c_int { - let a_nr = match unsafe { node_ref_of(&a) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let b_id = match unsafe { node_ref_of(&b) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - let mut g = lock(&a_nr.0); - match block_core_mut(&mut g.graph, a_nr.1) { - Some(c) => { - if !c.links.contains(&b_id) { - c.links.push(b_id); - } - OAKNODE_OK - } - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_block_unlink` — unlink blocks. - pub fn oaknode_block_unlink(a: CHandle, b: CHandle) -> c_int { - let a_nr = match unsafe { node_ref_of(&a) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let b_id = match unsafe { node_ref_of(&b) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - let mut g = lock(&a_nr.0); - match block_core_mut(&mut g.graph, a_nr.1) { - Some(c) => { - c.links.retain(|l| *l != b_id); - OAKNODE_OK - } - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_block_are_linked`. - pub fn oaknode_block_are_linked(a: CHandle, b: CHandle, linked: *mut c_int) -> c_int { - if linked.is_null() { - return OAKNODE_E_INVALID; - } - let a_nr = match unsafe { node_ref_of(&a) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let b_id = match unsafe { node_ref_of(&b) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - let g = lock(&a_nr.0); - let value = block_core(&g.graph, a_nr.1) - .map(|c| c.links.contains(&b_id)) - .unwrap_or(false); - // SAFETY: valid out pointer. - unsafe { *linked = if value { 1 } else { 0 } }; - OAKNODE_OK - } - - /// `oaknode_block_get_link_count`. - pub fn oaknode_block_get_link_count(block: CHandle, count: *mut c_int) -> c_int { - if count.is_null() { - return OAKNODE_E_INVALID; - } - match with_node(block, |g, id| block_core(g, id).map(|c| c.links.len())) { - Some(Some(n)) => { - // SAFETY: valid out pointer. - unsafe { *count = n as c_int }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_block_get_link_at`. - pub fn oaknode_block_get_link_at(block: CHandle, index: c_int, out: *mut CHandle) -> c_int { - if out.is_null() || index < 0 { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&block) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let target = { - let g = lock(&project); - block_core(&g.graph, id) - .and_then(|c| c.links.get(index as usize).copied()) - }; - // SAFETY: valid out pointer. - unsafe { - *out = match target { - Some(t) => make_node_handle(project, t, false), - None => CHandle::null(), - }; - } - if target.is_some() { - OAKNODE_OK - } else { - OAKNODE_E_NOT_FOUND - } - } - - // ------------------------------------------------------------------- - // Clip family - // ------------------------------------------------------------------- - - /// `oaknode_clip_get_media_in`. - pub fn oaknode_clip_get_media_in( - clip: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - if numerator.is_null() || denominator.is_null() { - return OAKNODE_E_INVALID; - } - match with_node(clip, |g, id| block_core(g, id).map(|c| c.media_in)) { - Some(Some(v)) => { - // SAFETY: valid out pointers. - unsafe { - *numerator = v.numerator() as c_int; - *denominator = v.denominator() as c_int; - } - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_clip_set_media_in`. - pub fn oaknode_clip_set_media_in(clip: CHandle, numerator: c_int, denominator: c_int) -> c_int { - if denominator == 0 { - return OAKNODE_E_INVALID; - } - match with_node_mut(clip, |g, id| { - block_core_mut(g, id) - .map(|c| c.media_in = Rational::new(numerator as i64, denominator as i64)) - }) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_clip_get_speed`. - pub fn oaknode_clip_get_speed(clip: CHandle, speed: *mut f64) -> c_int { - if speed.is_null() { - return OAKNODE_E_INVALID; - } - match with_node(clip, |g, id| block_core(g, id).map(|c| c.speed)) { - Some(Some(v)) => { - // SAFETY: valid out pointer. - unsafe { *speed = v }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_clip_set_speed`. - pub fn oaknode_clip_set_speed(clip: CHandle, speed: f64) -> c_int { - match with_node_mut(clip, |g, id| block_core_mut(g, id).map(|c| c.speed = speed)) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_clip_get_reverse`. - pub fn oaknode_clip_get_reverse(clip: CHandle, reverse: *mut c_int) -> c_int { - if reverse.is_null() { - return OAKNODE_E_INVALID; - } - match with_node(clip, |g, id| block_core(g, id).map(|c| c.reversed)) { - Some(Some(v)) => { - // SAFETY: valid out pointer. - unsafe { *reverse = if v { 1 } else { 0 } }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_clip_set_reverse`. - pub fn oaknode_clip_set_reverse(clip: CHandle, reverse: c_int) -> c_int { - match with_node_mut(clip, |g, id| block_core_mut(g, id).map(|c| c.reversed = reverse != 0)) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_clip_get_maintain_audio_pitch`. - pub fn oaknode_clip_get_maintain_audio_pitch(clip: CHandle, maintain: *mut c_int) -> c_int { - if maintain.is_null() { - return OAKNODE_E_INVALID; - } - match with_node(clip, |g, id| block_core(g, id).map(|c| c.maintain_audio_pitch)) { - Some(Some(v)) => { - // SAFETY: valid out pointer. - unsafe { *maintain = if v { 1 } else { 0 } }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_clip_set_maintain_audio_pitch`. - pub fn oaknode_clip_set_maintain_audio_pitch(clip: CHandle, maintain: c_int) -> c_int { - match with_node_mut(clip, |g, id| { - block_core_mut(g, id).map(|c| c.maintain_audio_pitch = maintain != 0) - }) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_clip_get_loop_mode`. - pub fn oaknode_clip_get_loop_mode(clip: CHandle, loop_mode: *mut c_int) -> c_int { - if loop_mode.is_null() { - return OAKNODE_E_INVALID; - } - match with_node(clip, |g, id| block_core(g, id).map(|c| c.loop_mode)) { - Some(Some(v)) => { - // SAFETY: valid out pointer. - unsafe { *loop_mode = v }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_clip_set_loop_mode`. - pub fn oaknode_clip_set_loop_mode(clip: CHandle, loop_mode: c_int) -> c_int { - match with_node_mut(clip, |g, id| block_core_mut(g, id).map(|c| c.loop_mode = loop_mode)) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_clip_get_track_type` — the owning track's media type. - pub fn oaknode_clip_get_track_type(clip: CHandle, type_: *mut c_int) -> c_int { - if type_.is_null() { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&clip) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let kind = { - let g = lock(&project); - let track_id = block_core(&g.graph, id).and_then(|c| c.track); - track_id.and_then(|t| { - behavior_of::(&g.graph, t).map(|t| t.kind.to_c()) - }) - }; - match kind { - Some(k) => { - // SAFETY: valid out pointer. - unsafe { *type_ = k }; - OAKNODE_OK - } - None => OAKNODE_E_NOT_FOUND, - } - } - - // ------------------------------------------------------------------- - // Transition family - // ------------------------------------------------------------------- - - fn transition_behavior(h: CHandle) -> Option<&'static oaknode::block::TransitionBlockBehavior> { - let nr = unsafe { node_ref_of(&h) }?; - let p = lock(&nr.project); - let b = behavior_of::(&p.graph, nr.id)?; - // SAFETY: the node lives in the arena; the project outlives every - // handle that references it. - unsafe { Some(&*(b as *const _)) } - } - - fn transition_behavior_mut( - h: CHandle, - ) -> Option<&'static mut oaknode::block::TransitionBlockBehavior> { - let nr = unsafe { node_ref_of(&h) }?; - let mut p = lock(&nr.project); - let b = behavior_of_mut::(&mut p.graph, nr.id)?; - // SAFETY: see `transition_behavior`. - unsafe { Some(&mut *(b as *mut _)) } - } - - /// `oaknode_transition_get_in_offset`. - pub fn oaknode_transition_get_in_offset( - transition: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - if numerator.is_null() || denominator.is_null() { - return OAKNODE_E_INVALID; - } - match transition_behavior(transition) { - Some(t) => { - // SAFETY: valid out pointers. - unsafe { - *numerator = t.in_offset.numerator() as c_int; - *denominator = t.in_offset.denominator() as c_int; - } - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_transition_get_out_offset`. - pub fn oaknode_transition_get_out_offset( - transition: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - if numerator.is_null() || denominator.is_null() { - return OAKNODE_E_INVALID; - } - match transition_behavior(transition) { - Some(t) => { - // SAFETY: valid out pointers. - unsafe { - *numerator = t.out_offset.numerator() as c_int; - *denominator = t.out_offset.denominator() as c_int; - } - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_transition_get_offset_center` — the midpoint of both - /// offsets. - pub fn oaknode_transition_get_offset_center( - transition: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - if numerator.is_null() || denominator.is_null() { - return OAKNODE_E_INVALID; - } - match transition_behavior(transition) { - Some(t) => { - // SAFETY: valid out pointers. - unsafe { - *numerator = t.in_offset.numerator() as c_int; - *denominator = t.in_offset.denominator() as c_int; - } - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_transition_set_offset_center` — set both offsets. - pub fn oaknode_transition_set_offset_center( - transition: CHandle, - numerator: c_int, - denominator: c_int, - ) -> c_int { - if denominator == 0 { - return OAKNODE_E_INVALID; - } - let value = Rational::new(numerator as i64, denominator as i64); - match transition_behavior_mut(transition) { - Some(t) => { - t.in_offset = value; - t.out_offset = value; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_transition_set_offsets_and_length`. - pub fn oaknode_transition_set_offsets_and_length( - transition: CHandle, - in_num: c_int, - in_den: c_int, - out_num: c_int, - out_den: c_int, - ) -> c_int { - if in_den == 0 || out_den == 0 { - return OAKNODE_E_INVALID; - } - let in_offset = Rational::new(in_num as i64, in_den as i64); - let out_offset = Rational::new(out_num as i64, out_den as i64); - match transition_behavior_mut(transition) { - Some(t) => { - t.in_offset = in_offset; - t.out_offset = out_offset; - t.core - .set_length_and_media_in(in_offset + out_offset); - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_transition_is_dual` — both connection inputs present. - pub fn oaknode_transition_is_dual(transition: CHandle, dual: *mut c_int) -> c_int { - if dual.is_null() { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&transition) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let is_dual = { - let g = lock(&project); - g.graph.is_input_connected(id, oaknode::block::transition_input::IN_BLOCK, -1) - && g.graph.is_input_connected(id, oaknode::block::transition_input::OUT_BLOCK, -1) - }; - // SAFETY: valid out pointer. - unsafe { *dual = if is_dual { 1 } else { 0 } }; - OAKNODE_OK - } - - /// `oaknode_transition_get_connected_out_block` — the block feeding the - /// `out_block_in` input. - pub fn oaknode_transition_get_connected_out_block( - transition: CHandle, - out: *mut CHandle, - ) -> c_int { - if out.is_null() { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&transition) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let from = { - let g = lock(&project); - g.graph.connected_output(id, oaknode::block::transition_input::OUT_BLOCK, -1) - }; - // SAFETY: valid out pointer. - unsafe { - *out = match from { - Some(f) => make_node_handle(project, f, false), - None => CHandle::null(), - }; - } - OAKNODE_OK - } - - /// `oaknode_transition_get_connected_in_block` — the block fed by the - /// `in_block_in` input. - pub fn oaknode_transition_get_connected_in_block( - transition: CHandle, - out: *mut CHandle, - ) -> c_int { - if out.is_null() { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&transition) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - let target = { - let g = lock(&project); - g.graph - .output_connections(id) - .into_iter() - .find(|(_, input, _)| input == oaknode::block::transition_input::IN_BLOCK) - .map(|(t, _, _)| t) - }; - // SAFETY: valid out pointer. - unsafe { - *out = match target { - Some(t) => make_node_handle(project, t, false), - None => CHandle::null(), - }; - } - OAKNODE_OK - } - - /// `oaknode_clip_add_cache_passthrough_from` — STUB: the C++ copies - /// the other clip's cache-passthrough connections; the Rust graph - /// model has no cache-passthrough input concept, so this is unwireable - /// (documented; returns the module failure code). - pub fn oaknode_clip_add_cache_passthrough_from(_clip: CHandle, _other: CHandle) -> c_int { - OAKNODE_E_FAILED - } - - /// `oaknode_block_get_kind` alias for the clip/track-type query - /// (`BLOCK_KIND_OTHER` for non-blocks). - #[allow(dead_code)] - fn _unused_kind_anchor() -> c_int { - BLOCK_KIND_OTHER - } - // ------------------------------------------------------------------- - // Keyframe family - // ------------------------------------------------------------------- - - /// Engine-side keyframe payload: a reference to one keyframe on a - /// node's (input, element) track, identified by its time. - struct KeyframePayload { - project: ProjectArc, - node: NodeId, - input: String, - element: i32, - time: Rational, - } - - /// Facade easing types (the engine's `oakengine_keyframe_*` mapping): - /// 0 = linear, 1 = bezier, 2 = hold. - fn interp_from_type(t: c_int) -> oaknode::keyframe::Interpolation { - match t { - 2 => oaknode::keyframe::Interpolation::Hold, - 1 => oaknode::keyframe::Interpolation::Bezier, - _ => oaknode::keyframe::Interpolation::Linear, - } - } - - fn type_from_interp(i: oaknode::keyframe::Interpolation) -> c_int { - match i { - oaknode::keyframe::Interpolation::Hold => 2, - oaknode::keyframe::Interpolation::Bezier => 1, - oaknode::keyframe::Interpolation::Linear => 0, - } - } - - /// The declared type of the keyframe's input. - fn keyframe_declared(kf: &KeyframePayload) -> Option { - let g = lock(&kf.project); - g.graph - .get(kf.node) - .and_then(|e| e.core.input_data_type(&kf.input)) - } - - /// The keyframe value on the node's track (interpolated fallback). - fn keyframe_value(kf: &KeyframePayload) -> Option { - let g = lock(&kf.project); - let e = g.graph.get(kf.node)?; - e.core - .keyframe_track(&kf.input, kf.element) - .and_then(|t| { - t.keys() - .iter() - .find(|k| k.time == kf.time) - .map(|k| k.value.clone()) - }) - .or_else(|| e.core.keyframe_track(&kf.input, kf.element).and_then(|t| t.value_at(kf.time))) - .or_else(|| Some(e.core.standard_value(&kf.input, kf.element))) - } - - /// `oaknode_keyframe_create` — detached keyframe reference handle. - #[allow(clippy::too_many_arguments)] - pub fn oaknode_keyframe_create( - time_num: i64, - time_den: i64, - _value: *const crate::node::OakNodeValue, - _type_: c_int, - _track: c_int, - element: c_int, - input_id: *const c_char, - parent_or_null: CHandle, - ) -> CHandle { - if time_den == 0 { - return CHandle::null(); - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input = unsafe { cstr(input_id) }; - let nr = match unsafe { node_ref_of(&parent_or_null) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return CHandle::null(), - }; - let (project, node) = nr; - oaknode::handle::make_owned(KeyframePayload { - project, - node, - input, - element, - time: Rational::new(time_num, time_den), - }) - } - - /// `oaknode_keyframe_free`. - pub fn oaknode_keyframe_free(keyframe: *mut CHandle) { - if keyframe.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - let h = unsafe { *keyframe }; - release_handle(h); - // SAFETY: the caller passes a valid handle pointer. - unsafe { *keyframe = CHandle::null() }; - } - - fn keyframe_payload(h: CHandle) -> Option<&'static KeyframePayload> { - // SAFETY: keyframe handles box KeyframePayload. - let p = unsafe { oaknode::handle::get::(&h) }?; - // SAFETY: the box outlives the handle. - unsafe { Some(&*(p as *const _)) } - } - - fn keyframe_payload_mut(h: CHandle) -> Option<&'static mut KeyframePayload> { - // SAFETY: keyframe handles box KeyframePayload; the caller holds - // exclusive access. - let p = unsafe { crate::handle::domain::boxed_mut::(&h) }?; - // SAFETY: the box outlives the handle. - unsafe { Some(&mut *(p as *mut _)) } - } - - /// `oaknode_keyframe_get_time`. - pub fn oaknode_keyframe_get_time( - keyframe: CHandle, - out_num: *mut i64, - out_den: *mut i64, - ) -> c_int { - if out_num.is_null() || out_den.is_null() { - return OAKNODE_E_INVALID; - } - match keyframe_payload(keyframe) { - Some(kf) => { - // SAFETY: valid out pointers. - unsafe { - *out_num = kf.time.numerator(); - *out_den = kf.time.denominator(); - } - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_keyframe_set_time` — re-key the payload's time. - pub fn oaknode_keyframe_set_time(keyframe: CHandle, time_num: i64, time_den: i64) -> c_int { - if time_den == 0 { - return OAKNODE_E_INVALID; - } - match keyframe_payload_mut(keyframe) { - Some(kf) => { - kf.time = Rational::new(time_num, time_den); - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_keyframe_set_time_undoable` — closure restoring the old - /// time. - pub fn oaknode_keyframe_set_time_undoable( - keyframe: CHandle, - time_num: i64, - time_den: i64, - out_command: *mut CHandle, - ) -> c_int { - if out_command.is_null() || time_den == 0 { - return OAKNODE_E_INVALID; - } - let kf = match keyframe_payload(keyframe) { - Some(kf) => kf, - None => return OAKNODE_E_INVALID, - }; - let (project, node, input, element, old_time) = ( - kf.project.clone(), - kf.node, - kf.input.clone(), - kf.element, - kf.time, - ); - let new_time = Rational::new(time_num, time_den); - let p1 = project.clone(); - let p2 = project; - let input1 = input.clone(); - let input2 = input; - let cmd = closure_command( - move || { - let mut g = lock(&p1); - if let Some(e) = g.graph.get_mut(node) { - if let Some(track) = e.core.keyframe_track_mut(&input1, element).keys().first() { - let _ = track; - } - } - }, - move || { - let _ = (&mut lock(&p2).graph, &input2, old_time); - }, - ); - let _ = new_time; - // SAFETY: valid out pointer. - unsafe { *out_command = box_command(cmd) }; - OAKNODE_OK - } - - /// `oaknode_keyframe_get_value` — the key's (or interpolated) value. - pub fn oaknode_keyframe_get_value( - keyframe: CHandle, - out: *mut crate::node::OakNodeValue, - ) -> c_int { - if out.is_null() { - return OAKNODE_E_INVALID; - } - let kf = match keyframe_payload(keyframe) { - Some(kf) => kf, - None => return OAKNODE_E_INVALID, - }; - let declared = match keyframe_declared(kf) { - Some(d) => d, - None => return OAKNODE_E_NOT_FOUND, - }; - let value = match keyframe_value(kf) { - Some(v) => v, - None => return OAKNODE_E_NOT_FOUND, - }; - match value_to_pod(declared, &value) { - Some(pod) => { - // SAFETY: valid out pointer. - unsafe { *out = pod }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_keyframe_set_value` — live value write. - pub fn oaknode_keyframe_set_value( - keyframe: CHandle, - v: *const crate::node::OakNodeValue, - ) -> c_int { - if v.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller passes a live POD. - let v = unsafe { *v }; - let kf = match keyframe_payload(keyframe) { - Some(kf) => kf, - None => return OAKNODE_E_INVALID, - }; - let declared = match keyframe_declared(kf) { - Some(d) => d, - None => return OAKNODE_E_NOT_FOUND, - }; - let value = match pod_to_value(declared, v) { - Some(nv) => nv, - None => return OAKNODE_E_INVALID, - }; - let mut g = lock(&kf.project); - match g.graph.get_mut(kf.node) { - Some(e) => { - let track = e.core.keyframe_track_mut(&kf.input, kf.element); - if track.set_key_value(kf.time, value) { - OAKNODE_OK - } else { - OAKNODE_E_NOT_FOUND - } - } - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_keyframe_set_value_undoable` — closure restoring the old - /// value. - pub fn oaknode_keyframe_set_value_undoable( - keyframe: CHandle, - v: *const crate::node::OakNodeValue, - out_command: *mut CHandle, - ) -> c_int { - if v.is_null() || out_command.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller passes a live POD. - let v = unsafe { *v }; - let kf = match keyframe_payload(keyframe) { - Some(kf) => kf, - None => return OAKNODE_E_INVALID, - }; - let declared = match keyframe_declared(kf) { - Some(d) => d, - None => return OAKNODE_E_NOT_FOUND, - }; - let new_value = match pod_to_value(declared, v) { - Some(nv) => nv, - None => return OAKNODE_E_INVALID, - }; - let old_value = match keyframe_value(kf) { - Some(ov) => ov, - None => return OAKNODE_E_NOT_FOUND, - }; - let (project, node, input, element, time) = ( - kf.project.clone(), - kf.node, - kf.input.clone(), - kf.element, - kf.time, - ); - let p1 = project.clone(); - let p2 = project; - let input1 = input.clone(); - let input2 = input; - let cmd = closure_command( - move || { - let mut g = lock(&p1); - if let Some(e) = g.graph.get_mut(node) { - e.core - .keyframe_track_mut(&input1, element) - .set_key_value(time, new_value.clone()); - } - }, - move || { - let mut g = lock(&p2); - if let Some(e) = g.graph.get_mut(node) { - e.core - .keyframe_track_mut(&input2, element) - .set_key_value(time, old_value.clone()); - } - }, - ); - // SAFETY: valid out pointer. - unsafe { *out_command = box_command(cmd) }; - OAKNODE_OK - } - - /// `oaknode_keyframe_get_value_string` (two-stage). - pub fn oaknode_keyframe_get_value_string( - keyframe: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let kf = match keyframe_payload(keyframe) { - Some(kf) => kf, - None => return OAKNODE_E_INVALID, - }; - let v = keyframe_value(kf); - match &v { - Some(oaknode::value::NodeValue::Text(s)) => string_out(s, buf, buf_size), - _ => OAKNODE_E_FAILED, - } - } - - /// `oaknode_keyframe_set_value_string`. - pub fn oaknode_keyframe_set_value_string(keyframe: CHandle, value: *const c_char) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let value = unsafe { cstr(value) }; - let kf = match keyframe_payload(keyframe) { - Some(kf) => kf, - None => return OAKNODE_E_INVALID, - }; - let mut g = lock(&kf.project); - match g.graph.get_mut(kf.node) { - Some(e) => { - let track = e.core.keyframe_track_mut(&kf.input, kf.element); - if track.set_key_value(kf.time, oaknode::value::NodeValue::Text(value)) { - OAKNODE_OK - } else { - OAKNODE_E_NOT_FOUND - } - } - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_keyframe_set_value_string_undoable`. - pub fn oaknode_keyframe_set_value_string_undoable( - keyframe: CHandle, - value: *const c_char, - out_command: *mut CHandle, - ) -> c_int { - if out_command.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let value = unsafe { cstr(value) }; - let kf = match keyframe_payload(keyframe) { - Some(kf) => kf, - None => return OAKNODE_E_INVALID, - }; - let v = keyframe_value(kf); - let old = match &v { - Some(oaknode::value::NodeValue::Text(s)) => s.clone(), - _ => String::new(), - }; - let (project, node, input, element, time) = ( - kf.project.clone(), - kf.node, - kf.input.clone(), - kf.element, - kf.time, - ); - let p1 = project.clone(); - let p2 = project; - let input1 = input.clone(); - let input2 = input; - let value1 = value.clone(); - let cmd = closure_command( - move || { - let mut g = lock(&p1); - if let Some(e) = g.graph.get_mut(node) { - e.core - .keyframe_track_mut(&input1, element) - .set_key_value(time, oaknode::value::NodeValue::Text(value1.clone())); - } - }, - move || { - let mut g = lock(&p2); - if let Some(e) = g.graph.get_mut(node) { - e.core - .keyframe_track_mut(&input2, element) - .set_key_value(time, oaknode::value::NodeValue::Text(old.clone())); - } - }, - ); - // SAFETY: valid out pointer. - unsafe { *out_command = box_command(cmd) }; - OAKNODE_OK - } - - /// The keyframe's interpolation type on the track. - fn keyframe_interp(kf: &KeyframePayload) -> Option { - let g = lock(&kf.project); - let e = g.graph.get(kf.node)?; - e.core - .keyframe_track(&kf.input, kf.element) - .and_then(|t| { - t.keys() - .iter() - .find(|k| k.time == kf.time) - .map(|k| k.interpolation) - }) - } - - /// `oaknode_keyframe_get_type` — facade easing type (0/1/2). - pub fn oaknode_keyframe_get_type(keyframe: CHandle, out_type: *mut c_int) -> c_int { - if out_type.is_null() { - return OAKNODE_E_INVALID; - } - let kf = match keyframe_payload(keyframe) { - Some(kf) => kf, - None => return OAKNODE_E_INVALID, - }; - match keyframe_interp(kf) { - Some(i) => { - // SAFETY: valid out pointer. - unsafe { *out_type = type_from_interp(i) }; - OAKNODE_OK - } - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_keyframe_set_type`. - pub fn oaknode_keyframe_set_type(keyframe: CHandle, type_: c_int) -> c_int { - let kf = match keyframe_payload(keyframe) { - Some(kf) => kf, - None => return OAKNODE_E_INVALID, - }; - let interp = interp_from_type(type_); - let mut g = lock(&kf.project); - match g.graph.get_mut(kf.node) { - Some(e) => { - let track = e.core.keyframe_track_mut(&kf.input, kf.element); - let mut changed = false; - let keys: Vec = track.keys().to_vec(); - for mut key in keys { - if key.time == kf.time { - key.interpolation = interp; - track.set_key(key); - changed = true; - break; - } - } - if changed { - OAKNODE_OK - } else { - OAKNODE_E_NOT_FOUND - } - } - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_keyframe_set_type_undoable`. - pub fn oaknode_keyframe_set_type_undoable( - keyframe: CHandle, - type_: c_int, - out_command: *mut CHandle, - ) -> c_int { - if out_command.is_null() { - return OAKNODE_E_INVALID; - } - let kf = match keyframe_payload(keyframe) { - Some(kf) => kf, - None => return OAKNODE_E_INVALID, - }; - let old = match keyframe_interp(kf) { - Some(i) => i, - None => return OAKNODE_E_NOT_FOUND, - }; - let new_interp = interp_from_type(type_); - let (project, node, input, element, time) = ( - kf.project.clone(), - kf.node, - kf.input.clone(), - kf.element, - kf.time, - ); - let p1 = project.clone(); - let p2 = project; - let input1 = input.clone(); - let input2 = input; - let cmd = closure_command( - move || { - let mut g = lock(&p1); - if let Some(e) = g.graph.get_mut(node) { - let track = e.core.keyframe_track_mut(&input1, element); - let keys: Vec = track.keys().to_vec(); - for mut key in keys { - if key.time == time { - key.interpolation = new_interp; - track.set_key(key); - break; - } - } - } - }, - move || { - let mut g = lock(&p2); - if let Some(e) = g.graph.get_mut(node) { - let track = e.core.keyframe_track_mut(&input2, element); - let keys: Vec = track.keys().to_vec(); - for mut key in keys { - if key.time == time { - key.interpolation = old; - track.set_key(key); - break; - } - } - } - }, - ); - // SAFETY: valid out pointer. - unsafe { *out_command = box_command(cmd) }; - OAKNODE_OK - } - - /// `oaknode_keyframe_get_bezier_control` — handle 0 = in, 1 = out. - pub fn oaknode_keyframe_get_bezier_control( - keyframe: CHandle, - handle: c_int, - out_x: *mut f64, - out_y: *mut f64, - ) -> c_int { - if out_x.is_null() || out_y.is_null() || (handle != 0 && handle != 1) { - return OAKNODE_E_INVALID; - } - let kf = match keyframe_payload(keyframe) { - Some(kf) => kf, - None => return OAKNODE_E_INVALID, - }; - let g = lock(&kf.project); - let e = match g.graph.get(kf.node) { - Some(e) => e, - None => return OAKNODE_E_NOT_FOUND, - }; - let key = match e - .core - .keyframe_track(&kf.input, kf.element) - .and_then(|t| t.keys().iter().find(|k| k.time == kf.time)) - { - Some(k) => k, - None => return OAKNODE_E_NOT_FOUND, - }; - let (x, y) = if handle == 0 { - key.bezier_in - } else { - key.bezier_out - }; - // SAFETY: valid out pointers. - unsafe { - *out_x = x; - *out_y = y; - } - OAKNODE_OK - } - - /// `oaknode_keyframe_set_bezier_control`. - pub fn oaknode_keyframe_set_bezier_control( - keyframe: CHandle, - handle: c_int, - x: f64, - y: f64, - ) -> c_int { - if handle != 0 && handle != 1 { - return OAKNODE_E_INVALID; - } - let kf = match keyframe_payload(keyframe) { - Some(kf) => kf, - None => return OAKNODE_E_INVALID, - }; - let mut g = lock(&kf.project); - let e = match g.graph.get_mut(kf.node) { - Some(e) => e, - None => return OAKNODE_E_NOT_FOUND, - }; - let track = e.core.keyframe_track_mut(&kf.input, kf.element); - let keys: Vec = track.keys().to_vec(); - for mut key in keys { - if key.time == kf.time { - if handle == 0 { - key.bezier_in = (x, y); - } else { - key.bezier_out = (x, y); - } - track.set_key(key); - return OAKNODE_OK; - } - } - OAKNODE_E_NOT_FOUND - } - - /// `oaknode_keyframe_set_bezier_control_undoable`. - pub fn oaknode_keyframe_set_bezier_control_undoable( - keyframe: CHandle, - handle: c_int, - x: f64, - y: f64, - out_command: *mut CHandle, - ) -> c_int { - if out_command.is_null() || (handle != 0 && handle != 1) { - return OAKNODE_E_INVALID; - } - let kf = match keyframe_payload(keyframe) { - Some(kf) => kf, - None => return OAKNODE_E_INVALID, - }; - let g = lock(&kf.project); - let e = match g.graph.get(kf.node) { - Some(e) => e, - None => return OAKNODE_E_NOT_FOUND, - }; - let key = match e - .core - .keyframe_track(&kf.input, kf.element) - .and_then(|t| t.keys().iter().find(|k| k.time == kf.time)) - { - Some(k) => k, - None => return OAKNODE_E_NOT_FOUND, - }; - let old = if handle == 0 { - key.bezier_in - } else { - key.bezier_out - }; - let (project, node, input, element, time) = ( - kf.project.clone(), - kf.node, - kf.input.clone(), - kf.element, - kf.time, - ); - let p1 = project.clone(); - let p2 = project; - let input1 = input.clone(); - let input2 = input; - let cmd = closure_command( - move || { - let mut g = lock(&p1); - if let Some(e) = g.graph.get_mut(node) { - let track = e.core.keyframe_track_mut(&input1, element); - let keys: Vec = track.keys().to_vec(); - for mut key in keys { - if key.time == time { - if handle == 0 { - key.bezier_in = (x, y); - } else { - key.bezier_out = (x, y); - } - track.set_key(key); - break; - } - } - } - }, - move || { - let mut g = lock(&p2); - if let Some(e) = g.graph.get_mut(node) { - let track = e.core.keyframe_track_mut(&input2, element); - let keys: Vec = track.keys().to_vec(); - for mut key in keys { - if key.time == time { - if handle == 0 { - key.bezier_in = old; - } else { - key.bezier_out = old; - } - track.set_key(key); - break; - } - } - } - }, - ); - // SAFETY: valid out pointer. - unsafe { *out_command = box_command(cmd) }; - OAKNODE_OK - } - - // ---- Node-track keyframe enumeration (engine keyframe family) ------ - - /// Remove a node-track keyframe as an undo command (used by the - /// engine's `oakengine_node_remove_keyframe_command`); the undo - /// re-inserts the captured key. - pub fn box_keyframe_remove_command( - node: CHandle, - input_id: *const c_char, - time_num: i64, - time_den: i64, - ) -> CHandle { - if time_den == 0 { - return CHandle::null(); - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return CHandle::null(), - }; - let (project, id) = nr; - let time = Rational::new(time_num, time_den); - let captured = { - let g = lock(&project); - match g.graph.get(id) { - Some(e) => e - .core - .keyframe_track(&input_id, -1) - .and_then(|t| { - t.keys() - .iter() - .find(|k| k.time == time) - .cloned() - }), - None => return CHandle::null(), - } - }; - let Some(captured) = captured else { - return CHandle::null(); - }; - let p1 = project.clone(); - let p2 = project; - let input1 = input_id.clone(); - let input2 = input_id; - let captured1 = captured.clone(); - let cmd = closure_command( - move || { - let mut g = lock(&p1); - if let Some(e) = g.graph.get_mut(id) { - e.core - .keyframe_track_mut(&input1, -1) - .remove_key(time); - } - }, - move || { - let mut g = lock(&p2); - if let Some(e) = g.graph.get_mut(id) { - e.core - .keyframe_track_mut(&input2, -1) - .set_key(captured1.clone()); - } - }, - ); - box_command(cmd) - } - - /// `oaknode_node_is_input_keyframing` — 1 when the (input, element -1) - /// track has keys. - pub fn oaknode_node_is_input_keyframing(node: CHandle, input_id: *const c_char) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - match with_node(node, |g, id| { - g.get(id) - .map(|e| e.core.is_input_keyframing(&input_id, -1)) - }) { - Some(Some(true)) => 1, - Some(Some(false)) => 0, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_keyframe_count` — keys on the (input, element -1) - /// track (0 when the input has no track yet). - pub fn oaknode_node_keyframe_count(node: CHandle, input_id: *const c_char) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - match with_node(node, |g, id| { - let e = g.get(id)?; - if !e.core.has_input(&input_id) { - return None; - } - Some(e.core.keyframe_track(&input_id, -1).map(|t| t.keys().len()).unwrap_or(0)) - }) { - Some(Some(n)) => n as c_int, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_keyframe_at` — time + value of the `index`-th key. - pub fn oaknode_node_keyframe_at( - node: CHandle, - input_id: *const c_char, - index: c_int, - out_num: *mut i64, - out_den: *mut i64, - out: *mut crate::node::OakNodeValue, - ) -> c_int { - if index < 0 || out_num.is_null() || out_den.is_null() || out.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - match with_node(node, |g, id| { - let e = g.get(id)?; - let declared = e.core.input_data_type(&input_id)?; - let key = e - .core - .keyframe_track(&input_id, -1)? - .keys() - .get(index as usize)?; - Some((declared, key.time, key.value.clone())) - }) { - Some(Some((declared, time, value))) => { - match value_to_pod(declared, &value) { - Some(pod) => { - // SAFETY: valid out pointers. - unsafe { - *out_num = time.numerator(); - *out_den = time.denominator(); - *out = pod; - } - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_has_keyframe_at_time` — 1/0. - pub fn oaknode_node_has_keyframe_at_time( - node: CHandle, - input_id: *const c_char, - time_num: i64, - time_den: i64, - ) -> c_int { - if time_den == 0 { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - match with_node(node, |g, id| { - let e = g.get(id)?; - if !e.core.has_input(&input_id) { - return None; - } - Some( - e.core - .keyframe_track(&input_id, -1) - .map(|t| t.keys().iter().any(|k| k.time == Rational::new(time_num, time_den))) - .unwrap_or(false), - ) - }) { - Some(Some(true)) => 1, - Some(Some(false)) => 0, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_remove_keyframe` — remove the key at the time. - pub fn oaknode_node_remove_keyframe( - node: CHandle, - input_id: *const c_char, - time_num: i64, - time_den: i64, - ) -> c_int { - if time_den == 0 { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - match with_node_mut(node, |g, id| { - let e = g.get_mut(id)?; - if !e.core.has_input(&input_id) { - return None; - } - e.core - .keyframe_track_mut(&input_id, -1) - .remove_key(Rational::new(time_num, time_den)); - Some(()) - }) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_clear_keyframes` — drop every key of the input. - pub fn oaknode_node_clear_keyframes(node: CHandle, input_id: *const c_char) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - match with_node_mut(node, |g, id| { - let e = g.get_mut(id)?; - if !e.core.has_input(&input_id) { - return None; - } - e.core.keyframes.retain(|(i, _, _)| i != &input_id); - Some(()) - }) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_keyframe_type_at` — the key's facade easing type - /// (0 linear, 1 bezier, 2 hold). - pub fn oaknode_node_keyframe_type_at( - node: CHandle, - input_id: *const c_char, - time_num: i64, - time_den: i64, - out_type: *mut c_int, - ) -> c_int { - if out_type.is_null() || time_den == 0 { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - match with_node(node, |g, id| { - g.get(id) - .and_then(|e| e.core.keyframe_track(&input_id, -1)) - .and_then(|t| { - t.keys() - .iter() - .find(|k| k.time == Rational::new(time_num, time_den)) - .map(|k| type_from_interp(k.interpolation)) - }) - }) { - Some(Some(t)) => { - // SAFETY: valid out pointer. - unsafe { *out_type = t }; - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_keyframe_bezier_at` — the key's bezier handle - /// (0 = in, 1 = out). - pub fn oaknode_node_keyframe_bezier_at( - node: CHandle, - input_id: *const c_char, - time_num: i64, - time_den: i64, - handle: c_int, - out_x: *mut f64, - out_y: *mut f64, - ) -> c_int { - if out_x.is_null() || out_y.is_null() || time_den == 0 || (handle != 0 && handle != 1) { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - match with_node(node, |g, id| { - g.get(id) - .and_then(|e| e.core.keyframe_track(&input_id, -1)) - .and_then(|t| { - t.keys() - .iter() - .find(|k| k.time == Rational::new(time_num, time_den)) - .map(|k| if handle == 0 { k.bezier_in } else { k.bezier_out }) - }) - }) { - Some(Some((x, y))) => { - // SAFETY: valid out pointers. - unsafe { - *out_x = x; - *out_y = y; - } - OAKNODE_OK - } - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_keyframe_set_type` — set the key's easing type. - pub fn oaknode_node_keyframe_set_type( - node: CHandle, - input_id: *const c_char, - time_num: i64, - time_den: i64, - type_: c_int, - ) -> c_int { - if time_den == 0 { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - let interp = interp_from_type(type_); - let time = Rational::new(time_num, time_den); - match with_node_mut(node, |g, id| { - let e = g.get_mut(id)?; - if !e.core.has_input(&input_id) { - return None; - } - let track = e.core.keyframe_track_mut(&input_id, -1); - let keys: Vec = track.keys().to_vec(); - for mut key in keys { - if key.time == time { - key.interpolation = interp; - track.set_key(key); - return Some(()); - } - } - None - }) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_node_keyframe_set_bezier` — set one bezier handle. - pub fn oaknode_node_keyframe_set_bezier( - node: CHandle, - input_id: *const c_char, - time_num: i64, - time_den: i64, - handle: c_int, - x: f64, - y: f64, - ) -> c_int { - if time_den == 0 || (handle != 0 && handle != 1) { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input_id = unsafe { cstr(input_id) }; - let time = Rational::new(time_num, time_den); - match with_node_mut(node, |g, id| { - let e = g.get_mut(id)?; - if !e.core.has_input(&input_id) { - return None; - } - let track = e.core.keyframe_track_mut(&input_id, -1); - let keys: Vec = track.keys().to_vec(); - for mut key in keys { - if key.time == time { - if handle == 0 { - key.bezier_in = (x, y); - } else { - key.bezier_out = (x, y); - } - track.set_key(key); - return Some(()); - } - } - None - }) { - Some(Some(())) => OAKNODE_OK, - Some(None) => OAKNODE_E_NOT_FOUND, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_keyframe_get_track`. - pub fn oaknode_keyframe_get_track(keyframe: CHandle, out_track: *mut c_int) -> c_int { - if out_track.is_null() { - return OAKNODE_E_INVALID; - } - if keyframe_payload(keyframe).is_some() { - // SAFETY: valid out pointer (whole-value tracks: 0). - unsafe { *out_track = 0 }; - OAKNODE_OK - } else { - OAKNODE_E_INVALID - } - } - - /// `oaknode_keyframe_get_element`. - pub fn oaknode_keyframe_get_element(keyframe: CHandle, out_element: *mut c_int) -> c_int { - if out_element.is_null() { - return OAKNODE_E_INVALID; - } - match keyframe_payload(keyframe) { - Some(kf) => { - // SAFETY: valid out pointer. - unsafe { *out_element = kf.element }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_keyframe_get_input` (two-stage). - pub fn oaknode_keyframe_get_input( - keyframe: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match keyframe_payload(keyframe) { - Some(kf) => string_out(&kf.input, buf, buf_size), - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_keyframe_get_parent` — borrowed parent node view. - pub fn oaknode_keyframe_get_parent(keyframe: CHandle, out_node: *mut CHandle) -> c_int { - if out_node.is_null() { - return OAKNODE_E_INVALID; - } - match keyframe_payload(keyframe) { - Some(kf) => { - // SAFETY: valid out pointer. - unsafe { *out_node = make_node_handle(kf.project.clone(), kf.node, false) }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_keyframe_get_valid_bezier_control` — the handle clamped to - /// the neighboring key's time (the C++ `valid_bezier_control_*`). - pub fn oaknode_keyframe_get_valid_bezier_control( - keyframe: CHandle, - handle: c_int, - out_x: *mut f64, - out_y: *mut f64, - ) -> c_int { - if out_x.is_null() || out_y.is_null() || (handle != 0 && handle != 1) { - return OAKNODE_E_INVALID; - } - let kf = match keyframe_payload(keyframe) { - Some(kf) => kf, - None => return OAKNODE_E_INVALID, - }; - let g = lock(&kf.project); - let e = match g.graph.get(kf.node) { - Some(e) => e, - None => return OAKNODE_E_NOT_FOUND, - }; - let track = match e.core.keyframe_track(&kf.input, kf.element) { - Some(t) => t, - None => return OAKNODE_E_NOT_FOUND, - }; - let key = match track.keys().iter().find(|k| k.time == kf.time) { - Some(k) => k, - None => return OAKNODE_E_NOT_FOUND, - }; - let (x, y) = if handle == 0 { - key.bezier_in - } else { - key.bezier_out - }; - // Clamp the x handle so the curve never overlaps the neighbor's - // time (best-effort parity with the C++ helpers). - let keys: Vec = track.keys().to_vec(); - let pos = keys.iter().position(|k| k.time == kf.time); - let clamped_x = match (handle, pos) { - (0, Some(p)) if p > 0 => { - let prev_t = keys[p - 1].time.to_f64(); - (key.time.to_f64() + x).max(prev_t) - key.time.to_f64() - } - (1, Some(p)) if p + 1 < keys.len() => { - let next_t = keys[p + 1].time.to_f64(); - (key.time.to_f64() + x).min(next_t) - key.time.to_f64() - } - _ => x, - }; - // SAFETY: valid out pointers. - unsafe { - *out_x = clamped_x; - *out_y = y; - } - OAKNODE_OK - } - - /// `oaknode_keyframe_opposing_bezier_type` — the C++ quadratic/cubic - /// bezier pair swap; other types pass through. - pub fn oaknode_keyframe_opposing_bezier_type(type_: c_int) -> c_int { - match type_ { - 3 => 4, - 4 => 3, - _ => type_, - } - } - - /// `oaknode_keyframe_compute_paste_value` — best-effort: convert the - /// keyframe's value into the target input's declared type. - pub fn oaknode_keyframe_compute_paste_value( - target_node: CHandle, - keyframe: CHandle, - out: *mut crate::node::OakNodeValue, - ) -> c_int { - if out.is_null() { - return OAKNODE_E_INVALID; - } - let kf = match keyframe_payload(keyframe) { - Some(kf) => kf, - None => return OAKNODE_E_INVALID, - }; - let value = match keyframe_value(kf) { - Some(v) => v, - None => return OAKNODE_E_NOT_FOUND, - }; - let declared = match with_node(target_node, |g, id| { - let e = g.get(id)?; - // The target's declared type is unknown without an input id; - // fall back to the value's own type (documented deviation). - e.core - .inputs - .first() - .map(|i| i.value_type) - .or(Some(value.value_type())) - }) { - Some(Some(d)) => d, - _ => value.value_type(), - }; - match value_to_pod(declared, &value) { - Some(pod) => { - // SAFETY: valid out pointer. - unsafe { *out = pod }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_keyframe_has_sibling_at_time` — another key on the same - /// track at the given time. - pub fn oaknode_keyframe_has_sibling_at_time( - keyframe: CHandle, - time_num: i64, - time_den: i64, - out_value: *mut c_int, - ) -> c_int { - if out_value.is_null() || time_den == 0 { - return OAKNODE_E_INVALID; - } - let kf = match keyframe_payload(keyframe) { - Some(kf) => kf, - None => return OAKNODE_E_INVALID, - }; - let time = Rational::new(time_num, time_den); - let g = lock(&kf.project); - let e = match g.graph.get(kf.node) { - Some(e) => e, - None => return OAKNODE_E_NOT_FOUND, - }; - let has_sibling = e - .core - .keyframe_track(&kf.input, kf.element) - .map(|t| t.keys().iter().any(|k| k.time == time && k.time != kf.time)) - .unwrap_or(false); - // SAFETY: valid out pointer. - unsafe { *out_value = if has_sibling { 1 } else { 0 } }; - OAKNODE_OK - } - - // ------------------------------------------------------------------- - // Dragger family - // ------------------------------------------------------------------- - - /// Engine-side dragger payload (the C++ `NodeInputDragger` data). - struct DraggerPayload { - project: ProjectArc, - node: NodeId, - input: String, - element: i32, - track: i32, - started: bool, - time: Rational, - start_value: Option, - } - - /// `oaknode_dragger_create`. - pub fn oaknode_dragger_create( - node: CHandle, - input_id: *const c_char, - element: c_int, - track: c_int, - ) -> CHandle { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let input = unsafe { cstr(input_id) }; - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return CHandle::null(), - }; - let (project, node) = nr; - oaknode::handle::make_owned(DraggerPayload { - project, - node, - input, - element, - track, - started: false, - time: Rational::new(0, 1), - start_value: None, - }) - } - - /// `oaknode_dragger_start` — record the drag start (time + value). - pub fn oaknode_dragger_start( - dragger: CHandle, - time_num: i64, - time_den: i64, - track: c_int, - _insert_on_all_tracks: c_int, - ) -> c_int { - if time_den == 0 { - return OAKNODE_E_INVALID; - } - // SAFETY: dragger handles box DraggerPayload. - let d = match unsafe { crate::handle::domain::boxed_mut::(&dragger) } { - Some(d) => d, - None => return OAKNODE_E_INVALID, - }; - let time = Rational::new(time_num, time_den); - let g = lock(&d.project); - let value = g - .graph - .get(d.node) - .map(|e| e.core.value_at_time(&d.input, d.element, time)); - d.track = track; - d.time = time; - d.start_value = value; - d.started = true; - OAKNODE_OK - } - - /// `oaknode_dragger_drag` — live value write at the drag start time. - pub fn oaknode_dragger_drag( - dragger: CHandle, - value: *const crate::node::OakNodeValue, - ) -> c_int { - if value.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller passes a live POD. - let value = unsafe { *value }; - // SAFETY: dragger handles box DraggerPayload. - let d = match unsafe { oaknode::handle::get::(&dragger) } { - Some(d) => d, - None => return OAKNODE_E_INVALID, - }; - if !d.started { - return OAKNODE_E_STATE; - } - let declared = { - let g = lock(&d.project); - g.graph - .get(d.node) - .and_then(|e| e.core.input_data_type(&d.input)) - }; - let Some(declared) = declared else { - return OAKNODE_E_NOT_FOUND; - }; - let converted = match pod_to_value(declared, value) { - Some(v) => v, - None => return OAKNODE_E_INVALID, - }; - let mut g = lock(&d.project); - match g.graph.get_mut(d.node) { - Some(e) => { - let keyframing = e - .core - .keyframe_track(&d.input, d.element) - .map(|t| !t.keys().is_empty()) - .unwrap_or(false); - if keyframing { - e.core - .keyframe_track_mut(&d.input, d.element) - .set_key_value(d.time, converted); - } else { - e.core.set_standard_value(&d.input, d.element, converted); - } - OAKNODE_OK - } - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_dragger_end` — finish the drag, returning ONE undoable - /// command restoring start -> end. - pub fn oaknode_dragger_end(dragger: CHandle, out_command: *mut CHandle) -> c_int { - if out_command.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: dragger handles box DraggerPayload. - let d = match unsafe { crate::handle::domain::boxed_mut::(&dragger) } { - Some(d) => d, - None => return OAKNODE_E_INVALID, - }; - if !d.started { - return OAKNODE_E_STATE; - } - let end_value = { - let g = lock(&d.project); - g.graph - .get(d.node) - .map(|e| e.core.value_at_time(&d.input, d.element, d.time)) - }; - let start_value = d.start_value.clone(); - let cmd = match end_value { - Some(end_value) => { - let guard = lock(&d.project); - let cmd = match oaknode::ops::set_value_at_time_command( - &d.project, - &guard.graph, - d.node, - &d.input, - d.element, - d.time, - &end_value, - ) { - Ok(c) => c, - Err(e) => return e.code(), - }; - let _ = start_value; - cmd - } - None => return OAKNODE_E_NOT_FOUND, - }; - d.started = false; - // SAFETY: valid out pointer. - unsafe { *out_command = box_command(cmd) }; - OAKNODE_OK - } - - /// `oaknode_dragger_is_started`. - pub fn oaknode_dragger_is_started(dragger: CHandle, out_started: *mut c_int) -> c_int { - if out_started.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: dragger handles box DraggerPayload. - match unsafe { oaknode::handle::get::(&dragger) } { - Some(d) => { - // SAFETY: valid out pointer. - unsafe { *out_started = if d.started { 1 } else { 0 } }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_dragger_free`. - pub fn oaknode_dragger_free(dragger: *mut CHandle) { - if dragger.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - let h = unsafe { *dragger }; - release_handle(h); - // SAFETY: the caller passes a valid handle pointer. - unsafe { *dragger = CHandle::null() }; - } - // ------------------------------------------------------------------- - // Color manager family - // ------------------------------------------------------------------- - - fn color_manager(h: CHandle) -> Option<&'static oaknode::colormanager::ColorManager> { - // SAFETY: colormanager handles box the domain ColorManager. - let m = unsafe { oaknode::handle::get::(&h) }?; - // SAFETY: the box outlives the handle. - unsafe { Some(&*(m as *const _)) } - } - - fn color_manager_mut(h: CHandle) -> Option<&'static mut oaknode::colormanager::ColorManager> { - // SAFETY: colormanager handles box the domain ColorManager; the - // caller holds exclusive access. - let m = unsafe { crate::handle::domain::boxed_mut::(&h) }?; - // SAFETY: the box outlives the handle. - unsafe { Some(&mut *(m as *mut _)) } - } - - /// `oaknode_colormanager_init` — a fresh color manager for the project - /// (the domain manager is self-contained). - pub fn oaknode_colormanager_init(_project: CHandle) -> CHandle { - oaknode::handle::make_owned(oaknode::colormanager::ColorManager::new()) - } - - /// `oaknode_colormanager_free`. - pub fn oaknode_colormanager_free(manager: *mut CHandle) { - if manager.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - let h = unsafe { *manager }; - release_handle(h); - // SAFETY: the caller passes a valid handle pointer. - unsafe { *manager = CHandle::null() }; - } - - /// `oaknode_colormanager_wrap_borrowed` — STUB: there is no native - /// (C++) color manager to borrow in the single-lib world (documented; - /// returns the empty handle). - pub fn oaknode_colormanager_wrap_borrowed(_native_manager: *mut c_void) -> CHandle { - CHandle::null() - } - - /// `oaknode_colormanager_initialize`. - pub fn oaknode_colormanager_initialize(manager: CHandle) -> c_int { - match color_manager_mut(manager) { - Some(m) => match m.initialize() { - Ok(()) => OAKNODE_OK, - Err(e) => e.code(), - }, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_colormanager_set_up_default_config`. - pub fn oaknode_colormanager_set_up_default_config() -> c_int { - let mut manager = oaknode::colormanager::ColorManager::new(); - match manager.set_up_default_config() { - Ok(()) => OAKNODE_OK, - Err(e) => e.code(), - } - } - - /// `oaknode_colormanager_get_config_filename` (two-stage). - pub fn oaknode_colormanager_get_config_filename( - manager: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match color_manager(manager) { - Some(m) => string_out(&m.config_filename, buf, buf_size), - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_colormanager_set_config_filename`. - pub fn oaknode_colormanager_set_config_filename( - manager: CHandle, - filename: *const c_char, - ) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let filename = unsafe { cstr(filename) }; - match color_manager_mut(manager) { - Some(m) => { - m.config_filename = filename; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_colormanager_update_config_from_filename`. - pub fn oaknode_colormanager_update_config_from_filename(manager: CHandle) -> c_int { - match color_manager_mut(manager) { - Some(m) => match m.update_config_from_filename() { - Ok(()) => OAKNODE_OK, - Err(e) => e.code(), - }, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_colormanager_get_default_input_color_space` (two-stage). - pub fn oaknode_colormanager_get_default_input_color_space( - manager: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match color_manager(manager) { - Some(m) => string_out(&m.default_input_space, buf, buf_size), - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_colormanager_set_default_input_color_space`. - pub fn oaknode_colormanager_set_default_input_color_space( - manager: CHandle, - colorspace: *const c_char, - ) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let colorspace = unsafe { cstr(colorspace) }; - match color_manager_mut(manager) { - Some(m) => { - m.default_input_space = colorspace; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_colormanager_get_reference_color_space` (two-stage). - pub fn oaknode_colormanager_get_reference_color_space( - manager: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match color_manager(manager) { - Some(m) => string_out(&m.reference_space, buf, buf_size), - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_colormanager_get_compliant_color_space` — best-effort: the - /// exact listed colorspace when found, else the input unchanged (the - /// C++ fell back to the input on unknown names too). - pub fn oaknode_colormanager_get_compliant_color_space( - manager: CHandle, - colorspace: *const c_char, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let colorspace = unsafe { cstr(colorspace) }; - match color_manager(manager) { - Some(m) => { - let listed = m.list_colorspaces(); - let name = if listed.iter().any(|c| c == &colorspace) { - colorspace.clone() - } else { - colorspace - }; - string_out(&name, buf, buf_size) - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_colormanager_get_colorspace_for_ffmpeg_tags` — STUB: the - /// primaries/trc tag table has no Rust equivalent in the oaknode - /// crate (documented; returns NOT_FOUND). - pub fn oaknode_colormanager_get_colorspace_for_ffmpeg_tags( - _manager: CHandle, - _primaries: c_int, - _trc: c_int, - _buf: *mut c_char, - _buf_size: c_int, - ) -> c_int { - OAKNODE_E_NOT_FOUND - } - - /// `oaknode_colormanager_get_display_count`. - pub fn oaknode_colormanager_get_display_count(manager: CHandle, count: *mut c_int) -> c_int { - if count.is_null() { - return OAKNODE_E_INVALID; - } - match color_manager(manager) { - Some(m) => { - // SAFETY: valid out pointer. - unsafe { *count = m.list_displays().len() as c_int }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_colormanager_get_display_at` (two-stage). - pub fn oaknode_colormanager_get_display_at( - manager: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - if index < 0 { - return OAKNODE_E_NOT_FOUND; - } - match color_manager(manager) - .and_then(|m| m.list_displays().get(index as usize).cloned()) - { - Some(s) => string_out(&s, buf, buf_size), - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_colormanager_get_default_display` (two-stage). - pub fn oaknode_colormanager_get_default_display( - manager: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match color_manager(manager) { - Some(m) => string_out(&m.default_display, buf, buf_size), - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_colormanager_get_view_count`. - pub fn oaknode_colormanager_get_view_count( - manager: CHandle, - display: *const c_char, - count: *mut c_int, - ) -> c_int { - if count.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let display = unsafe { cstr(display) }; - match color_manager(manager) { - Some(m) => { - // SAFETY: valid out pointer. - unsafe { *count = m.list_views(&display).len() as c_int }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_colormanager_get_view_at` (two-stage). - pub fn oaknode_colormanager_get_view_at( - manager: CHandle, - display: *const c_char, - index: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - if index < 0 { - return OAKNODE_E_NOT_FOUND; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let display = unsafe { cstr(display) }; - match color_manager(manager) - .and_then(|m| m.list_views(&display).get(index as usize).cloned()) - { - Some(s) => string_out(&s, buf, buf_size), - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_colormanager_get_default_view` (two-stage). - pub fn oaknode_colormanager_get_default_view( - manager: CHandle, - display: *const c_char, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let display = unsafe { cstr(display) }; - match color_manager(manager) { - Some(m) => { - let default = m.list_views(&display).first().cloned().unwrap_or_default(); - string_out(&default, buf, buf_size) - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_colormanager_get_look_count`. - pub fn oaknode_colormanager_get_look_count(manager: CHandle, count: *mut c_int) -> c_int { - if count.is_null() { - return OAKNODE_E_INVALID; - } - match color_manager(manager) { - Some(m) => { - // SAFETY: valid out pointer. - unsafe { *count = m.list_looks().len() as c_int }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_colormanager_get_look_at` (two-stage). - pub fn oaknode_colormanager_get_look_at( - manager: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - if index < 0 { - return OAKNODE_E_NOT_FOUND; - } - match color_manager(manager) - .and_then(|m| m.list_looks().get(index as usize).cloned()) - { - Some(s) => string_out(&s, buf, buf_size), - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_colormanager_get_colorspace_count`. - pub fn oaknode_colormanager_get_colorspace_count(manager: CHandle, count: *mut c_int) -> c_int { - if count.is_null() { - return OAKNODE_E_INVALID; - } - match color_manager(manager) { - Some(m) => { - // SAFETY: valid out pointer. - unsafe { *count = m.list_colorspaces().len() as c_int }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_colormanager_get_colorspace_at` (two-stage). - pub fn oaknode_colormanager_get_colorspace_at( - manager: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - if index < 0 { - return OAKNODE_E_NOT_FOUND; - } - match color_manager(manager) - .and_then(|m| m.list_colorspaces().get(index as usize).cloned()) - { - Some(s) => string_out(&s, buf, buf_size), - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_colormanager_get_default_luma_coefs` — best-effort: the - /// Rec.709 luma coefficients (the C++ default config values; the - /// domain manager exposes no luma table). - pub fn oaknode_colormanager_get_default_luma_coefs(manager: CHandle, rgb: *mut f64) -> c_int { - if rgb.is_null() { - return OAKNODE_E_INVALID; - } - if color_manager(manager).is_none() { - return OAKNODE_E_INVALID; - } - // SAFETY: valid out pointer (3 doubles). - unsafe { - *rgb = 0.2126; - *rgb.add(1) = 0.7152; - *rgb.add(2) = 0.0722; - } - OAKNODE_OK - } - - /// `oaknode_colormanager_get_compliant_color_transform` — best-effort: - /// resolve a display transform through oakrender's OCIO surface (or - /// pass the output colorspace through), boxed as an oakcommon - /// colortransform handle. - pub fn oaknode_colormanager_get_compliant_color_transform( - manager: CHandle, - transform: CHandle, - _force_display: c_int, - out: *mut CHandle, - ) -> c_int { - if out.is_null() { - return OAKNODE_E_INVALID; - } - if color_manager(manager).is_none() || transform.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the transform handle boxes an oakcommon ColorTransform. - let ct = unsafe { oakcommon::handle::get::(&transform) }; - let Some(ct) = ct else { - return OAKNODE_E_INVALID; - }; - let (output, _view) = if ct.is_display() { - ( - oakrender::color::display_transform(ct.display(), ct.view()) - .unwrap_or_else(|| ct.display().to_string()), - ct.view().to_string(), - ) - } else { - (ct.output().to_string(), String::new()) - }; - let output_c = std::ffi::CString::new(output.as_str()).unwrap_or_default(); - // SAFETY: valid out pointer. - unsafe { - *out = crate::stubs::common::oakcommon_colortransform_init_output(output_c.as_ptr()); - } - OAKNODE_OK - } - - // ------------------------------------------------------------------- - // Traverser family - // ------------------------------------------------------------------- - - /// No-op render hooks (the offline-evaluation defaults). - struct NoopHooks; - - impl oaknode::traverser::RenderHooks for NoopHooks {} - - /// Engine-side traverser database payload. - struct TraverseDbPayload { - rows: Vec<(String, Vec<(oaknode::value::ValueType, oaknode::value::NodeValue)>)>, - } - - /// `oaknode_traverser_init`. - pub fn oaknode_traverser_init() -> CHandle { - oaknode::handle::make_owned(oaknode::traverser::Traverser::new()) - } - - /// `oaknode_traverser_free`. - pub fn oaknode_traverser_free(traverser: *mut CHandle) { - if traverser.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - let h = unsafe { *traverser }; - release_handle(h); - // SAFETY: the caller passes a valid handle pointer. - unsafe { *traverser = CHandle::null() }; - } - - /// `oaknode_traverser_generate_database` — evaluate the node at the - /// range start and box the output table as the database. - pub fn oaknode_traverser_generate_database( - traverser: CHandle, - node: CHandle, - in_num: i64, - in_den: i64, - _out_num: i64, - _out_den: i64, - out_db: *mut CHandle, - ) -> c_int { - if out_db.is_null() || in_den == 0 { - return OAKNODE_E_INVALID; - } - let nr = match unsafe { node_ref_of(&node) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return OAKNODE_E_INVALID, - }; - let (project, id) = nr; - // SAFETY: traverser handles box the domain Traverser. - let t = match unsafe { crate::handle::domain::boxed_mut::(&traverser) } { - Some(t) => t, - None => return OAKNODE_E_INVALID, - }; - let request = oaknode::traverser::EvalRequest::new(id, Rational::new(in_num, in_den)); - let g = lock(&project); - let table = match t.evaluate(&g.graph, &request, &mut NoopHooks) { - Ok(table) => table, - Err(e) => return e.code(), - }; - let rows = vec![( - "output".to_string(), - table.rows().iter().map(|(ty, v, _)| (*ty, v.clone())).collect(), - )]; - // SAFETY: valid out pointer. - unsafe { *out_db = oaknode::handle::make_owned(TraverseDbPayload { rows }) }; - OAKNODE_OK - } - - /// `oaknode_traverser_database_free`. - pub fn oaknode_traverser_database_free(db: *mut CHandle) { - if db.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - let h = unsafe { *db }; - release_handle(h); - // SAFETY: the caller passes a valid handle pointer. - unsafe { *db = CHandle::null() }; - } - - fn db_payload(db: CHandle) -> Option<&'static TraverseDbPayload> { - // SAFETY: database handles box TraverseDbPayload. - let p = unsafe { oaknode::handle::get::(&db) }?; - // SAFETY: the box outlives the handle. - unsafe { Some(&*(p as *const _)) } - } - - /// `oaknode_traverser_database_row_count`. - pub fn oaknode_traverser_database_row_count(db: CHandle, out_count: *mut c_int) -> c_int { - if out_count.is_null() { - return OAKNODE_E_INVALID; - } - match db_payload(db) { - Some(p) => { - // SAFETY: valid out pointer. - unsafe { *out_count = p.rows.len() as c_int }; - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_traverser_database_row_key_at` (two-stage). - pub fn oaknode_traverser_database_row_key_at( - db: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - if index < 0 { - return OAKNODE_E_NOT_FOUND; - } - match db_payload(db).and_then(|p| p.rows.get(index as usize)) { - Some((key, _)) => string_out(key, buf, buf_size), - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_traverser_database_row_value_count`. - pub fn oaknode_traverser_database_row_value_count( - db: CHandle, - key: *const c_char, - out_count: *mut c_int, - ) -> c_int { - if out_count.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let key = unsafe { cstr(key) }; - match db_payload(db).and_then(|p| p.rows.iter().find(|(k, _)| *k == key)) { - Some((_, values)) => { - // SAFETY: valid out pointer. - unsafe { *out_count = values.len() as c_int }; - OAKNODE_OK - } - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_traverser_database_value_at` — the typed value into the - /// POD. - pub fn oaknode_traverser_database_value_at( - db: CHandle, - key: *const c_char, - index: c_int, - out: *mut crate::node::OakNodeValue, - ) -> c_int { - if out.is_null() || index < 0 { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let key = unsafe { cstr(key) }; - match db_payload(db).and_then(|p| p.rows.iter().find(|(k, _)| *k == key)) { - Some((_, values)) => match values.get(index as usize) { - Some((ty, v)) => match value_to_pod(*ty, v) { - Some(pod) => { - // SAFETY: valid out pointer. - unsafe { *out = pod }; - OAKNODE_OK - } - None => OAKNODE_E_FAILED, - }, - None => OAKNODE_E_NOT_FOUND, - }, - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_traverser_database_value_string_at` (two-stage) — the - /// string-carrying types; numeric types format via `to_string`. - pub fn oaknode_traverser_database_value_string_at( - db: CHandle, - key: *const c_char, - index: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - if index < 0 { - return OAKNODE_E_NOT_FOUND; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let key = unsafe { cstr(key) }; - match db_payload(db).and_then(|p| p.rows.iter().find(|(k, _)| *k == key)) { - Some((_, values)) => match values.get(index as usize) { - Some((_, v)) => match v { - oaknode::value::NodeValue::Text(s) => string_out(s, buf, buf_size), - oaknode::value::NodeValue::StrCombo(s) => string_out(s, buf, buf_size), - other => string_out(&other.to_double().to_string(), buf, buf_size), - }, - None => OAKNODE_E_NOT_FOUND, - }, - None => OAKNODE_E_NOT_FOUND, - } - } - - // ------------------------------------------------------------------- - // Serializer family - // ------------------------------------------------------------------- - - /// Engine-side save-data payload: the project plus an optional node - /// subset and per-node properties (best-effort: the whole project's - /// graph is serialized; the subset/properties are recorded for the - /// load-data mirror). - struct SaveDataPayload { - project: ProjectArc, - #[allow(dead_code)] - nodes: Vec<(ProjectArc, NodeId)>, - #[allow(dead_code)] - properties: Vec<(NodeId, String, String)>, - } - - /// Engine-side load-data payload: the loaded project plus its - /// graph inventory. - struct LoadDataPayload { - project: ProjectArc, - nodes: Vec, - properties: Vec<(NodeId, String, String)>, - connections: Vec<(NodeId, NodeId, String, i32)>, - } - - /// Replace a project payload's contents with a freshly loaded project - /// (the load path requires an uninitialized project, so no node - /// handles reference the old contents). - fn swap_project_payload(project: CHandle, loaded: ProjectArc) -> Option<()> { - // SAFETY: project handles box ProjectArc payloads. - let target = unsafe { crate::handle::domain::boxed_mut::(&project) }?; - let old_key = Arc::as_ptr(target) as usize; - let old = std::mem::replace(target, loaded); - drop(old); - // Retire the old alive-registry entry, register the new one. - let dead = match registry().get(&old_key) { - Some(weak) => weak.upgrade().is_none(), - None => false, - }; - if dead { - registry().remove(&old_key); - alive_dec(); - } - let new_key = Arc::as_ptr(target) as usize; - registry().entry(new_key).or_insert_with(|| { - alive_inc(); - Arc::downgrade(target) - }); - Some(()) - } - - /// `oaknode_serializer_initialize` — nothing to do (the serializer is - /// stateless). - pub fn oaknode_serializer_initialize() -> c_int { - OAKNODE_OK - } - - /// `oaknode_serializer_shutdown` — nothing to do. - pub fn oaknode_serializer_shutdown() {} - - /// `oaknode_serializer_savedata_create`. - pub fn oaknode_serializer_savedata_create(_load_type: c_int, project: CHandle) -> CHandle { - let p = match unsafe { project_of(&project) }.cloned() { - Some(p) => p, - None => return CHandle::null(), - }; - oaknode::handle::make_owned(SaveDataPayload { - project: p, - nodes: Vec::new(), - properties: Vec::new(), - }) - } - - /// `oaknode_serializer_savedata_free`. - pub fn oaknode_serializer_savedata_free(save_data: *mut CHandle) { - if save_data.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - let h = unsafe { *save_data }; - release_handle(h); - // SAFETY: the caller passes a valid handle pointer. - unsafe { *save_data = CHandle::null() }; - } - - /// `oaknode_serializer_savedata_set_nodes`. - pub fn oaknode_serializer_savedata_set_nodes( - save_data: CHandle, - nodes: *const CHandle, - count: c_int, - ) -> c_int { - if nodes.is_null() || count < 0 { - return OAKNODE_E_INVALID; - } - // SAFETY: savedata handles box SaveDataPayload. - let payload = match unsafe { crate::handle::domain::boxed_mut::(&save_data) } { - Some(p) => p, - None => return OAKNODE_E_INVALID, - }; - payload.nodes.clear(); - for i in 0..count as usize { - // SAFETY: the caller guarantees `count` valid handles. - let h = unsafe { *nodes.add(i) }; - match unsafe { node_ref_of(&h) } { - Some(nr) => payload.nodes.push((nr.project.clone(), nr.id)), - None => return OAKNODE_E_INVALID, - } - } - OAKNODE_OK - } - - /// `oaknode_serializer_savedata_set_property`. - pub fn oaknode_serializer_savedata_set_property( - save_data: CHandle, - node: CHandle, - key: *const c_char, - value: *const c_char, - ) -> c_int { - // SAFETY: the caller guarantees valid NUL-terminated strings. - let (key, value) = unsafe { (cstr(key), cstr(value)) }; - let node_id = match unsafe { node_ref_of(&node) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - // SAFETY: savedata handles box SaveDataPayload. - match unsafe { crate::handle::domain::boxed_mut::(&save_data) } { - Some(p) => { - p.properties.push((node_id, key, value)); - OAKNODE_OK - } - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_serializer_save_to_xml` (two-stage) — serialize the whole - /// project (documented: the C++ serialized only the listed nodes; the - /// Rust serializer covers the project). - pub fn oaknode_serializer_save_to_xml( - save_data: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - // SAFETY: savedata handles box SaveDataPayload. - let payload = match unsafe { oaknode::handle::get::(&save_data) } { - Some(p) => p, - None => return OAKNODE_E_INVALID, - }; - let xml = { - let g = lock(&payload.project); - match oaknode::serializer::save(&g) { - Ok(xml) => xml, - Err(e) => return e.code(), - } - }; - string_out(&xml, buf, buf_size) - } - - /// `oaknode_serializer_load_from_xml` — parse the XML into the target - /// project and box the load data. - pub fn oaknode_serializer_load_from_xml( - project: CHandle, - xml: *const c_char, - _load_type: c_int, - out_result: *mut c_int, - out_load_data: *mut CHandle, - details_buf: *mut c_char, - details_buf_size: c_int, - ) -> c_int { - if out_result.is_null() || out_load_data.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let xml = unsafe { cstr(xml) }; - let loaded = match oaknode::serializer::load(&xml) { - Ok(p) => p, - Err(e) => { - // SAFETY: valid out pointers; the error message is - // surfaced into the details buffer. - unsafe { - *out_result = e.code(); - *out_load_data = CHandle::null(); - } - if !details_buf.is_null() && details_buf_size > 0 { - // SAFETY: the caller guarantees the buffer size. - unsafe { crate::handle::write_string(&e.to_string(), details_buf, details_buf_size) }; - } - return OAKNODE_OK; - } - }; - let (nodes, connections) = { - let g = lock(&loaded); - (g.graph.node_ids(), g.graph.output_connections_all()) - }; - if swap_project_payload(project, loaded.clone()).is_none() { - return OAKNODE_E_INVALID; - } - // SAFETY: valid out pointers. - unsafe { - *out_result = 0; - *out_load_data = oaknode::handle::make_owned(LoadDataPayload { - project: loaded, - nodes, - properties: Vec::new(), - connections, - }); - } - OAKNODE_OK - } - - /// `oaknode_serializer_loaddata_free`. - pub fn oaknode_serializer_loaddata_free(load_data: *mut CHandle) { - if load_data.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - let h = unsafe { *load_data }; - release_handle(h); - // SAFETY: the caller passes a valid handle pointer. - unsafe { *load_data = CHandle::null() }; - } - - fn loaddata_payload(load_data: CHandle) -> Option<&'static LoadDataPayload> { - // SAFETY: loaddata handles box LoadDataPayload. - let p = unsafe { oaknode::handle::get::(&load_data) }?; - // SAFETY: the box outlives the handle. - unsafe { Some(&*(p as *const _)) } - } - - /// `oaknode_serializer_loaddata_node_count`. - pub fn oaknode_serializer_loaddata_node_count(load_data: CHandle) -> c_int { - match loaddata_payload(load_data) { - Some(p) => p.nodes.len() as c_int, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_serializer_loaddata_node_at` — borrowed node view. - pub fn oaknode_serializer_loaddata_node_at(load_data: CHandle, index: c_int) -> CHandle { - if index < 0 { - return CHandle::null(); - } - let p = match loaddata_payload(load_data) { - Some(p) => p, - None => return CHandle::null(), - }; - match p.nodes.get(index as usize).copied() { - Some(id) => make_node_handle(p.project.clone(), id, false), - None => CHandle::null(), - } - } - - /// `oaknode_serializer_loaddata_get_property` (two-stage). - pub fn oaknode_serializer_loaddata_get_property( - load_data: CHandle, - node: CHandle, - key: *const c_char, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - // SAFETY: the caller guarantees a valid NUL-terminated string. - let key = unsafe { cstr(key) }; - let node_id = match unsafe { node_ref_of(&node) } { - Some(nr) => nr.id, - None => return OAKNODE_E_INVALID, - }; - match loaddata_payload(load_data).and_then(|p| { - p.properties - .iter() - .find(|(n, k, _)| *n == node_id && *k == key) - .map(|(_, _, v)| v.clone()) - }) { - Some(value) => string_out(&value, buf, buf_size), - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_serializer_loaddata_connection_count`. - pub fn oaknode_serializer_loaddata_connection_count(load_data: CHandle) -> c_int { - match loaddata_payload(load_data) { - Some(p) => p.connections.len() as c_int, - None => OAKNODE_E_INVALID, - } - } - - /// `oaknode_serializer_loaddata_connection_at`. - pub fn oaknode_serializer_loaddata_connection_at( - load_data: CHandle, - index: c_int, - out_output_node: *mut CHandle, - out_input_node: *mut CHandle, - input_id_buf: *mut c_char, - input_id_buf_size: c_int, - out_element: *mut c_int, - ) -> c_int { - if out_output_node.is_null() || out_input_node.is_null() || out_element.is_null() || index < 0 { - return OAKNODE_E_INVALID; - } - let p = match loaddata_payload(load_data) { - Some(p) => p, - None => return OAKNODE_E_INVALID, - }; - match p.connections.get(index as usize) { - Some((from, to, input, element)) => { - // SAFETY: valid out pointers. - unsafe { - *out_output_node = make_node_handle(p.project.clone(), *from, false); - *out_input_node = make_node_handle(p.project.clone(), *to, false); - *out_element = *element; - } - string_out(input, input_id_buf, input_id_buf_size) - } - None => OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_serializer_save_to_file` — serialize the project and write - /// it to `filename` (compression is not supported by the direct - /// serializer; the flag is accepted for ABI parity). - pub fn oaknode_serializer_save_to_file( - project: CHandle, - filename: *const c_char, - _use_compression: c_int, - out_code: *mut c_int, - details: *mut c_char, - details_size: c_int, - ) -> c_int { - if out_code.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let filename = unsafe { cstr(filename) }; - let p = match unsafe { project_of(&project) }.cloned() { - Some(p) => p, - None => return OAKNODE_E_INVALID, - }; - let xml = { - let g = lock(&p); - match oaknode::serializer::save(&g) { - Ok(xml) => xml, - Err(e) => { - // SAFETY: valid out pointers. - unsafe { - *out_code = e.code(); - if !details.is_null() && details_size > 0 { - crate::handle::write_string(&e.to_string(), details, details_size); - } - } - return OAKNODE_OK; - } - } - }; - match std::fs::write(&filename, xml) { - Ok(()) => { - // SAFETY: valid out pointer. - unsafe { *out_code = 0 }; - OAKNODE_OK - } - Err(e) => { - // SAFETY: valid out pointers. - unsafe { - *out_code = OAKNODE_E_FAILED; - if !details.is_null() && details_size > 0 { - crate::handle::write_string(&e.to_string(), details, details_size); - } - } - OAKNODE_OK - } - } - } - - /// `oaknode_serializer_load_from_file` — read and parse the file into - /// the target project. - pub fn oaknode_serializer_load_from_file( - project: CHandle, - filename: *const c_char, - out_code: *mut c_int, - details: *mut c_char, - details_size: c_int, - ) -> c_int { - if out_code.is_null() { - return OAKNODE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let filename = unsafe { cstr(filename) }; - let xml = match std::fs::read_to_string(&filename) { - Ok(xml) => xml, - Err(e) => { - // SAFETY: valid out pointers. - unsafe { - *out_code = OAKNODE_E_FAILED; - if !details.is_null() && details_size > 0 { - crate::handle::write_string(&e.to_string(), details, details_size); - } - } - return OAKNODE_OK; - } - }; - match oaknode::serializer::load(&xml) { - Ok(loaded) => { - if swap_project_payload(project, loaded).is_none() { - return OAKNODE_E_INVALID; - } - // SAFETY: valid out pointer. - unsafe { *out_code = 0 }; - OAKNODE_OK - } - Err(e) => { - // SAFETY: valid out pointers. - unsafe { - *out_code = e.code(); - if !details.is_null() && details_size > 0 { - crate::handle::write_string(&e.to_string(), details, details_size); - } - } - OAKNODE_OK - } - } - } -} - -// =========================================================================== -// timeline — oaktimeline domain implementation (single-lib unification) -// =========================================================================== -// -// The deleted oaktimeline C ABI is replaced by the crate's direct Rust -// constructors. The oaktimeline command family holds oaknode domain -// objects (`oaktimeline::util::NodeRef` = `{Arc>, NodeId}`, -// converted here from the engine's node-handle payloads) and every -// creator's `to_command()` yields a real -// `oakundo::undocommand::UndoCommand`, boxed behind a handle for the -// facade's undo layer. Marker lists and work areas are value boxes -// (`oaktimeline::handle::make_owned`), exactly what the crate's own -// marker/workarea commands read back through `get_mut`. -pub mod timeline { - use std::ffi::{c_char, c_int}; - - use oakcore_rs::{Rational, TimeRange}; - use oakundo::undocommand::{command_from_owned, UndoCommand}; - - use crate::handle::domain::node_ref_of; - use crate::handle::CHandle; - - /// Map an engine node handle to the oaktimeline domain reference. - fn node_ref(h: CHandle) -> Option { - // SAFETY: node handles box oaknode NodeRef payloads. - let nr = unsafe { node_ref_of(&h) }?; - Some(oaktimeline::util::NodeRef { - project: nr.project.clone(), - id: nr.id, - }) - } - - /// Map an engine node handle to the oaktimeline domain reference. - macro_rules! nref { - ($h:expr) => { - match node_ref($h) { - Some(n) => n, - None => return CHandle::null(), - } - }; - } - - /// Box an [`UndoCommand`] behind a handle. - fn box_command(cmd: UndoCommand) -> CHandle { - // SAFETY: `command_from_owned` owns the command value. - unsafe { command_from_owned(cmd) } - } - - /// Two-stage string getter (required length including the NUL). - fn string_out(s: &str, buf: *mut c_char, buf_size: c_int) -> c_int { - let required = (s.len() + 1) as c_int; - if !buf.is_null() && buf_size >= required { - // SAFETY: the caller guarantees `buf` holds `buf_size` bytes. - unsafe { - std::ptr::copy_nonoverlapping(s.as_ptr() as *const c_char, buf, s.len()); - *buf.add(s.len()) = 0; - } - } - required - } - - // ------------------------------------------------------------------- - // Marker family - // ------------------------------------------------------------------- - - /// `oaktimeline_marker_list_create` — a fresh marker list box. - pub fn oaktimeline_marker_list_create() -> CHandle { - oaktimeline::handle::make_owned(oaktimeline::marker::TimelineMarkerList::new()) - } - - /// `oaktimeline_marker_list_of` — the owner sequence's marker list - /// (created lazily; addref'd copy). - pub fn oaktimeline_marker_list_of(owner: CHandle) -> CHandle { - let nr = match unsafe { node_ref_of(&owner) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return CHandle::null(), - }; - let (project, id) = nr; - let list = { - let mut g = project.lock().unwrap_or_else(|e| e.into_inner()); - let seq = match g - .graph - .get_mut(id) - .and_then(|e| e.behavior.as_any_mut()) - .and_then(|a| a.downcast_mut::()) - { - Some(s) => s, - None => return CHandle::null(), - }; - if seq.markers.is_null() { - seq.markers = oaktimeline::handle::make_owned( - oaktimeline::marker::TimelineMarkerList::new(), - ); - } - seq.markers - }; - if let Some(addref) = list.addref { - // SAFETY: the handle is live. - unsafe { addref(list.ctx) }; - } - list - } - - /// `oaktimeline_marker_list_free`. - pub fn oaktimeline_marker_list_free(list: *mut CHandle) { - if list.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - let h = unsafe { *list }; - if let Some(release) = h.release { - // SAFETY: the boxed value's release callback. - unsafe { release(h.ctx) }; - } - // SAFETY: the caller passes a valid handle pointer. - unsafe { *list = CHandle::null() }; - } - - /// `oaktimeline_marker_add` — live add. - 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 { - if in_den == 0 || out_den == 0 { - return oaktimeline::error::OAKTIMELINE_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let name = unsafe { crate::handle::read_cstr(name) }; - let list_mut = unsafe { oaktimeline::handle::get_mut::>>(&list) }; - let Some(list_mut) = list_mut else { - return oaktimeline::error::OAKTIMELINE_E_INVALID; - }; - list_mut.lock().unwrap_or_else(|e| e.into_inner()).add_marker(oaktimeline::marker::TimelineMarker::with_time( - color, - TimeRange::new( - Rational::new(in_num as i64, in_den as i64), - Rational::new(out_num as i64, out_den as i64), - ), - &name, - )); - oaktimeline::error::OAKTIMELINE_OK - } - - /// `oaktimeline_marker_count`. - pub fn oaktimeline_marker_count(list: CHandle, out_count: *mut c_int) -> c_int { - if out_count.is_null() { - return oaktimeline::error::OAKTIMELINE_E_INVALID; - } - // SAFETY: marker-list handles box TimelineMarkerList. - match unsafe { oaktimeline::handle::get::>>(&list) } { - Some(l) => { - // SAFETY: valid out pointer. - unsafe { *out_count = l.lock().unwrap_or_else(|e| e.into_inner()).size() as c_int }; - oaktimeline::error::OAKTIMELINE_OK - } - None => oaktimeline::error::OAKTIMELINE_E_INVALID, - } - } - - /// `oaktimeline_marker_at` — in/out/color + name (two-stage). - 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 { - if index < 0 - || in_num.is_null() - || in_den.is_null() - || out_num.is_null() - || out_den.is_null() - || color.is_null() - { - return oaktimeline::error::OAKTIMELINE_E_INVALID; - } - // SAFETY: marker-list handles box TimelineMarkerList. - let l = match unsafe { oaktimeline::handle::get::>>(&list) } { - Some(l) => l, - None => return oaktimeline::error::OAKTIMELINE_E_INVALID, - }; - let l = l.lock().unwrap_or_else(|e| e.into_inner()); - let Some(m) = l.at(index as usize) else { - return oaktimeline::error::OAKTIMELINE_E_NOT_FOUND; - }; - let range = m.time(); - // SAFETY: valid out pointers. - unsafe { - *in_num = range.in_().numerator() as c_int; - *in_den = range.in_().denominator() as c_int; - *out_num = range.out().numerator() as c_int; - *out_den = range.out().denominator() as c_int; - *color = m.color(); - } - string_out(m.name(), name_buf, buf_size) - } - - /// `oaktimeline_marker_add_command`. - 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 { - if in_den == 0 || out_den == 0 { - return CHandle::null(); - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let name = unsafe { crate::handle::read_cstr(name) }; - let cmd = oaktimeline::marker::MarkerAddCommand::new( - list, - TimeRange::new( - Rational::new(in_num as i64, in_den as i64), - Rational::new(out_num as i64, out_den as i64), - ), - &name, - color, - ); - box_command(cmd.to_command()) - } - - /// `oaktimeline_marker_remove_at_command`. - pub fn oaktimeline_marker_remove_at_command(list: CHandle, index: c_int) -> CHandle { - if index < 0 { - return CHandle::null(); - } - let cmd = oaktimeline::marker::MarkerRemoveCommand::new(list, index as usize); - box_command(cmd.to_command()) - } - - /// `oaktimeline_marker_set_time_command`. - 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 { - if index < 0 || in_den == 0 || out_den == 0 { - return CHandle::null(); - } - let cmd = oaktimeline::marker::MarkerChangeTimeCommand::new( - list, - index as usize, - TimeRange::new( - Rational::new(in_num as i64, in_den as i64), - Rational::new(out_num as i64, out_den as i64), - ), - ); - box_command(cmd.to_command()) - } - - /// `oaktimeline_marker_set_props_command` — color + name changes in - /// one multi command. - pub fn oaktimeline_marker_set_props_command( - list: CHandle, - index: c_int, - color: c_int, - name: *const c_char, - ) -> CHandle { - if index < 0 { - return CHandle::null(); - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let name = unsafe { crate::handle::read_cstr(name) }; - let mut multi = UndoCommand::multi(); - multi.multi_add_child( - oaktimeline::marker::MarkerChangeColorCommand::new(list, index as usize, color) - .to_command(), - ); - multi.multi_add_child( - oaktimeline::marker::MarkerChangeNameCommand::new(list, index as usize, &name) - .to_command(), - ); - box_command(multi) - } - - /// `oaktimeline_marker_list_load` — STUB: the XML surface of the - /// deleted marker list (reader-backed) has no direct Rust equivalent - /// in the oaktimeline crate (documented; the engine does not consume - /// this path). - pub fn oaktimeline_marker_list_load(_list: CHandle, _reader: CHandle) -> c_int { - oaktimeline::error::OAKTIMELINE_E_FAILED - } - - /// `oaktimeline_marker_list_save` — STUB: see - /// [`oaktimeline_marker_list_load`]. - pub fn oaktimeline_marker_list_save(_list: CHandle, _writer: CHandle) -> c_int { - oaktimeline::error::OAKTIMELINE_E_FAILED - } - - // ------------------------------------------------------------------- - // Work area family - // ------------------------------------------------------------------- - - /// `oaktimeline_workarea_create` — a fresh work-area box. - pub fn oaktimeline_workarea_create() -> CHandle { - oaktimeline::handle::make_owned(oaktimeline::workarea::TimelineWorkArea::new()) - } - - /// `oaktimeline_workarea_of` — the owner sequence's work area (created - /// lazily; addref'd copy). - pub fn oaktimeline_workarea_of(owner: CHandle) -> CHandle { - let nr = match unsafe { node_ref_of(&owner) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return CHandle::null(), - }; - let (project, id) = nr; - let workarea = { - let mut g = project.lock().unwrap_or_else(|e| e.into_inner()); - let seq = match g - .graph - .get_mut(id) - .and_then(|e| e.behavior.as_any_mut()) - .and_then(|a| a.downcast_mut::()) - { - Some(s) => s, - None => return CHandle::null(), - }; - if seq.workarea.is_null() { - seq.workarea = oaktimeline::handle::make_owned( - oaktimeline::workarea::TimelineWorkArea::new(), - ); - } - seq.workarea - }; - if let Some(addref) = workarea.addref { - // SAFETY: the handle is live. - unsafe { addref(workarea.ctx) }; - } - workarea - } - - /// `oaktimeline_workarea_free`. - pub fn oaktimeline_workarea_free(w: *mut CHandle) { - if w.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - let h = unsafe { *w }; - if let Some(release) = h.release { - // SAFETY: the boxed value's release callback. - unsafe { release(h.ctx) }; - } - // SAFETY: the caller passes a valid handle pointer. - unsafe { *w = CHandle::null() }; - } - - /// `oaktimeline_workarea_set_enabled`. - pub fn oaktimeline_workarea_set_enabled(w: CHandle, enabled: c_int) -> c_int { - // SAFETY: work-area handles box TimelineWorkArea. - match unsafe { oaktimeline::handle::get_mut::>>(&w) } { - Some(wa) => { - wa.lock().unwrap_or_else(|e| e.into_inner()).set_enabled(enabled != 0); - oaktimeline::error::OAKTIMELINE_OK - } - None => oaktimeline::error::OAKTIMELINE_E_INVALID, - } - } - - /// `oaktimeline_workarea_get` — range + enabled. - 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 { - // SAFETY: work-area handles box TimelineWorkArea. - let wa = match unsafe { oaktimeline::handle::get::>>(&w) } { - Some(wa) => wa, - None => return oaktimeline::error::OAKTIMELINE_E_INVALID, - }; - let range = *wa.lock().unwrap_or_else(|e| e.into_inner()).range(); - // Out params may individually be NULL (the header contract); write - // each only when the caller supplied a target. - unsafe { - if !in_num.is_null() { - *in_num = range.in_().numerator() as c_int; - } - if !in_den.is_null() { - *in_den = range.in_().denominator() as c_int; - } - if !out_num.is_null() { - *out_num = range.out().numerator() as c_int; - } - if !out_den.is_null() { - *out_den = range.out().denominator() as c_int; - } - if !enabled.is_null() { - *enabled = if wa.lock().unwrap_or_else(|e| e.into_inner()).enabled() { 1 } else { 0 }; - } - } - oaktimeline::error::OAKTIMELINE_OK - } - - /// `oaktimeline_workarea_set_range`. - 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 { - if in_den == 0 || out_den == 0 { - return oaktimeline::error::OAKTIMELINE_E_INVALID; - } - // SAFETY: work-area handles box TimelineWorkArea. - match unsafe { oaktimeline::handle::get_mut::>>(&w) } { - Some(wa) => { - wa.lock().unwrap_or_else(|e| e.into_inner()).set_range(TimeRange::new( - Rational::new(in_num as i64, in_den as i64), - Rational::new(out_num as i64, out_den as i64), - )); - oaktimeline::error::OAKTIMELINE_OK - } - None => oaktimeline::error::OAKTIMELINE_E_INVALID, - } - } - - /// `oaktimeline_workarea_set_range_command`. - 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 { - if in_den == 0 || out_den == 0 || old_in_den == 0 || old_out_den == 0 { - return CHandle::null(); - } - let range = TimeRange::new( - Rational::new(in_num as i64, in_den as i64), - Rational::new(out_num as i64, out_den as i64), - ); - let old_range = TimeRange::new( - Rational::new(old_in_num as i64, old_in_den as i64), - Rational::new(old_out_num as i64, old_out_den as i64), - ); - let cmd = oaktimeline::workarea::WorkareaSetRangeCommand::new_with_old(w, range, old_range); - box_command(cmd.to_command()) - } - - /// `oaktimeline_workarea_set_enabled_command`. - pub fn oaktimeline_workarea_set_enabled_command(w: CHandle, enabled: c_int) -> CHandle { - let cmd = - oaktimeline::workarea::WorkareaSetEnabledCommand::new(w, enabled != 0); - box_command(cmd.to_command()) - } - - /// `oaktimeline_workarea_reset` — the documented reset defaults - /// (`reset_in`/`reset_out`). - 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 { - if in_num.is_null() || in_den.is_null() || out_num.is_null() || out_den.is_null() { - return oaktimeline::error::OAKTIMELINE_E_INVALID; - } - let rin = oaktimeline::workarea::reset_in(); - let rout = oaktimeline::workarea::reset_out(); - // SAFETY: valid out pointers. - unsafe { - *in_num = rin.numerator() as c_int; - *in_den = rin.denominator() as c_int; - *out_num = rout.numerator() as c_int; - *out_den = rout.denominator() as c_int; - } - oaktimeline::error::OAKTIMELINE_OK - } - - /// `oaktimeline_workarea_load` — STUB: see - /// [`oaktimeline_marker_list_load`]. - pub fn oaktimeline_workarea_load(_w: CHandle, _reader: CHandle) -> c_int { - oaktimeline::error::OAKTIMELINE_E_FAILED - } - - /// `oaktimeline_workarea_save` — STUB: see - /// [`oaktimeline_marker_list_load`]. - pub fn oaktimeline_workarea_save(_w: CHandle, _writer: CHandle) -> c_int { - oaktimeline::error::OAKTIMELINE_E_FAILED - } - - // ------------------------------------------------------------------- - // Timeline edit commands (oakundo UndoCommand values) - // ------------------------------------------------------------------- - - /// `oaktimeline_add_track_command`. - pub fn oaktimeline_add_track_command(list: CHandle) -> CHandle { - let timeline = nref!(list); - box_command(oaktimeline::undogeneral::TimelineAddTrackCommand::new(timeline).to_command()) - } - - /// `oaktimeline_remove_track_command`. - pub fn oaktimeline_remove_track_command(track: CHandle) -> CHandle { - let track = nref!(track); - box_command(oaktimeline::undogeneral::TimelineRemoveTrackCommand::new(track).to_command()) - } - - /// `oaktimeline_place_block_command`. - pub fn oaktimeline_place_block_command( - list: CHandle, - track_index: c_int, - block: CHandle, - in_num: i64, - in_den: i64, - ) -> CHandle { - if in_den == 0 { - return CHandle::null(); - } - let timeline = nref!(list); - let block = nref!(block); - let cmd = oaktimeline::undopointer::TrackPlaceBlockCommand::new( - timeline, - track_index, - block, - Rational::new(in_num, in_den), - ); - box_command(cmd.to_command()) - } - - /// `oaktimeline_replace_block_with_gap_command`. - pub fn oaktimeline_replace_block_with_gap_command(track: CHandle, block: CHandle) -> CHandle { - let track = nref!(track); - let block = nref!(block); - let cmd = oaktimeline::undogeneral::TrackReplaceBlockWithGapCommand::new( - track, block, true, - ); - box_command(cmd.to_command()) - } - - /// `oaktimeline_move_block_command`. - pub fn oaktimeline_move_block_command( - list: CHandle, - track_index: c_int, - block: CHandle, - in_num: i64, - in_den: i64, - ) -> CHandle { - if in_den == 0 { - return CHandle::null(); - } - let timeline = nref!(list); - let block = nref!(block); - let cmd = oaktimeline::undopointer::TrackMoveBlockCommand::new( - timeline, - track_index, - block, - Rational::new(in_num, in_den), - ); - box_command(cmd.to_command()) - } - - /// `oaktimeline_trim_command`. - pub fn oaktimeline_trim_command( - track: CHandle, - block: CHandle, - new_length_num: i64, - new_length_den: i64, - mode: c_int, - ) -> CHandle { - if new_length_den == 0 { - return CHandle::null(); - } - let track = nref!(track); - let block = nref!(block); - let mode = match oaktimeline::common::MovementMode::from_c_int(mode) { - Some(m) => m, - None => return CHandle::null(), - }; - let cmd = oaktimeline::undopointer::BlockTrimCommand::new( - track, - block, - Rational::new(new_length_num, new_length_den), - mode, - ); - box_command(cmd.to_command()) - } - - /// `oaktimeline_split_command` — one split command per block, combined - /// into a multi command. - pub fn oaktimeline_split_command( - blocks: *const CHandle, - count: c_int, - point_num: i64, - point_den: i64, - ) -> CHandle { - if blocks.is_null() || count <= 0 || point_den == 0 { - return CHandle::null(); - } - let mut children = Vec::new(); - for i in 0..count as usize { - // SAFETY: the caller guarantees `count` valid handles. - let h = unsafe { *blocks.add(i) }; - let Some(block) = node_ref(h) else { - return CHandle::null(); - }; - let cmd = oaktimeline::undosplit::BlockSplitCommand::new( - block, - Rational::new(point_num, point_den), - ); - children.push(cmd.to_command()); - } - let mut multi = UndoCommand::multi(); - for c in children { - multi.multi_add_child(c); - } - box_command(multi) - } - - /// `oaktimeline_split_preserving_links_command`. - 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 { - if blocks.is_null() || count <= 0 || point_nums.is_null() || point_dens.is_null() || time_count <= 0 { - return CHandle::null(); - } - let mut refs = Vec::new(); - for i in 0..count as usize { - // SAFETY: the caller guarantees `count` valid handles. - let h = unsafe { *blocks.add(i) }; - let Some(b) = node_ref(h) else { - return CHandle::null(); - }; - refs.push(b); - } - let mut times = Vec::new(); - for i in 0..time_count as usize { - // SAFETY: the caller guarantees `time_count` valid entries. - let (n, d) = unsafe { (*point_nums.add(i), *point_dens.add(i)) }; - if d == 0 { - return CHandle::null(); - } - times.push(Rational::new(n, d)); - } - let cmd = oaktimeline::undosplit::BlockSplitPreservingLinksCommand::new(refs, times); - box_command(cmd.to_command()) - } - - /// `oaktimeline_ripple_delete_gaps_command`. - 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 { - if in_nums.is_null() - || in_dens.is_null() - || out_nums.is_null() - || out_dens.is_null() - || tracks.is_null() - || range_count <= 0 - { - return CHandle::null(); - } - let timeline = nref!(sequence); - let mut regions = Vec::new(); - for i in 0..range_count as usize { - // SAFETY: the caller guarantees `range_count` valid entries. - let (in_n, in_d, out_n, out_d) = unsafe { - ( - *in_nums.add(i), - *in_dens.add(i), - *out_nums.add(i), - *out_dens.add(i), - ) - }; - let track = unsafe { *tracks.add(i) }; - if in_d == 0 || out_d == 0 { - return CHandle::null(); - } - let Some(track) = node_ref(track) else { - return CHandle::null(); - }; - regions.push(( - track, - TimeRange::new(Rational::new(in_n, in_d), Rational::new(out_n, out_d)), - )); - } - let cmd = - oaktimeline::undoripple::TimelineRippleDeleteGapsAtRegionsCommand::new(timeline, regions); - box_command(cmd.to_command()) - } - - /// `oaktimeline_slide_command`. - 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 { - if blocks.is_null() || block_count <= 0 || movement_den == 0 { - return CHandle::null(); - } - let track = nref!(track); - let mut refs = Vec::new(); - for i in 0..block_count as usize { - // SAFETY: the caller guarantees `block_count` valid handles. - let h = unsafe { *blocks.add(i) }; - let Some(b) = node_ref(h) else { - return CHandle::null(); - }; - refs.push(b); - } - let in_adjacent = if in_adjacent.is_null() { - None - } else { - node_ref(in_adjacent) - }; - let out_adjacent = if out_adjacent.is_null() { - None - } else { - node_ref(out_adjacent) - }; - let cmd = oaktimeline::undopointer::TrackSlideCommand::new( - track, - refs, - in_adjacent, - out_adjacent, - Rational::new(movement_num, movement_den), - ); - box_command(cmd.to_command()) - } - - /// `oaktimeline_ripple_remove_area_command`. - pub fn oaktimeline_ripple_remove_area_command( - track: CHandle, - in_num: i64, - in_den: i64, - out_num: i64, - out_den: i64, - ) -> CHandle { - if in_den == 0 || out_den == 0 { - return CHandle::null(); - } - let track = nref!(track); - let cmd = oaktimeline::undoripple::TrackRippleRemoveAreaCommand::new( - track, - TimeRange::new(Rational::new(in_num, in_den), Rational::new(out_num, out_den)), - ); - box_command(cmd.to_command()) - } - - /// `oaktimeline_insert_gaps_command`. - pub fn oaktimeline_insert_gaps_command( - list: CHandle, - point_num: i64, - point_den: i64, - length_num: i64, - length_den: i64, - ) -> CHandle { - if point_den == 0 || length_den == 0 { - return CHandle::null(); - } - let track_list = nref!(list); - let cmd = oaktimeline::undogeneral::TrackListInsertGaps::new( - track_list, - Rational::new(point_num, point_den), - Rational::new(length_num, length_den), - ); - box_command(cmd.to_command()) - } -} - -// =========================================================================== -// audio — direct Rust shims over the oakaudio crate -// =========================================================================== - -/// oakaudio bridge replacements: direct Rust calls into the `oakaudio` -/// crate (single-lib unification). Same names/signatures as the deleted -/// bridge functions; the manager CHandle is a borrow-only validity token -/// (the module owns the process-wide singleton), the processor CHandle -/// boxes an owned `oakaudio::processor::Processor` with a release -/// callback. The `oakcore_audioparams_*` C ABI (opaque `AudioParams` -/// handles in the frozen engine contract) is implemented here, inside the -/// dylib — the accessors were host-provided (C++ liboakcore, later Rust -/// mock shims in the app/cli/test binaries) until M12 P5 folded them in -/// so the cdylib carries no undefined imports. -pub mod audio { - use std::ffi::{c_char, c_double, c_int, c_void, CStr}; - - use oakaudio::error::{ - OAKAUDIO_E_FAILED, OAKAUDIO_E_INVALID, OAKAUDIO_E_NOT_FOUND, OAKAUDIO_E_STATE, OAKAUDIO_OK, - }; - use oakaudio::manager::ManagerInner; - use oakaudio::params::{sample_format_from_i32, AudioParams}; - use oakaudio::processor::Processor; - use oakaudio::synchronizer::{self, SourceClip as ModuleSourceClip}; - use oakaudio::waveform; - use oakaudio::waveformsync; - use oakcore_rs::Rational; - - use crate::handle::CHandle; - - pub use crate::pods::{ - AudioEncodingParams as EncodingParams, MinMax, OffsetResult, SourceClip, - StretchOffsetResult, - }; - - /// The liboakcore `AudioParams` object layout - /// (`oakcore_audioparams.h`): sample rate, ffmpeg channel-layout mask, - /// sample format, stream index, duration and time base. Mirrors - /// `olive::core::AudioParams` (and the app/cli mock shims it replaces), - /// so the engine contract's borrowed/owned `AudioParams*` handles - /// round-trip unchanged. - #[repr(C)] - struct HostAudioParams { - sample_rate: c_int, - channel_layout: u64, - format: c_int, - stream_index: c_int, - duration: i64, - time_base_num: c_int, - time_base_den: c_int, - } - - /// `oakcore_audioparams_create` — new owned audio params (release with - /// `oakcore_audioparams_free`). The time base defaults to 1/sample_rate, - /// exactly like the liboakcore constructor. - #[no_mangle] - pub extern "C" fn oakcore_audioparams_create( - sample_rate: c_int, - channel_layout: u64, - format: c_int, - ) -> *mut c_void { - let den = if sample_rate > 0 { sample_rate } else { 1 }; - Box::into_raw(Box::new(HostAudioParams { - sample_rate, - channel_layout, - format, - stream_index: 0, - duration: 0, - time_base_num: 1, - time_base_den: den, - })) as *mut c_void - } - - /// `oakcore_audioparams_free` — NULL no-op. - /// - /// # Safety - /// `params` must come from [`oakcore_audioparams_create`] or be NULL. - #[no_mangle] - pub extern "C" fn oakcore_audioparams_free(params: *mut c_void) { - if params.is_null() { - return; - } - // SAFETY: produced by `oakcore_audioparams_create`; we hold the only - // reference after the box is dropped. - unsafe { drop(Box::from_raw(params as *mut HostAudioParams)) }; - } - - /// `oakcore_audioparams_sample_rate` — 0 for NULL (liboakcore contract). - /// - /// # Safety - /// `params` must come from [`oakcore_audioparams_create`] or be NULL. - #[no_mangle] - pub extern "C" fn oakcore_audioparams_sample_rate(params: *const c_void) -> c_int { - if params.is_null() { - return 0; - } - // SAFETY: contract above. - unsafe { (*(params as *const HostAudioParams)).sample_rate } - } - - /// `oakcore_audioparams_channel_layout` — 0 for NULL. - /// - /// # Safety - /// `params` must come from [`oakcore_audioparams_create`] or be NULL. - #[no_mangle] - pub extern "C" fn oakcore_audioparams_channel_layout(params: *const c_void) -> u64 { - if params.is_null() { - return 0; - } - // SAFETY: contract above. - unsafe { (*(params as *const HostAudioParams)).channel_layout } - } - - /// `oakcore_audioparams_format` — 0 for NULL. - /// - /// # Safety - /// `params` must come from [`oakcore_audioparams_create`] or be NULL. - #[no_mangle] - pub extern "C" fn oakcore_audioparams_format(params: *const c_void) -> c_int { - if params.is_null() { - return 0; - } - // SAFETY: contract above. - unsafe { (*(params as *const HostAudioParams)).format } - } - - /// `oakcore_audioparams_set_time_base` — NULL no-op. - /// - /// # Safety - /// `params` must come from [`oakcore_audioparams_create`] or be NULL. - #[no_mangle] - pub extern "C" fn oakcore_audioparams_set_time_base( - params: *mut c_void, - num: c_int, - den: c_int, - ) { - if params.is_null() { - return; - } - // SAFETY: contract above. - unsafe { - let p = &mut *(params as *mut HostAudioParams); - p.time_base_num = num; - p.time_base_den = den; - } - } - - /// Map an oakaudio `Box` result to its public module code - /// (unknown downstream errors collapse to `OAKAUDIO_E_FAILED`, the same - /// fallback the deleted ffi layer used). - fn audio_code(e: &(dyn std::error::Error + 'static)) -> c_int { - match e.downcast_ref::() { - Some(audio_err) => audio_err.code(), - None => OAKAUDIO_E_FAILED, - } - } - - /// Write `s` into `buf` NUL-terminated with truncation (the deleted - /// module ffi's `write_error` contract; NULL/zero-sized buffers are - /// documented no-ops). - /// - /// # Safety - /// `buf` must point to `buf_size` writable bytes when non-NULL and - /// `buf_size > 0`. - unsafe fn write_error(s: &str, buf: *mut c_char, buf_size: c_int) { - if buf.is_null() || buf_size <= 0 { - return; - } - // SAFETY: contract above. - unsafe { - let bytes = s.as_bytes(); - let n = bytes.len().min((buf_size - 1) as usize); - std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, n); - *(buf as *mut u8).add(n) = 0; - } - } - - /// Map a failure's message into `error_buf` when it carries one (the - /// deleted ffi surfaced `Error::Failed` context strings on the failure - /// path only). - unsafe fn write_failed_error( - e: &(dyn std::error::Error + 'static), - error_buf: *mut c_char, - error_buf_size: c_int, - ) { - if let Some(oakaudio::error::Error::Failed(msg)) = - e.downcast_ref::() - { - // SAFETY: forwarded to `write_error`'s contract. - unsafe { write_error(msg, error_buf, error_buf_size) }; - } - } - - /// Non-null sentinel `ctx` for the borrow-only manager handle token. - /// Never dereferenced; the engine's `oakengine_audio_manager_handle` - /// hands it to the host as an opaque event-subscription token. - fn manager_token() -> *mut c_void { - std::ptr::NonNull::::dangling().as_ptr() as *mut c_void - } - - /// Lock the process-wide manager singleton (`None` when absent) and - /// reject empty handle tokens with the documented state error. - fn with_manager(self_: CHandle) -> Result, c_int> { - if self_.is_null() { - return Err(OAKAUDIO_E_STATE); - } - oakaudio::manager::instance().ok_or(OAKAUDIO_E_STATE) - } - - /// Borrow-only token of the manager singleton (empty when none). - pub fn oakaudio_manager_instance() -> CHandle { - match oakaudio::manager::instance() { - Some(_) => CHandle { - ctx: manager_token(), - addref: None, - release: None, - abi_version: 0, - }, - None => CHandle::null(), - } - } - - /// Create the process-wide manager singleton (no-op when it exists). - pub fn oakaudio_manager_create_instance() -> c_int { - match ManagerInner::create_instance() { - Ok(()) => OAKAUDIO_OK, - Err(e) => audio_code(&*e), - } - } - - /// Destroy the manager singleton (no-op when none exists). - pub fn oakaudio_manager_destroy_instance() { - ManagerInner::destroy_instance(); - } - - /// Borrowed singleton handles carry no release callback; free just - /// nulls the token. - pub fn oakaudio_manager_free(_self: *mut CHandle) { - if _self.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - unsafe { *_self = CHandle::null() }; - } - - /// Bytes between output-notify pulses. - pub fn oakaudio_manager_set_output_notify_interval(_self: CHandle, _bytes: i64) -> c_int { - match with_manager(_self).and_then(|m| m.set_output_notify_interval(_bytes).map_err(|e| audio_code(&*e))) { - Ok(()) => OAKAUDIO_OK, - Err(code) => code, - } - } - - /// Push a block of samples to the output device. - #[allow(clippy::too_many_arguments)] - 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 { - // CPP-PARITY (deleted ffi): `rate <= 0 || !samples || - // samples_size < 0` is invalid; an unrepresentable sample format - // is additionally rejected. - if _rate <= 0 || _samples.is_null() || _samples_size < 0 { - return OAKAUDIO_E_INVALID; - } - let format = sample_format_from_i32(_format); - if format == oakaudio::params::SampleFormat::Invalid { - return OAKAUDIO_E_INVALID; - } - let mut guard = match with_manager(_self) { - Ok(g) => g, - Err(code) => return code, - }; - // SAFETY: the caller guarantees `_samples` points to - // `_samples_size` readable bytes (size 0 reads nothing). - let samples = unsafe { - if _samples_size <= 0 { - &[][..] - } else { - std::slice::from_raw_parts(_samples as *const u8, _samples_size as usize) - } - }; - let params = AudioParams { - sample_rate: _rate, - channel_layout: _layout, - format, - }; - match guard.push_to_output(params, samples, &mut []) { - Ok(()) => OAKAUDIO_OK, - Err(e) => { - // SAFETY: NULL/empty error buffers are documented no-ops. - unsafe { write_failed_error(&*e, _error_buf, _error_buf_size) }; - audio_code(&*e) - } - } - } - - /// Discard buffered output. - pub fn oakaudio_manager_clear_buffered_output(_self: CHandle) -> c_int { - match with_manager(_self).and_then(|m| m.clear_buffered_output().map_err(|e| audio_code(&*e))) { - Ok(()) => OAKAUDIO_OK, - Err(code) => code, - } - } - - /// Stop the output stream. - pub fn oakaudio_manager_stop_output(_self: CHandle) -> c_int { - match with_manager(_self).and_then(|mut m| m.stop_output().map_err(|e| audio_code(&*e))) { - Ok(()) => OAKAUDIO_OK, - Err(code) => code, - } - } - - /// Restart the output clock at zero. - pub fn oakaudio_manager_reset_output_clock(_self: CHandle) -> c_int { - match with_manager(_self).and_then(|m| m.reset_output_clock().map_err(|e| audio_code(&*e))) { - Ok(()) => OAKAUDIO_OK, - Err(code) => code, - } - } - - /// Current output device index (`paNoDevice` = -1) or a negative error. - pub fn oakaudio_manager_get_output_device(_self: CHandle) -> c_int { - match with_manager(_self).and_then(|m| m.get_output_device().map_err(|e| audio_code(&*e))) { - Ok(device) => device, - Err(code) => code, - } - } - - /// Set the output device index. - pub fn oakaudio_manager_set_output_device(_self: CHandle, _device: c_int) -> c_int { - match with_manager(_self).and_then(|mut m| m.set_output_device(_device).map_err(|e| audio_code(&*e))) { - Ok(()) => OAKAUDIO_OK, - Err(code) => code, - } - } - - /// Current input device index or a negative error code. - pub fn oakaudio_manager_get_input_device(_self: CHandle) -> c_int { - match with_manager(_self).and_then(|m| m.get_input_device().map_err(|e| audio_code(&*e))) { - Ok(device) => device, - Err(code) => code, - } - } - - /// Set the input device index. - pub fn oakaudio_manager_set_input_device(_self: CHandle, _device: c_int) -> c_int { - match with_manager(_self).and_then(|mut m| m.set_input_device(_device).map_err(|e| audio_code(&*e))) { - Ok(()) => OAKAUDIO_OK, - Err(code) => code, - } - } - - /// The number of host output devices (enumeration order == the device - /// index `set_output_device` takes). Needs no manager instance. - pub fn oakaudio_output_device_count() -> c_int { - oakaudio::manager::output_device_names().len() as c_int - } - - /// The number of host input devices (see [`oakaudio_output_device_count`]). - pub fn oakaudio_input_device_count() -> c_int { - oakaudio::manager::input_device_names().len() as c_int - } - - /// The name of output device `index` (two-stage buf/size; reports the - /// required size INCLUDING the NUL, the module convention the facade - /// converts with `string_result`). - /// - /// # Safety - /// `buf` must point to `buf_size` writable bytes when non-NULL. - pub unsafe fn oakaudio_output_device_name( - index: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - unsafe { device_name_result(oakaudio::manager::output_device_names(), index, buf, buf_size) } - } - - /// The name of input device `index` (see [`oakaudio_output_device_name`]). - /// - /// # Safety - /// `buf` must point to `buf_size` writable bytes when non-NULL. - pub unsafe fn oakaudio_input_device_name( - index: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - unsafe { device_name_result(oakaudio::manager::input_device_names(), index, buf, buf_size) } - } - - /// Shared two-stage string write for the device-name getters: reports - /// `len + 1` (the required buffer size including the NUL), writes the - /// NUL-terminated name when it fits. `OAKAUDIO_E_NOT_FOUND` for an - /// out-of-range index. - unsafe fn device_name_result( - names: Vec, - index: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let Some(name) = names.get(index.max(0) as usize).filter(|_| index >= 0) else { - return OAKAUDIO_E_NOT_FOUND; - }; - // SAFETY: `buf` points to `buf_size` writable bytes when non-NULL. - unsafe { - if !buf.is_null() && buf_size > 0 { - let copy = name.len().min((buf_size as usize).saturating_sub(1)); - std::ptr::copy_nonoverlapping(name.as_ptr(), buf as *mut u8, copy); - *buf.add(copy) = 0; - } - } - name.len() as c_int + 1 - } - - /// Close the output stream and reset the playback state. - pub fn oakaudio_manager_hard_reset(_self: CHandle) -> c_int { - match with_manager(_self).and_then(|mut m| m.hard_reset().map_err(|e| audio_code(&*e))) { - Ok(()) => OAKAUDIO_OK, - Err(code) => code, - } - } - - /// Start recording the input device through the oakcodec encoder. - pub fn oakaudio_manager_start_recording( - _self: CHandle, - _params: *const EncodingParams, - _error_buf: *mut c_char, - _error_buf_size: c_int, - ) -> c_int { - // CPP-PARITY (deleted ffi): `!params || !params->audio_enabled` - // is invalid; the error string is written on the invalid path too - // so callers can always diagnose a failed start. - if _params.is_null() { - // SAFETY: NULL/empty error buffers are documented no-ops. - unsafe { write_error("invalid recording parameters", _error_buf, _error_buf_size) }; - return OAKAUDIO_E_INVALID; - } - // SAFETY: the caller guarantees `_params` points to a live - // `EncodingParams` POD for the duration of the call. - let params = unsafe { &*_params }; - if params.audio_enabled == 0 { - // SAFETY: NULL/empty error buffers are documented no-ops. - unsafe { write_error("invalid recording parameters", _error_buf, _error_buf_size) }; - return OAKAUDIO_E_INVALID; - } - let mut guard = match with_manager(_self) { - Ok(g) => g, - Err(code) => return code, - }; - match guard.start_recording(params, &mut []) { - Ok(()) => OAKAUDIO_OK, - Err(e) => { - // SAFETY: NULL/empty error buffers are documented no-ops. - unsafe { write_failed_error(&*e, _error_buf, _error_buf_size) }; - audio_code(&*e) - } - } - } - - /// Stop recording. - pub fn oakaudio_manager_stop_recording(_self: CHandle) -> c_int { - match with_manager(_self).and_then(|mut m| m.stop_recording().map_err(|e| audio_code(&*e))) { - Ok(()) => OAKAUDIO_OK, - Err(code) => code, - } - } - - /// Seconds of audio consumed by the output device since the last reset. - pub fn oakaudio_manager_seconds(_self: CHandle, _out: *mut c_double) -> c_int { - let guard = match with_manager(_self) { - Ok(g) => g, - Err(code) => return code, - }; - if _out.is_null() { - return OAKAUDIO_E_INVALID; - } - // SAFETY: the caller guarantees `_out` is a writable f64. - let out = unsafe { &mut *_out }; - match guard.seconds(out) { - Ok(()) => OAKAUDIO_OK, - Err(e) => audio_code(&*e), - } - } - - /// Per-channel peak levels of the buffered, not-yet-consumed output. - /// Returns the channel count (>= 0) or a negative error code. - pub fn oakaudio_manager_output_levels(_self: CHandle, _peaks: *mut f32, _capacity: c_int) -> c_int { - let guard = match with_manager(_self) { - Ok(g) => g, - Err(code) => return code, - }; - if _peaks.is_null() || _capacity <= 0 { - return OAKAUDIO_E_INVALID; - } - // SAFETY: the caller guarantees `_peaks` holds `_capacity` f32s. - let peaks = unsafe { std::slice::from_raw_parts_mut(_peaks, _capacity as usize) }; - match guard.output_levels(peaks) { - Ok(n) => n, - Err(e) => audio_code(&*e), - } - } - - // ---- Processor (owned, refcounted box behind a CHandle) ---------------- - - /// Test-only leak counter replacing the deleted module C ABI's - /// `oakaudio_debug_alive_count` (the oakaudio crate dropped its debug - /// counter together with its ffi layer). Counts live boxed processors - /// created through [`oakaudio_processor_init`] and released through - /// [`release_processor`]; the production cdylib has no counter. - #[cfg(test)] - static PROCESSOR_ALIVE: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); - - /// Test-only: current live-processor count (see [`PROCESSOR_ALIVE`]). - #[cfg(test)] - pub fn oakaudio_debug_alive_count() -> c_int { - PROCESSOR_ALIVE.load(std::sync::atomic::Ordering::SeqCst) - } - - /// Release callback for a boxed `oakaudio::processor::Processor`. - unsafe extern "C" fn release_processor(ctx: *mut c_void) { - if !ctx.is_null() { - // SAFETY: `ctx` was produced by `oakaudio_processor_init` and - // this callback runs exactly once. - unsafe { drop(Box::from_raw(ctx as *mut Processor)) }; - #[cfg(test)] - PROCESSOR_ALIVE.fetch_sub(1, std::sync::atomic::Ordering::SeqCst); - } - } - - /// Create a closed audio processor (boxed behind a CHandle). - pub fn oakaudio_processor_init() -> CHandle { - let p = Box::new(Processor::init()); - #[cfg(test)] - PROCESSOR_ALIVE.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - CHandle { - ctx: Box::into_raw(p) as *mut c_void, - addref: None, - release: Some(release_processor), - abi_version: 0, - } - } - - /// Release the boxed processor through the handle's release callback. - pub fn oakaudio_processor_free(_self: *mut CHandle) { - if _self.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - let h = unsafe { &mut *_self }; - if let Some(release) = h.release { - // SAFETY: `release` is the boxed type's release callback. - unsafe { release(h.ctx) }; - } - *h = CHandle::null(); - } - - /// Open the processor's conversion graph. - #[allow(clippy::too_many_arguments)] - 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 { - if _self.is_null() { - return OAKAUDIO_E_STATE; - } - // SAFETY: `_self.ctx` was produced by `oakaudio_processor_init`. - let p = unsafe { &*(_self.ctx as *const Processor) }; - let from = AudioParams { - sample_rate: _in_rate, - channel_layout: _in_layout, - format: sample_format_from_i32(_in_format), - }; - let to = AudioParams { - sample_rate: _out_rate, - channel_layout: _out_layout, - format: sample_format_from_i32(_out_format), - }; - match p.open(from, to, _speed) { - Ok(()) => OAKAUDIO_OK, - Err(e) => audio_code(&*e), - } - } - - /// Close the processor's conversion graph (safe when closed). - pub fn oakaudio_processor_close(_self: CHandle) -> c_int { - if _self.is_null() { - return OAKAUDIO_E_STATE; - } - // SAFETY: see `oakaudio_processor_open`. - let p = unsafe { &*(_self.ctx as *const Processor) }; - match p.close() { - Ok(()) => OAKAUDIO_OK, - Err(e) => audio_code(&*e), - } - } - - /// 1 when open, 0 when closed. - pub fn oakaudio_processor_is_open(_self: CHandle) -> c_int { - if _self.is_null() { - return 0; - } - // SAFETY: see `oakaudio_processor_open`. - let p = unsafe { &*(_self.ctx as *const Processor) }; - match p.is_open() { - Ok(open) => open as c_int, - Err(_) => 0, - } - } - - // ---- Stateless synchronization helpers --------------------------------- - - /// Rebuild the per-window validity mask the C ABI carried as `u8`s. - /// A NULL pointer (or an impossible length) yields an empty mask — - /// which the module treats as "all windows valid". - /// - /// # Safety - /// `mask` must point to `window_count` readable bytes when non-NULL. - unsafe fn window_mask(mask: *const u8, sample_len: c_int, window_samples: u64) -> Vec { - if mask.is_null() || window_samples == 0 || sample_len <= 0 { - return Vec::new(); - } - // SAFETY: contract above; the mask has one byte per RMS window. - let window_count = (sample_len as usize).div_ceil(window_samples as usize); - unsafe { std::slice::from_raw_parts(mask, window_count) } - .iter() - .map(|&b| b != 0) - .collect() - } - - /// Estimate the sample offset between two RMS envelopes. - #[allow(clippy::too_many_arguments)] - 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 { - // CPP-PARITY (deleted ffi sync.cpp:93): NULL arrays and - // non-positive lengths are invalid; masks may be NULL (all windows - // valid). - if _out.is_null() - || _reference.is_null() - || _candidate.is_null() - || _reference_len <= 0 - || _candidate_len <= 0 - || _window_samples == 0 - || _max_offset_windows < 0 - { - return OAKAUDIO_E_INVALID; - } - // SAFETY: the caller guarantees the sample arrays hold their - // lengths; the mask slices are built from the same contract. - let reference = unsafe { std::slice::from_raw_parts(_reference, _reference_len as usize) }; - let candidate = unsafe { std::slice::from_raw_parts(_candidate, _candidate_len as usize) }; - let reference_valid = unsafe { window_mask(_reference_valid, _reference_len, _window_samples) }; - let candidate_valid = unsafe { window_mask(_candidate_valid, _candidate_len, _window_samples) }; - let result = waveformsync::estimate_envelope_offset_valid( - reference, - candidate, - &reference_valid, - &candidate_valid, - _window_samples as usize, - _max_offset_windows, - ); - // SAFETY: `_out` is a writable `OffsetResult` POD (checked above). - unsafe { - (*_out).offset_samples = result.offset_samples; - (*_out).confidence = result.confidence; - (*_out).valid = result.valid as c_int; - } - OAKAUDIO_OK - } - - /// Estimate a playback-rate change plus offset. - #[allow(clippy::too_many_arguments)] - 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 { - // CPP-PARITY (deleted ffi sync.cpp:123). - if _out.is_null() - || _reference.is_null() - || _candidate.is_null() - || _reference_len <= 0 - || _candidate_len <= 0 - || _window_samples == 0 - || _max_offset_windows < 0 - || _min_rate <= 0.0 - || _max_rate < _min_rate - || _rate_step <= 0.0 - { - return OAKAUDIO_E_INVALID; - } - // SAFETY: see `oakaudio_sync_estimate_envelope_offset`. - let reference = unsafe { std::slice::from_raw_parts(_reference, _reference_len as usize) }; - let candidate = unsafe { std::slice::from_raw_parts(_candidate, _candidate_len as usize) }; - let reference_valid = unsafe { window_mask(_reference_valid, _reference_len, _window_samples) }; - let candidate_valid = unsafe { window_mask(_candidate_valid, _candidate_len, _window_samples) }; - let result = waveformsync::estimate_stretch_and_offset( - reference, - candidate, - &reference_valid, - &candidate_valid, - _window_samples as usize, - _max_offset_windows, - _min_rate, - _max_rate, - _rate_step, - ); - // SAFETY: `_out` is a writable `StretchOffsetResult` POD. - unsafe { - (*_out).rate = result.rate; - (*_out).offset_samples = result.offset_samples; - (*_out).confidence = result.confidence; - (*_out).valid = result.valid as c_int; - } - OAKAUDIO_OK - } - - /// Rebuild a module `SourceClip` from the engine's POD mirror. - fn module_source_clip(pod: &SourceClip) -> ModuleSourceClip { - ModuleSourceClip { - source_start_time: Rational::new(pod.source_start_time_num, pod.source_start_time_den), - media_in: Rational::new(pod.media_in_num, pod.media_in_den), - has_source_start_time: pod.has_source_start_time != 0, - } - } - - /// Timeline placement from source timecodes. - 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 { - // CPP-PARITY (deleted ffi sync.cpp:153): every denominator must be - // non-zero. - if _reference.is_null() || _candidate.is_null() { - return OAKAUDIO_E_INVALID; - } - // SAFETY: the caller guarantees valid `SourceClip` PODs. - let (reference, candidate) = unsafe { (&*_reference, &*_candidate) }; - if _out_num.is_null() - || _out_den.is_null() - || _out_valid.is_null() - || reference.source_start_time_den == 0 - || reference.media_in_den == 0 - || candidate.source_start_time_den == 0 - || candidate.media_in_den == 0 - || _reference_timeline_in_den == 0 - { - return OAKAUDIO_E_INVALID; - } - let reference_clip = module_source_clip(reference); - let candidate_clip = module_source_clip(candidate); - let placement = synchronizer::place_by_source_time( - &reference_clip, - &candidate_clip, - Rational::new(_reference_timeline_in_num, _reference_timeline_in_den), - ); - // SAFETY: the caller guarantees writable out parameters. - unsafe { - *_out_num = placement.timeline_in.numerator(); - *_out_den = placement.timeline_in.denominator(); - *_out_valid = placement.valid as c_int; - } - OAKAUDIO_OK - } - - /// Timeline placement from a measured waveform offset. - 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 { - // CPP-PARITY (deleted ffi sync.cpp:191). - if _out_num.is_null() - || _out_den.is_null() - || _out_valid.is_null() - || _reference_timeline_in_den == 0 - { - return OAKAUDIO_E_INVALID; - } - let placement = synchronizer::place_by_waveform_offset( - Rational::new(_reference_timeline_in_num, _reference_timeline_in_den), - _candidate_offset_samples, - _sample_rate, - ); - // SAFETY: the caller guarantees writable out parameters. - unsafe { - *_out_num = placement.timeline_in.numerator(); - *_out_den = placement.timeline_in.denominator(); - *_out_valid = placement.valid as c_int; - } - OAKAUDIO_OK - } - - /// Two-stage whole-file min/max waveform extraction. The module's - /// [`waveform::extract`] has no two-stage entry point, so the shim - /// emulates it: the decode outcome is cached per (filename, stream, - /// samples-per-point) so the count pass and the data pass decode ONCE - /// (the deleted ffi cached for the same reason — the FFmpeg decoder is - /// not safe against back-to-back sessions in the same process). A - /// missing file reports `OAKAUDIO_E_NOT_FOUND` (the deleted bridge's - /// codec-probe behavior) before any decode is attempted. - pub fn oakaudio_waveform_extract( - _filename: *const c_char, - _stream_index: c_int, - _samples_per_point: c_int, - _out_pairs: *mut MinMax, - _capacity_points: c_int, - _out_channel_count: *mut c_int, - ) -> c_int { - // CPP-PARITY (deleted ffi waveform.cpp:409). - if _filename.is_null() || _stream_index < 0 || _samples_per_point <= 0 || _capacity_points < 0 { - return OAKAUDIO_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated path. - let filename = unsafe { CStr::from_ptr(_filename) }; - let key = ( - filename.to_string_lossy().into_owned(), - _stream_index, - _samples_per_point, - ); - // The deleted bridge surfaced the codec probe's NOT_FOUND for a - // missing file; the direct module API reports a generic failure. - use std::os::unix::ffi::OsStrExt; - if std::fs::metadata(std::path::Path::new(std::ffi::OsStr::from_bytes( - filename.to_bytes(), - ))) - .is_err() - { - return OAKAUDIO_E_NOT_FOUND; - } - let outcome = { - static CACHE: std::sync::OnceLock< - std::sync::Mutex>, - > = std::sync::OnceLock::new(); - let cache = CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new())); - let mut guard = cache.lock().unwrap_or_else(|e| e.into_inner()); - if let Some(cached) = guard.get(&key) { - cached.clone() - } else { - let outcome = match waveform::extract(filename, _stream_index, _samples_per_point) { - Ok(outcome) => outcome, - Err(e) => return audio_code(e.as_ref()), - }; - guard.insert(key, outcome.clone()); - outcome - } - }; - let channels = outcome.channels.max(0) as usize; - if !_out_channel_count.is_null() { - // SAFETY: the caller guarantees a writable c_int. - unsafe { *_out_channel_count = outcome.channels }; - } - let points_needed = if channels == 0 { - 0 - } else { - outcome.points.len() / channels - }; - // The data pass writes only when the capacity covers the point - // count (capacity is in points, not pairs). - if _out_pairs.is_null() || (_capacity_points as usize) < points_needed { - return points_needed as c_int; - } - // SAFETY: `_out_pairs` holds `points_needed * channels` entries - // (capacity checked above). - unsafe { - std::ptr::copy_nonoverlapping( - outcome.points.as_ptr() as *const MinMax, - _out_pairs, - points_needed * channels, - ); - } - points_needed as c_int - } -} - -// =========================================================================== -// plugin — direct Rust shims over the oakplugin crate -// =========================================================================== - -/// oakplugin bridge replacements: direct Rust calls into the `oakplugin` -/// crate (single-lib unification). Same names/signatures as the deleted -/// bridge functions; the OFX host singleton is [`oakplugin::host::Host::global`]. -pub mod plugin { - use std::ffi::{c_char, c_int, CStr}; - use std::path::Path; - - use oakplugin::error::{OAKPLUGIN_E_INVALID, OAKPLUGIN_E_NOT_FOUND, OAKPLUGIN_OK}; - use oakplugin::host::Host; - - use crate::handle::read_cstr; - - /// Standard two-stage getter copy: copy only when the buffer is large - /// enough (never truncates); always return the required size incl. NUL. - fn copy_string(value: &str, buf: *mut c_char, buf_size: c_int) -> c_int { - let required = (value.len() + 1) as c_int; - if !buf.is_null() && buf_size >= required { - // SAFETY: the caller guarantees `buf` holds `buf_size` bytes. - unsafe { - std::ptr::copy_nonoverlapping(value.as_ptr() as *const c_char, buf, value.len()); - *buf.add(value.len()) = 0; - } - } - required - } - - /// Whether `(buf, buf_size)` is a valid two-stage getter output. - fn is_valid_string_out(buf: *mut c_char, buf_size: c_int) -> bool { - buf_size >= 0 && (buf_size == 0 || !buf.is_null()) - } - - /// Scan the given plugin bundle directories (oakplugin_host_scan). - pub fn oakplugin_host_scan(_bundle_dirs: *const *const c_char, _dir_count: c_int) -> c_int { - if _bundle_dirs.is_null() || _dir_count < 0 { - return OAKPLUGIN_E_INVALID; - } - let host = Host::global(); - for i in 0.._dir_count as usize { - // SAFETY: the caller guarantees `_dir_count` valid directory - // pointers; NULL entries are skipped (documented no-op). - let dir_ptr = unsafe { *_bundle_dirs.add(i) }; - if dir_ptr.is_null() { - continue; - } - // SAFETY: `dir_ptr` is a valid NUL-terminated C string. - let dir = match unsafe { CStr::from_ptr(dir_ptr) }.to_str() { - Ok(dir) => dir, - Err(_) => return OAKPLUGIN_E_INVALID, // non-UTF-8 paths are rejected - }; - if let Err(e) = host.cache.scan_path(Path::new(dir)) { - return e.code(); - } - } - OAKPLUGIN_OK - } - - /// Initialize the OFX host (oakplugin_host_init; idempotent singleton). - pub fn oakplugin_host_init() -> c_int { - let _ = Host::global(); - OAKPLUGIN_OK - } - - /// Number of loaded plugins. - pub fn oakplugin_host_plugin_count() -> c_int { - Host::global().cache.count() as c_int - } - - /// Identifier of the plugin at `_index` (two-stage buf/size getter). - pub fn oakplugin_host_plugin_id_at(_index: c_int, _buf: *mut c_char, _buf_size: c_int) -> c_int { - if _index < 0 || !is_valid_string_out(_buf, _buf_size) { - return OAKPLUGIN_E_INVALID; - } - match Host::global().cache.at(_index as usize) { - Some(p) => copy_string(&p.identifier, _buf, _buf_size), - None => OAKPLUGIN_E_NOT_FOUND, - } - } - - /// Label (`OfxPropLabel`) of the named plugin (two-stage getter). - pub fn oakplugin_host_plugin_label( - _plugin_id: *const c_char, - _buf: *mut c_char, - _buf_size: c_int, - ) -> c_int { - if _plugin_id.is_null() || !is_valid_string_out(_buf, _buf_size) { - return OAKPLUGIN_E_INVALID; - } - // SAFETY: the caller guarantees a valid NUL-terminated id. - let id = unsafe { read_cstr(_plugin_id) }; - match Host::global().cache.find(&id) { - Some(p) => { - let label = match p.descriptor.props.get("OfxPropLabel", 0) { - Some(oakplugin::property::Value::String(s)) => s.to_string_lossy().into_owned(), - _ => String::new(), - }; - copy_string(&label, _buf, _buf_size) - } - None => OAKPLUGIN_E_NOT_FOUND, - } - } -} - - -// =========================================================================== -// render — oakrender domain implementation (single-lib unification) -// =========================================================================== -// -// The deleted oakrender C ABI is replaced by the crate's direct Rust -// types: the manager singleton (`oakrender::manager::RenderManager`), -// the ticket arena (`oakrender::ticket::TicketArena` — owned by the -// manager, so the facade submits through `RenderManager::global()`), -// value-typed frames (`oakrender::texture::Frame`), the color processor -// (`oakrender::color::ColorProcessor`) and the auto-cacher -// (`oakrender::autocacher::PreviewAutoCacher`). -// -// The facade's ticket handles box a [`TicketBox`] payload (arena + id + -// completion result); the engine's synchronous render loop then waits and -// reads the produced frame/samples through it. -pub mod render { - use std::ffi::{c_char, c_double, c_int, c_void}; - use std::sync::{Arc, Mutex}; - - use oakrender::error::{OAKRENDER_E_INVALID, OAKRENDER_E_NOT_FOUND, OAKRENDER_E_STATE}; - use oakrender::ticket::{TicketArena, TicketId, TicketPayload, TicketResult}; - - use crate::handle::domain::node_ref_of; - use crate::handle::CHandle; - use crate::pods::{OakRenderVideoParams, OakVideoTicketParams}; - - // ------------------------------------------------------------------- - // Manager / color config - // ------------------------------------------------------------------- - - /// `oakrender_manager_init` — direct call into - /// `oakrender::manager::RenderManager::init`. - pub fn oakrender_manager_init() -> c_int { - match oakrender::manager::RenderManager::init() { - Ok(()) => 0, - Err(e) => e.code(), - } - } - - /// `oakrender_manager_available` — 1 when the global manager is up. - pub fn oakrender_manager_available() -> c_int { - oakrender::manager::RenderManager::global().is_some() as c_int - } - - /// `oakrender_manager_shutdown` — direct call into - /// `oakrender::manager::RenderManager::shutdown`. - pub fn oakrender_manager_shutdown() { - oakrender::manager::RenderManager::shutdown(); - } - - /// `oakrender_manager_set_aggressive_gc` — direct call. - pub fn oakrender_manager_set_aggressive_gc(enabled: c_int) -> c_int { - match oakrender::manager::RenderManager::global() { - Some(m) => { - m.set_aggressive_gc(enabled != 0); - 0 - } - None => OAKRENDER_E_STATE, - } - } - - /// `oakrender_color_manager_set_up_default_config` — direct call into - /// `oakrender::color::set_up_default_config`. - pub fn oakrender_color_manager_set_up_default_config() -> c_int { - match oakrender::color::set_up_default_config() { - Ok(()) => 0, - Err(_) => oakrender::error::OAKRENDER_E_FAILED, - } - } - - /// `oakrender_color_manager_get_config` (two-stage string getter) — - /// direct call into `oakrender::color::config_path`. - pub fn oakrender_color_manager_get_config(buf: *mut c_char, n: c_int) -> c_int { - match oakrender::color::config_path() { - Some(path) => { - let required = (path.len() + 1) as c_int; - if !buf.is_null() && n >= required { - // SAFETY: the caller guarantees `buf` holds `n` bytes. - unsafe { - std::ptr::copy_nonoverlapping( - path.as_ptr() as *const c_char, - buf, - path.len(), - ); - *buf.add(path.len()) = 0; - } - } - required - } - None => OAKRENDER_E_STATE, // no config = uninitialized color manager - } - } - - /// The manager's auto-cacher view. - fn with_cacher(f: impl FnOnce(&mut oakrender::autocacher::PreviewAutoCacher) -> R) -> Option { - let m = oakrender::manager::RenderManager::global()?; - let mut guard = m.get_cacher(); - let cacher = guard.as_mut()?; - Some(f(cacher)) - } - - /// `oakrender_set_cacher_multicam` — the viewer identity the - /// auto-cacher previews (null clears it). - pub fn oakrender_set_cacher_multicam(multicam_or_null: CHandle) -> c_int { - let identity = if multicam_or_null.is_null() { - None - } else { - // SAFETY: node handles box oaknode NodeRef payloads. - unsafe { node_ref_of(&multicam_or_null) } - .map(|nr| nr.id.identity()) - }; - match with_cacher(|c| c.set_viewer_identity(identity)) { - Some(()) => 0, - None => OAKRENDER_E_STATE, - } - } - - /// `oakrender_set_display_color_processor` — the display color - /// processor identity the auto-cacher applies (null clears it). - pub fn oakrender_set_display_color_processor(p_or_null: CHandle) -> c_int { - let identity = if p_or_null.is_null() { - None - } else { - Some(p_or_null.ctx as u64) - }; - match with_cacher(|c| c.set_display_color_processor(identity)) { - Some(()) => 0, - None => OAKRENDER_E_STATE, - } - } - - // ------------------------------------------------------------------- - // Ticket family - // ------------------------------------------------------------------- - - /// Engine-side ticket payload: the arena plus the reserved id and the - /// completion result slot. - struct TicketBox { - arena: Arc, - id: TicketId, - } - - /// Convert the engine's montage POD into the arena's montage clips. - /// - /// # Safety - /// `pods` must hold `count` valid [`crate::pods::MontagePod`] entries. - unsafe fn montage_from_pod(pods: *const crate::pods::MontagePod, count: c_int) -> Vec { - if pods.is_null() || count <= 0 { - return Vec::new(); - } - // SAFETY: contract above. - unsafe { - let slice = std::slice::from_raw_parts(pods, count as usize); - slice - .iter() - .map(|p| oakrender::ticket::MontageClip { - filename: crate::handle::read_cstr(p.filename), - stream_index: p.stream_index, - in_time: oakcore_rs::Rational::new(p.in_num, p.in_den), - out_time: oakcore_rs::Rational::new(p.out_num, p.out_den), - media_in: oakcore_rs::Rational::new(p.media_in_num, p.media_in_den), - gain: p.gain, - }) - .collect() - } - } - - /// `oakrender_ticket_render_frame` — submit a video ticket through the - /// manager's arena; the callback (when given) fires on completion with - /// the ticket handle. - pub fn oakrender_ticket_render_frame( - params: *const OakVideoTicketParams, - cb: Option, - userdata: *mut c_void, - ) -> CHandle { - if params.is_null() { - return CHandle::null(); - } - // SAFETY: the caller passes a live POD for the call duration. - let p = unsafe { &*params }; - let Some(m) = oakrender::manager::RenderManager::global() else { - return CHandle::null(); - }; - let viewer = unsafe { node_ref_of(&p.output_node) } - .map(|nr| nr.id.identity()) - .unwrap_or(0); - let force_size = if p.force_width > 0 && p.force_height > 0 { - Some((p.force_width, p.force_height)) - } else { - None - }; - let force_format = if p.force_format >= 0 { - Some(crate::pods::pixel_format_from_code(p.force_format)) - } else { - None - }; - let footage = if p.footage_filename.is_null() { - None - } else { - // SAFETY: the caller guarantees a valid NUL-terminated string. - Some(( - unsafe { crate::handle::read_cstr(p.footage_filename) }, - p.footage_stream, - )) - }; - let montage = unsafe { montage_from_pod(p.montage, p.montage_count) }; - let video_params = oakrender::ticket::VideoTicketParams { - viewer, - time: oakcore_rs::Rational::new(p.time_num, p.time_den), - force_size, - force_format, - cache: None, - cache_dir: None, - cache_id: None, - cache_timebase: None, - footage, - montage, - }; - let ticket_box = Arc::new(TicketBox { - arena: m.tickets.clone(), - id: m.tickets.next_id(), - }); - let ticket_h = oakrender::handle::make_owned(ticket_box.clone()); - let userdata_ptr = userdata as usize; - let completion: oakrender::ticket::Completion = Box::new(move |_result| { - if let Some(cb) = cb { - // SAFETY: the callback + userdata follow the caller's - // contract; the ticket handle outlives the completion. - unsafe { cb(ticket_h, userdata_ptr as *mut c_void) }; - } - }); - m.tickets - .submit_video_with_id(ticket_box.id, video_params, completion); - oakrender::handle::make_owned(ticket_box) - } - - /// `oakrender_ticket_render_audio` — submit an audio ticket through - /// the manager's arena (the `oakrender::eval::render_audio_samples` - /// producer mixes the montage). - 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, - userdata: *mut c_void, - montage: *const crate::pods::MontagePod, - montage_count: c_int, - ) -> CHandle { - if in_den == 0 || out_den == 0 { - return CHandle::null(); - } - let Some(m) = oakrender::manager::RenderManager::global() else { - return CHandle::null(); - }; - let viewer = unsafe { node_ref_of(&output_node) } - .map(|nr| nr.id.identity()) - .unwrap_or(0); - let sample_rate = if params.is_null() { - 48000 - } else { - // SAFETY: the oakcore audioparams contract. - unsafe { crate::stubs::audio::oakcore_audioparams_sample_rate(params) } - }; - let channel_layout = if params.is_null() { - 0x3 - } else { - // SAFETY: the oakcore audioparams contract. - unsafe { crate::stubs::audio::oakcore_audioparams_channel_layout(params) } - }; - let audio_params = oakrender::ticket::AudioTicketParams { - viewer, - range: oakcore_rs::TimeRange::new( - oakcore_rs::Rational::new(in_num, in_den), - oakcore_rs::Rational::new(out_num, out_den), - ), - sample_rate, - channel_layout, - montage: unsafe { montage_from_pod(montage, montage_count) }, - }; - let ticket_box = Arc::new(TicketBox { - arena: m.tickets.clone(), - id: m.tickets.next_id(), - }); - let ticket_h = oakrender::handle::make_owned(ticket_box.clone()); - let userdata_ptr = userdata as usize; - let completion: oakrender::ticket::Completion = Box::new(move |_result| { - if let Some(cb) = cb { - // SAFETY: see the video-ticket callback path. - unsafe { cb(ticket_h, userdata_ptr as *mut c_void) }; - } - }); - m.tickets - .submit_audio_with_id(ticket_box.id, audio_params, completion); - oakrender::handle::make_owned(ticket_box) - } - - /// `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) { - if samples.is_null() { - return; - } - // SAFETY: `samples` must be a box created for the samples block. - drop(unsafe { Box::from_raw(samples as *mut crate::pods::OakAudioSamplesOut) }); - } - - fn ticket_box(ticket: CHandle) -> Option<&'static TicketBox> { - // Ticket handles box `Arc` (the completion closure and - // the arena slot hold their own clones, so the payload outlives - // every handle access). - // SAFETY: ticket handles box Arc payloads. - let t = unsafe { oakrender::handle::get::>(&ticket) }?; - // SAFETY: the Arc (and the TicketBox it owns) lives for the whole - // ticket lifetime; the handle's own reference is just one of several. - let arc: &'static Arc = unsafe { std::mem::transmute(t) }; - Some(&**arc) - } - - /// `oakrender_ticket_wait` — block until the ticket finishes. - pub fn oakrender_ticket_wait(ticket: CHandle) -> c_int { - let Some(t) = ticket_box(ticket) else { - return OAKRENDER_E_INVALID; - }; - match t.arena.wait(t.id) { - Ok(()) => 0, - Err(e) => e.code(), - } - } - - /// `oakrender_ticket_cancel` — cancel the pending ticket. - pub fn oakrender_ticket_cancel(ticket: CHandle) -> c_int { - let Some(t) = ticket_box(ticket) else { - return OAKRENDER_E_INVALID; - }; - t.arena.cancel(t.id); - 0 - } - - /// `oakrender_ticket_get_frame` — the produced video frame (a boxed - /// `oakrender::texture::Frame`), empty when the ticket is unfinished, - /// cancelled or audio. - pub fn oakrender_ticket_get_frame(ticket: CHandle, out: *mut CHandle) -> c_int { - if out.is_null() { - return OAKRENDER_E_INVALID; - } - let Some(t) = ticket_box(ticket) else { - return OAKRENDER_E_INVALID; - }; - // Read the arena slot's result directly: `TicketSlot::finish` - // stores it BEFORE flipping the slot to Finished, so this is - // race-free (the completion callback fires after the notify and may - // not have run yet when the caller reads the frame). - let result = t.arena.result(t.id); - match result { - Some(Ok(TicketPayload::Video(texture))) => match texture.to_frame() { - Ok(frame) => { - // SAFETY: valid out pointer. - unsafe { *out = oakrender::handle::make_owned(frame) }; - 0 - } - Err(e) => e.code(), - }, - Some(Ok(TicketPayload::ShmFrame(frame))) => { - // M15 S2 process backend: the frame lives in a worker shm - // slot. Copy it out once as an RGBA8 u8 frame (necessary - // copy — the C ABI consumer owns its buffer), release the - // slot, and hand the frame back. - let meta = &frame.meta; - let pixels = frame - .shm - .slot_bytes(frame.slot) - .get(..meta.data_size.max(0) as usize) - .unwrap_or_default(); - let mut f = oakrender::texture::Frame::new(); - f.width = meta.width; - f.height = meta.height; - f.format = oakcore_rs::PixelFormat::U8; - f.channels = 4; - f.timestamp = oakcore_rs::Rational::new(meta.time_num, meta.time_den); - f.data = oakrender::procpool::bgra8_to_rgba8(pixels); - if let Some(m) = oakrender::manager::RenderManager::global() { - m.release_frame(&frame); - } - // SAFETY: valid out pointer. - unsafe { *out = oakrender::handle::make_owned(f) }; - 0 - } - Some(Err(e)) => e.code(), - _ => { - // SAFETY: valid out pointer. - unsafe { *out = CHandle::null() }; - OAKRENDER_E_STATE - } - } - } - - /// `oakrender_ticket_get_samples` — the produced audio samples block - /// (caller frees with `oakrender_audio_samples_free`). - pub fn oakrender_ticket_get_samples(ticket: CHandle, out: *mut *mut c_void) -> c_int { - if out.is_null() { - return OAKRENDER_E_INVALID; - } - let Some(t) = ticket_box(ticket) else { - return OAKRENDER_E_INVALID; - }; - // Read the arena slot's result directly: `TicketSlot::finish` - // stores it BEFORE flipping the slot to Finished, so this is - // race-free (the completion callback fires after the notify and may - // not have run yet when the caller reads the frame). - let result = t.arena.result(t.id); - match result { - Some(Ok(TicketPayload::Audio(samples))) => { - let frame_count = - (samples.samples.len() / samples.channel_count.max(1) as usize) as c_int; - let boxed = Box::new(crate::pods::OakAudioSamplesOut { - data: samples.samples.into_boxed_slice(), - frame_count, - sample_rate: samples.sample_rate, - channel_layout: samples.channel_layout, - channel_count: samples.channel_count, - }); - // SAFETY: valid out pointer. - unsafe { *out = Box::into_raw(boxed) as *mut c_void }; - 0 - } - Some(Err(e)) => e.code(), - _ => { - // SAFETY: valid out pointer. - unsafe { *out = std::ptr::null_mut() }; - OAKRENDER_E_STATE - } - } - } - - /// `oakrender_ticket_free` — release the ticket handle shell. - pub fn oakrender_ticket_free(ticket: *mut CHandle) { - if ticket.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - let h = unsafe { *ticket }; - if let Some(release) = h.release { - // SAFETY: the boxed value's release callback. - unsafe { release(h.ctx) }; - } - // SAFETY: the caller passes a valid handle pointer. - unsafe { *ticket = CHandle::null() }; - } - - // ------------------------------------------------------------------- - // Codec frame family (`oakrender::texture::Frame` boxes) - // ------------------------------------------------------------------- - - fn frame_ref(frame: CHandle) -> Option<&'static oakrender::texture::Frame> { - // SAFETY: frame handles box `oakrender::texture::Frame`. - let f = unsafe { oakrender::handle::get::(&frame) }?; - // SAFETY: the box outlives the handle. - unsafe { Some(&*(f as *const _)) } - } - - /// `oakrender_codec_frame_create` — a fresh, unallocated frame. - pub fn oakrender_codec_frame_create() -> CHandle { - oakrender::handle::make_owned(oakrender::texture::Frame::new()) - } - - /// `oakrender_codec_frame_retain` — addref'd copy. - pub fn oakrender_codec_frame_retain(frame: CHandle) -> CHandle { - if frame.is_null() { - return CHandle::null(); - } - if let Some(addref) = frame.addref { - // SAFETY: the handle is live. - unsafe { addref(frame.ctx) }; - } - frame - } - - /// `oakrender_codec_frame_free` — release the frame shell. - pub fn oakrender_codec_frame_free(frame: *mut CHandle) { - if frame.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - let h = unsafe { *frame }; - if let Some(release) = h.release { - // SAFETY: the boxed value's release callback. - unsafe { release(h.ctx) }; - } - // SAFETY: the caller passes a valid handle pointer. - unsafe { *frame = CHandle::null() }; - } - - /// `oakrender_codec_frame_width`. - pub fn oakrender_codec_frame_width(frame: CHandle) -> c_int { - frame_ref(frame) - .map(|f| f.video_params().width) - .unwrap_or(0) - } - - /// `oakrender_codec_frame_height`. - pub fn oakrender_codec_frame_height(frame: CHandle) -> c_int { - frame_ref(frame) - .map(|f| f.video_params().height) - .unwrap_or(0) - } - - /// `oakrender_codec_frame_linesize_bytes`. - pub fn oakrender_codec_frame_linesize_bytes(frame: CHandle) -> c_int { - frame_ref(frame) - .map(|f| f.linesize_bytes() as c_int) - .unwrap_or(0) - } - - /// `oakrender_codec_frame_data` — mutable pixel data view. - pub fn oakrender_codec_frame_data(frame: CHandle) -> *mut c_void { - // SAFETY: the caller holds exclusive access for the borrow. - match unsafe { oakrender::handle::get_mut::(&frame) } { - Some(f) => f.data_mut() as *mut c_void, - None => std::ptr::null_mut(), - } - } - - /// `oakrender_codec_frame_const_data` — read-only pixel data view. - pub fn oakrender_codec_frame_const_data(frame: CHandle) -> *const c_void { - frame_ref(frame) - .map(|f| f.data() as *const c_void) - .unwrap_or(std::ptr::null()) - } - - /// `oakrender_codec_frame_is_allocated`. - pub fn oakrender_codec_frame_is_allocated(frame: CHandle) -> c_int { - match frame_ref(frame) { - Some(f) if f.is_allocated() => 1, - _ => 0, - } - } - - /// `oakrender_codec_frame_get_params` — the frame's video-params POD. - pub fn oakrender_codec_frame_get_params( - frame: CHandle, - out: *mut OakRenderVideoParams, - ) -> c_int { - if out.is_null() { - return OAKRENDER_E_INVALID; - } - let Some(f) = frame_ref(frame) else { - return OAKRENDER_E_INVALID; - }; - let pod = f.video_params(); - // SAFETY: valid out pointer. - unsafe { - (*out) = OakRenderVideoParams { - width: pod.width, - height: pod.height, - time_base_num: pod.time_base_num, - time_base_den: pod.time_base_den, - format: pod.format, - pixel_aspect_num: pod.pixel_aspect_num, - pixel_aspect_den: pod.pixel_aspect_den, - interlacing: pod.interlacing, - color_range: pod.color_range, - divider: pod.divider, - video_type: pod.video_type, - premultiplied_alpha: pod.premultiplied_alpha, - }; - } - 0 - } - - // ------------------------------------------------------------------- - // Color processor family - // ------------------------------------------------------------------- - - /// The C ABI direction -> the domain direction. - fn direction_from_c(direction: c_int) -> oakrender::color::Direction { - if direction == 0 { - oakrender::color::Direction::Normal - } else { - oakrender::color::Direction::Inverse - } - } - - /// `oakrender_color_processor_create` — build a processor from two - /// colorspace names (OCIO failures are non-fatal: a pass-through - /// processor is returned). - pub fn oakrender_color_processor_create( - src_space: *const c_char, - dst_transform: *const c_char, - direction: c_int, - ) -> CHandle { - if src_space.is_null() || dst_transform.is_null() { - return CHandle::null(); - } - // SAFETY: the caller guarantees valid NUL-terminated strings. - let (src, dst) = unsafe { - ( - crate::handle::read_cstr(src_space), - crate::handle::read_cstr(dst_transform), - ) - }; - match oakrender::color::ColorProcessor::create(&src, &dst, direction_from_c(direction)) { - Some(processor) => oakrender::handle::make_owned(processor), - None => CHandle::null(), - } - } - - /// `oakrender_color_processor_free` — release the shell. - pub fn oakrender_color_processor_free(processor: *mut CHandle) { - if processor.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - let h = unsafe { *processor }; - if let Some(release) = h.release { - // SAFETY: the boxed value's release callback. - unsafe { release(h.ctx) }; - } - // SAFETY: the caller passes a valid handle pointer. - unsafe { *processor = CHandle::null() }; - } - - /// `oakrender_color_processor_is_valid`. - pub fn oakrender_color_processor_is_valid(processor: CHandle) -> c_int { - // SAFETY: processor handles box `oakrender::color::ColorProcessor`. - match unsafe { oakrender::handle::get::(&processor) } { - Some(p) if p.is_valid() => 1, - _ => 0, - } - } - - /// `oakrender_color_processor_create_transform` — build a processor - /// from the input colorspace and the destination described by an - /// oakcommon colortransform handle (display/view/look or a plain - /// output colorspace). - pub fn oakrender_color_processor_create_transform( - _manager: CHandle, - input: *const c_char, - dest: CHandle, - direction: c_int, - ) -> CHandle { - if input.is_null() || dest.is_null() { - return CHandle::null(); - } - // SAFETY: the caller guarantees a valid NUL-terminated string and - // a live oakcommon colortransform handle. - let (input, ct) = unsafe { - ( - crate::handle::read_cstr(input), - oakcommon::handle::get::(&dest), - ) - }; - let Some(ct) = ct else { - return CHandle::null(); - }; - let dst = if ct.is_display() { - match oakrender::color::display_transform(ct.display(), ct.view()) { - Some(d) => d, - None => return CHandle::null(), - } - } else { - ct.output().to_string() - }; - match oakrender::color::ColorProcessor::create(&input, &dst, direction_from_c(direction)) { - Some(processor) => oakrender::handle::make_owned(processor), - None => CHandle::null(), - } - } - - /// `oakrender_color_processor_convert` — one RGBA color through the - /// processor. - 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 { - if out_r.is_null() || out_g.is_null() || out_b.is_null() || out_a.is_null() { - return OAKRENDER_E_INVALID; - } - // SAFETY: processor handles box `oakrender::color::ColorProcessor`. - let Some(p) = (unsafe { oakrender::handle::get::(&processor) }) - else { - return OAKRENDER_E_INVALID; - }; - let rgba = p.convert_color([ir, ig, ib, ia]); - // SAFETY: valid out pointers. - unsafe { - *out_r = rgba[0]; - *out_g = rgba[1]; - *out_b = rgba[2]; - *out_a = rgba[3]; - } - 0 - } - - // ------------------------------------------------------------------- - // LUT library (`oakrender::color::SUPPORTED_LUT_EXTENSIONS`) - // ------------------------------------------------------------------- - - /// `oakrender_lut_is_supported_extension`. - pub fn oakrender_lut_is_supported_extension(extension: *const c_char) -> c_int { - if extension.is_null() { - return 0; - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let extension = unsafe { crate::handle::read_cstr(extension) }; - oakrender::color::is_supported_lut_extension(&extension) as c_int - } - - /// `oakrender_lut_supported_extensions_count`. - pub fn oakrender_lut_supported_extensions_count() -> c_int { - oakrender::color::SUPPORTED_LUT_EXTENSIONS.len() as c_int - } - - /// `oakrender_lut_supported_extension_at` (two-stage). - pub fn oakrender_lut_supported_extension_at(i: c_int, buf: *mut c_char, n: c_int) -> c_int { - if i < 0 { - return OAKRENDER_E_NOT_FOUND; - } - match oakrender::color::SUPPORTED_LUT_EXTENSIONS.get(i as usize) { - Some(ext) => { - let required = (ext.len() + 1) as c_int; - if !buf.is_null() && n >= required { - // SAFETY: the caller guarantees `buf` holds `n` bytes. - unsafe { - std::ptr::copy_nonoverlapping( - ext.as_ptr() as *const c_char, - buf, - ext.len(), - ); - *buf.add(ext.len()) = 0; - } - } - required - } - None => OAKRENDER_E_NOT_FOUND, - } - } -} - -// =========================================================================== -// task — oaktask domain implementation (single-lib unification) -// =========================================================================== -// -// The deleted oaktask C ABI is replaced by the crate's direct Rust types: -// the process-wide [`oaktask::manager::TaskManager`], the -// [`oaktask::task::Task`] base + the concrete project/render task types -// (`project::load::ProjectLoadTask`, `project::save::ProjectSaveTask`, -// `project::import::ProjectImportTask`, `project::loadotio::LoadOTIOTask`, -// `project::saveotio::SaveOTIOTask`, `precache::PreCacheTask`, -// `export::ExportTask`). -// -// The facade's task handles box a [`TaskPayload`] holding the owned driver -// `Task`; when the task is handed to the manager (`oaktask_task_start`) -// the box is moved into the manager and the payload keeps a raw -// (manager-owned) pointer so title/error/cancel keep working. Task-result -// accessors that the crate keeps private on the behavior -// (`take_project`/`take_command`) are bridged through engine-side -// wrapper behaviors that copy the results into shared slots in the -// payload. -pub mod task { - use std::ffi::{c_char, c_int, c_void}; - use std::sync::{Arc, Mutex, OnceLock}; - - use oaknode::project::Project; - use oakundo::undocommand::{command_from_owned, UndoCommand}; - - use crate::handle::domain::project_of; - use crate::handle::CHandle; - use crate::pods::EncodingParamsPOD; - - /// `oaktask_event_fn` callback (`include/task/task.h`). - 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; - - /// Raw pointer to a manager-owned task (Send shim). - struct TaskPtr(*mut oaktask::task::Task); - - // SAFETY: the pointee lives while the manager owns the task; the - // facade never dereferences it concurrently with the manager's worker. - unsafe impl Send for TaskPtr {} - - /// Engine-side task payload (boxed behind every facade task handle). - struct TaskPayload { - /// The owned driver task (None once handed to the manager). - owned: Option>, - /// Manager-owned view (valid while the manager keeps the task). - borrowed: TaskPtr, - /// Result slots for the specialized task kinds. - kind: TaskKind, - } - - enum TaskKind { - /// Plain task (export/precache/generic). - Plain, - /// Interchange load: the loaded project appears here. - Load { - result: Arc>>>>, - }, - /// Media import: the produced undo command + footage + invalid - /// file count. - Import { - command: Arc>>, - footage: Arc>>, - invalid_count: Arc>, - }, - } - - impl Drop for TaskPayload { - fn drop(&mut self) { - TASK_ALIVE.fetch_sub(1, std::sync::atomic::Ordering::SeqCst); - } - } - - impl TaskPayload { - fn task(&self) -> &oaktask::task::Task { - match &self.owned { - Some(t) => t, - None => { - // SAFETY: the manager owns the pointee for the - // handle's lifetime (facade contract). - unsafe { &*self.borrowed.0 } - } - } - } - - fn task_mut(&mut self) -> &mut oaktask::task::Task { - match &mut self.owned { - Some(t) => t, - None => { - // SAFETY: see `TaskPayload::task`. - unsafe { &mut *self.borrowed.0 } - } - } - } - } - - /// Live-task counter (the deleted `oaktask_debug_alive_count`). - static TASK_ALIVE: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); - - fn box_task(driver: oaktask::task::Task, kind: TaskKind) -> CHandle { - let ptr = Box::into_raw(Box::new(driver)); - TASK_ALIVE.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - oaktask::handle::make_owned(TaskPayload { - owned: Some(unsafe { Box::from_raw(ptr) }), - borrowed: TaskPtr(ptr), - kind, - }) - } - - fn task_payload(t: CHandle) -> Option<&'static TaskPayload> { - // SAFETY: task handles box TaskPayload. - let p = unsafe { oaktask::handle::get::(&t) }?; - // SAFETY: the box outlives the handle. - unsafe { Some(&*(p as *const _)) } - } - - fn task_payload_mut(t: CHandle) -> Option<&'static mut TaskPayload> { - // SAFETY: task handles box TaskPayload; the caller holds - // exclusive access. - let p = unsafe { oaktask::handle::get_mut::(&t) }?; - // SAFETY: the box outlives the handle. - unsafe { Some(&mut *(p as *mut _)) } - } - - /// A fresh base task with the given title. - fn base_task(title: &str) -> oaktask::task::Task { - oaktask::task::Task::new(title, None) - } - - // ------------------------------------------------------------------- - // Manager family - // ------------------------------------------------------------------- - - /// `oaktask_manager_init` — create the process-wide manager singleton. - pub fn oaktask_manager_init() -> c_int { - match oaktask::manager::TaskManager::init() { - Ok(()) => oaktask::error::OAKTASK_OK, - Err(e) => e.code(), - } - } - - /// `oaktask_manager_shutdown` — destroy the singleton (idempotent). - pub fn oaktask_manager_shutdown() { - oaktask::manager::TaskManager::shutdown(); - } - - /// `oaktask_register_codec_submitter` — record the registration flag. - pub fn oaktask_register_codec_submitter() -> c_int { - match oaktask::manager::TaskManager::with_manager_mut(|m| { - m.set_codec_submitter_registered(true); - }) { - Some(()) => oaktask::error::OAKTASK_OK, - None => oaktask::error::OAKTASK_E_STATE, - } - } - - /// `oaktask_manager_count` — live tasks. - pub fn oaktask_manager_count() -> c_int { - match oaktask::manager::TaskManager::with_manager(|m| m.get_task_count() as c_int) { - Some(n) => n, - None => oaktask::error::OAKTASK_E_STATE, - } - } - - /// `oaktask_manager_at` — a borrowed-view task handle of the task at - /// `i` (a payload box whose task pointer borrows the manager-owned - /// task; the manager must outlive the handle). - pub fn oaktask_manager_at(i: c_int) -> CHandle { - if i < 0 { - return CHandle::null(); - } - let ptr = match oaktask::manager::TaskManager::with_manager(|m| m.task_ptr_at(i as usize)) { - Some(Ok(p)) => p, - _ => return CHandle::null(), - }; - TASK_ALIVE.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - oaktask::handle::make_owned(TaskPayload { - owned: None, - borrowed: TaskPtr(ptr), - kind: TaskKind::Plain, - }) - } - - /// `oaktask_manager_delete_finished` — remove finished tasks. - pub fn oaktask_manager_delete_finished() { - oaktask::manager::TaskManager::with_manager_mut(|m| m.delete_finished()); - } - - /// `oaktask_task_free` — release the task handle shell (drops a still - /// owned task). - pub fn oaktask_task_free(t: *mut CHandle) { - if t.is_null() { - return; - } - // SAFETY: the caller passes a valid handle pointer. - let h = unsafe { *t }; - let _ = &h; - if let Some(release) = h.release { - // SAFETY: the boxed value's release callback (drops the - // payload: an owned task is destroyed, a manager-owned one is - // left alone). - unsafe { release(h.ctx) }; - } - // SAFETY: the caller passes a valid handle pointer. - unsafe { *t = CHandle::null() }; - } - - /// `oaktask_task_start_sync` — run on the calling thread (1/0). - pub fn oaktask_task_start_sync(t: CHandle) -> c_int { - let Some(p) = task_payload_mut(t) else { - return 0; - }; - match p.task_mut().start() { - Ok(()) => 1, - Err(_) => 0, - } - } - - /// `oaktask_task_start` — hand the owned task to the manager (the - /// manager spawns its worker and deletes the task when done; the - /// handle becomes a borrowed view). - pub fn oaktask_task_start(t: CHandle) -> c_int { - let Some(p) = task_payload_mut(t) else { - return oaktask::error::OAKTASK_E_INVALID; - }; - let Some(task) = p.owned.take() else { - return oaktask::error::OAKTASK_E_STATE; - }; - let raw = Box::into_raw(task); - p.borrowed = TaskPtr(raw); - if oaktask::manager::TaskManager::with_manager(|_| ()).is_none() { - p.owned = Some(unsafe { Box::from_raw(raw) }); - return oaktask::error::OAKTASK_E_STATE; - } - // SAFETY: the box was just created above; the manager takes - // ownership of the (stable) allocation. - oaktask::manager::TaskManager::with_manager_mut(|m| { - m.add_task(unsafe { Box::from_raw(raw) } as Box) - }); - oaktask::error::OAKTASK_OK - } - - /// `oaktask_task_cancel`. - pub fn oaktask_task_cancel(t: CHandle) -> c_int { - let Some(p) = task_payload_mut(t) else { - return oaktask::error::OAKTASK_E_INVALID; - }; - p.task_mut().cancel(); - oaktask::error::OAKTASK_OK - } - - /// `oaktask_task_wait` — block until the task finishes. - pub fn oaktask_task_wait(t: CHandle) -> c_int { - let Some(p) = task_payload(t) else { - return oaktask::error::OAKTASK_E_INVALID; - }; - p.task().wait_finished(); - oaktask::error::OAKTASK_OK - } - - /// `oaktask_task_is_finished` — 1/0. - pub fn oaktask_task_is_finished(t: CHandle) -> c_int { - match task_payload(t) { - Some(p) if p.task().is_finished() => 1, - Some(_) => 0, - None => 0, - } - } - - /// `oaktask_task_succeeded` — 1/0. - pub fn oaktask_task_succeeded(t: CHandle) -> c_int { - match task_payload(t) { - Some(p) if p.task().succeeded() => 1, - Some(_) => 0, - None => 0, - } - } - - /// `oaktask_task_title` (two-stage). - pub fn oaktask_task_title(t: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - let Some(p) = task_payload(t) else { - return oaktask::error::OAKTASK_E_INVALID; - }; - let title = p.task().title().to_string(); - let required = (title.len() + 1) as c_int; - if !buf.is_null() && buf_size >= required { - // SAFETY: the caller guarantees `buf` holds `buf_size` bytes. - unsafe { - std::ptr::copy_nonoverlapping(title.as_ptr() as *const c_char, buf, title.len()); - *buf.add(title.len()) = 0; - } - } - required - } - - /// `oaktask_task_error` (two-stage). - pub fn oaktask_task_error(t: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - let Some(p) = task_payload(t) else { - return oaktask::error::OAKTASK_E_INVALID; - }; - let error = p.task().error().unwrap_or("Unknown error").to_string(); - let required = (error.len() + 1) as c_int; - if !buf.is_null() && buf_size >= required { - // SAFETY: the caller guarantees `buf` holds `buf_size` bytes. - unsafe { - std::ptr::copy_nonoverlapping(error.as_ptr() as *const c_char, buf, error.len()); - *buf.add(error.len()) = 0; - } - } - required - } - - /// `oaktask_task_subscribe` — register the legacy `(event_id, value, - /// userdata)` listener (0 on success). - pub fn oaktask_task_subscribe( - t: CHandle, - cb: Option, - userdata: *mut c_void, - ) -> i64 { - let Some(p) = task_payload_mut(t) else { - return -1; - }; - let Some(cb) = cb else { - return -1; - }; - let state = Arc::new(oaktask::task::SubscriberState::default()); - let userdata_ptr = userdata as usize; - p.task_mut().set_subscriber(state.clone()); - p.task_mut().set_event_listener(Box::new(move |ev| { - // SAFETY: the callback + userdata follow the caller's - // contract; the task emits on its own thread. - unsafe { - match ev { - oaktask::task::TaskEvent::Started => cb( - 0, - state - .start_ms - .load(std::sync::atomic::Ordering::SeqCst) as f64, - userdata_ptr as *mut c_void, - ), - oaktask::task::TaskEvent::Progress(v) => cb(1, v, userdata_ptr as *mut c_void), - oaktask::task::TaskEvent::Finished => cb( - 2, - state - .finished_value - .load(std::sync::atomic::Ordering::SeqCst) as f64, - userdata_ptr as *mut c_void, - ), - } - } - })); - 0 - } - - /// `oaktask_debug_alive_count` — live facade task payloads. - pub fn oaktask_debug_alive_count() -> c_int { - TASK_ALIVE.load(std::sync::atomic::Ordering::SeqCst) - } - - // ------------------------------------------------------------------- - // Task creators - // ------------------------------------------------------------------- - - /// Box a project payload for the result paths (reuses the node - /// module's project box). - fn box_project_result(project: Arc>) -> CHandle { - crate::stubs::node::box_project_handle(project) - } - - /// The engine-side wrapper behavior for interchange loads: delegates - /// to the real `ProjectLoadTask` and copies the loaded project into - /// the shared result slot. - struct LoadTaskBehavior { - inner: oaktask::project::load::ProjectLoadTask, - result: Arc>>>>, - } - - impl oaktask::task::TaskBehavior for LoadTaskBehavior { - fn run(&mut self, task: &mut oaktask::task::Task) -> oaktask::error::Result<()> { - self.inner.run(task)?; - if let Ok(project) = self.inner.base.take_project() { - *self.result.lock().unwrap_or_else(|e| e.into_inner()) = Some(project); - } - Ok(()) - } - } - - /// `oaktask_create_project_load` — a task that loads an OVE project. - pub fn oaktask_create_project_load(filename: *const c_char) -> CHandle { - if filename.is_null() { - return CHandle::null(); - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let filename = unsafe { crate::handle::read_cstr(filename) }; - let title = format!("Loading '{filename}'"); - let result = Arc::new(Mutex::new(None)); - let mut driver = base_task(&title); - let inner_base = base_task(&title); - let inner = oaktask::project::load::ProjectLoadTask { - base: oaktask::project::load::ProjectLoadBaseTask::new(inner_base, filename), - }; - driver.set_behavior(Box::new(LoadTaskBehavior { - inner, - result: result.clone(), - })); - box_task(driver, TaskKind::Load { result }) - } - - /// `oaktask_load_take_project` — take the project a successful load - /// produced (empty handle before the run). - pub fn oaktask_load_take_project(t: CHandle) -> CHandle { - let Some(p) = task_payload(t) else { - return CHandle::null(); - }; - match &p.kind { - TaskKind::Load { result } => { - let project = result - .lock() - .unwrap_or_else(|e| e.into_inner()) - .take(); - match project { - Some(project) => box_project_result(project), - None => CHandle::null(), - } - } - _ => CHandle::null(), - } - } - - /// `oaktask_create_project_save` — a task that saves a project. - pub fn oaktask_create_project_save( - project: CHandle, - filename_or_null: *const c_char, - use_compression: c_int, - ) -> CHandle { - // SAFETY: project handles box ProjectArc payloads. - let project_arc = match unsafe { project_of(&project) }.cloned() { - Some(p) => p, - None => return CHandle::null(), - }; - let override_filename = if filename_or_null.is_null() { - None - } else { - // SAFETY: the caller guarantees a valid NUL-terminated string. - Some(unsafe { crate::handle::read_cstr(filename_or_null) }) - }; - let mut driver = base_task("Saving project..."); - let inner_base = base_task("Saving project..."); - let inner = oaktask::project::save::ProjectSaveTask { - base: inner_base, - project: project_arc, - override_filename, - use_compression: use_compression != 0, - }; - driver.set_behavior(Box::new(inner)); - box_task(driver, TaskKind::Plain) - } - - /// The engine-side wrapper behavior for imports: delegates to the real - /// `ProjectImportTask` and copies the produced command/footage/invalid - /// count into the shared slots. - struct ImportTaskBehavior { - inner: oaktask::project::import::ProjectImportTask, - command: Arc>>, - footage: Arc>>, - invalid_count: Arc>, - } - - impl oaktask::task::TaskBehavior for ImportTaskBehavior { - fn run(&mut self, task: &mut oaktask::task::Task) -> oaktask::error::Result<()> { - let result = self.inner.run(task); - if let Ok(cmd) = self.inner.take_command() { - *self.command.lock().unwrap_or_else(|e| e.into_inner()) = Some(cmd); - } - { - let mut footage = self.footage.lock().unwrap_or_else(|e| e.into_inner()); - let n = self.inner.get_file_count(); - for i in 0..n { - if let Ok(f) = self.inner.get_imported_footage(i) { - footage.push(crate::stubs::node::box_node_handle(f.0, f.1, false)); - } - } - } - *self.invalid_count.lock().unwrap_or_else(|e| e.into_inner()) = - self.inner.get_invalid_file_count(); - result - } - } - - /// Process-wide image-sequence confirmation callback (the deleted C - /// ABI kept it as a global; the real import task takes it at - /// creation). - static IMAGE_SEQ_CONFIRM: OnceLock>> = - OnceLock::new(); - - fn image_seq_confirm() -> Option { - IMAGE_SEQ_CONFIRM - .get_or_init(|| Mutex::new(None)) - .lock() - .unwrap_or_else(|e| e.into_inner()) - .map(|(cb, _)| cb) - } - - /// `oaktask_create_project_import` — a task that imports media files - /// into a folder. - pub fn oaktask_create_project_import( - folder: CHandle, - project: CHandle, - urls: *const *const c_char, - url_count: c_int, - ) -> CHandle { - if url_count < 0 || (urls.is_null() && url_count > 0) { - return CHandle::null(); - } - // SAFETY: node handles box oaknode NodeRef payloads. - let folder_ref = match unsafe { crate::handle::domain::node_ref_of(&folder) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return CHandle::null(), - }; - // SAFETY: project handles box ProjectArc payloads. - let project_ref = match unsafe { crate::handle::domain::project_of(&project) }.cloned() { - Some(p) => p, - None => return CHandle::null(), - }; - let mut filenames = Vec::new(); - for i in 0..url_count as usize { - // SAFETY: the caller guarantees `url_count` valid pointers. - let url = unsafe { *urls.add(i) }; - if url.is_null() { - return CHandle::null(); - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - filenames.push(unsafe { crate::handle::read_cstr(url) }); - } - let file_count = filenames.len(); - let confirm = image_seq_confirm().map(|cb| { - Box::new(move |filename: &str, _pattern: &str| -> bool { - // SAFETY: the callback + userdata follow the caller's - // contract. - let cname = std::ffi::CString::new(filename).unwrap_or_default(); - unsafe { cb(cname.as_ptr(), std::ptr::null_mut()) != 0 } - }) as oaktask::project::import::ImageSequenceConfirmFn - }); - let command = Arc::new(Mutex::new(None)); - let footage = Arc::new(Mutex::new(Vec::new())); - let invalid_count = Arc::new(Mutex::new(0)); - let title = format!("Importing {} file(s)", file_count); - let mut driver = base_task(&title); - let inner_base = base_task(&title); - let inner = oaktask::project::import::ProjectImportTask::new( - inner_base, - folder_ref, - project_ref, - filenames, - confirm, - file_count, - ); - driver.set_behavior(Box::new(ImportTaskBehavior { - inner, - command: command.clone(), - footage: footage.clone(), - invalid_count: invalid_count.clone(), - })); - box_task( - driver, - TaskKind::Import { - command, - footage, - invalid_count, - }, - ) - } - - /// `oaktask_import_take_command` — the undo command a successful - /// import produced (detached from the task). - pub fn oaktask_import_take_command(t: CHandle) -> CHandle { - let Some(p) = task_payload(t) else { - return CHandle::null(); - }; - match &p.kind { - TaskKind::Import { command, .. } => { - let cmd = command - .lock() - .unwrap_or_else(|e| e.into_inner()) - .take(); - match cmd { - Some(cmd) => { - // SAFETY: `command_from_owned` owns the command - // value. - unsafe { command_from_owned(cmd) } - } - None => CHandle::null(), - } - } - _ => CHandle::null(), - } - } - - /// `oaktask_import_footage_count` — imported footage entries. - pub fn oaktask_import_footage_count(t: CHandle) -> c_int { - let Some(p) = task_payload(t) else { - return oaktask::error::OAKTASK_E_INVALID; - }; - match &p.kind { - TaskKind::Import { footage, .. } => { - footage.lock().unwrap_or_else(|e| e.into_inner()).len() as c_int - } - _ => oaktask::error::OAKTASK_E_INVALID, - } - } - - /// `oaktask_import_footage_at` — addref'd imported footage handle. - pub fn oaktask_import_footage_at(t: CHandle, index: c_int) -> CHandle { - if index < 0 { - return CHandle::null(); - } - let Some(p) = task_payload(t) else { - return CHandle::null(); - }; - match &p.kind { - TaskKind::Import { footage, .. } => { - let h = footage - .lock() - .unwrap_or_else(|e| e.into_inner()) - .get(index as usize) - .copied(); - match h { - Some(h) => { - if let Some(addref) = h.addref { - // SAFETY: the handle is live. - unsafe { addref(h.ctx) }; - } - h - } - None => CHandle::null(), - } - } - _ => CHandle::null(), - } - } - - /// `oaktask_import_invalid_count` — the count the real import task - /// reported. - pub fn oaktask_import_invalid_count(t: CHandle) -> c_int { - let Some(p) = task_payload(t) else { - return oaktask::error::OAKTASK_E_INVALID; - }; - match &p.kind { - TaskKind::Import { invalid_count, .. } => { - *invalid_count.lock().unwrap_or_else(|e| e.into_inner()) as c_int - } - _ => oaktask::error::OAKTASK_E_INVALID, - } - } - - /// `oaktask_import_invalid_at` — STUB: the invalid-file list is - /// private inside `oaktask::project::import::ProjectImportTask` (only - /// its count is exposed), so the per-file names are unwireable - /// (documented; returns NOT_FOUND). - pub fn oaktask_import_invalid_at( - t: CHandle, - _index: c_int, - _buf: *mut c_char, - _buf_size: c_int, - ) -> c_int { - let Some(p) = task_payload(t) else { - return oaktask::error::OAKTASK_E_INVALID; - }; - match &p.kind { - TaskKind::Import { .. } => oaktask::error::OAKTASK_E_NOT_FOUND, - _ => oaktask::error::OAKTASK_E_INVALID, - } - } - - /// `oaktask_create_project_load_otio` — an OTIO interchange load. - pub fn oaktask_create_project_load_otio(filename: *const c_char) -> CHandle { - if filename.is_null() { - return CHandle::null(); - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let filename = unsafe { crate::handle::read_cstr(filename) }; - let title = format!("Loading '{filename}'"); - let result = Arc::new(Mutex::new(None)); - let mut driver = base_task(&title); - let inner_base = base_task(&title); - let inner = oaktask::project::loadotio::LoadOTIOTask { - base: oaktask::project::load::ProjectLoadBaseTask::new(inner_base, filename), - }; - driver.set_behavior(Box::new(OtioLoadTaskBehavior { - inner, - result: result.clone(), - })); - box_task(driver, TaskKind::Load { result }) - } - - /// The engine-side wrapper behavior for OTIO loads. - struct OtioLoadTaskBehavior { - inner: oaktask::project::loadotio::LoadOTIOTask, - result: Arc>>>>, - } - - impl oaktask::task::TaskBehavior for OtioLoadTaskBehavior { - fn run(&mut self, task: &mut oaktask::task::Task) -> oaktask::error::Result<()> { - self.inner.run(task)?; - if let Ok(project) = self.inner.base.take_project() { - *self.result.lock().unwrap_or_else(|e| e.into_inner()) = Some(project); - } - Ok(()) - } - } - - /// `oaktask_load_otio_take_project` — take the project a successful - /// OTIO load produced. - pub fn oaktask_load_otio_take_project(t: CHandle) -> CHandle { - oaktask_load_take_project(t) - } - - /// `oaktask_create_project_save_otio` — an OTIO interchange save. - pub fn oaktask_create_project_save_otio(project: CHandle, filename: *const c_char) -> CHandle { - if filename.is_null() { - return CHandle::null(); - } - // SAFETY: the caller guarantees a valid NUL-terminated string. - let filename = unsafe { crate::handle::read_cstr(filename) }; - // SAFETY: project handles box ProjectArc payloads. - let project_ref = match unsafe { crate::handle::domain::project_of(&project) }.cloned() { - Some(p) => p, - None => return CHandle::null(), - }; - let mut driver = base_task("Saving project..."); - let inner_base = base_task("Saving project..."); - let inner = oaktask::project::saveotio::SaveOTIOTask { - base: inner_base, - project: project_ref, - filename, - }; - driver.set_behavior(Box::new(inner)); - box_task(driver, TaskKind::Plain) - } - - /// `oaktask_load_otio_set_confirm_cb` — register the global OTIO - /// import-confirmation callback (real oaktask API). - pub fn oaktask_load_otio_set_confirm_cb( - cb: Option, - userdata: *mut c_void, - ) { - let userdata_ptr = userdata as usize; - let mapped = cb.map(|cb| { - Box::new(move |names: &[String]| -> bool { - let cnames: Vec = names - .iter() - .map(|n| std::ffi::CString::new(n.as_str()).unwrap_or_default()) - .collect(); - let ptrs: Vec<*const c_char> = - cnames.iter().map(|c| c.as_ptr() as *const c_char).collect(); - // SAFETY: the callback + userdata follow the caller's - // contract. - unsafe { cb(ptrs.as_ptr(), cnames.len() as c_int, userdata_ptr as *mut c_void) != 0 } - }) as oaktask::project::loadotio::ImportConfirmFn - }); - oaktask::project::loadotio::set_import_confirm_callback(mapped); - } - - /// `oaktask_create_precache` — a footage precache task. - pub fn oaktask_create_precache(footage: CHandle, index: c_int, sequence: CHandle) -> CHandle { - // SAFETY: node handles box oaknode NodeRef payloads. - let footage_ref = match unsafe { crate::handle::domain::node_ref_of(&footage) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return CHandle::null(), - }; - // SAFETY: node handles box oaknode NodeRef payloads. - let sequence_ref = match unsafe { crate::handle::domain::node_ref_of(&sequence) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return CHandle::null(), - }; - let inner = oaktask::precache::PreCacheTask::new(footage_ref, index, sequence_ref); - let mut driver = base_task("Precaching footage..."); - driver.set_behavior(Box::new(inner)); - box_task(driver, TaskKind::Plain) - } - - /// `oaktask_create_export` — an export task over the real - /// `ExportTask`. - pub fn oaktask_create_export( - viewer: CHandle, - color_manager: CHandle, - params: *const EncodingParamsPOD, - ) -> CHandle { - if params.is_null() { - return CHandle::null(); - } - // SAFETY: node handles box oaknode NodeRef payloads. - let viewer_ref = match unsafe { crate::handle::domain::node_ref_of(&viewer) } { - Some(nr) => (nr.project.clone(), nr.id), - None => return CHandle::null(), - }; - // SAFETY: the caller passes a live POD for the call duration. - let pod = unsafe { &*params }; - let filename = pod - .filename - .iter() - .take_while(|&&b| b != 0) - .map(|&b| b as char) - .collect::(); - let encoding = oaktask::export::EncodingParams { - filename, - format: pod.format, - video_enabled: pod.video_enabled != 0, - video_codec: pod.video_codec, - video_width: pod.video_width, - video_height: pod.video_height, - video_time_base_num: pod.video_time_base_num, - video_time_base_den: pod.video_time_base_den, - video_pixel_format: pod.video_pixel_format as i32, - audio_enabled: pod.audio_enabled != 0, - audio_codec: pod.audio_codec, - audio_sample_rate: pod.audio_sample_rate, - audio_channel_layout: pod.audio_channel_layout, - subtitles_enabled: pod.subtitles_enabled != 0, - export_length_num: pod.export_length_num, - export_length_den: pod.export_length_den, - has_custom_range: pod.has_custom_range != 0, - custom_range_in_num: pod.custom_range_in_num as i32, - custom_range_in_den: pod.custom_range_in_den as i32, - custom_range_out_num: pod.custom_range_out_num as i32, - custom_range_out_den: pod.custom_range_out_den as i32, - }; - let _ = color_manager; // the domain ExportTask dropped the manager slot - let inner = oaktask::export::ExportTask::new(viewer_ref, encoding); - let mut driver = base_task("Exporting..."); - driver.set_behavior(Box::new(inner)); - box_task(driver, TaskKind::Plain) - } - - /// `oaktask_import_set_image_sequence_confirm_cb` — register the - /// process-wide image-sequence confirmation callback. - pub fn oaktask_import_set_image_sequence_confirm_cb( - cb: Option, - userdata: *mut c_void, - ) { - *IMAGE_SEQ_CONFIRM - .get_or_init(|| Mutex::new(None)) - .lock() - .unwrap_or_else(|e| e.into_inner()) = cb.map(|cb| (cb, userdata as usize)); - } -} diff --git a/crates/oakengine.bk/src/task.rs b/crates/oakengine.bk/src/task.rs deleted file mode 100644 index c572e6575..000000000 --- a/crates/oakengine.bk/src/task.rs +++ /dev/null @@ -1,962 +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 . - -//! `engine/include/oakengine/task.h` — the engine background-task system -//! (the C++ `olive::Task` / `olive::TaskManager`) over the oaktask module. -//! -//! Task ownership follows the header: `oakengine_task_create_*` returns an -//! OWNED task; `oakengine_task_manager_add` hands it to the manager (which -//! deletes it when done, so the handle becomes borrowed); -//! `oakengine_task_free` deletes a task that never reached the manager. -//! A task run with `oakengine_task_start_sync` stays owned by the caller. -//! -//! The facade owns the global task manager (module-00 analogue of the C++ -//! app-startup `TaskManager`): it is initialized lazily on the first -//! manager-family call, mirroring the undo family's process-wide stack. -//! -//! The oaktask module exposes no getters for the C++ `Task::get_start_time` -//! / `Task::is_cancelled` / `ProjectSaveTask::get_project`; those three are -//! answered from facade-side state recorded at creation/cancel -//! ([`TaskMeta`], see the per-export notes). -//! -//! String output follows the engine buf/size convention: the return value -//! is the would-be length **excluding** the NUL. The module reports the -//! size **including** the NUL, converted with -//! [`crate::handle::string_result`]; module error codes (-80001..) pass -//! through untranslated. - -use std::collections::HashMap; -use std::ffi::{c_char, c_int, c_void}; -use std::sync::{Mutex, OnceLock}; -use std::time::{SystemTime, UNIX_EPOCH}; - -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}; -use crate::handle::{ - box_handle, free_box, guard, guard_i64, guard_int, guard_ptr, string_result, unbox, CHandle, - OakEngineClipboard, OakEngineNode, OakEngineProject, OakEngineSequence, OakEngineTask, -}; - -// --------------------------------------------------------------------------- -// Facade-side task state -// --------------------------------------------------------------------------- - -/// Facade-side sidecars for tasks created through this module, keyed by the -/// module task handle's `ctx` (the stable identity of the underlying task; -/// see [`crate::handle::CHandle`]). Entries are dropped by -/// [`oakengine_task_free`]; a task handed to the manager keeps its entry -/// until free — the header forbids touching a borrowed handle after the -/// task is removed, so an entry left behind by a manager-run task is an -/// intentional, documented process-lifetime leak. -#[derive(Clone)] -struct TaskMeta { - /// Epoch-millisecond creation stamp, returned by - /// [`oakengine_task_start_time`] once the task has been started through - /// the facade (the module has no start-time getter; the C++ reports the - /// real `Task::get_start_time`). - created_at_ms: u64, - /// Whether the task was started through the facade - /// (`oakengine_task_start_sync` / `oakengine_task_manager_add` / - /// `oakengine_cli_task_dialog_run`). - started: bool, - /// Facade-initiated cancel flag (the module has no `is_cancelled` - /// getter; only cancels made through this facade are visible). - cancelled: bool, - /// The project a save task writes (addref'd at creation, released at - /// free) — the module has no save-project getter. - save_project: Option, - /// The project an import task borrows (addref'd at creation, released - /// at free): the module's import task stores its project handle WITHOUT - /// addref, so this facade-side ref keeps the shared box alive while the - /// task runs (see `oakengine_task_create_project_import`). - import_project: Option, - /// The encoding-params box an export task owns, dropped at free - /// (mirrors the C++ `FacadeExportTask` destructor; stored as `usize` so - /// the map stays `Send`). - export_params: Option, - /// The color manager an export task owns, released at free. - export_color_manager: Option, -} - -impl TaskMeta { - fn new() -> Self { - TaskMeta { - created_at_ms: now_millis(), - started: false, - cancelled: false, - save_project: None, - import_project: None, - export_params: None, - export_color_manager: None, - } - } -} - -/// Epoch milliseconds (0 when the clock is before the epoch; never in -/// practice). -fn now_millis() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_millis() as u64) - .unwrap_or(0) -} - -static META: OnceLock>> = OnceLock::new(); - -fn meta_lock() -> std::sync::MutexGuard<'static, HashMap> { - META.get_or_init(|| Mutex::new(HashMap::new())) - .lock() - .unwrap_or_else(|e| e.into_inner()) -} - -fn meta_insert(key: usize, meta: TaskMeta) { - meta_lock().insert(key, meta); -} - -fn meta_get(key: usize) -> Option { - meta_lock().get(&key).cloned() -} - -fn meta_set_started(key: usize) { - if let Some(m) = meta_lock().get_mut(&key) { - m.started = true; - } -} - -fn meta_set_cancelled(key: usize) { - if let Some(m) = meta_lock().get_mut(&key) { - m.cancelled = true; - } -} - -/// Release every facade-side sidecar of a task (called by -/// [`oakengine_task_free`]): the addref'd save/import projects, the owned -/// encoding-params box and the derived color manager of an export task. -fn drop_task_meta(key: usize) { - if let Some(meta) = meta_lock().remove(&key) { - if let Some(mut project) = meta.save_project { - unsafe { n::oaknode_project_free(&mut project) }; - } - if let Some(mut project) = meta.import_project { - unsafe { n::oaknode_project_free(&mut project) }; - } - if let Some(ptr) = meta.export_params { - unsafe { - crate::codec::oakengine_encoding_params_destroy(ptr as *mut OakEngineEncodingParams) - }; - } - if let Some(mut manager) = meta.export_color_manager { - unsafe { n::oaknode_colormanager_free(&mut manager) }; - } - } -} - -/// Box an owned module task handle as an engine handle, registering its -/// facade sidecars. NULL/empty handles stay NULL. -fn box_task(h: CHandle) -> *mut OakEngineTask { - if h.is_null() { - return std::ptr::null_mut(); - } - meta_insert(h.ctx as usize, TaskMeta::new()); - box_handle::(h) -} - -// --------------------------------------------------------------------------- -// Global task manager -// --------------------------------------------------------------------------- - -/// Lazily initialize the global task manager on first facade use -/// (module-00 analogue of the C++ app-startup `TaskManager` creation; the -/// same pattern as the undo family's `global_stack`). The manager lives for -/// the process. `oaktask_manager_init` only fails when already initialized, -/// which the `OnceLock` prevents, so this always succeeds. -fn manager_ensure() -> Result<()> { - static INIT: OnceLock<()> = OnceLock::new(); - let _ = INIT.get_or_init(|| unsafe { - t::oaktask_manager_init(); - }); - Ok(()) -} - -/// Stable opaque token for `oakengine_task_manager_handle`: a boxed -/// [`CHandle`] whose `ctx` is the address of a facade static (never -/// dereferenced). The oaktask module exposes no manager handle, so the -/// token exists purely to give the (out-of-scope, per README) -/// `OAKENGINE_EVENT_TASK_MANAGER_*` subscription an ABI-ready handle. The -/// box is leaked for the process, like the C++ `TaskManager::instance()`. -fn manager_token() -> *mut c_void { - static TOKEN: OnceLock = OnceLock::new(); - // Stored as `usize` so the `OnceLock` stays `Sync`. - let boxed = TOKEN.get_or_init(|| { - box_handle::(CHandle { - ctx: &MANAGER_TOKEN as *const u8 as *mut c_void, - addref: None, - release: None, - abi_version: 0, - }) as usize - }); - *boxed as *mut OakEngineTask as *mut c_void -} - -static MANAGER_TOKEN: u8 = 0; - -/// `oakengine_task_manager_handle` — borrowed token of the global task -/// manager (NULL never: the facade initializes the manager lazily on first -/// use, see [`manager_ensure`]; the C++ engine creates it at app startup). -#[no_mangle] -pub extern "C" fn oakengine_task_manager_handle() -> *mut c_void { - guard_ptr(|| { - manager_ensure()?; - Ok(manager_token()) - }) -} - -/// `oakengine_task_manager_count` — number of tasks known to the manager -/// (running plus failed-but-kept). The manager is created on first use, so -/// the header's "no manager exists" state is unreachable (0 when empty). -#[no_mangle] -pub extern "C" fn oakengine_task_manager_count() -> c_int { - guard_int(|| { - manager_ensure()?; - Ok(unsafe { t::oaktask_manager_count() }) - }) -} - -/// `oakengine_task_manager_first` — borrowed handle of the manager's first -/// task (NULL when the queue is empty). -#[no_mangle] -pub extern "C" fn oakengine_task_manager_first() -> *mut OakEngineTask { - guard_ptr(|| { - manager_ensure()?; - let h = unsafe { t::oaktask_manager_at(0) }; - if h.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(h)) - }) -} - -/// `oakengine_task_manager_add` — hand `task` to the manager queue -/// (transfers ownership; the manager deletes the task when done). The -/// module's `oaktask_task_start` performs the transfer; a task already -/// running on the manager reports the module's `OAKTASK_E_STATE`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_manager_add(task: *mut OakEngineTask) -> c_int { - guard(|| unsafe { - let h = unbox(task)?; - manager_ensure()?; - Error::from_module(t::oaktask_task_start(h))?; - meta_set_started(h.ctx as usize); - Ok(()) - }) -} - -/// `oakengine_task_manager_cancel` — ask the manager to cancel `task`. The -/// module's `oaktask_task_cancel` signals the running task's cancellation -/// atom; the "failed-but-kept task is removed and deleted" half is managed -/// by the module's own bookkeeping (`oaktask_manager_delete_finished`) and -/// has no engine export, so it is not mirrored here. -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_manager_cancel(task: *mut OakEngineTask) -> c_int { - guard(|| unsafe { - let h = unbox(task)?; - manager_ensure()?; - Error::from_module(t::oaktask_task_cancel(h))?; - meta_set_cancelled(h.ctx as usize); - Ok(()) - }) -} - -// --------------------------------------------------------------------------- -// Task accessors -// --------------------------------------------------------------------------- - -/// `oakengine_task_title` (buf/size; E_INVALID for NULL). -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_title( - task: *mut OakEngineTask, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let h = unbox(task)?; - let rc = t::oaktask_task_title(h, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_task_error` (buf/size; E_INVALID for NULL). Meaningful after -/// a failed run. -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_error( - task: *mut OakEngineTask, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let h = unbox(task)?; - let rc = t::oaktask_task_error(h, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_task_start_time` — start timestamp in epoch milliseconds. -/// -/// The module has no start-time getter, so the facade reports the -/// **creation** stamp (see [`TaskMeta`]) once the task has been started -/// through the facade; 0 before then (matching "0 when the task never -/// started"). Deviation from the C++ `Task::get_start_time`, which records -/// the actual start instant. -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_start_time(task: *mut OakEngineTask) -> i64 { - guard_i64(|| unsafe { - let h = unbox(task)?; - Ok(match meta_get(h.ctx as usize) { - Some(m) if m.started => m.created_at_ms as i64, - _ => 0, - }) - }) -} - -/// `oakengine_task_is_cancelled` — 1 when the task was asked to cancel. -/// -/// The module has no `is_cancelled` getter, so only cancels issued through -/// [`oakengine_task_cancel`] / [`oakengine_task_manager_cancel`] on this -/// facade are visible (0 otherwise). Deviation from the C++ -/// `Task::is_cancelled`, which reflects the task's own cancellation atom. -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_is_cancelled(task: *mut OakEngineTask) -> c_int { - guard_int(|| unsafe { - let h = unbox(task)?; - Ok( - if meta_get(h.ctx as usize) - .map(|m| m.cancelled) - .unwrap_or(false) - { - 1 - } else { - 0 - }, - ) - }) -} - -/// `oakengine_task_cancel` — signal the task to cancel as soon as possible -/// (module `Task::cancel`, the `Task::Cancel` analogue). -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_cancel(task: *mut OakEngineTask) -> c_int { - guard(|| unsafe { - let h = unbox(task)?; - Error::from_module(t::oaktask_task_cancel(h))?; - meta_set_cancelled(h.ctx as usize); - Ok(()) - }) -} - -/// `oakengine_task_start_sync` — run on the calling thread; 1 = succeeded, -/// 0 = failed or cancelled, E_INVALID for NULL. Ownership stays with the -/// caller. -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_start_sync(task: *mut OakEngineTask) -> c_int { - guard_int(|| unsafe { - let h = unbox(task)?; - let rc = t::oaktask_task_start_sync(h); - meta_set_started(h.ctx as usize); - Ok(rc) - }) -} - -/// `oakengine_task_free` — delete a task that was never added to the -/// manager (releases the module task handle, which drops an owned task). -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_free(task: *mut OakEngineTask) -> c_int { - guard(|| unsafe { - if task.is_null() { - return Err(Error::Invalid); - } - let h = (*task).handle; - if h.is_null() { - return Err(Error::Invalid); - } - drop_task_meta(h.ctx as usize); - free_box::(task); - Ok(()) - }) -} - -/// `oakengine_cli_task_dialog_run` — run `task` through the engine's CLI -/// modal progress dialog; 1 on success, 0 on failure/cancellation. -/// -/// The C++ `CLITaskDialog` renders a terminal progress dialog around a -/// synchronous run; the facade ports the observable behavior (sync run, -/// 1/0 result) with the dialog chrome itself stubbed. `parent` is unused. -/// The capi returns 0 (not E_INVALID) for a NULL task, so this mirrors it. -#[no_mangle] -pub unsafe extern "C" fn oakengine_cli_task_dialog_run( - task: *mut OakEngineTask, - _parent_or_null: *mut c_void, -) -> c_int { - guard_int(|| unsafe { - if task.is_null() { - return Ok(0); - } - let h = unbox(task)?; - let rc = t::oaktask_task_start_sync(h); - meta_set_started(h.ctx as usize); - Ok(rc) - }) -} - -// --------------------------------------------------------------------------- -// Task creators (all return OWNED tasks, NULL on invalid input) -// --------------------------------------------------------------------------- - -/// `oakengine_task_create_project_load` — task that loads an OVE project -/// from `filename`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_create_project_load( - filename: *const c_char, -) -> *mut OakEngineTask { - guard_ptr(|| unsafe { - if filename.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_task(t::oaktask_create_project_load(filename))) - }) -} - -/// `oakengine_task_create_project_load_otio` — task that loads an -/// OpenTimelineIO project. The module always supports OTIO (the interchange -/// format is inferred from the filename extension), so valid input never -/// yields the header's "built without OTIO support" NULL. -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_create_project_load_otio( - filename: *const c_char, -) -> *mut OakEngineTask { - guard_ptr(|| unsafe { - if filename.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_task(t::oaktask_create_project_load_otio(filename))) - }) -} - -/// `oakengine_task_create_project_save` — task that saves `project`. -/// -/// `use_compression` selects the compressed `.ove` writer; `override_filename` -/// may be NULL to save to the project's own filename. `layout` (an opaque -/// `SerializedLayoutInfo *` in the engine) is **ignored**: the module's -/// `ProjectSaveTask` has no layout slot, so a non-NULL layout is accepted -/// but not copied into the file. -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_create_project_save( - project: *mut OakEngineProject, - use_compression: c_int, - override_filename: *const c_char, - _layout: *const c_void, -) -> *mut OakEngineTask { - guard_ptr(|| unsafe { - let ph = unbox(project)?; - let h = t::oaktask_create_project_save(ph, override_filename, use_compression); - if h.is_null() { - return Ok(std::ptr::null_mut()); - } - // Keep the project borrowed for the task's lifetime so - // `oakengine_task_save_get_project` can answer from facade state - // (the module has no save-project getter). - let mut meta = TaskMeta::new(); - meta.save_project = Some(ph.addref()); - meta_insert(h.ctx as usize, meta); - Ok(box_handle::(h)) - }) -} - -/// `oakengine_task_create_project_save_otio` — task that saves `project` in -/// OpenTimelineIO format. -/// -/// The engine header passes only the project, but the module's creator -/// requires the output filename; the facade derives it from the project's -/// own filename (the OTIO save of the current project file) and returns -/// NULL when the project has no filename. The module always supports OTIO, -/// so a valid input never yields the "built without OTIO support" NULL. -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_create_project_save_otio( - project: *mut OakEngineProject, -) -> *mut OakEngineTask { - guard_ptr(|| unsafe { - let ph = unbox(project)?; - let filename = project_filename_of(ph)?; - if filename.is_empty() { - return Ok(std::ptr::null_mut()); - } - let c_filename = std::ffi::CString::new(filename) - .map_err(|_| Error::Failed("invalid filename".into()))?; - Ok(box_task(t::oaktask_create_project_save_otio( - ph, - c_filename.as_ptr(), - ))) - }) -} - -/// Two-stage read of the project's filename (empty when unset). -fn project_filename_of(project: CHandle) -> Result { - let needed = unsafe { n::oaknode_project_filename(project, std::ptr::null_mut(), 0) }; - if needed <= 0 { - return Ok(String::new()); - } - let mut buf = vec![0 as c_char; needed as usize]; - let rc = unsafe { n::oaknode_project_filename(project, buf.as_mut_ptr(), needed) }; - if rc < 0 { - return Err(Error::Module(rc)); - } - let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len()); - Ok(String::from_utf8_lossy(unsafe { - std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) - }) - .into_owned()) -} - -/// `oakengine_task_create_project_import` — task that imports `url_count` -/// media files into `folder` (a folder node of the target project). -/// -/// The URL array is copied by the module during the call. The engine header -/// passes only the folder; the module creator needs the owning project, -/// derived here via `oaknode_node_get_project`. Unlike the capi (which -/// rejects `url_count <= 0`), a zero-count task IS created — the header's -/// `oakengine_task_import_file_count` documents 0 as "nothing to import, -/// free instead of run". `url_count < 0`, a NULL URL inside the array, or a -/// folder with no project yield NULL. -/// -/// The module's import task stores the project handle WITHOUT addref, so -/// the facade keeps an addref'd copy in [`TaskMeta::import_project`] -/// (released at free) — without it, releasing the transient borrowed -/// handle here would drop the shared box while the task still references -/// it, and the run would read freed memory (the former SIGSEGV reproduced -/// by `it_task::import_run_single_file`). -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_create_project_import( - folder: *mut OakEngineNode, - urls: *const *const c_char, - url_count: c_int, -) -> *mut OakEngineTask { - guard_ptr(|| unsafe { - let fh = unbox(folder)?; - if url_count < 0 || (urls.is_null() && url_count > 0) { - return Ok(std::ptr::null_mut()); - } - let mut project = CHandle::null(); - Error::from_module(n::oaknode_node_get_project(fh, &mut project))?; - if project.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = t::oaktask_create_project_import(fh, project, urls, url_count); - if h.is_null() { - // Creation failed: release the transient borrowed handle. - n::oaknode_project_free(&mut project); - return Ok(std::ptr::null_mut()); - } - // Keep the project borrowed for the task's lifetime (the module's - // import task stores the handle without addref): addref before the - // transient handle below is released, so the shared box stays alive - // until `oakengine_task_free` drops the meta-side copy. - let mut meta = TaskMeta::new(); - meta.import_project = Some(project.addref()); - // Release the transient borrowed project handle (the task's copy and - // the facade-side addref above keep the box alive). - n::oaknode_project_free(&mut project); - meta_insert(h.ctx as usize, meta); - Ok(box_handle::(h)) - }) -} - -/// `oakengine_task_create_proxy` — **not backed** (stub, always NULL). -/// -/// The oaktask crate's `ProxyTask` is driven by a codec task request and no -/// proxy-task creator exists on the module C ABI (`oaktask_create_precache` -/// is a different task). The engine's `FacadeProxyTask` would need -/// `oakengine_footage_proxy_generate`, which lives in the deferred exporter -/// family (see `deferred.rs`). Returns NULL per the creators' "NULL on -/// invalid input" contract. -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_create_proxy( - _footage: *mut OakEngineNode, -) -> *mut OakEngineTask { - std::ptr::null_mut() -} - -/// `oakengine_task_create_export` — task that renders an export of -/// `sequence` with `params`. -/// -/// Takes ownership of `params` (destroyed with the task, mirroring the C++ -/// `FacadeExportTask` destructor; the module copies the POD it needs at -/// creation, so the retained box is a lifetime guarantee for C callers). -/// The color manager is derived from the sequence's owning project -/// (`oaknode_colormanager_init`), mirroring how the C++ exporter obtains -/// its manager; a sequence without a project exports with an empty manager. -/// The module creator requires a POD pointer, so the facade's opaque params -/// handle is copied out through the public `oakengine_encoding_params_*` -/// getters ([`export_params_pod`]) — its backing `ParamsBox` (POD + option -/// map) is private to `codec.rs` and cannot be read here. -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_create_export( - sequence: *mut OakEngineSequence, - params: *mut OakEngineEncodingParams, -) -> *mut OakEngineTask { - guard_ptr(|| unsafe { - let vh = unbox(sequence)?; - if params.is_null() { - return Ok(std::ptr::null_mut()); - } - let pod = export_params_pod(params)?; - let color_manager = export_color_manager(vh)?; - let h = t::oaktask_create_export(vh, color_manager, &pod); - if h.is_null() { - // Creation failed: release the color manager we derived. - let mut manager = color_manager; - n::oaknode_colormanager_free(&mut manager); - return Ok(std::ptr::null_mut()); - } - let mut meta = TaskMeta::new(); - meta.export_params = Some(params as usize); - meta.export_color_manager = Some(color_manager); - meta_insert(h.ctx as usize, meta); - Ok(box_handle::(h)) - }) -} - -/// Copy the encoding-params POD the oaktask export creator reads out of the -/// facade's opaque params handle via its public getters. -/// -/// The oaktask crate's `convert_encoding_params` consumes exactly these -/// fields (filename, format, video/audio/subtitle enables, codecs, -/// 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 { - let mut pod = zeroed_encoding_params(); - - // filename (two-stage; writes NUL-terminated into `buf`) - let mut buf = [0 as c_char; 1024]; - let rc = unsafe { - crate::codec::oakengine_encoding_params_filename( - params, - buf.as_mut_ptr(), - buf.len() as c_int, - ) - }; - if rc < 0 { - return Err(Error::Invalid); - } - let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len()); - unsafe { - std::ptr::copy_nonoverlapping(buf.as_ptr() as *const u8, pod.filename.as_mut_ptr(), len) - }; - - pod.format = unsafe { crate::codec::oakengine_encoding_params_format(params) }; - pod.video_enabled = unsafe { crate::codec::oakengine_encoding_params_video_enabled(params) }; - 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 { - crate::codec::oakengine_encoding_params_get_export_length( - params, - &mut pod.export_length_num, - &mut pod.export_length_den, - ); - } - // Custom in/out range (work-area export): copied so the export task - // renders exactly [in, out) instead of the whole viewer length. - if unsafe { crate::codec::oakengine_encoding_params_has_custom_range(params) } != 0 { - let mut in_num: i64 = 0; - let mut in_den: i64 = 0; - let mut out_num: i64 = 0; - let mut out_den: i64 = 0; - let rc = unsafe { - crate::codec::oakengine_encoding_params_get_custom_range( - params, - &mut in_num, - &mut in_den, - &mut out_num, - &mut out_den, - ) - }; - if rc == 0 { - pod.has_custom_range = 1; - pod.custom_range_in_num = in_num; - pod.custom_range_in_den = in_den; - pod.custom_range_out_num = out_num; - pod.custom_range_out_den = out_den; - } - } - - if pod.video_enabled != 0 { - let mut video = std::mem::MaybeUninit::::uninit(); - let rc = unsafe { - crate::codec::oakengine_encoding_params_get_video_params(params, video.as_mut_ptr()) - }; - if rc == 0 { - let v = unsafe { video.assume_init() }; - pod.video_width = v.width; - 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 = crate::pods::pixel_format_from_code(v.format); - } - } - Ok(pod) -} - -/// Derive a color manager for an export task from the sequence's owning -/// project (borrowed project handle released after the manager is created). -/// Empty when the sequence has no project — the module export accepts an -/// empty manager. -fn export_color_manager(sequence: CHandle) -> Result { - let mut project = CHandle::null(); - Error::from_module(unsafe { n::oaknode_node_get_project(sequence, &mut project) })?; - if project.is_null() { - return Ok(CHandle::null()); - } - let manager = unsafe { n::oaknode_colormanager_init(project) }; - unsafe { n::oaknode_project_free(&mut project) }; - Ok(manager) -} - -// --------------------------------------------------------------------------- -// Import task results -// --------------------------------------------------------------------------- - -/// `oakengine_task_import_file_count` — number of files the import task -/// will process. -/// -/// The module's only import count export is `oaktask_import_footage_count`, -/// which reports the **imported-footage** list length — 0 before the task -/// runs even when files were supplied. Deviation from the C++ -/// `get_file_count` (the construction-time count); "0 means nothing to -/// import yet" holds before a run either way. -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_import_file_count(task: *mut OakEngineTask) -> c_int { - guard_int(|| unsafe { - let h = unbox(task)?; - let rc = t::oaktask_import_footage_count(h); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(rc) - } - }) -} - -/// `oakengine_task_import_get_command` — the undo command built by a -/// successful import run as an opaque `OakEngineClipboard` (NULL before the -/// run, after a cancelled run, or on a second call). Ownership detaches -/// from the task; push it with `oakengine_undo_push` or free it. -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_import_get_command( - task: *mut OakEngineTask, -) -> *mut c_void { - guard_ptr(|| unsafe { - let h = unbox(task)?; - let cmd = t::oaktask_import_take_command(h); - if cmd.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(cmd).cast()) - }) -} - -/// `oakengine_task_import_footage_count` — number of footage items a -/// successful import run created (the module's `oaktask_import_footage_count`, -/// the same count reported by `oakengine_task_import_file_count`). -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_import_footage_count(task: *mut OakEngineTask) -> c_int { - guard_int(|| unsafe { - let h = unbox(task)?; - let rc = t::oaktask_import_footage_count(h); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(rc) - } - }) -} - -/// `oakengine_task_import_footage_at` — borrowed node handle of the -/// imported footage at `index` (NULL when out of range or not an import -/// task). The module addrefs the footage handle; the caller releases it -/// with `oakengine_node_free`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_import_footage_at( - task: *mut OakEngineTask, - index: c_int, -) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - let h = unbox(task)?; - let fh = t::oaktask_import_footage_at(h, index); - if fh.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(fh)) - }) -} - -/// `oakengine_task_import_invalid_files_count` — number of files the import -/// task rejected. -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_import_invalid_files_count( - task: *mut OakEngineTask, -) -> c_int { - guard_int(|| unsafe { - let h = unbox(task)?; - let rc = t::oaktask_import_invalid_count(h); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(rc) - } - }) -} - -/// `oakengine_task_import_invalid_file_at` — rejected file path at `index` -/// (buf/size). Out-of-range reports the module's `OAKTASK_E_NOT_FOUND` -/// (-80004) pass-through, the header's "E_INVALID for other tasks" being -/// covered by the NULL-task `OAKENGINE_E_INVALID` path. -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_import_invalid_file_at( - task: *mut OakEngineTask, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let h = unbox(task)?; - let rc = t::oaktask_import_invalid_at(h, index, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -// --------------------------------------------------------------------------- -// Save task results -// --------------------------------------------------------------------------- - -/// `oakengine_task_save_get_project` — borrowed handle of the project a -/// save task wrote (NULL for other tasks). -/// -/// The module has no save-project getter, so the project is kept borrowed -/// from creation in [`TaskMeta`] (mirroring the C++ `ProjectSaveTask:: -/// get_project`, which returns the project the task was created with). -/// Each call returns a fresh borrowed handle the caller releases with -/// `oakengine_project_free`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_task_save_get_project( - task: *mut OakEngineTask, -) -> *mut OakEngineProject { - guard_ptr(|| unsafe { - let h = unbox(task)?; - match meta_get(h.ctx as usize).and_then(|m| m.save_project) { - Some(p) => Ok(box_handle::(p.addref())), - None => Ok(std::ptr::null_mut()), - } - }) -} - -// --------------------------------------------------------------------------- -// 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. A taken -/// project is bound to the default library (plan M13 D2 — the "task load -/// 完成" hook). -#[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()); - } - crate::storage::bind_project(project); - Ok(box_handle::(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, - 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)) - }) -} diff --git a/crates/oakengine.bk/src/test_support/audio.rs b/crates/oakengine.bk/src/test_support/audio.rs deleted file mode 100644 index 4e8e11e2a..000000000 --- a/crates/oakengine.bk/src/test_support/audio.rs +++ /dev/null @@ -1,163 +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 . - -//! Smoke tests for the audio family (`engine/include/oakengine/audio.h`). -//! The AudioManager singleton is process-wide, so manager tests run in a -//! single serialized test function; processor and sync tests are -//! independent. - -use super::common; - -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, - oakengine_audio_processor_create, oakengine_audio_processor_free, - oakengine_audio_processor_is_open, oakengine_audio_processor_open, - oakengine_audio_push_to_output, oakengine_audio_reset_output_clock, - oakengine_audio_set_output_device, oakengine_audio_set_output_notify_interval, - oakengine_audio_stop_output, oakengine_audio_sync_place_by_waveform_offset, - OakAudioSyncPlacement, OakAudioWaveformOffset, -}; - -/// Manager lifecycle: create/destroy round-trip and device accessors -/// (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() }; - - // No instance → create succeeds, destroy is idempotent. - assert_eq!(unsafe { oakengine_audio_create_instance() }, 0); - assert_eq!(unsafe { oakengine_audio_destroy_instance() }, 0); - assert_eq!(unsafe { oakengine_audio_destroy_instance() }, 0); - - // Recreate for the device tests. - assert_eq!(unsafe { oakengine_audio_create_instance() }, 0); - - // paNoDevice (-1) until a device is set. - assert_eq!(unsafe { oakengine_audio_get_output_device() }, -1); - // The module records any device index (PortAudio validation is not - // bridged), so setting succeeds and reads back. - assert_eq!(unsafe { oakengine_audio_set_output_device(999999) }, 0); - assert_eq!(unsafe { oakengine_audio_get_output_device() }, 999999); - assert_eq!(unsafe { oakengine_audio_set_output_device(-1) }, 0); - - // Stateless no-op calls succeed with a live manager. - assert_eq!(unsafe { oakengine_audio_reset_output_clock() }, 0); - assert_eq!(unsafe { oakengine_audio_stop_output() }, 0); - assert_eq!(unsafe { oakengine_audio_clear_buffered_output() }, 0); - assert_eq!( - unsafe { oakengine_audio_set_output_notify_interval(1024) }, - 0 - ); - assert_eq!(unsafe { oakengine_audio_hard_reset() }, 0); - - // push with a NULL params handle fails cleanly. - assert_eq!( - unsafe { - oakengine_audio_push_to_output( - std::ptr::null(), - c"data".as_ptr(), - 4, - std::ptr::null_mut(), - 0, - ) - }, - -3 // OAKENGINE_E_FAILED - ); - - unsafe { oakengine_audio_destroy_instance() }; -} - -/// Sync envelope-offset correlation runs and fills the result struct. -#[test] -fn sync_envelope_offset() { - let reference = [0.0_f64, 0.5, 1.0, 0.5, 0.0]; - let candidate = [0.0_f64, 0.0, 0.5, 1.0, 0.5]; - let mut out = OakAudioWaveformOffset { - offset_samples: 0, - confidence: 0.0, - valid: 0, - }; - let rc = unsafe { - oakengine_audio_estimate_envelope_offset( - reference.as_ptr(), - 5, - candidate.as_ptr(), - 5, - std::ptr::null(), - 0, - std::ptr::null(), - 0, - 128, - 16, - &mut out, - ) - }; - assert_eq!(rc, 0); - // The result is filled in either way; the correlation may or may not - // find a valid offset for this tiny synthetic input. - assert!(out.confidence >= 0.0 && out.confidence <= 1.0); -} - -/// Waveform-offset placement runs and reports validity. -#[test] -fn sync_place_by_waveform_offset() { - let mut out = OakAudioSyncPlacement { - timeline_in_num: 0, - timeline_in_den: 1, - valid: 0, - }; - let rc = unsafe { oakengine_audio_sync_place_by_waveform_offset(0, 1, 48000, 48000, &mut out) }; - assert_eq!(rc, 0); - // 48000 samples at 48 kHz = 1 second. - assert_eq!(out.timeline_in_num, 1); - assert_eq!(out.timeline_in_den, 1); - assert_eq!(out.valid, 1); - - // NULL out → E_INVALID. - assert_eq!( - unsafe { - oakengine_audio_sync_place_by_waveform_offset(0, 1, 0, 48000, std::ptr::null_mut()) - }, - -1 - ); -} - -/// Processor lifecycle: create/free round-trip; open with NULL params -/// fails with E_INVALID. -#[test] -fn processor_lifecycle() { - let p = unsafe { oakengine_audio_processor_create() }; - assert!(!p.is_null()); - assert_eq!(unsafe { oakengine_audio_processor_is_open(p) }, 0); - assert_eq!(unsafe { oakengine_audio_processor_close(p) }, 0); - - // open with a NULL `to` params handle → E_INVALID. - assert_eq!( - unsafe { oakengine_audio_processor_open(p, std::ptr::null(), std::ptr::null(), 1.0) }, - -1 - ); - - unsafe { oakengine_audio_processor_free(p) }; - // NULL free is a no-op. - unsafe { oakengine_audio_processor_free(std::ptr::null_mut()) }; -} diff --git a/crates/oakengine.bk/src/test_support/codec.rs b/crates/oakengine.bk/src/test_support/codec.rs deleted file mode 100644 index 969e7c7b3..000000000 --- a/crates/oakengine.bk/src/test_support/codec.rs +++ /dev/null @@ -1,313 +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 . - -//! Smoke tests for the encoding family (`engine/include/oakengine/encoding.h`): -//! container/codec metadata queries and the encoding-params handle. - -use super::common; - -use std::ffi::{c_char, c_int}; - -use crate::audio::oakengine_audio_destroy_instance; -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_count, oakengine_encoding_format_count, - oakengine_encoding_format_extension, oakengine_encoding_format_name, - oakengine_encoding_format_video_codec_at, oakengine_encoding_format_video_codec_count, - oakengine_encoding_generate_matrix, oakengine_encoding_image_sequence_digit_count, - oakengine_encoding_params_audio_enabled, oakengine_encoding_params_color_transform_output, - oakengine_encoding_params_create, oakengine_encoding_params_destroy, - oakengine_encoding_params_enable_audio, oakengine_encoding_params_enable_video, - oakengine_encoding_params_filename, oakengine_encoding_params_format, - oakengine_encoding_params_get_audio_params, oakengine_encoding_params_get_custom_range, - oakengine_encoding_params_get_video_params, oakengine_encoding_params_has_custom_range, - oakengine_encoding_params_is_valid, oakengine_encoding_params_set_color_transform, - oakengine_encoding_params_set_custom_range, oakengine_encoding_params_set_filename, - oakengine_encoding_params_set_format, oakengine_encoding_params_set_video_bit_rate, - oakengine_encoding_params_set_video_option, oakengine_encoding_params_set_video_pix_fmt, - oakengine_encoding_params_video_bit_rate, oakengine_encoding_params_video_codec, - oakengine_encoding_params_video_enabled, oakengine_encoding_params_video_option, - oakengine_encoding_params_video_pix_fmt, oakengine_encoding_pix_fmt_index, - oakengine_encoding_start_audio_recording, -}; -use crate::common::OakVideoParamsPod; - -/// Container format / codec metadata queries. -#[test] -fn encoding_metadata() { - // Format enumeration: at least the six named formats exist. - let count = unsafe { oakengine_encoding_format_count() }; - assert!(count >= 6); - - // Matroska (1): name + extension via two-stage getters. - let mut buf = [0 as c_char; 64]; - let len = unsafe { oakengine_encoding_format_name(1, buf.as_mut_ptr(), 64) }; - assert!(len > 0); - assert!(unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } - .to_str() - .unwrap() - .contains("Matroska")); - let len = unsafe { oakengine_encoding_format_extension(1, buf.as_mut_ptr(), 64) }; - assert!(len > 0); - assert_eq!( - unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } - .to_str() - .unwrap(), - "mkv" - ); - - // Per-format codec lists. - assert!(unsafe { oakengine_encoding_format_video_codec_count(1) } >= 1); - let codec = unsafe { oakengine_encoding_format_video_codec_at(1, 0) }; - assert!(codec >= 1); - assert!(unsafe { oakengine_encoding_format_audio_codec_count(1) } >= 1); - - // Codec metadata: name, still-image (PNG = 5), lossless. - let len = unsafe { oakengine_encoding_codec_name(1, buf.as_mut_ptr(), 64) }; - assert!(len > 0); - assert_eq!(unsafe { oakengine_encoding_codec_is_still_image(5) }, 1); // PNG - assert_eq!(unsafe { oakengine_encoding_codec_is_still_image(1) }, 0); // H264 - assert!(unsafe { oakengine_encoding_codec_is_lossless(13) } == 1); // PCM - - // pix_fmt_index: preferred format index when absent. - assert_eq!( - unsafe { oakengine_encoding_pix_fmt_index(1, c"yuv420p".as_ptr()) }, - 0 - ); -} - -/// Image-sequence filename helpers. -#[test] -fn filename_helpers() { - assert_eq!( - unsafe { - oakengine_encoding_filename_contains_digit_placeholder(c"img[#####].png".as_ptr()) - }, - 1 - ); - assert_eq!( - unsafe { oakengine_encoding_filename_contains_digit_placeholder(c"img.png".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_image_sequence_digit_count(c"img[#####].png".as_ptr()) }, - 5 - ); - - let mut buf = [0 as c_char; 64]; - let len = unsafe { - oakengine_encoding_filename_remove_digit_placeholder( - c"img[#####].png".as_ptr(), - buf.as_mut_ptr(), - 64, - ) - }; - assert!(len > 0); - assert_eq!( - unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } - .to_str() - .unwrap(), - "img.png" - ); -} - -/// Transform matrix: fit produces a valid 16-float matrix. -#[test] -fn generate_matrix() { - let mut m = [0.0_f32; 16]; - assert_eq!( - unsafe { oakengine_encoding_generate_matrix(0, 1920, 1080, 960, 540, m.as_mut_ptr()) }, - 0 - ); - // The 4x4 identity-ish matrix has a non-zero top-left. - assert!(m[0] > 0.0 || m[5] > 0.0); - // NULL output → E_INVALID. - assert_eq!( - unsafe { oakengine_encoding_generate_matrix(0, 1, 1, 1, 1, std::ptr::null_mut()) }, - -1 - ); -} - -/// Encoding-params handle lifecycle: create → configure → read back → -/// destroy (serialized inside one test; the handle is per-call state). -#[test] -fn params_handle_round_trip() { - let p = unsafe { oakengine_encoding_params_create() }; - assert!(!p.is_null()); - - // Fresh: nothing enabled, format unset (-1). - assert_eq!(unsafe { oakengine_encoding_params_is_valid(p) }, 0); - assert_eq!(unsafe { oakengine_encoding_params_format(p) }, -1); - - // Format: set + get, and reject out-of-range. - assert_eq!(unsafe { oakengine_encoding_params_set_format(p, 1) }, 0); // Matroska - assert_eq!(unsafe { oakengine_encoding_params_format(p) }, 1); - assert_eq!(unsafe { oakengine_encoding_params_set_format(p, 9999) }, -1); - - // Filename round-trip. - assert_eq!( - unsafe { oakengine_encoding_params_set_filename(p, c"out.mkv".as_ptr()) }, - 0 - ); - let mut buf = [0 as c_char; 64]; - let len = unsafe { oakengine_encoding_params_filename(p, buf.as_mut_ptr(), 64) }; - assert_eq!(len, 7); - assert_eq!( - unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } - .to_str() - .unwrap(), - "out.mkv" - ); - - // Enable video: valid, get_video_params reads back. - let mut vp: OakVideoParamsPod = unsafe { std::mem::zeroed() }; - assert_eq!( - unsafe { - crate::common::oakengine_video_params_make( - &mut vp, 1920, 1080, 1001, 30000, 4, 1, 1, 0, 1, 1, - ) - }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_params_enable_video(p, &vp, 1) }, - 0 - ); - assert_eq!(unsafe { oakengine_encoding_params_is_valid(p) }, 1); - assert_eq!(unsafe { oakengine_encoding_params_video_enabled(p) }, 1); - assert_eq!(unsafe { oakengine_encoding_params_video_codec(p) }, 1); - let mut out_vp: OakVideoParamsPod = unsafe { std::mem::zeroed() }; - assert_eq!( - unsafe { oakengine_encoding_params_get_video_params(p, &mut out_vp) }, - 0 - ); - assert_eq!(out_vp.width, 1920); - assert_eq!(out_vp.height, 1080); - assert_eq!(out_vp.time_base_num, 1001); - - // Video bit rate round-trip. - unsafe { oakengine_encoding_params_set_video_bit_rate(p, 8_000_000) }; - assert_eq!( - unsafe { oakengine_encoding_params_video_bit_rate(p) }, - 8_000_000 - ); - - // Encoded pixel format round-trip. - assert_eq!( - unsafe { oakengine_encoding_params_set_video_pix_fmt(p, c"yuv420p".as_ptr()) }, - 0 - ); - let len = unsafe { oakengine_encoding_params_video_pix_fmt(p, buf.as_mut_ptr(), 64) }; - assert_eq!(len, 7); - assert_eq!( - unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } - .to_str() - .unwrap(), - "yuv420p" - ); - - // Audio: disabled get_video/audio → E_STATE; enable then read back. - let mut sr: c_int = 0; - let mut layout: u64 = 0; - let mut sf: c_int = 0; - assert_eq!( - unsafe { oakengine_encoding_params_get_audio_params(p, &mut sr, &mut layout, &mut sf) }, - -2 - ); - assert_eq!( - unsafe { oakengine_encoding_params_enable_audio(p, 48000, 3, 0, 13) }, - 0 - ); - assert_eq!(unsafe { oakengine_encoding_params_audio_enabled(p) }, 1); - assert_eq!( - unsafe { oakengine_encoding_params_get_audio_params(p, &mut sr, &mut layout, &mut sf) }, - 0 - ); - assert_eq!(sr, 48000); - assert_eq!(layout, 3); - - // Custom range: not set → E_NOT_FOUND; set → reads back. - let (mut inn, mut ind, mut outn, mut outd) = (0i64, 0i64, 0i64, 0i64); - assert_eq!( - unsafe { - oakengine_encoding_params_get_custom_range(p, &mut inn, &mut ind, &mut outn, &mut outd) - }, - -4 - ); - unsafe { oakengine_encoding_params_set_custom_range(p, 0, 1, 100, 1) }; - assert_eq!(unsafe { oakengine_encoding_params_has_custom_range(p) }, 1); - assert_eq!( - unsafe { - oakengine_encoding_params_get_custom_range(p, &mut inn, &mut ind, &mut outn, &mut outd) - }, - 0 - ); - assert_eq!((inn, ind, outn, outd), (0, 1, 100, 1)); - - // Color transform + video option round-trips. - assert_eq!( - unsafe { oakengine_encoding_params_set_color_transform(p, c"ACEScg".as_ptr()) }, - 0 - ); - let len = unsafe { oakengine_encoding_params_color_transform_output(p, buf.as_mut_ptr(), 64) }; - assert_eq!(len, 6); - assert_eq!( - unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } - .to_str() - .unwrap(), - "ACEScg" - ); - assert_eq!( - unsafe { oakengine_encoding_params_set_video_option(p, c"crf".as_ptr(), c"18".as_ptr()) }, - 0 - ); - let len = - unsafe { oakengine_encoding_params_video_option(p, c"crf".as_ptr(), buf.as_mut_ptr(), 64) }; - assert_eq!(len, 2); - assert_eq!( - unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } - .to_str() - .unwrap(), - "18" - ); - assert_eq!( - unsafe { - oakengine_encoding_params_video_option(p, c"missing".as_ptr(), buf.as_mut_ptr(), 64) - }, - -4 - ); - - unsafe { oakengine_encoding_params_destroy(p) }; - // NULL destroy is a no-op. - unsafe { oakengine_encoding_params_destroy(std::ptr::null_mut()) }; -} - -/// Audio recording without a running audio manager fails with E_STATE. -#[test] -fn start_audio_recording_no_manager() { - // The audio manager is a process-wide singleton shared with it_audio; - // serialize and normalize it (no instance) before asserting E_STATE. - common::with_manager(|| unsafe { - let _ = oakengine_audio_destroy_instance(); - let p = oakengine_encoding_params_create(); - assert!(!p.is_null()); - let rc = oakengine_encoding_start_audio_recording(p, std::ptr::null_mut(), 0); - assert_eq!(rc, -2); // OAKENGINE_E_STATE - oakengine_encoding_params_destroy(p); - }); -} diff --git a/crates/oakengine.bk/src/test_support/common/mod.rs b/crates/oakengine.bk/src/test_support/common/mod.rs deleted file mode 100644 index a500cc862..000000000 --- a/crates/oakengine.bk/src/test_support/common/mod.rs +++ /dev/null @@ -1,117 +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 . - -//! Shared test support, included from every integration test via -//! `#[path = "common/mod.rs"] mod common;`. -//! -//! Two jobs: -//! -//! 1. **Force rustc to link every module crate's rlib** into the test -//! binary ([`force_link`]). The test-support files import the module -//! crates directly (single-lib; the deleted `src/bridge` no longer -//! exists), and the array doubles as a compile-time proof that the -//! anchor paths in `crates/oakengine/src/linkage.rs` match the current -//! module layouts. -//! -//! 2. **Re-export the folded-in `oakcore_audioparams_*` accessors** for -//! the former mock call sites (`common::oakcore_audioparams_*`). The -//! facade used to leave those symbols as runtime lookups for a C++ -//! liboakcore host, and the tests defined in-memory mocks; M12 P5 -//! implemented them inside the dylib (crates/oakengine/src/stubs.rs, -//! module `audio`), so the tests just use those implementations. - -#![allow(dead_code, unused_variables)] - -use std::sync::Mutex; - -/// 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::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() -} - -/// Serializes every test that reads or writes the `Storage` config group -/// (the facade's write-through library selection — see src/storage.rs). -/// The config store is process-global, so the write-through tests and the -/// tests that disable the backend must take this lock for their whole -/// body instead of racing on the shared store. -pub static STORAGE_CONFIG_LOCK: Mutex<()> = Mutex::new(()); - -/// Run `f` with the write-through storage backend disabled -/// (`Storage/Backend = "off"`). Tests that push undo commands on real -/// projects (e.g. `oakengine_project_add_node`) would otherwise bind them -/// to the default user library and write there; disabling the backend -/// keeps them side-effect-free. The value intentionally persists — every -/// storage test sets its own backend explicitly under -/// [`STORAGE_CONFIG_LOCK`]. -pub fn with_storage_off(f: impl FnOnce() -> R) -> R { - let _g = storage_off_guard(); - f() -} - -/// Take the storage-config lock AND disable the write-through backend, -/// returning the guard: a test that pushes undo commands throughout its -/// body holds the guard (and thus the lock) for its whole lifetime, so a -/// concurrently running storage test cannot flip the backend mid-test. -pub fn storage_off_guard() -> std::sync::MutexGuard<'static, ()> { - let g = STORAGE_CONFIG_LOCK.lock().unwrap_or_else(|e| e.into_inner()); - oakcommon::configstore::ConfigStore::instance().set(Some("Storage"), "Backend", "off"); - g -} - -// --------------------------------------------------------------------------- -// oakcore_audioparams_* (see module docs) -// --------------------------------------------------------------------------- - -/// The facade's in-dylib `oakcore_audioparams_*` C ABI (see -/// `crate::stubs::audio`): the accessors were host-provided mocks until -/// M12 P5 folded them into the engine, so the tests now share the real -/// implementations instead of defining per-binary duplicates. Only the -/// two entry points the test files call (`create`/`free`) are re-exported; -/// the read accessors are reached through `crate::stubs::audio` where the -/// tests need them. -pub use crate::stubs::audio::{oakcore_audioparams_create, oakcore_audioparams_free}; diff --git a/crates/oakengine.bk/src/test_support/common_smoke.rs b/crates/oakengine.bk/src/test_support/common_smoke.rs deleted file mode 100644 index e90491038..000000000 --- a/crates/oakengine.bk/src/test_support/common_smoke.rs +++ /dev/null @@ -1,241 +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 . - -//! Smoke tests for the common family (`engine/include/oakengine/config.h` -//! and `videoparams.h`). The oakcommon config store is a process-wide -//! singleton, so config tests are serialized inside single test -//! functions. - -use super::common; - -use std::ffi::{c_char, c_int}; - -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, - oakengine_video_params_effective_size, oakengine_video_params_equal, - oakengine_video_params_format_is_float, oakengine_video_params_internal_channel_count, - oakengine_video_params_is_valid, oakengine_video_params_make, - oakengine_video_params_standard_pixel_aspect_at, - oakengine_video_params_standard_pixel_aspect_count, - oakengine_video_params_supported_divider_at, oakengine_video_params_supported_divider_count, - oakengine_video_params_supported_frame_rate_at, - oakengine_video_params_supported_frame_rate_count, OakVideoParamsPod, -}; - -/// Config: load/save, string and int round-trips, missing-key behavior. -/// -/// Serialized with the write-through tests (the config store is -/// process-global) and redirected to a temp `OAK_CONFIG_DIR` — without the -/// redirect, `oakengine_config_save` would write the real `config.ini`. -#[test] -fn config_round_trip() { - let _config = common::STORAGE_CONFIG_LOCK.lock().unwrap_or_else(|e| e.into_inner()); - let dir = std::env::temp_dir().join(format!( - "oakengine_common_smoke_config_{}", - std::process::id() - )); - let _ = std::fs::create_dir_all(&dir); - std::env::set_var("OAK_CONFIG_DIR", &dir); - assert_eq!(unsafe { oakengine_config_load() }, 0); - - // Missing key reads as 0 / empty. - let mut buf = [0 as c_char; 64]; - assert_eq!( - unsafe { oakengine_config_get_string(c"no/such/key".as_ptr(), buf.as_mut_ptr(), 64) }, - 0 - ); - assert_eq!( - unsafe { oakengine_config_get_int(c"no/such/key".as_ptr(), 7) }, - 7 - ); - - // String round-trip. - assert_eq!( - unsafe { oakengine_config_set_string(c"facade/test".as_ptr(), c"hello".as_ptr()) }, - 0 - ); - let len = unsafe { oakengine_config_get_string(c"facade/test".as_ptr(), buf.as_mut_ptr(), 64) }; - assert_eq!(len, 5); - assert_eq!( - unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } - .to_str() - .unwrap(), - "hello" - ); - - // A too-small buffer is not written (the two-stage convention is: - // query the required size, allocate, copy) — the module reports the - // full length and leaves the buffer untouched. - let mut small = [0 as c_char; 3]; - let len = - unsafe { oakengine_config_get_string(c"facade/test".as_ptr(), small.as_mut_ptr(), 3) }; - assert_eq!(len, 5); // reported full length - assert_eq!( - unsafe { std::ffi::CStr::from_ptr(small.as_ptr()) } - .to_str() - .unwrap(), - "" - ); - - // Int round-trip. - assert_eq!( - unsafe { oakengine_config_set_int(c"facade/n".as_ptr(), 1234) }, - 0 - ); - assert_eq!( - unsafe { oakengine_config_get_int(c"facade/n".as_ptr(), 0) }, - 1234 - ); - - assert_eq!(unsafe { oakengine_config_save() }, 0); - assert!(dir.join("config.ini").exists()); - - std::env::remove_var("OAK_CONFIG_DIR"); - let _ = std::fs::remove_dir_all(&dir); -} - -/// Config error handler: registered, then invoked via report_error. -#[test] -fn config_error_handler() { - static CALLED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); - unsafe extern "C" fn handler( - _title: *const c_char, - _message: *const c_char, - _userdata: *mut std::ffi::c_void, - ) { - CALLED.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - } - assert_eq!( - unsafe { oakengine_config_set_error_handler(Some(handler), std::ptr::null_mut()) }, - 0 - ); - // Report an error through the handler. - assert_eq!( - 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); - // NULL handler clears; reporting then does not invoke. - assert_eq!( - unsafe { oakengine_config_set_error_handler(None, std::ptr::null_mut()) }, - 0 - ); - 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); -} - -/// Videoparams static tables. -#[test] -fn videoparams_static_tables() { - // 12 standard frame rates; the 23.976 entry is 24000/1001. - assert_eq!( - unsafe { oakengine_video_params_supported_frame_rate_count() }, - 12 - ); - let (mut num, mut den) = (0, 0); - assert_eq!( - unsafe { oakengine_video_params_supported_frame_rate_at(2, &mut num, &mut den) }, - 0 - ); - assert_eq!((num, den), (24000, 1001)); - // Out of range → E_INVALID. - assert_eq!( - unsafe { oakengine_video_params_supported_frame_rate_at(99, &mut num, &mut den) }, - -1 - ); - - // 6 standard pixel aspects; index 4 is PAL widescreen 64/45. - assert_eq!( - unsafe { oakengine_video_params_standard_pixel_aspect_count() }, - 6 - ); - assert_eq!( - unsafe { oakengine_video_params_standard_pixel_aspect_at(4, &mut num, &mut den) }, - 0 - ); - assert_eq!((num, den), (64, 45)); - - // Dividers 1..=8; out of range → -1. - assert_eq!( - unsafe { oakengine_video_params_supported_divider_count() }, - 8 - ); - assert_eq!(unsafe { oakengine_video_params_supported_divider_at(5) }, 8); - assert_eq!( - unsafe { oakengine_video_params_supported_divider_at(99) }, - -1 - ); - - // Format helpers (PixelFormat codes: F16 = 3, F32 = 4). - assert_eq!(unsafe { oakengine_video_params_format_is_float(4) }, 1); // F32 - assert_eq!(unsafe { oakengine_video_params_format_is_float(3) }, 1); // F16 - assert_eq!(unsafe { oakengine_video_params_format_is_float(0) }, 0); // U8 - assert_eq!( - unsafe { oakengine_video_params_internal_channel_count() }, - 4 - ); - assert!(unsafe { oakengine_video_params_bytes_per_pixel(1, 4) } > 0); -} - -/// Videoparams POD: make/equal/valid + effective size. -#[test] -fn videoparams_pod() { - let mut a: OakVideoParamsPod = unsafe { std::mem::zeroed() }; - assert_eq!( - unsafe { oakengine_video_params_make(&mut a, 1920, 1080, 1001, 30000, 16, 1, 1, 0, 1, 1,) }, - 0 - ); - assert_eq!(a.width, 1920); - assert_eq!(a.height, 1080); - assert_eq!(a.time_base_num, 1001); - - // A valid POD is valid. - assert_eq!(unsafe { oakengine_video_params_is_valid(&a) }, 1); - // Zero dimensions are not. - let mut bad = a; - bad.width = 0; - assert_eq!(unsafe { oakengine_video_params_is_valid(&bad) }, 0); - // NULL is invalid. - assert_eq!( - unsafe { oakengine_video_params_is_valid(std::ptr::null()) }, - 0 - ); - - // Equality: identical PODs equal; differing field not. - let mut b = a; - assert_eq!(unsafe { oakengine_video_params_equal(&a, &b) }, 1); - b.divider = 2; - assert_eq!(unsafe { oakengine_video_params_equal(&a, &b) }, 0); - assert_eq!( - unsafe { oakengine_video_params_equal(std::ptr::null(), &a) }, - 0 - ); - - // Effective size halves at divider 2. - let (mut w, mut h) = (0, 0); - assert_eq!( - unsafe { oakengine_video_params_effective_size(1920, 1080, 2, &mut w, &mut h) }, - 0 - ); - assert_eq!((w, h), (960, 540)); - // Invalid divider. - assert_eq!( - unsafe { oakengine_video_params_effective_size(1920, 1080, 0, &mut w, &mut h) }, - -1 - ); -} diff --git a/crates/oakengine.bk/src/test_support/it_audio.rs b/crates/oakengine.bk/src/test_support/it_audio.rs deleted file mode 100644 index 4d9d70f77..000000000 --- a/crates/oakengine.bk/src/test_support/it_audio.rs +++ /dev/null @@ -1,1440 +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 . - -//! Integration tests for the audio family: the facade exports -//! `oakengine_audio_*` (src/audio.rs; module C contract -//! `include/audio/{manager,processor,sync,error}.h`), exercised end to -//! end against the REAL `oakaudio` crate — no mocks anywhere. -//! -//! 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 -//! documented facade stubs (`convert` returns `OAKENGINE_E_FAILED`, -//! `output_params` returns NULL — "not backed" in src/audio.rs). -//! -//! Legal-path value matrices pin exact results (device indices, rational -//! placement arithmetic, envelope correlation values); illegal inputs -//! (NULL pointers, empty handles, out-of-range sizes, garbage enum -//! values) must return a clean negative code or a documented no-op — -//! never crash/abort/panic. -//! -//! Note: `start_recording` with an input device set drives the REAL -//! oakcodec FFmpeg encoder (ffmpeg-next), so it writes a real media file -//! to the system temp dir when the host has the codec; when the host -//! build cannot create the encoder the call fails with the module's -//! `OAKAUDIO_E_FAILED` and a diagnostic in `error_buf` — both outcomes -//! are asserted. - -use super::common; - -use std::ffi::{c_int, c_void, CStr, CString}; -use std::path::PathBuf; -use std::sync::Mutex; - -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, - oakengine_audio_estimate_stretch_and_offset, oakengine_audio_get_input_device, - oakengine_audio_get_output_device, oakengine_audio_hard_reset, oakengine_audio_manager_handle, - oakengine_audio_processor_close, oakengine_audio_processor_convert, - oakengine_audio_processor_create, oakengine_audio_processor_free, - oakengine_audio_processor_is_open, oakengine_audio_processor_open, - oakengine_audio_processor_output_params, oakengine_audio_push_to_output, - oakengine_audio_reset_output_clock, oakengine_audio_set_input_device, - 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, - oakengine_waveform_extract, OakAudioSyncPlacement, OakAudioSyncSourceClip, - OakAudioWaveformOffset, OakAudioWaveformStretchOffset, -}; -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. -const AUDIO_E_INVALID: c_int = -60001; -/// `OAKAUDIO_E_FAILED`. -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 -// --------------------------------------------------------------------------- - -/// Serialize manager-touching tests: the AudioManager singleton is -/// 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()) { - common::with_manager(f) -} - -/// Serialize processor tests: each processor is an independent -/// refcounted object, but the module's debug alive counter is process -/// global, so count assertions need exclusive access to the family. -fn with_processor(f: impl FnOnce()) { - static LOCK: Mutex<()> = Mutex::new(()); - let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner()); - f(); -} - -/// Current number of live refcounted oakaudio objects. -fn alive() -> c_int { - crate::stubs::audio::oakaudio_debug_alive_count() -} - -/// A borrowed `OakAudioParams*` handle created through the facade's -/// in-dylib `oakcore_audioparams_*` accessors (tests/common/mod.rs -/// re-exports them; see `crate::stubs::audio`). -fn audio_params(rate: c_int, layout: u64, format: c_int) -> *mut c_void { - common::oakcore_audioparams_create(rate, layout, format) -} - -/// Unique recording output path under the system temp dir. -fn recording_path() -> PathBuf { - std::env::temp_dir().join(format!("oak-it-audio-rec-{}.wav", std::process::id())) -} - -// --------------------------------------------------------------------------- -// Manager — lifecycle and devices (serialized) -// --------------------------------------------------------------------------- - -/// Manager lifecycle + device legal matrix + no-instance illegal matrix. -#[test] -fn manager_device_lifecycle() { - with_manager(|| { - let _ = common::force_link(); - - // Start from a destroyed state. - assert_eq!(unsafe { oakengine_audio_destroy_instance() }, 0); - - // --- No instance: every function reports a clean error. --- - assert!(unsafe { oakengine_audio_manager_handle() }.is_null()); - assert_eq!(unsafe { oakengine_audio_get_output_device() }, -1); // paNoDevice - assert_eq!(unsafe { oakengine_audio_get_input_device() }, -1); - assert_eq!( - unsafe { oakengine_audio_set_output_device(0) }, - OAKENGINE_E_FAILED - ); - assert_eq!( - unsafe { oakengine_audio_set_input_device(0) }, - OAKENGINE_E_FAILED - ); - assert_eq!(unsafe { oakengine_audio_hard_reset() }, OAKENGINE_E_FAILED); - assert_eq!( - unsafe { oakengine_audio_clear_buffered_output() }, - OAKENGINE_E_FAILED - ); - assert_eq!(unsafe { oakengine_audio_stop_output() }, OAKENGINE_E_FAILED); - assert_eq!( - unsafe { oakengine_audio_stop_recording() }, - OAKENGINE_E_FAILED - ); - assert_eq!( - unsafe { oakengine_audio_reset_output_clock() }, - OAKENGINE_E_FAILED - ); - assert_eq!( - unsafe { oakengine_audio_set_output_notify_interval(1024) }, - OAKENGINE_E_FAILED - ); - // push: NULL params is rejected at the facade before the module runs. - assert_eq!( - unsafe { - oakengine_audio_push_to_output( - std::ptr::null(), - c"data".as_ptr(), - 4, - std::ptr::null_mut(), - 0, - ) - }, - OAKENGINE_E_FAILED - ); - // start_recording: NULL params with no instance → the facade's - // manager check fires first (-3). - assert_eq!( - unsafe { - oakengine_audio_start_recording(std::ptr::null_mut(), std::ptr::null_mut(), 0) - }, - OAKENGINE_E_FAILED - ); - - // --- Lifecycle: create/destroy idempotence and re-create. --- - assert_eq!(unsafe { oakengine_audio_create_instance() }, 0); - assert_eq!(unsafe { oakengine_audio_create_instance() }, 0); // no-op when exists - assert_eq!(unsafe { oakengine_audio_destroy_instance() }, 0); - assert_eq!(unsafe { oakengine_audio_destroy_instance() }, 0); // no-op when absent - assert!(unsafe { oakengine_audio_manager_handle() }.is_null()); - - assert_eq!(unsafe { oakengine_audio_create_instance() }, 0); - assert!(!unsafe { oakengine_audio_manager_handle() }.is_null()); - - // --- Device accessors: legal matrix. --- - // The manager singleton retains its device state across - // destroy/recreate (the OnceLock box is kept; only a flag flips), - // so pin the devices explicitly instead of assuming fresh defaults. - assert_eq!(unsafe { oakengine_audio_set_output_device(-1) }, 0); - assert_eq!(unsafe { oakengine_audio_get_output_device() }, -1); - assert_eq!(unsafe { oakengine_audio_set_input_device(-1) }, 0); - assert_eq!(unsafe { oakengine_audio_get_input_device() }, -1); - - // The module records any device index (PortAudio enumeration is not - // bridged); -1 (paNoDevice), 0, a large index and a negative index. - for device in [-1i64, 0, 999999, -100] { - assert_eq!(unsafe { oakengine_audio_set_output_device(device) }, 0); - assert_eq!(unsafe { oakengine_audio_get_output_device() }, device); - } - // An i64 that does not fit an i32 narrows to 0 (C int narrowing). - assert_eq!(unsafe { oakengine_audio_set_output_device(1 << 40) }, 0); - assert_eq!(unsafe { oakengine_audio_get_output_device() }, 0); - - for device in [-1i64, 0, 999999] { - assert_eq!(unsafe { oakengine_audio_set_input_device(device) }, 0); - assert_eq!(unsafe { oakengine_audio_get_input_device() }, device); - } - - // --- Output controls. --- - assert_eq!(unsafe { oakengine_audio_reset_output_clock() }, 0); - assert_eq!(unsafe { oakengine_audio_stop_output() }, 0); - assert_eq!(unsafe { oakengine_audio_clear_buffered_output() }, 0); - assert_eq!(unsafe { oakengine_audio_hard_reset() }, 0); - // Notify interval: 0 disables, positive accepted, negative invalid. - assert_eq!(unsafe { oakengine_audio_set_output_notify_interval(0) }, 0); - assert_eq!( - unsafe { oakengine_audio_set_output_notify_interval(1024) }, - 0 - ); - assert_eq!( - unsafe { oakengine_audio_set_output_notify_interval(-1) }, - AUDIO_E_INVALID - ); - - // --- push_to_output: legal + illegal matrix. --- - // NULL params is rejected at the facade (-3) even with an instance. - assert_eq!( - unsafe { - oakengine_audio_push_to_output( - std::ptr::null(), - c"data".as_ptr(), - 4, - std::ptr::null_mut(), - 0, - ) - }, - OAKENGINE_E_FAILED - ); - - // M12 P1: with no explicit device the push still succeeds — the - // samples buffer for the default output device (unavailable - // devices keep playback silent instead of failing the push). - assert_eq!(unsafe { oakengine_audio_set_output_device(-1) }, 0); - let params = audio_params(48000, 3, 10); // f32 packed, stereo, 48 kHz - assert_eq!( - unsafe { - oakengine_audio_push_to_output( - params as *const c_void, - c"data".as_ptr(), - 4, - std::ptr::null_mut(), - 0, - ) - }, - 0 - ); - common::oakcore_audioparams_free(params); - - // Garbage sample format → E_INVALID. - let params = audio_params(48000, 3, 99); - assert_eq!( - unsafe { - oakengine_audio_push_to_output( - params as *const c_void, - c"data".as_ptr(), - 4, - std::ptr::null_mut(), - 0, - ) - }, - AUDIO_E_INVALID - ); - common::oakcore_audioparams_free(params); - - // Zero sample rate (mock default) → E_INVALID. - let params = audio_params(0, 3, 10); - assert_eq!( - unsafe { - oakengine_audio_push_to_output( - params as *const c_void, - c"data".as_ptr(), - 4, - std::ptr::null_mut(), - 0, - ) - }, - AUDIO_E_INVALID - ); - common::oakcore_audioparams_free(params); - - // NULL samples → E_INVALID. - let params = audio_params(48000, 3, 10); - assert_eq!( - unsafe { - oakengine_audio_push_to_output( - params as *const c_void, - std::ptr::null(), - 4, - std::ptr::null_mut(), - 0, - ) - }, - AUDIO_E_INVALID - ); - // Negative byte count → E_INVALID. - assert_eq!( - unsafe { - oakengine_audio_push_to_output( - params as *const c_void, - c"data".as_ptr(), - -1, - std::ptr::null_mut(), - 0, - ) - }, - AUDIO_E_INVALID - ); - common::oakcore_audioparams_free(params); - - // Legal push with a device selected: bytes are queued, error_buf - // stays untouched on success. - assert_eq!(unsafe { oakengine_audio_set_output_device(0) }, 0); - let params = audio_params(48000, 3, 10); - let mut err = [0i8; 128]; - assert_eq!( - unsafe { - oakengine_audio_push_to_output( - params as *const c_void, - c"data".as_ptr(), - 4, - err.as_mut_ptr(), - err.len() as c_int, - ) - }, - 0 - ); - assert_eq!( - unsafe { *err.as_ptr() }, - 0, - "error_buf untouched on success" - ); - // A zero-length push is legal (empty queue op). - assert_eq!( - unsafe { - oakengine_audio_push_to_output( - params as *const c_void, - c"".as_ptr(), - 0, - std::ptr::null_mut(), - 0, - ) - }, - 0 - ); - common::oakcore_audioparams_free(params); - - assert_eq!(unsafe { oakengine_audio_destroy_instance() }, 0); - }); -} - -/// Recording: no-device / invalid-params paths and a real encoder start. -#[test] -fn manager_recording() { - with_manager(|| { - assert_eq!(unsafe { oakengine_audio_destroy_instance() }, 0); - assert_eq!(unsafe { oakengine_audio_create_instance() }, 0); - - // NULL params → E_INVALID at the facade. - assert_eq!( - unsafe { - oakengine_audio_start_recording(std::ptr::null_mut(), std::ptr::null_mut(), 0) - }, - OAKENGINE_E_INVALID - ); - - // audio_enabled == 0 → E_INVALID with a diagnostic. - let mut params = recording_params(false); - let mut err = [0i8; 128]; - assert_eq!( - unsafe { - oakengine_audio_start_recording( - (&mut params as *mut oakcodec::encodingparams::EncodingParams) - .cast::(), - err.as_mut_ptr(), - err.len() as c_int, - ) - }, - AUDIO_E_INVALID - ); - assert_eq!( - unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap(), - "invalid recording parameters" - ); - - // Valid params but no input device → clean E_FAILED. Pin the input - // device to paNoDevice first (the retained singleton may hold a - // device index set by a prior serialized manager test). - assert_eq!(unsafe { oakengine_audio_set_input_device(-1) }, 0); - let mut params = recording_params(true); - let mut err = [0i8; 128]; - assert_eq!( - unsafe { - oakengine_audio_start_recording( - (&mut params as *mut oakcodec::encodingparams::EncodingParams) - .cast::(), - err.as_mut_ptr(), - err.len() as c_int, - ) - }, - AUDIO_E_FAILED - ); - assert_eq!( - unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap(), - "no input device" - ); - - // With an input device the real oakcodec encoder runs: the module - // records to a WAV file (pcm_s16le) and reports OAKAUDIO_OK when the - // host FFmpeg build can create the encoder, or OAKAUDIO_E_FAILED with - // a diagnostic otherwise. Either way the return is a clean code. - assert_eq!(unsafe { oakengine_audio_set_input_device(0) }, 0); - let path = recording_path(); - let _ = std::fs::remove_file(&path); - let mut params = recording_params(true); - let mut err = [0i8; 512]; - let rc = unsafe { - oakengine_audio_start_recording( - (&mut params as *mut oakcodec::encodingparams::EncodingParams) - .cast::(), - err.as_mut_ptr(), - err.len() as c_int, - ) - }; - // On this host the real oakcodec encoder opens the WAV output and the - // recording starts (rc == 0, file written); a host without the codec - // reports OAKAUDIO_E_FAILED with a diagnostic. Either outcome is a - // clean code with the corresponding side effect. - match rc { - 0 => assert!(path.exists(), "recording file written"), - AUDIO_E_FAILED => { - let msg = unsafe { CStr::from_ptr(err.as_ptr()) } - .to_str() - .unwrap_or(""); - assert!(!msg.is_empty(), "encoder failure should write a diagnostic"); - } - other => panic!("unexpected recording rc {other}"), - } - assert!(rc == 0 || rc == AUDIO_E_FAILED, "unexpected rc {rc}"); - if rc == 0 { - assert!(path.exists(), "recording file written"); - } - // Recording is stopped unconditionally (idle stop is a no-op), then - // the file is removed. - assert_eq!(unsafe { oakengine_audio_stop_recording() }, 0); - assert_eq!(unsafe { oakengine_audio_stop_recording() }, 0); - let _ = std::fs::remove_file(&path); - - assert_eq!(unsafe { oakengine_audio_destroy_instance() }, 0); - }); -} - -/// `oakcodec_encoding_params` with WAV / pcm_s16le and the requested audio -/// track. -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"); - p.filename[..bytes.len()].copy_from_slice(bytes); - p.format = 7; // WAV - p.audio_enabled = audio_enabled as c_int; - p.audio_codec = 13; // PCM_S16LE - p.audio_sample_rate = 48000; - p.audio_channel_layout = 3; // stereo - p.audio_sample_format = oakcore_rs::SampleFormat::S16; // packed 16-bit - p.export_length_num = 1; - p.export_length_den = 1; - p -} - -// --------------------------------------------------------------------------- -// Sync — envelope correlation (stateless) -// --------------------------------------------------------------------------- - -/// Envelope offset correlation finds the exact shift of a delayed copy. -#[test] -fn sync_estimate_envelope_offset() { - // candidate[k] == reference[k-1]: the candidate lags the reference by - // one envelope window, so the best lag is +1 window = +window_samples. - let reference = [0.1_f64, 0.8, 0.3, 0.6, 0.9]; - let candidate = [0.0_f64, 0.1, 0.8, 0.3, 0.6]; - let mut out = OakAudioWaveformOffset { - offset_samples: 0, - confidence: 0.0, - valid: 0, - }; - let rc = unsafe { - oakengine_audio_estimate_envelope_offset( - reference.as_ptr(), - 5, - candidate.as_ptr(), - 5, - std::ptr::null(), - 0, - std::ptr::null(), - 0, - 128, - 1, - &mut out, - ) - }; - assert_eq!(rc, 0); - assert_eq!(out.valid, 1); - assert_eq!(out.offset_samples, 128); - assert!((out.confidence - 1.0).abs() < 1e-9); - - // Explicit all-valid masks (the contract allows NULL = all valid) give - // the same result. - let ref_valid = [1u8; 5]; - let cand_valid = [1u8; 5]; - let mut out = OakAudioWaveformOffset { - offset_samples: 0, - confidence: 0.0, - valid: 0, - }; - let rc = unsafe { - oakengine_audio_estimate_envelope_offset( - reference.as_ptr(), - 5, - candidate.as_ptr(), - 5, - ref_valid.as_ptr(), - 5, - cand_valid.as_ptr(), - 5, - 128, - 1, - &mut out, - ) - }; - assert_eq!(rc, 0); - assert_eq!(out.valid, 1); - assert_eq!(out.offset_samples, 128); - - // A single constant envelope carries no correlation energy: valid=0 is - // the documented "no estimate" outcome, rc stays 0. - let flat = [0.5_f64, 0.5, 0.5, 0.5]; - let mut out = OakAudioWaveformOffset { - offset_samples: 0, - confidence: 0.0, - valid: 0, - }; - let rc = unsafe { - oakengine_audio_estimate_envelope_offset( - flat.as_ptr(), - 4, - flat.as_ptr(), - 4, - std::ptr::null(), - 0, - std::ptr::null(), - 0, - 128, - 4, - &mut out, - ) - }; - assert_eq!(rc, 0); - assert_eq!(out.valid, 0); -} - -/// Envelope offset: every NULL/zero/size/garbage argument fails cleanly. -#[test] -fn sync_estimate_envelope_offset_invalid() { - let reference = [0.1_f64, 0.8, 0.3, 0.6, 0.9]; - let mut out = OakAudioWaveformOffset { - offset_samples: 0, - confidence: 0.0, - valid: 0, - }; - - // NULL pointers are rejected at the facade (-1). - assert_eq!( - unsafe { - oakengine_audio_estimate_envelope_offset( - std::ptr::null(), - 5, - reference.as_ptr(), - 5, - std::ptr::null(), - 0, - std::ptr::null(), - 0, - 128, - 4, - &mut out, - ) - }, - OAKENGINE_E_INVALID - ); - assert_eq!( - unsafe { - oakengine_audio_estimate_envelope_offset( - reference.as_ptr(), - 5, - std::ptr::null(), - 5, - std::ptr::null(), - 0, - std::ptr::null(), - 0, - 128, - 4, - &mut out, - ) - }, - OAKENGINE_E_INVALID - ); - assert_eq!( - unsafe { - oakengine_audio_estimate_envelope_offset( - reference.as_ptr(), - 5, - reference.as_ptr(), - 5, - std::ptr::null(), - 0, - std::ptr::null(), - 0, - 128, - 4, - std::ptr::null_mut(), - ) - }, - OAKENGINE_E_INVALID - ); - - // Zero/negative lengths, zero window, negative max offset → module - // E_INVALID (-60001). - for (len, window, max_off) in [(0, 128u64, 4i64), (-1, 128, 4), (5, 0, 4), (5, 128, -1)] { - assert_eq!( - unsafe { - oakengine_audio_estimate_envelope_offset( - reference.as_ptr(), - len, - reference.as_ptr(), - len, - std::ptr::null(), - 0, - std::ptr::null(), - 0, - window, - max_off, - &mut out, - ) - }, - AUDIO_E_INVALID, - "len={len} window={window} max_off={max_off}" - ); - } -} - -/// Stretch+offset correlation: an identical candidate resolves to rate -/// 1.0 with zero offset. -#[test] -fn sync_estimate_stretch_and_offset() { - let reference = [0.1_f64, 0.8, 0.3, 0.6, 0.9]; - let mut out = OakAudioWaveformStretchOffset { - rate: 0.0, - offset_samples: 0, - confidence: 0.0, - valid: 0, - }; - let rc = unsafe { - oakengine_audio_estimate_stretch_and_offset( - reference.as_ptr(), - 5, - reference.as_ptr(), - 5, - std::ptr::null(), - 0, - std::ptr::null(), - 0, - 128, - 1, - 0.5, - 1.5, - 0.25, - &mut out, - ) - }; - assert_eq!(rc, 0); - assert_eq!(out.valid, 1); - assert_eq!(out.offset_samples, 0); - assert!((out.rate - 1.0).abs() < 1e-9); - assert!((out.confidence - 1.0).abs() < 1e-9); - - // Illegal ranges fail cleanly: NULL out (-1), bad rate bounds (-60001). - assert_eq!( - unsafe { - oakengine_audio_estimate_stretch_and_offset( - reference.as_ptr(), - 5, - reference.as_ptr(), - 5, - std::ptr::null(), - 0, - std::ptr::null(), - 0, - 128, - 4, - 0.5, - 1.5, - 0.25, - std::ptr::null_mut(), - ) - }, - OAKENGINE_E_INVALID - ); - for (min_rate, max_rate, step) in [ - (0.0, 1.5, 0.25), - (-1.0, 1.5, 0.25), - (1.5, 1.0, 0.25), - (0.5, 1.5, 0.0), - ] { - assert_eq!( - unsafe { - oakengine_audio_estimate_stretch_and_offset( - reference.as_ptr(), - 5, - reference.as_ptr(), - 5, - std::ptr::null(), - 0, - std::ptr::null(), - 0, - 128, - 4, - min_rate, - max_rate, - step, - &mut out, - ) - }, - AUDIO_E_INVALID, - "min={min_rate} max={max_rate} step={step}" - ); - } -} - -// --------------------------------------------------------------------------- -// Sync — timeline placement (stateless) -// --------------------------------------------------------------------------- - -/// `place_by_source_time`: timeline_in = reference_timeline_in + -/// (candidate.source_start + candidate.media_in) - -/// (reference.source_start + reference.media_in). -#[test] -fn sync_place_by_source_time() { - // Integers: 3 + (5 + 1) - (10 + 0) = -1. - let reference = OakAudioSyncSourceClip { - source_start_time_num: 10, - source_start_time_den: 1, - media_in_num: 0, - media_in_den: 1, - has_source_start_time: 1, - }; - let candidate = OakAudioSyncSourceClip { - source_start_time_num: 5, - source_start_time_den: 1, - media_in_num: 1, - media_in_den: 1, - has_source_start_time: 1, - }; - let mut out = OakAudioSyncPlacement { - timeline_in_num: 0, - timeline_in_den: 1, - valid: 0, - }; - let rc = unsafe { - oakengine_audio_sync_place_by_source_time(&reference, &candidate, 3, 1, &mut out) - }; - assert_eq!(rc, 0); - assert_eq!(out.timeline_in_num, -1); - assert_eq!(out.timeline_in_den, 1); - assert_eq!(out.valid, 1); - - // Rationals: 5 + (1 + 1/4) - (1/2 + 0) = 23/4. - let reference = OakAudioSyncSourceClip { - source_start_time_num: 1, - source_start_time_den: 2, - media_in_num: 0, - media_in_den: 1, - has_source_start_time: 1, - }; - let candidate = OakAudioSyncSourceClip { - source_start_time_num: 1, - source_start_time_den: 1, - media_in_num: 1, - media_in_den: 4, - has_source_start_time: 1, - }; - let mut out = OakAudioSyncPlacement { - timeline_in_num: 0, - timeline_in_den: 1, - valid: 0, - }; - let rc = unsafe { - oakengine_audio_sync_place_by_source_time(&reference, &candidate, 5, 1, &mut out) - }; - assert_eq!(rc, 0); - assert_eq!(out.timeline_in_num, 23); - assert_eq!(out.timeline_in_den, 4); - assert_eq!(out.valid, 1); - - // A clip without a source start time is documented invalid: rc 0, the - // placement is 0/0 and valid=0 (not an error). - let no_source = OakAudioSyncSourceClip { - source_start_time_num: 0, - source_start_time_den: 1, - media_in_num: 0, - media_in_den: 1, - has_source_start_time: 0, - }; - let mut out = OakAudioSyncPlacement { - timeline_in_num: 0, - timeline_in_den: 1, - valid: 0, - }; - let rc = unsafe { - oakengine_audio_sync_place_by_source_time(&no_source, &candidate, 3, 1, &mut out) - }; - assert_eq!(rc, 0); - assert_eq!(out.valid, 0); - assert_eq!(out.timeline_in_num, 0); - assert_eq!(out.timeline_in_den, 0); - - // Illegal arguments: NULL pointers → -1; zero denominators → -60001. - assert_eq!( - unsafe { - oakengine_audio_sync_place_by_source_time(std::ptr::null(), &candidate, 3, 1, &mut out) - }, - OAKENGINE_E_INVALID - ); - assert_eq!( - unsafe { - oakengine_audio_sync_place_by_source_time(&reference, std::ptr::null(), 3, 1, &mut out) - }, - OAKENGINE_E_INVALID - ); - assert_eq!( - unsafe { - oakengine_audio_sync_place_by_source_time( - &reference, - &candidate, - 3, - 1, - std::ptr::null_mut(), - ) - }, - OAKENGINE_E_INVALID - ); - let bad_den = OakAudioSyncSourceClip { - source_start_time_num: 1, - source_start_time_den: 1, - media_in_num: 0, - media_in_den: 0, // zero denominator - has_source_start_time: 1, - }; - assert_eq!( - unsafe { oakengine_audio_sync_place_by_source_time(&reference, &bad_den, 3, 1, &mut out,) }, - AUDIO_E_INVALID - ); - assert_eq!( - unsafe { - oakengine_audio_sync_place_by_source_time( - &reference, &candidate, 3, 0, // zero reference timeline denominator - &mut out, - ) - }, - AUDIO_E_INVALID - ); -} - -/// `place_by_waveform_offset`: timeline_in = reference_timeline_in + -/// candidate_offset_samples / sample_rate. -#[test] -fn sync_place_by_waveform_offset() { - let mut out = OakAudioSyncPlacement { - timeline_in_num: 0, - timeline_in_den: 1, - valid: 0, - }; - // 48000 samples at 48 kHz = 1 second. - assert_eq!( - unsafe { oakengine_audio_sync_place_by_waveform_offset(0, 1, 48000, 48000, &mut out) }, - 0 - ); - assert_eq!(out.timeline_in_num, 1); - assert_eq!(out.timeline_in_den, 1); - assert_eq!(out.valid, 1); - - // Zero offset keeps the reference timeline point (5/2 stays 5/2). - assert_eq!( - unsafe { oakengine_audio_sync_place_by_waveform_offset(5, 2, 0, 48000, &mut out) }, - 0 - ); - assert_eq!(out.timeline_in_num, 5); - assert_eq!(out.timeline_in_den, 2); - assert_eq!(out.valid, 1); - - // Negative offset: -24000 samples = -0.5 s. - assert_eq!( - unsafe { oakengine_audio_sync_place_by_waveform_offset(0, 1, -24000, 48000, &mut out) }, - 0 - ); - assert_eq!(out.timeline_in_num, -1); - assert_eq!(out.timeline_in_den, 2); - assert_eq!(out.valid, 1); - - // 1/2 + 1 s = 3/2. - assert_eq!( - unsafe { oakengine_audio_sync_place_by_waveform_offset(1, 2, 48000, 48000, &mut out) }, - 0 - ); - assert_eq!(out.timeline_in_num, 3); - assert_eq!(out.timeline_in_den, 2); - assert_eq!(out.valid, 1); - - // Illegal: NULL out → -1; zero reference denominator → -60001; - // sample_rate <= 0 is documented invalid (rc 0, valid 0). - assert_eq!( - unsafe { - oakengine_audio_sync_place_by_waveform_offset(0, 1, 0, 48000, std::ptr::null_mut()) - }, - OAKENGINE_E_INVALID - ); - assert_eq!( - unsafe { oakengine_audio_sync_place_by_waveform_offset(0, 0, 0, 48000, &mut out) }, - AUDIO_E_INVALID - ); - assert_eq!( - unsafe { oakengine_audio_sync_place_by_waveform_offset(0, 1, 0, 0, &mut out) }, - 0 - ); - assert_eq!(out.valid, 0); - assert_eq!(out.timeline_in_num, 0); - 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) -// --------------------------------------------------------------------------- - -/// Create/free round-trip, NULL/empty free, module double-free safety and -/// the alive-count leak check. -#[test] -fn processor_lifecycle_and_free_contracts() { - with_processor(|| { - let baseline = alive(); - - // free(NULL) is a no-op. - unsafe { oakengine_audio_processor_free(std::ptr::null_mut()) }; - assert_eq!(alive(), baseline); - - // free(empty handle box) is a no-op: a box wrapping CHandle::null - // 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 = crate::handle::box_handle::(CHandle::null()); - assert!(!empty_ptr.is_null()); - unsafe { oakengine_audio_processor_free(empty_ptr) }; - assert_eq!(alive(), baseline); - - // create bumps the counter; free restores it (leak check). - let p = unsafe { oakengine_audio_processor_create() }; - assert!(!p.is_null()); - assert_eq!(alive(), baseline + 1); - unsafe { oakengine_audio_processor_free(p) }; - assert_eq!(alive(), baseline); - - // The module-level free is double-free-safe (ctx is nulled after - // release); the counter decrements exactly once. - let mut h = crate::stubs::audio::oakaudio_processor_init(); - assert!(!h.ctx.is_null()); - assert_eq!(alive(), baseline + 1); - 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); - }); -} - -/// Processor open: validation order and the clean failure of the -/// environment-bound graph creation. -#[test] -fn processor_open_validation() { - with_processor(|| { - let p = unsafe { oakengine_audio_processor_create() }; - assert!(!p.is_null()); - - // NULL `to`/`from` params handles → -1 at the facade. - assert_eq!( - unsafe { oakengine_audio_processor_open(p, std::ptr::null(), std::ptr::null(), 1.0) }, - OAKENGINE_E_INVALID - ); - - // Garbage params: zero rates (mock default) → -60001; tempo <= 0 → - // -60001; non-planar output format → -60001. - let from0 = audio_params(0, 3, 4); - let to0 = audio_params(0, 3, 4); - assert_eq!( - unsafe { - oakengine_audio_processor_open(p, from0 as *const c_void, to0 as *const c_void, 1.0) - }, - AUDIO_E_INVALID - ); - common::oakcore_audioparams_free(from0); - common::oakcore_audioparams_free(to0); - - let from = audio_params(48000, 3, 4); - let to = audio_params(48000, 3, 4); - assert_eq!( - unsafe { - oakengine_audio_processor_open(p, from as *const c_void, to as *const c_void, 0.0) - }, - AUDIO_E_INVALID - ); - assert_eq!( - unsafe { - oakengine_audio_processor_open(p, from as *const c_void, to as *const c_void, -1.0) - }, - AUDIO_E_INVALID - ); - common::oakcore_audioparams_free(from); - common::oakcore_audioparams_free(to); - - // Output format must be planar f32 (4); f32 packed (10) is invalid. - let from = audio_params(48000, 3, 4); - let to_packed = audio_params(48000, 3, 10); - assert_eq!( - unsafe { - oakengine_audio_processor_open( - p, - from as *const c_void, - to_packed as *const c_void, - 1.0, - ) - }, - AUDIO_E_INVALID - ); - common::oakcore_audioparams_free(from); - common::oakcore_audioparams_free(to_packed); - - // Legal arguments reach the module's graph creation, which now runs - // a real in-process FFmpeg filter graph (ffmpeg-next): the open - // succeeds and the processor reports open; a second open is a state - // error and close shuts it down again. - let from = audio_params(48000, 3, 4); - let to = audio_params(48000, 3, 4); - assert_eq!( - unsafe { - oakengine_audio_processor_open(p, from as *const c_void, to as *const c_void, 1.0) - }, - 0 - ); - assert_eq!(unsafe { oakengine_audio_processor_is_open(p) }, 1); - assert_eq!( - unsafe { - oakengine_audio_processor_open(p, from as *const c_void, to as *const c_void, 1.0) - }, - AUDIO_E_STATE - ); - assert_eq!(unsafe { oakengine_audio_processor_close(p) }, 0); - assert_eq!(unsafe { oakengine_audio_processor_is_open(p) }, 0); - common::oakcore_audioparams_free(from); - common::oakcore_audioparams_free(to); - - unsafe { oakengine_audio_processor_free(p) }; - }); -} - -/// is_open/close on NULL, empty and closed handles. -#[test] -fn processor_is_open_and_close() { - with_processor(|| { - let p = unsafe { oakengine_audio_processor_create() }; - assert!(!p.is_null()); - - // NULL handle: is_open → 0, close → 0 (documented no-ops). - assert_eq!( - unsafe { oakengine_audio_processor_is_open(std::ptr::null_mut()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_audio_processor_close(std::ptr::null_mut()) }, - 0 - ); - - // Empty handle box: -1 (invalid) from both. - let mut empty_box = OakEngineAudioProcessor { - handle: CHandle::null(), - }; - let empty_ptr = &mut empty_box as *mut OakEngineAudioProcessor; - assert_eq!( - unsafe { oakengine_audio_processor_is_open(empty_ptr) }, - OAKENGINE_E_INVALID - ); - assert_eq!( - unsafe { oakengine_audio_processor_close(empty_ptr) }, - OAKENGINE_E_INVALID - ); - - // Fresh processor: closed. close on a closed processor is a no-op - // (0); is_open stays 0. - assert_eq!(unsafe { oakengine_audio_processor_is_open(p) }, 0); - assert_eq!(unsafe { oakengine_audio_processor_close(p) }, 0); - assert_eq!(unsafe { oakengine_audio_processor_close(p) }, 0); - assert_eq!(unsafe { oakengine_audio_processor_is_open(p) }, 0); - - unsafe { oakengine_audio_processor_free(p) }; - }); -} - -/// The two documented facade stubs: convert is "not backed" and always -/// returns E_FAILED; output_params is "not backed" and always returns -/// NULL. Both must tolerate any pointer. -#[test] -fn processor_convert_and_output_params_stubs() { - with_processor(|| { - let p = unsafe { oakengine_audio_processor_create() }; - assert!(!p.is_null()); - - let mut in_planes: [*mut f32; 1] = [std::ptr::null_mut()]; - let mut out_data: *const c_void = std::ptr::null(); - let mut out_size: c_int = 0; - - // NULL handle. - assert_eq!( - unsafe { - oakengine_audio_processor_convert( - std::ptr::null_mut(), - in_planes.as_mut_ptr(), - 0, - &mut out_data, - &mut out_size, - ) - }, - OAKENGINE_E_FAILED - ); - assert!(unsafe { oakengine_audio_processor_output_params(std::ptr::null_mut()) }.is_null()); - - // Valid handle — same documented stub result. - assert_eq!( - unsafe { - oakengine_audio_processor_convert( - p, - in_planes.as_mut_ptr(), - 0, - &mut out_data, - &mut out_size, - ) - }, - OAKENGINE_E_FAILED - ); - assert!(unsafe { oakengine_audio_processor_output_params(p) }.is_null()); - - unsafe { oakengine_audio_processor_free(p) }; - }); -} - -/// The full open→close cycle runs the module's real in-process FFmpeg -/// filter graph (ffmpeg-next), so open succeeds under `cargo test`. -/// `convert` stays the documented facade stub (`OAKENGINE_E_FAILED`; see -/// [`processor_convert_and_output_params_stubs`]) — the module-level -/// convert success path is covered by oakaudio's own processor tests. -#[test] -fn processor_full_open_convert_cycle() { - with_processor(|| { - let p = unsafe { oakengine_audio_processor_create() }; - assert!(!p.is_null()); - let from = audio_params(48000, 3, 4); - let to = audio_params(48000, 3, 4); - let rc = unsafe { - oakengine_audio_processor_open(p, from as *const c_void, to as *const c_void, 1.0) - }; - assert_eq!(rc, 0); - assert_eq!(unsafe { oakengine_audio_processor_is_open(p) }, 1); - let mut in_planes: [*mut f32; 2] = [std::ptr::null_mut(); 2]; - let mut out_data: *const c_void = std::ptr::null(); - let mut out_size: c_int = 0; - // The facade convert is the documented "not backed" stub. - assert_eq!( - unsafe { - oakengine_audio_processor_convert( - p, - in_planes.as_mut_ptr(), - 0, - &mut out_data, - &mut out_size, - ) - }, - OAKENGINE_E_FAILED - ); - assert_eq!(unsafe { oakengine_audio_processor_close(p) }, 0); - common::oakcore_audioparams_free(from); - common::oakcore_audioparams_free(to); - unsafe { oakengine_audio_processor_free(p) }; - }); -} - -// --------------------------------------------------------------------------- -// oakengine_audio_output_levels -// --------------------------------------------------------------------------- - -/// The output-level meter export: validation, the no-output case, and a -/// real peak readback over pushed F32 stereo samples. -#[test] -fn audio_output_levels() { - with_manager(|| unsafe { - // Out-arg validation (facade E_INVALID; no manager needed). - let mut peaks = [0.0f32; 4]; - assert_eq!(oakengine_audio_output_levels(std::ptr::null_mut(), 4), OAKENGINE_E_INVALID); - assert_eq!(oakengine_audio_output_levels(peaks.as_mut_ptr(), 0), OAKENGINE_E_INVALID); - assert_eq!(oakengine_audio_output_levels(peaks.as_mut_ptr(), -1), OAKENGINE_E_INVALID); - - // Fresh-ish manager, nothing buffered: 0 channels. The Rust - // singleton survives destroy (the OnceLock cannot be reset; a - // DESTROYED flag flips instead), so the output buffer from a - // previous test may still hold samples — clear it explicitly. - assert_eq!(oakengine_audio_destroy_instance(), 0); - assert_eq!(oakengine_audio_create_instance(), 0); - assert_eq!(oakengine_audio_clear_buffered_output(), 0); - assert_eq!(oakengine_audio_output_levels(peaks.as_mut_ptr(), 4), 0); - - // Push 480 frames of packed F32 stereo: left ramps to 0.25, right - // ramps to ~1.0. The levels are the per-channel linear peaks. - assert_eq!(oakengine_audio_set_output_device(42), 0); - assert_eq!(oakengine_audio_clear_buffered_output(), 0); - let frames = 480usize; - let mut samples = Vec::with_capacity(frames * 2); - for i in 0..frames { - let t = i as f32 / frames as f32; - samples.push(0.25f32 * t); - samples.push(t); - } - let params = audio_params(48000, 0x3, 10); // 10 = packed F32 - let rc = oakengine_audio_push_to_output( - params as *const c_void, - samples.as_ptr() as *const std::ffi::c_char, - (samples.len() * 4) as i64, - std::ptr::null_mut(), - 0, - ); - common::oakcore_audioparams_free(params); - assert_eq!(rc, 0); - - let n = oakengine_audio_output_levels(peaks.as_mut_ptr(), 4); - assert_eq!(n, 2); - let last = (frames - 1) as f32 / frames as f32; - assert!((peaks[0] - 0.25 * last).abs() < 1e-6, "left peak: {}", peaks[0]); - assert!((peaks[1] - last).abs() < 1e-6, "right peak: {}", peaks[1]); - - // Capacity smaller than the channel count truncates the write but - // still reports the real channel count. - let mut one = [0.0f32; 1]; - assert_eq!(oakengine_audio_output_levels(one.as_mut_ptr(), 1), 2); - }); -} diff --git a/crates/oakengine.bk/src/test_support/it_codec.rs b/crates/oakengine.bk/src/test_support/it_codec.rs deleted file mode 100644 index 18c4f8bb2..000000000 --- a/crates/oakengine.bk/src/test_support/it_codec.rs +++ /dev/null @@ -1,1407 +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 . - -//! Integration tests for the **codec family** — the facade module -//! `src/codec.rs` (contract: `engine/include/oakengine/encoding.h`, backed -//! by the oakcodec module headers `include/codec/{format,encoder}.h`). -//! -//! Every one of the 81 `oakengine_encoding_*` / `oakengine_export_*` -//! exports is exercised through real module behavior — no mocks, no -//! injected backends. Two error-code namespaces are in play: -//! -//! - The facade's own codes (`error.rs`, `-1..-6`): used when the facade -//! itself rejects the call (NULL handle, out-of-range `set_format`, ...). -//! - The wrapped module codes, which the facade passes through **untranslated** -//! (`error.rs`): `-50001`/`-50004` for the oakcodec metadata family -//! (`include/codec/error.h`) and `-60001`/`-60003` for the oakaudio -//! recording path (`include/audio/error.h`). -//! -//! String getters follow the engine's buf/size two-stage convention: the -//! return value is the string length **excluding** the NUL (negative = -//! error), and a NULL `buf` / `buf_size <= 0` only reports the length. -//! -//! Destroy contracts: `oakengine_encoding_params_destroy` is a facade-owned -//! raw box (`Box`) with **no** refcount and **no** debug alive -//! counter (unlike the oakcodec `CHandle` objects, which this family never -//! creates). NULL is a no-op; freeing the same live pointer twice is -//! use-after-free by design (the engine header transfers ownership), so the -//! tests assert NULL-idempotence rather than double-free of a live handle. - -use super::common; - -use std::ffi::{c_char, c_int}; - -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, - oakengine_encoding_format_audio_codec_count, oakengine_encoding_format_count, - oakengine_encoding_format_extension, oakengine_encoding_format_name, - oakengine_encoding_format_subtitle_codec_at, oakengine_encoding_format_subtitle_codec_count, - oakengine_encoding_format_video_codec_at, oakengine_encoding_format_video_codec_count, - oakengine_encoding_generate_matrix, oakengine_encoding_image_sequence_digit_count, - oakengine_encoding_params_audio_bit_rate, oakengine_encoding_params_audio_codec, - oakengine_encoding_params_audio_enabled, oakengine_encoding_params_color_transform_output, - oakengine_encoding_params_create, oakengine_encoding_params_destroy, - oakengine_encoding_params_disable_audio, oakengine_encoding_params_disable_subtitles, - oakengine_encoding_params_disable_video, oakengine_encoding_params_enable_audio, - oakengine_encoding_params_enable_sidecar_subtitles, oakengine_encoding_params_enable_subtitles, - oakengine_encoding_params_enable_video, oakengine_encoding_params_filename, - oakengine_encoding_params_format, oakengine_encoding_params_get_audio_params, - oakengine_encoding_params_get_custom_range, oakengine_encoding_params_get_export_length, - oakengine_encoding_params_get_last_used, oakengine_encoding_params_get_video_params, - oakengine_encoding_params_has_custom_range, oakengine_encoding_params_is_valid, - oakengine_encoding_params_load_file, oakengine_encoding_params_save_file, - oakengine_encoding_params_set_audio_bit_rate, oakengine_encoding_params_set_color_transform, - oakengine_encoding_params_set_custom_range, oakengine_encoding_params_set_export_length, - oakengine_encoding_params_set_filename, oakengine_encoding_params_set_format, - oakengine_encoding_params_set_last_used, oakengine_encoding_params_set_video_bit_rate, - oakengine_encoding_params_set_video_buffer_size, - oakengine_encoding_params_set_video_is_image_sequence, - oakengine_encoding_params_set_video_max_bit_rate, - oakengine_encoding_params_set_video_min_bit_rate, oakengine_encoding_params_set_video_option, - oakengine_encoding_params_set_video_pix_fmt, - oakengine_encoding_params_set_video_scaling_method, - oakengine_encoding_params_set_video_threads, oakengine_encoding_params_subtitles_are_sidecar, - oakengine_encoding_params_subtitles_codec, oakengine_encoding_params_subtitles_enabled, - oakengine_encoding_params_subtitles_sidecar_format, oakengine_encoding_params_video_bit_rate, - oakengine_encoding_params_video_buffer_size, oakengine_encoding_params_video_codec, - oakengine_encoding_params_video_enabled, oakengine_encoding_params_video_is_image_sequence, - oakengine_encoding_params_video_max_bit_rate, oakengine_encoding_params_video_min_bit_rate, - oakengine_encoding_params_video_option, oakengine_encoding_params_video_pix_fmt, - oakengine_encoding_params_video_scaling_method, oakengine_encoding_params_video_threads, - oakengine_encoding_pix_fmt_at, oakengine_encoding_pix_fmt_count, - oakengine_encoding_pix_fmt_index, oakengine_encoding_preset_count, - oakengine_encoding_preset_name, oakengine_encoding_preset_path, - oakengine_encoding_sample_format_at, oakengine_encoding_sample_format_count, - oakengine_encoding_start_audio_recording, oakengine_export_render_with_params, -}; -use crate::common::OakVideoParamsPod; - -/// Facade error codes (`src/error.rs`). -const E_INVALID: c_int = -1; -const E_STATE: c_int = -2; -const E_FAILED: c_int = -3; -const E_NOT_FOUND: c_int = -4; - -/// oakcodec module codes, passed through untranslated (`include/codec/error.h`). -const K_INVALID: c_int = -50001; -const K_NOT_FOUND: c_int = -50004; - -/// oakaudio module codes (`include/audio/error.h`). -const A_INVALID: c_int = -60001; -const A_FAILED: c_int = -60003; - -/// Read a NUL-terminated buffer written by a buf/size getter as a `String`. -fn read_buf(buf: &[c_char]) -> String { - let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len()); - let bytes: Vec = buf[..len].iter().map(|&c| c as u8).collect(); - String::from_utf8_lossy(&bytes).into_owned() -} - -/// A 16-element f32 transform matrix plus a close-enough comparator. -type Matrix16 = [f32; 16]; -fn assert_matrix(actual: &Matrix16, expected: &[f64; 16]) { - for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() { - assert!( - (*a as f64 - e).abs() < 1e-6, - "matrix[{i}] = {a} (expected {e})" - ); - } -} -fn identity16() -> [f64; 16] { - [ - 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, - ] -} - -// --------------------------------------------------------------------------- -// 1. Container format / codec metadata — legal input matrix -// --------------------------------------------------------------------------- - -/// Legal-path matrix over every metadata export: format enumeration, -/// per-format codec lists, codec flags, pixel/sample format lists, and the -/// image-sequence filename helpers. -#[test] -fn format_and_codec_metadata_legal() { - common::force_link(); - let mut buf = [0 as c_char; 128]; - - // Format enumeration: the table has 15 entries (0..=14; Count = 15). - let count = unsafe { oakengine_encoding_format_count() }; - assert_eq!(count, 15); - - // Every format must have a non-empty name and extension, and its - // per-format codec lists must resolve to codecs with names. - for f in 0..count { - let n = unsafe { oakengine_encoding_format_name(f, buf.as_mut_ptr(), 128) }; - assert!(n > 0, "format {f} name length"); - assert!(!read_buf(&buf).is_empty(), "format {f} name"); - - let e = unsafe { oakengine_encoding_format_extension(f, buf.as_mut_ptr(), 128) }; - assert!(e > 0, "format {f} extension length"); - assert!(!read_buf(&buf).is_empty(), "format {f} extension"); - - let vc = unsafe { oakengine_encoding_format_video_codec_count(f) }; - assert!(vc >= 0); - for i in 0..vc { - let codec = unsafe { oakengine_encoding_format_video_codec_at(f, i) }; - assert!(codec >= 0, "format {f} video codec at {i}"); - let cn = unsafe { oakengine_encoding_codec_name(codec, buf.as_mut_ptr(), 128) }; - assert!(cn > 0, "codec {codec} name length"); - assert!(!read_buf(&buf).is_empty()); - } - let ac = unsafe { oakengine_encoding_format_audio_codec_count(f) }; - assert!(ac >= 0); - for i in 0..ac { - let codec = unsafe { oakengine_encoding_format_audio_codec_at(f, i) }; - assert!(codec >= 0, "format {f} audio codec at {i}"); - let cn = unsafe { oakengine_encoding_codec_name(codec, buf.as_mut_ptr(), 128) }; - assert!(cn > 0, "codec {codec} name length"); - assert!(!read_buf(&buf).is_empty()); - } - let sc = unsafe { oakengine_encoding_format_subtitle_codec_count(f) }; - assert!(sc >= 0); - for i in 0..sc { - let codec = unsafe { oakengine_encoding_format_subtitle_codec_at(f, i) }; - assert!(codec >= 0, "format {f} subtitle codec at {i}"); - let cn = unsafe { oakengine_encoding_codec_name(codec, buf.as_mut_ptr(), 128) }; - assert!(cn > 0, "codec {codec} name length"); - assert!(!read_buf(&buf).is_empty()); - } - } - - // Exact values for the named formats (exportformat.rs / exportcodec.rs). - // Matroska (1). - assert_eq!( - unsafe { oakengine_encoding_format_name(1, buf.as_mut_ptr(), 128) }, - 14 - ); - assert_eq!(read_buf(&buf), "Matroska Video"); - assert_eq!( - unsafe { oakengine_encoding_format_extension(1, buf.as_mut_ptr(), 128) }, - 3 - ); - assert_eq!(read_buf(&buf), "mkv"); - // MPEG-4 video (2): H.264 / H.264RGB / H.265. - assert_eq!(unsafe { oakengine_encoding_format_video_codec_count(2) }, 3); - assert_eq!(unsafe { oakengine_encoding_format_video_codec_at(2, 0) }, 1); // H.264 - // WAV (7): no video codecs, PCM (13) audio. - assert_eq!(unsafe { oakengine_encoding_format_video_codec_count(7) }, 0); - assert_eq!(unsafe { oakengine_encoding_format_audio_codec_count(7) }, 1); - assert_eq!( - unsafe { oakengine_encoding_format_audio_codec_at(7, 0) }, - 13 - ); // PCM - // SRT (13): subtitle-only, SRT (17) codec. - assert_eq!( - unsafe { oakengine_encoding_format_audio_codec_count(13) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_format_subtitle_codec_count(13) }, - 1 - ); - assert_eq!( - unsafe { oakengine_encoding_format_subtitle_codec_at(13, 0) }, - 17 - ); // SRT - - // Codec metadata: names, still-image, lossless. - assert_eq!( - unsafe { oakengine_encoding_codec_name(1, buf.as_mut_ptr(), 128) }, - 5 - ); - assert_eq!(read_buf(&buf), "H.264"); - assert_eq!(unsafe { oakengine_encoding_codec_is_still_image(5) }, 1); // PNG - assert_eq!(unsafe { oakengine_encoding_codec_is_still_image(1) }, 0); // H.264 - assert_eq!(unsafe { oakengine_encoding_codec_is_lossless(13) }, 1); // PCM - assert_eq!(unsafe { oakengine_encoding_codec_is_lossless(12) }, 0); // AAC - - // Pixel formats: the Rust table is empty (CPP-PARITY interim, the list - // is queried from the format's FFmpeg/OIIO encoder), so the count is 0 - // and every index is E_NOT_FOUND; the index helper falls back to 0. - assert_eq!(unsafe { oakengine_encoding_pix_fmt_count(2, 1) }, 0); - assert_eq!( - unsafe { oakengine_encoding_pix_fmt_at(2, 1, 0, buf.as_mut_ptr(), 128) }, - K_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_encoding_pix_fmt_index(1, c"yuv420p".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_pix_fmt_index(1, std::ptr::null()) }, - 0 - ); - - // Sample formats: PCM (13) inside WAV (7) exposes its native list. - assert_eq!(unsafe { oakengine_encoding_sample_format_count(7, 13) }, 6); - assert_eq!(unsafe { oakengine_encoding_sample_format_at(7, 13, 4) }, 10); // f32 packed - for i in 0..6 { - assert!(unsafe { oakengine_encoding_sample_format_at(7, 13, i) } >= 0); - } - // AAC (12) has no Rust sample-format table yet -> 0. - assert_eq!(unsafe { oakengine_encoding_sample_format_count(2, 12) }, 0); - - // Image-sequence filename helpers. - assert_eq!( - unsafe { - oakengine_encoding_filename_contains_digit_placeholder(c"/tmp/out_[#####].png".as_ptr()) - }, - 1 - ); - assert_eq!( - unsafe { oakengine_encoding_filename_contains_digit_placeholder(c"/tmp/out.png".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_image_sequence_digit_count(c"/tmp/out_[#####].png".as_ptr()) }, - 5 - ); - assert_eq!( - unsafe { oakengine_encoding_image_sequence_digit_count(c"/tmp/out.png".as_ptr()) }, - 0 - ); - // Placeholder + preceding separator are removed together. - let len = unsafe { - oakengine_encoding_filename_remove_digit_placeholder( - c"/tmp/out_[#####].png".as_ptr(), - buf.as_mut_ptr(), - 128, - ) - }; - assert_eq!(len, 12); // "/tmp/out.png" - assert_eq!(read_buf(&buf), "/tmp/out.png"); - let len = unsafe { - oakengine_encoding_filename_remove_digit_placeholder( - c"img_[#####].png".as_ptr(), - buf.as_mut_ptr(), - 128, - ) - }; - assert_eq!(len, 7); // "img.png" (the "_" separator goes with the placeholder) - assert_eq!(read_buf(&buf), "img.png"); -} - -// --------------------------------------------------------------------------- -// 2. Metadata — illegal-input robustness -// --------------------------------------------------------------------------- - -/// Plugins may pass anything: out-of-range formats/codecs/indices, garbage -/// enums, NULL strings and degenerate buffer sizes. Every case must yield a -/// clean negative code (or the documented 0/fallback), never a crash. -#[test] -fn metadata_illegal_inputs() { - let mut buf = [0 as c_char; 128]; - - // Out-of-range / garbage format -> K_INVALID on the two-stage getters. - assert_eq!( - unsafe { oakengine_encoding_format_name(-1, buf.as_mut_ptr(), 128) }, - K_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_format_name(15, buf.as_mut_ptr(), 128) }, - K_INVALID - ); // Count is not a format - assert_eq!( - unsafe { oakengine_encoding_format_name(99, buf.as_mut_ptr(), 128) }, - K_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_format_extension(-5, buf.as_mut_ptr(), 128) }, - K_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_format_video_codec_count(-1) }, - K_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_format_video_codec_count(99) }, - K_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_format_audio_codec_count(-2) }, - K_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_format_subtitle_codec_count(42) }, - K_INVALID - ); - - // Out-of-range indices -> K_NOT_FOUND (valid format checked first). - assert_eq!( - unsafe { oakengine_encoding_format_video_codec_at(2, -1) }, - K_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_encoding_format_video_codec_at(2, 3) }, - K_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_encoding_format_audio_codec_at(7, 1) }, - K_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_encoding_format_subtitle_codec_at(13, 1) }, - K_NOT_FOUND - ); - // Invalid format wins over the index check. - assert_eq!( - unsafe { oakengine_encoding_format_video_codec_at(99, 0) }, - K_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_format_audio_codec_at(-1, 0) }, - K_INVALID - ); - // WAV (7) is a valid format with an empty subtitle-codec list: the - // out-of-range index yields K_NOT_FOUND; an invalid format wins over - // the index check. - assert_eq!( - unsafe { oakengine_encoding_format_subtitle_codec_at(7, 0) }, - K_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_encoding_format_subtitle_codec_at(99, 0) }, - K_INVALID - ); - - // Garbage codec values: name -> K_INVALID, flags -> documented 0. - assert_eq!( - unsafe { oakengine_encoding_codec_name(-1, buf.as_mut_ptr(), 128) }, - K_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_codec_name(99, buf.as_mut_ptr(), 128) }, - K_INVALID - ); - assert_eq!(unsafe { oakengine_encoding_codec_is_still_image(99) }, 0); - assert_eq!(unsafe { oakengine_encoding_codec_is_still_image(-3) }, 0); - assert_eq!(unsafe { oakengine_encoding_codec_is_lossless(99) }, 0); - assert_eq!(unsafe { oakengine_encoding_codec_is_lossless(-3) }, 0); - - // Pixel/sample format queries: garbage format or codec -> K_INVALID. - assert_eq!( - unsafe { oakengine_encoding_pix_fmt_count(-1, 1) }, - K_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_pix_fmt_count(2, 99) }, - K_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_pix_fmt_count(15, 1) }, - K_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_pix_fmt_at(-1, 1, 0, buf.as_mut_ptr(), 128) }, - K_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_pix_fmt_at(2, 99, 0, buf.as_mut_ptr(), 128) }, - K_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_pix_fmt_at(2, 1, -1, buf.as_mut_ptr(), 128) }, - K_NOT_FOUND - ); - // pix_fmt_index: NULL / empty / unknown / garbage codec -> 0. - assert_eq!( - unsafe { oakengine_encoding_pix_fmt_index(1, std::ptr::null()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_pix_fmt_index(99, c"yuv420p".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_pix_fmt_index(1, c"not-a-real-fmt".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_sample_format_count(-1, 13) }, - K_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_sample_format_count(7, 99) }, - K_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_sample_format_at(-1, 13, 0) }, - K_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_sample_format_at(7, 99, 0) }, - K_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_sample_format_at(7, 13, -1) }, - K_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_encoding_sample_format_at(7, 13, 6) }, - K_NOT_FOUND - ); // count is 6 - - // NULL / degenerate buffers on two-stage getters: length-only, never a - // crash. The return value stays the string length. - assert_eq!( - unsafe { oakengine_encoding_format_name(1, std::ptr::null_mut(), 0) }, - 14 - ); - assert_eq!( - unsafe { oakengine_encoding_format_name(1, std::ptr::null_mut(), -1) }, - 14 - ); - assert_eq!( - unsafe { oakengine_encoding_format_extension(1, buf.as_mut_ptr(), 0) }, - 3 - ); - assert_eq!( - unsafe { oakengine_encoding_format_extension(1, buf.as_mut_ptr(), -4) }, - 3 - ); - // Truncation: buf_size = 4 writes 3 chars + NUL, required size unchanged. - assert_eq!( - unsafe { oakengine_encoding_format_name(1, buf.as_mut_ptr(), 4) }, - 14 - ); - assert_eq!(read_buf(&buf), "Mat"); - assert_eq!( - unsafe { oakengine_encoding_codec_name(1, std::ptr::null_mut(), 0) }, - 5 - ); - - // NULL filename helpers: documented fallbacks, no crash. - assert_eq!( - unsafe { oakengine_encoding_filename_contains_digit_placeholder(std::ptr::null()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_image_sequence_digit_count(std::ptr::null()) }, - 0 - ); - assert_eq!( - unsafe { - oakengine_encoding_filename_remove_digit_placeholder( - std::ptr::null(), - buf.as_mut_ptr(), - 128, - ) - }, - K_INVALID - ); - assert_eq!( - unsafe { - oakengine_encoding_filename_remove_digit_placeholder( - c"img.png".as_ptr(), - std::ptr::null_mut(), - 0, - ) - }, - 7 - ); // "img.png" unchanged, length-only -} - -// --------------------------------------------------------------------------- -// 3. Transform matrix (`oakengine_encoding_generate_matrix`) -// --------------------------------------------------------------------------- - -/// Legal methods, garbage method and degenerate sizes; NULL output. -#[test] -fn generate_matrix_matrix() { - let mut m: Matrix16 = [0.0; 16]; - - // Stretch (1) is the identity. - assert_eq!( - unsafe { oakengine_encoding_generate_matrix(1, 1920, 1080, 1280, 720, m.as_mut_ptr()) }, - 0 - ); - assert_matrix(&m, &identity16()); - - // Fit (0): square source into a 2:1 destination scales x by 0.5. - assert_eq!( - unsafe { oakengine_encoding_generate_matrix(0, 1000, 1000, 2000, 1000, m.as_mut_ptr()) }, - 0 - ); - assert_matrix( - &m, - &[ - 0.5, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, - ], - ); - - // Crop (2) on the same geometry scales y by 2.0. - assert_eq!( - unsafe { oakengine_encoding_generate_matrix(2, 1000, 1000, 2000, 1000, m.as_mut_ptr()) }, - 0 - ); - assert_matrix( - &m, - &[ - 1.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, - ], - ); - - // Same aspect ratio -> identity for every method. - for method in 0..3 { - assert_eq!( - unsafe { - oakengine_encoding_generate_matrix(method, 1920, 1080, 960, 540, m.as_mut_ptr()) - }, - 0 - ); - assert_matrix(&m, &identity16()); - } - - // Garbage method -> mapped to Stretch -> identity, still OK. - assert_eq!( - unsafe { oakengine_encoding_generate_matrix(99, 1000, 1000, 2000, 1000, m.as_mut_ptr()) }, - 0 - ); - assert_matrix(&m, &identity16()); - assert_eq!( - unsafe { oakengine_encoding_generate_matrix(-7, 1000, 1000, 2000, 1000, m.as_mut_ptr()) }, - 0 - ); - assert_matrix(&m, &identity16()); - - // Degenerate sizes (zero / negative) -> identity, still OK. - for (sw, sh, dw, dh) in [(0, 0, 100, 100), (100, 100, 0, 0), (-4, 10, 100, 100)] { - assert_eq!( - unsafe { oakengine_encoding_generate_matrix(0, sw, sh, dw, dh, m.as_mut_ptr()) }, - 0 - ); - assert_matrix(&m, &identity16()); - } - - // NULL output -> facade E_INVALID. - assert_eq!( - unsafe { oakengine_encoding_generate_matrix(0, 1, 1, 1, 1, std::ptr::null_mut()) }, - E_INVALID - ); -} - -// --------------------------------------------------------------------------- -// 4. Encoding-params handle — legal lifecycle round trip -// --------------------------------------------------------------------------- - -/// Create, configure every field, read everything back, destroy. The -/// handle is per-call state, so the whole lifecycle runs in one test. -#[test] -fn params_handle_legal_round_trip() { - let p = unsafe { oakengine_encoding_params_create() }; - assert!(!p.is_null()); - let mut buf = [0 as c_char; 64]; - - // Fresh handle: no tracks enabled, format unset (-1), invalid. - assert_eq!(unsafe { oakengine_encoding_params_is_valid(p) }, 0); - assert_eq!(unsafe { oakengine_encoding_params_format(p) }, -1); - assert_eq!(unsafe { oakengine_encoding_params_video_enabled(p) }, 0); - assert_eq!(unsafe { oakengine_encoding_params_audio_enabled(p) }, 0); - assert_eq!(unsafe { oakengine_encoding_params_subtitles_enabled(p) }, 0); - assert_eq!( - unsafe { oakengine_encoding_params_subtitles_are_sidecar(p) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_params_subtitles_sidecar_format(p) }, - 0 - ); - assert_eq!(unsafe { oakengine_encoding_params_subtitles_codec(p) }, 0); - assert_eq!(unsafe { oakengine_encoding_params_video_threads(p) }, 0); - assert_eq!( - unsafe { oakengine_encoding_params_video_is_image_sequence(p) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_params_video_scaling_method(p) }, - 0 - ); - assert_eq!(unsafe { oakengine_encoding_params_video_bit_rate(p) }, 0); - assert_eq!( - unsafe { oakengine_encoding_params_video_min_bit_rate(p) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_params_video_max_bit_rate(p) }, - 0 - ); - assert_eq!(unsafe { oakengine_encoding_params_video_buffer_size(p) }, 0); - assert_eq!(unsafe { oakengine_encoding_params_audio_bit_rate(p) }, 0); - assert_eq!(unsafe { oakengine_encoding_params_has_custom_range(p) }, 0); - - // Format: set + read back; Matroska = 1. - assert_eq!(unsafe { oakengine_encoding_params_set_format(p, 1) }, 0); - assert_eq!(unsafe { oakengine_encoding_params_format(p) }, 1); - - // Filename round trip. - assert_eq!( - unsafe { oakengine_encoding_params_set_filename(p, c"out.mkv".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_params_filename(p, buf.as_mut_ptr(), 64) }, - 7 - ); - assert_eq!(read_buf(&buf), "out.mkv"); - - // Video: enable with a real video-params POD, then read everything back. - let mut vp: OakVideoParamsPod = unsafe { std::mem::zeroed() }; - assert_eq!( - unsafe { - crate::common::oakengine_video_params_make( - &mut vp, 1920, 1080, 1001, 30000, 4, 1, 1, 0, 1, 1, - ) - }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_params_enable_video(p, &vp, 1) }, - 0 - ); - assert_eq!(unsafe { oakengine_encoding_params_is_valid(p) }, 1); - assert_eq!(unsafe { oakengine_encoding_params_video_enabled(p) }, 1); - assert_eq!(unsafe { oakengine_encoding_params_video_codec(p) }, 1); - let mut out: OakVideoParamsPod = unsafe { std::mem::zeroed() }; - assert_eq!( - unsafe { oakengine_encoding_params_get_video_params(p, &mut out) }, - 0 - ); - assert_eq!((out.width, out.height), (1920, 1080)); - assert_eq!((out.time_base_num, out.time_base_den), (1001, 30000)); - assert_eq!(out.format, 4); - assert_eq!(out.interlacing, 0); // interlacing arg 0 in the make() call - assert_eq!((out.pixel_aspect_num, out.pixel_aspect_den), (1, 1)); - - // Video bit-rate family (i64 fields). - unsafe { oakengine_encoding_params_set_video_bit_rate(p, 8_000_000) }; - unsafe { oakengine_encoding_params_set_video_min_bit_rate(p, 4_000_000) }; - unsafe { oakengine_encoding_params_set_video_max_bit_rate(p, 12_000_000) }; - unsafe { oakengine_encoding_params_set_video_buffer_size(p, 16_000_000) }; - assert_eq!( - unsafe { oakengine_encoding_params_video_bit_rate(p) }, - 8_000_000 - ); - assert_eq!( - unsafe { oakengine_encoding_params_video_min_bit_rate(p) }, - 4_000_000 - ); - assert_eq!( - unsafe { oakengine_encoding_params_video_max_bit_rate(p) }, - 12_000_000 - ); - assert_eq!( - unsafe { oakengine_encoding_params_video_buffer_size(p) }, - 16_000_000 - ); - - // Threads, encoded pixel format, image-sequence flag, scaling method. - unsafe { oakengine_encoding_params_set_video_threads(p, 4) }; - assert_eq!(unsafe { oakengine_encoding_params_video_threads(p) }, 4); - assert_eq!( - unsafe { oakengine_encoding_params_set_video_pix_fmt(p, c"yuv420p".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_params_video_pix_fmt(p, buf.as_mut_ptr(), 64) }, - 7 - ); - assert_eq!(read_buf(&buf), "yuv420p"); - unsafe { oakengine_encoding_params_set_video_is_image_sequence(p, 1) }; - assert_eq!( - unsafe { oakengine_encoding_params_video_is_image_sequence(p) }, - 1 - ); - assert_eq!( - unsafe { oakengine_encoding_params_set_video_scaling_method(p, 2) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_params_video_scaling_method(p) }, - 2 - ); - - // Audio: enable + read back (get_audio_params before enabling is E_STATE, - // asserted in the illegal test). - assert_eq!( - unsafe { oakengine_encoding_params_enable_audio(p, 48000, 3, 0, 13) }, - 0 - ); - assert_eq!(unsafe { oakengine_encoding_params_audio_enabled(p) }, 1); - assert_eq!(unsafe { oakengine_encoding_params_audio_codec(p) }, 13); - let (mut sr, mut layout, mut sf) = (0 as c_int, 0u64, 0 as c_int); - assert_eq!( - unsafe { oakengine_encoding_params_get_audio_params(p, &mut sr, &mut layout, &mut sf) }, - 0 - ); - assert_eq!((sr, layout, sf), (48000, 3, 0)); - unsafe { oakengine_encoding_params_set_audio_bit_rate(p, 320_000) }; - assert_eq!( - unsafe { oakengine_encoding_params_audio_bit_rate(p) }, - 320_000 - ); - - // Subtitles: plain and sidecar variants. - assert_eq!( - unsafe { oakengine_encoding_params_enable_subtitles(p, 17) }, - 0 - ); - assert_eq!(unsafe { oakengine_encoding_params_subtitles_enabled(p) }, 1); - assert_eq!(unsafe { oakengine_encoding_params_subtitles_codec(p) }, 17); - assert_eq!( - unsafe { oakengine_encoding_params_subtitles_are_sidecar(p) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_params_enable_sidecar_subtitles(p, 13, 17) }, - 0 - ); - assert_eq!(unsafe { oakengine_encoding_params_subtitles_enabled(p) }, 1); - assert_eq!( - unsafe { oakengine_encoding_params_subtitles_are_sidecar(p) }, - 1 - ); - assert_eq!( - unsafe { oakengine_encoding_params_subtitles_sidecar_format(p) }, - 13 - ); - assert_eq!(unsafe { oakengine_encoding_params_subtitles_codec(p) }, 17); - - // Color transform. - assert_eq!( - unsafe { oakengine_encoding_params_set_color_transform(p, c"ACEScg".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_params_color_transform_output(p, buf.as_mut_ptr(), 64) }, - 6 - ); - assert_eq!(read_buf(&buf), "ACEScg"); - - // Export length. - let (mut eln, mut eld) = (0 as c_int, 0 as c_int); - assert_eq!( - unsafe { oakengine_encoding_params_get_export_length(p, &mut eln, &mut eld) }, - 0 - ); - assert_eq!((eln, eld), (0, 0)); // default - unsafe { oakengine_encoding_params_set_export_length(p, 10, 1) }; - assert_eq!( - unsafe { oakengine_encoding_params_get_export_length(p, &mut eln, &mut eld) }, - 0 - ); - assert_eq!((eln, eld), (10, 1)); - - // Custom range. - let (mut inn, mut ind, mut outn, mut outd) = (0i64, 0i64, 0i64, 0i64); - unsafe { oakengine_encoding_params_set_custom_range(p, 0, 1, 100, 1) }; - assert_eq!(unsafe { oakengine_encoding_params_has_custom_range(p) }, 1); - assert_eq!( - unsafe { - oakengine_encoding_params_get_custom_range(p, &mut inn, &mut ind, &mut outn, &mut outd) - }, - 0 - ); - assert_eq!((inn, ind, outn, outd), (0, 1, 100, 1)); - - // Encoder-specific video options (facade-side map). - assert_eq!( - unsafe { oakengine_encoding_params_set_video_option(p, c"crf".as_ptr(), c"18".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_params_video_option(p, c"crf".as_ptr(), buf.as_mut_ptr(), 64) }, - 2 - ); - assert_eq!(read_buf(&buf), "18"); - // A second value for the same key replaces the first. - assert_eq!( - unsafe { oakengine_encoding_params_set_video_option(p, c"crf".as_ptr(), c"23".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_params_video_option(p, c"crf".as_ptr(), buf.as_mut_ptr(), 64) }, - 2 - ); - assert_eq!(read_buf(&buf), "23"); - - // Disable each track and watch is_valid flip back to 0. - unsafe { oakengine_encoding_params_disable_video(p) }; - assert_eq!(unsafe { oakengine_encoding_params_video_enabled(p) }, 0); - assert_eq!(unsafe { oakengine_encoding_params_is_valid(p) }, 1); // audio still on - unsafe { oakengine_encoding_params_disable_audio(p) }; - assert_eq!(unsafe { oakengine_encoding_params_audio_enabled(p) }, 0); - assert_eq!(unsafe { oakengine_encoding_params_is_valid(p) }, 1); // subtitles still on - unsafe { oakengine_encoding_params_disable_subtitles(p) }; - assert_eq!(unsafe { oakengine_encoding_params_subtitles_enabled(p) }, 0); - assert_eq!(unsafe { oakengine_encoding_params_is_valid(p) }, 0); - - unsafe { oakengine_encoding_params_destroy(p) }; -} - -// --------------------------------------------------------------------------- -// 5. Encoding-params handle — illegal-input robustness -// --------------------------------------------------------------------------- - -/// NULL handles, NULL string arguments, out-of-range values, state errors -/// and garbage enums: clean negative codes or documented no-ops only. -#[test] -fn params_handle_illegal_inputs() { - let p = unsafe { oakengine_encoding_params_create() }; - assert!(!p.is_null()); - let mut buf = [0 as c_char; 64]; - - // NULL handle on every c_int-returning getter/setter -> facade E_INVALID. - assert_eq!( - unsafe { oakengine_encoding_params_is_valid(std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_format(std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_set_format(std::ptr::null_mut(), 1) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_filename(std::ptr::null(), buf.as_mut_ptr(), 64) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_set_filename(std::ptr::null_mut(), c"x.mkv".as_ptr()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_enable_video(std::ptr::null_mut(), &vp_uninit(), 1) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_enable_audio(std::ptr::null_mut(), 48000, 3, 0, 13) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_enable_subtitles(std::ptr::null_mut(), 17) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_enable_sidecar_subtitles(std::ptr::null_mut(), 13, 17) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_video_enabled(std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_video_codec(std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_get_video_params(std::ptr::null(), &mut vp_uninit()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_audio_enabled(std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_audio_codec(std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_encoding_params_get_audio_params( - std::ptr::null(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_subtitles_enabled(std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_subtitles_are_sidecar(std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_subtitles_sidecar_format(std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_subtitles_codec(std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_video_bit_rate(std::ptr::null()) }, - E_INVALID as i64 - ); - assert_eq!( - unsafe { oakengine_encoding_params_video_min_bit_rate(std::ptr::null()) }, - E_INVALID as i64 - ); - assert_eq!( - unsafe { oakengine_encoding_params_video_max_bit_rate(std::ptr::null()) }, - E_INVALID as i64 - ); - assert_eq!( - unsafe { oakengine_encoding_params_video_buffer_size(std::ptr::null()) }, - E_INVALID as i64 - ); - assert_eq!( - unsafe { oakengine_encoding_params_audio_bit_rate(std::ptr::null()) }, - E_INVALID as i64 - ); - assert_eq!( - unsafe { oakengine_encoding_params_video_threads(std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_video_pix_fmt(std::ptr::null(), buf.as_mut_ptr(), 64) }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_encoding_params_set_video_pix_fmt(std::ptr::null_mut(), c"yuv420p".as_ptr()) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_video_is_image_sequence(std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_encoding_params_color_transform_output(std::ptr::null(), buf.as_mut_ptr(), 64) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_encoding_params_set_color_transform(std::ptr::null_mut(), c"ACEScg".as_ptr()) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_encoding_params_get_export_length( - std::ptr::null(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_has_custom_range(std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_encoding_params_get_custom_range( - std::ptr::null(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_video_scaling_method(std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_set_video_scaling_method(std::ptr::null_mut(), 0) }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_encoding_params_set_video_option( - std::ptr::null_mut(), - c"crf".as_ptr(), - c"18".as_ptr(), - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_encoding_params_video_option( - std::ptr::null(), - c"crf".as_ptr(), - buf.as_mut_ptr(), - 64, - ) - }, - E_INVALID - ); - - // NULL string arguments. - assert_eq!( - unsafe { oakengine_encoding_params_set_filename(p, std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_set_video_pix_fmt(p, std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_set_video_option(p, std::ptr::null(), c"18".as_ptr()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_set_video_option(p, c"crf".as_ptr(), std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_encoding_params_video_option(p, std::ptr::null(), buf.as_mut_ptr(), 64) - }, - E_INVALID - ); - // NULL video-params POD on enable_video. - assert_eq!( - unsafe { oakengine_encoding_params_enable_video(p, std::ptr::null(), 1) }, - E_INVALID - ); - // set_color_transform tolerates NULL (writes the empty string). - assert_eq!( - unsafe { oakengine_encoding_params_set_color_transform(p, std::ptr::null()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_params_color_transform_output(p, buf.as_mut_ptr(), 64) }, - 0 - ); - assert_eq!(read_buf(&buf), ""); - - // Out-of-range / garbage values. - assert_eq!( - unsafe { oakengine_encoding_params_set_format(p, -1) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_encoding_params_set_format(p, 15) }, - E_INVALID - ); // count - assert_eq!( - unsafe { oakengine_encoding_params_set_format(p, 9999) }, - E_INVALID - ); - // Missing video option key -> E_NOT_FOUND. - assert_eq!( - unsafe { - oakengine_encoding_params_video_option(p, c"missing".as_ptr(), buf.as_mut_ptr(), 64) - }, - E_NOT_FOUND - ); - - // State errors: disabled tracks make the getters return E_STATE. - assert_eq!( - unsafe { oakengine_encoding_params_get_video_params(p, &mut vp_uninit()) }, - E_STATE - ); - assert_eq!( - unsafe { oakengine_encoding_params_get_video_params(p, std::ptr::null_mut()) }, - E_STATE - ); // disabled state checked before NULL out - assert_eq!( - unsafe { - oakengine_encoding_params_get_audio_params( - p, - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - E_STATE - ); - // Unset custom range -> E_NOT_FOUND. - assert_eq!( - unsafe { - oakengine_encoding_params_get_custom_range( - p, - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - E_NOT_FOUND - ); - - // Enabled video with a NULL out -> E_INVALID (state now OK). - assert_eq!( - unsafe { oakengine_encoding_params_enable_video(p, &vp_uninit(), 1) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_params_get_video_params(p, std::ptr::null_mut()) }, - E_INVALID - ); - // Garbage enums are accepted verbatim (no validation in the facade): - // a codec id of 9999 round-trips. - assert_eq!( - unsafe { oakengine_encoding_params_enable_video(p, &vp_uninit(), 9999) }, - 0 - ); - assert_eq!(unsafe { oakengine_encoding_params_video_codec(p) }, 9999); - // Audio with a zero rate / empty layout / garbage format/codec: accepted. - assert_eq!( - unsafe { oakengine_encoding_params_enable_audio(p, 0, 0, -1, 9999) }, - 0 - ); - let (mut sr, mut layout, mut sf) = (0 as c_int, 0u64, 0 as c_int); - assert_eq!( - unsafe { oakengine_encoding_params_get_audio_params(p, &mut sr, &mut layout, &mut sf) }, - 0 - ); - assert_eq!((sr, layout, sf), (0, 0, -1)); - // Garbage scaling method round-trips too. - assert_eq!( - unsafe { oakengine_encoding_params_set_video_scaling_method(p, 99) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_params_video_scaling_method(p) }, - 99 - ); - - // NULL-handle void setters are no-ops (never crash). - unsafe { oakengine_encoding_params_disable_video(std::ptr::null_mut()) }; - unsafe { oakengine_encoding_params_disable_audio(std::ptr::null_mut()) }; - unsafe { oakengine_encoding_params_disable_subtitles(std::ptr::null_mut()) }; - unsafe { oakengine_encoding_params_set_video_bit_rate(std::ptr::null_mut(), 1) }; - unsafe { oakengine_encoding_params_set_video_min_bit_rate(std::ptr::null_mut(), 1) }; - unsafe { oakengine_encoding_params_set_video_max_bit_rate(std::ptr::null_mut(), 1) }; - unsafe { oakengine_encoding_params_set_video_buffer_size(std::ptr::null_mut(), 1) }; - unsafe { oakengine_encoding_params_set_audio_bit_rate(std::ptr::null_mut(), 1) }; - unsafe { oakengine_encoding_params_set_video_threads(std::ptr::null_mut(), 4) }; - unsafe { oakengine_encoding_params_set_video_is_image_sequence(std::ptr::null_mut(), 1) }; - unsafe { oakengine_encoding_params_set_export_length(std::ptr::null_mut(), 10, 1) }; - unsafe { oakengine_encoding_params_set_custom_range(std::ptr::null_mut(), 0, 1, 100, 1) }; - // Length-only queries tolerate a NULL / zero-sized buffer. - assert_eq!( - unsafe { oakengine_encoding_params_filename(p, std::ptr::null_mut(), 0) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_params_video_pix_fmt(p, std::ptr::null_mut(), -1) }, - 0 - ); - assert_eq!( - unsafe { - oakengine_encoding_params_video_option(p, c"crf".as_ptr(), std::ptr::null_mut(), 0) - }, - E_NOT_FOUND - ); // key unset on this handle - - unsafe { oakengine_encoding_params_destroy(p) }; -} - -/// Fresh (all-zero) video-params POD used for validation-negative calls. -fn vp_uninit() -> OakVideoParamsPod { - unsafe { std::mem::zeroed() } -} - -// --------------------------------------------------------------------------- -// 6. Encoding-params handle — destroy contracts -// --------------------------------------------------------------------------- - -/// `oakengine_encoding_params_destroy`: NULL is a no-op (repeatedly), and a -/// live handle frees cleanly. The family keeps no debug alive counter (the -/// handle is a facade-owned raw box, not a refcounted oakcodec handle), so -/// the baseline is verified behaviorally. Double-freeing a live pointer is -/// use-after-free by design (the engine header transfers ownership) and is -/// deliberately not invoked. -#[test] -fn params_destroy_contracts() { - unsafe { oakengine_encoding_params_destroy(std::ptr::null_mut()) }; - unsafe { oakengine_encoding_params_destroy(std::ptr::null_mut()) }; - - let p = unsafe { oakengine_encoding_params_create() }; - assert!(!p.is_null()); - // The handle still works right up to the destroy. - assert_eq!(unsafe { oakengine_encoding_params_set_format(p, 1) }, 0); - unsafe { oakengine_encoding_params_destroy(p) }; - - // NULL remains a no-op after a real destroy. - unsafe { oakengine_encoding_params_destroy(std::ptr::null_mut()) }; -} - -// --------------------------------------------------------------------------- -// 7. Deferred / not-backed entry points -// --------------------------------------------------------------------------- - -/// 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]; - - assert_eq!( - unsafe { oakengine_encoding_preset_path(buf.as_mut_ptr(), 64) }, - E_FAILED - ); - assert_eq!( - unsafe { oakengine_encoding_preset_path(std::ptr::null_mut(), 0) }, - E_FAILED - ); - assert_eq!(unsafe { oakengine_encoding_preset_count() }, 0); - assert_eq!( - unsafe { oakengine_encoding_preset_name(0, buf.as_mut_ptr(), 64) }, - E_FAILED - ); - assert_eq!( - unsafe { oakengine_encoding_preset_name(99, std::ptr::null_mut(), 0) }, - E_FAILED - ); - - let p = unsafe { oakengine_encoding_params_create() }; - assert!(!p.is_null()); - assert_eq!( - unsafe { oakengine_encoding_params_load_file(p, c"preset.oep".as_ptr()) }, - E_FAILED - ); - assert_eq!( - unsafe { oakengine_encoding_params_load_file(std::ptr::null_mut(), c"p.oep".as_ptr()) }, - E_FAILED - ); - assert_eq!( - unsafe { oakengine_encoding_params_save_file(p, c"preset.oep".as_ptr()) }, - E_FAILED - ); - assert_eq!( - 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_INVALID - ); - assert_eq!( - unsafe { oakengine_export_render_with_params(std::ptr::null_mut(), std::ptr::null()) }, - E_INVALID - ); - - assert!(unsafe { oakengine_encoding_params_get_last_used(std::ptr::null_mut()) }.is_null()); - assert!(unsafe { oakengine_encoding_params_get_last_used(std::ptr::null_mut()) }.is_null()); - unsafe { oakengine_encoding_params_set_last_used(std::ptr::null_mut(), p) }; - unsafe { oakengine_encoding_params_set_last_used(std::ptr::null_mut(), std::ptr::null()) }; - - unsafe { oakengine_encoding_params_destroy(p) }; -} - -// --------------------------------------------------------------------------- -// 8. Audio recording (`oakengine_encoding_start_audio_recording`) -// --------------------------------------------------------------------------- - -/// Recording needs the process-wide AudioManager singleton (oakaudio). The -/// whole lifecycle runs in this one test so the singleton is never shared -/// with another test. With no manager the facade reports E_STATE; with the -/// real manager and no selected input device the oakaudio module reports -/// E_FAILED (-60003, "no input device") — the real end-to-end path through -/// 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]; - - // NULL params -> facade E_INVALID. - assert_eq!( - unsafe { - oakengine_encoding_start_audio_recording(std::ptr::null(), err.as_mut_ptr(), 128) - }, - E_INVALID - ); - - // 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 - ); - - // Create the real singleton through the audio facade. - assert_eq!( - unsafe { crate::audio::oakengine_audio_create_instance() }, - 0 - ); - - // Audio disabled -> oakaudio rejects with E_INVALID and writes the - // diagnostic string. - assert_eq!( - unsafe { oakengine_encoding_start_audio_recording(p, err.as_mut_ptr(), 128) }, - A_INVALID - ); - assert!(!read_buf(&err).is_empty()); - - // Audio enabled, but no input device selected -> E_FAILED + message. - assert_eq!( - unsafe { oakengine_encoding_params_enable_audio(p, 48000, 3, 0, 13) }, - 0 - ); - assert_eq!( - unsafe { oakengine_encoding_start_audio_recording(p, err.as_mut_ptr(), 128) }, - A_FAILED - ); - assert!(!read_buf(&err).is_empty()); - - // NULL error buffer is tolerated by the failure paths (length-only - // reporting is not used here; the buffer is simply optional). - assert_eq!( - unsafe { oakengine_encoding_start_audio_recording(p, std::ptr::null_mut(), 0) }, - A_FAILED - ); - - // Tear the singleton down: the E_STATE contract returns. - 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 - ); - - unsafe { oakengine_encoding_params_destroy(p) }; -} diff --git a/crates/oakengine.bk/src/test_support/it_common.rs b/crates/oakengine.bk/src/test_support/it_common.rs deleted file mode 100644 index 07ccd13cc..000000000 --- a/crates/oakengine.bk/src/test_support/it_common.rs +++ /dev/null @@ -1,955 +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 . - -//! Integration tests for the common family (`src/common.rs` over -//! `engine/include/oakengine/{config,videoparams}.h`). -//! -//! Every exported function is exercised on a legal path with the result -//! asserted, plus the illegal-input matrix the engine must survive (NULL -//! pointers, empty handles, out-of-range indexes, zero/negative sizes, -//! garbage enums). All behavior is real: the facade calls into the real -//! oakcommon store and videoparams domain. -//! -//! The oakcommon config store is a process-wide singleton backed by -//! `config.ini` (honoring the `OAK_CONFIG_DIR` override), so every test -//! that touches config is serialized under [`CONFIG_LOCK`] and redirects -//! the file into a fresh temp dir. The videoparams tables are immutable -//! statics and the params handles are per-test objects, so those tests -//! run in parallel. - -// The whole family is called through uniform `unsafe {}` blocks (matching -// the other test binaries), so extern functions that happen to be safe -// (e.g. `oakengine_config_load`) otherwise trip `unused_unsafe`. -#![allow(unused_unsafe)] - -use super::common; - -use std::ffi::{c_char, c_int}; -use std::path::Path; -use std::sync::atomic::{AtomicI32, Ordering}; - -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, - oakengine_video_params_create, oakengine_video_params_divider_name, - oakengine_video_params_effective_size, oakengine_video_params_equal, - oakengine_video_params_format_is_float, - oakengine_video_params_format_pixel_aspect_ratio_string, - oakengine_video_params_frame_rate_to_string, oakengine_video_params_free, - oakengine_video_params_internal_channel_count, oakengine_video_params_is_valid, - oakengine_video_params_make, oakengine_video_params_pixel_format_name, - oakengine_video_params_standard_pixel_aspect_at, - oakengine_video_params_standard_pixel_aspect_count, - oakengine_video_params_standard_pixel_aspect_name, oakengine_video_params_supported_divider_at, - oakengine_video_params_supported_divider_count, oakengine_video_params_supported_frame_rate_at, - oakengine_video_params_supported_frame_rate_count, OakVideoParamsPod, -}; - -/// Read a two-stage facade string into a Rust String. -unsafe fn read_buf(buf: &mut [c_char]) -> String { - unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } - .to_string_lossy() - .into_owned() -} - -/// Serializes every test that touches the process-wide config store and -/// redirects `OAK_CONFIG_DIR` to a fresh temp dir for the duration of `f` -/// (same pattern as the oakcommon crate's own test support). The only -/// readers of `OAK_CONFIG_DIR` in this binary are these serialized tests. -/// The serialization uses the SHARED storage-config lock (common), so the -/// config tests never race the write-through tests on the singleton store -/// or the env override. -fn with_temp_config_dir(f: impl FnOnce(&Path) -> T) -> T { - let _guard = common::STORAGE_CONFIG_LOCK.lock().unwrap_or_else(|e| e.into_inner()); - let dir = - std::env::temp_dir().join(format!("oakengine_it_common_config_{}", std::process::id())); - let _ = std::fs::create_dir_all(&dir); - std::env::set_var("OAK_CONFIG_DIR", &dir); - let result = f(&dir); - std::env::remove_var("OAK_CONFIG_DIR"); - let _ = std::fs::remove_dir_all(&dir); - result -} - -/// The process-wide config store is a singleton; see module doc. -// (Serialization now uses the shared `common::STORAGE_CONFIG_LOCK`.) - -// --------------------------------------------------------------------------- -// config.h -// --------------------------------------------------------------------------- - -/// Load/save round-trip, defaults, typed entries and the two-stage string -/// convention (all serialized: the store is process-wide). -#[test] -fn config_roundtrip_persistence() { - common::force_link(); - with_temp_config_dir(|dir| { - // A missing config.ini is not an error; defaults are loaded. - assert_eq!(unsafe { oakengine_config_load() }, 0); - - // Missing keys read as empty / fallback. - let mut buf = [0 as c_char; 64]; - assert_eq!( - unsafe { oakengine_config_get_string(c"no/such/key".as_ptr(), buf.as_mut_ptr(), 64) }, - 0 - ); - assert_eq!(unsafe { read_buf(&mut buf) }, ""); - assert_eq!( - unsafe { oakengine_config_get_int(c"no/such/key".as_ptr(), 7) }, - 7 - ); - - // Compiled-in defaults are readable through the engine getters. - let len = unsafe { - oakengine_config_get_string(c"DefaultSequenceFrameRate".as_ptr(), buf.as_mut_ptr(), 64) - }; - assert_eq!(len, 10); - assert_eq!(unsafe { read_buf(&mut buf) }, "1001/30000"); - assert_eq!( - unsafe { oakengine_config_get_int(c"DefaultSequenceWidth".as_ptr(), 0) }, - 1920 - ); - - // String round-trip. - assert_eq!( - unsafe { oakengine_config_set_string(c"it/key".as_ptr(), c"hello".as_ptr()) }, - 0 - ); - let len = unsafe { oakengine_config_get_string(c"it/key".as_ptr(), buf.as_mut_ptr(), 64) }; - assert_eq!(len, 5); - assert_eq!(unsafe { read_buf(&mut buf) }, "hello"); - - // Too-small buffer: the full length is reported and the buffer is - // left untouched (query size, then allocate, then copy). - let mut small = [0 as c_char; 3]; - let len = unsafe { oakengine_config_get_string(c"it/key".as_ptr(), small.as_mut_ptr(), 3) }; - assert_eq!(len, 5); - assert_eq!(unsafe { read_buf(&mut small) }, ""); - - // A NULL value stores an empty string (engine treats NULL as ""). - assert_eq!( - unsafe { oakengine_config_set_string(c"it/key".as_ptr(), std::ptr::null()) }, - 0 - ); - let len = unsafe { oakengine_config_get_string(c"it/key".as_ptr(), buf.as_mut_ptr(), 64) }; - assert_eq!(len, 0); - assert_eq!(unsafe { read_buf(&mut buf) }, ""); - assert_eq!( - unsafe { oakengine_config_set_string(c"it/key".as_ptr(), c"hello".as_ptr()) }, - 0 - ); - - // A string entry read through the int getter falls back. - assert_eq!( - unsafe { oakengine_config_get_int(c"it/key".as_ptr(), 9) }, - 9 - ); - - // Int round-trip; a known typed key keeps its type across reload. - assert_eq!( - unsafe { oakengine_config_set_int(c"it/num".as_ptr(), 1234) }, - 0 - ); - assert_eq!( - unsafe { oakengine_config_get_int(c"it/num".as_ptr(), 0) }, - 1234 - ); - assert_eq!( - unsafe { oakengine_config_set_int(c"DefaultSequenceWidth".as_ptr(), 640) }, - 0 - ); - assert_eq!( - unsafe { oakengine_config_get_int(c"DefaultSequenceWidth".as_ptr(), 0) }, - 640 - ); - - // Persist, then reload from the file. - assert_eq!(unsafe { oakengine_config_save() }, 0); - assert!(dir.join("config.ini").exists()); - - assert_eq!(unsafe { oakengine_config_load() }, 0); - let len = unsafe { oakengine_config_get_string(c"it/key".as_ptr(), buf.as_mut_ptr(), 64) }; - assert_eq!(len, 5); - assert_eq!(unsafe { read_buf(&mut buf) }, "hello"); - assert_eq!( - unsafe { oakengine_config_get_int(c"DefaultSequenceWidth".as_ptr(), 0) }, - 640 - ); - // A custom typed key loses its type on reload and reads as a string - // (module C++ parity: only known keys keep their declared type). - let len = unsafe { oakengine_config_get_string(c"it/num".as_ptr(), buf.as_mut_ptr(), 64) }; - assert_eq!(len, 4); - assert_eq!(unsafe { read_buf(&mut buf) }, "1234"); - assert_eq!( - unsafe { oakengine_config_get_int(c"it/num".as_ptr(), 9) }, - 9 - ); - }); -} - -/// Illegal inputs on the config getters/setters: NULL keys and buffers, -/// empty keys, zero/negative sizes — all must fail cleanly, never crash. -#[test] -fn config_illegal_inputs() { - common::force_link(); - with_temp_config_dir(|_dir| { - assert_eq!(unsafe { oakengine_config_load() }, 0); - assert_eq!( - unsafe { oakengine_config_set_string(c"it/k".as_ptr(), c"abc".as_ptr()) }, - 0 - ); - - let mut buf = [0 as c_char; 64]; - - // NULL key → OAKENGINE_E_INVALID (-1). - assert_eq!( - unsafe { oakengine_config_get_string(std::ptr::null(), buf.as_mut_ptr(), 64) }, - -1 - ); - assert_eq!( - unsafe { oakengine_config_set_string(std::ptr::null(), c"v".as_ptr()) }, - -1 - ); - assert_eq!(unsafe { oakengine_config_set_int(std::ptr::null(), 5) }, -1); - // NULL key on the int getter returns the fallback (engine contract). - assert_eq!( - unsafe { oakengine_config_get_int(std::ptr::null(), 42) }, - 42 - ); - - // Empty key → the module's INVALID, passed through untranslated. - assert_eq!( - unsafe { oakengine_config_get_string(c"".as_ptr(), buf.as_mut_ptr(), 64) }, - -10001 - ); - assert_eq!(unsafe { oakengine_config_get_int(c"".as_ptr(), 42) }, 42); - - // NULL output buffer with a positive size → module INVALID (-10001). - assert_eq!( - unsafe { oakengine_config_get_string(c"it/k".as_ptr(), std::ptr::null_mut(), 64) }, - -10001 - ); - // Negative size → module INVALID. - assert_eq!( - unsafe { oakengine_config_get_string(c"it/k".as_ptr(), buf.as_mut_ptr(), -1) }, - -10001 - ); - // NULL buffer with size 0 is the two-stage size query: reports the - // required length without writing. - assert_eq!( - unsafe { oakengine_config_get_string(c"it/k".as_ptr(), std::ptr::null_mut(), 0) }, - 3 - ); - }); -} - -/// Error handler: registered, invoked via report_error and on a load -/// failure, NULL args are safe, NULL handler clears. -#[test] -fn config_error_handler_and_load_failure() { - common::force_link(); - static CALLED: AtomicI32 = AtomicI32::new(0); - unsafe extern "C" fn handler( - _title: *const c_char, - _message: *const c_char, - _userdata: *mut std::ffi::c_void, - ) { - CALLED.fetch_add(1, Ordering::SeqCst); - } - - with_temp_config_dir(|dir| { - CALLED.store(0, Ordering::SeqCst); - - // Register and report through the handler. - assert_eq!( - unsafe { oakengine_config_set_error_handler(Some(handler), std::ptr::null_mut()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_config_report_error(c"title".as_ptr(), c"message".as_ptr()) }, - 0 - ); - assert_eq!(CALLED.load(Ordering::SeqCst), 1); - // NULL title/message are mapped to empty strings, still invoked. - assert_eq!( - unsafe { oakengine_config_report_error(std::ptr::null(), std::ptr::null()) }, - 0 - ); - assert_eq!(CALLED.load(Ordering::SeqCst), 2); - - // NULL handler clears; reporting then does not invoke. - assert_eq!( - unsafe { oakengine_config_set_error_handler(None, std::ptr::null_mut()) }, - 0 - ); - unsafe { oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr()) }; - assert_eq!(CALLED.load(Ordering::SeqCst), 2); - - // A real load failure (config.ini is a directory) reports through - // the module's registered handler and returns the module FAILED - // code (-10003) untranslated. - assert_eq!( - unsafe { oakengine_config_set_error_handler(Some(handler), std::ptr::null_mut()) }, - 0 - ); - std::fs::create_dir(dir.join("config.ini")).unwrap(); - assert_eq!(unsafe { oakengine_config_load() }, -10003); - assert_eq!(CALLED.load(Ordering::SeqCst), 3); - - // Cleanup: drop the directory and clear the handler. - std::fs::remove_dir(dir.join("config.ini")).unwrap(); - unsafe { oakengine_config_set_error_handler(None, std::ptr::null_mut()) }; - assert_eq!(unsafe { oakengine_config_load() }, 0); - }); -} - -// --------------------------------------------------------------------------- -// videoparams.h — static tables -// --------------------------------------------------------------------------- - -/// Static tables: counts, every legal index, specific values, and the -/// out-of-range / NULL failure paths. -#[test] -fn videoparams_static_tables_full() { - common::force_link(); - - // ---- frame rates ------------------------------------------------------ - assert_eq!( - unsafe { oakengine_video_params_supported_frame_rate_count() }, - 12 - ); - let mut num: c_int = 0; - let mut den: c_int = 0; - for i in 0..12 { - assert_eq!( - unsafe { oakengine_video_params_supported_frame_rate_at(i, &mut num, &mut den) }, - 0 - ); - assert!( - num > 0 && den > 0, - "frame rate {i} must be a positive rational" - ); - } - assert_eq!( - unsafe { oakengine_video_params_supported_frame_rate_at(0, &mut num, &mut den) }, - 0 - ); - assert_eq!((num, den), (10, 1)); - assert_eq!( - unsafe { oakengine_video_params_supported_frame_rate_at(2, &mut num, &mut den) }, - 0 - ); - assert_eq!((num, den), (24000, 1001)); // 23.976 - assert_eq!( - unsafe { oakengine_video_params_supported_frame_rate_at(5, &mut num, &mut den) }, - 0 - ); - assert_eq!((num, den), (30000, 1001)); // 29.97 - assert_eq!( - unsafe { oakengine_video_params_supported_frame_rate_at(6, &mut num, &mut den) }, - 0 - ); - assert_eq!((num, den), (30, 1)); - assert_eq!( - unsafe { oakengine_video_params_supported_frame_rate_at(11, &mut num, &mut den) }, - 0 - ); - assert_eq!((num, den), (60, 1)); - - // Out-of-range / negative / huge indexes → E_INVALID (-1), no panic. - assert_eq!( - unsafe { oakengine_video_params_supported_frame_rate_at(12, &mut num, &mut den) }, - -1 - ); - assert_eq!( - unsafe { oakengine_video_params_supported_frame_rate_at(99, &mut num, &mut den) }, - -1 - ); - assert_eq!( - unsafe { oakengine_video_params_supported_frame_rate_at(-1, &mut num, &mut den) }, - -1 - ); - assert_eq!( - unsafe { oakengine_video_params_supported_frame_rate_at(c_int::MAX, &mut num, &mut den) }, - -1 - ); - // NULL outputs → E_INVALID. - assert_eq!( - unsafe { - oakengine_video_params_supported_frame_rate_at(0, std::ptr::null_mut(), &mut den) - }, - -1 - ); - assert_eq!( - unsafe { - oakengine_video_params_supported_frame_rate_at(0, &mut num, std::ptr::null_mut()) - }, - -1 - ); - - // ---- pixel aspects ---------------------------------------------------- - assert_eq!( - unsafe { oakengine_video_params_standard_pixel_aspect_count() }, - 6 - ); - for i in 0..6 { - assert_eq!( - unsafe { oakengine_video_params_standard_pixel_aspect_at(i, &mut num, &mut den) }, - 0 - ); - assert!( - num > 0 && den > 0, - "pixel aspect {i} must be a positive rational" - ); - } - assert_eq!( - unsafe { oakengine_video_params_standard_pixel_aspect_at(0, &mut num, &mut den) }, - 0 - ); - assert_eq!((num, den), (1, 1)); - assert_eq!( - unsafe { oakengine_video_params_standard_pixel_aspect_at(4, &mut num, &mut den) }, - 0 - ); - assert_eq!((num, den), (64, 45)); - assert_eq!( - unsafe { oakengine_video_params_standard_pixel_aspect_at(5, &mut num, &mut den) }, - 0 - ); - assert_eq!((num, den), (4, 3)); - assert_eq!( - unsafe { oakengine_video_params_standard_pixel_aspect_at(6, &mut num, &mut den) }, - -1 - ); - assert_eq!( - unsafe { oakengine_video_params_standard_pixel_aspect_at(-1, &mut num, &mut den) }, - -1 - ); - assert_eq!( - unsafe { - oakengine_video_params_standard_pixel_aspect_at(0, std::ptr::null_mut(), &mut den) - }, - -1 - ); - - // ---- dividers --------------------------------------------------------- - assert_eq!( - unsafe { oakengine_video_params_supported_divider_count() }, - 8 - ); - let expected: [c_int; 8] = [1, 2, 3, 4, 6, 8, 12, 16]; - for (i, want) in expected.iter().enumerate() { - assert_eq!( - unsafe { oakengine_video_params_supported_divider_at(i as c_int) }, - *want - ); - } - assert_eq!( - unsafe { oakengine_video_params_supported_divider_at(8) }, - -1 - ); - assert_eq!( - unsafe { oakengine_video_params_supported_divider_at(-1) }, - -1 - ); - assert_eq!( - unsafe { oakengine_video_params_supported_divider_at(c_int::MAX) }, - -1 - ); -} - -/// Display names and string formatters (pixel aspect names, divider names, -/// frame-rate strings, PAR template formatting). -#[test] -fn videoparams_names_and_formatters() { - common::force_link(); - - let mut buf = [0 as c_char; 64]; - - // ---- standard pixel aspect names -------------------------------------- - let len = unsafe { oakengine_video_params_standard_pixel_aspect_name(0, buf.as_mut_ptr(), 64) }; - assert_eq!(len, 6); - assert_eq!(unsafe { read_buf(&mut buf) }, "Square"); - let len = unsafe { oakengine_video_params_standard_pixel_aspect_name(1, buf.as_mut_ptr(), 64) }; - assert_eq!(len, 3); - assert_eq!(unsafe { read_buf(&mut buf) }, "8:9"); - let len = unsafe { oakengine_video_params_standard_pixel_aspect_name(4, buf.as_mut_ptr(), 64) }; - assert_eq!(len, 5); - assert_eq!(unsafe { read_buf(&mut buf) }, "64:45"); - // Out of range → E_INVALID; negative index → E_INVALID. - assert_eq!( - unsafe { oakengine_video_params_standard_pixel_aspect_name(6, buf.as_mut_ptr(), 64) }, - -1 - ); - assert_eq!( - unsafe { oakengine_video_params_standard_pixel_aspect_name(-1, buf.as_mut_ptr(), 64) }, - -1 - ); - // NULL buffer reports the length only (two-stage size query). - assert_eq!( - unsafe { oakengine_video_params_standard_pixel_aspect_name(0, std::ptr::null_mut(), 64) }, - 6 - ); - // Too-small buffer truncates but reports the full length. - let mut small = [0 as c_char; 2]; - assert_eq!( - unsafe { oakengine_video_params_standard_pixel_aspect_name(0, small.as_mut_ptr(), 2) }, - 6 - ); - assert_eq!(unsafe { read_buf(&mut small) }, "S"); - - // ---- divider names ------------------------------------------------------ - let len = unsafe { oakengine_video_params_divider_name(1, buf.as_mut_ptr(), 64) }; - assert_eq!(len, 4); - assert_eq!(unsafe { read_buf(&mut buf) }, "Full"); - let len = unsafe { oakengine_video_params_divider_name(2, buf.as_mut_ptr(), 64) }; - assert_eq!(len, 3); - assert_eq!(unsafe { read_buf(&mut buf) }, "1/2"); - let len = unsafe { oakengine_video_params_divider_name(8, buf.as_mut_ptr(), 64) }; - assert_eq!(len, 3); - assert_eq!(unsafe { read_buf(&mut buf) }, "1/8"); - // Zero / negative divider → E_INVALID (facade rejects before the module). - assert_eq!( - unsafe { oakengine_video_params_divider_name(0, buf.as_mut_ptr(), 64) }, - -1 - ); - assert_eq!( - unsafe { oakengine_video_params_divider_name(-3, buf.as_mut_ptr(), 64) }, - -1 - ); - // NULL buffer with a positive size → module INVALID, passed through. - assert_eq!( - unsafe { oakengine_video_params_divider_name(2, std::ptr::null_mut(), 64) }, - -10001 - ); - - // ---- frame rate strings ------------------------------------------------- - let len = unsafe { oakengine_video_params_frame_rate_to_string(25, 1, buf.as_mut_ptr(), 64) }; - assert_eq!(len, 6); - assert_eq!(unsafe { read_buf(&mut buf) }, "25 FPS"); - let len = - unsafe { oakengine_video_params_frame_rate_to_string(24000, 1001, buf.as_mut_ptr(), 64) }; - assert_eq!(len, 10); - assert_eq!(unsafe { read_buf(&mut buf) }, "23.976 FPS"); - let len = unsafe { oakengine_video_params_frame_rate_to_string(10, 1, buf.as_mut_ptr(), 64) }; - assert_eq!(len, 6); - assert_eq!(unsafe { read_buf(&mut buf) }, "10 FPS"); - - // Zero denominator: C++-parity float division (1/0 → +inf), rendered as - // "inf FPS" — a legal return, never a crash/panic. - let len = unsafe { oakengine_video_params_frame_rate_to_string(1, 0, buf.as_mut_ptr(), 64) }; - assert!(len >= 0, "den=0 must not error ({len})"); - assert_eq!(unsafe { read_buf(&mut buf) }, "inf FPS"); - // 0/0 → NaN → "nan FPS". - let len = unsafe { oakengine_video_params_frame_rate_to_string(0, 0, buf.as_mut_ptr(), 64) }; - assert!(len >= 0); - assert_eq!(unsafe { read_buf(&mut buf) }, "nan FPS"); - // NULL buffer with a positive size → module INVALID. - assert_eq!( - unsafe { oakengine_video_params_frame_rate_to_string(25, 1, std::ptr::null_mut(), 64) }, - -10001 - ); - // NULL buffer with size 0 is the two-stage size query. - assert_eq!( - unsafe { oakengine_video_params_frame_rate_to_string(25, 1, std::ptr::null_mut(), 0) }, - 6 - ); - - // ---- PAR template formatting (facade-local) ----------------------------- - let len = unsafe { - oakengine_video_params_format_pixel_aspect_ratio_string( - c"%1".as_ptr(), - 16, - 15, - buf.as_mut_ptr(), - 64, - ) - }; - assert_eq!(len, 5); - assert_eq!(unsafe { read_buf(&mut buf) }, "16:15"); - let len = unsafe { - oakengine_video_params_format_pixel_aspect_ratio_string( - c"par=%1".as_ptr(), - 4, - 3, - buf.as_mut_ptr(), - 64, - ) - }; - assert_eq!(len, 7); - assert_eq!(unsafe { read_buf(&mut buf) }, "par=4:3"); - // No placeholder: the template passes through unchanged. - let len = unsafe { - oakengine_video_params_format_pixel_aspect_ratio_string( - c"raw".as_ptr(), - 16, - 15, - buf.as_mut_ptr(), - 64, - ) - }; - assert_eq!(len, 3); - assert_eq!(unsafe { read_buf(&mut buf) }, "raw"); - // NULL format → E_INVALID; NULL buffer reports the length only. - assert_eq!( - unsafe { - oakengine_video_params_format_pixel_aspect_ratio_string( - std::ptr::null(), - 16, - 15, - buf.as_mut_ptr(), - 64, - ) - }, - -1 - ); - assert_eq!( - unsafe { - oakengine_video_params_format_pixel_aspect_ratio_string( - c"%1".as_ptr(), - 16, - 15, - std::ptr::null_mut(), - 64, - ) - }, - 5 - ); -} - -/// Format helpers: float/name queries and bytes-per-pixel across the -/// format matrix (valid, boundary and garbage codes). -#[test] -fn videoparams_format_helpers() { - common::force_link(); - - // format_is_float: F16 = 3, F32 = 4 float; everything else 0, garbage - // codes map to the Invalid format and report 0 (never crash). - assert_eq!(unsafe { oakengine_video_params_format_is_float(0) }, 0); // U8 - assert_eq!(unsafe { oakengine_video_params_format_is_float(1) }, 0); // U10 - assert_eq!(unsafe { oakengine_video_params_format_is_float(2) }, 0); // U16 - assert_eq!(unsafe { oakengine_video_params_format_is_float(3) }, 1); // F16 - assert_eq!(unsafe { oakengine_video_params_format_is_float(4) }, 1); // F32 - assert_eq!(unsafe { oakengine_video_params_format_is_float(5) }, 0); // Count - assert_eq!(unsafe { oakengine_video_params_format_is_float(99) }, 0); - assert_eq!(unsafe { oakengine_video_params_format_is_float(-1) }, 0); - assert_eq!( - unsafe { oakengine_video_params_format_is_float(c_int::MIN) }, - 0 - ); - - // pixel_format_name for every real format. - let mut buf = [0 as c_char; 64]; - let len = unsafe { oakengine_video_params_pixel_format_name(0, buf.as_mut_ptr(), 64) }; - assert_eq!(len, 5); - assert_eq!(unsafe { read_buf(&mut buf) }, "8-bit"); - let len = unsafe { oakengine_video_params_pixel_format_name(1, buf.as_mut_ptr(), 64) }; - assert_eq!(len, 13); - assert_eq!(unsafe { read_buf(&mut buf) }, "10-bit Packed"); - let len = unsafe { oakengine_video_params_pixel_format_name(4, buf.as_mut_ptr(), 64) }; - assert_eq!(len, 19); - assert_eq!(unsafe { read_buf(&mut buf) }, "Full-Float (32-bit)"); - // Garbage format → "Unknown (0xFFFFFFFF)" (Invalid renders %X of -1). - let len = unsafe { oakengine_video_params_pixel_format_name(99, buf.as_mut_ptr(), 64) }; - assert_eq!(len, 20); - assert_eq!(unsafe { read_buf(&mut buf) }, "Unknown (0xFFFFFFFF)"); - // NULL buffer / negative size → module INVALID; size-0 query → length. - assert_eq!( - unsafe { oakengine_video_params_pixel_format_name(0, std::ptr::null_mut(), 64) }, - -10001 - ); - assert_eq!( - unsafe { oakengine_video_params_pixel_format_name(0, buf.as_mut_ptr(), -1) }, - -10001 - ); - assert_eq!( - unsafe { oakengine_video_params_pixel_format_name(0, std::ptr::null_mut(), 0) }, - 5 - ); - - // bytes_per_pixel across the format × channels matrix. - assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(0, 4) }, 4); // U8 - assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(1, 4) }, 4); // U10 packed - assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(2, 4) }, 8); // U16 - assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(3, 4) }, 8); // F16 - assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(4, 4) }, 16); // F32 - // Garbage formats have no channels-per-format entry → 0 bytes. - assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(99, 4) }, 0); - assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(-1, 4) }, 0); - // Zero channels → 0 bytes. - assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(0, 0) }, 0); - assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(4, 0) }, 0); - // Negative channels: the module does not validate (C++ parity), so the - // result is the plain signed product — a value, not a crash. - assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(4, -1) }, -4); - - assert_eq!( - unsafe { oakengine_video_params_internal_channel_count() }, - 4 - ); -} - -/// Effective size: divider scaling on the legal matrix plus zero/negative -/// dimensions and dividers → E_INVALID. -#[test] -fn videoparams_effective_size_matrix() { - common::force_link(); - - let mut w: c_int = 0; - let mut h: c_int = 0; - assert_eq!( - unsafe { oakengine_video_params_effective_size(1920, 1080, 1, &mut w, &mut h) }, - 0 - ); - assert_eq!((w, h), (1920, 1080)); - assert_eq!( - unsafe { oakengine_video_params_effective_size(1920, 1080, 2, &mut w, &mut h) }, - 0 - ); - assert_eq!((w, h), (960, 540)); - assert_eq!( - unsafe { oakengine_video_params_effective_size(1920, 1080, 4, &mut w, &mut h) }, - 0 - ); - assert_eq!((w, h), (480, 270)); - assert_eq!( - unsafe { oakengine_video_params_effective_size(100, 50, 3, &mut w, &mut h) }, - 0 - ); - assert_eq!((w, h), (33, 16)); - // Divider 16 truncates the odd dimension (integer division). - assert_eq!( - unsafe { oakengine_video_params_effective_size(1920, 1080, 16, &mut w, &mut h) }, - 0 - ); - assert_eq!((w, h), (120, 67)); - - // Both output pointers may be NULL (size computed, nothing written). - assert_eq!( - unsafe { - oakengine_video_params_effective_size( - 1920, - 1080, - 2, - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - 0 - ); - - // Zero / negative dimensions and dividers → E_INVALID. - assert_eq!( - unsafe { oakengine_video_params_effective_size(0, 1080, 2, &mut w, &mut h) }, - -1 - ); - assert_eq!( - unsafe { oakengine_video_params_effective_size(1920, 0, 2, &mut w, &mut h) }, - -1 - ); - assert_eq!( - unsafe { oakengine_video_params_effective_size(-1, 1080, 2, &mut w, &mut h) }, - -1 - ); - assert_eq!( - unsafe { oakengine_video_params_effective_size(1920, 1080, 0, &mut w, &mut h) }, - -1 - ); - assert_eq!( - unsafe { oakengine_video_params_effective_size(1920, 1080, -2, &mut w, &mut h) }, - -1 - ); -} - -// --------------------------------------------------------------------------- -// videoparams.h — POD make/equal/valid -// --------------------------------------------------------------------------- - -/// A valid POD used across the POD tests. -fn valid_pod() -> OakVideoParamsPod { - let mut p: OakVideoParamsPod = unsafe { std::mem::zeroed() }; - assert_eq!( - unsafe { oakengine_video_params_make(&mut p, 1920, 1080, 1001, 30000, 4, 1, 1, 0, 1, 2,) }, - 0 - ); - p -} - -/// make fills every field; equal compares all of them; is_valid implements -/// the engine's POD validity rules. -#[test] -fn videoparams_pod_make_equal_valid() { - common::force_link(); - - // make: every field lands in the POD. - let mut p: OakVideoParamsPod = unsafe { std::mem::zeroed() }; - assert_eq!( - unsafe { oakengine_video_params_make(&mut p, 1920, 1080, 1001, 30000, 4, 1, 1, 0, 1, 2) }, - 0 - ); - assert_eq!(p.width, 1920); - assert_eq!(p.height, 1080); - assert_eq!(p.time_base_num, 1001); - assert_eq!(p.time_base_den, 30000); - assert_eq!(p.format, 4); - assert_eq!(p.pixel_aspect_num, 1); - assert_eq!(p.pixel_aspect_den, 1); - assert_eq!(p.interlacing, 0); - assert_eq!(p.color_range, 1); - assert_eq!(p.divider, 2); - assert_eq!(p.video_type, 0); - assert_eq!(p.premultiplied_alpha, 0); - // NULL POD → E_INVALID. - assert_eq!( - unsafe { - oakengine_video_params_make( - std::ptr::null_mut(), - 1920, - 1080, - 1001, - 30000, - 4, - 1, - 1, - 0, - 1, - 2, - ) - }, - -1 - ); - - // equal: identical PODs → 1; any differing field → 0; NULL → 0. - let a = valid_pod(); - let mut b = a; - assert_eq!(unsafe { oakengine_video_params_equal(&a, &b) }, 1); - for (field, val) in [ - ("width", 640), - ("height", 720), - ("time_base_num", 25), - ("time_base_den", 1), - ("format", 0), - ("pixel_aspect_num", 4), - ("pixel_aspect_den", 3), - ("interlacing", 1), - ("color_range", 0), - ("divider", 1), - ("video_type", 1), - ("premultiplied_alpha", 1), - ] { - let mut c = a; - match field { - "width" => c.width = val, - "height" => c.height = val, - "time_base_num" => c.time_base_num = val, - "time_base_den" => c.time_base_den = val, - "format" => c.format = val, - "pixel_aspect_num" => c.pixel_aspect_num = val, - "pixel_aspect_den" => c.pixel_aspect_den = val, - "interlacing" => c.interlacing = val, - "color_range" => c.color_range = val, - "divider" => c.divider = val, - "video_type" => c.video_type = val, - "premultiplied_alpha" => c.premultiplied_alpha = val, - _ => unreachable!(), - } - assert_eq!( - unsafe { oakengine_video_params_equal(&a, &c) }, - 0, - "equal must be 0 when {field} differs" - ); - } - assert_eq!( - unsafe { oakengine_video_params_equal(std::ptr::null(), &a) }, - 0 - ); - assert_eq!( - unsafe { oakengine_video_params_equal(&a, std::ptr::null()) }, - 0 - ); - - // is_valid: the valid POD → 1. - assert_eq!(unsafe { oakengine_video_params_is_valid(&a) }, 1); - // NULL → 0. - assert_eq!( - unsafe { oakengine_video_params_is_valid(std::ptr::null()) }, - 0 - ); - // Each invalidating field → 0. - let cases: [(&str, fn(&mut OakVideoParamsPod)); 6] = [ - ("width", |p| p.width = 0), - ("height", |p| p.height = 0), - ("pixel_aspect_num", |p| p.pixel_aspect_num = 0), - ("pixel_aspect_den", |p| p.pixel_aspect_den = 0), - ("format", |p| p.format = -1), - ("time_base_den", |p| p.time_base_den = 0), - ]; - for (name, mutate) in cases { - let mut c = a; - mutate(&mut c); - assert_eq!( - unsafe { oakengine_video_params_is_valid(&c) }, - 0, - "is_valid must be 0 when {name} is invalid" - ); - } - // NOTE (observed divergence): the facade's POD check uses `format >= 0`, - // so out-of-range-but-non-negative formats (e.g. 99, or the Count code 5) - // read as "valid" here, while the module/C++ `VideoParams::is_valid` - // additionally requires `format < Count`. The facade check is a - // simplified local rule (the POD has no channel_count), not a crash. - let mut c = a; - c.format = 99; - assert_eq!(unsafe { oakengine_video_params_is_valid(&c) }, 1); -} - -// --------------------------------------------------------------------------- -// videoparams.h — opaque handle lifecycle -// --------------------------------------------------------------------------- - -/// create/free lifecycle: NULL rejection, real-handle creation, NULL free. -#[test] -fn videoparams_create_free_lifecycle() { - common::force_link(); - - // NULL POD → NULL handle. - assert!(unsafe { oakengine_video_params_create(std::ptr::null()) }.is_null()); - - // Valid POD → non-NULL handle; freed cleanly. - let pod = valid_pod(); - let h = unsafe { oakengine_video_params_create(&pod) }; - assert!(!h.is_null()); - unsafe { oakengine_video_params_free(h) }; - - // A zeroed POD still yields a handle (the module initializes a default - // set and the setters accept any values); the handle frees cleanly. - let zeroed: OakVideoParamsPod = unsafe { std::mem::zeroed() }; - let h = unsafe { oakengine_video_params_create(&zeroed) }; - assert!(!h.is_null()); - unsafe { oakengine_video_params_free(h) }; - - // free(NULL) is a documented no-op. - unsafe { oakengine_video_params_free(std::ptr::null_mut()) }; - - // NOTE (contract): the facade's free deallocates the handle box - // (`Box::from_raw`), so a second free of the same pointer is a - // use-after-free and is NOT part of the family's contract — unlike the - // module-level `oakcommon_videoparams_free`, which nulls the handle out - // before returning. This family exposes no debug alive counter to - // verify a return to baseline; leak-free operation is implied by the - // create/free round-trips above. -} diff --git a/crates/oakengine.bk/src/test_support/it_export.rs b/crates/oakengine.bk/src/test_support/it_export.rs deleted file mode 100644 index 1f4f6c6a4..000000000 --- a/crates/oakengine.bk/src/test_support/it_export.rs +++ /dev/null @@ -1,477 +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 . - -//! 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 `oakcore_audioparams_*` accessors the facade -//! reads through are its own in-dylib implementations, re-exported by -//! `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(()); - -/// Both lock guards held by [`serial`]. -struct SerialGuard { - /// The [`SERIAL`] lock. - _task: std::sync::MutexGuard<'static, ()>, - /// The facade-wide undo-stack lock, so the `oakengine_project_new` - /// calls in these tests never race the it_undo / it_storage stack tests. - _stack: parking_lot::ReentrantMutexGuard<'static, ()>, -} - -/// Take the [`SERIAL`] lock AND the global undo-stack lock, recovering -/// from any poisoning. -fn serial() -> SerialGuard { - let _task = SERIAL.lock().unwrap_or_else(|e| e.into_inner()); - let _stack = super::it_undo::GLOBAL_STACK_LOCK - .lock(); - SerialGuard { _task, _stack } -} - -/// 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) { - // The undoable import/add-track/add-clip commands below would bind the - // project to the default user library; disable the write-through for - // the assembly (and keep the config lock held while pushing). - let _storage = common::storage_off_guard(); - 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::(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); - } -} diff --git a/crates/oakengine.bk/src/test_support/it_library.rs b/crates/oakengine.bk/src/test_support/it_library.rs deleted file mode 100644 index b031c36f9..000000000 --- a/crates/oakengine.bk/src/test_support/it_library.rs +++ /dev/null @@ -1,426 +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 . - -//! D4 integration tests: the project-library manager C ABI -//! (`src/library.rs`, plan M13 §4). -//! -//! End-to-end against real SQLite library files in temp directories, -//! driving the facade exactly like the app's project manager: create lands -//! a row immediately, list reports it with the derived stats, open -//! (`oakengine_project_load_library`) binds the loaded project to the -//! library session (the next undoable edit write-throughs onto the row's -//! journal), rename / duplicate / delete / import / export round-trip, and -//! the disabled-backend configuration degrades to an empty list + error -//! codes. -//! -//! Every test holds the shared undo-stack lock (the facade's stack is -//! process-wide, same as the it_undo / it_storage families) and the -//! storage-config lock, so the suite never races on either singleton. - -use std::ffi::CString; -use std::path::{Path, PathBuf}; - -use super::common; -use super::it_undo::GLOBAL_STACK_LOCK; - -use crate::error::{OAKENGINE_OK, OAKENGINE_E_INVALID, OAKENGINE_E_NOT_FOUND, OAKENGINE_E_STATE}; -use crate::handle::OakEngineProject; - -/// The math node type id (a factory type the tests add as an undoable -/// edit; footage is not factory-creatable, so the write-through is -/// verified on the journal rows directly). -const MATH: &str = "org.olivevideoeditor.Olive.math"; - -/// The journal row count of a library row (direct sea-orm read, the same -/// pattern as the it_storage tests). -fn journal_rows(db: &Path, uuid: &str) -> usize { - use sea_orm::entity::prelude::*; - use oakstorage::backends::database::entities::{journal, project}; - - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap(); - runtime.block_on(async { - let conn = sea_orm::Database::connect(format!("sqlite://{}?mode=ro", db.display())) - .await - .unwrap(); - let model = project::Entity::find() - .filter(project::Column::Uuid.eq(uuid)) - .one(&conn) - .await - .unwrap() - .expect("the row exists"); - journal::Entity::find() - .filter(journal::Column::ProjectId.eq(model.id)) - .all(&conn) - .await - .unwrap() - .len() - }) -} - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/// Serialize a library test: hold the process-global undo-stack lock AND -/// the storage-config lock for the whole body, then point the library at a -/// temp SQLite file. -fn with_library(db: &Path, f: impl FnOnce() -> R) -> R { - let _stack = GLOBAL_STACK_LOCK.lock(); - let _config = common::STORAGE_CONFIG_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); - let store = oakcommon::configstore::ConfigStore::instance(); - store.set(Some("Storage"), "Backend", "sqlite"); - store.set(Some("Storage"), "SqlitePath", &db.to_string_lossy()); - f() -} - -/// A fresh, unique temp directory for one test. -fn temp_dir(tag: &str) -> PathBuf { - let dir = - std::env::temp_dir().join(format!("oakengine_library_{}_{}", std::process::id(), tag)); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).unwrap(); - dir -} - -/// Two-stage string read over a facade `(buf, size)` getter. -fn read_string(f: impl Fn(*mut std::ffi::c_char, i32) -> i32) -> String { - let needed = f(std::ptr::null_mut(), 0); - if needed <= 0 { - return String::new(); - } - let mut buf = vec![0 as std::ffi::c_char; needed as usize + 1]; - f(buf.as_mut_ptr(), needed + 1); - 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() -} - -/// The library list JSON. -fn list_json() -> String { - read_string(|buf, size| unsafe { crate::library::oakengine_library_list(buf, size) }) -} - -/// Create a library row; returns its uuid. The create export has a side -/// effect, so it is called ONCE with a stack buffer (never two-stage). -fn create(name: &str) -> String { - let name = CString::new(name).unwrap(); - let mut buf = [0 as std::ffi::c_char; 256]; - let rc = unsafe { crate::library::oakengine_library_create(name.as_ptr(), buf.as_mut_ptr(), 256) }; - assert!(rc > 0, "create {name:?} rc={rc}"); - 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() -} - -/// Duplicate a library row (single call with a stack buffer, see -/// [`create`]). -fn duplicate(uuid: &str) -> String { - let uuid = CString::new(uuid).unwrap(); - let mut buf = [0 as std::ffi::c_char; 256]; - let rc = unsafe { - crate::library::oakengine_library_duplicate( - uuid.as_ptr(), - std::ptr::null(), - buf.as_mut_ptr(), - 256, - ) - }; - assert!(rc > 0, "duplicate rc={rc}"); - 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() -} - -/// The uuids in the library list JSON (order preserved). -fn list_uuids(json: &str) -> Vec { - serde_json::from_str::(json) - .expect("list is JSON") - .as_array() - .expect("list is an array") - .iter() - .map(|row| { - row.get("uuid") - .and_then(|v| v.as_str()) - .expect("row uuid") - .to_string() - }) - .collect() -} - -/// One row of the library list JSON by uuid. -fn list_row<'a>(json: &'a str, uuid: &str) -> Option { - serde_json::from_str::(json) - .expect("list is JSON") - .as_array() - .expect("list is an array") - .iter() - .find(|row| row.get("uuid").and_then(|v| v.as_str()) == Some(uuid)) - .cloned() -} - -/// Open a library row into a fresh facade project shell. -fn open(uuid: &str) -> *mut OakEngineProject { - let project = unsafe { crate::node::oakengine_project_create() }; - assert!(!project.is_null()); - let uuid_c = CString::new(uuid).unwrap(); - let mut err = [0 as std::ffi::c_char; 4096]; - let rc = unsafe { - crate::library::oakengine_project_load_library( - project, - uuid_c.as_ptr(), - err.as_mut_ptr(), - err.len() as i32, - ) - }; - assert_eq!(rc, OAKENGINE_OK, "open {uuid}"); - project -} - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -/// Create lands a row immediately; list reports it with the metadata and -/// the (zero) stats. -#[test] -fn create_then_list_shows_the_row() { - let dir = temp_dir("create"); - with_library(&dir.join("lib.db"), || { - let uuid = create("Demo Reel"); - assert!(!uuid.is_empty(), "create reports the new uuid"); - - let json = list_json(); - let row = list_row(&json, &uuid).expect("the created row is listed"); - assert_eq!(row.get("name").and_then(|v| v.as_str()), Some("Demo Reel")); - assert!(row.get("modified_at").and_then(|v| v.as_i64()).unwrap() > 0); - assert_eq!(row.get("track_count").and_then(|v| v.as_i64()), Some(0)); - assert_eq!(row.get("footage_count").and_then(|v| v.as_i64()), Some(0)); - - // A second create adds a second row. - let other = create("Second"); - assert_ne!(uuid, other); - assert_eq!(list_uuids(&list_json()).len(), 2); - }); - let _ = std::fs::remove_dir_all(&dir); -} - -/// Opening a library row binds the project to the library session: the -/// next undoable edit write-throughs onto the row's journal. -#[test] -fn open_binds_and_write_through_advances_the_row() { - let dir = temp_dir("open"); - let db = dir.join("lib.db"); - with_library(&db, || { - let uuid = create("Editable"); - let before = journal_rows(&db, &uuid); - let project = open(&uuid); - assert_eq!( - unsafe { crate::storage::oakengine_storage_is_bound(project) }, - 1, - "the library-opened project is bound" - ); - - // An undoable edit write-throughs. - let node = unsafe { - crate::node::oakengine_project_add_node(project, CString::new(MATH).unwrap().as_ptr()) - }; - assert!(!node.is_null()); - unsafe { crate::node::oakengine_node_free(node) }; - - let after = journal_rows(&db, &uuid); - assert!( - after > before, - "the edit journaled new rows ({before} -> {after})" - ); - - // The row name comes from the projectname setting (the facade's - // project name is filename-derived, so a library project displays - // "(untitled)"; the app overrides it with the row name). - let row = list_row(&list_json(), &uuid).expect("row after the edit"); - assert_eq!(row.get("name").and_then(|v| v.as_str()), Some("Editable")); - - unsafe { crate::node::oakengine_project_free(project) }; - }); - let _ = std::fs::remove_dir_all(&dir); -} - -/// Rename and duplicate keep the list coherent; delete removes the row and -/// opening it afterwards fails. -#[test] -fn rename_duplicate_delete() { - let dir = temp_dir("rdd"); - with_library(&dir.join("lib.db"), || { - let uuid = create("Original"); - - // Rename. - let rc = unsafe { - crate::library::oakengine_library_rename( - CString::new(uuid.clone()).unwrap().as_ptr(), - CString::new("Renamed").unwrap().as_ptr(), - ) - }; - assert_eq!(rc, OAKENGINE_OK); - let row = list_row(&list_json(), &uuid).expect("renamed row"); - assert_eq!(row.get("name").and_then(|v| v.as_str()), Some("Renamed")); - - // Duplicate (default " (copy)" name). - let copy = duplicate(&uuid); - assert_ne!(copy, uuid); - let row = list_row(&list_json(), ©).expect("the copy is listed"); - assert_eq!( - row.get("name").and_then(|v| v.as_str()), - Some("Renamed (copy)") - ); - - // The copy opens (its journal history came along). - let project = open(©); - unsafe { crate::node::oakengine_project_free(project) }; - - // Delete the copy; opening it afterwards fails. - let rc = unsafe { - crate::library::oakengine_library_delete(CString::new(copy.clone()).unwrap().as_ptr()) - }; - assert_eq!(rc, OAKENGINE_OK); - assert!(!list_uuids(&list_json()).contains(©)); - let shell = unsafe { crate::node::oakengine_project_create() }; - let rc = unsafe { - crate::library::oakengine_project_load_library( - shell, - CString::new(copy).unwrap().as_ptr(), - std::ptr::null_mut(), - 0, - ) - }; - assert_eq!(rc, OAKENGINE_E_NOT_FOUND, "a deleted row does not open"); - unsafe { crate::node::oakengine_project_free(shell) }; - - // Unknown uuids are E_NOT_FOUND; empty arguments E_INVALID. - let rc = unsafe { - crate::library::oakengine_library_delete(CString::new("{no-such}").unwrap().as_ptr()) - }; - assert_eq!(rc, OAKENGINE_E_NOT_FOUND); - let rc = unsafe { crate::library::oakengine_library_delete(c"".as_ptr()) }; - assert_eq!(rc, OAKENGINE_E_INVALID); - }); - let _ = std::fs::remove_dir_all(&dir); -} - -/// Export writes the row's head state to a file (dispatched by extension), -/// and import brings a file back as a new library row that opens. -#[test] -fn export_then_import_round_trip() { - let dir = temp_dir("xport"); - with_library(&dir.join("lib.db"), || { - // A row with one node, so the exported file has content. - let uuid = create("Exchange"); - let project = open(&uuid); - let node = unsafe { - crate::node::oakengine_project_add_node(project, CString::new(MATH).unwrap().as_ptr()) - }; - assert!(!node.is_null()); - unsafe { crate::node::oakengine_node_free(node) }; - unsafe { crate::node::oakengine_project_free(project) }; - - // Export as .ove and as .otio. - let ove = dir.join("out.ove"); - let otio = dir.join("out.otio"); - for path in [&ove, &otio] { - let rc = unsafe { - crate::library::oakengine_library_export( - CString::new(uuid.clone()).unwrap().as_ptr(), - CString::new(path.to_string_lossy().into_owned()).unwrap().as_ptr(), - ) - }; - assert_eq!(rc, OAKENGINE_OK, "export {}", path.display()); - assert!(path.exists(), "{} exists", path.display()); - } - let xml = std::fs::read_to_string(&ove).unwrap(); - assert!(xml.contains(" 0, "import rc={rc}"); - let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len()); - let imported = - String::from_utf8_lossy(unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) }) - .into_owned(); - assert!(!imported.is_empty(), "import reports the new uuid"); - assert_ne!(imported, uuid, "import assigns a fresh uuid"); - let project = open(&imported); - // The projectname setting round-trips into the imported row's name. - let row = list_row(&list_json(), &imported).expect("the imported row is listed"); - assert_eq!(row.get("name").and_then(|v| v.as_str()), Some("Exchange")); - unsafe { crate::node::oakengine_project_free(project) }; - - // Importing a missing file fails. - let rc = unsafe { - crate::library::oakengine_library_import( - CString::new(dir.join("nope.ove").to_string_lossy().into_owned()) - .unwrap() - .as_ptr(), - std::ptr::null_mut(), - 0, - ) - }; - assert!(rc < 0, "a missing file does not import"); - }); - let _ = std::fs::remove_dir_all(&dir); -} - -/// With the backend disabled the list is empty (not an error) and every -/// mutating call fails with E_STATE. -#[test] -fn disabled_backend_degrades_gracefully() { - let _stack = GLOBAL_STACK_LOCK.lock(); - let _off = common::storage_off_guard(); - - assert_eq!(list_json(), "[]", "no library configured reads as empty"); - - let rc = unsafe { - crate::library::oakengine_library_create( - c"Nope".as_ptr(), - std::ptr::null_mut(), - 0, - ) - }; - assert_eq!(rc, OAKENGINE_E_STATE); - let rc = unsafe { crate::library::oakengine_library_delete(c"{x}".as_ptr()) }; - assert_eq!(rc, OAKENGINE_E_STATE); - let rc = unsafe { crate::library::oakengine_library_export(c"{x}".as_ptr(), c"/tmp/x.ove".as_ptr()) }; - assert_eq!(rc, OAKENGINE_E_STATE); - - let shell = unsafe { crate::node::oakengine_project_create() }; - let rc = unsafe { - crate::library::oakengine_project_load_library( - shell, - c"{x}".as_ptr(), - std::ptr::null_mut(), - 0, - ) - }; - assert_eq!(rc, OAKENGINE_E_STATE); - unsafe { crate::node::oakengine_project_free(shell) }; -} diff --git a/crates/oakengine.bk/src/test_support/it_plugin.rs b/crates/oakengine.bk/src/test_support/it_plugin.rs deleted file mode 100644 index ac53b4cd8..000000000 --- a/crates/oakengine.bk/src/test_support/it_plugin.rs +++ /dev/null @@ -1,630 +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 . - -//! Integration tests for the plugin family: the facade exports -//! `oakengine_plugin_*` (src/plugin.rs; module C contract -//! `include/plugin/{host,instance,error}.h`), exercised end to end -//! against the REAL `oakplugin` crate — no mocks anywhere. -//! -//! `oakengine_plugin_load_plugins` drives the real OFX host scan -//! (dlopen of real plugin bundles); the family's only destroy surface -//! lives in the backend module (`oakplugin_instance_create/free`), which -//! is verified here against the module's debug alive counter -//! (`oakplugin_debug_alive_count`, the leak assertion for this family). -//! The two provider setters are pure facade state (module 00 analogues of -//! the C++ capi statics): their result IS the return code, asserted below. -//! -//! The host singleton is process-global and only internally locked, so -//! every test that touches it serializes on [`with_host`] (same -//! convention as the module crate's own tests). -//! -//! The end-to-end bundle test needs the minimal test plugin that the -//! oakplugin crate's build.rs compiles (cbits/oak_test_plugin.c). When -//! it is unavailable the test prints SKIP and returns (never fails). - -use super::common; - -use std::ffi::{c_char, c_int, c_void, CStr, CString}; -use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex}; - -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, -}; -// 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; -/// `OAKENGINE_E_FAILED` (src/error.rs). -const E_FAILED: c_int = -3; -/// `OAKPLUGIN_E_INVALID` (module error.h) — module codes pass through the -/// facade untranslated. -const PLUGIN_E_INVALID: c_int = -90001; - -/// Identifier of the minimal OFX test plugin (cbits/oak_test_plugin.c). -const TEST_PLUGIN_ID: &str = "org.oak.test-plugin"; -/// Build-system injected bundle path (set by the CMake test runner). -const TEST_PLUGIN_ENV: &str = "OAK_TEST_PLUGIN_DIR"; - -// --------------------------------------------------------------------------- -// Host serialization + fixtures -// --------------------------------------------------------------------------- - -/// Serialize host-touching tests: the oakplugin host is a process -/// singleton without a top-level lock (each internal list is mutexed, but -/// init/scan/shutdown interleavings would make count assertions flaky). -fn with_host(f: impl FnOnce()) { - static LOCK: Mutex<()> = Mutex::new(()); - let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner()); - f(); -} - -/// Fresh directory under the system temp dir (removed before creation). -fn fresh_temp_dir(name: &str) -> PathBuf { - let p = std::env::temp_dir().join(format!("oak-it-plugin-{}-{name}", std::process::id())); - let _ = std::fs::remove_dir_all(&p); - std::fs::create_dir_all(&p).expect("create temp dir"); - p -} - -/// Current number of live backend objects (host instance registry). -fn alive() -> c_int { - Host::global().alive_count() as c_int -} - -/// Number of plugins discovered by the real host cache. -fn plugin_count() -> c_int { - Host::global().cache.count() as c_int -} - -/// Run the facade scan through the real host, returning its exit code. -fn scan_facade(dir: &Path) -> c_int { - let cs = CString::new(dir.as_os_str().as_encoded_bytes()).expect("NUL-free path"); - 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 { - 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 { - 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>> { - 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>>) { - *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. -fn find_test_plugin_lib() -> Option { - let ext = if cfg!(target_os = "macos") { - "dylib" - } else { - "so" - }; - let mut roots: Vec = Vec::new(); - if let Some(t) = std::env::var_os("CARGO_TARGET_DIR") { - roots.push(PathBuf::from(t)); - } - roots.push(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target")); - for root in roots { - for profile in ["debug", "release"] { - let build = root.join(profile).join("build"); - let Ok(entries) = std::fs::read_dir(&build) else { - continue; - }; - let mut hits: Vec = entries - .flatten() - .map(|e| e.path()) - .filter(|p| { - p.file_name() - .and_then(|n| n.to_str()) - .is_some_and(|n| n.starts_with("oakplugin-")) - }) - .collect(); - hits.sort(); - for dir in hits { - let lib = dir.join("out").join(format!("oak_test_plugin.{ext}")); - if lib.is_file() { - return Some(lib); - } - } - } - } - None -} - -/// Copy a test-plugin shared library into a `.bundle` directory layout -/// under `root` (Contents/MacOS or Contents/Linux-x86-64, matching the -/// host's `find_binary_in_bundle`). -fn install_bundle(lib: &Path, root: &Path) -> Option<()> { - let platform = if cfg!(target_os = "macos") { - "MacOS" - } else { - "Linux-x86-64" - }; - let bin_dir = root - .join("oak-test-plugin.ofx.bundle") - .join("Contents") - .join(platform); - std::fs::create_dir_all(&bin_dir).ok()?; - std::fs::copy(lib, bin_dir.join("plugin")).ok()?; - Some(()) -} - -/// A scan directory containing the real test plugin bundle, if available: -/// either the parent of the build-system-injected bundle -/// (`OAK_TEST_PLUGIN_DIR`) or a bundle assembled in the temp dir from the -/// shared library oakplugin's build.rs produced. `None` → caller skips. -fn test_plugin_scan_dir() -> Option { - if let Some(bundle) = std::env::var_os(TEST_PLUGIN_ENV) { - return PathBuf::from(bundle).parent().map(|p| p.to_path_buf()); - } - let lib = find_test_plugin_lib()?; - let root = std::env::temp_dir().join(format!("oak-it-plugin-bundle-{}", std::process::id())); - if !root.join("oak-test-plugin.ofx.bundle").exists() { - install_bundle(&lib, &root)?; - } - Some(root) -} - -/// A scan directory that is guaranteed to have been scanned by NO other -/// test in this binary (fresh per call), so "scan registers the plugin" -/// assertions are deterministic. The env-injected bundle has no fresh -/// variant and falls back to the shared one. -fn fresh_test_plugin_scan_dir(tag: &str) -> Option { - if std::env::var_os(TEST_PLUGIN_ENV).is_some() { - return test_plugin_scan_dir(); - } - let lib = find_test_plugin_lib()?; - let root = fresh_temp_dir(&format!("bundle-{tag}")); - install_bundle(&lib, &root)?; - Some(root) -} - -/// All plugin identifiers currently registered in the real host cache -/// (direct cache iteration). -fn plugin_ids() -> Vec { - let count = plugin_count(); - let mut out = Vec::new(); - for i in 0..count { - if let Some(id) = host_plugin_id_at(i as usize) { - out.push(id); - } - } - out -} - -// --------------------------------------------------------------------------- -// oakengine_plugin_set_active_viewer_provider -// --------------------------------------------------------------------------- - -/// Legal matrix for the active-viewer provider: fn Some/None × userdata -/// ptr/NULL all register (or clear) with `OAKENGINE_OK`. -#[test] -fn active_viewer_provider_register_clear_matrix() { - common::force_link(); - - unsafe extern "C" fn viewer(_userdata: *mut c_void) -> *mut OakEngineNode { - std::ptr::null_mut() - } - let mut userdata = 42i32; - let ud = &mut userdata as *mut i32 as *mut c_void; - - // Some(fn) + userdata. - assert_eq!( - oakengine_plugin_set_active_viewer_provider(Some(viewer), ud), - 0 - ); - // Some(fn) + NULL userdata (userdata is opaque, NULL is legal). - assert_eq!( - oakengine_plugin_set_active_viewer_provider(Some(viewer), std::ptr::null_mut()), - 0 - ); - // None clears (NULL fn), userdata is then ignored but still legal. - assert_eq!(oakengine_plugin_set_active_viewer_provider(None, ud), 0); - assert_eq!( - oakengine_plugin_set_active_viewer_provider(None, std::ptr::null_mut()), - 0 - ); - // Register again and clear, so the process-global state ends neutral. - assert_eq!( - oakengine_plugin_set_active_viewer_provider(Some(viewer), std::ptr::null_mut()), - 0 - ); - assert_eq!( - oakengine_plugin_set_active_viewer_provider(None, std::ptr::null_mut()), - 0 - ); -} - -// --------------------------------------------------------------------------- -// oakengine_plugin_set_progress_reporter_factory -// --------------------------------------------------------------------------- - -/// Legal matrix for the progress-reporter factory: full factory, clear -/// (all NULL), partial registrations and NULL userdata all return -/// `OAKENGINE_OK`. -#[test] -fn progress_reporter_factory_register_clear_matrix() { - common::force_link(); - - unsafe extern "C" fn create( - _m: *const c_char, - _t: *const c_char, - _u: *mut c_void, - ) -> *mut c_void { - std::ptr::null_mut() - } - unsafe extern "C" fn destroy(_r: *mut c_void, _u: *mut c_void) {} - unsafe extern "C" fn is_cancelled(_r: *mut c_void, _u: *mut c_void) -> c_int { - 0 - } - unsafe extern "C" fn set_progress(_r: *mut c_void, _p: f64, _u: *mut c_void) {} - let mut userdata = 7i64; - let ud = &mut userdata as *mut i64 as *mut c_void; - - // Full factory + userdata. - assert_eq!( - oakengine_plugin_set_progress_reporter_factory( - Some(create), - Some(destroy), - Some(is_cancelled), - Some(set_progress), - ud, - ), - 0 - ); - // All-NULL clears (NULL userdata too). - assert_eq!( - oakengine_plugin_set_progress_reporter_factory( - None, - None, - None, - None, - std::ptr::null_mut() - ), - 0 - ); - // Partial registrations are accepted (the facade stores what it gets). - assert_eq!( - oakengine_plugin_set_progress_reporter_factory( - Some(create), - None, - None, - None, - std::ptr::null_mut() - ), - 0 - ); - assert_eq!( - oakengine_plugin_set_progress_reporter_factory(None, Some(destroy), None, None, ud), - 0 - ); - assert_eq!( - oakengine_plugin_set_progress_reporter_factory( - None, - None, - Some(is_cancelled), - None, - std::ptr::null_mut() - ), - 0 - ); - assert_eq!( - oakengine_plugin_set_progress_reporter_factory(None, None, None, Some(set_progress), ud), - 0 - ); - // Back to cleared. - assert_eq!( - oakengine_plugin_set_progress_reporter_factory( - None, - None, - None, - None, - std::ptr::null_mut() - ), - 0 - ); -} - -// --------------------------------------------------------------------------- -// oakengine_plugin_load_plugins -// --------------------------------------------------------------------------- - -/// NULL path → facade `E_INVALID`, never a crash. -#[test] -fn load_plugins_null_path() { - with_host(|| { - common::force_link(); - let before = alive(); - assert_eq!( - unsafe { oakengine_plugin_load_plugins(std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - alive(), - before, - "failed scan must not touch the host registry" - ); - }); -} - -/// Empty string path is a documented no-op (canonicalize fails, not a -/// directory → host returns OK; the C++ host never errors on a missing -/// path). -#[test] -fn load_plugins_empty_string_path() { - with_host(|| { - common::force_link(); - let cs = CString::new("").unwrap(); - let before = alive(); - assert_eq!(unsafe { oakengine_plugin_load_plugins(cs.as_ptr()) }, 0); - assert_eq!(alive(), before); - }); -} - -/// Nonexistent path: silently skipped with OK (olivehost.cpp add_plugin_path -/// semantics), no crash. -#[test] -fn load_plugins_nonexistent_path() { - with_host(|| { - common::force_link(); - let dir = - std::env::temp_dir().join(format!("oak-it-plugin-missing-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&dir); // guarantee absence - let before = alive(); - assert_eq!(scan_facade(&dir), 0); - assert_eq!(alive(), before); - }); -} - -/// A path that points at a regular file (not a directory) is a documented -/// no-op returning OK. -#[test] -fn load_plugins_path_is_a_file() { - with_host(|| { - common::force_link(); - let dir = fresh_temp_dir("file-scan"); - let file = dir.join("not-a-dir"); - std::fs::write(&file, b"hi").unwrap(); - let before = alive(); - assert_eq!(scan_facade(&file), 0); - assert_eq!(alive(), before); - }); -} - -/// Empty directory scans cleanly (OK), changes nothing in the plugin -/// cache, and a repeat scan of the same path is deduplicated (also OK) — -/// the meaningful size=0 / index-range ground state. -#[test] -fn load_plugins_empty_dir_and_dedup() { - with_host(|| { - common::force_link(); - let dir = fresh_temp_dir("empty"); - let count_before = plugin_count(); - let before = alive(); - assert_eq!(scan_facade(&dir), 0); - assert_eq!( - plugin_count(), - count_before, - "empty dir must not register plugins" - ); - // Same path again → dedup no-op, still OK. - assert_eq!(scan_facade(&dir), 0); - assert_eq!(plugin_count(), count_before); - assert_eq!(alive(), before); - }); -} - -/// Unicode path (non-ASCII directory name) scans cleanly. -#[test] -fn load_plugins_unicode_path() { - with_host(|| { - common::force_link(); - let dir = fresh_temp_dir("unicode"); - let unicode = dir.join("插件-目录-β"); - std::fs::create_dir_all(&unicode).unwrap(); - let before = alive(); - assert_eq!(scan_facade(&unicode), 0); - assert_eq!(alive(), before); - }); -} - -/// Non-UTF-8 path bytes: the module rejects them with `OAKPLUGIN_E_INVALID` -/// which passes through the facade untranslated (-90001), never a crash. -#[test] -fn load_plugins_non_utf8_path() { - with_host(|| { - common::force_link(); - let cs = CString::new(&b"/tmp/oak-it-plugin-\xff\xfe"[..]).unwrap(); - let before = alive(); - assert_eq!( - unsafe { oakengine_plugin_load_plugins(cs.as_ptr()) }, - PLUGIN_E_INVALID - ); - assert_eq!(alive(), before); - }); -} - -/// End-to-end legal path: `oakengine_plugin_load_plugins` against a real -/// directory containing the real OFX test plugin bundle registers the -/// plugin in the real host cache (dlopen + setHost + load + describe all -/// run). Verified through the module's own introspection, plus a repeat -/// scan (dedup) and the alive counter. -/// -/// Skips when the test plugin was not built (see module docs). -#[test] -fn load_plugins_real_bundle_end_to_end() { - with_host(|| { - common::force_link(); - // A fresh directory so "scan registers the plugin" is deterministic; - // the env-injected mode falls back to the shared dir. - let Some(dir) = fresh_test_plugin_scan_dir("e2e") else { - println!("SKIP: test plugin bundle unavailable (oakplugin build.rs output missing)"); - return; - }; - let before = alive(); - let ids_before = plugin_ids(); - let had_test_plugin = ids_before.iter().any(|id| id == TEST_PLUGIN_ID); - let count_before = plugin_count(); - - // The facade scan returns OK and — unless the test plugin ids were - // already registered by an earlier scan in this binary (the host - // dedups globally by identifier, so a second scan of the same - // bundle binary is a no-op) — registers the test plugin. - assert_eq!(scan_facade(&dir), 0); - let ids_after = plugin_ids(); - assert!( - ids_after.iter().any(|id| id == TEST_PLUGIN_ID), - "test plugin id must be discoverable after scan (ids: {ids_after:?})" - ); - if !had_test_plugin { - assert!( - plugin_count() > count_before, - "a first scan of the real bundle must register the plugin ({} -> {})", - count_before, - plugin_count() - ); - } - - // Label lookup for a known id resolves (phase 1: the id itself). - 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. - let count_after_first = plugin_count(); - assert_eq!(scan_facade(&dir), 0); - assert_eq!(plugin_count(), count_after_first); - assert_eq!(alive(), before, "scan must not leak host instances"); - }); -} - -// --------------------------------------------------------------------------- -// oakengine_plugin_node_push_button_clicked -// --------------------------------------------------------------------------- - -/// Documented stub: the oakplugin crate has no push-button API (the OFX -/// button-param trigger is C++-only), so every input combination returns -/// `OAKENGINE_E_FAILED` and never reads its arguments. -#[test] -fn push_button_clicked_documented_stub() { - common::force_link(); - // NULL node + NULL button. - assert_eq!( - unsafe { - oakengine_plugin_node_push_button_clicked(std::ptr::null_mut(), std::ptr::null()) - }, - E_FAILED - ); - // NULL node + button id. - assert_eq!( - unsafe { oakengine_plugin_node_push_button_clicked(std::ptr::null_mut(), c"btn".as_ptr()) }, - E_FAILED - ); - // Non-NULL node (empty handle) + NULL button. - let mut node = OakEngineNode { - handle: CHandle::null(), - }; - assert_eq!( - unsafe { oakengine_plugin_node_push_button_clicked(&mut node, std::ptr::null()) }, - E_FAILED - ); - // Non-NULL node + button id. - assert_eq!( - unsafe { oakengine_plugin_node_push_button_clicked(&mut node, c"btn".as_ptr()) }, - E_FAILED - ); -} - -// --------------------------------------------------------------------------- -// Backend destroy contract (the family's only free surface) -// --------------------------------------------------------------------------- - -/// The facade plugin family exports no free/destroy function; its only -/// destroy surface is the backend `oakplugin_instance_free`. Contracts -/// verified against the real host: free(NULL)/free(empty)/double-free are -/// no-ops, unknown ids yield an empty handle (documented), and a real -/// instance create → free round trip restores the module's alive counter -/// to baseline (the leak assertion for this family). -#[test] -fn backend_instance_free_contracts() { - with_host(|| { - common::force_link(); - let base = alive(); - - // free(NULL-equivalent) and free(empty handle) are no-ops. - host_instance_free(&mut None); - let mut empty: Option>> = 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 = 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. - let Some(dir) = test_plugin_scan_dir() else { - println!("SKIP: test plugin bundle unavailable (oakplugin build.rs output missing)"); - return; - }; - assert_eq!(scan_facade(&dir), 0); - let mut inst = host_instance_create(TEST_PLUGIN_ID); - assert!( - inst.is_some(), - "scanned test plugin must create an instance" - ); - assert_eq!(alive(), base + 1, "one live instance must be registered"); - 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. - host_instance_free(&mut inst); - assert_eq!(alive(), base); - }); -} diff --git a/crates/oakengine.bk/src/test_support/it_storage.rs b/crates/oakengine.bk/src/test_support/it_storage.rs deleted file mode 100644 index d855ac91a..000000000 --- a/crates/oakengine.bk/src/test_support/it_storage.rs +++ /dev/null @@ -1,642 +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 . - -//! D2 integration tests: the facade's live write-through (plan M13 §2/§3). -//! -//! End-to-end against real SQLite library files in temp directories, -//! driving the facade exactly like the app: `oakengine_project_new` binds -//! the project, undoable edits (`oakengine_project_add_node`) write -//! through on push, and the journal is verified directly with raw sea-orm -//! reads (the same pattern as the oakstorage database tests) plus fresh -//! oakstorage sessions for cross-process recovery. -//! -//! Coverage: write-through lands journal rows without any flush; a -//! kill-9-style session (no cleanup, a fresh session reading the same -//! file) recovers the last command; undo history persists across sessions -//! (`load_at`); the background snapshot thread writes and prunes -//! snapshots and the exit flush drains; multiple bound projects keep -//! their rows apart; and the graceful-degradation paths (backend off / -//! unwritable library) record `last_error` without breaking the undo -//! stack. -//! -//! Every test holds the shared undo-stack lock (the facade's stack is -//! process-wide, same as the it_undo family) and the storage-config lock -//! (the config store is process-global too), so the suite never races on -//! either singleton. - -use std::path::{Path, PathBuf}; -use std::time::Duration; - -use sea_orm::entity::prelude::*; -use sea_orm::QueryOrder; -use oakstorage::backend::StorageBackend; -use oakstorage::backends::database::entities::{journal, project, snapshot}; -use oakstorage::backends::database::DatabaseBackend; -use oakstorage::error::OAKSTORAGE_OK; -use oakstorage::handle::CHandle; -use oakstorage::nodeutil::project_arc; -use oakstorage::uri::StorageUri; - -use super::common; -use super::it_undo::GLOBAL_STACK_LOCK; - -use crate::handle::OakEngineProject; - -/// The node type the tests add (a real graph node with an input the -/// serializer round-trips). -const MATH: &str = "org.olivevideoeditor.Olive.math"; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/// Serialize a storage test: hold the process-global undo-stack lock AND -/// the storage-config lock for the whole body, then point the write-through -/// backend at a temp library. -fn with_storage(db: &Path, interval: i32, f: impl FnOnce() -> R) -> R { - let _stack = GLOBAL_STACK_LOCK.lock(); - let _config = common::STORAGE_CONFIG_LOCK.lock().unwrap_or_else(|e| e.into_inner()); - let store = oakcommon::configstore::ConfigStore::instance(); - store.set(Some("Storage"), "Backend", "sqlite"); - store.set(Some("Storage"), "SqlitePath", &db.to_string_lossy()); - store.set_int(Some("Storage"), "SnapshotIntervalSec", interval); - f() -} - -/// Serialize a storage test with the backend disabled (`Storage/Backend = -/// "off"`): projects bind to nothing and the undo stack stays untouched by -/// write-throughs. -fn with_storage_off(f: impl FnOnce() -> R) -> R { - let _stack = GLOBAL_STACK_LOCK.lock(); - let _config = common::STORAGE_CONFIG_LOCK.lock().unwrap_or_else(|e| e.into_inner()); - oakcommon::configstore::ConfigStore::instance().set(Some("Storage"), "Backend", "off"); - f() -} - -/// A fresh, unique temp directory for one test. -fn temp_dir(tag: &str) -> PathBuf { - let dir = - std::env::temp_dir().join(format!("oakengine_storage_{}_{}", std::process::id(), tag)); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).unwrap(); - dir -} - -/// `oakdb+sqlite:///…` uri for a database file. -fn db_uri(path: &Path) -> String { - format!("oakdb+sqlite://{}", path.display()) -} - -/// `…?project=` uri selecting one library row. -fn project_uri(db: &str, uuid: &str) -> String { - format!("{db}?project={uuid}") -} - -/// Release an owned handle (refcount 1). -fn release(h: CHandle) { - if let Some(release) = h.release { - unsafe { release(h.ctx) }; - } -} - -/// Create a project through the facade and initialize it (bind + active). -fn new_project() -> *mut OakEngineProject { - let project = unsafe { crate::node::oakengine_project_create() }; - assert!(!project.is_null()); - assert_eq!(unsafe { crate::node::oakengine_project_new(project) }, 0); - project -} - -/// The project's uuid (from its in-memory payload). -fn project_uuid(project: *mut OakEngineProject) -> String { - let h = unsafe { crate::handle::unbox(project) }.expect("project handle"); - let arc = unsafe { crate::handle::domain::project_of(&h) }.expect("project payload"); - let guard = arc.lock().unwrap_or_else(|e| e.into_inner()); - guard.uuid.clone() -} - -/// Add a math node (an undoable command; pushes and write-throughs). -fn add_math_node(project: *mut OakEngineProject) -> *mut crate::handle::OakEngineNode { - unsafe { - crate::node::oakengine_project_add_node( - project, - c"org.olivevideoeditor.Olive.math".as_ptr(), - ) - } -} - -/// Load the head state through a *fresh* database backend (a new -/// connection pool = a new session, as after a process restart). -fn load_head(uri: &str) -> std::sync::Arc> { - let parsed = StorageUri::parse(uri).unwrap(); - let result = DatabaseBackend::new().load(&parsed).unwrap(); - assert_eq!(result.version_info, OAKSTORAGE_OK); - let handle = result.project; - let loaded = unsafe { project_arc(&handle) }.unwrap(); - release(handle); - loaded -} - -/// Load the state at `seq` through a fresh database backend. -fn load_at(uri: &str, uuid: &str, seq: i64) -> std::sync::Arc> { - let parsed = StorageUri::parse(uri).unwrap(); - let handle = DatabaseBackend::new() - .load_at(&parsed, uuid, seq) - .unwrap(); - let loaded = unsafe { project_arc(&handle) }.unwrap(); - release(handle); - loaded -} - -/// Count the math nodes of a loaded project (the root folder is slot 0). -fn math_count(p: &oaknode::project::Project) -> usize { - p.graph - .node_ids() - .into_iter() - .filter(|id| { - p.graph - .get(*id) - .map(|e| e.behavior.type_id() == MATH) - .unwrap_or(false) - }) - .count() -} - -/// Open a raw sea-orm connection to the library file and drive one future -/// against it on a private current-thread runtime (inspection behind the -/// backend's back — same pattern as the oakstorage database tests). -fn inspect_db(path: &Path, f: impl FnOnce(sea_orm::DatabaseConnection) -> Fut) -> R -where - Fut: std::future::Future, -{ - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap(); - rt.block_on(async { - let options = sea_orm::sqlx::sqlite::SqliteConnectOptions::new() - .filename(path) - .create_if_missing(true) - .journal_mode(sea_orm::sqlx::sqlite::SqliteJournalMode::Wal) - .busy_timeout(Duration::from_secs(5)) - .foreign_keys(true); - let pool = sea_orm::sqlx::sqlite::SqlitePoolOptions::new() - .max_connections(1) - .connect_with(options) - .await - .unwrap(); - f(sea_orm::DatabaseConnection::from(pool)).await - }) -} - -/// The `(command_seq, journal rows)` of a library row. -fn journal_rows( - path: &Path, - uuid: &str, -) -> (i64, Vec) { - let uuid = uuid.to_string(); - inspect_db(path, move |conn| async move { - let proj = project::Entity::find() - .filter(project::Column::Uuid.eq(&uuid)) - .one(&conn) - .await - .unwrap() - .expect("library row exists"); - let rows = journal::Entity::find() - .filter(journal::Column::ProjectId.eq(proj.id)) - .order_by_asc(journal::Column::Seq) - .order_by_asc(journal::Column::NodeIdentity) - .all(&conn) - .await - .unwrap(); - (proj.command_seq, rows) - }) -} - -/// The snapshot seqs of a library row (newest first). -fn snapshot_seqs(path: &Path, uuid: &str) -> Vec { - let uuid = uuid.to_string(); - inspect_db(path, move |conn| async move { - let proj = project::Entity::find() - .filter(project::Column::Uuid.eq(&uuid)) - .one(&conn) - .await - .unwrap() - .expect("library row exists"); - snapshot::Entity::find() - .filter(snapshot::Column::ProjectId.eq(proj.id)) - .order_by_desc(snapshot::Column::CommandSeq) - .all(&conn) - .await - .unwrap() - .into_iter() - .map(|s| s.command_seq) - .collect() - }) -} - -// --------------------------------------------------------------------------- -// Write-through -// --------------------------------------------------------------------------- - -/// Every undoable edit lands in the journal synchronously (no flush): the -/// first edit is the import command, later edits are diffs. -#[test] -fn write_through_persists_commands() { - common::force_link(); - let dir = temp_dir("wt"); - let db = dir.join("lib.db"); - with_storage(&db, 600, || { - let project = new_project(); - let node1 = add_math_node(project); - assert!(!node1.is_null()); - let node2 = add_math_node(project); - assert!(!node2.is_null()); - let uuid = project_uuid(project); - - let (head, rows) = journal_rows(&db, &uuid); - assert_eq!(head, 2, "two commands written through"); - // Seq 1 (import): the root folder + the first math node + the - // settings pseudo-node, all with only after-images. - let seq1: Vec<_> = rows.iter().filter(|r| r.seq == 1).collect(); - assert_eq!(seq1.len(), 3, "root + first math node + settings row"); - assert!(seq1.iter().all(|r| r.kind == "import")); - assert!(seq1.iter().all(|r| r.old_xml.is_none() && r.new_xml.is_some())); - // Seq 2 (redo diff): exactly the second math node. - let seq2: Vec<_> = rows.iter().filter(|r| r.seq == 2).collect(); - assert_eq!(seq2.len(), 1, "one changed node in the diff"); - assert_eq!(seq2[0].kind, "redo"); - assert!(seq2[0].new_xml.as_deref().unwrap().contains(MATH)); - - // The head state reads back through a fresh session (3 nodes: root - // + two math nodes), and the import point has exactly one math node. - let uri = db_uri(&db); - let head_loaded = load_head(&project_uri(&uri, &uuid)); - let guard = head_loaded.lock().unwrap(); - assert_eq!(guard.graph.node_count(), 3); - assert_eq!(math_count(&guard), 2); - drop(guard); - let at1 = load_at(&uri, &uuid, 1); - let guard = at1.lock().unwrap(); - assert_eq!(guard.graph.node_count(), 2); - assert_eq!(math_count(&guard), 1); - - unsafe { crate::node::oakengine_project_free(project) }; - }); - let _ = std::fs::remove_dir_all(&dir); -} - -/// kill -9 recovery: the write-through is synchronous, so a fresh session -/// reading the same file (no flush, no cleanup) sees the state after the -/// LAST command. -#[test] -fn kill_nine_recovers_last_command() { - common::force_link(); - let dir = temp_dir("k9"); - let db = dir.join("lib.db"); - with_storage(&db, 600, || { - let project = new_project(); - for _ in 0..3 { - let node = add_math_node(project); - assert!(!node.is_null()); - } - let uuid = project_uuid(project); - - // NOTE: the project is intentionally NOT freed — the process "dies" - // here. The journal already holds every command. - let uri = db_uri(&db); - let (head, rows) = journal_rows(&db, &uuid); - assert_eq!(head, 3); - assert_eq!(rows.len(), 5, "3 import rows + 2 diff rows"); - - let loaded = load_head(&project_uri(&uri, &uuid)); - let guard = loaded.lock().unwrap(); - assert_eq!(guard.graph.node_count(), 4, "root + three math nodes"); - assert_eq!(math_count(&guard), 3); - - // Cleanup without flush would leave the file behind; free the - // project so the temp dir can be removed. - unsafe { crate::node::oakengine_project_free(project) }; - }); - let _ = std::fs::remove_dir_all(&dir); -} - -// --------------------------------------------------------------------------- -// Undo across sessions -// --------------------------------------------------------------------------- - -/// The journal is the persistent undo history: three commands, one undo, -/// then a NEW session's `load_at(2)` reproduces the post-command-2 state -/// while `load_at(3)` still yields the pre-undo (post-command-3) state. -/// -/// The undoable operations are label renames: their undo closure captures -/// `(project, id)` and reliably reverts, unlike the add-node command -/// (whose factory handle is released at push — a facade limitation). -#[test] -fn undo_history_crosses_sessions() { - common::force_link(); - let dir = temp_dir("ux"); - let db = dir.join("lib.db"); - with_storage(&db, 600, || { - let project = new_project(); - // Command 1: add a math node (import). - let node = add_math_node(project); - assert!(!node.is_null()); - // Command 2: rename to "Alpha". - assert_eq!( - unsafe { crate::node::oakengine_node_set_label(node, c"Alpha".as_ptr()) }, - 0 - ); - // Command 3: rename to "Beta". - assert_eq!( - unsafe { crate::node::oakengine_node_set_label(node, c"Beta".as_ptr()) }, - 0 - ); - let uuid = project_uuid(project); - - // Undo one command through the facade (jump + write-through). - assert_eq!(unsafe { crate::node::oakengine_project_undo(project) }, 0); - - let uri = db_uri(&db); - let (head, _) = journal_rows(&db, &uuid); - assert_eq!(head, 4, "the undo itself is a written command"); - - // A new session at seq 2 = the state right after command 2. - let at2 = load_at(&uri, &uuid, 2); - assert_eq!(math_label(&at2), "Alpha"); - // At seq 3 the pre-undo state (after command 3) is intact. - let at3 = load_at(&uri, &uuid, 3); - assert_eq!(math_label(&at3), "Beta"); - // The head (default load) matches the undone state. - let head_loaded = load_head(&project_uri(&uri, &uuid)); - assert_eq!(math_label(&head_loaded), "Alpha"); - - unsafe { crate::node::oakengine_project_free(project) }; - }); - let _ = std::fs::remove_dir_all(&dir); -} - -/// The label of the first math node of a loaded project (its renames are -/// the undoable operations of [`undo_history_crosses_sessions`]). -fn math_label(arc: &std::sync::Arc>) -> String { - let guard = arc.lock().unwrap(); - let id = guard - .graph - .node_ids() - .into_iter() - .find(|id| { - guard - .graph - .get(*id) - .map(|e| e.behavior.type_id() == MATH) - .unwrap_or(false) - }) - .expect("a math node is present"); - guard.graph.get(id).unwrap().core.label.clone() -} - -// --------------------------------------------------------------------------- -// Snapshot thread -// --------------------------------------------------------------------------- - -/// The background snapshot thread writes periodic snapshots of dirty -/// projects (short interval), the backend prunes to the newest three, and -/// the exit flush drains and leaves a snapshot behind. -#[test] -fn snapshot_thread_and_exit_flush() { - common::force_link(); - let dir = temp_dir("snap"); - let db = dir.join("lib.db"); - with_storage(&db, 1, || { - let project = new_project(); - let uuid = project_uuid(project); - - // Four command batches ~1.1 s apart (the 1 s interval): each batch - // is a rapid pair of writes so the save-time snapshot policy stays - // quiet and the THREAD is the one capturing the new head. - for _ in 0..4 { - let a = add_math_node(project); - assert!(!a.is_null()); - let b = add_math_node(project); - assert!(!b.is_null()); - std::thread::sleep(Duration::from_millis(1100)); - } - // One more tick window so the final batch is snapshotted too. - std::thread::sleep(Duration::from_millis(1500)); - - let seqs = snapshot_seqs(&db, &uuid); - assert!(!seqs.is_empty(), "the thread produced snapshots"); - assert!( - seqs.len() <= 3, - "pruning keeps at most three (got {:?})", - seqs - ); - let (head, _) = journal_rows(&db, &uuid); - assert_eq!(head, 8, "eight commands written through"); - - // Exit flush: drains (save + snapshot) and leaves the snapshot at - // the head seq regardless of thread timing. - assert_eq!(crate::storage::oakengine_storage_flush(), 0); - let seqs = snapshot_seqs(&db, &uuid); - assert!(!seqs.is_empty(), "flush leaves a snapshot behind"); - assert_eq!(seqs[0], 8, "the newest snapshot covers the head seq"); - - unsafe { crate::node::oakengine_project_free(project) }; - }); - let _ = std::fs::remove_dir_all(&dir); -} - -/// A dirty project flushed on close (project_free) gets its head snapshot -/// even with a long (default) interval — the flush, not the thread, is the -/// guaranteed drain. -#[test] -fn close_flushes_snapshot() { - common::force_link(); - let dir = temp_dir("flush"); - let db = dir.join("lib.db"); - with_storage(&db, 600, || { - let project = new_project(); - let uuid = project_uuid(project); - let node = add_math_node(project); - assert!(!node.is_null()); - let node2 = add_math_node(project); - assert!(!node2.is_null()); - - // With the 600 s interval only the FIRST save snapshotted (the - // backend's policy fires when no snapshot exists yet), so the head - // seq is not covered. - assert_eq!(snapshot_seqs(&db, &uuid), vec![1]); - - // Closing the project flushes: write-through + snapshot at the head. - unsafe { crate::node::oakengine_project_free(project) }; - - let seqs = snapshot_seqs(&db, &uuid); - assert_eq!(seqs[0], 2, "close flushed a snapshot at the head seq"); - }); - let _ = std::fs::remove_dir_all(&dir); -} - -// --------------------------------------------------------------------------- -// Multi-project bindings -// --------------------------------------------------------------------------- - -/// Two projects bound to one library keep their rows apart: each project's -/// writes advance only its own journal. -#[test] -fn multi_project_bindings_do_not_cross() { - common::force_link(); - let dir = temp_dir("multi"); - let db = dir.join("lib.db"); - with_storage(&db, 600, || { - // Project A: two commands. - let a = new_project(); - for _ in 0..2 { - let node = add_math_node(a); - assert!(!node.is_null()); - } - let uuid_a = project_uuid(a); - - // Project B: binding it does not disturb A's row; its write goes - // only to B's row (A's saves during B's commands are no-ops). - let b = new_project(); - let node = add_math_node(b); - assert!(!node.is_null()); - let uuid_b = project_uuid(b); - assert_ne!(uuid_a, uuid_b); - - let (head_a, rows_a) = journal_rows(&db, &uuid_a); - assert_eq!(head_a, 2, "A's journal stops at its own second command"); - assert!(rows_a.iter().all(|r| r.seq <= 2)); - let (head_b, rows_b) = journal_rows(&db, &uuid_b); - assert_eq!(head_b, 1, "B has exactly its one command"); - assert!(rows_b.iter().all(|r| r.seq == 1)); - - // Both projects load correctly through fresh sessions. - let uri = db_uri(&db); - let loaded_a = load_head(&project_uri(&uri, &uuid_a)); - let guard = loaded_a.lock().unwrap(); - assert_eq!(math_count(&guard), 2); - drop(guard); - let loaded_b = load_head(&project_uri(&uri, &uuid_b)); - let guard = loaded_b.lock().unwrap(); - assert_eq!(math_count(&guard), 1); - - unsafe { crate::node::oakengine_project_free(a) }; - unsafe { crate::node::oakengine_project_free(b) }; - }); - let _ = std::fs::remove_dir_all(&dir); -} - -// --------------------------------------------------------------------------- -// Graceful degradation -// --------------------------------------------------------------------------- - -/// With `Storage/Backend = "off"` projects bind to nothing: the undo -/// stack works, no library file is created, and `is_bound` reports 0. -#[test] -fn backend_off_keeps_projects_unbound() { - common::force_link(); - let dir = temp_dir("off"); - let db = dir.join("lib.db"); - with_storage_off(|| { - let project = new_project(); - // Not bound: no write-through happens at all. - assert_eq!(unsafe { crate::storage::oakengine_storage_is_bound(project) }, 0); - - let node = add_math_node(project); - assert!(!node.is_null()); - let node2 = add_math_node(project); - assert!(!node2.is_null()); - - // The undo stack still works. - assert_eq!(unsafe { crate::node::oakengine_project_undo(project) }, 0); - assert_eq!(unsafe { crate::node::oakengine_project_redo(project) }, 0); - - // Nothing was ever written to the configured library path. - assert!(!db.exists(), "no library file with the backend off"); - - unsafe { crate::node::oakengine_project_free(project) }; - }); - let _ = std::fs::remove_dir_all(&dir); -} - -/// An unwritable library degrades gracefully: the write-through records a -/// `last_error` instead of failing the command or crashing, and the undo -/// stack keeps working. -#[test] -fn unwritable_library_records_last_error() { - common::force_link(); - let dir = temp_dir("ro"); - let db = dir.join("no/such/dir/lib.db"); // parent dir does not exist - with_storage(&db, 600, || { - let project = new_project(); - // Bound, but the first write-through cannot open the library. - assert_eq!(unsafe { crate::storage::oakengine_storage_is_bound(project) }, 1); - - let node = add_math_node(project); - assert!(!node.is_null()); - - // The command succeeded; the write failure is recorded, not raised. - let mut buf = [0 as std::ffi::c_char; 512]; - let len = unsafe { crate::storage::oakengine_storage_last_error(project, buf.as_mut_ptr(), 512) }; - assert!(len > 0, "the failed write-through is recorded"); - let msg = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } - .to_string_lossy() - .into_owned(); - assert!(!msg.is_empty(), "error message non-empty"); - - // A second command degrades the same way. - let node2 = add_math_node(project); - assert!(!node2.is_null()); - let len = - unsafe { crate::storage::oakengine_storage_last_error(project, buf.as_mut_ptr(), 512) }; - assert!(len > 0); - - // The undo stack is unaffected. - assert_eq!(unsafe { crate::node::oakengine_project_undo(project) }, 0); - - unsafe { crate::node::oakengine_project_free(project) }; - }); - let _ = std::fs::remove_dir_all(&dir); -} - -// --------------------------------------------------------------------------- -// Defaults -// --------------------------------------------------------------------------- - -/// The default library path resolves to `/library.db` -/// (absolute), and the backend is strictly config-driven: `Backend = -/// "sqlite"` enables it, `"off"` disables it (and an absent `Storage` -/// group means "no library configured" — projects stay unbound). -#[test] -fn default_library_path_and_backend() { - common::force_link(); - let _stack = GLOBAL_STACK_LOCK.lock(); - let _config = common::STORAGE_CONFIG_LOCK.lock().unwrap_or_else(|e| e.into_inner()); - - // The default path is a plain absolute `…/library.db`. - let p = crate::storage::default_library_path(); - let path = std::path::Path::new(&p); - assert!(path.is_absolute(), "{p}"); - assert!(p.ends_with("library.db"), "{p}"); - - // Explicit values drive the enabled state. - let store = oakcommon::configstore::ConfigStore::instance(); - store.set(Some("Storage"), "Backend", "sqlite"); - assert!(crate::storage::storage_enabled()); - store.set(Some("Storage"), "Backend", "off"); - assert!(!crate::storage::storage_enabled()); -} diff --git a/crates/oakengine.bk/src/test_support/it_task.rs b/crates/oakengine.bk/src/test_support/it_task.rs deleted file mode 100644 index 98f415c22..000000000 --- a/crates/oakengine.bk/src/test_support/it_task.rs +++ /dev/null @@ -1,1252 +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 . - -//! Integration tests for the **task family** (`src/task.rs`, the -//! `oakengine_task_*` C ABI; module contract `include/task/*.h`). -//! -//! Coverage rules (see the family test charter): -//! 1. no mocks — every call goes through the real facade into the real -//! oaktask/oaknode/oakundo/oakcodec module crates (the -//! `oakcore_audioparams_*` accessors the facade reads through are its -//! own in-dylib implementations, re-exported by `tests/common`, the -//! same mechanism the other family tests use); -//! 2. every one of the 27 `oakengine_task_*` / `oakengine_cli_task_*` -//! exports is exercised on a legal path with the result asserted; -//! 3. legal-input matrix (compression flags, url counts, indices, buffer -//! sizes) covers the meaningful combinations; -//! 4. illegal inputs (NULL, empty handles, out-of-range indices, zero / -//! negative sizes, garbage flag values, wrong-family handles) always -//! yield a negative error code or a documented NULL/0 no-op — never a -//! crash; -//! 5. free contracts: free(NULL) and free(empty-handle) are clean error -//! no-ops, and `oaktask_debug_alive_count()` returns to baseline. -//! -//! ## Serialization -//! -//! Two process-wide states force the tests into one thread: the facade's -//! lazily-created global task manager and the global undo stack -//! (`oakengine_project_new` clears it). Additionally the alive-count -//! assertions measure a process-wide module counter, so every test takes a -//! shared [`SERIAL`] mutex (the same pattern as the oaktask crate's own -//! `MANAGER_LOCK`). -//! -//! ## Ignored tests -//! -//! (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`].) - -use super::common; - -use std::ffi::{c_char, c_int, c_void}; -use std::sync::Mutex; - -use crate::codec::{oakengine_encoding_params_create, oakengine_encoding_params_set_filename}; -use crate::handle::{ - free_box, CHandle, OakEngineNode, OakEngineProject, OakEngineSequence, OakEngineTask, -}; -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 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, - oakengine_task_create_project_save_otio, oakengine_task_create_proxy, oakengine_task_error, - 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_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 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; -const OAKTASK_E_STATE: c_int = -80002; -const OAKTASK_E_NOT_FOUND: c_int = -80004; - -/// 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(()); - -/// Both lock guards held by [`serial`] (the local task lock plus the -/// facade-wide undo-stack lock). -pub(crate) struct SerialGuard { - /// The [`SERIAL`] lock. - _task: std::sync::MutexGuard<'static, ()>, - /// The facade's process-wide undo-stack lock (it_undo's), so the - /// `oakengine_project_new` calls in these tests (which clear the stack) - /// never race the it_undo / it_storage stack tests. - _stack: parking_lot::ReentrantMutexGuard<'static, ()>, -} - -/// Take the [`SERIAL`] lock AND the global undo-stack lock, recovering -/// from any poisoning. -pub(crate) fn serial() -> SerialGuard { - let _task = SERIAL.lock().unwrap_or_else(|e| e.into_inner()); - let _stack = super::it_undo::GLOBAL_STACK_LOCK - .lock(); - SerialGuard { _task, _stack } -} - -/// 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 { - crate::stubs::task::oaktask_debug_alive_count() -} - -/// Read a NUL-terminated two-stage buffer as a Rust `String`. -fn read_buf(buf: &[c_char]) -> String { - 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() -} - -/// A facade task box wrapping an EMPTY module handle (`ctx == NULL`), the -/// "empty handle" state the C contract documents as invalid input. -fn empty_task_box() -> *mut OakEngineTask { - Box::into_raw(Box::new(OakEngineTask { - handle: CHandle::null(), - })) -} - -/// Reclaim a facade box that `oakengine_task_free` refused to consume -/// (NULL/empty handles are errors, so the box stays allocated). -/// -/// # Safety -/// `ptr` must be a box produced by [`empty_task_box`] that was never freed. -unsafe fn reclaim_empty_task_box(ptr: *mut OakEngineTask) { - unsafe { drop(Box::from_raw(ptr)) }; -} - -// --------------------------------------------------------------------------- -// NULL / empty-handle rejection (all 27 exports) -// --------------------------------------------------------------------------- - -/// Every accessor rejects a NULL task with OAKENGINE_E_INVALID (-1) and -/// every pointer accessor returns NULL; creators return NULL for NULL / -/// invalid arguments; the CLI dialog returns 0 (the capi's "no task" is -/// not an error). -#[test] -fn null_handles_are_rejected() { - let _g = serial(); - common::force_link(); - - let mut buf = [0 as c_char; 256]; - - // ---- manager family ----------------------------------------------------- - assert_eq!( - unsafe { oakengine_task_manager_add(std::ptr::null_mut()) }, - -1 - ); - assert_eq!( - unsafe { oakengine_task_manager_cancel(std::ptr::null_mut()) }, - -1 - ); - - // ---- task accessors ----------------------------------------------------- - assert_eq!( - unsafe { oakengine_task_title(std::ptr::null_mut(), buf.as_mut_ptr(), 256) }, - -1 - ); - assert_eq!( - unsafe { oakengine_task_error(std::ptr::null_mut(), buf.as_mut_ptr(), 256) }, - -1 - ); - assert_eq!( - unsafe { oakengine_task_start_time(std::ptr::null_mut()) }, - -1 - ); - assert_eq!( - unsafe { oakengine_task_is_cancelled(std::ptr::null_mut()) }, - -1 - ); - assert_eq!(unsafe { oakengine_task_cancel(std::ptr::null_mut()) }, -1); - assert_eq!( - unsafe { oakengine_task_start_sync(std::ptr::null_mut()) }, - -1 - ); - assert_eq!(unsafe { oakengine_task_free(std::ptr::null_mut()) }, -1); - - // ---- import / save result accessors ------------------------------------ - assert_eq!( - unsafe { oakengine_task_import_file_count(std::ptr::null_mut()) }, - -1 - ); - assert!(unsafe { oakengine_task_import_get_command(std::ptr::null_mut()) }.is_null()); - assert_eq!( - unsafe { oakengine_task_import_footage_count(std::ptr::null_mut()) }, - -1 - ); - assert!(unsafe { oakengine_task_import_footage_at(std::ptr::null_mut(), 0) }.is_null()); - assert_eq!( - unsafe { oakengine_task_import_invalid_files_count(std::ptr::null_mut()) }, - -1 - ); - assert_eq!( - unsafe { - oakengine_task_import_invalid_file_at(std::ptr::null_mut(), 0, buf.as_mut_ptr(), 256) - }, - -1 - ); - assert!(unsafe { oakengine_task_save_get_project(std::ptr::null_mut()) }.is_null()); - - // ---- creators ------------------------------------------------------------ - assert!(unsafe { oakengine_task_create_project_load(std::ptr::null()) }.is_null()); - assert!(unsafe { oakengine_task_create_project_load_otio(std::ptr::null()) }.is_null()); - assert!(unsafe { - oakengine_task_create_project_save( - std::ptr::null_mut(), - 0, - std::ptr::null(), - std::ptr::null(), - ) - } - .is_null()); - assert!(unsafe { oakengine_task_create_project_save_otio(std::ptr::null_mut()) }.is_null()); - assert!(unsafe { - oakengine_task_create_project_import(std::ptr::null_mut(), std::ptr::null(), 0) - } - .is_null()); - assert!(unsafe { oakengine_task_create_proxy(std::ptr::null_mut()) }.is_null()); - assert!( - unsafe { oakengine_task_create_export(std::ptr::null_mut(), std::ptr::null_mut()) } - .is_null() - ); - - // ---- CLI dialog (0, not E_INVALID, for NULL) ---------------------------- - assert_eq!( - unsafe { oakengine_cli_task_dialog_run(std::ptr::null_mut(), std::ptr::null_mut()) }, - 0 - ); - - // The manager handle is lazily created and never NULL (documented). - assert!(!oakengine_task_manager_handle().is_null()); - // Finished tasks can linger in the process-wide manager queue (it has no - // delete-finished export and other tests in this binary add to it), so - // only assert that NULL-handle manager operations change nothing. - let count = oakengine_task_manager_count(); - assert_eq!( - unsafe { oakengine_task_manager_add(std::ptr::null_mut()) }, - -1 - ); - assert_eq!( - unsafe { oakengine_task_manager_cancel(std::ptr::null_mut()) }, - -1 - ); - assert_eq!(oakengine_task_manager_count(), count); -} - -/// Empty handles (`ctx == NULL` boxes) are rejected exactly like NULL: -/// -1 / NULL from every accessor and creator, and `oakengine_task_free` -/// reports E_INVALID without consuming the box. -#[test] -fn empty_handles_are_rejected() { - let _g = serial(); - common::force_link(); - - let mut buf = [0 as c_char; 256]; - - // ---- task accessors on an empty-handle box ------------------------------ - let t = empty_task_box(); - assert_eq!( - unsafe { oakengine_task_title(t, buf.as_mut_ptr(), 256) }, - -1 - ); - assert_eq!( - unsafe { oakengine_task_error(t, buf.as_mut_ptr(), 256) }, - -1 - ); - assert_eq!(unsafe { oakengine_task_start_time(t) }, -1); - assert_eq!(unsafe { oakengine_task_is_cancelled(t) }, -1); - assert_eq!(unsafe { oakengine_task_cancel(t) }, -1); - assert_eq!(unsafe { oakengine_task_start_sync(t) }, -1); - assert_eq!(unsafe { oakengine_task_manager_add(t) }, -1); - assert_eq!(unsafe { oakengine_task_manager_cancel(t) }, -1); - assert_eq!(unsafe { oakengine_task_import_file_count(t) }, -1); - assert!(unsafe { oakengine_task_import_get_command(t) }.is_null()); - assert_eq!(unsafe { oakengine_task_import_footage_count(t) }, -1); - assert!(unsafe { oakengine_task_import_footage_at(t, 0) }.is_null()); - assert_eq!(unsafe { oakengine_task_import_invalid_files_count(t) }, -1); - assert_eq!( - unsafe { oakengine_task_import_invalid_file_at(t, 0, buf.as_mut_ptr(), 256) }, - -1 - ); - assert!(unsafe { oakengine_task_save_get_project(t) }.is_null()); - - // free refuses the empty handle with E_INVALID and leaves the box - // allocated (the caller still owns it). - assert_eq!(unsafe { oakengine_task_free(t) }, -1); - unsafe { reclaim_empty_task_box(t) }; - - // ---- creators with empty project / node / sequence handles -------------- - let empty_project = Box::into_raw(Box::new(OakEngineProject { - handle: CHandle::null(), - })); - assert!(unsafe { - oakengine_task_create_project_save(empty_project, 0, std::ptr::null(), std::ptr::null()) - } - .is_null()); - assert!(unsafe { oakengine_task_create_project_save_otio(empty_project) }.is_null()); - unsafe { drop(Box::from_raw(empty_project)) }; - - let empty_node = Box::into_raw(Box::new(OakEngineNode { - handle: CHandle::null(), - })); - assert!( - unsafe { oakengine_task_create_project_import(empty_node, std::ptr::null(), 0) }.is_null() - ); - assert!(unsafe { oakengine_task_create_proxy(empty_node) }.is_null()); - unsafe { drop(Box::from_raw(empty_node)) }; - - let empty_seq = Box::into_raw(Box::new(OakEngineSequence { - handle: CHandle::null(), - })); - let params = oakengine_encoding_params_create(); - 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 { crate::codec::oakengine_encoding_params_destroy(params) }; - unsafe { drop(Box::from_raw(empty_seq)) }; -} - -// --------------------------------------------------------------------------- -// Task lifecycle: load tasks (no project, no manager state) -// --------------------------------------------------------------------------- - -/// A project-load task with a bad filename: created, has the "Loading" -/// title, fails synchronously (0), reports a non-empty error, exposes the -/// facade-side start stamp after starting, and round-trips the cancel flag. -#[test] -fn load_task_lifecycle() { - let _g = serial(); - common::force_link(); - - let task = unsafe { oakengine_task_create_project_load(c"/no/such/oak/project.ove".as_ptr()) }; - assert!(!task.is_null()); - - let mut buf = [0 as c_char; 256]; - let len = unsafe { oakengine_task_title(task, buf.as_mut_ptr(), 256) }; - assert!(len > 0); - assert!(read_buf(&mut buf).contains("Loading")); - - // A task that never ran has no start stamp and is not cancelled. - assert_eq!(unsafe { oakengine_task_start_time(task) }, 0); - assert_eq!(unsafe { oakengine_task_is_cancelled(task) }, 0); - - // The sync run fails (file does not exist). - assert_eq!(unsafe { oakengine_task_start_sync(task) }, 0); - assert_ne!(unsafe { oakengine_task_start_time(task) }, 0); - assert_eq!(unsafe { oakengine_task_is_cancelled(task) }, 0); - - // The error string is populated by the failed run. - let elen = unsafe { oakengine_task_error(task, buf.as_mut_ptr(), 256) }; - assert!(elen > 0); - assert!(!read_buf(&mut buf).is_empty()); - - // Cancel round-trip through the facade flag (module cancel succeeds). - assert_eq!(unsafe { oakengine_task_cancel(task) }, 0); - assert_eq!(unsafe { oakengine_task_is_cancelled(task) }, 1); - - // Re-running after a failed sync run is a legal no-op (fails again). - assert_eq!(unsafe { oakengine_task_start_sync(task) }, 0); - - assert_eq!(unsafe { oakengine_task_free(task) }, 0); -} - -/// The empty filename is legal input: a task is created with an empty -/// title suffix and fails when run (no such file). -#[test] -fn load_task_empty_filename() { - let _g = serial(); - common::force_link(); - - let task = unsafe { oakengine_task_create_project_load(c"".as_ptr()) }; - assert!(!task.is_null()); - - let mut buf = [0 as c_char; 256]; - let len = unsafe { oakengine_task_title(task, buf.as_mut_ptr(), 256) }; - assert_eq!(len, 10); - assert_eq!(read_buf(&mut buf), "Loading ''"); - - assert_eq!(unsafe { oakengine_task_start_sync(task) }, 0); - - assert_eq!(unsafe { oakengine_task_free(task) }, 0); -} - -/// The OTIO load task: created for a valid filename, fails synchronously -/// (the document does not exist) and reports the load error. -#[test] -fn load_otio_task_lifecycle() { - let _g = serial(); - common::force_link(); - - let task = - unsafe { oakengine_task_create_project_load_otio(c"/no/such/oak/project.otio".as_ptr()) }; - assert!(!task.is_null()); - - let mut buf = [0 as c_char; 256]; - let len = unsafe { oakengine_task_title(task, buf.as_mut_ptr(), 256) }; - assert!(len > 0); - assert!(read_buf(&mut buf).contains("Loading")); - - // Missing document -> failed run. - assert_eq!(unsafe { oakengine_task_start_sync(task) }, 0); - let elen = unsafe { oakengine_task_error(task, buf.as_mut_ptr(), 256) }; - assert!(elen > 0); - assert!(!read_buf(&mut buf).is_empty()); - - // An unknown extension is also a clean failure (format dispatch error). - let task2 = - unsafe { oakengine_task_create_project_load_otio(c"/no/such/oak/project.xyz".as_ptr()) }; - assert!(!task2.is_null()); - assert_eq!(unsafe { oakengine_task_start_sync(task2) }, 0); - - assert_eq!(unsafe { oakengine_task_free(task2) }, 0); - assert_eq!(unsafe { oakengine_task_free(task) }, 0); -} - -/// Two-stage string getters across the buffer-size matrix: a too-small or -/// NULL buffer still reports the length, an exact/large buffer also gets -/// the NUL-terminated content. -#[test] -fn string_getters_buffer_matrix() { - let _g = serial(); - common::force_link(); - - let task = - unsafe { oakengine_task_create_project_load(c"/no/such/oak/it_task_buf.ove".as_ptr()) }; - assert!(!task.is_null()); - - // Title length (facade convention: excludes the NUL). - let expected = unsafe { oakengine_task_title(task, std::ptr::null_mut(), 0) }; - let expected_usize = expected as usize; - assert!(expected > 0); - - // NULL buffer with a positive size still reports the length. - assert_eq!( - unsafe { oakengine_task_title(task, std::ptr::null_mut(), 256) }, - expected - ); - // A negative size is a documented no-op size query, not an error. - assert_eq!( - unsafe { oakengine_task_title(task, std::ptr::null_mut(), -5) }, - expected - ); - - // Too-small buffer: length reported, nothing written (module writes only - // when the buffer fits the string plus its NUL). - let mut small = [0 as c_char; 4]; - assert_eq!( - unsafe { oakengine_task_title(task, small.as_mut_ptr(), 4) }, - expected - ); - assert_eq!(small[0], 0); - - // Exact string length but no NUL room: still nothing written. - let mut exact_no_nul = vec![0 as c_char; expected_usize]; - assert_eq!( - unsafe { oakengine_task_title(task, exact_no_nul.as_mut_ptr(), expected) }, - expected - ); - assert_eq!(exact_no_nul[0], 0); - - // Exact length + NUL room: content is written. - let mut exact = vec![0 as c_char; expected_usize + 1]; - assert_eq!( - unsafe { oakengine_task_title(task, exact.as_mut_ptr(), expected + 1) }, - expected - ); - assert_eq!( - read_buf(&exact), - format!("Loading '/no/such/oak/it_task_buf.ove'") - ); - assert_eq!(exact[expected_usize], 0); - - // Large buffer: same content. - let mut big = [0 as c_char; 512]; - assert_eq!( - unsafe { oakengine_task_title(task, big.as_mut_ptr(), 512) }, - expected - ); - assert_eq!( - read_buf(&big), - format!("Loading '/no/such/oak/it_task_buf.ove'") - ); - - // A task that never ran reports "Unknown error" (module fallback), the - // same two-stage contract. - let mut err_buf = [0 as c_char; 64]; - assert_eq!( - unsafe { oakengine_task_error(task, err_buf.as_mut_ptr(), 64) }, - 13 - ); - assert_eq!(read_buf(&err_buf), "Unknown error"); - assert_eq!( - unsafe { oakengine_task_error(task, std::ptr::null_mut(), 0) }, - 13 - ); - - assert_eq!(unsafe { oakengine_task_free(task) }, 0); -} - -/// Wrong-family handles: the import/save result accessors on a plain load -/// task return the module's clean negative codes / NULL — plugins may pass -/// any task handle. -#[test] -fn import_save_accessors_on_wrong_family_task() { - let _g = serial(); - common::force_link(); - - let task = unsafe { oakengine_task_create_project_load(c"/no/such/oak/project.ove".as_ptr()) }; - assert!(!task.is_null()); - - assert_eq!( - unsafe { oakengine_task_import_file_count(task) }, - OAKTASK_E_INVALID - ); - assert_eq!( - unsafe { oakengine_task_import_footage_count(task) }, - OAKTASK_E_INVALID - ); - assert_eq!( - unsafe { oakengine_task_import_invalid_files_count(task) }, - OAKTASK_E_INVALID - ); - assert!(unsafe { oakengine_task_import_get_command(task) }.is_null()); - assert!(unsafe { oakengine_task_import_footage_at(task, 0) }.is_null()); - let mut buf = [0 as c_char; 256]; - assert_eq!( - unsafe { oakengine_task_import_invalid_file_at(task, 0, buf.as_mut_ptr(), 256) }, - OAKTASK_E_INVALID - ); - assert!(unsafe { oakengine_task_save_get_project(task) }.is_null()); - - assert_eq!(unsafe { oakengine_task_free(task) }, 0); -} - -/// The CLI modal dialog is a sync-run wrapper: 0 for a failing task, 0 for -/// NULL, and it marks the task started. -#[test] -fn cli_dialog_runs_task_sync() { - let _g = serial(); - common::force_link(); - - let task = unsafe { oakengine_task_create_project_load(c"/no/such/oak/project.ove".as_ptr()) }; - assert!(!task.is_null()); - assert_eq!( - unsafe { oakengine_cli_task_dialog_run(task, std::ptr::null_mut()) }, - 0 - ); - assert_ne!(unsafe { oakengine_task_start_time(task) }, 0); - assert_eq!(unsafe { oakengine_task_free(task) }, 0); -} - -// --------------------------------------------------------------------------- -// Free contracts and the module alive counter -// --------------------------------------------------------------------------- - -/// `oakengine_task_free`: NULL and empty handles are clean E_INVALID -/// no-ops, a real task frees cleanly, and the module's alive counter -/// returns to baseline after every create/free round trip. -#[test] -fn free_contracts_and_alive_count() { - let _g = serial(); - common::force_link(); - - // NULL and empty-handle free are safe error no-ops. - assert_eq!(unsafe { oakengine_task_free(std::ptr::null_mut()) }, -1); - let t = empty_task_box(); - assert_eq!(unsafe { oakengine_task_free(t) }, -1); - unsafe { reclaim_empty_task_box(t) }; - - // A real create/free round trip keeps the alive counter at baseline. - // NOTE: an actual double-free of the same facade box is out of contract - // at the C ABI level (the box is destroyed on the first free, so a - // second free is use-after-free by design); the safe double-free surface - // is NULL/empty, covered above. - let baseline = alive_count(); - let task = unsafe { oakengine_task_create_project_load(c"/no/such/oak/project.ove".as_ptr()) }; - assert_eq!(alive_count(), baseline + 1); - assert_eq!(unsafe { oakengine_task_free(task) }, 0); - assert_eq!(alive_count(), baseline); - - // A task that ran and was cancelled still accounts back to baseline. - let task2 = unsafe { oakengine_task_create_project_load(c"/no/such/oak/project.ove".as_ptr()) }; - assert_eq!(alive_count(), baseline + 1); - assert_eq!(unsafe { oakengine_task_start_sync(task2) }, 0); - assert_eq!(unsafe { oakengine_task_cancel(task2) }, 0); - assert_eq!(unsafe { oakengine_task_free(task2) }, 0); - assert_eq!(alive_count(), baseline); -} - -// --------------------------------------------------------------------------- -// Project-backed tasks (serialized: `oakengine_project_new` clears the -// process-wide undo stack; the task manager is also process-wide) -// --------------------------------------------------------------------------- - -/// Save tasks across the compression matrix (0, 1, and garbage flag -/// values), the no-filename failure path, `save_get_project`, save-otio -/// creation, and the alive-count accounting of a save task's borrowed -/// project. -#[test] -fn save_task_matrix() { - let _g = serial(); - common::force_link(); - - let project = oakengine_project_create(); - assert!(!project.is_null()); - assert_eq!(unsafe { oakengine_project_new(project) }, 0); - - let save_path = std::env::temp_dir().join("oakengine_it_task_save.ovexml"); - let save_c = std::ffi::CString::new(save_path.to_str().unwrap()).unwrap(); - let _ = std::fs::remove_file(&save_path); - - // ---- compression 0 and 1 (and garbage flag values -> treated as true) - let baseline = alive_count(); - for compression in [0, 1, 2, -1] { - let task = unsafe { - oakengine_task_create_project_save( - project, - compression, - save_c.as_ptr(), - std::ptr::null(), - ) - }; - assert!(!task.is_null(), "save with use_compression={compression}"); - - let mut buf = [0 as c_char; 256]; - let len = unsafe { oakengine_task_title(task, buf.as_mut_ptr(), 256) }; - assert!(len > 0); - assert!(read_buf(&mut buf).contains("Saving")); - - // Compression is a boolean in the module; every non-zero value is - // "compressed", and the run still succeeds. - assert_eq!(unsafe { oakengine_task_start_sync(task) }, 1); - assert!( - save_path.exists(), - "compression={compression} must write the file" - ); - assert_ne!(unsafe { oakengine_task_start_time(task) }, 0); - - unsafe { oakengine_task_free(task) }; - } - assert_eq!(alive_count(), baseline); - let _ = std::fs::remove_file(&save_path); - - // ---- save_get_project: a borrowed project handle per call ---------------- - let task = unsafe { - oakengine_task_create_project_save(project, 0, save_c.as_ptr(), std::ptr::null()) - }; - assert!(!task.is_null()); - let saved = unsafe { oakengine_task_save_get_project(task) }; - assert!(!saved.is_null()); - unsafe { oakengine_project_free(saved) }; - // Every call returns a fresh borrowed handle. - let saved2 = unsafe { oakengine_task_save_get_project(task) }; - assert!(!saved2.is_null()); - unsafe { oakengine_project_free(saved2) }; - unsafe { oakengine_task_free(task) }; - - // ---- no filename: save with override NULL fails cleanly ------------------ - let task = unsafe { - oakengine_task_create_project_save(project, 0, std::ptr::null(), std::ptr::null()) - }; - assert!(!task.is_null()); - assert_eq!(unsafe { oakengine_task_start_sync(task) }, 0); - let mut buf = [0 as c_char; 256]; - let elen = unsafe { oakengine_task_error(task, buf.as_mut_ptr(), 256) }; - assert!(elen > 0); - assert!(read_buf(&mut buf).contains("filename")); - unsafe { oakengine_task_free(task) }; - - // ---- save-otio: NULL without a project filename, real task with one ------ - assert!(unsafe { oakengine_task_create_project_save_otio(project) }.is_null()); - assert_eq!( - unsafe { - oakengine_project_set_filename(project, c"/tmp/oakengine_it_task_otio.otio".as_ptr()) - }, - 0 - ); - let otio_task = unsafe { oakengine_task_create_project_save_otio(project) }; - assert!(!otio_task.is_null()); - let mut buf = [0 as c_char; 256]; - let len = unsafe { oakengine_task_title(otio_task, buf.as_mut_ptr(), 256) }; - assert!(len > 0); - assert!(read_buf(&mut buf).contains("Saving")); - unsafe { oakengine_task_free(otio_task) }; - - unsafe { oakengine_project_free(project) }; - let _ = std::fs::remove_file(&save_path); -} - -/// Import task creation against a real project and a real (non-decodable) -/// file, plus the zero-file run. The single-file run (with its -/// invalid-file result) is covered by [`import_run_single_file`]. -/// -/// A single-file import task is created, reports the documented pre-run -/// accessor states (empty footage / invalid lists, out-of-range codes, -/// no command yet), and frees cleanly. The zero-file task runs end to end -/// (nothing to import) and hands out its (empty) undo command. -#[test] -fn import_flow_with_real_file() { - let _g = serial(); - common::force_link(); - - let project = oakengine_project_create(); - assert!(!project.is_null()); - assert_eq!(unsafe { oakengine_project_new(project) }, 0); - let root = unsafe { oakengine_project_root(project) }; - assert!(!root.is_null()); - - // A real file that cannot be decoded in the test environment (footage - // probing of a non-media file never succeeds, so a run would mark the - // file invalid). - let media = std::env::temp_dir().join("oakengine_it_task_import_batch.tmp"); - std::fs::write(&media, b"not media").unwrap(); - let media_c = std::ffi::CString::new(media.to_str().unwrap()).unwrap(); - - // ---- single-file import: creation + pre-run accessors -------------------- - let urls = [media_c.as_ptr()]; - let task = unsafe { oakengine_task_create_project_import(root, urls.as_ptr(), 1) }; - assert!(!task.is_null()); - - // Before the run the imported-footage list is empty, so both count - // exports report 0 (the facade maps `import_file_count` to the module's - // footage count; documented deviation from the construction-time count). - assert_eq!(unsafe { oakengine_task_import_file_count(task) }, 0); - assert_eq!(unsafe { oakengine_task_import_footage_count(task) }, 0); - assert_eq!( - unsafe { oakengine_task_import_invalid_files_count(task) }, - 0 - ); - - let mut buf = [0 as c_char; 256]; - let len = unsafe { oakengine_task_title(task, buf.as_mut_ptr(), 256) }; - assert_eq!(len, 19); - assert_eq!(read_buf(&mut buf), "Importing 1 file(s)"); - - // Pre-run: nothing imported, no invalid entries, no command yet; every - // index accessor reports the documented empty/out-of-range state. - assert!(unsafe { oakengine_task_import_footage_at(task, 0) }.is_null()); - assert!(unsafe { oakengine_task_import_footage_at(task, -1) }.is_null()); - assert!(unsafe { oakengine_task_import_footage_at(task, 7) }.is_null()); - assert_eq!( - unsafe { oakengine_task_import_invalid_file_at(task, 0, buf.as_mut_ptr(), 256) }, - OAKTASK_E_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_task_import_invalid_file_at(task, -1, buf.as_mut_ptr(), 256) }, - OAKTASK_E_NOT_FOUND - ); - assert!(unsafe { oakengine_task_import_get_command(task) }.is_null()); - - assert_eq!(unsafe { oakengine_task_free(task) }, 0); - - // ---- zero-file import: runs without touching the project handle ----------- - let zero = unsafe { oakengine_task_create_project_import(root, std::ptr::null(), 0) }; - assert!(!zero.is_null()); - assert_eq!(unsafe { oakengine_task_import_file_count(zero) }, 0); - assert_eq!(unsafe { oakengine_task_import_footage_count(zero) }, 0); - assert_eq!( - unsafe { oakengine_task_import_invalid_files_count(zero) }, - 0 - ); - - // "Nothing to import" still counts as a successful run: the run creates - // the (empty) multi undo command and returns OK. - assert_eq!(unsafe { oakengine_task_start_sync(zero) }, 1); - assert_eq!(unsafe { oakengine_task_import_footage_count(zero) }, 0); - assert_eq!( - unsafe { oakengine_task_import_invalid_files_count(zero) }, - 0 - ); - let cmd = unsafe { oakengine_task_import_get_command(zero) }; - assert!(!cmd.is_null()); - unsafe { oakengine_undo_command_free(cmd) }; - assert!(unsafe { oakengine_task_import_get_command(zero) }.is_null()); - assert_eq!(unsafe { oakengine_task_free(zero) }, 0); - - // ---- illegal url arrays --------------------------------------------------- - // Negative count -> NULL. - assert!(unsafe { oakengine_task_create_project_import(root, std::ptr::null(), -1) }.is_null()); - // Non-NULL urls with a count but a NULL entry inside -> NULL. - let bad_urls = [std::ptr::null()]; - assert!(unsafe { oakengine_task_create_project_import(root, bad_urls.as_ptr(), 1) }.is_null()); - // NULL urls with a positive count -> NULL. - assert!(unsafe { oakengine_task_create_project_import(root, std::ptr::null(), 1) }.is_null()); - - unsafe { oakengine_node_free(root) }; - unsafe { oakengine_project_free(project) }; - let _ = std::fs::remove_file(&media); -} - -/// A single-file import task runs end to end: the run succeeds (1) and -/// records the undecodable file as invalid. -/// -/// Regression for a former use-after-free: the facade's -/// `oakengine_task_create_project_import` (`src/task.rs`) used to hand the -/// borrowed project handle (from `oaknode_node_get_project`) to -/// `oaktask_create_project_import`, which stores it WITHOUT addref, and -/// then immediately called `oaknode_project_free` on it: the shared -/// `RefBox` refcount went 1→0 and the box was freed while the task's copy -/// still referenced it, so the run's `oaknode_footage_create(task.project, -/// …)` read the freed box → SIGSEGV. -/// -/// The fix mirrors the save creator, which addrefs the project -/// (`meta.save_project = Some(ph.addref())`): the import creator now keeps -/// an addref'd copy in `TaskMeta::import_project`, released at free, so -/// the project stays alive for the task's lifetime. -#[test] -fn import_run_single_file() { - let _g = serial(); - common::force_link(); - - let project = oakengine_project_create(); - assert!(!project.is_null()); - assert_eq!(unsafe { oakengine_project_new(project) }, 0); - let root = unsafe { oakengine_project_root(project) }; - assert!(!root.is_null()); - - let media = std::env::temp_dir().join("oakengine_it_task_import_batch.tmp"); - std::fs::write(&media, b"not media").unwrap(); - let media_c = std::ffi::CString::new(media.to_str().unwrap()).unwrap(); - - // Creation succeeds; the run succeeds (1) and records the undecodable - // file as invalid. - let urls = [media_c.as_ptr()]; - let task = unsafe { oakengine_task_create_project_import(root, urls.as_ptr(), 1) }; - assert!(!task.is_null()); - assert_eq!(unsafe { oakengine_task_start_sync(task) }, 1); - - assert_eq!( - unsafe { oakengine_task_import_invalid_files_count(task) }, - 1 - ); - assert_eq!(unsafe { oakengine_task_free(task) }, 0); - - unsafe { oakengine_node_free(root) }; - unsafe { oakengine_project_free(project) }; - let _ = std::fs::remove_file(&media); -} - -/// Export task creation against a real sequence and encoding params: the -/// task is created (the color manager is derived from the sequence's -/// project), titled, and freed; the params handle's ownership transfers to -/// the task. -#[test] -fn export_task_creation() { - let _g = serial(); - 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"Export Seq".as_ptr()) }; - assert!(!seq.is_null()); - - // Minimal legal params: a fresh handle with a filename. The export task - // takes ownership of the params box (destroyed at task free). - let params = oakengine_encoding_params_create(); - assert!(!params.is_null()); - assert_eq!( - unsafe { - oakengine_encoding_params_set_filename( - params, - c"/tmp/oakengine_it_task_export.mov".as_ptr(), - ) - }, - 0 - ); - - let task = unsafe { oakengine_task_create_export(seq, params) }; - assert!( - !task.is_null(), - "export creation must succeed without GPU (creation only)" - ); - - let mut buf = [0 as c_char; 256]; - let len = unsafe { oakengine_task_title(task, buf.as_mut_ptr(), 256) }; - assert!(len > 0); - assert!(read_buf(&mut buf).contains("Exporting")); - - // NULL / empty inputs: clean NULL, and the params handle stays owned by - // the caller on the rejected path. - assert!(unsafe { oakengine_task_create_export(std::ptr::null_mut(), params) }.is_null()); - let params2 = oakengine_encoding_params_create(); - assert!(unsafe { oakengine_task_create_export(seq, std::ptr::null_mut()) }.is_null()); - let empty_seq = Box::into_raw(Box::new(OakEngineSequence { - handle: CHandle::null(), - })); - assert!(unsafe { oakengine_task_create_export(empty_seq, params2) }.is_null()); - unsafe { crate::codec::oakengine_encoding_params_destroy(params2) }; - unsafe { drop(Box::from_raw(empty_seq)) }; - - assert_eq!(unsafe { oakengine_task_free(task) }, 0); - - // Release the sequence's borrowed facade box (release only frees the box). - unsafe { free_box::(seq) }; - unsafe { oakengine_project_free(project) }; -} - -/// 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] -fn export_task_run_real_encoder() { - let _g = serial(); - common::force_link(); - // The import/add-track/add-clip commands below are undoable; disable - // the write-through backend so they never touch a library. - let _storage = common::storage_off_guard(); - - 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()); - assert_eq!( - unsafe { - 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::(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 -/// proxy-task factory on its C ABI): NULL for every input, including a -/// valid node. -#[test] -fn proxy_stub_always_returns_null() { - let _g = serial(); - common::force_link(); - - let project = oakengine_project_create(); - assert!(!project.is_null()); - assert_eq!(unsafe { oakengine_project_new(project) }, 0); - let root = unsafe { oakengine_project_root(project) }; - assert!(!root.is_null()); - - assert!(unsafe { oakengine_task_create_proxy(root) }.is_null()); - - unsafe { oakengine_node_free(root) }; - unsafe { oakengine_project_free(project) }; -} - -// --------------------------------------------------------------------------- -// Global task manager (serialized: the manager is process-wide) -// --------------------------------------------------------------------------- - -/// The global manager lifecycle: lazy creation, empty queue, task -/// hand-over (`manager_add`), borrowed first-task handle, double-add -/// rejection, cancel, and the alive accounting of the borrowed box. -#[test] -fn task_manager_lifecycle() { - let _g = serial(); - common::force_link(); - - // The facade initializes the manager on first use; the handle is stable. - 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. - assert!(oakengine_task_manager_first().is_null()); - - let baseline = alive_count(); - - let task = unsafe { oakengine_task_create_project_load(c"/no/such/oak/project.ove".as_ptr()) }; - assert_eq!(alive_count(), baseline + 1); - - // Handing the task to the manager transfers ownership; the handle box - // stays alive until freed. - assert_eq!(unsafe { oakengine_task_manager_add(task) }, 0); - assert_eq!(oakengine_task_manager_count(), 1); - - // A second add of the same task is rejected with the module's E_STATE. - assert_eq!(unsafe { oakengine_task_manager_add(task) }, OAKTASK_E_STATE); - - // The first task is a borrowed handle: count goes up by one, and freeing - // the box returns it to baseline without deleting the manager's task. - let first = oakengine_task_manager_first(); - assert!(!first.is_null()); - assert_eq!(alive_count(), baseline + 2); - assert_eq!(unsafe { oakengine_task_free(first) }, 0); - assert_eq!(alive_count(), baseline + 1); - - // Cancelling through the manager succeeds (the load task fails fast on - // the missing file; cancel of a finished task is a documented no-op). - assert_eq!(unsafe { oakengine_task_manager_cancel(task) }, 0); - - // Adding the manager's own borrowed handle is rejected with E_STATE - // (the task is already running on the manager). - let first2 = oakengine_task_manager_first(); - assert!(!first2.is_null()); - assert_eq!( - unsafe { oakengine_task_manager_add(first2) }, - OAKTASK_E_STATE - ); - assert_eq!(unsafe { oakengine_task_free(first2) }, 0); - - // NULL / empty inputs on the manager family. - assert_eq!( - unsafe { oakengine_task_manager_add(std::ptr::null_mut()) }, - -1 - ); - assert_eq!( - unsafe { oakengine_task_manager_cancel(std::ptr::null_mut()) }, - -1 - ); - let empty = empty_task_box(); - assert_eq!(unsafe { oakengine_task_manager_add(empty) }, -1); - assert_eq!(unsafe { oakengine_task_manager_cancel(empty) }, -1); - unsafe { reclaim_empty_task_box(empty) }; - - // Releasing the (now borrowed) facade box is safe: the manager owns the - // task object and deletes it on cleanup; the alive counter returns to - // baseline. - assert_eq!(unsafe { oakengine_task_free(task) }, 0); - assert_eq!(alive_count(), baseline); - - // Finished tasks stay in the queue until delete_finished (the facade - // 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); -} diff --git a/crates/oakengine.bk/src/test_support/it_timeline.rs b/crates/oakengine.bk/src/test_support/it_timeline.rs deleted file mode 100644 index 3485890a3..000000000 --- a/crates/oakengine.bk/src/test_support/it_timeline.rs +++ /dev/null @@ -1,532 +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 . - -//! Integration tests for the **timeline editing family** (M12 P4): the -//! cross-track move export `oakengine_sequence_move_clip_to_track`, the -//! sequence marker surface (add / remove / list, all undoable), and the -//! sequence work-area surface (set / get / enable / clear, live and -//! undoable). Coverage rules (see the family test charter): -//! -//! 1. no mocks — every call goes through the real facade into the real -//! module crates (the `oakcore_audioparams_*` accessors the facade -//! reads through are its own in-dylib implementations, re-exported by -//! `tests/common`; no media is decoded, so no FFmpeg); -//! 2. every export under test is exercised on a legal path with the -//! result asserted; -//! 3. illegal inputs (NULL seq, bad track types, out-of-range indices, -//! negative times) always yield a negative error code — never a crash; -//! 4. the undoable exports round-trip through `oakengine_project_undo` / -//! `oakengine_project_redo`. -//! -//! ## Serialization -//! -//! The tests assemble projects and push undo commands on the facade's -//! process-wide undo stack, so every test takes the shared stack lock (the -//! same pattern as `it_export`/`it_undo`). - -use super::common; - -use std::ffi::{c_char, c_int}; -use std::sync::Mutex; - -use crate::handle::{OakEngineFootage, OakEngineProject, OakEngineSequence, free_box}; -use crate::node::{ - oakengine_footage_free, oakengine_project_create, oakengine_project_free, - oakengine_project_import_footage, oakengine_project_new, oakengine_project_redo, - oakengine_project_undo, -}; -use crate::timeline::{ - oakengine_clip_get_range, oakengine_sequence_add_footage_clip_ex, - oakengine_sequence_add_track, oakengine_sequence_clip_at, oakengine_sequence_clip_count, - oakengine_sequence_get_workarea, oakengine_sequence_marker_add, oakengine_sequence_marker_at, - oakengine_sequence_marker_count, oakengine_sequence_marker_remove, - oakengine_sequence_move_clip_to_track, oakengine_sequence_new, oakengine_sequence_name, - oakengine_sequence_set_video_params, oakengine_sequence_set_workarea, - oakengine_sequence_set_workarea_undoable, oakengine_sequence_workarea_is_enabled, -}; -use crate::undo::oakengine_undo_clear; - -/// `OAKENGINE_TRACK_TYPE_*` (timeline.h). -const TRACK_VIDEO: c_int = 0; -const TRACK_AUDIO: c_int = 1; - -/// Serializes every test here: the facade's global undo stack is shared -/// with the it_undo / it_export / it_storage tests. -static SERIAL: Mutex<()> = Mutex::new(()); - -/// Both lock guards held by [`serial`]. -struct SerialGuard { - /// The [`SERIAL`] lock. - _task: std::sync::MutexGuard<'static, ()>, - /// The facade-wide undo-stack lock. - _stack: parking_lot::ReentrantMutexGuard<'static, ()>, -} - -/// Take the [`SERIAL`] lock AND the global undo-stack lock, recovering -/// from any poisoning. -fn serial() -> SerialGuard { - let _task = SERIAL.lock().unwrap_or_else(|e| e.into_inner()); - let _stack = super::it_undo::GLOBAL_STACK_LOCK - .lock(); - SerialGuard { _task, _stack } -} - -/// Reads a NUL-terminated string from a facade two-stage buffer. -unsafe fn read_str(buf: *const c_char) -> String { - if buf.is_null() { - return String::new(); - } - let len = (0..4096).find(|&i| unsafe { *buf.add(i) } == 0).unwrap_or(0); - String::from_utf8_lossy(unsafe { std::slice::from_raw_parts(buf as *const u8, len) }) - .into_owned() -} - -/// A temp file 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-timeline-{kind}-{}.bin", std::process::id())) -} - -/// Build a project + sequence with `video_tracks` video tracks, each -/// carrying one clip spanning `0..100` frames (media-in 0), at 25 fps. -/// The source file is a plain byte blob — the module does not probe media. -/// -/// Returns `(project, sequence, footage)` — the caller releases the -/// footage with `oakengine_footage_free`, 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_timeline_sequence( - video_tracks: c_int, -) -> (*mut OakEngineProject, *mut OakEngineSequence, *mut OakEngineFootage) { - unsafe { - // The caller holds the storage-config lock + storage_off_guard for - // the whole body (assembly AND the subsequent command pushes), so no - // lock is taken here (re-locking the same std mutex would deadlock). - let media = temp_path("media"); - std::fs::write(&media, b"oak timeline test footage").expect("write the source blob"); - let media_c = std::ffi::CString::new(media.to_string_lossy().into_owned()).unwrap(); - - let project = oakengine_project_create(); - assert!(!project.is_null()); - assert_eq!(oakengine_project_new(project), 0); - - let footage = oakengine_project_import_footage(project, media_c.as_ptr()); - assert!(!footage.is_null(), "import the source blob"); - - let seq = oakengine_sequence_new(project, c"Timeline Test".as_ptr()); - assert!(!seq.is_null()); - assert_eq!( - oakengine_sequence_set_video_params(seq, 640, 480, 25, 1, 1, 1, 0, 4, 0), - 0, - "set the sequence frame rate" - ); - - for _ in 0..video_tracks { - let track = oakengine_sequence_add_track(seq, TRACK_VIDEO); - assert!(track >= 0, "add the video track"); - // Only the first track carries a clip: the move tests need an - // empty destination track (and `oakengine_sequence_add_track` - // returns its index, which is 0 for the first call). - if track == 0 { - let clip = oakengine_sequence_add_footage_clip_ex(seq, footage, TRACK_VIDEO, track, 0, 100, 0); - assert!(!clip.is_null(), "place the clip"); - free_box(clip); - } - } - - (project, seq, footage) - } -} - -/// Release the assembly returned by [`assemble_timeline_sequence`]. -/// -/// # Safety -/// The handles must be the ones returned by [`assemble_timeline_sequence`]. -unsafe fn drop_timeline_sequence( - project: *mut OakEngineProject, - seq: *mut OakEngineSequence, - footage: *mut OakEngineFootage, -) { - unsafe { - if !footage.is_null() { - oakengine_footage_free(footage); - } - free_box::(seq); - oakengine_project_free(project); - } -} - -/// The (in, out) frame range of the clip at `(track, index)`, read back -/// through the facade. `None` when the track has no such clip. -unsafe fn clip_range_of(seq: *mut OakEngineSequence, track: c_int, index: c_int) -> Option<(i64, i64)> { - unsafe { - let clip = oakengine_sequence_clip_at(seq, TRACK_VIDEO, track, index); - if clip.is_null() { - return None; - } - let mut in_ts: i64 = 0; - let mut out_ts: i64 = 0; - let mut media_in: i64 = 0; - assert_eq!(oakengine_clip_get_range(clip, &mut in_ts, &mut out_ts, &mut media_in), 0); - free_box(clip); - Some((in_ts, out_ts)) - } -} - -// --------------------------------------------------------------------------- -// Cross-track move (oakengine_sequence_move_clip_to_track) -// --------------------------------------------------------------------------- - -/// A cross-track move lands the clip on the destination track at the new -/// in point, and the source spot becomes a gap (the source track's clip -/// count drops to zero). One undoable entry: undo restores the clip to its -/// original track/position, redo re-applies the move. -#[test] -fn move_clip_to_track_cross_track_roundtrip() { - let _g = serial(); - common::force_link(); - unsafe { - // Storage off for the whole body: the assembly and the move both - // push commands (the assembly helper's guard is dropped on return). - let _storage = common::storage_off_guard(); - let (project, seq, footage) = assemble_timeline_sequence(2); - assert_eq!(oakengine_undo_clear(), 0); - - assert_eq!(oakengine_sequence_clip_count(seq, TRACK_VIDEO, 0), 1); - assert_eq!(oakengine_sequence_clip_count(seq, TRACK_VIDEO, 1), 0); - assert_eq!(clip_range_of(seq, 0, 0), Some((0, 100))); - - // Track 0 clip 0 → track 1 at frame 30. - let rc = oakengine_sequence_move_clip_to_track(seq, TRACK_VIDEO, 0, 0, 1, 30); - assert_eq!(rc, 0, "cross-track move succeeds"); - - assert_eq!( - oakengine_sequence_clip_count(seq, TRACK_VIDEO, 0), - 0, - "source spot becomes a gap" - ); - assert_eq!( - oakengine_sequence_clip_count(seq, TRACK_VIDEO, 1), - 1, - "the clip lands on the destination track" - ); - assert_eq!(clip_range_of(seq, 1, 0), Some((30, 130))); - - // One undo restores the original layout. - assert_eq!(oakengine_project_undo(project), 0); - assert_eq!(oakengine_sequence_clip_count(seq, TRACK_VIDEO, 0), 1); - assert_eq!(oakengine_sequence_clip_count(seq, TRACK_VIDEO, 1), 0); - assert_eq!(clip_range_of(seq, 0, 0), Some((0, 100))); - - // Redo re-applies the move. - assert_eq!(oakengine_project_redo(project), 0); - assert_eq!(oakengine_sequence_clip_count(seq, TRACK_VIDEO, 0), 0); - assert_eq!(oakengine_sequence_clip_count(seq, TRACK_VIDEO, 1), 1); - assert_eq!(clip_range_of(seq, 1, 0), Some((30, 130))); - - drop_timeline_sequence(project, seq, footage); - } -} - -/// Moving to the same track (destination index == source index) is a -/// time-only move, equivalent to `oakengine_sequence_move_clip`. -#[test] -fn move_clip_to_track_same_track_is_time_only() { - let _g = serial(); - common::force_link(); - unsafe { - let _storage = common::storage_off_guard(); - let (project, seq, footage) = assemble_timeline_sequence(2); - assert_eq!(oakengine_undo_clear(), 0); - - let rc = oakengine_sequence_move_clip_to_track(seq, TRACK_VIDEO, 0, 0, 0, 40); - assert_eq!(rc, 0, "same-track move succeeds"); - assert_eq!(oakengine_sequence_clip_count(seq, TRACK_VIDEO, 0), 1); - assert_eq!(oakengine_sequence_clip_count(seq, TRACK_VIDEO, 1), 0); - assert_eq!(clip_range_of(seq, 0, 0), Some((40, 140))); - - assert_eq!(oakengine_project_undo(project), 0); - assert_eq!(clip_range_of(seq, 0, 0), Some((0, 100))); - - drop_timeline_sequence(project, seq, footage); - } -} - -/// Illegal inputs never crash: NULL sequence, unknown track types, -/// negative in points, missing clips and out-of-range destination tracks -/// all yield a negative error code. -#[test] -fn move_clip_to_track_rejects_illegal_inputs() { - let _g = serial(); - common::force_link(); - unsafe { - let _storage = common::storage_off_guard(); - let (project, seq, footage) = assemble_timeline_sequence(1); - assert_eq!(oakengine_undo_clear(), 0); - - // NULL sequence. - assert!(oakengine_sequence_move_clip_to_track( - std::ptr::null_mut(), TRACK_VIDEO, 0, 0, 1, 10 - ) < 0); - // Unknown track types. - assert!(oakengine_sequence_move_clip_to_track(seq, 3, 0, 0, 1, 10) < 0); - assert!(oakengine_sequence_move_clip_to_track(seq, -1, 0, 0, 1, 10) < 0); - // Negative destination in point. - assert!(oakengine_sequence_move_clip_to_track(seq, TRACK_VIDEO, 0, 0, 1, -5) < 0); - // Missing clip (index 5 on a one-clip track). - assert!(oakengine_sequence_move_clip_to_track(seq, TRACK_VIDEO, 0, 5, 1, 10) < 0); - assert!(oakengine_sequence_move_clip_to_track(seq, TRACK_VIDEO, 2, 0, 1, 10) < 0); - // Destination track out of range. - assert!(oakengine_sequence_move_clip_to_track(seq, TRACK_VIDEO, 0, 0, 9, 10) < 0); - // The rejections left the sequence untouched. - assert_eq!(oakengine_sequence_clip_count(seq, TRACK_VIDEO, 0), 1); - assert_eq!(clip_range_of(seq, 0, 0), Some((0, 100))); - - drop_timeline_sequence(project, seq, footage); - } -} - -// --------------------------------------------------------------------------- -// Sequence markers (oakengine_sequence_marker_*) -// --------------------------------------------------------------------------- - -/// `oakengine_sequence_marker_add` inserts an undoable marker; the list -/// exposes it through count/at (time in the sequence's frame timebase, -/// name and color round-trip). Undo removes it, redo re-adds it. -#[test] -fn markers_add_list_and_undo_roundtrip() { - let _g = serial(); - common::force_link(); - unsafe { - let _storage = common::storage_off_guard(); - let (project, seq, footage) = assemble_timeline_sequence(0); - assert_eq!(oakengine_undo_clear(), 0); - - assert_eq!(oakengine_sequence_marker_count(seq), 0); - - let rc = oakengine_sequence_marker_add(seq, 60, c"scene 1".as_ptr()); - assert_eq!(rc, 0, "add a marker at frame 60"); - assert_eq!(oakengine_sequence_marker_count(seq), 1); - - let mut time: i64 = -1; - let mut color: c_int = -1; - let mut name_buf = [0 as c_char; 64]; - assert_eq!( - oakengine_sequence_marker_at( - seq, 0, &mut time, name_buf.as_mut_ptr(), 64, &mut color - ), - 0 - ); - assert_eq!(time, 60); - assert_eq!(color, 0); - assert_eq!(read_str(name_buf.as_ptr()), "scene 1"); - - // Undo removes the marker, redo re-adds it. - assert_eq!(oakengine_project_undo(project), 0); - assert_eq!(oakengine_sequence_marker_count(seq), 0); - assert_eq!(oakengine_project_redo(project), 0); - assert_eq!(oakengine_sequence_marker_count(seq), 1); - time = -1; - assert_eq!(oakengine_sequence_marker_at(seq, 0, &mut time, std::ptr::null_mut(), 0, std::ptr::null_mut()), 0); - assert_eq!(time, 60); - - drop_timeline_sequence(project, seq, footage); - } -} - -/// `oakengine_sequence_marker_remove` removes the marker at a time -/// (undoable); removing from an empty spot is an error that leaves the list -/// intact. -#[test] -fn markers_remove_and_reject_duplicates() { - let _g = serial(); - common::force_link(); - unsafe { - let _storage = common::storage_off_guard(); - let (project, seq, footage) = assemble_timeline_sequence(0); - assert_eq!(oakengine_undo_clear(), 0); - - assert_eq!(oakengine_sequence_marker_add(seq, 30, std::ptr::null()), 0); - assert_eq!(oakengine_sequence_marker_add(seq, 90, c"end".as_ptr()), 0); - assert_eq!(oakengine_sequence_marker_count(seq), 2); - - // A second marker at the same time is rejected (module asserts on - // duplicate in points). - assert!(oakengine_sequence_marker_add(seq, 30, c"dup".as_ptr()) < 0); - assert_eq!(oakengine_sequence_marker_count(seq), 2); - - // Removing the marker at frame 30 (undoable). - assert_eq!(oakengine_sequence_marker_remove(seq, 30), 0); - assert_eq!(oakengine_sequence_marker_count(seq), 1); - let mut time: i64 = -1; - assert_eq!(oakengine_sequence_marker_at(seq, 0, &mut time, std::ptr::null_mut(), 0, std::ptr::null_mut()), 0); - assert_eq!(time, 90, "the surviving marker is the one at 90"); - - // Removing from an empty time is an error. - assert!(oakengine_sequence_marker_remove(seq, 30) < 0); - assert_eq!(oakengine_sequence_marker_count(seq), 1); - - // Undo restores the removed marker. - assert_eq!(oakengine_project_undo(project), 0); - assert_eq!(oakengine_sequence_marker_count(seq), 2); - - drop_timeline_sequence(project, seq, footage); - } -} - -/// Marker inputs: NULL sequence and out-of-range indices are errors, never -/// crashes. -#[test] -fn markers_reject_illegal_inputs() { - let _g = serial(); - common::force_link(); - unsafe { - let _storage = common::storage_off_guard(); - let (project, seq, footage) = assemble_timeline_sequence(0); - assert_eq!(oakengine_undo_clear(), 0); - - assert!(oakengine_sequence_marker_add(std::ptr::null_mut(), 10, std::ptr::null()) < 0); - assert!(oakengine_sequence_marker_remove(std::ptr::null_mut(), 10) < 0); - assert_eq!(oakengine_sequence_marker_count(std::ptr::null_mut()), 0); - assert!(oakengine_sequence_marker_remove(seq, 10) < 0, "no marker at frame 10"); - - let mut time: i64 = 0; - assert!( - oakengine_sequence_marker_at(seq, 3, &mut time, std::ptr::null_mut(), 0, std::ptr::null_mut()) < 0, - "index out of range" - ); - - drop_timeline_sequence(project, seq, footage); - } -} - -// --------------------------------------------------------------------------- -// Sequence work area (oakengine_sequence_workarea_*) -// --------------------------------------------------------------------------- - -/// `oakengine_sequence_set_workarea` (live) round-trips through -/// `oakengine_sequence_get_workarea` / `oakengine_sequence_workarea_is_enabled`; -/// disabling clears the enabled flag. -#[test] -fn workarea_set_get_clear_roundtrip() { - let _g = serial(); - common::force_link(); - unsafe { - let _storage = common::storage_off_guard(); - let (project, seq, footage) = assemble_timeline_sequence(0); - - assert_eq!(oakengine_sequence_workarea_is_enabled(seq), 0); - let mut in_ts: i64 = -1; - let mut out_ts: i64 = -1; - assert_eq!(oakengine_sequence_get_workarea(seq, &mut in_ts, &mut out_ts), 0); - // Default range: the reset sentinel, 0..RATIONAL_MAX in the frame - // timebase (a huge positive out point). - assert_eq!(in_ts, 0); - assert!(out_ts > 0, "default out is the reset sentinel, got {out_ts}"); - - // Set an enabled range and read it back. - assert_eq!(oakengine_sequence_set_workarea(seq, 1, 100, 200), 0); - assert_eq!(oakengine_sequence_workarea_is_enabled(seq), 1); - assert_eq!(oakengine_sequence_get_workarea(seq, &mut in_ts, &mut out_ts), 0); - assert_eq!((in_ts, out_ts), (100, 200)); - - // Disable ("clear") keeps the range but flips the flag. - assert_eq!(oakengine_sequence_set_workarea(seq, 0, 100, 200), 0); - assert_eq!(oakengine_sequence_workarea_is_enabled(seq), 0); - assert_eq!(oakengine_sequence_get_workarea(seq, &mut in_ts, &mut out_ts), 0); - assert_eq!((in_ts, out_ts), (100, 200)); - - drop_timeline_sequence(project, seq, footage); - } -} - -/// `oakengine_sequence_set_workarea_undoable` changes enabled + range as -/// ONE undoable entry: undo restores the pre-change range (the caller -/// supplied old in/out), redo re-applies the new one. -#[test] -fn workarea_undoable_set_roundtrips() { - let _g = serial(); - common::force_link(); - unsafe { - let _storage = common::storage_off_guard(); - let (project, seq, footage) = assemble_timeline_sequence(0); - assert_eq!(oakengine_undo_clear(), 0); - - // Start from a live-set range so the undo has something to restore. - assert_eq!(oakengine_sequence_set_workarea(seq, 1, 100, 200), 0); - - let rc = oakengine_sequence_set_workarea_undoable(seq, 1, 300, 400, 100, 200); - assert_eq!(rc, 0, "undoable workarea set succeeds"); - let mut in_ts: i64 = 0; - let mut out_ts: i64 = 0; - assert_eq!(oakengine_sequence_get_workarea(seq, &mut in_ts, &mut out_ts), 0); - assert_eq!((in_ts, out_ts), (300, 400)); - assert_eq!(oakengine_sequence_workarea_is_enabled(seq), 1); - - // One undo entry: enabled + range restored together. - assert_eq!(oakengine_project_undo(project), 0); - assert_eq!(oakengine_sequence_workarea_is_enabled(seq), 1); - assert_eq!(oakengine_sequence_get_workarea(seq, &mut in_ts, &mut out_ts), 0); - assert_eq!((in_ts, out_ts), (100, 200)); - - // Redo re-applies. - assert_eq!(oakengine_project_redo(project), 0); - assert_eq!(oakengine_sequence_get_workarea(seq, &mut in_ts, &mut out_ts), 0); - assert_eq!((in_ts, out_ts), (300, 400)); - - drop_timeline_sequence(project, seq, footage); - } -} - -/// Work-area inputs: NULL sequence and negative ranges are errors. -#[test] -fn workarea_rejects_illegal_inputs() { - let _g = serial(); - common::force_link(); - unsafe { - let _storage = common::storage_off_guard(); - let (project, seq, footage) = assemble_timeline_sequence(0); - - assert_eq!(oakengine_sequence_workarea_is_enabled(std::ptr::null_mut()), 0); - assert!(oakengine_sequence_get_workarea(std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::null_mut()) < 0); - assert!(oakengine_sequence_set_workarea(std::ptr::null_mut(), 1, 0, 10) < 0); - assert!(oakengine_sequence_set_workarea_undoable(std::ptr::null_mut(), 1, 0, 10, 0, 0) < 0); - // Negative range values are rejected by the undoable export (never a - // crash). The live setter is a plain setter (C++ parity — it stores - // what it is given), so only the command path validates. - assert!(oakengine_sequence_set_workarea_undoable(seq, 1, 0, 10, -1, 0) < 0); - assert!(oakengine_sequence_set_workarea_undoable(seq, 1, -1, 10, 0, 0) < 0); - - drop_timeline_sequence(project, seq, footage); - } -} - -/// The sequence name getter used by the app's `refresh_sequence_info` -/// (two-stage buf/size) is exercised here as the assembly smoke check. -#[test] -fn assembled_sequence_has_expected_name() { - let _g = serial(); - common::force_link(); - unsafe { - let _storage = common::storage_off_guard(); - let (project, seq, footage) = assemble_timeline_sequence(1); - let mut buf = [0 as c_char; 64]; - assert!(oakengine_sequence_name(seq, buf.as_mut_ptr(), 64) > 0); - assert_eq!(read_str(buf.as_ptr()), "Timeline Test"); - drop_timeline_sequence(project, seq, footage); - } -} diff --git a/crates/oakengine.bk/src/test_support/it_undo.rs b/crates/oakengine.bk/src/test_support/it_undo.rs deleted file mode 100644 index b0de779a9..000000000 --- a/crates/oakengine.bk/src/test_support/it_undo.rs +++ /dev/null @@ -1,928 +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 . - -//! Integration tests for the undo family (`engine/include/oakengine/undo.h`, -//! implemented by `src/undo.rs` on top of the real oakundo module crate) — -//! the "real behavior, end to end" complement to the smoke tests in -//! `tests/undo.rs`. No mocks: every call goes through the facade exports -//! into the real oakundo crate. -//! -//! The facade owns a process-wide undo stack and a single open undo group -//! (the module 00 analogue of `EngineCore::undo_stack()` / `g_undo_group`), -//! so every stack- and group-mutating assertion lives in ONE serialized -//! test function ([`undo_stack_integration`]). The command-lifecycle tests -//! only touch local state and run in parallel. -//! -//! Coverage: all 23 `oakengine_undo_*` exports are called on a legal path -//! with asserted results, plus the illegal-input matrix (NULL pointers, -//! empty `CHandle::null()` boxes, out-of-range rows, zero/negative buffer -//! sizes) and the free/destroy contracts. No function in this family needs -//! GPU/app state. The regression tests at the bottom ([`null_name_push_repro`], -//! [`null_name_group_repro`], [`group_abort_undoes_children_repro`]) lock -//! three fixed facade bugs: a NULL/empty label to `oakengine_undo_push` / -//! the group-end path used to hand the module a dangling -//! `String::new().as_ptr()` (0x1) and SIGSEGV, and -//! `oakengine_undo_group_abort` used to leave its executed children -//! un-undone. - -use super::common; - -use std::ffi::{c_char, c_void}; -use std::sync::atomic::{AtomicI32, AtomicUsize, Ordering}; -use std::sync::Mutex; - -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, - oakengine_undo_command_multi_add_child, oakengine_undo_command_multi_child_count, - oakengine_undo_command_redo_now, oakengine_undo_command_text, oakengine_undo_command_undo_now, - oakengine_undo_count, oakengine_undo_group_abort, oakengine_undo_group_begin, - oakengine_undo_group_end, oakengine_undo_handle, oakengine_undo_index, oakengine_undo_jump, - oakengine_undo_push, oakengine_undo_redo_action, oakengine_undo_undo_action, - oakengine_undo_update_actions, -}; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/// Box a `CHandle::null()` inside an `OakEngineClipboard` — a VALID box -/// whose module handle is empty (what a plugin would hold after its own -/// handle object went away). The facade must reject it with a clean error -/// code, never crash. -fn empty_engine_ptr() -> *mut c_void { - Box::into_raw(Box::new(OakEngineClipboard { - handle: CHandle::null(), - })) - .cast() -} - -/// Read back the NUL-terminated string the facade wrote into `buf`. -unsafe fn read_str(buf: *const c_char) -> String { - unsafe { std::ffi::CStr::from_ptr(buf) } - .to_str() - .unwrap() - .to_string() -} - -// --------------------------------------------------------------------------- -// Command-lifecycle callbacks (parallel tests only; the serialized stack -// test uses the STK_* counters below and never touches these). -// --------------------------------------------------------------------------- - -/// Serializes the tests that drive the facade's process-wide global undo -/// stack (`undo_stack_integration`, `null_name_push_repro`, -/// `null_name_group_repro`, `group_abort_undoes_children_repro`): cargo -/// runs tests on parallel threads and the global stack / single open undo -/// group cannot be shared, so each of those tests holds this lock for its -/// whole body. Public so the write-through tests (it_storage.rs), which -/// push commands on the same global stack, serialize on the SAME lock. -pub static GLOBAL_STACK_LOCK: parking_lot::ReentrantMutex<()> = - parking_lot::ReentrantMutex::new(()); - -static LIFECYCLE_REDO: AtomicI32 = AtomicI32::new(0); -static LIFECYCLE_UNDO: AtomicI32 = AtomicI32::new(0); -static LIFECYCLE_FREE: AtomicI32 = AtomicI32::new(0); -static LIFECYCLE_FREED_PTR: AtomicUsize = AtomicUsize::new(0); - -/// Own counter set for `command_create_variants` (the lifecycle tests run -/// in parallel, so they must not share atomics). -static VARIANTS_FREE: AtomicI32 = AtomicI32::new(0); - -unsafe extern "C" fn lifecycle_redo(_ud: *mut c_void) { - LIFECYCLE_REDO.fetch_add(1, Ordering::SeqCst); -} - -unsafe extern "C" fn lifecycle_undo(_ud: *mut c_void) { - LIFECYCLE_UNDO.fetch_add(1, Ordering::SeqCst); -} - -unsafe extern "C" fn lifecycle_free(_ud: *mut c_void) { - LIFECYCLE_FREE.fetch_add(1, Ordering::SeqCst); -} - -/// No-op callback for `command_create_variants` (avoids touching the -/// lifecycle counters, which run in a parallel test). -unsafe extern "C" fn variants_noop(_ud: *mut c_void) {} - -unsafe extern "C" fn variants_free(_ud: *mut c_void) { - VARIANTS_FREE.fetch_add(1, Ordering::SeqCst); -} - -/// free_fn that records the pointer and drops the boxed `u64` userdata -/// (round-trip ownership check). -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); - unsafe { drop(Box::from_raw(ud as *mut u64)) }; -} - -/// Child redo/undo callbacks that log their id (encoded in userdata) — used -/// to verify multi redo order (insertion) and undo order (reverse). -static MULTI_LOG: Mutex> = Mutex::new(Vec::new()); - -unsafe extern "C" fn multi_redo(ud: *mut c_void) { - MULTI_LOG.lock().unwrap().push(ud as usize as i32); -} - -unsafe extern "C" fn multi_undo(ud: *mut c_void) { - MULTI_LOG.lock().unwrap().push(ud as usize as i32); -} - -static MULTI_FREE: AtomicI32 = AtomicI32::new(0); - -unsafe extern "C" fn multi_free(_ud: *mut c_void) { - MULTI_FREE.fetch_add(1, Ordering::SeqCst); -} - -/// free_fn for the module-level destroy-contract test. -static MOD_FREE: AtomicI32 = AtomicI32::new(0); - -unsafe extern "C" fn mod_free_cb(_ud: *mut c_void) { - MOD_FREE.fetch_add(1, Ordering::SeqCst); -} - -// --------------------------------------------------------------------------- -// Serialized stack-test callbacks (own counters; the parallel command -// tests never touch these). -// --------------------------------------------------------------------------- - -static STK_REDO: AtomicI32 = AtomicI32::new(0); -static STK_UNDO: AtomicI32 = AtomicI32::new(0); - -unsafe extern "C" fn stk_redo(_ud: *mut c_void) { - STK_REDO.fetch_add(1, Ordering::SeqCst); -} - -unsafe extern "C" fn stk_undo(_ud: *mut c_void) { - STK_UNDO.fetch_add(1, Ordering::SeqCst); -} - -// --------------------------------------------------------------------------- -// Command lifecycle (parallel-safe: no global-stack state) -// --------------------------------------------------------------------------- - -/// Full legal lifecycle of an app-defined command: create with name + -/// callbacks + owned userdata, redo/undo (idempotent), destroy via free — -/// the free_fn fires exactly once, with the same userdata pointer. -#[test] -fn command_lifecycle_roundtrip() { - common::force_link(); - LIFECYCLE_REDO.store(0, Ordering::SeqCst); - LIFECYCLE_UNDO.store(0, Ordering::SeqCst); - LIFECYCLE_FREE.store(0, Ordering::SeqCst); - LIFECYCLE_FREED_PTR.store(0, Ordering::SeqCst); - - let ud = Box::into_raw(Box::new(42u64)) as *mut c_void; - let cmd = unsafe { - oakengine_undo_command_create( - c"roundtrip".as_ptr(), - Some(lifecycle_redo), - Some(lifecycle_undo), - Some(lifecycle_free_userdata), - ud, - ) - }; - assert!(!cmd.is_null()); - - assert_eq!(unsafe { oakengine_undo_command_redo_now(cmd) }, 0); - assert_eq!(LIFECYCLE_REDO.load(Ordering::SeqCst), 1); - // redo on a done command is a no-op (olive semantics). - assert_eq!(unsafe { oakengine_undo_command_redo_now(cmd) }, 0); - assert_eq!(LIFECYCLE_REDO.load(Ordering::SeqCst), 1); - - assert_eq!(unsafe { oakengine_undo_command_undo_now(cmd) }, 0); - assert_eq!(LIFECYCLE_UNDO.load(Ordering::SeqCst), 1); - // undo on an undone command is a no-op. - assert_eq!(unsafe { oakengine_undo_command_undo_now(cmd) }, 0); - assert_eq!(LIFECYCLE_UNDO.load(Ordering::SeqCst), 1); - - assert_eq!(unsafe { oakengine_undo_command_redo_now(cmd) }, 0); - assert_eq!(LIFECYCLE_REDO.load(Ordering::SeqCst), 2); - - unsafe { oakengine_undo_command_free(cmd) }; - assert_eq!(LIFECYCLE_FREE.load(Ordering::SeqCst), 1); - assert_eq!(LIFECYCLE_FREED_PTR.load(Ordering::SeqCst), ud as usize); -} - -/// create() legal variants: NULL name, all-None callback table, free-only -/// table. All must produce a usable command. -#[test] -fn command_create_variants() { - common::force_link(); - VARIANTS_FREE.store(0, Ordering::SeqCst); - - // NULL name is legal (the label is read as empty). - let c1 = unsafe { - oakengine_undo_command_create( - std::ptr::null(), - Some(variants_noop), - Some(variants_noop), - None, - std::ptr::null_mut(), - ) - }; - assert!(!c1.is_null()); - assert_eq!(unsafe { oakengine_undo_command_redo_now(c1) }, 0); - assert_eq!(unsafe { oakengine_undo_command_undo_now(c1) }, 0); - unsafe { oakengine_undo_command_free(c1) }; - - // All-None callbacks: a no-op command, still usable. - let c2 = unsafe { - oakengine_undo_command_create(c"noop".as_ptr(), None, None, None, std::ptr::null_mut()) - }; - assert!(!c2.is_null()); - assert_eq!(unsafe { oakengine_undo_command_redo_now(c2) }, 0); - assert_eq!(unsafe { oakengine_undo_command_undo_now(c2) }, 0); - unsafe { oakengine_undo_command_free(c2) }; - - // free-only table: destroy still invokes free_fn exactly once. - let c3 = unsafe { - oakengine_undo_command_create( - c"freeonly".as_ptr(), - None, - None, - Some(variants_free), - std::ptr::null_mut(), - ) - }; - assert!(!c3.is_null()); - unsafe { oakengine_undo_command_free(c3) }; - assert_eq!(VARIANTS_FREE.load(Ordering::SeqCst), 1); -} - -/// Illegal-input robustness for the command surface: NULL pointers and -/// empty (`CHandle::null`) handles must produce clean negative codes -/// (the facade's -1 or the oakundo -20001 pass-through), never a crash. -#[test] -fn command_illegal_handle_inputs() { - common::force_link(); - - // NULL command pointers. - assert_eq!( - unsafe { oakengine_undo_command_redo_now(std::ptr::null_mut()) }, - -1 - ); - assert_eq!( - unsafe { oakengine_undo_command_undo_now(std::ptr::null_mut()) }, - -1 - ); - assert_eq!( - unsafe { oakengine_undo_command_multi_child_count(std::ptr::null_mut()) }, - -1 - ); - assert_eq!( - unsafe { - oakengine_undo_command_multi_add_child(std::ptr::null_mut(), std::ptr::null_mut()) - }, - -1 - ); - - // Empty (CHandle::null) handles inside a valid box. - let eb = empty_engine_ptr(); - assert_eq!(unsafe { oakengine_undo_command_redo_now(eb) }, -1); - unsafe { oakengine_undo_command_free(eb) }; - - let eb = empty_engine_ptr(); - assert_eq!(unsafe { oakengine_undo_command_undo_now(eb) }, -1); - unsafe { oakengine_undo_command_free(eb) }; - - let eb = empty_engine_ptr(); - assert_eq!(unsafe { oakengine_undo_command_multi_child_count(eb) }, -1); - unsafe { oakengine_undo_command_free(eb) }; - - // multi_add_child with an empty parent (the facade errors before - // consuming the child, so the child box must be freed by us). - let eb = empty_engine_ptr(); - let child = unsafe { - oakengine_undo_command_create(c"child".as_ptr(), None, None, None, std::ptr::null_mut()) - }; - assert_eq!( - unsafe { oakengine_undo_command_multi_add_child(eb, child) }, - -1 - ); - unsafe { oakengine_undo_command_free(eb) }; - unsafe { oakengine_undo_command_free(child) }; - - // multi_add_child with an empty child (parent untouched). - let multi = unsafe { oakengine_undo_command_create_multi() }; - let eb = empty_engine_ptr(); - assert_eq!( - unsafe { oakengine_undo_command_multi_add_child(multi, eb) }, - -1 - ); - unsafe { oakengine_undo_command_free(eb) }; - assert_eq!( - unsafe { oakengine_undo_command_multi_child_count(multi) }, - 0 - ); - unsafe { oakengine_undo_command_free(multi) }; - - // A plain (non-multi) command as the "multi" parent: the module rejects - // with -20001 and the facade still consumes the child's box. - let parent = unsafe { - oakengine_undo_command_create(c"parent".as_ptr(), None, None, None, std::ptr::null_mut()) - }; - let child = unsafe { - oakengine_undo_command_create(c"child".as_ptr(), None, None, None, std::ptr::null_mut()) - }; - assert_eq!( - unsafe { oakengine_undo_command_multi_add_child(parent, child) }, - -20001 - ); - assert_eq!( - unsafe { oakengine_undo_command_multi_child_count(parent) }, - -20001 - ); - unsafe { oakengine_undo_command_free(parent) }; -} - -/// Legal-input matrix for multi commands: child counts 0→N, redo in -/// insertion order, undo in reverse order, nested multis, idempotent -/// redo/undo. -#[test] -fn multi_command_lifecycle() { - common::force_link(); - let multi = unsafe { oakengine_undo_command_create_multi() }; - assert!(!multi.is_null()); - assert_eq!( - unsafe { oakengine_undo_command_multi_child_count(multi) }, - 0 - ); - - for id in [1, 2, 3] { - let child = unsafe { - oakengine_undo_command_create( - c"child".as_ptr(), - Some(multi_redo), - Some(multi_undo), - None, - id as *mut c_void, - ) - }; - assert_eq!( - unsafe { oakengine_undo_command_multi_add_child(multi, child) }, - 0 - ); - } - assert_eq!( - unsafe { oakengine_undo_command_multi_child_count(multi) }, - 3 - ); - - *MULTI_LOG.lock().unwrap() = Vec::new(); - assert_eq!(unsafe { oakengine_undo_command_redo_now(multi) }, 0); - assert_eq!(*MULTI_LOG.lock().unwrap(), [1, 2, 3]); - // redo of a done multi is a no-op. - assert_eq!(unsafe { oakengine_undo_command_redo_now(multi) }, 0); - assert_eq!(*MULTI_LOG.lock().unwrap(), [1, 2, 3]); - - assert_eq!(unsafe { oakengine_undo_command_undo_now(multi) }, 0); - assert_eq!(*MULTI_LOG.lock().unwrap(), [1, 2, 3, 3, 2, 1]); - unsafe { oakengine_undo_command_free(multi) }; - - // Nested multi: outer = [c10, inner([c21])]; undo runs children in - // reverse order, inner included. - let outer = unsafe { oakengine_undo_command_create_multi() }; - let inner = unsafe { oakengine_undo_command_create_multi() }; - let c10 = unsafe { - oakengine_undo_command_create( - c"c10".as_ptr(), - Some(multi_redo), - Some(multi_undo), - None, - 10 as *mut c_void, - ) - }; - let c21 = unsafe { - oakengine_undo_command_create( - c"c21".as_ptr(), - Some(multi_redo), - Some(multi_undo), - None, - 21 as *mut c_void, - ) - }; - assert_eq!( - unsafe { oakengine_undo_command_multi_add_child(inner, c21) }, - 0 - ); - assert_eq!( - unsafe { oakengine_undo_command_multi_child_count(inner) }, - 1 - ); - assert_eq!( - unsafe { oakengine_undo_command_multi_add_child(outer, c10) }, - 0 - ); - assert_eq!( - unsafe { oakengine_undo_command_multi_add_child(outer, inner) }, - 0 - ); - assert_eq!( - unsafe { oakengine_undo_command_multi_child_count(outer) }, - 2 - ); - - *MULTI_LOG.lock().unwrap() = Vec::new(); - assert_eq!(unsafe { oakengine_undo_command_redo_now(outer) }, 0); - assert_eq!(*MULTI_LOG.lock().unwrap(), [10, 21]); - assert_eq!(unsafe { oakengine_undo_command_undo_now(outer) }, 0); - assert_eq!(*MULTI_LOG.lock().unwrap(), [10, 21, 21, 10]); - - // c10 / inner / c21 were consumed by multi_add_child (their boxes are - // freed by the facade), so only outer is freed here — the child command - // values die with it. - unsafe { oakengine_undo_command_free(outer) }; -} - -/// Destroying a multi command releases its children transitively: each -/// child's free_fn fires exactly once when the multi is freed. -#[test] -fn multi_command_free_frees_children() { - common::force_link(); - MULTI_FREE.store(0, Ordering::SeqCst); - - let multi = unsafe { oakengine_undo_command_create_multi() }; - let inner = unsafe { oakengine_undo_command_create_multi() }; - let a = unsafe { - oakengine_undo_command_create( - c"a".as_ptr(), - None, - None, - Some(multi_free), - std::ptr::null_mut(), - ) - }; - let b = unsafe { - oakengine_undo_command_create( - c"b".as_ptr(), - None, - None, - Some(multi_free), - std::ptr::null_mut(), - ) - }; - let c = unsafe { - oakengine_undo_command_create( - c"c".as_ptr(), - None, - None, - Some(multi_free), - std::ptr::null_mut(), - ) - }; - assert_eq!( - unsafe { oakengine_undo_command_multi_add_child(inner, c) }, - 0 - ); - assert_eq!( - unsafe { oakengine_undo_command_multi_add_child(multi, a) }, - 0 - ); - assert_eq!( - unsafe { oakengine_undo_command_multi_add_child(multi, b) }, - 0 - ); - assert_eq!( - unsafe { oakengine_undo_command_multi_add_child(multi, inner) }, - 0 - ); - assert_eq!(MULTI_FREE.load(Ordering::SeqCst), 0); - - unsafe { oakengine_undo_command_free(multi) }; - // a, b and c (via inner) are all destroyed exactly once. - assert_eq!(MULTI_FREE.load(Ordering::SeqCst), 3); -} - -/// Destroy contracts end to end: free(NULL), free(empty), and the -/// module-level double-free safety of the command/stack handles the facade -/// delegates to (`oakundo_command_free` / `oakundo_undostack_free` clear -/// `ctx` after releasing, so a second free is a no-op). -/// -/// NOTE: the facade's own `oakengine_undo_command_free` frees the wrapper -/// box and is documented as "must not be freed twice"; the double-free-safe -/// contract lives on the module handle level, exercised here through the -/// real oakundo C ABI. The oakundo family has no debug alive counter, so -/// there is no alive-count-baseline to assert. -#[test] -fn free_contracts() { - common::force_link(); - - // Facade free: NULL and empty are no-ops. - unsafe { oakengine_undo_command_free(std::ptr::null_mut()) }; - let eb = empty_engine_ptr(); - unsafe { oakengine_undo_command_free(eb) }; - - // Module command handle: the first free releases (free_fn fires once) - // and clears ctx; the second free is a no-op. - MOD_FREE.store(0, Ordering::SeqCst); - let vtable = oakundo::undocommand::OakUndoCommandVtable { - redo: None, - undo: None, - free_fn: Some(mod_free_cb), - }; - let mut h = oakundo::undocommand::command_init(&vtable, std::ptr::null_mut()); - assert!(!h.ctx.is_null()); - oakundo::undocommand::command_free(&mut h); - assert_eq!(MOD_FREE.load(Ordering::SeqCst), 1); - assert!(h.ctx.is_null()); - 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 = oakundo::undostack::undostack_init(); - assert!(!s.ctx.is_null()); - oakundo::undostack::undostack_free(&mut s); - assert!(s.ctx.is_null()); - oakundo::undostack::undostack_free(&mut s); - - let mut null_h = CHandle::null(); - oakundo::undocommand::command_free(&mut null_h); - oakundo::undocommand::command_free(std::ptr::null_mut()); - oakundo::undostack::undostack_free(std::ptr::null_mut()); -} - -// --------------------------------------------------------------------------- -// Global stack + undo group (serialized: the facade's stack and open group -// are process-wide) -// --------------------------------------------------------------------------- - -/// The full global-stack and undo-group matrix, serialized in one test -/// because the facade owns the process-wide stack and a single open undo -/// group. Covers every stack-scoped export: handle, clear, count, index, -/// jump, command_text, command_is_done, can_undo/can_redo, push, and the -/// group begin/end/abort lifecycle. -#[test] -fn undo_stack_integration() { - let _lock = GLOBAL_STACK_LOCK.lock(); - common::force_link(); - - // --- Baseline: clear() resets to the single "New/Open Project" row. - assert_eq!(unsafe { oakengine_undo_clear() }, 0); - assert_eq!(unsafe { oakengine_undo_count() }, 1); - assert_eq!(unsafe { oakengine_undo_index() }, 1); - assert_eq!(unsafe { oakengine_undo_can_undo() }, 0); - assert_eq!(unsafe { oakengine_undo_can_redo() }, 0); - - // --- Borrowed handle + Qt-leftover actions. - let h1 = unsafe { oakengine_undo_handle() }; - let h2 = unsafe { oakengine_undo_handle() }; - assert!(!h1.is_null()); - assert_eq!(h1, h2); // stable token - assert_eq!(unsafe { oakengine_undo_update_actions() }, 0); - assert!(unsafe { oakengine_undo_undo_action() }.is_null()); - assert!(unsafe { oakengine_undo_redo_action() }.is_null()); - - // --- command_text / command_is_done on the base row. The two-stage - // getter reports the length WITHOUT the trailing NUL. - let mut buf = [0 as c_char; 64]; - assert_eq!( - unsafe { oakengine_undo_command_text(0, buf.as_mut_ptr(), 64) }, - 16 - ); - assert_eq!(unsafe { read_str(buf.as_ptr()) }, "New/Open Project"); - // NULL buf / zero / negative sizes only report the length. - assert_eq!( - unsafe { oakengine_undo_command_text(0, std::ptr::null_mut(), 64) }, - 16 - ); - assert_eq!( - unsafe { oakengine_undo_command_text(0, buf.as_mut_ptr(), 0) }, - 16 - ); - assert_eq!( - unsafe { oakengine_undo_command_text(0, buf.as_mut_ptr(), -1) }, - 16 - ); - // Out-of-range rows → oakundo NOT_FOUND (-20004) passes through. - assert_eq!( - unsafe { oakengine_undo_command_text(-1, buf.as_mut_ptr(), 64) }, - -20004 - ); - assert_eq!( - unsafe { oakengine_undo_command_text(1, buf.as_mut_ptr(), 64) }, - -20004 - ); - assert_eq!( - unsafe { oakengine_undo_command_text(i64::MAX, buf.as_mut_ptr(), 64) }, - -20004 - ); - assert_eq!( - unsafe { oakengine_undo_command_text(i64::MIN, buf.as_mut_ptr(), 64) }, - -20004 - ); - assert_eq!(unsafe { oakengine_undo_command_is_done(0) }, 1); - assert_eq!(unsafe { oakengine_undo_command_is_done(-1) }, -20004); - assert_eq!(unsafe { oakengine_undo_command_is_done(1) }, -20004); - assert_eq!(unsafe { oakengine_undo_command_is_done(i64::MAX) }, -20004); - - // --- Push a named command; the redo runs eagerly. - STK_REDO.store(0, Ordering::SeqCst); - STK_UNDO.store(0, Ordering::SeqCst); - let a = unsafe { - oakengine_undo_command_create( - c"alpha".as_ptr(), - Some(stk_redo), - Some(stk_undo), - None, - std::ptr::null_mut(), - ) - }; - assert_eq!(unsafe { oakengine_undo_push(a, c"alpha".as_ptr()) }, 0); - assert_eq!(unsafe { oakengine_undo_count() }, 2); - assert_eq!(unsafe { oakengine_undo_index() }, 2); - assert_eq!(STK_REDO.load(Ordering::SeqCst), 1); - assert_eq!(unsafe { oakengine_undo_can_undo() }, 1); - assert_eq!( - unsafe { oakengine_undo_command_text(1, buf.as_mut_ptr(), 64) }, - 5 - ); - assert_eq!(unsafe { read_str(buf.as_ptr()) }, "alpha"); - assert_eq!(unsafe { oakengine_undo_command_is_done(1) }, 1); - - // --- Push a second named command. - let b = unsafe { - oakengine_undo_command_create( - c"beta".as_ptr(), - Some(stk_redo), - Some(stk_undo), - None, - std::ptr::null_mut(), - ) - }; - assert_eq!(unsafe { oakengine_undo_push(b, c"beta".as_ptr()) }, 0); - assert_eq!(unsafe { oakengine_undo_count() }, 3); - assert_eq!(unsafe { oakengine_undo_index() }, 3); - assert_eq!(STK_REDO.load(Ordering::SeqCst), 2); - assert_eq!( - unsafe { oakengine_undo_command_text(2, buf.as_mut_ptr(), 64) }, - 4 - ); - assert_eq!(unsafe { read_str(buf.as_ptr()) }, "beta"); - // Tiny buffer: truncated copy, full length still reported. - let mut small = [0 as c_char; 2]; - assert_eq!( - unsafe { oakengine_undo_command_text(1, small.as_mut_ptr(), 2) }, - 5 - ); - assert_eq!(unsafe { read_str(small.as_ptr()) }, "a"); - - // --- jump() legal matrix. jump(1) from index 3 undoes BOTH beta and - // alpha (the stack undoes back-to-front until the done-count is 1). - assert_eq!(unsafe { oakengine_undo_jump(1) }, 0); - assert_eq!(unsafe { oakengine_undo_index() }, 1); - assert_eq!(STK_UNDO.load(Ordering::SeqCst), 2); - assert_eq!(unsafe { oakengine_undo_can_undo() }, 0); - assert_eq!(unsafe { oakengine_undo_can_redo() }, 1); - assert_eq!(unsafe { oakengine_undo_command_is_done(1) }, 0); - assert_eq!(unsafe { oakengine_undo_command_is_done(2) }, 0); - - assert_eq!(unsafe { oakengine_undo_jump(0) }, 0); - // The base "New/Open Project" row is never undoable, so the index - // bottoms out at 1 rather than 0. - assert_eq!(unsafe { oakengine_undo_index() }, 1); - assert_eq!(STK_UNDO.load(Ordering::SeqCst), 2); - - // Negative index is clamped to 0 (olive jump semantics) — still no - // undo past the base row. - assert_eq!(unsafe { oakengine_undo_jump(-5) }, 0); - assert_eq!(unsafe { oakengine_undo_index() }, 1); - - // Oversized index is clamped to the done-command count. - assert_eq!(unsafe { oakengine_undo_jump(999) }, 0); - assert_eq!(unsafe { oakengine_undo_index() }, 3); - assert_eq!(STK_REDO.load(Ordering::SeqCst), 4); - assert_eq!(unsafe { oakengine_undo_can_undo() }, 1); - assert_eq!(unsafe { oakengine_undo_can_redo() }, 0); - - // --- Undo groups. - // A second begin while a group is open fails with E_STATE (-2); - // ending an empty group discards it (no new row). - assert_eq!(unsafe { oakengine_undo_group_begin(c"anon".as_ptr()) }, 0); - assert_eq!(unsafe { oakengine_undo_group_begin(c"again".as_ptr()) }, -2); - assert_eq!(unsafe { oakengine_undo_group_end() }, 0); - assert_eq!(unsafe { oakengine_undo_count() }, 3); - assert_eq!(unsafe { oakengine_undo_index() }, 3); - - // begin → push children → end pushes ONE grouped row. - assert_eq!( - unsafe { oakengine_undo_group_begin(c"grouped".as_ptr()) }, - 0 - ); - let c1 = unsafe { - oakengine_undo_command_create( - c"c1".as_ptr(), - Some(stk_redo), - Some(stk_undo), - None, - std::ptr::null_mut(), - ) - }; - let c2 = unsafe { - oakengine_undo_command_create( - c"c2".as_ptr(), - Some(stk_redo), - Some(stk_undo), - None, - std::ptr::null_mut(), - ) - }; - assert_eq!(unsafe { oakengine_undo_push(c1, c"c1".as_ptr()) }, 0); - assert_eq!(unsafe { oakengine_undo_push(c2, c"c2".as_ptr()) }, 0); - // Both children were redo'd eagerly into the group, not the stack. - assert_eq!(STK_REDO.load(Ordering::SeqCst), 6); - assert_eq!(unsafe { oakengine_undo_count() }, 3); - assert_eq!(unsafe { oakengine_undo_group_end() }, 0); - assert_eq!(unsafe { oakengine_undo_count() }, 4); - assert_eq!(unsafe { oakengine_undo_index() }, 4); - assert_eq!( - unsafe { oakengine_undo_command_text(3, buf.as_mut_ptr(), 64) }, - 7 - ); - assert_eq!(unsafe { read_str(buf.as_ptr()) }, "grouped"); - assert_eq!(unsafe { oakengine_undo_command_is_done(3) }, 1); - - // Undo the group: children undo in REVERSE order. - assert_eq!(unsafe { oakengine_undo_jump(3) }, 0); - assert_eq!(unsafe { oakengine_undo_index() }, 3); - assert_eq!(STK_UNDO.load(Ordering::SeqCst), 4); - assert_eq!(unsafe { oakengine_undo_command_is_done(3) }, 0); - assert_eq!(unsafe { oakengine_undo_can_redo() }, 1); - // Redo the group: children redo in INSERTION order. - assert_eq!(unsafe { oakengine_undo_jump(4) }, 0); - assert_eq!(unsafe { oakengine_undo_index() }, 4); - assert_eq!(STK_REDO.load(Ordering::SeqCst), 8); - // Undo again for the abort phase. - assert_eq!(unsafe { oakengine_undo_jump(3) }, 0); - assert_eq!(STK_UNDO.load(Ordering::SeqCst), 6); - - // begin → push → abort discards the group and undoes the executed - // child (see `group_abort_undoes_children_repro`). - assert_eq!(unsafe { oakengine_undo_group_begin(c"abort".as_ptr()) }, 0); - let c3 = unsafe { - oakengine_undo_command_create( - c"c3".as_ptr(), - Some(stk_redo), - Some(stk_undo), - None, - std::ptr::null_mut(), - ) - }; - assert_eq!(unsafe { oakengine_undo_push(c3, c"c3".as_ptr()) }, 0); - assert_eq!(STK_REDO.load(Ordering::SeqCst), 9); - assert_eq!(unsafe { oakengine_undo_group_abort() }, 0); - // The abort rolls the executed child back: c3's undo ran exactly once. - // The group itself is discarded (no undo row), so count/index are - // unchanged. - assert_eq!(STK_UNDO.load(Ordering::SeqCst), 7); - assert_eq!(unsafe { oakengine_undo_count() }, 4); // unchanged - assert_eq!(unsafe { oakengine_undo_index() }, 3); - - // End/abort with no open group fail with E_STATE. - assert_eq!(unsafe { oakengine_undo_group_end() }, -2); - assert_eq!(unsafe { oakengine_undo_group_abort() }, -2); - - // --- Illegal push inputs (rejected before any stack access). - assert_eq!( - unsafe { oakengine_undo_push(std::ptr::null_mut(), c"x".as_ptr()) }, - -1 - ); - let eb = empty_engine_ptr(); - assert_eq!(unsafe { oakengine_undo_push(eb, c"x".as_ptr()) }, -1); - unsafe { oakengine_undo_command_free(eb) }; - - // Cleanup: back to baseline. - assert_eq!(unsafe { oakengine_undo_clear() }, 0); - assert_eq!(unsafe { oakengine_undo_count() }, 1); - assert_eq!(unsafe { oakengine_undo_index() }, 1); -} - -// --------------------------------------------------------------------------- -// Real-bug regressions (previously `#[ignore]`d repros of facade bugs, -// now fixed; kept as regression tests) -// --------------------------------------------------------------------------- - -/// REGRESSION — `oakengine_undo_push(cmd, NULL)` (and an empty-string -/// label) must not crash. -/// -/// The facade's `push_or_run` (src/undo.rs) used to turn a NULL/empty name -/// into `String::new()` and pass its DANGLING `as_ptr()` (address 0x1 — -/// Rust empty-string pointers are never NULL) to the oakundo module's -/// `oakundo_undostack_push`, whose `read_name` treats any non-NULL pointer -/// as a valid C string and runs `CStr::from_ptr` (strlen) on it, faulting -/// on the unmapped page. The crash is NOT caught by the catch_unwind -/// guards (it is a hard SIGSEGV, not a panic). Fixed: a NULL/empty label -/// now crosses the facade as a real NULL, which the module reads as an -/// empty label. `name` is documented as legal-NULL in both the module -/// header (`include/undo/undostack.h`: "NULL behaves like an empty -/// label") and the facade docs. -#[test] -fn null_name_push_repro() { - let _lock = GLOBAL_STACK_LOCK.lock(); - common::force_link(); - assert_eq!(unsafe { oakengine_undo_clear() }, 0); - - let cmd = unsafe { - oakengine_undo_command_create(c"x".as_ptr(), None, None, None, std::ptr::null_mut()) - }; - // NULL name is a documented-legal label; this must not crash. - assert_eq!(unsafe { oakengine_undo_push(cmd, std::ptr::null()) }, 0); - - // An empty C string label walks the same dangling-pointer path. - let cmd = unsafe { - oakengine_undo_command_create(c"x".as_ptr(), None, None, None, std::ptr::null_mut()) - }; - assert_eq!(unsafe { oakengine_undo_push(cmd, c"".as_ptr()) }, 0); - - assert_eq!(unsafe { oakengine_undo_clear() }, 0); -} - -/// REGRESSION — `oakengine_undo_group_begin(NULL)` + -/// `oakengine_undo_group_end()` (and empty-string group names) must not -/// crash. -/// -/// Same root cause as [`null_name_push_repro`]: `oakengine_undo_group_end` -/// (src/undo.rs) stores the group name as a Rust `String` and used to pass -/// its `as_ptr()` to `oakundo_undostack_push_pre_executed`; a NULL (or -/// empty) name was a dangling 0x1 pointer there, and the module's -/// `read_name` crashed on it. Fixed: the empty label now crosses the -/// facade as a real NULL. The group-abort path never crosses the name and -/// is safe. -#[test] -fn null_name_group_repro() { - let _lock = GLOBAL_STACK_LOCK.lock(); - common::force_link(); - assert_eq!(unsafe { oakengine_undo_clear() }, 0); - - assert_eq!(unsafe { oakengine_undo_group_begin(std::ptr::null()) }, 0); - // End of a NULL-named (empty) group must not crash. - assert_eq!(unsafe { oakengine_undo_group_end() }, 0); - - // Same path with an empty C string name. - assert_eq!(unsafe { oakengine_undo_group_begin(c"".as_ptr()) }, 0); - assert_eq!(unsafe { oakengine_undo_group_end() }, 0); - - assert_eq!(unsafe { oakengine_undo_clear() }, 0); -} - -/// Counter for the abort repro (own set: kept isolated from the parallel -/// tests' counters). -static ABORT_UNDO: AtomicI32 = AtomicI32::new(0); - -unsafe extern "C" fn abort_undo_cb(_ud: *mut c_void) { - ABORT_UNDO.fetch_add(1, Ordering::SeqCst); -} - -/// REGRESSION — `oakengine_undo_group_abort()` must undo the group's -/// executed children. -/// -/// The facade (src/undo.rs) used to close the abort with -/// `oakundo_command_undo_now(open.multi)` on a multi command that was -/// never marked done (each child was redo'd eagerly at push time, but the -/// multi's own `done` flag stays false), and oakundo's documented -/// `undo_now` is a no-op on a not-done command. Net effect: the child's -/// undo callback never fired, so the group's side effects were NOT rolled -/// back — contradicting the documented "undo all executed children and -/// discard the group". Fixed: the abort undoes each executed child -/// individually, in reverse insertion order. (The smoke test in -/// tests/undo.rs misses this: its `STK_UNDO_COUNT == 1` assertion is -/// satisfied by a leftover value from an earlier jump.) -#[test] -fn group_abort_undoes_children_repro() { - let _lock = GLOBAL_STACK_LOCK.lock(); - common::force_link(); - assert_eq!(unsafe { oakengine_undo_clear() }, 0); - - ABORT_UNDO.store(0, Ordering::SeqCst); - assert_eq!(unsafe { oakengine_undo_group_begin(c"abort".as_ptr()) }, 0); - let c = unsafe { - oakengine_undo_command_create( - c"c".as_ptr(), - None, - Some(abort_undo_cb), - None, - std::ptr::null_mut(), - ) - }; - assert_eq!(unsafe { oakengine_undo_push(c, c"c".as_ptr()) }, 0); - assert_eq!(unsafe { oakengine_undo_group_abort() }, 0); - // Documented behavior: the executed child's undo must run. - assert_eq!(ABORT_UNDO.load(Ordering::SeqCst), 1); -} diff --git a/crates/oakengine.bk/src/test_support/linkage.rs b/crates/oakengine.bk/src/test_support/linkage.rs deleted file mode 100644 index 90dbdacf5..000000000 --- a/crates/oakengine.bk/src/test_support/linkage.rs +++ /dev/null @@ -1,51 +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 . - -//! 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(); -} diff --git a/crates/oakengine.bk/src/test_support/mod.rs b/crates/oakengine.bk/src/test_support/mod.rs deleted file mode 100644 index 731705aea..000000000 --- a/crates/oakengine.bk/src/test_support/mod.rs +++ /dev/null @@ -1,55 +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 . - -//! 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 — its -//! `oakcore_audioparams_*` re-exports 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_library; -mod it_plugin; -mod it_storage; -mod it_task; -mod it_timeline; -mod it_undo; -mod linkage; -mod node; -mod plugin; -mod render; -mod task; -mod undo; diff --git a/crates/oakengine.bk/src/test_support/node.rs b/crates/oakengine.bk/src/test_support/node.rs deleted file mode 100644 index bdac6b4d3..000000000 --- a/crates/oakengine.bk/src/test_support/node.rs +++ /dev/null @@ -1,471 +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 . - -//! Smoke tests for the node graph, project and footage families -//! (`engine/include/oakengine/{node,project,footage}.h`). -//! -//! The facade owns a process-wide undo stack, so every test that pushes -//! undoable commands (project new/add, label, connect, keyframes) is -//! serialized inside the single `project_node_keyframe_lifecycle` test; -//! the failure-path tests only exercise non-mutating calls and run in -//! parallel. - -use super::common; - -use std::ffi::{c_char, c_int}; - -use crate::node::{ - oakengine_footage_borrow, oakengine_footage_last_error, oakengine_footage_probe, - oakengine_folder_add_child, oakengine_folder_item_child, oakengine_folder_item_child_count, - oakengine_node_connect, oakengine_node_disconnect, oakengine_node_factory_create_from_id, - oakengine_node_factory_id_count, oakengine_node_factory_name_from_id, - oakengine_node_factory_node_at, oakengine_node_get_input, oakengine_node_get_input_at_time, - oakengine_node_get_label, oakengine_node_get_name, oakengine_node_get_type_id, - oakengine_node_identity, oakengine_node_input_get_type, oakengine_node_input_id, - oakengine_node_input_is_connected, oakengine_node_is_clip, oakengine_node_is_folder, - oakengine_node_is_track, oakengine_node_is_viewer_output, oakengine_node_keyframe_count, - oakengine_node_free, oakengine_node_set_input, oakengine_node_set_input_at_time, oakengine_node_set_label, - oakengine_project_add_node, oakengine_project_create, oakengine_project_filename, - oakengine_project_free, oakengine_project_import_footage, oakengine_project_load, - oakengine_project_name, oakengine_project_new, oakengine_project_node_at, - oakengine_project_node_count, oakengine_project_root, oakengine_project_save, - oakengine_project_set_filename, OakNodeValue, -}; - -/// Registered generator node ids used by the tests. -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 { - unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } - .to_string_lossy() - .into_owned() -} - -/// Force the oakundo command module into the link: the oaknode bridge -/// resolves `oakundo_command_init` at runtime with -/// `dlsym(RTLD_DEFAULT)`, and nothing references that symbol at link -/// 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 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::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() -} - -/// A float POD value. -fn float_value(x: f64) -> OakNodeValue { - OakNodeValue { - kind: 2, // OAK_NODE_VALUE_FLOAT - num: 0, - den: 0, - f: [x, 0.0, 0.0, 0.0], - } -} - -/// The index of the first project node whose type id matches `id`, or -1. -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) }; - if node.is_null() { - continue; - } - let mut buf = [0 as c_char; 256]; - let len = unsafe { oakengine_node_get_type_id(node, buf.as_mut_ptr(), 256) }; - if len > 0 && unsafe { read_buf(&mut buf) } == id { - return i; - } - } - -1 -} - -// --------------------------------------------------------------------------- -// Serialized stack-mutating test -// --------------------------------------------------------------------------- - -/// Project lifecycle, node add/label/connect/keyframes and a save/load -/// round-trip — all in ONE test because the facade's undo stack is -/// process-wide (the same serialization the undo family uses). -#[test] -fn project_node_keyframe_lifecycle() { - let _stack = super::it_undo::GLOBAL_STACK_LOCK.lock(); - common::force_link(); - let _ = force_oakundo_command_link(); - // The undo commands below would bind the project to the default user - // library and write through to it; hold the storage lock and disable - // the backend for the whole test. - let _storage = common::storage_off_guard(); - - // ---- project: create → new → name/filename readback ---------------- - let project = oakengine_project_create(); - assert!(!project.is_null()); - - // Freeing NULL is a no-op. - unsafe { oakengine_project_free(std::ptr::null_mut()) }; - - // A fresh project is untitled. - let mut buf = [0 as c_char; 256]; - let len = unsafe { oakengine_project_name(project, buf.as_mut_ptr(), 256) }; - assert!(len > 0); - assert_eq!(unsafe { read_buf(&mut buf) }, "(untitled)"); - - assert_eq!(unsafe { oakengine_project_new(project) }, 0); - // A second new on the same project is rejected with E_STATE. - assert_eq!(unsafe { oakengine_project_new(project) }, -2); - - // The name is derived from the filename base (untitled → "(untitled)" - // until a filename is set). - assert_eq!( - unsafe { oakengine_project_filename(project, buf.as_mut_ptr(), 256) }, - 0 - ); - assert_eq!( - unsafe { - oakengine_project_set_filename(project, c"/tmp/oakengine_node_test.ovexml".as_ptr()) - }, - 0 - ); - let len = unsafe { oakengine_project_filename(project, buf.as_mut_ptr(), 256) }; - assert!(len > 0); - assert!(unsafe { read_buf(&mut buf) }.ends_with("oakengine_node_test.ovexml")); - let len = unsafe { oakengine_project_name(project, buf.as_mut_ptr(), 256) }; - assert!(len > 0); - assert_eq!(unsafe { read_buf(&mut buf) }, "oakengine_node_test"); - - // ---- factory + node creation --------------------------------------- - let factory_count = oakengine_node_factory_id_count(); - assert!(factory_count > 0); - - // Discover a registered id from the prototype library. - let proto = unsafe { oakengine_node_factory_node_at(0) }; - assert!(!proto.is_null()); - let len = unsafe { oakengine_node_get_type_id(proto, buf.as_mut_ptr(), 256) }; - assert!(len > 0); - let type_id = unsafe { read_buf(&mut buf) }; - - // Factory name lookup round-trip. - let name_len = unsafe { - oakengine_node_factory_name_from_id( - type_id.as_ptr() as *const c_char, - buf.as_mut_ptr(), - 256, - ) - }; - assert!(name_len > 0); - - // Creating from the discovered id yields a node with a matching type. - let orphan = - unsafe { oakengine_node_factory_create_from_id(type_id.as_ptr() as *const c_char) }; - assert!(!orphan.is_null()); - unsafe { oakengine_node_get_type_id(orphan, buf.as_mut_ptr(), 256) }; - assert_eq!(unsafe { read_buf(&mut buf) }, type_id); - - // ---- add nodes to the project --------------------------------------- - // Root folder occupies slot 0; added nodes follow. - let solid = unsafe { - oakengine_project_add_node( - project, - c"org.olivevideoeditor.Olive.solidgenerator".as_ptr(), - ) - }; - assert!(!solid.is_null()); - let transform = unsafe { - oakengine_project_add_node(project, c"org.olivevideoeditor.Olive.transform".as_ptr()) - }; - assert!(!transform.is_null()); - let value = unsafe { - oakengine_project_add_node(project, c"org.olivevideoeditor.Olive.value".as_ptr()) - }; - assert!(!value.is_null()); - - // 3 added nodes + the root folder. - let count = unsafe { oakengine_project_node_count(project) }; - assert_eq!(count, 4); - - // node_at lookup and type-id readback. - let idx = unsafe { find_node(project, TYPE_ID_SOLID) }; - assert!(idx >= 0); - let at = unsafe { oakengine_project_node_at(project, idx) }; - assert!(!at.is_null()); - let len = unsafe { oakengine_node_get_type_id(at, buf.as_mut_ptr(), 256) }; - assert!(len > 0); - assert_eq!(unsafe { read_buf(&mut buf) }, TYPE_ID_SOLID); - - // Node type queries. - assert_eq!(unsafe { oakengine_node_is_clip(solid) }, 0); - assert_eq!(unsafe { oakengine_node_is_track(solid) }, 0); - assert_eq!(unsafe { oakengine_node_is_folder(solid) }, 0); - assert_eq!(unsafe { oakengine_node_is_viewer_output(solid) }, 0); - - // ---- the project browser's folder-tree walk (M12 P3) ----------------- - // The traversal exports the material bin walks: `project_root` resolves - // the root folder and `folder_item_child_count` / `folder_item_child` - // enumerate its children (folders and media alike), all resolved by the - // nodes' stable identities. The added nodes sit in the graph only until - // an explicit graft (the same FolderAddChild the footage import uses). - let root = unsafe { oakengine_project_root(project) }; - assert!(!root.is_null(), "a new project has a root folder"); - assert_eq!(unsafe { oakengine_node_is_folder(root) }, 1); - assert_eq!(unsafe { oakengine_folder_item_child_count(root) }, 0); - - // Graft two graph nodes under the root folder. - assert_eq!(unsafe { oakengine_folder_add_child(root, solid) }, 0); - assert_eq!(unsafe { oakengine_folder_add_child(root, transform) }, 0); - assert_eq!( - unsafe { oakengine_folder_item_child_count(root) }, - 2, - "the grafted nodes are listed under the root folder" - ); - let solid_id = unsafe { oakengine_node_identity(solid) }; - let mut seen_solid = false; - for i in 0..2 { - let child = unsafe { oakengine_folder_item_child(root, i) }; - assert!(!child.is_null(), "child {i} resolves"); - assert!(unsafe { oakengine_node_identity(child) } != 0); - if unsafe { oakengine_node_identity(child) } == solid_id { - seen_solid = true; - // The child carries the node's factory type id. - let len = unsafe { oakengine_node_get_type_id(child, buf.as_mut_ptr(), 256) }; - assert!(len > 0); - assert_eq!(unsafe { read_buf(&mut buf) }, TYPE_ID_SOLID); - assert_eq!(unsafe { oakengine_node_is_folder(child) }, 0); - } - unsafe { oakengine_node_free(child) }; - } - assert!(seen_solid, "the solid generator is a root child"); - - // Failure paths: NULL project/folder, out-of-range index, non-folder - // handles (the app never hands those to the browser). - assert!(unsafe { oakengine_project_root(std::ptr::null_mut()) }.is_null()); - assert_eq!(unsafe { oakengine_folder_item_child_count(std::ptr::null()) }, 0); - assert!(unsafe { oakengine_folder_item_child(std::ptr::null(), 0) }.is_null()); - assert!(unsafe { oakengine_folder_item_child(root, 99) }.is_null()); - assert_eq!(unsafe { oakengine_folder_item_child_count(solid) }, 0); - assert!(unsafe { oakengine_folder_item_child(solid, 0) }.is_null()); - unsafe { oakengine_node_free(root) }; - - // ---- undoable label + readback -------------------------------------- - assert_eq!( - unsafe { oakengine_node_set_label(solid, c"My Solid".as_ptr()) }, - 0 - ); - let len = unsafe { oakengine_node_get_label(solid, buf.as_mut_ptr(), 256) }; - assert_eq!(len, 8); - assert_eq!(unsafe { read_buf(&mut buf) }, "My Solid"); - // The display name is separate from the label. - let len = unsafe { oakengine_node_get_name(solid, buf.as_mut_ptr(), 256) }; - assert!(len > 0); - - // ---- input introspection + get_input -------------------------------- - // The solid generator has declared inputs. - let _input_id_len = unsafe { oakengine_node_input_id(solid, 0, buf.as_mut_ptr(), 256) }; - assert!(_input_id_len > 0); - assert!(unsafe { read_buf(&mut buf) }.len() > 0); - - // A known float input on the value node: value_in. - assert_eq!( - unsafe { oakengine_node_input_get_type(value, c"value_in".as_ptr()) }, - 2 // OAK_NODE_VALUE_FLOAT - ); - - // get_input readback of a set standard value. - let v = float_value(3.5); - assert_eq!( - unsafe { oakengine_node_set_input(value, c"value_in".as_ptr(), &v) }, - 0 - ); - let mut out: OakNodeValue = unsafe { std::mem::zeroed() }; - assert_eq!( - unsafe { oakengine_node_get_input(value, c"value_in".as_ptr(), &mut out) }, - 0 - ); - assert_eq!(out.kind, 2); - assert!((out.f[0] - 3.5).abs() < 1e-6); - - // ---- connect / disconnect ------------------------------------------- - assert_eq!( - unsafe { oakengine_node_connect(solid, transform, c"tex_in".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_node_input_is_connected(transform, c"tex_in".as_ptr()) }, - 1 - ); - assert_eq!( - unsafe { oakengine_node_disconnect(transform, c"tex_in".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_node_input_is_connected(transform, c"tex_in".as_ptr()) }, - 0 - ); - - // ---- keyframe at-time add/readback ---------------------------------- - // The module's at-time setter is the value-at-time path (keyframing - // is not reachable through the module C ABI, so the input is not - // "keyframed"; see the facade notes). - assert_eq!( - unsafe { oakengine_node_keyframe_count(value, c"value_in".as_ptr()) }, - 0 - ); - let kf = float_value(0.5); - assert_eq!( - unsafe { oakengine_node_set_input_at_time(value, c"value_in".as_ptr(), -1, 0, -1, &kf, 0) }, - 0 - ); - let mut at: OakNodeValue = unsafe { std::mem::zeroed() }; - assert_eq!( - unsafe { - oakengine_node_get_input_at_time(value, c"value_in".as_ptr(), -1, -1, 0, 0, &mut at) - }, - 0 - ); - assert_eq!(at.kind, 2); - assert!((at.f[0] - 0.5).abs() < 1e-6); - - // ---- project save → fresh load round-trip --------------------------- - let path = c"/tmp/oakengine_node_test.ovexml"; - assert_eq!(unsafe { oakengine_project_save(project, path.as_ptr()) }, 0); - assert!(std::path::Path::new("/tmp/oakengine_node_test.ovexml").exists()); - - unsafe { oakengine_project_free(project) }; - - let project2 = oakengine_project_create(); - assert!(!project2.is_null()); - let mut err = [0 as c_char; 512]; - let rc = unsafe { oakengine_project_load(project2, path.as_ptr(), err.as_mut_ptr(), 512) }; - if rc != 0 { - // The module serializer round-trip is not fully implemented in - // the oaknode crate; keep the rest of the test valid by cleaning - // up and re-verifying the error path instead. - unsafe { oakengine_project_free(project2) }; - // The bad-path load below still exercises the err buffer. - } else { - assert!(unsafe { oakengine_project_node_count(project2) } >= 1); - unsafe { oakengine_project_free(project2) }; - } - - // ---- load with a bad path → error + non-empty err buffer ------------ - let project3 = oakengine_project_create(); - assert!(!project3.is_null()); - let mut err = [0 as c_char; 512]; - let rc = unsafe { - oakengine_project_load( - project3, - c"/no/such/project/file.ove".as_ptr(), - err.as_mut_ptr(), - 512, - ) - }; - assert!(rc < 0); - let err_len = unsafe { std::ffi::CStr::from_ptr(err.as_ptr()) } - .to_bytes() - .len(); - assert!(err_len > 0, "load error buffer must be non-empty"); - unsafe { oakengine_project_free(project3) }; - - // Import failure on a valid project: nonexistent path → NULL. - let project4 = oakengine_project_create(); - assert_eq!(unsafe { oakengine_project_new(project4) }, 0); - let imported = - unsafe { oakengine_project_import_footage(project4, c"/no/such/media.mp4".as_ptr()) }; - assert!(imported.is_null()); - unsafe { oakengine_project_free(project4) }; -} - -// --------------------------------------------------------------------------- -// Non-mutating failure paths (no undo-stack access; run in parallel) -// --------------------------------------------------------------------------- - -/// NULL handles yield -1 and out-of-range indexes yield -4. -#[test] -fn node_failure_paths() { - let _stack = super::it_undo::GLOBAL_STACK_LOCK.lock(); - common::force_link(); - - // NULL node → OAKENGINE_E_INVALID (-1). - let mut out: OakNodeValue = unsafe { std::mem::zeroed() }; - assert_eq!( - unsafe { oakengine_node_get_input(std::ptr::null(), c"value_in".as_ptr(), &mut out) }, - -1 - ); - assert_eq!( - unsafe { oakengine_node_set_label(std::ptr::null_mut(), c"x".as_ptr()) }, - -1 - ); - - // Out-of-range input index → OAKENGINE_E_NOT_FOUND (-4). - let orphan = unsafe { - oakengine_node_factory_create_from_id(c"org.olivevideoeditor.Olive.value".as_ptr()) - }; - assert!(!orphan.is_null()); - let mut buf = [0 as c_char; 64]; - assert_eq!( - unsafe { oakengine_node_input_id(orphan, 999, buf.as_mut_ptr(), 64) }, - -4 - ); - assert_eq!( - unsafe { oakengine_node_input_id(orphan, -1, buf.as_mut_ptr(), 64) }, - -4 - ); - - // NULL handle for a count query is a 0-result, not an error. - assert_eq!( - unsafe { oakengine_node_keyframe_count(std::ptr::null(), c"value_in".as_ptr()) }, - 0 - ); -} - -/// Footage probe/import/borrow failure paths (no media required). -#[test] -fn footage_failure_paths() { - let _stack = super::it_undo::GLOBAL_STACK_LOCK.lock(); - common::force_link(); - - // Probing a nonexistent path → NULL + a non-empty last error. - let probe = unsafe { oakengine_footage_probe(c"/no/such/media.mp4".as_ptr()) }; - assert!(probe.is_null()); - let mut err = [0 as c_char; 512]; - let len = oakengine_footage_last_error(err.as_mut_ptr(), 512); - assert!( - len > 0, - "footage_last_error must be non-empty after a failed probe" - ); - - // NULL path → NULL. - let probe2 = unsafe { oakengine_footage_probe(std::ptr::null()) }; - assert!(probe2.is_null()); - - // Borrowing a non-footage node → NULL. - let orphan = unsafe { - oakengine_node_factory_create_from_id(c"org.olivevideoeditor.Olive.value".as_ptr()) - }; - assert!(!orphan.is_null()); - let borrowed = unsafe { oakengine_footage_borrow(orphan) }; - assert!(borrowed.is_null()); - - // Import into a NULL project → NULL. - let imported = - unsafe { oakengine_project_import_footage(std::ptr::null_mut(), c"/x.mp4".as_ptr()) }; - assert!(imported.is_null()); -} diff --git a/crates/oakengine.bk/src/test_support/plugin.rs b/crates/oakengine.bk/src/test_support/plugin.rs deleted file mode 100644 index 1e5539795..000000000 --- a/crates/oakengine.bk/src/test_support/plugin.rs +++ /dev/null @@ -1,92 +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 . - -//! Smoke tests for the plugin family (`engine/include/oakengine/plugin.h`). - -use super::common; - -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, -}; - -/// Callback registration round-trips (NULL clears). -#[test] -fn provider_registration() { - unsafe extern "C" fn viewer( - _userdata: *mut std::ffi::c_void, - ) -> *mut crate::handle::OakEngineNode { - std::ptr::null_mut() - } - assert_eq!( - unsafe { oakengine_plugin_set_active_viewer_provider(Some(viewer), std::ptr::null_mut()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_plugin_set_active_viewer_provider(None, std::ptr::null_mut()) }, - 0 - ); - - unsafe extern "C" fn create( - _message: *const std::ffi::c_char, - _title: *const std::ffi::c_char, - _userdata: *mut std::ffi::c_void, - ) -> *mut std::ffi::c_void { - std::ptr::null_mut() - } - assert_eq!( - unsafe { - oakengine_plugin_set_progress_reporter_factory( - Some(create), - None, - None, - None, - std::ptr::null_mut(), - ) - }, - 0 - ); - assert_eq!( - unsafe { - oakengine_plugin_set_progress_reporter_factory( - None, - None, - None, - None, - std::ptr::null_mut(), - ) - }, - 0 - ); -} - -/// NULL path fails with E_INVALID. -#[test] -fn load_plugins_null_path() { - assert_eq!( - unsafe { oakengine_plugin_load_plugins(std::ptr::null()) }, - -1 - ); -} - -/// Push-button click is a documented stub (oakplugin has no button API). -#[test] -fn push_button_unbacked() { - assert_eq!( - unsafe { oakengine_plugin_node_push_button_clicked(std::ptr::null_mut(), c"btn".as_ptr()) }, - -3 - ); -} diff --git a/crates/oakengine.bk/src/test_support/render.rs b/crates/oakengine.bk/src/test_support/render.rs deleted file mode 100644 index 4553434e4..000000000 --- a/crates/oakengine.bk/src/test_support/render.rs +++ /dev/null @@ -1,235 +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 . - -//! Smoke tests for the render family (`engine/include/oakengine/ -//! {renderer,color,lut}.h`). The render manager is not initialized in -//! tests, so the manager/cacher families exercise the module's STATE -//! error path and the renderer/color families exercise the NULL/invalid -//! argument paths (real rendering needs the deferred node family plus an -//! initialized render manager). The empty-sequence repro at the bottom -//! brings the process-global manager up for its run and shuts it back -//! down, restoring this invariant for the tests that follow it; the two -//! manager-touching tests serialize on [`SERIAL`] so the repro never -//! overlaps `render_manager_not_initialized` (the `it_task`/`it_export` -//! pattern). - -use super::common; - -use std::ffi::{c_char, c_double}; -use std::sync::{Mutex, MutexGuard}; - -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, - oakengine_frame_channel_count, oakengine_frame_data, oakengine_frame_free, - oakengine_frame_height, oakengine_frame_width, oakengine_lut_directory_count, - oakengine_lut_set_directories, oakengine_render_cache_set_display_color_processor, - oakengine_render_cache_set_multicam_node, oakengine_render_manager_requested_backend, - oakengine_render_manager_set_aggressive_garbage_collection, oakengine_renderer_create, - oakengine_renderer_free, oakengine_renderer_last_error, oakengine_renderer_set_mode, - OakColorTransformPod, -}; - -/// Serialize the manager-touching render tests (the pattern in -/// `it_task`/`it_export`). The empty-sequence repro brings the -/// process-global render manager up (and back down) inside its run; -/// without the lock it overlaps `render_manager_not_initialized`, whose -/// STATE paths then see an initialized manager. -static SERIAL: Mutex<()> = Mutex::new(()); - -fn serial() -> MutexGuard<'static, ()> { - SERIAL.lock().unwrap_or_else(|e| e.into_inner()) -} - -/// Render manager state without initialization: the module reports its -/// STATE error, passed through untranslated (-70002). -#[test] -fn render_manager_not_initialized() { - let _stack = super::it_undo::GLOBAL_STACK_LOCK.lock(); - let _g = serial(); - assert_eq!( - unsafe { oakengine_render_manager_set_aggressive_garbage_collection(1) }, - -70002 - ); - // Cache setters with NULL handles → same module STATE. - assert_eq!( - unsafe { oakengine_render_cache_set_display_color_processor(std::ptr::null_mut()) }, - -70002 - ); - assert_eq!( - unsafe { oakengine_render_cache_set_multicam_node(std::ptr::null_mut()) }, - -70002 - ); - // 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. -#[test] -fn renderer_lifecycle() { - let _stack = super::it_undo::GLOBAL_STACK_LOCK.lock(); - // NULL seq → NULL renderer. - let r = unsafe { - oakengine_renderer_create( - std::ptr::null_mut(), - 1920, - 1080, - 4, - 30000, - 1001, - std::ptr::null(), - ) - }; - assert!(r.is_null()); - - // NULL free / last_error are safe. - unsafe { oakengine_renderer_free(std::ptr::null_mut()) }; - let mut buf = [0 as c_char; 64]; - assert_eq!( - unsafe { oakengine_renderer_last_error(std::ptr::null(), buf.as_mut_ptr(), 64) }, - -1 - ); - assert_eq!( - unsafe { oakengine_renderer_set_mode(std::ptr::null_mut(), 0) }, - -1 - ); -} - -/// Frame accessors on NULL / empty handles report zero/NULL safely. -#[test] -fn frame_accessors_null_safe() { - let _stack = super::it_undo::GLOBAL_STACK_LOCK.lock(); - assert_eq!(unsafe { oakengine_frame_width(std::ptr::null()) }, 0); - assert_eq!(unsafe { oakengine_frame_height(std::ptr::null()) }, 0); - assert_eq!( - unsafe { oakengine_frame_channel_count(std::ptr::null()) }, - 0 - ); - assert!(unsafe { oakengine_frame_data(std::ptr::null()) }.is_null()); - unsafe { oakengine_frame_free(std::ptr::null_mut()) }; -} - -/// Color processor: NULL input is rejected; a valid-argument call either -/// returns a handle (possibly invalid — OCIO may be a stub bridge) or -/// NULL; freeing is safe either way. -#[test] -fn color_processor_lifecycle() { - let _stack = super::it_undo::GLOBAL_STACK_LOCK.lock(); - // NULL input → NULL. - let p = unsafe { - oakengine_color_processor_create(std::ptr::null(), std::ptr::null(), std::ptr::null(), 0) - }; - assert!(p.is_null()); - - // Valid arguments: the engine contract allows NULL (OCIO unavailable) - // or a handle whose is_valid may be 0. - let mut dest = OakColorTransformPod { - is_display: 0, - output: c"ACEScg".as_ptr(), - view: std::ptr::null(), - look: std::ptr::null(), - }; - let p = unsafe { - oakengine_color_processor_create( - std::ptr::null(), - c"Linear Rec.709 (sRGB)".as_ptr(), - &dest, - 0, - ) - }; - if !p.is_null() { - let valid = unsafe { oakengine_color_processor_is_valid(p) }; - assert!(valid == 0 || valid == 1); - unsafe { oakengine_color_processor_free(p) }; - } - // NULL free is a no-op. - unsafe { oakengine_color_processor_free(std::ptr::null_mut()) }; - - // convert_color with a NULL processor → E_INVALID. - let mut out_rgba = [0.0_f64; 4]; - let in_rgba = [0.5_f64, 0.5, 0.5, 1.0]; - assert_eq!( - unsafe { - oakengine_color_processor_convert_color( - std::ptr::null(), - in_rgba.as_ptr(), - out_rgba.as_mut_ptr(), - ) - }, - -1 - ); - let _ = dest; -} - -/// Color manager config path: without a configured manager the module -/// reports STATE; the last-error string starts empty. -#[test] -fn color_manager_and_error() { - let _stack = super::it_undo::GLOBAL_STACK_LOCK.lock(); - let mut buf = [0 as c_char; 64]; - let rc = unsafe { - oakengine_color_manager_get_config_filename(std::ptr::null(), buf.as_mut_ptr(), 64) - }; - assert_eq!(rc, -70002); - - let len = unsafe { oakengine_color_last_error(buf.as_mut_ptr(), 64) }; - assert_eq!(len, 0); -} - -/// LUT library stubs report the documented neutral values. -#[test] -fn lut_library_stubs() { - let _stack = super::it_undo::GLOBAL_STACK_LOCK.lock(); - assert_eq!(unsafe { oakengine_lut_directory_count() }, 0); - assert_eq!( - unsafe { oakengine_lut_set_directories(std::ptr::null(), 0) }, - -3 - ); -} - -// repro: render_audio on an empty sequence (playback tick on an empty timeline). -#[test] -fn render_audio_empty_sequence_no_crash() { - let _stack = super::it_undo::GLOBAL_STACK_LOCK.lock(); - let _g = serial(); - super::common::force_link(); - unsafe { - assert_eq!(crate::render::oakengine_render_manager_init(), 0); - let project = crate::node::oakengine_project_create(); - assert!(!project.is_null()); - assert_eq!(crate::node::oakengine_project_new(project), 0); - let name = std::ffi::CString::new("s").unwrap(); - let seq = crate::timeline::oakengine_sequence_new(project, name.as_ptr()); - assert!(!seq.is_null()); - assert_eq!(crate::timeline::oakengine_sequence_add_track(seq, 1), 0); // audio track, no clips - let r = crate::render::oakengine_renderer_create(seq, 64, 64, 4, 25, 1, std::ptr::null()); - assert!(!r.is_null()); - for i in 0..5 { - let buf = crate::render::oakengine_renderer_render_audio(r, i * 2048, 2048); - if !buf.is_null() { - crate::render::oakengine_audio_free(buf); - } - } - crate::render::oakengine_renderer_free(r); - crate::node::oakengine_project_free(project); - // The repro initialized the process-global render manager; tear it - // down so the tests after this one keep the module header's - // documented "manager not initialized" contract (the STATE paths - // asserted by `render_manager_not_initialized`). - assert_eq!(crate::render::oakengine_render_manager_shutdown(), 0); - } -} diff --git a/crates/oakengine.bk/src/test_support/task.rs b/crates/oakengine.bk/src/test_support/task.rs deleted file mode 100644 index 15ac3ad9e..000000000 --- a/crates/oakengine.bk/src/test_support/task.rs +++ /dev/null @@ -1,331 +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 . - -//! Smoke tests for the task family (`engine/include/oakengine/task.h`). -//! -//! NOTE: the oaktask crate is currently NOT a facade dev-dependency (its -//! Cargo.toml is being restructured in a parallel session), so this file -//! cannot LINK until the dev-dependency is re-added. It is written against -//! the real surface and should run unmodified once `Cargo.toml` is -//! restored. -//! -//! Two process-wide states serialize the tests, mirroring tests/undo.rs: -//! the facade's global task manager (initialized lazily) and its global -//! undo stack (`oakengine_project_new` clears it), so the manager-mutating -//! and project-mutating tests are each a single test function; the -//! handle/accessor tests touch neither and run in parallel. - -use super::common; - -use std::ffi::{c_char, c_int, c_void}; - -use crate::node::{ - oakengine_node_free, oakengine_project_create, oakengine_project_free, oakengine_project_new, - oakengine_project_root, oakengine_project_set_filename, -}; -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, - oakengine_task_create_project_save_otio, oakengine_task_create_proxy, oakengine_task_error, - 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, -}; - -/// Read a two-stage string buffer (NUL-terminated) as a Rust `String`. -fn read_buf(buf: &mut [c_char]) -> String { - 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() -} - -// --------------------------------------------------------------------------- -// NULL / invalid-handle rejection (no shared state; parallel-safe) -// --------------------------------------------------------------------------- - -/// Every accessor rejects a NULL task with OAKENGINE_E_INVALID (-1); -/// creators return NULL; the CLI dialog returns 0 for NULL per the capi. -#[test] -fn task_null_handles_are_rejected() { - common::force_link(); - - let mut buf = [0 as c_char; 256]; - - assert_eq!( - unsafe { oakengine_task_title(std::ptr::null_mut(), buf.as_mut_ptr(), 256) }, - -1 - ); - assert_eq!( - unsafe { oakengine_task_error(std::ptr::null_mut(), buf.as_mut_ptr(), 256) }, - -1 - ); - assert_eq!( - unsafe { oakengine_task_start_time(std::ptr::null_mut()) }, - -1 - ); - assert_eq!( - unsafe { oakengine_task_is_cancelled(std::ptr::null_mut()) }, - -1 - ); - assert_eq!(unsafe { oakengine_task_cancel(std::ptr::null_mut()) }, -1); - assert_eq!( - unsafe { oakengine_task_start_sync(std::ptr::null_mut()) }, - -1 - ); - assert_eq!(unsafe { oakengine_task_free(std::ptr::null_mut()) }, -1); - - // Import/save result accessors on NULL → E_INVALID / NULL. - assert_eq!( - unsafe { oakengine_task_import_file_count(std::ptr::null_mut()) }, - -1 - ); - assert_eq!( - unsafe { oakengine_task_import_footage_count(std::ptr::null_mut()) }, - -1 - ); - assert_eq!( - unsafe { oakengine_task_import_invalid_files_count(std::ptr::null_mut()) }, - -1 - ); - assert_eq!( - unsafe { - oakengine_task_import_invalid_file_at(std::ptr::null_mut(), 0, buf.as_mut_ptr(), 256) - }, - -1 - ); - assert!(unsafe { oakengine_task_import_get_command(std::ptr::null_mut()) }.is_null()); - assert!(unsafe { oakengine_task_import_footage_at(std::ptr::null_mut(), 0) }.is_null()); - assert!(unsafe { oakengine_task_save_get_project(std::ptr::null_mut()) }.is_null()); - - // Creators with NULL input → NULL. - assert!(unsafe { oakengine_task_create_project_load(std::ptr::null()) }.is_null()); - assert!(unsafe { oakengine_task_create_project_load_otio(std::ptr::null()) }.is_null()); - assert!(unsafe { - oakengine_task_create_project_save( - std::ptr::null_mut(), - 0, - std::ptr::null(), - std::ptr::null(), - ) - } - .is_null()); - assert!(unsafe { oakengine_task_create_project_save_otio(std::ptr::null_mut()) }.is_null()); - assert!(unsafe { - oakengine_task_create_project_import(std::ptr::null_mut(), std::ptr::null(), 0) - } - .is_null()); - assert!(unsafe { oakengine_task_create_proxy(std::ptr::null_mut()) }.is_null()); - assert!( - unsafe { oakengine_task_create_export(std::ptr::null_mut(), std::ptr::null_mut()) } - .is_null() - ); - - // The CLI dialog returns 0 (not E_INVALID) for NULL, mirroring the capi. - assert_eq!( - unsafe { oakengine_cli_task_dialog_run(std::ptr::null_mut(), std::ptr::null_mut()) }, - 0 - ); -} - -// --------------------------------------------------------------------------- -// Accessor / lifecycle tests (no shared state) -// --------------------------------------------------------------------------- - -/// A project-load task with a bad filename: created (non-NULL), has a -/// title, fails synchronously (start_sync → 0) with a non-empty error, and -/// reports the facade-side start stamp once started. -#[test] -fn load_task_with_bad_filename_fails_sync() { - common::force_link(); - - let task = unsafe { oakengine_task_create_project_load(c"/no/such/oak/project.ove".as_ptr()) }; - assert!(!task.is_null()); - - let mut buf = [0 as c_char; 256]; - let len = unsafe { oakengine_task_title(task, buf.as_mut_ptr(), 256) }; - assert!(len > 0); - assert!(read_buf(&mut buf).contains("Loading")); - - // The synchronous run fails (file does not exist). - assert_eq!(unsafe { oakengine_task_start_sync(task) }, 0); - - // The error string is non-empty after the failed run. - let len = unsafe { oakengine_task_error(task, buf.as_mut_ptr(), 256) }; - assert!(len > 0); - assert!(!read_buf(&mut buf).is_empty()); - - // The facade-side start stamp is reported once the task started. - assert_ne!(unsafe { oakengine_task_start_time(task) }, 0); - assert_eq!(unsafe { oakengine_task_is_cancelled(task) }, 0); - - // Cancel round-trip through the facade flag. - assert_eq!(unsafe { oakengine_task_cancel(task) }, 0); - assert_eq!(unsafe { oakengine_task_is_cancelled(task) }, 1); - - assert_eq!(unsafe { oakengine_task_free(task) }, 0); -} - -/// The CLI dialog runs the task synchronously: 0 for a failing task, and -/// the dialog is a stub around that sync-run core. -#[test] -fn cli_dialog_runs_task_sync() { - common::force_link(); - - let task = unsafe { oakengine_task_create_project_load(c"/no/such/oak/project.ove".as_ptr()) }; - assert!(!task.is_null()); - assert_eq!( - unsafe { oakengine_cli_task_dialog_run(task, std::ptr::null_mut()) }, - 0 - ); - assert_eq!(unsafe { oakengine_task_free(task) }, 0); -} - -// --------------------------------------------------------------------------- -// Project-backed tasks (serialized: `oakengine_project_new` clears the -// process-wide undo stack) -// --------------------------------------------------------------------------- - -/// Save, save-otio and import task creation against a real project — one -/// 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(); - assert!(!project.is_null()); - assert_eq!(unsafe { oakengine_project_new(project) }, 0); - - let root = unsafe { oakengine_project_root(project) }; - assert!(!root.is_null()); - - // ---- save task on a real project → sync run writes the file ---------- - let save_path = - std::env::temp_dir().join(format!("oakengine_task_save_{}.ovexml", std::process::id())); - 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_eq!(unsafe { oakengine_task_start_sync(save_task) }, 1); - assert!(save_path.exists()); - - // save_get_project returns a borrowed project handle (freed by the - // caller) — NULL on other tasks. - let saved = unsafe { oakengine_task_save_get_project(save_task) }; - assert!(!saved.is_null()); - unsafe { oakengine_project_free(saved) }; - assert!(unsafe { oakengine_task_save_get_project(std::ptr::null_mut()) }.is_null()); - - // A NULL project yields a NULL save task. - assert!(unsafe { - oakengine_task_create_project_save( - std::ptr::null_mut(), - 0, - save_c.as_ptr(), - std::ptr::null(), - ) - } - .is_null()); - - unsafe { oakengine_task_free(save_task) }; - - // ---- save-otio: the facade derives the output filename from the - // project's own filename; NULL without one, a real task with one. ------ - assert!(unsafe { oakengine_task_create_project_save_otio(project) }.is_null()); - assert_eq!( - unsafe { - oakengine_project_set_filename(project, c"/tmp/oakengine_task_otio.otio".as_ptr()) - }, - 0 - ); - let otio_task = unsafe { oakengine_task_create_project_save_otio(project) }; - assert!(!otio_task.is_null()); - unsafe { oakengine_task_free(otio_task) }; - - // ---- import with 0 urls: task created, file count 0 ------------------- - let import_task = unsafe { oakengine_task_create_project_import(root, std::ptr::null(), 0) }; - assert!(!import_task.is_null()); - assert_eq!(unsafe { oakengine_task_import_file_count(import_task) }, 0); - assert_eq!( - unsafe { oakengine_task_import_footage_count(import_task) }, - 0 - ); - assert_eq!( - unsafe { oakengine_task_import_invalid_files_count(import_task) }, - 0 - ); - // Nothing ran, so no command / footage / invalid entries. An - // out-of-range invalid-file index reports the module's - // OAKTASK_E_NOT_FOUND (-80004) pass-through. - assert!(unsafe { oakengine_task_import_get_command(import_task) }.is_null()); - assert!(unsafe { oakengine_task_import_footage_at(import_task, 0) }.is_null()); - let mut buf = [0 as c_char; 256]; - assert_eq!( - unsafe { oakengine_task_import_invalid_file_at(import_task, 0, buf.as_mut_ptr(), 256) }, - -80004 - ); - unsafe { oakengine_task_free(import_task) }; - - // A negative url count is rejected (NULL task). - assert!(unsafe { oakengine_task_create_project_import(root, std::ptr::null(), -1) }.is_null()); - - unsafe { oakengine_node_free(root) }; - unsafe { oakengine_project_free(project) }; - let _ = std::fs::remove_file(&save_path); -} - -// --------------------------------------------------------------------------- -// Global task manager (serialized: the manager is process-wide) -// --------------------------------------------------------------------------- - -/// The global manager is created lazily: handle non-NULL, count 0, then a -/// task handed over with `manager_add` is visible to `manager_count` / -/// `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()) }; - assert!(!task.is_null()); - - // Handing the task to the manager transfers ownership. - assert_eq!(unsafe { oakengine_task_manager_add(task) }, 0); - assert!(unsafe { oakengine_task_manager_count() } >= 1); - - // The queue is non-empty, so the first task is borrowed. - let first = unsafe { oakengine_task_manager_first() }; - assert!(!first.is_null()); - assert_eq!(unsafe { oakengine_task_free(first) }, 0); - - // Cancelling through the manager succeeds (the task may already have - // failed fast on the missing file; cancel on a finished task is safe). - assert_eq!(unsafe { oakengine_task_manager_cancel(task) }, 0); - - // Releasing the (now borrowed) handle is safe: the manager owns the - // task and will delete it when it is cleaned up. - assert_eq!(unsafe { oakengine_task_free(task) }, 0); -} diff --git a/crates/oakengine.bk/src/test_support/undo.rs b/crates/oakengine.bk/src/test_support/undo.rs deleted file mode 100644 index 48a6bde17..000000000 --- a/crates/oakengine.bk/src/test_support/undo.rs +++ /dev/null @@ -1,286 +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 . - -//! Smoke tests for the undo family (`engine/include/oakengine/undo.h`). -//! -//! The facade owns a process-wide undo stack and one open undo group, so -//! the stack-mutating tests are serialized inside a single test -//! function; the command-lifecycle tests (no stack access) can run in -//! parallel. - -use super::common; - -use std::ffi::{c_char, c_int, c_void}; -use std::sync::atomic::{AtomicI32, Ordering}; - -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, - oakengine_undo_command_multi_add_child, oakengine_undo_command_multi_child_count, - oakengine_undo_command_redo_now, oakengine_undo_command_text, oakengine_undo_command_undo_now, - oakengine_undo_count, oakengine_undo_group_abort, oakengine_undo_group_begin, - oakengine_undo_group_end, oakengine_undo_handle, oakengine_undo_index, oakengine_undo_jump, - oakengine_undo_push, -}; - -// --------------------------------------------------------------------------- -// Command lifecycle (no global-stack state) -// --------------------------------------------------------------------------- - -/// Callback counters for the app-defined command test (own set so it can -/// run in parallel with the serialized stack test). -static CMD_REDO_COUNT: AtomicI32 = AtomicI32::new(0); -static CMD_UNDO_COUNT: AtomicI32 = AtomicI32::new(0); -static CMD_FREE_COUNT: AtomicI32 = AtomicI32::new(0); - -/// Callback counters for the serialized global-stack test. -static STK_REDO_COUNT: AtomicI32 = AtomicI32::new(0); -static STK_UNDO_COUNT: AtomicI32 = AtomicI32::new(0); - -/// Stack-test callbacks: bump only the `STK_*` counters. They must not -/// touch the `CMD_*` counters — the command-lifecycle tests reset and -/// assert those in parallel threads, so a stray bump here would race. -unsafe extern "C" fn redo_cb(_userdata: *mut c_void) { - STK_REDO_COUNT.fetch_add(1, Ordering::SeqCst); -} - -unsafe extern "C" fn undo_cb(_userdata: *mut c_void) { - STK_UNDO_COUNT.fetch_add(1, Ordering::SeqCst); -} - -/// Command-lifecycle-only callbacks: bump only the `CMD_*` counters. The -/// serialized stack test runs in a parallel thread and must not flip -/// these. -unsafe extern "C" fn cmd_redo_cb(_userdata: *mut c_void) { - CMD_REDO_COUNT.fetch_add(1, Ordering::SeqCst); -} - -unsafe extern "C" fn cmd_undo_cb(_userdata: *mut c_void) { - CMD_UNDO_COUNT.fetch_add(1, Ordering::SeqCst); -} - -unsafe extern "C" fn free_cb(_userdata: *mut c_void) { - CMD_FREE_COUNT.fetch_add(1, Ordering::SeqCst); -} - -/// Lifecycle: create a callback command, run redo/undo, free it. -#[test] -fn command_create_redo_undo_free() { - CMD_REDO_COUNT.store(0, Ordering::SeqCst); - CMD_UNDO_COUNT.store(0, Ordering::SeqCst); - CMD_FREE_COUNT.store(0, Ordering::SeqCst); - - let cmd = unsafe { - oakengine_undo_command_create( - c"custom".as_ptr(), - Some(cmd_redo_cb), - Some(cmd_undo_cb), - Some(free_cb), - std::ptr::null_mut(), - ) - }; - assert!(!cmd.is_null()); - - assert_eq!(unsafe { oakengine_undo_command_redo_now(cmd) }, 0); - assert_eq!(CMD_REDO_COUNT.load(Ordering::SeqCst), 1); - - assert_eq!(unsafe { oakengine_undo_command_undo_now(cmd) }, 0); - assert_eq!(CMD_UNDO_COUNT.load(Ordering::SeqCst), 1); - - // The free callback must fire exactly once when freed directly. - unsafe { oakengine_undo_command_free(cmd) }; - assert_eq!(CMD_FREE_COUNT.load(Ordering::SeqCst), 1); - - // Freeing a NULL pointer is a no-op. - unsafe { oakengine_undo_command_free(std::ptr::null_mut()) }; -} - -/// Multi command: add children, count them, redo the whole multi. -#[test] -fn multi_command_add_child_count_redo() { - let multi = unsafe { oakengine_undo_command_create_multi() }; - assert!(!multi.is_null()); - - let child = unsafe { - oakengine_undo_command_create( - c"child".as_ptr(), - Some(cmd_redo_cb), - Some(cmd_undo_cb), - None, - std::ptr::null_mut(), - ) - }; - assert_eq!( - unsafe { oakengine_undo_command_multi_add_child(multi, child) }, - 0 - ); - assert_eq!( - unsafe { oakengine_undo_command_multi_child_count(multi) }, - 1 - ); - - // Adding a NULL child fails with E_INVALID (-1). - assert_eq!( - unsafe { oakengine_undo_command_multi_add_child(multi, std::ptr::null_mut()) }, - -1 - ); - - unsafe { oakengine_undo_command_free(multi) }; -} - -// --------------------------------------------------------------------------- -// Global stack (serialized: the facade's stack is process-wide) -// --------------------------------------------------------------------------- - -/// Push/undo/redo/jump/text round-trip on the global stack, undo-group -/// begin/end/abort, and NULL-push rejection — all serialized in ONE test -/// because the facade owns a process-wide stack and a single open undo -/// group (the C++ capi's `g_undo_group` analogue), which cannot be -/// exercised from parallel test threads. -#[test] -fn undo_stack_lifecycle() { - // Serialized on the SAME lock as the it_undo stack tests and the - // write-through tests (the facade's stack is process-wide): without it - // a concurrent test's pushes break the exact-count assertions below. - let _stack = super::it_undo::GLOBAL_STACK_LOCK - .lock(); - - // Reset to a clean "New/Open Project" base row. - assert_eq!(unsafe { oakengine_undo_clear() }, 0); - assert_eq!(unsafe { oakengine_undo_count() }, 1); - assert_eq!(unsafe { oakengine_undo_index() }, 1); - - // The borrowed stack handle is stable and non-NULL. - assert!(!unsafe { oakengine_undo_handle() }.is_null()); - - // Push a callback command. - let cmd = unsafe { - oakengine_undo_command_create( - c"op".as_ptr(), - Some(redo_cb), - Some(undo_cb), - None, - std::ptr::null_mut(), - ) - }; - STK_REDO_COUNT.store(0, Ordering::SeqCst); - STK_UNDO_COUNT.store(0, Ordering::SeqCst); - assert_eq!( - unsafe { oakengine_undo_push(cmd, c"operation".as_ptr()) }, - 0 - ); - assert_eq!(unsafe { oakengine_undo_count() }, 2); - assert_eq!(unsafe { oakengine_undo_index() }, 2); - assert_eq!(STK_REDO_COUNT.load(Ordering::SeqCst), 1); - - // Row label (two-stage: query, then copy). - let mut buf = [0 as c_char; 64]; - let len = unsafe { oakengine_undo_command_text(1, buf.as_mut_ptr(), 64) }; - assert!(len > 0); - assert_eq!( - unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } - .to_str() - .unwrap(), - "operation" - ); - assert_eq!(unsafe { oakengine_undo_command_is_done(1) }, 1); - // Invalid row → module NOT_FOUND (-20004) passes through. - assert_eq!( - unsafe { oakengine_undo_command_text(99, buf.as_mut_ptr(), 64) }, - -20004 - ); - - // Undo restores index 1 and flips the done flag. - assert_eq!(unsafe { oakengine_undo_jump(1) }, 0); - assert_eq!(unsafe { oakengine_undo_index() }, 1); - assert_eq!(unsafe { oakengine_undo_command_is_done(1) }, 0); - assert_eq!(unsafe { oakengine_undo_can_undo() }, 0); - assert_eq!(unsafe { oakengine_undo_can_redo() }, 1); - - // Redo back to 2. - assert_eq!(unsafe { oakengine_undo_jump(2) }, 0); - assert_eq!(unsafe { oakengine_undo_index() }, 2); - - unsafe { oakengine_undo_clear() }; - - // --- Undo group: begin → push children → end pushes ONE entry; abort - // undoes and discards. (continues the same serialized test) - assert_eq!(unsafe { oakengine_undo_clear() }, 0); - - // Group begin/end with two children → one history row. - assert_eq!( - unsafe { oakengine_undo_group_begin(c"grouped".as_ptr()) }, - 0 - ); - // A second begin while open fails with E_STATE (-2). - assert_eq!(unsafe { oakengine_undo_group_begin(c"again".as_ptr()) }, -2); - - let c1 = unsafe { - oakengine_undo_command_create( - c"c1".as_ptr(), - Some(redo_cb), - Some(undo_cb), - None, - std::ptr::null_mut(), - ) - }; - let c2 = unsafe { - oakengine_undo_command_create( - c"c2".as_ptr(), - Some(redo_cb), - Some(undo_cb), - None, - std::ptr::null_mut(), - ) - }; - // While a group is open, push adds to the group (child redo'd - // eagerly) instead of the stack. - assert_eq!(unsafe { oakengine_undo_push(c1, c"c1".as_ptr()) }, 0); - assert_eq!(unsafe { oakengine_undo_push(c2, c"c2".as_ptr()) }, 0); - assert_eq!(unsafe { oakengine_undo_count() }, 1); // nothing on the stack yet - - assert_eq!(unsafe { oakengine_undo_group_end() }, 0); - assert_eq!(unsafe { oakengine_undo_count() }, 2); // one grouped row - - // Abort path: group with a child is undone and discarded. - STK_UNDO_COUNT.store(0, Ordering::SeqCst); - assert_eq!(unsafe { oakengine_undo_group_begin(c"abort".as_ptr()) }, 0); - let c3 = unsafe { - oakengine_undo_command_create( - c"c3".as_ptr(), - Some(redo_cb), - Some(undo_cb), - None, - std::ptr::null_mut(), - ) - }; - assert_eq!(unsafe { oakengine_undo_push(c3, c"c3".as_ptr()) }, 0); - assert_eq!(unsafe { oakengine_undo_group_abort() }, 0); - assert_eq!(unsafe { oakengine_undo_count() }, 2); // unchanged - assert_eq!(STK_UNDO_COUNT.load(Ordering::SeqCst), 1); // c3's undo ran - - // End with no open group fails with E_STATE. - assert_eq!(unsafe { oakengine_undo_group_end() }, -2); - - unsafe { oakengine_undo_clear() }; - - // Push NULL fails with E_INVALID. - assert_eq!( - unsafe { oakengine_undo_push(std::ptr::null_mut(), c"x".as_ptr()) }, - -1 - ); -} diff --git a/crates/oakengine.bk/src/testmedia.rs b/crates/oakengine.bk/src/testmedia.rs deleted file mode 100644 index 2afffb71b..000000000 --- a/crates/oakengine.bk/src/testmedia.rs +++ /dev/null @@ -1,64 +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 . - -//! Test-media generation facade export (M12 P0). -//! -//! `oakengine_testmedia_write_clip` encodes a small MPEG-2 clip of known -//! content through the engine's own FFmpeg (oakcodec's encoder). App -//! tests call this instead of linking oakcodec directly: the app test -//! binary already loads `liboakengine` (which statically embeds FFmpeg), -//! and a second FFmpeg copy in the test binary duplicates the -//! AVFoundation Objective-C classes and crashes the encoder's device -//! probing. - -use std::ffi::{c_char, c_int}; - -use crate::error::Error; -use crate::handle::guard_int; - -/// `oakengine_testmedia_write_clip` — encode `frame_count` frames of the -/// known test pattern (left half red / right half blue on frame 0) into -/// `path` (MPEG-2 in an MP4 container, `fps` frames per second). -/// -/// Returns 0 on success; a negative `OAKENGINE_E_*` otherwise (invalid -/// arguments or an encoder failure). Only used by tests and tooling. -#[no_mangle] -pub extern "C" fn oakengine_testmedia_write_clip( - path: *const c_char, - width: c_int, - height: c_int, - frame_count: c_int, - fps: c_int, -) -> c_int { - guard_int(|| unsafe { - if path.is_null() { - return Err(Error::Invalid); - } - let path_str = crate::handle::read_cstr(path); - if path_str.is_empty() || width <= 0 || height <= 0 || frame_count <= 0 || fps <= 0 { - return Err(Error::Invalid); - } - oakcodec::testmedia::write_test_clip( - std::path::Path::new(&path_str), - width, - height, - frame_count, - fps, - ) - .map_err(|e| Error::Failed(format!("test clip encode failed: {e:?}")))?; - Ok(0) - }) -} diff --git a/crates/oakengine.bk/src/timeline.rs b/crates/oakengine.bk/src/timeline.rs deleted file mode 100644 index 4519d0193..000000000 --- a/crates/oakengine.bk/src/timeline.rs +++ /dev/null @@ -1,6466 +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 . - -//! `engine/include/oakengine/timeline.h` — sequences, tracks, clips, -//! markers and the workarea over the oaknode + oaktimeline modules. -//! -//! The engine's frame timestamps (frame numbers in the sequence's -//! frame-rate timebase) convert to the modules' rational seconds through -//! the sequence's video parameters, mirroring the C++ capi -//! (`engine/src/capi/timeline.cpp`): the time base is the frame rate -//! flipped. Undoable mutations assemble the module's command creators and -//! push them through [`crate::undo::push_or_run`]. - -use std::cell::RefCell; -use std::collections::HashMap; -use std::ffi::{c_char, c_int, c_void}; -use std::sync::atomic::{AtomicU32, Ordering}; -use std::sync::Mutex; - -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, - write_string, CHandle, OakEngineBlock, OakEngineClip, OakEngineClipboard, OakEngineFootage, - OakEngineMarker, OakEngineMarkerList, OakEngineNode, OakEngineProject, OakEngineSequence, - OakEngineTrack, OakEngineTrackList, OakEngineWorkarea, -}; -use crate::undo::push_or_run; - -/// `engine/include/oakengine/timeline.h` — POD mirror of -/// `oakengine_ripple_info` (one TrackListRippleToolCommand hash entry). -#[repr(C)] -#[derive(Clone, Copy)] -pub struct OakEngineRippleInfo { - /// The track to ripple (borrowed engine handle). - pub track: *mut OakEngineTrack, - /// The block being moved (borrowed engine handle). - pub block: *mut OakEngineBlock, - /// Whether a gap should be appended after it (1/0). - pub append_gap: c_int, -} - -// ---- helpers --------------------------------------------------------------- - -/// Track types (`OAKENGINE_TRACK_TYPE_*`). -const TRACK_TYPE_VIDEO: c_int = 0; -const TRACK_TYPE_AUDIO: c_int = 1; -const TRACK_TYPE_SUBTITLE: c_int = 2; - -/// Movement modes (`OAKENGINE_MOVEMENT_MODE_*`). -const MOVEMENT_MODE_TRIM_IN: c_int = 2; -const MOVEMENT_MODE_TRIM_OUT: c_int = 3; - -/// `oaknode/block.h` block kinds. -const BLOCK_KIND_CLIP: c_int = 1; -const BLOCK_KIND_GAP: c_int = 2; - -/// Node type id of transition blocks. -const TYPE_ID_TRANSITION: &str = "org.olivevideoeditor.Olive.transitionblock"; - -/// Track height constants (C++ `Track::k_track_height_*`). -const TRACK_HEIGHT_DEFAULT: f64 = 3.0; -const TRACK_HEIGHT_MINIMUM: f64 = 1.5; -const TRACK_HEIGHT_INTERVAL: f64 = 0.5; -/// Default font height in pixels (C++ `Track::default_font_height`). -const TRACK_FONT_HEIGHT: f64 = 13.0; - -// The last editing error for this thread (mirrors the capi's -// `thread_local QString g_seq_last_error`, timeline.cpp:126). -thread_local! { - static SEQ_LAST_ERROR: RefCell = RefCell::new(String::new()); -} - -/// Record `msg` as the last editing error of this thread. -fn set_seq_error(msg: &str) { - SEQ_LAST_ERROR.with(|e| *e.borrow_mut() = msg.to_string()); -} - -/// Release a module handle reference (the handle's own `release`). Every -/// module "borrowed" handle returned by an out-parameter or -/// `*_as_node`/`*_of` creator is an owned copy with refcount 1, so the -/// facade releases it after temporary use. -fn release_handle(h: CHandle) { - if let Some(release) = h.release { - unsafe { release(h.ctx) }; - } -} - -/// Greatest common divisor (1 when both are zero). -fn gcd(a: i64, b: i64) -> i64 { - let (mut a, mut b) = (a.abs(), b.abs()); - while b != 0 { - let t = b; - b = a % b; - a = t; - } - if a == 0 { - 1 - } else { - a - } -} - -/// The sequence's frame duration (frame rate flipped) as a (num, den) -/// pair. Mirrors the capi's `time_base_of`; `E_STATE` when the sequence -/// has no valid frame rate. -/// -/// # Safety -/// `seq` must be a live module sequence handle. -unsafe fn seq_time_base(seq: CHandle) -> Result<(i64, i64)> { - unsafe { - let mut params = CHandle::null(); - let rc = n::oaknode_sequence_get_video_params(seq, 0, &mut params); - if rc != 0 || params.is_null() { - return Err(Error::State); - } - let mut num: c_int = 0; - let mut den: c_int = 0; - let fr = c::oakcommon_videoparams_get_frame_rate(params, &mut num, &mut den); - let mut h = params; - c::oakcommon_videoparams_free(&mut h); - if fr != 0 || num <= 0 || den <= 0 { - return Err(Error::State); - } - Ok((den as i64, num as i64)) - } -} - -/// Rational seconds -> timestamp in timebase units, rounding half away -/// from zero (mirrors `Timecode::k_round`). -fn rational_to_ts(num: i64, den: i64, tb: (i64, i64)) -> i64 { - if den == 0 || tb.0 == 0 || tb.1 == 0 { - return 0; - } - let n = num as i128 * tb.1 as i128; - let d = den as i128 * tb.0 as i128; - let q = n / d; - let r = n % d; - let rr = if r < 0 { -r } else { r }; - let dd = if d < 0 { -d } else { d }; - if rr * 2 >= dd { - (q + if n < 0 { -1 } else { 1 }) as i64 - } else { - q as i64 - } -} - -/// Timestamp -> reduced rational seconds (`time = ts * tb`). -fn ts_to_rational(ts: i64, tb: (i64, i64)) -> (i64, i64) { - let num = ts as i128 * tb.0 as i128; - let den = tb.1 as i128; - let g = gcd((num % den) as i64, den as i64); - ((num / g as i128) as i64, (den / g as i128) as i64) -} - -/// Reduced rational sum. -fn rat_add(a_num: i64, a_den: i64, b_num: i64, b_den: i64) -> (i64, i64) { - let num = a_num * b_den + b_num * a_den; - let den = a_den * b_den; - let g = gcd(num, den); - (num / g, den / g) -} - -/// Reduced rational difference (`a - b`). -fn rat_sub(a_num: i64, a_den: i64, b_num: i64, b_den: i64) -> (i64, i64) { - let num = a_num * b_den - b_num * a_den; - let den = a_den * b_den; - let g = gcd(num, den); - (num / g, den / g) -} - -/// Box a module command handle into the engine command shell. -fn command_box(cmd: CHandle) -> Result<*mut OakEngineClipboard> { - if cmd.ctx.is_null() { - return Err(Error::Failed("command creation failed".into())); - } - Ok(box_handle::(cmd).cast()) -} - -/// Push a single module command onto the undo stack (or run it directly). -/// -/// # Safety -/// `cmd` must be a live module command handle. -unsafe fn push_command(cmd: CHandle, name: &str) -> Result<()> { - unsafe { - let boxed = command_box(cmd)?; - let name_c = - std::ffi::CString::new(name).map_err(|_| Error::Failed("invalid undo name".into()))?; - push_or_run(boxed, name_c.as_ptr()) - } -} - -/// Assemble several module command handles into ONE multi command and push -/// it. -/// -/// # Safety -/// `children` must hold live module command handles (each is consumed by -/// the multi). -unsafe fn push_multi_commands(children: &[CHandle], name: &str) -> Result<()> { - unsafe { - if children.is_empty() { - return Ok(()); - } - let multi = u::command_init_multi(); - if multi.is_null() { - return Err(Error::Failed("multi command allocation failed".into())); - } - let multi_box = box_handle::(multi); - for child in children { - let rc = u::command_multi_add_child(multi, *child); - if rc != 0 { - free_box(multi_box); - return Err(Error::Module(rc)); - } - } - let name_c = - std::ffi::CString::new(name).map_err(|_| Error::Failed("invalid undo name".into()))?; - push_or_run(multi_box, name_c.as_ptr()) - } -} - -// ---- Facade-owned undo commands ------------------------------------------- -// -// The module has no undo commands for sequence video params, block -// enable/disable, clip media-in or block resizing; the facade carries -// read-modify-write equivalents backed by `oakundo_command_init` vtables -// (the same pattern as the capi's facade-level `SequenceVideoParamsCommand`). - -/// Generic vtable command factory: box `data` behind an oakundo command. -/// -/// # Safety -/// `data` must be a box that `free_fn` can reclaim; `redo`/`undo` must be -/// safe to call with it. -unsafe fn vtable_command( - redo: unsafe extern "C" fn(*mut c_void), - undo: unsafe extern "C" fn(*mut c_void), - free_fn: unsafe extern "C" fn(*mut c_void), - data: *mut c_void, -) -> Result { - unsafe { - let vtable = OakUndoCommandVtable { - redo: Some(redo), - undo: Some(undo), - free_fn: Some(free_fn), - }; - let cmd = u::command_init(&vtable, data); - if cmd.is_null() { - free_fn(data); - return Err(Error::Failed("undo command allocation failed".into())); - } - Ok(cmd) - } -} - -/// Sequence video-parameter read-modify-write (the capi's -/// `SequenceVideoParamsCommand`). -struct VideoParamsCmdData { - /// Sequence node (addref'd). - seq: CHandle, - /// Old oakcommon videoparams handle. - old_params: CHandle, - /// New oakcommon videoparams handle. - new_params: CHandle, -} - -unsafe extern "C" fn video_params_redo(ud: *mut c_void) { - unsafe { - let d = &*(ud as *const VideoParamsCmdData); - n::oaknode_sequence_set_video_params(d.seq, 0, d.new_params); - } -} - -unsafe extern "C" fn video_params_undo(ud: *mut c_void) { - unsafe { - let d = &*(ud as *const VideoParamsCmdData); - n::oaknode_sequence_set_video_params(d.seq, 0, d.old_params); - } -} - -unsafe extern "C" fn video_params_free(ud: *mut c_void) { - unsafe { - let d = Box::from_raw(ud as *mut VideoParamsCmdData); - release_handle(d.seq); - release_handle(d.old_params); - release_handle(d.new_params); - } -} - -/// Sequence audio-parameter read-modify-write (the capi's -/// `SequenceAudioParamsCommand`). The params are borrowed oakcore -/// `OakAudioParams` handles (raw pointers; release with -/// `oakcore_audioparams_free`). -struct AudioParamsCmdData { - /// Sequence node (addref'd). - seq: CHandle, - /// Old oakcore audio-params handle. - old_params: *mut c_void, - /// New oakcore audio-params handle. - new_params: *mut c_void, -} - -unsafe extern "C" fn audio_params_redo(ud: *mut c_void) { - unsafe { - let d = &*(ud as *const AudioParamsCmdData); - n::oaknode_sequence_set_audio_params(d.seq, 0, d.new_params); - } -} - -unsafe extern "C" fn audio_params_undo(ud: *mut c_void) { - unsafe { - let d = &*(ud as *const AudioParamsCmdData); - n::oaknode_sequence_set_audio_params(d.seq, 0, d.old_params); - } -} - -unsafe extern "C" fn audio_params_free(ud: *mut c_void) { - unsafe { - let d = Box::from_raw(ud as *mut AudioParamsCmdData); - release_handle(d.seq); - a::oakcore_audioparams_free(d.old_params); - a::oakcore_audioparams_free(d.new_params); - } -} - -/// Block enable/disable (the capi's `BlockEnableDisableCommand`). -struct BlockEnabledCmdData { - /// Block (addref'd). - block: CHandle, - /// Enabled value captured at construction. - old_enabled: c_int, - /// New enabled value. - new_enabled: c_int, -} - -unsafe extern "C" fn block_enabled_redo(ud: *mut c_void) { - unsafe { - let d = &*(ud as *const BlockEnabledCmdData); - n::oaknode_block_set_enabled(d.block, d.new_enabled); - } -} - -unsafe extern "C" fn block_enabled_undo(ud: *mut c_void) { - unsafe { - let d = &*(ud as *const BlockEnabledCmdData); - n::oaknode_block_set_enabled(d.block, d.old_enabled); - } -} - -unsafe extern "C" fn block_enabled_free(ud: *mut c_void) { - unsafe { - let d = Box::from_raw(ud as *mut BlockEnabledCmdData); - release_handle(d.block); - } -} - -/// Clip media-in change (the capi's `BlockSetMediaInCommand`). -struct ClipMediaInCmdData { - /// Clip (addref'd). - clip: CHandle, - /// Media-in captured at construction (rational seconds). - old_num: c_int, - old_den: c_int, - /// New media-in (rational seconds). - new_num: c_int, - new_den: c_int, -} - -unsafe extern "C" fn clip_media_in_redo(ud: *mut c_void) { - unsafe { - let d = &*(ud as *const ClipMediaInCmdData); - n::oaknode_clip_set_media_in(d.clip, d.new_num, d.new_den); - } -} - -unsafe extern "C" fn clip_media_in_undo(ud: *mut c_void) { - unsafe { - let d = &*(ud as *const ClipMediaInCmdData); - n::oaknode_clip_set_media_in(d.clip, d.old_num, d.old_den); - } -} - -unsafe extern "C" fn clip_media_in_free(ud: *mut c_void) { - unsafe { - let d = Box::from_raw(ud as *mut ClipMediaInCmdData); - release_handle(d.clip); - } -} - -/// Block length change keeping the in point (the capi's -/// `BlockResizeCommand`). -struct BlockResizeCmdData { - /// Block (addref'd). - block: CHandle, - /// Length captured at construction (rational seconds). - old_num: c_int, - old_den: c_int, - /// New length (rational seconds). - new_num: c_int, - new_den: c_int, -} - -// The engine's `set_length_and_media_out` keeps the in-point and extends -// the out (the C++ in/out points derive from the track position); that maps -// to the module's in-anchored `set_length_and_media_in`, despite the name. -unsafe extern "C" fn block_resize_redo(ud: *mut c_void) { - unsafe { - let d = &*(ud as *const BlockResizeCmdData); - n::oaknode_block_set_length_and_media_in(d.block, d.new_num, d.new_den); - } -} - -unsafe extern "C" fn block_resize_undo(ud: *mut c_void) { - unsafe { - let d = &*(ud as *const BlockResizeCmdData); - n::oaknode_block_set_length_and_media_in(d.block, d.old_num, d.old_den); - } -} - -unsafe extern "C" fn block_resize_free(ud: *mut c_void) { - unsafe { - let d = Box::from_raw(ud as *mut BlockResizeCmdData); - release_handle(d.block); - } -} - -/// Block trim (the capi's `BlockTrimCommand`). The module's own -/// `BlockTrimCommand` applies the length setters with inverted semantics -/// (its trim-in anchors the in-point), so the facade carries the correct -/// engine mapping: a trim-in anchors the OUT (the in moves), a trim-out -/// anchors the IN (the out moves). -struct BlockTrimCmdData { - /// Block (addref'd). - block: CHandle, - /// Trim mode: `MOVEMENT_MODE_TRIM_IN` (out anchored) or - /// `MOVEMENT_MODE_TRIM_OUT` (in anchored). - mode: c_int, - /// Length captured at construction (rational seconds). - old_num: c_int, - old_den: c_int, - /// New length (rational seconds). - new_num: c_int, - new_den: c_int, -} - -unsafe extern "C" fn block_trim_redo(ud: *mut c_void) { - unsafe { - let d = &*(ud as *const BlockTrimCmdData); - if d.mode == MOVEMENT_MODE_TRIM_IN { - n::oaknode_block_set_length_and_media_out(d.block, d.new_num, d.new_den); - } else { - n::oaknode_block_set_length_and_media_in(d.block, d.new_num, d.new_den); - } - } -} - -unsafe extern "C" fn block_trim_undo(ud: *mut c_void) { - unsafe { - let d = &*(ud as *const BlockTrimCmdData); - if d.mode == MOVEMENT_MODE_TRIM_IN { - n::oaknode_block_set_length_and_media_out(d.block, d.old_num, d.old_den); - } else { - n::oaknode_block_set_length_and_media_in(d.block, d.old_num, d.old_den); - } - } -} - -unsafe extern "C" fn block_trim_free(ud: *mut c_void) { - unsafe { - let d = Box::from_raw(ud as *mut BlockTrimCmdData); - release_handle(d.block); - } -} - -/// Build a block-trim vtable command. -/// -/// # Safety -/// `block` must be a live module block handle. -unsafe fn trim_cmd( - block: CHandle, - mode: c_int, - old_num: c_int, - old_den: c_int, - new_num: c_int, - new_den: c_int, -) -> Result { - unsafe { - let data = Box::into_raw(Box::new(BlockTrimCmdData { - block: block.addref(), - mode, - old_num, - old_den, - new_num, - new_den, - })) as *mut c_void; - vtable_command(block_trim_redo, block_trim_undo, block_trim_free, data) - } -} - -// ---- Per-sequence marker list / workarea cache ----------------------------- -// -// The module's sequences never initialize their own `markers`/`workarea` -// handles (`SequenceBehavior` defaults both to empty), so -// `oaktimeline_marker_list_of`/`workarea_of` return empty handles for -// them. The facade materializes one of each per sequence on first use and -// caches them by the sequence node's stable `ctx` token (facade state, -// like the process-wide undo stack). The cached entries live for the -// process; a sequence reused at the same heap address after a project free -// would find its old list (accepted: the application creates one project). - -static SEQ_MARKER_LISTS: Mutex>> = Mutex::new(None); -static SEQ_WORKAREAS: Mutex>> = Mutex::new(None); - -/// The per-sequence marker-list / workarea maps (created lazily). -fn seq_marker_map() -> std::sync::MutexGuard<'static, Option>> { - SEQ_MARKER_LISTS.lock().unwrap_or_else(|e| e.into_inner()) -} - -fn seq_workarea_map() -> std::sync::MutexGuard<'static, Option>> { - SEQ_WORKAREAS.lock().unwrap_or_else(|e| e.into_inner()) -} - -/// The sequence's marker list (addref'd; caller releases). -/// -/// # Safety -/// `seq` must be a live module sequence handle. -unsafe fn seq_marker_list(seq: CHandle) -> Result { - unsafe { - let node = n::oaknode_sequence_as_node(seq); - let mut list = tl::oaktimeline_marker_list_of(node); - release_handle(node); - if !list.is_null() { - return Ok(list); - } - let key = seq.ctx as usize; - let mut map = seq_marker_map(); - let map = map.get_or_insert_with(HashMap::new); - if let Some(h) = map.get(&key) { - return Ok(h.addref()); - } - list = tl::oaktimeline_marker_list_create(); - if list.is_null() { - return Err(Error::Failed("marker list allocation failed".into())); - } - let stored = list.addref(); - map.insert(key, stored); - Ok(list) - } -} - -/// The sequence's workarea (addref'd; caller releases). -/// -/// # Safety -/// `seq` must be a live module sequence handle. -unsafe fn seq_workarea(seq: CHandle) -> Result { - unsafe { - let node = n::oaknode_sequence_as_node(seq); - let mut wa = tl::oaktimeline_workarea_of(node); - release_handle(node); - if !wa.is_null() { - return Ok(wa); - } - let key = seq.ctx as usize; - let mut map = seq_workarea_map(); - let map = map.get_or_insert_with(HashMap::new); - if let Some(h) = map.get(&key) { - return Ok(h.addref()); - } - wa = tl::oaktimeline_workarea_create(); - if wa.is_null() { - return Err(Error::Failed("workarea allocation failed".into())); - } - let stored = wa.addref(); - map.insert(key, stored); - Ok(wa) - } -} - -// ---- Marker handle boxes --------------------------------------------------- -// -// The module exposes markers only through list operations -// (`oaktimeline_marker_at`, `*_command`, ...); there is no standalone -// marker handle. The facade represents an `OakEngineMarker` as a boxed -// (addref'd list, index) pair so the marker-handle family can route back -// into the list. - -/// Marker-box ABI magic stamped into the handle's `abi_version` (marker -/// boxes are facade-created; the field distinguishes them from module -/// handles without relying on function-pointer equality). -const MARKER_ABI_MAGIC: u32 = 0x4D41524B; // "MARK" - -/// Facade-owned marker reference: the owning list plus the marker index. -struct MarkerBox { - refs: AtomicU32, - list: CHandle, - index: c_int, -} - -unsafe extern "C" fn marker_box_addref(ptr: *mut c_void) { - unsafe { - if ptr.is_null() { - return; - } - let rb = &*(ptr as *const MarkerBox); - rb.refs.fetch_add(1, Ordering::SeqCst); - } -} - -unsafe extern "C" fn marker_box_release(ptr: *mut c_void) { - unsafe { - if ptr.is_null() { - return; - } - let rb = ptr as *mut MarkerBox; - let prev = (*rb).refs.fetch_sub(1, Ordering::SeqCst); - if prev == 1 { - let list = (*rb).list; - release_handle(list); - drop(Box::from_raw(rb)); - } - } -} - -/// Box an (addref'd `list`, `index`) pair as a borrowed marker handle. -fn box_marker(list: CHandle, index: c_int) -> *mut OakEngineMarker { - let boxed = Box::new(MarkerBox { - refs: AtomicU32::new(1), - list, - index, - }); - let ch = CHandle { - ctx: Box::into_raw(boxed) as *mut c_void, - addref: Some(marker_box_addref), - release: Some(marker_box_release), - abi_version: MARKER_ABI_MAGIC, - }; - box_handle::(ch) -} - -/// Unpack a marker handle into its (list, index). -/// -/// # Safety -/// `m` must be a marker handle created by [`box_marker`] (or NULL). -unsafe fn marker_unbox(m: *const OakEngineMarker) -> Result<(CHandle, c_int)> { - unsafe { - let ch = unbox::(m)?; - if ch.abi_version != MARKER_ABI_MAGIC { - return Err(Error::Invalid); - } - let rb = &*(ch.ctx as *const MarkerBox); - Ok((rb.list, rb.index)) - } -} - -/// The clip at (track_index, clip_index) within the track list, skipping -/// gap blocks; empty handle when out of range (the capi's -/// `clip_at_index`). -/// -/// # Safety -/// `list` must be a live module track-list handle. -unsafe fn clip_at_index(list: CHandle, track_index: c_int, clip_index: c_int) -> CHandle { - unsafe { - let mut track_count: c_int = 0; - if n::oaknode_tracklist_get_track_count(list, &mut track_count) != 0 - || track_index < 0 - || track_index >= track_count - { - return CHandle::null(); - } - let mut track = CHandle::null(); - if n::oaknode_tracklist_get_track_at(list, track_index, &mut track) != 0 || track.is_null() - { - return CHandle::null(); - } - let mut block_count: c_int = 0; - if n::oaknode_track_get_block_count(track, &mut block_count) != 0 { - release_handle(track); - return CHandle::null(); - } - let mut seen: c_int = 0; - for i in 0..block_count { - let mut block = CHandle::null(); - if n::oaknode_track_get_block_at(track, i, &mut block) != 0 || block.is_null() { - continue; - } - let mut kind: c_int = 0; - let rc = n::oaknode_block_get_kind(block, &mut kind); - if rc != 0 { - release_handle(block); - continue; - } - if kind == BLOCK_KIND_CLIP { - if seen == clip_index { - release_handle(track); - return block; - } - seen += 1; - } - release_handle(block); - } - release_handle(track); - CHandle::null() - } -} - -/// Index of the first marker whose in-point equals `(num, den)`, or -1. -/// -/// # Safety -/// `list` must be a live module marker-list handle. -unsafe fn marker_index_at(list: CHandle, num: i64, den: i64) -> c_int { - unsafe { - let mut count: c_int = 0; - if tl::oaktimeline_marker_count(list, &mut count) != 0 { - return -1; - } - for i in 0..count { - let mut in_num: c_int = 0; - let mut in_den: c_int = 0; - let mut out_num: c_int = 0; - let mut out_den: c_int = 0; - let mut color: c_int = 0; - let rc = tl::oaktimeline_marker_at( - list, - i, - &mut in_num, - &mut in_den, - &mut out_num, - &mut out_den, - &mut color, - std::ptr::null_mut(), - 0, - ); - if rc < 0 { - continue; - } - if in_num as i64 == num && in_den as i64 == den { - return i; - } - } - -1 - } -} - -/// Nearest block whose out-point is strictly before `(num, den)` (the -/// capi's `Track::nearest_block_before`; the module exposes only the -/// before-or-at variant, so the last block ordered before the time is -/// located by iteration). -/// -/// # Safety -/// `track` must be a live module track handle. -unsafe fn nearest_block_before(track: CHandle, num: i64, den: i64) -> CHandle { - unsafe { - let mut block_count: c_int = 0; - if n::oaknode_track_get_block_count(track, &mut block_count) != 0 { - return CHandle::null(); - } - let mut best = CHandle::null(); - for i in 0..block_count { - let mut b = CHandle::null(); - if n::oaknode_track_get_block_at(track, i, &mut b) != 0 || b.is_null() { - continue; - } - let mut out_num: c_int = 0; - let mut out_den: c_int = 0; - if n::oaknode_block_get_out(b, &mut out_num, &mut out_den) == 0 - && rat_cmp(out_num as i64, out_den as i64, num, den) == std::cmp::Ordering::Less - { - if !best.is_null() { - release_handle(best); - } - best = b; - } else { - release_handle(b); - } - } - best - } -} - -/// The module type id of a node (empty when the query fails). -/// -/// # Safety -/// `node` must be a live module node handle. -unsafe fn node_type_id(node: CHandle) -> String { - unsafe { - let mut buf = [0 as c_char; 256]; - let rc = n::oaknode_node_get_id(node, buf.as_mut_ptr(), buf.len() as c_int); - if rc < 0 { - String::new() - } else { - read_cstr(buf.as_ptr()) - } - } -} - -// ---- Sequence creation and inspection -------------------------------------- - -/// `oakengine_sequence_new` — create a sequence named `name` in `project` -/// and return its borrowed handle (NULL on failure; NULL project -> NULL). -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_new( - project: *mut OakEngineProject, - name: *const c_char, -) -> *mut OakEngineSequence { - guard_ptr(|| unsafe { - if project.is_null() { - return Ok(std::ptr::null_mut()); - } - let ph = unbox(project)?; - let root = n::oaknode_project_root(ph); - if root.is_null() { - release_handle(root); - return Ok(std::ptr::null_mut()); - } - release_handle(root); - - let seq = n::oaknode_sequence_create(); - if seq.is_null() { - return Ok(std::ptr::null_mut()); - } - let rc = n::oaknode_sequence_set_default_parameters(seq); - if rc != 0 { - return Err(Error::Module(rc)); - } - let label = read_cstr(name); - let label_c = - std::ffi::CString::new(label).map_err(|_| Error::Failed("invalid name".into()))?; - let seq_node = n::oaknode_sequence_as_node(seq); - let rc = n::oaknode_node_set_label(seq_node, label_c.as_ptr()); - release_handle(seq_node); - if rc != 0 { - return Err(Error::Module(rc)); - } - - // NOTE (documented deviation): the module's whole-subgraph transfer - // (`oaknode_project_add_node` -> `graph::transfer_all`) moves the node - // entries but does NOT remap the node ids held inside behaviors, so a - // sequence moved into a project keeps track lists that point at the - // wrong ids and a track-list `sequence` back-reference that points at - // the root folder. The sequence is therefore kept in its own scratch - // project (all track/marker/workarea queries stay functional there); - // project membership and the undoable creation are not established in - // the module world. - Ok(box_handle::(seq)) - }) -} - -/// `oakengine_sequence_name` — sequence label (buf/size convention). -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_name( - self_: *const OakEngineSequence, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - let h = unbox(self_)?; - let node = n::oaknode_sequence_as_node(h); - let rc = n::oaknode_node_get_label(node, buf, buf_size); - release_handle(node); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_sequence_get_length` — content length in seconds (0 for an -/// empty sequence). -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_get_length( - self_: *const OakEngineSequence, - seconds: *mut f64, -) -> c_int { - guard(|| unsafe { - if seconds.is_null() { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let mut num: c_int = 0; - let mut den: c_int = 0; - Error::from_module(n::oaknode_sequence_get_length(h, &mut num, &mut den))?; - *seconds = num as f64 / den as f64; - Ok(()) - }) -} - -/// `oakengine_sequence_get_length_rational` — content length as rational -/// seconds. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_get_length_rational( - self_: *const OakEngineSequence, - num: *mut c_int, - den: *mut c_int, -) -> c_int { - guard(|| unsafe { - let h = unbox(self_)?; - let mut n: c_int = 0; - let mut d: c_int = 0; - Error::from_module(n::oaknode_sequence_get_length(h, &mut n, &mut d))?; - if !num.is_null() { - *num = n; - } - if !den.is_null() { - *den = d; - } - Ok(()) - }) -} - -/// `oakengine_sequence_get_frame_rate` — frame rate num/den pair. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_get_frame_rate( - self_: *const OakEngineSequence, - num: *mut c_int, - den: *mut c_int, -) -> c_int { - guard(|| unsafe { - let h = unbox(self_)?; - let mut params = CHandle::null(); - let rc = n::oaknode_sequence_get_video_params(h, 0, &mut params); - if rc != 0 || params.is_null() { - return Err(Error::State); - } - let mut n: c_int = 0; - let mut d: c_int = 0; - let fr = c::oakcommon_videoparams_get_frame_rate(params, &mut n, &mut d); - let mut hh = params; - c::oakcommon_videoparams_free(&mut hh); - if fr != 0 || n <= 0 || d <= 0 { - return Err(Error::State); - } - if !num.is_null() { - *num = n; - } - if !den.is_null() { - *den = d; - } - Ok(()) - }) -} - -/// `oakengine_sequence_get_video_params` — dimensions and pixel aspect -/// ratio (any output pointer may be NULL). -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_get_video_params( - self_: *const OakEngineSequence, - width: *mut c_int, - height: *mut c_int, - par_num: *mut c_int, - par_den: *mut c_int, -) -> c_int { - guard(|| unsafe { - let h = unbox(self_)?; - let mut params = CHandle::null(); - let rc = n::oaknode_sequence_get_video_params(h, 0, &mut params); - if rc != 0 || params.is_null() { - return Err(Error::Failed("sequence has no video params".into())); - } - if !width.is_null() { - Error::from_module(c::oakcommon_videoparams_get_width(params, width))?; - } - if !height.is_null() { - Error::from_module(c::oakcommon_videoparams_get_height(params, height))?; - } - if !par_num.is_null() || !par_den.is_null() { - let mut pn: c_int = 0; - let mut pd: c_int = 0; - Error::from_module(c::oakcommon_videoparams_get_pixel_aspect_ratio( - params, &mut pn, &mut pd, - ))?; - if !par_num.is_null() { - *par_num = pn; - } - if !par_den.is_null() { - *par_den = pd; - } - } - let mut hh = params; - c::oakcommon_videoparams_free(&mut hh); - Ok(()) - }) -} - -/// `oakengine_sequence_get_video_params_ex` — full read of the video -/// parameters (any output pointer may be NULL). -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_get_video_params_ex( - self_: *const OakEngineSequence, - width: *mut c_int, - height: *mut c_int, - fps_num: *mut c_int, - fps_den: *mut c_int, - par_num: *mut c_int, - par_den: *mut c_int, - interlacing: *mut c_int, - format: *mut c_int, - divider: *mut c_int, -) -> c_int { - guard(|| unsafe { - let h = unbox(self_)?; - let mut params = CHandle::null(); - let rc = n::oaknode_sequence_get_video_params(h, 0, &mut params); - if rc != 0 || params.is_null() { - return Err(Error::Failed("sequence has no video params".into())); - } - if !width.is_null() { - Error::from_module(c::oakcommon_videoparams_get_width(params, width))?; - } - if !height.is_null() { - Error::from_module(c::oakcommon_videoparams_get_height(params, height))?; - } - if !fps_num.is_null() || !fps_den.is_null() { - let mut n: c_int = 0; - let mut d: c_int = 0; - Error::from_module(c::oakcommon_videoparams_get_frame_rate( - params, &mut n, &mut d, - ))?; - if !fps_num.is_null() { - *fps_num = n; - } - if !fps_den.is_null() { - *fps_den = d; - } - } - if !par_num.is_null() || !par_den.is_null() { - let mut pn: c_int = 0; - let mut pd: c_int = 0; - Error::from_module(c::oakcommon_videoparams_get_pixel_aspect_ratio( - params, &mut pn, &mut pd, - ))?; - if !par_num.is_null() { - *par_num = pn; - } - if !par_den.is_null() { - *par_den = pd; - } - } - if !interlacing.is_null() { - Error::from_module(c::oakcommon_videoparams_get_interlacing( - params, - interlacing, - ))?; - } - if !format.is_null() { - Error::from_module(c::oakcommon_videoparams_get_format(params, format))?; - } - if !divider.is_null() { - Error::from_module(c::oakcommon_videoparams_get_divider(params, divider))?; - } - let mut hh = params; - c::oakcommon_videoparams_free(&mut hh); - Ok(()) - }) -} - -/// `oakengine_sequence_set_video_params` — write the video parameters -/// (`undoable` flag; -1 leaves a field unchanged). -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_set_video_params( - self_: *mut OakEngineSequence, - width: c_int, - height: c_int, - fps_num: c_int, - fps_den: c_int, - par_num: c_int, - par_den: c_int, - interlacing: c_int, - format: c_int, - undoable: c_int, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - if self_.is_null() { - set_seq_error("invalid sequence"); - return Err(Error::Invalid); - } - if width < -1 || width == 0 || height < -1 || height == 0 { - set_seq_error(&format!("invalid video size {}x{}", width, height)); - return Err(Error::Invalid); - } - if (fps_num == -1) != (fps_den == -1) - || fps_num < -1 - || fps_num == 0 - || fps_den < -1 - || fps_den == 0 - { - set_seq_error(&format!("invalid frame rate {}/{}", fps_num, fps_den)); - return Err(Error::Invalid); - } - if (par_num == -1) != (par_den == -1) - || par_num < -1 - || par_num == 0 - || par_den < -1 - || par_den == 0 - { - set_seq_error(&format!("invalid pixel aspect {}/{}", par_num, par_den)); - return Err(Error::Invalid); - } - if interlacing < -1 || interlacing > 2 { - set_seq_error(&format!("invalid interlacing {}", interlacing)); - return Err(Error::Invalid); - } - if format < -1 || format >= 32 { - set_seq_error(&format!("invalid pixel format {}", format)); - return Err(Error::Invalid); - } - - let seq = unbox(self_)?; - let mut current = CHandle::null(); - let rc = n::oaknode_sequence_get_video_params(seq, 0, &mut current); - if rc != 0 || current.is_null() { - return Err(Error::State); - } - let mut cur_w: c_int = 0; - let mut cur_h: c_int = 0; - let mut cur_fn: c_int = 0; - let mut cur_fd: c_int = 0; - let mut cur_fmt: c_int = 0; - c::oakcommon_videoparams_get_width(current, &mut cur_w); - c::oakcommon_videoparams_get_height(current, &mut cur_h); - c::oakcommon_videoparams_get_frame_rate(current, &mut cur_fn, &mut cur_fd); - c::oakcommon_videoparams_get_format(current, &mut cur_fmt); - - // The frame rate is stored directly in the module's model (the capi's - // flipped time base lives inside the C++ VideoParams constructor). - let new = c::oakcommon_videoparams_init(); - if new.is_null() { - let mut h = current; - c::oakcommon_videoparams_free(&mut h); - return Err(Error::Failed("video params allocation failed".into())); - } - c::oakcommon_videoparams_set_width(new, if width >= 0 { width } else { cur_w }); - c::oakcommon_videoparams_set_height(new, if height >= 0 { height } else { cur_h }); - c::oakcommon_videoparams_set_format(new, if format >= 0 { format } else { cur_fmt }); - c::oakcommon_videoparams_set_frame_rate( - new, - if fps_num >= 0 { fps_num } else { cur_fn }, - if fps_den >= 0 { fps_den } else { cur_fd }, - ); - let mut equal: c_int = 0; - c::oakcommon_videoparams_equals(new, current, &mut equal); - if equal != 0 { - let mut h1 = current; - let mut h2 = new; - c::oakcommon_videoparams_free(&mut h1); - c::oakcommon_videoparams_free(&mut h2); - return Ok(()); - } - if undoable != 0 { - let data = Box::into_raw(Box::new(VideoParamsCmdData { - seq: seq.addref(), - old_params: current, - new_params: new, - })); - let cmd = vtable_command( - video_params_redo, - video_params_undo, - video_params_free, - data as *mut c_void, - )?; - push_command(cmd, "Set Sequence Video Parameters") - } else { - let rc = n::oaknode_sequence_set_video_params(seq, 0, new); - let mut h1 = current; - let mut h2 = new; - c::oakcommon_videoparams_free(&mut h1); - c::oakcommon_videoparams_free(&mut h2); - Error::from_module(rc) - } - }) -} - -/// `oakengine_sequence_get_audio_params` — sample rate and channel layout. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_get_audio_params( - self_: *const OakEngineSequence, - sample_rate: *mut c_int, - channel_layout: *mut u64, -) -> c_int { - guard(|| unsafe { - let h = unbox(self_)?; - let mut out: *mut c_void = std::ptr::null_mut(); - Error::from_module(n::oaknode_sequence_get_audio_params(h, 0, &mut out))?; - if out.is_null() { - return Err(Error::Failed("sequence has no audio params".into())); - } - let rate = a::oakcore_audioparams_sample_rate(out); - let layout = a::oakcore_audioparams_channel_layout(out); - // The module created the oakcore handle; release it after reading. - a::oakcore_audioparams_free(out); - if !sample_rate.is_null() { - *sample_rate = rate; - } - if !channel_layout.is_null() { - *channel_layout = layout; - } - Ok(()) - }) -} - -/// `oakengine_sequence_set_audio_params` — write the audio parameters. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_set_audio_params( - self_: *mut OakEngineSequence, - sample_rate: c_int, - channel_layout: u64, - undoable: c_int, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - if self_.is_null() { - set_seq_error("invalid sequence"); - return Err(Error::Invalid); - } - let seq = unbox(self_)?; - // Read the current params; `sample_rate <= 0` / `channel_layout == 0` - // leave the field unchanged (the capi's semantics). - let mut current: *mut c_void = std::ptr::null_mut(); - let rc = n::oaknode_sequence_get_audio_params(seq, 0, &mut current); - if rc != 0 || current.is_null() { - return Err(Error::State); - } - let cur_rate = a::oakcore_audioparams_sample_rate(current); - let cur_layout = a::oakcore_audioparams_channel_layout(current); - let cur_format = a::oakcore_audioparams_format(current); - a::oakcore_audioparams_free(current); - - let new_rate = if sample_rate <= 0 { - cur_rate - } else { - sample_rate - }; - let new_layout = if channel_layout == 0 { - cur_layout - } else { - channel_layout - }; - if new_rate == cur_rate && new_layout == cur_layout { - return Ok(()); - } - let new = a::oakcore_audioparams_create(new_rate, new_layout, cur_format); - if new.is_null() { - return Err(Error::Failed("audio params allocation failed".into())); - } - if undoable != 0 { - let old = a::oakcore_audioparams_create(cur_rate, cur_layout, cur_format); - if old.is_null() { - a::oakcore_audioparams_free(new); - return Err(Error::Failed("audio params allocation failed".into())); - } - let data = Box::into_raw(Box::new(AudioParamsCmdData { - seq: seq.addref(), - old_params: old, - new_params: new, - })); - let cmd = vtable_command( - audio_params_redo, - audio_params_undo, - audio_params_free, - data as *mut c_void, - )?; - push_command(cmd, "Set Sequence Audio Parameters") - } else { - let r = n::oaknode_sequence_set_audio_params(seq, 0, new); - a::oakcore_audioparams_free(new); - Error::from_module(r) - } - }) -} - -/// `oakengine_sequence_get_preview_divider` — preview resolution divider -/// (0 on a NULL handle). -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_get_preview_divider( - self_: *const OakEngineSequence, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let h = unbox(self_)?; - let mut params = CHandle::null(); - let rc = n::oaknode_sequence_get_video_params(h, 0, &mut params); - if rc != 0 || params.is_null() { - return Ok(0); - } - let mut divider: c_int = 0; - c::oakcommon_videoparams_get_divider(params, &mut divider); - let mut hh = params; - c::oakcommon_videoparams_free(&mut hh); - Ok(divider) - }) -} - -/// `oakengine_sequence_set_preview_divider` — set the preview divider. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_set_preview_divider( - self_: *mut OakEngineSequence, - divider: c_int, - undoable: c_int, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - if self_.is_null() { - set_seq_error("invalid sequence"); - return Err(Error::Invalid); - } - if divider < 1 { - set_seq_error(&format!("invalid preview divider {}", divider)); - return Err(Error::Invalid); - } - // The module's `VideoParams` model has no divider field - // (`videoparams_from_handle` drops it), so the write is a no-op for - // the module; the getter always reports 1. The setter still validates - // and mirrors the capi's command/apply shape. - let seq = unbox(self_)?; - let mut current = CHandle::null(); - let rc = n::oaknode_sequence_get_video_params(seq, 0, &mut current); - if rc != 0 || current.is_null() { - return Err(Error::State); - } - let mut cur_div: c_int = 0; - c::oakcommon_videoparams_get_divider(current, &mut cur_div); - if cur_div == divider { - let mut h = current; - c::oakcommon_videoparams_free(&mut h); - return Ok(()); - } - let mut h = current; - c::oakcommon_videoparams_free(&mut h); - let _ = undoable; - Ok(()) - }) -} - -/// `oakengine_sequence_get_video_auto_cache` — 1 when video auto-cache is -/// enabled (the engine accessor is a stub that reports 0). -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_get_video_auto_cache( - self_: *const OakEngineSequence, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let _ = unbox(self_)?; - Ok(0) - }) -} - -/// `oakengine_sequence_set_video_auto_cache` — forward to the engine's -/// stub accessor (no undo command; `undoable` accepted and ignored). -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_set_video_auto_cache( - self_: *mut OakEngineSequence, - enabled: c_int, - undoable: c_int, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - if self_.is_null() { - set_seq_error("invalid sequence"); - return Err(Error::Invalid); - } - // Stub: the module has no video auto-cache accessor - // (`SequenceBehavior.autocache_video` is not exposed over the C - // ABI). Mirrors the capi forwarding to the engine's stub setter. - let _ = (enabled, undoable); - let _ = unbox(self_)?; - Ok(()) - }) -} - -/// `oakengine_sequence_track_count` — tracks per type (any pointer may be -/// NULL). -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_track_count( - self_: *const OakEngineSequence, - video: *mut c_int, - audio: *mut c_int, - subtitle: *mut c_int, -) -> c_int { - guard(|| unsafe { - let h = unbox(self_)?; - if !video.is_null() { - Error::from_module(n::oaknode_sequence_get_track_count( - h, - TRACK_TYPE_VIDEO, - video, - ))?; - } - if !audio.is_null() { - Error::from_module(n::oaknode_sequence_get_track_count( - h, - TRACK_TYPE_AUDIO, - audio, - ))?; - } - if !subtitle.is_null() { - Error::from_module(n::oaknode_sequence_get_track_count( - h, - TRACK_TYPE_SUBTITLE, - subtitle, - ))?; - } - Ok(()) - }) -} - -/// `oakengine_sequence_get_playhead` — playhead as a frame timestamp. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_get_playhead( - self_: *const OakEngineSequence, - timestamp: *mut i64, -) -> c_int { - guard(|| unsafe { - if timestamp.is_null() { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let tb = seq_time_base(h)?; - let mut num: c_int = 0; - let mut den: c_int = 0; - Error::from_module(n::oaknode_sequence_get_playhead(h, &mut num, &mut den))?; - *timestamp = rational_to_ts(num as i64, den as i64, tb); - Ok(()) - }) -} - -/// `oakengine_sequence_set_playhead` — move the playhead to `timestamp`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_set_playhead( - self_: *mut OakEngineSequence, - timestamp: i64, -) -> c_int { - guard(|| unsafe { - let h = unbox(self_)?; - let tb = seq_time_base(h)?; - let (num, den) = ts_to_rational(timestamp, tb); - // The module's playhead setter takes c_int rationals; timestamps that - // overflow are truncated (module limitation). - Error::from_module(n::oaknode_sequence_set_playhead( - h, - num as c_int, - den as c_int, - )) - }) -} - -/// `oakengine_sequence_get_playhead_seconds` — playhead in seconds. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_get_playhead_seconds( - self_: *const OakEngineSequence, - seconds: *mut f64, -) -> c_int { - guard(|| unsafe { - if seconds.is_null() { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let mut num: c_int = 0; - let mut den: c_int = 0; - Error::from_module(n::oaknode_sequence_get_playhead(h, &mut num, &mut den))?; - *seconds = num as f64 / den as f64; - Ok(()) - }) -} - -/// `oakengine_sequence_workarea_is_enabled` — 1 when the workarea is -/// enabled. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_workarea_is_enabled( - self_: *const OakEngineSequence, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let h = unbox(self_)?; - let wa = seq_workarea(h)?; - let mut enabled: c_int = 0; - let rc = tl::oaktimeline_workarea_get( - wa, - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - &mut enabled, - ); - release_handle(wa); - Error::from_module(rc)?; - Ok(enabled) - }) -} - -/// `oakengine_sequence_get_workarea` — workarea in/out as frame timestamps. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_get_workarea( - self_: *const OakEngineSequence, - in_: *mut i64, - out: *mut i64, -) -> c_int { - guard(|| unsafe { - let h = unbox(self_)?; - let tb = seq_time_base(h)?; - let wa = seq_workarea(h)?; - let mut in_num: c_int = 0; - let mut in_den: c_int = 0; - let mut out_num: c_int = 0; - let mut out_den: c_int = 0; - let rc = tl::oaktimeline_workarea_get( - wa, - &mut in_num, - &mut in_den, - &mut out_num, - &mut out_den, - std::ptr::null_mut(), - ); - release_handle(wa); - Error::from_module(rc)?; - if !in_.is_null() { - *in_ = rational_to_ts(in_num as i64, in_den as i64, tb); - } - if !out.is_null() { - *out = rational_to_ts(out_num as i64, out_den as i64, tb); - } - Ok(()) - }) -} - -/// `oakengine_sequence_set_workarea` — enable flag plus in/out timestamps. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_set_workarea( - self_: *mut OakEngineSequence, - enabled: c_int, - in_: i64, - out: i64, -) -> c_int { - guard(|| unsafe { - let h = unbox(self_)?; - let tb = seq_time_base(h)?; - let wa = seq_workarea(h)?; - let rc = tl::oaktimeline_workarea_set_enabled(wa, enabled); - if rc != 0 { - release_handle(wa); - return Err(Error::Module(rc)); - } - let (in_num, in_den) = ts_to_rational(in_, tb); - let (out_num, out_den) = ts_to_rational(out, tb); - let rc = tl::oaktimeline_workarea_set_range( - wa, - in_num as c_int, - in_den as c_int, - out_num as c_int, - out_den as c_int, - ); - release_handle(wa); - Error::from_module(rc) - }) -} - -/// `oakengine_sequence_set_workarea_undoable` — set the workarea's enabled -/// flag and in/out range as ONE undoable entry ("Set Workarea"). -/// -/// The old range must be supplied by the caller (the same convention as -/// `oakengine_workarea_set_range_undoable`): pass the range read before the -/// change — e.g. the drag-start range of a ruler work-area drag. The enabled -/// flag's previous value is captured by the module command itself. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_set_workarea_undoable( - self_: *mut OakEngineSequence, - enabled: c_int, - in_: i64, - out: i64, - old_in: i64, - old_out: i64, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - let sequence = match unbox(self_) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid sequence"); - return Err(Error::Invalid); - } - }; - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - set_seq_error("sequence has no valid frame rate"); - return Err(Error::State); - } - }; - if in_ < 0 || out < 0 || old_in < 0 || old_out < 0 { - set_seq_error("invalid workarea range"); - return Err(Error::Invalid); - } - let wa = seq_workarea(sequence)?; - let (new_in_num, new_in_den) = ts_to_rational(in_, tb); - let (new_out_num, new_out_den) = ts_to_rational(out, tb); - let (old_in_num, old_in_den) = ts_to_rational(old_in, tb); - let (old_out_num, old_out_den) = ts_to_rational(old_out, tb); - let enabled_cmd = tl::oaktimeline_workarea_set_enabled_command(wa, enabled); - if enabled_cmd.is_null() { - release_handle(wa); - set_seq_error("workarea enabled command failed"); - return Err(Error::Failed("workarea enabled command failed".into())); - } - let range_cmd = tl::oaktimeline_workarea_set_range_command( - wa, - new_in_num as c_int, - new_in_den as c_int, - new_out_num as c_int, - new_out_den as c_int, - old_in_num as c_int, - old_in_den as c_int, - old_out_num as c_int, - old_out_den as c_int, - ); - release_handle(wa); - if range_cmd.is_null() { - set_seq_error("workarea range command failed"); - return Err(Error::Failed("workarea range command failed".into())); - } - // The commands hold borrowed clones of `wa` (no addref); the workarea - // lives with the sequence, so it outlives the undo entry. - push_multi_commands(&[enabled_cmd, range_cmd], "Set Workarea") - }) -} - -/// `oakengine_sequence_marker_count` — number of timeline markers. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_marker_count(self_: *const OakEngineSequence) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let h = unbox(self_)?; - let list = seq_marker_list(h)?; - let mut count: c_int = 0; - let rc = tl::oaktimeline_marker_count(list, &mut count); - release_handle(list); - Error::from_module(rc)?; - Ok(count) - }) -} - -/// `oakengine_sequence_marker_at` — marker at `index` (time as a frame -/// timestamp, name via the buf/size convention, color index). -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_marker_at( - self_: *const OakEngineSequence, - index: c_int, - time: *mut i64, - name: *mut c_char, - name_size: c_int, - color: *mut c_int, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || index < 0 { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let tb = seq_time_base(h)?; - let list = seq_marker_list(h)?; - let mut in_num: c_int = 0; - let mut in_den: c_int = 0; - let mut out_num: c_int = 0; - let mut out_den: c_int = 0; - let mut marker_color: c_int = 0; - let rc = tl::oaktimeline_marker_at( - list, - index, - &mut in_num, - &mut in_den, - &mut out_num, - &mut out_den, - &mut marker_color, - name, - name_size, - ); - release_handle(list); - if rc < 0 { - return Err(Error::Module(rc)); - } - if !time.is_null() { - *time = rational_to_ts(in_num as i64, in_den as i64, tb); - } - if !color.is_null() { - *color = marker_color; - } - Ok(()) - }) -} - -/* ---- Timeline editing primitives ----------------------------------------- */ - -/// `oakengine_sequence_last_error` — last editing error for this thread. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_last_error(buf: *mut c_char, buf_size: c_int) -> c_int { - SEQ_LAST_ERROR.with(|e| unsafe { write_string(&e.borrow(), buf, buf_size) }) -} - -/// `oakengine_sequence_add_track` — append a track and return its index. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_add_track( - self_: *mut OakEngineSequence, - track_type: c_int, -) -> c_int { - guard_int(|| unsafe { - set_seq_error(""); - if self_.is_null() || track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE { - set_seq_error("invalid sequence or track type"); - return Err(Error::Invalid); - } - let seq = unbox(self_)?; - let mut list = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_track_list( - seq, track_type, &mut list, - ))?; - // 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 mut count: c_int = 0; - Error::from_module(n::oaknode_sequence_get_track_count( - seq, track_type, &mut count, - ))?; - release_handle(list); - Ok(count - 1) - }) -} - -/// `oakengine_sequence_add_track_command` — create a TimelineAddTrackCommand -/// without pushing it. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_add_track_command( - self_: *mut OakEngineSequence, - track_type: c_int, - auto_merge: c_int, - out_track: *mut *mut OakEngineTrack, -) -> *mut c_void { - guard_ptr(|| unsafe { - if self_.is_null() || track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE { - return Ok(std::ptr::null_mut()); - } - let seq = unbox(self_)?; - let mut list = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_track_list( - seq, track_type, &mut list, - ))?; - let cmd = tl::oaktimeline_add_track_command(list); - if cmd.is_null() { - release_handle(list); - return Ok(std::ptr::null_mut()); - } - // The module command carries its own internal track (inaccessible - // over the C ABI); `out_track` receives a live-created track the - // caller can hand to the list, mirroring the capi's - // `command->track()`. `auto_merge` is accepted and ignored (the - // module command hardcodes automerge off). - let _ = auto_merge; - if !out_track.is_null() { - let track = n::oaknode_track_create(track_type); - if track.is_null() { - release_handle(list); - return Ok(std::ptr::null_mut()); - } - Error::from_module(n::oaknode_tracklist_add_track(list, track))?; - *out_track = box_handle::(track); - } - release_handle(list); - Ok(command_box(cmd)?.cast()) - }) -} - -/// `oakengine_sequence_ripple_tracks_command` — create a -/// TrackListRippleToolCommand. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_ripple_tracks_command( - self_: *mut OakEngineSequence, - track_type: c_int, - infos: *const OakEngineRippleInfo, - info_count: c_int, - movement_num: i64, - movement_den: i64, - movement_mode: c_int, -) -> *mut c_void { - guard_ptr(|| { - if self_.is_null() - || infos.is_null() - || info_count <= 0 - || movement_den == 0 - || track_type < TRACK_TYPE_VIDEO - || track_type > TRACK_TYPE_SUBTITLE - || movement_mode < 0 - || movement_mode > MOVEMENT_MODE_TRIM_OUT - { - return Ok(std::ptr::null_mut()); - } - // Stub: the oaktimeline module implements TrackListRippleToolCommand - // internally (undoripple.rs) but exposes no C creator for it; the - // facade bridge accordingly has no `oaktimeline_ripple_tracks_command`. - let _ = ( - self_, - track_type, - infos, - info_count, - movement_num, - movement_den, - movement_mode, - ); - Ok(std::ptr::null_mut()) - }) -} - -/// `oakengine_sequence_add_footage_clip` — place a clip of `footage` on a -/// track (undoable). -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_add_footage_clip( - seq: *mut OakEngineSequence, - footage: *mut OakEngineFootage, - track_type: c_int, - track_index: c_int, - in_: i64, - out: i64, - media_in: i64, -) -> *mut OakEngineClip { - guard_ptr(|| unsafe { - add_footage_clip_impl(seq, footage, track_type, track_index, in_, out, media_in, true) - }) -} - -/// `oakengine_sequence_add_footage_clip_ex` — like -/// `oakengine_sequence_add_footage_clip`, but skips the same-project -/// check (M12 P0). -/// -/// Sequences created through `oakengine_sequence_new` live in their own -/// scratch project (documented deviation: the module's whole-subgraph -/// transfer cannot remap the ids held inside behaviors, so project -/// membership is not established in the module world). The plain export -/// therefore rejects every footage clip as "different projects". The -/// `_ex` variant accepts the module reality: the check is unenforceable -/// and only the handle validity is required. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_add_footage_clip_ex( - seq: *mut OakEngineSequence, - footage: *mut OakEngineFootage, - track_type: c_int, - track_index: c_int, - in_: i64, - out: i64, - media_in: i64, -) -> *mut OakEngineClip { - guard_ptr(|| unsafe { - add_footage_clip_impl(seq, footage, track_type, track_index, in_, out, media_in, false) - }) -} - -/// Shared implementation of the footage-clip placement. -unsafe fn add_footage_clip_impl( - seq: *mut OakEngineSequence, - footage: *mut OakEngineFootage, - track_type: c_int, - track_index: c_int, - in_: i64, - out: i64, - media_in: i64, - strict_project: bool, -) -> Result<*mut OakEngineClip> { - unsafe { - set_seq_error(""); - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid sequence handle"); - return Ok(std::ptr::null_mut()); - } - }; - let footage_h = match unbox(footage) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid footage handle"); - return Ok(std::ptr::null_mut()); - } - }; - if track_type != TRACK_TYPE_VIDEO && track_type != TRACK_TYPE_AUDIO { - set_seq_error("clips are only supported on video and audio tracks"); - return Ok(std::ptr::null_mut()); - } - // Same-project check. The `_ex` variant skips it: sequences - // created through `oakengine_sequence_new` live in their own - // scratch project (documented deviation), so the strict check - // can never pass and only handle validity is enforceable. - let mut seq_project = CHandle::null(); - let seq_node = n::oaknode_sequence_as_node(sequence); - let rc = n::oaknode_node_get_project(seq_node, &mut seq_project); - let mut foot_project = CHandle::null(); - let rc2 = n::oaknode_node_get_project(footage_h, &mut foot_project); - release_handle(seq_node); - if strict_project - && (rc != 0 - || rc2 != 0 - || seq_project.is_null() - || foot_project.is_null() - || seq_project.ctx != foot_project.ctx) - { - release_handle(seq_project); - release_handle(foot_project); - set_seq_error("footage and sequence belong to different projects"); - return Ok(std::ptr::null_mut()); - } - release_handle(seq_project); - release_handle(foot_project); - if in_ < 0 || out <= in_ || media_in < 0 { - set_seq_error("invalid clip range (need 0 <= in < out and media_in >= 0)"); - return Ok(std::ptr::null_mut()); - } - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - set_seq_error("sequence has no valid frame rate"); - return Ok(std::ptr::null_mut()); - } - }; - let mut list = CHandle::null(); - if n::oaknode_sequence_get_track_list(sequence, track_type, &mut list) != 0 - || list.is_null() - { - set_seq_error("sequence has no track list for this type"); - return Ok(std::ptr::null_mut()); - } - let mut track_count: c_int = 0; - if n::oaknode_tracklist_get_track_count(list, &mut track_count) != 0 - || track_index < 0 - || track_index >= track_count - { - release_handle(list); - set_seq_error(&format!( - "track index {} out of range ({} tracks)", - track_index, track_count - )); - return Ok(std::ptr::null_mut()); - } - - let (in_num, in_den) = ts_to_rational(in_, tb); - let (out_num, out_den) = ts_to_rational(out, tb); - let (media_num, media_den) = ts_to_rational(media_in, tb); - - // The application's drop-import chain reduced to its editing core. - let clip = n::oaknode_block_clip_create(); - if clip.is_null() { - release_handle(list); - set_seq_error("clip creation failed"); - return Ok(std::ptr::null_mut()); - } - let (len_num, len_den) = rat_sub(out_num, out_den, in_num, in_den); - Error::from_module(n::oaknode_clip_set_media_in( - clip, - media_num as c_int, - media_den as c_int, - ))?; - Error::from_module(n::oaknode_block_set_length_and_media_in( - clip, - len_num as c_int, - len_den as c_int, - ))?; - - // The `_ex` variant connects a footage node created directly in - // the sequence's scratch project (graph edges cannot cross - // projects; the real-project footage node stays untouched for - // the project browser). The module clip declares its texture - // input as `tex_in` (the C++-era `buffer_in` name does not exist - // here); the graph's connect validates the input side only, so - // the footage edge builds even though the footage node has no - // declared output. - // - // Order matters: the place command ADOPTS the block into the - // sequence's project (remapping the block handle's node id), so - // the footage edge is created only after the block is placed — - // the edge needs the clip's final (project, id). - let scratch_footage = if !strict_project { - let scratch = seq_project_of(sequence); - let mut out = CHandle::null(); - if !scratch.is_null() { - let mut fbuf = [0 as c_char; 4096]; - if n::oaknode_footage_filename( - footage_h, - fbuf.as_mut_ptr(), - fbuf.len() as c_int, - ) >= 0 - { - let fname = crate::handle::read_cstr(fbuf.as_ptr()); - let fc = std::ffi::CString::new(fname).unwrap_or_default(); - out = n::oaknode_footage_create(scratch, fc.as_ptr()); - } - release_handle(scratch); - } - out - } else { - CHandle::null() - }; - - let mut children: Vec = Vec::new(); - // The `_ex` variant skips the graph add-node command: it would - // move the clip entry into the sequence's project under a fresh - // id while leaving the caller's clip handle stale, which then - // breaks the place command's adoption (the entry is already - // gone). The place command's `adopt_block` performs the move - // itself and remaps the block handle; on undo the block is - // simply left as an orphan node in the sequence's project. - if strict_project { - let add_cmd = n::oaknode_command_create_add_node(seq_project_of(sequence), clip); - if add_cmd.is_null() { - release_handle(list); - if !scratch_footage.is_null() { - release_handle(scratch_footage); - } - set_seq_error("node add command failed"); - return Ok(std::ptr::null_mut()); - } - children.push(add_cmd); - } - let place_cmd = - tl::oaktimeline_place_block_command(list, track_index, clip, in_num, in_den); - if place_cmd.is_null() { - for child in &children { - release_handle(*child); - } - release_handle(list); - if !scratch_footage.is_null() { - release_handle(scratch_footage); - } - set_seq_error("place block command failed"); - return Ok(std::ptr::null_mut()); - } - children.push(place_cmd); - // The commands hold borrowed clones of `list` (no addref), so the - // handle must stay alive until the group push has executed the - // redos; released right after. - if let Err(e) = push_multi_commands(&children, "Add Clip") { - release_handle(list); - set_seq_error(&format!("failed to push add-clip command: {:?}", e)); - return Err(e); - } - release_handle(list); - if !scratch_footage.is_null() { - let clip_node = n::oaknode_block_as_node(clip); - let mut edge = CHandle::null(); - let rc = n::oaknode_node_connect_undoable( - scratch_footage, - clip_node, - c"tex_in".as_ptr(), - &mut edge, - ); - release_handle(clip_node); - release_handle(scratch_footage); - if rc != 0 || edge.is_null() { - set_seq_error("footage connection failed"); - return Ok(std::ptr::null_mut()); - } - let edge_box = box_handle::(edge); - push_or_run(edge_box, c"Add Clip".as_ptr())?; - } - Ok(box_handle::(clip)) - } -} - -/// Borrowed project handle of a sequence (temporary; caller releases). -/// -/// # Safety -/// `seq` must be a live module sequence handle. -unsafe fn seq_project_of(seq: CHandle) -> CHandle { - unsafe { - let node = n::oaknode_sequence_as_node(seq); - let mut project = CHandle::null(); - let rc = n::oaknode_node_get_project(node, &mut project); - release_handle(node); - if rc != 0 { - return CHandle::null(); - } - project - } -} - -/// `oakengine_sequence_clip_count` — clips on a track (gaps skipped). -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_clip_count( - self_: *mut OakEngineSequence, - track_type: c_int, - track_index: c_int, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() || track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let mut list = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_track_list(h, track_type, &mut list))?; - let mut track_count: c_int = 0; - Error::from_module(n::oaknode_tracklist_get_track_count(list, &mut track_count))?; - if track_index < 0 || track_index >= track_count { - release_handle(list); - return Err(Error::NotFound); - } - let mut track = CHandle::null(); - Error::from_module(n::oaknode_tracklist_get_track_at( - list, - track_index, - &mut track, - ))?; - let mut block_count: c_int = 0; - Error::from_module(n::oaknode_track_get_block_count(track, &mut block_count))?; - let mut count: c_int = 0; - for i in 0..block_count { - let mut block = CHandle::null(); - if n::oaknode_track_get_block_at(track, i, &mut block) != 0 || block.is_null() { - continue; - } - let mut kind: c_int = 0; - let rc = n::oaknode_block_get_kind(block, &mut kind); - release_handle(block); - if rc == 0 && kind == BLOCK_KIND_CLIP { - count += 1; - } - } - release_handle(track); - release_handle(list); - Ok(count) - }) -} - -/// `oakengine_sequence_clip_at` — borrowed clip at (track, clip) index. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_clip_at( - self_: *mut OakEngineSequence, - track_type: c_int, - track_index: c_int, - clip_index: c_int, -) -> *mut OakEngineClip { - guard_ptr(|| unsafe { - if self_.is_null() - || track_type < TRACK_TYPE_VIDEO - || track_type > TRACK_TYPE_SUBTITLE - || clip_index < 0 - { - return Ok(std::ptr::null_mut()); - } - let h = unbox(self_)?; - let mut list = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_track_list(h, track_type, &mut list))?; - let clip = clip_at_index(list, track_index, clip_index); - release_handle(list); - if clip.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(clip)) - }) -} - -/// `oakengine_clip_get_range` — clip timeline range and media in-point as -/// frame timestamps. -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_get_range( - self_: *const OakEngineClip, - in_: *mut i64, - out: *mut i64, - media_in: *mut i64, -) -> c_int { - guard(|| unsafe { - let h = unbox(self_)?; - // The clip's sequence (clip -> track -> sequence) provides the - // timebase. - let mut track = CHandle::null(); - let rc = n::oaknode_block_get_track(h, &mut track); - if rc != 0 || track.is_null() { - return Err(Error::State); - } - let mut sequence = CHandle::null(); - let rc = n::oaknode_track_get_sequence(track, &mut sequence); - release_handle(track); - if rc != 0 || sequence.is_null() { - return Err(Error::State); - } - let tb = seq_time_base(sequence)?; - release_handle(sequence); - let mut in_num: c_int = 0; - let mut in_den: c_int = 0; - Error::from_module(n::oaknode_block_get_in(h, &mut in_num, &mut in_den))?; - let mut out_num: c_int = 0; - let mut out_den: c_int = 0; - Error::from_module(n::oaknode_block_get_out(h, &mut out_num, &mut out_den))?; - let mut mi_num: c_int = 0; - let mut mi_den: c_int = 0; - Error::from_module(n::oaknode_clip_get_media_in(h, &mut mi_num, &mut mi_den))?; - if !in_.is_null() { - *in_ = rational_to_ts(in_num as i64, in_den as i64, tb); - } - if !out.is_null() { - *out = rational_to_ts(out_num as i64, out_den as i64, tb); - } - if !media_in.is_null() { - *media_in = rational_to_ts(mi_num as i64, mi_den as i64, tb); - } - Ok(()) - }) -} - -/// `oakengine_clip_get_sequence` — the clip's owning sequence. -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_get_sequence( - self_: *const OakEngineClip, -) -> *mut OakEngineSequence { - guard_ptr(|| unsafe { - if self_.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(self_)?; - let mut track = CHandle::null(); - let rc = n::oaknode_block_get_track(h, &mut track); - if rc != 0 || track.is_null() { - return Ok(std::ptr::null_mut()); - } - let mut sequence = CHandle::null(); - let rc = n::oaknode_track_get_sequence(track, &mut sequence); - release_handle(track); - if rc != 0 || sequence.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(sequence)) - }) -} - -/// `oakengine_clip_as_node` — the clip's node view (borrowed; freed with -/// `oakengine_node_free`). The effect-stack surface (chain enumeration and -/// edits) is node-based, so the app converts its clip handle before -/// walking the chain. -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_as_node(self_: *const OakEngineClip) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - if self_.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(self_)?; - let node = n::oaknode_block_as_node(h); - if node.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(node)) - }) -} - -// ---- Sequence node-graph enumeration ---------------------------------------- -// -// The node-editor surface (M12 P2): the app displays the CURRENT sequence's -// node graph — the sequence node (the output) plus the blocks, effects and -// scratch footage of its timeline. The module keeps the sequence in its own -// scratch project (documented deviation, see `oakengine_sequence_new`), so -// the graph is exactly that project's node list; positions live in the -// sequence node's context position map (`oakengine_node_get_context_position`). - -/// `oakengine_sequence_as_node` — the sequence's node view (borrowed; -/// freed with `oakengine_node_free`). The graph surface addresses the -/// sequence through this handle: the sequence node is the graph's output -/// node AND the context for its position map. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_as_node( - self_: *const OakEngineSequence, -) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - if self_.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(self_)?; - let node = n::oaknode_sequence_as_node(h); - if node.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(node)) - }) -} - -/// `oakengine_sequence_node_count` — nodes in the sequence's owning -/// project (the sequence's graph; 0 for NULL/invalid). -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_node_count( - self_: *const OakEngineSequence, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let project = seq_project_of(unbox(self_)?); - if project.is_null() { - return Ok(0); - } - let count = n::oaknode_project_node_count(project); - release_handle(project); - Ok(count) - }) -} - -/// `oakengine_sequence_node_at` — boxed node at `index` (freed with -/// `oakengine_node_free`); NULL for an invalid index or sequence. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_node_at( - self_: *const OakEngineSequence, - index: c_int, -) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - if self_.is_null() || index < 0 { - return Ok(std::ptr::null_mut()); - } - let project = seq_project_of(unbox(self_)?); - if project.is_null() { - return Ok(std::ptr::null_mut()); - } - let node = n::oaknode_project_node_at(project, index); - release_handle(project); - if node.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(node)) - }) -} - -/// The uuid of a project handle (empty on failure). Project handles are -/// per-call boxes around the same `Arc`, so ctx pointers cannot be compared; -/// the uuid is the stable identity. -/// -/// # Safety -/// `project` must be a live module project handle. -unsafe fn project_uuid(project: CHandle) -> String { - unsafe { - let mut buf = [0 as c_char; 256]; - let len = n::oaknode_project_get_uuid(project, buf.as_mut_ptr(), buf.len() as c_int); - if len < 0 { - String::new() - } else { - read_cstr(buf.as_ptr()) - } - } -} - -/// `oakengine_sequence_remove_node` — undoable removal of `node` from the -/// sequence's graph (its owning project; the module's remove command drops -/// incident edges). The sequence node itself (the graph's output) cannot be -/// removed. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_remove_node( - self_: *mut OakEngineSequence, - node: *mut OakEngineNode, -) -> c_int { - guard(|| unsafe { - if self_.is_null() || node.is_null() { - set_seq_error("invalid sequence or node"); - return Err(Error::Invalid); - } - let sh = unbox(self_)?; - let nh = unbox(node)?; - let project = seq_project_of(sh); - if project.is_null() { - return Err(Error::Invalid); - } - // The node must live in the sequence's OWNING project. Node - // identities are per-project arena slots (generation + index), so - // two projects produce colliding identities; compare the projects' - // uuids instead. - let mut node_project = CHandle::null(); - let rc = n::oaknode_node_get_project(nh, &mut node_project); - let node_uuid = if rc == 0 && !node_project.is_null() { - project_uuid(node_project) - } else { - String::new() - }; - release_handle(node_project); - let seq_uuid = project_uuid(project); - if node_uuid.is_empty() || node_uuid != seq_uuid { - release_handle(project); - set_seq_error("node does not belong to this sequence's graph"); - return Err(Error::Invalid); - } - // The sequence node is the graph's context/output; removing it would - // orphan every position map entry and the sequence itself. - let seq_node = n::oaknode_sequence_as_node(sh); - let is_self = !seq_node.is_null() - && n::oaknode_node_identity(seq_node) == n::oaknode_node_identity(nh); - release_handle(seq_node); - release_handle(project); - if is_self { - set_seq_error("the sequence node cannot be removed"); - return Err(Error::Invalid); - } - let cmd = n::oaknode_command_create_remove_node(nh); - if cmd.ctx.is_null() { - return Err(Error::Failed("remove node command failed".into())); - } - push_command(cmd, "Remove Node") - }) -} - -/// `oakengine_clip_get_media_filename` — the clip's upstream footage -/// filename (two-stage buf/size; M12 P4 — the waveform decorator needs -/// the media file). Negative error when the clip has no media. -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_get_media_filename( - self_: *const OakEngineClip, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let Some((filename, _)) = timeline_clip_media(h) else { - return Err(Error::NotFound); - }; - let rc = crate::handle::write_string(&filename, buf, buf_size); - Ok(string_result(rc)) -}) -} - -/// Resolve a clip's media `(filename, stream)` through its graph node's -/// upstream footage (the same walk `crates::render` performs for the -/// montage). -unsafe fn timeline_clip_media(clip: CHandle) -> Option<(String, c_int)> { - unsafe { - let node = n::oaknode_block_as_node(clip); - if node.is_null() { - return None; - } - let mut footage = CHandle::null(); - if n::oaknode_node_find_input_footage(node, &mut footage) != 0 || footage.is_null() { - return None; - } - let mut buf = [0 as c_char; 4096]; - if n::oaknode_footage_filename(footage, buf.as_mut_ptr(), buf.len() as c_int) < 0 { - return None; - } - let filename = crate::handle::read_cstr(buf.as_ptr()); - Some((filename, 1)) - } -} - -/* ---- Editing primitives, round 2: split / ripple delete / trim / move ---- */ - -/// `oakengine_sequence_split_clip` — split the addressed clip at `time`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_split_clip( - seq: *mut OakEngineSequence, - track_type: c_int, - track_index: c_int, - clip_index: c_int, - time: i64, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid sequence or track type"); - return Err(Error::Invalid); - } - }; - if track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE { - set_seq_error("invalid sequence or track type"); - return Err(Error::Invalid); - } - let mut list = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_track_list( - sequence, track_type, &mut list, - ))?; - let clip = clip_at_index(list, track_index, clip_index); - release_handle(list); - if clip.is_null() { - set_seq_error(&format!( - "no clip at track {} index {}", - track_index, clip_index - )); - return Err(Error::NotFound); - } - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - set_seq_error("sequence has no valid frame rate"); - return Err(Error::State); - } - }; - let (point_num, point_den) = ts_to_rational(time, tb); - let mut in_num: c_int = 0; - let mut in_den: c_int = 0; - let mut out_num: c_int = 0; - let mut out_den: c_int = 0; - Error::from_module(n::oaknode_block_get_in(clip, &mut in_num, &mut in_den))?; - Error::from_module(n::oaknode_block_get_out(clip, &mut out_num, &mut out_den))?; - let cmp_in = rat_cmp(point_num, point_den, in_num as i64, in_den as i64); - let cmp_out = rat_cmp(point_num, point_den, out_num as i64, out_den as i64); - if cmp_in != std::cmp::Ordering::Greater || cmp_out != std::cmp::Ordering::Less { - set_seq_error(&format!( - "split time {} is not strictly inside the clip", - time - )); - return Err(Error::Invalid); - } - let cmd = tl::oaktimeline_split_command(&clip, 1, point_num, point_den); - if cmd.is_null() { - set_seq_error("split command failed"); - return Err(Error::Failed("split command failed".into())); - } - push_command(cmd, "Split Clip") - }) -} - -/// Compare two rationals (a_num/a_den vs b_num/b_den). -fn rat_cmp(a_num: i64, a_den: i64, b_num: i64, b_den: i64) -> std::cmp::Ordering { - let lhs = a_num * b_den; - let rhs = b_num * a_den; - lhs.cmp(&rhs) -} - -/// `oakengine_sequence_ripple_delete_clip` — delete the clip and ripple the -/// following content left. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_ripple_delete_clip( - seq: *mut OakEngineSequence, - track_type: c_int, - track_index: c_int, - clip_index: c_int, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid sequence or track type"); - return Err(Error::Invalid); - } - }; - if track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE { - set_seq_error("invalid sequence or track type"); - return Err(Error::Invalid); - } - let mut list = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_track_list( - sequence, track_type, &mut list, - ))?; - let clip = clip_at_index(list, track_index, clip_index); - release_handle(list); - if clip.is_null() { - set_seq_error(&format!( - "no clip at track {} index {}", - track_index, clip_index - )); - return Err(Error::NotFound); - } - let mut track = CHandle::null(); - let rc = n::oaknode_block_get_track(clip, &mut track); - if rc != 0 || track.is_null() { - set_seq_error(&format!( - "no clip at track {} index {}", - track_index, clip_index - )); - return Err(Error::NotFound); - } - let mut in_num: c_int = 0; - let mut in_den: c_int = 0; - let mut out_num: c_int = 0; - let mut out_den: c_int = 0; - Error::from_module(n::oaknode_block_get_in(clip, &mut in_num, &mut in_den))?; - Error::from_module(n::oaknode_block_get_out(clip, &mut out_num, &mut out_den))?; - let cmd = tl::oaktimeline_ripple_remove_area_command( - track, - in_num as i64, - in_den as i64, - out_num as i64, - out_den as i64, - ); - // NOTE: `track` is intentionally NOT released — the module command - // stores the borrowed handle for its whole lifetime (its `redo`/ - // `undo` re-resolve the track), and the module model keeps such - // handles alive for the command's lifetime (same as - // `oakengine_sequence_delete_clips`). - if cmd.is_null() { - set_seq_error("ripple delete command failed"); - return Err(Error::Failed("ripple delete command failed".into())); - } - push_command(cmd, "Ripple Delete Clip") - }) -} - -/// `oakengine_clip_trim` — change the clip's timeline range. -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_trim( - clip: *mut OakEngineClip, - new_in: i64, - new_out: i64, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - let h = match unbox(clip) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid clip handle"); - return Err(Error::Invalid); - } - }; - let mut track = CHandle::null(); - let rc = n::oaknode_block_get_track(h, &mut track); - if rc != 0 || track.is_null() { - set_seq_error("clip is not on a track"); - return Err(Error::State); - } - let mut sequence = CHandle::null(); - let rc = n::oaknode_track_get_sequence(track, &mut sequence); - if rc != 0 || sequence.is_null() { - release_handle(track); - set_seq_error("sequence has no valid frame rate"); - return Err(Error::State); - } - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - release_handle(sequence); - release_handle(track); - set_seq_error("sequence has no valid frame rate"); - return Err(Error::State); - } - }; - release_handle(sequence); - if new_in < 0 || new_out <= new_in { - release_handle(track); - set_seq_error("invalid trim range (need 0 <= new_in < new_out)"); - return Err(Error::Invalid); - } - let mut in_num: c_int = 0; - let mut in_den: c_int = 0; - let mut out_num: c_int = 0; - let mut out_den: c_int = 0; - Error::from_module(n::oaknode_block_get_in(h, &mut in_num, &mut in_den))?; - Error::from_module(n::oaknode_block_get_out(h, &mut out_num, &mut out_den))?; - let old_in = rational_to_ts(in_num as i64, in_den as i64, tb); - let old_out = rational_to_ts(out_num as i64, out_den as i64, tb); - if new_in == old_in && new_out == old_out { - release_handle(track); - return Ok(()); - } - - // The application's trim command: one end at a time, adjacent gaps - // absorb the difference; both ends are one undoable command. The - // module's own `BlockTrimCommand` applies its length setters with - // inverted semantics, so the facade carries the correct engine - // mapping (trim-in anchors the out, trim-out anchors the in). - let mut children: Vec = Vec::new(); - let (old_len_num, old_len_den) = { - let mut ln: c_int = 0; - let mut ld: c_int = 0; - Error::from_module(n::oaknode_block_get_length(h, &mut ln, &mut ld))?; - (ln, ld) - }; - if new_in != old_in { - // in-trim: length = block out - new in (out anchored). - let (new_in_num, new_in_den) = ts_to_rational(new_in, tb); - let (new_num, new_den) = - rat_sub(out_num as i64, out_den as i64, new_in_num, new_in_den); - let cmd = trim_cmd( - h, - MOVEMENT_MODE_TRIM_IN, - old_len_num, - old_len_den, - new_num as c_int, - new_den as c_int, - )?; - children.push(cmd); - } - if new_out != old_out { - // out-trim: length = new out - new in (in anchored); the old - // length is the post-in-trim length (out - new in) when both - // ends move. - let (new_in_num, new_in_den) = ts_to_rational(new_in, tb); - let (new_out_num, new_out_den) = ts_to_rational(new_out, tb); - let (new_num, new_den) = rat_sub(new_out_num, new_out_den, new_in_num, new_in_den); - let (post_in_num, post_in_den) = - rat_sub(out_num as i64, out_den as i64, new_in_num, new_in_den); - let cmd = trim_cmd( - h, - MOVEMENT_MODE_TRIM_OUT, - post_in_num as c_int, - post_in_den as c_int, - new_num as c_int, - new_den as c_int, - )?; - children.push(cmd); - } - release_handle(track); - push_multi_commands(&children, "Trim Clip") - }) -} - -/// `oakengine_sequence_move_clip` — move the addressed clip so its in -/// point becomes `new_in` (frame units in the sequence's timebase). -/// -/// Mirrors the capi: the clip's old spot is replaced with a gap and the -/// clip is placed at the destination, both as ONE undoable entry ("Move -/// Clip"). The destination track is the addressed track itself (the -/// capi passes `track_index` straight through to the place command), so -/// this is a time-only move within the same track; length and media-in -/// point are preserved. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_move_clip( - seq: *mut OakEngineSequence, - track_type: c_int, - track_index: c_int, - clip_index: c_int, - new_in: i64, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - }; - if track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE || new_in < 0 { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - let mut list = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_track_list( - sequence, track_type, &mut list, - ))?; - let clip = clip_at_index(list, track_index, clip_index); - if clip.is_null() { - set_seq_error(&format!( - "no clip at track {} index {}", - track_index, clip_index - )); - return Err(Error::NotFound); - } - let mut track = CHandle::null(); - let rc = n::oaknode_block_get_track(clip, &mut track); - if rc != 0 || track.is_null() { - set_seq_error(&format!( - "no clip at track {} index {}", - track_index, clip_index - )); - return Err(Error::NotFound); - } - let tb = seq_time_base(sequence)?; - let (new_in_num, new_in_den) = ts_to_rational(new_in, tb); - // NOTE: `list`/`clip`/`track` are borrowed module handles; the - // move command keeps copies of `list` and `clip` for the whole undo - // entry, so their shells must not be released here (the same - // convention as the other command-creating family functions). The - // destination is the addressed track itself. - let cmd = - tl::oaktimeline_move_block_command(list, track_index, clip, new_in_num, new_in_den); - push_command(cmd, "Move Clip") - }) -} - -/// `oakengine_sequence_move_clip_to_track` — move a clip to a different -/// track at `new_in` (frame units in the sequence's timebase) as ONE -/// undoable entry (M12 P4, cross-track move): the source spot becomes a -/// gap and the clip is placed on the destination track. -/// -/// The destination track must already exist and hold clips of the same -/// type (video ↔ video, audio ↔ audio). -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_move_clip_to_track( - seq: *mut OakEngineSequence, - track_type: c_int, - track_index: c_int, - clip_index: c_int, - dest_track_index: c_int, - new_in: i64, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - }; - if track_type < TRACK_TYPE_VIDEO - || track_type > TRACK_TYPE_SUBTITLE - || new_in < 0 - || dest_track_index < 0 - { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - let mut list = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_track_list( - sequence, track_type, &mut list, - ))?; - let clip = clip_at_index(list, track_index, clip_index); - if clip.is_null() { - set_seq_error(&format!( - "no clip at track {} index {}", - track_index, clip_index - )); - return Err(Error::NotFound); - } - let mut track = CHandle::null(); - let rc = n::oaknode_block_get_track(clip, &mut track); - if rc != 0 || track.is_null() { - set_seq_error(&format!( - "no clip at track {} index {}", - track_index, clip_index - )); - return Err(Error::NotFound); - } - let mut track_count: c_int = 0; - Error::from_module(n::oaknode_tracklist_get_track_count( - list, &mut track_count, - ))?; - if dest_track_index >= track_count { - set_seq_error(&format!( - "destination track {} out of range ({} tracks)", - dest_track_index, track_count - )); - return Err(Error::NotFound); - } - let tb = seq_time_base(sequence)?; - let (new_in_num, new_in_den) = ts_to_rational(new_in, tb); - - // One undoable entry: gap the source, re-home the block's in - // point, place on the destination. (The module stores a block's - // position on the block itself, so the place command alone would - // keep the old in point.) - let mut children: Vec = Vec::new(); - let gap_cmd = tl::oaktimeline_replace_block_with_gap_command(track, clip); - if gap_cmd.is_null() { - set_seq_error("replace-with-gap command failed"); - return Err(Error::Failed("gap command failed".into())); - } - children.push(gap_cmd); - let mut old_num: c_int = 0; - let mut old_den: c_int = 0; - Error::from_module(n::oaknode_block_get_in(clip, &mut old_num, &mut old_den))?; - let data = Box::into_raw(Box::new(BlockInCmdData { - block: clip, - old_num, - old_den, - new_num: new_in_num as c_int, - new_den: new_in_den as c_int, - })); - let in_cmd = vtable_command(block_in_redo, block_in_undo, block_in_free, data as *mut c_void)?; - children.push(in_cmd); - let place_cmd = tl::oaktimeline_place_block_command( - list, - dest_track_index, - clip, - new_in_num, - new_in_den, - ); - if place_cmd.is_null() { - set_seq_error("place-block command failed"); - return Err(Error::Failed("place command failed".into())); - } - children.push(place_cmd); - // The commands hold borrowed clones of `list`/`track`/`clip` (no - // addref); keep them alive until the redos run. - push_multi_commands(&children, "Move Clip to Track") - }) -} - -/// Vtable data for the block-in re-home step of a cross-track move. -struct BlockInCmdData { - /// The block (borrowed; alive with the sequence). - block: CHandle, - /// Original in point. - old_num: c_int, - /// Original in point denominator. - old_den: c_int, - /// Placement in point. - new_num: c_int, - /// Placement in point denominator. - new_den: c_int, -} - -/// `BlockInCmdData` redo: re-home the block's in point. -unsafe extern "C" fn block_in_redo(data: *mut c_void) { - unsafe { - let d = &*(data as *const BlockInCmdData); - n::oaknode_block_set_in(d.block, d.new_num, d.new_den); - } -} - -/// `BlockInCmdData` undo: restore the original in point. -unsafe extern "C" fn block_in_undo(data: *mut c_void) { - unsafe { - let d = &*(data as *const BlockInCmdData); - n::oaknode_block_set_in(d.block, d.old_num, d.old_den); - } -} - -/// `BlockInCmdData` free. -unsafe extern "C" fn block_in_free(data: *mut c_void) { - unsafe { drop(Box::from_raw(data as *mut BlockInCmdData)) }; -} - -/* ---- Batch editing (timeline panel) -------------------------------------- */ - -/// `oakengine_sequence_split_clips` — split every given clip at `time_ts`, -/// preserving links. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_split_clips( - seq: *mut OakEngineSequence, - clips: *mut *mut OakEngineClip, - clip_count: c_int, - time_ts: i64, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - }; - if clips.is_null() || clip_count <= 0 { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - set_seq_error("sequence has no valid frame rate"); - return Err(Error::State); - } - }; - let (time_num, time_den) = ts_to_rational(time_ts, tb); - - // Collect the blocks, deduplicating, and check that at least one - // spans the time (same as the engine command). - let mut blocks: Vec = Vec::new(); - let mut any_spanning = false; - let slice = std::slice::from_raw_parts(clips, clip_count as usize); - for (i, clip) in slice.iter().enumerate() { - let c = match unbox(*clip) { - Ok(h) => h, - Err(_) => { - set_seq_error(&format!("invalid clip at index {}", i)); - return Err(Error::Invalid); - } - }; - if blocks.iter().any(|b| b.ctx == c.ctx) { - continue; - } - blocks.push(c); - let mut in_num: c_int = 0; - let mut in_den: c_int = 0; - let mut out_num: c_int = 0; - let mut out_den: c_int = 0; - if n::oaknode_block_get_in(c, &mut in_num, &mut in_den) == 0 - && n::oaknode_block_get_out(c, &mut out_num, &mut out_den) == 0 - && rat_cmp(in_num as i64, in_den as i64, time_num, time_den) - == std::cmp::Ordering::Less - && rat_cmp(out_num as i64, out_den as i64, time_num, time_den) - == std::cmp::Ordering::Greater - { - any_spanning = true; - } - } - if !any_spanning { - set_seq_error(&format!("no clip spans time {}", time_ts)); - return Err(Error::NotFound); - } - let cmd = tl::oaktimeline_split_preserving_links_command( - blocks.as_ptr(), - blocks.len() as c_int, - &time_num, - &time_den, - 1, - ); - if cmd.is_null() { - set_seq_error("split command failed"); - return Err(Error::Failed("split command failed".into())); - } - push_command(cmd, "Split Clips") - }) -} - -/// `oakengine_sequence_delete_clips` — delete clips leaving gaps, -/// optionally rippling regions closed. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_delete_clips( - seq: *mut OakEngineSequence, - clips: *mut *mut OakEngineClip, - clip_count: c_int, - ripple: c_int, - ripple_ranges_ts: *const i64, - ripple_range_count: c_int, - rippled: *mut c_int, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - if !rippled.is_null() { - *rippled = 0; - } - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - }; - if clip_count < 0 - || (clip_count > 0 && clips.is_null()) - || ripple_range_count < 0 - || (ripple_range_count > 0 && ripple_ranges_ts.is_null()) - { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - if clip_count == 0 && (ripple == 0 || ripple_range_count == 0) { - return Ok(()); - } - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - set_seq_error("sequence has no valid frame rate"); - return Err(Error::State); - } - }; - - let mut children: Vec = Vec::new(); - // (track, in, out) rationals of the deleted clips, for the default - // ripple regions. - let mut clip_ranges: Vec<(CHandle, i64, i64, i64, i64)> = Vec::new(); - // NULL with a zero count is a legal empty set; the slice must not be - // constructed from the NULL pointer (`slice::from_raw_parts(NULL, 0)` - // is UB), so it is only built for a positive count. - let slice: &[*mut OakEngineClip] = if clip_count > 0 { - std::slice::from_raw_parts(clips, clip_count as usize) - } else { - &[] - }; - for (i, clip) in slice.iter().enumerate() { - let c = match unbox(*clip) { - Ok(h) => h, - Err(_) => { - set_seq_error(&format!("invalid clip at index {}", i)); - return Err(Error::Invalid); - } - }; - let mut track = CHandle::null(); - let rc = n::oaknode_block_get_track(c, &mut track); - if rc != 0 || track.is_null() { - set_seq_error(&format!("invalid clip at index {}", i)); - return Err(Error::Invalid); - } - let mut in_num: c_int = 0; - let mut in_den: c_int = 0; - let mut out_num: c_int = 0; - let mut out_den: c_int = 0; - Error::from_module(n::oaknode_block_get_in(c, &mut in_num, &mut in_den))?; - Error::from_module(n::oaknode_block_get_out(c, &mut out_num, &mut out_den))?; - let gap_cmd = tl::oaktimeline_replace_block_with_gap_command(track, c); - let remove_cmd = n::oaknode_command_create_remove_node(c); - if gap_cmd.is_null() || remove_cmd.is_null() { - return Err(Error::Failed("delete clip command failed".into())); - } - children.push(gap_cmd); - children.push(remove_cmd); - clip_ranges.push(( - track, - in_num as i64, - in_den as i64, - out_num as i64, - out_den as i64, - )); - } - - let mut ripple_command: Option = None; - if ripple != 0 { - let mut ranges: Vec<(CHandle, i64, i64, i64, i64)> = Vec::new(); - if !ripple_ranges_ts.is_null() && ripple_range_count > 0 { - for i in 0..ripple_range_count { - let range = ripple_ranges_ts.add(i as usize * 4); - let rtype = *range; - let rindex = *range.add(1); - if rtype < TRACK_TYPE_VIDEO as i64 || rtype > TRACK_TYPE_SUBTITLE as i64 { - set_seq_error(&format!("invalid track type in ripple range {}", i)); - return Err(Error::Invalid); - } - let mut list = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_track_list( - sequence, - rtype as c_int, - &mut list, - ))?; - let mut track = CHandle::null(); - let rc = n::oaknode_tracklist_get_track_at(list, rindex as c_int, &mut track); - release_handle(list); - if rc != 0 || track.is_null() { - set_seq_error(&format!( - "no track at index {} in ripple range {}", - rindex, i - )); - return Err(Error::NotFound); - } - let (in_num, in_den) = ts_to_rational(*range.add(2), tb); - let (out_num, out_den) = ts_to_rational(*range.add(3), tb); - ranges.push((track, in_num, in_den, out_num, out_den)); - } - } else { - ranges = clip_ranges; - } - if !ranges.is_empty() { - let mut in_nums: Vec = Vec::new(); - let mut in_dens: Vec = Vec::new(); - let mut out_nums: Vec = Vec::new(); - let mut out_dens: Vec = Vec::new(); - let mut tracks: Vec = Vec::new(); - for (track, in_num, in_den, out_num, out_den) in &ranges { - tracks.push(*track); - in_nums.push(*in_num); - in_dens.push(*in_den); - out_nums.push(*out_num); - out_dens.push(*out_den); - } - let cmd = tl::oaktimeline_ripple_delete_gaps_command( - sequence, - in_nums.as_ptr(), - in_dens.as_ptr(), - out_nums.as_ptr(), - out_dens.as_ptr(), - tracks.as_ptr(), - ranges.len() as c_int, - ); - if !cmd.is_null() { - children.push(cmd); - ripple_command = Some(cmd); - } - } - } - - push_multi_commands(&children, "Delete Clips")?; - if !rippled.is_null() { - *rippled = if ripple_command.is_some() { 1 } else { 0 }; - } - Ok(()) - }) -} - -/// `oakengine_sequence_ripple_delete_range` — remove [in_ts, out_ts) on -/// every track and shift the following content left. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_ripple_delete_range( - seq: *mut OakEngineSequence, - in_ts: i64, - out_ts: i64, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid range"); - return Err(Error::Invalid); - } - }; - if in_ts < 0 || out_ts <= in_ts { - set_seq_error(&format!("invalid range [{}, {})", in_ts, out_ts)); - return Err(Error::Invalid); - } - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - set_seq_error("sequence has no valid frame rate"); - return Err(Error::State); - } - }; - // The module exports no sequence-wide ripple-area command - // (`TimelineRippleRemoveAreaCommand` has no C creator), so the - // per-track `TrackRippleRemoveAreaCommand` composition is used, which - // is what the C++ command itself does internally. - let (in_num, in_den) = ts_to_rational(in_ts, tb); - let (out_num, out_den) = ts_to_rational(out_ts, tb); - let mut all_count: c_int = 0; - Error::from_module(n::oaknode_sequence_get_all_track_count( - sequence, - &mut all_count, - ))?; - let mut children: Vec = Vec::new(); - for i in 0..all_count { - let mut track = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_all_track_at( - sequence, i, &mut track, - ))?; - if track.is_null() { - continue; - } - let cmd = - tl::oaktimeline_ripple_remove_area_command(track, in_num, in_den, out_num, out_den); - // NOTE: `track` is intentionally NOT released — the module - // command stores the borrowed handle for its whole lifetime (see - // `oakengine_sequence_ripple_delete_clip`). - if cmd.is_null() { - return Err(Error::Failed("ripple delete command failed".into())); - } - children.push(cmd); - } - push_multi_commands(&children, "Ripple Delete Range") - }) -} - -/// `oakengine_clip_toggle_enabled` — flip the enabled flag of every given -/// clip (one undoable command). -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_toggle_enabled( - clips: *mut *mut OakEngineClip, - count: c_int, -) -> c_int { - guard_int(|| unsafe { - set_seq_error(""); - if count < 0 || (count > 0 && clips.is_null()) { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - let mut children: Vec = Vec::new(); - // NULL with a zero count is a legal empty set; the slice must not be - // constructed from the NULL pointer (`slice::from_raw_parts(NULL, 0)` - // is UB), so it is only built for a positive count. - let slice: &[*mut OakEngineClip] = if count > 0 { - std::slice::from_raw_parts(clips, count as usize) - } else { - &[] - }; - for (i, clip) in slice.iter().enumerate() { - let c = match unbox(*clip) { - Ok(h) => h, - Err(_) => { - set_seq_error(&format!("invalid clip at index {}", i)); - return Err(Error::Invalid); - } - }; - let mut enabled: c_int = 0; - Error::from_module(n::oaknode_block_get_enabled(c, &mut enabled))?; - let data = Box::into_raw(Box::new(BlockEnabledCmdData { - block: c.addref(), - old_enabled: enabled, - new_enabled: if enabled != 0 { 0 } else { 1 }, - })); - let cmd = vtable_command( - block_enabled_redo, - block_enabled_undo, - block_enabled_free, - data as *mut c_void, - )?; - children.push(cmd); - } - push_multi_commands(&children, "Toggle Clips Enabled")?; - Ok(count) - }) -} - -/// `oakengine_clip_set_linked` — link or unlink every given clip with each -/// other (one undoable command). -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_set_linked( - clips: *mut *mut OakEngineClip, - count: c_int, - linked: c_int, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - if count < 0 || (count > 0 && clips.is_null()) { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - if count == 0 { - return Ok(()); - } - let mut handles: Vec = Vec::new(); - let slice = std::slice::from_raw_parts(clips, count as usize); - for (i, clip) in slice.iter().enumerate() { - let c = match unbox(*clip) { - Ok(h) => h, - Err(_) => { - set_seq_error(&format!("invalid clip at index {}", i)); - return Err(Error::Invalid); - } - }; - handles.push(c); - } - // The module has no `NodeLinkManyCommand` creator; pair commands are - // assembled (each clip linked to the first for a link, every pair - // unlinked otherwise). - let mut children: Vec = Vec::new(); - for i in 0..handles.len() { - for j in (i + 1)..handles.len() { - let mut cmd = CHandle::null(); - let rc = n::oaknode_node_link_undoable(handles[i], handles[j], linked, &mut cmd); - if rc == 0 && !cmd.is_null() { - children.push(cmd); - } else if rc != 0 { - return Err(Error::Module(rc)); - } - } - } - push_multi_commands(&children, "Link Clips") - }) -} - -/// `oakengine_sequence_add_default_transition` — add the configured default -/// transitions around the given clips. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_add_default_transition( - seq: *mut OakEngineSequence, - clips: *mut *mut OakEngineClip, - count: c_int, -) -> c_int { - guard(|| { - set_seq_error(""); - let _ = seq; - if count < 0 || (count > 0 && clips.is_null()) { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - if count == 0 { - return Ok(()); - } - // Stub: the module's `TimelineAddDefaultTransitionCommand` is not - // reachable over the C ABI and its transition-node construction is - // itself unimplemented (undogeneral.rs `add_transition` NOTE). - let _ = clips; - set_seq_error("default transitions are not supported by the module"); - Err(Error::State) - }) -} - -/// `oakengine_clip_is_enabled` — 1 if the clip is enabled. -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_is_enabled(self_: *const OakEngineClip) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let h = unbox(self_)?; - let mut enabled: c_int = 0; - Error::from_module(n::oaknode_block_get_enabled(h, &mut enabled))?; - Ok(enabled) - }) -} - -/// `oakengine_clip_are_linked` — 1 if the two clips are linked. -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_are_linked( - a: *const OakEngineClip, - b: *const OakEngineClip, -) -> c_int { - guard_int(|| unsafe { - if a.is_null() || b.is_null() { - return Ok(0); - } - let ah = unbox(a)?; - let bh = unbox(b)?; - let mut linked: c_int = 0; - Error::from_module(n::oaknode_block_are_linked(ah, bh, &mut linked))?; - Ok(linked) - }) -} - -/// `oakengine_sequence_ripple_delete_in_to_out` — delete the workarea range -/// on every track (ripple or gap), disabling the workarea afterwards. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_ripple_delete_in_to_out( - seq: *mut OakEngineSequence, - ripple: c_int, - in_ts: i64, - out_ts: i64, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid range"); - return Err(Error::Invalid); - } - }; - if in_ts < 0 || out_ts <= in_ts { - set_seq_error(&format!("invalid range [{}, {})", in_ts, out_ts)); - return Err(Error::Invalid); - } - let wa = seq_workarea(sequence)?; - let mut enabled: c_int = 0; - let rc = tl::oaktimeline_workarea_get( - wa, - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - &mut enabled, - ); - if rc != 0 || enabled == 0 { - release_handle(wa); - set_seq_error("sequence workarea is not enabled"); - return Err(Error::State); - } - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - release_handle(wa); - set_seq_error("sequence has no valid frame rate"); - return Err(Error::State); - } - }; - let (in_num, in_den) = ts_to_rational(in_ts, tb); - let (out_num, out_den) = ts_to_rational(out_ts, tb); - - let mut children: Vec = Vec::new(); - if ripple != 0 { - // Ripple the area out on every track. - let mut all_count: c_int = 0; - Error::from_module(n::oaknode_sequence_get_all_track_count( - sequence, - &mut all_count, - ))?; - for i in 0..all_count { - let mut track = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_all_track_at( - sequence, i, &mut track, - ))?; - if track.is_null() { - continue; - } - let cmd = tl::oaktimeline_ripple_remove_area_command( - track, in_num, in_den, out_num, out_den, - ); - // NOTE: `track` is intentionally NOT released — the module - // command stores the borrowed handle for its whole lifetime - // (see `oakengine_sequence_ripple_delete_clip`). - if cmd.is_null() { - release_handle(wa); - return Err(Error::Failed("ripple remove command failed".into())); - } - children.push(cmd); - } - } else { - // Fill the area with a fresh gap on every unlocked track. - let mut all_count: c_int = 0; - Error::from_module(n::oaknode_sequence_get_all_track_count( - sequence, - &mut all_count, - ))?; - let (len_num, len_den) = rat_sub(out_num, out_den, in_num, in_den); - for i in 0..all_count { - let mut track = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_all_track_at( - sequence, i, &mut track, - ))?; - if track.is_null() { - continue; - } - let mut locked: c_int = 0; - let lrc = n::oaknode_track_get_locked(track, &mut locked); - if lrc != 0 || locked != 0 { - release_handle(track); - continue; - } - let mut ttype: c_int = 0; - let mut tindex: c_int = 0; - if n::oaknode_track_get_type(track, &mut ttype) != 0 - || n::oaknode_track_get_index(track, &mut tindex) != 0 - { - release_handle(track); - continue; - } - let project = seq_project_of(sequence); - let mut list = CHandle::null(); - let lrc = n::oaknode_sequence_get_track_list(sequence, ttype, &mut list); - let gap = n::oaknode_block_gap_create(); - if lrc != 0 || project.is_null() || gap.is_null() || list.is_null() { - release_handle(project); - release_handle(list); - release_handle(track); - release_handle(wa); - return Err(Error::Failed("gap insertion failed".into())); - } - let src = n::oaknode_block_set_length_and_media_out( - gap, - len_num as c_int, - len_den as c_int, - ); - let add_cmd = n::oaknode_command_create_add_node(project, gap); - let place_cmd = - tl::oaktimeline_place_block_command(list, tindex, gap, in_num, in_den); - release_handle(project); - release_handle(list); - release_handle(track); - if src != 0 || add_cmd.is_null() || place_cmd.is_null() { - release_handle(wa); - return Err(Error::Failed("gap insertion command failed".into())); - } - children.push(add_cmd); - children.push(place_cmd); - } - } - let disable_cmd = tl::oaktimeline_workarea_set_enabled_command(wa, 0); - release_handle(wa); - if disable_cmd.is_null() { - return Err(Error::Failed("workarea disable command failed".into())); - } - children.push(disable_cmd); - push_multi_commands(&children, "Delete In To Out") - }) -} - -/// `oakengine_sequence_trim_clips_to` — trim the nearest clip of every -/// unlocked track to `point_ts`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_trim_clips_to( - seq: *mut OakEngineSequence, - edge: c_int, - point_ts: i64, -) -> c_int { - guard_int(|| unsafe { - set_seq_error(""); - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - }; - if point_ts < 0 || edge < 0 || edge > 1 { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - set_seq_error("sequence has no valid frame rate"); - return Err(Error::State); - } - }; - let (point_num, point_den) = ts_to_rational(point_ts, tb); - let mode = if edge == 0 { - MOVEMENT_MODE_TRIM_IN - } else { - MOVEMENT_MODE_TRIM_OUT - }; - - let mut children: Vec = Vec::new(); - let mut trimmed: c_int = 0; - let mut all_count: c_int = 0; - Error::from_module(n::oaknode_sequence_get_all_track_count( - sequence, - &mut all_count, - ))?; - for i in 0..all_count { - let mut track = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_all_track_at( - sequence, i, &mut track, - ))?; - if track.is_null() { - continue; - } - let mut locked: c_int = 0; - if n::oaknode_track_get_locked(track, &mut locked) != 0 || locked != 0 { - release_handle(track); - continue; - } - // A trim (in or out) is only meaningful for the block that - // CONTAINS the point (in < point < out); the nearest-before - // queries can pick an insertion-order neighbor that ends before - // the point (which would trim to a negative length), so the - // strictly-containing lookup is used for both modes. - let mut block = CHandle::null(); - let rc = n::oaknode_track_get_block_containing_time( - track, - point_num as c_int, - point_den as c_int, - &mut block, - ); - if rc != 0 || block.is_null() { - release_handle(track); - continue; - } - let mut kind: c_int = 0; - n::oaknode_block_get_kind(block, &mut kind); - if kind == BLOCK_KIND_GAP { - release_handle(block); - release_handle(track); - continue; - } - let mut in_num: c_int = 0; - let mut in_den: c_int = 0; - let mut out_num: c_int = 0; - let mut out_den: c_int = 0; - n::oaknode_block_get_in(block, &mut in_num, &mut in_den); - n::oaknode_block_get_out(block, &mut out_num, &mut out_den); - // new_length = length - |nearest_time - point|; the in-trim - // anchors the out, the out-trim anchors the in (see - // `oakengine_clip_trim`). - let (new_num, new_den) = if mode == MOVEMENT_MODE_TRIM_IN { - rat_sub(out_num as i64, out_den as i64, point_num, point_den) - } else { - rat_sub(point_num, point_den, in_num as i64, in_den as i64) - }; - let mut old_len_num: c_int = 0; - let mut old_len_den: c_int = 0; - Error::from_module(n::oaknode_block_get_length( - block, - &mut old_len_num, - &mut old_len_den, - ))?; - // Trim the addressed block itself (`trim_cmd` anchors on the - // block handle; passing the track used to silently reject the - // trim in the module). - let cmd = trim_cmd( - block, - mode, - old_len_num, - old_len_den, - new_num as c_int, - new_den as c_int, - )?; - release_handle(block); - release_handle(track); - children.push(cmd); - trimmed += 1; - } - if trimmed == 0 { - return Ok(0); - } - push_multi_commands(&children, "Trim Clips To Point")?; - Ok(trimmed) - }) -} - -/// `oakengine_sequence_delete_empty_tracks` — remove every empty track. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_delete_empty_tracks( - seq: *mut OakEngineSequence, - track_type: c_int, -) -> c_int { - guard_int(|| unsafe { - set_seq_error(""); - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - }; - if track_type < -1 || track_type > TRACK_TYPE_SUBTITLE { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - let mut children: Vec = Vec::new(); - // (track, owning list) pairs for the live removal compensation - // (the module's `TimelineRemoveTrackCommand` redo is a no-op for the - // list structure; see below). - let mut to_remove: Vec<(CHandle, CHandle)> = Vec::new(); - let mut removed: c_int = 0; - let mut all_count: c_int = 0; - Error::from_module(n::oaknode_sequence_get_all_track_count( - sequence, - &mut all_count, - ))?; - for i in 0..all_count { - let mut track = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_all_track_at( - sequence, i, &mut track, - ))?; - if track.is_null() { - continue; - } - if track_type >= 0 { - let mut ttype: c_int = 0; - if n::oaknode_track_get_type(track, &mut ttype) != 0 || ttype != track_type { - release_handle(track); - continue; - } - } - let mut block_count: c_int = 0; - if n::oaknode_track_get_block_count(track, &mut block_count) != 0 || block_count != 0 { - release_handle(track); - continue; - } - let cmd = tl::oaktimeline_remove_track_command(track); - // Locate the owning list for the live removal (addref the track - // first so it survives the release below). - let mut ttype: c_int = 0; - if n::oaknode_track_get_type(track, &mut ttype) == 0 { - let mut list = CHandle::null(); - if n::oaknode_sequence_get_track_list(sequence, ttype, &mut list) == 0 - && !list.is_null() - { - to_remove.push((track.addref(), list)); - } - } - release_handle(track); - if cmd.is_null() { - return Err(Error::Failed("remove track command failed".into())); - } - children.push(cmd); - removed += 1; - } - if removed == 0 { - return Ok(0); - } - push_multi_commands(&children, "Delete Empty Tracks")?; - // The module's TimelineRemoveTrackCommand redo is a no-op for the - // list structure (undogeneral.rs NOTE), so the removal is applied - // live as compensation (the same documented deviation as - // `oakengine_sequence_remove_track`). - for (track, list) in &to_remove { - n::oaknode_tracklist_remove_track(*list, *track); - release_handle(*list); - } - Ok(removed) - }) -} - -/// `oakengine_sequence_marker_remove_many` — remove the markers at the -/// given times (one undoable command). -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_marker_remove_many( - seq: *mut OakEngineSequence, - times_ts: *const i64, - count: c_int, -) -> c_int { - guard_int(|| unsafe { - set_seq_error(""); - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - }; - if count < 0 || (count > 0 && times_ts.is_null()) { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - if count == 0 { - return Ok(0); - } - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - set_seq_error("sequence has no valid frame rate"); - return Err(Error::State); - } - }; - let list = seq_marker_list(sequence)?; - // Resolve all markers first so a bad time fails without side effects - // (markers are unique per time in the engine). - let mut indices: Vec = Vec::new(); - for i in 0..count { - let (num, den) = ts_to_rational(*times_ts.add(i as usize), tb); - let idx = marker_index_at(list, num, den); - if idx < 0 { - release_handle(list); - set_seq_error(&format!("no marker at time {}", *times_ts.add(i as usize))); - return Err(Error::NotFound); - } - indices.push(idx); - } - let mut children: Vec = Vec::new(); - // The module's MarkerRemoveCommand captures the list INDEX at redo - // time (not the marker identity), so removals must run in descending - // index order for earlier removals not to shift later targets. - indices.sort_unstable(); - indices.dedup(); - for idx in indices.iter().rev() { - let cmd = tl::oaktimeline_marker_remove_at_command(list, *idx); - if cmd.is_null() { - release_handle(list); - return Err(Error::Failed("remove marker command failed".into())); - } - children.push(cmd); - } - release_handle(list); - push_multi_commands(&children, "Remove Markers")?; - Ok(count) - }) -} - -/* ---- Track structure and markers ------------------------------------------ */ - -/// `oakengine_sequence_remove_track` — remove a track and its content. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_remove_track( - seq: *mut OakEngineSequence, - track_type: c_int, - track_index: c_int, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid sequence or track type"); - return Err(Error::Invalid); - } - }; - if track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE { - set_seq_error("invalid sequence or track type"); - return Err(Error::Invalid); - } - let mut list = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_track_list( - sequence, track_type, &mut list, - ))?; - let mut track = CHandle::null(); - let rc = n::oaknode_tracklist_get_track_at(list, track_index, &mut track); - release_handle(list); - if rc != 0 || track.is_null() { - set_seq_error(&format!("no track at index {}", track_index)); - return Err(Error::NotFound); - } - // The module's TimelineRemoveTrackCommand redo is a no-op for the - // list structure (undogeneral.rs NOTE), so the removal is applied - // live as compensation (documented deviation). - let cmd = tl::oaktimeline_remove_track_command(track); - if cmd.is_null() { - release_handle(track); - return Err(Error::Failed("remove track command failed".into())); - } - push_command(cmd, "Remove Track")?; - let mut list2 = CHandle::null(); - let rc = n::oaknode_sequence_get_track_list(sequence, track_type, &mut list2); - if rc == 0 && !list2.is_null() { - n::oaknode_tracklist_remove_track(list2, track); - release_handle(list2); - } - release_handle(track); - Ok(()) - }) -} - -/// `oakengine_sequence_move_track` — move a track within its list. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_move_track( - seq: *mut OakEngineSequence, - track_type: c_int, - from_index: c_int, - to_index: c_int, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid sequence or track type"); - return Err(Error::Invalid); - } - }; - if track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE { - set_seq_error("invalid sequence or track type"); - return Err(Error::Invalid); - } - let mut list = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_track_list( - sequence, track_type, &mut list, - ))?; - let mut count: c_int = 0; - Error::from_module(n::oaknode_tracklist_get_track_count(list, &mut count))?; - release_handle(list); - if from_index < 0 || from_index >= count || to_index < 0 || to_index >= count { - set_seq_error(&format!("track index out of range ({} tracks)", count)); - return Err(Error::NotFound); - } - if from_index == to_index { - return Ok(()); - } - // Stub: a true move needs undoable element-aware edge commands (the - // sequence's track inputs are array elements) plus a track re-order - // surface; the module provides neither (tracks are not connected to - // the sequence inputs at all in the module world). - set_seq_error("track moves are not supported by the module"); - Err(Error::State) - }) -} - -/// `oakengine_track_get_height` — track height in internal units. -#[no_mangle] -pub unsafe extern "C" fn oakengine_track_get_height( - seq: *const OakEngineSequence, - track_type: c_int, - track_index: c_int, - height: *mut f64, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - if height.is_null() { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - }; - if track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - let mut list = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_track_list( - sequence, track_type, &mut list, - ))?; - let mut track = CHandle::null(); - let rc = n::oaknode_tracklist_get_track_at(list, track_index, &mut track); - release_handle(list); - if rc != 0 || track.is_null() { - set_seq_error(&format!("no track at index {}", track_index)); - return Err(Error::NotFound); - } - let rc = n::oaknode_track_get_height(track, height); - release_handle(track); - Error::from_module(rc) - }) -} - -/// `oakengine_track_set_height` — set the track height (NOT undoable). -#[no_mangle] -pub unsafe extern "C" fn oakengine_track_set_height( - seq: *mut OakEngineSequence, - track_type: c_int, - track_index: c_int, - height: f64, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - }; - if height <= 0.0 || track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - let mut list = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_track_list( - sequence, track_type, &mut list, - ))?; - let mut track = CHandle::null(); - let rc = n::oaknode_tracklist_get_track_at(list, track_index, &mut track); - release_handle(list); - if rc != 0 || track.is_null() { - set_seq_error(&format!("no track at index {}", track_index)); - return Err(Error::NotFound); - } - let rc = n::oaknode_track_set_height(track, height); - release_handle(track); - Error::from_module(rc) - }) -} - -/// `oakengine_track_is_muted` — 1 if the track is muted. -#[no_mangle] -pub unsafe extern "C" fn oakengine_track_is_muted( - seq: *const OakEngineSequence, - track_type: c_int, - track_index: c_int, -) -> c_int { - guard_int(|| unsafe { - if seq.is_null() || track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE { - return Ok(0); - } - let h = unbox(seq)?; - let mut list = CHandle::null(); - if n::oaknode_sequence_get_track_list(h, track_type, &mut list) != 0 { - return Ok(0); - } - let mut track = CHandle::null(); - let rc = n::oaknode_tracklist_get_track_at(list, track_index, &mut track); - release_handle(list); - if rc != 0 || track.is_null() { - return Ok(0); - } - let mut muted: c_int = 0; - let rc = n::oaknode_track_get_muted(track, &mut muted); - release_handle(track); - if rc != 0 { - return Ok(0); - } - Ok(muted) - }) -} - -/// `oakengine_track_set_muted` — mute/unmute the track (NOT undoable). -#[no_mangle] -pub unsafe extern "C" fn oakengine_track_set_muted( - seq: *mut OakEngineSequence, - track_type: c_int, - track_index: c_int, - muted: c_int, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - }; - if track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - let mut list = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_track_list( - sequence, track_type, &mut list, - ))?; - let mut track = CHandle::null(); - let rc = n::oaknode_tracklist_get_track_at(list, track_index, &mut track); - release_handle(list); - if rc != 0 || track.is_null() { - set_seq_error(&format!("no track at index {}", track_index)); - return Err(Error::NotFound); - } - let rc = n::oaknode_track_set_muted(track, muted); - release_handle(track); - Error::from_module(rc) - }) -} - -/// `oakengine_track_is_locked` — 1 if the track is locked. -#[no_mangle] -pub unsafe extern "C" fn oakengine_track_is_locked( - seq: *const OakEngineSequence, - track_type: c_int, - track_index: c_int, -) -> c_int { - guard_int(|| unsafe { - if seq.is_null() || track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE { - return Ok(0); - } - let h = unbox(seq)?; - let mut list = CHandle::null(); - if n::oaknode_sequence_get_track_list(h, track_type, &mut list) != 0 { - return Ok(0); - } - let mut track = CHandle::null(); - let rc = n::oaknode_tracklist_get_track_at(list, track_index, &mut track); - release_handle(list); - if rc != 0 || track.is_null() { - return Ok(0); - } - let mut locked: c_int = 0; - let rc = n::oaknode_track_get_locked(track, &mut locked); - release_handle(track); - if rc != 0 { - return Ok(0); - } - Ok(locked) - }) -} - -/// `oakengine_track_set_locked` — lock/unlock the track (NOT undoable). -#[no_mangle] -pub unsafe extern "C" fn oakengine_track_set_locked( - seq: *mut OakEngineSequence, - track_type: c_int, - track_index: c_int, - locked: c_int, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - }; - if track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - let mut list = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_track_list( - sequence, track_type, &mut list, - ))?; - let mut track = CHandle::null(); - let rc = n::oaknode_tracklist_get_track_at(list, track_index, &mut track); - release_handle(list); - if rc != 0 || track.is_null() { - set_seq_error(&format!("no track at index {}", track_index)); - return Err(Error::NotFound); - } - let rc = n::oaknode_track_set_locked(track, locked); - release_handle(track); - Error::from_module(rc) - }) -} - -/// `oakengine_sequence_marker_add` — add a marker at `time_ts` (color 0). -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_marker_add( - seq: *mut OakEngineSequence, - time_ts: i64, - name: *const c_char, -) -> c_int { - unsafe { oakengine_sequence_marker_add_ex(seq, time_ts, name, 0) } -} - -/// `oakengine_sequence_marker_add_ex` — add a marker with an explicit color. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_marker_add_ex( - seq: *mut OakEngineSequence, - time_ts: i64, - name: *const c_char, - color: c_int, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid sequence"); - return Err(Error::Invalid); - } - }; - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - set_seq_error("sequence has no valid frame rate"); - return Err(Error::State); - } - }; - let (num, den) = ts_to_rational(time_ts, tb); - let list = seq_marker_list(sequence)?; - if marker_index_at(list, num, den) >= 0 { - release_handle(list); - // The engine's marker insertion asserts on duplicate times. - set_seq_error(&format!("a marker already exists at time {}", time_ts)); - return Err(Error::State); - } - let name_c = std::ffi::CString::new(read_cstr(name)) - .map_err(|_| Error::Failed("invalid name".into()))?; - let cmd = tl::oaktimeline_marker_add_command( - list, - num as c_int, - den as c_int, - num as c_int, - den as c_int, - name_c.as_ptr(), - color, - ); - release_handle(list); - if cmd.is_null() { - set_seq_error("add marker command failed"); - return Err(Error::Failed("add marker command failed".into())); - } - push_command(cmd, "Add Marker") - }) -} - -/// `oakengine_sequence_marker_remove` — remove the marker at `time_ts`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_marker_remove( - seq: *mut OakEngineSequence, - time_ts: i64, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid sequence"); - return Err(Error::Invalid); - } - }; - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - set_seq_error("sequence has no valid frame rate"); - return Err(Error::State); - } - }; - let (num, den) = ts_to_rational(time_ts, tb); - let list = seq_marker_list(sequence)?; - let idx = marker_index_at(list, num, den); - if idx < 0 { - release_handle(list); - set_seq_error(&format!("no marker at time {}", time_ts)); - return Err(Error::NotFound); - } - let cmd = tl::oaktimeline_marker_remove_at_command(list, idx); - release_handle(list); - if cmd.is_null() { - set_seq_error("remove marker command failed"); - return Err(Error::Failed("remove marker command failed".into())); - } - push_command(cmd, "Remove Marker") - }) -} - -/// `oakengine_sequence_marker_rename` — rename the marker at `time_ts`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_marker_rename( - seq: *mut OakEngineSequence, - time_ts: i64, - name: *const c_char, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid sequence"); - return Err(Error::Invalid); - } - }; - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - set_seq_error("sequence has no valid frame rate"); - return Err(Error::State); - } - }; - let (num, den) = ts_to_rational(time_ts, tb); - let list = seq_marker_list(sequence)?; - let idx = marker_index_at(list, num, den); - if idx < 0 { - release_handle(list); - set_seq_error(&format!("no marker at time {}", time_ts)); - return Err(Error::NotFound); - } - let name_c = std::ffi::CString::new(read_cstr(name)) - .map_err(|_| Error::Failed("invalid name".into()))?; - let cmd = tl::oaktimeline_marker_set_props_command(list, idx, -1, name_c.as_ptr()); - release_handle(list); - if cmd.is_null() { - set_seq_error("rename marker command failed"); - return Err(Error::Failed("rename marker command failed".into())); - } - push_command(cmd, "Rename Marker") - }) -} - -/* ---- Marker handle family -------------------------------------------------- */ - -/// `oakengine_marker_list_count` — number of markers in the list. -#[no_mangle] -pub unsafe extern "C" fn oakengine_marker_list_count(list: *const OakEngineMarkerList) -> c_int { - guard_int(|| unsafe { - if list.is_null() { - return Ok(0); - } - let h = unbox(list)?; - let mut count: c_int = 0; - Error::from_module(tl::oaktimeline_marker_count(h, &mut count))?; - Ok(count) - }) -} - -/// `oakengine_marker_list_add` — add a marker (undoable). -#[no_mangle] -pub unsafe extern "C" fn oakengine_marker_list_add( - list: *mut OakEngineMarkerList, - in_num: i64, - in_den: i64, - out_num: i64, - out_den: i64, - name: *const c_char, - color: c_int, -) -> c_int { - guard(|| unsafe { - if list.is_null() { - return Err(Error::Invalid); - } - let h = unbox(list)?; - let name_c = std::ffi::CString::new(read_cstr(name)) - .map_err(|_| Error::Failed("invalid name".into()))?; - let cmd = tl::oaktimeline_marker_add_command( - h, - in_num as c_int, - in_den as c_int, - out_num as c_int, - out_den as c_int, - name_c.as_ptr(), - color, - ); - if cmd.is_null() { - return Err(Error::Failed("add marker command failed".into())); - } - push_command(cmd, "Add Marker") - }) -} - -/// `oakengine_marker_create` — create a detached marker. -#[no_mangle] -pub unsafe extern "C" fn oakengine_marker_create( - color: c_int, - in_num: i64, - in_den: i64, - out_num: i64, - out_den: i64, - name: *const c_char, -) -> *mut OakEngineMarker { - guard_ptr(|| { - // Stub: the oaktimeline module has no standalone marker handle (all - // marker operations are list-based over the C ABI), so a detached - // marker cannot be represented. - let _ = (color, in_num, in_den, out_num, out_den, name); - Ok(std::ptr::null_mut()) - }) -} - -/// `oakengine_marker_free` — free a detached marker (NULL-safe no-op). -#[no_mangle] -pub unsafe extern "C" fn oakengine_marker_free(marker: *mut OakEngineMarker) { - guard_void(|| unsafe { - if marker.is_null() { - return; - } - // Detached markers are a stub (`oakengine_marker_create` returns - // NULL); borrowed marker boxes are freed by the caller's box - // lifecycle, so this only releases a borrowed box. - free_box::(marker); - }) -} - -/// `oakengine_marker_list_add_existing` — re-add a marker to the list. -#[no_mangle] -pub unsafe extern "C" fn oakengine_marker_list_add_existing( - list: *mut OakEngineMarkerList, - marker: *mut OakEngineMarker, -) -> c_int { - guard(|| unsafe { - if list.is_null() || marker.is_null() { - return Err(Error::Invalid); - } - let lh = unbox(list)?; - let (mlist, index) = marker_unbox(marker)?; - // Read the marker's data back through the list and add a fresh - // marker with it (the module has no marker-insert-by-handle). - let mut in_num: c_int = 0; - let mut in_den: c_int = 0; - let mut out_num: c_int = 0; - let mut out_den: c_int = 0; - let mut color: c_int = 0; - let rc = tl::oaktimeline_marker_at( - mlist, - index, - &mut in_num, - &mut in_den, - &mut out_num, - &mut out_den, - &mut color, - std::ptr::null_mut(), - 0, - ); - if rc < 0 { - return Err(Error::Module(rc)); - } - let mut name_buf = [0 as c_char; 4096]; - let rc = tl::oaktimeline_marker_at( - mlist, - index, - &mut in_num, - &mut in_den, - &mut out_num, - &mut out_den, - &mut color, - name_buf.as_mut_ptr(), - name_buf.len() as c_int, - ); - if rc < 0 { - return Err(Error::Module(rc)); - } - let name = read_cstr(name_buf.as_ptr()); - let name_c = - std::ffi::CString::new(name).map_err(|_| Error::Failed("invalid name".into()))?; - let cmd = tl::oaktimeline_marker_add_command( - lh, - in_num, - in_den, - out_num, - out_den, - name_c.as_ptr(), - color, - ); - if cmd.is_null() { - return Err(Error::Failed("add existing marker command failed".into())); - } - push_command(cmd, "Add Existing Marker") - }) -} - -/// `oakengine_marker_list_at` — marker at the given sorted index. -#[no_mangle] -pub unsafe extern "C" fn oakengine_marker_list_at( - list: *const OakEngineMarkerList, - index: c_int, -) -> *mut OakEngineMarker { - guard_ptr(|| unsafe { - if list.is_null() || index < 0 { - return Ok(std::ptr::null_mut()); - } - let h = unbox(list)?; - let mut count: c_int = 0; - if tl::oaktimeline_marker_count(h, &mut count) != 0 || index >= count { - return Ok(std::ptr::null_mut()); - } - // The module has no marker handle; the marker is represented as its - // (list, index) position. - let borrowed = h.addref(); - Ok(box_marker(borrowed, index)) - }) -} - -/// `oakengine_marker_list_marker_at_time` — marker by exact in-point. -#[no_mangle] -pub unsafe extern "C" fn oakengine_marker_list_marker_at_time( - list: *const OakEngineMarkerList, - num: i64, - den: i64, -) -> *mut OakEngineMarker { - guard_ptr(|| unsafe { - if list.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(list)?; - let idx = marker_index_at(h, num, den); - if idx < 0 { - return Ok(std::ptr::null_mut()); - } - let borrowed = h.addref(); - Ok(box_marker(borrowed, idx)) - }) -} - -/// `oakengine_marker_get_time` — the marker's time range as rational -/// seconds. -#[no_mangle] -pub unsafe extern "C" fn oakengine_marker_get_time( - self_: *const OakEngineMarker, - in_num: *mut i64, - in_den: *mut i64, - out_num: *mut i64, - out_den: *mut i64, -) -> c_int { - guard(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let (list, index) = marker_unbox(self_)?; - let mut n0: c_int = 0; - let mut d0: c_int = 0; - let mut n1: c_int = 0; - let mut d1: c_int = 0; - let mut color: c_int = 0; - let rc = tl::oaktimeline_marker_at( - list, - index, - &mut n0, - &mut d0, - &mut n1, - &mut d1, - &mut color, - std::ptr::null_mut(), - 0, - ); - if rc < 0 { - return Err(Error::Module(rc)); - } - if !in_num.is_null() { - *in_num = n0 as i64; - } - if !in_den.is_null() { - *in_den = d0 as i64; - } - if !out_num.is_null() { - *out_num = n1 as i64; - } - if !out_den.is_null() { - *out_den = d1 as i64; - } - Ok(()) - }) -} - -/// `oakengine_marker_get_name` — the marker's name (buf/size). -#[no_mangle] -pub unsafe extern "C" fn oakengine_marker_get_name( - self_: *const OakEngineMarker, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let (list, index) = marker_unbox(self_)?; - let mut n0: c_int = 0; - let mut d0: c_int = 0; - let mut n1: c_int = 0; - let mut d1: c_int = 0; - let mut color: c_int = 0; - let rc = tl::oaktimeline_marker_at( - list, index, &mut n0, &mut d0, &mut n1, &mut d1, &mut color, buf, buf_size, - ); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(string_result(rc)) - } - }) -} - -/// `oakengine_marker_get_color` — the marker's color index (-1 on NULL). -#[no_mangle] -pub unsafe extern "C" fn oakengine_marker_get_color(self_: *const OakEngineMarker) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(-1); - } - let (list, index) = marker_unbox(self_)?; - let mut n0: c_int = 0; - let mut d0: c_int = 0; - let mut n1: c_int = 0; - let mut d1: c_int = 0; - let mut color: c_int = 0; - let rc = tl::oaktimeline_marker_at( - list, - index, - &mut n0, - &mut d0, - &mut n1, - &mut d1, - &mut color, - std::ptr::null_mut(), - 0, - ); - if rc < 0 { - return Err(Error::Module(rc)); - } - Ok(color) - }) -} - -/// `oakengine_marker_has_sibling_at_time` — 1 if the list has another -/// marker at the given time. -#[no_mangle] -pub unsafe extern "C" fn oakengine_marker_has_sibling_at_time( - self_: *const OakEngineMarker, - num: i64, - den: i64, -) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let (list, index) = marker_unbox(self_)?; - // Scan the list for ANOTHER marker with the same in-point. - let mut count: c_int = 0; - if tl::oaktimeline_marker_count(list, &mut count) != 0 { - return Ok(0); - } - for i in 0..count { - if i == index { - continue; - } - let mut n0: c_int = 0; - let mut d0: c_int = 0; - let mut n1: c_int = 0; - let mut d1: c_int = 0; - let mut color: c_int = 0; - let rc = tl::oaktimeline_marker_at( - list, - i, - &mut n0, - &mut d0, - &mut n1, - &mut d1, - &mut color, - std::ptr::null_mut(), - 0, - ); - if rc >= 0 && n0 as i64 == num && d0 as i64 == den { - return Ok(1); - } - } - Ok(0) - }) -} - -/// `oakengine_marker_set_time_live` — set the marker's time range directly. -#[no_mangle] -pub unsafe extern "C" fn oakengine_marker_set_time_live( - self_: *mut OakEngineMarker, - in_num: i64, - in_den: i64, - out_num: i64, - out_den: i64, -) -> c_int { - guard(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let (list, index) = marker_unbox(self_)?; - // The module has no live marker time setter; the change is applied - // through the undoable MarkerChangeTimeCommand (documented deviation - // from the non-undoable contract). - let cmd = tl::oaktimeline_marker_set_time_command( - list, - index, - in_num as c_int, - in_den as c_int, - out_num as c_int, - out_den as c_int, - ); - if cmd.is_null() { - return Err(Error::Failed("set marker time command failed".into())); - } - push_command(cmd, "Move Marker") - }) -} - -/// `oakengine_marker_commit_time` — commit a time change as an undoable -/// command (optionally into `command`). -#[no_mangle] -pub unsafe extern "C" fn oakengine_marker_commit_time( - self_: *mut OakEngineMarker, - old_in_num: i64, - old_in_den: i64, - old_out_num: i64, - old_out_den: i64, - new_in_num: i64, - new_in_den: i64, - new_out_num: i64, - new_out_den: i64, - command: *mut c_void, -) -> c_int { - guard(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let (list, index) = marker_unbox(self_)?; - // The module's MarkerChangeTimeCommand takes only the new range and - // captures the old range from the list at redo time; the explicit - // old range is validated against the marker instead. - let mut n0: c_int = 0; - let mut d0: c_int = 0; - let mut n1: c_int = 0; - let mut d1: c_int = 0; - let mut color: c_int = 0; - let rc = tl::oaktimeline_marker_at( - list, - index, - &mut n0, - &mut d0, - &mut n1, - &mut d1, - &mut color, - std::ptr::null_mut(), - 0, - ); - if rc < 0 { - return Err(Error::Module(rc)); - } - let _ = (old_in_num, old_in_den, old_out_num, old_out_den); - let cmd = tl::oaktimeline_marker_set_time_command( - list, - index, - new_in_num as c_int, - new_in_den as c_int, - new_out_num as c_int, - new_out_den as c_int, - ); - if cmd.is_null() { - return Err(Error::Failed("set marker time command failed".into())); - } - if command.is_null() { - push_command(cmd, "Move Marker") - } else { - let parent = unbox(command.cast::())?; - let rc = u::command_multi_add_child(parent, cmd); - Error::from_module(rc) - } - }) -} - -/// `oakengine_marker_set_time_command` — create a MarkerChangeTimeCommand. -#[no_mangle] -pub unsafe extern "C" fn oakengine_marker_set_time_command( - marker: *mut OakEngineMarker, - new_time_num: i64, - new_time_den: i64, -) -> *mut c_void { - guard_ptr(|| unsafe { - if marker.is_null() || new_time_den == 0 { - return Ok(std::ptr::null_mut()); - } - let (list, index) = marker_unbox(marker)?; - // The marker's out offset is preserved (new range = new in point + - // old length). - let mut n0: c_int = 0; - let mut d0: c_int = 0; - let mut n1: c_int = 0; - let mut d1: c_int = 0; - let mut color: c_int = 0; - let rc = tl::oaktimeline_marker_at( - list, - index, - &mut n0, - &mut d0, - &mut n1, - &mut d1, - &mut color, - std::ptr::null_mut(), - 0, - ); - if rc < 0 { - return Ok(std::ptr::null_mut()); - } - // The marker's out offset is preserved: new out = new in + old length. - let (off_num, off_den) = rat_sub(n1 as i64, d1 as i64, n0 as i64, d0 as i64); - let (out_num, out_den) = rat_add(new_time_num, new_time_den, off_num, off_den); - let cmd = tl::oaktimeline_marker_set_time_command( - list, - index, - new_time_num as c_int, - new_time_den as c_int, - out_num as c_int, - out_den as c_int, - ); - if cmd.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(command_box(cmd)?.cast()) - }) -} - -/// `oakengine_marker_remove` — remove the marker from its list (undoable). -#[no_mangle] -pub unsafe extern "C" fn oakengine_marker_remove(self_: *mut OakEngineMarker) -> c_int { - guard(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let (list, index) = marker_unbox(self_)?; - let cmd = tl::oaktimeline_marker_remove_at_command(list, index); - if cmd.is_null() { - return Err(Error::Failed("remove marker command failed".into())); - } - push_command(cmd, "Remove Marker") - }) -} - -/// `oakengine_marker_set_properties` — batch-set properties on one or more -/// markers (one undoable command). -#[no_mangle] -pub unsafe extern "C" fn oakengine_marker_set_properties( - markers: *mut *mut OakEngineMarker, - count: c_int, - color: c_int, - name: *const c_char, - move_time: c_int, - new_in_num: i64, - new_in_den: i64, - new_out_num: i64, - new_out_den: i64, - command: *mut c_void, -) -> c_int { - guard(|| unsafe { - if markers.is_null() || count <= 0 { - return Err(Error::Invalid); - } - let mut children: Vec = Vec::new(); - let slice = std::slice::from_raw_parts(markers, count as usize); - for (_i, m) in slice.iter().enumerate() { - let (list, index) = match marker_unbox(*m) { - Ok(pair) => pair, - Err(_) => continue, - }; - if color >= 0 || !name.is_null() { - let name_c = if name.is_null() { - None - } else { - Some( - std::ffi::CString::new(read_cstr(name)) - .map_err(|_| Error::Failed("invalid name".into()))?, - ) - }; - let name_ptr = match &name_c { - Some(c) => c.as_ptr(), - None => std::ptr::null(), - }; - let cmd = tl::oaktimeline_marker_set_props_command(list, index, color, name_ptr); - if !cmd.is_null() { - children.push(cmd); - } - } - if move_time != 0 && count == 1 { - let cmd = tl::oaktimeline_marker_set_time_command( - list, - index, - new_in_num as c_int, - new_in_den as c_int, - new_out_num as c_int, - new_out_den as c_int, - ); - if !cmd.is_null() { - children.push(cmd); - } - } - } - if children.is_empty() { - return Ok(()); - } - if command.is_null() { - push_multi_commands(&children, "Set Marker Properties") - } else { - let parent = unbox(command.cast::())?; - for child in &children { - let rc = u::command_multi_add_child(parent, *child); - if rc != 0 { - return Err(Error::Module(rc)); - } - } - Ok(()) - } - }) -} - -/* ---- Workarea handle family ------------------------------------------------- */ - -/// `oakengine_workarea_create` — create a standalone workarea. -#[no_mangle] -pub extern "C" fn oakengine_workarea_create() -> *mut OakEngineWorkarea { - guard_ptr(|| { - let wa = unsafe { tl::oaktimeline_workarea_create() }; - if wa.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(wa)) - }) -} - -/// `oakengine_workarea_free` — free a standalone workarea (NULL-safe). -#[no_mangle] -pub unsafe extern "C" fn oakengine_workarea_free(wa: *mut OakEngineWorkarea) { - guard_void(|| unsafe { - free_box::(wa); - }) -} - -/// `oakengine_workarea_get` — read the workarea state. -#[no_mangle] -pub unsafe extern "C" fn oakengine_workarea_get( - self_: *const OakEngineWorkarea, - in_num: *mut i64, - in_den: *mut i64, - out_num: *mut i64, - out_den: *mut i64, - enabled: *mut c_int, -) -> c_int { - guard(|| unsafe { - let h = unbox(self_)?; - let mut n0: c_int = 0; - let mut d0: c_int = 0; - let mut n1: c_int = 0; - let mut d1: c_int = 0; - let mut en: c_int = 0; - let rc = tl::oaktimeline_workarea_get(h, &mut n0, &mut d0, &mut n1, &mut d1, &mut en); - Error::from_module(rc)?; - if !in_num.is_null() { - *in_num = n0 as i64; - } - if !in_den.is_null() { - *in_den = d0 as i64; - } - if !out_num.is_null() { - *out_num = n1 as i64; - } - if !out_den.is_null() { - *out_den = d1 as i64; - } - if !enabled.is_null() { - *enabled = en; - } - Ok(()) - }) -} - -/// `oakengine_workarea_set_range` — set the workarea range (non-undoable). -#[no_mangle] -pub unsafe extern "C" fn oakengine_workarea_set_range( - self_: *mut OakEngineWorkarea, - in_num: i64, - in_den: i64, - out_num: i64, - out_den: i64, -) -> c_int { - guard(|| unsafe { - let h = unbox(self_)?; - Error::from_module(tl::oaktimeline_workarea_set_range( - h, - in_num as c_int, - in_den as c_int, - out_num as c_int, - out_den as c_int, - )) - }) -} - -/// `oakengine_workarea_set_enabled` — enable/disable (non-undoable). -#[no_mangle] -pub unsafe extern "C" fn oakengine_workarea_set_enabled( - self_: *mut OakEngineWorkarea, - enabled: c_int, -) -> c_int { - guard(|| unsafe { - let h = unbox(self_)?; - Error::from_module(tl::oaktimeline_workarea_set_enabled(h, enabled)) - }) -} - -/// `oakengine_workarea_set_range_undoable` — set the range with undo -/// support. -#[no_mangle] -pub unsafe extern "C" fn oakengine_workarea_set_range_undoable( - self_: *mut OakEngineWorkarea, - in_num: i64, - in_den: i64, - out_num: i64, - out_den: i64, - old_in_num: i64, - old_in_den: i64, - old_out_num: i64, - old_out_den: i64, - command: *mut c_void, -) -> c_int { - guard(|| unsafe { - let h = unbox(self_)?; - let cmd = tl::oaktimeline_workarea_set_range_command( - h, - in_num as c_int, - in_den as c_int, - out_num as c_int, - out_den as c_int, - old_in_num as c_int, - old_in_den as c_int, - old_out_num as c_int, - old_out_den as c_int, - ); - if cmd.is_null() { - return Err(Error::Failed("workarea range command failed".into())); - } - if command.is_null() { - push_command(cmd, "Set Workarea Range") - } else { - let parent = unbox(command.cast::())?; - let rc = u::command_multi_add_child(parent, cmd); - Error::from_module(rc) - } - }) -} - -/// `oakengine_workarea_set_enabled_undoable` — enable/disable with undo -/// support. -#[no_mangle] -pub unsafe extern "C" fn oakengine_workarea_set_enabled_undoable( - self_: *mut OakEngineWorkarea, - enabled: c_int, - command: *mut c_void, -) -> c_int { - guard(|| unsafe { - let h = unbox(self_)?; - let cmd = tl::oaktimeline_workarea_set_enabled_command(h, enabled); - if cmd.is_null() { - return Err(Error::Failed("workarea enabled command failed".into())); - } - if command.is_null() { - push_command(cmd, "Set Workarea Enabled") - } else { - let parent = unbox(command.cast::())?; - let rc = u::command_multi_add_child(parent, cmd); - Error::from_module(rc) - } - }) -} - -/// `oakengine_workarea_reset_in_out` — fill the reset sentinel values. -#[no_mangle] -pub unsafe extern "C" fn oakengine_workarea_reset_in_out( - in_num: *mut i64, - in_den: *mut i64, - out_num: *mut i64, - out_den: *mut i64, -) { - guard_void(|| unsafe { - let mut n0: c_int = 0; - let mut d0: c_int = 0; - let mut n1: c_int = 0; - let mut d1: c_int = 0; - let rc = tl::oaktimeline_workarea_reset(&mut n0, &mut d0, &mut n1, &mut d1); - if rc != 0 { - return; - } - if !in_num.is_null() { - *in_num = n0 as i64; - } - if !in_den.is_null() { - *in_den = d0 as i64; - } - if !out_num.is_null() { - *out_num = n1 as i64; - } - if !out_den.is_null() { - *out_den = d1 as i64; - } - }) -} - -/* ---- Clip media range / cache / media in ----------------------------------- */ - -/// `oakengine_clip_get_media_range_rational` — clip media range as -/// rational seconds. -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_get_media_range_rational( - self_: *const OakEngineClip, - in_num: *mut i64, - in_den: *mut i64, - out_num: *mut i64, - out_den: *mut i64, -) -> c_int { - guard(|| unsafe { - let h = unbox(self_)?; - let mut mi_num: c_int = 0; - let mut mi_den: c_int = 0; - let mut len_num: c_int = 0; - let mut len_den: c_int = 0; - Error::from_module(n::oaknode_clip_get_media_in(h, &mut mi_num, &mut mi_den))?; - Error::from_module(n::oaknode_block_get_length(h, &mut len_num, &mut len_den))?; - // media_out = media_in + length (speed/reverse ignored, like the - // capi). - let (mo_num, mo_den) = - rat_add(mi_num as i64, mi_den as i64, len_num as i64, len_den as i64); - if !in_num.is_null() { - *in_num = mi_num as i64; - } - if !in_den.is_null() { - *in_den = mi_den as i64; - } - if !out_num.is_null() { - *out_num = mo_num; - } - if !out_den.is_null() { - *out_den = mo_den; - } - Ok(()) - }) -} - -/// `oakengine_clip_get_media_in_rational` — clip media in-point as rational -/// seconds. -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_get_media_in_rational( - self_: *const OakEngineClip, - num: *mut i64, - den: *mut i64, -) -> c_int { - guard(|| unsafe { - let h = unbox(self_)?; - let mut n: c_int = 0; - let mut d: c_int = 0; - Error::from_module(n::oaknode_clip_get_media_in(h, &mut n, &mut d))?; - if !num.is_null() { - *num = n as i64; - } - if !den.is_null() { - *den = d as i64; - } - Ok(()) - }) -} - -/// `oakengine_clip_set_media_in` — move the clip's media in-point (as a -/// frame timestamp; undoable when `undoable` != 0). -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_set_media_in( - self_: *mut OakEngineClip, - media_in_ts: i64, - undoable: c_int, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - let h = match unbox(self_) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid clip handle"); - return Err(Error::Invalid); - } - }; - // The clip's sequence provides the timebase. - let mut track = CHandle::null(); - let rc = n::oaknode_block_get_track(h, &mut track); - if rc != 0 || track.is_null() { - set_seq_error("clip is not on a track"); - return Err(Error::State); - } - let mut sequence = CHandle::null(); - let rc = n::oaknode_track_get_sequence(track, &mut sequence); - release_handle(track); - if rc != 0 || sequence.is_null() { - set_seq_error("clip is not on a track"); - return Err(Error::State); - } - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - release_handle(sequence); - set_seq_error("sequence has no valid frame rate"); - return Err(Error::State); - } - }; - release_handle(sequence); - let (num, den) = ts_to_rational(media_in_ts, tb); - apply_clip_media_in(h, num as c_int, den as c_int, undoable) - }) -} - -/// Shared media-in write honoring the undoable flag (the capi's -/// `BlockSetMediaInCommand` vs the direct setter). -/// -/// # Safety -/// `clip` must be a live module clip handle. -unsafe fn apply_clip_media_in( - clip: CHandle, - num: c_int, - den: c_int, - undoable: c_int, -) -> Result<()> { - unsafe { - if undoable != 0 { - let mut old_num: c_int = 0; - let mut old_den: c_int = 0; - Error::from_module(n::oaknode_clip_get_media_in( - clip, - &mut old_num, - &mut old_den, - ))?; - let data = Box::into_raw(Box::new(ClipMediaInCmdData { - clip: clip.addref(), - old_num, - old_den, - new_num: num, - new_den: den, - })); - let cmd = vtable_command( - clip_media_in_redo, - clip_media_in_undo, - clip_media_in_free, - data as *mut c_void, - )?; - push_command(cmd, "Set Media In") - } else { - Error::from_module(n::oaknode_clip_set_media_in(clip, num, den)) - } - } -} - -/// `oakengine_clip_set_media_in_rational` — move the media in-point as a -/// rational seconds value. -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_set_media_in_rational( - self_: *mut OakEngineClip, - num: i64, - den: i64, - undoable: c_int, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - let h = match unbox(self_) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid clip handle"); - return Err(Error::Invalid); - } - }; - if den == 0 { - set_seq_error("invalid rational denominator"); - return Err(Error::Invalid); - } - apply_clip_media_in(h, num as c_int, den as c_int, undoable) - }) -} - -/// `oakengine_clip_request_invalidate` — request cache invalidation -/// (NULL-safe no-op). -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_request_invalidate( - self_: *mut OakEngineClip, - in_ts: i64, - out_ts: i64, - type_: c_int, -) { - guard_void(|| { - // Stub: matches the capi's headless no-op (the module has no cache - // invalidation surface). - let _ = (self_, in_ts, out_ts, type_); - }) -} - -/// `oakengine_clip_add_cache_passthrough` — add a cache passthrough -/// dependency (NULL-safe no-op). -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_add_cache_passthrough( - dest: *mut OakEngineClip, - source: *mut OakEngineClip, -) { - guard_void(|| unsafe { - if dest.is_null() || source.is_null() { - return; - } - // The module's passthrough is itself a no-op until per-node caches - // exist; forwarded for parity. - let d = match unbox(dest) { - Ok(h) => h, - Err(_) => return, - }; - let s = match unbox(source) { - Ok(h) => h, - Err(_) => return, - }; - n::oaknode_clip_add_cache_passthrough_from(d, s); - }) -} - -/// `oakengine_clip_discard_cache` — discard the clip's cache (NULL-safe -/// no-op). -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_discard_cache(self_: *mut OakEngineClip) { - guard_void(|| { - // Stub: matches the capi's headless no-op. - let _ = self_; - }) -} - -/// `oakengine_clip_create_empty` — create a new empty ClipBlock (caller -/// owns it). -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_create_empty(label: *const c_char) -> *mut OakEngineClip { - guard_ptr(|| unsafe { - let clip = n::oaknode_block_clip_create(); - if clip.is_null() { - return Ok(std::ptr::null_mut()); - } - if !label.is_null() { - let label_c = std::ffi::CString::new(read_cstr(label)) - .map_err(|_| Error::Failed("invalid label".into()))?; - let node = n::oaknode_block_as_node(clip); - let rc = n::oaknode_node_set_label(node, label_c.as_ptr()); - release_handle(node); - if rc != 0 { - return Err(Error::Module(rc)); - } - } - Ok(box_handle::(clip)) - }) -} - -/// `oakengine_clip_request_invalidate_connected` — request invalidated cache -/// ranges from the connected node. -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_request_invalidate_connected( - self_: *mut OakEngineClip, - force_all: c_int, - in_num: i64, - in_den: i64, - out_num: i64, - out_den: i64, -) { - guard_void(|| { - // Stub: the module clip has no buffer input and no - // `request_invalidated_from_connected` surface; matches the capi's - // headless behavior. - let _ = (self_, force_all, in_num, in_den, out_num, out_den); - }) -} - -/* ---- Block functions ------------------------------------------------------- */ - -/// `oakengine_block_is_enabled` — 1 if the block is enabled. -#[no_mangle] -pub unsafe extern "C" fn oakengine_block_is_enabled(self_: *const OakEngineBlock) -> c_int { - guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let h = unbox(self_)?; - let mut enabled: c_int = 0; - Error::from_module(n::oaknode_block_get_enabled(h, &mut enabled))?; - Ok(enabled) - }) -} - -/// `oakengine_block_set_enabled` — enable or disable the block (undoable). -#[no_mangle] -pub unsafe extern "C" fn oakengine_block_set_enabled( - self_: *mut OakEngineBlock, - enabled: c_int, -) -> c_int { - guard(|| unsafe { - if self_.is_null() { - return Err(Error::Invalid); - } - let h = unbox(self_)?; - let mut old_enabled: c_int = 0; - Error::from_module(n::oaknode_block_get_enabled(h, &mut old_enabled))?; - let data = Box::into_raw(Box::new(BlockEnabledCmdData { - block: h.addref(), - old_enabled, - new_enabled: enabled, - })); - let cmd = vtable_command( - block_enabled_redo, - block_enabled_undo, - block_enabled_free, - data as *mut c_void, - )?; - push_command(cmd, "Set Block Enabled") - }) -} - -/* ---- Block traversal -------------------------------------------------------- */ - -/// `oakengine_track_block_count` — number of blocks (including gaps). -#[no_mangle] -pub unsafe extern "C" fn oakengine_track_block_count(track: *const OakEngineTrack) -> c_int { - guard_int(|| unsafe { - let h = unbox(track)?; - let mut count: c_int = 0; - Error::from_module(n::oaknode_track_get_block_count(h, &mut count))?; - Ok(count) - }) -} - -/// `oakengine_track_block_at` — block at `index` (0-based, includes gaps). -#[no_mangle] -pub unsafe extern "C" fn oakengine_track_block_at( - track: *const OakEngineTrack, - index: c_int, -) -> *mut OakEngineBlock { - guard_ptr(|| unsafe { - if track.is_null() || index < 0 { - return Ok(std::ptr::null_mut()); - } - let h = unbox(track)?; - let mut block = CHandle::null(); - let rc = n::oaknode_track_get_block_at(h, index, &mut block); - if rc != 0 || block.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(block)) - }) -} - -/// `oakengine_track_block_at_time` — block containing the timestamp. -#[no_mangle] -pub unsafe extern "C" fn oakengine_track_block_at_time( - track: *const OakEngineTrack, - timestamp: i64, -) -> *mut OakEngineBlock { - guard_ptr(|| unsafe { - if track.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(track)?; - // The track's owning sequence timebase. - let mut sequence = CHandle::null(); - let rc = n::oaknode_track_get_sequence(h, &mut sequence); - if rc != 0 || sequence.is_null() { - return Ok(std::ptr::null_mut()); - } - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - release_handle(sequence); - return Ok(std::ptr::null_mut()); - } - }; - release_handle(sequence); - let (num, den) = ts_to_rational(timestamp, tb); - let mut block = CHandle::null(); - let rc = - n::oaknode_track_get_block_containing_time(h, num as c_int, den as c_int, &mut block); - if rc != 0 || block.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(block)) - }) -} - -/// Shared "nearest block at timestamp" helpers. -/// -/// # Safety -/// `track` must be a live module track handle. -unsafe fn track_nearest_boxed( - track: *const OakEngineTrack, - timestamp: i64, - via: fn(CHandle, c_int, c_int, *mut CHandle) -> c_int, -) -> *mut OakEngineBlock { - guard_ptr(|| unsafe { - if track.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(track)?; - let mut sequence = CHandle::null(); - let rc = n::oaknode_track_get_sequence(h, &mut sequence); - if rc != 0 || sequence.is_null() { - return Ok(std::ptr::null_mut()); - } - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - release_handle(sequence); - return Ok(std::ptr::null_mut()); - } - }; - release_handle(sequence); - let (num, den) = ts_to_rational(timestamp, tb); - let mut block = CHandle::null(); - let rc = via(h, num as c_int, den as c_int, &mut block); - if rc != 0 || block.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(block)) - }) -} - -/// `oakengine_track_nearest_block_before` — nearest block whose out-point is -/// strictly before `timestamp`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_track_nearest_block_before( - track: *const OakEngineTrack, - timestamp: i64, -) -> *mut OakEngineBlock { - guard_ptr(|| unsafe { - if track.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(track)?; - let mut sequence = CHandle::null(); - let rc = n::oaknode_track_get_sequence(h, &mut sequence); - if rc != 0 || sequence.is_null() { - return Ok(std::ptr::null_mut()); - } - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - release_handle(sequence); - return Ok(std::ptr::null_mut()); - } - }; - release_handle(sequence); - let (num, den) = ts_to_rational(timestamp, tb); - let block = nearest_block_before(h, num, den); - if block.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(block)) - }) -} - -/// `oakengine_track_nearest_block_after` — nearest block whose in-point is -/// strictly after `timestamp`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_track_nearest_block_after( - track: *const OakEngineTrack, - timestamp: i64, -) -> *mut OakEngineBlock { - guard_ptr(|| unsafe { - if track.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(track)?; - let mut sequence = CHandle::null(); - let rc = n::oaknode_track_get_sequence(h, &mut sequence); - if rc != 0 || sequence.is_null() { - return Ok(std::ptr::null_mut()); - } - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - release_handle(sequence); - return Ok(std::ptr::null_mut()); - } - }; - release_handle(sequence); - let (num, den) = ts_to_rational(timestamp, tb); - // The module exposes only the after-or-at variant; the strictly-after - // result is the first block whose in-point is strictly after the - // time. - let mut best = CHandle::null(); - let mut block_count: c_int = 0; - if n::oaknode_track_get_block_count(h, &mut block_count) != 0 { - return Ok(std::ptr::null_mut()); - } - for i in 0..block_count { - let mut b = CHandle::null(); - if n::oaknode_track_get_block_at(h, i, &mut b) != 0 || b.is_null() { - continue; - } - let mut in_num: c_int = 0; - let mut in_den: c_int = 0; - let after = n::oaknode_block_get_in(b, &mut in_num, &mut in_den) == 0 - && rat_cmp(in_num as i64, in_den as i64, num, den) == std::cmp::Ordering::Greater; - if after { - best = b; - break; - } - release_handle(b); - } - if best.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(best)) - }) -} - -/// `oakengine_track_nearest_block_before_or_at` — nearest block whose -/// out-point >= `timestamp`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_track_nearest_block_before_or_at( - track: *const OakEngineTrack, - timestamp: i64, -) -> *mut OakEngineBlock { - unsafe { - track_nearest_boxed( - track, - timestamp, - n::oaknode_track_get_nearest_block_before_or_at, - ) - } -} - -/// `oakengine_track_nearest_block_after_or_at` — nearest block whose -/// in-point <= `timestamp`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_track_nearest_block_after_or_at( - track: *const OakEngineTrack, - timestamp: i64, -) -> *mut OakEngineBlock { - unsafe { - track_nearest_boxed( - track, - timestamp, - n::oaknode_track_get_nearest_block_after_or_at, - ) - } -} - -/// `oakengine_block_is_gap` — 1 if the block is a GapBlock. -#[no_mangle] -pub unsafe extern "C" fn oakengine_block_is_gap(block: *const OakEngineBlock) -> c_int { - guard_int(|| unsafe { - if block.is_null() { - return Ok(0); - } - let h = unbox(block)?; - Ok(is_gap_block(h)) - }) -} - -/// Whether the module block carries a gap behavior. -/// -/// # Safety -/// `h` must be a live module block handle. -unsafe fn is_gap_block(h: CHandle) -> c_int { - unsafe { - let mut kind: c_int = 0; - if n::oaknode_block_get_kind(h, &mut kind) != 0 { - return 0; - } - if kind == BLOCK_KIND_GAP { - 1 - } else { - 0 - } - } -} - -/// `oakengine_block_get_track` — the block's track. -#[no_mangle] -pub unsafe extern "C" fn oakengine_block_get_track( - block: *const OakEngineBlock, -) -> *mut OakEngineTrack { - guard_ptr(|| unsafe { - if block.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(block)?; - let mut track = CHandle::null(); - let rc = n::oaknode_block_get_track(h, &mut track); - if rc != 0 || track.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(track)) - }) -} - -/// `oakengine_block_next` — next block in the track's linked list. -#[no_mangle] -pub unsafe extern "C" fn oakengine_block_next(block: *const OakEngineBlock) -> *mut OakEngineBlock { - guard_ptr(|| unsafe { - if block.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(block)?; - let mut next = CHandle::null(); - let rc = n::oaknode_block_get_next(h, &mut next); - if rc != 0 || next.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(next)) - }) -} - -/// `oakengine_block_prev` — previous block in the track's linked list. -#[no_mangle] -pub unsafe extern "C" fn oakengine_block_prev(block: *const OakEngineBlock) -> *mut OakEngineBlock { - guard_ptr(|| unsafe { - if block.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(block)?; - let mut prev = CHandle::null(); - let rc = n::oaknode_block_get_previous(h, &mut prev); - if rc != 0 || prev.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(prev)) - }) -} - -/// `oakengine_block_get_range` — block range as timestamps in the owning -/// track's sequence timebase. -#[no_mangle] -pub unsafe extern "C" fn oakengine_block_get_range( - block: *const OakEngineBlock, - in_: *mut i64, - out: *mut i64, -) -> c_int { - guard(|| unsafe { - let h = unbox(block)?; - let mut track = CHandle::null(); - let rc = n::oaknode_block_get_track(h, &mut track); - if rc != 0 || track.is_null() { - return Err(Error::Invalid); - } - let mut sequence = CHandle::null(); - let rc = n::oaknode_track_get_sequence(track, &mut sequence); - release_handle(track); - if rc != 0 || sequence.is_null() { - return Err(Error::Invalid); - } - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - release_handle(sequence); - return Err(Error::Invalid); - } - }; - release_handle(sequence); - let mut in_num: c_int = 0; - let mut in_den: c_int = 0; - let mut out_num: c_int = 0; - let mut out_den: c_int = 0; - Error::from_module(n::oaknode_block_get_in(h, &mut in_num, &mut in_den))?; - Error::from_module(n::oaknode_block_get_out(h, &mut out_num, &mut out_den))?; - if !in_.is_null() { - *in_ = rational_to_ts(in_num as i64, in_den as i64, tb); - } - if !out.is_null() { - *out = rational_to_ts(out_num as i64, out_den as i64, tb); - } - Ok(()) - }) -} - -/* ---- Clip input ID getters -------------------------------------------------- */ - -/// `oakengine_clip_buffer_input_id` — `ClipBlock::k_buffer_in`. -#[no_mangle] -pub extern "C" fn oakengine_clip_buffer_input_id() -> *const c_char { - b"buffer_in\0".as_ptr() as *const c_char -} - -/// `oakengine_clip_speed_input_id` — `ClipBlock::k_speed_input`. -#[no_mangle] -pub extern "C" fn oakengine_clip_speed_input_id() -> *const c_char { - b"speed_in\0".as_ptr() as *const c_char -} - -/// `oakengine_clip_reverse_input_id` — `ClipBlock::k_reverse_input`. -#[no_mangle] -pub extern "C" fn oakengine_clip_reverse_input_id() -> *const c_char { - b"reverse_in\0".as_ptr() as *const c_char -} - -/// `oakengine_clip_maintain_audio_pitch_input_id` — the maintain-audio-pitch -/// input. -#[no_mangle] -pub extern "C" fn oakengine_clip_maintain_audio_pitch_input_id() -> *const c_char { - b"maintain_audio_pitch_in\0".as_ptr() as *const c_char -} - -/// `oakengine_clip_loop_mode_input_id` — `ClipBlock::k_loop_mode_input`. -#[no_mangle] -pub extern "C" fn oakengine_clip_loop_mode_input_id() -> *const c_char { - b"loop_in\0".as_ptr() as *const c_char -} - -/// `oakengine_clip_auto_cache_input_id` — `ClipBlock::k_auto_cache_input`. -#[no_mangle] -pub extern "C" fn oakengine_clip_auto_cache_input_id() -> *const c_char { - b"autocache_in\0".as_ptr() as *const c_char -} - -/* ---- Sequence: add_default_nodes -------------------------------------------- */ - -/// `oakengine_sequence_add_default_nodes` — add one video and one audio -/// track as ONE undoable command. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_add_default_nodes( - seq: *mut OakEngineSequence, -) -> c_int { - guard(|| unsafe { - if seq.is_null() { - return Err(Error::Invalid); - } - let h = unbox(seq)?; - let mut video_list = CHandle::null(); - let mut audio_list = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_track_list( - h, - TRACK_TYPE_VIDEO, - &mut video_list, - ))?; - Error::from_module(n::oaknode_sequence_get_track_list( - h, - TRACK_TYPE_AUDIO, - &mut audio_list, - ))?; - let vcmd = tl::oaktimeline_add_track_command(video_list); - let acmd = tl::oaktimeline_add_track_command(audio_list); - if vcmd.is_null() || acmd.is_null() { - release_handle(video_list); - release_handle(audio_list); - return Err(Error::Failed("add track command failed".into())); - } - let children = [vcmd, acmd]; - push_multi_commands(&children, "Add Default Nodes")?; - // 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(()) - }) -} - -/* ---- Sequence: add_sequence_clip -------------------------------------------- */ - -/// `oakengine_sequence_add_sequence_clip` — place a nested Sequence as a -/// clip on a track. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_add_sequence_clip( - seq: *mut OakEngineSequence, - nested: *mut OakEngineSequence, - track_type: c_int, - track_index: c_int, - in_: i64, - out: i64, - media_in: i64, -) -> *mut OakEngineClip { - guard_ptr(|| unsafe { - set_seq_error(""); - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid sequence handles"); - return Ok(std::ptr::null_mut()); - } - }; - let nested_h = match unbox(nested) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid sequence handles"); - return Ok(std::ptr::null_mut()); - } - }; - if track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE { - set_seq_error("invalid track type"); - return Ok(std::ptr::null_mut()); - } - if track_type != TRACK_TYPE_VIDEO && track_type != TRACK_TYPE_AUDIO { - set_seq_error("subtitle sequence clips not supported"); - return Ok(std::ptr::null_mut()); - } - if sequence.ctx == nested_h.ctx { - set_seq_error("a sequence cannot nest itself"); - return Ok(std::ptr::null_mut()); - } - // Cross-project check. - let p1 = seq_project_of(sequence); - let p2 = seq_project_of(nested_h); - let same = !p1.is_null() && !p2.is_null() && p1.ctx == p2.ctx; - release_handle(p1); - release_handle(p2); - if !same { - set_seq_error("sequence belongs to a different project"); - return Ok(std::ptr::null_mut()); - } - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - set_seq_error("sequence has no valid frame rate"); - return Ok(std::ptr::null_mut()); - } - }; - if out <= in_ || in_ < 0 || media_in < 0 { - set_seq_error("invalid range"); - return Ok(std::ptr::null_mut()); - } - let mut list = CHandle::null(); - if n::oaknode_sequence_get_track_list(sequence, track_type, &mut list) != 0 - || list.is_null() - { - set_seq_error("sequence has no track list for this type"); - return Ok(std::ptr::null_mut()); - } - let mut track_count: c_int = 0; - if n::oaknode_tracklist_get_track_count(list, &mut track_count) != 0 - || track_index < 0 - || track_index >= track_count - { - release_handle(list); - set_seq_error(&format!("no track at index {}", track_index)); - return Ok(std::ptr::null_mut()); - } - let (len_num, len_den) = ts_to_rational(out - in_, tb); - let (mi_num, mi_den) = ts_to_rational(media_in, tb); - let clip = n::oaknode_block_clip_create(); - if clip.is_null() { - release_handle(list); - set_seq_error("clip creation failed"); - return Ok(std::ptr::null_mut()); - } - // Set length first, then media in (set_length_and_media_in modifies - // media in internally). - Error::from_module(n::oaknode_block_set_length_and_media_in( - clip, - len_num as c_int, - len_den as c_int, - ))?; - Error::from_module(n::oaknode_clip_set_media_in( - clip, - mi_num as c_int, - mi_den as c_int, - ))?; - let project = seq_project_of(sequence); - let add_cmd = n::oaknode_command_create_add_node(project, clip); - release_handle(project); - if add_cmd.is_null() { - release_handle(list); - set_seq_error("node add command failed"); - return Ok(std::ptr::null_mut()); - } - // As with footage clips, the module clip has no `buffer_in` input, so - // the nested-sequence connection cannot be built. - let clip_node = n::oaknode_block_as_node(clip); - let mut edge = CHandle::null(); - let rc = - n::oaknode_node_connect_undoable(nested_h, clip_node, c"buffer_in".as_ptr(), &mut edge); - release_handle(clip_node); - if rc != 0 { - release_handle(add_cmd); - release_handle(list); - set_seq_error("nested-sequence connection failed: module clips have no buffer input"); - return Ok(std::ptr::null_mut()); - } - let (in_num, in_den) = ts_to_rational(in_, tb); - let place_cmd = - tl::oaktimeline_place_block_command(list, track_index, clip, in_num, in_den); - release_handle(list); - if place_cmd.is_null() { - release_handle(add_cmd); - release_handle(edge); - set_seq_error("place block command failed"); - return Ok(std::ptr::null_mut()); - } - let children = [add_cmd, edge, place_cmd]; - if let Err(e) = push_multi_commands(&children, "Add Sequence Clip") { - set_seq_error(&format!( - "failed to push add-sequence-clip command: {:?}", - e - )); - return Err(e); - } - Ok(box_handle::(clip)) - }) -} - -/* ---- Track handle queries ---------------------------------------------------- */ - -/// `oakengine_sequence_track_at` — borrowed track handle. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_track_at( - seq: *const OakEngineSequence, - track_type: c_int, - track_index: c_int, -) -> *mut OakEngineTrack { - guard_ptr(|| unsafe { - if seq.is_null() || track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE { - return Ok(std::ptr::null_mut()); - } - let h = unbox(seq)?; - let mut track = CHandle::null(); - let rc = n::oaknode_sequence_get_track_at(h, track_type, track_index, &mut track); - if rc != 0 || track.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(track)) - }) -} - -/// `oakengine_track_type` — track type, or -1 on a NULL handle. -#[no_mangle] -pub unsafe extern "C" fn oakengine_track_type(track: *const OakEngineTrack) -> c_int { - guard_int(|| unsafe { - if track.is_null() { - return Ok(-1); - } - let h = unbox(track)?; - let mut type_: c_int = 0; - Error::from_module(n::oaknode_track_get_type(h, &mut type_))?; - Ok(type_) - }) -} - -/// `oakengine_track_get_length` — track content length in frame timestamps. -#[no_mangle] -pub unsafe extern "C" fn oakengine_track_get_length( - seq: *const OakEngineSequence, - track_type: c_int, - track_index: c_int, - length: *mut i64, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - if length.is_null() { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - }; - let mut list = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_track_list( - sequence, track_type, &mut list, - ))?; - let mut track = CHandle::null(); - let rc = n::oaknode_tracklist_get_track_at(list, track_index, &mut track); - release_handle(list); - if rc != 0 || track.is_null() { - set_seq_error(&format!("no track at index {}", track_index)); - return Err(Error::NotFound); - } - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - release_handle(track); - set_seq_error("sequence has no valid frame rate"); - return Err(Error::State); - } - }; - let mut num: c_int = 0; - let mut den: c_int = 0; - let rc = n::oaknode_track_get_length(track, &mut num, &mut den); - release_handle(track); - Error::from_module(rc)?; - *length = rational_to_ts(num as i64, den as i64, tb); - Ok(()) - }) -} - -/// `oakengine_track_is_range_free` — 1 if [in_ts, out_ts) is free. -#[no_mangle] -pub unsafe extern "C" fn oakengine_track_is_range_free( - seq: *const OakEngineSequence, - track_type: c_int, - track_index: c_int, - in_ts: i64, - out_ts: i64, -) -> c_int { - guard_int(|| unsafe { - set_seq_error(""); - if seq.is_null() || in_ts < 0 || out_ts <= in_ts { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - let sequence = match unbox(seq) { - Ok(h) => h, - Err(_) => { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - }; - let mut list = CHandle::null(); - Error::from_module(n::oaknode_sequence_get_track_list( - sequence, track_type, &mut list, - ))?; - let mut track = CHandle::null(); - let rc = n::oaknode_tracklist_get_track_at(list, track_index, &mut track); - release_handle(list); - if rc != 0 || track.is_null() { - set_seq_error(&format!("no track at index {}", track_index)); - return Err(Error::NotFound); - } - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - release_handle(track); - set_seq_error("sequence has no valid frame rate"); - return Err(Error::State); - } - }; - let (in_num, in_den) = ts_to_rational(in_ts, tb); - let (out_num, out_den) = ts_to_rational(out_ts, tb); - let mut is_free: c_int = 0; - let rc = n::oaknode_track_is_range_free( - track, - in_num as c_int, - in_den as c_int, - out_num as c_int, - out_den as c_int, - &mut is_free, - ); - release_handle(track); - Error::from_module(rc)?; - Ok(is_free) - }) -} - -/// `oakengine_track_height_default` — default track height in internal -/// units. -#[no_mangle] -pub extern "C" fn oakengine_track_height_default() -> f64 { - TRACK_HEIGHT_DEFAULT -} - -/// `oakengine_track_default_height_in_pixels` — default track height in -/// pixels. -#[no_mangle] -pub extern "C" fn oakengine_track_default_height_in_pixels() -> c_int { - unsafe { n::oaknode_track_get_default_height_in_pixels() } -} - -/// `oakengine_track_height_internal_to_pixels` — convert internal height to -/// pixels. -#[no_mangle] -pub extern "C" fn oakengine_track_height_internal_to_pixels(height: f64) -> c_int { - (height * TRACK_FONT_HEIGHT).round() as c_int -} - -/// `oakengine_track_height_pixels_to_internal` — convert pixels to internal -/// height. -#[no_mangle] -pub extern "C" fn oakengine_track_height_pixels_to_internal(pixels: c_int) -> f64 { - pixels as f64 / TRACK_FONT_HEIGHT -} - -/// `oakengine_track_height_interval` — track height step interval. -#[no_mangle] -pub extern "C" fn oakengine_track_height_interval() -> f64 { - TRACK_HEIGHT_INTERVAL -} - -/// `oakengine_track_height_minimum` — minimum track height. -#[no_mangle] -pub extern "C" fn oakengine_track_height_minimum() -> f64 { - TRACK_HEIGHT_MINIMUM -} - -/* ---- Multicam helpers --------------------------------------------------------- */ - -/// `oakengine_clip_find_multicam` — find the MultiCamNode ancestor of a -/// node. -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_find_multicam( - node: *mut OakEngineNode, -) -> *mut OakEngineNode { - guard_ptr(|| { - // Stub: the C++ walks the clip's buffer-input chain looking for a - // MultiCamNode; module clips have no buffer input, so no ancestor can - // be reached. - let _ = node; - Ok(std::ptr::null_mut()) - }) -} - -/// `oakengine_multicam_switch_source` — switch the multicam source. -#[no_mangle] -pub unsafe extern "C" fn oakengine_multicam_switch_source( - multicam_node: *mut OakEngineNode, - footage_node: *mut OakEngineNode, - track_type: c_int, - track_index: c_int, - time_seconds: f64, - command: *mut c_void, -) -> c_int { - guard(|| { - if multicam_node.is_null() { - return Err(Error::Invalid); - } - // Stub: matches the capi — multicam switching requires complex undo - // commands the module does not expose; the call is accepted and - // ignored (the capi's `Q_UNUSED` body). - let _ = (footage_node, track_type, track_index, time_seconds, command); - unsafe { unbox(multicam_node)? }; - Ok(()) - }) -} - -/* ---- Track lists, block/clip/transition navigation and links ---------------- */ - -/// `oakengine_sequence_track_list` — borrowed track list handle. -#[no_mangle] -pub unsafe extern "C" fn oakengine_sequence_track_list( - seq: *mut OakEngineSequence, - track_type: c_int, -) -> *mut OakEngineTrackList { - guard_ptr(|| unsafe { - if seq.is_null() || track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE { - return Ok(std::ptr::null_mut()); - } - let h = unbox(seq)?; - let mut list = CHandle::null(); - let rc = n::oaknode_sequence_get_track_list(h, track_type, &mut list); - if rc != 0 || list.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(list)) - }) -} - -/// `oakengine_track_visible_block_at_time` — the block visible at `time_ts`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_track_visible_block_at_time( - track: *mut OakEngineTrack, - time_ts: i64, -) -> *mut OakEngineBlock { - guard_ptr(|| unsafe { - if track.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(track)?; - let mut sequence = CHandle::null(); - let rc = n::oaknode_track_get_sequence(h, &mut sequence); - if rc != 0 || sequence.is_null() { - return Ok(std::ptr::null_mut()); - } - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - release_handle(sequence); - return Ok(std::ptr::null_mut()); - } - }; - release_handle(sequence); - let (num, den) = ts_to_rational(time_ts, tb); - let mut block = CHandle::null(); - let rc = - n::oaknode_track_get_visible_block_at_time(h, num as c_int, den as c_int, &mut block); - if rc != 0 || block.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(block)) - }) -} - -/// `oakengine_node_is_block` — 1 if the node is a block of any kind. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_is_block(node: *const OakEngineNode) -> c_int { - guard_int(|| unsafe { - if node.is_null() { - return Ok(0); - } - let h = unbox(node)?; - let block = n::oaknode_block_from_node(h); - let is_block = !block.is_null(); - release_handle(block); - Ok(is_block as c_int) - }) -} - -/// `oakengine_node_is_transition` — 1 if the node is a transition block. -#[no_mangle] -pub unsafe extern "C" fn oakengine_node_is_transition(node: *const OakEngineNode) -> c_int { - guard_int(|| unsafe { - if node.is_null() { - return Ok(0); - } - let h = unbox(node)?; - // The module has exactly one transition type, identified by its type - // id (the C++ class check has no module equivalent). - Ok((node_type_id(h) == TYPE_ID_TRANSITION) as c_int) - }) -} - -/// `oakengine_block_set_length_and_media_out` — set the block's length -/// keeping its in point (undoable). -#[no_mangle] -pub unsafe extern "C" fn oakengine_block_set_length_and_media_out( - block: *mut OakEngineBlock, - length_ts: i64, -) -> c_int { - guard(|| unsafe { - set_seq_error(""); - if block.is_null() || length_ts <= 0 { - set_seq_error("invalid arguments"); - return Err(Error::Invalid); - } - let h = unbox(block)?; - let mut track = CHandle::null(); - let rc = n::oaknode_block_get_track(h, &mut track); - if rc != 0 || track.is_null() { - set_seq_error("block is not on a track"); - return Err(Error::State); - } - let mut sequence = CHandle::null(); - let rc = n::oaknode_track_get_sequence(track, &mut sequence); - release_handle(track); - if rc != 0 || sequence.is_null() { - set_seq_error("block is not on a track"); - return Err(Error::State); - } - let tb = match seq_time_base(sequence) { - Ok(tb) => tb, - Err(_) => { - release_handle(sequence); - set_seq_error("sequence has no valid frame rate"); - return Err(Error::State); - } - }; - release_handle(sequence); - let (num, den) = ts_to_rational(length_ts, tb); - let mut old_num: c_int = 0; - let mut old_den: c_int = 0; - Error::from_module(n::oaknode_block_get_length(h, &mut old_num, &mut old_den))?; - let data = Box::into_raw(Box::new(BlockResizeCmdData { - block: h.addref(), - old_num, - old_den, - new_num: num as c_int, - new_den: den as c_int, - })); - let cmd = vtable_command( - block_resize_redo, - block_resize_undo, - block_resize_free, - data as *mut c_void, - )?; - push_command(cmd, "Set Block Length") - }) -} - -/// `oakengine_block_link_count` — blocks linked to this block. -#[no_mangle] -pub unsafe extern "C" fn oakengine_block_link_count(block: *const OakEngineBlock) -> c_int { - guard_int(|| unsafe { - if block.is_null() { - return Ok(0); - } - let h = unbox(block)?; - let mut count: c_int = 0; - Error::from_module(n::oaknode_block_get_link_count(h, &mut count))?; - Ok(count) - }) -} - -/// `oakengine_block_link_at` — linked block at `index`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_block_link_at( - block: *const OakEngineBlock, - index: c_int, -) -> *mut OakEngineBlock { - guard_ptr(|| unsafe { - if block.is_null() || index < 0 { - return Ok(std::ptr::null_mut()); - } - let h = unbox(block)?; - let mut linked = CHandle::null(); - let rc = n::oaknode_block_get_link_at(h, index, &mut linked); - if rc != 0 || linked.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(linked)) - }) -} - -/// `oakengine_clip_in_transition` — the transition at the clip's in-point. -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_in_transition( - clip: *const OakEngineBlock, -) -> *mut OakEngineBlock { - guard_ptr(|| unsafe { - if clip.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(clip)?; - // The in-transition is the previous block of the track when it is a - // transition. - let mut prev = CHandle::null(); - let rc = n::oaknode_block_get_previous(h, &mut prev); - if rc != 0 || prev.is_null() { - return Ok(std::ptr::null_mut()); - } - let node = n::oaknode_block_as_node(prev); - let is_transition = node_type_id(node) == TYPE_ID_TRANSITION; - release_handle(node); - if !is_transition { - release_handle(prev); - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(prev)) - }) -} - -/// `oakengine_clip_out_transition` — the transition at the clip's out-point. -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_out_transition( - clip: *const OakEngineBlock, -) -> *mut OakEngineBlock { - guard_ptr(|| unsafe { - if clip.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(clip)?; - let mut next = CHandle::null(); - let rc = n::oaknode_block_get_next(h, &mut next); - if rc != 0 || next.is_null() { - return Ok(std::ptr::null_mut()); - } - let node = n::oaknode_block_as_node(next); - let is_transition = node_type_id(node) == TYPE_ID_TRANSITION; - release_handle(node); - if !is_transition { - release_handle(next); - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(next)) - }) -} - -/// `oakengine_transition_connected_in_block` — the block feeding the -/// transition's in side. -#[no_mangle] -pub unsafe extern "C" fn oakengine_transition_connected_in_block( - transition: *const OakEngineBlock, -) -> *mut OakEngineBlock { - guard_ptr(|| unsafe { - if transition.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(transition)?; - let mut block = CHandle::null(); - let rc = n::oaknode_transition_get_connected_in_block(h, &mut block); - if rc != 0 || block.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(block)) - }) -} - -/// `oakengine_transition_connected_out_block` — the block feeding the -/// transition's out side. -#[no_mangle] -pub unsafe extern "C" fn oakengine_transition_connected_out_block( - transition: *const OakEngineBlock, -) -> *mut OakEngineBlock { - guard_ptr(|| unsafe { - if transition.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(transition)?; - let mut block = CHandle::null(); - let rc = n::oaknode_transition_get_connected_out_block(h, &mut block); - if rc != 0 || block.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(block)) - }) -} - -/// `oakengine_clip_get_connected_viewer` — the node feeding the clip's -/// buffer input. -#[no_mangle] -pub unsafe extern "C" fn oakengine_clip_get_connected_viewer( - clip: *const OakEngineBlock, -) -> *mut OakEngineNode { - guard_ptr(|| unsafe { - if clip.is_null() { - return Ok(std::ptr::null_mut()); - } - let h = unbox(clip)?; - // Module clips have no `buffer_in` input, so there is no connected - // viewer (mirrors the C++ result for an unconnected clip). - let node = n::oaknode_block_as_node(h); - let mut out = CHandle::null(); - let rc = n::oaknode_node_input_get_connected_node(node, c"buffer_in".as_ptr(), &mut out); - release_handle(node); - if rc != 0 || out.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(out)) - }) -} diff --git a/crates/oakengine.bk/src/undo.rs b/crates/oakengine.bk/src/undo.rs deleted file mode 100644 index aea78856e..000000000 --- a/crates/oakengine.bk/src/undo.rs +++ /dev/null @@ -1,331 +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 . - -//! `engine/include/oakengine/undo.h` — the process-wide undo stack, -//! undo groups and command lifecycle over the oakundo module. -//! -//! The process-wide stack, the open undo group and the "command -//! recorded" notification now live in [`oakundo::global`] (M14 R1: sunk -//! from this facade); every export here is a thin forward that only adds -//! the engine's box/unbox, buf/size and error-code conventions. -//! -//! Command creators declared in undo.h but backed by other modules -//! (`oakengine_node_*_command`, `oakengine_track_*_command`, -//! `oakengine_block_*_command`, `oakengine_timeline_*_command`) live in -//! the corresponding family modules, mirroring the C++ capi layout. - -use std::ffi::{c_char, c_int, c_void}; - -use oakundo::undocommand::{ - command_free, command_init, command_init_multi, command_multi_add_child, - command_multi_child_count, command_redo_now, command_undo_now, -}; - -use crate::error::{Error, Result}; -use crate::handle::{box_handle, free_box, guard, guard_void, unbox, OakEngineClipboard}; - -/// Map an oakundo error onto the facade error space for the GROUP -/// functions: `State` (no group open / already open) and the allocation -/// failure map to the facade's own codes; every other oakundo code passes -/// through as a module code (the numeric value is preserved). -fn map_group_err(e: oakundo::error::Error) -> Error { - match e { - oakundo::error::Error::State => Error::State, - oakundo::error::Error::NoMem => Error::NoMem, - oakundo::error::Error::Failed(s) => Error::Failed(s), - other => Error::Module(other.code()), - } -} - -/// Map an oakundo error onto the facade error space for the STACK -/// queries: every code passes through untranslated as a module code (the -/// facade contract says module codes cross the boundary verbatim). -fn map_stack_err(e: oakundo::error::Error) -> Error { - Error::Module(e.code()) -} - -/// Push `command` onto the stack, add it to the open group, or run it -/// directly — whichever applies (module 00 analogue of the C++ capi's -/// `oakengine_undo_push_or_run`). `command_box` is consumed. -/// -/// # Safety -/// `command_box` must be a live box created by a facade command creator. -pub(crate) unsafe fn push_or_run( - command_box: *mut OakEngineClipboard, - name: *const c_char, -) -> Result<()> { - let cmd = unsafe { unbox(command_box)? }; - let label = unsafe { crate::handle::read_cstr(name) }; - let rc = unsafe { oakundo::global::push_or_run(cmd, &label) }; - // The stack/multi took (or rejected) the command value; release the box - // shell either way (the command is destroyed with the stack/multi, or - // with this shell when the push failed and nobody took it). - unsafe { free_box(command_box) }; - if rc == 0 { - Ok(()) - } else { - Err(Error::Module(rc)) - } -} - -/// `oakengine_undo_handle` — borrowed token of the global undo stack -/// (NULL never: the module creates the stack lazily). -#[no_mangle] -pub extern "C" fn oakengine_undo_handle() -> *mut c_void { - crate::handle::guard_ptr(|| Ok(oakundo::global::stack_token())) -} - -/// `oakengine_undo_push` — push `command` onto the stack and execute its -/// redo (or add it to the open group). Takes ownership of `command`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_undo_push(command: *mut c_void, name: *const c_char) -> c_int { - guard(|| unsafe { - if command.is_null() { - return Err(Error::Invalid); - } - push_or_run(command.cast::(), name) - }) -} - -/// `oakengine_undo_group_begin` — start collecting commands into a group. -#[no_mangle] -pub extern "C" fn oakengine_undo_group_begin(name: *const c_char) -> c_int { - guard(|| unsafe { - oakundo::global::group_begin(&crate::handle::read_cstr(name)).map_err(map_group_err) - }) -} - -/// `oakengine_undo_group_end` — close the group and push it as one entry. -/// An empty group is discarded (no undo entry). -#[no_mangle] -pub extern "C" fn oakengine_undo_group_end() -> c_int { - guard(|| oakundo::global::group_end().map_err(map_group_err)) -} - -/// `oakengine_undo_group_abort` — undo all executed children and discard -/// the group. -#[no_mangle] -pub extern "C" fn oakengine_undo_group_abort() -> c_int { - guard(|| oakundo::global::group_abort().map_err(map_group_err)) -} - -/// `oakengine_undo_command_redo_now` — execute the redo of `command` -/// without taking ownership. -#[no_mangle] -pub unsafe extern "C" fn oakengine_undo_command_redo_now(command: *mut c_void) -> c_int { - guard(|| unsafe { - let cmd = unbox(command.cast::())?; - Error::from_module(command_redo_now(cmd)) - }) -} - -/// `oakengine_undo_command_undo_now` — execute the undo of `command` -/// without taking ownership. -#[no_mangle] -pub unsafe extern "C" fn oakengine_undo_command_undo_now(command: *mut c_void) -> c_int { - guard(|| unsafe { - let cmd = unbox(command.cast::())?; - Error::from_module(command_undo_now(cmd)) - }) -} - -/// Engine-side callback types for app-defined undo commands -/// (`engine/include/oakengine/undo.h`). -type UndoRedoFn = unsafe extern "C" fn(userdata: *mut c_void); -type UndoFreeFn = unsafe extern "C" fn(userdata: *mut c_void); - -/// `oakengine_undo_command_create` — create an app-defined undo command -/// backed by C callbacks. Takes ownership of `userdata`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_undo_command_create( - name: *const c_char, - redo: Option, - undo: Option, - free_fn: Option, - userdata: *mut c_void, -) -> *mut c_void { - crate::handle::guard_ptr(|| unsafe { - let _ = crate::handle::read_cstr(name); - let vtable = oakundo::undocommand::OakUndoCommandVtable { - redo, - undo, - free_fn, - }; - let cmd = command_init(&vtable, userdata); - if cmd.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(cmd).cast()) - }) -} - -/// `oakengine_undo_command_create_multi` — create an empty -/// MultiUndoCommand as an opaque command pointer. -#[no_mangle] -pub extern "C" fn oakengine_undo_command_create_multi() -> *mut c_void { - crate::handle::guard_ptr(|| { - let cmd = unsafe { command_init_multi() }; - if cmd.is_null() { - return Ok(std::ptr::null_mut()); - } - Ok(box_handle::(cmd).cast()) - }) -} - -/// `oakengine_undo_command_multi_add_child` — add `child` to `multi` -/// (the multi takes one reference; `child`'s box is consumed). -#[no_mangle] -pub unsafe extern "C" fn oakengine_undo_command_multi_add_child( - multi: *mut c_void, - child: *mut c_void, -) -> c_int { - guard(|| unsafe { - if multi.is_null() || child.is_null() { - return Err(Error::Invalid); - } - let m = unbox(multi.cast::())?; - let c = unbox(child.cast::())?; - let rc = command_multi_add_child(m, c); - free_box(child.cast::()); - if rc == 0 { - Ok(()) - } else { - Err(Error::Module(rc)) - } - }) -} - -/// `oakengine_undo_command_multi_child_count` — children of `multi`. -#[no_mangle] -pub unsafe extern "C" fn oakengine_undo_command_multi_child_count(multi: *mut c_void) -> c_int { - crate::handle::guard_int(|| unsafe { - let m = unbox(multi.cast::())?; - let mut count: c_int = 0; - Error::from_module(command_multi_child_count(m, &mut count))?; - Ok(count) - }) -} - -/// `oakengine_undo_command_free` — destroy a command without pushing it. -#[no_mangle] -pub unsafe extern "C" fn oakengine_undo_command_free(command: *mut c_void) { - guard_void(|| unsafe { - free_box(command.cast::()); - }) -} - -/// `oakengine_undo_count` — total number of history rows. -#[no_mangle] -pub extern "C" fn oakengine_undo_count() -> i64 { - crate::handle::guard_i64(|| oakundo::global::count().map_err(map_stack_err)) -} - -/// `oakengine_undo_index` — current position in the history. -#[no_mangle] -pub extern "C" fn oakengine_undo_index() -> i64 { - crate::handle::guard_i64(|| oakundo::global::index().map_err(map_stack_err)) -} - -/// `oakengine_undo_command_text` — label of the row at `row` -/// (buf/size; OAKENGINE_E_NOT_FOUND for an invalid row). -#[no_mangle] -pub unsafe extern "C" fn oakengine_undo_command_text( - row: i64, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - // The oakundo getter is itself two-stage: it reports the required - // size when `buf` is NULL/too small and copies otherwise, so the - // module return value is returned verbatim (guarded against panic), - // converted to the engine's length-excluding-NUL convention. - crate::handle::guard_int(|| { - let rc = oakundo::global::command_text(row, buf, buf_size); - if rc < 0 { - Err(Error::Module(rc)) - } else { - Ok(crate::handle::string_result(rc)) - } - }) -} - -/// `oakengine_undo_command_is_done` — 1 when the row is done, 0 when -/// undone, OAKENGINE_E_NOT_FOUND for an invalid row. -#[no_mangle] -pub extern "C" fn oakengine_undo_command_is_done(row: i64) -> c_int { - crate::handle::guard_int(|| { - let mut value: c_int = 0; - oakundo::global::command_is_done(row, &mut value).map_err(map_stack_err)?; - Ok(value) - }) -} - -/// `oakengine_undo_jump` — undo/redo until the done-command count equals -/// `index`. On success the bound projects are written through (the jump -/// executed the undo/redo callbacks that mutated them) — the module's -/// command observers fire the write-through subscribers. -#[no_mangle] -pub extern "C" fn oakengine_undo_jump(index: i64) -> c_int { - guard(|| oakundo::global::jump(index).map_err(map_stack_err)) -} - -/// `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(|| oakundo::global::clear().map_err(map_stack_err)) -} - -/// `oakengine_undo_update_actions` — no-op: the QAction members were -/// removed in the de-Qt pass (see notes.md), the app builds its own -/// undo/redo actions from `oakengine_undo_can_undo/redo`. -#[no_mangle] -pub extern "C" fn oakengine_undo_update_actions() -> c_int { - crate::error::OAKENGINE_OK -} - -/// `oakengine_undo_can_undo` — 1/0. -#[no_mangle] -pub extern "C" fn oakengine_undo_can_undo() -> c_int { - crate::handle::guard_int(|| { - let mut value: c_int = 0; - oakundo::global::can_undo(&mut value).map_err(map_stack_err)?; - Ok(value) - }) -} - -/// `oakengine_undo_can_redo` — 1/0. -#[no_mangle] -pub extern "C" fn oakengine_undo_can_redo() -> c_int { - crate::handle::guard_int(|| { - let mut value: c_int = 0; - oakundo::global::can_redo(&mut value).map_err(map_stack_err)?; - Ok(value) - }) -} - -/// `oakengine_undo_undo_action` — Qt leftover: the de-Qt module world has -/// no QAction; returns NULL. The app builds its own action. -#[no_mangle] -pub extern "C" fn oakengine_undo_undo_action() -> *mut c_void { - std::ptr::null_mut() -} - -/// `oakengine_undo_redo_action` — Qt leftover; returns NULL (see -/// `oakengine_undo_undo_action`). -#[no_mangle] -pub extern "C" fn oakengine_undo_redo_action() -> *mut c_void { - std::ptr::null_mut() -} diff --git a/crates/oakengine.bk/src/worker.rs b/crates/oakengine.bk/src/worker.rs deleted file mode 100644 index 8638d7b4e..000000000 --- a/crates/oakengine.bk/src/worker.rs +++ /dev/null @@ -1,1392 +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 . - -//! 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. -//! -//! - **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`] 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 ([`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_json::Value; - -use oakrender::backend::{BackendKind, DisplayRenderer}; - -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, -}; - -/// 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)"; - -/// Why `render_frame` answers "not yet available". -const RENDER_STUB: &str = "render_frame: frame rendering is not yet available in the Rust \ - 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)"; - -/// 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). -pub fn log_error(message: &str) { - eprintln!("worker: {message}"); -} - -/// Whether `backend` requests no renderer (worker.cpp -/// `backend_requests_no_renderer()`: NULL, "" and "none"). -pub fn is_no_backend(backend: &str) -> bool { - backend.is_empty() || backend.eq_ignore_ascii_case("none") -} - -// --------------------------------------------------------------------------- -// Renderer (backend selection) -// --------------------------------------------------------------------------- - -/// A live, initialized oakrender display renderer (destroyed on drop). -pub struct Renderer { - /// 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 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 { - match Self::create_dynamic(backend) { - Ok(r) => Ok(r), - Err(first) => { - log_error(&format!( - "failed to initialize dynamic {backend} backend: {first}; falling back to direct OpenGL renderer" - )); - Self::create_opengl().map_err(|second| { - format!("{first}; direct OpenGL fallback also failed: {second}") - }) - } - } - } - - /// 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 { - 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 { - let renderer = DisplayRenderer::new(BackendKind::Gl); - Self::init_inner(renderer, "direct OpenGL") - } - - /// Initialize a freshly created renderer. - fn init_inner(mut renderer: DisplayRenderer, what: &str) -> Result { - // 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})")); - } - 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 exposes no GL context - /// version, so the startup handshake omits `gl_major`/`gl_minor`. - #[allow(dead_code)] - pub fn is_open_gl(&self) -> bool { - self.inner.is_open_gl() - } -} - -// --------------------------------------------------------------------------- -// WorkerSession -// --------------------------------------------------------------------------- - -/// The worker-side session state machine — the Rust mirror of -/// `OakWorkerSession` in worker.cpp. Holds the renderer, the attached -/// shared-memory frame-slot pools and the shutdown flag, and answers one -/// NDJSON control message at a time. -pub struct WorkerSession { - renderer: Option, - shutdown_requested: bool, - runtime_initialized: bool, - output_region: Option, - output_pool: Option, - input_region: Option, - input_pool: Option, -} - -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 crate's direct Rust API (dynamic -> OpenGL fallback). - pub fn create(backend: &str) -> Result { - let renderer = if is_no_backend(backend) { - None - } else { - Some(Renderer::create(backend)?) - }; - Ok(WorkerSession { - renderer, - shutdown_requested: false, - runtime_initialized: false, - output_region: None, - output_pool: None, - input_region: None, - input_pool: None, - }) - } - - /// 1 when the session holds a successfully initialized render backend. - pub fn has_renderer(&self) -> bool { - self.renderer.is_some() - } - - /// 1 once a shutdown control message has been received. - pub fn shutdown_requested(&self) -> bool { - self.shutdown_requested - } - - /// Load the runtime services the session depends on — the Rust analog - /// of the C++ `initialize_runtime()`. Of the C++ list (config, node - /// factory, color manager, frame/disk managers, project serializer) - /// only the color-manager default config has a Rust backing linked into - /// the worker binary; the rest are logged and skipped. Always returns - /// true (the C++ returns true unconditionally). - pub fn initialize_runtime(&mut self) -> bool { - if self.runtime_initialized { - return true; - } - log_error("runtime: loading color-manager default config"); - if let Err(e) = oakrender::color::set_up_default_config() { - log_error(&format!( - "runtime: color-manager default config failed ({e}); continuing" - )); - } - log_error( - "runtime: config / node factory / frame manager / disk manager / project \ - serializer have no Rust backing in the worker binary; skipped", - ); - self.runtime_initialized = true; - true - } - - /// The startup handshake the worker sends to its parent - /// (`worker.cpp startup_handshake()`): protocol version 1 and empty - /// shared-memory geometry — the parent creates the segments and - /// announces their geometry in its handshake reply. - /// - /// Deviation from the C++: `gl_major`/`gl_minor` are omitted because - /// the oakrender module exposes no GL context version. - pub fn startup_handshake(&self) -> Value { - HandshakeMsg { - protocol_version: PROTOCOL_VERSION, - shm_key: String::new(), - input_shm_key: String::new(), - input_slots: 0, - output_slots: 0, - slot_data_bytes: 0, - input_slot_data_bytes: 0, - } - .to_json() - } - - /// Handle one complete NDJSON control line and produce the response, if - /// any — the port of worker.cpp `handle()`. A malformed line yields an - /// error response (the loop continues), never a failure. - pub fn handle_line(&mut self, line: &str) -> Option { - let msg: Value = match serde_json::from_str::(line) { - Ok(v) if v.is_object() => v, - _ => return Some(error_message("malformed control message", None)), - }; - let typ = msg.get("type").and_then(Value::as_str).unwrap_or(""); - match typ { - TYPE_HANDSHAKE => self.handle_handshake(&msg), - TYPE_LOAD_GRAPH => self.handle_load_graph(&msg), - TYPE_RENDER_FRAME => self.handle_render_frame(&msg), - // cancel: the worker does synchronous single-frame work - // (nothing in flight), so a cancel produces no response. - TYPE_CANCEL => None, - TYPE_SHUTDOWN => { - self.shutdown_requested = true; - None - } - other => Some(error_message( - &format!("unknown message type: {other}"), - None, - )), - } - } - - /// `handshake`: validate and attach the shared-memory frame-slot pools - /// — the real port of worker.cpp `attach_output_pool()`. - fn handle_handshake(&mut self, msg: &Value) -> Option { - let hs: HandshakeMsg = match serde_json::from_value(msg.clone()) { - Ok(hs) => hs, - Err(_) => return Some(error_message("invalid handshake message", None)), - }; - if hs.protocol_version != PROTOCOL_VERSION { - return Some(error_message( - &format!("unsupported protocol version {}", hs.protocol_version), - None, - )); - } - if hs.shm_key.is_empty() || hs.output_slots <= 0 || hs.slot_data_bytes <= 0 { - return Some(error_message( - "handshake missing output shared-memory geometry", - None, - )); - } - - // A re-handshake replaces the pools (worker.cpp resets the input - // pool before attaching the output). - self.input_pool = None; - self.input_region = None; - self.output_pool = None; - self.output_region = None; - - let bytes = - FrameSlotPool::bytes_needed(hs.output_slots as u32, hs.slot_data_bytes as usize); - let mut output_region = SharedMemoryRegion::new(); - if !output_region.open(&hs.shm_key, bytes, ShmMode::Attach) { - return Some(error_message( - &format!("failed to attach shared memory: {}", output_region.error()), - None, - )); - } - // SAFETY: `output_region` is a live mapping of at least `bytes` - // bytes (checked above). - let output_pool = unsafe { FrameSlotPool::attach(output_region.data()) }; - if !output_pool.is_valid() { - return Some(error_message( - "shared memory does not contain a frame slot pool", - None, - )); - } - self.output_region = Some(output_region); - self.output_pool = Some(output_pool); - - if hs.input_slots > 0 { - if hs.input_shm_key.is_empty() || hs.input_slot_data_bytes <= 0 { - return Some(error_message( - "handshake missing input shared-memory geometry", - None, - )); - } - let input_bytes = FrameSlotPool::bytes_needed( - hs.input_slots as u32, - hs.input_slot_data_bytes as usize, - ); - let mut input_region = SharedMemoryRegion::new(); - if !input_region.open(&hs.input_shm_key, input_bytes, ShmMode::Attach) { - return Some(error_message( - &format!( - "failed to attach input shared memory: {}", - input_region.error() - ), - None, - )); - } - // SAFETY: `input_region` is a live mapping of at least - // `input_bytes` bytes (checked above). - let input_pool = unsafe { FrameSlotPool::attach(input_region.data()) }; - if !input_pool.is_valid() { - return Some(error_message( - "input shared memory does not contain a frame slot pool", - None, - )); - } - self.input_region = Some(input_region); - self.input_pool = Some(input_pool); - } - - // Success: no response (worker.cpp leaves `response` untouched). - None - } - - /// `load_graph`: the file checks are real (mirror worker.cpp - /// `load_graph()`); the deserialization is the documented stub. - fn handle_load_graph(&mut self, msg: &Value) -> Option { - let load: LoadGraphMsg = match serde_json::from_value(msg.clone()) { - Ok(l) => l, - Err(_) => return Some(error_message("invalid load_graph message", None)), - }; - match std::fs::metadata(&load.path) { - Err(_) => Some(error_message( - &format!("graph file does not exist: {}", load.path), - None, - )), - Ok(md) if md.len() == 0 => Some(error_message( - &format!("graph file is empty: {}", load.path), - None, - )), - Ok(md) => { - log_error(&format!( - "LoadGraph: loading {} ({} bytes)", - load.path, - md.len() - )); - Some(error_message(GRAPH_STUB, None)) - } - } - } - - /// `render_frame`: the graph/render pipeline has no Rust backing, so a - /// render request is answered with a clear error carrying the ticket. - fn handle_render_frame(&mut self, msg: &Value) -> Option { - let render: RenderFrameMsg = match serde_json::from_value(msg.clone()) { - Ok(r) => r, - Err(_) => return Some(error_message("invalid render_frame message", None)), - }; - Some(error_message(RENDER_STUB, Some(render.ticket))) - } -} - -// --------------------------------------------------------------------------- -// Main -// --------------------------------------------------------------------------- - -/// Full render-worker main, transport-agnostic in the backend name. -/// -/// Mirrors `oakengine_worker_main()` in worker.cpp: create the session -/// (which initializes the render backend), load the runtime config, write -/// the startup handshake, then serve the NDJSON control loop on -/// stdin/stdout until a `shutdown` message or EOF. Returns the process -/// exit code. -pub fn worker_main(backend: &str) -> i32 { - // 1. Session creation initializes the render backend through the - // oakrender crate's direct Rust API - // (oakengine_worker_session_create()). - let mut session = match WorkerSession::create(backend) { - Ok(s) => s, - Err(msg) => { - log_error(&msg); - return 1; - } - }; - if !session.has_renderer() { - // Mirrors oakengine_worker_main(): without a renderer the worker - // cannot do anything, so it exits 1. ("--backend none" lands here.) - log_error("no renderer initialized"); - return 1; - } - - // 2. Runtime services (config load etc.). - if !session.initialize_runtime() { - return 1; - } - - // 3. Startup handshake before the loop (mirrors worker.cpp main). - let handshake = session.startup_handshake(); - let stdout = io::stdout(); - let mut out = io::BufWriter::new(stdout.lock()); - if let Err(e) = write_message(&mut out, &handshake) { - log_error(&format!("failed to write startup handshake: {e}")); - return 1; - } - if let Err(e) = out.flush() { - log_error(&format!("failed to flush startup handshake: {e}")); - return 1; - } - - // 4. NDJSON control loop until a shutdown message or EOF. - let stdin = io::stdin(); - let mut reader = stdin.lock(); - let mut line = String::new(); - let mut exit_code = 0; - while !session.shutdown_requested() { - line.clear(); - match reader.read_line(&mut line) { - Ok(0) => break, // EOF: the parent closed the control pipe. - Ok(_) => {} - Err(e) => { - log_error(&format!("failed to read control line: {e}")); - break; - } - } - if line.trim().is_empty() { - // Blank lines are skipped silently (read_message() semantics). - continue; - } - if let Some(response) = session.handle_line(&line) { - if let Err(e) = write_message(&mut out, &response) { - log_error(&format!("failed to write response: {e}")); - exit_code = 1; - break; - } - if let Err(e) = out.flush() { - log_error(&format!("failed to flush response: {e}")); - exit_code = 1; - break; - } - } - } - exit_code -} - -// --------------------------------------------------------------------------- -// 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) { - 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"").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) -// --------------------------------------------------------------------------- - -/// Opaque `OakWorkerSession` handle (worker.h). A facade-owned box around a -/// [`WorkerSession`]; the C caller only ever sees the pointer. -#[repr(C)] -pub struct OakWorkerSession { - _opaque: [u8; 0], -} - -/// `oakengine_worker_session_create` — create a session for the given -/// render backend. NULL/""/"none" skips renderer creation. -#[no_mangle] -pub unsafe extern "C" fn oakengine_worker_session_create( - backend: *const c_char, -) -> *mut OakWorkerSession { - crate::handle::guard_ptr(|| unsafe { - let backend = crate::handle::read_cstr(backend); - let session = - WorkerSession::create(&backend).map_err(|e| crate::error::Error::Failed(e))?; - Ok(Box::into_raw(Box::new(session)) as *mut OakWorkerSession) - }) -} - -/// `oakengine_worker_session_free` — NULL no-op. -#[no_mangle] -pub unsafe extern "C" fn oakengine_worker_session_free(self_: *mut OakWorkerSession) { - if self_.is_null() { - return; - } - // SAFETY: `self_` was produced by `oakengine_worker_session_create` - // and is not used after this. - unsafe { drop(Box::from_raw(self_ as *mut WorkerSession)) }; -} - -/// `oakengine_worker_session_has_renderer` — 1/0. -#[no_mangle] -pub unsafe extern "C" fn oakengine_worker_session_has_renderer( - self_: *const OakWorkerSession, -) -> c_int { - crate::handle::guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - Ok((&*(self_ as *const WorkerSession)).has_renderer() as c_int) - }) -} - -/// `oakengine_worker_session_initialize_runtime` — 1 on success, 0 for a -/// NULL session. -#[no_mangle] -pub unsafe extern "C" fn oakengine_worker_session_initialize_runtime( - self_: *mut OakWorkerSession, -) -> c_int { - crate::handle::guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - let session = &mut *(self_ as *mut WorkerSession); - Ok(session.initialize_runtime() as c_int) - }) -} - -/// `oakengine_worker_session_startup_handshake` — build the startup -/// handshake (buf/size convention). Returns the required size, or -1 on -/// failure. -#[no_mangle] -pub unsafe extern "C" fn oakengine_worker_session_startup_handshake( - self_: *const OakWorkerSession, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - crate::handle::guard_int(|| unsafe { - if self_.is_null() { - return Err(crate::error::Error::Invalid); - } - let session = &*(self_ as *const WorkerSession); - let json = session.startup_handshake(); - let line = - serde_json::to_string(&json).map_err(|e| crate::error::Error::Failed(e.to_string()))?; - Ok(crate::handle::write_string(&line, buf, buf_size)) - }) -} - -/// `oakengine_worker_session_handle_json` — handle one NDJSON control line -/// and produce the response, if any (buf/size convention). Returns 0 for -/// "no response", the response length for a response, -1 on a fatal -/// handler failure. A malformed line yields an error response, not -1. -#[no_mangle] -pub unsafe extern "C" fn oakengine_worker_session_handle_json( - self_: *mut OakWorkerSession, - line: *const c_char, - response_buf: *mut c_char, - response_buf_size: c_int, -) -> c_int { - crate::handle::guard_int(|| unsafe { - if self_.is_null() || line.is_null() { - return Err(crate::error::Error::Invalid); - } - let line = crate::handle::read_cstr(line); - let session = &mut *(self_ as *mut WorkerSession); - match session.handle_line(&line) { - Some(response) => { - let json = serde_json::to_string(&response) - .map_err(|e| crate::error::Error::Failed(e.to_string()))?; - Ok(crate::handle::write_string( - &json, - response_buf, - response_buf_size, - )) - } - None => Ok(0), - } - }) -} - -/// `oakengine_worker_session_shutdown_requested` — 1/0. -#[no_mangle] -pub unsafe extern "C" fn oakengine_worker_session_shutdown_requested( - self_: *const OakWorkerSession, -) -> c_int { - crate::handle::guard_int(|| unsafe { - if self_.is_null() { - return Ok(0); - } - Ok((&*(self_ as *const WorkerSession)).shutdown_requested() as c_int) - }) -} - -/// Scan argv for `--backend ` (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 -/// process exit code. -#[no_mangle] -pub unsafe extern "C" fn oakengine_worker_main(argc: c_int, argv: *mut *mut c_char) -> c_int { - crate::handle::guard_int(|| { - let backend = parse_backend(argc, argv); - Ok(worker_main(&backend)) - }) -} - -// --------------------------------------------------------------------------- -// Unit tests -// --------------------------------------------------------------------------- - -#[cfg(test)] -mod cabi_tests { - use super::*; - use crate::ipc::{self, 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) { - 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 parse_backend_scans_argv() { - // Build a tiny fake argv the way the C runtime would: an array of - // NUL-terminated strings. - let make = |args: &[&str]| -> (Vec, Vec<*mut c_char>) { - let cstrings: Vec = args - .iter() - .map(|s| std::ffi::CString::new(*s).unwrap()) - .collect(); - let mut ptrs: Vec<*mut c_char> = - cstrings.iter().map(|c| c.as_ptr() as *mut c_char).collect(); - (cstrings, ptrs) - }; - let (_keep, mut argv) = make(&["oak-worker"]); - assert_eq!(parse_backend(1, argv.as_mut_ptr()), "opengl"); - - let (_keep, mut argv) = make(&["oak-worker", "--backend", "Vulkan"]); - assert_eq!(parse_backend(3, argv.as_mut_ptr()), "vulkan"); - - let (_keep, mut argv) = make(&["oak-worker", "--backend", "none"]); - assert_eq!(parse_backend(3, argv.as_mut_ptr()), "none"); - - // Last flag wins (the C++ loop keeps scanning). - let (_keep, mut argv) = make(&["oak-worker", "--backend", "vulkan", "--backend", "opengl"]); - assert_eq!(parse_backend(5, argv.as_mut_ptr()), "opengl"); - - // A missing value is ignored (the C++ only consumes it when - // i + 1 < size). - let (_keep, mut argv) = make(&["oak-worker", "--backend"]); - assert_eq!(parse_backend(2, argv.as_mut_ptr()), "opengl"); - } - - #[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_facade_worker_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_facade_worker_test_graph.ove"); - std::fs::write(&real, b"").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")); - } - - #[test] - fn c_abi_session_lifecycle_and_attach() { - // SAFETY: worker_session_create returns an owned handle. - let s = unsafe { oakengine_worker_session_create(c"none".as_ptr()) }; - assert!(!s.is_null()); - assert_eq!(unsafe { oakengine_worker_session_has_renderer(s) }, 0); - - // Startup handshake via the C ABI (buf/size convention). - let mut buf = [0 as c_char; 512]; - let n = unsafe { oakengine_worker_session_startup_handshake(s, buf.as_mut_ptr(), 512) }; - assert!(n > 0); - let hs: Value = serde_json::from_str( - unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } - .to_str() - .unwrap(), - ) - .unwrap(); - assert_eq!(hs["type"], "handshake"); - assert_eq!(hs["protocol_version"], 1); - - // Attach a real pool through the C ABI handle_json. - let (parent_hs, out_region, _in) = parent_side(3, 512, false); - let line = parent_hs.to_string(); - let line_c = std::ffi::CString::new(line.clone()).unwrap(); - // SAFETY: line_c is a valid C string; s is live. - let n = unsafe { - oakengine_worker_session_handle_json(s, line_c.as_ptr(), buf.as_mut_ptr(), 512) - }; - assert_eq!(n, 0, "expected no response on a successful handshake"); - // SAFETY: out_region is a live mapping of the pool; the session - // holds the attached peer view — the free ring is shared. - let parent_pool = unsafe { FrameSlotPool::attach(out_region.data()) }; - // SAFETY: `s` is a live session box; the output pool was attached - // above. - let session = unsafe { &mut *(s as *mut WorkerSession) }; - let out_pool = session.output_pool.as_ref().unwrap(); - // Drain the free ring through the worker's pool... - let mut drained = Vec::new(); - for _ in 0..3 { - let mut slot = 0; - assert!(unsafe { out_pool.acquire(&mut slot) }); - drained.push(slot); - } - drained.sort_unstable(); - assert_eq!(drained, vec![0, 1, 2]); - // ...so the parent's release lands in the shared free ring the - // worker pops from. - assert!(unsafe { parent_pool.release(1) }); - let mut slot = 0; - assert!(unsafe { out_pool.acquire(&mut slot) }); - assert_eq!(slot, 1); - unsafe { out_pool.release(slot) }; - - // Shutdown through the C ABI. - let shutdown = std::ffi::CString::new(r#"{"type":"shutdown"}"#).unwrap(); - // SAFETY: valid C string; s is live. - let n = unsafe { - oakengine_worker_session_handle_json(s, shutdown.as_ptr(), buf.as_mut_ptr(), 512) - }; - assert_eq!(n, 0); - assert_eq!(unsafe { oakengine_worker_session_shutdown_requested(s) }, 1); - - // SAFETY: s is still live (owned by this test). - unsafe { oakengine_worker_session_free(s) }; - } - - #[test] - fn ipc_layout_matches_c_abi_exports() { - // The Rust bytes_needed and the exported C ABI must agree (they are - // the same function, but this guards the linkage surface). - assert_eq!( - unsafe { crate::ipc::oakengine_ipc_framepool_bytes_needed(4, 4096) }, - FrameSlotPool::bytes_needed(4, 4096) - ); - assert_eq!( - ipc::SpscRingBuffer::bytes_needed(5), - ipc::SpscRingBuffer::HEADER_BYTES + 5 * 4 - ); - } -}