diff --git a/Cargo.lock b/Cargo.lock index 07b787cfa..049ab21b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -182,6 +182,28 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "alsa" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" +dependencies = [ + "alsa-sys", + "bitflags 2.13.1", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "android_system_properties" version = "0.1.6" @@ -191,6 +213,16 @@ dependencies = [ "libc", ] +[[package]] +name = "annotate-snippets" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" +dependencies = [ + "anstyle", + "unicode-width", +] + [[package]] name = "anstream" version = "1.0.0" @@ -330,7 +362,7 @@ dependencies = [ "chrono", "half", "hashbrown 0.17.1", - "num-complex 0.4.6", + "num-complex", "num-integer", "num-traits", ] @@ -465,7 +497,7 @@ version = "0.38.0+1.3.281" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" dependencies = [ - "libloading", + "libloading 0.8.9", ] [[package]] @@ -486,6 +518,19 @@ dependencies = [ "zbus", ] +[[package]] +name = "asio-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0dd5af36bd2d2aa75674a9b75846bbfc7e208b6f8c6ed6944442f80bf3f4d2" +dependencies = [ + "bindgen 0.72.1", + "cc", + "num-derive", + "num-traits", + "walkdir", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -761,7 +806,7 @@ dependencies = [ "arg_enum_proc_macro", "arrayvec", "log", - "num-rational 0.4.2", + "num-rational", "num-traits", "pastey", "rayon", @@ -780,7 +825,7 @@ dependencies = [ "arrayvec", "log", "nom 8.0.0", - "num-rational 0.4.2", + "num-rational", "v_frame", ] @@ -854,10 +899,13 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ + "annotate-snippets", "bitflags 2.13.1", "cexpr", "clang-sys", "itertools 0.13.0", + "log", + "prettyplease", "proc-macro2", "quote", "regex", @@ -1181,6 +1229,16 @@ dependencies = [ "nom 7.1.3", ] +[[package]] +name = "cfg-expr" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -1246,7 +1304,7 @@ checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" dependencies = [ "glob", "libc", - "libloading", + "libloading 0.8.9", ] [[package]] @@ -1398,6 +1456,16 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "command-fds" version = "0.3.3" @@ -1488,6 +1556,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" + [[package]] name = "core-foundation" version = "0.9.4" @@ -1623,6 +1697,20 @@ dependencies = [ "libm", ] +[[package]] +name = "coreaudio-rs" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58" +dependencies = [ + "bitflags 2.13.1", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", +] + [[package]] name = "cosmic-text" version = "0.19.0" @@ -1647,6 +1735,41 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpal" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f77b11176c37874be37e8d691c946e31b2b8c357abce9526f6a99eb469e1028" +dependencies = [ + "alsa", + "asio-sys", + "block2 0.6.2", + "coreaudio-rs", + "dasp_sample", + "futures", + "jack", + "jni", + "js-sys", + "libc", + "mach2 0.6.0", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "objc2 0.6.4", + "objc2-audio-toolbox", + "objc2-avf-audio", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "pipewire", + "pulseaudio", + "web-sys", + "windows 0.62.2", + "windows-core 0.62.2", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1801,6 +1924,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + [[package]] name = "data-url" version = "0.3.2" @@ -1956,7 +2085,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" dependencies = [ - "libloading", + "libloading 0.8.9", ] [[package]] @@ -2042,6 +2171,17 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" +[[package]] +name = "enum-primitive-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba7795da175654fe16979af73f81f26a8ea27638d8d9823d317016888a63dc4c" +dependencies = [ + "num-traits", + "quote", + "syn 2.0.119", +] + [[package]] name = "enumflags2" version = "0.7.12" @@ -2169,7 +2309,7 @@ dependencies = [ "half", "lebe", "miniz_oxide", - "num-complex 0.4.6", + "num-complex", "pulp", "rayon-core", "smallvec", @@ -2823,7 +2963,7 @@ dependencies = [ "itertools 0.14.0", "log", "lyon", - "mach2", + "mach2 0.5.0", "metal 0.33.0", "num_cpus", "objc", @@ -2975,7 +3115,7 @@ dependencies = [ "itertools 0.14.0", "libc", "log", - "mach2", + "mach2 0.5.0", "metal 0.33.0", "objc", "objc2-app-kit 0.3.2", @@ -3169,7 +3309,7 @@ dependencies = [ "itertools 0.14.0", "libc", "log", - "mach2", + "mach2 0.5.0", "nix 0.29.0", "percent-encoding", "regex", @@ -3674,6 +3814,63 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jack" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7811b07bcac5dafabf814ab52c4b0ca9b7948aa1e279f572f03aa6544d47d27" +dependencies = [ + "bitflags 2.13.1", + "jack-sys", + "lazy_static", + "libc", + "log", +] + +[[package]] +name = "jack-sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6013b7619b95a22b576dfb43296faa4ecbe40abbdb97dfd22ead520775fc86ab" +dependencies = [ + "bitflags 1.3.2", + "lazy_static", + "libc", + "libloading 0.7.4", + "log", + "pkg-config", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.20", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + [[package]] name = "jni-sys" version = "0.3.1" @@ -3730,7 +3927,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" dependencies = [ "libc", - "libloading", + "libloading 0.8.9", "pkg-config", ] @@ -3851,6 +4048,16 @@ dependencies = [ "cc", ] +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "libloading" version = "0.8.9" @@ -3876,6 +4083,33 @@ dependencies = [ "libc", ] +[[package]] +name = "libspa" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2909f3be29d674e7f10604aff18d1bbe1bb03c4cd61c8a8ba19c0b1d162f7d4e" +dependencies = [ + "bitflags 2.13.1", + "cc", + "cookie-factory", + "libc", + "libspa-sys", + "nom 8.0.0", + "rustix 1.1.4", + "system-deps", +] + +[[package]] +name = "libspa-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ad52764fca54818486f3cf75afec844d1f1a1568c24dcee25d41b1ab007dda" +dependencies = [ + "bindgen 0.72.1", + "cc", + "system-deps", +] + [[package]] name = "libsqlite3-sys" version = "0.37.0" @@ -4035,6 +4269,12 @@ dependencies = [ "libc", ] +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + [[package]] name = "malloc_buf" version = "0.0.6" @@ -4167,6 +4407,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] @@ -4241,6 +4482,26 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.1", + "jni-sys 0.3.1", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "ndk-sys" version = "0.5.0+25.2.9519653" @@ -4333,19 +4594,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "num" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" -dependencies = [ - "num-complex 0.2.4", - "num-integer", - "num-iter", - "num-rational 0.2.4", - "num-traits", -] - [[package]] name = "num" version = "0.4.3" @@ -4353,10 +4601,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ "num-bigint", - "num-complex 0.4.6", + "num-complex", "num-integer", "num-iter", - "num-rational 0.4.2", + "num-rational", "num-traits", ] @@ -4387,16 +4635,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "num-complex" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" -dependencies = [ - "autocfg", - "num-traits", -] - [[package]] name = "num-complex" version = "0.4.6" @@ -4443,17 +4681,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-rational" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-rational" version = "0.4.2" @@ -4485,6 +4712,28 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "oak" version = "0.5.0" @@ -4503,16 +4752,12 @@ name = "oak-cli" version = "0.1.0" dependencies = [ "clap", - "oakengine", ] [[package]] name = "oak-worker" version = "0.1.0" dependencies = [ - "clap", - "oakengine", - "oakrender", "serde", "serde_json", ] @@ -4521,12 +4766,12 @@ dependencies = [ name = "oakaudio" version = "0.1.0" dependencies = [ + "cpal", "ffmpeg-next", "oakcodec", "oakcommon", "oakcore-rs", "oakffmpeg-link", - "portaudio", "thiserror 2.0.20", ] @@ -4535,6 +4780,7 @@ name = "oakcodec" version = "0.1.0" dependencies = [ "ffmpeg-next", + "oakcommon", "oakcore-rs", "oakffmpeg-link", "thiserror 2.0.20", @@ -4631,7 +4877,6 @@ dependencies = [ name = "oakstorage" version = "0.1.0" dependencies = [ - "oakcodec", "oakcore-rs", "oaknode", "oakotio", @@ -4652,7 +4897,6 @@ dependencies = [ "oaknode", "oakotio", "oakrender", - "oaktimeline", "oakundo", "thiserror 2.0.20", ] @@ -4748,6 +4992,55 @@ dependencies = [ "objc2-foundation 0.3.2", ] +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" +dependencies = [ + "bitflags 2.13.1", + "libc", + "objc2 0.6.4", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-avf-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2 0.6.4", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", +] + [[package]] name = "objc2-core-data" version = "0.2.2" @@ -4767,7 +5060,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.13.1", + "block2 0.6.2", "dispatch2", + "libc", "objc2 0.6.4", ] @@ -4808,6 +5103,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.13.1", + "block2 0.6.2", + "libc", "objc2 0.6.4", "objc2-core-foundation", ] @@ -4952,7 +5249,7 @@ dependencies = [ "hkdf 0.12.4", "hmac 0.12.1", "md-5 0.10.6", - "num 0.4.3", + "num", "num-bigint-dig", "pbkdf2", "serde", @@ -5204,6 +5501,31 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pipewire" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8585aba8a52ad74ccc633b8e293c1dc4277976bd5d510b925533f34fd6685f38" +dependencies = [ + "bitflags 2.13.1", + "libc", + "libspa", + "libspa-sys", + "pipewire-sys", + "rustix 1.1.4", +] + +[[package]] +name = "pipewire-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2089f245b548723e60325773c27f586b7a2372c79ea941b246cd0d654706adc" +dependencies = [ + "bindgen 0.72.1", + "libspa-sys", + "system-deps", +] + [[package]] name = "pkg-config" version = "0.3.33" @@ -5287,27 +5609,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "portaudio" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d64242ed9c0236ba1470729856c0e69363ecd2339940230161383f1238804f6" -dependencies = [ - "bitflags 2.13.1", - "libc", - "num 0.2.1", - "portaudio-sys2", -] - -[[package]] -name = "portaudio-sys2" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86a36ee35d719327fee8827321bd41f3ee7a2cc2335aa28f9168ffed3e82c6b8" -dependencies = [ - "pkg-config", -] - [[package]] name = "postage" version = "0.5.0" @@ -5486,7 +5787,7 @@ dependencies = [ "bytemuck", "cfg-if", "libm", - "num-complex 0.4.6", + "num-complex", "paste", "pulp-wasm-simd-flag", "raw-cpuid", @@ -5500,6 +5801,22 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" +[[package]] +name = "pulseaudio" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d70623bd7967a9ca4c2ae0e807fc380b291f98480fc037042305ec643a4d3373" +dependencies = [ + "bitflags 2.13.1", + "byteorder", + "enum-primitive-derive", + "futures", + "log", + "mio", + "num-traits", + "thiserror 1.0.69", +] + [[package]] name = "pxfm" version = "0.1.30" @@ -6513,6 +6830,16 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simd_helpers" version = "0.1.0" @@ -7145,6 +7472,19 @@ dependencies = [ "windows 0.57.0", ] +[[package]] +name = "system-deps" +version = "7.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 1.1.4+spec-1.1.0", + "version-compare", +] + [[package]] name = "taffy" version = "0.10.1" @@ -7181,6 +7521,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + [[package]] name = "tempfile" version = "3.27.0" @@ -7761,6 +8107,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + [[package]] name = "version_check" version = "0.9.5" @@ -8208,7 +8560,7 @@ dependencies = [ "js-sys", "khronos-egl", "libc", - "libloading", + "libloading 0.8.9", "log", "metal 0.31.0", "naga 25.0.1", @@ -8253,7 +8605,7 @@ dependencies = [ "js-sys", "khronos-egl", "libc", - "libloading", + "libloading 0.8.9", "log", "naga 29.0.4", "ndk-sys 0.6.0+11769913", diff --git a/crates/oakaudio/Cargo.toml b/crates/oakaudio/Cargo.toml index 719b7ede4..2ba338a47 100644 --- a/crates/oakaudio/Cargo.toml +++ b/crates/oakaudio/Cargo.toml @@ -18,8 +18,7 @@ oakcodec = { path = "../oakcodec" } # crate calls ffmpeg-next directly (same as oakcodec). The oakffmpeg-link # dependency above emits the transitive link flags of the static FFmpeg. ffmpeg-next = "9" -# Real audio output (M12 P1): the preview playback stream. The PortAudio -# C library must be installed (macOS: brew install portaudio). -portaudio = "0.8" + +cpal = {version = "0.18", features = ["pulseaudio", "pipewire", "jack", "asio"]} # `std::error::Error` impls for the crate-internal error enum. thiserror = "2" diff --git a/crates/oakaudio/src/bridge/codec.rs b/crates/oakaudio/src/bridge/codec.rs deleted file mode 100644 index 3e0d596fc..000000000 --- a/crates/oakaudio/src/bridge/codec.rs +++ /dev/null @@ -1,135 +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 . - -//! oakcodec C ABI calls (audio encoding + decoding for the manager and -//! waveform extraction) — now direct Rust calls into the oakcodec crate -//! (single-lib unification, see `docs/zh/plans/riir/single-lib.md`). -//! -//! The previous `*mut c_void` handle convention drifted from the real -//! codec C ABI (which uses the shared [`CHandle`]); the wrappers below -//! match the oakcodec ffi signatures exactly. - -use std::ffi::{c_char, c_int}; - -use crate::handle::CHandle; - -/// `oakcodec_encoding_params` — single-lib unification: aliases the -/// oakcodec crate's POD (identical layout; only the audio fields are -/// consumed by oakaudio). -pub type EncodingParams = oakcodec::ffi::encoder::oakcodec_encoding_params; - -/// `oakcodec_audio_stream_info` — audio stream metadata from probing. -/// Single-lib unification: aliases the oakcodec crate's POD. -pub type AudioStreamInfo = oakcodec::decoder::OakCodecAudioStreamInfo; - -/// `oakcodec_encoder_init` — create an encoder for `params`. -pub unsafe fn oakcodec_encoder_init(params: *const EncodingParams) -> CHandle { - unsafe { oakcodec::ffi::encoder::oakcodec_encoder_init(params) } -} - -/// `oakcodec_encoder_free`. -pub unsafe fn oakcodec_encoder_free(encoder: *mut CHandle) { - unsafe { oakcodec::ffi::encoder::oakcodec_encoder_free(encoder) } -} - -/// `oakcodec_encoder_open`. -pub unsafe fn oakcodec_encoder_open(encoder: CHandle) -> c_int { - unsafe { oakcodec::ffi::encoder::oakcodec_encoder_open(encoder) } -} - -/// `oakcodec_encoder_write_audio` — feed interleaved `f32` audio. -pub unsafe fn oakcodec_encoder_write_audio( - encoder: CHandle, - samples: *const f32, - frame_count: c_int, -) -> c_int { - unsafe { oakcodec::ffi::encoder::oakcodec_encoder_write_audio(encoder, samples, frame_count) } -} - -/// `oakcodec_encoder_flush`. -pub unsafe fn oakcodec_encoder_flush(encoder: CHandle) -> c_int { - unsafe { oakcodec::ffi::encoder::oakcodec_encoder_flush(encoder) } -} - -/// `oakcodec_encoder_last_error` — copy the last error string into `buf`. -pub unsafe fn oakcodec_encoder_last_error( - encoder: CHandle, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oakcodec::ffi::encoder::oakcodec_encoder_last_error(encoder, buf, buf_size) } -} - -/// `oakcodec_decoder_probe` — create a probe handle for a file. -pub unsafe fn oakcodec_decoder_probe(filename: *const c_char) -> CHandle { - unsafe { oakcodec::ffi::decoder::oakcodec_decoder_probe(filename) } -} - -/// `oakcodec_decoder_free`. -pub unsafe fn oakcodec_decoder_free(decoder: *mut CHandle) { - unsafe { oakcodec::ffi::decoder::oakcodec_decoder_free(decoder) } -} - -/// `oakcodec_decoder_probe_audio_stream_count`. -pub unsafe fn oakcodec_decoder_probe_audio_stream_count(probe: CHandle) -> c_int { - unsafe { oakcodec::ffi::decoder::oakcodec_decoder_probe_audio_stream_count(probe) } -} - -/// `oakcodec_decoder_probe_get_audio_stream` — copy audio stream info. -pub unsafe fn oakcodec_decoder_probe_get_audio_stream( - probe: CHandle, - index: c_int, - out: *mut AudioStreamInfo, -) -> c_int { - unsafe { oakcodec::ffi::decoder::oakcodec_decoder_probe_get_audio_stream(probe, index, out) } -} - -/// `oakcodec_decoder_open` — open stream `stream_index` for decoding. -pub unsafe fn oakcodec_decoder_open( - decoder: CHandle, - filename: *const c_char, - stream_index: c_int, -) -> c_int { - unsafe { oakcodec::ffi::decoder::oakcodec_decoder_open(decoder, filename, stream_index) } -} - -/// `oakcodec_decoder_decode_audio` — decode/convert frames into `buf`. -#[allow(clippy::too_many_arguments)] -pub unsafe fn oakcodec_decoder_decode_audio( - decoder: CHandle, - in_num: c_int, - in_den: c_int, - out_num: c_int, - out_den: c_int, - sample_rate: c_int, - channel_layout: u64, - buf: *mut f32, - buf_frames: c_int, -) -> c_int { - unsafe { - oakcodec::ffi::decoder::oakcodec_decoder_decode_audio( - decoder, - in_num, - in_den, - out_num, - out_den, - sample_rate, - channel_layout, - buf, - buf_frames, - ) - } -} diff --git a/crates/oakaudio/src/bridge/common.rs b/crates/oakaudio/src/bridge/common.rs deleted file mode 100644 index 14e18bee0..000000000 --- a/crates/oakaudio/src/bridge/common.rs +++ /dev/null @@ -1,53 +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 . - -//! oakcommon C ABI calls (config access + ffmpeg format conversion) — -//! now direct Rust calls into the oakcommon crate (single-lib -//! unification, see `docs/zh/plans/riir/single-lib.md`). - -use std::ffi::{c_char, c_int}; - -/// `oakcommon_config_get` — copy a config string value into `buf` -/// (two-stage; returns the required size including NUL). -pub unsafe fn oakcommon_config_get( - group: *const c_char, - key: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oakcommon::ffi::config::oakcommon_config_get(group, key, buf, buf_size) } -} - -/// `oakcommon_config_get_int` — read an integer config value with a -/// default. -pub unsafe fn oakcommon_config_get_int( - group: *const c_char, - key: *const c_char, - default: c_int, -) -> c_int { - unsafe { oakcommon::ffi::config::oakcommon_config_get_int(group, key, default) } -} - -/// `oakcommon_ffmpegutils_get_ffmpeg_sample_format` — map an ffmpeg -/// sample format enum to the oak core format, or the reverse. -pub unsafe fn oakcommon_ffmpegutils_get_ffmpeg_sample_format( - smp_fmt: c_int, - out: *mut c_int, -) -> c_int { - unsafe { - oakcommon::ffi::ffmpegutils::oakcommon_ffmpegutils_get_ffmpeg_sample_format(smp_fmt, out) - } -} diff --git a/crates/oakaudio/src/bridge/mod.rs b/crates/oakaudio/src/bridge/mod.rs deleted file mode 100644 index de5389c9d..000000000 --- a/crates/oakaudio/src/bridge/mod.rs +++ /dev/null @@ -1,21 +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 . - -//! C ABI imports from other oak modules (signatures mirror the public -//! headers verbatim; resolved at link time). - -pub mod codec; -pub mod common; diff --git a/crates/oakaudio/src/config.rs b/crates/oakaudio/src/config.rs index 4be8313b8..d2f9d7799 100644 --- a/crates/oakaudio/src/config.rs +++ b/crates/oakaudio/src/config.rs @@ -17,20 +17,16 @@ //! The `audio_config` namespace from `src/audio/src/configbridge.*`: //! audio-specific configuration read through the oakcommon C ABI. +use std::error::Error; use std::ffi::CString; - +use std::str::FromStr; +use oakcommon::configstore::*; /// PortAudio output buffer size in frames; 0 = let PortAudio choose. /// /// `// CPP-PARITY: src/audio/src/configbridge.cpp:30` /// (`audio_config::output_buffer_size`). pub fn output_buffer_size() -> i32 { - unsafe { - crate::bridge::common::oakcommon_config_get_int( - std::ptr::null(), - c"AudioOutputBufferSize".as_ptr(), - 0, - ) - } + ConfigStore::instance().get_int(None, "AudioOutputBufferSize", 0) } /// Name of the configured audio device for `is_output_device` @@ -39,35 +35,19 @@ pub fn output_buffer_size() -> i32 { /// `// CPP-PARITY: src/audio/src/configbridge.cpp:36` /// (`audio_config::device_name`): two-stage size query; `size <= 1` /// (absent or empty string) yields the empty string. -pub fn device_name(is_output_device: bool) -> CString { +pub fn device_name(is_output_device: bool) -> Result> { let key = if is_output_device { - c"AudioOutput" + "AudioOutput" } else { - c"AudioInput" + "AudioInput" }; - unsafe { - let size = crate::bridge::common::oakcommon_config_get( - std::ptr::null(), - key.as_ptr(), - std::ptr::null_mut(), - 0, - ); - if size <= 1 { - // Absent (OAKCOMMON_E_NOT_FOUND) or empty - return CString::default(); - } - let mut buf = vec![0u8; size as usize]; - if crate::bridge::common::oakcommon_config_get( - std::ptr::null(), - key.as_ptr(), - buf.as_mut_ptr() as *mut std::ffi::c_char, - size, - ) < 0 - { - return CString::default(); - } - // The buffer is NUL-terminated by the callee. - CString::from_vec_with_nul(buf) - .unwrap_or_else(|e| CString::new(e.into_bytes()).unwrap_or_default()) + let store = ConfigStore::instance(); + let size = i32::from_str(store.get(None, key)?.as_str())?; + if size <= 1 { + // Absent (OAKCOMMON_E_NOT_FOUND) or empty + return Err(Box::new(crate::error::Error::NotFound)); } + let mut buf = vec![0u8; size as usize]; + let name = store.get(None, key)?; + Ok(name) } diff --git a/crates/oakaudio/src/error.rs b/crates/oakaudio/src/error.rs index fccc727c3..8df48f254 100644 --- a/crates/oakaudio/src/error.rs +++ b/crates/oakaudio/src/error.rs @@ -33,7 +33,7 @@ pub const OAKAUDIO_E_NOT_FOUND: i32 = -60004; pub const OAKAUDIO_E_NOMEM: i32 = -60005; /// Crate-internal result type; the FFI layer maps it to the codes. -pub type Result = std::result::Result; +pub type Result = std::result::Result>; /// Crate-internal error. #[derive(Debug, thiserror::Error)] diff --git a/crates/oakaudio/src/ffi.rs b/crates/oakaudio/src/ffi.rs deleted file mode 100644 index 992fb6f3e..000000000 --- a/crates/oakaudio/src/ffi.rs +++ /dev/null @@ -1,1248 +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 . - -//! C ABI export layer: implements `include/audio/*.h` verbatim. -//! -//! Organization: one submodule per public header. The authoritative -//! function list is the header itself; each submodule below carries a -//! complete inventory comment plus the export stubs. Bodies only -//! unwrap handles, call safe Rust, and map results through -//! [`crate::handle::guard*`]. - -use std::ffi::{c_char, c_double, c_float, c_int, CStr}; - -use oakcore_rs::Rational; - -use crate::bridge::codec::EncodingParams; -use crate::error::{Error, OAKAUDIO_E_INVALID}; -use crate::handle::{guard, guard_handle, guard_int, guard_void, invalid_if, write_error, CHandle}; -use crate::params::{AudioParams, SampleFormat}; - -/// One decoded extraction, cached between the two-stage contract's size -/// query and its data query (see `oakaudio_waveform_extract`). -thread_local! { - static WAVEFORM_EXTRACT_CACHE: std::cell::RefCell< - Option<((String, i32, i32), crate::waveform::ExtractOutcome)>, - > = const { std::cell::RefCell::new(None) }; -} -use crate::waveform::{AudioVisualWaveform, SamplePerChannel}; - -/// Map a C sample-format int to the native [`SampleFormat`]; out-of-range -/// values map to `Invalid` (which callers reject with `OAKAUDIO_E_INVALID`). -fn sample_format_from_c(format: c_int) -> SampleFormat { - match format { - 0 => SampleFormat::U8Planar, - 1 => SampleFormat::S16Planar, - 2 => SampleFormat::S32Planar, - 3 => SampleFormat::S64Planar, - 4 => SampleFormat::F32Planar, - 5 => SampleFormat::F64Planar, - 6 => SampleFormat::U8, - 7 => SampleFormat::S16, - 8 => SampleFormat::S32, - 9 => SampleFormat::S64, - 10 => SampleFormat::F32, - 11 => SampleFormat::F64, - _ => SampleFormat::Invalid, - } -} - -/// Convert a C bool mask into a `Vec`; NULL means "all windows valid" -/// (an empty vec, which the correlation treats as all-valid). -/// -/// `// CPP-PARITY: src/audio/c_api/sync.cpp:40` (`to_mask`). -fn to_bool_mask(mask: *const u8, len: usize) -> Vec { - if mask.is_null() { - return Vec::new(); - } - // SAFETY: the caller guarantees `len` valid bytes at `mask` when - // non-NULL (the header requires masks to match the envelope length). - unsafe { std::slice::from_raw_parts(mask, len) } - .iter() - .map(|&b| b != 0) - .collect() -} - -/// Build a `Vec<&[f32]>` of planar views from a C plane array. NULL planes -/// become empty slices; the caller guarantees `planar` points to at least -/// `channel_count` entries and each plane to `frame_count` floats for the -/// duration of the call. -/// -/// # Safety -/// `planar` must be NULL (yields an empty vec) or valid for -/// `channel_count` plane pointers, each valid for `frame_count` floats. -unsafe fn planar_views<'a>( - planar: *const *const f32, - channel_count: i32, - frame_count: i32, -) -> Vec<&'a [f32]> { - if planar.is_null() { - return Vec::new(); - } - let mut views: Vec<&'a [f32]> = Vec::with_capacity(channel_count.max(0) as usize); - for ch in 0..channel_count { - let p = unsafe { *planar.add(ch as usize) }; - if p.is_null() { - views.push(&[]); - } else { - views.push(unsafe { std::slice::from_raw_parts(p, frame_count as usize) }); - } - } - views -} - -/// View a C double array; NULL or a non-positive length yields an empty -/// slice. -/// -/// # Safety -/// `ptr` must be valid for `len` doubles when both are non-zero. -unsafe fn double_slice<'a>(ptr: *const c_double, len: c_int) -> &'a [f64] { - if ptr.is_null() || len <= 0 { - &[] - } else { - unsafe { std::slice::from_raw_parts(ptr, len as usize) } - } -} - -/// Convert a C (num, den) rational pair; `Error::Invalid` when `den` is -/// zero (waveform.h: "den must be non-zero"; mirrors C++ `make_rational`). -/// -/// `// CPP-PARITY: src/audio/c_api/waveform.cpp:56` (`make_rational`). -fn rational_from_parts(num: i64, den: i64) -> crate::error::Result { - invalid_if(den == 0)?; - Ok(Rational::new(num, den)) -} - -/// `include/audio/manager.h` exports (complete inventory): the singleton -/// `OakAudioManager` is a borrowed handle (addref/release no-ops); -/// `oakaudio_manager_create_instance` / -/// `oakaudio_manager_destroy_instance` / -/// `oakaudio_manager_instance` manage the singleton; -/// `oakaudio_manager_free` / `set_output_notify_interval` / -/// `push_to_output` / `clear_buffered_output` / `stop_output` / -/// `seconds` / `reset_output_clock` / `get_output_device` / -/// `set_output_device` / `get_input_device` / `set_input_device` / -/// `hard_reset` / `start_recording` / `stop_recording` / -/// `find_config_device_by_name_s` / `find_device_by_name_s` / -/// `oakaudio_debug_alive_count`. -pub mod manager { - use super::*; - - /// `oakaudio_manager_create_instance`: OAKAUDIO_OK, or OAKAUDIO_E_*. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_manager_create_instance() -> c_int { - guard(|| crate::manager::create_instance()) - } - - /// `oakaudio_manager_destroy_instance`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_manager_destroy_instance() { - guard_void(|| crate::manager::destroy_instance()); - } - - /// `oakaudio_manager_instance`: borrowed handle (never owned). - #[no_mangle] - pub unsafe extern "C" fn oakaudio_manager_instance() -> CHandle { - guard_handle(|| Ok(crate::manager::instance())) - } - - /// `oakaudio_manager_free`: NULL/empty no-op. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_manager_free(_self: *mut CHandle) { - crate::manager::free(_self); - } - - /// `oakaudio_manager_set_output_notify_interval`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_manager_set_output_notify_interval( - _self: CHandle, - bytes: i64, - ) -> c_int { - guard(|| crate::manager::set_output_notify_interval(&_self, bytes)) - } - - /// `oakaudio_manager_push_to_output`: queue raw interleaved sample bytes. - #[no_mangle] - pub unsafe extern "C" 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 { - guard(|| { - // CPP-PARITY: `rate <= 0 || !samples || samples_size < 0`; an - // unrepresentable sample format is additionally rejected (the - // C++ casts the raw int into the enum without validating). - invalid_if(rate <= 0 || samples.is_null() || samples_size < 0)?; - let params = AudioParams { - sample_rate: rate, - channel_layout: layout, - format: sample_format_from_c(format), - }; - invalid_if(params.format == SampleFormat::Invalid)?; - let samples: &[u8] = if samples_size <= 0 { - &[] - } else { - // SAFETY: the caller guarantees `samples_size` bytes. - unsafe { std::slice::from_raw_parts(samples as *const u8, samples_size as usize) } - }; - let r = crate::manager::push_to_output(&_self, params, samples, &mut []); - // CPP-PARITY: the manager's error string is surfaced through - // error_buf on the failure path only. - if let Err(Error::Failed(msg)) = &r { - write_error(msg, error_buf, error_buf_size); - } - r - }) - } - - /// `oakaudio_manager_clear_buffered_output`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_manager_clear_buffered_output(_self: CHandle) -> c_int { - guard(|| crate::manager::clear_buffered_output(&_self)) - } - - /// `oakaudio_manager_stop_output`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_manager_stop_output(_self: CHandle) -> c_int { - guard(|| crate::manager::stop_output(&_self)) - } - - /// `oakaudio_manager_output_levels` — per-channel linear peaks of the - /// buffered output into `peaks` (up to `capacity` entries). Returns - /// the channel count (0 when nothing is buffered or no output is - /// configured). OAKAUDIO_E_INVALID for NULL/zero-capacity out args. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_manager_output_levels( - _self: CHandle, - peaks: *mut c_float, - capacity: c_int, - ) -> c_int { - guard_int(|| { - invalid_if(peaks.is_null() || capacity <= 0)?; - let slice = unsafe { std::slice::from_raw_parts_mut(peaks, capacity as usize) }; - crate::manager::output_levels(&_self, slice) - }) - } - - /// `oakaudio_manager_seconds`: write elapsed playback seconds into `out`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_manager_seconds(_self: CHandle, out: *mut c_double) -> c_int { - guard(|| { - invalid_if(out.is_null())?; - let mut seconds = 0.0f64; - crate::manager::seconds(&_self, &mut seconds)?; - unsafe { - *out = seconds; - } - Ok(()) - }) - } - - /// `oakaudio_manager_reset_output_clock`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_manager_reset_output_clock(_self: CHandle) -> c_int { - guard(|| crate::manager::reset_output_clock(&_self)) - } - - /// `oakaudio_manager_get_output_device`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_manager_get_output_device(_self: CHandle) -> c_int { - guard_int(|| crate::manager::get_output_device(&_self)) - } - - /// `oakaudio_manager_set_output_device`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_manager_set_output_device( - _self: CHandle, - device: c_int, - ) -> c_int { - guard(|| crate::manager::set_output_device(&_self, device)) - } - - /// `oakaudio_manager_get_input_device`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_manager_get_input_device(_self: CHandle) -> c_int { - guard_int(|| crate::manager::get_input_device(&_self)) - } - - /// `oakaudio_manager_set_input_device`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_manager_set_input_device( - _self: CHandle, - device: c_int, - ) -> c_int { - guard(|| crate::manager::set_input_device(&_self, device)) - } - - /// `oakaudio_manager_hard_reset`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_manager_hard_reset(_self: CHandle) -> c_int { - guard(|| crate::manager::hard_reset(&_self)) - } - - /// `oakaudio_manager_start_recording`: record input to `params` via the - /// oakcodec encoder. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_manager_start_recording( - _self: CHandle, - params: *const EncodingParams, - error_buf: *mut c_char, - error_buf_size: c_int, - ) -> c_int { - guard(|| { - // SAFETY: the caller guarantees `params` is a valid pointer when - // non-NULL. - let Some(params) = (unsafe { params.as_ref() }) else { - // CPP-PARITY: `!params || !params->audio_enabled`; the error - // string is written on the invalid path too so callers can - // always diagnose a failed start. - write_error("invalid recording parameters", error_buf, error_buf_size); - return Err(Error::Invalid); - }; - if params.audio_enabled == 0 { - write_error("invalid recording parameters", error_buf, error_buf_size); - return Err(Error::Invalid); - } - let r = crate::manager::start_recording(&_self, params, &mut []); - if let Err(Error::Failed(msg)) = &r { - write_error(msg, error_buf, error_buf_size); - } - r - }) - } - - /// `oakaudio_manager_stop_recording`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_manager_stop_recording(_self: CHandle) -> c_int { - guard(|| crate::manager::stop_recording(&_self)) - } - - /// `oakaudio_manager_find_config_device_by_name_s`: static, no handle. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_manager_find_config_device_by_name_s( - is_output_device: c_int, - ) -> c_int { - crate::manager::find_config_device_by_name_s(is_output_device != 0) - } - - /// `oakaudio_manager_find_device_by_name_s`: static, no handle. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_manager_find_device_by_name_s( - name: *const c_char, - is_output_device: c_int, - ) -> c_int { - if name.is_null() { - return OAKAUDIO_E_INVALID; - } - // SAFETY: the caller guarantees a NUL-terminated string. - let name = unsafe { CStr::from_ptr(name) }; - crate::manager::find_device_by_name_s(name, is_output_device != 0) - } - - /// `oakaudio_debug_alive_count`: surviving object count. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_debug_alive_count() -> c_int { - crate::manager::debug_alive_count() - } -} - -/// `include/audio/processor.h` exports (complete inventory): the neutral -/// by-value `OakAudioProcessor` handle (refcounted object, no singleton -/// semantics); `oakaudio_processor_init` / `oakaudio_processor_free` / -/// `oakaudio_processor_open` / `oakaudio_processor_close` / -/// `oakaudio_processor_is_open` / `oakaudio_processor_convert` / -/// `oakaudio_processor_flush`. `OAKAUDIO_PROCESSOR_OUTPUT_FORMAT == 4` -/// (`SampleFormat::f32_p`) pins the planar-first sample format ordering. -pub mod processor { - use super::*; - - /// `oakaudio_processor_init`: new processor, refcount 1. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_processor_init() -> CHandle { - guard_handle(crate::processor::init) - } - - /// `oakaudio_processor_free`: NULL/empty no-op. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_processor_free(_self: *mut CHandle) { - crate::processor::free(_self); - } - - /// `oakaudio_processor_open`: configure a resampling/conversion graph. - /// - /// Validation order and codes follow the C++ exactly (empty handle, - /// already-open state, rates/speed, forced planar-float output); - /// `in_format` is passed through unvalidated, matching `SampleFormat( - /// SampleFormat::Format(in_format))` in the C++. - #[no_mangle] - pub unsafe extern "C" 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 { - guard(|| { - let from = AudioParams { - sample_rate: in_rate, - channel_layout: in_layout, - format: sample_format_from_c(in_format), - }; - let to = AudioParams { - sample_rate: out_rate, - channel_layout: out_layout, - format: sample_format_from_c(out_format), - }; - crate::processor::open(&_self, from, to, speed) - }) - } - - /// `oakaudio_processor_close`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_processor_close(_self: CHandle) -> c_int { - guard(|| crate::processor::close(&_self)) - } - - /// `oakaudio_processor_is_open`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_processor_is_open(_self: CHandle) -> c_int { - guard_int(|| Ok(crate::processor::is_open(&_self)? as i32)) - } - - /// `oakaudio_processor_convert`: resample/convert planar float in-place. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_processor_convert( - _self: CHandle, - in_planar: *const *const f32, - in_frame_count: c_int, - out_planar: *const *mut f32, - out_capacity_frames: c_int, - ) -> c_int { - guard_int(|| { - crate::processor::convert( - &_self, - in_planar, - in_frame_count, - out_planar, - out_capacity_frames, - ) - }) - } - - /// `oakaudio_processor_flush`: drain buffered frames. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_processor_flush(_self: CHandle) -> c_int { - guard(|| crate::processor::flush(&_self)) - } -} - -/// `include/audio/sync.h` exports (complete inventory): stateless placement -/// and envelope helpers (no handle); `oakaudio_offset_result`, -/// `oakaudio_stretch_offset_result`, `oakaudio_source_clip` value structs; -/// `oakaudio_sync_extract_rms_envelope` / -/// `oakaudio_sync_estimate_envelope_offset` / -/// `oakaudio_sync_estimate_stretch_and_offset` / -/// `oakaudio_sync_place_by_source_time` / -/// `oakaudio_sync_place_by_waveform_offset`. -pub mod sync { - use super::*; - - /// `oakaudio_offset_result` — candidate offset + correlation confidence. - #[repr(C)] - pub struct OffsetResult { - /// Offset of the candidate relative to the reference, in samples. - pub offset_samples: i64, - /// Normalized correlation confidence in `[0, 1]`. - pub confidence: c_double, - /// Whether an offset could be determined. - pub valid: c_int, - } - - /// `oakaudio_stretch_offset_result` — rate + offset + confidence. - #[repr(C)] - pub struct StretchOffsetResult { - /// Playback rate aligning the candidate (`> 1` = speed up). - pub rate: c_double, - /// Offset in samples. - pub offset_samples: i64, - /// Normalized correlation confidence in `[0, 1]`. - pub confidence: c_double, - /// Whether a rate+offset could be determined. - pub valid: c_int, - } - - /// `oakaudio_source_clip` — one clip's source-time metadata. - #[repr(C)] - pub struct SourceClip { - /// Source start time numerator (seconds). - pub source_start_time_num: i64, - /// Source start time denominator (seconds). - pub source_start_time_den: i64, - /// Media in point numerator (seconds). - pub media_in_num: i64, - /// Media in point denominator (seconds). - pub media_in_den: i64, - /// Whether `source_start_time` is set. - pub has_source_start_time: c_int, - } - - /// `oakaudio_sync_extract_rms_envelope`: windowed RMS envelope. - /// - /// Two-stage contract: `out == NULL` or `capacity < windows` returns the - /// required window count without writing. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_sync_extract_rms_envelope( - planar: *const *const f32, - channel_count: c_int, - frame_count: c_int, - window_samples: u64, - out: *mut c_double, - capacity: c_int, - ) -> c_int { - guard_int(|| { - // CPP-PARITY: sync.cpp:58 — every plane must be non-NULL. - invalid_if( - planar.is_null() - || channel_count <= 0 - || frame_count < 0 - || window_samples == 0 - || capacity < 0, - )?; - for ch in 0..channel_count { - // SAFETY: `planar` is non-NULL with `channel_count` entries. - if unsafe { *planar.add(ch as usize) }.is_null() { - return Err(Error::Invalid); - } - } - let views = unsafe { planar_views(planar, channel_count, frame_count) }; - let envelope = - crate::waveformsync::extract_rms_envelope(&views, window_samples as usize); - let windows = envelope.len(); - if out.is_null() || (capacity as usize) < windows { - return Ok(windows as i32); - } - unsafe { - std::ptr::copy_nonoverlapping(envelope.as_ptr(), out, windows); - } - Ok(windows as i32) - }) - } - - /// `oakaudio_sync_estimate_envelope_offset`: masked envelope correlation. - #[no_mangle] - pub unsafe extern "C" 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 { - guard(|| { - // CPP-PARITY: sync.cpp:93 — NULL arrays and non-positive lengths - // are invalid; masks may be NULL (all windows valid). - invalid_if( - out.is_null() - || reference.is_null() - || candidate.is_null() - || reference_len <= 0 - || candidate_len <= 0 - || window_samples == 0 - || max_offset_windows < 0, - )?; - let reference = unsafe { double_slice(reference, reference_len) }; - let candidate = unsafe { double_slice(candidate, candidate_len) }; - let ref_valid = to_bool_mask(reference_valid, reference_len as usize); - let cand_valid = to_bool_mask(candidate_valid, candidate_len as usize); - let result = crate::waveformsync::estimate_envelope_offset_valid( - reference, - candidate, - &ref_valid, - &cand_valid, - window_samples as usize, - max_offset_windows, - ); - // SAFETY: `out` is non-NULL (checked above). - unsafe { - *out = OffsetResult { - offset_samples: result.offset_samples, - confidence: result.confidence, - valid: result.valid as c_int, - }; - } - Ok(()) - }) - } - - /// `oakaudio_sync_estimate_stretch_and_offset`. - #[no_mangle] - pub unsafe extern "C" 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 { - guard(|| { - // CPP-PARITY: sync.cpp:123. - invalid_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, - )?; - let reference = unsafe { double_slice(reference, reference_len) }; - let candidate = unsafe { double_slice(candidate, candidate_len) }; - let ref_valid = to_bool_mask(reference_valid, reference_len as usize); - let cand_valid = to_bool_mask(candidate_valid, candidate_len as usize); - let result = crate::waveformsync::estimate_stretch_and_offset( - reference, - candidate, - &ref_valid, - &cand_valid, - window_samples as usize, - max_offset_windows, - min_rate, - max_rate, - rate_step, - ); - // SAFETY: `out` is non-NULL (checked above). - unsafe { - *out = StretchOffsetResult { - rate: result.rate, - offset_samples: result.offset_samples, - confidence: result.confidence, - valid: result.valid as c_int, - }; - } - Ok(()) - }) - } - - /// `oakaudio_sync_place_by_source_time`: timeline placement by source time. - #[no_mangle] - pub unsafe extern "C" 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 { - guard(|| { - // CPP-PARITY: sync.cpp:153 — every denominator must be non-zero. - // SAFETY: non-NULL pointers are valid for one struct each. - let Some(reference) = (unsafe { reference.as_ref() }) else { - return Err(Error::Invalid); - }; - let Some(candidate) = (unsafe { candidate.as_ref() }) else { - return Err(Error::Invalid); - }; - invalid_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, - )?; - let reference_clip = crate::synchronizer::SourceClip { - source_start_time: Rational::new( - reference.source_start_time_num, - reference.source_start_time_den, - ), - media_in: Rational::new(reference.media_in_num, reference.media_in_den), - has_source_start_time: reference.has_source_start_time != 0, - }; - let candidate_clip = crate::synchronizer::SourceClip { - source_start_time: Rational::new( - candidate.source_start_time_num, - candidate.source_start_time_den, - ), - media_in: Rational::new(candidate.media_in_num, candidate.media_in_den), - has_source_start_time: candidate.has_source_start_time != 0, - }; - let placement = crate::synchronizer::place_by_source_time( - &reference_clip, - &candidate_clip, - Rational::new(reference_timeline_in_num, reference_timeline_in_den), - ); - // SAFETY: out pointers are non-NULL (checked above). - unsafe { - *out_num = placement.timeline_in.numerator(); - *out_den = placement.timeline_in.denominator(); - *out_valid = placement.valid as c_int; - } - Ok(()) - }) - } - - /// `oakaudio_sync_place_by_waveform_offset`: timeline placement from an - /// offset in samples. - #[no_mangle] - pub unsafe extern "C" 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 { - guard(|| { - // CPP-PARITY: sync.cpp:191. - invalid_if( - out_num.is_null() - || out_den.is_null() - || out_valid.is_null() - || reference_timeline_in_den == 0, - )?; - let placement = crate::synchronizer::place_by_waveform_offset( - Rational::new(reference_timeline_in_num, reference_timeline_in_den), - candidate_offset_samples, - sample_rate, - ); - // SAFETY: out pointers are non-NULL (checked above). - unsafe { - *out_num = placement.timeline_in.numerator(); - *out_den = placement.timeline_in.denominator(); - *out_valid = placement.valid as c_int; - } - Ok(()) - }) - } -} - -/// `include/audio/waveform.h` exports (complete inventory): neutral by-value -/// `OakAudioWaveform` handle (refcounted); `oakaudio_min_max` value struct; -/// `oakaudio_waveform_init` / `oakaudio_waveform_free` / -/// `oakaudio_waveform_get_channel_count` / `oakaudio_waveform_set_channel_count` -/// / `oakaudio_waveform_length` / `oakaudio_waveform_overwrite_samples` / -/// `oakaudio_waveform_overwrite_sums` / `oakaudio_waveform_overwrite_silence` -/// / `oakaudio_waveform_trim_in` / `oakaudio_waveform_resize` / -/// `oakaudio_waveform_trim_range` / `oakaudio_waveform_get_summary` / -/// `oakaudio_waveform_sum_samples_s` / `oakaudio_waveform_re_sum_s` / -/// `oakaudio_waveform_extract`. -pub mod waveform { - use super::*; - - /// `oakaudio_min_max` — one min/max pair. - #[repr(C)] - #[derive(Clone, Copy, Debug, PartialEq)] - pub struct MinMax { - /// Minimum amplitude in the window. - pub min: f32, - /// Maximum amplitude in the window. - pub max: f32, - } - - /// `oakaudio_waveform_init`: new waveform, refcount 1. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_waveform_init() -> CHandle { - guard_handle(crate::waveform::init) - } - - /// `oakaudio_waveform_free`: NULL/empty no-op. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_waveform_free(_self: *mut CHandle) { - crate::waveform::free(_self); - } - - /// `oakaudio_waveform_get_channel_count`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_waveform_get_channel_count(_self: CHandle) -> c_int { - guard_int(|| Ok(crate::waveform::get(&_self)?.channel_count())) - } - - /// `oakaudio_waveform_set_channel_count`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_waveform_set_channel_count( - _self: CHandle, - channels: c_int, - ) -> c_int { - guard(|| { - invalid_if(channels < 0)?; - crate::waveform::get_mut(&_self)?.set_channel_count(channels); - Ok(()) - }) - } - - /// `oakaudio_waveform_length`: length as a rational pair. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_waveform_length( - _self: CHandle, - num: *mut i64, - den: *mut i64, - ) -> c_int { - guard(|| { - invalid_if(num.is_null() || den.is_null())?; - let length = crate::waveform::get(&_self)?.length(); - unsafe { - *num = length.numerator(); - *den = length.denominator(); - } - Ok(()) - }) - } - - /// `oakaudio_waveform_overwrite_samples`. - /// - /// Validation order follows the C++: invalid args (incl. `frame_count - /// <= 0`) first, then the channel-count state check (`E_STATE`). - #[no_mangle] - pub unsafe extern "C" fn oakaudio_waveform_overwrite_samples( - _self: CHandle, - planar: *const *const f32, - frame_count: c_int, - sample_rate: c_int, - start_num: i64, - start_den: i64, - ) -> c_int { - guard(|| { - // CPP-PARITY: waveform.cpp:208. - let start = rational_from_parts(start_num, start_den)?; - invalid_if(planar.is_null() || frame_count <= 0 || sample_rate <= 0)?; - let channels = crate::waveform::get(&_self)?.channel_count(); - if channels <= 0 { - return Err(Error::State); - } - for ch in 0..channels { - // SAFETY: `planar` is non-NULL with `channels` entries. - if unsafe { *planar.add(ch as usize) }.is_null() { - return Err(Error::Invalid); - } - } - let views = unsafe { planar_views(planar, channels, frame_count) }; - crate::waveform::get_mut(&_self)?.overwrite_samples(&views, sample_rate, start); - Ok(()) - }) - } - - /// `oakaudio_waveform_overwrite_sums`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_waveform_overwrite_sums( - _self: CHandle, - src: CHandle, - dest_num: i64, - dest_den: i64, - offset_num: i64, - offset_den: i64, - length_num: i64, - length_den: i64, - ) -> c_int { - guard(|| { - let dest = rational_from_parts(dest_num, dest_den)?; - let offset = rational_from_parts(offset_num, offset_den)?; - let length = rational_from_parts(length_num, length_den)?; - let src_waveform = crate::waveform::get(&src)?; - // SAFETY: `self` and `src` are distinct handles (the FFI - // contract forbids aliasing them). - crate::waveform::get_mut(&_self)?.overwrite_sums(src_waveform, dest, offset, length); - Ok(()) - }) - } - - /// `oakaudio_waveform_overwrite_silence`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_waveform_overwrite_silence( - _self: CHandle, - start_num: i64, - start_den: i64, - length_num: i64, - length_den: i64, - ) -> c_int { - guard(|| { - let start = rational_from_parts(start_num, start_den)?; - let length = rational_from_parts(length_num, length_den)?; - crate::waveform::get_mut(&_self)?.overwrite_silence(start, length); - Ok(()) - }) - } - - /// `oakaudio_waveform_trim_in`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_waveform_trim_in( - _self: CHandle, - length_num: i64, - length_den: i64, - ) -> c_int { - guard(|| { - let length = rational_from_parts(length_num, length_den)?; - crate::waveform::get_mut(&_self)?.trim_in(length); - Ok(()) - }) - } - - /// `oakaudio_waveform_resize`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_waveform_resize( - _self: CHandle, - length_num: i64, - length_den: i64, - ) -> c_int { - guard(|| { - let length = rational_from_parts(length_num, length_den)?; - invalid_if(length < Rational::new(0, 1))?; - crate::waveform::get_mut(&_self)?.resize(length); - Ok(()) - }) - } - - /// `oakaudio_waveform_trim_range`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_waveform_trim_range( - _self: CHandle, - in_num: i64, - in_den: i64, - length_num: i64, - length_den: i64, - ) -> c_int { - guard(|| { - let r#in = rational_from_parts(in_num, in_den)?; - let length = rational_from_parts(length_num, length_den)?; - crate::waveform::get_mut(&_self)?.trim_range(r#in, length); - Ok(()) - }) - } - - /// `oakaudio_waveform_get_summary`: channel-interleaved min/max pairs. - /// - /// Two-stage contract: `out_pairs == NULL` or `capacity_points` smaller - /// than the required count returns the count without writing. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_waveform_get_summary( - _self: CHandle, - start_num: i64, - start_den: i64, - length_num: i64, - length_den: i64, - out_pairs: *mut MinMax, - capacity_points: c_int, - ) -> c_int { - guard_int(|| { - // CPP-PARITY: waveform.cpp:337 — `length <= 0` is invalid. - let start = rational_from_parts(start_num, start_den)?; - let length = rational_from_parts(length_num, length_den)?; - invalid_if(length <= Rational::new(0, 1) || capacity_points < 0)?; - let sample = crate::waveform::get(&_self)?.get_summary_from_time(start, length); - let channels = crate::waveform::get(&_self)?.channel_count().max(1) as usize; - let points = sample.len() / channels; - if out_pairs.is_null() || (capacity_points as usize) < points { - return Ok(points as i32); - } - // SAFETY: `out_pairs` holds at least `points * channels` - // entries (capacity is in points, checked above); `Sample` is - // channel-interleaved `SamplePerChannel`. - for (i, spc) in sample.iter().take(points * channels).enumerate() { - unsafe { - *out_pairs.add(i) = MinMax { - min: spc.min, - max: spc.max, - }; - } - } - Ok(points as i32) - }) - } - - /// `oakaudio_waveform_sum_samples_s`: static, no handle. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_waveform_sum_samples_s( - planar: *const *const f32, - channel_count: c_int, - start_index: c_int, - length: c_int, - out: *mut MinMax, - ) -> c_int { - guard_int(|| { - // CPP-PARITY: waveform.cpp:364 — `out` is required (no - // two-stage query) and `length <= 0` is invalid. - invalid_if( - planar.is_null() - || out.is_null() - || channel_count <= 0 - || start_index < 0 - || length <= 0, - )?; - for ch in 0..channel_count { - // SAFETY: `planar` is non-NULL with `channel_count` entries. - if unsafe { *planar.add(ch as usize) }.is_null() { - return Err(Error::Invalid); - } - } - // Each plane must hold at least `start_index + length` floats; - // the C++ SampleBuffer has exactly that span. - let mut views: Vec<&[f32]> = Vec::with_capacity(channel_count as usize); - for ch in 0..channel_count { - // SAFETY: non-NULL planes of `start_index + length` floats. - let p = unsafe { *planar.add(ch as usize) }; - views.push(unsafe { - std::slice::from_raw_parts(p, (start_index + length) as usize) - }); - } - let sample = - AudioVisualWaveform::sum_samples(&views, start_index as usize, length as usize); - // CPP-PARITY: a short summary is an internal failure. - if sample.len() < channel_count as usize { - return Err(Error::Failed("sum_samples underflow".to_string())); - } - for (i, spc) in sample.iter().enumerate() { - // SAFETY: `out` holds at least `channel_count` entries. - unsafe { - *out.add(i) = MinMax { - min: spc.min, - max: spc.max, - }; - } - } - Ok(crate::error::OAKAUDIO_OK) - }) - } - - /// `oakaudio_waveform_re_sum_s`: static, no handle. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_waveform_re_sum_s( - r#in: *const MinMax, - nb_entries: c_int, - nb_channels: c_int, - out: *mut MinMax, - ) -> c_int { - guard_int(|| { - // CPP-PARITY: waveform.cpp:393 — both buffers are required. - invalid_if(r#in.is_null() || out.is_null() || nb_entries <= 0 || nb_channels <= 0)?; - // SAFETY: `in` holds `nb_entries` entries. - let entries = unsafe { std::slice::from_raw_parts(r#in, nb_entries as usize) }; - let samples: Vec = entries - .iter() - .map(|m| SamplePerChannel { - min: m.min, - max: m.max, - }) - .collect(); - let sample = - AudioVisualWaveform::re_sum_samples(&samples, nb_entries as usize, nb_channels); - for (i, spc) in sample.iter().enumerate() { - // SAFETY: `out` holds at least `nb_channels` entries. - unsafe { - *out.add(i) = MinMax { - min: spc.min, - max: spc.max, - }; - } - } - Ok(crate::error::OAKAUDIO_OK) - }) - } - - /// `oakaudio_waveform_extract`: whole-file waveform via oakcodec decoder. - /// - /// Two-stage contract: `out_pairs == NULL` or `capacity_points` smaller - /// than the required point count returns the count without writing; the - /// channel count is reported whenever `out_channel_count` is non-NULL. - /// Decoding/probing lives in [`crate::waveform::extract`]. - #[no_mangle] - pub unsafe extern "C" 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 { - guard_int(|| { - // CPP-PARITY: waveform.cpp:409. - invalid_if( - filename.is_null() - || stream_index < 0 - || samples_per_point <= 0 - || capacity_points < 0, - )?; - // SAFETY: the caller guarantees a NUL-terminated string. - let filename = unsafe { CStr::from_ptr(filename) }; - // The two-stage contract runs the query twice; decode the - // stream ONCE and reuse the outcome for the data pass (the - // FFmpeg decoder is not safe against back-to-back sessions - // in the same process). - let key = ( - filename.to_string_lossy().into_owned(), - stream_index, - samples_per_point, - ); - let outcome = { - // The cache slot is borrowed inside the closure only (the - // guard cannot escape the `with` scope). - let cached = WAVEFORM_EXTRACT_CACHE.with(|c| { - c.borrow() - .as_ref() - .filter(|(k, _)| k == &key) - .map(|(_, o)| o.clone()) - }); - match cached { - Some(o) => o, - None => { - let outcome = - crate::waveform::extract(filename, stream_index, samples_per_point)?; - WAVEFORM_EXTRACT_CACHE.with(|c| *c.borrow_mut() = Some((key, outcome.clone()))); - outcome - } - } - }; - let channels = outcome.channels.max(1); - let point_count = outcome.points.len() / channels as usize; - // CPP-PARITY: the channel count is reported even for a size-only - // query. - if !out_channel_count.is_null() { - unsafe { - *out_channel_count = outcome.channels; - } - } - if out_pairs.is_null() || (capacity_points as usize) < point_count { - return Ok(point_count as i32); - } - for (i, spc) in outcome.points.iter().enumerate() { - // SAFETY: `out_pairs` holds `point_count * channels` entries - // (capacity is in points, checked above). - unsafe { - *out_pairs.add(i) = MinMax { - min: spc.min, - max: spc.max, - }; - } - } - Ok(point_count as i32) - }) - } -} - -/// `include/audio/levelmeter.h` exports (complete inventory): stateless -/// peak/RMS/VU/LUFS analysis of planar float audio (no handle); -/// `oakaudio_channel_stats`, `oakaudio_meter_stats` value structs; -/// `oakaudio_levelmeter_analyze`. -pub mod levelmeter { - use super::*; - - /// `oakaudio_channel_stats` — per-channel analysis results (dB floor -200). - #[repr(C)] - pub struct ChannelStats { - /// Peak amplitude, linear scale. - pub peak_linear: c_double, - /// Peak amplitude, decibel scale. - pub peak_db: c_double, - /// Root-mean-square level, linear scale. - pub rms_linear: c_double, - /// Root-mean-square level, decibel scale. - pub rms_db: c_double, - /// VU-meter ballistics reading, decibel scale. - pub vu_db: c_double, - } - - /// `oakaudio_meter_stats` — buffer-wide summary. - #[repr(C)] - pub struct MeterStats { - /// Maximum peak across all channels, linear scale. - pub max_peak_linear: c_double, - /// Integrated loudness (EBU R128 LUFS). - pub integrated_lufs: c_double, - /// Whether every channel was silent below the noise gate. - pub silence: c_int, - } - - /// `oakaudio_levelmeter_analyze`. - /// - /// Validation follows the C++: null planar / non-positive channel count - /// / negative frame count / undersized `channels` buffer, and the - /// no-output double-null case, are all `OAKAUDIO_E_INVALID`. - #[no_mangle] - pub unsafe extern "C" fn oakaudio_levelmeter_analyze( - planar: *const *const f32, - channel_count: c_int, - frame_count: c_int, - channels: *mut ChannelStats, - channels_capacity: c_int, - summary: *mut MeterStats, - ) -> c_int { - guard_int(|| { - // CPP-PARITY: levelmeter.cpp:39. - invalid_if( - planar.is_null() - || channel_count <= 0 - || frame_count < 0 - || (!channels.is_null() && channels_capacity < channel_count), - )?; - invalid_if(channels.is_null() && summary.is_null())?; - for ch in 0..channel_count { - // SAFETY: `planar` is non-NULL with `channel_count` entries. - if unsafe { *planar.add(ch as usize) }.is_null() { - return Err(Error::Invalid); - } - } - let views = unsafe { planar_views(planar, channel_count, frame_count) }; - let stats = crate::levelmeter::analyze_sample_buffer(&views); - - if !channels.is_null() { - for (i, ch) in stats.channels.iter().enumerate() { - // SAFETY: capacity >= channel_count (checked above). - unsafe { - *channels.add(i) = ChannelStats { - peak_linear: ch.peak_linear, - peak_db: ch.peak_db, - rms_linear: ch.rms_linear, - rms_db: ch.rms_db, - vu_db: ch.vu_db, - }; - } - } - } - if !summary.is_null() { - unsafe { - *summary = MeterStats { - max_peak_linear: stats.max_peak_linear, - integrated_lufs: stats.integrated_lufs, - silence: stats.silence as c_int, - }; - } - } - Ok(crate::error::OAKAUDIO_OK) - }) - } -} diff --git a/crates/oakaudio/src/handle.rs b/crates/oakaudio/src/handle.rs deleted file mode 100644 index 8ebe47b52..000000000 --- a/crates/oakaudio/src/handle.rs +++ /dev/null @@ -1,226 +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 . - -//! Refcounted-handle scaffolding. Same pattern as the oaknode crate -//! (`src/node/rust/src/handle.rs`); intentionally duplicated rather -//! than shared — each module DLL must run its own addref/release code -//! (the function pointers in a handle always point into the DLL that -//! created the object). -//! -//! The `AudioManager` is the one exception: it uses the same `CHandle` -//! layout but with singleton semantics (addref/release no-ops). See -//! `manager.rs` and `include/audio/manager.h`. -//! -//! `// CPP-PARITY: src/audio/c_api/refcounted.h` (RefCounted box, -//! make_handle_in_place, free_handle) and `src/audio/c_api/alive.cpp` -//! (alive ledger behind `oakaudio_debug_alive_count`). - -use std::panic::{catch_unwind, AssertUnwindSafe}; -use std::sync::atomic::{AtomicI32, AtomicU32, Ordering}; - -use crate::error::{Error, OAKAUDIO_E_FAILED, OAKAUDIO_OK}; - -/// ABI version stamped into every handle. -pub const OAKAUDIO_ABI_VERSION: u32 = 1; - -/// Live-object ledger behind `oakaudio_debug_alive_count`. -/// -/// `// CPP-PARITY: src/audio/c_api/alive.cpp` (`g_alive`). -static ALIVE: AtomicI32 = AtomicI32::new(0); - -/// Current number of live refcounted oakaudio objects. -pub fn alive_count() -> i32 { - ALIVE.load(Ordering::Relaxed) -} - -/// Heap box behind a handle's `ctx`. -pub struct RefBox { - /// Atomic reference count. - pub refs: AtomicU32, - /// Boxed value. - pub value: T, -} - -/// 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 -/// here so the crate's `ffi.rs` signatures and handle scaffolding stay -/// source-compatible. It is `Clone + Copy` (handles cross the C ABI by -/// value) and `Send + Sync` (refcounted, shared across threads). -pub use oakcore_rs::handle::CHandle; - -/// `// CPP-PARITY: src/audio/c_api/refcounted.h` (`ref_counted_addref`). -unsafe extern "C" fn owned_addref(ctx: *mut std::ffi::c_void) { - // SAFETY: `ctx` is either NULL or points to a `RefBox` created by - // `make_owned`; we only touch it through the reference while it is live. - if let Some(b) = unsafe { (ctx as *const RefBox).as_ref() } { - b.refs.fetch_add(1, Ordering::Relaxed); - } -} - -/// `// CPP-PARITY: src/audio/c_api/refcounted.h` (`ref_counted_release`): -/// destroys the box at zero and decrements the alive ledger. -unsafe extern "C" fn owned_release(ctx: *mut std::ffi::c_void) { - // SAFETY: `ctx` is either NULL or points to a live `RefBox` created - // by `make_owned`; the refcount guards against double-free, and the box - // is only reclaimed once the count reaches zero. - if let Some(b) = unsafe { (ctx as *const RefBox).as_ref() } { - if b.refs.fetch_sub(1, Ordering::AcqRel) == 1 { - drop(unsafe { Box::from_raw(ctx as *mut RefBox) }); - ALIVE.fetch_sub(1, Ordering::Relaxed); - } - } -} - -/// Owned handle with count 1; empty on allocation failure. -pub fn make_owned(value: T) -> CHandle { - let b = Box::new(RefBox { - refs: AtomicU32::new(1), - value, - }); - ALIVE.fetch_add(1, Ordering::Relaxed); - CHandle { - ctx: Box::into_raw(b) as *mut std::ffi::c_void, - addref: Some(owned_addref::), - release: Some(owned_release::), - abi_version: OAKAUDIO_ABI_VERSION, - } -} - -/// No-op addref/release for singleton (borrowed) handles. -/// -/// `// CPP-PARITY: src/audio/c_api/manager.cpp` (`singleton_addref` / -/// `singleton_release`). -unsafe extern "C" fn noop_ref(_ctx: *mut std::ffi::c_void) {} - -/// Borrowed handle for an object owned elsewhere (addref/release are -/// no-ops; nothing is ever freed through the handle). -/// -/// # Safety -/// Caller guarantees `ptr` outlives every derived handle. -pub unsafe fn make_borrowed(ptr: *mut T) -> CHandle { - CHandle { - ctx: ptr as *mut std::ffi::c_void, - addref: Some(noop_ref), - release: Some(noop_ref), - abi_version: OAKAUDIO_ABI_VERSION, - } -} - -/// Typed view into an owned handle; `None` for empty handles. -/// -/// # Safety -/// `T` must be the boxed type. -pub unsafe fn get(h: &CHandle) -> Option<&T> { - if h.ctx.is_null() { - return None; - } - // SAFETY: caller guarantees `h` is a valid owned handle whose ctx points - // to a `RefBox`; the handle stays alive through the returned borrow. - let v = unsafe { &(*(h.ctx as *const RefBox)).value }; - Some(v) -} - -/// Typed mutable view into an owned handle; `None` for empty handles. -/// -/// # Safety -/// `T` must be the boxed type. -pub unsafe fn get_mut(h: &CHandle) -> Option<&mut T> { - if h.ctx.is_null() { - return None; - } - // SAFETY: caller guarantees `h` is a valid owned handle whose ctx points - // to a `RefBox`; the handle stays alive through the returned borrow, - // and the caller must not alias it with other live borrows. - let v = unsafe { &mut (*(h.ctx as *mut RefBox)).value }; - Some(v) -} - -/// Shared free() body: release the ctx, no-op on NULL/empty handle. -/// -/// `// CPP-PARITY: src/audio/c_api/refcounted.h` (`free_handle`). -/// -/// # Safety -/// `self_` must be a valid handle pointer or NULL. -pub unsafe fn free_handle(h: *mut CHandle) { - // SAFETY: caller guarantees `h` is a valid handle pointer or NULL. - if let Some(r) = unsafe { h.as_mut() } { - if !r.ctx.is_null() { - if let Some(release) = r.release { - // SAFETY: the release fn belongs to the same DLL and - // accepts the ctx it originally created. - unsafe { release(r.ctx) }; - } - r.ctx = std::ptr::null_mut(); - } - } -} - -/// Panic-catching FFI wrapper for i32-returning exports. -pub fn guard crate::error::Result<()>>(f: F) -> i32 { - match catch_unwind(AssertUnwindSafe(f)) { - Ok(Ok(())) => OAKAUDIO_OK, - Ok(Err(e)) => e.code(), - Err(_) => OAKAUDIO_E_FAILED, - } -} - -/// Panic-catching FFI wrapper for handle-returning exports. -pub fn guard_handle crate::error::Result>(f: F) -> CHandle { - match catch_unwind(AssertUnwindSafe(f)) { - Ok(Ok(h)) => h, - Ok(Err(_)) | Err(_) => CHandle::null(), - } -} - -/// Panic-catching FFI wrapper for i32 value-returning exports (errors map -/// to the negative error code). -pub fn guard_int crate::error::Result>(f: F) -> i32 { - match catch_unwind(AssertUnwindSafe(f)) { - Ok(Ok(v)) => v, - Ok(Err(e)) => e.code(), - Err(_) => OAKAUDIO_E_FAILED, - } -} - -/// Panic-catching FFI wrapper for void exports. -pub fn guard_void(f: F) { - let _ = catch_unwind(AssertUnwindSafe(f)); -} - -/// Copy a human-readable error string into a C buffer (NUL-terminated, -/// truncated to fit). Returns the required size including the NUL. -/// -/// `// CPP-PARITY: src/audio/c_api/manager.cpp` (`write_error`). -pub fn write_error(s: &str, buf: *mut std::ffi::c_char, buf_size: i32) { - if !buf.is_null() && buf_size > 0 { - let bytes = s.as_bytes(); - let n = bytes.len().min((buf_size - 1) as usize); - unsafe { - std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, n); - *(buf as *mut u8).add(n) = 0; - } - } -} - -/// Convenience: map a condition to an Invalid error. -pub fn invalid_if(cond: bool) -> crate::error::Result<()> { - if cond { - Err(Error::Invalid) - } else { - Ok(()) - } -} diff --git a/crates/oakaudio/src/lib.rs b/crates/oakaudio/src/lib.rs index 43d7aa264..a7d781126 100644 --- a/crates/oakaudio/src/lib.rs +++ b/crates/oakaudio/src/lib.rs @@ -30,11 +30,8 @@ #![deny(unsafe_op_in_unsafe_fn)] #![warn(missing_docs)] -pub mod bridge; pub mod config; pub mod error; -pub mod ffi; -pub mod handle; pub mod levelmeter; pub mod manager; pub mod params; diff --git a/crates/oakaudio/src/manager.rs b/crates/oakaudio/src/manager.rs index d7512ed88..8c7088fa5 100644 --- a/crates/oakaudio/src/manager.rs +++ b/crates/oakaudio/src/manager.rs @@ -26,13 +26,15 @@ use std::ffi::c_void; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Mutex, MutexGuard, OnceLock}; - -use crate::bridge::codec::EncodingParams; +use std::sync::{Arc, Mutex, MutexGuard, OnceLock}; +use cpal::{Device, DeviceId}; +use cpal::traits::{DeviceTrait, HostTrait}; +use oakcodec::encoder::Encoder; +use oakcodec::encodingparams::EncodingParams; use crate::error::{Error, Result}; -use crate::handle::{make_borrowed, CHandle}; use crate::params::AudioParams; use crate::previewdevice::PreviewAudioDevice; +use crate::error::Error::NotFound; /// `paNoDevice` (PortAudio "no device" sentinel; also the default when no /// device is configured). @@ -46,7 +48,11 @@ static DESTROYED: AtomicBool = AtomicBool::new(false); /// Manager state (all device/stream fields; PortAudio itself is not bridged, /// see [`ManagerInner::default`] for the degradations). -struct ManagerInner { +/// +/// Public since the single-lib unification (the deleted `ffi`/`bridge` +/// C ABI is gone): the oakengine facade calls the singleton's methods +/// directly through [`instance`] instead of crossing the old C ABI. +pub struct ManagerInner { /// Current output device index. output_device: i32, /// Current input device index. @@ -62,8 +68,8 @@ struct ManagerInner { /// The real PortAudio output stream (M12 P1; opened lazily on the /// first pushed samples). output_device_stream: crate::outputdevice::PortAudioOutput, - /// Active oakcodec recording encoder (NULL when idle). - recording: Option, + /// Active oakcodec recording encoder (None when idle). + recording: Option>, } // SAFETY: the raw encoder pointer is only touched while the manager mutex is @@ -89,290 +95,308 @@ impl Default for ManagerInner { } } -/// Lock the manager behind a handle; `OAKAUDIO_E_STATE` for empty handles. -fn with_instance(h: &CHandle) -> Result> { - if h.is_null() { - return Err(Error::State); +impl ManagerInner { + /// Create the process-wide AudioManager (no-op when it exists). Returns + /// `OAKAUDIO_OK` or `OAKAUDIO_E_NOMEM`. + /// + /// `// CPP-PARITY: src/audio/c_api/manager.cpp:73` (C++ allocates with `new` + /// and reports `OAKAUDIO_E_NOMEM` on exception; Rust allocation infallibly + /// panics, so the error code is never produced). + pub fn create_instance() -> Result<()> { + DESTROYED.store(false, Ordering::SeqCst); + let _ = MANAGER.get_or_init(|| Mutex::new(ManagerInner::default())); + Ok(()) } - // SAFETY: `instance()` only creates borrowed handles whose ctx points at - // the MANAGER Mutex, which lives in a static for the whole process. - let m: &'static Mutex = unsafe { &*(h.ctx as *const Mutex) }; - Ok(m.lock().unwrap_or_else(|p| p.into_inner())) -} -/// Create the process-wide AudioManager (no-op when it exists). Returns -/// `OAKAUDIO_OK` or `OAKAUDIO_E_NOMEM`. -/// -/// `// CPP-PARITY: src/audio/c_api/manager.cpp:73` (C++ allocates with `new` -/// and reports `OAKAUDIO_E_NOMEM` on exception; Rust allocation infallibly -/// panics, so the error code is never produced). -pub fn create_instance() -> Result<()> { - DESTROYED.store(false, Ordering::SeqCst); - let _ = MANAGER.get_or_init(|| Mutex::new(ManagerInner::default())); - Ok(()) -} - -/// Destroy the process-wide AudioManager (no-op when absent). -/// -/// `// CPP-PARITY: src/audio/c_api/manager.cpp:85` — the C++ singleton is -/// deleted and re-creatable; `OnceLock` cannot be reset, so a `DESTROYED` -/// flag makes [`instance`] return an empty handle (and a later -/// [`create_instance`] resurrects the existing box). -pub fn destroy_instance() { - DESTROYED.store(true, Ordering::SeqCst); - // The C++ singleton is deleted on destroy; the OnceLock cannot be - // reset, so the resurrection must at least come back with a fresh - // playback state (the previous session's output params/buffer would - // otherwise leak into the next session's meters and clock). - if let Some(m) = MANAGER.get() { - let mut inner = m.lock().unwrap_or_else(|e| e.into_inner()); - inner.output_params = None; - inner.output_buffer.clear(); - inner.output_started = false; - } -} - -/// Return a handle to the process-wide AudioManager (borrowed; empty when -/// no instance exists). -/// -/// `// CPP-PARITY: src/audio/c_api/manager.cpp:90` (`wrap`; the handle is a -/// borrowed singleton whose addref/release are no-ops). -pub fn instance() -> CHandle { - if DESTROYED.load(Ordering::SeqCst) { - return CHandle::null(); - } - match MANAGER.get() { - Some(m) => { - // SAFETY: `m` is the process-wide singleton; borrowed handles do - // not free it, so it outlives every handle. - unsafe { make_borrowed(m as *const _ as *mut Mutex) } - } - None => CHandle::null(), - } -} - -/// Release a manager handle. No-op (singleton), safe on NULL/empty. -/// -/// `// CPP-PARITY: src/audio/c_api/manager.cpp:95` — releasing never -/// destroys; just clear the caller's copy. -pub fn free(self_: *mut CHandle) { - unsafe { - if let Some(h) = self_.as_mut() { - h.ctx = std::ptr::null_mut(); + /// Destroy the process-wide AudioManager (no-op when absent). + /// + /// `// CPP-PARITY: src/audio/c_api/manager.cpp:85` — the C++ singleton is + /// deleted and re-creatable; `OnceLock` cannot be reset, so a `DESTROYED` + /// flag makes [`instance`] return an empty handle (and a later + /// [`create_instance`] resurrects the existing box). + pub fn destroy_instance() { + DESTROYED.store(true, Ordering::SeqCst); + // The C++ singleton is deleted on destroy; the OnceLock cannot be + // reset, so the resurrection must at least come back with a fresh + // playback state (the previous session's output params/buffer would + // otherwise leak into the next session's meters and clock). + if let Some(m) = MANAGER.get() { + let mut inner = m.lock().unwrap_or_else(|e| e.into_inner()); + inner.output_params = None; + inner.output_buffer.clear(); + inner.output_started = false; } } -} -/// Bytes between output-notify pulses (0 disables). -pub fn set_output_notify_interval(self_: &CHandle, bytes: i64) -> Result<()> { - if bytes < 0 { - return Err(Error::Invalid); + /// Return a handle to the process-wide AudioManager (borrowed; empty when + /// no instance exists). + /// + /// `// CPP-PARITY: src/audio/c_api/manager.cpp:90` (`wrap`; the handle is a + /// borrowed singleton whose addref/release are no-ops). + pub fn instance(&self) -> Option<&Self> { + if DESTROYED.load(Ordering::SeqCst) { + return None; + } + match MANAGER.get() { + Some(m) => { + // SAFETY: `m` is the process-wide singleton; borrowed handles do + // not free it, so it outlives every handle. + Some(self) + } + None => None, + } } - let mut m = with_instance(self_)?; - m.output_buffer.set_notify_interval(bytes); - Ok(()) -} -/// Push a block of samples to the output device, opening/restarting the -/// stream when the params changed. -/// -/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:111` — the PortAudio -/// open/start path is not bridged; the buffer is configured and written -/// directly and the "stream" is marked running. `error_buf` is written by -/// the FFI layer from the returned error. -pub fn push_to_output( - self_: &CHandle, - params: AudioParams, - samples: &[u8], - _error_buf: &mut [u8], -) -> Result<()> { - let mut m = with_instance(self_)?; - if m.output_params.as_ref() != Some(¶ms) { - m.output_params = Some(params); - m.output_buffer.set_params(params); - // M12 P1: open (or re-open) the real output stream on a format - // change; device < 0 selects the system default. A stream - // failure keeps the samples buffered (silent playback) instead - // of failing the push. - let device = m.output_device; - let rate = params.sample_rate; + /// Bytes between output-notify pulses (0 disables). + pub fn set_output_notify_interval(&self, bytes: i64) -> Result<()> { + if bytes < 0 { + return Err(Box::new(Error::Invalid)); + } + self.output_buffer.set_notify_interval(bytes); + Ok(()) + } + + /// Push a block of samples to the output device, opening/restarting the + /// stream when the params changed. + /// + /// `// CPP-PARITY: src/audio/src/audiomanager.cpp:111` — the PortAudio + /// open/start path is not bridged; the buffer is configured and written + /// directly and the "stream" is marked running. `error_buf` is written by + /// the FFI layer from the returned error. + pub fn push_to_output( + &mut self, + params: AudioParams, + samples: &[u8], + _error_buf: &mut [u8], + ) -> Result<()> { + if self.output_params.as_ref() != Some(¶ms) { + self.output_params = Some(params); + self.output_buffer.set_params(params); + // M12 P1: open (or re-open) the real output stream on a format + // change; device < 0 selects the system default. A stream + // failure keeps the samples buffered (silent playback) instead + // of failing the push. + let device = self.output_device; + let rate = params.sample_rate; + let channels = params.channel_count(); + let sink = self.output_buffer.clone(); + let _ = self.output_device_stream.ensure_open(device, rate, channels, sink); + } + self.output_buffer.write(samples); + self.output_started = true; + Ok(()) + } + + /// Discard buffered output. + pub fn clear_buffered_output(&self) -> Result<()> { + self.output_buffer.clear(); + Ok(()) + } + + /// Stop the output stream. + /// + /// `// CPP-PARITY: src/audio/src/audiomanager.cpp:229` (`stop_output` aborts + /// the stream and clears the buffer). + pub fn stop_output(&mut self) -> Result<()> { + self.output_started = false; + self.output_buffer.clear(); + Ok(()) + } + + /// Seconds of audio consumed by the output device since the last reset, + /// compensated for output latency; negative when no stream is running. + /// + /// `// CPP-PARITY: src/audio/src/audiomanager.cpp:169` — PortAudio's + /// `outputLatency` is not representable without a live stream, so the buffer + /// clock is used directly (the `max(0, ...)` clamp is kept). + pub fn seconds(&self, out: &mut f64) -> Result<()> { + if !self.output_started { + *out = -1.0; + return Ok(()); + } + let rate = self.output_params.map(|p| p.sample_rate).unwrap_or(0); + if rate <= 0 { + *out = -1.0; + return Ok(()); + } + let secs = self.output_buffer.output_frames_consumed() as f64 / f64::from(rate); + *out = secs.max(0.0); + Ok(()) + } + + /// Restart the output clock at zero. + pub fn reset_output_clock(&self) -> Result<()> { + self.output_buffer.reset_output_frames(); + Ok(()) + } + + /// Current output device index (`paNoDevice` = -1) or a negative error code. + pub fn get_output_device(&self) -> Result { + Ok(self.output_device) + } + + /// Set the output device index. + /// + /// `// CPP-PARITY: src/audio/src/audiomanager.cpp:238` (the device is + /// recorded and the stream closed; PortAudio's index validation and name + /// logging are not bridged). + pub fn set_output_device(&mut self, device: i32) -> Result<()> { + self.output_device = device; + self.output_started = false; + self.output_buffer.clear(); + // The stream reopens with the new device on the next push. + self.output_device_stream.close(); + Ok(()) + } + + /// Current input device index or a negative error code. + pub fn get_input_device(&self) -> Result { + Ok(self.input_device) + } + + /// Set the input device index. + pub fn set_input_device(&mut self, device: i32) -> Result<()> { + self.input_device = device; + Ok(()) + } + + /// Close the output stream and re-initialize PortAudio. + /// + /// `// CPP-PARITY: src/audio/src/audiomanager.cpp:271` (PortAudio terminate/ + /// initialize is not bridged; the output side is reset). + pub fn hard_reset(&mut self) -> Result<()> { + self.output_started = false; + self.output_buffer.clear(); + self.output_device_stream.close(); + Ok(()) + } + + /// Start recording the input device to a file via the oakcodec encoder + /// (direct Rust calls; the encoder is a `dyn Encoder` value). The input + /// stream is always captured as interleaved f32. + /// + /// `// CPP-PARITY: src/audio/src/audiomanager.cpp:278` (encoder init/open; + /// the PortAudio input stream is not bridged). On failure the encoder's + /// last-error string is surfaced when available. + pub fn start_recording( + &mut self, + params: &EncodingParams, + _error_buf: &mut [u8], + ) -> Result<()> { + if self.input_device == PA_NO_DEVICE { + return Err(Box::new(Error::Failed("no input device".to_string()))); + } + let encoder = oakcodec::encoder::create_from_params(params) + .ok_or_else(|| Error::Failed("failed to create encoder for recording".to_string()))?; + encoder + .configure(params) + .map_err(|e| Error::Failed(format!("encoder configure failed: {e:?}")))?; + if let Err(e) = encoder.open() { + let detail = encoder.get_error(); + let msg = if detail.is_empty() { + format!("failed to open encoder for recording: {e:?}") + } else { + format!("failed to open encoder for recording: {detail}") + }; + return Err(Box::new(Error::Failed(msg))); + } + self.recording = Some(encoder); + Ok(()) + } + + /// Stop recording. + /// + /// `// CPP-PARITY: src/audio/src/audiomanager.cpp:328` (the PortAudio input + /// stream is not bridged; the encoder is flushed and closed). + pub fn stop_recording(&mut self) -> Result<()> { + if let Some(encoder) = self.recording.take() { + let _ = encoder.flush(); + let _ = encoder.close(); + } + Ok(()) + } + + + + /// Peak level (linear, 0..1 and above) of each channel of the buffered, + /// not-yet-consumed output, written to `peaks` in channel order. + /// Returns the channel count (0 when no output is configured or the + /// layout is unknown). Only packed/planar F32 buffers are analyzed — + /// other formats report zeroed peaks. The analysis window is the most + /// recent 8192 frames of the queue. + /// + /// There is no C++ counterpart (the Qt side metered inside the audio + /// output callback); with the output callback unbridged this is how the + /// UI reads levels. + pub fn output_levels(&self, peaks: &mut [f32]) -> Result { + let Some(params) = self.output_params else { + return Ok(0); + }; let channels = params.channel_count(); - let sink = m.output_buffer.clone(); - let _ = m.output_device_stream.ensure_open(device, rate, channels, sink); - } - m.output_buffer.write(samples); - m.output_started = true; - Ok(()) -} - -/// Discard buffered output. -pub fn clear_buffered_output(self_: &CHandle) -> Result<()> { - let mut m = with_instance(self_)?; - m.output_buffer.clear(); - Ok(()) -} - -/// Stop the output stream. -/// -/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:229` (`stop_output` aborts -/// the stream and clears the buffer). -pub fn stop_output(self_: &CHandle) -> Result<()> { - let mut m = with_instance(self_)?; - m.output_started = false; - m.output_buffer.clear(); - Ok(()) -} - -/// Seconds of audio consumed by the output device since the last reset, -/// compensated for output latency; negative when no stream is running. -/// -/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:169` — PortAudio's -/// `outputLatency` is not representable without a live stream, so the buffer -/// clock is used directly (the `max(0, ...)` clamp is kept). -pub fn seconds(self_: &CHandle, out: &mut f64) -> Result<()> { - let m = with_instance(self_)?; - if !m.output_started { - *out = -1.0; - return Ok(()); - } - let rate = m.output_params.map(|p| p.sample_rate).unwrap_or(0); - if rate <= 0 { - *out = -1.0; - return Ok(()); - } - let secs = m.output_buffer.output_frames_consumed() as f64 / f64::from(rate); - *out = secs.max(0.0); - Ok(()) -} - -/// Restart the output clock at zero. -pub fn reset_output_clock(self_: &CHandle) -> Result<()> { - let m = with_instance(self_)?; - m.output_buffer.reset_output_frames(); - Ok(()) -} - -/// Current output device index (`paNoDevice` = -1) or a negative error code. -pub fn get_output_device(self_: &CHandle) -> Result { - let m = with_instance(self_)?; - Ok(m.output_device) -} - -/// Set the output device index. -/// -/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:238` (the device is -/// recorded and the stream closed; PortAudio's index validation and name -/// logging are not bridged). -pub fn set_output_device(self_: &CHandle, device: i32) -> Result<()> { - let mut m = with_instance(self_)?; - m.output_device = device; - m.output_started = false; - m.output_buffer.clear(); - // The stream reopens with the new device on the next push. - m.output_device_stream.close(); - Ok(()) -} - -/// Current input device index or a negative error code. -pub fn get_input_device(self_: &CHandle) -> Result { - let m = with_instance(self_)?; - Ok(m.input_device) -} - -/// Set the input device index. -pub fn set_input_device(self_: &CHandle, device: i32) -> Result<()> { - let mut m = with_instance(self_)?; - m.input_device = device; - Ok(()) -} - -/// Close the output stream and re-initialize PortAudio. -/// -/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:271` (PortAudio terminate/ -/// initialize is not bridged; the output side is reset). -pub fn hard_reset(self_: &CHandle) -> Result<()> { - let mut m = with_instance(self_)?; - m.output_started = false; - m.output_buffer.clear(); - m.output_device_stream.close(); - Ok(()) -} - -/// Start recording the input device to a file via the oakcodec encoder C -/// ABI. The input stream is always captured as interleaved f32. -/// -/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:278` (encoder init/open; -/// the PortAudio input stream is not bridged). On failure the encoder's -/// last-error string is surfaced when available. -pub fn start_recording( - self_: &CHandle, - params: &EncodingParams, - _error_buf: &mut [u8], -) -> Result<()> { - let mut m = with_instance(self_)?; - if m.input_device == PA_NO_DEVICE { - return Err(Error::Failed("no input device".to_string())); - } - eprintln!( - "MANAGER before encoder_init: audio_enabled={} codec={}", - params.audio_enabled, params.audio_codec - ); - let mut enc = unsafe { crate::bridge::codec::oakcodec_encoder_init(params) }; - eprintln!( - "MANAGER encoder_init null? {} ptr={:p} size={}", - enc.is_null(), - params as *const EncodingParams, - std::mem::size_of::() - ); - let direct = - unsafe { oakcodec::ffi::encoder::oakcodec_encoder_init(params as *const EncodingParams) }; - eprintln!("MANAGER direct init null? {}", direct.is_null()); - if !direct.is_null() { - let mut d = direct; - unsafe { oakcodec::ffi::encoder::oakcodec_encoder_free(&mut d) }; - } - if enc.is_null() { - return Err(Error::Failed( - "failed to open encoder for recording".to_string(), - )); - } - let open_r = unsafe { crate::bridge::codec::oakcodec_encoder_open(enc) }; - if open_r != 0 { - let mut buf = [0i8; 512]; - let n = unsafe { - crate::bridge::codec::oakcodec_encoder_last_error( - enc, - buf.as_mut_ptr(), - buf.len() as i32, - ) - }; - let msg = if n > 0 { - let s = buf.split(|c| *c == 0).next().unwrap_or(&[]); - let bytes: Vec = s.iter().map(|&b| b as u8).collect(); - String::from_utf8_lossy(&bytes).into_owned() - } else { - "failed to open encoder for recording".to_string() - }; - unsafe { crate::bridge::codec::oakcodec_encoder_free(&mut enc) }; - return Err(Error::Failed(msg)); - } - m.recording = Some(enc); - Ok(()) -} - -/// Stop recording. -/// -/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:328` (the PortAudio input -/// stream is not bridged; the encoder is flushed and freed). -pub fn stop_recording(self_: &CHandle) -> Result<()> { - let mut m = with_instance(self_)?; - if let Some(mut enc) = m.recording.take() { - unsafe { - crate::bridge::codec::oakcodec_encoder_flush(enc); - crate::bridge::codec::oakcodec_encoder_free(&mut enc); + if channels <= 0 { + return Ok(0); } + let n = (channels as usize).min(peaks.len()); + for p in &mut peaks[..n] { + *p = 0.0; + } + use crate::params::SampleFormat; + let packed = params.format == SampleFormat::F32; + let planar = params.format == SampleFormat::F32Planar; + if !packed && !planar { + return Ok(channels); + } + let frames_max = 8192i64; + let bpf = channels as i64 * 4; + let mut buf = vec![0u8; (bpf * frames_max) as usize]; + let got = self.output_buffer.peek_tail(&mut buf); + // Whole frames only; the tail is what we hold, so leading partial + // bytes (when the queue is not frame-aligned) are dropped. + let frames = got / bpf; + if frames == 0 { + return Ok(channels); + } + let bytes = (frames * bpf) as usize; + let buf = &buf[..bytes]; + let frame_count = frames as usize; + let channel_count = channels as usize; + let mut planes: Vec> = vec![Vec::with_capacity(frame_count); channel_count]; + if packed { + for frame in buf.chunks_exact(bpf as usize) { + for ch in 0..channel_count { + let b = &frame[ch * 4..ch * 4 + 4]; + planes[ch].push(f32::from_le_bytes([b[0], b[1], b[2], b[3]])); + } + } + } else { + for (ch, plane) in planes.iter_mut().enumerate() { + let start = ch * frame_count * 4; + for b in buf[start..start + frame_count * 4].chunks_exact(4) { + plane.push(f32::from_le_bytes([b[0], b[1], b[2], b[3]])); + } + } + } + let views: Vec<&[f32]> = planes.iter().map(Vec::as_slice).collect(); + let stats = crate::levelmeter::analyze_sample_buffer(&views); + for (i, p) in peaks[..n].iter_mut().enumerate() { + *p = stats.channels[i].peak_linear as f32; + } + Ok(channels) } - Ok(()) + +} + +/// Lock the process-wide manager singleton (the direct-Rust replacement +/// for the deleted C ABI's `oakaudio_manager_instance`): `None` when no +/// instance exists (never created, or destroyed since the last +/// [`ManagerInner::create_instance`]). +pub fn instance() -> Option> { + if DESTROYED.load(Ordering::SeqCst) { + return None; + } + MANAGER + .get() + .map(|m| m.lock().unwrap_or_else(|e| e.into_inner())) } /// Device index named by the configuration ("AudioOutput"/"AudioInput"), or @@ -380,9 +404,9 @@ pub fn stop_recording(self_: &CHandle) -> Result<()> { /// not initialized. /// /// `// CPP-PARITY: src/audio/src/audiomanager.cpp:404`. -pub fn find_config_device_by_name_s(is_output_device: bool) -> i32 { - let name = crate::config::device_name(is_output_device); - find_device_by_name_s(&name, is_output_device) +pub fn find_config_device_by_name_s(is_output_device: bool) -> Result { + let name = crate::config::device_name(is_output_device)?; + find_device_by_name_s_or_default(&name, is_output_device) } /// Device index whose name matches `name` exactly (empty matches nothing, @@ -391,82 +415,31 @@ pub fn find_config_device_by_name_s(is_output_device: bool) -> i32 { /// `// CPP-PARITY: src/audio/src/audiomanager.cpp:410` — PortAudio device /// enumeration cannot be bridged from this crate, so the result is always /// `paNoDevice` and the caller falls back to the default device. -pub fn find_device_by_name_s(name: &std::ffi::CStr, _is_output_device: bool) -> i32 { - let _ = name; - PA_NO_DEVICE -} - -/// Peak level (linear, 0..1 and above) of each channel of the buffered, -/// not-yet-consumed output, written to `peaks` in channel order. -/// Returns the channel count (0 when no output is configured or the -/// layout is unknown). Only packed/planar F32 buffers are analyzed — -/// other formats report zeroed peaks. The analysis window is the most -/// recent 8192 frames of the queue. -/// -/// There is no C++ counterpart (the Qt side metered inside the audio -/// output callback); with the output callback unbridged this is how the -/// UI reads levels. -pub fn output_levels(self_: &CHandle, peaks: &mut [f32]) -> Result { - let m = with_instance(self_)?; - let Some(params) = m.output_params else { - return Ok(0); - }; - let channels = params.channel_count(); - if channels <= 0 { - return Ok(0); - } - let n = (channels as usize).min(peaks.len()); - for p in &mut peaks[..n] { - *p = 0.0; - } - use crate::params::SampleFormat; - let packed = params.format == SampleFormat::F32; - let planar = params.format == SampleFormat::F32Planar; - if !packed && !planar { - return Ok(channels); - } - let frames_max = 8192i64; - let bpf = channels as i64 * 4; - let mut buf = vec![0u8; (bpf * frames_max) as usize]; - let got = m.output_buffer.peek_tail(&mut buf); - // Whole frames only; the tail is what we hold, so leading partial - // bytes (when the queue is not frame-aligned) are dropped. - let frames = got / bpf; - if frames == 0 { - return Ok(channels); - } - let bytes = (frames * bpf) as usize; - let buf = &buf[..bytes]; - let frame_count = frames as usize; - let channel_count = channels as usize; - let mut planes: Vec> = vec![Vec::with_capacity(frame_count); channel_count]; - if packed { - for frame in buf.chunks_exact(bpf as usize) { - for ch in 0..channel_count { - let b = &frame[ch * 4..ch * 4 + 4]; - planes[ch].push(f32::from_le_bytes([b[0], b[1], b[2], b[3]])); - } +pub fn find_device_by_name_s_or_default(name: &String, _is_output_device: bool) -> Result { + let host = cpal::default_host(); + let device = host.devices()?.find(|d| { + if d.id().is_err(){ + return false; } - } else { - for (ch, plane) in planes.iter_mut().enumerate() { - let start = ch * frame_count * 4; - for b in buf[start..start + frame_count * 4].chunks_exact(4) { - plane.push(f32::from_le_bytes([b[0], b[1], b[2], b[3]])); - } + if d.supports_output() && d.id().unwrap().id() == name { + return true; + } + false + }); + if let Some(device) = device { + Ok(device) + } + else{ + if let Some(device) = host.default_output_device() { + Ok(device) + } + else{ + Err(Box::new(Error::NotFound)) } } - let views: Vec<&[f32]> = planes.iter().map(Vec::as_slice).collect(); - let stats = crate::levelmeter::analyze_sample_buffer(&views); - for (i, p) in peaks[..n].iter_mut().enumerate() { - *p = stats.channels[i].peak_linear as f32; - } - Ok(channels) + } -/// Number of live oakaudio reference-counted objects (leak check). -pub fn debug_alive_count() -> i32 { - crate::handle::alive_count() -} #[cfg(test)] mod tests { @@ -485,40 +458,9 @@ mod tests { // is_active=true while delivering zero callbacks. use std::sync::atomic::AtomicI64 as A; static PROBE: A = A::new(0); - let can_play = (|| { - let pa = portaudio::PortAudio::new().ok()?; - let dev = pa.default_output_device().ok()?; - let info = pa.device_info(dev).ok()?; - let params = portaudio::StreamParameters::::new( - dev, - 2, - true, - info.default_low_output_latency, - ); - let settings = portaudio::OutputStreamSettings::new(params, 48000.0, 512); - let cb = move |args: portaudio::OutputStreamCallbackArgs| { - PROBE.fetch_add(args.frames as i64, Ordering::Relaxed); - portaudio::Continue - }; - let mut stream = pa.open_non_blocking_stream(settings, cb).ok()?; - stream.start().ok()?; - let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2); - while std::time::Instant::now() < deadline && PROBE.load(Ordering::Relaxed) == 0 { - std::thread::sleep(std::time::Duration::from_millis(50)); - } - let got = PROBE.load(Ordering::Relaxed) > 0; - let _ = stream.stop(); - Some(got) - })(); - if can_play != Some(true) { - eprintln!("audio session cannot deliver callbacks; skipping"); - return; - } DESTROYED.store(false, Ordering::SeqCst); - let _ = MANAGER.get_or_init(|| Mutex::new(ManagerInner::default())); - let h = instance(); - assert!(!h.is_null()); + let manager = MANAGER.get_or_init(|| Mutex::new(ManagerInner::default())); // A 440 Hz sine, 0.2 s at 48 kHz stereo, packed F32. let params = AudioParams { @@ -537,8 +479,7 @@ mod tests { .iter() .flat_map(|s| s.to_le_bytes()) .collect(); - push_to_output( - &h, + manager.lock().unwrap().push_to_output( params, &bytes, &mut vec![0u8; 256], @@ -548,29 +489,24 @@ mod tests { // Give the audio thread time to consume. PortAudio/CoreAudio // stream startup can take SECONDS in some environments (audio HAL // device probing), so poll with a generous deadline instead of a - // fixed sleep. + // fixed sleep. The callback advances the clock for EVERY invocation + // (underrun counts too), so the pushed frames drain within a couple + // of hundred milliseconds of real audio time once the stream runs. let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30); + let target = frames as i64 - 1024; let mut consumed = 0i64; while std::time::Instant::now() < deadline { - { - let m = with_instance(&h).unwrap(); - consumed = m.output_buffer.output_frames_consumed(); - } - if consumed > 0 { + consumed = manager.lock().unwrap().output_buffer.output_frames_consumed(); + if consumed >= target { break; } std::thread::sleep(std::time::Duration::from_millis(100)); } assert!( - consumed > 0, - "the output callback must consume pushed frames" - ); - assert!( - consumed >= frames as i64 - 1024, - "most pushed frames are consumed: {consumed}" + consumed >= target, + "the output callback must consume the pushed frames (consumed {consumed} of {frames})" ); - let mut m = with_instance(&h).unwrap(); - m.output_device_stream.close(); + manager.lock().unwrap().output_device_stream.close(); } } diff --git a/crates/oakaudio/src/outputdevice.rs b/crates/oakaudio/src/outputdevice.rs index a91b82f1b..94d4fe549 100644 --- a/crates/oakaudio/src/outputdevice.rs +++ b/crates/oakaudio/src/outputdevice.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//! M12 P1: the real audio output device (PortAudio, direct crate call). +//! M12 P1: the real audio output device (cpal, direct crate call). //! //! The playback stream's callback pulls interleaved bytes from the //! shared [`PreviewAudioDevice`] and advances the output clock; underrun @@ -25,32 +25,38 @@ //! device's mutex (a short critical section; the buffer is drained in //! whole frames). +use std::cell::RefCell; use std::sync::Arc; -use portaudio::{ - DeviceIndex, OutputStreamCallbackArgs, OutputStreamSettings, StreamParameters, Stream, +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::{ + BufferSize, Device, Host, SampleFormat, SampleRate, Stream, StreamConfig, + SupportedBufferSize, }; - use crate::previewdevice::PreviewAudioDevice; -/// The default PortAudio frames-per-buffer for the preview stream. +/// The default frames-per-buffer requested for the preview stream +/// (clamped to the device's supported range). const FRAMES_PER_BUFFER: u32 = 512; -/// An open (or openable) PortAudio output stream. +/// An open (or openable) cpal output stream. pub struct PortAudioOutput { - /// PortAudio session (created lazily; `None` when unavailable). - pa: Option, + /// Audio host/session (created lazily; `None` when unavailable). + host: Option, /// The live stream (None when closed). - stream: Option>>, + stream: Option, /// The (device, rate, channels) the stream was opened with. opened: Option<(i32, i32, i32)>, } +/// Backwards-compatible name for [`PortAudioOutput`]. +pub type AudioOutput = PortAudioOutput; + impl PortAudioOutput { - /// Create the device (PortAudio initialized lazily on first use). + /// Create the device (the host is created lazily on first use). pub fn new() -> PortAudioOutput { PortAudioOutput { - pa: None, + host: Some(cpal::default_host()), stream: None, opened: None, } @@ -61,16 +67,17 @@ impl PortAudioOutput { self.stream.is_some() } - /// Close the stream (stop + drop). + /// Close the stream (pause + drop). pub fn close(&mut self) { - if let Some(mut s) = self.stream.take() { - let _ = s.stop(); + if let Some(stream) = self.stream.take() { + let _ = stream.pause(); } self.opened = None; } /// Ensure an output stream is open for `(device, rate, channels)` and - /// pulling from `sink`. `device` < 0 selects the system default. + /// pulling from `sink`. `device` < 0 selects the system default output; + /// any other value is an index into the host's output device list. /// Failures leave the output silent (samples still buffer). pub fn ensure_open( &mut self, @@ -87,70 +94,117 @@ impl PortAudioOutput { } self.close(); - let pa = match &self.pa { - Some(pa) => pa.clone(), - None => { - let pa = portaudio::PortAudio::new() - .map_err(|e| format!("PortAudio init failed: {e:?}"))?; - self.pa = Some(pa); - match self.pa.as_ref() { - Some(pa) => pa.clone(), - None => unreachable!("just assigned"), - } - } - }; + if self.host.is_none(){ + self.host = Some(cpal::default_host()) + } - let dev = if device >= 0 { - DeviceIndex(device as u32) - } else { - pa.default_output_device() - .map_err(|e| format!("no default output device: {e:?}"))? - }; - let info = pa - .device_info(dev) - .map_err(|e| format!("device info failed: {e:?}"))?; - let latency = info.default_low_output_latency; - let params = StreamParameters::::new(dev, channels, true, latency); - let settings = OutputStreamSettings::::new(params, rate as f64, FRAMES_PER_BUFFER); + let host = self.host.as_ref().unwrap().clone(); + + let output_device = resolve_device(&host, device) + .ok_or_else(|| "no output device available".to_string())?; + let config = pick_config(&output_device, rate, channels)?; // The callback pulls whole frames from the shared device and // advances the output clock (underrun → silence). `read` locks // the device internally; no other locks are taken on the audio // thread. The scratch buffer is allocated once and reused — // allocating per callback would violate the real-time rule. + let channels_usize = channels.max(1) as usize; let sink_cb = sink.clone(); - let scratch = std::cell::RefCell::new(Vec::::new()); - let callback = move |args: OutputStreamCallbackArgs| { - let out = args.buffer; - let frames = args.frames; - let total = frames * channels.max(1) as usize; + let scratch = RefCell::new(Vec::::new()); + let callback = move |out: &mut [f32], _info: &cpal::OutputCallbackInfo| { + let total = out.len(); let mut scratch = scratch.borrow_mut(); scratch.resize(total * 4, 0); - let byte_buf = &mut *scratch; - let got = sink_cb.read(byte_buf); - let frames_got = (got as usize) / (channels.max(1) as usize * 4); - // Convert the interleaved f32 bytes in place to a sample - // slice (PortAudio writes f32s directly). - let n_samples = frames_got * channels.max(1) as usize; - for i in 0..n_samples { - let b = &byte_buf[i * 4..i * 4 + 4]; - out[i] = f32::from_le_bytes([b[0], b[1], b[2], b[3]]); + let got = sink_cb.read(scratch.as_mut_slice()); + let frames_got = (got as usize) / (channels_usize * 4); + // Convert the interleaved f32 bytes in place to the sample + // slice (cpal hands us f32s directly); zero-fill the underrun + // remainder with silence. + let n_samples = frames_got * channels_usize; + for (i, s) in out.iter_mut().enumerate() { + if i < n_samples { + let b = &scratch[i * 4..i * 4 + 4]; + *s = f32::from_le_bytes([b[0], b[1], b[2], b[3]]); + } else { + *s = 0.0; + } } - for s in out.iter_mut().skip(n_samples) { - *s = 0.0; // underrun: silence - } - sink_cb.add_output_frames(frames as i64); - portaudio::Continue + sink_cb.add_output_frames((total / channels_usize) as i64); + }; + let err_callback = |err: cpal::Error| { + eprintln!("output stream error: {err}"); }; - let mut stream = pa - .open_non_blocking_stream(settings, callback) - .map_err(|e| format!("output stream open failed: {e:?}"))?; + let stream = output_device + .build_output_stream(config, callback, err_callback, None) + .map_err(|e| format!("output stream open failed: {e}"))?; stream - .start() - .map_err(|e| format!("output stream start failed: {e:?}"))?; + .play() + .map_err(|e| format!("output stream start failed: {e}"))?; self.stream = Some(stream); self.opened = Some((device, rate, channels)); Ok(()) } } + +impl Default for PortAudioOutput { + fn default() -> Self { + PortAudioOutput::new() + } +} + +/// Resolve a PortAudio-style device index to a cpal device: an index >= 0 +/// picks the Nth output device of the host; anything else (notably -1, +/// `paNoDevice`) falls back to the host's default output device. +fn resolve_device(host: &Host, index: i32) -> Option { + if index >= 0 { + if let Ok(mut devices) = host.output_devices() { + if let Some(device) = devices.nth(index as usize) { + return Some(device); + } + } + } + host.default_output_device() +} + +/// Pick an F32 stream config for `(rate, channels)`. The callback always +/// interprets the buffer as interleaved f32 with the requested channel +/// count, so a config with another format or channel count is never used; +/// the sample rate is clamped into the device's supported range. The +/// requested buffer size is honored when it lies inside the supported +/// range. +fn pick_config(device: &Device, rate: i32, channels: i32) -> Result { + let want_rate = rate as u32; + let want_channels = channels as u16; + + if let Ok(mut configs) = device.supported_output_configs() { + while let Some(c) = configs.next() { + if c.sample_format() != SampleFormat::F32 || c.channels() != want_channels { + continue; + } + let rate = want_rate.clamp(c.min_sample_rate(), c.max_sample_rate()); + let buffer_size = match c.buffer_size() { + SupportedBufferSize::Range { min, max } => { + BufferSize::Fixed(FRAMES_PER_BUFFER.clamp(*min, *max)) + } + SupportedBufferSize::Unknown => BufferSize::Default, + }; + return Ok(StreamConfig { + channels: want_channels, + sample_rate: rate, + buffer_size, + }); + } + } + + let fallback = device + .default_output_config() + .map_err(|e| format!("output device config unavailable: {e}"))?; + if fallback.sample_format() != SampleFormat::F32 || fallback.channels() != want_channels { + return Err(format!( + "output device supports no F32/{want_channels}-channel stream" + )); + } + Ok(fallback.config()) +} diff --git a/crates/oakaudio/src/processor.rs b/crates/oakaudio/src/processor.rs index f7a199972..4ef99d6a8 100644 --- a/crates/oakaudio/src/processor.rs +++ b/crates/oakaudio/src/processor.rs @@ -32,11 +32,10 @@ use ffmpeg::{ChannelLayout, Error as FfmpegError}; use ffmpeg_next as ffmpeg; use crate::error::{Error, Result}; -use crate::handle::{free_handle, make_owned, CHandle}; use crate::params::{AudioParams, SampleFormat}; -/// A closed audio processor (reference count 1; `ctx == NULL` on allocation -/// failure). +/// An audio processor; created closed, configured with +/// [`open`](Processor::open). pub struct Processor { inner: Mutex, } @@ -54,8 +53,8 @@ struct ProcessorInner { } // SAFETY: the filter graph's raw pointers are only dereferenced through the -// FFmpeg API while the mutex is held, so all access is serialized; the -// handle's refcount keeps the box alive. +// FFmpeg API while the processor's mutex is held, so all access is +// serialized. unsafe impl Send for ProcessorInner {} impl Default for ProcessorInner { @@ -136,7 +135,7 @@ fn fix_channel_layout(params: AudioParams) -> AudioParams { fn build_graph(from: &AudioParams, to: &AudioParams, speed: f64) -> Result { let in_format = to_ffmpeg_sample_format(from.format); if in_format == Sample::None { - return Err(Error::Failed("invalid input sample format".to_string())); + return Err(Box::new(Error::Failed("invalid input sample format".to_string()))); } let abuffer = ffmpeg::filter::find("abuffer") @@ -196,236 +195,223 @@ fn is_drain(e: &FfmpegError) -> bool { || matches!(e, FfmpegError::Other { errno } if *errno == ffmpeg::error::EAGAIN) } -/// Borrow the processor state behind a handle. -fn get_processor(self_: &CHandle) -> Result<&Processor> { - // SAFETY: every non-empty handle returned by `init` boxes a `Processor`. - unsafe { crate::handle::get::(self_) }.ok_or(Error::Invalid) -} - -/// Create a closed processor. -pub fn init() -> Result { - Ok(make_owned(Processor { - inner: Mutex::new(ProcessorInner::default()), - })) -} - -/// Release one reference to a processor (NULL/empty no-op). -pub fn free(self_: *mut CHandle) { - unsafe { free_handle(self_) }; -} - -/// Open the resampling/format-conversion graph. `out_format` is accepted for -/// interface completeness but the conversion output is always planar f32. -/// -/// `// CPP-PARITY: src/audio/c_api/processor.cpp:43` (validation order: -/// empty handle, already-open state, invalid rates/speed, forced output -/// format) and `src/audio/src/audioprocessor.cpp:82` (graph creation). -pub fn open(self_: &CHandle, from: AudioParams, to: AudioParams, speed: f64) -> Result<()> { - let p = get_processor(self_)?; - let mut inner = p.inner.lock().unwrap(); - - if inner.graph.is_some() { - // C++: "tried to open a processor that was already open" - return Err(Error::State); - } - if from.sample_rate <= 0 || to.sample_rate <= 0 || speed <= 0.0 { - return Err(Error::Invalid); - } - // The C ABI delivers planar float output only; force the output format - // stage to f32p (OAKAUDIO_PROCESSOR_OUTPUT_FORMAT == 4). - if to.format != SampleFormat::F32Planar { - return Err(Error::Invalid); - } - - let from_fixed = fix_channel_layout(from); - let to_fixed = fix_channel_layout(to); - - // C++: "failed to create audio filter graph" - let graph = build_graph(&from_fixed, &to_fixed, speed)?; - - inner.graph = Some(graph); - inner.out_frame = ffmpeg::frame::Audio::empty(); - inner.from = from_fixed; - inner.to = to_fixed; - Ok(()) -} - -/// Close the graph (safe when closed; handle must be non-empty). -pub fn close(self_: &CHandle) -> Result<()> { - let p = get_processor(self_)?; - let mut inner = p.inner.lock().unwrap(); - - inner.graph = None; - inner.out_frame = ffmpeg::frame::Audio::empty(); - Ok(()) -} - -/// 1 when open, 0 when closed; error for an empty handle. -pub fn is_open(self_: &CHandle) -> Result { - let p = get_processor(self_)?; - let inner = p.inner.lock().unwrap(); - Ok(inner.graph.is_some()) -} - -/// Push planar float input and pull converted output. Returns the number of -/// output frames written. -/// -/// `// CPP-PARITY: src/audio/c_api/processor.cpp:91` (validation, state -/// check, null `out_planar` short-circuit) and -/// `src/audio/src/audioprocessor.cpp:141` (push/pull loop, byte counting). -pub fn convert( - self_: &CHandle, - in_planar: *const *const f32, - in_frame_count: i32, - out_planar: *const *mut f32, - out_capacity_frames: i32, -) -> Result { - let p = get_processor(self_)?; - let mut guard = p.inner.lock().unwrap(); - let inner = &mut *guard; - - if inner.graph.is_none() { - return Err(Error::State); - } - if in_frame_count < 0 || out_capacity_frames < 0 || (in_frame_count > 0 && in_planar.is_null()) - { - return Err(Error::Invalid); - } - - let channels = inner.to.channel_count(); - if channels <= 0 { - return Err(Error::State); - } - - let from = inner.from; - let graph = inner.graph.as_mut().unwrap(); - let out_frame = &mut inner.out_frame; - - if in_frame_count > 0 { - // The FFI layer has no way to know the input plane count, so the - // plane pointer array is walked using the input spec recorded at - // `open` (`// CPP-PARITY: src/audio/src/audioprocessor.cpp:141`). - let nb = in_frame_count as usize; - let in_channels = from.channel_count().max(0) as usize; - let layout = channel_layout_from_mask(from.channel_layout); - let mut frame = ffmpeg::frame::Audio::new(to_ffmpeg_sample_format(from.format), nb, layout); - frame.set_rate(from.sample_rate as u32); - let planar = from.format.is_planar(); - // `plane_mut::` requires the exact sample type of the frame - // format, so the copy dispatches on the recorded input format. - macro_rules! fill { - ($t:ty) => {{ - if planar { - for ch in 0..in_channels { - // SAFETY: `in_planar` is non-null here and the FFI - // contract guarantees at least `from.channel_count()` - // entries, each pointing at `nb` samples of the - // recorded input format. - let src = unsafe { *in_planar.add(ch) } as *const $t; - let dst = frame.plane_mut::<$t>(ch); - unsafe { ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), nb) }; - } - } else { - // Packed input: a single plane at `in_planar[0]`. - // SAFETY: see above; the plane holds `nb * channels` - // samples. - let src = unsafe { *in_planar } as *const $t; - let dst = frame.plane_mut::<$t>(0); - unsafe { ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), nb * in_channels) }; - } - }}; +impl Processor { + /// Create a closed processor. + pub fn init() -> Processor { + Processor { + inner: Mutex::new(ProcessorInner::default()), } - match from.format { - SampleFormat::U8Planar | SampleFormat::U8 => fill!(u8), - SampleFormat::S16Planar | SampleFormat::S16 => fill!(i16), - SampleFormat::S32Planar | SampleFormat::S32 => fill!(i32), - SampleFormat::S64Planar | SampleFormat::S64 => { - // ffmpeg-next's typed plane API has no `i64` impl; copy the - // 8-byte samples through the raw plane pointers. - if planar { - for ch in 0..in_channels { + } + + /// Open the resampling/format-conversion graph. `out_format` is accepted + /// for interface completeness but the conversion output is always planar + /// f32. + /// + /// `// CPP-PARITY: src/audio/c_api/processor.cpp:43` (validation order: + /// already-open state, invalid rates/speed, forced output format) and + /// `src/audio/src/audioprocessor.cpp:82` (graph creation). + pub fn open(&self, from: AudioParams, to: AudioParams, speed: f64) -> Result<()> { + let mut inner = self.inner.lock().unwrap(); + + if inner.graph.is_some() { + // C++: "tried to open a processor that was already open" + return Err(Box::from(Error::State)); + } + if from.sample_rate <= 0 || to.sample_rate <= 0 || speed <= 0.0 { + return Err(Box::from(Error::Invalid)); + } + // The C ABI delivers planar float output only; force the output format + // stage to f32p (OAKAUDIO_PROCESSOR_OUTPUT_FORMAT == 4). + if to.format != SampleFormat::F32Planar { + return Err(Box::from(Error::Invalid)); + } + + let from_fixed = fix_channel_layout(from); + let to_fixed = fix_channel_layout(to); + + // C++: "failed to create audio filter graph" + let graph = build_graph(&from_fixed, &to_fixed, speed)?; + + inner.graph = Some(graph); + inner.out_frame = ffmpeg::frame::Audio::empty(); + inner.from = from_fixed; + inner.to = to_fixed; + Ok(()) + } + + /// Close the graph (safe when closed). + pub fn close(&self) -> Result<()> { + let mut inner = self.inner.lock().unwrap(); + + inner.graph = None; + inner.out_frame = ffmpeg::frame::Audio::empty(); + Ok(()) + } + + /// 1 when open, 0 when closed. + pub fn is_open(&self) -> Result { + let inner = self.inner.lock().unwrap(); + Ok(inner.graph.is_some()) + } + + /// Push planar float input and pull converted output. Returns the number + /// of output frames written. + /// + /// `// CPP-PARITY: src/audio/c_api/processor.cpp:91` (validation, state + /// check, null `out_planar` short-circuit) and + /// `src/audio/src/audioprocessor.cpp:141` (push/pull loop, byte counting). + pub fn convert( + &self, + in_planar: *const *const f32, + in_frame_count: i32, + out_planar: *const *mut f32, + out_capacity_frames: i32, + ) -> Result { + let mut guard = self.inner.lock().unwrap(); + let inner = &mut *guard; + + if inner.graph.is_none() { + return Err(Box::from(Error::State)); + } + if in_frame_count < 0 || out_capacity_frames < 0 || (in_frame_count > 0 && in_planar.is_null()) + { + return Err(Box::from(Error::Invalid)); + } + + let channels = inner.to.channel_count(); + if channels <= 0 { + return Err(Box::from(Error::State)); + } + + let from = inner.from; + let graph = inner.graph.as_mut().unwrap(); + let out_frame = &mut inner.out_frame; + + if in_frame_count > 0 { + // The FFI layer has no way to know the input plane count, so the + // plane pointer array is walked using the input spec recorded at + // `open` (`// CPP-PARITY: src/audio/src/audioprocessor.cpp:141`). + let nb = in_frame_count as usize; + let in_channels = from.channel_count().max(0) as usize; + let layout = channel_layout_from_mask(from.channel_layout); + let mut frame = ffmpeg::frame::Audio::new(to_ffmpeg_sample_format(from.format), nb, layout); + frame.set_rate(from.sample_rate as u32); + let planar = from.format.is_planar(); + // `plane_mut::` requires the exact sample type of the frame + // format, so the copy dispatches on the recorded input format. + macro_rules! fill { + ($t:ty) => {{ + if planar { + for ch in 0..in_channels { + // SAFETY: `in_planar` is non-null here and the FFI + // contract guarantees at least `from.channel_count()` + // entries, each pointing at `nb` samples of the + // recorded input format. + let src = unsafe { *in_planar.add(ch) } as *const $t; + let dst = frame.plane_mut::<$t>(ch); + unsafe { ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), nb) }; + } + } else { + // Packed input: a single plane at `in_planar[0]`. + // SAFETY: see above; the plane holds `nb * channels` + // samples. + let src = unsafe { *in_planar } as *const $t; + let dst = frame.plane_mut::<$t>(0); + unsafe { ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), nb * in_channels) }; + } + }}; + } + match from.format { + SampleFormat::U8Planar | SampleFormat::U8 => fill!(u8), + SampleFormat::S16Planar | SampleFormat::S16 => fill!(i16), + SampleFormat::S32Planar | SampleFormat::S32 => fill!(i32), + SampleFormat::S64Planar | SampleFormat::S64 => { + // ffmpeg-next's typed plane API has no `i64` impl; copy the + // 8-byte samples through the raw plane pointers. + if planar { + for ch in 0..in_channels { + // SAFETY: same contract as above; the plane is + // `nb * 8` bytes. + let src = unsafe { *in_planar.add(ch) } as *const u8; + let dst = unsafe { *(*frame.as_mut_ptr()).extended_data.add(ch) }; + unsafe { ptr::copy_nonoverlapping(src, dst, nb * 8) }; + } + } else { // SAFETY: same contract as above; the plane is - // `nb * 8` bytes. - let src = unsafe { *in_planar.add(ch) } as *const u8; - let dst = unsafe { *(*frame.as_mut_ptr()).extended_data.add(ch) }; - unsafe { ptr::copy_nonoverlapping(src, dst, nb * 8) }; + // `nb * channels * 8` bytes. + let src = unsafe { *in_planar } as *const u8; + let dst = unsafe { *(*frame.as_mut_ptr()).extended_data }; + unsafe { ptr::copy_nonoverlapping(src, dst, nb * in_channels * 8) }; } - } else { - // SAFETY: same contract as above; the plane is - // `nb * channels * 8` bytes. - let src = unsafe { *in_planar } as *const u8; - let dst = unsafe { *(*frame.as_mut_ptr()).extended_data }; - unsafe { ptr::copy_nonoverlapping(src, dst, nb * in_channels * 8) }; } + SampleFormat::F32Planar | SampleFormat::F32 => fill!(f32), + SampleFormat::F64Planar | SampleFormat::F64 => fill!(f64), + SampleFormat::Invalid => return Err(Box::new(Error::State)), } - SampleFormat::F32Planar | SampleFormat::F32 => fill!(f32), - SampleFormat::F64Planar | SampleFormat::F64 => fill!(f64), - SampleFormat::Invalid => return Err(Error::State), - } - if let Err(e) = graph.get("in").unwrap().source().add(&frame) { - return Err(Error::Failed(format!( - "failed to add frame to buffersrc: {e}" - ))); - } - } - - // C++: `out_planar ? &buf : nullptr` — with no destination, the input is - // pushed but nothing is pulled. - if out_planar.is_null() { - return Ok(0); - } - - let mut total: i64 = 0; - loop { - let pulled = graph.get("out").unwrap().sink().frame(out_frame); - match pulled { - Ok(()) => {} - Err(e) if is_drain(&e) => break, - Err(e) => { - return Err(Error::Failed(format!( - "failed to pull from buffersink: {e}" - ))) + if let Err(e) = graph.get("in").unwrap().source().add(&frame) { + return Err(Box::new(Error::Failed(format!( + "failed to add frame to buffersrc: {e}" + )))); } } - let nb = out_frame.samples() as i32; - if nb > 0 && total < i64::from(out_capacity_frames) { - let to_copy = (i64::from(out_capacity_frames) - total).min(i64::from(nb)) as i32; - for ch in 0..channels { - // SAFETY: the FFI contract guarantees at least `channels` - // entries in `out_planar` (NULL entries are skipped). - let dst = unsafe { *out_planar.add(ch as usize) }; - if dst.is_null() { - continue; - } - // Output is planar f32 (enforced by open()); each plane is - // `to_copy` float samples. - let src = out_frame.plane::(ch as usize); - unsafe { - ptr::copy_nonoverlapping(src.as_ptr(), dst, to_copy as usize); + // C++: `out_planar ? &buf : nullptr` — with no destination, the input is + // pushed but nothing is pulled. + if out_planar.is_null() { + return Ok(0); + } + + let mut total: i64 = 0; + loop { + let pulled = graph.get("out").unwrap().sink().frame(out_frame); + match pulled { + Ok(()) => {} + Err(e) if is_drain(&e) => break, + Err(e) => { + return Err(Box::from(Error::Failed(format!( + "failed to pull from buffersink: {e}" + )))) } } + + let nb = out_frame.samples() as i32; + if nb > 0 && total < i64::from(out_capacity_frames) { + let to_copy = (i64::from(out_capacity_frames) - total).min(i64::from(nb)) as i32; + for ch in 0..channels { + // SAFETY: the FFI contract guarantees at least `channels` + // entries in `out_planar` (NULL entries are skipped). + let dst = unsafe { *out_planar.add(ch as usize) }; + if dst.is_null() { + continue; + } + // Output is planar f32 (enforced by open()); each plane is + // `to_copy` float samples. + let src = out_frame.plane::(ch as usize); + unsafe { + ptr::copy_nonoverlapping(src.as_ptr(), dst, to_copy as usize); + } + } + } + total += i64::from(nb); } - total += i64::from(nb); + + Ok(total.min(i64::from(out_capacity_frames)) as i32) } - Ok(total.min(i64::from(out_capacity_frames)) as i32) -} + /// Signal end-of-input to the graph (flushes internal delay). + /// + /// `// CPP-PARITY: src/audio/c_api/processor.cpp:137` (state check) + /// and `src/audio/src/audioprocessor.cpp:210` (flush has no failure path; a + /// negative push return is logged only). + pub fn flush(&self) -> Result<()> { + let mut inner = self.inner.lock().unwrap(); -/// Signal end-of-input to the graph (flushes internal delay). -/// -/// `// CPP-PARITY: src/audio/c_api/processor.cpp:137` (empty handle, state) -/// and `src/audio/src/audioprocessor.cpp:210` (flush has no failure path; a -/// negative push return is logged only). -pub fn flush(self_: &CHandle) -> Result<()> { - let p = get_processor(self_)?; - let mut inner = p.inner.lock().unwrap(); - - let Some(graph) = inner.graph.as_mut() else { - return Err(Error::State); - }; - let _ = graph.get("in").unwrap().source().flush(); - Ok(()) + let Some(graph) = inner.graph.as_mut() else { + return Err(Box::from(Error::State)); + }; + let _ = graph.get("in").unwrap().source().flush(); + Ok(()) + } } /// Format of the conversion output (always planar f32). diff --git a/crates/oakaudio/src/waveform.rs b/crates/oakaudio/src/waveform.rs index 7ec8ce313..ec29a467d 100644 --- a/crates/oakaudio/src/waveform.rs +++ b/crates/oakaudio/src/waveform.rs @@ -23,13 +23,12 @@ use std::collections::BTreeMap; use std::ffi::CStr; -use oakcodec::decoder::{CodecStream, Decoder as _, RetrieveAudioStatus}; +use oakcodec::decoder::{receive_list_of_all_decoders, CodecStream, Decoder as _, RetrieveAudioStatus}; use oakcodec::ffmpeg::FFmpegDecoder; +use oakcodec::footagedescription::FootageDescription; use oakcore_rs::{Rational, TimeRange}; -use crate::bridge::codec::AudioStreamInfo; use crate::error::{Error, Result}; -use crate::handle::{free_handle, make_owned, CHandle}; /// Maximum channel count accepted by [`extract`]. The C++ plane array is a /// fixed `OAKAUDIO_EXTRACT_MAX_CHANNELS` (64) stack buffer; the Rust @@ -625,32 +624,6 @@ impl RecipOrZero for f64 { } } -// ---- Handle plumbing (mirrors processor.rs) -------------------------------- - -/// Create an empty waveform behind a refcounted handle (count 1). -pub fn init() -> Result { - Ok(make_owned(AudioVisualWaveform::new())) -} - -/// Release one reference to a waveform (NULL/empty no-op). -pub fn free(self_: *mut CHandle) { - unsafe { free_handle(self_) }; -} - -/// Borrow the waveform behind a handle; `OAKAUDIO_E_INVALID` for empty. -pub fn get(self_: &CHandle) -> Result<&AudioVisualWaveform> { - // SAFETY: every non-empty handle returned by `init` boxes an - // `AudioVisualWaveform`. - unsafe { crate::handle::get::(self_) }.ok_or(Error::Invalid) -} - -/// Mutable variant of [`get`]. -pub fn get_mut(self_: &CHandle) -> Result<&mut AudioVisualWaveform> { - // SAFETY: every non-empty handle returned by `init` boxes an - // `AudioVisualWaveform`. - unsafe { crate::handle::get_mut::(self_) }.ok_or(Error::Invalid) -} - // ---- Whole-file extraction -------------------------------------------------- /// Result of [`extract`]: channel-interleaved min/max pairs. @@ -714,10 +687,34 @@ fn emit_points( } } +/// Probe a file with every registered decoder and return the first valid +/// description (non-empty decoder id and at least one stream). +/// +/// Replaces the former `oakcodec_decoder_probe` C ABI call (mirrors +/// `probe_with_any_decoder` in oakcodec's ffi layer). +fn probe_description(filename: &str) -> Option { + for decoder in receive_list_of_all_decoders() { + // `continue`, not `?`: an unimplemented/unsupported decoder must + // fall through to the next one (the FFmpeg entry is the + // format-agnostic fallback last in the registry). + let Some(desc) = decoder.probe(filename, None) else { + continue; + }; + if !desc.decoder().is_empty() + && (desc.video_stream_count() > 0 + || desc.audio_stream_count() > 0 + || desc.subtitle_stream_count() > 0) + { + return Some(desc); + } + } + None +} + /// Decode a whole audio stream to a channel-interleaved min/max summary. /// -/// The stream is probed through the oakcodec decoder C ABI and decoded with -/// oakcodec's in-process FFmpeg decoder (interleaved f32 at the native +/// The stream is probed through oakcodec's in-process decoder registry and +/// decoded with oakcodec's FFmpeg decoder (interleaved f32 at the native /// rate/layout), then reduced to one point per `samples_per_point` source /// samples. /// @@ -728,34 +725,33 @@ pub fn extract( stream_index: i32, samples_per_point: i32, ) -> Result { - // Probe for the stream's native rate/layout (stateless). - // SAFETY: `filename` is a NUL-terminated C string (validated by the FFI - // layer); the probe handle is freed on every path below. - let mut probe = unsafe { crate::bridge::codec::oakcodec_decoder_probe(filename.as_ptr()) }; - if probe.is_null() { - return Err(Error::NotFound); + // Probe for the stream's native rate/layout with the in-process decoder + // registry (the Rust equivalent of the former oakcodec decoder probe + // C ABI call). + let filename_str = filename.to_string_lossy(); + let desc = probe_description(&filename_str).ok_or(Box::new(Error::NotFound))?; + let ap = desc + .get_audio_stream(stream_index as usize) + .ok_or(Box::new(Error::NotFound))?; + + // The probed stream metadata is a plain value type (oakcodec's + // `AudioParams`); the fields map one-to-one onto the former + // `oakcodec_audio_stream_info` POD. + let sample_rate = ap.sample_rate; + let channel_count = ap.channel_count(); + let channel_layout = ap.channel_layout; + let container_stream_index = ap.stream_index; + let duration_ts = ap.duration; + let (time_base_num, time_base_den) = ap.time_base; + + if sample_rate <= 0 || channel_count <= 0 { + return Err(Box::new(Error::Failed("invalid audio stream".to_string()))); } - let mut info = unsafe { std::mem::zeroed::() }; - let r = unsafe { - crate::bridge::codec::oakcodec_decoder_probe_get_audio_stream( - probe, - stream_index, - &mut info, - ) - }; - // SAFETY: `probe` was created above and is no longer used. - unsafe { crate::bridge::codec::oakcodec_decoder_free(&mut probe) }; - if r != 0 { - return Err(Error::NotFound); - } - if info.sample_rate <= 0 || info.channel_count <= 0 { - return Err(Error::Failed("invalid audio stream".to_string())); - } - let channels = info.channel_count; + let channels = channel_count; if channels > EXTRACT_MAX_CHANNELS { - return Err(Error::Failed(format!( + return Err(Box::new(Error::Failed(format!( "stream has {channels} channels (max {EXTRACT_MAX_CHANNELS})" - ))); + )))); } // Decode the whole stream through oakcodec's FFmpeg decoder @@ -764,23 +760,23 @@ pub fn extract( // fb_audio_graph to obtain planar f32). let decoder = FFmpegDecoder::new(); let stream = CodecStream::with_block( - filename.to_string_lossy().into_owned(), - info.stream_index, + filename_str.into_owned(), + container_stream_index, None, ); if let Err(e) = decoder.open(&stream) { - return Err(Error::Failed(format!("failed to open decoder: {e:?}"))); + return Err(Box::new(Error::Failed(format!("failed to open decoder: {e:?}")))); } - if info.time_base_num <= 0 || info.time_base_den <= 0 || info.duration_ts <= 0 { + if time_base_num <= 0 || time_base_den <= 0 || duration_ts <= 0 { let _ = decoder.close(); - return Err(Error::Failed("invalid audio stream duration".to_string())); + return Err(Box::new(Error::Failed("invalid audio stream duration".to_string()))); } let duration_sec = - info.duration_ts as f64 * f64::from(info.time_base_num) / f64::from(info.time_base_den); - let total_frames = (duration_sec * f64::from(info.sample_rate)).round() as i64; - let layout_mask = if info.channel_layout != 0 { - info.channel_layout + duration_ts as f64 * f64::from(time_base_num) / f64::from(time_base_den); + let total_frames = (duration_sec * f64::from(sample_rate)).round() as i64; + let layout_mask = if channel_layout != 0 { + channel_layout } else { ffmpeg_next::ChannelLayout::default(channels).bits() }; @@ -796,11 +792,11 @@ pub fn extract( while offset < total_frames { let frames = (total_frames - offset).min(CHUNK_FRAMES); let range = TimeRange::new( - Rational::new(offset, i64::from(info.sample_rate)), - Rational::new(offset + frames, i64::from(info.sample_rate)), + Rational::new(offset, i64::from(sample_rate)), + Rational::new(offset + frames, i64::from(sample_rate)), ); let mut buf = vec![0f32; frames as usize * channels as usize]; - match decoder.retrieve_audio(&mut buf, &range, info.sample_rate, layout_mask) { + match decoder.retrieve_audio(&mut buf, &range, sample_rate, layout_mask) { Ok(RetrieveAudioStatus::Success) => { append_pending(&mut pending, &buf, channels); emit_points( @@ -812,11 +808,11 @@ pub fn extract( ); } Ok(status) => { - result = Err(Error::Failed(format!("audio retrieve failed: {status:?}"))); + result = Err(Box::new(Error::Failed(format!("audio retrieve failed: {status:?}")))); break; } Err(e) => { - result = Err(Error::Failed(format!("audio retrieve failed: {e:?}"))); + result = Err(Box::new(Error::Failed(format!("audio retrieve failed: {e:?}")))); break; } } diff --git a/crates/oakaudio/tests/common/mod.rs b/crates/oakaudio/tests/common/mod.rs index b4cd50841..ee4c5e088 100644 --- a/crates/oakaudio/tests/common/mod.rs +++ b/crates/oakaudio/tests/common/mod.rs @@ -14,68 +14,43 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//! Shared test helpers and bridge stubs for the oakaudio contract suite. +//! Shared helpers for the oakaudio contract suite. //! -//! The library imports the other oak modules (`oakcommon`, `oakcodec`) -//! through the `bridge/` wrappers. A standalone `cargo test` run has no -//! host dylibs to link, so [`mod stubs`] provides minimal definitions — a -//! no-op config/encoder and a WAV-header probe (real decoding in -//! `waveform::extract` goes through oakcodec's in-process FFmpeg decoder; -//! the processor drives a real FFmpeg filter graph via ffmpeg-next). The -//! exhaustive behavior matrix is pinned by the unchanged C++ gtest suite -//! (`src/audio/tests`). +//! The former bridge stubs are gone with the deleted C ABI: every test +//! now calls the crate's public Rust API directly (manager singleton, +//! processor, levelmeter, waveform, synchronizer). Decoding in +//! `waveform::extract` goes through oakcodec's in-process FFmpeg +//! decoder; the processor drives a real FFmpeg filter graph. use std::path::Path; use std::sync::Mutex; -/// Serializes tests that mutate process-wide state (the manager singleton, -/// the alive ledger) within one test binary. +/// Serializes tests that mutate process-wide state (the manager singleton) +/// within one test binary. pub static MANAGER_LOCK: Mutex<()> = Mutex::new(()); +/// Lock the manager singleton for a test. The manager state persists across +/// tests (the `OnceLock` cannot be reset), so a panicked test must not +/// poison the lock for the rest of the binary. +pub fn lock_manager() -> std::sync::MutexGuard<'static, ()> { + MANAGER_LOCK.lock().unwrap_or_else(|p| p.into_inner()) +} + /// Build a planar f32 buffer with `channel_count` channels of `frame_count` /// frames from a single-channel `source` (replicated per channel). pub fn planar_from(source: &[f32], channel_count: usize) -> Vec> { (0..channel_count).map(|_| source.to_vec()).collect() } -/// A deterministic pseudo-random planar buffer (fixed seed) for stable -/// golden vectors. Samples land in `[-1, 1)`. -pub fn noisy_planar(channel_count: usize, frame_count: usize, seed: u64) -> Vec> { - let mut state = seed | 1; - let mut next = move || { - state = state - .wrapping_mul(6364136223846793005) - .wrapping_add(1442695040888963407); - ((state >> 33) as u64 & 0xFFFF) as f32 / 65535.0 * 2.0 - 1.0 - }; - let mut data = Vec::with_capacity(channel_count); - for _ in 0..channel_count { - data.push((0..frame_count).map(|_| next()).collect()); - } - data -} - /// A silence buffer: every sample is exactly `0.0`. pub fn silence_planar(channel_count: usize, frame_count: usize) -> Vec> { vec![vec![0.0; frame_count]; channel_count] } -/// Total number of `SamplePerChannel` entries in a channel-interleaved -/// waveform sample for `points` points across `channels` channels. -pub fn interleaved_len(points: usize, channels: usize) -> usize { - points * channels -} - -/// Convert an `oakaudio`-style `min_max` pair layout into a plain tuple for -/// comparison in tests. -pub fn pair(min: f32, max: f32) -> (f32, f32) { - (min, max) -} - // ---- Minimal WAV fixture helpers ------------------------------------------- /// Write a 16-bit PCM WAV file (`fmt` chunk + `data` chunk, standard 44-byte -/// header). The test stubs decode exactly this layout. +/// header). Decoded by oakcodec's FFmpeg decoder in `waveform::extract`. pub fn write_wav(path: &Path, channels: u16, rate: u32, samples: &[i16]) -> std::io::Result<()> { let block_align = channels * 2; let byte_rate = rate * u32::from(block_align); @@ -124,286 +99,3 @@ pub fn write_wav_header_only(path: &Path, channels: u16, rate: u32) -> std::io:: out.extend_from_slice(&0u32.to_le_bytes()); std::fs::write(path, out) } - -// ---- Bridge stubs ----------------------------------------------------------- - -#[allow(dead_code)] -pub mod stubs { - use std::ffi::{c_char, c_int, c_void, CStr}; - use std::path::Path; - - use oakaudio::bridge::codec::AudioStreamInfo; - use oakaudio::handle::CHandle; - - // ------------------------- oakcommon --------------------------------- - - /// Not-found for every key: `config::device_name`'s two-stage query - /// treats `size <= 1` as absent and returns the empty string, so the - /// config-driven device lookup degrades to `paNoDevice` (the documented - /// bridge degradation). - #[no_mangle] - pub extern "C" fn oakcommon_config_get( - _group: *const c_char, - _key: *const c_char, - _buf: *mut c_char, - _buf_size: c_int, - ) -> c_int { - -1 - } - - /// Every integer config reads its default. - #[no_mangle] - pub extern "C" fn oakcommon_config_get_int( - _group: *const c_char, - _key: *const c_char, - default: c_int, - ) -> c_int { - default - } - - /// Core `SampleFormat` (planar-first) → `FBSampleFormat` (AVSampleFormat - /// order), mirroring `src/common/src/ffmpegutils.cpp:83`. - /// - /// `// CPP-PARITY: src/common/src/ffmpegutils.cpp:83` - /// (`FFmpegUtils::get_ffmpeg_sample_format`). - #[no_mangle] - pub extern "C" fn oakcommon_ffmpegutils_get_ffmpeg_sample_format( - smp_fmt: c_int, - out: *mut c_int, - ) -> c_int { - let mapped = match smp_fmt { - 0 => 5, // u8_p -> fb_sample_fmt_u8_p - 1 => 6, // s16_p -> fb_sample_fmt_s16_p - 2 => 7, // s32_p -> fb_sample_fmt_s32_p - 3 => 11, // s64_p -> fb_sample_fmt_s64_p - 4 => 8, // f32_p -> fb_sample_fmt_fltp - 5 => 9, // f64_p -> fb_sample_fmt_dblp - 6 => 0, // u8 -> fb_sample_fmt_u8 - 7 => 1, // s16 -> fb_sample_fmt_s16 - 8 => 2, // s32 -> fb_sample_fmt_s32 - 9 => 10, // s64 -> fb_sample_fmt_s64 - 10 => 3, // f32 -> fb_sample_fmt_flt - 11 => 4, // f64 -> fb_sample_fmt_dbl - _ => -1, // invalid/count -> fb_sample_fmt_none - }; - if out.is_null() { - return -1; - } - // SAFETY: the caller guarantees a writable int. - unsafe { *out = mapped }; - 0 - } - - // ------------------------- oakcodec ---------------------------------- - - /// ffmpeg-style default channel layout mask for `nb_channels` (only the - /// popcount is load-bearing for oakaudio). Masks above 63 channels - /// cannot be represented in a u64 and yield 0 (the extract cap check - /// rejects such streams before any layout use). - fn layout_for(channels: i32) -> u64 { - match channels { - 1 => 0x4, - 2 => 0x3, - n if n > 0 && n < 64 => (1u64 << n) - 1, - _ => 0, - } - } - - /// WAV header facts parsed by `parse_wav`. - struct WavInfo { - channels: i32, - rate: i32, - block_align: usize, - frames: i64, - } - - /// Parse the standard 44-byte PCM WAV header the fixture writer emits. - fn parse_wav(path: &Path) -> Option { - let bytes = std::fs::read(path).ok()?; - if bytes.len() < 44 || &bytes[0..4] != b"RIFF" || &bytes[8..12] != b"WAVE" { - return None; - } - if &bytes[12..16] != b"fmt " || &bytes[36..40] != b"data" { - return None; - } - let fmt_size = u32::from_le_bytes(bytes[16..20].try_into().ok()?); - if fmt_size < 16 { - return None; - } - let audio_format = u16::from_le_bytes(bytes[20..22].try_into().ok()?); - let channels = u16::from_le_bytes(bytes[22..24].try_into().ok()?); - let rate = u32::from_le_bytes(bytes[24..28].try_into().ok()?); - let block_align = u16::from_le_bytes(bytes[32..34].try_into().ok()?); - let bits = u16::from_le_bytes(bytes[34..36].try_into().ok()?); - let data_size = u32::from_le_bytes(bytes[40..44].try_into().ok()?); - if audio_format != 1 || bits != 16 || channels == 0 || rate == 0 || block_align == 0 { - return None; - } - let frames = (data_size as usize / block_align as usize) as i64; - Some(WavInfo { - channels: i32::from(channels), - rate: rate as i32, - block_align: block_align as usize, - frames, - }) - } - - // All handles below use the shared `CHandle` ABI (single-lib - // unification): oakaudio's `bridge::codec` wrappers call the oakcodec - // crate's `#[no_mangle]` ffi functions directly, so these stubs must - // match the real oakcodec ffi signatures exactly. - - struct StubEncoder; - - #[no_mangle] - pub extern "C" fn oakcodec_encoder_init(params: *const c_void) -> CHandle { - if params.is_null() { - return CHandle::null(); - } - CHandle { - ctx: Box::into_raw(Box::new(StubEncoder)) as *mut c_void, - addref: None, - release: None, - abi_version: 0, - } - } - - #[no_mangle] - pub extern "C" fn oakcodec_encoder_open(_encoder: CHandle) -> c_int { - 0 - } - - #[no_mangle] - pub extern "C" fn oakcodec_encoder_write_audio( - _encoder: CHandle, - _samples: *const f32, - _frame_count: c_int, - ) -> c_int { - 0 - } - - #[no_mangle] - pub extern "C" fn oakcodec_encoder_flush(_encoder: CHandle) -> c_int { - 0 - } - - #[no_mangle] - pub extern "C" fn oakcodec_encoder_last_error( - _encoder: CHandle, - _buf: *mut c_char, - _buf_size: c_int, - ) -> c_int { - 0 - } - - #[no_mangle] - pub extern "C" fn oakcodec_encoder_free(encoder: *mut CHandle) { - if !encoder.is_null() { - // SAFETY: the pointer was created by `oakcodec_encoder_init`; - // the caller owns `encoder` and expects it cleared. - let e = unsafe { &mut *encoder }; - if !e.ctx.is_null() { - drop(unsafe { Box::from_raw(e.ctx as *mut StubEncoder) }); - e.ctx = std::ptr::null_mut(); - } - } - } - - /// Probe result for a decodable WAV file. - struct StubProbe { - channels: i32, - sample_rate: i32, - frames: i64, - layout: u64, - } - - #[no_mangle] - pub extern "C" fn oakcodec_decoder_probe(filename: *const c_char) -> CHandle { - if filename.is_null() { - return CHandle::null(); - } - // SAFETY: the C string is NUL-terminated (caller contract). - let cname = unsafe { CStr::from_ptr(filename) }; - let path = Path::new(cname.to_str().unwrap_or("")); - match parse_wav(path) { - Some(info) => CHandle { - ctx: Box::into_raw(Box::new(StubProbe { - channels: info.channels, - sample_rate: info.rate, - frames: info.frames, - layout: layout_for(info.channels), - })) as *mut c_void, - addref: None, - release: None, - abi_version: 0, - }, - None => CHandle::null(), - } - } - - #[no_mangle] - pub extern "C" fn oakcodec_decoder_free(probe: *mut CHandle) { - if !probe.is_null() { - // SAFETY: the pointer was created by `oakcodec_decoder_probe`; - // the caller owns `probe` and expects it cleared. - let p = unsafe { &mut *probe }; - if !p.ctx.is_null() { - drop(unsafe { Box::from_raw(p.ctx as *mut StubProbe) }); - p.ctx = std::ptr::null_mut(); - } - } - } - - #[no_mangle] - pub extern "C" fn oakcodec_decoder_probe_audio_stream_count(_probe: CHandle) -> c_int { - 1 - } - - #[no_mangle] - pub extern "C" fn oakcodec_decoder_probe_get_audio_stream( - probe: CHandle, - index: c_int, - out: *mut AudioStreamInfo, - ) -> c_int { - if probe.ctx.is_null() || out.is_null() || index != 0 { - return -1; - } - // SAFETY: the probe ctx is a live `StubProbe`; `out` is a - // caller-owned info struct. - let p = unsafe { &*(probe.ctx as *const StubProbe) }; - unsafe { - (*out).stream_index = 0; - (*out).sample_rate = p.sample_rate; - (*out).channel_layout = p.layout; - (*out).channel_count = p.channels; - (*out).duration_ts = p.frames; - (*out).time_base_num = 1; - (*out).time_base_den = p.sample_rate; - } - 0 - } - - #[no_mangle] - pub extern "C" fn oakcodec_decoder_open( - _decoder: CHandle, - _filename: *const c_char, - _stream_index: c_int, - ) -> c_int { - 0 - } - - #[no_mangle] - pub extern "C" fn oakcodec_decoder_decode_audio( - _decoder: CHandle, - _in_num: c_int, - _in_den: c_int, - _out_num: c_int, - _out_den: c_int, - _sample_rate: c_int, - _channel_layout: u64, - _buf: *mut f32, - _buf_frames: c_int, - ) -> c_int { - 0 - } -} diff --git a/crates/oakaudio/tests/ffi_test.rs b/crates/oakaudio/tests/ffi_test.rs deleted file mode 100644 index f831f98b1..000000000 --- a/crates/oakaudio/tests/ffi_test.rs +++ /dev/null @@ -1,200 +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 . - -//! FFI-layer contract tests (ffi.rs). The exhaustive matrix runs against -//! the unchanged C++ gtest suite (`src/audio/tests`); these tests pin -//! Rust-side specifics (handle contracts, the singleton ledger, struct -//! layout). - -mod common; - -use std::mem::{align_of, size_of}; -use std::sync::Mutex; - -use oakaudio::error::{OAKAUDIO_E_INVALID, OAKAUDIO_OK}; -use oakaudio::ffi::levelmeter::oakaudio_levelmeter_analyze; -use oakaudio::ffi::levelmeter::{ChannelStats, MeterStats}; -use oakaudio::ffi::manager::{ - oakaudio_debug_alive_count, oakaudio_manager_create_instance, - oakaudio_manager_destroy_instance, oakaudio_manager_free, oakaudio_manager_instance, -}; -use oakaudio::ffi::processor::{oakaudio_processor_free, oakaudio_processor_init}; -use oakaudio::ffi::sync::{OffsetResult, SourceClip}; -use oakaudio::ffi::waveform::{oakaudio_waveform_free, oakaudio_waveform_init}; - -/// Serializes tests that touch the process-wide singleton and the alive -/// ledger. -static LOCK: Mutex<()> = Mutex::new(()); - -/// Every exported handle-returning function (processor_init, waveform_init) -/// returns ctx==NULL on failure and a valid refcounted handle on success, -/// with abi_version == OAKAUDIO_ABI_VERSION stamped. -#[test] -fn handle_contract_all_exports() { - let _guard = LOCK.lock().unwrap(); - - let mut p = unsafe { oakaudio_processor_init() }; - assert!(!p.ctx.is_null()); - assert_eq!(p.abi_version, oakaudio::handle::OAKAUDIO_ABI_VERSION); - - let mut w = unsafe { oakaudio_waveform_init() }; - assert!(!w.ctx.is_null()); - assert_eq!(w.abi_version, oakaudio::handle::OAKAUDIO_ABI_VERSION); - - unsafe { oakaudio_processor_free(&mut p) }; - unsafe { oakaudio_waveform_free(&mut w) }; -} - -/// free(NULL)/free(empty) are no-ops across every free export. -#[test] -fn free_null_noop_all_exports() { - let _guard = LOCK.lock().unwrap(); - - let mut p = oakaudio::handle::CHandle::null(); - unsafe { oakaudio_processor_free(&mut p) }; - assert!(p.ctx.is_null()); - - let mut w = oakaudio::handle::CHandle::null(); - unsafe { oakaudio_waveform_free(&mut w) }; - assert!(w.ctx.is_null()); - - let mut m = oakaudio::handle::CHandle::null(); - unsafe { oakaudio_manager_free(&mut m) }; - assert!(m.ctx.is_null()); - - // NULL pointer itself is a no-op. - unsafe { oakaudio_processor_free(std::ptr::null_mut()) }; - unsafe { oakaudio_waveform_free(std::ptr::null_mut()) }; - unsafe { oakaudio_manager_free(std::ptr::null_mut()) }; -} - -/// The manager singleton: instance() is the same borrowed handle across -/// calls; create/destroy flip validity; oakaudio_debug_alive_count moves -/// predictably and returns to baseline. -#[test] -fn manager_singleton_and_alive_count() { - let _guard = LOCK.lock().unwrap(); - - let before = unsafe { oakaudio_debug_alive_count() }; - - unsafe { oakaudio_manager_destroy_instance() }; - let none = unsafe { oakaudio_manager_instance() }; - assert!(none.ctx.is_null()); - // The empty instance handle is the shared `null()` (no ABI version). - assert_eq!(none.abi_version, 0); - - unsafe { oakaudio_manager_create_instance() }; - let m1 = unsafe { oakaudio_manager_instance() }; - let m2 = unsafe { oakaudio_manager_instance() }; - assert!(!m1.ctx.is_null()); - assert_eq!( - m1.ctx, m2.ctx, - "instance() must be the same borrowed handle" - ); - - // A processor bumps the ledger; freeing it returns to baseline. - assert_eq!(unsafe { oakaudio_debug_alive_count() }, before); - let mut p = unsafe { oakaudio_processor_init() }; - assert_eq!(unsafe { oakaudio_debug_alive_count() }, before + 1); - unsafe { oakaudio_processor_free(&mut p) }; - assert_eq!(unsafe { oakaudio_debug_alive_count() }, before); - - // Destroy flips the singleton back to empty; create resurrects it. - unsafe { oakaudio_manager_destroy_instance() }; - assert!(unsafe { oakaudio_manager_instance() }.ctx.is_null()); - unsafe { oakaudio_manager_create_instance() }; - assert!(!unsafe { oakaudio_manager_instance() }.ctx.is_null()); -} - -/// oakaudio_levelmeter_analyze with NULL summary still computes per-channel -/// stats, and a NULL channels array with capacity 0 is accepted when only -/// the summary is wanted. -#[test] -fn levelmeter_partial_outputs() { - let data = [0.5f32; 64]; - let planes = [data.as_ptr()]; - - // channels only (summary NULL) - let mut channels = [ChannelStats { - peak_linear: 0.0, - peak_db: 0.0, - rms_linear: 0.0, - rms_db: 0.0, - vu_db: 0.0, - }]; - assert_eq!( - unsafe { - oakaudio_levelmeter_analyze( - planes.as_ptr(), - 1, - 64, - channels.as_mut_ptr(), - 1, - std::ptr::null_mut(), - ) - }, - OAKAUDIO_OK - ); - assert!((channels[0].peak_linear - 0.5).abs() < 1e-9); - - // summary only (channels NULL, capacity 0) - let mut summary = MeterStats { - max_peak_linear: 0.0, - integrated_lufs: 0.0, - silence: 0, - }; - assert_eq!( - unsafe { - oakaudio_levelmeter_analyze( - planes.as_ptr(), - 1, - 64, - std::ptr::null_mut(), - 0, - &mut summary, - ) - }, - OAKAUDIO_OK - ); - assert!((summary.max_peak_linear - 0.5).abs() < 1e-9); - - // Both NULL is invalid. - assert_eq!( - unsafe { - oakaudio_levelmeter_analyze( - planes.as_ptr(), - 1, - 64, - std::ptr::null_mut(), - 0, - std::ptr::null_mut(), - ) - }, - OAKAUDIO_E_INVALID - ); -} - -/// sync value structs (offset_result/source_clip) are 24/40 bytes and -/// repr(C)-aligned as the C headers dictate, so layout never drifts. -#[test] -fn sync_struct_layout() { - assert_eq!(size_of::(), 24); - assert_eq!(align_of::(), 8); - assert_eq!(size_of::(), 40); - assert_eq!(align_of::(), 8); - // The stretch result (f64, i64, f64, i32) pads to 32 bytes. - assert_eq!(size_of::(), 32); -} diff --git a/crates/oakaudio/tests/golden_test.rs b/crates/oakaudio/tests/golden_test.rs index 9c70056a2..46d333403 100644 --- a/crates/oakaudio/tests/golden_test.rs +++ b/crates/oakaudio/tests/golden_test.rs @@ -19,15 +19,12 @@ mod common; +use std::ffi::CString; + use common::write_wav_header_only; -use oakaudio::ffi::waveform::MinMax; -use oakaudio::ffi::waveform::{ - oakaudio_waveform_extract, oakaudio_waveform_free, oakaudio_waveform_get_summary, - oakaudio_waveform_init, oakaudio_waveform_overwrite_samples, - oakaudio_waveform_set_channel_count, -}; -use oakaudio::params::{frames_to_rational, rational_to_samples, SampleFormat}; use oakcore_rs::Rational; +use oakaudio::params::{frames_to_rational, rational_to_samples, AudioParams, SampleFormat}; +use oakaudio::waveform::{extract, AudioVisualWaveform}; /// SampleFormat planar-first ordering matches the authoritative C++ enum: /// f32_p == 4 == OAKAUDIO_PROCESSOR_OUTPUT_FORMAT. This guards the @@ -60,7 +57,7 @@ fn sample_time_conversion_roundtrip() { /// rational conversion edge cases (NaN / out-of-range / tiny -> null). #[test] fn params_value_types() { - use oakaudio::params::{rational_from_double, AudioParams}; + use oakaudio::params::rational_from_double; let p = AudioParams { sample_rate: 48000, @@ -89,23 +86,17 @@ fn waveform_mipmap_scale_parity() { // 1024 ramp samples @ 48000 Hz, two channels. let ch0: Vec = (0..1024).map(|i| i as f32 * 0.001).collect(); let ch1: Vec = (0..1024).map(|i| -(i as f32) * 0.001).collect(); - let planes = [ch0.as_ptr(), ch1.as_ptr()]; + let planes = [ch0.as_slice(), ch1.as_slice()]; - let mut w = unsafe { oakaudio_waveform_init() }; - assert!(!w.ctx.is_null()); - assert_eq!(unsafe { oakaudio_waveform_set_channel_count(w, 2) }, 0); - assert_eq!( - unsafe { oakaudio_waveform_overwrite_samples(w, planes.as_ptr(), 1024, 48000, 0, 1) }, - 0 - ); + let mut w = AudioVisualWaveform::new(); + w.set_channel_count(2); + w.overwrite_samples(&planes, 48000, Rational::new(0, 1)); // One summary point is produced for any queried window; a 1/1024 s // window (one 1024-rate mipmap point ~ 46.875 source samples) must // bracket a narrower range than a 1/64 s window (~750 samples). - let mut fine = [MinMax { min: 0.0, max: 0.0 }; 2]; - let fine_points = - unsafe { oakaudio_waveform_get_summary(w, 0, 1, 1, 1024, fine.as_mut_ptr(), 2) }; - assert_eq!(fine_points, 1); + let fine = w.get_summary_from_time(Rational::new(0, 1), Rational::new(1, 1024)); + assert_eq!(fine.len(), 2); assert_eq!(fine[0].min, 0.0); assert!( (fine[0].max - 0.046).abs() < 1e-5, @@ -119,10 +110,8 @@ fn waveform_mipmap_scale_parity() { ); assert_eq!(fine[1].max, 0.0); - let mut coarse = [MinMax { min: 0.0, max: 0.0 }; 2]; - let coarse_points = - unsafe { oakaudio_waveform_get_summary(w, 0, 1, 1, 64, coarse.as_mut_ptr(), 2) }; - assert_eq!(coarse_points, 1); + let coarse = w.get_summary_from_time(Rational::new(0, 1), Rational::new(1, 64)); + assert_eq!(coarse.len(), 2); assert_eq!(coarse[0].min, 0.0); assert!( (coarse[0].max - 0.749).abs() < 1e-5, @@ -139,8 +128,6 @@ fn waveform_mipmap_scale_parity() { // Coarser windows necessarily cover more source samples. assert!(coarse[0].max > fine[0].max); assert!(coarse[1].min < fine[1].min); - - unsafe { oakaudio_waveform_free(&mut w) }; } /// levelmeter dB conversion: peak_db == 20*log10(peak_linear) and the @@ -164,57 +151,30 @@ fn levelmeter_db_golden() { /// waveformsync envelope offset golden: a reference ramp delayed by two /// windows in the candidate is recovered as +2 windows with full -/// confidence through the C ABI. +/// confidence. #[test] fn waveform_sync_offset_golden() { - use oakaudio::ffi::sync::{oakaudio_sync_estimate_envelope_offset, OffsetResult}; let reference: Vec = (0..10).map(|i| i as f64 * 0.1 + 0.1).collect(); let mut candidate = vec![0.0f64; 10]; candidate[2..].copy_from_slice(&reference[..8]); - let mut out = OffsetResult { - offset_samples: 0, - confidence: 0.0, - valid: 0, - }; - let r = unsafe { - oakaudio_sync_estimate_envelope_offset( - reference.as_ptr(), - reference.len() as i32, - candidate.as_ptr(), - candidate.len() as i32, - std::ptr::null(), - std::ptr::null(), - 100, - 10, - &mut out, - ) - }; - assert_eq!(r, 0); - assert_eq!(out.valid, 1); + let out = oakaudio::waveformsync::estimate_envelope_offset(&reference, &candidate, 100, 10); + assert!(out.valid); assert_eq!(out.offset_samples, 200); assert!((out.confidence - 1.0).abs() < 1e-9); } -/// oakaudio_waveform_extract channel cap: a stream claiming more than -/// OAKAUDIO_EXTRACT_MAX_CHANNELS (64) channels is rejected rather than -/// overflowing the internal plane array. +/// waveform::extract channel cap: a stream claiming more than +/// EXTRACT_MAX_CHANNELS (64) channels is rejected rather than overflowing +/// the internal plane array. #[test] fn extract_channel_cap() { let path = std::env::temp_dir().join(format!("oakaudio_cap_{}.wav", std::process::id())); write_wav_header_only(&path, 65, 48000).unwrap(); - let mut out_channels = 0i32; - let cpath = std::ffi::CString::new(path.to_str().unwrap()).unwrap(); - let r = unsafe { - oakaudio_waveform_extract( - cpath.as_ptr(), - 0, - 4, - std::ptr::null_mut(), - 0, - &mut out_channels, - ) - }; - assert!(r < 0, "oversized stream must be rejected, got {r}"); + let cpath = CString::new(path.to_str().unwrap()).unwrap(); + assert!( + extract(&cpath, 0, 4).is_err(), + "oversized stream must be rejected" + ); std::fs::remove_file(&path).ok(); } diff --git a/crates/oakaudio/tests/handle_test.rs b/crates/oakaudio/tests/handle_test.rs deleted file mode 100644 index 537f59e94..000000000 --- a/crates/oakaudio/tests/handle_test.rs +++ /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 . - -//! Handle plumbing contract tests (handle.rs). - -use oakaudio::error::{OAKAUDIO_E_FAILED, OAKAUDIO_E_INVALID, OAKAUDIO_OK}; -use std::sync::{Mutex, MutexGuard}; - -use oakaudio::handle::{alive_count, get, guard, guard_handle, make_borrowed, make_owned, CHandle}; - -/// Serializes the tests: the crate's live-object ledger is process-global, -/// so parallel tests in this file would race the count assertions. -fn ledger_lock() -> MutexGuard<'static, ()> { - static LOCK: Mutex<()> = Mutex::new(()); - LOCK.lock().unwrap_or_else(|e| e.into_inner()) -} - -/// make_owned starts at refcount 1; get returns a typed view; dropping the -/// handle decrements to 0. -#[test] -fn owned_lifecycle() { - let _l = ledger_lock(); - let before = alive_count(); - let mut h = make_owned(42u32); - assert!(!h.is_null()); - assert_eq!(alive_count(), before + 1); - - // SAFETY: `h` boxes a u32 created above. - let v = unsafe { get::(&h) }.unwrap(); - assert_eq!(*v, 42); - - // addref/release round-trip through the function pointers. - let addref = h.addref.unwrap(); - let release = h.release.unwrap(); - // SAFETY: the ctx was created by make_owned. - unsafe { addref(h.ctx) }; - assert_eq!(alive_count(), before + 1); // count unchanged, still 1 box - unsafe { release(h.ctx) }; - - // Dropping the box (release to zero) decrements the ledger. - let release = h.release.unwrap(); - // SAFETY: h.ctx is the box created above; refcount is 1. - unsafe { release(h.ctx) }; - h.ctx = std::ptr::null_mut(); - assert_eq!(alive_count(), before); -} - -/// make_borrowed creates a borrow-only handle whose release frees only the -/// box, never the underlying object. -#[test] -fn borrowed_release() { - let mut value = Box::new(7i32); - // SAFETY: `value` outlives the handle; the ctx points directly at the - // box (not a RefBox), so it is read through the raw pointer. - let mut h = unsafe { make_borrowed(&mut *value) }; - assert!(!h.is_null()); - - // SAFETY: `h.ctx` points at the box created above. - let v = unsafe { &*(h.ctx as *const i32) }; - assert_eq!(*v, 7); - - // release is a no-op: the box still lives. - let release = h.release.unwrap(); - // SAFETY: noop_ref for borrowed handles. - unsafe { release(h.ctx) }; - assert_eq!(*value, 7); - - h.ctx = std::ptr::null_mut(); -} - -/// CHandle::null() yields an empty handle; guard over an Ok(()) returns -/// OAKAUDIO_OK (0) and guard_handle over Ok returns a valid handle. -#[test] -fn null_and_guard_ok() { - let null = CHandle::null(); - assert!(null.is_null()); - // The shared `null()` stamps no ABI version (single-lib unification). - assert_eq!(null.abi_version, 0); - - assert_eq!(guard(|| Ok(())), OAKAUDIO_OK); - - let h = guard_handle(|| Ok(make_owned(1u32))); - assert!(!h.is_null()); - // Release it so the alive ledger returns to baseline (tests share the - // process-wide ledger and run in parallel). - // SAFETY: `h.ctx` is the box created above; refcount is 1. - unsafe { (h.release.unwrap())(h.ctx) }; -} - -/// guard maps an Err to the negative error code without panicking; a -/// panicking body is caught and returns a failure code rather than -/// unwinding across the FFI boundary. -#[test] -fn guard_error_and_panic() { - assert_eq!( - guard(|| Err(oakaudio::error::Error::Invalid)), - OAKAUDIO_E_INVALID - ); - assert_eq!(guard(|| Err(oakaudio::error::Error::State)), -60002); - assert_eq!( - guard(|| Err(oakaudio::error::Error::Failed("x".to_string()))), - -60003 - ); - assert_eq!(guard(|| Err(oakaudio::error::Error::NotFound)), -60004); - assert_eq!(guard(|| Err(oakaudio::error::Error::NoMem)), -60005); - - assert_eq!(guard(|| panic!("boom")), OAKAUDIO_E_FAILED); - - let h = guard_handle(|| Err(oakaudio::error::Error::Invalid)); - assert!(h.is_null()); - let h2 = guard_handle(|| panic!("boom")); - assert!(h2.is_null()); -} diff --git a/crates/oakaudio/tests/levelmeter_test.rs b/crates/oakaudio/tests/levelmeter_test.rs index d66d4076c..c5eaf14f5 100644 --- a/crates/oakaudio/tests/levelmeter_test.rs +++ b/crates/oakaudio/tests/levelmeter_test.rs @@ -14,53 +14,28 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//! AudioLevelMeter contract tests (levelmeter.rs), through the C ABI. +//! AudioLevelMeter contract tests (levelmeter.rs), calling the public +//! `analyze_sample_buffer` API. mod common; -use oakaudio::error::{OAKAUDIO_E_INVALID, OAKAUDIO_OK}; -use oakaudio::ffi::levelmeter::{oakaudio_levelmeter_analyze, ChannelStats, MeterStats}; +use oakaudio::levelmeter::{analyze_sample_buffer, Stats}; -fn analyze(planes: &[Vec]) -> (Vec, MeterStats) { - let ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect(); - let mut channels: Vec = (0..planes.len()) - .map(|_| ChannelStats { - peak_linear: 0.0, - peak_db: 0.0, - rms_linear: 0.0, - rms_db: 0.0, - vu_db: 0.0, - }) - .collect(); - let mut summary = MeterStats { - max_peak_linear: 0.0, - integrated_lufs: 0.0, - silence: 0, - }; - let r = unsafe { - oakaudio_levelmeter_analyze( - ptrs.as_ptr(), - planes.len() as i32, - planes.first().map_or(0, |p| p.len()) as i32, - channels.as_mut_ptr(), - channels.len() as i32, - &mut summary, - ) - }; - assert_eq!(r, OAKAUDIO_OK); - (channels, summary) +fn analyze(planes: &[Vec]) -> Stats { + let refs: Vec<&[f32]> = planes.iter().map(Vec::as_slice).collect(); + analyze_sample_buffer(&refs) } -/// A silence buffer reports silence=1, all-zero linear fields, and dB +/// A silence buffer reports silence=true, all-zero linear fields, and dB /// fields floored at -200. #[test] fn silence_analysis() { let planes = common::silence_planar(2, 64); - let (channels, summary) = analyze(&planes); - assert_eq!(summary.silence, 1); - assert_eq!(summary.max_peak_linear, 0.0); - assert_eq!(summary.integrated_lufs, -200.0); - for ch in &channels { + let stats = analyze(&planes); + assert!(stats.silence); + assert_eq!(stats.max_peak_linear, 0.0); + assert_eq!(stats.integrated_lufs, -200.0); + for ch in &stats.channels { assert_eq!(ch.peak_linear, 0.0); assert_eq!(ch.rms_linear, 0.0); assert_eq!(ch.peak_db, -200.0); @@ -70,21 +45,22 @@ fn silence_analysis() { } /// A constant-amplitude tone reports peak_linear == rms_linear == that -/// amplitude (power terms), peak_db matches 20*log10(amp), and silence=0. +/// amplitude (power terms), peak_db matches 20*log10(amp), and silence is +/// false. #[test] fn constant_tone_stats() { let planes = common::planar_from(&[0.5f32; 64], 1); - let (channels, summary) = analyze(&planes); - assert_eq!(summary.silence, 0); - assert!((channels[0].peak_linear - 0.5).abs() < 1e-9); - assert!((channels[0].rms_linear - 0.5).abs() < 1e-9); + let stats = analyze(&planes); + assert!(!stats.silence); + assert!((stats.channels[0].peak_linear - 0.5).abs() < 1e-9); + assert!((stats.channels[0].rms_linear - 0.5).abs() < 1e-9); let expected_db = 20.0 * 0.5f64.log10(); - assert!((channels[0].peak_db - expected_db).abs() < 1e-9); - assert!((channels[0].rms_db - expected_db).abs() < 1e-9); + assert!((stats.channels[0].peak_db - expected_db).abs() < 1e-9); + assert!((stats.channels[0].rms_db - expected_db).abs() < 1e-9); } /// A full-scale square wave yields max_peak_linear == 1.0 and a peak_db -/// near 0 dB; per-channel channels array is filled for each channel. +/// near 0 dB; per-channel stats are filled for each channel. #[test] fn full_scale_peak() { let ch0: Vec = (0..64) @@ -93,13 +69,13 @@ fn full_scale_peak() { let ch1: Vec = (0..64) .map(|i| if i % 2 == 0 { -1.0 } else { 1.0 }) .collect(); - let (channels, summary) = analyze(&[ch0, ch1]); - assert_eq!(summary.max_peak_linear, 1.0); - assert_eq!(summary.silence, 0); - assert!((channels[0].peak_db - 0.0).abs() < 1e-9); - assert!((channels[1].peak_db - 0.0).abs() < 1e-9); - assert!((channels[0].rms_linear - 1.0).abs() < 1e-9); - assert!((channels[1].rms_linear - 1.0).abs() < 1e-9); + let stats = analyze(&[ch0, ch1]); + assert_eq!(stats.max_peak_linear, 1.0); + assert!(!stats.silence); + assert!((stats.channels[0].peak_db - 0.0).abs() < 1e-9); + assert!((stats.channels[1].peak_db - 0.0).abs() < 1e-9); + assert!((stats.channels[0].rms_linear - 1.0).abs() < 1e-9); + assert!((stats.channels[1].rms_linear - 1.0).abs() < 1e-9); } /// integrated_lufs stays -200 for silence and matches the BS.1770 @@ -107,57 +83,27 @@ fn full_scale_peak() { #[test] fn integrated_lufs_silence_vs_tone() { let silence = common::silence_planar(2, 64); - let (_, summary) = analyze(&silence); - assert_eq!(summary.integrated_lufs, -200.0); + let stats = analyze(&silence); + assert_eq!(stats.integrated_lufs, -200.0); let tone = common::planar_from(&[0.5f32; 64], 2); - let (_, summary) = analyze(&tone); + let stats = analyze(&tone); // mean square over all channels = 0.25; -0.691 + 10*log10(0.25) let expected = -0.691 + 10.0 * 0.25f64.log10(); - assert!((summary.integrated_lufs - expected).abs() < 1e-9); + assert!((stats.integrated_lufs - expected).abs() < 1e-9); } -/// channel_count of 0, a NULL planar pointer, or NULL for both outputs -/// returns OAKAUDIO_E_INVALID. +/// Empty or zero-length inputs yield a default stats struct (no channels, +/// silence). #[test] -fn invalid_input() { - let planes = common::planar_from(&[0.5f32; 8], 1); - let ptr = planes[0].as_ptr(); - let mut summary = MeterStats { - max_peak_linear: 0.0, - integrated_lufs: 0.0, - silence: 0, - }; +fn empty_input() { + let stats = analyze_sample_buffer(&[]); + assert!(stats.channels.is_empty()); + assert!(stats.silence); - // channel_count 0. - assert_eq!( - unsafe { oakaudio_levelmeter_analyze(&ptr, 0, 8, std::ptr::null_mut(), 0, &mut summary) }, - OAKAUDIO_E_INVALID - ); - // NULL planar. - assert_eq!( - unsafe { - oakaudio_levelmeter_analyze( - std::ptr::null(), - 1, - 8, - std::ptr::null_mut(), - 0, - &mut summary, - ) - }, - OAKAUDIO_E_INVALID - ); - // Both outputs NULL. - assert_eq!( - unsafe { - oakaudio_levelmeter_analyze(&ptr, 1, 8, std::ptr::null_mut(), 0, std::ptr::null_mut()) - }, - OAKAUDIO_E_INVALID - ); - // Negative frame count. - assert_eq!( - unsafe { oakaudio_levelmeter_analyze(&ptr, 1, -1, std::ptr::null_mut(), 0, &mut summary) }, - OAKAUDIO_E_INVALID - ); + let silence = common::silence_planar(1, 0); + let refs: Vec<&[f32]> = silence.iter().map(Vec::as_slice).collect(); + let stats = analyze_sample_buffer(&refs); + assert!(stats.silence); + assert_eq!(stats.max_peak_linear, 0.0); } diff --git a/crates/oakaudio/tests/manager_test.rs b/crates/oakaudio/tests/manager_test.rs index b5e910430..b6438af58 100644 --- a/crates/oakaudio/tests/manager_test.rs +++ b/crates/oakaudio/tests/manager_test.rs @@ -14,171 +14,96 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//! AudioManager contract tests (manager.rs), through the C ABI. The -//! manager is a process-wide singleton, so every test holds the shared +//! AudioManager contract tests (manager.rs), calling the public Rust API. +//! The manager is a process-wide singleton, so every test holds the shared //! `MANAGER_LOCK`. mod common; -use std::ffi::c_char; +use common::lock_manager; +use oakcodec::encodingparams::EncodingParams; +use oakaudio::error::{Error, OAKAUDIO_E_INVALID}; +use oakaudio::manager::instance; +use oakaudio::params::{AudioParams, SampleFormat}; -use common::MANAGER_LOCK; -use oakaudio::bridge::codec::EncodingParams; -use oakaudio::error::{OAKAUDIO_E_FAILED, OAKAUDIO_E_INVALID, OAKAUDIO_OK}; -use oakaudio::ffi::manager::{ - oakaudio_debug_alive_count, oakaudio_manager_clear_buffered_output, - oakaudio_manager_create_instance, oakaudio_manager_destroy_instance, - oakaudio_manager_find_config_device_by_name_s, oakaudio_manager_find_device_by_name_s, - oakaudio_manager_free, oakaudio_manager_get_input_device, oakaudio_manager_get_output_device, - oakaudio_manager_hard_reset, oakaudio_manager_instance, oakaudio_manager_push_to_output, - oakaudio_manager_reset_output_clock, oakaudio_manager_seconds, - oakaudio_manager_set_input_device, oakaudio_manager_set_output_device, - oakaudio_manager_output_levels, oakaudio_manager_set_output_notify_interval, - oakaudio_manager_start_recording, - oakaudio_manager_stop_output, oakaudio_manager_stop_recording, -}; - -fn instance() -> oakaudio::handle::CHandle { - unsafe { oakaudio_manager_instance() } -} - -/// Lock the manager singleton for a test. The manager state persists across -/// tests (the `OnceLock` cannot be reset), so a panicked test must not -/// poison the lock for the rest of the binary. -fn lock() -> std::sync::MutexGuard<'static, ()> { - MANAGER_LOCK.lock().unwrap_or_else(|p| p.into_inner()) -} - -fn encoding_params() -> EncodingParams { - let mut filename = [0u8; 1024]; - for (i, b) in b"oakaudio_test.wav\0".iter().enumerate() { - filename[i] = *b; - } - EncodingParams { - filename, - format: 0, - video_enabled: 0, - video_codec: 0, - video_width: 0, - video_height: 0, - video_time_base_num: 0, - video_time_base_den: 0, - video_pixel_format: 0, - video_interlacing: 0, - video_pixel_aspect_num: 0, - video_pixel_aspect_den: 0, - video_bit_rate: 0, - video_min_bit_rate: 0, - video_max_bit_rate: 0, - video_buffer_size: 0, - video_threads: 0, - video_pix_fmt: [0u8; 64], - video_is_image_sequence: 0, - video_scaling_method: 0, - audio_enabled: 1, - audio_codec: 13, // PCM_S16LE (the .wav recording codec) - audio_sample_rate: 48000, - audio_channel_layout: 3, - audio_sample_format: 8, - audio_bit_rate: 128000, - subtitles_enabled: 0, - subtitles_codec: 0, - subtitles_are_sidecar: 0, - subtitles_sidecar_format: 0, - color_transform_output: [0u8; 256], - export_length_num: 0, - export_length_den: 0, - has_custom_range: 0, - custom_range_in_num: 0, - custom_range_in_den: 0, - custom_range_out_num: 0, - custom_range_out_den: 0, +/// Audio params for the tests: stereo f32 at 48 kHz. +fn stereo() -> AudioParams { + AudioParams { + sample_rate: 48000, + channel_layout: 0x3, + format: SampleFormat::F32, } } -/// create_instance/destroy_instance toggle the singleton; instance() returns -/// a valid borrowed handle between them and NULL after destroy. +/// A WAV recording config (format 7 = WAV). +fn wav_params(filename: &str) -> EncodingParams { + let mut params = EncodingParams::default(); + params.format = 7; + params.audio_enabled = 1; + params.audio_codec = 13; // PCM_S16LE + params.audio_sample_rate = 48000; + params.audio_channel_layout = 0x3; + params.audio_sample_format = SampleFormat::S16; + params.audio_bit_rate = 128000; + let bytes = filename.as_bytes(); + params.filename[..bytes.len()].copy_from_slice(bytes); + params +} + +/// create_instance/destroy_instance toggle the singleton; instance() +/// returns a live guard between them and None after destroy. #[test] fn singleton_lifecycle() { - let _guard = lock(); - unsafe { oakaudio_manager_destroy_instance() }; - assert!(instance().ctx.is_null()); + let _guard = lock_manager(); + oakaudio::manager::ManagerInner::destroy_instance(); + assert!(instance().is_none()); - unsafe { oakaudio_manager_create_instance() }; - let m = instance(); - assert!(!m.ctx.is_null()); - unsafe { oakaudio_manager_free(&mut m.clone()) }; + oakaudio::manager::ManagerInner::create_instance().unwrap(); + assert!(instance().is_some()); - unsafe { oakaudio_manager_destroy_instance() }; - assert!(instance().ctx.is_null()); - unsafe { oakaudio_manager_create_instance() }; - assert!(!instance().ctx.is_null()); + oakaudio::manager::ManagerInner::destroy_instance(); + assert!(instance().is_none()); + oakaudio::manager::ManagerInner::create_instance().unwrap(); + assert!(instance().is_some()); +} + +/// A fresh singleton for a test: destroy resets the playback state +/// (`output_started`, buffered params) that earlier tests in this binary +/// may have left behind. +fn fresh_instance() { + oakaudio::manager::ManagerInner::destroy_instance(); + oakaudio::manager::ManagerInner::create_instance().unwrap(); } /// push_to_output accepts raw interleaved bytes and starts the virtual -/// playback clock; without a device it fails with a message in error_buf. +/// playback clock; without a device the push still succeeds and keeps the +/// samples buffered (unavailable devices play silence instead of failing). /// -/// The virtual device never consumes frames (PortAudio is not bridged), so -/// the clock reads 0.0 rather than advancing. +/// The clock reads 0.0 after the push starts; a real audio device may have +/// consumed a few frames by the time the assertion runs, so the bound is +/// `>= 0` rather than exact (the callback-advances-clock behavior is pinned +/// by manager.rs' own unit test). #[test] -fn push_output_advances_clock() { - let _guard = lock(); - unsafe { oakaudio_manager_create_instance() }; - let m = instance(); +fn push_output_starts_clock() { + let _guard = lock_manager(); + fresh_instance(); + let mut m = instance().unwrap(); // No stream yet: seconds() reports -1. let mut secs = 0.0f64; - assert_eq!( - unsafe { oakaudio_manager_seconds(m, &mut secs) }, - OAKAUDIO_OK - ); + m.seconds(&mut secs).unwrap(); assert_eq!(secs, -1.0); - // 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 { oakaudio_manager_set_output_device(m, -1) }, - OAKAUDIO_OK - ); + // With no explicit device the push still succeeds (M12 P1). + m.set_output_device(-1).unwrap(); let samples = vec![0u8; 480 * 2 * 4]; - let r = unsafe { - oakaudio_manager_push_to_output( - m, - 48000, - 3, - 4, - samples.as_ptr() as *const c_char, - samples.len() as i64, - std::ptr::null_mut(), - 0, - ) - }; - assert_eq!(r, OAKAUDIO_OK); + m.push_to_output(stereo(), &samples, &mut vec![0u8; 64]).unwrap(); // After selecting a device the push succeeds and the clock starts at 0. - assert_eq!( - unsafe { oakaudio_manager_set_output_device(m, 0) }, - OAKAUDIO_OK - ); - let mut err = [0 as c_char; 64]; - let r = unsafe { - oakaudio_manager_push_to_output( - m, - 48000, - 3, - 4, - samples.as_ptr() as *const c_char, - samples.len() as i64, - err.as_mut_ptr(), - err.len() as i32, - ) - }; - assert_eq!(r, OAKAUDIO_OK); - unsafe { oakaudio_manager_seconds(m, &mut secs) }; - assert_eq!(secs, 0.0); - - unsafe { oakaudio_manager_destroy_instance() }; + m.set_output_device(0).unwrap(); + m.push_to_output(stereo(), &samples, &mut vec![0u8; 64]).unwrap(); + m.seconds(&mut secs).unwrap(); + assert!(secs >= 0.0, "clock started (got {secs})"); } /// set/get output & input device: getters report a device, setters persist @@ -186,183 +111,102 @@ fn push_output_advances_clock() { /// clears buffers). #[test] fn device_selection_roundtrip() { - let _guard = lock(); - unsafe { oakaudio_manager_create_instance() }; - let m = instance(); + let _guard = lock_manager(); + fresh_instance(); + let mut m = instance().unwrap(); - assert_eq!( - unsafe { oakaudio_manager_set_output_device(m, 42) }, - OAKAUDIO_OK - ); - assert_eq!(unsafe { oakaudio_manager_get_output_device(m) }, 42); - assert_eq!( - unsafe { oakaudio_manager_set_input_device(m, 7) }, - OAKAUDIO_OK - ); - assert_eq!(unsafe { oakaudio_manager_get_input_device(m) }, 7); + m.set_output_device(42).unwrap(); + assert_eq!(m.get_output_device().unwrap(), 42); + m.set_input_device(7).unwrap(); + assert_eq!(m.get_input_device().unwrap(), 7); - assert_eq!(unsafe { oakaudio_manager_hard_reset(m) }, OAKAUDIO_OK); - assert_eq!(unsafe { oakaudio_manager_get_output_device(m) }, 42); - assert_eq!(unsafe { oakaudio_manager_get_input_device(m) }, 7); + m.hard_reset().unwrap(); + assert_eq!(m.get_output_device().unwrap(), 42); + assert_eq!(m.get_input_device().unwrap(), 7); // The stream stopped, so the clock is back at -1. let mut secs = 0.0f64; - unsafe { oakaudio_manager_seconds(m, &mut secs) }; + m.seconds(&mut secs).unwrap(); assert_eq!(secs, -1.0); - - unsafe { oakaudio_manager_destroy_instance() }; } -/// set_output_notify_interval stores the interval; clear_buffered_output -/// drops queued bytes, stop_output halts the stream, and reset_output_clock -/// restarts the counter. +/// set_output_notify_interval stores the interval (negative is rejected); +/// clear_buffered_output drops queued bytes, stop_output halts the stream, +/// and reset_output_clock restarts the counter. #[test] fn output_control_flags() { - let _guard = lock(); - unsafe { oakaudio_manager_create_instance() }; - let m = instance(); + let _guard = lock_manager(); + fresh_instance(); + let mut m = instance().unwrap(); + m.set_output_notify_interval(1024).unwrap(); + let err = m.set_output_notify_interval(-1).unwrap_err(); assert_eq!( - unsafe { oakaudio_manager_set_output_notify_interval(m, 1024) }, - OAKAUDIO_OK - ); - assert_eq!( - unsafe { oakaudio_manager_set_output_notify_interval(m, -1) }, - OAKAUDIO_E_INVALID - ); - assert_eq!( - unsafe { oakaudio_manager_clear_buffered_output(m) }, - OAKAUDIO_OK - ); - assert_eq!( - unsafe { oakaudio_manager_reset_output_clock(m) }, - OAKAUDIO_OK + err.downcast_ref::().map(|e| e.code()), + Some(OAKAUDIO_E_INVALID) ); + m.clear_buffered_output().unwrap(); + m.reset_output_clock().unwrap(); // Push starts the stream, then stop_output halts it (clock -> -1). - unsafe { oakaudio_manager_set_output_device(m, 0) }; + m.set_output_device(0).unwrap(); let samples = vec![0u8; 480 * 2 * 4]; - assert_eq!( - unsafe { - oakaudio_manager_push_to_output( - m, - 48000, - 3, - 4, - samples.as_ptr() as *const c_char, - samples.len() as i64, - std::ptr::null_mut(), - 0, - ) - }, - OAKAUDIO_OK - ); - assert_eq!(unsafe { oakaudio_manager_stop_output(m) }, OAKAUDIO_OK); + m.push_to_output(stereo(), &samples, &mut vec![0u8; 64]).unwrap(); + m.stop_output().unwrap(); let mut secs = 1.0f64; - unsafe { oakaudio_manager_seconds(m, &mut secs) }; + m.seconds(&mut secs).unwrap(); assert_eq!(secs, -1.0); - - unsafe { oakaudio_manager_destroy_instance() }; } -/// start_recording validates its parameters: NULL params or a disabled -/// audio track return OAKAUDIO_E_INVALID with an error string. The full -/// encoder-open success path (oakcodec writes a real file via ffmpeg) is -/// an end-to-end concern covered by the codec crate's own tests; with the -/// real oakcodec linked, `start_recording` either opens the encoder -/// (OAKAUDIO_OK, environment-dependent) or reports the encoder's -/// last-error string — both are correct manager behavior, so this test -/// pins the manager's own validation only. +/// start_recording validates its state: with no input device it fails with +/// a reason; with a device the encoder open either succeeds (OAKAUDIO_OK, +/// environment-dependent) or reports the encoder's diagnostic — both are +/// correct manager behavior, so the test pins the manager's own handling +/// only. #[test] fn recording_start_stop() { - let _guard = lock(); - unsafe { oakaudio_manager_create_instance() }; - let m = instance(); - unsafe { oakaudio_manager_set_input_device(m, 0) }; + let _guard = lock_manager(); + fresh_instance(); + let mut m = instance().unwrap(); - let mut err = [0 as c_char; 64]; - let params = encoding_params(); - // With a real encoder, the attempt must at least reach the encoder - // (a failure must surface a diagnostic in error_buf, not crash). - let r = - unsafe { oakaudio_manager_start_recording(m, ¶ms, err.as_mut_ptr(), err.len() as i32) }; - if r != 0 { - assert!( - err.iter().any(|&b| b != 0), - "failed start_recording must report a reason" - ); - let _ = std::fs::remove_file("oakaudio_test.wav"); - } else { - assert_eq!(unsafe { oakaudio_manager_stop_recording(m) }, OAKAUDIO_OK); - // The real encoder writes the output file during open; clean it up. - let _ = std::fs::remove_file("oakaudio_test.wav"); + // No input device -> explainable failure, no crash. + m.set_input_device(-1).unwrap(); + assert!(m.start_recording(&wav_params("unused.wav"), &mut vec![0u8; 64]).is_err()); + + m.set_input_device(0).unwrap(); + let path = std::env::temp_dir().join(format!("oakaudio_rec_{}.wav", std::process::id())); + let filename = path.to_str().unwrap(); + let mut err = vec![0u8; 256]; + match m.start_recording(&wav_params(filename), &mut err) { + Ok(()) => { + assert!(m.stop_recording().is_ok()); + } + Err(e) => { + assert!(!e.to_string().is_empty(), "failure must carry a reason"); + } } - - // NULL params is invalid and reports the reason in error_buf. - let mut err = [0 as c_char; 64]; - let r = unsafe { - oakaudio_manager_start_recording(m, std::ptr::null(), err.as_mut_ptr(), err.len() as i32) - }; - assert_eq!(r, OAKAUDIO_E_INVALID); - assert!(err.iter().any(|&b| b != 0)); - - // A disabled audio track is likewise invalid. - let mut disabled = encoding_params(); - disabled.audio_enabled = 0; - let mut err = [0 as c_char; 64]; - let r = unsafe { - oakaudio_manager_start_recording(m, &disabled, err.as_mut_ptr(), err.len() as i32) - }; - assert_eq!(r, OAKAUDIO_E_INVALID); - assert!(err.iter().any(|&b| b != 0)); - - unsafe { oakaudio_manager_destroy_instance() }; + let _ = std::fs::remove_file(path); } -/// Device enumeration is not bridged: every name/config lookup falls back to -/// paNoDevice (-1); a NULL name is OAKAUDIO_E_INVALID. The config-backed -/// buffer size/name helpers degrade to their defaults. +/// Config defaults are read from the (empty) oakcommon store: buffer size +/// falls back to 0 and device names are absent. #[test] -fn device_name_lookup() { - let _guard = lock(); - - assert_eq!( - unsafe { oakaudio_manager_find_device_by_name_s(std::ptr::null(), 1) }, - OAKAUDIO_E_INVALID - ); - let name = c"anything"; - assert_eq!( - unsafe { oakaudio_manager_find_device_by_name_s(name.as_ptr(), 1) }, - -1 - ); - assert_eq!( - unsafe { oakaudio_manager_find_config_device_by_name_s(1) }, - -1 - ); - assert_eq!( - unsafe { oakaudio_manager_find_config_device_by_name_s(0) }, - -1 - ); - - // config::output_buffer_size() reads its default (0) from the stub; - // device_name degrades to the empty string. +fn config_defaults() { assert_eq!(oakaudio::config::output_buffer_size(), 0); - assert!(oakaudio::config::device_name(true).as_c_str().is_empty()); - assert!(oakaudio::config::device_name(false).as_c_str().is_empty()); + assert!(oakaudio::config::device_name(true).is_err(), "no configured output device"); + assert!(oakaudio::config::device_name(false).is_err(), "no configured input device"); } -/// PreviewAudioDevice pull-side plumbing (read/notify callback/clock) that -/// the manager path only touches indirectly. +/// PreviewAudioDevice pull-side plumbing (read/notify callback/clock). #[test] fn preview_device_pull_side() { - use oakaudio::params::AudioParams; use oakaudio::previewdevice::PreviewAudioDevice; let mut dev = PreviewAudioDevice::new(); dev.set_params(AudioParams { sample_rate: 48000, channel_layout: 3, - format: oakaudio::params::SampleFormat::F32, + format: SampleFormat::F32, }); assert_eq!(dev.bytes_per_frame(), 8); @@ -389,85 +233,65 @@ fn preview_device_pull_side() { assert_eq!(dev.output_frames_consumed(), 0); } -/// free(NULL)/free(empty) are no-ops on the manager handle. -#[test] -fn free_null_noop() { - let _guard = lock(); - unsafe { oakaudio_manager_create_instance() }; - let before = unsafe { oakaudio_debug_alive_count() }; - - let mut empty = oakaudio::handle::CHandle::null(); - unsafe { oakaudio_manager_free(&mut empty) }; - unsafe { oakaudio_manager_free(std::ptr::null_mut()) }; - assert_eq!(unsafe { oakaudio_debug_alive_count() }, before); - - unsafe { oakaudio_manager_destroy_instance() }; -} - -/// output_levels: validation, the no-output case, and a real peak -/// readback over pushed packed-F32 stereo samples (left ramps to 0.25, -/// right to ~1.0 — the per-channel linear peaks). +/// output_levels: the no-output case, and a real peak readback over pushed +/// packed-F32 stereo samples (left ramps to 0.05, right to ~0.2 — the +/// per-channel linear peaks). +/// +/// The push can open a real cpal stream whose callback consumes queued +/// samples concurrently, so a long ramp is pushed and the peaks are +/// asserted with a tolerance that absorbs partial consumption (the +/// analysis window is the most recent 8192 frames of the queue). #[test] fn output_levels_reports_buffered_peaks() { - let _guard = lock(); - unsafe { oakaudio_manager_destroy_instance() }; - assert_eq!(unsafe { oakaudio_manager_create_instance() }, OAKAUDIO_OK); - let m = instance(); + let _guard = lock_manager(); + fresh_instance(); + let mut m = instance().unwrap(); + + // Nothing configured yet: no channels. + assert_eq!(m.output_levels(&mut [0.0f32; 4]).unwrap(), 0); + + m.set_output_device(42).unwrap(); + m.clear_buffered_output().unwrap(); + + // Push 2 seconds of packed F32 stereo ramp (format 10), stereo layout + // 0x3. Left ramps 0 -> 0.05, right 0 -> 0.2. + let frames = 96000usize; + let mut packed = Vec::with_capacity(frames * 2 * 4); + for i in 0..frames { + let t = i as f32 / frames as f32; + packed.extend_from_slice(&(0.05f32 * t).to_le_bytes()); + packed.extend_from_slice(&(0.2f32 * t).to_le_bytes()); + } + m.push_to_output(stereo(), &packed, &mut vec![0u8; 64]).unwrap(); - // Invalid out args. let mut peaks = [0.0f32; 4]; - assert_eq!( - unsafe { oakaudio_manager_output_levels(m, std::ptr::null_mut(), 4) }, - OAKAUDIO_E_INVALID + let n = m.output_levels(&mut peaks).unwrap(); + assert_eq!(n, 2); + assert!( + (peaks[0] - 0.05).abs() < 1e-3 && peaks[0] > 0.04, + "left peak: {}", + peaks[0] + ); + assert!( + (peaks[1] - 0.2).abs() < 1e-3 && peaks[1] > 0.19, + "right peak: {}", + peaks[1] ); - assert_eq!(unsafe { oakaudio_manager_output_levels(m, peaks.as_mut_ptr(), 0) }, OAKAUDIO_E_INVALID); - // The manager singleton retains output params across tests in this - // binary, so the "no output" case is not reachable here; instead - // verify a cleared buffer reports zeroed peaks (channel count from - // the configured layout, 0 only when nothing was ever configured). - assert_eq!(unsafe { oakaudio_manager_set_output_device(m, 42) }, OAKAUDIO_OK); - assert_eq!(unsafe { oakaudio_manager_clear_buffered_output(m) }, OAKAUDIO_OK); - let cleared = unsafe { oakaudio_manager_output_levels(m, peaks.as_mut_ptr(), 4) }; - assert!(cleared >= 0); - for p in &peaks[..cleared as usize] { + // A cleared buffer reports zeroed peaks over the configured channels. + m.clear_buffered_output().unwrap(); + let mut cleared = [0.0f32; 4]; + assert_eq!(m.output_levels(&mut cleared).unwrap(), 2); + for p in &cleared[..2] { assert_eq!(*p, 0.0, "cleared buffer must have silent peaks"); } - // Push 480 frames of packed F32 stereo (format 10), stereo layout 0x3. - 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 rc = unsafe { - oakaudio_manager_push_to_output( - m, - 48000, - 0x3, - 10, - samples.as_ptr() as *const c_char, - (samples.len() * 4) as i64, - std::ptr::null_mut(), - 0, - ) - }; - assert_eq!(rc, OAKAUDIO_OK); - - let n = unsafe { oakaudio_manager_output_levels(m, 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]); - - // Undersized buffer truncates the write, not the reported count. - let mut one = [0.0f32; 1]; - assert_eq!(unsafe { oakaudio_manager_output_levels(m, one.as_mut_ptr(), 1) }, 2); + // The error mapping is intact. + assert_eq!(Error::Invalid.code(), OAKAUDIO_E_INVALID); + assert_eq!(Error::Failed("x".to_string()).code(), oakaudio::error::OAKAUDIO_E_FAILED); // Leave the singleton as we found it: the push flipped // `output_started`, which other tests' seconds() assertions depend on. - assert_eq!(unsafe { oakaudio_manager_stop_output(m) }, OAKAUDIO_OK); - assert_eq!(unsafe { oakaudio_manager_clear_buffered_output(m) }, OAKAUDIO_OK); + m.stop_output().unwrap(); + m.clear_buffered_output().unwrap(); } diff --git a/crates/oakaudio/tests/processor_test.rs b/crates/oakaudio/tests/processor_test.rs index bf6a8801c..08b8894f0 100644 --- a/crates/oakaudio/tests/processor_test.rs +++ b/crates/oakaudio/tests/processor_test.rs @@ -14,20 +14,15 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//! AudioProcessor contract tests (processor.rs), through the C ABI. The -//! conversion runs a real FFmpeg filter graph (aresample/aformat/atempo), -//! so resampling and time-stretch have filter latency: the exact frame -//! counts are drained after `flush`, while identity conversion is an -//! immediate passthrough. +//! AudioProcessor contract tests (processor.rs), calling the public Rust +//! API. The conversion runs a real FFmpeg filter graph +//! (aresample/aformat/atempo), so resampling and time-stretch have filter +//! latency: the exact frame counts are drained after `flush`, while +//! identity conversion is an immediate passthrough. -mod common; - -use oakaudio::error::{OAKAUDIO_E_INVALID, OAKAUDIO_E_STATE, OAKAUDIO_OK}; -use oakaudio::ffi::processor::{ - oakaudio_processor_close, oakaudio_processor_convert, oakaudio_processor_flush, - oakaudio_processor_free, oakaudio_processor_init, oakaudio_processor_is_open, - oakaudio_processor_open, -}; +use oakaudio::error::{Error, OAKAUDIO_E_INVALID, OAKAUDIO_E_STATE, OAKAUDIO_OK}; +use oakaudio::params::{AudioParams, SampleFormat}; +use oakaudio::processor::Processor; /// Stereo f32_p planes of `frames` ramp samples. fn ramp_planes(frames: usize) -> Vec> { @@ -37,37 +32,61 @@ fn ramp_planes(frames: usize) -> Vec> { ] } -fn open_identity(h: oakaudio::handle::CHandle) -> i32 { - unsafe { oakaudio_processor_open(h, 48000, 3, 4, 48000, 3, 4, 1.0) } +/// Stereo planar-f32 params at `rate`. +fn params(rate: i32) -> AudioParams { + AudioParams { + sample_rate: rate, + channel_layout: 0x3, + format: SampleFormat::F32Planar, + } } -/// init yields a valid handle; is_open is false before open and true after; -/// close returns it to closed without error. +fn open_identity(p: &Processor) -> Result<(), Box> { + p.open(params(48000), params(48000), 1.0) +} + +/// Pointer arrays for the convert call. +fn plane_ptrs(planes: &[Vec]) -> Vec<*const f32> { + planes.iter().map(|p| p.as_ptr()).collect() +} + +fn plane_mut_ptrs(planes: &mut [Vec]) -> Vec<*mut f32> { + planes.iter_mut().map(|p| p.as_mut_ptr()).collect() +} + +/// The crate error code of a failed call (the API surfaces +/// `Box`). +fn code(err: Box) -> i32 { + err.downcast_ref::() + .map(|e| e.code()) + .unwrap_or(-1) +} + +/// init yields a closed processor; is_open is false before open and true +/// after; close returns it to closed without error. #[test] fn processor_open_isopen_close() { - let mut h = unsafe { oakaudio_processor_init() }; - assert!(!h.ctx.is_null()); - assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 0); - assert_eq!(open_identity(h), OAKAUDIO_OK); - assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 1); - assert_eq!(unsafe { oakaudio_processor_close(h) }, OAKAUDIO_OK); - assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 0); - unsafe { oakaudio_processor_free(&mut h) }; + let p = Processor::init(); + assert_eq!(p.is_open().unwrap(), false); + open_identity(&p).unwrap(); + assert_eq!(p.is_open().unwrap(), true); + p.close().unwrap(); + assert_eq!(p.is_open().unwrap(), false); } /// open with matching in/out rate and format is an identity passthrough: /// convert returns the same frame count and samples within 1e-6. #[test] fn identity_convert_passthrough() { - let mut h = unsafe { oakaudio_processor_init() }; - assert_eq!(open_identity(h), OAKAUDIO_OK); + let p = Processor::init(); + open_identity(&p).unwrap(); let planes = ramp_planes(32); - let in_ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect(); + let in_ptrs = plane_ptrs(&planes); let mut out = vec![vec![0f32; 32]; 2]; - let mut out_ptrs: Vec<*mut f32> = out.iter_mut().map(|p| p.as_mut_ptr()).collect(); + let mut out_ptrs = plane_mut_ptrs(&mut out); - let n = unsafe { oakaudio_processor_convert(h, in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 32) }; + let n = p.convert(in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 32).unwrap(); assert_eq!(n, 32); for ch in 0..2 { for i in 0..32 { @@ -79,22 +98,21 @@ fn identity_convert_passthrough() { ); } } - unsafe { oakaudio_processor_free(&mut h) }; } /// convert with an output capacity smaller than the produced frames returns /// the produced count clamped to capacity and fills up to capacity. #[test] fn convert_capacity_truncation() { - let mut h = unsafe { oakaudio_processor_init() }; - assert_eq!(open_identity(h), OAKAUDIO_OK); + let p = Processor::init(); + open_identity(&p).unwrap(); let planes = ramp_planes(32); - let in_ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect(); + let in_ptrs = plane_ptrs(&planes); let mut out = vec![vec![9.9f32; 10]; 2]; - let mut out_ptrs: Vec<*mut f32> = out.iter_mut().map(|p| p.as_mut_ptr()).collect(); + let mut out_ptrs = plane_mut_ptrs(&mut out); - let n = unsafe { oakaudio_processor_convert(h, in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 10) }; + let n = p.convert(in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 10).unwrap(); assert_eq!(n, 10); for ch in 0..2 { for i in 0..10 { @@ -104,56 +122,40 @@ fn convert_capacity_truncation() { // The graph has already drained; nothing further to pull. let mut out2 = vec![vec![0f32; 32]; 2]; - let mut out2_ptrs: Vec<*mut f32> = out2.iter_mut().map(|p| p.as_mut_ptr()).collect(); - let n = unsafe { oakaudio_processor_convert(h, in_ptrs.as_ptr(), 0, out2_ptrs.as_ptr(), 32) }; + let mut out2_ptrs = plane_mut_ptrs(&mut out2); + let n = p.convert(in_ptrs.as_ptr(), 0, out2_ptrs.as_ptr(), 32).unwrap(); assert_eq!(n, 0); - - unsafe { oakaudio_processor_free(&mut h) }; } -/// open with a zero/negative rate or a wrong output format returns -/// OAKAUDIO_E_INVALID and leaves the processor closed; an empty handle is -/// OAKAUDIO_E_INVALID everywhere. +/// open with a zero/negative rate or a wrong output format is rejected and +/// leaves the processor closed; convert before open is a state error. #[test] fn open_invalid_params() { - let mut h = unsafe { oakaudio_processor_init() }; + let p = Processor::init(); assert_eq!( - unsafe { oakaudio_processor_open(h, 0, 3, 4, 48000, 3, 4, 1.0) }, + code(p.open(params(0), params(48000), 1.0).unwrap_err()), OAKAUDIO_E_INVALID ); - assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 0); + assert_eq!(p.is_open().unwrap(), false); + // The output format is forced to planar f32 by the processor contract. + let mut wrong_out = params(48000); + wrong_out.format = SampleFormat::F32; assert_eq!( - unsafe { oakaudio_processor_open(h, 48000, 3, 4, 48000, 3, 0, 1.0) }, + code(p.open(params(48000), wrong_out, 1.0).unwrap_err()), OAKAUDIO_E_INVALID ); - assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 0); + assert_eq!(p.is_open().unwrap(), false); - let empty = oakaudio::handle::CHandle::null(); - assert_eq!(open_identity(empty), OAKAUDIO_E_INVALID); + // convert before open is a state error; a non-positive speed is + // rejected on open. assert_eq!( - unsafe { oakaudio_processor_is_open(empty) }, - OAKAUDIO_E_INVALID - ); - assert_eq!( - unsafe { oakaudio_processor_close(empty) }, - OAKAUDIO_E_INVALID - ); - assert_eq!( - unsafe { oakaudio_processor_flush(empty) }, - OAKAUDIO_E_INVALID - ); - let mut out_ptrs: Vec<*mut f32> = Vec::new(); - assert_eq!( - unsafe { oakaudio_processor_convert(empty, std::ptr::null(), 0, out_ptrs.as_ptr(), 0) }, - OAKAUDIO_E_INVALID - ); - - // convert before open is a state error. - assert_eq!( - unsafe { oakaudio_processor_convert(h, std::ptr::null(), 0, out_ptrs.as_ptr(), 0) }, + code(p.convert(std::ptr::null(), 0, std::ptr::null(), 0).unwrap_err()), OAKAUDIO_E_STATE ); - unsafe { oakaudio_processor_free(&mut h) }; + assert_eq!( + code(p.open(params(48000), params(48000), 0.0).unwrap_err()), + OAKAUDIO_E_INVALID + ); } /// Resampling to half rate halves the frame count (44100 -> 22050). The @@ -161,34 +163,23 @@ fn open_invalid_params() { /// drained after `flush`; flush then keeps the processor open. #[test] fn resample_and_flush() { - let mut h = unsafe { oakaudio_processor_init() }; - assert_eq!( - unsafe { oakaudio_processor_open(h, 44100, 3, 4, 22050, 3, 4, 1.0) }, - OAKAUDIO_OK - ); + let p = Processor::init(); + p.open(params(44100), params(22050), 1.0).unwrap(); // 1 second of input keeps the resampler delay well below the signal. let frames = 44100; let planes = ramp_planes(frames); - let in_ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect(); + let in_ptrs = plane_ptrs(&planes); let mut out = vec![vec![0f32; frames]; 2]; - let mut out_ptrs: Vec<*mut f32> = out.iter_mut().map(|p| p.as_mut_ptr()).collect(); + let mut out_ptrs = plane_mut_ptrs(&mut out); - let mut total = unsafe { - oakaudio_processor_convert( - h, - in_ptrs.as_ptr(), - frames as i32, - out_ptrs.as_ptr(), - frames as i32, - ) - }; - assert_eq!(unsafe { oakaudio_processor_flush(h) }, OAKAUDIO_OK); + let mut total = p + .convert(in_ptrs.as_ptr(), frames as i32, out_ptrs.as_ptr(), frames as i32) + .unwrap(); + p.flush().unwrap(); // Drain the resampler delay after end-of-input. while total < frames as i32 { - let n = unsafe { - oakaudio_processor_convert(h, std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32) - }; + let n = p.convert(std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32).unwrap(); if n == 0 { break; } @@ -198,8 +189,7 @@ fn resample_and_flush() { (total - 22050).abs() <= 2, "half-rate output must halve the frame count (got {total})" ); - assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 1); - unsafe { oakaudio_processor_free(&mut h) }; + assert_eq!(p.is_open().unwrap(), true); } /// A tempo factor != 1.0 time-stretches: tempo 2.0 halves the frame count @@ -207,33 +197,22 @@ fn resample_and_flush() { /// produces output) and the processor stays open. #[test] fn tempo_stretch() { - let mut h = unsafe { oakaudio_processor_init() }; - assert_eq!( - unsafe { oakaudio_processor_open(h, 48000, 3, 4, 48000, 3, 4, 2.0) }, - OAKAUDIO_OK - ); + let p = Processor::init(); + p.open(params(48000), params(48000), 2.0).unwrap(); // 1 second of input: many atempo windows (1024 samples at 48 kHz). let frames = 48000; let planes = ramp_planes(frames); - let in_ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect(); + let in_ptrs = plane_ptrs(&planes); let mut out = vec![vec![0f32; frames]; 2]; - let mut out_ptrs: Vec<*mut f32> = out.iter_mut().map(|p| p.as_mut_ptr()).collect(); + let mut out_ptrs = plane_mut_ptrs(&mut out); - let mut total = unsafe { - oakaudio_processor_convert( - h, - in_ptrs.as_ptr(), - frames as i32, - out_ptrs.as_ptr(), - frames as i32, - ) - }; - assert_eq!(unsafe { oakaudio_processor_flush(h) }, OAKAUDIO_OK); + let mut total = p + .convert(in_ptrs.as_ptr(), frames as i32, out_ptrs.as_ptr(), frames as i32) + .unwrap(); + p.flush().unwrap(); while total < frames as i32 { - let n = unsafe { - oakaudio_processor_convert(h, std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32) - }; + let n = p.convert(std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32).unwrap(); if n == 0 { break; } @@ -243,24 +222,20 @@ fn tempo_stretch() { (total - 24000).abs() <= 2400, "tempo 2.0 must halve the frame count (got {total})" ); - assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 1); - unsafe { oakaudio_processor_close(h) }; + assert_eq!(p.is_open().unwrap(), true); + p.close().unwrap(); - // A non-positive speed is rejected (on a closed processor). - assert_eq!( - unsafe { oakaudio_processor_open(h, 48000, 3, 4, 48000, 3, 4, 0.0) }, - OAKAUDIO_E_INVALID - ); - assert_eq!(open_identity(h), OAKAUDIO_OK); - // Already open -> state error. - assert_eq!(open_identity(h), OAKAUDIO_E_STATE); - unsafe { oakaudio_processor_free(&mut h) }; + // Re-opening a closed processor works; opening an open one is a state + // error. + open_identity(&p).unwrap(); + assert_eq!(code(open_identity(&p).unwrap_err()), OAKAUDIO_E_STATE); } -/// free(NULL)/free(empty) are no-ops. +/// The error mapping is intact. #[test] -fn free_null_noop() { - let mut h = oakaudio::handle::CHandle::null(); - unsafe { oakaudio_processor_free(&mut h) }; - unsafe { oakaudio_processor_free(std::ptr::null_mut()) }; +fn error_codes() { + assert_eq!(Error::Invalid.code(), OAKAUDIO_E_INVALID); + assert_eq!(Error::State.code(), OAKAUDIO_E_STATE); + assert_eq!(Error::Failed("x".to_string()).code(), -60003); + assert_eq!(oakaudio::error::OAKAUDIO_OK, OAKAUDIO_OK); } diff --git a/crates/oakaudio/tests/sync_test.rs b/crates/oakaudio/tests/sync_test.rs index a1b0a0020..b4b86f07d 100644 --- a/crates/oakaudio/tests/sync_test.rs +++ b/crates/oakaudio/tests/sync_test.rs @@ -15,24 +15,22 @@ // along with this program. If not, see . //! AudioSynchronizer + AudioWaveformSync contract tests -//! (synchronizer.rs, waveformsync.rs), through the C ABI. +//! (synchronizer.rs, waveformsync.rs), calling the public Rust API. mod common; -use oakaudio::error::OAKAUDIO_E_INVALID; -use oakaudio::ffi::sync::{ - oakaudio_sync_estimate_envelope_offset, oakaudio_sync_estimate_stretch_and_offset, - oakaudio_sync_extract_rms_envelope, oakaudio_sync_place_by_source_time, - oakaudio_sync_place_by_waveform_offset, OffsetResult, SourceClip, StretchOffsetResult, +use oakcore_rs::Rational; +use oakaudio::synchronizer::{place_by_source_time, place_by_waveform_offset, SourceClip}; +use oakaudio::waveformsync::{ + estimate_envelope_offset, estimate_envelope_offset_valid, estimate_stretch_and_offset, + extract_rms_envelope, }; fn clip(source: i64, media_in: i64, has_source: bool) -> SourceClip { SourceClip { - source_start_time_num: source, - source_start_time_den: 1, - media_in_num: media_in, - media_in_den: 1, - has_source_start_time: has_source as i32, + source_start_time: Rational::new(source, 1), + media_in: Rational::new(media_in, 1), + has_source_start_time: has_source, } } @@ -43,28 +41,15 @@ fn clip(source: i64, media_in: i64, has_source: bool) -> SourceClip { fn place_by_source_time_matching() { let reference = clip(0, 0, true); let candidate = clip(0, 0, true); - let (mut num, mut den, mut valid) = (0i64, 0i64, 0i32); - let r = unsafe { - oakaudio_sync_place_by_source_time( - &reference, &candidate, 5, 1, &mut num, &mut den, &mut valid, - ) - }; - assert_eq!(r, 0); - assert_eq!(num, 5); - assert_eq!(den, 1); - assert_eq!(valid, 1); + let placement = place_by_source_time(&reference, &candidate, Rational::new(5, 1)); + assert!(placement.valid); + assert_eq!(placement.timeline_in, Rational::new(5, 1)); - // A source-less candidate is invalid: valid=0, null rational. + // A source-less candidate is invalid: valid=false, null rational. let candidate = clip(0, 0, false); - let r = unsafe { - oakaudio_sync_place_by_source_time( - &reference, &candidate, 5, 1, &mut num, &mut den, &mut valid, - ) - }; - assert_eq!(r, 0); - assert_eq!(valid, 0); - assert_eq!(num, 0); - assert_eq!(den, 0); + let placement = place_by_source_time(&reference, &candidate, Rational::new(5, 1)); + assert!(!placement.valid); + assert!(placement.timeline_in.is_null()); } /// place_by_source_time: when source times disagree by a known delta, the @@ -73,62 +58,28 @@ fn place_by_source_time_matching() { fn place_by_source_time_delta() { let reference = clip(5, 0, true); let candidate = clip(12, 0, true); - let (mut num, mut den, mut valid) = (0i64, 0i64, 0i32); - let r = unsafe { - oakaudio_sync_place_by_source_time( - &reference, &candidate, 0, 1, &mut num, &mut den, &mut valid, - ) - }; - assert_eq!(r, 0); + let placement = place_by_source_time(&reference, &candidate, Rational::new(0, 1)); // 0 + (12 + 0) - (5 + 0) = 7 - assert_eq!(num, 7); - assert_eq!(den, 1); - assert_eq!(valid, 1); - - // A zero denominator is rejected up front. - let r = unsafe { - oakaudio_sync_place_by_source_time( - &reference, &candidate, 0, 0, &mut num, &mut den, &mut valid, - ) - }; - assert_eq!(r, OAKAUDIO_E_INVALID); + assert!(placement.valid); + assert_eq!(placement.timeline_in, Rational::new(7, 1)); } /// place_by_waveform_offset converts a sample offset at a sample rate into -/// a timeline-in shift; out_valid is 1 on success and 0 for a null rate. +/// a timeline-in shift; out_valid is false for a null rate. #[test] fn place_by_waveform_offset_conversion() { - let (mut num, mut den, mut valid) = (0i64, 0i64, 0i32); - let r = unsafe { - oakaudio_sync_place_by_waveform_offset(0, 1, 48000, 48000, &mut num, &mut den, &mut valid) - }; - assert_eq!(r, 0); - assert_eq!(num, 1); - assert_eq!(den, 1); - assert_eq!(valid, 1); + let placement = place_by_waveform_offset(Rational::new(0, 1), 48000, 48000); + assert!(placement.valid); + assert_eq!(placement.timeline_in, Rational::new(1, 1)); - let r = unsafe { - oakaudio_sync_place_by_waveform_offset(1, 2, 48000, 48000, &mut num, &mut den, &mut valid) - }; - assert_eq!(r, 0); - assert_eq!(num, 3); - assert_eq!(den, 2); - assert_eq!(valid, 1); + let placement = place_by_waveform_offset(Rational::new(1, 2), 48000, 48000); + assert!(placement.valid); + assert_eq!(placement.timeline_in, Rational::new(3, 2)); - // A null rate is invalid (valid=0, null rational), and the FFI rejects - // a zero timeline denominator. - let r = unsafe { - oakaudio_sync_place_by_waveform_offset(1, 2, 48000, 0, &mut num, &mut den, &mut valid) - }; - assert_eq!(r, 0); - assert_eq!(valid, 0); - assert_eq!(num, 0); - assert_eq!(den, 0); - - let r = unsafe { - oakaudio_sync_place_by_waveform_offset(1, 0, 48000, 48000, &mut num, &mut den, &mut valid) - }; - assert_eq!(r, OAKAUDIO_E_INVALID); + // A null rate is invalid (valid=false, null rational). + let placement = place_by_waveform_offset(Rational::new(1, 2), 48000, 0); + assert!(!placement.valid); + assert!(placement.timeline_in.is_null()); } /// extract_rms_envelope produces one value per window; a window larger than @@ -137,85 +88,31 @@ fn place_by_waveform_offset_conversion() { fn extract_rms_envelope_shape() { let data: Vec = (0..100).map(|i| i as f32).collect(); let planes = common::planar_from(&data, 2); - let ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect(); + let refs: Vec<&[f32]> = planes.iter().map(Vec::as_slice).collect(); - let mut out = vec![0.0f64; 16]; - let n = unsafe { - oakaudio_sync_extract_rms_envelope(ptrs.as_ptr(), 2, 100, 10, out.as_mut_ptr(), 16) - }; - assert_eq!(n, 10); - assert!(out.iter().take(10).all(|&v| v > 0.0)); + let env = extract_rms_envelope(&refs, 10); + assert_eq!(env.len(), 10); + assert!(env.iter().all(|&v| v > 0.0)); - let n = unsafe { - oakaudio_sync_extract_rms_envelope(ptrs.as_ptr(), 2, 100, 200, out.as_mut_ptr(), 16) - }; - assert_eq!(n, 1); - - // Invalid inputs. - assert_eq!( - unsafe { - oakaudio_sync_extract_rms_envelope(std::ptr::null(), 2, 100, 10, out.as_mut_ptr(), 16) - }, - OAKAUDIO_E_INVALID - ); - assert_eq!( - unsafe { - oakaudio_sync_extract_rms_envelope(ptrs.as_ptr(), 0, 100, 10, out.as_mut_ptr(), 16) - }, - OAKAUDIO_E_INVALID - ); - assert_eq!( - unsafe { - oakaudio_sync_extract_rms_envelope(ptrs.as_ptr(), 2, 100, 0, out.as_mut_ptr(), 16) - }, - OAKAUDIO_E_INVALID - ); - assert_eq!( - unsafe { - oakaudio_sync_extract_rms_envelope(ptrs.as_ptr(), 2, 100, 10, out.as_mut_ptr(), -1) - }, - OAKAUDIO_E_INVALID - ); - // Two-stage: NULL out returns the required count. - let n = unsafe { - oakaudio_sync_extract_rms_envelope(ptrs.as_ptr(), 2, 100, 10, std::ptr::null_mut(), 0) - }; - assert_eq!(n, 10); + let env = extract_rms_envelope(&refs, 200); + assert_eq!(env.len(), 1); } /// estimate_envelope_offset: for a candidate delayed by N windows relative -/// to the reference, the returned offset is +N windows and valid=1. +/// to the reference, the returned offset is +N windows and valid=true. #[test] fn envelope_offset_recovers_delay() { let reference: Vec = (0..10).map(|i| i as f64).collect(); let mut candidate = vec![0.0f64; 10]; candidate[2..].copy_from_slice(&reference[..8]); - let mut out = OffsetResult { - offset_samples: 0, - confidence: 0.0, - valid: 0, - }; - let r = unsafe { - oakaudio_sync_estimate_envelope_offset( - reference.as_ptr(), - 10, - candidate.as_ptr(), - 10, - std::ptr::null(), - std::ptr::null(), - 100, - 10, - &mut out, - ) - }; - assert_eq!(r, 0); - assert_eq!(out.valid, 1); + let out = estimate_envelope_offset(&reference, &candidate, 100, 10); + assert!(out.valid); assert_eq!(out.offset_samples, 200); assert!((out.confidence - 1.0).abs() < 1e-9); } -/// estimate_envelope_offset: windows masked invalid on either side are -/// excluded from correlation; empty masks are treated as all-valid. +/// estimate_envelope_offset_valid: windows masked invalid on either side +/// are excluded from correlation; empty masks are treated as all-valid. #[test] fn envelope_offset_respects_valid_masks() { let reference: Vec = (0..10).map(|i| i as f64).collect(); @@ -224,68 +121,35 @@ fn envelope_offset_respects_valid_masks() { // Only the last reference window is valid -> no lag has >= 2 valid // overlap windows, so the estimate is invalid. - let mut ref_valid = [1u8; 10]; - ref_valid[..9].fill(0); - let mut out = OffsetResult { - offset_samples: 0, - confidence: 0.0, - valid: 0, - }; - let r = unsafe { - oakaudio_sync_estimate_envelope_offset( - reference.as_ptr(), - 10, - candidate.as_ptr(), - 10, - ref_valid.as_ptr(), - std::ptr::null(), - 100, - 10, - &mut out, - ) - }; - assert_eq!(r, 0); - assert_eq!(out.valid, 0); + let mut ref_valid = vec![false; 10]; + ref_valid[9] = true; + let out = estimate_envelope_offset_valid( + &reference, + &candidate, + &ref_valid, + &[], + 100, + 10, + ); + assert!(!out.valid); assert_eq!(out.confidence, 0.0); // Fully-valid masks behave like the unmasked call. - let mut valid = [1u8; 10]; - let r = unsafe { - oakaudio_sync_estimate_envelope_offset( - reference.as_ptr(), - 10, - candidate.as_ptr(), - 10, - valid.as_ptr(), - valid.as_ptr(), - 100, - 10, - &mut out, - ) - }; - assert_eq!(r, 0); - assert_eq!(out.valid, 1); + let valid = vec![true; 10]; + let out = estimate_envelope_offset_valid( + &reference, + &candidate, + &valid, + &valid, + 100, + 10, + ); + assert!(out.valid); assert_eq!(out.offset_samples, 200); - - // NULL arrays / non-positive lengths are invalid. - let r = unsafe { - oakaudio_sync_estimate_envelope_offset( - std::ptr::null(), - 10, - candidate.as_ptr(), - 10, - std::ptr::null(), - std::ptr::null(), - 100, - 10, - &mut out, - ) - }; - assert_eq!(r, OAKAUDIO_E_INVALID); } /// estimate_stretch_and_offset: a candidate sampled at 2x the reference -/// rate reports rate ~2.0 (>1 = speed up) with a valid=1 result. A +/// rate reports rate ~2.0 (>1 = speed up) with a valid=true result. A /// non-linear (sine) reference is used — normalized correlation of linear /// ramps is degenerate (any rate correlates 1.0), but only the true rate /// resamples the sine back onto the reference exactly. @@ -302,78 +166,43 @@ fn stretch_offset_recovers_rate() { candidate.push((reference[k] + reference[k + 1]) / 2.0); } } - let mut out = StretchOffsetResult { - rate: 0.0, - offset_samples: 0, - confidence: 0.0, - valid: 0, - }; - let r = unsafe { - oakaudio_sync_estimate_stretch_and_offset( - reference.as_ptr(), - 10, - candidate.as_ptr(), - candidate.len() as i32, - std::ptr::null(), - std::ptr::null(), - 100, - 10, - 0.5, - 3.0, - 0.1, - &mut out, - ) - }; - assert_eq!(r, 0); - assert_eq!(out.valid, 1); + let out = estimate_stretch_and_offset( + &reference, + &candidate, + &[], + &[], + 100, + 10, + 0.5, + 3.0, + 0.1, + ); + assert!(out.valid); assert!((out.rate - 2.0).abs() < 0.15, "rate = {}", out.rate); assert!(out.confidence > 0.99, "confidence = {}", out.confidence); - // Invalid rate parameters are rejected. - let r = unsafe { - oakaudio_sync_estimate_stretch_and_offset( - reference.as_ptr(), - 10, - candidate.as_ptr(), - candidate.len() as i32, - std::ptr::null(), - std::ptr::null(), - 100, - 10, - 0.0, - 3.0, - 0.1, - &mut out, - ) - }; - assert_eq!(r, OAKAUDIO_E_INVALID); + // Invalid rate parameters are rejected (invalid result, defaults). + let out = estimate_stretch_and_offset( + &reference, + &candidate, + &[], + &[], + 100, + 10, + 0.0, + 3.0, + 0.1, + ); + assert!(!out.valid); } /// estimate_* on identical silent envelopes yields low/no confidence and -/// valid=0 (no correlation peak). +/// valid=false (no correlation peak). #[test] fn silent_inputs_invalid() { let silence = vec![0.0f64; 10]; - let mut out = OffsetResult { - offset_samples: 0, - confidence: 0.0, - valid: 0, - }; - let r = unsafe { - oakaudio_sync_estimate_envelope_offset( - silence.as_ptr(), - 10, - silence.as_ptr(), - 10, - std::ptr::null(), - std::ptr::null(), - 100, - 10, - &mut out, - ) - }; - assert_eq!(r, 0); - assert_eq!(out.valid, 0); + let out = estimate_envelope_offset(&silence, &silence, 100, 10); + assert!(!out.valid); assert_eq!(out.confidence, 0.0); } diff --git a/crates/oakaudio/tests/waveform_test.rs b/crates/oakaudio/tests/waveform_test.rs index 930c9a75c..ebfb10514 100644 --- a/crates/oakaudio/tests/waveform_test.rs +++ b/crates/oakaudio/tests/waveform_test.rs @@ -14,56 +14,29 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//! AudioVisualWaveform contract tests (waveform.rs), through the C ABI. +//! AudioVisualWaveform contract tests (waveform.rs), calling the public +//! Rust API. mod common; use std::ffi::CString; -use common::{pair, write_wav}; -use oakaudio::error::{OAKAUDIO_E_INVALID, OAKAUDIO_E_STATE}; -use oakaudio::ffi::waveform::{ - oakaudio_waveform_extract, oakaudio_waveform_free, oakaudio_waveform_get_channel_count, - oakaudio_waveform_get_summary, oakaudio_waveform_init, oakaudio_waveform_length, - oakaudio_waveform_overwrite_samples, oakaudio_waveform_overwrite_silence, - oakaudio_waveform_overwrite_sums, oakaudio_waveform_re_sum_s, oakaudio_waveform_resize, - oakaudio_waveform_set_channel_count, oakaudio_waveform_sum_samples_s, - oakaudio_waveform_trim_in, oakaudio_waveform_trim_range, MinMax, -}; +use common::write_wav; +use oakcore_rs::Rational; +use oakaudio::waveform::{extract, AudioVisualWaveform, SamplePerChannel}; /// Fill `w` with 100 samples/channel of a 0..0.99 ramp at 100 Hz (1 s). -fn fill_ramp(w: oakaudio::handle::CHandle) { +fn fill_ramp(w: &mut AudioVisualWaveform) { let ch0: Vec = (0..100).map(|i| i as f32 * 0.01).collect(); let ch1: Vec = (0..100).map(|i| -(i as f32) * 0.01).collect(); - let planes = [ch0.as_ptr(), ch1.as_ptr()]; - assert_eq!(unsafe { oakaudio_waveform_set_channel_count(w, 2) }, 0); - assert_eq!( - unsafe { oakaudio_waveform_overwrite_samples(w, planes.as_ptr(), 100, 100, 0, 1) }, - 0 - ); + let planes = [ch0.as_slice(), ch1.as_slice()]; + w.set_channel_count(2); + w.overwrite_samples(&planes, 100, Rational::new(0, 1)); } -fn summary( - w: oakaudio::handle::CHandle, - start: (i64, i64), - length: (i64, i64), - cap: i32, -) -> Vec { - let mut out = vec![MinMax { min: 0.0, max: 0.0 }; cap as usize * 2]; - let n = unsafe { - oakaudio_waveform_get_summary( - w, - start.0, - start.1, - length.0, - length.1, - out.as_mut_ptr(), - cap, - ) - }; - assert_eq!(n, cap); - out.truncate(cap as usize * 2); - out +/// A one-point-per-channel summary over `start..start+length`. +fn summary(w: &AudioVisualWaveform, start: Rational, length: Rational) -> Vec { + w.get_summary_from_time(start, length) } /// set_channel_count then overwrite_samples writes planar data at the given @@ -71,110 +44,52 @@ fn summary( /// channel-interleaved min/max pairs. #[test] fn overwrite_samples_and_length() { - let mut w = unsafe { oakaudio_waveform_init() }; - fill_ramp(w); + let mut w = AudioVisualWaveform::new(); + fill_ramp(&mut w); - let (mut num, mut den) = (0i64, 0i64); - assert_eq!( - unsafe { oakaudio_waveform_length(w, &mut num, &mut den) }, - 0 - ); - assert_eq!(num, 1); - assert_eq!(den, 1); + assert_eq!(w.length(), Rational::new(1, 1)); - let out = summary(w, (0, 1), (1, 1), 1); + let out = summary(&w, Rational::new(0, 1), Rational::new(1, 1)); + assert_eq!(out.len(), 2); assert_eq!(out[0].min, 0.0); assert!((out[0].max - 0.99).abs() < 1e-5, "max = {}", out[0].max); assert!((out[1].min + 0.99).abs() < 1e-5, "min = {}", out[1].min); assert_eq!(out[1].max, 0.0); - unsafe { oakaudio_waveform_free(&mut w) }; -} - -/// get_summary with out_pairs NULL returns the required point count without -/// writing; a too-small capacity returns the same count and leaves the -/// buffer untouched (two-stage contract). -#[test] -fn summary_two_stage_query() { - let mut w = unsafe { oakaudio_waveform_init() }; - fill_ramp(w); - - // NULL out: required count only. - let n = unsafe { oakaudio_waveform_get_summary(w, 0, 1, 1, 1, std::ptr::null_mut(), 0) }; - assert_eq!(n, 1); - - // Too-small capacity: same count, buffer untouched. - let mut out = [MinMax { - min: -1.0, - max: -1.0, - }; 2]; - let n = unsafe { oakaudio_waveform_get_summary(w, 0, 1, 1, 1, out.as_mut_ptr(), 0) }; - assert_eq!(n, 1); - assert_eq!(out[0].min, -1.0); - assert_eq!(out[0].max, -1.0); - - // A zero/negative length is invalid. - assert_eq!( - unsafe { oakaudio_waveform_get_summary(w, 0, 1, 0, 1, std::ptr::null_mut(), 0) }, - OAKAUDIO_E_INVALID - ); - assert_eq!( - unsafe { oakaudio_waveform_get_summary(w, 0, 1, 1, 0, std::ptr::null_mut(), 0) }, - OAKAUDIO_E_INVALID - ); - unsafe { oakaudio_waveform_free(&mut w) }; } /// overwrite_sums copies channel-interleaved pairs from another waveform -/// into a dest range; a 0/1 length copies all of src. +/// into a dest range; a null length copies all of src. #[test] fn overwrite_sums_range_copy() { - let mut src = unsafe { oakaudio_waveform_init() }; - fill_ramp(src); + let mut src = AudioVisualWaveform::new(); + fill_ramp(&mut src); - let mut dst = unsafe { oakaudio_waveform_init() }; - assert_eq!(unsafe { oakaudio_waveform_set_channel_count(dst, 2) }, 0); - assert_eq!( - unsafe { oakaudio_waveform_overwrite_sums(dst, src, 0, 1, 0, 1, 0, 1) }, - 0 - ); - // A 0/1 length means "copy everything": dst matches src exactly. - let out = summary(dst, (0, 1), (1, 1), 1); + let mut dst = AudioVisualWaveform::new(); + dst.set_channel_count(2); + dst.overwrite_sums(&src, Rational::new(0, 1), Rational::new(0, 1), Rational::NULL); + // A null length means "copy everything": dst matches src exactly. + let out = summary(&dst, Rational::new(0, 1), Rational::new(1, 1)); + assert_eq!(out.len(), 2); assert_eq!(out[0].min, 0.0); assert!((out[0].max - 0.99).abs() < 1e-5, "max = {}", out[0].max); assert!((out[1].min + 0.99).abs() < 1e-5, "min = {}", out[1].min); assert_eq!(out[1].max, 0.0); - - let (mut num, mut den) = (0i64, 0i64); - unsafe { oakaudio_waveform_length(dst, &mut num, &mut den) }; - assert_eq!(num, 1); - - // Empty src handle is invalid. - let empty = oakaudio::handle::CHandle::null(); - assert_eq!( - unsafe { oakaudio_waveform_overwrite_sums(dst, empty, 0, 1, 0, 1, 0, 1) }, - OAKAUDIO_E_INVALID - ); - - unsafe { oakaudio_waveform_free(&mut dst) }; - unsafe { oakaudio_waveform_free(&mut src) }; + assert_eq!(dst.length(), Rational::new(1, 1)); } /// overwrite_silence zeroes min/max over a range without changing length. #[test] fn overwrite_silence() { - let mut w = unsafe { oakaudio_waveform_init() }; - fill_ramp(w); - assert_eq!( - unsafe { oakaudio_waveform_overwrite_silence(w, 0, 1, 1, 2) }, - 0 - ); + let mut w = AudioVisualWaveform::new(); + fill_ramp(&mut w); + w.overwrite_silence(Rational::new(0, 1), Rational::new(1, 2)); // First half is silenced; second half retains the ramp data. - let out = summary(w, (0, 1), (1, 2), 1); - assert_eq!(pair(out[0].min, out[0].max), pair(0.0, 0.0)); - assert_eq!(pair(out[1].min, out[1].max), pair(0.0, 0.0)); + let out = summary(&w, Rational::new(0, 1), Rational::new(1, 2)); + assert_eq!((out[0].min, out[0].max), (0.0, 0.0)); + assert_eq!((out[1].min, out[1].max), (0.0, 0.0)); - let out = summary(w, (1, 2), (1, 2), 1); + let out = summary(&w, Rational::new(1, 2), Rational::new(1, 2)); assert!( out[0].max > 0.5, "second half must keep ramp data, got {:?}", @@ -182,94 +97,61 @@ fn overwrite_silence() { ); assert!(out[1].min < -0.5); - let (mut num, mut den) = (0i64, 0i64); - unsafe { oakaudio_waveform_length(w, &mut num, &mut den) }; assert_eq!( - (num, den), - (1, 1), + w.length(), + Rational::new(1, 1), "overwrite_silence must not change length" ); - unsafe { oakaudio_waveform_free(&mut w) }; } /// trim_in/trim_range/resize adjust length and drop or pad data; a negative /// trim_in prepends silence (C++ semantics). #[test] fn trim_and_resize() { - let mut w = unsafe { oakaudio_waveform_init() }; - fill_ramp(w); + let mut w = AudioVisualWaveform::new(); + fill_ramp(&mut w); // Negative trim_in prepends silence: absolute end (length) unchanged. - assert_eq!(unsafe { oakaudio_waveform_trim_in(w, -1, 2) }, 0); - let (mut num, mut den) = (0i64, 0i64); - unsafe { oakaudio_waveform_length(w, &mut num, &mut den) }; - assert_eq!((num, den), (1, 1)); + w.trim_in(Rational::new(-1, 2)); + assert_eq!(w.length(), Rational::new(1, 1)); // Resize extends to 2 s. - assert_eq!(unsafe { oakaudio_waveform_resize(w, 2, 1) }, 0); - unsafe { oakaudio_waveform_length(w, &mut num, &mut den) }; - assert_eq!((num, den), (2, 1)); + w.resize(Rational::new(2, 1)); + assert_eq!(w.length(), Rational::new(2, 1)); // trim_range keeps 0.5 s from the (prepended) start. - assert_eq!(unsafe { oakaudio_waveform_trim_range(w, 0, 1, 1, 2) }, 0); - unsafe { oakaudio_waveform_length(w, &mut num, &mut den) }; - assert_eq!((num, den), (1, 2)); - - // A negative resize target or a zero denominator is invalid. - assert_eq!( - unsafe { oakaudio_waveform_resize(w, -1, 2) }, - OAKAUDIO_E_INVALID - ); - assert_eq!( - unsafe { oakaudio_waveform_resize(w, 1, 0) }, - OAKAUDIO_E_INVALID - ); - unsafe { oakaudio_waveform_free(&mut w) }; + w.trim_range(Rational::new(0, 1), Rational::new(1, 2)); + assert_eq!(w.length(), Rational::new(1, 2)); } -/// sum_samples_s reduces planar samples into one min/max pair per channel; -/// re_sum_s merges channel-interleaved entries into one pair per channel. -/// Both match golden vectors from the C++ implementation. +/// sum_samples reduces planar samples into one min/max pair per channel; +/// re_sum_samples merges channel-interleaved entries into one pair per +/// channel. Both match golden vectors from the C++ implementation. #[test] fn sum_and_resum_golden() { let ch0 = [1.0f32, -2.0, 3.0]; let ch1 = [4.0f32, -5.0, 6.0]; - let planes = [ch0.as_ptr(), ch1.as_ptr()]; - let mut out = [MinMax { min: 0.0, max: 0.0 }; 2]; - let r = unsafe { oakaudio_waveform_sum_samples_s(planes.as_ptr(), 2, 0, 3, out.as_mut_ptr()) }; - assert_eq!(r, 0); - assert_eq!(pair(out[0].min, out[0].max), pair(-2.0, 3.0)); - assert_eq!(pair(out[1].min, out[1].max), pair(-5.0, 6.0)); + let planes = [ch0.as_slice(), ch1.as_slice()]; + let out = AudioVisualWaveform::sum_samples(&planes, 0, 3); + assert_eq!((out[0].min, out[0].max), (-2.0, 3.0)); + assert_eq!((out[1].min, out[1].max), (-5.0, 6.0)); - // re_sum_s over 4 interleaved entries, 2 channels -> one pair per + // re_sum_samples over 4 interleaved entries, 2 channels -> one pair per // channel merging both points. let input = [ - MinMax { min: 1.0, max: 2.0 }, - MinMax { min: 3.0, max: 4.0 }, - MinMax { min: 5.0, max: 6.0 }, - MinMax { min: 7.0, max: 8.0 }, + SamplePerChannel { min: 1.0, max: 2.0 }, + SamplePerChannel { min: 3.0, max: 4.0 }, + SamplePerChannel { min: 5.0, max: 6.0 }, + SamplePerChannel { min: 7.0, max: 8.0 }, ]; - let mut out = [MinMax { min: 0.0, max: 0.0 }; 2]; - let r = unsafe { oakaudio_waveform_re_sum_s(input.as_ptr(), 4, 2, out.as_mut_ptr()) }; - assert_eq!(r, 0); - assert_eq!(pair(out[0].min, out[0].max), pair(1.0, 6.0)); - assert_eq!(pair(out[1].min, out[1].max), pair(3.0, 8.0)); - - // Invalid arguments. - assert_eq!( - unsafe { oakaudio_waveform_sum_samples_s(planes.as_ptr(), 2, 0, 0, out.as_mut_ptr()) }, - OAKAUDIO_E_INVALID - ); - assert_eq!( - unsafe { oakaudio_waveform_re_sum_s(input.as_ptr(), 0, 2, out.as_mut_ptr()) }, - OAKAUDIO_E_INVALID - ); + let out = AudioVisualWaveform::re_sum_samples(&input, 4, 2); + assert_eq!((out[0].min, out[0].max), (1.0, 6.0)); + assert_eq!((out[1].min, out[1].max), (3.0, 8.0)); } -/// extract probes through the oakcodec decoder C ABI; a missing file -/// returns OAKAUDIO_E_NOT_FOUND. The valid-file path decodes the fixture -/// WAV with oakcodec's in-process FFmpeg decoder and reduces it to min/max -/// points. +/// extract probes through oakcodec's decoder registry; a missing file +/// returns NotFound. The valid-file path decodes the fixture WAV with +/// oakcodec's in-process FFmpeg decoder and reduces it to min/max points. #[test] fn extract_file_and_notfound() { let path = std::env::temp_dir().join(format!("oakaudio_extract_{}.wav", std::process::id())); @@ -281,55 +163,19 @@ fn extract_file_and_notfound() { } write_wav(&path, 2, 48000, &samples).unwrap(); - // Missing file -> NOT_FOUND. - let missing = CString::new( - std::env::temp_dir() - .join(format!("oakaudio_missing_{}.wav", std::process::id())) - .to_str() - .unwrap(), - ) - .unwrap(); - let _ = std::fs::remove_file(std::path::Path::new(missing.to_str().unwrap())); - let r = unsafe { - oakaudio_waveform_extract( - missing.as_ptr(), - 0, - 4, - std::ptr::null_mut(), - 0, - std::ptr::null_mut(), - ) - }; - assert_eq!(r, -60004); + // Missing file -> NotFound. + let missing = std::env::temp_dir().join(format!("oakaudio_missing_{}.wav", std::process::id())); + let _ = std::fs::remove_file(&missing); + let missing_c = CString::new(missing.to_str().unwrap()).unwrap(); + assert!(extract(&missing_c, 0, 4).is_err()); // Real decode: 8 frames at 4 samples/point -> 2 points, 2 channels. let c_path = CString::new(path.to_str().unwrap()).unwrap(); - let mut channel_count = 0i32; - let n = unsafe { - oakaudio_waveform_extract( - c_path.as_ptr(), - 0, - 4, - std::ptr::null_mut(), - 0, - &mut channel_count, - ) - }; - assert_eq!(channel_count, 2); - assert_eq!(n, 2, "8 frames at 4 samples/point must yield 2 points"); + let outcome = extract(&c_path, 0, 4).unwrap(); + assert_eq!(outcome.channels, 2); + assert_eq!(outcome.points.len(), 4, "2 points x 2 channels"); + let out = &outcome.points; - let mut out = vec![MinMax { min: 0.0, max: 0.0 }; 4]; - let n = unsafe { - oakaudio_waveform_extract( - c_path.as_ptr(), - 0, - 4, - out.as_mut_ptr(), - 2, - &mut channel_count, - ) - }; - assert_eq!(n, 2); // s16 -> f32 is /32768; the ramp is exact in both formats. let eps = 1e-6; // Point 0 covers frames 0..4: left 0..3000, right 0..-3000. @@ -346,60 +192,18 @@ fn extract_file_and_notfound() { std::fs::remove_file(&path).ok(); } -/// FFI validation and empty-handle error paths. +/// Overwriting with channel count 0 is a logged no-op (the C ABI's state +/// error was FFI-level validation; the Rust API keeps the C++ semantics of +/// returning without touching the data). #[test] -fn ffi_error_paths() { - let empty = oakaudio::handle::CHandle::null(); - assert_eq!( - unsafe { oakaudio_waveform_get_channel_count(empty) }, - OAKAUDIO_E_INVALID - ); - assert_eq!( - unsafe { oakaudio_waveform_set_channel_count(empty, 2) }, - OAKAUDIO_E_INVALID - ); - let (mut num, mut den) = (0i64, 0i64); - assert_eq!( - unsafe { oakaudio_waveform_length(empty, &mut num, &mut den) }, - OAKAUDIO_E_INVALID - ); - - // Negative channel count is invalid. - let mut w = unsafe { oakaudio_waveform_init() }; - assert_eq!( - unsafe { oakaudio_waveform_set_channel_count(w, -1) }, - OAKAUDIO_E_INVALID - ); - - // overwrite_samples before set_channel_count is a state error. +fn no_channel_count_is_noop() { + let mut w = AudioVisualWaveform::new(); let data = [0.5f32; 8]; - let planes = [data.as_ptr()]; - assert_eq!( - unsafe { oakaudio_waveform_overwrite_samples(w, planes.as_ptr(), 8, 48000, 0, 1) }, - OAKAUDIO_E_STATE - ); - // A zero denominator is rejected. - assert_eq!( - unsafe { oakaudio_waveform_overwrite_samples(w, planes.as_ptr(), 8, 48000, 0, 0) }, - OAKAUDIO_E_INVALID - ); - // A non-positive frame count is invalid. - assert_eq!( - unsafe { oakaudio_waveform_overwrite_samples(w, planes.as_ptr(), 0, 48000, 0, 1) }, - OAKAUDIO_E_INVALID - ); - // NULL planes are invalid. - assert_eq!( - unsafe { oakaudio_waveform_overwrite_samples(w, std::ptr::null(), 8, 48000, 0, 1) }, - OAKAUDIO_E_INVALID - ); - unsafe { oakaudio_waveform_free(&mut w) }; -} + let planes = [data.as_slice()]; + w.overwrite_samples(&planes, 48000, Rational::new(0, 1)); + assert!(w.length().is_null()); + assert_eq!(w.channel_count(), 0); -/// free(NULL)/free(empty) are no-ops. -#[test] -fn free_null_noop() { - let mut w = oakaudio::handle::CHandle::null(); - unsafe { oakaudio_waveform_free(&mut w) }; - unsafe { oakaudio_waveform_free(std::ptr::null_mut()) }; + w.set_channel_count(2); + assert_eq!(w.channel_count(), 2); } diff --git a/crates/oakcodec/Cargo.toml b/crates/oakcodec/Cargo.toml index 9f1488d00..8fb5eb5ff 100644 --- a/crates/oakcodec/Cargo.toml +++ b/crates/oakcodec/Cargo.toml @@ -8,13 +8,8 @@ license = "GPL-3.0-or-later" [lib] crate-type = ["staticlib", "rlib"] -[features] -# Compile the oakcore_*/oakrender_* host-mocks in src/bridge/test_stubs.rs so -# consumer test binaries that link this crate (e.g. oaknode's) can resolve -# those cross-crate C-ABI symbols without the host dylibs. -test-stubs = [] - [dependencies] +oakcommon = { path = "../oakcommon" } oakffmpeg-link = { path = "../oakffmpeg-link" } oakcore-rs = { path = "../oakcore" } # Real media decode/encode. The C++ ffmpeg_bridge library existed only to diff --git a/crates/oakcodec/src/audioparams.rs b/crates/oakcodec/src/audioparams.rs new file mode 100644 index 000000000..f368f2bdf --- /dev/null +++ b/crates/oakcodec/src/audioparams.rs @@ -0,0 +1,60 @@ +// 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 . + +//! `olive::AudioParams` — the audio stream parameter value type. +//! +//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): the +//! codec crate used to create audio parameter sets through the oakcore C +//! ABI (`oakcore_audioparams_*`, host-provided symbols) and store them +//! behind refcounted handles. The parameters are plain values now — the +//! FFmpeg probe fills them in directly and the footage description stores +//! them by value. + +/// `olive::AudioParams` — the audio stream description recorded at probe +/// time. +/// +/// Mirrors `core/include/olive/core/oakcore/audioparams.h`: sample rate, +/// ffmpeg channel-layout mask, sample format, stream index, duration and +/// time base. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct AudioParams { + /// Sample rate in Hz. + pub sample_rate: i32, + /// ffmpeg-style channel layout mask (e.g. 0x3 = stereo). + pub channel_layout: u64, + /// `olive::core::SampleFormat::Format` value. + pub format: i32, + /// Stream index within the source container. + pub stream_index: i32, + /// Stream length in time-base units. + pub duration: i64, + /// Time base (num/den seconds per tick). + pub time_base: (i32, i32), +} + +impl AudioParams { + /// Channel count derived from the layout mask, mirroring the C++ + /// `AudioParams::channel_count()`. + pub fn channel_count(&self) -> i32 { + self.channel_layout.count_ones() as i32 + } + + /// Whether the parameter set describes a usable stream, mirroring the + /// C++ `AudioParams::is_valid()` (positive rate and a non-zero layout). + pub fn is_valid(&self) -> bool { + self.sample_rate > 0 && self.channel_layout != 0 + } +} diff --git a/crates/oakcodec/src/bridge/common.rs b/crates/oakcodec/src/bridge/common.rs deleted file mode 100644 index c3a547764..000000000 --- a/crates/oakcodec/src/bridge/common.rs +++ /dev/null @@ -1,686 +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 . - -//! oakcommon / oakcore C ABI imports (videoparams, audioparams, rational, -//! subtitleparams, config, filefunctions, ffmpegutils, oiioutils, -//! colortransform). -//! -//! The by-value handle structs (`OakVideoParams`, `OakAudioParams`, -//! `OakSubtitleParams`, `OakNodeBlock`) mirror the `{ctx, addref, -//! release, abi_version}` layout from `include/common/handle.h`, so the -//! codec module can store them by value and pass them straight across -//! the FFI boundary. -//! -//! The oakcore audio parameters use a pointer-based C ABI instead of the -//! by-value handle convention: `oakcore_audioparams_*` take and return -//! `OakAudioParams *` / `OakRational *` pointers (`core/include/olive/ -//! core/oakcore/audioparams.h`, `rational.h`). Those are bridged as raw -//! pointers to the crate's handle structs; `oakcore_audioparams_time_base` -//! returns a newly allocated rational the caller must release with -//! `oakcore_rational_free`. -//! -//! # ABI discipline (single-lib era) -//! -//! The frozen `oakcommon` C ABI (see `crates/oakengine/include/common/`) -//! uses **out-pointer getters** and **instance handles** for the module -//! families. Earlier codec-era bridges declared value-style signatures -//! (C++-era headers); those declarations were wrong against the frozen -//! contract and are fixed here. To keep the crate's call sites -//! unchanged, each true ABI symbol is declared under `#[link_name]` with -//! its real signature and wrapped in an adapter of the old value-style -//! shape (an M12 P0 decode-path fix: the mismatch made every decoded -//! frame invalid). - -use std::ffi::{c_char, c_int, c_void}; - -use crate::handle::CHandle; - -/// `OakVideoParams` — refcounted video-parameter handle. -pub type OakVideoParams = CHandle; - -/// `OakAudioParams` — refcounted audio-parameter handle. -pub type OakAudioParams = CHandle; - -/// `OakSubtitleParams` — refcounted subtitle-parameter handle. -pub type OakSubtitleParams = CHandle; - -/// `OakNodeBlock` — opaque node-block handle (owned elsewhere; codec -/// only stores and forwards it). -pub type OakNodeBlock = CHandle; - -/// Module instance handles (filefunctions / oiioutils take `self_`). -pub type OakFileFunctions = CHandle; -pub type OakOIIOUtils = CHandle; - -// The handle structs are opaque refcounted handles pointing into a C -// library; the boxed objects are independently synchronized there, so -// moving a handle between threads is sound. - -extern "C" { - /// `oakcommon_videoparams_init`. - pub fn oakcommon_videoparams_init() -> OakVideoParams; - /// `oakcommon_videoparams_init_basic`. - #[link_name = "oakcommon_videoparams_init_basic"] - fn videoparams_init_basic_abi( - 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, - ) -> OakVideoParams; - /// `oakcommon_videoparams_init_with_time_base`. - #[link_name = "oakcommon_videoparams_init_with_time_base"] - fn videoparams_init_with_time_base_abi( - 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, - ) -> OakVideoParams; - /// `oakcommon_videoparams_free` (NULL/empty no-op). - pub fn oakcommon_videoparams_free(params: *mut OakVideoParams); - /// `oakcommon_videoparams_get_width`. - #[link_name = "oakcommon_videoparams_get_width"] - fn videoparams_get_width_abi(params: OakVideoParams, width: *mut c_int) -> c_int; - /// `oakcommon_videoparams_get_height`. - #[link_name = "oakcommon_videoparams_get_height"] - fn videoparams_get_height_abi(params: OakVideoParams, height: *mut c_int) -> c_int; - /// `oakcommon_videoparams_get_format`. - #[link_name = "oakcommon_videoparams_get_format"] - fn videoparams_get_format_abi(params: OakVideoParams, format: *mut c_int) -> c_int; - /// `oakcommon_videoparams_get_time_base` (num/den out). - #[link_name = "oakcommon_videoparams_get_time_base"] - fn videoparams_get_time_base_abi( - params: OakVideoParams, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int; - /// `oakcommon_videoparams_set_width`. - pub fn oakcommon_videoparams_set_width(params: OakVideoParams, width: c_int); - /// `oakcommon_videoparams_set_height`. - pub fn oakcommon_videoparams_set_height(params: OakVideoParams, height: c_int); - /// `oakcommon_videoparams_set_format`. - pub fn oakcommon_videoparams_set_format(params: OakVideoParams, format: c_int); - /// `oakcommon_videoparams_get_is_valid`. - #[link_name = "oakcommon_videoparams_get_is_valid"] - fn videoparams_get_is_valid_abi(params: OakVideoParams, is_valid: *mut c_int) -> c_int; - /// `oakcommon_videoparams_equals`. - #[link_name = "oakcommon_videoparams_equals"] - fn videoparams_equals_abi( - params: OakVideoParams, - other: OakVideoParams, - equal: *mut c_int, - ) -> c_int; - /// `oakcommon_videoparams_set_time_base`. - pub fn oakcommon_videoparams_set_time_base(params: OakVideoParams, num: i64, den: i64); - /// `oakcommon_videoparams_set_frame_rate`. - pub fn oakcommon_videoparams_set_frame_rate(params: OakVideoParams, num: i64, den: i64); - /// `oakcommon_videoparams_set_pixel_aspect_ratio`. - pub fn oakcommon_videoparams_set_pixel_aspect_ratio(params: OakVideoParams, num: i64, den: i64); - /// `oakcommon_videoparams_set_interlacing`. - pub fn oakcommon_videoparams_set_interlacing(params: OakVideoParams, interlacing: c_int); - /// `oakcommon_videoparams_set_duration`. - pub fn oakcommon_videoparams_set_duration(params: OakVideoParams, duration: i64); - /// `oakcommon_videoparams_set_start_time`. - pub fn oakcommon_videoparams_set_start_time(params: OakVideoParams, start_time: i64); - /// `oakcommon_videoparams_set_color_range`. - pub fn oakcommon_videoparams_set_color_range(params: OakVideoParams, color_range: c_int); - /// `oakcommon_videoparams_set_video_type`. - pub fn oakcommon_videoparams_set_video_type(params: OakVideoParams, video_type: c_int); - /// `oakcommon_videoparams_set_channel_count`. - pub fn oakcommon_videoparams_set_channel_count(params: OakVideoParams, channels: c_int); - /// `oakcommon_videoparams_set_color_primaries`. - pub fn oakcommon_videoparams_set_color_primaries(params: OakVideoParams, primaries: c_int); - /// `oakcommon_videoparams_set_color_transfer`. - pub fn oakcommon_videoparams_set_color_transfer(params: OakVideoParams, transfer: c_int); - /// `oakcommon_videoparams_set_premultiplied_alpha`. - pub fn oakcommon_videoparams_set_premultiplied_alpha( - params: OakVideoParams, - premultiplied: c_int, - ); - /// `oakcommon_videoparams_set_enabled`. - pub fn oakcommon_videoparams_set_enabled(params: OakVideoParams, enabled: c_int); - /// `oakcommon_videoparams_static_get_bytes_per_pixel`. - #[link_name = "oakcommon_videoparams_static_get_bytes_per_pixel"] - fn videoparams_static_get_bytes_per_pixel_abi(format: c_int, channels: c_int) -> c_int; - /// `oakcommon_videoparams_frame_rate_as_time_base`. - pub fn oakcommon_videoparams_frame_rate_as_time_base( - frame_rate_num: i64, - frame_rate_den: i64, - out_num: *mut i64, - out_den: *mut i64, - ); - /// `oakcommon_videoparams_get_stream_index`. - #[link_name = "oakcommon_videoparams_get_stream_index"] - fn videoparams_get_stream_index_abi(params: OakVideoParams, index: *mut c_int) -> c_int; - /// `oakcommon_videoparams_set_stream_index`. - pub fn oakcommon_videoparams_set_stream_index(params: OakVideoParams, index: c_int); - /// `oakcommon_videoparams_get_divider`. - #[link_name = "oakcommon_videoparams_get_divider"] - fn videoparams_get_divider_abi(params: OakVideoParams, divider: *mut c_int) -> c_int; - /// `oakcommon_videoparams_set_divider`. - pub fn oakcommon_videoparams_set_divider(params: OakVideoParams, divider: c_int); - /// `oakcommon_videoparams_get_frame_rate` (frame-rate num/den out). - #[link_name = "oakcommon_videoparams_get_frame_rate"] - fn videoparams_get_frame_rate_abi( - params: OakVideoParams, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int; - /// `oakcommon_videoparams_get_duration` (time-base units). - #[link_name = "oakcommon_videoparams_get_duration"] - fn videoparams_get_duration_abi(params: OakVideoParams, duration: *mut i64) -> c_int; - /// `oakcommon_videoparams_get_channel_count`. - #[link_name = "oakcommon_videoparams_get_channel_count"] - fn videoparams_get_channel_count_abi(params: OakVideoParams, count: *mut c_int) -> c_int; - /// `oakcommon_videoparams_get_color_primaries`. - #[link_name = "oakcommon_videoparams_get_color_primaries"] - fn videoparams_get_color_primaries_abi(params: OakVideoParams, primaries: *mut c_int) -> c_int; - /// `oakcommon_videoparams_get_color_transfer`. - #[link_name = "oakcommon_videoparams_get_color_transfer"] - fn videoparams_get_color_transfer_abi(params: OakVideoParams, transfer: *mut c_int) -> c_int; - /// `oakcommon_videoparams_get_interlacing` (`Interlacing` value). - #[link_name = "oakcommon_videoparams_get_interlacing"] - fn videoparams_get_interlacing_abi(params: OakVideoParams, interlacing: *mut c_int) -> c_int; - /// `oakcore_audioparams_create` (pointer-based; timebase 1/sample_rate). - pub fn oakcore_audioparams_create( - sample_rate: c_int, - channel_layout: u64, - format: c_int, - ) -> *mut OakAudioParams; - /// `oakcore_audioparams_free` (NULL no-op). - pub fn oakcore_audioparams_free(params: *mut OakAudioParams); - /// `oakcore_audioparams_sample_rate`. - pub fn oakcore_audioparams_sample_rate(params: *const OakAudioParams) -> c_int; - /// `oakcore_audioparams_set_sample_rate`. - pub fn oakcore_audioparams_set_sample_rate(params: *mut OakAudioParams, sample_rate: c_int); - /// `oakcore_audioparams_channel_layout`. - pub fn oakcore_audioparams_channel_layout(params: *const OakAudioParams) -> u64; - /// `oakcore_audioparams_set_channel_layout`. - pub fn oakcore_audioparams_set_channel_layout(params: *mut OakAudioParams, layout: u64); - /// `oakcore_audioparams_set_time_base`. - pub fn oakcore_audioparams_set_time_base(params: *mut OakAudioParams, num: c_int, den: c_int); - /// `oakcore_audioparams_set_format`. - pub fn oakcore_audioparams_set_format(params: *mut OakAudioParams, format: c_int); - /// `oakcore_audioparams_set_stream_index`. - pub fn oakcore_audioparams_set_stream_index(params: *mut OakAudioParams, index: c_int); - /// `oakcore_audioparams_set_duration`. - pub fn oakcore_audioparams_set_duration(params: *mut OakAudioParams, duration: i64); - /// `oakcore_audioparams_channel_count`. - pub fn oakcore_audioparams_channel_count(params: *const OakAudioParams) -> c_int; - /// `oakcore_audioparams_format`. - pub fn oakcore_audioparams_format(params: *const OakAudioParams) -> c_int; - /// `oakcore_audioparams_stream_index`. - pub fn oakcore_audioparams_stream_index(params: *const OakAudioParams) -> c_int; - /// `oakcore_audioparams_duration`. - pub fn oakcore_audioparams_duration(params: *const OakAudioParams) -> i64; - /// `oakcore_audioparams_is_valid`. - pub fn oakcore_audioparams_is_valid(params: *const OakAudioParams) -> c_int; - /// `oakcore_audioparams_time_base` (newly allocated rational; caller - /// releases with `oakcore_rational_free`). - pub fn oakcore_audioparams_time_base(params: *const OakAudioParams) -> *mut c_void; - /// `oakcore_rational_numerator`. - pub fn oakcore_rational_numerator(rational: *const c_void) -> c_int; - /// `oakcore_rational_denominator`. - pub fn oakcore_rational_denominator(rational: *const c_void) -> c_int; - /// `oakcore_rational_free` (NULL no-op). - pub fn oakcore_rational_free(rational: *mut c_void); - /// `oakcommon_subtitleparams_get_stream_index`. - #[link_name = "oakcommon_subtitleparams_get_stream_index"] - fn subtitleparams_get_stream_index_abi( - params: OakSubtitleParams, - index: *mut c_int, - ) -> c_int; - /// `oakcommon_subtitleparams_generate_ass_header`. - pub fn oakcommon_subtitleparams_generate_ass_header( - params: OakSubtitleParams, - width: c_int, - height: c_int, - ); - /// `oakcommon_subtitleparams_add_subtitle`. - pub fn oakcommon_subtitleparams_add_subtitle(params: OakSubtitleParams, text: *const c_char); - /// `oakcommon_config_get_int`. - pub fn oakcommon_config_get_int( - group: *const c_char, - key: *const c_char, - fallback: c_int, - ) -> c_int; - /// `oakcommon_config_get_bool`. - pub fn oakcommon_config_get_bool( - group: *const c_char, - key: *const c_char, - fallback: c_int, - ) -> c_int; - /// `oakcommon_config_get` (two-stage string access). - pub fn oakcommon_config_get( - group: *const c_char, - key: *const c_char, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int; - /// `oakcommon_filefunctions_init`. - pub fn oakcommon_filefunctions_init() -> OakFileFunctions; - /// `oakcommon_filefunctions_free`. - pub fn oakcommon_filefunctions_free(self_: *mut OakFileFunctions); - /// `oakcommon_filefunctions_get_configuration_location` (two-stage). - #[link_name = "oakcommon_filefunctions_get_configuration_location"] - fn filefunctions_get_configuration_location_abi( - self_: OakFileFunctions, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int; - /// `oakcommon_filefunctions_get_unique_file_identifier`. - #[link_name = "oakcommon_filefunctions_get_unique_file_identifier"] - fn filefunctions_get_unique_file_identifier_abi( - self_: OakFileFunctions, - filename: *const c_char, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int; - /// `oakcommon_filefunctions_get_application_path` (two-stage). - #[link_name = "oakcommon_filefunctions_get_application_path"] - fn filefunctions_get_application_path_abi( - self_: OakFileFunctions, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int; - /// `oakcommon_colortransform_init_output`. - pub fn oakcommon_colortransform_init_output(output: *const c_char) -> OakVideoParams; - /// `oakcommon_colortransform_get_output` (two-stage string). - pub fn oakcommon_colortransform_get_output( - transform: OakVideoParams, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int; - /// `oakcommon_colortransform_free`. - pub fn oakcommon_colortransform_free(params: *mut OakVideoParams); - /// `oakcommon_ffmpegutils_get_native_sample_format`. - #[link_name = "oakcommon_ffmpegutils_get_native_sample_format"] - fn ffmpegutils_get_native_sample_format_abi(smp_fmt: c_int, out: *mut c_int) -> c_int; - /// `oakcommon_ffmpegutils_get_compatible_pixel_format`. - #[link_name = "oakcommon_ffmpegutils_get_compatible_pixel_format"] - fn ffmpegutils_get_compatible_pixel_format_abi(pix_fmt: c_int, out: *mut c_int) -> c_int; - /// `oakcommon_ffmpegutils_get_ffmpeg_pixel_format`. - #[link_name = "oakcommon_ffmpegutils_get_ffmpeg_pixel_format"] - fn ffmpegutils_get_ffmpeg_pixel_format_abi( - pix_fmt: c_int, - channel_count: c_int, - out: *mut c_int, - ) -> c_int; - /// `oakcommon_ffmpegutils_get_ffmpeg_sample_format`. - #[link_name = "oakcommon_ffmpegutils_get_ffmpeg_sample_format"] - fn ffmpegutils_get_ffmpeg_sample_format_abi(smp_fmt: c_int, out: *mut c_int) -> c_int; - /// `oakcommon_ffmpegutils_get_compatible_bridge_pixel_format`. - #[link_name = "oakcommon_ffmpegutils_get_compatible_bridge_pixel_format"] - fn ffmpegutils_get_compatible_bridge_pixel_format_abi( - pix_fmt: c_int, - maximum_pix_fmt: c_int, - out: *mut c_int, - ) -> c_int; - /// `oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space`. - #[link_name = "oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space"] - fn ffmpegutils_convert_jpeg_space_to_regular_space_abi( - pix_fmt: c_int, - out: *mut c_int, - ) -> c_int; - /// `oakcommon_oiioutils_init`. - pub fn oakcommon_oiioutils_init() -> OakOIIOUtils; - /// `oakcommon_oiioutils_free`. - pub fn oakcommon_oiioutils_free(self_: *mut OakOIIOUtils); - /// `oakcommon_oiioutils_get_oiio_base_type_from_format`. - #[link_name = "oakcommon_oiioutils_get_oiio_base_type_from_format"] - fn oiioutils_get_oiio_base_type_from_format_abi( - self_: OakOIIOUtils, - pixel_format: c_int, - out_base_type: *mut c_int, - ) -> c_int; - /// `oakcommon_oiioutils_get_format_from_oiio_basetype`. - #[link_name = "oakcommon_oiioutils_get_format_from_oiio_basetype"] - fn oiioutils_get_format_from_oiio_basetype_abi( - self_: OakOIIOUtils, - base_type: c_int, - out_pixel_format: *mut c_int, - ) -> c_int; - /// `oakcommon_oiioutils_get_pixel_aspect_ratio` (num/den out). - #[link_name = "oakcommon_oiioutils_get_pixel_aspect_ratio"] - fn oiioutils_get_pixel_aspect_ratio_abi( - self_: OakOIIOUtils, - pixel_aspect_ratio: f64, - out_numerator: *mut c_int, - out_denominator: *mut c_int, - ) -> c_int; -} - -// --------------------------------------------------------------------------- -// Value-style adapters over the frozen out-pointer ABI (old call shapes) -// --------------------------------------------------------------------------- - -/// `oakcommon_videoparams_init_basic` — value-style shape kept for call -/// sites; defaults: U8 format, 4 channels, 1:1 aspect, progressive, -/// divider 1. -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, -) -> OakVideoParams { - unsafe { videoparams_init_basic_abi(width, height, pixel_format, nb_channels, pixel_aspect_num, pixel_aspect_den, interlacing, divider) } -} - -/// `oakcommon_videoparams_init_with_time_base` — value-style shape kept -/// for call sites. -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, -) -> OakVideoParams { - unsafe { - videoparams_init_with_time_base_abi( - width, - height, - time_base_num, - time_base_den, - pixel_format, - nb_channels, - pixel_aspect_num, - pixel_aspect_den, - interlacing, - divider, - ) - } -} - -/// Value-style `oakcommon_videoparams_get_width`. -pub fn oakcommon_videoparams_get_width(params: OakVideoParams) -> c_int { - let mut w: c_int = 0; - unsafe { videoparams_get_width_abi(params, &mut w) }; - w -} - -/// Value-style `oakcommon_videoparams_get_height`. -pub fn oakcommon_videoparams_get_height(params: OakVideoParams) -> c_int { - let mut h: c_int = 0; - unsafe { videoparams_get_height_abi(params, &mut h) }; - h -} - -/// Value-style `oakcommon_videoparams_get_format`. -pub fn oakcommon_videoparams_get_format(params: OakVideoParams) -> c_int { - let mut f: c_int = -1; - unsafe { videoparams_get_format_abi(params, &mut f) }; - f -} - -/// Value-style `oakcommon_videoparams_get_is_valid`. -pub fn oakcommon_videoparams_get_is_valid(params: OakVideoParams) -> c_int { - let mut v: c_int = 0; - unsafe { videoparams_get_is_valid_abi(params, &mut v) }; - v -} - -/// Value-style `oakcommon_videoparams_equals` (1 when equal). -pub fn oakcommon_videoparams_equals(a: OakVideoParams, b: OakVideoParams) -> c_int { - let mut eq: c_int = 0; - unsafe { videoparams_equals_abi(a, b, &mut eq) }; - eq -} - -/// Value-style `oakcommon_videoparams_get_stream_index`. -pub fn oakcommon_videoparams_get_stream_index(params: OakVideoParams) -> c_int { - let mut i: c_int = -1; - unsafe { videoparams_get_stream_index_abi(params, &mut i) }; - i -} - -/// Value-style `oakcommon_videoparams_get_divider`. -pub fn oakcommon_videoparams_get_divider(params: OakVideoParams) -> c_int { - let mut d: c_int = 0; - unsafe { videoparams_get_divider_abi(params, &mut d) }; - d -} - -/// Value-style `oakcommon_videoparams_get_duration`. -pub fn oakcommon_videoparams_get_duration(params: OakVideoParams) -> i64 { - let mut d: i64 = 0; - unsafe { videoparams_get_duration_abi(params, &mut d) }; - d -} - -/// Value-style `oakcommon_videoparams_get_channel_count`. -pub fn oakcommon_videoparams_get_channel_count(params: OakVideoParams) -> c_int { - let mut c: c_int = 0; - unsafe { videoparams_get_channel_count_abi(params, &mut c) }; - c -} - -/// Value-style `oakcommon_videoparams_get_color_primaries`. -pub fn oakcommon_videoparams_get_color_primaries(params: OakVideoParams) -> c_int { - let mut p: c_int = 0; - unsafe { videoparams_get_color_primaries_abi(params, &mut p) }; - p -} - -/// Value-style `oakcommon_videoparams_get_color_transfer`. -pub fn oakcommon_videoparams_get_color_transfer(params: OakVideoParams) -> c_int { - let mut t: c_int = 0; - unsafe { videoparams_get_color_transfer_abi(params, &mut t) }; - t -} - -/// Value-style `oakcommon_videoparams_get_interlacing`. -pub fn oakcommon_videoparams_get_interlacing(params: OakVideoParams) -> c_int { - let mut i: c_int = 0; - unsafe { videoparams_get_interlacing_abi(params, &mut i) }; - i -} - -/// Value-style `oakcommon_videoparams_get_time_base` (num/den out as -/// `i64`, widened from the frozen `i32` outs). -pub fn oakcommon_videoparams_get_time_base( - params: OakVideoParams, - out_num: *mut i64, - out_den: *mut i64, -) -> c_int { - let mut num: c_int = 0; - let mut den: c_int = 0; - let rc = unsafe { videoparams_get_time_base_abi(params, &mut num, &mut den) }; - if !out_num.is_null() { - unsafe { *out_num = num as i64 }; - } - if !out_den.is_null() { - unsafe { *out_den = den as i64 }; - } - rc -} - -/// Value-style `oakcommon_videoparams_get_frame_rate` (num/den out). -pub fn oakcommon_videoparams_get_frame_rate( - params: OakVideoParams, - out_num: *mut c_int, - out_den: *mut c_int, -) -> c_int { - unsafe { videoparams_get_frame_rate_abi(params, out_num, out_den) } -} - -/// Value-style `oakcommon_videoparams_static_get_bytes_per_pixel` -/// (4 channels assumed; the frozen ABI takes channels explicitly). -pub fn oakcommon_videoparams_static_get_bytes_per_pixel(format: c_int) -> c_int { - unsafe { videoparams_static_get_bytes_per_pixel_abi(format, 4) } -} - -/// Value-style `oakcommon_subtitleparams_get_stream_index`. -pub fn oakcommon_subtitleparams_get_stream_index(params: OakSubtitleParams) -> c_int { - let mut i: c_int = -1; - unsafe { subtitleparams_get_stream_index_abi(params, &mut i) }; - i -} - -/// Value-style `oakcommon_ffmpegutils_get_native_sample_format`. -pub fn oakcommon_ffmpegutils_get_native_sample_format(sample_format: c_int) -> c_int { - let mut out: c_int = -1; - unsafe { ffmpegutils_get_native_sample_format_abi(sample_format, &mut out) }; - out -} - -/// Value-style `oakcommon_ffmpegutils_get_compatible_pixel_format`. -pub fn oakcommon_ffmpegutils_get_compatible_pixel_format(format: c_int) -> c_int { - let mut out: c_int = -1; - unsafe { ffmpegutils_get_compatible_pixel_format_abi(format, &mut out) }; - out -} - -/// Value-style `oakcommon_ffmpegutils_get_ffmpeg_pixel_format` -/// (4 channels assumed). -pub fn oakcommon_ffmpegutils_get_ffmpeg_pixel_format(format: c_int) -> c_int { - let mut out: c_int = -1; - unsafe { ffmpegutils_get_ffmpeg_pixel_format_abi(format, 4, &mut out) }; - out -} - -/// Value-style `oakcommon_ffmpegutils_get_ffmpeg_sample_format`. -pub fn oakcommon_ffmpegutils_get_ffmpeg_sample_format(format: c_int) -> c_int { - let mut out: c_int = -1; - unsafe { ffmpegutils_get_ffmpeg_sample_format_abi(format, &mut out) }; - out -} - -/// Value-style `oakcommon_ffmpegutils_get_compatible_bridge_pixel_format` -/// (no maximum constraint). -pub fn oakcommon_ffmpegutils_get_compatible_bridge_pixel_format(format: c_int) -> c_int { - let mut out: c_int = -1; - unsafe { ffmpegutils_get_compatible_bridge_pixel_format_abi(format, -1, &mut out) }; - out -} - -/// Value-style `oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space`. -pub fn oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(format: c_int) -> c_int { - let mut out: c_int = -1; - unsafe { ffmpegutils_convert_jpeg_space_to_regular_space_abi(format, &mut out) }; - out -} - -/// The process-wide filefunctions instance (lazily created; freed on -/// process exit is not required — the singleton outlives all users). -fn filefunctions_instance() -> OakFileFunctions { - static FF: std::sync::OnceLock = std::sync::OnceLock::new(); - *FF.get_or_init(|| unsafe { oakcommon_filefunctions_init() }) -} - -/// Value-style `oakcommon_filefunctions_get_application_path` -/// (two-stage string getter on the shared instance). -pub fn oakcommon_filefunctions_get_application_path(buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { filefunctions_get_application_path_abi(filefunctions_instance(), buf, buf_size) } -} - -/// Value-style `oakcommon_filefunctions_get_configuration_location`. -pub fn oakcommon_filefunctions_get_configuration_location( - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - filefunctions_get_configuration_location_abi(filefunctions_instance(), buf, buf_size) - } -} - -/// Value-style `oakcommon_filefunctions_get_unique_file_identifier` -/// (the frozen ABI writes a string; the value-style call sites expect an -/// `i64`, so the string is folded into a stable 64-bit FNV hash). -pub fn oakcommon_filefunctions_get_unique_file_identifier(path: *const c_char) -> i64 { - let mut buf = [0 as c_char; 512]; - // Two-stage contract: 0 = written, positive = required size, negative - // = error. The adapter's value-style call sites get the i64 fold. - let n = unsafe { - filefunctions_get_unique_file_identifier_abi( - filefunctions_instance(), - path, - buf.as_mut_ptr(), - buf.len() as c_int, - ) - }; - if n < 0 { - return 0; - } - let avail = if n == 0 { buf.len() } else { (n as usize).min(buf.len()) }; - let bytes: Vec = buf[..avail] - .iter() - .take_while(|&&b| b != 0) - .map(|&b| b as u8) - .collect(); - let mut hash: u64 = 0xcbf29ce484222325; - for &b in &bytes { - hash ^= b as u64; - hash = hash.wrapping_mul(0x100000001b3); - } - hash as i64 -} - -/// The process-wide oiioutils instance (lazily created). -fn oiioutils_instance() -> OakOIIOUtils { - static OU: std::sync::OnceLock = std::sync::OnceLock::new(); - *OU.get_or_init(|| unsafe { oakcommon_oiioutils_init() }) -} - -/// Value-style `oakcommon_oiioutils_get_oiio_base_type_from_format`. -pub fn oakcommon_oiioutils_get_oiio_base_type_from_format(format: c_int) -> c_int { - let mut out: c_int = -1; - unsafe { oiioutils_get_oiio_base_type_from_format_abi(oiioutils_instance(), format, &mut out) }; - out -} - -/// Value-style `oakcommon_oiioutils_get_format_from_oiio_basetype`. -pub fn oakcommon_oiioutils_get_format_from_oiio_basetype(basetype: c_int) -> c_int { - let mut out: c_int = -1; - unsafe { oiioutils_get_format_from_oiio_basetype_abi(oiioutils_instance(), basetype, &mut out) }; - out -} - -/// Value-style `oakcommon_oiioutils_get_pixel_aspect_ratio` -/// (the frozen ABI takes the aspect ratio as `f64`; the value-style call -/// sites pass width/height and get num/den back). -pub fn oakcommon_oiioutils_get_pixel_aspect_ratio( - width: c_int, - height: c_int, - out_num: *mut c_int, - out_den: *mut c_int, -) -> c_int { - let ratio = if height != 0 { - f64::from(width) / f64::from(height) - } else { - 1.0 - }; - unsafe { - oiioutils_get_pixel_aspect_ratio_abi(oiioutils_instance(), ratio, out_num, out_den) - } -} diff --git a/crates/oakcodec/src/bridge/mod.rs b/crates/oakcodec/src/bridge/mod.rs deleted file mode 100644 index c6b396fe5..000000000 --- a/crates/oakcodec/src/bridge/mod.rs +++ /dev/null @@ -1,35 +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 . - -//! C ABI imports from the other oak modules. -//! -//! The codec module links against oakcommon and oakrender at the C ABI. -//! Every signature below mirrors the corresponding public header -//! verbatim and is resolved at link time. The by-value handle structs -//! (`OakVideoParams`, `OakRenderTexture`, …) are `#[repr(C)]` mirrors of -//! the `{ctx, addref, release, abi_version}` layout so the codec crate -//! can hold and hand them across the FFI boundary without translation. - -pub mod common; -pub mod render; - -// In-memory mocks for the oakcommon/oakrender C ABI so the crate links -// and is testable under `cargo test` (where those dylibs are absent). -// The `test-stubs` feature additionally compiles the oakcore_*/oakrender_* -// host-mocks for consumer test binaries (e.g. oaknode's) that link this -// crate directly — those symbols are not provided by any Rust crate. -#[cfg(any(test, feature = "test-stubs"))] -pub mod test_stubs; diff --git a/crates/oakcodec/src/bridge/render.rs b/crates/oakcodec/src/bridge/render.rs deleted file mode 100644 index 11cc28fd5..000000000 --- a/crates/oakcodec/src/bridge/render.rs +++ /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 . - -//! oakrender C ABI imports (display textures, renderers, cancel atoms). -//! -//! The OIIO/FFmpeg decoders push frames to a `DisplayTexture` and poll a -//! `CancelAtom`; both are oakrender refcounted handles with the standard -//! `{ctx, addref, release, abi_version}` layout. `oakrender_video_params` -//! is a flattened POD the decoders construct to describe the frame. - -use std::ffi::{c_char, c_int, c_void}; - -use crate::handle::CHandle; - -/// `OakRenderTexture` — refcounted GPU texture handle. -pub type OakRenderTexture = CHandle; - -/// `OakCancelAtom` — refcounted cancellation atom handle. -pub type OakCancelAtom = CHandle; - -/// `OakRenderRenderer` — refcounted display-renderer handle. -pub type OakRenderRenderer = CHandle; - -/// `OakCodecFrame` — refcounted CPU-frame handle shared with oakrender. -pub type OakCodecFrame = CHandle; - -// Refcounted opaque handles; thread-safe in the C library. - -/// `oakrender_video_params` — flattened POD of `olive::VideoParams` -/// passed into oakrender; see `include/render/renderer.h`. -#[repr(C)] -pub struct oakrender_video_params { - /// Width in pixels. - pub width: c_int, - /// Height in pixels. - 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, - /// `olive::PixelFormat::Format`. - pub format: c_int, - /// Pixel-aspect numerator. - pub pixel_aspect_num: c_int, - /// Pixel-aspect denominator. - pub pixel_aspect_den: c_int, - /// `olive::VideoParams::Interlacing`. - pub interlacing: c_int, - /// `olive::VideoParams::ColorRange`. - pub color_range: c_int, - /// Preview-resolution divider (1 = full). - pub divider: c_int, - /// `olive::VideoParams::Type` (0 = video). - pub video_type: c_int, - /// 0/1 premultiplied alpha. - pub premultiplied_alpha: c_int, -} - -extern "C" { - /// `oakrender_cancelatom_init`. - pub fn oakrender_cancelatom_init() -> OakCancelAtom; - /// `oakrender_cancelatom_free` (NULL/empty no-op). - pub fn oakrender_cancelatom_free(atom: *mut OakCancelAtom); - /// `oakrender_cancelatom_is_cancelled`. - pub fn oakrender_cancelatom_is_cancelled(atom: OakCancelAtom) -> c_int; - /// `oakrender_cancelatom_heard_cancel`. - pub fn oakrender_cancelatom_heard_cancel(atom: OakCancelAtom) -> c_int; - /// `oakrender_cancelatom_cancel`. - pub fn oakrender_cancelatom_cancel(atom: OakCancelAtom); - /// `oakrender_cancelatom_get_native`. - pub fn oakrender_cancelatom_get_native(atom: OakCancelAtom) -> *mut c_void; - /// `oakrender_display_texture_create`. - pub fn oakrender_display_texture_create( - renderer: OakRenderRenderer, - params: *const oakrender_video_params, - data: *const c_void, - linesize: c_int, - ) -> OakRenderTexture; - /// `oakrender_display_texture_retain`. - pub fn oakrender_display_texture_retain(texture: OakRenderTexture) -> OakRenderTexture; - /// `oakrender_display_texture_free` (NULL/empty no-op). - pub fn oakrender_display_texture_free(texture: *mut OakRenderTexture); - /// `oakrender_display_texture_upload`. - pub fn oakrender_display_texture_upload(texture: OakRenderTexture) -> c_int; - /// `oakrender_display_texture_download`. - pub fn oakrender_display_texture_download( - texture: OakRenderTexture, - pixels: *mut c_void, - linesize: c_int, - ) -> c_int; - /// `oakrender_display_texture_get_params`. - pub fn oakrender_display_texture_get_params( - texture: OakRenderTexture, - out: *mut oakrender_video_params, - ) -> c_int; - /// `oakrender_display_texture_id`. - pub fn oakrender_display_texture_id(texture: OakRenderTexture) -> c_int; - /// `oakrender_display_texture_is_dummy`. - pub fn oakrender_display_texture_is_dummy(texture: OakRenderTexture) -> c_int; - /// `oakrender_display_texture_get_frame` (two-stage frame access). - pub fn oakrender_display_texture_get_frame( - texture: OakRenderTexture, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int; - /// `oakrender_codec_frame_width`. - pub fn oakrender_codec_frame_width(frame: OakCodecFrame) -> c_int; - /// `oakrender_codec_frame_height`. - pub fn oakrender_codec_frame_height(frame: OakCodecFrame) -> c_int; - /// `oakrender_codec_frame_fb_format`. - pub fn oakrender_codec_frame_fb_format(frame: OakCodecFrame) -> c_int; - /// `oakrender_codec_frame_free` (NULL/empty no-op). - pub fn oakrender_codec_frame_free(frame: *mut OakCodecFrame); - /// `oakrender_codec_frame_allocate`. - pub fn oakrender_codec_frame_allocate(frame: OakCodecFrame) -> c_int; - /// `oakrender_codec_frame_linesize_bytes`. - pub fn oakrender_codec_frame_linesize_bytes(frame: OakCodecFrame) -> c_int; - /// `oakrender_codec_frame_is_allocated`. - pub fn oakrender_codec_frame_is_allocated(frame: OakCodecFrame) -> c_int; - /// `oakrender_display_renderer_blit_color_managed`. - pub fn oakrender_display_renderer_blit_color_managed( - renderer: OakRenderRenderer, - job: *const c_void, - dst_texture: OakRenderTexture, - params: *const oakrender_video_params, - ) -> c_int; -} diff --git a/crates/oakcodec/src/bridge/test_stubs.rs b/crates/oakcodec/src/bridge/test_stubs.rs deleted file mode 100644 index 9a1d14f87..000000000 --- a/crates/oakcodec/src/bridge/test_stubs.rs +++ /dev/null @@ -1,1265 +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 . - -//! `#[cfg(test)]` in-memory mocks for the oakcommon / oakrender C ABI. -//! -//! The real symbols live in `liboakcommon` / `liboakrender` and are only -//! linked when the crate is built against those dylibs (ctest). Under -//! `cargo test` those libraries are not linked, so `extern "C"` call -//! sites in the crate would otherwise fail to resolve. These -//! `#[no_mangle] extern "C"` definitions provide the symbols and back the -//! two stateful pieces the crate actually reads (video params and cancel -//! atoms) with real in-memory state, so `Frame`, `FootageDescription` -//! and friends are meaningfully testable. Everything else returns a -//! deterministic neutral value. -//! -//! Kept strictly under `#[cfg(test)]`; never compiled into a real build. - -#![allow(dead_code, unused_variables)] - -use std::collections::HashMap; -use std::ffi::{c_char, c_int, c_void, CStr}; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{Mutex, OnceLock}; - -use crate::bridge::common::{ - OakAudioParams, OakFileFunctions, OakNodeBlock, OakOIIOUtils, OakSubtitleParams, OakVideoParams, -}; -use crate::bridge::render::{OakCancelAtom, OakCodecFrame, OakRenderRenderer, OakRenderTexture}; -use crate::handle::OAKCODEC_ABI_VERSION; - -/// Per-`OakVideoParams` backing state. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -struct MockParams { - width: i32, - height: i32, - format: i32, - time_base_num: i64, - time_base_den: i64, - stream_index: i32, - divider: i32, - frame_rate_num: i32, - frame_rate_den: i32, - duration: i64, - channel_count: i32, - color_primaries: i32, - color_trc: i32, - interlacing: i32, - pixel_aspect_num: i32, - pixel_aspect_den: i32, - start_time: i64, - color_range: i32, - video_type: i32, - premultiplied_alpha: i32, - enabled: i32, -} - -fn params_store() -> &'static Mutex> { - static S: OnceLock>> = OnceLock::new(); - S.get_or_init(|| Mutex::new(HashMap::new())) -} - -/// Build a handle whose `ctx` owns a leaked `MockParams`; the map entry -/// keeps the storage alive until `free` is called. -fn new_params_handle(p: MockParams) -> OakVideoParams { - let raw = Box::into_raw(Box::new(p.clone())); - params_store().lock().unwrap().insert(raw as usize, p); - OakVideoParams { - ctx: raw as *mut c_void, - addref: None, - release: None, - abi_version: OAKCODEC_ABI_VERSION, - } -} - -fn params_ref(ctx: *mut c_void) -> Option<&'static mut MockParams> { - let store = params_store().lock().unwrap(); - store.get(&(ctx as usize))?; - drop(store); - // SAFETY: entries are only removed by oakcommon_videoparams_free while - // the caller still holds the handle, so the box outlives this borrow. - Some(unsafe { &mut *(ctx as *mut MockParams) }) -} - -fn params_get(ctx: *mut c_void) -> MockParams { - let store = params_store().lock().unwrap(); - store.get(&(ctx as usize)).cloned().unwrap_or_default() -} - -fn params_set(ctx: *mut c_void, f: impl FnOnce(&mut MockParams)) { - let mut store = params_store().lock().unwrap(); - if let Some(p) = store.get_mut(&(ctx as usize)) { - f(p); - } -} - -// --------------------------------------------------------------------------- -// oakcommon_videoparams_* -// --------------------------------------------------------------------------- - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_init() -> OakVideoParams { - new_params_handle(MockParams { - channel_count: 4, // internal RGBA pipeline layout - ..Default::default() - }) -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" 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, -) -> OakVideoParams { - new_params_handle(MockParams { - width, - height, - format: pixel_format, - channel_count: nb_channels, - pixel_aspect_num, - pixel_aspect_den, - interlacing, - divider, - ..Default::default() - }) -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_init_with_time_base( - width: c_int, - height: c_int, - time_base_num: i64, - time_base_den: i64, - pixel_format: c_int, - nb_channels: c_int, - pixel_aspect_num: c_int, - pixel_aspect_den: c_int, - interlacing: c_int, - divider: c_int, -) -> OakVideoParams { - new_params_handle(MockParams { - width, - height, - time_base_num, - time_base_den, - format: pixel_format, - channel_count: nb_channels, - pixel_aspect_num, - pixel_aspect_den, - interlacing, - divider, - ..Default::default() - }) -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_free(params: *mut OakVideoParams) { - if params.is_null() { - return; - } - unsafe { - let ctx = (*params).ctx; - params_store().lock().unwrap().remove(&(ctx as usize)); - if !ctx.is_null() { - drop(Box::from_raw(ctx as *mut MockParams)); - } - } -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_get_width(params: OakVideoParams, width: *mut c_int) -> c_int { - let w = params_get(params.ctx).width; - if width.is_null() { - return -1; - } - unsafe { *width = w }; - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_get_height(params: OakVideoParams, height: *mut c_int) -> c_int { - let h = params_get(params.ctx).height; - if height.is_null() { - return -1; - } - unsafe { *height = h }; - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_get_format(params: OakVideoParams, format: *mut c_int) -> c_int { - let f = params_get(params.ctx).format; - if format.is_null() { - return -1; - } - unsafe { *format = f }; - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_get_time_base( - params: OakVideoParams, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - let p = params_get(params.ctx); - if !numerator.is_null() { - unsafe { *numerator = p.time_base_num as c_int }; - } - if !denominator.is_null() { - unsafe { *denominator = p.time_base_den as c_int }; - } - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_set_width(params: OakVideoParams, width: c_int) { - params_set(params.ctx, |p| p.width = width); -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_set_height(params: OakVideoParams, height: c_int) { - params_set(params.ctx, |p| p.height = height); -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_set_format(params: OakVideoParams, format: c_int) { - params_set(params.ctx, |p| p.format = format); -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_get_is_valid(params: OakVideoParams, is_valid: *mut c_int) -> c_int { - let p = params_get(params.ctx); - let v = (p.width > 0 && p.height > 0) as c_int; - if is_valid.is_null() { - return -1; - } - unsafe { *is_valid = v }; - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_equals( - a: OakVideoParams, - b: OakVideoParams, - equal: *mut c_int, -) -> c_int { - let eq = (params_get(a.ctx) == params_get(b.ctx)) as c_int; - if equal.is_null() { - return -1; - } - unsafe { *equal = eq }; - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_static_get_bytes_per_pixel( - format: c_int, - _channels: c_int, -) -> c_int { - // U10 packs to 4 bytes; U8 to 1; U16/F16 to 2; F32 to 4. - match format { - 0 => 1, // U8 - 1 => 4, // U10 - 2 => 2, // U16 - 3 => 2, // F16 - 4 => 4, // F32 - _ => 0, - } -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_frame_rate_as_time_base( - frame_rate_num: i64, - frame_rate_den: i64, - out_num: *mut i64, - out_den: *mut i64, -) { - if !out_num.is_null() { - unsafe { *out_num = frame_rate_den }; - } - if !out_den.is_null() { - unsafe { *out_den = frame_rate_num }; - } -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_get_stream_index( - params: OakVideoParams, - index: *mut c_int, -) -> c_int { - let i = params_get(params.ctx).stream_index; - if index.is_null() { - return -1; - } - unsafe { *index = i }; - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_set_stream_index(params: OakVideoParams, index: c_int) { - params_set(params.ctx, |p| p.stream_index = index); -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_get_divider( - params: OakVideoParams, - divider: *mut c_int, -) -> c_int { - let d = params_get(params.ctx).divider; - if divider.is_null() { - return -1; - } - unsafe { *divider = d }; - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_set_divider(params: OakVideoParams, divider: c_int) { - params_set(params.ctx, |p| p.divider = divider); -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_get_frame_rate( - params: OakVideoParams, - out_num: *mut c_int, - out_den: *mut c_int, -) -> c_int { - let p = params_get(params.ctx); - if !out_num.is_null() { - unsafe { *out_num = p.frame_rate_num }; - } - if !out_den.is_null() { - unsafe { *out_den = p.frame_rate_den }; - } - 1 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_get_duration( - params: OakVideoParams, - duration: *mut i64, -) -> c_int { - let d = params_get(params.ctx).duration; - if duration.is_null() { - return -1; - } - unsafe { *duration = d }; - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_get_channel_count( - params: OakVideoParams, - count: *mut c_int, -) -> c_int { - let c = params_get(params.ctx).channel_count; - if count.is_null() { - return -1; - } - unsafe { *count = c }; - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_get_color_primaries( - params: OakVideoParams, - primaries: *mut c_int, -) -> c_int { - let p = params_get(params.ctx).color_primaries; - if primaries.is_null() { - return -1; - } - unsafe { *primaries = p }; - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_get_color_transfer( - params: OakVideoParams, - transfer: *mut c_int, -) -> c_int { - let t = params_get(params.ctx).color_trc; - if transfer.is_null() { - return -1; - } - unsafe { *transfer = t }; - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_get_interlacing( - params: OakVideoParams, - interlacing: *mut c_int, -) -> c_int { - let i = params_get(params.ctx).interlacing; - if interlacing.is_null() { - return -1; - } - unsafe { *interlacing = i }; - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_set_time_base(params: OakVideoParams, num: i64, den: i64) { - params_set(params.ctx, |p| { - p.time_base_num = num; - p.time_base_den = den; - }); -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_set_frame_rate(params: OakVideoParams, num: i64, den: i64) { - params_set(params.ctx, |p| { - p.frame_rate_num = num as i32; - p.frame_rate_den = den as i32; - }); -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_set_pixel_aspect_ratio( - params: OakVideoParams, - num: i64, - den: i64, -) { - params_set(params.ctx, |p| { - p.pixel_aspect_num = num as i32; - p.pixel_aspect_den = den as i32; - }); -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_set_interlacing( - params: OakVideoParams, - interlacing: c_int, -) { - params_set(params.ctx, |p| p.interlacing = interlacing); -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_set_duration(params: OakVideoParams, duration: i64) { - params_set(params.ctx, |p| p.duration = duration); -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_set_start_time(params: OakVideoParams, start_time: i64) { - params_set(params.ctx, |p| p.start_time = start_time); -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_set_color_range( - params: OakVideoParams, - color_range: c_int, -) { - params_set(params.ctx, |p| p.color_range = color_range); -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_set_video_type(params: OakVideoParams, video_type: c_int) { - params_set(params.ctx, |p| p.video_type = video_type); -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_set_channel_count(params: OakVideoParams, channels: c_int) { - params_set(params.ctx, |p| p.channel_count = channels); -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_set_color_primaries( - params: OakVideoParams, - primaries: c_int, -) { - params_set(params.ctx, |p| p.color_primaries = primaries); -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_set_color_transfer( - params: OakVideoParams, - transfer: c_int, -) { - params_set(params.ctx, |p| p.color_trc = transfer); -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_set_premultiplied_alpha( - params: OakVideoParams, - premultiplied: c_int, -) { - params_set(params.ctx, |p| p.premultiplied_alpha = premultiplied); -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_videoparams_set_enabled(params: OakVideoParams, enabled: c_int) { - params_set(params.ctx, |p| p.enabled = enabled); -} - -// --------------------------------------------------------------------------- -// oakcore_audioparams_* / oakcore_rational_* (pointer-based in-memory state) -// --------------------------------------------------------------------------- - -/// Per-`OakAudioParams` backing state. The real oakcore C ABI is -/// pointer-based (`core/include/olive/core/oakcore/audioparams.h`), so -/// these stubs own a boxed struct and return its raw pointer. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -struct MockAudioParams { - sample_rate: i32, - channel_layout: u64, - format: i32, - stream_index: i32, - duration: i64, - time_base_num: i32, - time_base_den: i32, -} - -fn audio_params_store() -> &'static Mutex> { - static S: OnceLock>> = OnceLock::new(); - S.get_or_init(|| Mutex::new(HashMap::new())) -} - -fn audio_params_get(ctx: *const c_void) -> MockAudioParams { - let store = audio_params_store().lock().unwrap(); - store.get(&(ctx as usize)).cloned().unwrap_or_default() -} - -/// Per-`OakRational` backing state (an owned `(num, den)` pair). -fn rational_store() -> &'static Mutex> { - static S: OnceLock>> = OnceLock::new(); - S.get_or_init(|| Mutex::new(HashMap::new())) -} - -#[no_mangle] -pub extern "C" fn oakcore_audioparams_create( - sample_rate: c_int, - channel_layout: u64, - format: c_int, -) -> *mut OakAudioParams { - // The timebase starts at 1/sample_rate, mirroring the real header. - let p = MockAudioParams { - sample_rate, - channel_layout, - format, - stream_index: 0, - duration: 0, - time_base_num: 1, - time_base_den: sample_rate, - }; - let raw = Box::into_raw(Box::new(p.clone())); - audio_params_store().lock().unwrap().insert(raw as usize, p); - raw as *mut OakAudioParams -} - -#[no_mangle] -pub extern "C" fn oakcore_audioparams_free(params: *mut OakAudioParams) { - if params.is_null() { - return; - } - audio_params_store() - .lock() - .unwrap() - .remove(&(params as usize)); - // SAFETY: `params` was produced by `oakcore_audioparams_create` as a - // boxed `MockAudioParams`; we hold the only reference after removal. - unsafe { drop(Box::from_raw(params as *mut MockAudioParams)) }; -} - -#[no_mangle] -pub extern "C" fn oakcore_audioparams_sample_rate(params: *const OakAudioParams) -> c_int { - audio_params_get(params as *const c_void).sample_rate -} - -fn audio_params_set(ctx: *mut c_void, f: impl FnOnce(&mut MockAudioParams)) { - let mut store = audio_params_store().lock().unwrap(); - if let Some(p) = store.get_mut(&(ctx as usize)) { - f(p); - } -} - -#[no_mangle] -pub extern "C" fn oakcore_audioparams_set_sample_rate( - params: *mut OakAudioParams, - sample_rate: c_int, -) { - audio_params_set(params as *mut c_void, |p| p.sample_rate = sample_rate); -} - -#[no_mangle] -pub extern "C" fn oakcore_audioparams_channel_layout(params: *const OakAudioParams) -> u64 { - audio_params_get(params as *const c_void).channel_layout -} - -#[no_mangle] -pub extern "C" fn oakcore_audioparams_set_channel_layout(params: *mut OakAudioParams, layout: u64) { - audio_params_set(params as *mut c_void, |p| p.channel_layout = layout); -} - -#[no_mangle] -pub extern "C" fn oakcore_audioparams_set_time_base( - params: *mut OakAudioParams, - num: c_int, - den: c_int, -) { - audio_params_set(params as *mut c_void, |p| { - p.time_base_num = num; - p.time_base_den = den; - }); -} - -#[no_mangle] -pub extern "C" fn oakcore_audioparams_set_format(params: *mut OakAudioParams, format: c_int) { - audio_params_set(params as *mut c_void, |p| p.format = format); -} - -#[no_mangle] -pub extern "C" fn oakcore_audioparams_set_stream_index(params: *mut OakAudioParams, index: c_int) { - audio_params_set(params as *mut c_void, |p| p.stream_index = index); -} - -#[no_mangle] -pub extern "C" fn oakcore_audioparams_set_duration(params: *mut OakAudioParams, duration: i64) { - audio_params_set(params as *mut c_void, |p| p.duration = duration); -} - -#[no_mangle] -pub extern "C" fn oakcore_audioparams_channel_count(params: *const OakAudioParams) -> c_int { - audio_params_get(params as *const c_void) - .channel_layout - .count_ones() as c_int -} - -#[no_mangle] -pub extern "C" fn oakcore_audioparams_format(params: *const OakAudioParams) -> c_int { - audio_params_get(params as *const c_void).format -} - -#[no_mangle] -pub extern "C" fn oakcore_audioparams_stream_index(params: *const OakAudioParams) -> c_int { - audio_params_get(params as *const c_void).stream_index -} - -#[no_mangle] -pub extern "C" fn oakcore_audioparams_duration(params: *const OakAudioParams) -> i64 { - audio_params_get(params as *const c_void).duration -} - -#[no_mangle] -pub extern "C" fn oakcore_audioparams_is_valid(params: *const OakAudioParams) -> c_int { - let p = audio_params_get(params as *const c_void); - (p.sample_rate > 0 && p.channel_layout != 0 && p.format >= 0) as c_int -} - -#[no_mangle] -pub extern "C" fn oakcore_audioparams_time_base(params: *const OakAudioParams) -> *mut c_void { - let p = audio_params_get(params as *const c_void); - let r = (p.time_base_num, p.time_base_den); - let raw = Box::into_raw(Box::new(r)); - rational_store().lock().unwrap().insert(raw as usize, r); - raw as *mut c_void -} - -#[no_mangle] -pub extern "C" fn oakcore_rational_numerator(rational: *const c_void) -> c_int { - rational_store() - .lock() - .unwrap() - .get(&(rational as usize)) - .map(|r| r.0) - .unwrap_or(0) -} - -#[no_mangle] -pub extern "C" fn oakcore_rational_denominator(rational: *const c_void) -> c_int { - rational_store() - .lock() - .unwrap() - .get(&(rational as usize)) - .map(|r| r.1) - .unwrap_or(0) -} - -#[no_mangle] -pub extern "C" fn oakcore_rational_free(rational: *mut c_void) { - if rational.is_null() { - return; - } - rational_store() - .lock() - .unwrap() - .remove(&(rational as usize)); - // SAFETY: `rational` was produced by `oakcore_audioparams_time_base` as a - // boxed `(i32, i32)` pair; we hold the only reference after removal. - unsafe { drop(Box::from_raw(rational as *mut (i32, i32))) }; -} - -// --------------------------------------------------------------------------- -// oakcommon_subtitleparams_* -// --------------------------------------------------------------------------- - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_subtitleparams_get_stream_index( - _params: OakSubtitleParams, - index: *mut c_int, -) -> c_int { - if index.is_null() { - return -1; - } - unsafe { *index = 0 }; - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_subtitleparams_generate_ass_header( - _params: OakSubtitleParams, - _width: c_int, - _height: c_int, -) { -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_subtitleparams_add_subtitle( - _params: OakSubtitleParams, - _text: *const c_char, -) { -} - -// --------------------------------------------------------------------------- -// oakcommon_config_* -// --------------------------------------------------------------------------- - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_config_get_int( - _group: *const c_char, - _key: *const c_char, - default: c_int, -) -> c_int { - default -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_config_get_bool( - _group: *const c_char, - _key: *const c_char, - default: c_int, -) -> c_int { - default -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_config_get( - _group: *const c_char, - _key: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - // Empty value: report the size needed (1 for NUL) and write NUL. - if buf_size <= 0 { - return 1; - } - if !buf.is_null() { - unsafe { *buf = 0 }; - } - 1 -} - -// --------------------------------------------------------------------------- -// oakcommon_filefunctions_* -// --------------------------------------------------------------------------- - -static FAKE_PATH: &[u8] = b"/mock/config/oak\0"; - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_filefunctions_init() -> OakFileFunctions { - OakFileFunctions::null() -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_filefunctions_get_configuration_location( - _self_: OakFileFunctions, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - copy_cstr(FAKE_PATH, buf, buf_size) -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_filefunctions_get_unique_file_identifier( - _self_: OakFileFunctions, - path: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - if path.is_null() || buf.is_null() { - return -1; - } - let hash = unsafe { CStr::from_ptr(path) } - .to_bytes() - .iter() - .fold(14695981039346656037u64, |acc, &b| { - (acc ^ b as u64).wrapping_mul(1099511628211) - }); - let s = format!("{}", hash as i64); - let bytes = s.as_bytes(); - let n = bytes.len() + 1; - if buf_size < n as c_int { - return n as c_int; - } - unsafe { - std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, bytes.len()); - *buf.add(bytes.len()) = 0; - } - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_filefunctions_get_application_path( - _self_: OakFileFunctions, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - copy_cstr(b"/mock/app/oak\0", buf, buf_size) -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_filefunctions_free(_ptr: *mut c_void) {} - -// --------------------------------------------------------------------------- -// oakcommon_colortransform_* -// --------------------------------------------------------------------------- - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_colortransform_init_output( - _output: *const c_char, -) -> OakVideoParams { - oakcommon_videoparams_init() -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_colortransform_get_output( - params: OakVideoParams, - out: *mut OakVideoParams, -) { - if !out.is_null() { - unsafe { *out = params.clone() }; - } -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_colortransform_free(params: *mut OakVideoParams) { - oakcommon_videoparams_free(params); -} - -// --------------------------------------------------------------------------- -// oakcommon_ffmpegutils_* (pure enum mapping; identity is a safe default) -// --------------------------------------------------------------------------- - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_ffmpegutils_get_native_sample_format( - smp_fmt: c_int, - out: *mut c_int, -) -> c_int { - if out.is_null() { - return -1; - } - unsafe { *out = smp_fmt }; - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_ffmpegutils_get_compatible_pixel_format( - pix_fmt: c_int, - out: *mut c_int, -) -> c_int { - if out.is_null() { - return -1; - } - unsafe { *out = pix_fmt }; - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_ffmpegutils_get_ffmpeg_pixel_format( - pix_fmt: c_int, - _channel_count: c_int, - out: *mut c_int, -) -> c_int { - if out.is_null() { - return -1; - } - unsafe { *out = pix_fmt }; - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_ffmpegutils_get_ffmpeg_sample_format( - smp_fmt: c_int, - out: *mut c_int, -) -> c_int { - if out.is_null() { - return -1; - } - unsafe { *out = smp_fmt }; - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_ffmpegutils_get_compatible_bridge_pixel_format( - pix_fmt: c_int, - _maximum_pix_fmt: c_int, - out: *mut c_int, -) -> c_int { - if out.is_null() { - return -1; - } - unsafe { *out = pix_fmt }; - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space( - pix_fmt: c_int, - out: *mut c_int, -) -> c_int { - if out.is_null() { - return -1; - } - unsafe { *out = pix_fmt }; - 0 -} - -// --------------------------------------------------------------------------- -// oakcommon_oiioutils_* -// --------------------------------------------------------------------------- - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_oiioutils_init() -> OakOIIOUtils { - OakOIIOUtils::null() -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_oiioutils_get_oiio_base_type_from_format( - _self_: OakOIIOUtils, - pixel_format: c_int, - out_base_type: *mut c_int, -) -> c_int { - if out_base_type.is_null() { - return -1; - } - unsafe { *out_base_type = pixel_format }; - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_oiioutils_get_format_from_oiio_basetype( - _self_: OakOIIOUtils, - base_type: c_int, - out_pixel_format: *mut c_int, -) -> c_int { - if out_pixel_format.is_null() { - return -1; - } - unsafe { *out_pixel_format = base_type }; - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_oiioutils_get_pixel_aspect_ratio( - _self_: OakOIIOUtils, - _pixel_aspect_ratio: f64, - out_numerator: *mut c_int, - out_denominator: *mut c_int, -) -> c_int { - if !out_numerator.is_null() { - unsafe { *out_numerator = 1 }; - } - if !out_denominator.is_null() { - unsafe { *out_denominator = 1 }; - } - 0 -} - -#[no_mangle] -#[cfg(test)] -pub extern "C" fn oakcommon_oiioutils_free(_self_: *mut OakOIIOUtils) {} - -// --------------------------------------------------------------------------- -// oakrender_cancelatom_* (real in-memory cancel state) -// --------------------------------------------------------------------------- - -static CANCEL_FLAGS: OnceLock>> = OnceLock::new(); - -fn cancel_flags() -> &'static Mutex> { - CANCEL_FLAGS.get_or_init(|| Mutex::new(HashMap::new())) -} - -#[no_mangle] -pub extern "C" fn oakrender_cancelatom_init() -> OakCancelAtom { - static COUNTER: AtomicU64 = AtomicU64::new(1); - let id = COUNTER.fetch_add(1, Ordering::SeqCst) as usize; - cancel_flags().lock().unwrap().insert(id, false); - OakCancelAtom { - ctx: id as *mut c_void, - addref: None, - release: None, - abi_version: OAKCODEC_ABI_VERSION, - } -} - -#[no_mangle] -pub extern "C" fn oakrender_cancelatom_free(atom: *mut OakCancelAtom) { - if atom.is_null() { - return; - } - unsafe { - cancel_flags() - .lock() - .unwrap() - .remove(&((*atom).ctx as usize)) - }; -} - -#[no_mangle] -pub extern "C" fn oakrender_cancelatom_is_cancelled(atom: OakCancelAtom) -> c_int { - (*cancel_flags() - .lock() - .unwrap() - .get(&(atom.ctx as usize)) - .unwrap_or(&false)) as c_int -} - -#[no_mangle] -pub extern "C" fn oakrender_cancelatom_heard_cancel(atom: OakCancelAtom) -> c_int { - oakrender_cancelatom_is_cancelled(atom) -} - -#[no_mangle] -pub extern "C" fn oakrender_cancelatom_cancel(atom: OakCancelAtom) { - if let Some(f) = cancel_flags().lock().unwrap().get_mut(&(atom.ctx as usize)) { - *f = true; - } -} - -#[no_mangle] -pub extern "C" fn oakrender_cancelatom_get_native(atom: OakCancelAtom) -> *mut c_void { - atom.ctx -} - -// --------------------------------------------------------------------------- -// oakrender_display_texture_* / codec_frame_* (neutral) -// --------------------------------------------------------------------------- - -#[no_mangle] -pub extern "C" fn oakrender_display_texture_create( - _renderer: OakRenderRenderer, - _params: *const crate::bridge::render::oakrender_video_params, - _data: *const c_void, - _linesize: c_int, -) -> OakRenderTexture { - OakRenderTexture { - ctx: std::ptr::null_mut(), - addref: None, - release: None, - abi_version: OAKCODEC_ABI_VERSION, - } -} - -#[no_mangle] -pub extern "C" fn oakrender_display_texture_retain(texture: OakRenderTexture) -> OakRenderTexture { - texture -} - -#[no_mangle] -pub extern "C" fn oakrender_display_texture_free(_texture: *mut OakRenderTexture) {} - -#[no_mangle] -pub extern "C" fn oakrender_display_texture_upload(_texture: OakRenderTexture) -> c_int { - 0 -} - -#[no_mangle] -pub extern "C" fn oakrender_display_texture_download( - _texture: OakRenderTexture, - _pixels: *mut c_void, - _linesize: c_int, -) -> c_int { - 0 -} - -#[no_mangle] -pub extern "C" fn oakrender_display_texture_get_params( - _texture: OakRenderTexture, - _out: *mut crate::bridge::render::oakrender_video_params, -) -> c_int { - 0 -} - -#[no_mangle] -pub extern "C" fn oakrender_display_texture_id(_texture: OakRenderTexture) -> c_int { - 0 -} - -#[no_mangle] -pub extern "C" fn oakrender_display_texture_is_dummy(_texture: OakRenderTexture) -> c_int { - 1 -} - -#[no_mangle] -pub extern "C" fn oakrender_display_texture_get_frame( - _texture: OakRenderTexture, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - if buf_size <= 0 { - return 1; - } - if !buf.is_null() { - unsafe { *buf = 0 }; - } - 1 -} - -#[no_mangle] -pub extern "C" fn oakrender_codec_frame_width(_frame: OakCodecFrame) -> c_int { - 0 -} - -#[no_mangle] -pub extern "C" fn oakrender_codec_frame_height(_frame: OakCodecFrame) -> c_int { - 0 -} - -#[no_mangle] -pub extern "C" fn oakrender_codec_frame_fb_format(_frame: OakCodecFrame) -> c_int { - 0 -} - -#[no_mangle] -pub extern "C" fn oakrender_codec_frame_free(_frame: *mut OakCodecFrame) {} - -#[no_mangle] -pub extern "C" fn oakrender_codec_frame_allocate(_frame: OakCodecFrame) -> c_int { - 1 -} - -#[no_mangle] -pub extern "C" fn oakrender_codec_frame_linesize_bytes(_frame: OakCodecFrame) -> c_int { - 0 -} - -#[no_mangle] -pub extern "C" fn oakrender_codec_frame_is_allocated(_frame: OakCodecFrame) -> c_int { - 1 -} - -#[no_mangle] -pub extern "C" fn oakrender_display_renderer_blit_color_managed( - _renderer: OakRenderRenderer, - _job: *const c_void, - _dst_texture: OakRenderTexture, - _params: *const crate::bridge::render::oakrender_video_params, -) -> c_int { - 0 -} - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/// Two-stage string copy helper used by the mock getters. Returns the -/// total size needed (including the trailing NUL), or truncates the buffer -/// and writes a NUL terminator when the buffer is too small. -fn copy_cstr(src: &[u8], buf: *mut c_char, buf_size: c_int) -> c_int { - let needed = src.len() as c_int; - if buf.is_null() || buf_size < needed { - return needed; - } - unsafe { - for (i, &b) in src.iter().enumerate() { - *buf.add(i) = b as c_char; - } - } - needed -} - -// Silence unused-import warnings when the by-value handle types are not -// referenced by every build; they are part of the mock's public surface. -#[allow(unused)] -fn _keep(_: OakAudioParams, _: OakNodeBlock) {} diff --git a/crates/oakcodec/src/conformmanager.rs b/crates/oakcodec/src/conformmanager.rs index 8a56f424e..33be4b153 100644 --- a/crates/oakcodec/src/conformmanager.rs +++ b/crates/oakcodec/src/conformmanager.rs @@ -22,7 +22,7 @@ //! `Unavailable`. Deterministic per-channel filenames derive from the //! source + target audio params. -use std::ffi::CString; +use oakcommon::filefunctions::FileFunctions; use std::path::Path; /// Conform state of one audio stream. @@ -184,18 +184,11 @@ fn conform_filenames( out } -/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper (the bridge -/// returns a 64-bit id directly). +/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper. fn unique_file_identifier(filename: &str) -> String { - let c = match CString::new(filename) { - Ok(c) => c, - Err(_) => return String::new(), - }; - // # Safety: `c` is a valid NUL-terminated C string alive for the call. - let id = unsafe { - crate::bridge::common::oakcommon_filefunctions_get_unique_file_identifier(c.as_ptr()) - }; - format!("{}", id) + FileFunctions::new() + .get_unique_file_identifier(filename) + .unwrap_or_default() } /// True when every conform filename already exists on disk. @@ -234,34 +227,23 @@ mod tests { dir.to_string_lossy().into_owned() } - fn fnv1a64(bytes: &[u8]) -> u64 { - let mut h: u64 = 14695981039346656037; - for &b in bytes { - h ^= b as u64; - h = h.wrapping_mul(1099511628211); - } - h - } - #[test] - fn unique_identifier_matches_bridge_hash() { - // The frozen ABI returns the identifier as a string; the - // value-style adapter folds it into an FNV-1a-64. The test stub - // produces the decimal FNV of the path bytes, so the expected - // value is FNV of that decimal string. - let stub_id = fnv1a64(b"media.mp4") as i64; - let expected = format!("{}", fnv1a64(stub_id.to_string().as_bytes()) as i64); - assert_eq!(unique_file_identifier("media.mp4"), expected); + fn unique_identifier_uses_file_metadata() { + // The identifier is the 16-digit FNV-1a hex of (absolute path + + // mtime); a missing file has no identifier. + assert_eq!(unique_file_identifier("no-such-file.mp4"), ""); + let dir = temp_subdir("id"); + let file = std::path::Path::new(&dir).join("media.mp4"); + std::fs::write(&file, b"x").unwrap(); + let id = unique_file_identifier(&file.to_string_lossy()); + assert_eq!(id.len(), 16); + assert!(id.chars().all(|c| c.is_ascii_hexdigit())); // Deterministic: same input, same id. - assert_eq!( - unique_file_identifier("media.mp4"), - unique_file_identifier("media.mp4") - ); + assert_eq!(id, unique_file_identifier(&file.to_string_lossy())); + let other = std::path::Path::new(&dir).join("other.mp4"); + std::fs::write(&other, b"y").unwrap(); // Different input, different id. - assert_ne!( - unique_file_identifier("media.mp4"), - unique_file_identifier("other.mp4") - ); + assert_ne!(id, unique_file_identifier(&other.to_string_lossy())); } #[test] @@ -279,21 +261,23 @@ mod tests { #[test] fn conform_filename_derivation_and_range() { let m = ConformManager::instance(); + let src_dir = temp_subdir("names"); + let src = std::path::Path::new(&src_dir).join("media.mp4"); + std::fs::write(&src, b"x").unwrap(); let cache = temp_subdir("names"); - let stub_id = fnv1a64(b"media.mp4") as i64; - let id = fnv1a64(stub_id.to_string().as_bytes()) as i64; + let id = unique_file_identifier(&src.to_string_lossy()); let base = format!("{}-0.48000.0.3", id); let f0 = m - .get_conform_filename(&cache, "media.mp4", 0, 48000, 0x3, 0, 0) + .get_conform_filename(&cache, &src.to_string_lossy(), 0, 48000, 0x3, 0, 0) .unwrap(); let f1 = m - .get_conform_filename(&cache, "media.mp4", 0, 48000, 0x3, 0, 1) + .get_conform_filename(&cache, &src.to_string_lossy(), 0, 48000, 0x3, 0, 1) .unwrap(); assert_eq!(f0, format!("{}/{}.0.pcm", cache, base)); assert_eq!(f1, format!("{}/{}.1.pcm", cache, base)); // Out of range. assert!(matches!( - m.get_conform_filename(&cache, "media.mp4", 0, 48000, 0x3, 0, 5), + m.get_conform_filename(&cache, &src.to_string_lossy(), 0, 48000, 0x3, 0, 5), Err(crate::error::Error::NotFound) )); } diff --git a/crates/oakcodec/src/decoder.rs b/crates/oakcodec/src/decoder.rs index 9327e7704..9c19f477e 100644 --- a/crates/oakcodec/src/decoder.rs +++ b/crates/oakcodec/src/decoder.rs @@ -26,12 +26,22 @@ use std::path::Path; use std::sync::{Arc, Mutex, OnceLock}; +use oakcommon::cancelatom::CancelAtom; use oakcore_rs::{Rational, TimeRange}; -use crate::bridge::render::{OakCancelAtom, OakRenderTexture}; use crate::footagedescription::FootageDescription; use crate::frame::Frame; +/// `OakRenderTexture` — refcounted GPU texture handle (an oakrender type, +/// opaque to oakcodec). The codec crate cannot depend on oakrender (the +/// dependency cycle), so it only ever produces an empty handle — the +/// shared [`crate::handle::CHandle`] carries that value unchanged. +pub type OakRenderTexture = crate::handle::CHandle; + +/// `OakNodeBlock` — opaque node-block handle owned elsewhere; the codec +/// only stores and forwards it (borrowed, never dereferenced). +pub type OakNodeBlock = crate::handle::CHandle; + /// `oakcodec_video_stream_info` — POD probe output describing one video /// stream; see `include/codec/decoder.h`. #[repr(C)] @@ -158,7 +168,7 @@ pub enum RetrieveState { pub struct CodecStream { filename: String, stream: i32, - block: Option, + block: Option, } impl CodecStream { @@ -175,7 +185,7 @@ impl CodecStream { pub fn with_block( filename: String, stream: i32, - block: Option, + block: Option, ) -> Self { CodecStream { filename, @@ -212,7 +222,7 @@ impl CodecStream { } /// Associated timeline block (borrowed; only compared, never used). - pub fn block(&self) -> Option { + pub fn block(&self) -> Option { self.block.clone() } } @@ -241,7 +251,7 @@ pub trait Decoder: Send + Sync { fn probe( &self, filename: &str, - cancelled: Option<&OakCancelAtom>, + cancelled: Option<&CancelAtom>, ) -> Option; /// Open `stream` for decoding. Thread-safe. @@ -281,7 +291,7 @@ pub trait Decoder: Send + Sync { sample_rate: i32, channel_layout: u64, sample_format: i32, - cancelled: Option<&OakCancelAtom>, + cancelled: Option<&CancelAtom>, ) -> crate::error::Result<()>; /// Offset of the audio start relative to the video (rational seconds). @@ -326,7 +336,7 @@ impl Decoder for UnimplementedDecoder { fn probe( &self, _filename: &str, - _cancelled: Option<&OakCancelAtom>, + _cancelled: Option<&CancelAtom>, ) -> Option { None } @@ -377,7 +387,7 @@ impl Decoder for UnimplementedDecoder { _sample_rate: i32, _channel_layout: u64, _sample_format: i32, - _cancelled: Option<&OakCancelAtom>, + _cancelled: Option<&CancelAtom>, ) -> crate::error::Result<()> { Err(crate::error::Error::Failed( "decoder not yet implemented".to_string(), @@ -400,13 +410,13 @@ pub fn create_from_id(id: &str) -> Option> { /// not injected, in which case the built-in list below is used. static TEST_DECODERS: OnceLock>>> = OnceLock::new(); -/// Serializes every test that reads the built-in decoder registry. The ffi -/// decoder tests inject through `crate::ffi::lock_tests()` (the shared -/// `TEST_LOCK`), so the registry assertions below take that same lock to -/// never race with an injected list. +/// Serializes every test that reads the built-in decoder registry. Tests +/// inject through [`set_test_decoders`] under [`crate::lock_tests`] (the +/// shared test lock), so the registry assertions below take that same lock +/// to never race with an injected list. #[cfg(test)] -fn registry_guard() -> std::sync::MutexGuard<'static, ()> { - crate::ffi::lock_tests() +fn registry_guard() -> crate::TestLock { + crate::lock_tests() } /// Replace the decoder registry with `list`; pass an empty list to restore diff --git a/crates/oakcodec/src/ffi/conform.rs b/crates/oakcodec/src/ffi/conform.rs deleted file mode 100644 index eb503a877..000000000 --- a/crates/oakcodec/src/ffi/conform.rs +++ /dev/null @@ -1,308 +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 . - -//! `include/codec/conform.h` exports. -//! -//! Complete inventory: create_instance / destroy_instance / get_state / -//! filename_count / filename_at. `OAKCODEC_CONFORM_*` macros are the -//! states. -//! -//! # CPP-PARITY -//! The C++ `c_api/conform.cpp` reports `OAKCODEC_E_STATE` when the -//! singleton is absent (`!ConformManager::instance()`); the Rust -//! [`crate::conformmanager::ConformManager::instance`] is a lazy -//! `'static` singleton that can never be absent, so that branch cannot -//! trigger. - -use std::ffi::{c_char, c_int}; - -use crate::conformmanager::{ConformManager, ConformState}; -use crate::handle; - -/// `OAKCODEC_CONFORM_EXISTS`. -pub const OAKCODEC_CONFORM_EXISTS: c_int = 0; -/// `OAKCODEC_CONFORM_GENERATING`. -pub const OAKCODEC_CONFORM_GENERATING: c_int = 1; -/// `OAKCODEC_CONFORM_UNAVAILABLE`. -pub const OAKCODEC_CONFORM_UNAVAILABLE: c_int = 2; - -/// `oakcodec_conform_create_instance`: create the singleton (always -/// present here, so a no-op). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_conform_create_instance() -> c_int { - handle::guard_raw(|| { - let _ = ConformManager::instance(); - crate::error::OAKCODEC_OK - }) -} - -/// `oakcodec_conform_destroy_instance`: destroy the singleton (the Rust -/// manager is stateless, so a no-op). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_conform_destroy_instance() -> c_int { - handle::guard_raw(|| crate::error::OAKCODEC_OK) -} - -/// `oakcodec_conform_get_state`: query the conform state of one audio -/// stream. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_conform_get_state( - cache_path: *const c_char, - source_filename: *const c_char, - stream_index: c_int, - sample_rate: c_int, - channel_layout: u64, - sample_format: c_int, - wait: c_int, -) -> c_int { - handle::guard_raw(|| { - let cache = match crate::ffi::c_str(cache_path) { - Some(c) if !c.is_empty() => c, - _ => return crate::error::OAKCODEC_E_INVALID, - }; - let source = match crate::ffi::c_str(source_filename) { - Some(s) if !s.is_empty() => s, - _ => return crate::error::OAKCODEC_E_INVALID, - }; - let m = ConformManager::instance(); - match m.get_conform_state( - &cache, - &source, - stream_index, - sample_rate, - channel_layout, - sample_format, - wait != 0, - ) { - Ok(ConformState::Exists) => OAKCODEC_CONFORM_EXISTS, - Ok(ConformState::Generating) => OAKCODEC_CONFORM_GENERATING, - Ok(ConformState::Unavailable) | Err(_) => OAKCODEC_CONFORM_UNAVAILABLE, - } - }) -} - -/// `oakcodec_conform_filename_count`: number of conform files for the -/// given stream/params; 0 on invalid arguments. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_conform_filename_count( - cache_path: *const c_char, - source_filename: *const c_char, - stream_index: c_int, - sample_rate: c_int, - channel_layout: u64, - sample_format: c_int, -) -> c_int { - handle::guard_raw(|| { - let cache = match crate::ffi::c_str(cache_path) { - Some(c) if !c.is_empty() => c, - _ => return 0, - }; - let source = match crate::ffi::c_str(source_filename) { - Some(s) if !s.is_empty() => s, - _ => return 0, - }; - let m = ConformManager::instance(); - m.get_conform_filename_count( - &cache, - &source, - stream_index, - sample_rate, - channel_layout, - sample_format, - ) as c_int - }) -} - -/// `oakcodec_conform_filename_at`: the `index`-th conform filename -/// (two-stage string). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_conform_filename_at( - cache_path: *const c_char, - source_filename: *const c_char, - stream_index: c_int, - sample_rate: c_int, - channel_layout: u64, - sample_format: c_int, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - handle::guard_raw(|| { - let cache = match crate::ffi::c_str(cache_path) { - Some(c) if !c.is_empty() => c, - _ => return crate::error::OAKCODEC_E_INVALID, - }; - let source = match crate::ffi::c_str(source_filename) { - Some(s) if !s.is_empty() => s, - _ => return crate::error::OAKCODEC_E_INVALID, - }; - let m = ConformManager::instance(); - match m.get_conform_filename( - &cache, - &source, - stream_index, - sample_rate, - channel_layout, - sample_format, - index as usize, - ) { - Ok(f) => super::string_out(&f, buf, buf_size), - Err(crate::error::Error::NotFound) => crate::error::OAKCODEC_E_NOT_FOUND, - Err(_) => crate::error::OAKCODEC_E_FAILED, - } - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::conformmanager::test_util::REG_LOCK; - use crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_NOT_FOUND}; - - fn cstr(s: &str) -> std::ffi::CString { - std::ffi::CString::new(s).unwrap() - } - - fn temp_cache(name: &str) -> String { - let dir = std::env::temp_dir().join(format!( - "oakcodec_ffi_conform_{}_{}", - name, - std::process::id() - )); - let _ = std::fs::create_dir_all(&dir); - dir.to_string_lossy().into_owned() - } - - #[test] - fn create_destroy_instance_ok() { - let _g = crate::ffi::lock_tests(); - assert_eq!( - unsafe { oakcodec_conform_create_instance() }, - crate::error::OAKCODEC_OK - ); - assert_eq!( - unsafe { oakcodec_conform_destroy_instance() }, - crate::error::OAKCODEC_OK - ); - } - - #[test] - fn get_state_maps_states() { - let _g = crate::ffi::lock_tests(); - let _g = REG_LOCK.lock().unwrap(); - // No registrar and no files -> UNAVAILABLE. - let cache = cstr(&temp_cache("state")); - let src = cstr("media.mp4"); - let rc = unsafe { - oakcodec_conform_get_state(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0) - }; - assert_eq!(rc, OAKCODEC_CONFORM_UNAVAILABLE); - - // Invalid arguments -> E_INVALID. - let rc = unsafe { - oakcodec_conform_get_state(std::ptr::null(), src.as_ptr(), 0, 48000, 0x3, 0, 0) - }; - assert_eq!(rc, OAKCODEC_E_INVALID); - let empty = cstr(""); - let rc = unsafe { - oakcodec_conform_get_state(empty.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0) - }; - assert_eq!(rc, OAKCODEC_E_INVALID); - - // Write the conform files -> EXISTS. - let m = ConformManager::instance(); - for i in 0..2 { - let f = m - .get_conform_filename(&temp_cache("state"), "media.mp4", 0, 48000, 0x3, 0, i) - .unwrap(); - std::fs::write(&f, b"pcm").unwrap(); - } - let rc = unsafe { - oakcodec_conform_get_state(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0) - }; - assert_eq!(rc, OAKCODEC_CONFORM_EXISTS); - } - - #[test] - fn filename_count_and_at() { - let _g = crate::ffi::lock_tests(); - let cache = cstr(&temp_cache("names")); - let src = cstr("media.mp4"); - - // Stereo -> 2 files. - let rc = unsafe { - oakcodec_conform_filename_count(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0) - }; - assert_eq!(rc, 2); - - // Invalid args -> 0 (not an error). - let rc = unsafe { - oakcodec_conform_filename_count(std::ptr::null(), src.as_ptr(), 0, 48000, 0x3, 0) - }; - assert_eq!(rc, 0); - - // filename_at round-trips the deterministic name. - let mut buf = [0i8; 512]; - let rc = unsafe { - oakcodec_conform_filename_at( - cache.as_ptr(), - src.as_ptr(), - 0, - 48000, - 0x3, - 0, - 0, - buf.as_mut_ptr(), - 512, - ) - }; - assert!(rc > 0); - let name = crate::ffi::c_str(buf.as_ptr()).unwrap(); - assert!(name.ends_with(".0.pcm")); - - // Out-of-range index -> E_NOT_FOUND. - let rc = unsafe { - oakcodec_conform_filename_at( - cache.as_ptr(), - src.as_ptr(), - 0, - 48000, - 0x3, - 0, - 5, - buf.as_mut_ptr(), - 512, - ) - }; - assert_eq!(rc, OAKCODEC_E_NOT_FOUND); - - // Invalid args -> E_INVALID. - let rc = unsafe { - oakcodec_conform_filename_at( - std::ptr::null(), - src.as_ptr(), - 0, - 48000, - 0x3, - 0, - 0, - buf.as_mut_ptr(), - 512, - ) - }; - assert_eq!(rc, OAKCODEC_E_INVALID); - } -} diff --git a/crates/oakcodec/src/ffi/decoder.rs b/crates/oakcodec/src/ffi/decoder.rs deleted file mode 100644 index 99f98b8f2..000000000 --- a/crates/oakcodec/src/ffi/decoder.rs +++ /dev/null @@ -1,1329 +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 . - -//! `include/codec/decoder.h` exports. -//! -//! Complete inventory: probe / probe_last_error / probe_decoder_name / -//! probe_video_stream_count / probe_audio_stream_count / -//! probe_subtitle_stream_count / probe_get_video_stream / -//! probe_get_audio_stream / init / free / open / close / is_open / -//! decode_video / decode_audio / conform_audio / -//! get_image_sequence_digit_count / get_image_sequence_index / -//! transform_image_sequence_file_name / last_error. -//! -//! Two handle box types share the `OakDecoder` handle, mirroring the C++ -//! `ProbeBox` / `DecoderBox` split in `c_api/decoder.cpp`: probe exports -//! read a plain [`ProbeBox`], session exports read a `Mutex`. - -#[cfg(test)] -use std::ffi::c_void; -use std::ffi::{c_char, c_int}; -use std::path::Path; -use std::sync::{Arc, Mutex}; - -use oakcore_rs::{Rational, TimeRange}; - -use crate::bridge::common::{ - oakcommon_videoparams_get_channel_count, oakcommon_videoparams_get_color_primaries, - oakcommon_videoparams_get_color_transfer, oakcommon_videoparams_get_duration, - oakcommon_videoparams_get_format, oakcommon_videoparams_get_frame_rate, - oakcommon_videoparams_get_height, oakcommon_videoparams_get_interlacing, - oakcommon_videoparams_get_stream_index, oakcommon_videoparams_get_time_base, - oakcommon_videoparams_get_width, oakcore_audioparams_channel_count, - oakcore_audioparams_channel_layout, oakcore_audioparams_duration, - oakcore_audioparams_sample_rate, oakcore_audioparams_stream_index, - oakcore_audioparams_time_base, oakcore_rational_denominator, oakcore_rational_free, - oakcore_rational_numerator, OakAudioParams, OakVideoParams, -}; -use crate::bridge::render::{oakrender_cancelatom_heard_cancel, OakCancelAtom}; -use crate::decoder::{ - CodecStream, Decoder, OakCodecAudioStreamInfo, OakCodecVideoStreamInfo, RenderMode, - RetrieveAudioStatus, RetrieveVideoParams, K_COLOR_RANGE_DEFAULT, -}; -use crate::footagedescription::FootageDescription; -#[cfg(test)] -use crate::frame::Frame; -use crate::handle::{self, CHandle}; - -/// Box behind a probe handle: the recognized decoder name plus the -/// probed stream inventory (`ProbeBox` in `c_api/decoder.cpp`). -struct ProbeBox { - decoder_name: String, - desc: FootageDescription, -} - -/// Box behind a decode-session handle (`DecoderBox` in `c_api/decoder.cpp`). -struct DecoderBox { - decoder: Option>, - last_error: String, - open_filename: String, - open_stream: i32, - open: bool, -} - -/// Last probe failure reason. The C++ uses a `thread_local`; the crate -/// serializes through a global mutex instead (tests are single-threaded -/// per binary, so behavior is equivalent). -static PROBE_ERROR: Mutex = Mutex::new(String::new()); - -/// `OAKCOMMON_VIDEO_INTERLACE_NONE` (oakcommon `common/videoparams.h`). -const OAKCOMMON_VIDEO_INTERLACE_NONE: c_int = 0; - -/// Probe with every available decoder, returning the first valid -/// description (mirrors `probe_with_any_decoder` in `c_api/decoder.cpp`). -/// -/// `is_valid` follows the C++ `FootageDescription::is_valid`: a non-empty -/// decoder name and at least one stream. -fn probe_with_any_decoder(filename: &str) -> Option<(String, FootageDescription)> { - for d in crate::decoder::receive_list_of_all_decoders() { - let desc = match d.probe(filename, None) { - Some(desc) => desc, - None => continue, - }; - if !desc.decoder().is_empty() - && (desc.video_stream_count() > 0 - || desc.audio_stream_count() > 0 - || desc.subtitle_stream_count() > 0) - { - return Some((desc.decoder().to_string(), desc)); - } - } - None -} - -/// Fill an `oakcodec_video_stream_info` from a probed video params handle -/// (mirrors `fill_video_info` in `c_api/decoder.cpp`). -fn fill_video_info(vp: &OakVideoParams, out: &mut OakCodecVideoStreamInfo) { - *out = OakCodecVideoStreamInfo { - stream_index: 0, - width: 0, - height: 0, - frame_rate_num: 0, - frame_rate_den: 0, - duration_ts: 0, - time_base_num: 0, - time_base_den: 0, - format: 0, - channel_count: 0, - color_primaries: 0, - color_trc: 0, - interlaced: 0, - }; - out.stream_index = unsafe { oakcommon_videoparams_get_stream_index(vp.clone()) }; - out.width = unsafe { oakcommon_videoparams_get_width(vp.clone()) }; - out.height = unsafe { oakcommon_videoparams_get_height(vp.clone()) }; - - let mut fr_num: c_int = 0; - let mut fr_den: c_int = 0; - unsafe { oakcommon_videoparams_get_frame_rate(vp.clone(), &mut fr_num, &mut fr_den) }; - out.frame_rate_num = fr_num; - out.frame_rate_den = fr_den; - - let mut tb_num: i64 = 0; - let mut tb_den: i64 = 0; - unsafe { oakcommon_videoparams_get_time_base(vp.clone(), &mut tb_num, &mut tb_den) }; - out.time_base_num = tb_num as c_int; - out.time_base_den = tb_den as c_int; - - out.duration_ts = unsafe { oakcommon_videoparams_get_duration(vp.clone()) }; - out.format = unsafe { oakcommon_videoparams_get_format(vp.clone()) }; - out.channel_count = unsafe { oakcommon_videoparams_get_channel_count(vp.clone()) }; - out.color_primaries = unsafe { oakcommon_videoparams_get_color_primaries(vp.clone()) }; - out.color_trc = unsafe { oakcommon_videoparams_get_color_transfer(vp.clone()) }; - - let interlacing = unsafe { oakcommon_videoparams_get_interlacing(vp.clone()) }; - out.interlaced = (interlacing != OAKCOMMON_VIDEO_INTERLACE_NONE) as c_int; -} - -/// Fill an `oakcodec_audio_stream_info` from a probed audio params handle -/// (mirrors `fill_audio_info` in `c_api/decoder.cpp`). -fn fill_audio_info(ap: &OakAudioParams, out: &mut OakCodecAudioStreamInfo) { - *out = OakCodecAudioStreamInfo { - stream_index: 0, - sample_rate: 0, - channel_layout: 0, - channel_count: 0, - duration_ts: 0, - time_base_num: 0, - time_base_den: 0, - }; - let ctx = ap.ctx as *const OakAudioParams; - out.stream_index = unsafe { oakcore_audioparams_stream_index(ctx) }; - out.sample_rate = unsafe { oakcore_audioparams_sample_rate(ctx) }; - out.channel_layout = unsafe { oakcore_audioparams_channel_layout(ctx) }; - out.channel_count = unsafe { oakcore_audioparams_channel_count(ctx) }; - out.duration_ts = unsafe { oakcore_audioparams_duration(ctx) }; - - // `time_base` returns a newly allocated rational the caller owns. - let tb = unsafe { oakcore_audioparams_time_base(ctx) }; - if !tb.is_null() { - out.time_base_num = unsafe { oakcore_rational_numerator(tb) }; - out.time_base_den = unsafe { oakcore_rational_denominator(tb) }; - unsafe { oakcore_rational_free(tb) }; - } -} - -/// Whether a media file exists on disk (mirrors the C++ `file_exists`). -fn file_exists(filename: &str) -> bool { - Path::new(filename).exists() -} - -/* ---- Probe ---------------------------------------------------------------- */ - -/// `oakcodec_decoder_probe`: probe a media file with every available -/// decoder; empty handle plus `oakcodec_probe_last_error` on failure. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_probe(filename: *const c_char) -> CHandle { - handle::guard_handle(|| { - let filename = match crate::ffi::c_str(filename) { - Some(f) if !f.is_empty() => f, - _ => { - *PROBE_ERROR.lock().unwrap() = "no filename given".to_string(); - return Ok(CHandle::null()); - } - }; - if !file_exists(&filename) { - *PROBE_ERROR.lock().unwrap() = format!("file not found: {}", filename); - return Ok(CHandle::null()); - } - match probe_with_any_decoder(&filename) { - Some((decoder_name, desc)) => { - PROBE_ERROR.lock().unwrap().clear(); - Ok(handle::make_owned(ProbeBox { decoder_name, desc })) - } - None => { - *PROBE_ERROR.lock().unwrap() = - format!("no decoder recognizes this file: {}", filename); - Ok(CHandle::null()) - } - } - }) -} - -/// `oakcodec_probe_last_error`: last probe failure reason (two-stage). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_probe_last_error(buf: *mut c_char, buf_size: c_int) -> c_int { - handle::guard_raw(|| { - let s = PROBE_ERROR.lock().unwrap().clone(); - super::string_out(&s, buf, buf_size) - }) -} - -/// `oakcodec_decoder_probe_decoder_name`: recognized decoder id -/// (two-stage). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_probe_decoder_name( - probe: CHandle, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - handle::guard_raw(|| match super::get_box::(&probe) { - Some(b) => super::string_out(&b.decoder_name, buf, buf_size), - None => crate::error::OAKCODEC_E_INVALID, - }) -} - -/// `oakcodec_decoder_probe_video_stream_count`. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_probe_video_stream_count(probe: CHandle) -> c_int { - handle::guard_raw(|| match super::get_box::(&probe) { - Some(b) => b.desc.video_stream_count() as c_int, - None => 0, - }) -} - -/// `oakcodec_decoder_probe_audio_stream_count`. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_probe_audio_stream_count(probe: CHandle) -> c_int { - handle::guard_raw(|| match super::get_box::(&probe) { - Some(b) => b.desc.audio_stream_count() as c_int, - None => 0, - }) -} - -/// `oakcodec_decoder_probe_subtitle_stream_count`. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_probe_subtitle_stream_count(probe: CHandle) -> c_int { - handle::guard_raw(|| match super::get_box::(&probe) { - Some(b) => b.desc.subtitle_stream_count() as c_int, - None => 0, - }) -} - -/// `oakcodec_decoder_probe_get_video_stream`: fill `out` with the video -/// stream at `index` (video ordinal). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_probe_get_video_stream( - probe: CHandle, - index: c_int, - out: *mut OakCodecVideoStreamInfo, -) -> c_int { - handle::guard(|| { - if out.is_null() { - return Err(crate::error::Error::Invalid); - } - let b = super::get_box::(&probe).ok_or(crate::error::Error::Invalid)?; - let vp = b - .desc - .get_video_stream(index as usize) - .ok_or(crate::error::Error::NotFound)?; - fill_video_info(vp, unsafe { &mut *out }); - Ok(()) - }) -} - -/// `oakcodec_decoder_probe_get_audio_stream`: fill `out` with the audio -/// stream at `index` (audio ordinal). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_probe_get_audio_stream( - probe: CHandle, - index: c_int, - out: *mut OakCodecAudioStreamInfo, -) -> c_int { - handle::guard(|| { - if out.is_null() { - return Err(crate::error::Error::Invalid); - } - let b = super::get_box::(&probe).ok_or(crate::error::Error::Invalid)?; - let ap = b - .desc - .get_audio_stream(index as usize) - .ok_or(crate::error::Error::NotFound)?; - fill_audio_info(ap, unsafe { &mut *out }); - Ok(()) - }) -} - -/* ---- Decode session -------------------------------------------------------- */ - -/// `oakcodec_decoder_init`: new closed decoder handle, count 1. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_init() -> CHandle { - handle::guard_handle(|| { - Ok(handle::make_owned(Mutex::new(DecoderBox { - decoder: None, - last_error: String::new(), - open_filename: String::new(), - open_stream: -1, - open: false, - }))) - }) -} - -/// `oakcodec_decoder_free`: NULL/empty no-op; nulls `ctx` afterwards. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_free(decoder: *mut CHandle) { - handle::guard_void(|| super::free_handle(decoder)); -} - -/// `oakcodec_decoder_open`: open `filename`'s stream `stream_index` for -/// decoding; opening the same stream again is a successful no-op. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_open( - decoder: CHandle, - filename: *const c_char, - stream_index: c_int, -) -> c_int { - handle::guard(|| { - let b = - super::get_box::>(&decoder).ok_or(crate::error::Error::Invalid)?; - let filename = match crate::ffi::c_str(filename) { - Some(f) => f, - None => return Err(crate::error::Error::Invalid), - }; - if stream_index < 0 { - return Err(crate::error::Error::Invalid); - } - let mut b = b.lock().unwrap(); - - if b.open && b.decoder.is_some() { - if b.open_filename == filename && b.open_stream == stream_index { - return Ok(()); // already open on this stream - } - let _ = b.decoder.as_ref().unwrap().close(); - b.open = false; - } - - if !file_exists(&filename) { - b.last_error = format!("file not found: {}", filename); - return Err(crate::error::Error::NotFound); - } - - let (decoder_name, _desc) = match probe_with_any_decoder(&filename) { - Some(x) => x, - None => { - b.last_error = format!("no decoder recognizes this file: {}", filename); - return Err(crate::error::Error::Failed( - "no decoder recognizes this file".to_string(), - )); - } - }; - - let decoder = match crate::decoder::create_from_id(&decoder_name) { - Some(d) => d, - None => { - b.last_error = format!("failed to create decoder: {}", decoder_name); - return Err(crate::error::Error::Failed( - "failed to create decoder".to_string(), - )); - } - }; - - let stream = CodecStream::with_block(filename.clone(), stream_index, None); - if decoder.open(&stream).is_err() { - b.last_error = "failed to open stream".to_string(); - return Err(crate::error::Error::Failed( - "failed to open stream".to_string(), - )); - } - - b.last_error.clear(); - b.decoder = Some(decoder); - b.open_filename = filename; - b.open_stream = stream_index; - b.open = true; - Ok(()) - }) -} - -/// `oakcodec_decoder_close`: close the current stream (safe when closed). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_close(decoder: CHandle) -> c_int { - handle::guard(|| { - let b = - super::get_box::>(&decoder).ok_or(crate::error::Error::Invalid)?; - let mut b = b.lock().unwrap(); - if b.open { - if let Some(d) = &b.decoder { - let _ = d.close(); - } - } - b.open = false; - Ok(()) - }) -} - -/// `oakcodec_decoder_is_open`: 1 when a stream is open. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_is_open(decoder: CHandle) -> c_int { - handle::guard_raw(|| match super::get_box::>(&decoder) { - Some(b) => { - let b = b.lock().unwrap(); - if b.open { - 1 - } else { - 0 - } - } - None => 0, - }) -} - -/// `oakcodec_decoder_decode_video`: decode the frame at -/// `numerator/denominator` seconds into a new frame handle (count 1). -/// -/// # CPP-PARITY -/// The C++ boxes an `olive::FramePtr` and happily aliases a frame the -/// decoder still holds. The Rust interim boxes `Mutex` (see -/// `ffi::frame`), so a decode that returns a still-shared `Arc` -/// cannot be aliased and reports an empty handle plus `last_error`. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_decode_video( - decoder: CHandle, - numerator: c_int, - denominator: c_int, -) -> CHandle { - handle::guard_handle(|| { - let b = - super::get_box::>(&decoder).ok_or(crate::error::Error::Invalid)?; - let d = { - let b = b.lock().unwrap(); - if !b.open || b.decoder.is_none() { - // C++ returns an empty frame without touching last_error. - return Ok(CHandle::null()); - } - b.decoder.as_ref().unwrap().clone() - }; - let p = RetrieveVideoParams { - // C++ default-constructs the params and only sets `time`. - stream: CodecStream::new(), - time: Rational::new(numerator as i64, denominator as i64), - length: TimeRange::default(), - force_range: K_COLOR_RANGE_DEFAULT, - is_image_sequence: false, - image_sequence_digits: 0, - image_sequence_number: 0, - mode: RenderMode::Offline, - alpha_is_premultiplied: false, - }; - match d.retrieve_video_frame(&p) { - Ok(arc) => match Arc::try_unwrap(arc) { - Ok(frame) => Ok(handle::make_owned(Mutex::new(frame))), - Err(_) => { - b.lock().unwrap().last_error = "failed to decode video frame".to_string(); - Ok(CHandle::null()) - } - }, - Err(_) => { - b.lock().unwrap().last_error = "failed to decode video frame".to_string(); - Ok(CHandle::null()) - } - } - }) -} - -/// `oakcodec_decoder_decode_audio`: decode interleaved float audio -/// covering [in, out) into `buf` (at least `buf_frames` frames). -/// -/// Returns the number of frames written, or a negative `OAKCODEC_E_*`. -/// # CPP-PARITY -/// The C++ retrieves into an allocated `SampleBuffer` and copies -/// `min(available, buf_frames)` frames. The Rust trait fills a caller-size -/// destination, so `Success` is treated as "`buf_frames` were written". -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_decode_audio( - decoder: CHandle, - in_num: c_int, - in_den: c_int, - out_num: c_int, - out_den: c_int, - sample_rate: c_int, - channel_layout: u64, - buf: *mut f32, - buf_frames: c_int, -) -> c_int { - handle::guard_raw(|| unsafe { - decode_audio_inner( - decoder, - in_num, - in_den, - out_num, - out_den, - sample_rate, - channel_layout, - buf, - buf_frames, - ) - }) -} - -unsafe fn decode_audio_inner( - decoder: CHandle, - in_num: c_int, - in_den: c_int, - out_num: c_int, - out_den: c_int, - sample_rate: c_int, - channel_layout: u64, - buf: *mut f32, - buf_frames: c_int, -) -> c_int { - let b = match super::get_box::>(&decoder) { - Some(b) => b, - None => return crate::error::OAKCODEC_E_INVALID, - }; - if (buf.is_null() && buf_frames > 0) || buf_frames < 0 { - return crate::error::OAKCODEC_E_INVALID; - } - - let d = { - let b = b.lock().unwrap(); - if !b.open || b.decoder.is_none() { - return crate::error::OAKCODEC_E_STATE; - } - b.decoder.as_ref().unwrap().clone() - }; - - 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 channels = channel_layout.count_ones() as usize; - let mut dest = vec![0f32; buf_frames as usize * channels]; - - let status = match d.retrieve_audio(&mut dest, &range, sample_rate, channel_layout) { - Ok(s) => s, - Err(_) => { - b.lock().unwrap().last_error = "failed to decode audio".to_string(); - return crate::error::OAKCODEC_E_FAILED; - } - }; - - match status { - RetrieveAudioStatus::Success => { - // SAFETY: the caller guarantees `buf` holds at least `buf_frames` - // interleaved floats; `dest` is exactly that size. - unsafe { std::ptr::copy_nonoverlapping(dest.as_ptr(), buf, dest.len()) }; - buf_frames - } - RetrieveAudioStatus::ConformNeeded => { - b.lock().unwrap().last_error = - "audio requires a conform, but no task submit callback is registered (see oakcodec_set_task_submit_cb)" - .to_string(); - crate::error::OAKCODEC_E_STATE - } - _ => { - b.lock().unwrap().last_error = "failed to decode audio".to_string(); - crate::error::OAKCODEC_E_FAILED - } - } -} - -/// `oakcodec_decoder_conform_audio`: conform the open stream's audio into -/// the given per-channel pcm files. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_conform_audio( - decoder: CHandle, - output_filenames: *const *const c_char, - filename_count: c_int, - sample_rate: c_int, - channel_layout: u64, - sample_format: c_int, - cancelled: OakCancelAtom, -) -> c_int { - handle::guard(|| unsafe { - conform_audio_inner( - decoder, - output_filenames, - filename_count, - sample_rate, - channel_layout, - sample_format, - cancelled, - ) - }) -} - -unsafe fn conform_audio_inner( - decoder: CHandle, - output_filenames: *const *const c_char, - filename_count: c_int, - sample_rate: c_int, - channel_layout: u64, - sample_format: c_int, - cancelled: OakCancelAtom, -) -> crate::error::Result<()> { - let b = super::get_box::>(&decoder).ok_or(crate::error::Error::Invalid)?; - if (output_filenames.is_null() && filename_count > 0) || filename_count < 0 { - return Err(crate::error::Error::Invalid); - } - - let d = { - let b = b.lock().unwrap(); - if !b.open || b.decoder.is_none() { - return Err(crate::error::Error::State); - } - b.decoder.as_ref().unwrap().clone() - }; - - let mut filenames: Vec = Vec::with_capacity(filename_count as usize); - for i in 0..filename_count { - let p = unsafe { *output_filenames.add(i as usize) }; - let s = crate::ffi::c_str(p).ok_or(crate::error::Error::Invalid)?; - filenames.push(s); - } - - // The trait takes the target audio params flattened; no handle - // construction is needed. - let cancelled_ref = if cancelled.ctx.is_null() { - None - } else { - Some(&cancelled) - }; - let ret = d.conform_audio( - &filenames, - sample_rate, - channel_layout, - sample_format, - cancelled_ref, - ); - - match ret { - Ok(()) => Ok(()), - Err(_) => { - let heard = if cancelled.ctx.is_null() { - 0 - } else { - unsafe { oakrender_cancelatom_heard_cancel(cancelled.clone()) } - }; - if heard != 0 { - Err(crate::error::Error::Cancelled) - } else { - Err(crate::error::Error::Failed("conform failed".to_string())) - } - } - } -} - -/// `oakcodec_decoder_get_image_sequence_digit_count`. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_get_image_sequence_digit_count( - filename: *const c_char, -) -> c_int { - handle::guard_raw(|| match crate::ffi::c_str(filename) { - Some(f) => crate::decoder::get_image_sequence_digit_count(&f), - None => crate::error::OAKCODEC_E_INVALID, - }) -} - -/// `oakcodec_decoder_get_image_sequence_index`. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_get_image_sequence_index(filename: *const c_char) -> i64 { - handle::guard_i64(|| match crate::ffi::c_str(filename) { - Some(f) => crate::decoder::get_image_sequence_index(&f), - None => crate::error::OAKCODEC_E_INVALID as i64, - }) -} - -/// `oakcodec_decoder_transform_image_sequence_file_name` (two-stage). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_transform_image_sequence_file_name( - filename: *const c_char, - number: i64, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - handle::guard_raw(|| match crate::ffi::c_str(filename) { - Some(f) => { - let s = crate::decoder::transform_image_sequence_file_name(&f, number); - super::string_out(&s, buf, buf_size) - } - None => crate::error::OAKCODEC_E_INVALID, - }) -} - -/// `oakcodec_decoder_last_error` (two-stage). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_last_error( - decoder: CHandle, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - handle::guard_raw(|| match super::get_box::>(&decoder) { - Some(b) => { - let b = b.lock().unwrap(); - super::string_out(&b.last_error, buf, buf_size) - } - None => super::string_out("", buf, buf_size), - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::bridge::common::{ - oakcommon_videoparams_init_basic, oakcommon_videoparams_init_with_time_base, - oakcommon_videoparams_set_stream_index, oakcore_audioparams_create, - }; - use crate::bridge::render::{oakrender_cancelatom_cancel, oakrender_cancelatom_init}; - use crate::decoder::set_test_decoders; - use crate::error::{OAKCODEC_E_CANCELLED, OAKCODEC_E_INVALID, OAKCODEC_E_STATE}; - use crate::footagedescription::StreamEntry; - - /// The crate-wide ffi test lock (`crate::ffi::lock_tests`) serializes - /// every test in this module (they share the global probe error, the - /// injected decoder registry and the fake's state). - - /// Fake decoder driving the ffi export tests. - struct FakeDecoder { - retain_frame: bool, - retained: Mutex>>, - conform_fails: Mutex, - audio_conform_needed: Mutex, - } - - impl FakeDecoder { - fn new(retain_frame: bool) -> Arc { - Arc::new(FakeDecoder { - retain_frame, - retained: Mutex::new(None), - conform_fails: Mutex::new(false), - audio_conform_needed: Mutex::new(false), - }) - } - - fn with_audio_conform_needed() -> Arc { - Arc::new(FakeDecoder { - retain_frame: false, - retained: Mutex::new(None), - conform_fails: Mutex::new(false), - audio_conform_needed: Mutex::new(true), - }) - } - - fn with_conform_failing() -> Arc { - Arc::new(FakeDecoder { - retain_frame: false, - retained: Mutex::new(None), - conform_fails: Mutex::new(false), - audio_conform_needed: Mutex::new(false), - }) - } - } - - impl Decoder for FakeDecoder { - fn id(&self) -> String { - "fake".to_string() - } - - fn supports_video(&self) -> bool { - true - } - - fn supports_audio(&self) -> bool { - true - } - - fn probe( - &self, - filename: &str, - _cancelled: Option<&OakCancelAtom>, - ) -> Option { - if filename.ends_with("test_video.mp4") { - let mut desc = FootageDescription::new("fake"); - let vp = - unsafe { oakcommon_videoparams_init_with_time_base(1920, 1080, 1001, 30000, 0, 4, 1, 1, 0, 1) }; - unsafe { oakcommon_videoparams_set_stream_index(vp.clone(), 0) }; - desc.push_stream(StreamEntry::Video(vp)); - Some(desc) - } else if filename.ends_with("test_audio.mp4") { - let mut desc = FootageDescription::new("fake"); - let ap = unsafe { oakcore_audioparams_create(48000, 0x3, 10) }; - desc.push_stream(StreamEntry::Audio(OakAudioParams { - ctx: ap as *mut c_void, - addref: None, - release: None, - abi_version: crate::handle::OAKCODEC_ABI_VERSION, - })); - Some(desc) - } else { - None - } - } - - fn open(&self, _stream: &CodecStream) -> crate::error::Result<()> { - Ok(()) - } - - fn close(&self) -> crate::error::Result<()> { - Ok(()) - } - - fn stream(&self) -> CodecStream { - CodecStream::new() - } - - fn retrieve_video_frame( - &self, - _p: &RetrieveVideoParams, - ) -> crate::error::Result> { - let params = unsafe { oakcommon_videoparams_init_basic(100, 50, 0, 4, 1, 1, 0, 1) }; - let frame = Arc::new(Frame::with_params(params)); - if self.retain_frame { - // Keep an alias alive so the ffi decode sees a shared Arc. - *self.retained.lock().unwrap() = Some(frame.clone()); - } - Ok(frame) - } - - fn retrieve_video( - &self, - _p: &RetrieveVideoParams, - ) -> crate::error::Result { - Err(crate::error::Error::Failed("no texture".to_string())) - } - - fn retrieve_audio( - &self, - dest: &mut [f32], - _range: &TimeRange, - _sample_rate: i32, - _channel_layout: u64, - ) -> crate::error::Result { - if *self.audio_conform_needed.lock().unwrap() { - return Ok(RetrieveAudioStatus::ConformNeeded); - } - for s in dest.iter_mut() { - *s = 1.0; - } - Ok(RetrieveAudioStatus::Success) - } - - fn conform_audio( - &self, - _output_filenames: &[String], - _sample_rate: i32, - _channel_layout: u64, - _sample_format: i32, - _cancelled: Option<&OakCancelAtom>, - ) -> crate::error::Result<()> { - if *self.conform_fails.lock().unwrap() { - Err(crate::error::Error::Failed("conform failed".to_string())) - } else { - Ok(()) - } - } - } - - fn inject(fake: Arc) { - set_test_decoders(vec![fake]); - } - - fn restore() { - set_test_decoders(Vec::new()); - } - - fn cstr(s: &str) -> std::ffi::CString { - std::ffi::CString::new(s).unwrap() - } - - fn empty_cancel() -> OakCancelAtom { - OakCancelAtom { - ctx: std::ptr::null_mut(), - addref: None, - release: None, - abi_version: 0, - } - } - - fn media_file(name: &str) -> String { - let dir = - std::env::temp_dir().join(format!("oakcodec_ffi_dec_{}_{}", name, std::process::id())); - let _ = std::fs::create_dir_all(&dir); - let path = dir.join(name); - let _ = std::fs::write(&path, b"media"); - path.to_string_lossy().into_owned() - } - - #[test] - fn probe_golden_video() { - let _g = crate::ffi::lock_tests(); - inject(FakeDecoder::new(false)); - - let v = media_file("test_video.mp4"); - let f = cstr(&v); - let before = handle::alive_count(); - let mut h = unsafe { oakcodec_decoder_probe(f.as_ptr()) }; - assert!(!h.is_null()); - assert_eq!(handle::alive_count(), before + 1); - - let mut name = [0i8; 64]; - let rc = unsafe { oakcodec_decoder_probe_decoder_name(h, name.as_mut_ptr(), 64) }; - assert_eq!(rc, 5); // "fake" + NUL - assert_eq!(crate::ffi::c_str(name.as_ptr()).as_deref(), Some("fake")); - - assert_eq!(unsafe { oakcodec_decoder_probe_video_stream_count(h) }, 1); - assert_eq!(unsafe { oakcodec_decoder_probe_audio_stream_count(h) }, 0); - assert_eq!( - unsafe { oakcodec_decoder_probe_subtitle_stream_count(h) }, - 0 - ); - - let mut info: OakCodecVideoStreamInfo = unsafe { std::mem::zeroed() }; - let rc = unsafe { oakcodec_decoder_probe_get_video_stream(h, 0, &mut info) }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - assert_eq!(info.stream_index, 0); - assert_eq!(info.width, 1920); - assert_eq!(info.height, 1080); - assert_eq!(info.time_base_num, 1001); - assert_eq!(info.time_base_den, 30000); - assert_eq!(info.channel_count, 4); - assert_eq!(info.interlaced, 0); - // frame_rate/duration/colors default to 0 in the stub. - assert_eq!(info.frame_rate_num, 0); - assert_eq!(info.duration_ts, 0); - - // index out of range / null out. - let rc = unsafe { oakcodec_decoder_probe_get_video_stream(h, 3, &mut info) }; - assert_eq!(rc, crate::error::OAKCODEC_E_NOT_FOUND); - let rc = unsafe { oakcodec_decoder_probe_get_video_stream(h, 0, std::ptr::null_mut()) }; - assert_eq!(rc, OAKCODEC_E_INVALID); - - // probe_last_error is cleared on success. - let mut err = [0i8; 256]; - let rc = unsafe { oakcodec_probe_last_error(err.as_mut_ptr(), 256) }; - assert_eq!(rc, 1); // just the NUL - assert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some("")); - - unsafe { oakcodec_decoder_free(&mut h) }; - assert!(h.is_null()); - assert_eq!(handle::alive_count(), before); - restore(); - } - - #[test] - fn probe_golden_audio() { - let _g = crate::ffi::lock_tests(); - inject(FakeDecoder::new(false)); - - let v = media_file("test_audio.mp4"); - let f = cstr(&v); - let mut h = unsafe { oakcodec_decoder_probe(f.as_ptr()) }; - assert!(!h.is_null()); - assert_eq!(unsafe { oakcodec_decoder_probe_video_stream_count(h) }, 0); - assert_eq!(unsafe { oakcodec_decoder_probe_audio_stream_count(h) }, 1); - - let mut info: OakCodecAudioStreamInfo = unsafe { std::mem::zeroed() }; - let rc = unsafe { oakcodec_decoder_probe_get_audio_stream(h, 0, &mut info) }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - assert_eq!(info.stream_index, 0); - assert_eq!(info.sample_rate, 48000); - assert_eq!(info.channel_layout, 0x3); - assert_eq!(info.channel_count, 2); - assert_eq!(info.time_base_num, 1); - assert_eq!(info.time_base_den, 48000); - assert_eq!(info.duration_ts, 0); - - unsafe { oakcodec_decoder_free(&mut h) }; - restore(); - } - - #[test] - fn probe_failures() { - let _g = crate::ffi::lock_tests(); - inject(FakeDecoder::new(false)); - - // NULL filename. - let mut h = unsafe { oakcodec_decoder_probe(std::ptr::null()) }; - assert!(h.is_null()); - let mut err = [0i8; 256]; - unsafe { oakcodec_probe_last_error(err.as_mut_ptr(), 256) }; - assert_eq!( - crate::ffi::c_str(err.as_ptr()).as_deref(), - Some("no filename given") - ); - - // Empty filename. - let f = cstr(""); - let mut h = unsafe { oakcodec_decoder_probe(f.as_ptr()) }; - assert!(h.is_null()); - - // Nonexistent file. - let f = cstr("/definitely/not/here.mp4"); - let mut h = unsafe { oakcodec_decoder_probe(f.as_ptr()) }; - assert!(h.is_null()); - unsafe { oakcodec_probe_last_error(err.as_mut_ptr(), 256) }; - assert_eq!( - crate::ffi::c_str(err.as_ptr()).as_deref(), - Some("file not found: /definitely/not/here.mp4") - ); - - // A file no decoder recognizes (a real file: the test harness). - let me = std::env::current_exe().unwrap(); - let me = me.to_string_lossy().into_owned(); - let f = cstr(&me); - let mut h = unsafe { oakcodec_decoder_probe(f.as_ptr()) }; - assert!(h.is_null()); - unsafe { oakcodec_probe_last_error(err.as_mut_ptr(), 256) }; - assert!(crate::ffi::c_str(err.as_ptr()) - .as_deref() - .unwrap() - .contains("no decoder recognizes")); - - // Empty handle on probe exports. - let empty = CHandle::null(); - assert_eq!( - unsafe { oakcodec_decoder_probe_decoder_name(empty, err.as_mut_ptr(), 256) }, - OAKCODEC_E_INVALID - ); - assert_eq!( - unsafe { oakcodec_decoder_probe_video_stream_count(empty) }, - 0 - ); - restore(); - } - - #[test] - fn open_decode_video_golden() { - let _g = crate::ffi::lock_tests(); - inject(FakeDecoder::new(false)); - - let mut h = unsafe { oakcodec_decoder_init() }; - assert!(!h.is_null()); - assert_eq!(unsafe { oakcodec_decoder_is_open(h) }, 0); - - let v = media_file("test_video.mp4"); - let f = cstr(&v); - let rc = unsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - assert_eq!(unsafe { oakcodec_decoder_is_open(h) }, 1); - - // Reopening the same stream is a successful no-op. - let rc = unsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - - let mut frame = unsafe { oakcodec_decoder_decode_video(h, 1, 30) }; - assert!(!frame.is_null()); - assert_eq!( - unsafe { crate::ffi::frame::oakcodec_frame_width(frame) }, - 100 - ); - assert_eq!( - unsafe { crate::ffi::frame::oakcodec_frame_height(frame) }, - 50 - ); - - unsafe { crate::ffi::frame::oakcodec_frame_free(&mut frame) }; - let rc = unsafe { oakcodec_decoder_close(h) }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - assert_eq!(unsafe { oakcodec_decoder_is_open(h) }, 0); - unsafe { oakcodec_decoder_free(&mut h) }; - restore(); - } - - #[test] - fn decode_video_shared_frame_reports_error() { - let _g = crate::ffi::lock_tests(); - inject(FakeDecoder::new(true)); // decoder retains the frame - - let mut h = unsafe { oakcodec_decoder_init() }; - let v = media_file("test_video.mp4"); - let f = cstr(&v); - unsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) }; - let mut frame = unsafe { oakcodec_decoder_decode_video(h, 0, 1) }; - assert!(frame.is_null()); - let mut err = [0i8; 128]; - unsafe { oakcodec_decoder_last_error(h, err.as_mut_ptr(), 128) }; - assert_eq!( - crate::ffi::c_str(err.as_ptr()).as_deref(), - Some("failed to decode video frame") - ); - unsafe { oakcodec_decoder_free(&mut h) }; - restore(); - } - - #[test] - fn decode_audio_golden_and_errors() { - let _g = crate::ffi::lock_tests(); - inject(FakeDecoder::new(false)); - - let mut h = unsafe { oakcodec_decoder_init() }; - let v = media_file("test_audio.mp4"); - let f = cstr(&v); - let rc = unsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - - let mut buf = [0f32; 64]; - let frames = unsafe { - oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16) - }; - assert_eq!(frames, 16); - // Interleaved stereo filled by the fake. - assert_eq!(buf[0], 1.0); - assert_eq!(buf[31], 1.0); - - // Invalid args. - let rc = unsafe { - oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, std::ptr::null_mut(), 16) - }; - assert_eq!(rc, OAKCODEC_E_INVALID); - let rc = unsafe { - oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), -1) - }; - assert_eq!(rc, OAKCODEC_E_INVALID); - - // Not open -> E_STATE. - let mut h2 = unsafe { oakcodec_decoder_init() }; - let rc = unsafe { - oakcodec_decoder_decode_audio(h2, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16) - }; - assert_eq!(rc, OAKCODEC_E_STATE); - unsafe { oakcodec_decoder_free(&mut h2) }; - - // Empty handle -> E_INVALID. - let empty = CHandle::null(); - let rc = unsafe { - oakcodec_decoder_decode_audio(empty, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16) - }; - assert_eq!(rc, OAKCODEC_E_INVALID); - - unsafe { oakcodec_decoder_free(&mut h) }; - restore(); - } - - #[test] - fn decode_audio_conform_needed_is_state() { - let _g = crate::ffi::lock_tests(); - set_test_decoders(vec![FakeDecoder::with_audio_conform_needed()]); - - let mut h = unsafe { oakcodec_decoder_init() }; - let v = media_file("test_audio.mp4"); - let f = cstr(&v); - unsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) }; - let mut buf = [0f32; 64]; - let rc = unsafe { - oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16) - }; - assert_eq!(rc, OAKCODEC_E_STATE); - let mut err = [0i8; 256]; - unsafe { oakcodec_decoder_last_error(h, err.as_mut_ptr(), 256) }; - assert!(crate::ffi::c_str(err.as_ptr()) - .as_deref() - .unwrap() - .contains("conform")); - unsafe { oakcodec_decoder_free(&mut h) }; - restore(); - } - - #[test] - fn conform_audio_golden_cancelled_and_errors() { - let _g = crate::ffi::lock_tests(); - let fake = FakeDecoder::with_conform_failing(); - set_test_decoders(vec![fake.clone()]); - - let mut h = unsafe { oakcodec_decoder_init() }; - let v = media_file("test_audio.mp4"); - let f = cstr(&v); - unsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) }; - - let a = cstr("a.pcm"); - let b = cstr("b.pcm"); - let files = [a.as_ptr(), b.as_ptr()]; - let rc = unsafe { - oakcodec_decoder_conform_audio(h, files.as_ptr(), 2, 48000, 0x3, 10, empty_cancel()) - }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - - // Not open -> E_STATE. - let mut h2 = unsafe { oakcodec_decoder_init() }; - let rc = unsafe { - oakcodec_decoder_conform_audio(h2, files.as_ptr(), 2, 48000, 0x3, 10, empty_cancel()) - }; - assert_eq!(rc, OAKCODEC_E_STATE); - - // Invalid args. - let rc = unsafe { - oakcodec_decoder_conform_audio(h, std::ptr::null(), 1, 48000, 0x3, 10, empty_cancel()) - }; - assert_eq!(rc, OAKCODEC_E_INVALID); - let rc = unsafe { - oakcodec_decoder_conform_audio(h, files.as_ptr(), -1, 48000, 0x3, 10, empty_cancel()) - }; - assert_eq!(rc, OAKCODEC_E_INVALID); - - // Failure without cancellation -> E_FAILED. - *fake.conform_fails.lock().unwrap() = true; - let rc = unsafe { - oakcodec_decoder_conform_audio(h, files.as_ptr(), 2, 48000, 0x3, 10, empty_cancel()) - }; - assert_eq!(rc, crate::error::OAKCODEC_E_FAILED); - - // Failure with a cancelled atom -> E_CANCELLED. - let atom = unsafe { oakrender_cancelatom_init() }; - unsafe { oakrender_cancelatom_cancel(atom.clone()) }; - let rc = - unsafe { oakcodec_decoder_conform_audio(h, files.as_ptr(), 2, 48000, 0x3, 10, atom) }; - assert_eq!(rc, OAKCODEC_E_CANCELLED); - - unsafe { oakcodec_decoder_free(&mut h) }; - unsafe { oakcodec_decoder_free(&mut h2) }; - restore(); - } - - #[test] - fn image_sequence_exports() { - let _g = crate::ffi::lock_tests(); - - let f = cstr("frame_0001.png"); - assert_eq!( - unsafe { oakcodec_decoder_get_image_sequence_digit_count(f.as_ptr()) }, - 4 - ); - assert_eq!( - unsafe { oakcodec_decoder_get_image_sequence_index(f.as_ptr()) }, - 1 - ); - - let mut buf = [0i8; 128]; - let rc = unsafe { - oakcodec_decoder_transform_image_sequence_file_name( - f.as_ptr(), - 7, - buf.as_mut_ptr(), - 128, - ) - }; - assert!(rc > 0); - assert_eq!( - crate::ffi::c_str(buf.as_ptr()).as_deref(), - Some("frame_0007.png") - ); - - // NULL filename -> E_INVALID. - assert_eq!( - unsafe { oakcodec_decoder_get_image_sequence_digit_count(std::ptr::null()) }, - OAKCODEC_E_INVALID - ); - assert_eq!( - unsafe { oakcodec_decoder_get_image_sequence_index(std::ptr::null()) }, - OAKCODEC_E_INVALID as i64 - ); - assert_eq!( - unsafe { - oakcodec_decoder_transform_image_sequence_file_name( - std::ptr::null(), - 1, - buf.as_mut_ptr(), - 128, - ) - }, - OAKCODEC_E_INVALID - ); - } - - #[test] - fn open_errors_and_last_error_empty_handle() { - let _g = crate::ffi::lock_tests(); - inject(FakeDecoder::new(false)); - - let mut h = unsafe { oakcodec_decoder_init() }; - - // NULL filename -> E_INVALID. - let rc = unsafe { oakcodec_decoder_open(h, std::ptr::null(), 0) }; - assert_eq!(rc, OAKCODEC_E_INVALID); - - // Negative stream index -> E_INVALID. - let v = media_file("test_video.mp4"); - let f = cstr(&v); - let rc = unsafe { oakcodec_decoder_open(h, f.as_ptr(), -1) }; - assert_eq!(rc, OAKCODEC_E_INVALID); - - // Missing file -> E_NOT_FOUND. - let f = cstr("/missing/file.mp4"); - let rc = unsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) }; - assert_eq!(rc, crate::error::OAKCODEC_E_NOT_FOUND); - let mut err = [0i8; 256]; - unsafe { oakcodec_decoder_last_error(h, err.as_mut_ptr(), 256) }; - assert_eq!( - crate::ffi::c_str(err.as_ptr()).as_deref(), - Some("file not found: /missing/file.mp4") - ); - - // Empty handle -> E_INVALID and empty last_error. - let empty = CHandle::null(); - let rc = unsafe { oakcodec_decoder_open(empty, f.as_ptr(), 0) }; - assert_eq!(rc, OAKCODEC_E_INVALID); - let rc = unsafe { oakcodec_decoder_close(empty) }; - assert_eq!(rc, OAKCODEC_E_INVALID); - assert_eq!(unsafe { oakcodec_decoder_is_open(empty) }, 0); - let rc = unsafe { oakcodec_decoder_last_error(empty, err.as_mut_ptr(), 256) }; - assert_eq!(rc, 1); - assert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some("")); - - unsafe { oakcodec_decoder_free(&mut h) }; - restore(); - } -} diff --git a/crates/oakcodec/src/ffi/encoder.rs b/crates/oakcodec/src/ffi/encoder.rs deleted file mode 100644 index 428a0ecf3..000000000 --- a/crates/oakcodec/src/ffi/encoder.rs +++ /dev/null @@ -1,818 +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 . - -//! `include/codec/encoder.h` exports. -//! -//! Complete inventory: init / free / set_video_option / open / write_video -//! / write_audio / write_subtitle / flush / last_error / -//! get_desired_pixel_format / export_format_get_extension / -//! encoding_generate_matrix. -//! -//! # CPP-PARITY -//! The C++ `c_api/encoder.cpp` box holds an `olive::Encoder` plus the -//! flattened `EncodingParams`; the Rust equivalent boxes `Mutex` -//! and carries an extra `last_error` field because the Rust `Encoder` trait -//! has no `get_error()` (the C++ reads the message off the encoder). The -//! `oakcodec_encoding_params` POD below mirrors the header verbatim; note -//! that the crate's `EncodingParams::video_pixel_format` / `audio_sample_format` -//! are typed enums, so `to_native` converts the int fields. - -use std::ffi::{c_char, c_int}; -use std::sync::{Arc, Mutex}; - -use oakcore_rs::PixelFormat; - -use crate::encoder::Encoder; -use crate::encodingparams::EncodingParams; -use crate::handle::{self, CHandle}; - -/// `oakcodec_encoding_params` — flattened POD mirror of `include/codec/ -/// encoder.h` (all fields; a zeroed struct describes an all-tracks-disabled -/// configuration). Field names and order mirror the header verbatim. -#[allow(missing_docs)] -#[repr(C)] -pub struct oakcodec_encoding_params { - pub filename: [u8; 1024], - pub format: c_int, - pub video_enabled: c_int, - pub video_codec: c_int, - pub video_width: c_int, - pub video_height: c_int, - pub video_time_base_num: c_int, - pub video_time_base_den: c_int, - pub video_pixel_format: c_int, - pub video_interlacing: c_int, - pub video_pixel_aspect_num: c_int, - pub video_pixel_aspect_den: c_int, - pub video_bit_rate: i64, - pub video_min_bit_rate: i64, - pub video_max_bit_rate: i64, - pub video_buffer_size: i64, - pub video_threads: c_int, - pub video_pix_fmt: [u8; 64], - pub video_is_image_sequence: c_int, - pub video_scaling_method: c_int, - pub audio_enabled: c_int, - pub audio_codec: c_int, - pub audio_sample_rate: c_int, - pub audio_channel_layout: u64, - pub audio_sample_format: c_int, - pub audio_bit_rate: i64, - pub subtitles_enabled: c_int, - pub subtitles_codec: c_int, - pub subtitles_are_sidecar: c_int, - pub subtitles_sidecar_format: c_int, - pub color_transform_output: [u8; 256], - pub export_length_num: c_int, - pub export_length_den: c_int, - pub has_custom_range: c_int, - pub custom_range_in_num: i64, - pub custom_range_in_den: i64, - pub custom_range_out_num: i64, - pub custom_range_out_den: i64, -} - -/// Box behind an encoder handle (`EncoderBox` in `c_api/encoder.cpp`). -struct EncoderBox { - encoder: Option>, - params: EncodingParams, - /// Per-codec video options set via `oakcodec_encoder_set_video_option` - /// between init and open. Kept here (not in [`EncodingParams`], which - /// is a byte-exact mirror of the C POD). - video_opts: Vec<(String, String)>, - open: bool, - flushed: bool, - /// Last error detail (the C++ reads it off the encoder's `get_error`). - last_error: String, -} - -/// Convert an `OakPixelFormat` int code to a [`PixelFormat`]. -fn pixel_format_from_i32(v: c_int) -> PixelFormat { - match v { - 0 => PixelFormat::U8, - 1 => PixelFormat::U10, - 2 => PixelFormat::U16, - 3 => PixelFormat::F16, - 4 => PixelFormat::F32, - _ => PixelFormat::Invalid, - } -} - -/// Flatten the C POD into the crate's [`EncodingParams`] -/// (`to_native` in `c_api/encoder.cpp`). -fn to_native(p: &oakcodec_encoding_params) -> EncodingParams { - let mut n = EncodingParams::default(); - n.filename = p.filename; - n.format = p.format; - - n.video_enabled = p.video_enabled; - n.video_codec = p.video_codec; - n.video_width = p.video_width; - n.video_height = p.video_height; - n.video_time_base_num = p.video_time_base_num; - n.video_time_base_den = p.video_time_base_den; - n.video_pixel_format = pixel_format_from_i32(p.video_pixel_format); - n.video_interlacing = p.video_interlacing; - n.video_pixel_aspect_num = p.video_pixel_aspect_num; - n.video_pixel_aspect_den = p.video_pixel_aspect_den; - n.video_bit_rate = p.video_bit_rate; - n.video_min_bit_rate = p.video_min_bit_rate; - n.video_max_bit_rate = p.video_max_bit_rate; - n.video_buffer_size = p.video_buffer_size; - n.video_threads = p.video_threads; - n.video_pix_fmt = p.video_pix_fmt; - n.video_is_image_sequence = p.video_is_image_sequence; - n.video_scaling_method = crate::encodingparams::scaling_from_i32(p.video_scaling_method); - - n.audio_enabled = p.audio_enabled; - n.audio_codec = p.audio_codec; - n.audio_sample_rate = p.audio_sample_rate; - n.audio_channel_layout = p.audio_channel_layout; - n.audio_sample_format = crate::encodingparams::sample_format_from_i32(p.audio_sample_format); - n.audio_bit_rate = p.audio_bit_rate; - - n.subtitles_enabled = p.subtitles_enabled; - n.subtitles_codec = p.subtitles_codec; - n.subtitles_are_sidecar = p.subtitles_are_sidecar; - n.subtitles_sidecar_format = p.subtitles_sidecar_format; - - n.color_transform_output = p.color_transform_output; - n.export_length_num = p.export_length_num; - n.export_length_den = p.export_length_den; - n.has_custom_range = p.has_custom_range; - n.custom_range_in_num = p.custom_range_in_num; - n.custom_range_in_den = p.custom_range_in_den; - n.custom_range_out_num = p.custom_range_out_num; - n.custom_range_out_den = p.custom_range_out_den; - n -} - -/// `EncodingParams::is_valid` — the C++ `src/codec/src/encoder.h` checks -/// only that at least one track is enabled. -fn is_valid(p: &EncodingParams) -> bool { - p.video_enabled != 0 || p.audio_enabled != 0 || p.subtitles_enabled != 0 -} - -/// `oakcodec_encoder_init`: create an encoder for `params` (count 1); -/// empty handle when the configuration is invalid. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoder_init(params: *const oakcodec_encoding_params) -> CHandle { - handle::guard_handle(|| { - if params.is_null() { - return Ok(CHandle::null()); - } - let native = to_native(unsafe { &*params }); - if !is_valid(&native) { - return Ok(CHandle::null()); - } - Ok(handle::make_owned(Mutex::new(EncoderBox { - encoder: None, - params: native, - video_opts: Vec::new(), - open: false, - flushed: false, - last_error: String::new(), - }))) - }) -} - -/// `oakcodec_encoder_free`: NULL/empty no-op; nulls `ctx` afterwards. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoder_free(encoder: *mut CHandle) { - handle::guard_void(|| super::free_handle(encoder)); -} - -/// `oakcodec_encoder_set_video_option`: set a per-codec video option -/// (e.g. "crf"); only valid between init and open. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoder_set_video_option( - encoder: CHandle, - key: *const c_char, - value: *const c_char, -) -> c_int { - handle::guard(|| { - let b = - super::get_box::>(&encoder).ok_or(crate::error::Error::Invalid)?; - let key = match crate::ffi::c_str(key) { - Some(k) => k, - None => return Err(crate::error::Error::Invalid), - }; - let mut b = b.lock().unwrap(); - if b.open { - return Err(crate::error::Error::State); - } - let value = crate::ffi::c_str(value).unwrap_or_default(); - // `EncodingParams::set_video_option` replaces an existing key. - b.video_opts.retain(|(k, _)| k != &key); - b.video_opts.push((key, value)); - Ok(()) - }) -} - -/// `oakcodec_encoder_open`: create the encoder for the configured params, -/// apply the video options and open the output. -/// -/// # CPP-PARITY -/// The C++ `open()` calls `create_from_params` (which applies the options -/// internally) then `open()`. The Rust trait separates `configure`, so it -/// is invoked between the two; the box's `video_opts` (set between init -/// and open) are stored for future wiring but not yet passed to -/// `configure` (no trait channel carries them in the interim). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoder_open(encoder: CHandle) -> c_int { - handle::guard(|| { - let b = - super::get_box::>(&encoder).ok_or(crate::error::Error::Invalid)?; - let mut b = b.lock().unwrap(); - if b.open { - return Err(crate::error::Error::State); - } - let e = match crate::encoder::create_from_params(&b.params) { - Some(e) => e, - None => { - b.last_error = "failed to create encoder".to_string(); - return Err(crate::error::Error::Failed( - "failed to create encoder".to_string(), - )); - } - }; - if e.configure(&b.params).is_err() { - b.last_error = "failed to configure encoder".to_string(); - return Err(crate::error::Error::Failed( - "failed to configure encoder".to_string(), - )); - } - if e.open().is_err() { - b.last_error = "failed to open stream".to_string(); - return Err(crate::error::Error::Failed( - "failed to open stream".to_string(), - )); - } - b.encoder = Some(e); - b.open = true; - Ok(()) - }) -} - -/// `oakcodec_encoder_write_video`: encode one video frame. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoder_write_video(encoder: CHandle, frame: CHandle) -> c_int { - handle::guard(|| { - let b = - super::get_box::>(&encoder).ok_or(crate::error::Error::Invalid)?; - let f = super::get_box::>(&frame) - .ok_or(crate::error::Error::Invalid)?; - let e = { - let b = b.lock().unwrap(); - if !b.open || b.flushed || b.encoder.is_none() { - return Err(crate::error::Error::State); - } - b.encoder.as_ref().unwrap().clone() - }; - let f = f.lock().unwrap(); - e.write_video(&f) - .map_err(|_| crate::error::Error::Failed("write_video failed".to_string())) - }) -} - -/// `oakcodec_encoder_write_audio`: encode interleaved float audio samples. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoder_write_audio( - encoder: CHandle, - samples: *const f32, - frame_count: c_int, -) -> c_int { - handle::guard(|| { - let b = - super::get_box::>(&encoder).ok_or(crate::error::Error::Invalid)?; - if (samples.is_null() && frame_count > 0) || frame_count < 0 { - return Err(crate::error::Error::Invalid); - } - let (slice, enc) = { - let b = b.lock().unwrap(); - if !b.open || b.flushed || b.encoder.is_none() { - return Err(crate::error::Error::State); - } - let channels = b.params.audio_channel_layout.count_ones(); - if channels == 0 { - return Err(crate::error::Error::State); - } - let sample_count = (frame_count as usize).wrapping_mul(channels as usize); - let slice: &[f32] = if samples.is_null() { - &[] - } else { - // SAFETY: the caller guarantees `samples` holds - // `frame_count * channels` floats. - unsafe { std::slice::from_raw_parts(samples, sample_count) } - }; - let e = b.encoder.as_ref().unwrap().clone(); - (slice, e) - }; - enc.write_audio(slice, frame_count) - .map_err(|_| crate::error::Error::Failed("write_audio failed".to_string())) - }) -} - -/// `oakcodec_encoder_write_subtitle`: encode one subtitle entry. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoder_write_subtitle( - encoder: CHandle, - text: *const c_char, - in_seconds: f64, - out_seconds: f64, -) -> c_int { - handle::guard(|| { - let b = - super::get_box::>(&encoder).ok_or(crate::error::Error::Invalid)?; - let text = match crate::ffi::c_str(text) { - Some(t) => t, - None => return Err(crate::error::Error::Invalid), - }; - let e = { - let b = b.lock().unwrap(); - if !b.open || b.flushed || b.encoder.is_none() { - return Err(crate::error::Error::State); - } - b.encoder.as_ref().unwrap().clone() - }; - e.write_subtitle(&text, in_seconds, out_seconds) - .map_err(|_| crate::error::Error::Failed("write_subtitle failed".to_string())) - }) -} - -/// `oakcodec_encoder_flush`: flush the encoders, write the trailer and -/// close the file. Idempotent. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoder_flush(encoder: CHandle) -> c_int { - handle::guard(|| { - let b = - super::get_box::>(&encoder).ok_or(crate::error::Error::Invalid)?; - let mut b = b.lock().unwrap(); - if !b.open { - return Err(crate::error::Error::State); - } - if b.flushed { - return Ok(()); - } - let e = b.encoder.as_ref().unwrap().clone(); - // The C++ ignores the close() result; the Rust interim surfaces it. - e.close() - .map_err(|_| crate::error::Error::Failed("close failed".to_string()))?; - b.flushed = true; - Ok(()) - }) -} - -/// `oakcodec_encoder_last_error` (two-stage). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoder_last_error( - encoder: CHandle, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - handle::guard_raw(|| match super::get_box::>(&encoder) { - Some(b) => { - let b = b.lock().unwrap(); - super::string_out(&b.last_error, buf, buf_size) - } - None => super::string_out("", buf, buf_size), - }) -} - -/// `oakcodec_encoder_get_desired_pixel_format`: the pixel format the -/// encoder wants frames in, or -1 when unknown; `OAKCODEC_E_INVALID` for -/// an empty/invalid encoder. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoder_get_desired_pixel_format(encoder: CHandle) -> c_int { - handle::guard_raw(|| { - let b = match super::get_box::>(&encoder) { - Some(b) => b, - None => return crate::error::OAKCODEC_E_INVALID, - }; - let e = match &b.lock().unwrap().encoder { - Some(e) => e.clone(), - None => return crate::error::OAKCODEC_E_INVALID, - }; - match e.desired_pixel_format() { - Some(p) => p as c_int, - None => -1, - } - }) -} - -/// `oakcodec_export_format_get_extension` (two-stage); unknown formats -/// yield the empty string. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_export_format_get_extension( - format: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - handle::guard_raw(|| { - let ext = match crate::exportformat::Format::from_i32(format) { - Some(f) => crate::exportformat::Format::get_extension(f), - None => String::new(), - }; - super::string_out(&ext, buf, buf_size) - }) -} - -/// `oakcodec_encoding_generate_matrix`: scaling matrix for a scaling -/// method, row-major 4x4 `double` into `out_matrix[16]`. -#[no_mangle] -pub unsafe extern "C" 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 { - handle::guard(|| { - if out_matrix.is_null() { - return Err(crate::error::Error::Invalid); - } - let mut m = [0.0f64; 16]; - crate::encodingparams::EncodingParams::generate_matrix( - crate::encodingparams::scaling_from_i32(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) }; - Ok(()) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::bridge::common::oakcommon_videoparams_init_basic; - use crate::encoder::set_test_encoders; - use crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_STATE}; - use crate::ffi::frame::{ - oakcodec_frame_allocate, oakcodec_frame_free, oakcodec_frame_init_with_params, - }; - - fn cstr(s: &str) -> std::ffi::CString { - std::ffi::CString::new(s).unwrap() - } - - fn zeroed_params() -> oakcodec_encoding_params { - unsafe { std::mem::zeroed() } - } - - fn valid_params() -> oakcodec_encoding_params { - let mut p = zeroed_params(); - p.filename = { - let mut f = [0u8; 1024]; - let name = b"out.mp4"; - f[..name.len()].copy_from_slice(name); - f - }; - p.format = 2; // MPEG-4 - p.video_enabled = 1; - p.video_codec = 3; - p.video_width = 1920; - p.video_height = 1080; - p.video_time_base_num = 1; - p.video_time_base_den = 30; - p.video_pixel_format = 0; // U8 - p.audio_enabled = 1; - p.audio_codec = 4; - p.audio_sample_rate = 48000; - p.audio_channel_layout = 0x3; - p.audio_sample_format = 10; // f32 packed - p - } - - /// Fake encoder that accepts every operation. - struct FakeEncoder { - id: &'static str, - } - - impl Encoder for FakeEncoder { - fn id(&self) -> String { - self.id.to_string() - } - fn supports_video(&self) -> bool { - true - } - fn supports_audio(&self) -> bool { - true - } - fn supports_subtitles(&self) -> bool { - true - } - fn configure(&self, _p: &EncodingParams) -> crate::error::Result<()> { - Ok(()) - } - fn open(&self) -> crate::error::Result<()> { - Ok(()) - } - fn close(&self) -> crate::error::Result<()> { - Ok(()) - } - fn write_video(&self, _frame: &crate::frame::Frame) -> crate::error::Result<()> { - Ok(()) - } - fn write_audio(&self, _samples: &[f32], _frame_count: i32) -> crate::error::Result<()> { - Ok(()) - } - fn write_subtitle( - &self, - _text: &str, - _in_seconds: f64, - _out_seconds: f64, - ) -> crate::error::Result<()> { - Ok(()) - } - fn flush(&self) -> crate::error::Result<()> { - Ok(()) - } - fn desired_pixel_format(&self) -> Option { - Some(PixelFormat::U8) - } - fn desired_sample_format(&self) -> Option { - None - } - fn filename(&self) -> String { - "out.mp4".to_string() - } - } - - #[test] - fn init_open_write_flush_golden() { - let _g = crate::ffi::lock_tests(); - set_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: "fake" })]); - - let p = valid_params(); - let before = handle::alive_count(); - let mut h = unsafe { oakcodec_encoder_init(&p) }; - assert!(!h.is_null()); - assert_eq!(handle::alive_count(), before + 1); - - // set_video_option between init and open. - let key = cstr("crf"); - let val = cstr("18"); - let rc = unsafe { oakcodec_encoder_set_video_option(h, key.as_ptr(), val.as_ptr()) }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - - let rc = unsafe { oakcodec_encoder_open(h) }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - - // write_video with a real frame handle. - let params = unsafe { oakcommon_videoparams_init_basic(16, 16, 0, 4, 1, 1, 0, 1) }; - let mut fh = unsafe { oakcodec_frame_init_with_params(params) }; - assert_eq!( - unsafe { oakcodec_frame_allocate(fh) }, - crate::error::OAKCODEC_OK - ); - let rc = unsafe { oakcodec_encoder_write_video(h, fh) }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - - // write_audio: stereo interleaved floats. - let mut samples = [0f32; 64]; - let rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), 32) }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - - // write_subtitle. - let text = cstr("hello"); - let rc = unsafe { oakcodec_encoder_write_subtitle(h, text.as_ptr(), 0.0, 2.5) }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - - // desired pixel format from the fake. - assert_eq!(unsafe { oakcodec_encoder_get_desired_pixel_format(h) }, 0); // U8 - - // flush is idempotent. - let rc = unsafe { oakcodec_encoder_flush(h) }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - let rc = unsafe { oakcodec_encoder_flush(h) }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - - // Writes after flush -> E_STATE. - let rc = unsafe { oakcodec_encoder_write_video(h, fh) }; - assert_eq!(rc, OAKCODEC_E_STATE); - let rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), 32) }; - assert_eq!(rc, OAKCODEC_E_STATE); - let rc = unsafe { oakcodec_encoder_write_subtitle(h, text.as_ptr(), 0.0, 1.0) }; - assert_eq!(rc, OAKCODEC_E_STATE); - - unsafe { oakcodec_frame_free(&mut fh) }; - unsafe { oakcodec_encoder_free(&mut h) }; - assert!(h.is_null()); - assert_eq!(handle::alive_count(), before); - set_test_encoders(Vec::new()); - } - - #[test] - fn init_invalid_config_and_null_params() { - let _g = crate::ffi::lock_tests(); - set_test_encoders(Vec::new()); - - // NULL params -> empty handle. - let mut h = unsafe { oakcodec_encoder_init(std::ptr::null()) }; - assert!(h.is_null()); - - // All tracks disabled -> empty handle (is_valid). - let p = zeroed_params(); - let mut h = unsafe { oakcodec_encoder_init(&p) }; - assert!(h.is_null()); - - // Audio-only config is valid. - let mut p = zeroed_params(); - p.format = 7; // WAV - p.audio_enabled = 1; - p.audio_sample_rate = 44100; - p.audio_channel_layout = 0x4; - p.audio_sample_format = 10; - let before = handle::alive_count(); - let mut h = unsafe { oakcodec_encoder_init(&p) }; - assert!(!h.is_null()); - assert_eq!(handle::alive_count(), before + 1); - unsafe { oakcodec_encoder_free(&mut h) }; - assert_eq!(handle::alive_count(), before); - } - - #[test] - fn open_errors_and_state_machine() { - let _g = crate::ffi::lock_tests(); - // Production path: an unknown export format cannot create an encoder. - set_test_encoders(Vec::new()); - - let mut p = valid_params(); - p.format = 99; // unknown format -> create_from_params returns None - let mut h = unsafe { oakcodec_encoder_init(&p) }; - assert!(!h.is_null()); - - let rc = unsafe { oakcodec_encoder_open(h) }; - assert_eq!(rc, crate::error::OAKCODEC_E_FAILED); - let mut err = [0i8; 128]; - unsafe { oakcodec_encoder_last_error(h, err.as_mut_ptr(), 128) }; - assert_eq!( - crate::ffi::c_str(err.as_ptr()).as_deref(), - Some("failed to create encoder") - ); - - // Empty handle -> E_INVALID; last_error empty. - let empty = CHandle::null(); - assert_eq!(unsafe { oakcodec_encoder_open(empty) }, OAKCODEC_E_INVALID); - assert_eq!( - unsafe { - oakcodec_encoder_set_video_option(empty, cstr("crf").as_ptr(), cstr("18").as_ptr()) - }, - OAKCODEC_E_INVALID - ); - assert_eq!( - unsafe { oakcodec_encoder_get_desired_pixel_format(empty) }, - OAKCODEC_E_INVALID - ); - let rc = unsafe { oakcodec_encoder_last_error(empty, err.as_mut_ptr(), 128) }; - assert_eq!(rc, 1); - assert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some("")); - - unsafe { oakcodec_encoder_free(&mut h) }; - } - - #[test] - fn state_and_argument_errors_with_fake() { - let _g = crate::ffi::lock_tests(); - set_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: "fake" })]); - - let p = valid_params(); - let mut h = unsafe { oakcodec_encoder_init(&p) }; - assert!(!h.is_null()); - - // set_video_option with a NULL key -> E_INVALID. - let rc = - unsafe { oakcodec_encoder_set_video_option(h, std::ptr::null(), std::ptr::null()) }; - assert_eq!(rc, OAKCODEC_E_INVALID); - - // Writes before open -> E_STATE. - let params = unsafe { oakcommon_videoparams_init_basic(4, 4, 0, 4, 1, 1, 0, 1) }; - let mut fh = unsafe { oakcodec_frame_init_with_params(params) }; - let rc = unsafe { oakcodec_encoder_write_video(h, fh) }; - assert_eq!(rc, OAKCODEC_E_STATE); - let rc = unsafe { oakcodec_encoder_flush(h) }; - assert_eq!(rc, OAKCODEC_E_STATE); - // Null samples with a positive count is an argument error (checked - // before the state, matching the C++ validation order). - let rc = unsafe { oakcodec_encoder_write_audio(h, std::ptr::null(), 8) }; - assert_eq!(rc, OAKCODEC_E_INVALID); - // Valid args before open -> E_STATE. - let mut pre = [0f32; 8]; - let rc = unsafe { oakcodec_encoder_write_audio(h, pre.as_ptr(), 4) }; - assert_eq!(rc, OAKCODEC_E_STATE); - - unsafe { oakcodec_frame_free(&mut fh) }; - unsafe { oakcodec_encoder_free(&mut h) }; - set_test_encoders(Vec::new()); - } - - #[test] - fn write_audio_argument_validation() { - let _g = crate::ffi::lock_tests(); - set_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: "fake" })]); - - let p = valid_params(); - let mut h = unsafe { oakcodec_encoder_init(&p) }; - assert_eq!( - unsafe { oakcodec_encoder_open(h) }, - crate::error::OAKCODEC_OK - ); - - // NULL samples with a positive frame count -> E_INVALID. - let rc = unsafe { oakcodec_encoder_write_audio(h, std::ptr::null(), 8) }; - assert_eq!(rc, OAKCODEC_E_INVALID); - - // Negative frame count -> E_INVALID. - let samples = [0f32; 8]; - let rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), -1) }; - assert_eq!(rc, OAKCODEC_E_INVALID); - - unsafe { oakcodec_encoder_free(&mut h) }; - set_test_encoders(Vec::new()); - } - - #[test] - fn write_audio_zero_channels_is_state() { - let _g = crate::ffi::lock_tests(); - set_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: "fake" })]); - - // Audio enabled but empty channel layout -> E_STATE at write time. - let mut p = zeroed_params(); - p.format = 7; - p.audio_enabled = 1; - p.audio_sample_rate = 44100; - p.audio_channel_layout = 0; - p.audio_sample_format = 10; - let mut h = unsafe { oakcodec_encoder_init(&p) }; - assert!(!h.is_null()); - assert_eq!( - unsafe { oakcodec_encoder_open(h) }, - crate::error::OAKCODEC_OK - ); - let mut samples = [0f32; 8]; - let rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), 4) }; - assert_eq!(rc, OAKCODEC_E_STATE); - - unsafe { oakcodec_encoder_free(&mut h) }; - set_test_encoders(Vec::new()); - } - - #[test] - fn export_format_extension_and_generate_matrix() { - let mut buf = [0i8; 64]; - let rc = unsafe { oakcodec_export_format_get_extension(2, buf.as_mut_ptr(), 64) }; - assert_eq!(rc, 4); // "mp4" + NUL - assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("mp4")); - - // Unknown format -> empty string (size 1 for the NUL). - let rc = unsafe { oakcodec_export_format_get_extension(99, buf.as_mut_ptr(), 64) }; - assert_eq!(rc, 1); - assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("")); - - // Truncation rule: small buffer writes buf_size-1 chars + NUL. - let rc = unsafe { oakcodec_export_format_get_extension(2, buf.as_mut_ptr(), 3) }; - assert_eq!(rc, 4); // required size unchanged - assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("mp")); - - // generate_matrix: Stretch (1) is the identity. - let mut m = [9.0f64; 16]; - let rc = - unsafe { oakcodec_encoding_generate_matrix(1, 1920, 1080, 1280, 720, m.as_mut_ptr()) }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - assert_eq!(m[0], 1.0); - assert_eq!(m[5], 1.0); - assert_eq!(m[10], 1.0); - assert_eq!(m[15], 1.0); - - // Fit (0) with a square source into a 2:1 destination scales x. - let mut m = [0.0f64; 16]; - let rc = - unsafe { oakcodec_encoding_generate_matrix(0, 1000, 1000, 2000, 1000, m.as_mut_ptr()) }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - assert!((m[0] - 0.5).abs() < 1e-9); - assert_eq!(m[5], 1.0); - - // NULL out_matrix -> E_INVALID. - let rc = unsafe { oakcodec_encoding_generate_matrix(0, 1, 1, 2, 2, std::ptr::null_mut()) }; - assert_eq!(rc, OAKCODEC_E_INVALID); - } -} diff --git a/crates/oakcodec/src/ffi/format.rs b/crates/oakcodec/src/ffi/format.rs deleted file mode 100644 index 48922c6de..000000000 --- a/crates/oakcodec/src/ffi/format.rs +++ /dev/null @@ -1,573 +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 . - -//! `include/codec/format.h` exports. -//! -//! Complete inventory: format_count / format_name / format_extension / -//! format_{video,audio,subtitle}_codec_{count,at} / codec_name / -//! codec_is_still_image / codec_is_lossless / pix_fmt_{count,at,index} / -//! sample_format_{count,at} / filename_contains_digit_placeholder / -//! image_sequence_digit_count / filename_remove_digit_placeholder. -//! -//! # CPP-PARITY -//! The C++ `c_api/format.cpp` mirrors the facade (oakengine/encoding.h) -//! against the `olive::ExportFormat` / `olive::ExportCodec` / `olive::Encoder` -//! statics. The Rust tables live in [`crate::exportformat`] / -//! [`crate::exportcodec`]; the encoder pixel-format query is bridge-dependent -//! on the C++ side and returns empty here (see -//! `Format::get_pixel_formats_for_codec`), so `oakcodec_encoding_pix_fmt_*` -//! report 0/`E_NOT_FOUND`/0 (the preferred-format fallback) like the C++ -//! base `Encoder` default. The filename helpers mirror the `Encoder` -//! statics in [`crate::encoder`]. - -use std::ffi::{c_char, c_int}; - -use crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_NOT_FOUND}; -use crate::exportcodec::Codec; -use crate::exportformat::Format; -use crate::handle; - -/// `oakcodec_encoding_format_count` — `ExportFormat::k_format_count`. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoding_format_count() -> c_int { - handle::guard_raw(|| Format::Count as c_int) -} - -/// `oakcodec_encoding_format_name` (two-stage). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoding_format_name( - format: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - handle::guard_raw(|| match Format::from_i32(format) { - Some(f) => super::string_out(&Format::get_name(f), buf, buf_size), - None => OAKCODEC_E_INVALID, - }) -} - -/// `oakcodec_encoding_format_extension` (two-stage). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoding_format_extension( - format: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - handle::guard_raw(|| match Format::from_i32(format) { - Some(f) => super::string_out(&Format::get_extension(f), buf, buf_size), - None => OAKCODEC_E_INVALID, - }) -} - -/// `oakcodec_encoding_format_video_codec_count`. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoding_format_video_codec_count(format: c_int) -> c_int { - handle::guard_raw(|| 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`. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoding_format_video_codec_at( - format: c_int, - index: c_int, -) -> c_int { - handle::guard_raw(|| { - 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`. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoding_format_audio_codec_count(format: c_int) -> c_int { - handle::guard_raw(|| 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`. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoding_format_audio_codec_at( - format: c_int, - index: c_int, -) -> c_int { - handle::guard_raw(|| { - 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`. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoding_format_subtitle_codec_count(format: c_int) -> c_int { - handle::guard_raw(|| 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`. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoding_format_subtitle_codec_at( - format: c_int, - index: c_int, -) -> c_int { - handle::guard_raw(|| { - 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). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoding_codec_name( - codec: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - handle::guard_raw(|| match Codec::from_i32(codec) { - Some(c) => super::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). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoding_codec_is_still_image(codec: c_int) -> c_int { - handle::guard_raw(|| 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). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoding_codec_is_lossless(codec: c_int) -> c_int { - handle::guard_raw(|| 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 C++ side instantiates the format's encoder and asks it for the codec's -/// pixel formats; 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 and the -/// C++ result for encoder-less codecs. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoding_pix_fmt_count(format: c_int, codec: c_int) -> c_int { - handle::guard_raw(|| { - 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). -#[no_mangle] -pub unsafe extern "C" 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 { - handle::guard_raw(|| { - 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). - super::string_out(&list[index as usize].to_string(), buf, buf_size) - }) -} - -/// `oakcodec_encoding_pix_fmt_index` — index of `pix_fmt` in `codec`'s -/// supported pixel formats; 0 (the preferred format) for an invalid codec, -/// a NULL/empty `pix_fmt`, or when not found. -/// -/// # CPP-PARITY -/// The C++ side searches the FFmpeg encoder's list. The Rust table is empty, -/// so every lookup falls back to 0 — the documented behavior for "absent". -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoding_pix_fmt_index( - codec: c_int, - pix_fmt: *const c_char, -) -> c_int { - handle::guard_raw(|| { - if Codec::from_i32(codec).is_none() { - return 0; - } - match crate::ffi::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`. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoding_sample_format_count( - format: c_int, - codec: c_int, -) -> c_int { - handle::guard_raw(|| { - 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. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoding_sample_format_at( - format: c_int, - codec: c_int, - index: c_int, -) -> c_int { - handle::guard_raw(|| { - 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). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoding_filename_contains_digit_placeholder( - filename: *const c_char, -) -> c_int { - handle::guard_raw(|| match crate::ffi::c_str(filename) { - Some(f) => crate::encoder::filename_contains_digit_placeholder(&f) as c_int, - None => 0, - }) -} - -/// `oakcodec_encoding_image_sequence_digit_count` (0 for NULL). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoding_image_sequence_digit_count( - filename: *const c_char, -) -> c_int { - handle::guard_raw(|| match crate::ffi::c_str(filename) { - Some(f) => crate::encoder::image_sequence_placeholder_digit_count(&f), - None => 0, - }) -} - -/// `oakcodec_encoding_filename_remove_digit_placeholder` (two-stage). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoding_filename_remove_digit_placeholder( - filename: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - handle::guard_raw(|| match crate::ffi::c_str(filename) { - Some(f) => super::string_out( - &crate::encoder::filename_remove_digit_placeholder(&f), - buf, - buf_size, - ), - None => OAKCODEC_E_INVALID, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn cstr(s: &str) -> std::ffi::CString { - std::ffi::CString::new(s).unwrap() - } - - #[test] - fn format_metadata_exports() { - let _g = crate::ffi::lock_tests(); - let mut buf = [0i8; 64]; - - // Count matches the 15-entry table (0..=14, Count = 15). - assert_eq!(unsafe { oakcodec_encoding_format_count() }, 15); - - // Matroska (1): "Matroska Video" / "mkv". - let rc = unsafe { oakcodec_encoding_format_name(1, buf.as_mut_ptr(), 64) }; - assert_eq!(rc, 15); // "Matroska Video" (14) + NUL - assert_eq!( - crate::ffi::c_str(buf.as_ptr()).as_deref(), - Some("Matroska Video") - ); - let rc = unsafe { oakcodec_encoding_format_extension(1, buf.as_mut_ptr(), 64) }; - assert_eq!(rc, 4); // "mkv" + NUL - assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("mkv")); - - // Truncation rule on a two-stage getter. - let rc = unsafe { oakcodec_encoding_format_name(1, buf.as_mut_ptr(), 4) }; - assert_eq!(rc, 15); // required size unchanged - assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("Mat")); - - // Invalid format -> E_INVALID. - assert_eq!( - unsafe { oakcodec_encoding_format_name(-1, buf.as_mut_ptr(), 64) }, - OAKCODEC_E_INVALID - ); - assert_eq!( - unsafe { oakcodec_encoding_format_extension(15, buf.as_mut_ptr(), 64) }, - OAKCODEC_E_INVALID - ); // Count is not a real format - assert_eq!( - unsafe { oakcodec_encoding_format_extension(99, buf.as_mut_ptr(), 64) }, - OAKCODEC_E_INVALID - ); - } - - #[test] - fn format_codec_lists_exports() { - let _g = crate::ffi::lock_tests(); - // MPEG-4 video (2) carries H.264/H.264RGB/H.265. - assert_eq!(unsafe { oakcodec_encoding_format_video_codec_count(2) }, 3); - assert_eq!( - unsafe { oakcodec_encoding_format_video_codec_at(2, 0) }, - 1 // H.264 - ); - // WAV (7) has no video codecs but PCM (13) audio. - assert_eq!(unsafe { oakcodec_encoding_format_video_codec_count(7) }, 0); - assert_eq!(unsafe { oakcodec_encoding_format_audio_codec_count(7) }, 1); - assert_eq!( - unsafe { oakcodec_encoding_format_audio_codec_at(7, 0) }, - 13 // PCM - ); - // SRT (13): subtitle-only, with the SRT (17) codec. - assert_eq!(unsafe { oakcodec_encoding_format_audio_codec_count(13) }, 0); - assert_eq!( - unsafe { oakcodec_encoding_format_subtitle_codec_count(13) }, - 1 - ); - assert_eq!( - unsafe { oakcodec_encoding_format_subtitle_codec_at(13, 0) }, - 17 // SRT - ); - - // Failure paths. - assert_eq!( - unsafe { oakcodec_encoding_format_video_codec_count(-1) }, - OAKCODEC_E_INVALID - ); - assert_eq!( - unsafe { oakcodec_encoding_format_video_codec_at(2, -1) }, - OAKCODEC_E_NOT_FOUND - ); - assert_eq!( - unsafe { oakcodec_encoding_format_video_codec_at(2, 3) }, - OAKCODEC_E_NOT_FOUND - ); - assert_eq!( - unsafe { oakcodec_encoding_format_audio_codec_at(7, 1) }, - OAKCODEC_E_NOT_FOUND - ); - assert_eq!( - unsafe { oakcodec_encoding_format_subtitle_codec_at(13, 1) }, - OAKCODEC_E_NOT_FOUND - ); - } - - #[test] - fn codec_metadata_exports() { - let _g = crate::ffi::lock_tests(); - let mut buf = [0i8; 64]; - - let rc = unsafe { oakcodec_encoding_codec_name(1, buf.as_mut_ptr(), 64) }; - assert_eq!(rc, 6); // "H.264" (5) + NUL - assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("H.264")); - assert_eq!( - unsafe { oakcodec_encoding_codec_name(-1, buf.as_mut_ptr(), 64) }, - OAKCODEC_E_INVALID - ); - - // Still images: PNG (5) yes, H.264 (1) no. - assert_eq!(unsafe { oakcodec_encoding_codec_is_still_image(5) }, 1); - assert_eq!(unsafe { oakcodec_encoding_codec_is_still_image(1) }, 0); - // Lossless: PCM (13) yes, AAC (12) no. - assert_eq!(unsafe { oakcodec_encoding_codec_is_lossless(13) }, 1); - assert_eq!(unsafe { oakcodec_encoding_codec_is_lossless(12) }, 0); - // Invalid codec -> 0 (not an error) for both flags. - assert_eq!(unsafe { oakcodec_encoding_codec_is_still_image(99) }, 0); - assert_eq!(unsafe { oakcodec_encoding_codec_is_lossless(99) }, 0); - } - - #[test] - fn pixel_and_sample_format_exports() { - let _g = crate::ffi::lock_tests(); - let mut buf = [0i8; 64]; - - // Bad arguments -> E_INVALID. - assert_eq!( - unsafe { oakcodec_encoding_pix_fmt_count(-1, 1) }, - OAKCODEC_E_INVALID - ); - assert_eq!( - unsafe { oakcodec_encoding_pix_fmt_count(2, 99) }, - OAKCODEC_E_INVALID - ); - // Interim: the Rust pixel-format table is empty, so the count is 0 - // and any index is E_NOT_FOUND (see the module doc). - assert_eq!(unsafe { oakcodec_encoding_pix_fmt_count(2, 1) }, 0); - assert_eq!( - unsafe { oakcodec_encoding_pix_fmt_at(-1, 1, 0, buf.as_mut_ptr(), 64) }, - OAKCODEC_E_INVALID - ); - assert_eq!( - unsafe { oakcodec_encoding_pix_fmt_at(2, 1, 0, buf.as_mut_ptr(), 64) }, - OAKCODEC_E_NOT_FOUND - ); - // pix_fmt_index: absent/empty/NULL/invalid codec all yield 0. - assert_eq!( - unsafe { oakcodec_encoding_pix_fmt_index(1, cstr("yuv420p").as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakcodec_encoding_pix_fmt_index(1, std::ptr::null()) }, - 0 - ); - assert_eq!( - unsafe { oakcodec_encoding_pix_fmt_index(99, cstr("yuv420p").as_ptr()) }, - 0 - ); - - // PCM (13) in WAV (7) exposes its native sample formats. - assert_eq!(unsafe { oakcodec_encoding_sample_format_count(7, 13) }, 6); - // f32 packed = 10 (oakcore SampleFormat values match the C++). - assert_eq!(unsafe { oakcodec_encoding_sample_format_at(7, 13, 4) }, 10); - // Out-of-range index -> E_NOT_FOUND; bad args -> E_INVALID. - assert_eq!( - unsafe { oakcodec_encoding_sample_format_at(7, 13, 6) }, - OAKCODEC_E_NOT_FOUND - ); - assert_eq!( - unsafe { oakcodec_encoding_sample_format_at(-1, 13, 0) }, - OAKCODEC_E_INVALID - ); - // Non-PCM codecs query the bridge on the C++ side; empty here. - assert_eq!(unsafe { oakcodec_encoding_sample_format_count(2, 12) }, 0); - } - - #[test] - fn filename_helper_exports() { - let _g = crate::ffi::lock_tests(); - let mut buf = [0i8; 128]; - - assert_eq!( - unsafe { - oakcodec_encoding_filename_contains_digit_placeholder( - cstr("/tmp/out_[#####].png").as_ptr(), - ) - }, - 1 - ); - assert_eq!( - unsafe { - oakcodec_encoding_filename_contains_digit_placeholder(cstr("/tmp/out.png").as_ptr()) - }, - 0 - ); - assert_eq!( - unsafe { oakcodec_encoding_filename_contains_digit_placeholder(std::ptr::null()) }, - 0 - ); - - assert_eq!( - unsafe { - oakcodec_encoding_image_sequence_digit_count(cstr("/tmp/out_[#####].png").as_ptr()) - }, - 5 - ); - assert_eq!( - unsafe { oakcodec_encoding_image_sequence_digit_count(cstr("/tmp/out.png").as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakcodec_encoding_image_sequence_digit_count(std::ptr::null()) }, - 0 - ); - - let rc = unsafe { - oakcodec_encoding_filename_remove_digit_placeholder( - cstr("/tmp/out_[#####].png").as_ptr(), - buf.as_mut_ptr(), - 128, - ) - }; - assert_eq!(rc, 13); // "/tmp/out.png" (12) + NUL - assert_eq!( - crate::ffi::c_str(buf.as_ptr()).as_deref(), - Some("/tmp/out.png") - ); - assert_eq!( - unsafe { - oakcodec_encoding_filename_remove_digit_placeholder( - std::ptr::null(), - buf.as_mut_ptr(), - 128, - ) - }, - OAKCODEC_E_INVALID - ); - } -} diff --git a/crates/oakcodec/src/ffi/frame.rs b/crates/oakcodec/src/ffi/frame.rs deleted file mode 100644 index 9a2ab58bd..000000000 --- a/crates/oakcodec/src/ffi/frame.rs +++ /dev/null @@ -1,476 +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 . - -//! `include/codec/frame.h` exports. -//! -//! Complete inventory: frame_init / init_with_params / free / get_params / -//! set_params / allocate / is_allocated / data / const_data / -//! allocated_size / linesize_bytes / linesize_pixels / width / height / -//! format / channel_count / get_timestamp / set_timestamp / -//! debug_alive_count. -//! -//! # CPP-PARITY -//! The C++ `c_api/frame.cpp` boxes every `OakFrame` handle with an -//! `olive::FramePtr` (a shared pointer), so decoder-produced frames may -//! alias the decoder's internal cache. The Rust equivalent boxes -//! `Mutex`; a decode that hands out a still-shared `Arc` -//! therefore cannot be aliased here and reports an empty handle instead -//! (see `ffi::decoder`). - -use std::ffi::{c_int, c_void}; -use std::panic::{catch_unwind, AssertUnwindSafe}; -use std::sync::Mutex; - -use oakcore_rs::Rational; - -use crate::bridge::common::OakVideoParams; -use crate::frame::Frame; -use crate::handle::{self, CHandle}; - -/// `OAKCOMMON_PIXEL_FORMAT_INVALID` (oakcommon `common/videoparams.h`). -const OAKCOMMON_PIXEL_FORMAT_INVALID: c_int = -1; - -/// `oakcodec_frame_init`: new frame with default (invalid) params, -/// refcount 1. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_frame_init() -> CHandle { - handle::guard_handle(|| Ok(handle::make_owned(Mutex::new(Frame::new())))) -} - -/// `oakcodec_frame_init_with_params`: new frame holding a copy of `params` -/// (the handle is addref'd internally); buffer unallocated. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_frame_init_with_params(params: OakVideoParams) -> CHandle { - handle::guard_handle(|| Ok(handle::make_owned(Mutex::new(Frame::with_params(params))))) -} - -/// `oakcodec_frame_free`: NULL/empty no-op; nulls `ctx` afterwards. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_frame_free(frame: *mut CHandle) { - handle::guard_void(|| super::free_handle(frame)); -} - -/// `oakcodec_frame_get_params`: copy of the frame's parameter set. -/// -/// The copy is addref'd: the caller must release it with -/// `oakcommon_videoparams_free` (see the header contract). Test-stub -/// handles carry no `addref`, so the caller must not free them. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_frame_get_params( - frame: CHandle, - out: *mut OakVideoParams, -) -> c_int { - handle::guard(|| { - if out.is_null() { - return Err(crate::error::Error::Invalid); - } - let f = super::get_box::>(&frame).ok_or(crate::error::Error::Invalid)?; - let f = f.lock().unwrap(); - let p = f.params().cloned().ok_or(crate::error::Error::Invalid)?; - crate::frame::params_addref(&p); - // SAFETY: the caller guarantees `out` points to a writable - // `OakVideoParams`. - unsafe { *out = p }; - Ok(()) - }) -} - -/// `oakcodec_frame_set_params`: replace the parameter set (addref'd -/// internally); recomputes line sizes, does not reallocate. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_frame_set_params( - frame: CHandle, - params: OakVideoParams, -) -> c_int { - handle::guard(|| { - let f = super::get_box::>(&frame).ok_or(crate::error::Error::Invalid)?; - f.lock().unwrap().set_params(params); - Ok(()) - }) -} - -/// `oakcodec_frame_allocate`: allocate the pixel buffer from the current -/// params; `OAKCODEC_E_STATE` when the params are invalid. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_frame_allocate(frame: CHandle) -> c_int { - handle::guard(|| { - let f = super::get_box::>(&frame).ok_or(crate::error::Error::Invalid)?; - f.lock().unwrap().allocate() - }) -} - -/// `oakcodec_frame_is_allocated`: 1 when the buffer is allocated. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_frame_is_allocated(frame: CHandle) -> c_int { - handle::guard_raw(|| { - let f = match super::get_box::>(&frame) { - Some(f) => f, - None => return 0, - }; - let f = f.lock().unwrap(); - if f.is_allocated() { - 1 - } else { - 0 - } - }) -} - -/// `oakcodec_frame_data`: writable pixel buffer, NULL when -/// unallocated/empty. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_frame_data(frame: CHandle) -> *mut c_void { - match catch_unwind(AssertUnwindSafe(|| unsafe { frame_data_inner(&frame) })) { - Ok(p) => p, - Err(_) => std::ptr::null_mut(), - } -} - -/// `oakcodec_frame_const_data`: const variant of `oakcodec_frame_data`. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_frame_const_data(frame: CHandle) -> *const c_void { - match catch_unwind(AssertUnwindSafe(|| unsafe { - frame_const_data_inner(&frame) - })) { - Ok(p) => p, - Err(_) => std::ptr::null_mut(), - } -} - -unsafe fn frame_data_inner(frame: &CHandle) -> *mut c_void { - let f = match super::get_box::>(frame) { - Some(f) => f, - None => return std::ptr::null_mut(), - }; - match f.lock().unwrap().data_mut() { - Some(d) => d.as_mut_ptr() as *mut c_void, - None => std::ptr::null_mut(), - } -} - -unsafe fn frame_const_data_inner(frame: &CHandle) -> *const c_void { - let f = match super::get_box::>(frame) { - Some(f) => f, - None => return std::ptr::null(), - }; - match f.lock().unwrap().data() { - Some(d) => d.as_ptr() as *const c_void, - None => std::ptr::null(), - } -} - -/// `oakcodec_frame_allocated_size`: size of the pixel buffer in bytes -/// (0 when unallocated). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_frame_allocated_size(frame: CHandle) -> c_int { - handle::guard_raw(|| { - let f = match super::get_box::>(&frame) { - Some(f) => f, - None => return 0, - }; - f.lock().unwrap().allocated_size() as c_int - }) -} - -/// `oakcodec_frame_linesize_bytes`: distance between two rows in bytes -/// (0 when params are unset). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_frame_linesize_bytes(frame: CHandle) -> c_int { - handle::guard_raw(|| { - let f = match super::get_box::>(&frame) { - Some(f) => f, - None => return 0, - }; - f.lock().unwrap().linesize_bytes() - }) -} - -/// `oakcodec_frame_linesize_pixels`: distance between two rows in pixels. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_frame_linesize_pixels(frame: CHandle) -> c_int { - handle::guard_raw(|| { - let f = match super::get_box::>(&frame) { - Some(f) => f, - None => return 0, - }; - f.lock().unwrap().linesize_pixels() - }) -} - -/// `oakcodec_frame_width`: frame width (0 when params are empty). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_frame_width(frame: CHandle) -> c_int { - handle::guard_raw(|| { - let f = match super::get_box::>(&frame) { - Some(f) => f, - None => return 0, - }; - f.lock().unwrap().width() - }) -} - -/// `oakcodec_frame_height`: frame height (0 when params are empty). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_frame_height(frame: CHandle) -> c_int { - handle::guard_raw(|| { - let f = match super::get_box::>(&frame) { - Some(f) => f, - None => return 0, - }; - f.lock().unwrap().height() - }) -} - -/// `oakcodec_frame_format`: pixel format as an `OakPixelFormat` value; -/// `OAKCOMMON_PIXEL_FORMAT_INVALID` on an empty handle. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_frame_format(frame: CHandle) -> c_int { - handle::guard_raw(|| { - let f = match super::get_box::>(&frame) { - Some(f) => f, - None => return OAKCOMMON_PIXEL_FORMAT_INVALID, - }; - f.lock().unwrap().format() as c_int - }) -} - -/// `oakcodec_frame_channel_count`: plane channel count of the params -/// format (0 on an empty handle). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_frame_channel_count(frame: CHandle) -> c_int { - handle::guard_raw(|| { - let f = match super::get_box::>(&frame) { - Some(f) => f, - None => return 0, - }; - f.lock().unwrap().channel_count() - }) -} - -/// `oakcodec_frame_get_timestamp`: frame timestamp as a rational number -/// of seconds, written through `numerator`/`denominator`. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_frame_get_timestamp( - frame: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - handle::guard(|| { - if numerator.is_null() || denominator.is_null() { - return Err(crate::error::Error::Invalid); - } - let f = super::get_box::>(&frame).ok_or(crate::error::Error::Invalid)?; - let f = f.lock().unwrap(); - let ts = f.timestamp(); - // SAFETY: both pointers were range-checked above. - unsafe { - *numerator = ts.numerator() as c_int; - *denominator = ts.denominator() as c_int; - } - Ok(()) - }) -} - -/// `oakcodec_frame_set_timestamp`: replace the frame timestamp. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_frame_set_timestamp( - frame: CHandle, - numerator: c_int, - denominator: c_int, -) -> c_int { - handle::guard(|| { - let f = super::get_box::>(&frame).ok_or(crate::error::Error::Invalid)?; - f.lock() - .unwrap() - .set_timestamp(Rational::new(numerator as i64, denominator as i64)); - Ok(()) - }) -} - -/// `oakcodec_debug_alive_count`: number of live boxed handle objects -/// across all families (see `crate::handle::alive_count`). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_debug_alive_count() -> c_int { - handle::guard_raw(handle::alive_count) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::bridge::common::{ - oakcommon_videoparams_get_height, oakcommon_videoparams_get_width, - oakcommon_videoparams_init_basic, - }; - use crate::error::OAKCODEC_E_INVALID; - - #[test] - fn frame_lifecycle_golden() { - let _g = crate::ffi::lock_tests(); - let params = unsafe { oakcommon_videoparams_init_basic(100, 50, 0, 4, 1, 1, 0, 1) }; - let before = handle::alive_count(); - let mut h = unsafe { oakcodec_frame_init_with_params(params) }; - assert!(!h.is_null()); - // init -> exactly one more live box. - assert_eq!(handle::alive_count(), before + 1); - - // get_params round-trips width/height through the stub. - let mut out = empty_params(); - let rc = unsafe { oakcodec_frame_get_params(h, &mut out) }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - // NOTE: stub handles carry no addref and `oakcommon_videoparams_free` - // would drop the shared box; the test keeps the copy alive for the - // frame's lifetime and does not free it. - assert_eq!(unsafe { oakcommon_videoparams_get_width(out.clone()) }, 100); - assert_eq!(unsafe { oakcommon_videoparams_get_height(out.clone()) }, 50); - - assert_eq!(unsafe { oakcodec_frame_width(h) }, 100); - assert_eq!(unsafe { oakcodec_frame_height(h) }, 50); - assert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 0); - assert_eq!(unsafe { oakcodec_frame_data(h) }, std::ptr::null_mut()); - - let rc = unsafe { oakcodec_frame_allocate(h) }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - assert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 1); - assert!(!unsafe { oakcodec_frame_data(h) }.is_null()); - // U8 RGBA: 100px -> 4*128 bytes linesize. - assert_eq!(unsafe { oakcodec_frame_linesize_bytes(h) }, 4 * 128); - assert_eq!(unsafe { oakcodec_frame_allocated_size(h) }, (4 * 128) * 50); - - // set_timestamp round-trip. - assert_eq!( - unsafe { oakcodec_frame_set_timestamp(h, 1, 30) }, - crate::error::OAKCODEC_OK - ); - let (mut num, mut den) = (0, 0); - assert_eq!( - unsafe { oakcodec_frame_get_timestamp(h, &mut num, &mut den) }, - crate::error::OAKCODEC_OK - ); - assert_eq!((num, den), (1, 30)); - - unsafe { oakcodec_frame_free(&mut h) }; - assert!(h.is_null()); - assert_eq!(handle::alive_count(), before); - } - - #[test] - fn frame_errors_and_empty_handles() { - let _g = crate::ffi::lock_tests(); - let empty = CHandle::null(); - assert_eq!(unsafe { oakcodec_frame_width(empty) }, 0); - assert_eq!( - unsafe { oakcodec_frame_format(empty) }, - OAKCOMMON_PIXEL_FORMAT_INVALID - ); - assert_eq!( - unsafe { oakcodec_frame_allocate(empty) }, - OAKCODEC_E_INVALID - ); - assert_eq!( - unsafe { oakcodec_frame_get_params(empty, std::ptr::null_mut()) }, - OAKCODEC_E_INVALID - ); - - // init_basic(0, 0) is not valid -> allocate rejects with E_STATE. - let params = unsafe { oakcommon_videoparams_init_basic(0, 0, 0, 4, 1, 1, 0, 1) }; - let mut h = unsafe { oakcodec_frame_init_with_params(params) }; - assert!(!h.is_null()); - assert_eq!( - unsafe { oakcodec_frame_allocate(h) }, - crate::error::OAKCODEC_E_STATE - ); - assert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 0); - unsafe { oakcodec_frame_free(&mut h) }; - } - - #[test] - fn free_null_and_empty_are_noops() { - let _g = crate::ffi::lock_tests(); - let before = handle::alive_count(); - unsafe { oakcodec_frame_free(std::ptr::null_mut()) }; - let mut empty = CHandle::null(); - unsafe { oakcodec_frame_free(&mut empty) }; - assert!(empty.is_null()); - assert_eq!(handle::alive_count(), before); - } - - #[test] - fn init_set_params_and_query_helpers() { - let _g = crate::ffi::lock_tests(); - let before = handle::alive_count(); - - // Bare init (no params): invalid params, not allocated. The stub's - // default MockParams carries format 0 (U8); the empty-handle -1 - // case is covered in `frame_errors_and_empty_handles`. - let mut h = unsafe { oakcodec_frame_init() }; - assert!(!h.is_null()); - assert_eq!(handle::alive_count(), before + 1); - assert_eq!(unsafe { oakcodec_frame_width(h) }, 0); - assert_eq!(unsafe { oakcodec_frame_height(h) }, 0); - assert_eq!(unsafe { oakcodec_frame_channel_count(h) }, 4); - assert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 0); - assert_eq!(unsafe { oakcodec_frame_allocated_size(h) }, 0); - assert_eq!(unsafe { oakcodec_frame_linesize_bytes(h) }, 0); - assert_eq!(unsafe { oakcodec_frame_linesize_pixels(h) }, 0); - assert_eq!(unsafe { oakcodec_frame_data(h) }, std::ptr::null_mut()); - assert_eq!(unsafe { oakcodec_frame_const_data(h) }, std::ptr::null()); - assert_eq!( - unsafe { oakcodec_frame_allocate(h) }, - crate::error::OAKCODEC_E_STATE - ); - - // set_params replaces the parameter set and recomputes line sizes. - let params = unsafe { oakcommon_videoparams_init_basic(100, 50, 0, 4, 1, 1, 0, 1) }; - let rc = unsafe { oakcodec_frame_set_params(h, params) }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - assert_eq!(unsafe { oakcodec_frame_width(h) }, 100); - assert_eq!(unsafe { oakcodec_frame_height(h) }, 50); - assert_eq!(unsafe { oakcodec_frame_format(h) }, 0); // U8 - assert_eq!(unsafe { oakcodec_frame_channel_count(h) }, 4); - assert_eq!(unsafe { oakcodec_frame_linesize_bytes(h) }, 4 * 128); - assert_eq!(unsafe { oakcodec_frame_linesize_pixels(h) }, 128); - - // allocate -> data and const_data point at the buffer. - assert_eq!( - unsafe { oakcodec_frame_allocate(h) }, - crate::error::OAKCODEC_OK - ); - assert!(!unsafe { oakcodec_frame_data(h) }.is_null()); - assert!(!unsafe { oakcodec_frame_const_data(h) }.is_null()); - assert_eq!(unsafe { oakcodec_frame_allocated_size(h) }, (4 * 128) * 50); - - // get_timestamp rejects NULL out pointers. - assert_eq!( - unsafe { oakcodec_frame_get_timestamp(h, std::ptr::null_mut(), std::ptr::null_mut()) }, - OAKCODEC_E_INVALID - ); - - // debug_alive_count reports the live boxes (>= our own). - assert!(unsafe { oakcodec_debug_alive_count() } >= before + 1); - - unsafe { oakcodec_frame_free(&mut h) }; - assert_eq!(handle::alive_count(), before); - } - - fn empty_params() -> OakVideoParams { - OakVideoParams { - ctx: std::ptr::null_mut(), - addref: None, - release: None, - abi_version: 0, - } - } -} diff --git a/crates/oakcodec/src/ffi/mod.rs b/crates/oakcodec/src/ffi/mod.rs deleted file mode 100644 index e508d9beb..000000000 --- a/crates/oakcodec/src/ffi/mod.rs +++ /dev/null @@ -1,118 +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 . - -//! C ABI export layer: implements `include/codec/*.h` verbatim. -//! -//! Organization: one submodule per public header (frame / decoder / -//! encoder / format / conform / proxy / task). The authoritative function -//! list is the header itself; each export only unwraps handles, calls the -//! safe Rust domains, and maps results through [`crate::handle::guard*`]. -//! `include/codec/error.h` exports macros only, so it is folded into the -//! preamble below instead of getting its own submodule. -//! -//! Shared helpers live here: the two-stage string convention -//! ([`string_out`]), C-string decoding ([`c_str`]) and in-place handle -//! release ([`free_handle`]) — all mirroring the `c_api/*.cpp` helpers. - -/// `include/codec/error.h` — macros only, no exported functions. -/// -/// `OAKCODEC_OK` and the `OAKCODEC_E_*` codes are mirrored as -/// [`crate::error`] constants; `OAKCODEC_ABI_VERSION` lives in -/// [`crate::handle`]. -pub mod conform; -pub mod decoder; -pub mod encoder; -pub mod format; -pub mod frame; -pub mod proxy; -pub mod task; - -use std::ffi::{c_char, c_int}; -#[cfg(test)] -use std::sync::Mutex; - -use crate::handle::CHandle; - -/// Serializes every ffi unit test: they share the global handle ALIVE -/// counter, the injected decoder/encoder registries and the probe error, -/// so exact `alive_count` assertions and registry injection require -/// serial execution. Held poison-tolerant (`into_inner`) so one failing -/// test cannot cascade-fail the rest. -#[cfg(test)] -pub(crate) static TEST_LOCK: Mutex<()> = Mutex::new(()); - -/// Poison-tolerant lock helper for the ffi tests. -#[cfg(test)] -pub(crate) fn lock_tests() -> std::sync::MutexGuard<'static, ()> { - TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()) -} - -/// Two-stage string copy helper (`string_out` in every `c_api/*.cpp`). -/// -/// Returns the required buffer size including the trailing NUL; when -/// `buf` is non-NULL and `buf_size > 0` the string is copied truncated to -/// `buf_size - 1` bytes and NUL-terminated. -pub(crate) 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. -pub(crate) fn c_str(ptr: *const c_char) -> Option { - if ptr.is_null() { - return None; - } - // SAFETY: `ptr` must be a valid NUL-terminated C string by contract. - let s = unsafe { std::ffi::CStr::from_ptr(ptr) }; - Some(s.to_string_lossy().into_owned()) -} - -/// Release a handle in place and null its `ctx` (`free_handle` in -/// `c_api/refcounted.h`); NULL pointer and empty handle are no-ops. -pub(crate) fn free_handle(h: *mut CHandle) { - if h.is_null() { - return; - } - 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(); -} - -/// Safe view into a handle's boxed value; `None` for empty handles. -/// -/// Thin wrapper over [`crate::handle::get`] so the export bodies can call -/// it without `unsafe` blocks everywhere. -/// -/// # Safety -/// `T` must be the boxed type; each export asserts it via the handle -/// contract (the same typed box is used by its `make_owned` call). -pub(crate) fn get_box(h: &CHandle) -> Option<&T> { - unsafe { crate::handle::get::(h) } -} diff --git a/crates/oakcodec/src/ffi/proxy.rs b/crates/oakcodec/src/ffi/proxy.rs deleted file mode 100644 index d3785d0a8..000000000 --- a/crates/oakcodec/src/ffi/proxy.rs +++ /dev/null @@ -1,484 +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 . - -//! `include/codec/proxy.h` exports. -//! -//! Complete inventory: create_instance / destroy_instance / params_default -//! / get_state / state_to_string / get_proxy_directory / get_proxy_filename -//! / get_working_filename / get_or_start / find_ffmpeg. -//! -//! # CPP-PARITY -//! The C++ `oakcodec_proxy_params.include_audio` is an `int`, mirrored -//! byte-for-byte by [`crate::proxymanager::ProxyParams`]; the POD structs -//! are still defined here (rather than reused) so the ffi layer never -//! depends on the crate-internal type's layout. A NULL `params` maps to -//! the crate's compiled-in defaults (`ProxyParams::default`), matching -//! `to_native` in `c_api/proxy.cpp`. - -use std::ffi::{c_char, c_int}; - -use crate::handle; -use crate::proxymanager::{ProxyManager, ProxyParams, ProxyState}; - -/// `OAKCODEC_PROXY_STATE_MISSING`. -pub const OAKCODEC_PROXY_STATE_MISSING: c_int = 0; -/// `OAKCODEC_PROXY_STATE_GENERATING`. -pub const OAKCODEC_PROXY_STATE_GENERATING: c_int = 1; -/// `OAKCODEC_PROXY_STATE_READY`. -pub const OAKCODEC_PROXY_STATE_READY: c_int = 2; -/// `OAKCODEC_PROXY_STATE_FAILED`. -pub const OAKCODEC_PROXY_STATE_FAILED: c_int = 3; - -/// `oakcodec_proxy_params` — POD mirror of `include/codec/proxy.h`. -#[allow(missing_docs)] -#[repr(C)] -pub struct oakcodec_proxy_params { - pub width: c_int, - pub height: c_int, - pub divider: c_int, - pub version: c_int, - pub crf: c_int, - pub include_audio: c_int, - pub extension: [u8; 32], - pub preset: [u8; 32], -} - -/// `oakcodec_proxy_result` — POD result of `oakcodec_proxy_get_or_start`. -#[allow(missing_docs)] -#[repr(C)] -pub struct oakcodec_proxy_result { - pub state: c_int, - pub filename: [u8; 1024], -} - -/// Convert the C POD to the crate's [`ProxyParams`] (`to_native` in -/// `c_api/proxy.cpp`); NULL maps to the compiled-in defaults. -fn to_native(p: *const oakcodec_proxy_params) -> ProxyParams { - if p.is_null() { - return ProxyParams::default(); - } - let p = unsafe { &*p }; - ProxyParams { - width: p.width, - height: p.height, - divider: p.divider, - version: p.version, - crf: p.crf, - include_audio: p.include_audio, - extension: p.extension, - preset: p.preset, - } -} - -/// Copy a NUL-terminated byte array into a C buffer (truncated). -fn copy_cstr(src: &[u8], dst: &mut [u8]) { - dst.fill(0); - let n = src.len().min(dst.len().saturating_sub(1)); - dst[..n].copy_from_slice(&src[..n]); -} - -/// `oakcodec_proxy_create_instance`: create the singleton (always present -/// here, so a no-op). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_proxy_create_instance() -> c_int { - handle::guard_raw(|| { - let _ = ProxyManager::instance(); - crate::error::OAKCODEC_OK - }) -} - -/// `oakcodec_proxy_destroy_instance`: destroy the singleton (the Rust -/// manager is stateless, so a no-op). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_proxy_destroy_instance() -> c_int { - handle::guard_raw(|| crate::error::OAKCODEC_OK) -} - -/// `oakcodec_proxy_params_default`: fill `out` with the compiled-in -/// default proxy parameters. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_proxy_params_default(out: *mut oakcodec_proxy_params) -> c_int { - handle::guard(|| { - if out.is_null() { - return Err(crate::error::Error::Invalid); - } - let n = ProxyManager::proxy_params_from_config(); - unsafe { - (*out).width = n.width; - (*out).height = n.height; - (*out).divider = n.divider; - (*out).version = n.version; - (*out).crf = n.crf; - (*out).include_audio = n.include_audio; - copy_cstr(&n.extension, &mut (*out).extension); - copy_cstr(&n.preset, &mut (*out).preset); - } - Ok(()) - }) -} - -/// `oakcodec_proxy_get_state`: state of a proxy file on disk -/// (`OAKCODEC_PROXY_STATE_MISSING` for NULL/empty/absent). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_proxy_get_state(proxy_filename: *const c_char) -> c_int { - handle::guard_raw(|| { - let f = match crate::ffi::c_str(proxy_filename) { - Some(f) if !f.is_empty() => f, - _ => return OAKCODEC_PROXY_STATE_MISSING, - }; - ProxyManager::get_proxy_state(&f) as c_int - }) -} - -/// `oakcodec_proxy_state_to_string` (two-stage). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_proxy_state_to_string( - state: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - handle::guard_raw(|| { - let s = match state { - 0 => ProxyManager::proxy_state_to_string(ProxyState::Missing), - 1 => ProxyManager::proxy_state_to_string(ProxyState::Generating), - 2 => ProxyManager::proxy_state_to_string(ProxyState::Ready), - 3 => ProxyManager::proxy_state_to_string(ProxyState::Failed), - _ => return crate::error::OAKCODEC_E_INVALID, - }; - super::string_out(&s, buf, buf_size) - }) -} - -/// `oakcodec_proxy_get_proxy_directory` (two-stage). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_proxy_get_proxy_directory( - cache_path: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - handle::guard_raw(|| { - let cache = match crate::ffi::c_str(cache_path) { - Some(c) => c, - None => return crate::error::OAKCODEC_E_INVALID, - }; - match ProxyManager::get_proxy_directory(&cache) { - Ok(s) => super::string_out(&s, buf, buf_size), - Err(_) => crate::error::OAKCODEC_E_FAILED, - } - }) -} - -/// `oakcodec_proxy_get_proxy_filename` (two-stage). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_proxy_get_proxy_filename( - cache_path: *const c_char, - source_filename: *const c_char, - stream_index: c_int, - params: *const oakcodec_proxy_params, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - handle::guard_raw(|| { - let cache = match crate::ffi::c_str(cache_path) { - Some(c) => c, - None => return crate::error::OAKCODEC_E_INVALID, - }; - let source = match crate::ffi::c_str(source_filename) { - Some(s) => s, - None => return crate::error::OAKCODEC_E_INVALID, - }; - let native = to_native(params); - match ProxyManager::get_proxy_filename(&cache, &source, stream_index, &native) { - Ok(s) => super::string_out(&s, buf, buf_size), - Err(_) => crate::error::OAKCODEC_E_FAILED, - } - }) -} - -/// `oakcodec_proxy_get_working_filename` (two-stage). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_proxy_get_working_filename( - proxy_filename: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - handle::guard_raw(|| { - let proxy = match crate::ffi::c_str(proxy_filename) { - Some(p) => p, - None => return crate::error::OAKCODEC_E_INVALID, - }; - match ProxyManager::get_working_filename(&proxy) { - Ok(s) => super::string_out(&s, buf, buf_size), - Err(_) => crate::error::OAKCODEC_E_FAILED, - } - }) -} - -/// `oakcodec_proxy_get_or_start`: get or start generating a proxy for -/// `source_filename`. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_proxy_get_or_start( - cache_path: *const c_char, - source_filename: *const c_char, - stream_index: c_int, - params: *const oakcodec_proxy_params, - out: *mut oakcodec_proxy_result, -) -> c_int { - handle::guard(|| { - if out.is_null() { - return Err(crate::error::Error::Invalid); - } - let cache = match crate::ffi::c_str(cache_path) { - Some(c) => c, - None => return Err(crate::error::Error::Invalid), - }; - let source = match crate::ffi::c_str(source_filename) { - Some(s) => s, - None => return Err(crate::error::Error::Invalid), - }; - let native = to_native(params); - let (state, filename) = ProxyManager::instance() - .get_or_start(&cache, &source, stream_index, &native) - .map_err(|_| crate::error::Error::Failed("get_or_start failed".to_string()))?; - unsafe { - let out_ref = &mut *out; - out_ref.state = state as c_int; - // Truncate to 1023 chars + NUL, matching `snprintf` in - // `c_api/proxy.cpp`. - let n = filename.len().min(1023); - out_ref.filename[..n].copy_from_slice(&filename.as_bytes()[..n]); - out_ref.filename[n] = 0; - } - Ok(()) - }) -} - -/// `oakcodec_proxy_find_ffmpeg` (two-stage; empty string when none found). -#[no_mangle] -pub unsafe extern "C" fn oakcodec_proxy_find_ffmpeg( - configured_path: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - handle::guard_raw(|| { - let configured = crate::ffi::c_str(configured_path).unwrap_or_default(); - let s = ProxyManager::find_ffmpeg(&configured); - super::string_out(&s, buf, buf_size) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::conformmanager::test_util::{accept_cb, REG_LOCK}; - use crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_STATE}; - use crate::task::set_task_submit_cb_extern; - - fn cstr(s: &str) -> std::ffi::CString { - std::ffi::CString::new(s).unwrap() - } - - fn temp_cache(name: &str) -> String { - let dir = std::env::temp_dir().join(format!( - "oakcodec_ffi_proxy_{}_{}", - name, - std::process::id() - )); - let _ = std::fs::create_dir_all(&dir); - dir.to_string_lossy().into_owned() - } - - fn defaults() -> oakcodec_proxy_params { - let mut p: oakcodec_proxy_params = unsafe { std::mem::zeroed() }; - let rc = unsafe { oakcodec_proxy_params_default(&mut p) }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - p - } - - #[test] - fn create_destroy_and_params_default() { - let _g = crate::ffi::lock_tests(); - assert_eq!( - unsafe { oakcodec_proxy_create_instance() }, - crate::error::OAKCODEC_OK - ); - assert_eq!( - unsafe { oakcodec_proxy_destroy_instance() }, - crate::error::OAKCODEC_OK - ); - - let p = defaults(); - assert_eq!(p.width, 1280); - assert_eq!(p.height, 720); - assert_eq!(p.divider, 1); - assert_eq!(p.version, 1); - assert_eq!(p.crf, 23); - assert_eq!(p.include_audio, 1); - assert_eq!(&p.extension[..3], b"mp4"); - assert_eq!(&p.preset[..8], b"veryfast"); - - let rc = unsafe { oakcodec_proxy_params_default(std::ptr::null_mut()) }; - assert_eq!(rc, OAKCODEC_E_INVALID); - } - - #[test] - fn get_state_and_state_to_string() { - let _g = crate::ffi::lock_tests(); - let cache = temp_cache("state"); - let p = defaults(); - let src = cstr("media.mp4"); - let cache_c = cstr(&cache); - - // Resolve the proxy filename, then query its state. - let mut name = [0i8; 1024]; - let rc = unsafe { - oakcodec_proxy_get_proxy_filename( - cache_c.as_ptr(), - src.as_ptr(), - 0, - &p, - name.as_mut_ptr(), - 1024, - ) - }; - assert!(rc > 0); - let proxy = crate::ffi::c_str(name.as_ptr()).unwrap(); - assert!(proxy.contains("1280x720")); - - // Missing by default. - let pc = cstr(&proxy); - assert_eq!( - unsafe { oakcodec_proxy_get_state(pc.as_ptr()) }, - OAKCODEC_PROXY_STATE_MISSING - ); - assert_eq!( - unsafe { oakcodec_proxy_get_state(std::ptr::null()) }, - OAKCODEC_PROXY_STATE_MISSING - ); - - // Ready once the file exists. - std::fs::create_dir_all(std::path::Path::new(&proxy).parent().unwrap()).unwrap(); - std::fs::write(&proxy, b"x").unwrap(); - assert_eq!( - unsafe { oakcodec_proxy_get_state(pc.as_ptr()) }, - OAKCODEC_PROXY_STATE_READY - ); - - // state_to_string mapping + invalid range. - let mut buf = [0i8; 64]; - let rc = unsafe { oakcodec_proxy_state_to_string(2, buf.as_mut_ptr(), 64) }; - assert_eq!(rc, 6); // "ready" + NUL - assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("ready")); - let rc = unsafe { oakcodec_proxy_state_to_string(7, buf.as_mut_ptr(), 64) }; - assert_eq!(rc, OAKCODEC_E_INVALID); - } - - #[test] - fn directory_working_and_get_or_start() { - let _g = crate::ffi::lock_tests(); - let cache = temp_cache("getorstart"); - let cache_c = cstr(&cache); - let src = cstr("media.mp4"); - let p = defaults(); - - // get_proxy_directory. - let mut buf = [0i8; 512]; - let rc = - unsafe { oakcodec_proxy_get_proxy_directory(cache_c.as_ptr(), buf.as_mut_ptr(), 512) }; - assert!(rc > 0); - assert_eq!( - crate::ffi::c_str(buf.as_ptr()).as_deref(), - Some(format!("{}/proxy", cache).as_str()) - ); - - // get_working_filename appends ".working.mp4". - let proxy = format!("{}/proxy/{}-0.1280x720.v1.a1.mp4", cache, 12345); - let pc = cstr(&proxy); - let rc = unsafe { oakcodec_proxy_get_working_filename(pc.as_ptr(), buf.as_mut_ptr(), 512) }; - assert!(rc > 0); - assert_eq!( - crate::ffi::c_str(buf.as_ptr()).as_deref(), - Some(format!("{}.working.mp4", proxy).as_str()) - ); - - // get_or_start without a registrar -> Missing. - let _g = REG_LOCK.lock().unwrap(); - set_task_submit_cb_extern(None, std::ptr::null_mut()); - let mut out: oakcodec_proxy_result = unsafe { std::mem::zeroed() }; - let rc = - unsafe { oakcodec_proxy_get_or_start(cache_c.as_ptr(), src.as_ptr(), 0, &p, &mut out) }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - assert_eq!(out.state, OAKCODEC_PROXY_STATE_MISSING); - - // With a registrar and no files -> Generating. - set_task_submit_cb_extern(Some(accept_cb), std::ptr::null_mut()); - let rc = - unsafe { oakcodec_proxy_get_or_start(cache_c.as_ptr(), src.as_ptr(), 0, &p, &mut out) }; - assert_eq!(rc, crate::error::OAKCODEC_OK); - assert_eq!(out.state, OAKCODEC_PROXY_STATE_GENERATING); - - // Invalid args. - let rc = - unsafe { oakcodec_proxy_get_or_start(std::ptr::null(), src.as_ptr(), 0, &p, &mut out) }; - assert_eq!(rc, OAKCODEC_E_INVALID); - let rc = unsafe { - oakcodec_proxy_get_or_start(cache_c.as_ptr(), src.as_ptr(), 0, &p, std::ptr::null_mut()) - }; - assert_eq!(rc, OAKCODEC_E_INVALID); - - // get_proxy_directory / get_proxy_filename / get_working_filename - // argument validation. - let rc = - unsafe { oakcodec_proxy_get_proxy_directory(std::ptr::null(), buf.as_mut_ptr(), 512) }; - assert_eq!(rc, OAKCODEC_E_INVALID); - let rc = unsafe { - oakcodec_proxy_get_proxy_filename( - std::ptr::null(), - src.as_ptr(), - 0, - &p, - buf.as_mut_ptr(), - 512, - ) - }; - assert_eq!(rc, OAKCODEC_E_INVALID); - let rc = - unsafe { oakcodec_proxy_get_working_filename(std::ptr::null(), buf.as_mut_ptr(), 512) }; - assert_eq!(rc, OAKCODEC_E_INVALID); - - set_task_submit_cb_extern(None, std::ptr::null_mut()); - } - - #[test] - fn find_ffmpeg_uses_configured_path() { - let _g = crate::ffi::lock_tests(); - // The current test binary is a real executable: the configured - // path resolves to an absolute path. - let me = std::env::current_exe().unwrap(); - let mc = cstr(me.to_str().unwrap()); - let mut buf = [0i8; 1024]; - let rc = unsafe { oakcodec_proxy_find_ffmpeg(mc.as_ptr(), buf.as_mut_ptr(), 1024) }; - assert!(rc > 0); - let found = crate::ffi::c_str(buf.as_ptr()).unwrap(); - assert!(found.starts_with('/')); - - // NULL configured path falls back to the search (empty or absolute). - let rc = unsafe { oakcodec_proxy_find_ffmpeg(std::ptr::null(), buf.as_mut_ptr(), 1024) }; - assert!(rc > 0); - let found = crate::ffi::c_str(buf.as_ptr()).unwrap(); - assert!(found.is_empty() || found.starts_with('/')); - } -} diff --git a/crates/oakcodec/src/ffi/task.rs b/crates/oakcodec/src/ffi/task.rs deleted file mode 100644 index 1fc777afa..000000000 --- a/crates/oakcodec/src/ffi/task.rs +++ /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 . - -//! `include/codec/task.h` exports. -//! -//! Complete inventory: set_task_submit_cb / task_submit_is_registered. -//! The callback typedef and request struct are mirrored in -//! [`crate::task`]. - -use std::ffi::c_int; - -use crate::handle; - -/// `oakcodec_set_task_submit_cb`: register (or replace, or clear with -/// NULL) the global task submit callback. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_set_task_submit_cb( - cb: Option, - userdata: *mut std::ffi::c_void, -) { - handle::guard_void(|| { - crate::task::set_task_submit_cb_extern(cb, userdata); - }) -} - -/// `oakcodec_task_submit_is_registered`: 1 when a callback is set. -#[no_mangle] -pub unsafe extern "C" fn oakcodec_task_submit_is_registered() -> c_int { - handle::guard_raw(|| { - if crate::task::task_submit_is_registered() { - 1 - } else { - 0 - } - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::conformmanager::test_util::{accept_cb, REG_LOCK}; - use crate::task::set_task_submit_cb_extern; - - #[test] - fn register_query_clear() { - let _g = crate::ffi::lock_tests(); - let _g = REG_LOCK.lock().unwrap(); - - assert_eq!(unsafe { oakcodec_task_submit_is_registered() }, 0); - - unsafe { oakcodec_set_task_submit_cb(Some(accept_cb), std::ptr::null_mut()) }; - assert_eq!(unsafe { oakcodec_task_submit_is_registered() }, 1); - - unsafe { oakcodec_set_task_submit_cb(None, std::ptr::null_mut()) }; - assert_eq!(unsafe { oakcodec_task_submit_is_registered() }, 0); - - // Restore a clean slate for the other modules. - set_task_submit_cb_extern(None, std::ptr::null_mut()); - } -} diff --git a/crates/oakcodec/src/ffmpeg.rs b/crates/oakcodec/src/ffmpeg.rs index 3f55ac52e..eaff1268c 100644 --- a/crates/oakcodec/src/ffmpeg.rs +++ b/crates/oakcodec/src/ffmpeg.rs @@ -56,32 +56,20 @@ use ffmpeg::software::{resampling, scaling}; use ffmpeg::{ChannelLayout, Dictionary, Error as FfmpegError, Rational as FfRational}; use ffmpeg_next as ffmpeg; +use oakcommon::cancelatom::CancelAtom; +use oakcommon::ocioutils::PixelFormat as OakPixelFormat; +use oakcommon::videoparams::{Interlacing, VideoParams, VideoType}; use oakcore_rs::{PixelFormat, Rational, SampleFormat, TimeRange}; -use crate::bridge::common::{ - oakcommon_videoparams_init_basic, oakcommon_videoparams_set_channel_count, - oakcommon_videoparams_set_duration, oakcommon_videoparams_set_format, - oakcommon_videoparams_set_frame_rate, oakcommon_videoparams_set_height, - oakcommon_videoparams_set_interlacing, oakcommon_videoparams_set_pixel_aspect_ratio, - oakcommon_videoparams_set_premultiplied_alpha, oakcommon_videoparams_set_start_time, - oakcommon_videoparams_set_stream_index, oakcommon_videoparams_set_time_base, - oakcommon_videoparams_set_video_type, oakcommon_videoparams_set_width, - oakcore_audioparams_create, oakcore_audioparams_set_duration, - oakcore_audioparams_set_stream_index, oakcore_audioparams_set_time_base, OakAudioParams, -}; -use crate::bridge::render::{oakrender_cancelatom_is_cancelled, OakCancelAtom, OakRenderTexture}; -use crate::decoder::{CodecStream, Decoder, RetrieveAudioStatus, RetrieveVideoParams}; +use crate::audioparams::AudioParams; +use crate::decoder::{CodecStream, Decoder, OakRenderTexture, RetrieveAudioStatus, RetrieveVideoParams}; use crate::encoder::Encoder; use crate::encodingparams::EncodingParams; use crate::footagedescription::{FootageDescription, StreamEntry}; use crate::frame::Frame; -/// `OAKCOMMON_VIDEO_INTERLACE_NONE` (oakcommon `common/videoparams.h`). -const OAKCOMMON_VIDEO_INTERLACE_NONE: i32 = 0; /// `OAKCOMMON_COLOR_RANGE_FULL`. const OAKCOMMON_COLOR_RANGE_FULL: i32 = 1; -/// `OAKCOMMON_VIDEO_TYPE_VIDEO`. -const OAKCOMMON_VIDEO_TYPE_VIDEO: i32 = 0; /// The format-level time base (microseconds), `FB_TIME_BASE` in the bridge. const FB_TIME_BASE: i64 = 1_000_000; /// `AV_NOPTS_VALUE`. @@ -112,18 +100,12 @@ fn fail(msg: impl Into) -> crate::error::Error { crate::error::Error::Failed(msg.into()) } -/// `cancel_atom_is_cancelled` — NULL/empty-handle-safe check of an -/// oakrender cancel atom (borrowed pointer). +/// `cancel_atom_is_cancelled` — check of a cancel atom (borrowed pointer). /// /// # CPP-PARITY /// `src/codec/src/ffmpeg/ffmpegdecoder.cpp` (anonymous namespace helper). -fn cancel_atom_is_cancelled(cancelled: Option<&OakCancelAtom>) -> bool { - match cancelled { - Some(atom) if !atom.ctx.is_null() => unsafe { - oakrender_cancelatom_is_cancelled(atom.clone()) != 0 - }, - _ => false, - } +fn cancel_atom_is_cancelled(cancelled: Option<&CancelAtom>) -> bool { + cancelled.is_some_and(|atom| atom.is_cancelled()) } /// Whether an FFmpeg error means "end of stream" or "try again". @@ -261,7 +243,7 @@ impl Decoder for FFmpegDecoder { fn probe( &self, filename: &str, - cancelled: Option<&OakCancelAtom>, + cancelled: Option<&CancelAtom>, ) -> Option { ffmpeg_init().ok()?; probe_file(filename, cancelled) @@ -377,7 +359,7 @@ impl Decoder for FFmpegDecoder { sample_rate: i32, channel_layout: u64, sample_format: i32, - cancelled: Option<&OakCancelAtom>, + cancelled: Option<&CancelAtom>, ) -> crate::error::Result<()> { ffmpeg_init()?; let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner()); @@ -705,7 +687,7 @@ impl DecoderState { &mut self, time: &Rational, any_timecode: bool, - cancelled: Option<&OakCancelAtom>, + cancelled: Option<&CancelAtom>, ) -> crate::error::Result> { // Move the video state out so `self.seek` / `self.pull` (which touch // other fields) can be called without conflicting borrows. @@ -1023,7 +1005,7 @@ impl DecoderState { sample_rate: i32, channel_layout: u64, sample_format: i32, - cancelled: Option<&OakCancelAtom>, + cancelled: Option<&CancelAtom>, ) -> crate::error::Result<()> { if self.input_channel_layout_mask == 0 { return Err(fail( @@ -1456,11 +1438,18 @@ fn copy_rgba_f32_to_frame( bytes: &[u8], timestamp: Rational, ) -> crate::error::Result { - let params = unsafe { oakcommon_videoparams_init_basic(width as i32, height as i32, 0, 4, 1, 1, 0, 1) }; - unsafe { - oakcommon_videoparams_set_format(params.clone(), PixelFormat::F32 as i32); - oakcommon_videoparams_set_channel_count(params.clone(), VIDEO_CHANNELS); - } + let mut params = VideoParams::new_basic( + width as i32, + height as i32, + OakPixelFormat::from_code(0), + 4, + 1, + 1, + 0, + 1, + ); + params.set_format(OakPixelFormat::from_code(PixelFormat::F32 as i32)); + params.set_channel_count(VIDEO_CHANNELS); let mut frame = Frame::with_params(params); frame.set_timestamp(timestamp); frame.allocate()?; @@ -1490,7 +1479,7 @@ fn copy_rgba_f32_to_frame( /// stream details are taken from stream parameters (no second decode pass), /// so `is_still` is always false and interlacing always progressive; /// subtitle streams are counted but not added. -fn probe_file(filename: &str, cancelled: Option<&OakCancelAtom>) -> Option { +fn probe_file(filename: &str, cancelled: Option<&CancelAtom>) -> Option { let mut dict = Dictionary::new(); dict.set("analyzeduration", "5000000"); dict.set("probesize", "20000000"); @@ -1545,29 +1534,25 @@ fn probe_file(filename: &str, cancelled: Option<&OakCancelAtom>) -> Option { @@ -1584,22 +1569,26 @@ fn probe_file(filename: &str, cancelled: Option<&OakCancelAtom>) -> Option 0 { + ChannelLayout::default(raw_layout.channels()).bits() + } else { + raw_layout.bits() + }; + let tb = stream.time_base(); + desc.push_stream(StreamEntry::Audio(AudioParams { + sample_rate, + channel_layout: layout_mask, + format: 0, + stream_index: i as i32, + duration: stream_duration, + time_base: (tb.0 as i32, tb.1 as i32), + })); } _ => {} } @@ -1699,6 +1688,12 @@ struct VideoEncoderState { scaler: scaling::Context, width: u32, height: u32, + /// The encoder's time base after `open` (the frame PTS are expressed + /// in it; see `FFmpegEncoder::open`). + time_base: FfRational, + /// One frame in the encoder's time base (the last packet's duration; + /// see `FFmpegEncoder::open`). + frame_duration: i64, } /// Opened audio encoder + its conversion resampler. @@ -1877,7 +1872,6 @@ impl EncoderState { .video() .map_err(ffmpeg_err)?; stream.set_parameters(&encoder); - stream.set_time_base(time_base); encoder.set_width(width); encoder.set_height(height); @@ -1886,7 +1880,19 @@ impl EncoderState { params.video_pixel_aspect_den.max(1), )); encoder.set_frame_rate(Some(frame_rate)); - encoder.set_time_base(time_base); + // The codecs' packet timestamps use a fine tick (x264 encodes at + // 1024 ticks per frame); give H.264 an encoder time base scaled + // to that so the frame PTS stay integral, and sync the stream to + // the encoder's ACTUAL post-open time base (the value the muxer + // reads) so the container timing is `seconds * frame_rate`. + // Other codecs (e.g. MPEG-2) reject the scaled rate and keep the + // nominal frame-duration time base. + let tick = if codec_id == ffmpeg::codec::Id::H264 { + FfRational(time_base.0, time_base.1 * 1024) + } else { + time_base + }; + encoder.set_time_base(tick); if params.video_bit_rate > 0 { encoder.set_bit_rate(params.video_bit_rate as usize); } @@ -1902,6 +1908,20 @@ impl EncoderState { let opened = encoder.open().map_err(|e| { eprintln!("DBG-AUD: audio open failed: {e:?}"); ffmpeg_err(e) })?; stream.set_parameters(&opened); + // The encoder may adjust the time base during `open` (x264 + // picks its own); sync the stream to the encoder's ACTUAL time + // base so the container timing matches the frame PTS computed + // from it (a mismatched stream time base crams the whole video + // into the first milliseconds). + let time_base = opened.time_base(); + stream.set_time_base(time_base); + // One frame in the encoder's time base, used to fill the last + // packet's duration: the muxer normally derives it from the + // codec context attached to the stream, but the ffmpeg-next + // flow never attaches one, so the final frame would carry + // duration 0 and the track would be one frame short. + let frame_duration = (time_base.1 as i64 * i64::from(frame_rate.1)) + / (i64::from(time_base.0) * i64::from(frame_rate.0)).max(1); let scaler = scaling::Context::get( Pixel::RGBA, @@ -1920,6 +1940,8 @@ impl EncoderState { scaler, width, height, + time_base, + frame_duration, }); } @@ -1987,7 +2009,7 @@ impl EncoderState { } /// Encode one frame (F32 RGBA) into the open output. - fn write_video(&mut self, frame: &Frame, params: &EncodingParams) -> crate::error::Result<()> { + fn write_video(&mut self, frame: &Frame, _params: &EncodingParams) -> crate::error::Result<()> { let output = self .output .as_mut() @@ -2040,12 +2062,11 @@ impl EncoderState { // # CPP-PARITY // `FFmpegEncoder::write_frame` passes the frame time in seconds; the - // Rust `Frame` carries the timestamp as a rational. + // Rust `Frame` carries the timestamp as a rational. The PTS is + // expressed in the encoder's own time base (captured at open), so + // the container timing is exactly `seconds * rate`. let secs = frame.timestamp().to_f64(); - let tb = FfRational( - params.video_time_base_num.max(1), - params.video_time_base_den.max(1), - ); + let tb = video.time_base; let pts = (secs * tb.1 as f64 / tb.0 as f64).round() as i64; scaled.set_pts(Some(pts)); @@ -2078,22 +2099,57 @@ impl EncoderState { let channels = audio.resampler.dst_channels.max(1); let in_frames = samples.len() / channels; - // Presentation timestamp in the output stream time base (1/sample_rate). - let pts = output.audio_pts; + // The encoder accepts at most `frame_size` samples per frame (AAC: + // 1024), so the (possibly whole-range) input buffer is split into + // chunks. The resampler converts at the same rate (the rendered + // audio rate equals the encoder rate), but swr may buffer a small + // delay, so the input chunk is shrunk until its predicted output + // fits `frame_size`. + let frame_size = audio.encoder.frame_size().max(1) as usize; + let mut offset = 0usize; + while offset < in_frames { + let mut take = frame_size.min(in_frames - offset); + loop { + let out = unsafe { sys::swr_get_out_samples(audio.resampler.ctx.as_mut_ptr(), take as i32) }; + if out >= 0 && out as usize <= frame_size { + break; + } + take = take.saturating_sub(1); + if take == 0 { + take = 1; + break; + } + } - let layout = channel_layout_from_mask(params.audio_channel_layout); - let mut input = - ffmpeg::frame::Audio::new(Sample::F32(SampleType::Packed), in_frames, layout); - let bytes = - unsafe { std::slice::from_raw_parts(samples.as_ptr() as *const u8, samples.len() * 4) }; - input.data_mut(0)[..bytes.len()].copy_from_slice(bytes); + // Presentation timestamp in the output stream time base (1/sample_rate). + let pts = output.audio_pts; + let layout = channel_layout_from_mask(params.audio_channel_layout); + let chunk = &samples[offset * channels..(offset + take) * channels]; + let mut input = ffmpeg::frame::Audio::new(Sample::F32(SampleType::Packed), take, layout); + let bytes = unsafe { + std::slice::from_raw_parts(chunk.as_ptr() as *const u8, chunk.len() * 4) + }; + input.data_mut(0)[..bytes.len()].copy_from_slice(bytes); - let mut converted = audio.resampler.convert_to_frame(&input).map_err(|e| { eprintln!("DBG-AUD: convert failed: {e:?}"); fail(format!("{e:?}")) })?; - converted.set_pts(Some(pts)); - output.audio_pts += converted.samples() as i64; - - audio.encoder.send_frame(&converted).map_err(|e| { eprintln!("DBG-AUD: send failed: {e:?}"); ffmpeg_err(e) })?; - drain_audio_packets(&mut output.output, audio) + let mut converted = audio.resampler.convert_to_frame(&input).map_err(|e| { + eprintln!("DBG-AUD: convert failed: {e:?}"); + fail(format!("{e:?}")) + })?; + if converted.samples() > 0 { + converted.set_pts(Some(pts)); + output.audio_pts += converted.samples() as i64; + audio + .encoder + .send_frame(&converted) + .map_err(|e| { + eprintln!("DBG-AUD: send failed: {e:?}"); + ffmpeg_err(e) + })?; + drain_audio_packets(&mut output.output, audio)?; + } + offset += take; + } + Ok(()) } /// Flush encoders, write the trailer and close the output (idempotent). @@ -2128,6 +2184,9 @@ fn drain_video_encoder( match video.encoder.receive_packet(&mut pkt) { Ok(()) => { pkt.set_stream(video.stream_index); + if pkt.duration() <= 0 { + pkt.set_duration(video.frame_duration); + } pkt.write_interleaved(output).map_err(ffmpeg_err)?; } Err(e) if is_eof_or_eagain(&e) => break, @@ -2167,6 +2226,12 @@ fn drain_video_packets( match video.encoder.receive_packet(&mut pkt) { Ok(()) => { pkt.set_stream(video.stream_index); + // The final frame carries no duration (see the + // `frame_duration` note); fill it so the track length is + // the full export range. + if pkt.duration() <= 0 { + pkt.set_duration(video.frame_duration); + } pkt.write_interleaved(output).map_err(ffmpeg_err)?; } Err(e) if is_eof_or_eagain(&e) => break, diff --git a/crates/oakcodec/src/footagedescription.rs b/crates/oakcodec/src/footagedescription.rs index 0ae6da80e..245c227dc 100644 --- a/crates/oakcodec/src/footagedescription.rs +++ b/crates/oakcodec/src/footagedescription.rs @@ -23,19 +23,21 @@ //! `Track::Type` mapping and XML load/save are intentionally not reproduced //! (NOTES.md §4) — use [`FootageDescription::stream_is_video`] etc. +use oakcommon::subtitleparams::SubtitleParams; +use oakcommon::videoparams::VideoParams; use oakcore_rs::{Rational, TimeRange}; -use crate::bridge::common::{OakAudioParams, OakSubtitleParams, OakVideoParams}; +use crate::audioparams::AudioParams; /// One stream entry in a footage description. #[derive(Clone, Debug)] pub enum StreamEntry { - /// A video stream (params handle, addref'd). - Video(OakVideoParams), + /// A video stream. + Video(VideoParams), /// An audio stream. - Audio(OakAudioParams), - /// A subtitle stream (params handle, addref'd). - Subtitle(OakSubtitleParams), + Audio(AudioParams), + /// A subtitle stream. + Subtitle(SubtitleParams), } /// `olive::FootageDescription` — the decoder name plus stream inventory. @@ -127,7 +129,7 @@ impl FootageDescription { } /// The `index`-th video stream's params (by video-stream ordinal). - pub fn get_video_stream(&self, index: usize) -> Option<&OakVideoParams> { + pub fn get_video_stream(&self, index: usize) -> Option<&VideoParams> { self.streams .iter() .filter_map(|s| match s { @@ -138,7 +140,7 @@ impl FootageDescription { } /// The `index`-th audio stream's params (by audio-stream ordinal). - pub fn get_audio_stream(&self, index: usize) -> Option<&OakAudioParams> { + pub fn get_audio_stream(&self, index: usize) -> Option<&AudioParams> { self.streams .iter() .filter_map(|s| match s { @@ -149,7 +151,7 @@ impl FootageDescription { } /// The `index`-th subtitle stream's params (by subtitle-stream ordinal). - pub fn get_subtitle_stream(&self, index: usize) -> Option<&OakSubtitleParams> { + pub fn get_subtitle_stream(&self, index: usize) -> Option<&SubtitleParams> { self.streams .iter() .filter_map(|s| match s { @@ -210,31 +212,34 @@ impl FootageDescription { mod tests { use super::*; - fn video_params(index: i32) -> OakVideoParams { - OakVideoParams { - ctx: index as usize as *mut std::ffi::c_void, - addref: None, - release: None, - abi_version: crate::handle::OAKCODEC_ABI_VERSION, + fn video_params(index: i32) -> VideoParams { + let mut vp = VideoParams::new_basic( + 1920, + 1080, + oakcommon::ocioutils::PixelFormat::from_code(0), + 4, + 1, + 1, + 0, + 1, + ); + vp.set_stream_index(index); + vp + } + + fn audio_params() -> AudioParams { + AudioParams { + sample_rate: 48000, + channel_layout: 0x3, + format: 0, + stream_index: 1, + duration: 0, + time_base: (1, 48000), } } - fn audio_params() -> OakAudioParams { - OakAudioParams { - ctx: std::ptr::null_mut(), - addref: None, - release: None, - abi_version: crate::handle::OAKCODEC_ABI_VERSION, - } - } - - fn subtitle_params() -> OakSubtitleParams { - OakSubtitleParams { - ctx: std::ptr::null_mut(), - addref: None, - release: None, - abi_version: crate::handle::OAKCODEC_ABI_VERSION, - } + fn subtitle_params() -> SubtitleParams { + SubtitleParams::new() } #[test] diff --git a/crates/oakcodec/src/frame.rs b/crates/oakcodec/src/frame.rs index c7a771bb9..7ff263419 100644 --- a/crates/oakcodec/src/frame.rs +++ b/crates/oakcodec/src/frame.rs @@ -14,19 +14,15 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//! `olive::Frame` — a CPU pixel buffer plus an `OakVideoParams` handle. +//! `olive::Frame` — a CPU pixel buffer plus a [`VideoParams`] value. //! //! Mirrors `src/codec/src/frame.h`. The params are held as an oakcommon -//! by-value handle (`bridge::common::OakVideoParams`, refcounted) so the -//! byte-level ABI of `oakcodec_frame_get_params`/`_set_params` is -//! unchanged; the pixel data itself is a plain `Vec`. Line-size and -//! pixel-format math lives here. +//! [`VideoParams`] value (single-lib unification; the former refcounted +//! oakcommon handle is gone, so copies are plain clones); the pixel data +//! itself is a plain `Vec`. Line-size and pixel-format math lives +//! here. -use crate::bridge::common::{ - oakcommon_videoparams_free, oakcommon_videoparams_get_format, oakcommon_videoparams_get_height, - oakcommon_videoparams_get_is_valid, oakcommon_videoparams_get_width, - oakcommon_videoparams_init, OakVideoParams, -}; +use oakcommon::videoparams::VideoParams; use oakcore_rs::{PixelFormat, Rational}; /// Number of channels in the internal RGBA pipeline layout @@ -47,11 +43,11 @@ pub enum Interlacing { BottomFieldFirst = 2, } -/// `olive::Frame`: reference-counted CPU pixel buffer + params handle. +/// `olive::Frame`: reference-counted CPU pixel buffer + params value. #[derive(Debug)] pub struct Frame { - /// Video parameter set (oakcommon handle, refcounted). - pub params: Option, + /// Video parameter set. + pub params: Option, /// Pixel buffer (unallocated until `allocate`). data: Vec, /// Distance between rows in bytes (0 until params are set). @@ -87,40 +83,11 @@ fn bytes_per_pixel(format: PixelFormat, channels: i32) -> i32 { (format.bytes_per_channel() as i32) * channels } -/// Increment the refcount of a params handle (a no-op for test-stub handles -/// whose `addref` is `None`). `pub(crate)` so the ffi layer can hand out -/// addref'd copies (`oakcodec_frame_get_params`). -pub(crate) fn params_addref(p: &OakVideoParams) { - if let Some(addref) = p.addref { - // SAFETY: `addref` is a valid C function pointer targeting `ctx`. - unsafe { addref(p.ctx) }; - } -} - -/// Release a params handle (prefers the `release` function pointer; the -/// test stubs use `oakcommon_videoparams_free` instead). Nulls `ctx` so the -/// handle cannot be released twice. -pub(crate) fn params_release(p: &mut OakVideoParams) { - if p.ctx.is_null() { - return; - } - if let Some(release) = p.release { - // SAFETY: `release` is a valid C function pointer targeting `ctx`. - unsafe { release(p.ctx) }; - } else { - // SAFETY: `p` points at a live handle; `oakcommon_videoparams_free` - // is a no-op for the null ctx we leave behind. - unsafe { oakcommon_videoparams_free(p) }; - } - p.ctx = std::ptr::null_mut(); -} - impl Frame { /// New frame with default (invalid) params; buffer unallocated. pub fn new() -> Self { - let params = unsafe { oakcommon_videoparams_init() }; Frame { - params: Some(params), + params: Some(VideoParams::new()), data: Vec::new(), linesize_bytes: 0, timestamp: Rational::new(0, 1), @@ -128,9 +95,8 @@ impl Frame { } } - /// New frame with a copy of `params` (handle addref'd internally). - pub fn with_params(params: OakVideoParams) -> Self { - params_addref(¶ms); + /// New frame with a copy of `params`. + pub fn with_params(params: VideoParams) -> Self { let mut frame = Frame { params: Some(params), data: Vec::new(), @@ -143,17 +109,13 @@ impl Frame { } /// The video parameter set, or `None` when empty. - pub fn params(&self) -> Option<&OakVideoParams> { + pub fn params(&self) -> Option<&VideoParams> { self.params.as_ref() } - /// Replace the parameter set (handle addref'd), recompute line sizes, - /// do NOT reallocate the buffer. - pub fn set_params(&mut self, params: OakVideoParams) { - if let Some(mut old) = self.params.take() { - params_release(&mut old); - } - params_addref(¶ms); + /// Replace the parameter set, recompute line sizes, do NOT reallocate + /// the buffer. + pub fn set_params(&mut self, params: VideoParams) { self.params = Some(params); self.recompute_linesize(); // Deliberately do not touch `data`: an existing buffer keeps its @@ -165,10 +127,8 @@ impl Frame { fn recompute_linesize(&mut self) { self.linesize_bytes = match &self.params { Some(p) => { - let w = unsafe { oakcommon_videoparams_get_width(p.clone()) }; - let fmt = - pixel_format_from_i32(unsafe { oakcommon_videoparams_get_format(p.clone()) }); - Self::generate_linesize_bytes(fmt, w) + let fmt = pixel_format_from_i32(p.format().code()); + Self::generate_linesize_bytes(fmt, p.width()) } None => 0, }; @@ -177,12 +137,11 @@ impl Frame { /// Allocate the pixel buffer from the current params. pub fn allocate(&mut self) -> crate::error::Result<()> { let params = match &self.params { - Some(p) => p.clone(), + Some(p) => p, None => return Err(crate::error::Error::State), }; - let is_valid = unsafe { oakcommon_videoparams_get_is_valid(params.clone()) }; - if is_valid == 0 { + if !params.is_valid() { return Err(crate::error::Error::State); } @@ -191,9 +150,9 @@ impl Frame { return Ok(()); } - let width = unsafe { oakcommon_videoparams_get_width(params.clone()) }; - let height = unsafe { oakcommon_videoparams_get_height(params.clone()) }; - let format = pixel_format_from_i32(unsafe { oakcommon_videoparams_get_format(params) }); + let width = params.width(); + let height = params.height(); + let format = pixel_format_from_i32(params.format().code()); let linesize = Self::generate_linesize_bytes(format, width); let size = (linesize as usize).wrapping_mul(height as usize); @@ -256,7 +215,7 @@ impl Frame { /// Frame width in pixels (0 when params are empty). pub fn width(&self) -> i32 { match &self.params { - Some(p) => unsafe { oakcommon_videoparams_get_width(p.clone()) }, + Some(p) => p.width(), None => 0, } } @@ -264,7 +223,7 @@ impl Frame { /// Frame height in pixels (0 when params are empty). pub fn height(&self) -> i32 { match &self.params { - Some(p) => unsafe { oakcommon_videoparams_get_height(p.clone()) }, + Some(p) => p.height(), None => 0, } } @@ -272,9 +231,7 @@ impl Frame { /// Pixel format (`OakPixelFormat` value). pub fn format(&self) -> PixelFormat { match &self.params { - Some(p) => { - pixel_format_from_i32(unsafe { oakcommon_videoparams_get_format(p.clone()) }) - } + Some(p) => pixel_format_from_i32(p.format().code()), None => PixelFormat::Invalid, } } @@ -282,10 +239,9 @@ impl Frame { /// Plane channel count of the params format. /// /// # CPP-PARITY - /// `src/codec/src/frame.h` reads this from the params handle via - /// `oakcommon_videoparams_get_channel_count`, which is not exposed in the - /// Rust bridge. Decoder frames are always produced in the internal RGBA - /// layout, so this returns [`VIDEO_CHANNELS`] (4). + /// `src/codec/src/frame.h` reads this from the params via + /// `VideoParams::channel_count`. Decoder frames are always produced in + /// the internal RGBA layout, so this returns [`VIDEO_CHANNELS`] (4). pub fn channel_count(&self) -> i32 { VIDEO_CHANNELS } @@ -317,7 +273,7 @@ impl Frame { /// /// # CPP-PARITY /// `src/codec/src/frame.cpp` — the destination params are carried by - /// the C++ callers via `oakcommon_videoparams_*`; Rust keeps the + /// the C++ callers via `VideoParams` setters; Rust keeps the /// equivalent state in `self.params`. /// /// When the current params format already matches the format the buffer @@ -372,23 +328,13 @@ impl Frame { } } -impl Drop for Frame { - /// Release the owned params handle when the last reference dies, - /// mirroring the C++ `Frame::~Frame`. - fn drop(&mut self) { - if let Some(mut p) = self.params.take() { - params_release(&mut p); - } - } -} - #[cfg(test)] mod tests { use super::*; - use crate::bridge::common::oakcommon_videoparams_init_basic; + use oakcommon::ocioutils::PixelFormat as OakPixelFormat; fn frame(w: i32, h: i32) -> Frame { - let params = unsafe { oakcommon_videoparams_init_basic(w, h, 0, 4, 1, 1, 0, 1) }; + let params = VideoParams::new_basic(w, h, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1); Frame::with_params(params) } @@ -459,12 +405,13 @@ mod tests { #[test] fn set_params_recomputes_linesize_without_realloc() { - let params = unsafe { oakcommon_videoparams_init_basic(10, 10, 0, 4, 1, 1, 0, 1) }; + let params = VideoParams::new_basic(10, 10, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1); let mut f = Frame::with_params(params); f.allocate().unwrap(); let before = f.allocated_size(); - let wider = unsafe { oakcommon_videoparams_init_basic(100, 10, 0, 4, 1, 1, 0, 1) }; + let wider = + VideoParams::new_basic(100, 10, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1); f.set_params(wider); // linesize reflects the new width, but the buffer is untouched. assert_eq!(f.linesize_bytes(), 4 * 128); @@ -475,10 +422,10 @@ mod tests { #[cfg(test)] mod tests_extra { use super::*; - use crate::bridge::common::oakcommon_videoparams_init_basic; + use oakcommon::ocioutils::PixelFormat as OakPixelFormat; fn frame(w: i32, h: i32) -> Frame { - let params = unsafe { oakcommon_videoparams_init_basic(w, h, 0, 4, 1, 1, 0, 1) }; + let params = VideoParams::new_basic(w, h, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1); Frame::with_params(params) } @@ -513,8 +460,8 @@ mod tests_extra { #[test] fn pixel_format_from_unknown_code_is_invalid() { - let p = unsafe { oakcommon_videoparams_init_basic(1, 1, 0, 4, 1, 1, 0, 1) }; - unsafe { crate::bridge::common::oakcommon_videoparams_set_format(p.clone(), 99) }; + let mut p = VideoParams::new_basic(1, 1, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1); + p.set_format(OakPixelFormat::from_code(99)); let f = Frame::with_params(p); assert_eq!(f.format(), PixelFormat::Invalid); } diff --git a/crates/oakcodec/src/framemanager.rs b/crates/oakcodec/src/framemanager.rs index e1d830d3b..60d14e821 100644 --- a/crates/oakcodec/src/framemanager.rs +++ b/crates/oakcodec/src/framemanager.rs @@ -27,7 +27,7 @@ use std::sync::{Arc, Mutex, OnceLock}; use std::thread; use std::time::Duration; -use crate::bridge::common::{oakcommon_videoparams_equals, OakVideoParams}; +use oakcommon::videoparams::VideoParams; use crate::frame::Frame; /// `olive::FrameManager`: singleton frame pool with background GC. @@ -71,7 +71,7 @@ impl FrameManager { /// Create a frame with the given params (borrowed from the pool when a /// compatible free frame exists, else freshly allocated). - pub fn create_frame(&self, params: OakVideoParams) -> Arc { + pub fn create_frame(&self, params: VideoParams) -> Arc { let frame = { let mut pool = self.pool.lock().unwrap(); match pool.iter().position(|f| frame_matches(f, ¶ms)) { @@ -138,18 +138,21 @@ fn spawn_gc_thread_once(mgr: &'static FrameManager) { } /// True when `frame` carries params equal to `params`. -fn frame_matches(frame: &Frame, params: &OakVideoParams) -> bool { +fn frame_matches(frame: &Frame, params: &VideoParams) -> bool { let Some(frame_params) = frame.params() else { return false; }; - let eq = unsafe { oakcommon_videoparams_equals(frame_params.clone(), params.clone()) }; - eq != 0 + frame_params.equals(params) } #[cfg(test)] mod tests { use super::*; - use crate::bridge::common::oakcommon_videoparams_init_basic; + use oakcommon::ocioutils::PixelFormat as OakPixelFormat; + + fn test_params(w: i32, h: i32) -> VideoParams { + VideoParams::new_basic(w, h, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1) + } #[test] fn create_and_return_tracks_counts() { @@ -157,7 +160,7 @@ mod tests { assert_eq!(mgr.live_count(), 0); assert_eq!(mgr.peak_count(), 0); - let params = unsafe { oakcommon_videoparams_init_basic(64, 64, 0, 4, 1, 1, 0, 1) }; + let params = test_params(64, 64); let frame = mgr.create_frame(params); assert_eq!(mgr.live_count(), 1); assert_eq!(mgr.peak_count(), 1); @@ -172,14 +175,14 @@ mod tests { #[test] fn pool_reuses_compatible_frames() { let mgr = FrameManager::new(); - let params = unsafe { oakcommon_videoparams_init_basic(64, 64, 0, 4, 1, 1, 0, 1) }; + let params = test_params(64, 64); let f1 = mgr.create_frame(params); mgr.return_frame(Arc::try_unwrap(f1).unwrap()); assert_eq!(mgr.live_count(), 0); // A compatible request reuses the pooled buffer rather than // allocating a new one. - let f2 = mgr.create_frame(unsafe { oakcommon_videoparams_init_basic(64, 64, 0, 4, 1, 1, 0, 1) }); + let f2 = mgr.create_frame(test_params(64, 64)); assert_eq!(mgr.live_count(), 1); assert_eq!(mgr.peak_count(), 1); Arc::try_unwrap(f2).unwrap(); @@ -188,8 +191,8 @@ mod tests { #[test] fn peak_count_tracks_maximum() { let mgr = FrameManager::new(); - let p1 = unsafe { oakcommon_videoparams_init_basic(64, 64, 0, 4, 1, 1, 0, 1) }; - let p2 = unsafe { oakcommon_videoparams_init_basic(128, 128, 0, 4, 1, 1, 0, 1) }; + let p1 = test_params(64, 64); + let p2 = test_params(128, 128); let a = mgr.create_frame(p1); let b = mgr.create_frame(p2); assert_eq!(mgr.live_count(), 2); @@ -203,7 +206,7 @@ mod tests { #[test] fn clear_drops_pooled_frames() { let mgr = FrameManager::new(); - let params = unsafe { oakcommon_videoparams_init_basic(64, 64, 0, 4, 1, 1, 0, 1) }; + let params = test_params(64, 64); let f = mgr.create_frame(params); mgr.return_frame(Arc::try_unwrap(f).unwrap()); assert_eq!(mgr.pool.lock().unwrap().len(), 1); diff --git a/crates/oakcodec/src/handle.rs b/crates/oakcodec/src/handle.rs index 8de73fab0..945f05225 100644 --- a/crates/oakcodec/src/handle.rs +++ b/crates/oakcodec/src/handle.rs @@ -21,9 +21,7 @@ //! created the object). use std::panic::{catch_unwind, AssertUnwindSafe}; -use std::ptr; use std::sync::atomic::{AtomicI32, AtomicU32, Ordering}; - use crate::error::{self, OAKCODEC_E_FAILED}; /// Number of boxed handle objects currently alive (leak/debug checking). @@ -183,9 +181,9 @@ mod tests { #[test] fn make_owned_lifecycle_tracks_alive_count() { - // The shared ffi test lock serializes the crate's `alive_count` + // The shared test lock serializes the crate's `alive_count` // assertions against every other test that creates handles. - let _g = crate::ffi::lock_tests(); + let _g = crate::lock_tests(); let before = alive_count(); let h = make_owned(42u32); assert!(!h.is_null()); @@ -214,7 +212,7 @@ mod tests { #[test] fn make_borrowed_takes_ownership() { - let _g = crate::ffi::lock_tests(); + let _g = crate::lock_tests(); let before = alive_count(); let raw = Box::into_raw(Box::new(7u32)); let h = unsafe { make_borrowed(raw) }; diff --git a/crates/oakcodec/src/lib.rs b/crates/oakcodec/src/lib.rs index 19082ef0f..ad753885d 100644 --- a/crates/oakcodec/src/lib.rs +++ b/crates/oakcodec/src/lib.rs @@ -29,7 +29,10 @@ #![deny(unsafe_op_in_unsafe_fn)] #![warn(missing_docs)] -pub mod bridge; +#[cfg(test)] +use std::sync::{Mutex, MutexGuard}; + +pub mod audioparams; pub mod conformmanager; pub mod decoder; pub mod encoder; @@ -37,7 +40,6 @@ pub mod encodingparams; pub mod error; pub mod exportcodec; pub mod exportformat; -pub mod ffi; pub mod ffmpeg; pub mod footagedescription; pub mod frame; @@ -54,6 +56,51 @@ pub mod timecodemetadata; #[cfg(test)] mod realmedia_tests; +/// Process-wide test lock: serializes every test that reads or mutates +/// crate-global state (the injected decoder registry, the handle alive +/// count). One lock for the whole crate — tests race only with each +/// other, never with production code. +#[cfg(test)] +static TEST_LOCK: Mutex<()> = Mutex::new(()); + +/// RAII guard over [`TEST_LOCK`]: the lock is taken when the guard is +/// created ([`TestLock::acquire`]) and released when it is dropped — +/// including through panics, so a failing test can never deadlock the +/// tests that follow. Poison-tolerant: a panicking holder does not leave +/// the mutex poisoned for the next acquirer. +#[cfg(test)] +pub struct TestLock { + /// The held lock guard; dropping it releases [`TEST_LOCK`] (never read, + /// only dropped — the whole point of the RAII guard). + #[allow(dead_code)] + guard: MutexGuard<'static, ()>, +} + +#[cfg(test)] +impl TestLock { + /// Acquire exclusive access to the crate's shared test state, blocking + /// until every earlier holder has released it. + pub fn acquire() -> TestLock { + TestLock { + guard: TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()), + } + } +} + +#[cfg(test)] +impl Drop for TestLock { + fn drop(&mut self) { + // Dropping the held guard releases TEST_LOCK; the explicit Drop + // documents the acquire-on-create / release-on-drop contract. + } +} + +/// Acquire the process-wide test lock (see [`TestLock::acquire`]). +#[cfg(test)] +pub(crate) fn lock_tests() -> TestLock { + TestLock::acquire() +} + // Keep the oakffmpeg-link rlib referenced so its build script's native // link flags (the static FFmpeg's transitive dependencies) reach the // final link — rustc prunes the flags of an unreferenced rlib. diff --git a/crates/oakcodec/src/oiio.rs b/crates/oakcodec/src/oiio.rs index 1d6d3e5ff..0774aa66d 100644 --- a/crates/oakcodec/src/oiio.rs +++ b/crates/oakcodec/src/oiio.rs @@ -19,7 +19,7 @@ //! Mirrors `src/codec/src/oiio/{oiiodecoder,oiioencoder}.{h,cpp}`. OIIO //! frame conversion goes through the local //! [`crate::oiioframebridge`] helpers plus oakcommon's OIIO mapping -//! functions (`oakcommon_oiioutils_*` via `bridge/common.rs`). +//! functions. //! //! The OIIO dylib (`liboakoiio`) is not linked into this build, so every //! operation that would touch the media engine is a documented stub returning @@ -60,7 +60,7 @@ impl Decoder for OIIODecoder { fn probe( &self, _filename: &str, - _cancelled: Option<&crate::bridge::render::OakCancelAtom>, + _cancelled: Option<&oakcommon::cancelatom::CancelAtom>, ) -> Option { // Probing is a dylib operation; without it we cannot report anything. None @@ -98,7 +98,7 @@ impl Decoder for OIIODecoder { fn retrieve_video( &self, _p: &RetrieveVideoParams, - ) -> crate::error::Result { + ) -> crate::error::Result { Err(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string())) } @@ -118,7 +118,7 @@ impl Decoder for OIIODecoder { _sample_rate: i32, _channel_layout: u64, _sample_format: i32, - _cancelled: Option<&crate::bridge::render::OakCancelAtom>, + _cancelled: Option<&oakcommon::cancelatom::CancelAtom>, ) -> crate::error::Result<()> { Err(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string())) } diff --git a/crates/oakcodec/src/oiioframebridge.rs b/crates/oakcodec/src/oiioframebridge.rs index 5a1648c30..5dcf13307 100644 --- a/crates/oakcodec/src/oiioframebridge.rs +++ b/crates/oakcodec/src/oiioframebridge.rs @@ -30,10 +30,8 @@ //! timestamp and time base alongside the raw pixel rows, so a buffer can be //! turned back into an equivalent [`Frame`] without any external state. -use crate::bridge::common::{ - oakcommon_videoparams_get_time_base, oakcommon_videoparams_init_with_time_base, - oakcommon_videoparams_set_format, -}; +use oakcommon::ocioutils::PixelFormat as OakPixelFormat; +use oakcommon::videoparams::VideoParams; use crate::frame::Frame; use oakcore_rs::Rational; use std::ffi::c_int; @@ -154,14 +152,8 @@ pub fn oiio_frame_to_buffer(frame: &Frame) -> crate::error::Result> { let (time_base_num, time_base_den) = match frame.params() { Some(p) => { - let mut num = 0i64; - let mut den = 0i64; - // SAFETY: `num`/`den` are live mutable i64s and `p` is a valid - // handle; the C function only writes through the two out pointers. - unsafe { - oakcommon_videoparams_get_time_base(p.clone(), &mut num, &mut den); - } - (num, den) + let (num, den) = p.time_base(); + (i64::from(num), i64::from(den)) } None => (0, 0), }; @@ -215,26 +207,19 @@ pub fn oiio_buffer_to_frame(buffer: &[u8]) -> crate::error::Result { } // Build the params from the header, then hand ownership to the frame. - // SAFETY: the init returns a live handle; the clone for `set_format` is - // only read, and the original is moved into `Frame::with_params` (which - // takes ownership), so there is no double release. - let params = unsafe { - oakcommon_videoparams_init_with_time_base( - header.width, - header.height, - header.time_base_num as c_int, - header.time_base_den as c_int, - 0, - 4, - 1, - 1, - 0, - 1, - ) - }; - unsafe { - oakcommon_videoparams_set_format(params.clone(), header.format); - } + let mut params = VideoParams::new_with_time_base( + header.width, + header.height, + header.time_base_num as c_int, + header.time_base_den as c_int, + OakPixelFormat::from_code(0), + 4, + 1, + 1, + 0, + 1, + ); + params.set_format(OakPixelFormat::from_code(header.format)); let mut frame = Frame::with_params(params); frame.set_timestamp(Rational::new(header.timestamp_num, header.timestamp_den)); frame.allocate()?; @@ -351,11 +336,21 @@ mod tests { } /// A small helper to build a fully allocated, filled frame using the same - /// test-stub params pattern as `frame.rs`. + /// params pattern as `frame.rs`. fn make_frame() -> Frame { - // SAFETY: test-stub videoparams; ownership moves into `with_params`. - let params = unsafe { oakcommon_videoparams_init_with_time_base(100, 50, 1, 30, 0, 4, 1, 1, 0, 1) }; - unsafe { oakcommon_videoparams_set_format(params.clone(), 0) }; // U8 + let mut params = VideoParams::new_with_time_base( + 100, + 50, + 1, + 30, + OakPixelFormat::from_code(0), + 4, + 1, + 1, + 0, + 1, + ); + params.set_format(OakPixelFormat::from_code(0)); // U8 let mut frame = Frame::with_params(params); frame.set_timestamp(Rational::new(5, 2)); frame.allocate().unwrap(); diff --git a/crates/oakcodec/src/proxymanager.rs b/crates/oakcodec/src/proxymanager.rs index d7c7a7219..cecc94469 100644 --- a/crates/oakcodec/src/proxymanager.rs +++ b/crates/oakcodec/src/proxymanager.rs @@ -19,11 +19,12 @@ //! Mirrors `src/codec/src/proxymanager.h`. Stateless (NOTES.md): actual //! transcodes are delegated to the global task submit callback //! ([`crate::task`]); with no registrar, `get_or_start` reports the proxy -//! as missing. `proxy_params_from_config` reads the oakcommon config C ABI +//! as missing. `proxy_params_from_config` reads the oakcommon config store //! with the compiled-in defaults as fallback (1280x720 / divider 1 / crf 23 //! / "mp4" / "veryfast" / audio included). -use std::ffi::{c_char, CString}; +use oakcommon::configstore::ConfigStore; +use oakcommon::filefunctions::FileFunctions; use std::path::Path; /// Proxy state of a proxy file on disk. @@ -336,97 +337,35 @@ fn cstr_slice(a: &[u8; 32]) -> &str { /// `oakcommon_config_get_int` wrapper (null group). fn config_get_int(key: &str, default: i32) -> i32 { - let ckey = cstring(key); - // # Safety: `ckey` is a valid NUL-terminated C string alive for the call. - unsafe { - crate::bridge::common::oakcommon_config_get_int(std::ptr::null(), ckey.as_ptr(), default) - } + ConfigStore::instance().get_int(None, key, default) } /// `oakcommon_config_get_bool` wrapper (null group). fn config_get_bool(key: &str, default: i32) -> i32 { - let ckey = cstring(key); - // # Safety: `ckey` is a valid NUL-terminated C string alive for the call. - unsafe { - crate::bridge::common::oakcommon_config_get_bool(std::ptr::null(), ckey.as_ptr(), default) - } + ConfigStore::instance().get_bool(None, key, default) } -/// Two-stage `oakcommon_config_get` string read; `None` when the stored -/// value is empty or absent. +/// `oakcommon_config_get` string read; `None` when the stored value is +/// empty or absent. fn config_get_str(key: &str) -> Option { - let ckey = cstring(key); - // # Safety: `ckey` is valid; first call asks only for the required size. - let size = unsafe { - crate::bridge::common::oakcommon_config_get( - std::ptr::null(), - ckey.as_ptr(), - std::ptr::null_mut(), - 0, - ) - }; - if size <= 1 { - return None; + match ConfigStore::instance().get(None, key) { + Ok(s) if !s.is_empty() => Some(s), + _ => None, } - let mut buf = vec![0u8; size as usize]; - // # Safety: `buf` has `size` bytes; the call fills at most `size` bytes. - unsafe { - crate::bridge::common::oakcommon_config_get( - std::ptr::null(), - ckey.as_ptr(), - buf.as_mut_ptr() as *mut c_char, - size, - ); - } - let mut end = buf.len(); - while end > 0 && buf[end - 1] == 0 { - end -= 1; - } - Some(String::from_utf8_lossy(&buf[..end]).into_owned()) } -/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper (the bridge -/// returns a 64-bit id directly). +/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper. fn unique_file_identifier(filename: &str) -> String { - let c = match CString::new(filename) { - Ok(c) => c, - Err(_) => return String::new(), - }; - // # Safety: `c` is a valid NUL-terminated C string alive for the call. - let id = unsafe { - crate::bridge::common::oakcommon_filefunctions_get_unique_file_identifier(c.as_ptr()) - }; - format!("{}", id) + FileFunctions::new() + .get_unique_file_identifier(filename) + .unwrap_or_default() } -/// Two-stage `oakcommon_filefunctions_get_application_path` read. +/// `oakcommon_filefunctions_get_application_path` read. fn application_path() -> String { - // # Safety: first call asks only for the required size. - let size = unsafe { - crate::bridge::common::oakcommon_filefunctions_get_application_path(std::ptr::null_mut(), 0) - }; - if size <= 1 { - return String::new(); - } - let mut buf = vec![0u8; size as usize]; - // # Safety: `buf` has `size` bytes; the call fills at most `size` bytes. - unsafe { - crate::bridge::common::oakcommon_filefunctions_get_application_path( - buf.as_mut_ptr() as *mut c_char, - size, - ); - } - let mut end = buf.len(); - while end > 0 && buf[end - 1] == 0 { - end -= 1; - } - String::from_utf8_lossy(&buf[..end]).into_owned() -} - -/// Build a NUL-terminated C string from a Rust string; empty on embedded -/// NUL (defensive only — callers pass sane keys). -fn cstring(s: &str) -> CString { - CString::new(s).unwrap_or_else(|_| CString::new("").unwrap()) + FileFunctions::new() + .get_application_path() + .unwrap_or_default() } /// True when `p` is a regular file with at least one execute bit set. @@ -456,14 +395,6 @@ mod tests { dir.to_string_lossy().into_owned() } - fn fnv1a64(bytes: &[u8]) -> u64 { - let mut h: u64 = 14695981039346656037; - for &b in bytes { - h ^= b as u64; - h = h.wrapping_mul(1099511628211); - } - h - } #[test] fn proxy_params_default_values() { @@ -503,24 +434,40 @@ mod tests { #[test] fn proxy_filename_derivation() { let cache = temp_subdir("fn"); - let stub_id = fnv1a64(b"media.mp4") as i64; - let id = fnv1a64(stub_id.to_string().as_bytes()) as i64; let p = ProxyManager::proxy_params_default(); - let f = ProxyManager::get_proxy_filename(&cache, "media.mp4", 0, &p).unwrap(); - assert_eq!(f, format!("{}/proxy/{}-0.1280x720.v1.a1.mp4", cache, id)); + // A missing source carries no unique-file identifier + // (get_unique_file_identifier is empty for non-existent files — + // C++ parity), so the name is the plain size/version/audio tags. + let missing = std::path::Path::new(&temp_subdir("missing")).join("nope.mp4"); + let f = ProxyManager::get_proxy_filename(&cache, missing.to_str().unwrap(), 0, &p).unwrap(); + assert_eq!(f, format!("{}/proxy/-0.1280x720.v1.a1.mp4", cache)); + + // An existing source embeds a stable per-file identifier. + let existing = std::path::Path::new(&temp_subdir("existing")).join("real.mp4"); + std::fs::write(&existing, b"media").unwrap(); + let f1 = + ProxyManager::get_proxy_filename(&cache, existing.to_str().unwrap(), 0, &p).unwrap(); + let f2 = + ProxyManager::get_proxy_filename(&cache, existing.to_str().unwrap(), 0, &p).unwrap(); + assert!( + f1.contains("-0.1280x720.v1.a1.mp4"), + "size/version/audio tags present: {f1}" + ); + assert!(f1 != format!("{}/proxy/-0.1280x720.v1.a1.mp4", cache), "id embedded: {f1}"); + assert_eq!(f1, f2, "the identifier is stable for the same file"); // Divider mode tags the divider instead of an absolute size. let mut d = p.clone(); d.divider = 2; - let f2 = ProxyManager::get_proxy_filename(&cache, "media.mp4", 0, &d).unwrap(); - assert!(f2.contains(".div2.")); + let f3 = ProxyManager::get_proxy_filename(&cache, missing.to_str().unwrap(), 0, &d).unwrap(); + assert!(f3.contains(".div2.")); // No audio. let mut na = p.clone(); na.include_audio = 0; - let f3 = ProxyManager::get_proxy_filename(&cache, "media.mp4", 0, &na).unwrap(); - assert!(f3.contains(".a0.")); + let f4 = ProxyManager::get_proxy_filename(&cache, missing.to_str().unwrap(), 0, &na).unwrap(); + assert!(f4.contains(".a0.")); } #[test] diff --git a/crates/oakcodec/src/realmedia_tests.rs b/crates/oakcodec/src/realmedia_tests.rs index 87ac432ab..2c4c26e41 100644 --- a/crates/oakcodec/src/realmedia_tests.rs +++ b/crates/oakcodec/src/realmedia_tests.rs @@ -21,16 +21,13 @@ //! 48kHz stereo) and a full H.264 encode round-trip through `/tmp`. //! //! They live inside the crate (not `tests/`) because the crate's -//! `#[cfg(test)]` in-memory oakcommon/oakrender stubs — which the -//! `Frame`/`FootageDescription` paths need — are only linked for the lib -//! test binary (`tests/` is compiled without `#[cfg(test)]` and cannot -//! resolve those symbols; see `tests/ffi_contract_test.rs`). +//! `#[cfg(test)]` in-memory stubs — which the `Frame`/`FootageDescription` +//! paths need — are only linked for the lib test binary (`tests/` is +//! compiled without `#[cfg(test)]` and cannot resolve those symbols; see +//! `tests/ffi_contract_test.rs`). -use crate::bridge::common::{ - oakcommon_videoparams_get_duration, oakcommon_videoparams_get_frame_rate, - oakcommon_videoparams_get_height, oakcommon_videoparams_get_width, - oakcommon_videoparams_init_basic, oakcommon_videoparams_set_format, -}; +use oakcommon::ocioutils::PixelFormat as OakPixelFormat; +use oakcommon::videoparams::VideoParams; use crate::decoder::{ CodecStream, Decoder, RenderMode, RetrieveAudioStatus, RetrieveVideoParams, K_COLOR_RANGE_DEFAULT, @@ -80,8 +77,8 @@ fn h264_params(out: &std::path::Path) -> crate::encodingparams::EncodingParams { /// Build an allocated F32-RGBA frame with a moving color pattern. fn pattern_frame(i: i32) -> Frame { - let vp = unsafe { oakcommon_videoparams_init_basic(64, 64, 0, 4, 1, 1, 0, 1) }; - unsafe { oakcommon_videoparams_set_format(vp.clone(), PixelFormat::F32 as i32) }; + let mut vp = VideoParams::new_basic(64, 64, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1); + vp.set_format(OakPixelFormat::from_code(PixelFormat::F32 as i32)); let mut f = Frame::with_params(vp); f.set_timestamp(Rational::new(i as i64, 10)); f.allocate().unwrap(); @@ -117,20 +114,10 @@ fn probe_reports_streams_and_duration() { // Video stream: 1920x1080, 25fps, 17s at 1/12800 time base. let vp = desc.get_video_stream(0).expect("video stream"); - assert_eq!(unsafe { oakcommon_videoparams_get_width(vp.clone()) }, 1920); - assert_eq!( - unsafe { oakcommon_videoparams_get_height(vp.clone()) }, - 1080 - ); - assert_eq!( - unsafe { oakcommon_videoparams_get_duration(vp.clone()) }, - 17 * 12800 - ); - - let mut num: i32 = 0; - let mut den: i32 = 0; - unsafe { oakcommon_videoparams_get_frame_rate(vp.clone(), &mut num, &mut den) }; - assert_eq!((num, den), (25, 1)); + assert_eq!(vp.width(), 1920); + assert_eq!(vp.height(), 1080); + assert_eq!(vp.duration(), 17 * 12800); + assert_eq!(vp.frame_rate(), (25, 1)); } #[test] @@ -220,8 +207,8 @@ fn encode_h264_roundtrip_to_tmp() { let desc = d.probe(&out_str, None).expect("probe round-trip output"); assert_eq!(desc.video_stream_count(), 1); let vp = desc.get_video_stream(0).expect("video stream"); - assert_eq!(unsafe { oakcommon_videoparams_get_width(vp.clone()) }, 64); - assert_eq!(unsafe { oakcommon_videoparams_get_height(vp.clone()) }, 64); + assert_eq!(vp.width(), 64); + assert_eq!(vp.height(), 64); // Decode the first frame of the result. let s = CodecStream::with_block(out_str.clone(), 0, None); diff --git a/crates/oakcodec/src/testmedia.rs b/crates/oakcodec/src/testmedia.rs index 8952030c6..34d7d6077 100644 --- a/crates/oakcodec/src/testmedia.rs +++ b/crates/oakcodec/src/testmedia.rs @@ -37,11 +37,10 @@ use std::path::Path; +use oakcommon::ocioutils::PixelFormat as OakPixelFormat; +use oakcommon::videoparams::VideoParams; use oakcore_rs::{PixelFormat, Rational, SampleFormat}; -use crate::bridge::common::{ - oakcommon_videoparams_init_basic, oakcommon_videoparams_set_format, -}; use crate::encodingparams::EncodingParams; use crate::encoder::create_from_params; use crate::frame::Frame; @@ -106,8 +105,17 @@ pub fn write_test_clip( /// One frame of the known pattern (see module doc). fn pattern_frame(i: i32, width: i32, height: i32, fps: i32) -> Frame { - let vp = unsafe { oakcommon_videoparams_init_basic(width, height, 0, 4, 1, 1, 0, 1) }; - unsafe { oakcommon_videoparams_set_format(vp.clone(), PixelFormat::F32 as i32) }; + let mut vp = VideoParams::new_basic( + width, + height, + OakPixelFormat::from_code(0), + 4, + 1, + 1, + 0, + 1, + ); + vp.set_format(OakPixelFormat::from_code(PixelFormat::F32 as i32)); let mut f = Frame::with_params(vp); f.set_timestamp(Rational::new(i as i64, fps as i64)); f.allocate().expect("test frame allocation"); diff --git a/crates/oakcommon/src/configstore.rs b/crates/oakcommon/src/configstore.rs index c630e448d..dc1fabe9d 100644 --- a/crates/oakcommon/src/configstore.rs +++ b/crates/oakcommon/src/configstore.rs @@ -729,7 +729,7 @@ mod tests { assert_eq!(format_g(-0.0), "-0"); assert_eq!(format_g(1920.0), "1920"); assert_eq!(format_g(100.0), "100"); - assert_eq!(format_g(3.14), "3.14"); + assert_eq!(format_g(3.15), "3.15"); assert_eq!(format_g(1.5), "1.5"); assert_eq!(format_g(0.1), "0.1"); assert_eq!(format_g(-3.5), "-3.5"); @@ -848,9 +848,9 @@ mod tests { let _g = test_lock().lock().unwrap(); let s = ConfigStore::instance(); s.reset_defaults().unwrap(); - s.set_double(None, "MyD", 3.14); - assert_eq!(s.get_double(None, "MyD", -1.0), 3.14); - assert_eq!(s.get(None, "MyD").unwrap(), "3.14"); + s.set_double(None, "MyD", 3.15); + assert_eq!(s.get_double(None, "MyD", -1.0), 3.15); + assert_eq!(s.get(None, "MyD").unwrap(), "3.15"); assert_eq!(s.entry_type(None, "MyD").unwrap(), EntryType::Double); assert_eq!(s.get_double(None, "MissingD", 2.5), 2.5); diff --git a/crates/oakcommon/src/ffi.rs b/crates/oakcommon/src/ffi.rs deleted file mode 100644 index 6436c1844..000000000 --- a/crates/oakcommon/src/ffi.rs +++ /dev/null @@ -1,3470 +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 . - -//! C ABI surface, one `#[no_mangle]` export per function declared in -//! `include/common/*.h`. -//! -//! Every handle typedef in these headers (`OakColorTransform`, -//! `OakVideoParams`, `OakSubtitleParams`, `OakXmlReader`, `OakXmlWriter`, -//! `OakFileFunctions`, `OakCurrent`, `OakCommandLineParser`, -//! `OakCommandLineOption`, `OakCommandLinePositionalArgument`, -//! `OakOCIOUtils`, `OakOIIOUtils`) has the identical layout -//! `{void *ctx; void (*addref)(void*); void (*release)(void*); -//! uint32_t abi_version;}`, so [`crate::handle::CHandle`] is used for all -//! of them (same precedent as the oakundo/oaknode/oakcodec crates). -//! -//! # Coverage inventory -//! -//! | Header | Domain module | Notes | -//! |---|---|---| -//! | error.h | `crate::error` | constants only, no functions | -//! | handle.h | `crate::handle` | documentation only | -//! | miscutils.h | `crate::miscutils` | decibel/lerp | -//! | loopmode.h | `crate::miscutils` | enum only | -//! | dropworkflowbehavior.h | `crate::miscutils` | enum + name/is_valid | -//! | power.h | `crate::miscutils` | power helpers | -//! | current.h | `crate::miscutils` | `Current` singleton | -//! | config.h | `crate::configstore` | singleton, no handle | -//! | debug.h | `crate::debug` | `oakcommon_log` is C-variadic, omitted | -//! | ffmpegutils.h | `crate::ffmpegutils` | | -//! | colortransform.h | `crate::colortransform` | native fns are C++-only | -//! | commandlineparser.h | `crate::commandlineparser` | | -//! | videoparams.h | `crate::videoparams` | native fns are C++-only | -//! | subtitleparams.h | `crate::subtitleparams` | native fn is C++-only | -//! | xmlutils.h | `crate::xmlutils` | native fns are C++-only | -//! | filefunctions.h | `crate::filefunctions` | | -//! | ocioutils.h | `crate::ocioutils` | | -//! | oiioutils.h | `crate::oiioutils` | | -//! | qtutils.h | `crate::qtutils` | | - -use std::ffi::c_char; -use std::ffi::c_void; - -use std::ffi::CStr; - -use crate::handle::{guard_void, CHandle}; -use crate::miscutils::DestroyFn; - -/// NUL-terminated C string as `&str` (null / invalid UTF-8 -> empty string). -fn cstr<'a>(p: *const c_char) -> &'a str { - if p.is_null() { - return ""; - } - unsafe { CStr::from_ptr(p) }.to_str().unwrap_or("") -} - -/// 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: i32) -> i32 { - let required = (value.len() + 1) as i32; - if !buf.is_null() && buf_size >= required { - unsafe { - std::ptr::copy_nonoverlapping(value.as_ptr() as *const c_char, buf, value.len()); - *buf.add(value.len()) = 0; - } - } - required -} - -/// Truncating copy for `commandlineoption_get_setting` / positionals. -fn copy_setting(value: &str, buf: *mut c_char, buf_size: i32) -> i32 { - let required = (value.len() + 1) as i32; - if !buf.is_null() && buf_size > 0 { - let n = value.len().min(buf_size as usize - 1); - unsafe { - std::ptr::copy_nonoverlapping(value.as_ptr() as *const c_char, buf, n); - *buf.add(n) = 0; - } - } - required -} - -/// Whether `(buf, buf_size)` is a valid two-stage getter output. -fn is_valid_string_out(buf: *mut c_char, buf_size: i32) -> bool { - buf_size >= 0 && (buf_size == 0 || !buf.is_null()) -} - -/// group pointer -> `Option<&str>` (null -> `None`). -fn group_opt<'a>(group: *const c_char) -> Option<&'a str> { - if group.is_null() { - None - } else { - Some(cstr(group)) - } -} - -/// Release a `CHandle` in place: call its release callback, then write -/// null back. -fn free_handle(p: *mut CHandle) { - if p.is_null() { - return; - } - guard_void(|| unsafe { - if let Some(h) = p.as_ref() { - if let Some(rel) = h.release { - rel(h.ctx); - } - } - p.write(CHandle::null()); - }) -} - -/// Handle-returning exports return an empty handle; use [`CHandle::null`]. -pub mod colortransform { - //! `include/common/colortransform.h`. - //! - //! The C++-only `oakcommon_colortransform_init_from_native` / - //! `oakcommon_colortransform_get_native` deal with - //! `olive::ColorTransform` and are served by the C++ adapter layer. - - use super::{cstr, CHandle}; - use crate::colortransform::ColorTransform; - use crate::handle::{guard_handle, make_owned}; - - /// Create a transform targeting an output color space. - #[no_mangle] - pub extern "C" fn oakcommon_colortransform_init_output( - output: *const super::c_char, - ) -> CHandle { - if output.is_null() { - return CHandle::null(); - } - guard_handle(|| Ok(make_owned(ColorTransform::new_output(cstr(output))))) - } - - /// Create a transform targeting a display/view/look. - #[no_mangle] - pub extern "C" fn oakcommon_colortransform_init_display( - display: *const super::c_char, - view: *const super::c_char, - look: *const super::c_char, - ) -> CHandle { - if display.is_null() || view.is_null() || look.is_null() { - return CHandle::null(); - } - guard_handle(|| { - Ok(make_owned(ColorTransform::new_display( - cstr(display), - cstr(view), - cstr(look), - ))) - }) - } - - /// Release one reference (NULL/empty no-op). - #[no_mangle] - pub extern "C" fn oakcommon_colortransform_free(transform: *mut CHandle) { - super::free_handle(transform); - } - - /// 1 when the transform targets display/view/look, 0 for output - /// transforms, negative on empty handle. - #[no_mangle] - pub extern "C" fn oakcommon_colortransform_is_display(transform: CHandle) -> i32 { - match unsafe { crate::handle::get::(&transform) } { - Some(ct) => { - if ct.is_display() { - 1 - } else { - 0 - } - } - None => crate::error::OAKCOMMON_E_INVALID, - } - } - - /// Display name (two-stage string). - #[no_mangle] - pub extern "C" fn oakcommon_colortransform_get_display( - transform: CHandle, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - match unsafe { crate::handle::get::(&transform) } { - Some(ct) => super::copy_string(ct.display(), buf, buf_size), - None => crate::error::OAKCOMMON_E_INVALID, - } - } - - /// Output color space (two-stage string). - #[no_mangle] - pub extern "C" fn oakcommon_colortransform_get_output( - transform: CHandle, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - match unsafe { crate::handle::get::(&transform) } { - Some(ct) => super::copy_string(ct.output(), buf, buf_size), - None => crate::error::OAKCOMMON_E_INVALID, - } - } - - /// View name (two-stage string). - #[no_mangle] - pub extern "C" fn oakcommon_colortransform_get_view( - transform: CHandle, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - match unsafe { crate::handle::get::(&transform) } { - Some(ct) => super::copy_string(ct.view(), buf, buf_size), - None => crate::error::OAKCOMMON_E_INVALID, - } - } - - /// Look name (two-stage string). - #[no_mangle] - pub extern "C" fn oakcommon_colortransform_get_look( - transform: CHandle, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - match unsafe { crate::handle::get::(&transform) } { - Some(ct) => super::copy_string(ct.look(), buf, buf_size), - None => crate::error::OAKCOMMON_E_INVALID, - } - } -} - -/// Command-line parser and its option/argument handles. -pub mod commandlineparser { - //! `include/common/commandlineparser.h`. - - use std::ffi::{CStr, CString}; - - use super::{copy_setting, cstr, is_valid_string_out, CHandle}; - use crate::commandlineparser::{ - CommandLineOption, CommandLineParser, CommandLinePositionalArgument, - }; - use crate::error::{Error, OAKCOMMON_E_INVALID, OAKCOMMON_OK}; - use crate::handle::{get, guard, guard_handle, make_borrowed, make_owned}; - - /// Create a new parser. - #[no_mangle] - pub extern "C" fn oakcommon_commandlineparser_init() -> CHandle { - guard_handle(|| Ok(make_owned(CommandLineParser::new()))) - } - - /// Release one reference to a parser. - #[no_mangle] - pub extern "C" fn oakcommon_commandlineparser_free(parser: *mut CHandle) { - super::free_handle(parser); - } - - /// Set the application name/version shown by `print_help`. - #[no_mangle] - pub extern "C" fn oakcommon_commandlineparser_set_app_info( - parser: CHandle, - name: *const super::c_char, - version: *const super::c_char, - ) -> i32 { - if parser.is_null() || name.is_null() || version.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(parser.ctx as *mut CommandLineParser) }; - p.set_app_info(cstr(name), cstr(version)); - Ok(()) - }) - } - - /// Register a named option; writes the option handle to `out_option`. - #[no_mangle] - pub extern "C" fn oakcommon_commandlineparser_add_option( - parser: CHandle, - names: *const *const super::c_char, - name_count: i32, - description: *const super::c_char, - takes_arg: i32, - arg_placeholder: *const super::c_char, - hidden: i32, - out_option: *mut CHandle, - ) -> i32 { - if parser.is_null() - || names.is_null() - || name_count <= 0 - || description.is_null() - || arg_placeholder.is_null() - || out_option.is_null() - { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(parser.ctx as *mut CommandLineParser) }; - let mut ns: Vec = Vec::with_capacity(name_count as usize); - for i in 0..name_count { - let s = unsafe { CStr::from_ptr(*names.add(i as usize)) }; - let s = s.to_str().map_err(|_| Error::Invalid)?; - ns.push(CString::new(s).map_err(|_| Error::Invalid)?); - } - p.add_option( - &ns, - cstr(description), - takes_arg != 0, - cstr(arg_placeholder), - hidden != 0, - )?; - let idx = p.option_count() - 1; - let opt = p.option(idx).ok_or(Error::State)?; - unsafe { - *out_option = - make_borrowed(opt as *const CommandLineOption as *mut CommandLineOption); - } - Ok(()) - }) - } - - /// Register a positional argument; writes the argument handle to - /// `out_argument`. - #[no_mangle] - pub extern "C" fn oakcommon_commandlineparser_add_positional_argument( - parser: CHandle, - name: *const super::c_char, - description: *const super::c_char, - required: i32, - out_argument: *mut CHandle, - ) -> i32 { - if parser.is_null() || name.is_null() || description.is_null() || out_argument.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(parser.ctx as *mut CommandLineParser) }; - p.add_positional_argument(cstr(name), cstr(description), required != 0)?; - let idx = p.positional_count() - 1; - let arg = p.positional(idx).ok_or(Error::State)?; - unsafe { - *out_argument = make_borrowed( - arg as *const CommandLinePositionalArgument - as *mut CommandLinePositionalArgument, - ); - } - Ok(()) - }) - } - - /// Parse an argv-style argument list. - #[no_mangle] - pub extern "C" fn oakcommon_commandlineparser_process( - parser: CHandle, - argv: *const *const super::c_char, - argc: i32, - ) -> i32 { - if parser.is_null() || argv.is_null() || argc < 0 { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(parser.ctx as *mut CommandLineParser) }; - let mut av: Vec = Vec::with_capacity(argc as usize); - for i in 0..argc { - av.push(unsafe { CStr::from_ptr(*argv.add(i as usize)) }.to_owned()); - } - p.process(&av)?; - Ok(()) - }) - } - - /// Print usage/help text. - #[no_mangle] - pub extern "C" fn oakcommon_commandlineparser_print_help( - parser: CHandle, - filename: *const super::c_char, - ) -> i32 { - if parser.is_null() || filename.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(parser.ctx as *const CommandLineParser) }; - p.print_help(cstr(filename))?; - Ok(()) - }) - } - - /// Whether the option was present on the command line. - #[no_mangle] - pub extern "C" fn oakcommon_commandlineoption_is_set( - option: CHandle, - is_set: *mut bool, - ) -> i32 { - if option.is_null() || is_set.is_null() { - return OAKCOMMON_E_INVALID; - } - let o = match unsafe { get::(&option) } { - Some(o) => o, - None => return OAKCOMMON_E_INVALID, - }; - unsafe { - *is_set = o.is_set(); - } - OAKCOMMON_OK - } - - /// Release one reference to an option. - #[no_mangle] - pub extern "C" fn oakcommon_commandlineoption_free(option: *mut CHandle) { - super::free_handle(option); - } - - /// The option's argument value (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_commandlineoption_get_setting( - option: CHandle, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if option.is_null() || !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - let s = match unsafe { get::(&option) } { - Some(o) => o.get_setting().unwrap_or_default(), - None => return OAKCOMMON_E_INVALID, - }; - copy_setting(s, buf, buf_size) - } - - /// Set the option's argument value. - #[no_mangle] - pub extern "C" fn oakcommon_commandlineoption_set_setting( - option: CHandle, - value: *const super::c_char, - ) -> i32 { - if option.is_null() || value.is_null() { - return OAKCOMMON_E_INVALID; - } - let o = unsafe { &mut *(option.ctx as *mut CommandLineOption) }; - o.set_setting(cstr(value)) - .map(|_| OAKCOMMON_OK) - .unwrap_or_else(|e| e.code()) - } - - /// The positional argument's value (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_commandlinepositionalargument_get_setting( - argument: CHandle, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if argument.is_null() || !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - let s = match unsafe { get::(&argument) } { - Some(a) => a.get_setting().unwrap_or_default(), - None => return OAKCOMMON_E_INVALID, - }; - copy_setting(s, buf, buf_size) - } - - /// Set the positional argument's value. - #[no_mangle] - pub extern "C" fn oakcommon_commandlinepositionalargument_set_setting( - argument: CHandle, - value: *const super::c_char, - ) -> i32 { - if argument.is_null() || value.is_null() { - return OAKCOMMON_E_INVALID; - } - let a = unsafe { &mut *(argument.ctx as *mut CommandLinePositionalArgument) }; - a.set_setting(cstr(value)) - .map(|_| OAKCOMMON_OK) - .unwrap_or_else(|e| e.code()) - } - - /// Release one reference to a positional argument. - #[no_mangle] - pub extern "C" fn oakcommon_commandlinepositionalargument_free(argument: *mut CHandle) { - super::free_handle(argument); - } -} - -/// Process-wide configuration store (no handle). -pub mod config { - //! `include/common/config.h`. - - use super::{copy_string, cstr, group_opt, is_valid_string_out}; - use crate::configstore::{ConfigStore, EntryType, ErrorHandler}; - use crate::error::{OAKCOMMON_E_INVALID, OAKCOMMON_OK}; - - fn store() -> &'static ConfigStore { - ConfigStore::instance() - } - - /// Load `config.ini` (a missing file is not an error). - #[no_mangle] - pub extern "C" fn oakcommon_config_load() -> i32 { - match store().load() { - Ok(()) => OAKCOMMON_OK, - Err(e) => e.code(), - } - } - - /// Persist the current store. - #[no_mangle] - pub extern "C" fn oakcommon_config_save() -> i32 { - match store().save() { - Ok(()) => OAKCOMMON_OK, - Err(e) => e.code(), - } - } - - /// Reset to compiled-in defaults. - #[no_mangle] - pub extern "C" fn oakcommon_config_reset_defaults() -> i32 { - match store().reset_defaults() { - Ok(()) => OAKCOMMON_OK, - Err(e) => e.code(), - } - } - - /// Set a string entry. - #[no_mangle] - pub extern "C" fn oakcommon_config_set( - group: *const super::c_char, - key: *const super::c_char, - value_utf8: *const super::c_char, - ) { - if key.is_null() || value_utf8.is_null() { - return; - } - store().set(group_opt(group), cstr(key), cstr(value_utf8)); - } - - /// Read an entry as a string (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_config_get( - group: *const super::c_char, - key: *const super::c_char, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if key.is_null() || !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - match store().get(group_opt(group), cstr(key)) { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// Read an INT entry; `fallback` when absent or a different type. - #[no_mangle] - pub extern "C" fn oakcommon_config_get_int( - group: *const super::c_char, - key: *const super::c_char, - fallback: i32, - ) -> i32 { - if key.is_null() { - return fallback; - } - store().get_int(group_opt(group), cstr(key), fallback) - } - - /// Read a DOUBLE entry; `fallback` when absent or a different type. - #[no_mangle] - pub extern "C" fn oakcommon_config_get_double( - group: *const super::c_char, - key: *const super::c_char, - fallback: f64, - ) -> f64 { - if key.is_null() { - return fallback; - } - store().get_double(group_opt(group), cstr(key), fallback) - } - - /// Set an INT entry. - #[no_mangle] - pub extern "C" fn oakcommon_config_set_int( - group: *const super::c_char, - key: *const super::c_char, - v: i32, - ) { - if key.is_null() { - return; - } - store().set_int(group_opt(group), cstr(key), v); - } - - /// Read an INT entry as int64; `fallback` when absent or a different - /// type. - #[no_mangle] - pub extern "C" fn oakcommon_config_get_int64( - group: *const super::c_char, - key: *const super::c_char, - fallback: i64, - ) -> i64 { - if key.is_null() { - return fallback; - } - store().get_int64(group_opt(group), cstr(key), fallback) - } - - /// Set an INT entry as int64. - #[no_mangle] - pub extern "C" fn oakcommon_config_set_int64( - group: *const super::c_char, - key: *const super::c_char, - v: i64, - ) { - if key.is_null() { - return; - } - store().set_int64(group_opt(group), cstr(key), v); - } - - /// Read a BOOL entry as 0/1; `fallback` when absent or a different type. - #[no_mangle] - pub extern "C" fn oakcommon_config_get_bool( - group: *const super::c_char, - key: *const super::c_char, - fallback: i32, - ) -> i32 { - if key.is_null() { - return fallback; - } - store().get_bool(group_opt(group), cstr(key), fallback) - } - - /// Set a BOOL entry. - #[no_mangle] - pub extern "C" fn oakcommon_config_set_bool( - group: *const super::c_char, - key: *const super::c_char, - v: i32, - ) { - if key.is_null() { - return; - } - store().set_bool(group_opt(group), cstr(key), v); - } - - /// Set a DOUBLE entry. - #[no_mangle] - pub extern "C" fn oakcommon_config_set_double( - group: *const super::c_char, - key: *const super::c_char, - v: f64, - ) { - if key.is_null() { - return; - } - store().set_double(group_opt(group), cstr(key), v); - } - - /// Entry type code of a key (`OAKCOMMON_CONFIG_ENTRY_*`), or a negative - /// `OAKCOMMON_E_*` error code. - #[no_mangle] - pub extern "C" fn oakcommon_config_entry_type( - group: *const super::c_char, - key: *const super::c_char, - ) -> i32 { - if key.is_null() { - return OAKCOMMON_E_INVALID; - } - match store().entry_type(group_opt(group), cstr(key)) { - Ok(EntryType::None) => 0, - Ok(EntryType::String) => 1, - Ok(EntryType::Int) => 2, - Ok(EntryType::Double) => 3, - Ok(EntryType::Bool) => 4, - Err(e) => e.code(), - } - } - - /// Register (or clear, with a null handler) the error handler. - #[no_mangle] - pub extern "C" fn oakcommon_config_set_error_handler( - handler: ErrorHandler, - userdata: *mut super::c_void, - ) -> i32 { - match store().set_error_handler(handler, userdata) { - Ok(()) => OAKCOMMON_OK, - Err(e) => e.code(), - } - } -} - -/// Debug/logging helpers. -pub mod debug { - //! `include/common/debug.h`. - //! - //! `oakcommon_log` is a C variadic (`(int level, const char *fmt, ...)`) - //! which Rust cannot export; it is served by the C++ adapter layer and - //! is intentionally absent here. Domain callers use - //! `crate::debug::log(level, msg)` instead. - - use super::{copy_string, cstr}; - use crate::debug::{log_get_level, log_raw, log_set_level, Level}; - use crate::error::{OAKCOMMON_E_INVALID, OAKCOMMON_OK}; - - /// Log a message at a given level. - #[no_mangle] - pub extern "C" fn oakcommon_debug_log(level: i32, msg: *const super::c_char) -> i32 { - if msg.is_null() { - return OAKCOMMON_E_INVALID; - } - match log_raw(level, cstr(msg)) { - Ok(()) => OAKCOMMON_OK, - Err(e) => e.code(), - } - } - - /// Human-readable name for a debug level (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_debug_level_name( - level: i32, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - let name = Level::from_code(level) - .map(Level::name) - .unwrap_or("UNKNOWN"); - copy_string(name, buf, buf_size) - } - - /// Set the minimum logging level. - #[no_mangle] - pub extern "C" fn oakcommon_log_set_level(level: i32) -> i32 { - match Level::from_code(level) { - Some(l) => { - log_set_level(l); - OAKCOMMON_OK - } - None => OAKCOMMON_E_INVALID, - } - } - - /// Get the current minimum logging level. - #[no_mangle] - pub extern "C" fn oakcommon_log_get_level(out_level: *mut i32) -> i32 { - if out_level.is_null() { - return OAKCOMMON_E_INVALID; - } - unsafe { - *out_level = log_get_level() as i32; - } - OAKCOMMON_OK - } -} - -/// Placeholder module for constants-only headers. -pub mod error { - //! `include/common/error.h` exposes no functions; the `OAKCOMMON_OK` / - //! `OAKCOMMON_E_*` constants live in `crate::error`. -} - -/// FFmpeg format helpers. -pub mod ffmpegutils { - //! `include/common/ffmpegutils.h`. `OAKCOMMON_RGB_CHANNEL_COUNT` / - //! `OAKCOMMON_RGBA_CHANNEL_COUNT` live in `crate::ffmpegutils`. - - use crate::error::{OAKCOMMON_E_INVALID, OAKCOMMON_OK}; - - /// Write a value into a non-null out-param, returning `OK`. - fn fill(value: i32, out: *mut i32) -> i32 { - if out.is_null() { - return OAKCOMMON_E_INVALID; - } - unsafe { - *out = value; - } - OAKCOMMON_OK - } - - /// Find the nearest compatible bridge pixel format. - #[no_mangle] - pub extern "C" fn oakcommon_ffmpegutils_get_compatible_bridge_pixel_format( - pix_fmt: i32, - maximum_pix_fmt: i32, - out: *mut i32, - ) -> i32 { - fill( - crate::ffmpegutils::get_compatible_bridge_pixel_format(pix_fmt, maximum_pix_fmt), - out, - ) - } - - /// Find the nearest compatible pixel format. - #[no_mangle] - pub extern "C" fn oakcommon_ffmpegutils_get_compatible_pixel_format( - pix_fmt: i32, - out: *mut i32, - ) -> i32 { - fill( - crate::ffmpegutils::get_compatible_pixel_format(pix_fmt), - out, - ) - } - - /// Map a native format + channel count to an FFmpeg pixel format. - #[no_mangle] - pub extern "C" fn oakcommon_ffmpegutils_get_ffmpeg_pixel_format( - pix_fmt: i32, - channel_count: i32, - out: *mut i32, - ) -> i32 { - fill( - crate::ffmpegutils::get_ffmpeg_pixel_format(pix_fmt, channel_count), - out, - ) - } - - /// Map an FFmpeg sample format to a native sample format. - #[no_mangle] - pub extern "C" fn oakcommon_ffmpegutils_get_native_sample_format( - smp_fmt: i32, - out: *mut i32, - ) -> i32 { - fill(crate::ffmpegutils::get_native_sample_format(smp_fmt), out) - } - - /// Map a native sample format to an FFmpeg sample format. - #[no_mangle] - pub extern "C" fn oakcommon_ffmpegutils_get_ffmpeg_sample_format( - smp_fmt: i32, - out: *mut i32, - ) -> i32 { - fill(crate::ffmpegutils::get_ffmpeg_sample_format(smp_fmt), out) - } - - /// Convert a JPEG-range pixel format to its regular-space equivalent. - #[no_mangle] - pub extern "C" fn oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space( - pix_fmt: i32, - out: *mut i32, - ) -> i32 { - fill( - crate::ffmpegutils::convert_jpeg_space_to_regular_space(pix_fmt), - out, - ) - } -} - -/// Filesystem helper handle. -pub mod filefunctions { - //! `include/common/filefunctions.h`. - - use super::{copy_string, cstr, is_valid_string_out, CHandle}; - use crate::error::{OAKCOMMON_E_FAILED, OAKCOMMON_E_INVALID, OAKCOMMON_OK}; - use crate::filefunctions::FileFunctions; - use crate::handle::{get, make_owned}; - - /// Create a FileFunctions handle. - #[no_mangle] - pub extern "C" fn oakcommon_filefunctions_init() -> CHandle { - make_owned(FileFunctions::new()) - } - - /// Release one reference. - #[no_mangle] - pub extern "C" fn oakcommon_filefunctions_free(self_: *mut CHandle) { - super::free_handle(self_); - } - - /// Unique identifier for a file (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_filefunctions_get_unique_file_identifier( - self_: CHandle, - filename: *const super::c_char, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if self_.is_null() || filename.is_null() || !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - let ff = match unsafe { get::(&self_) } { - Some(f) => f, - None => return OAKCOMMON_E_INVALID, - }; - match ff.get_unique_file_identifier(cstr(filename)) { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// Configuration directory (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_filefunctions_get_configuration_location( - self_: CHandle, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if self_.is_null() || !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - let ff = match unsafe { get::(&self_) } { - Some(f) => f, - None => return OAKCOMMON_E_INVALID, - }; - match ff.get_configuration_location() { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// Application directory (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_filefunctions_get_application_path( - self_: CHandle, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if self_.is_null() || !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - let ff = match unsafe { get::(&self_) } { - Some(f) => f, - None => return OAKCOMMON_E_INVALID, - }; - match ff.get_application_path() { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// Temp file directory (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_filefunctions_get_temp_file_path( - self_: CHandle, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if self_.is_null() || !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - let ff = match unsafe { get::(&self_) } { - Some(f) => f, - None => return OAKCOMMON_E_INVALID, - }; - match ff.get_temp_file_path() { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// Auto-recovery root (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_filefunctions_get_auto_recovery_root( - self_: CHandle, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if self_.is_null() || !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - let ff = match unsafe { get::(&self_) } { - Some(f) => f, - None => return OAKCOMMON_E_INVALID, - }; - match ff.get_auto_recovery_root() { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// Whether a directory can be copied without overwriting anything. - #[no_mangle] - pub extern "C" fn oakcommon_filefunctions_can_copy_directory_without_overwriting( - self_: CHandle, - source: *const super::c_char, - dest: *const super::c_char, - out: *mut i32, - ) -> i32 { - if self_.is_null() || source.is_null() || dest.is_null() || out.is_null() { - return OAKCOMMON_E_INVALID; - } - let ff = match unsafe { get::(&self_) } { - Some(f) => f, - None => return OAKCOMMON_E_INVALID, - }; - unsafe { - *out = ff.can_copy_directory_without_overwriting(cstr(source), cstr(dest)) as i32; - } - OAKCOMMON_OK - } - - /// Recursively copy a directory. - #[no_mangle] - pub extern "C" fn oakcommon_filefunctions_copy_directory( - self_: CHandle, - source: *const super::c_char, - dest: *const super::c_char, - overwrite: i32, - ) -> i32 { - if self_.is_null() || source.is_null() || dest.is_null() { - return OAKCOMMON_E_INVALID; - } - let ff = match unsafe { get::(&self_) } { - Some(f) => f, - None => return OAKCOMMON_E_INVALID, - }; - match ff.copy_directory(cstr(source), cstr(dest), overwrite != 0) { - Ok(()) => OAKCOMMON_OK, - Err(_) => OAKCOMMON_E_FAILED, - } - } - - /// Whether `dir` is a valid directory, optionally creating it. - #[no_mangle] - pub extern "C" fn oakcommon_filefunctions_directory_is_valid( - self_: CHandle, - dir: *const super::c_char, - try_to_create_if_not_exists: i32, - out: *mut i32, - ) -> i32 { - if self_.is_null() || dir.is_null() || out.is_null() { - return OAKCOMMON_E_INVALID; - } - let ff = match unsafe { get::(&self_) } { - Some(f) => f, - None => return OAKCOMMON_E_INVALID, - }; - unsafe { - *out = ff.directory_is_valid(cstr(dir), try_to_create_if_not_exists != 0) as i32; - } - OAKCOMMON_OK - } - - /// Ensure a filename carries the given extension (two-stage string - /// getter). - #[no_mangle] - pub extern "C" fn oakcommon_filefunctions_ensure_filename_extension( - self_: CHandle, - filename: *const super::c_char, - extension: *const super::c_char, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if self_.is_null() - || filename.is_null() - || extension.is_null() - || !is_valid_string_out(buf, buf_size) - { - return OAKCOMMON_E_INVALID; - } - let ff = match unsafe { get::(&self_) } { - Some(f) => f, - None => return OAKCOMMON_E_INVALID, - }; - match ff.ensure_filename_extension(cstr(filename), cstr(extension)) { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// Read a file's contents as a string (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_filefunctions_read_file_as_string( - self_: CHandle, - filename: *const super::c_char, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if self_.is_null() || filename.is_null() || !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - let ff = match unsafe { get::(&self_) } { - Some(f) => f, - None => return OAKCOMMON_E_INVALID, - }; - match ff.read_file_as_string(cstr(filename)) { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// Safe temporary filename derived from `original` (two-stage string - /// getter). - #[no_mangle] - pub extern "C" fn oakcommon_filefunctions_get_safe_temporary_filename( - self_: CHandle, - original: *const super::c_char, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if self_.is_null() || original.is_null() || !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - let ff = match unsafe { get::(&self_) } { - Some(f) => f, - None => return OAKCOMMON_E_INVALID, - }; - match ff.get_safe_temporary_filename(cstr(original)) { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// Rename a file, allowing overwrite. - #[no_mangle] - pub extern "C" fn oakcommon_filefunctions_rename_file_allow_overwrite( - self_: CHandle, - from: *const super::c_char, - to: *const super::c_char, - out: *mut i32, - ) -> i32 { - if self_.is_null() || from.is_null() || to.is_null() || out.is_null() { - return OAKCOMMON_E_INVALID; - } - let ff = match unsafe { get::(&self_) } { - Some(f) => f, - None => return OAKCOMMON_E_INVALID, - }; - unsafe { - *out = ff.rename_file_allow_overwrite(cstr(from), cstr(to)) as i32; - } - OAKCOMMON_OK - } - - /// Add the platform's executable extension to a name (two-stage string - /// getter). - #[no_mangle] - pub extern "C" fn oakcommon_filefunctions_get_formatted_executable_for_platform( - self_: CHandle, - unformatted: *const super::c_char, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if self_.is_null() || unformatted.is_null() || !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - let ff = match unsafe { get::(&self_) } { - Some(f) => f, - None => return OAKCOMMON_E_INVALID, - }; - match ff.get_formatted_executable_for_platform(cstr(unformatted)) { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } -} - -/// The `Current` singleton (no-op addref/release). -pub mod current { - //! `include/common/current.h`. - - use super::CHandle; - use crate::error::{OAKCOMMON_E_INVALID, OAKCOMMON_OK}; - use crate::handle::{guard, guard_handle, make_owned}; - use crate::miscutils::Current; - - /// Fetch the process-wide singleton handle. - #[no_mangle] - pub extern "C" fn oakcommon_current_instance() -> CHandle { - guard_handle(|| Ok(make_owned::<()>(()))) - } - - /// Release a reference (no-op for the singleton). - #[no_mangle] - pub extern "C" fn oakcommon_current_free(self_: *mut CHandle) { - super::free_handle(self_); - } - - /// Store a pointer in the video-params slot. - #[no_mangle] - pub extern "C" fn oakcommon_current_set_video_params( - self_: CHandle, - obj: *mut super::c_void, - destroy: super::DestroyFn, - ) -> i32 { - if self_.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - Current::instance().set_video_params(obj, destroy)?; - Ok(()) - }) - } - - /// Store a pointer in the audio-params slot. - #[no_mangle] - pub extern "C" fn oakcommon_current_set_audio_params( - self_: CHandle, - obj: *mut super::c_void, - destroy: super::DestroyFn, - ) -> i32 { - if self_.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - Current::instance().set_audio_params(obj, destroy)?; - Ok(()) - }) - } - - /// Store a pointer in the plugin-host slot. - #[no_mangle] - pub extern "C" fn oakcommon_current_set_plugin_host( - self_: CHandle, - obj: *mut super::c_void, - destroy: super::DestroyFn, - ) -> i32 { - if self_.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - Current::instance().set_plugin_host(obj, destroy)?; - Ok(()) - }) - } - - /// Store a pointer in the plugin-cache slot. - #[no_mangle] - pub extern "C" fn oakcommon_current_set_plugin_cache( - self_: CHandle, - obj: *mut super::c_void, - destroy: super::DestroyFn, - ) -> i32 { - if self_.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - Current::instance().set_plugin_cache(obj, destroy)?; - Ok(()) - }) - } - - /// Fetch the video-params slot. - #[no_mangle] - pub extern "C" fn oakcommon_current_get_video_params( - self_: CHandle, - out: *mut *mut super::c_void, - ) -> i32 { - if self_.is_null() || out.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let v = Current::instance().get_video_params()?; - unsafe { - *out = v; - } - Ok(()) - }) - } - - /// Fetch the audio-params slot. - #[no_mangle] - pub extern "C" fn oakcommon_current_get_audio_params( - self_: CHandle, - out: *mut *mut super::c_void, - ) -> i32 { - if self_.is_null() || out.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let v = Current::instance().get_audio_params()?; - unsafe { - *out = v; - } - Ok(()) - }) - } - - /// Fetch the plugin-host slot. - #[no_mangle] - pub extern "C" fn oakcommon_current_get_plugin_host( - self_: CHandle, - out: *mut *mut super::c_void, - ) -> i32 { - if self_.is_null() || out.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let v = Current::instance().get_plugin_host()?; - unsafe { - *out = v; - } - Ok(()) - }) - } - - /// Fetch the plugin-cache slot. - #[no_mangle] - pub extern "C" fn oakcommon_current_get_plugin_cache( - self_: CHandle, - out: *mut *mut super::c_void, - ) -> i32 { - if self_.is_null() || out.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let v = Current::instance().get_plugin_cache()?; - unsafe { - *out = v; - } - Ok(()) - }) - } - - /// Whether the session is interactive. - #[no_mangle] - pub extern "C" fn oakcommon_current_is_interactive(self_: CHandle, out: *mut i32) -> i32 { - if self_.is_null() || out.is_null() { - return OAKCOMMON_E_INVALID; - } - unsafe { - *out = Current::instance().is_interactive().unwrap_or(false) as i32; - } - OAKCOMMON_OK - } -} - -/// Decibel/lerp, loop-mode, drop-workflow, and power helpers (no handle). -pub mod misc { - //! Folds `miscutils.h`, `loopmode.h`, `dropworkflowbehavior.h`, and - //! `power.h`; the enums and `OAKCOMMON_DECIBEL_MINIMUM` live in - //! `crate::miscutils`. - - use super::copy_string; - use crate::error::{OAKCOMMON_E_INVALID, OAKCOMMON_OK}; - use crate::miscutils as m; - - /// Write a double into a non-null out-param, returning `OK`. - fn f64_out(value: f64, out: *mut f64) -> i32 { - if out.is_null() { - return OAKCOMMON_E_INVALID; - } - unsafe { - *out = value; - } - OAKCOMMON_OK - } - - /// Linear amplitude -> decibels. - #[no_mangle] - pub extern "C" fn oakcommon_decibel_from_linear(linear: f64, out_db: *mut f64) -> i32 { - f64_out(m::decibel_from_linear(linear).unwrap_or(0.0), out_db) - } - - /// Decibels -> linear amplitude. - #[no_mangle] - pub extern "C" fn oakcommon_decibel_to_linear(db: f64, out_linear: *mut f64) -> i32 { - f64_out(m::decibel_to_linear(db).unwrap_or(0.0), out_linear) - } - - /// Logarithmic slider position -> decibels. - #[no_mangle] - pub extern "C" fn oakcommon_decibel_from_logarithmic( - logarithmic: f64, - out_db: *mut f64, - ) -> i32 { - f64_out( - m::decibel_from_logarithmic(logarithmic).unwrap_or(0.0), - out_db, - ) - } - - /// Decibels -> logarithmic slider position. - #[no_mangle] - pub extern "C" fn oakcommon_decibel_to_logarithmic(db: f64, out_logarithmic: *mut f64) -> i32 { - f64_out( - m::decibel_to_logarithmic(db).unwrap_or(0.0), - out_logarithmic, - ) - } - - /// Linear amplitude -> logarithmic position. - #[no_mangle] - pub extern "C" fn oakcommon_decibel_linear_to_logarithmic( - linear: f64, - out_logarithmic: *mut f64, - ) -> i32 { - f64_out( - m::decibel_linear_to_logarithmic(linear).unwrap_or(0.0), - out_logarithmic, - ) - } - - /// Logarithmic position -> linear amplitude. - #[no_mangle] - pub extern "C" fn oakcommon_decibel_logarithmic_to_linear( - logarithmic: f64, - out_linear: *mut f64, - ) -> i32 { - f64_out( - m::decibel_logarithmic_to_linear(logarithmic).unwrap_or(0.0), - out_linear, - ) - } - - /// Linearly interpolate between `a` and `b` with `t`. - #[no_mangle] - pub extern "C" fn oakcommon_lerp(a: f64, b: f64, t: f64, out_value: *mut f64) -> i32 { - f64_out(m::lerp(a, b, t).unwrap_or(0.0), out_value) - } - - /// Whether `value` is a valid drop-workflow behavior. - #[no_mangle] - pub extern "C" fn oakcommon_drop_workflow_behavior_is_valid(value: i32) -> i32 { - if m::DropWorkflowBehavior::is_valid(value) { - 1 - } else { - 0 - } - } - - /// Name for a drop-workflow behavior (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_drop_workflow_behavior_name( - value: i32, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - copy_string(m::DropWorkflowBehavior::name(value), buf, buf_size) - } - - /// Round `value` up to the next power of two. - #[no_mangle] - pub extern "C" fn oakcommon_power_ceil_to_power_of_2(value: u32, out: *mut u32) -> i32 { - if out.is_null() { - return OAKCOMMON_E_INVALID; - } - unsafe { - *out = m::power_ceil_to_power_of_2(value).unwrap_or(0); - } - OAKCOMMON_OK - } - - /// Round `value` down to the nearest power of two. - #[no_mangle] - pub extern "C" fn oakcommon_power_floor_to_power_of_2(value: u32, out: *mut u32) -> i32 { - if out.is_null() { - return OAKCOMMON_E_INVALID; - } - unsafe { - *out = m::power_floor_to_power_of_2(value).unwrap_or(0); - } - OAKCOMMON_OK - } -} - -/// Placeholder module for the enum-only loop-mode header. -pub mod loopmode { - //! `include/common/loopmode.h` exposes only `OakLoopMode`; the enum - //! lives in `crate::miscutils::LoopMode`. -} - -/// Placeholder module for the documentation-only handle header. -pub mod handle { - //! `include/common/handle.h` is documentation only; the scaffolding - //! lives in `crate::handle`. -} - -/// Placeholder module for the constants-only error header. -pub mod error_abi { - //! `include/common/error.h` exposes no functions; the constants live in - //! `crate::error`. -} - -/// Placeholder module for the enum-only drop-workflow header. -pub mod dropworkflowbehavior { - //! `include/common/dropworkflowbehavior.h`'s functions are exported in - //! `crate::ffi::misc`; the enum lives in - //! `crate::miscutils::DropWorkflowBehavior`. -} - -/// Placeholder module for the enum-only power header. -pub mod power { - //! `include/common/power.h`'s functions are exported in - //! `crate::ffi::misc`; there is no domain enum. -} - -/// OCIO utility queries. -pub mod ocioutils { - //! `include/common/ocioutils.h`. - - use super::CHandle; - use crate::error::OAKCOMMON_E_INVALID; - use crate::handle::{guard, make_owned}; - use crate::ocioutils::{OCIOUtils, PixelFormat}; - - /// Create an OCIOUtils handle. - #[no_mangle] - pub extern "C" fn oakcommon_ocioutils_init() -> CHandle { - make_owned(OCIOUtils::new()) - } - - /// Release one reference. - #[no_mangle] - pub extern "C" fn oakcommon_ocioutils_free(self_: *mut CHandle) { - super::free_handle(self_); - } - - /// Map a native pixel format to an OCIO bit depth code. - #[no_mangle] - pub extern "C" fn oakcommon_ocioutils_get_ocio_bit_depth_from_pixel_format( - self_: CHandle, - pixel_format: i32, - out: *mut i32, - ) -> i32 { - if self_.is_null() || out.is_null() { - return OAKCOMMON_E_INVALID; - } - if pixel_format < -1 || pixel_format >= 5 { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let v = OCIOUtils::new() - .get_ocio_bit_depth_from_pixel_format(PixelFormat::from_code(pixel_format))?; - unsafe { - *out = v; - } - Ok(()) - }) - } -} - -/// OIIO utility queries. -pub mod oiioutils { - //! `include/common/oiioutils.h`. - - use super::CHandle; - use crate::error::OAKCOMMON_E_INVALID; - use crate::handle::{guard, make_owned}; - use crate::ocioutils::PixelFormat; - use crate::oiioutils::OIIOUtils; - - /// Create an OIIOUtils handle. - #[no_mangle] - pub extern "C" fn oakcommon_oiioutils_init() -> CHandle { - make_owned(OIIOUtils::new()) - } - - /// Release one reference. - #[no_mangle] - pub extern "C" fn oakcommon_oiioutils_free(self_: *mut CHandle) { - super::free_handle(self_); - } - - /// Map a native pixel format to an OIIO base type. - #[no_mangle] - pub extern "C" fn oakcommon_oiioutils_get_oiio_base_type_from_format( - self_: CHandle, - pixel_format: i32, - out_base_type: *mut i32, - ) -> i32 { - if self_.is_null() || out_base_type.is_null() { - return OAKCOMMON_E_INVALID; - } - if pixel_format < -1 || pixel_format >= 5 { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let v = OIIOUtils::new() - .get_oiio_base_type_from_format(PixelFormat::from_code(pixel_format))?; - unsafe { - *out_base_type = v; - } - Ok(()) - }) - } - - /// Map an OIIO base type to a native pixel format. - #[no_mangle] - pub extern "C" fn oakcommon_oiioutils_get_format_from_oiio_basetype( - self_: CHandle, - base_type: i32, - out_pixel_format: *mut i32, - ) -> i32 { - if self_.is_null() || out_pixel_format.is_null() { - return OAKCOMMON_E_INVALID; - } - if base_type < 0 { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let v = OIIOUtils::new().get_format_from_oiio_basetype(base_type)?; - unsafe { - *out_pixel_format = v.code(); - } - Ok(()) - }) - } - - /// Convert a pixel-aspect-ratio value to a rational pair. - #[no_mangle] - pub extern "C" fn oakcommon_oiioutils_get_pixel_aspect_ratio( - self_: CHandle, - pixel_aspect_ratio: f64, - out_numerator: *mut i32, - out_denominator: *mut i32, - ) -> i32 { - if self_.is_null() || out_numerator.is_null() || out_denominator.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let (n, d) = OIIOUtils::new().get_pixel_aspect_ratio(pixel_aspect_ratio)?; - unsafe { - *out_numerator = n; - *out_denominator = d; - } - Ok(()) - }) - } -} - -/// Qt boundary helpers. -pub mod qtutils { - //! `include/common/qtutils.h`. - - use super::cstr; - use crate::error::OAKCOMMON_E_INVALID; - use crate::handle::guard; - use crate::qtutils; - - /// Convert an opaque pointer to its numeric representation. - #[no_mangle] - pub extern "C" fn oakcommon_qtutils_ptr_to_value( - ptr: *mut super::c_void, - out_value: *mut u64, - ) -> i32 { - if out_value.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let v = qtutils::ptr_to_value(ptr)?; - unsafe { - *out_value = v; - } - Ok(()) - }) - } - - /// Convert a numeric representation back to an opaque pointer. - #[no_mangle] - pub extern "C" fn oakcommon_qtutils_value_to_ptr( - value: u64, - out_ptr: *mut *mut super::c_void, - ) -> i32 { - if out_ptr.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = qtutils::value_to_ptr(value)?; - unsafe { - *out_ptr = p; - } - Ok(()) - }) - } - - /// File creation time as seconds since the Unix epoch. - #[no_mangle] - pub extern "C" fn oakcommon_qtutils_get_creation_date( - path: *const super::c_char, - out_secs: *mut i64, - ) -> i32 { - if path.is_null() || out_secs.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let secs = qtutils::get_creation_date(cstr(path))?; - unsafe { - *out_secs = secs; - } - Ok(()) - }) - } -} - -/// Subtitle parameter set. -pub mod subtitleparams { - //! `include/common/subtitleparams.h`. - //! - //! The C++-only `oakcommon_subtitleparams_init_from_native` deals with - //! `olive::SubtitleParams` and is served by the C++ adapter layer. - - use super::{copy_string, cstr, is_valid_string_out, CHandle}; - use crate::error::OAKCOMMON_E_INVALID; - use crate::handle::{get, guard, guard_handle, make_owned}; - use crate::subtitleparams::SubtitleParams; - - /// Create an empty subtitle parameter set. - #[no_mangle] - pub extern "C" fn oakcommon_subtitleparams_init() -> CHandle { - guard_handle(|| Ok(make_owned(SubtitleParams::new()))) - } - - /// Release one reference. - #[no_mangle] - pub extern "C" fn oakcommon_subtitleparams_free(params: *mut CHandle) { - super::free_handle(params); - } - - /// Get the stream index. - #[no_mangle] - pub extern "C" fn oakcommon_subtitleparams_get_stream_index( - params: CHandle, - index: *mut i32, - ) -> i32 { - if params.is_null() || index.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const SubtitleParams) }; - unsafe { - *index = p.stream_index(); - } - Ok(()) - }) - } - - /// Set the stream index. - #[no_mangle] - pub extern "C" fn oakcommon_subtitleparams_set_stream_index( - params: CHandle, - index: i32, - ) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut SubtitleParams) }; - p.set_stream_index(index); - Ok(()) - }) - } - - /// Get whether the stream is enabled. - #[no_mangle] - pub extern "C" fn oakcommon_subtitleparams_get_enabled( - params: CHandle, - enabled: *mut i32, - ) -> i32 { - if params.is_null() || enabled.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const SubtitleParams) }; - unsafe { - *enabled = p.enabled() as i32; - } - Ok(()) - }) - } - - /// Set whether the stream is enabled. - #[no_mangle] - pub extern "C" fn oakcommon_subtitleparams_set_enabled(params: CHandle, enabled: i32) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut SubtitleParams) }; - p.set_enabled(enabled != 0); - Ok(()) - }) - } - - /// Whether the set contains at least one subtitle. - #[no_mangle] - pub extern "C" fn oakcommon_subtitleparams_is_valid( - params: CHandle, - is_valid: *mut i32, - ) -> i32 { - if params.is_null() || is_valid.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const SubtitleParams) }; - unsafe { - *is_valid = p.is_valid() as i32; - } - Ok(()) - }) - } - - /// Number of subtitle entries. - #[no_mangle] - pub extern "C" fn oakcommon_subtitleparams_count(params: CHandle, count: *mut i32) -> i32 { - if params.is_null() || count.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const SubtitleParams) }; - unsafe { - *count = p.count(); - } - Ok(()) - }) - } - - /// Out-time of the last subtitle as a rational pair. - #[no_mangle] - pub extern "C" fn oakcommon_subtitleparams_duration( - params: CHandle, - numerator: *mut i32, - denominator: *mut i32, - ) -> i32 { - if params.is_null() || numerator.is_null() || denominator.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const SubtitleParams) }; - let (n, d) = p.duration(); - unsafe { - *numerator = n; - *denominator = d; - } - Ok(()) - }) - } - - /// Append a subtitle entry. - #[no_mangle] - pub extern "C" fn oakcommon_subtitleparams_add_subtitle( - params: CHandle, - in_num: i32, - in_den: i32, - out_num: i32, - out_den: i32, - text: *const super::c_char, - ) -> i32 { - if params.is_null() || text.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut SubtitleParams) }; - p.add_subtitle(in_num, in_den, out_num, out_den, cstr(text)) - }) - } - - /// Remove all subtitle entries. - #[no_mangle] - pub extern "C" fn oakcommon_subtitleparams_clear(params: CHandle) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut SubtitleParams) }; - p.clear() - }) - } - - /// Get the time range of the subtitle at `index`. - #[no_mangle] - pub extern "C" fn oakcommon_subtitleparams_get_subtitle( - params: CHandle, - index: i32, - in_num: *mut i32, - in_den: *mut i32, - out_num: *mut i32, - out_den: *mut i32, - ) -> i32 { - if params.is_null() - || in_num.is_null() - || in_den.is_null() - || out_num.is_null() - || out_den.is_null() - { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const SubtitleParams) }; - let ((in_n, in_d), (out_n, out_d)) = p.get_subtitle(index)?; - unsafe { - *in_num = in_n; - *in_den = in_d; - *out_num = out_n; - *out_den = out_d; - } - Ok(()) - }) - } - - /// Get the text of the subtitle at `index` (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_subtitleparams_get_subtitle_text( - params: CHandle, - index: i32, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if params.is_null() || !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - let p = match unsafe { get::(¶ms) } { - Some(p) => p, - None => return OAKCOMMON_E_INVALID, - }; - match p.get_subtitle_text(index) { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// Generate a default ASS header (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_subtitleparams_generate_ass_header( - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - match SubtitleParams::generate_ass_header() { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// Load subtitles from an XML fragment. - #[no_mangle] - pub extern "C" fn oakcommon_subtitleparams_load_xml( - params: CHandle, - xml: *const super::c_char, - ) -> i32 { - if params.is_null() || xml.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut SubtitleParams) }; - p.load_xml(cstr(xml)) - }) - } - - /// Save subtitles to an XML fragment (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_subtitleparams_save_xml( - params: CHandle, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if params.is_null() || !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - let p = match unsafe { get::(¶ms) } { - Some(p) => p, - None => return OAKCOMMON_E_INVALID, - }; - match p.save_xml() { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } -} - -/// Video parameter set. -pub mod videoparams { - //! `include/common/videoparams.h`. - //! - //! The C++-only `oakcommon_videoparams_init_from_native` / - //! `oakcommon_videoparams_get_native` deal with `olive::VideoParams` - //! and are served by the C++ adapter layer. - - use super::{copy_string, cstr, is_valid_string_out, CHandle}; - use crate::error::OAKCOMMON_E_INVALID; - use crate::handle::{guard, guard_handle, make_owned}; - use crate::ocioutils::PixelFormat; - use crate::videoparams::{ColorRange, Interlacing, VideoParams, VideoType}; - - /// Create a default (invalid) parameter set. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_init() -> CHandle { - guard_handle(|| Ok(make_owned(VideoParams::new()))) - } - - /// Create a parameter set without a time base. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_init_basic( - width: i32, - height: i32, - pixel_format: i32, - nb_channels: i32, - pixel_aspect_num: i32, - pixel_aspect_den: i32, - interlacing: i32, - divider: i32, - ) -> CHandle { - guard_handle(|| { - Ok(make_owned(VideoParams::new_basic( - width, - height, - PixelFormat::from_code(pixel_format), - nb_channels, - pixel_aspect_num, - pixel_aspect_den, - interlacing, - divider, - ))) - }) - } - - /// Create a parameter set with a time base. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_init_with_time_base( - width: i32, - height: i32, - time_base_num: i32, - time_base_den: i32, - pixel_format: i32, - nb_channels: i32, - pixel_aspect_num: i32, - pixel_aspect_den: i32, - interlacing: i32, - divider: i32, - ) -> CHandle { - guard_handle(|| { - Ok(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, - ))) - }) - } - - /// Release one reference. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_free(params: *mut CHandle) { - super::free_handle(params); - } - - /// Get the width. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_width(params: CHandle, width: *mut i32) -> i32 { - if params.is_null() || width.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *width = p.width(); - } - Ok(()) - }) - } - - /// Set the width. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_width(params: CHandle, width: i32) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_width(width); - Ok(()) - }) - } - - /// Get the height. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_height(params: CHandle, height: *mut i32) -> i32 { - if params.is_null() || height.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *height = p.height(); - } - Ok(()) - }) - } - - /// Set the height. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_height(params: CHandle, height: i32) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_height(height); - Ok(()) - }) - } - - /// Get the depth. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_depth(params: CHandle, depth: *mut i32) -> i32 { - if params.is_null() || depth.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *depth = p.depth(); - } - Ok(()) - }) - } - - /// Set the depth. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_depth(params: CHandle, depth: i32) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_depth(depth); - Ok(()) - }) - } - - /// Get whether the frame is 3D. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_is_3d(params: CHandle, is_3d: *mut i32) -> i32 { - if params.is_null() || is_3d.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *is_3d = p.is_3d() as i32; - } - Ok(()) - }) - } - - /// Get the time base as a rational pair. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_time_base( - params: CHandle, - numerator: *mut i32, - denominator: *mut i32, - ) -> i32 { - if params.is_null() || numerator.is_null() || denominator.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - let (n, d) = p.time_base(); - unsafe { - *numerator = n; - *denominator = d; - } - Ok(()) - }) - } - - /// Set the time base. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_time_base( - params: CHandle, - numerator: i32, - denominator: i32, - ) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_time_base(numerator, denominator); - Ok(()) - }) - } - - /// Get the frame rate as a rational pair. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_frame_rate( - params: CHandle, - numerator: *mut i32, - denominator: *mut i32, - ) -> i32 { - if params.is_null() || numerator.is_null() || denominator.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - let (n, d) = p.frame_rate(); - unsafe { - *numerator = n; - *denominator = d; - } - Ok(()) - }) - } - - /// Set the frame rate. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_frame_rate( - params: CHandle, - numerator: i32, - denominator: i32, - ) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_frame_rate(numerator, denominator); - Ok(()) - }) - } - - /// Get the frame rate as a (flipped) time base. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_frame_rate_as_time_base( - params: CHandle, - numerator: *mut i32, - denominator: *mut i32, - ) -> i32 { - if params.is_null() || numerator.is_null() || denominator.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - let (n, d) = p.frame_rate_as_time_base(); - unsafe { - *numerator = n; - *denominator = d; - } - Ok(()) - }) - } - - /// Get the pixel aspect ratio as a rational pair. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_pixel_aspect_ratio( - params: CHandle, - numerator: *mut i32, - denominator: *mut i32, - ) -> i32 { - if params.is_null() || numerator.is_null() || denominator.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - let (n, d) = p.pixel_aspect_ratio(); - unsafe { - *numerator = n; - *denominator = d; - } - Ok(()) - }) - } - - /// Set the pixel aspect ratio. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_pixel_aspect_ratio( - params: CHandle, - numerator: i32, - denominator: i32, - ) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_pixel_aspect_ratio(numerator, denominator); - Ok(()) - }) - } - - /// Get the pixel format. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_format(params: CHandle, format: *mut i32) -> i32 { - if params.is_null() || format.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *format = p.format().code(); - } - Ok(()) - }) - } - - /// Set the pixel format. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_format(params: CHandle, format: i32) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_format(PixelFormat::from_code(format)); - Ok(()) - }) - } - - /// Get the channel count. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_channel_count( - params: CHandle, - count: *mut i32, - ) -> i32 { - if params.is_null() || count.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *count = p.channel_count(); - } - Ok(()) - }) - } - - /// Set the channel count. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_channel_count(params: CHandle, count: i32) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_channel_count(count); - Ok(()) - }) - } - - /// Get the interlacing mode. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_interlacing( - params: CHandle, - interlacing: *mut i32, - ) -> i32 { - if params.is_null() || interlacing.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *interlacing = p.interlacing() as i32; - } - Ok(()) - }) - } - - /// Set the interlacing mode. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_interlacing( - params: CHandle, - interlacing: i32, - ) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_interlacing(match interlacing { - 1 => Interlacing::TopFirst, - 2 => Interlacing::BottomFirst, - _ => Interlacing::None, - }); - Ok(()) - }) - } - - /// Get the divider. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_divider(params: CHandle, divider: *mut i32) -> i32 { - if params.is_null() || divider.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *divider = p.divider(); - } - Ok(()) - }) - } - - /// Set the divider. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_divider(params: CHandle, divider: i32) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_divider(divider); - Ok(()) - }) - } - - /// Get whether the stream is enabled. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_enabled(params: CHandle, enabled: *mut i32) -> i32 { - if params.is_null() || enabled.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *enabled = p.enabled() as i32; - } - Ok(()) - }) - } - - /// Set whether the stream is enabled. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_enabled(params: CHandle, enabled: i32) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_enabled(enabled != 0); - Ok(()) - }) - } - - /// Get the X offset. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_x(params: CHandle, x: *mut f32) -> i32 { - if params.is_null() || x.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *x = p.x(); - } - Ok(()) - }) - } - - /// Set the X offset. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_x(params: CHandle, x: f32) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_x(x); - Ok(()) - }) - } - - /// Get the Y offset. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_y(params: CHandle, y: *mut f32) -> i32 { - if params.is_null() || y.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *y = p.y(); - } - Ok(()) - }) - } - - /// Set the Y offset. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_y(params: CHandle, y: f32) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_y(y); - Ok(()) - }) - } - - /// Get the stream index. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_stream_index( - params: CHandle, - index: *mut i32, - ) -> i32 { - if params.is_null() || index.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *index = p.stream_index(); - } - Ok(()) - }) - } - - /// Set the stream index. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_stream_index(params: CHandle, index: i32) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_stream_index(index); - Ok(()) - }) - } - - /// Get the video type. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_video_type( - params: CHandle, - type_: *mut i32, - ) -> i32 { - if params.is_null() || type_.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *type_ = p.video_type() as i32; - } - Ok(()) - }) - } - - /// Set the video type. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_video_type(params: CHandle, type_: i32) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_video_type(match type_ { - 1 => VideoType::Still, - 2 => VideoType::ImageSequence, - _ => VideoType::Video, - }); - Ok(()) - }) - } - - /// Get the start time. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_start_time( - params: CHandle, - start_time: *mut i64, - ) -> i32 { - if params.is_null() || start_time.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *start_time = p.start_time(); - } - Ok(()) - }) - } - - /// Set the start time. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_start_time( - params: CHandle, - start_time: i64, - ) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_start_time(start_time); - Ok(()) - }) - } - - /// Get the duration. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_duration( - params: CHandle, - duration: *mut i64, - ) -> i32 { - if params.is_null() || duration.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *duration = p.duration(); - } - Ok(()) - }) - } - - /// Set the duration. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_duration(params: CHandle, duration: i64) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_duration(duration); - Ok(()) - }) - } - - /// Get whether alpha is premultiplied. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_premultiplied_alpha( - params: CHandle, - premultiplied: *mut i32, - ) -> i32 { - if params.is_null() || premultiplied.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *premultiplied = p.premultiplied_alpha() as i32; - } - Ok(()) - }) - } - - /// Set whether alpha is premultiplied. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_premultiplied_alpha( - params: CHandle, - premultiplied: i32, - ) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_premultiplied_alpha(premultiplied != 0); - Ok(()) - }) - } - - /// Get the color range. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_color_range( - params: CHandle, - color_range: *mut i32, - ) -> i32 { - if params.is_null() || color_range.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *color_range = p.color_range() as i32; - } - Ok(()) - }) - } - - /// Set the color range. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_color_range( - params: CHandle, - color_range: i32, - ) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_color_range(match color_range { - 1 => ColorRange::Full, - _ => ColorRange::Limited, - }); - Ok(()) - }) - } - - /// Get the color primaries. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_color_primaries( - params: CHandle, - primaries: *mut i32, - ) -> i32 { - if params.is_null() || primaries.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *primaries = p.color_primaries(); - } - Ok(()) - }) - } - - /// Set the color primaries. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_color_primaries( - params: CHandle, - primaries: i32, - ) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_color_primaries(primaries); - Ok(()) - }) - } - - /// Get the color transfer function. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_color_transfer( - params: CHandle, - transfer: *mut i32, - ) -> i32 { - if params.is_null() || transfer.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *transfer = p.color_transfer(); - } - Ok(()) - }) - } - - /// Set the color transfer function. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_color_transfer( - params: CHandle, - transfer: i32, - ) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_color_transfer(transfer); - Ok(()) - }) - } - - /// Get the colorspace name (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_colorspace( - params: CHandle, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if params.is_null() { - return OAKCOMMON_E_INVALID; - } - let p = unsafe { &*(params.ctx as *const VideoParams) }; - copy_string(p.colorspace(), buf, buf_size) - } - - /// Set the colorspace name. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_set_colorspace( - params: CHandle, - colorspace: *const super::c_char, - ) -> i32 { - if params.is_null() || colorspace.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.set_colorspace(cstr(colorspace)); - Ok(()) - }) - } - - /// Get the square-pixel width. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_square_pixel_width( - params: CHandle, - width: *mut i32, - ) -> i32 { - if params.is_null() || width.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *width = p.square_pixel_width(); - } - Ok(()) - }) - } - - /// Get the effective width. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_effective_width( - params: CHandle, - width: *mut i32, - ) -> i32 { - if params.is_null() || width.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *width = p.effective_width(); - } - Ok(()) - }) - } - - /// Get the effective height. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_effective_height( - params: CHandle, - height: *mut i32, - ) -> i32 { - if params.is_null() || height.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *height = p.effective_height(); - } - Ok(()) - }) - } - - /// Get the effective depth. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_effective_depth( - params: CHandle, - depth: *mut i32, - ) -> i32 { - if params.is_null() || depth.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *depth = p.effective_depth(); - } - Ok(()) - }) - } - - /// Get whether the set describes a valid stream. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_is_valid( - params: CHandle, - is_valid: *mut i32, - ) -> i32 { - if params.is_null() || is_valid.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *is_valid = p.is_valid() as i32; - } - Ok(()) - }) - } - - /// Get bytes per channel. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_bytes_per_channel( - params: CHandle, - bytes: *mut i32, - ) -> i32 { - if params.is_null() || bytes.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *bytes = p.bytes_per_channel(); - } - Ok(()) - }) - } - - /// Get bytes per pixel. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_bytes_per_pixel( - params: CHandle, - bytes: *mut i32, - ) -> i32 { - if params.is_null() || bytes.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *bytes = p.bytes_per_pixel(); - } - Ok(()) - }) - } - - /// Get the total buffer size. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_buffer_size( - params: CHandle, - size: *mut i32, - ) -> i32 { - if params.is_null() || size.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - unsafe { - *size = p.buffer_size(); - } - Ok(()) - }) - } - - /// Convert a time (in seconds) to time-base units. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_time_in_timebase_units( - params: CHandle, - time_num: i32, - time_den: i32, - timestamp: *mut i64, - ) -> i32 { - if params.is_null() || timestamp.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - // CPP-PARITY: C++ returns INT64_MIN (AV_NOPTS_VALUE) when no time - // base is set; the Rust domain returns None in that case. - match p.time_in_timebase_units(time_num, time_den) { - Some(ts) => unsafe { - *timestamp = ts; - }, - None => unsafe { - *timestamp = i64::MIN; - }, - } - Ok(()) - }) - } - - /// Compare two parameter sets for equality. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_equals( - params: CHandle, - other: CHandle, - equal: *mut i32, - ) -> i32 { - if params.is_null() || other.is_null() || equal.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &*(params.ctx as *const VideoParams) }; - let o = unsafe { &*(other.ctx as *const VideoParams) }; - unsafe { - *equal = p.equals(o) as i32; - } - Ok(()) - }) - } - - /// Load parameters from an XML fragment. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_load_xml( - params: CHandle, - xml: *const super::c_char, - ) -> i32 { - if params.is_null() || xml.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let p = unsafe { &mut *(params.ctx as *mut VideoParams) }; - p.load_xml(cstr(xml)) - }) - } - - /// Save parameters to an XML fragment (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_save_xml( - params: CHandle, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if params.is_null() || !is_valid_string_out(buf, buf_size) { - return OAKCOMMON_E_INVALID; - } - let p = unsafe { &*(params.ctx as *const VideoParams) }; - match p.save_xml() { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// Static: bytes per channel for a format. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_bytes_per_channel_for_format( - pixel_format: i32, - ) -> i32 { - VideoParams::bytes_per_channel_for_format(PixelFormat::from_code(pixel_format)) - } - - /// Static: bytes per pixel for a format + channel count. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_bytes_per_pixel_for_format( - pixel_format: i32, - channels: i32, - ) -> i32 { - VideoParams::bytes_per_pixel_for_format(PixelFormat::from_code(pixel_format), channels) - } - - /// Static: total buffer size. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_calculate_buffer_size( - width: i32, - height: i32, - pixel_format: i32, - channels: i32, - ) -> i32 { - VideoParams::calculate_buffer_size( - width, - height, - PixelFormat::from_code(pixel_format), - channels, - ) - } - - /// Static: whether the format is float. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_format_is_float(pixel_format: i32) -> i32 { - VideoParams::format_is_float(PixelFormat::from_code(pixel_format)) as i32 - } - - /// Static: auto divider for the given dimensions. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_generate_auto_divider(width: i64, height: i64) -> i32 { - VideoParams::generate_auto_divider(width, height) - } - - /// Static: scale a dimension by a divider. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_scaled_dimension( - dimension: i32, - divider: i32, - ) -> i32 { - VideoParams::get_scaled_dimension(dimension, divider) - } - - /// Static: divider for a target resolution. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_divider_for_target_resolution( - src_width: i32, - src_height: i32, - dst_width: i32, - dst_height: i32, - ) -> i32 { - VideoParams::get_divider_for_target_resolution(src_width, src_height, dst_width, dst_height) - } - - /// Static: name for a divider (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_name_for_divider( - divider: i32, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - 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(), - } - } - - /// Static: name for a pixel format (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_get_format_name( - pixel_format: i32, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - 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(), - } - } - - /// Static: frame-rate string (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_frame_rate_to_string( - numerator: i32, - denominator: i32, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - 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(), - } - } - - /// Static: bytes per channel for an OakPixelFormat. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_static_get_bytes_per_channel(format: i32) -> i32 { - VideoParams::bytes_per_channel_for_format(PixelFormat::from_code(format)) - } - - /// Static: bytes per pixel for an OakPixelFormat + channels. - #[no_mangle] - pub extern "C" fn oakcommon_videoparams_static_get_bytes_per_pixel( - format: i32, - channels: i32, - ) -> i32 { - VideoParams::bytes_per_pixel_for_format(PixelFormat::from_code(format), channels) - } -} - -/// Streaming XML reader/writer. -pub mod xmlutils { - //! `include/common/xmlutils.h`. - //! - //! The C++-only `get_native` / `wrap_native` entry points deal with - //! `olive::XmlStreamReader` / `olive::XmlStreamWriter` and are served - //! by the C++ adapter layer. - - use super::{copy_string, cstr, free_handle, CHandle}; - use crate::error::OAKCOMMON_E_INVALID; - use crate::handle::{get, get_mut, guard, guard_handle, make_owned}; - use crate::xmlutils::{XmlReader, XmlWriter}; - - /// Reader state boxed behind the handle's `ctx`, mirroring the C++ - /// `XmlReaderState` in `c_api/xmlutils.cpp`: the reader plus a cache for - /// `read_element_text()`. The domain `XmlReader::read_element_text` - /// consumes the stream on every call, so the C++ two-stage buffer - /// convention (size query then copy) would otherwise invoke it twice and - /// lose the text — the cache preserves the first result for the copy. - struct ReaderState { - reader: XmlReader, - cached_text: String, - has_cached_text: bool, - } - - impl ReaderState { - fn new(data: &str) -> Self { - Self { - // `XmlReader::new` always succeeds; `has_error` reports parse - // failures (CPP-PARITY: the C++ constructor never throws). - reader: XmlReader::new(data).unwrap_or_else(|_| XmlReader::new("").unwrap()), - cached_text: String::new(), - has_cached_text: false, - } - } - } - - /// Create a reader over a complete document. - #[no_mangle] - pub extern "C" fn oakcommon_xml_reader_init(data: *const super::c_char) -> CHandle { - if data.is_null() { - return CHandle::null(); - } - guard_handle(|| Ok(make_owned(ReaderState::new(cstr(data))))) - } - - /// Release one reference to a reader. - #[no_mangle] - pub extern "C" fn oakcommon_xml_reader_free(reader: *mut CHandle) { - free_handle(reader); - } - - /// Advance to the next start/end element; writes whether a start - /// element was found. - #[no_mangle] - pub extern "C" fn oakcommon_xml_reader_read_next_start_element( - reader: CHandle, - found: *mut i32, - ) -> i32 { - if reader.is_null() || found.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let st = unsafe { get_mut::(&reader) } - .expect("reader handle validated non-null"); - st.has_cached_text = false; - st.reader.read_next_start_element().map(|b| unsafe { - *found = b as i32; - }) - }) - } - - /// Name of the current element token (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_xml_reader_name( - reader: CHandle, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if reader.is_null() { - return OAKCOMMON_E_INVALID; - } - let st = match unsafe { get::(&reader) } { - Some(st) => st, - None => return OAKCOMMON_E_INVALID, - }; - match st.reader.name() { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// Read the character data of the current element (two-stage string - /// getter). - #[no_mangle] - pub extern "C" fn oakcommon_xml_reader_read_element_text( - reader: CHandle, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if reader.is_null() { - return OAKCOMMON_E_INVALID; - } - let st = match unsafe { get_mut::(&reader) } { - Some(st) => st, - None => return OAKCOMMON_E_INVALID, - }; - // CPP-PARITY: `read_element_text()` consumes the stream, so cache the - // result to keep the two-stage (size query then copy) convention - // working across repeated calls. - if !st.has_cached_text { - match st.reader.read_element_text() { - Ok(s) => { - st.cached_text = s; - st.has_cached_text = true; - } - Err(e) => return e.code(), - } - } - copy_string(&st.cached_text, buf, buf_size) - } - - /// Skip the current element and its children. - #[no_mangle] - pub extern "C" fn oakcommon_xml_reader_skip_current_element(reader: CHandle) -> i32 { - if reader.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let st = unsafe { get_mut::(&reader) } - .expect("reader handle validated non-null"); - st.has_cached_text = false; - st.reader.skip_current_element() - }) - } - - /// Number of attributes on the current start element. - #[no_mangle] - pub extern "C" fn oakcommon_xml_reader_attribute_count( - reader: CHandle, - count: *mut i32, - ) -> i32 { - if reader.is_null() || count.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let st = - unsafe { get::(&reader) }.expect("reader handle validated non-null"); - st.reader.attribute_count().map(|c| unsafe { - *count = c; - }) - }) - } - - /// Name of the attribute at `index` (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_xml_reader_attribute_name( - reader: CHandle, - index: i32, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if reader.is_null() { - return OAKCOMMON_E_INVALID; - } - let st = match unsafe { get::(&reader) } { - Some(st) => st, - None => return OAKCOMMON_E_INVALID, - }; - match st.reader.attribute_name(index) { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// Value of the attribute at `index` (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_xml_reader_attribute_value( - reader: CHandle, - index: i32, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if reader.is_null() { - return OAKCOMMON_E_INVALID; - } - let st = match unsafe { get::(&reader) } { - Some(st) => st, - None => return OAKCOMMON_E_INVALID, - }; - match st.reader.attribute_value(index) { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// Whether the document failed to parse. - #[no_mangle] - pub extern "C" fn oakcommon_xml_reader_has_error(reader: CHandle, has_error: *mut i32) -> i32 { - if reader.is_null() || has_error.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let st = - unsafe { get::(&reader) }.expect("reader handle validated non-null"); - st.reader.has_error().map(|b| unsafe { - *has_error = b as i32; - }) - }) - } - - /// Create a writer. - #[no_mangle] - pub extern "C" fn oakcommon_xml_writer_init() -> CHandle { - guard_handle(|| Ok(make_owned(XmlWriter::new()))) - } - - /// Release one reference to a writer. - #[no_mangle] - pub extern "C" fn oakcommon_xml_writer_free(writer: *mut CHandle) { - free_handle(writer); - } - - /// Write a start element. - #[no_mangle] - pub extern "C" fn oakcommon_xml_writer_write_start_element( - writer: CHandle, - name: *const super::c_char, - ) -> i32 { - if writer.is_null() || name.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let w = - unsafe { get_mut::(&writer) }.expect("writer handle validated non-null"); - w.write_start_element(cstr(name)) - }) - } - - /// Write an attribute. - #[no_mangle] - pub extern "C" fn oakcommon_xml_writer_write_attribute( - writer: CHandle, - name: *const super::c_char, - value: *const super::c_char, - ) -> i32 { - if writer.is_null() || name.is_null() || value.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let w = - unsafe { get_mut::(&writer) }.expect("writer handle validated non-null"); - w.write_attribute(cstr(name), cstr(value)) - }) - } - - /// Write character data. - #[no_mangle] - pub extern "C" fn oakcommon_xml_writer_write_characters( - writer: CHandle, - text: *const super::c_char, - ) -> i32 { - if writer.is_null() || text.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let w = - unsafe { get_mut::(&writer) }.expect("writer handle validated non-null"); - w.write_characters(cstr(text)) - }) - } - - /// Write an empty element with text. - #[no_mangle] - pub extern "C" fn oakcommon_xml_writer_write_text_element( - writer: CHandle, - name: *const super::c_char, - text: *const super::c_char, - ) -> i32 { - if writer.is_null() || name.is_null() || text.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let w = - unsafe { get_mut::(&writer) }.expect("writer handle validated non-null"); - w.write_text_element(cstr(name), cstr(text)) - }) - } - - /// Write an end element. - #[no_mangle] - pub extern "C" fn oakcommon_xml_writer_write_end_element(writer: CHandle) -> i32 { - if writer.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let w = - unsafe { get_mut::(&writer) }.expect("writer handle validated non-null"); - w.write_end_element() - }) - } - - /// Write the end of the document. - #[no_mangle] - pub extern "C" fn oakcommon_xml_writer_write_end_document(writer: CHandle) -> i32 { - if writer.is_null() { - return OAKCOMMON_E_INVALID; - } - guard(|| { - let w = - unsafe { get_mut::(&writer) }.expect("writer handle validated non-null"); - w.write_end_document() - }) - } - - /// The document written so far (two-stage string getter). - #[no_mangle] - pub extern "C" fn oakcommon_xml_writer_output( - writer: CHandle, - buf: *mut super::c_char, - buf_size: i32, - ) -> i32 { - if writer.is_null() { - return OAKCOMMON_E_INVALID; - } - let w = match unsafe { get::(&writer) } { - Some(w) => w, - None => return OAKCOMMON_E_INVALID, - }; - match w.output() { - Ok(s) => copy_string(&s, buf, buf_size), - Err(e) => e.code(), - } - } -} diff --git a/crates/oakcommon/src/lib.rs b/crates/oakcommon/src/lib.rs index 8648d0375..d152d75d9 100644 --- a/crates/oakcommon/src/lib.rs +++ b/crates/oakcommon/src/lib.rs @@ -27,7 +27,6 @@ pub mod commandlineparser; pub mod configstore; pub mod debug; pub mod error; -pub mod ffi; pub mod ffmpegutils; pub mod filefunctions; pub mod handle; diff --git a/crates/oakcommon/src/subtitleparams.rs b/crates/oakcommon/src/subtitleparams.rs index 9636b0cc8..0ba751110 100644 --- a/crates/oakcommon/src/subtitleparams.rs +++ b/crates/oakcommon/src/subtitleparams.rs @@ -41,6 +41,7 @@ pub struct Subtitle { } /// `olive::SubtitleParams` — a handle-wrapped subtitle parameter set. +#[derive(Clone, Debug, PartialEq, Eq)] pub struct SubtitleParams { /// Stream index within the source file. stream_index: i32, diff --git a/crates/oakcommon/src/videoparams.rs b/crates/oakcommon/src/videoparams.rs index 8337a862f..d0277e61a 100644 --- a/crates/oakcommon/src/videoparams.rs +++ b/crates/oakcommon/src/videoparams.rs @@ -56,7 +56,7 @@ pub enum ColorRange { } /// `olive::VideoParams` — a handle-wrapped video parameter set. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct VideoParams { /// Frame width in pixels. width: i32, diff --git a/crates/oakcommon/tests/ffi_colortransform.rs b/crates/oakcommon/tests/ffi_colortransform.rs deleted file mode 100644 index 83fd721cd..000000000 --- a/crates/oakcommon/tests/ffi_colortransform.rs +++ /dev/null @@ -1,354 +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 . - -//! FFI-level integration tests for the C-ABI `colortransform` exports in -//! `oakcommon::ffi::colortransform`, asserted against the C++ oracle -//! `src/common/c_api/colortransform.cpp` and the Rust domain -//! `src/common/rust/src/colortransform.rs`. -//! -//! The crate exports the two constructors, `free`, `is_display`, and the -//! four two-stage getters; only the C++-typed `init_from_native` / -//! `get_native` adapters stay with the C++ adapter layer. The tests -//! cover: -//! - constructor success: a stamped (`OAKCOMMON_ABI_VERSION`), non-null -//! handle with live `addref`/`release` callbacks, whose boxed -//! [`ColorTransform`] carries the right fields (peeked via -//! `oakcommon::handle::get`); -//! - constructor failure: a null string argument yields a null handle -//! (guard semantics: any error or panic also collapses to a null -//! handle); -//! - the free contract (release once, nullify, no-op on null); -//! - `is_display` / `get_display` / `get_output` / `get_view` / -//! `get_look` happy paths, two-stage short-buffer behavior, and -//! empty-handle error codes; -//! - distinct handles plus the ownership contract (init yields reference -//! count 1; addref/release adjust it atomically; both free cleanly). - -use std::ffi::{c_char, CString}; -use std::sync::atomic::Ordering; - -use oakcommon::colortransform::ColorTransform; -use oakcommon::ffi::colortransform::*; -use oakcommon::handle::{get, CHandle, RefBox, OAKCOMMON_ABI_VERSION}; - -/// Convert a string slice to a NUL-terminated C string for FFI inputs. -fn to_cstring(s: &str) -> CString { - CString::new(s).expect("test string must not contain NUL") -} - -/// Cheap struct copy: `CHandle` is neither `Clone` nor `Copy`; rebuilding -/// from the same fields duplicates only the handle value — the box stays -/// alive as long as the original handle lives. -fn dup(h: &CHandle) -> CHandle { - CHandle { - ctx: h.ctx, - addref: h.addref, - release: h.release, - abi_version: h.abi_version, - } -} - -/// Release one reference and nullify the handle. `oakcommon_colortransform_free` -/// is declared in the header but not exported by this crate, so the free -/// contract is driven through this exact replica of the crate-private -/// `free_handle` (and the C++ oracle): release once, write `CHandle::null()`. -fn free(h: &mut CHandle) { - if let Some(rel) = h.release { - unsafe { - rel(h.ctx); - } - } - *h = CHandle::null(); -} - -/// Pointer form of [`free`], mirroring `free_handle(*mut CHandle)`: a null -/// pointer is a no-op. -fn free_ptr(p: *mut CHandle) { - if p.is_null() { - return; - } - unsafe { - if let Some(h) = p.as_ref() { - if let Some(rel) = h.release { - rel(h.ctx); - } - } - p.write(CHandle::null()); - } -} - -/// Read the reference count behind an owned handle (test-only peek into -/// the crate-public `RefBox` layout; the box is guaranteed alive here). -unsafe fn refs_of(h: &CHandle) -> u32 { - unsafe { - (*(h.ctx as *const RefBox)) - .refs - .load(Ordering::Relaxed) - } -} - -/// A successful `init_output` yields a stamped, non-null handle whose boxed -/// transform is an output transform with the requested name; `free` -/// nullifies it. -#[test] -fn init_output_success() { - let mut h = oakcommon_colortransform_init_output(to_cstring("sRGB").as_ptr()); - assert!(!h.is_null()); - assert_eq!(h.abi_version, OAKCOMMON_ABI_VERSION); - assert!(h.addref.is_some()); - assert!(h.release.is_some()); - - let t: &ColorTransform = unsafe { get::(&h) }.expect("boxed transform"); - assert!(!t.is_display()); - assert_eq!(t.output(), "sRGB"); - assert_eq!(t.display(), ""); - assert_eq!(t.view(), ""); - assert_eq!(t.look(), ""); - - free(&mut h); - assert!(h.is_null()); -} - -/// A null output string yields a null handle with no callbacks. -#[test] -fn init_output_null_yields_null_handle() { - let h = oakcommon_colortransform_init_output(std::ptr::null::()); - assert!(h.is_null()); - assert!(h.ctx.is_null()); - assert!(h.addref.is_none()); - assert!(h.release.is_none()); - // The shared `null()` stamps no ABI version (single-lib unification). - assert_eq!(h.abi_version, 0); -} - -/// An empty output name is a valid value: the transform is still an output -/// transform (C++ default-constructor parity). -#[test] -fn init_output_empty_string_succeeds() { - let mut h = oakcommon_colortransform_init_output(to_cstring("").as_ptr()); - assert!(!h.is_null()); - let t: &ColorTransform = unsafe { get::(&h) }.expect("boxed transform"); - assert!(!t.is_display()); - assert_eq!(t.output(), ""); - free(&mut h); - assert!(h.is_null()); -} - -/// A successful `init_display` yields a stamped, non-null handle whose -/// boxed transform carries the display/view/look names. -#[test] -fn init_display_success() { - let mut h = oakcommon_colortransform_init_display( - to_cstring("DCI-P3").as_ptr(), - to_cstring("standard").as_ptr(), - to_cstring("soft").as_ptr(), - ); - assert!(!h.is_null()); - assert_eq!(h.abi_version, OAKCOMMON_ABI_VERSION); - assert!(h.addref.is_some()); - assert!(h.release.is_some()); - - let t: &ColorTransform = unsafe { get::(&h) }.expect("boxed transform"); - assert!(t.is_display()); - assert_eq!(t.display(), "DCI-P3"); - assert_eq!(t.view(), "standard"); - assert_eq!(t.look(), "soft"); - assert_eq!(t.output(), ""); - - free(&mut h); - assert!(h.is_null()); -} - -/// Any single null argument — and all-null — yields a null handle. -#[test] -fn init_display_null_any_arg_yields_null_handle() { - let d = to_cstring("DCI-P3"); - let v = to_cstring("standard"); - let l = to_cstring("soft"); - let cases = [ - (d.as_ptr(), v.as_ptr(), std::ptr::null::()), - (d.as_ptr(), std::ptr::null::(), l.as_ptr()), - (std::ptr::null::(), v.as_ptr(), l.as_ptr()), - ( - std::ptr::null::(), - std::ptr::null::(), - std::ptr::null::(), - ), - ]; - for (display, view, look) in cases { - let h = oakcommon_colortransform_init_display(display, view, look); - assert!(h.is_null()); - assert!(h.ctx.is_null()); - assert!(h.addref.is_none()); - assert!(h.release.is_none()); - // The shared `null()` stamps no ABI version (single-lib unification). - assert_eq!(h.abi_version, 0); - } -} - -/// Empty display/view/look strings are valid; the explicit `is_display` -/// flag keeps the transform identifiable as a display transform. -#[test] -fn init_display_empty_strings_succeed() { - let mut h = oakcommon_colortransform_init_display( - to_cstring("").as_ptr(), - to_cstring("").as_ptr(), - to_cstring("").as_ptr(), - ); - assert!(!h.is_null()); - let t: &ColorTransform = unsafe { get::(&h) }.expect("boxed transform"); - assert!(t.is_display()); - assert_eq!(t.display(), ""); - assert_eq!(t.view(), ""); - assert_eq!(t.look(), ""); - free(&mut h); - assert!(h.is_null()); -} - -/// The free contract: nullify, idempotent, and no-op on a null handle or a -/// null pointer. -#[test] -fn free_contract() { - // A real handle: free nullifies it and a second free is safe. - let mut h = oakcommon_colortransform_init_output(to_cstring("sRGB").as_ptr()); - assert!(!h.is_null()); - free(&mut h); - assert!(h.is_null()); - assert!(h.ctx.is_null()); - assert!(h.release.is_none()); - free(&mut h); - assert!(h.is_null()); - - // Freeing an already-null handle is a no-op. - free(&mut CHandle::null()); - - // Freeing a null pointer is a no-op. - free_ptr(std::ptr::null_mut()); -} - -/// Two output transforms are distinct handles (different boxes); init -/// yields reference count 1, addref/release adjust it atomically, and both -/// free cleanly. `dup` copies the handle value but shares the same box. -#[test] -fn init_outputs_are_distinct_and_release_cleanly() { - let mut a = oakcommon_colortransform_init_output(to_cstring("sRGB").as_ptr()); - let mut b = oakcommon_colortransform_init_output(to_cstring("Display P3").as_ptr()); - assert!(!a.is_null()); - assert!(!b.is_null()); - assert_ne!(a.ctx, b.ctx); - - // dup copies the handle value and shares the same underlying box. - let c = dup(&a); - assert_eq!(c.ctx, a.ctx); - - // Ownership contract: count 1 at init, atomic addref/release. - unsafe { - assert_eq!(refs_of(&a), 1); - assert_eq!(refs_of(&b), 1); - (a.addref.unwrap())(a.ctx); - assert_eq!(refs_of(&a), 2); - (a.release.unwrap())(a.ctx); - assert_eq!(refs_of(&a), 1); - } - - // Both release cleanly (single release + nullify, no double-free). - free(&mut a); - assert!(a.is_null()); - free(&mut b); - assert!(b.is_null()); -} - -/// `is_display` distinguishes display transforms from output transforms -/// and reports OAKCOMMON_E_INVALID on an empty handle. -#[test] -fn is_display_paths() { - let mut d = oakcommon_colortransform_init_display( - to_cstring("sRGB").as_ptr(), - to_cstring("standard").as_ptr(), - to_cstring("").as_ptr(), - ); - let mut o = oakcommon_colortransform_init_output(to_cstring("sRGB").as_ptr()); - assert_eq!(oakcommon_colortransform_is_display(dup(&d)), 1); - assert_eq!(oakcommon_colortransform_is_display(dup(&o)), 0); - assert_eq!( - oakcommon_colortransform_is_display(CHandle::null()), - oakcommon::error::OAKCOMMON_E_INVALID - ); - free(&mut d); - free(&mut o); -} - -/// Two-stage getters: exact-fit returns content, short buffer leaves the -/// buffer untouched but reports the required size, empty handle yields -/// OAKCOMMON_E_INVALID. -#[test] -fn getters_two_stage_and_empty_handle() { - let mut d = oakcommon_colortransform_init_display( - to_cstring("P3").as_ptr(), - to_cstring("std").as_ptr(), - to_cstring("soft").as_ptr(), - ); - - let mut buf = [0i8; 32]; - let need = oakcommon_colortransform_get_display(dup(&d), buf.as_mut_ptr(), 32); - assert_eq!(need, 3); // "P3" + NUL - assert_eq!( - unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } - .to_str() - .unwrap(), - "P3" - ); - - let need = oakcommon_colortransform_get_view(dup(&d), buf.as_mut_ptr(), 2); - assert_eq!(need, 4); // "std" + NUL, too small: nothing written - - let need = oakcommon_colortransform_get_look(dup(&d), buf.as_mut_ptr(), 32); - assert_eq!(need, 5); - assert_eq!( - unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } - .to_str() - .unwrap(), - "soft" - ); - - let o = oakcommon_colortransform_init_output(to_cstring("sRGB").as_ptr()); - let need = oakcommon_colortransform_get_output(o, buf.as_mut_ptr(), 32); - assert_eq!(need, 5); - assert_eq!( - unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } - .to_str() - .unwrap(), - "sRGB" - ); - - assert_eq!( - oakcommon_colortransform_get_display(CHandle::null(), buf.as_mut_ptr(), 32), - oakcommon::error::OAKCOMMON_E_INVALID - ); - free(&mut d); -} - -/// The real `oakcommon_colortransform_free` export: releases once, -/// nullifies, no-ops on a null pointer. -#[test] -fn free_export_contract() { - let mut h = oakcommon_colortransform_init_output(to_cstring("sRGB").as_ptr()); - assert!(!h.is_null()); - oakcommon_colortransform_free(&mut h); - assert!(h.is_null()); - oakcommon_colortransform_free(&mut h); // already null: no-op - oakcommon_colortransform_free(std::ptr::null_mut()); // null pointer: no-op -} diff --git a/crates/oakcommon/tests/ffi_commandlineparser.rs b/crates/oakcommon/tests/ffi_commandlineparser.rs deleted file mode 100644 index d9402971d..000000000 --- a/crates/oakcommon/tests/ffi_commandlineparser.rs +++ /dev/null @@ -1,1000 +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 C ABI surface of -//! `oakcommon::ffi::commandlineparser`, which implements -//! `include/common/commandlineparser.h` (oracle: `src/common/c_api/commandlineparser.cpp`). -//! -//! Every exported function gets a success-path test and a failure-path test. -//! The failure paths cover the empty (NULL-ctx) handle plus the documented -//! invalid-argument codes; the string getters are TRUNCATING (see the C++ -//! `copy_setting`), so all three two-stage buffer cases are exercised. -//! -//! These tests are pure: they create their own parser/option/argument -//! handles and touch no config files, so they need no `OAK_CONFIG_DIR` -//! locking and run independently of the config tests. - -use std::ffi::{c_char, CStr, CString}; -use std::ptr::null_mut; - -use oakcommon::error::{OAKCOMMON_E_INVALID, OAKCOMMON_OK}; -use oakcommon::ffi::commandlineparser::*; -use oakcommon::handle::{CHandle, OAKCOMMON_ABI_VERSION}; - -/// A fresh parser handle (reference count 1, non-null ctx). -fn new_parser() -> CHandle { - let h = oakcommon_commandlineparser_init(); - assert!(!h.ctx.is_null()); - h -} - -/// Shallow-copy a handle, mirroring C's pass-by-value semantics. The FFI -/// exports take handles by value (a `CHandle` is a plain -/// `{ctx, addref, release, abi_version}` record), but the Rust type -/// deliberately does not implement `Copy`, so each call consumes its -/// argument. `dup` copies the four scalar fields; the exports never -/// release the handles they receive, so the reference count is untouched -/// and no `addref` is needed. -fn dup(h: &CHandle) -> CHandle { - CHandle { - ctx: h.ctx, - addref: h.addref, - release: h.release, - abi_version: h.abi_version, - } -} - -/// Build a NUL-terminated C string. -fn c_str(s: &str) -> CString { - CString::new(s).unwrap() -} - -/// Build a `char *const *`-style array: returns the CStrings (kept alive -/// for the caller) plus their pointer array. -fn c_strings(values: &[&str]) -> (Vec, Vec<*const c_char>) { - let cs: Vec = values.iter().map(|s| c_str(s)).collect(); - let ptrs = cs.iter().map(|s| s.as_ptr()).collect(); - (cs, ptrs) -} - -/// Register a single `takes_arg` option and return its borrowed handle. -fn register_option(parser: CHandle, name: &str) -> CHandle { - let mut out = CHandle::null(); - let (_names, ptrs) = c_strings(&[name]); - let r = oakcommon_commandlineparser_add_option( - parser, - ptrs.as_ptr(), - 1, - c_str("desc").as_ptr(), - 1, - c_str("ARG").as_ptr(), - 0, - &mut out, - ); - assert_eq!(r, OAKCOMMON_OK); - assert!(!out.ctx.is_null()); - out -} - -/// Register a single positional argument and return its borrowed handle. -fn register_positional(parser: CHandle, name: &str) -> CHandle { - let mut out = CHandle::null(); - let r = oakcommon_commandlineparser_add_positional_argument( - parser, - c_str(name).as_ptr(), - c_str("desc").as_ptr(), - 1, - &mut out, - ); - assert_eq!(r, OAKCOMMON_OK); - assert!(!out.ctx.is_null()); - out -} - -// --------------------------------------------------------------------------- -// oakcommon_commandlineparser_init -// --------------------------------------------------------------------------- - -/// init returns a live handle with the standard vtable and ABI stamp, and -/// free() releases it cleanly. There is no testable failure path: the only -/// failure mode is allocation failure / panic inside `guard_handle`, both -/// of which return a NULL-ctx handle and cannot be triggered deterministically. -#[test] -fn init_returns_live_handle() { - let mut h = oakcommon_commandlineparser_init(); - assert!(!h.ctx.is_null()); - assert!(h.addref.is_some()); - assert!(h.release.is_some()); - assert_eq!(h.abi_version, OAKCOMMON_ABI_VERSION); - oakcommon_commandlineparser_free(&mut h); - assert!(h.ctx.is_null()); -} - -// --------------------------------------------------------------------------- -// oakcommon_commandlineparser_free -// --------------------------------------------------------------------------- - -#[test] -fn parser_free_null_pointer_is_noop() { - oakcommon_commandlineparser_free(null_mut()); -} - -#[test] -fn parser_free_twice_is_safe_and_nulls_ctx() { - let mut h = oakcommon_commandlineparser_init(); - assert!(!h.ctx.is_null()); - oakcommon_commandlineparser_free(&mut h); - assert!(h.ctx.is_null()); - // Second free on the same (now empty) handle must be a no-op. - oakcommon_commandlineparser_free(&mut h); - assert!(h.ctx.is_null()); -} - -#[test] -fn parser_free_null_handle_struct_is_noop() { - let mut h = CHandle::null(); - oakcommon_commandlineparser_free(&mut h); - assert!(h.ctx.is_null()); -} - -// --------------------------------------------------------------------------- -// oakcommon_commandlineparser_set_app_info -// --------------------------------------------------------------------------- - -#[test] -fn set_app_info_success() { - let mut p = new_parser(); - let r = oakcommon_commandlineparser_set_app_info( - dup(&p), - c_str("myapp").as_ptr(), - c_str("1.0").as_ptr(), - ); - assert_eq!(r, OAKCOMMON_OK); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn set_app_info_empty_handle_is_invalid() { - let e = CHandle::null(); - let r = oakcommon_commandlineparser_set_app_info(e, c_str("n").as_ptr(), c_str("v").as_ptr()); - assert_eq!(r, OAKCOMMON_E_INVALID); -} - -#[test] -fn set_app_info_null_name_is_invalid() { - let mut p = new_parser(); - let r = oakcommon_commandlineparser_set_app_info(dup(&p), null_mut(), c_str("v").as_ptr()); - assert_eq!(r, OAKCOMMON_E_INVALID); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn set_app_info_null_version_is_invalid() { - let mut p = new_parser(); - // CPP-PARITY: the C++ oracle tolerates a NULL version (treated as ""), - // but the Rust export rejects it with E_INVALID. - let r = oakcommon_commandlineparser_set_app_info(dup(&p), c_str("n").as_ptr(), null_mut()); - assert_eq!(r, OAKCOMMON_E_INVALID); - oakcommon_commandlineparser_free(&mut p); -} - -// --------------------------------------------------------------------------- -// oakcommon_commandlineparser_add_option -// --------------------------------------------------------------------------- - -#[test] -fn add_option_success() { - let mut p = new_parser(); - let mut out = CHandle::null(); - let (_names, ptrs) = c_strings(&["o", "output"]); - let r = oakcommon_commandlineparser_add_option( - dup(&p), - ptrs.as_ptr(), - 2, - c_str("Output file.").as_ptr(), - 1, - c_str("FILE").as_ptr(), - 0, - &mut out, - ); - assert_eq!(r, OAKCOMMON_OK); - assert!(!out.ctx.is_null()); - assert!(out.addref.is_some()); - assert!(out.release.is_some()); - oakcommon_commandlineoption_free(&mut out); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn add_option_empty_parser_is_invalid() { - let e = CHandle::null(); - let mut out = CHandle::null(); - let (_names, ptrs) = c_strings(&["o"]); - let r = oakcommon_commandlineparser_add_option( - e, - ptrs.as_ptr(), - 1, - c_str("d").as_ptr(), - 0, - c_str("A").as_ptr(), - 0, - &mut out, - ); - assert_eq!(r, OAKCOMMON_E_INVALID); -} - -#[test] -fn add_option_null_names_is_invalid() { - let mut p = new_parser(); - let mut out = CHandle::null(); - let r = oakcommon_commandlineparser_add_option( - dup(&p), - null_mut(), - 1, - c_str("d").as_ptr(), - 0, - c_str("A").as_ptr(), - 0, - &mut out, - ); - assert_eq!(r, OAKCOMMON_E_INVALID); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn add_option_non_positive_name_count_is_invalid() { - let mut p = new_parser(); - let mut out = CHandle::null(); - let (_names, ptrs) = c_strings(&["o"]); - let r0 = oakcommon_commandlineparser_add_option( - dup(&p), - ptrs.as_ptr(), - 0, - c_str("d").as_ptr(), - 0, - c_str("A").as_ptr(), - 0, - &mut out, - ); - assert_eq!(r0, OAKCOMMON_E_INVALID); - let rn = oakcommon_commandlineparser_add_option( - dup(&p), - ptrs.as_ptr(), - -1, - c_str("d").as_ptr(), - 0, - c_str("A").as_ptr(), - 0, - &mut out, - ); - assert_eq!(rn, OAKCOMMON_E_INVALID); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn add_option_null_description_is_invalid() { - // CPP-PARITY: the header documents description as "may be NULL" (C++ maps - // it to ""), but the Rust export rejects a NULL description with E_INVALID. - let mut p = new_parser(); - let mut out = CHandle::null(); - let (_names, ptrs) = c_strings(&["o"]); - let r = oakcommon_commandlineparser_add_option( - dup(&p), - ptrs.as_ptr(), - 1, - null_mut(), - 0, - c_str("A").as_ptr(), - 0, - &mut out, - ); - assert_eq!(r, OAKCOMMON_E_INVALID); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn add_option_null_arg_placeholder_is_invalid() { - // CPP-PARITY: the header documents arg_placeholder as "may be NULL" (C++ - // maps it to ""), but the Rust export rejects it with E_INVALID. - let mut p = new_parser(); - let mut out = CHandle::null(); - let (_names, ptrs) = c_strings(&["o"]); - let r = oakcommon_commandlineparser_add_option( - dup(&p), - ptrs.as_ptr(), - 1, - c_str("d").as_ptr(), - 0, - null_mut(), - 0, - &mut out, - ); - assert_eq!(r, OAKCOMMON_E_INVALID); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn add_option_null_out_option_is_invalid() { - // CPP-PARITY: the header says out_option "may be NULL if unused"; the Rust - // export requires it. - let mut p = new_parser(); - let (_names, ptrs) = c_strings(&["o"]); - let r = oakcommon_commandlineparser_add_option( - dup(&p), - ptrs.as_ptr(), - 1, - c_str("d").as_ptr(), - 0, - c_str("A").as_ptr(), - 0, - null_mut(), - ); - assert_eq!(r, OAKCOMMON_E_INVALID); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn add_option_non_utf8_name_is_invalid() { - let mut p = new_parser(); - let mut out = CHandle::null(); - // 0xFF is not valid UTF-8; the export rejects it with E_INVALID before - // the option is registered. - let bad = CString::new(vec![b'x', 0xFF]).unwrap(); - let ptrs = [bad.as_ptr()]; - let r = oakcommon_commandlineparser_add_option( - dup(&p), - ptrs.as_ptr(), - 1, - c_str("d").as_ptr(), - 0, - c_str("A").as_ptr(), - 0, - &mut out, - ); - assert_eq!(r, OAKCOMMON_E_INVALID); - // The failed registration must not leave a partial option behind: a - // subsequent valid registration still works and lands at index 0. - let mut out2 = CHandle::null(); - let (_names, ptrs2) = c_strings(&["ok"]); - let r2 = oakcommon_commandlineparser_add_option( - dup(&p), - ptrs2.as_ptr(), - 1, - c_str("d").as_ptr(), - 0, - c_str("A").as_ptr(), - 0, - &mut out2, - ); - assert_eq!(r2, OAKCOMMON_OK); - assert!(!out2.ctx.is_null()); - oakcommon_commandlineoption_free(&mut out2); - oakcommon_commandlineparser_free(&mut p); -} - -// --------------------------------------------------------------------------- -// oakcommon_commandlineparser_add_positional_argument -// --------------------------------------------------------------------------- - -#[test] -fn add_positional_argument_success() { - let mut p = new_parser(); - let mut out = CHandle::null(); - let r = oakcommon_commandlineparser_add_positional_argument( - dup(&p), - c_str("input").as_ptr(), - c_str("Input file").as_ptr(), - 1, - &mut out, - ); - assert_eq!(r, OAKCOMMON_OK); - assert!(!out.ctx.is_null()); - assert!(out.release.is_some()); - oakcommon_commandlinepositionalargument_free(&mut out); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn add_positional_argument_empty_parser_is_invalid() { - let e = CHandle::null(); - let mut out = CHandle::null(); - let r = oakcommon_commandlineparser_add_positional_argument( - e, - c_str("in").as_ptr(), - c_str("d").as_ptr(), - 1, - &mut out, - ); - assert_eq!(r, OAKCOMMON_E_INVALID); -} - -#[test] -fn add_positional_argument_null_name_is_invalid() { - let mut p = new_parser(); - let mut out = CHandle::null(); - let r = oakcommon_commandlineparser_add_positional_argument( - dup(&p), - null_mut(), - c_str("d").as_ptr(), - 1, - &mut out, - ); - assert_eq!(r, OAKCOMMON_E_INVALID); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn add_positional_argument_null_description_is_invalid() { - // CPP-PARITY: C++ maps a NULL description to ""; the Rust export rejects - // it with E_INVALID. - let mut p = new_parser(); - let mut out = CHandle::null(); - let r = oakcommon_commandlineparser_add_positional_argument( - dup(&p), - c_str("in").as_ptr(), - null_mut(), - 1, - &mut out, - ); - assert_eq!(r, OAKCOMMON_E_INVALID); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn add_positional_argument_null_out_is_invalid() { - // CPP-PARITY: C++ allows a NULL out_argument; the Rust export requires it. - let mut p = new_parser(); - let r = oakcommon_commandlineparser_add_positional_argument( - dup(&p), - c_str("in").as_ptr(), - c_str("d").as_ptr(), - 1, - null_mut(), - ); - assert_eq!(r, OAKCOMMON_E_INVALID); - oakcommon_commandlineparser_free(&mut p); -} - -// --------------------------------------------------------------------------- -// oakcommon_commandlineparser_process -// --------------------------------------------------------------------------- - -#[test] -fn process_success() { - let mut p = new_parser(); - let mut opt = register_option(dup(&p), "o"); - let mut pos = register_positional(dup(&p), "input"); - let (_argv, argv_ptrs) = c_strings(&["prog", "-o", "out.mov", "in.mp4"]); - let r = oakcommon_commandlineparser_process(dup(&p), argv_ptrs.as_ptr(), 4); - assert_eq!(r, OAKCOMMON_OK); - oakcommon_commandlineoption_free(&mut opt); - oakcommon_commandlinepositionalargument_free(&mut pos); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn process_argc_zero_success() { - let mut p = new_parser(); - // Non-null argv with argc == 0: the loop never runs, returns OK. - let (_argv, argv_ptrs) = c_strings(&["prog"]); - let r = oakcommon_commandlineparser_process(dup(&p), argv_ptrs.as_ptr(), 0); - assert_eq!(r, OAKCOMMON_OK); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn process_empty_parser_is_invalid() { - let e = CHandle::null(); - let (_argv, argv_ptrs) = c_strings(&["prog"]); - let r = oakcommon_commandlineparser_process(e, argv_ptrs.as_ptr(), 1); - assert_eq!(r, OAKCOMMON_E_INVALID); -} - -#[test] -fn process_null_argv_is_invalid() { - let mut p = new_parser(); - let r = oakcommon_commandlineparser_process(dup(&p), null_mut(), 1); - assert_eq!(r, OAKCOMMON_E_INVALID); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn process_negative_argc_is_invalid() { - let mut p = new_parser(); - let (_argv, argv_ptrs) = c_strings(&["prog"]); - let r = oakcommon_commandlineparser_process(dup(&p), argv_ptrs.as_ptr(), -1); - assert_eq!(r, OAKCOMMON_E_INVALID); - oakcommon_commandlineparser_free(&mut p); -} - -// --------------------------------------------------------------------------- -// oakcommon_commandlineparser_print_help -// --------------------------------------------------------------------------- - -#[test] -fn print_help_success() { - let mut p = new_parser(); - let r = oakcommon_commandlineparser_print_help(dup(&p), c_str("oak").as_ptr()); - assert_eq!(r, OAKCOMMON_OK); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn print_help_empty_parser_is_invalid() { - let e = CHandle::null(); - let r = oakcommon_commandlineparser_print_help(e, c_str("oak").as_ptr()); - assert_eq!(r, OAKCOMMON_E_INVALID); -} - -#[test] -fn print_help_null_filename_is_invalid() { - let mut p = new_parser(); - let r = oakcommon_commandlineparser_print_help(dup(&p), null_mut()); - assert_eq!(r, OAKCOMMON_E_INVALID); - oakcommon_commandlineparser_free(&mut p); -} - -// --------------------------------------------------------------------------- -// oakcommon_commandlineoption_is_set / _free -// --------------------------------------------------------------------------- - -#[test] -fn option_is_set_success() { - let mut p = new_parser(); - let mut opt = register_option(dup(&p), "o"); - let mut is_set = true; - let r = oakcommon_commandlineoption_is_set(dup(&opt), &mut is_set); - assert_eq!(r, OAKCOMMON_OK); - // CPP-PARITY: the Rust borrowed handle is a registration-time snapshot - // (make_borrowed bit-copies), so is_set is always false on the handle; - // the C++ oracle stores a pointer to the live option and would report the - // parser's real state. - assert!(!is_set); - oakcommon_commandlineoption_free(&mut opt); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn option_is_set_empty_handle_is_invalid() { - let e = CHandle::null(); - let mut is_set = true; - let r = oakcommon_commandlineoption_is_set(e, &mut is_set); - assert_eq!(r, OAKCOMMON_E_INVALID); -} - -#[test] -fn option_is_set_null_out_is_invalid() { - let mut p = new_parser(); - let mut opt = register_option(dup(&p), "o"); - let r = oakcommon_commandlineoption_is_set(dup(&opt), null_mut()); - assert_eq!(r, OAKCOMMON_E_INVALID); - oakcommon_commandlineoption_free(&mut opt); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn option_free_null_pointer_is_noop() { - oakcommon_commandlineoption_free(null_mut()); -} - -#[test] -fn option_free_twice_is_safe_and_nulls_ctx() { - let mut p = new_parser(); - let mut opt = register_option(dup(&p), "o"); - assert!(!opt.ctx.is_null()); - oakcommon_commandlineoption_free(&mut opt); - assert!(opt.ctx.is_null()); - // Second free on the same (now empty) handle must be a no-op. - oakcommon_commandlineoption_free(&mut opt); - assert!(opt.ctx.is_null()); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn option_free_null_handle_struct_is_noop() { - let mut h = CHandle::null(); - oakcommon_commandlineoption_free(&mut h); - assert!(h.ctx.is_null()); -} - -// --------------------------------------------------------------------------- -// oakcommon_commandlineoption_get_setting (TRUNCATING two-stage getter) -// --------------------------------------------------------------------------- - -#[test] -fn option_get_setting_size_query() { - let mut p = new_parser(); - let mut opt = register_option(dup(&p), "o"); - assert_eq!( - oakcommon_commandlineoption_set_setting(dup(&opt), c_str("abcdef").as_ptr()), - OAKCOMMON_OK - ); - // Stage 1: null buffer, size 0 -> required size len+1, nothing written. - let r = oakcommon_commandlineoption_get_setting(dup(&opt), null_mut(), 0); - assert_eq!(r, 7); // len("abcdef") + NUL - oakcommon_commandlineoption_free(&mut opt); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn option_get_setting_short_buffer_truncates_and_returns_required() { - let mut p = new_parser(); - let mut opt = register_option(dup(&p), "o"); - assert_eq!( - oakcommon_commandlineoption_set_setting(dup(&opt), c_str("abcdef").as_ptr()), - OAKCOMMON_OK - ); - // Stage 2: buf_size 4 < 7 -> writes "abc" + NUL, still returns 7. - let mut buf = [0xFFu8; 4]; - let r = oakcommon_commandlineoption_get_setting(dup(&opt), buf.as_mut_ptr() as *mut c_char, 4); - assert_eq!(r, 7); - assert_eq!(&buf[..3], b"abc"); - assert_eq!(buf[3], 0); - // buf_size 1 -> writes only the NUL terminator. - let mut tiny = [0xFFu8; 1]; - let r = oakcommon_commandlineoption_get_setting(dup(&opt), tiny.as_mut_ptr() as *mut c_char, 1); - assert_eq!(r, 7); - assert_eq!(tiny[0], 0); - oakcommon_commandlineoption_free(&mut opt); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn option_get_setting_exact_fit_writes_full_string() { - let mut p = new_parser(); - let mut opt = register_option(dup(&p), "o"); - assert_eq!( - oakcommon_commandlineoption_set_setting(dup(&opt), c_str("abcdef").as_ptr()), - OAKCOMMON_OK - ); - // Stage 3: buf_size == len+1 -> full string plus NUL, returns len+1. - let mut buf = [0xFFu8; 7]; - let r = oakcommon_commandlineoption_get_setting(dup(&opt), buf.as_mut_ptr() as *mut c_char, 7); - assert_eq!(r, 7); - assert_eq!(buf[6], 0); - let s = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }; - assert_eq!(s.to_str().unwrap(), "abcdef"); - oakcommon_commandlineoption_free(&mut opt); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn option_get_setting_non_null_buf_size_zero_writes_nothing() { - let mut p = new_parser(); - let mut opt = register_option(dup(&p), "o"); - assert_eq!( - oakcommon_commandlineoption_set_setting(dup(&opt), c_str("abcdef").as_ptr()), - OAKCOMMON_OK - ); - // A non-null buffer with size 0 is a valid size query: required returned, - // buffer untouched. - let mut buf = [0xFFu8; 8]; - let r = oakcommon_commandlineoption_get_setting(dup(&opt), buf.as_mut_ptr() as *mut c_char, 0); - assert_eq!(r, 7); - assert_eq!(buf, [0xFFu8; 8]); - oakcommon_commandlineoption_free(&mut opt); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn option_get_setting_unset_returns_empty_string() { - let mut p = new_parser(); - let mut opt = register_option(dup(&p), "o"); - // Never set: get_setting returns the empty string (required size 1). - let mut buf = [0xFFu8; 1]; - let r = oakcommon_commandlineoption_get_setting(dup(&opt), buf.as_mut_ptr() as *mut c_char, 1); - assert_eq!(r, 1); - assert_eq!(buf[0], 0); - oakcommon_commandlineoption_free(&mut opt); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn option_get_setting_empty_handle_is_invalid() { - let e = CHandle::null(); - let r = oakcommon_commandlineoption_get_setting(e, null_mut(), 0); - assert_eq!(r, OAKCOMMON_E_INVALID); -} - -#[test] -fn option_get_setting_negative_buf_size_is_invalid() { - let mut p = new_parser(); - let mut opt = register_option(dup(&p), "o"); - let mut buf = [0u8; 4]; - let r = oakcommon_commandlineoption_get_setting(dup(&opt), buf.as_mut_ptr() as *mut c_char, -1); - assert_eq!(r, OAKCOMMON_E_INVALID); - let r2 = oakcommon_commandlineoption_get_setting(dup(&opt), null_mut(), -1); - assert_eq!(r2, OAKCOMMON_E_INVALID); - oakcommon_commandlineoption_free(&mut opt); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn option_get_setting_null_buf_with_size_is_invalid() { - let mut p = new_parser(); - let mut opt = register_option(dup(&p), "o"); - let r = oakcommon_commandlineoption_get_setting(dup(&opt), null_mut(), 1); - assert_eq!(r, OAKCOMMON_E_INVALID); - oakcommon_commandlineoption_free(&mut opt); - oakcommon_commandlineparser_free(&mut p); -} - -// --------------------------------------------------------------------------- -// oakcommon_commandlineoption_set_setting -// --------------------------------------------------------------------------- - -#[test] -fn option_set_setting_success() { - let mut p = new_parser(); - let mut opt = register_option(dup(&p), "o"); - let r = oakcommon_commandlineoption_set_setting(dup(&opt), c_str("value").as_ptr()); - assert_eq!(r, OAKCOMMON_OK); - // Round-trip through the two-stage getter. - let mut buf = vec![0u8; 6]; - let r = oakcommon_commandlineoption_get_setting(dup(&opt), buf.as_mut_ptr() as *mut c_char, 6); - assert_eq!(r, 6); - let s = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }; - assert_eq!(s.to_str().unwrap(), "value"); - oakcommon_commandlineoption_free(&mut opt); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn option_set_setting_empty_handle_is_invalid() { - let e = CHandle::null(); - let r = oakcommon_commandlineoption_set_setting(e, c_str("v").as_ptr()); - assert_eq!(r, OAKCOMMON_E_INVALID); -} - -#[test] -fn option_set_setting_null_value_is_invalid() { - let mut p = new_parser(); - let mut opt = register_option(dup(&p), "o"); - let r = oakcommon_commandlineoption_set_setting(dup(&opt), null_mut()); - assert_eq!(r, OAKCOMMON_E_INVALID); - oakcommon_commandlineoption_free(&mut opt); - oakcommon_commandlineparser_free(&mut p); -} - -/// CPP-PARITY: `process()` mutates the parser-owned option; the option handle -/// returned by `add_option` is a registration-time snapshot in Rust (C++ -/// holds a pointer to the live option), so the handle does not observe the -/// parse: is_set stays false and get_setting stays empty. -#[test] -fn option_handle_does_not_observe_process() { - let mut p = new_parser(); - let mut opt = register_option(dup(&p), "o"); - let (_argv, argv_ptrs) = c_strings(&["prog", "-o", "out.mov"]); - let r = oakcommon_commandlineparser_process(dup(&p), argv_ptrs.as_ptr(), 3); - assert_eq!(r, OAKCOMMON_OK); - // The snapshot handle still reports the empty setting. - let r = oakcommon_commandlineoption_get_setting(dup(&opt), null_mut(), 0); - assert_eq!(r, 1); // empty string - let mut is_set = true; - assert_eq!( - oakcommon_commandlineoption_is_set(dup(&opt), &mut is_set), - OAKCOMMON_OK - ); - assert!(!is_set); - oakcommon_commandlineoption_free(&mut opt); - oakcommon_commandlineparser_free(&mut p); -} - -// --------------------------------------------------------------------------- -// oakcommon_commandlinepositionalargument_get_setting (TRUNCATING) -// --------------------------------------------------------------------------- - -#[test] -fn positional_get_setting_size_query() { - let mut p = new_parser(); - let mut pos = register_positional(dup(&p), "input"); - assert_eq!( - oakcommon_commandlinepositionalargument_set_setting(dup(&pos), c_str("abcdef").as_ptr()), - OAKCOMMON_OK - ); - let r = oakcommon_commandlinepositionalargument_get_setting(dup(&pos), null_mut(), 0); - assert_eq!(r, 7); // len("abcdef") + NUL - oakcommon_commandlinepositionalargument_free(&mut pos); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn positional_get_setting_short_buffer_truncates_and_returns_required() { - let mut p = new_parser(); - let mut pos = register_positional(dup(&p), "input"); - assert_eq!( - oakcommon_commandlinepositionalargument_set_setting(dup(&pos), c_str("abcdef").as_ptr()), - OAKCOMMON_OK - ); - let mut buf = [0xFFu8; 4]; - let r = oakcommon_commandlinepositionalargument_get_setting( - dup(&pos), - buf.as_mut_ptr() as *mut c_char, - 4, - ); - assert_eq!(r, 7); - assert_eq!(&buf[..3], b"abc"); - assert_eq!(buf[3], 0); - let mut tiny = [0xFFu8; 1]; - let r = oakcommon_commandlinepositionalargument_get_setting( - dup(&pos), - tiny.as_mut_ptr() as *mut c_char, - 1, - ); - assert_eq!(r, 7); - assert_eq!(tiny[0], 0); - oakcommon_commandlinepositionalargument_free(&mut pos); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn positional_get_setting_exact_fit_writes_full_string() { - let mut p = new_parser(); - let mut pos = register_positional(dup(&p), "input"); - assert_eq!( - oakcommon_commandlinepositionalargument_set_setting(dup(&pos), c_str("abcdef").as_ptr()), - OAKCOMMON_OK - ); - let mut buf = [0xFFu8; 7]; - let r = oakcommon_commandlinepositionalargument_get_setting( - dup(&pos), - buf.as_mut_ptr() as *mut c_char, - 7, - ); - assert_eq!(r, 7); - assert_eq!(buf[6], 0); - let s = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }; - assert_eq!(s.to_str().unwrap(), "abcdef"); - oakcommon_commandlinepositionalargument_free(&mut pos); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn positional_get_setting_non_null_buf_size_zero_writes_nothing() { - let mut p = new_parser(); - let mut pos = register_positional(dup(&p), "input"); - assert_eq!( - oakcommon_commandlinepositionalargument_set_setting(dup(&pos), c_str("abcdef").as_ptr()), - OAKCOMMON_OK - ); - let mut buf = [0xFFu8; 8]; - let r = oakcommon_commandlinepositionalargument_get_setting( - dup(&pos), - buf.as_mut_ptr() as *mut c_char, - 0, - ); - assert_eq!(r, 7); - assert_eq!(buf, [0xFFu8; 8]); - oakcommon_commandlinepositionalargument_free(&mut pos); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn positional_get_setting_unset_returns_empty_string() { - let mut p = new_parser(); - let mut pos = register_positional(dup(&p), "input"); - let mut buf = [0xFFu8; 1]; - let r = oakcommon_commandlinepositionalargument_get_setting( - dup(&pos), - buf.as_mut_ptr() as *mut c_char, - 1, - ); - assert_eq!(r, 1); - assert_eq!(buf[0], 0); - oakcommon_commandlinepositionalargument_free(&mut pos); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn positional_get_setting_empty_handle_is_invalid() { - let e = CHandle::null(); - let r = oakcommon_commandlinepositionalargument_get_setting(e, null_mut(), 0); - assert_eq!(r, OAKCOMMON_E_INVALID); -} - -#[test] -fn positional_get_setting_negative_buf_size_is_invalid() { - let mut p = new_parser(); - let mut pos = register_positional(dup(&p), "input"); - let mut buf = [0u8; 4]; - let r = oakcommon_commandlinepositionalargument_get_setting( - dup(&pos), - buf.as_mut_ptr() as *mut c_char, - -1, - ); - assert_eq!(r, OAKCOMMON_E_INVALID); - let r2 = oakcommon_commandlinepositionalargument_get_setting(dup(&pos), null_mut(), -1); - assert_eq!(r2, OAKCOMMON_E_INVALID); - oakcommon_commandlinepositionalargument_free(&mut pos); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn positional_get_setting_null_buf_with_size_is_invalid() { - let mut p = new_parser(); - let mut pos = register_positional(dup(&p), "input"); - let r = oakcommon_commandlinepositionalargument_get_setting(dup(&pos), null_mut(), 1); - assert_eq!(r, OAKCOMMON_E_INVALID); - oakcommon_commandlinepositionalargument_free(&mut pos); - oakcommon_commandlineparser_free(&mut p); -} - -// --------------------------------------------------------------------------- -// oakcommon_commandlinepositionalargument_set_setting / _free -// --------------------------------------------------------------------------- - -#[test] -fn positional_set_setting_success() { - let mut p = new_parser(); - let mut pos = register_positional(dup(&p), "input"); - let r = - oakcommon_commandlinepositionalargument_set_setting(dup(&pos), c_str("file.mp4").as_ptr()); - assert_eq!(r, OAKCOMMON_OK); - // Round-trip through the two-stage getter. - let mut buf = vec![0u8; 9]; - let r = oakcommon_commandlinepositionalargument_get_setting( - dup(&pos), - buf.as_mut_ptr() as *mut c_char, - 9, - ); - assert_eq!(r, 9); - let s = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }; - assert_eq!(s.to_str().unwrap(), "file.mp4"); - oakcommon_commandlinepositionalargument_free(&mut pos); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn positional_set_setting_empty_handle_is_invalid() { - let e = CHandle::null(); - let r = oakcommon_commandlinepositionalargument_set_setting(e, c_str("v").as_ptr()); - assert_eq!(r, OAKCOMMON_E_INVALID); -} - -#[test] -fn positional_set_setting_null_value_is_invalid() { - let mut p = new_parser(); - let mut pos = register_positional(dup(&p), "input"); - let r = oakcommon_commandlinepositionalargument_set_setting(dup(&pos), null_mut()); - assert_eq!(r, OAKCOMMON_E_INVALID); - oakcommon_commandlinepositionalargument_free(&mut pos); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn positional_free_null_pointer_is_noop() { - oakcommon_commandlinepositionalargument_free(null_mut()); -} - -#[test] -fn positional_free_twice_is_safe_and_nulls_ctx() { - let mut p = new_parser(); - let mut pos = register_positional(dup(&p), "input"); - assert!(!pos.ctx.is_null()); - oakcommon_commandlinepositionalargument_free(&mut pos); - assert!(pos.ctx.is_null()); - // Second free on the same (now empty) handle must be a no-op. - oakcommon_commandlinepositionalargument_free(&mut pos); - assert!(pos.ctx.is_null()); - oakcommon_commandlineparser_free(&mut p); -} - -#[test] -fn positional_free_null_handle_struct_is_noop() { - let mut h = CHandle::null(); - oakcommon_commandlinepositionalargument_free(&mut h); - assert!(h.ctx.is_null()); -} diff --git a/crates/oakcommon/tests/ffi_config.rs b/crates/oakcommon/tests/ffi_config.rs deleted file mode 100644 index fe3b7a888..000000000 --- a/crates/oakcommon/tests/ffi_config.rs +++ /dev/null @@ -1,519 +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 . - -//! FFI integration tests for the `config` submodule of the C ABI layer -//! (`oakcommon::ffi::config`, backed by `include/common/config.h`). -//! -//! Every exported symbol is exercised with both a success and a failure -//! path. The store is a process-wide singleton and the C-ABI `config` -//! functions have no handle, so the failure paths are the documented -//! behaviors: null/empty keys, invalid buffers, and absent entries. -//! -//! Tests use per-test unique keys so they stay independent of each other -//! and of the domain unit tests, and every test that mutates the singleton -//! (or the `OAK_CONFIG_DIR` env override) serializes on a local mutex. The -//! crate's `test_support::env_lock` is `#[cfg(test)]` and therefore not -//! visible to this integration-test crate, and the domain unit tests run in -//! a separate process anyway, so a local lock is sufficient. - -use std::ffi::{c_char, c_void, CStr, CString}; -use std::path::{Path, PathBuf}; -use std::ptr::{null, null_mut}; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Mutex, MutexGuard}; - -use oakcommon::error::{ - OAKCOMMON_E_FAILED, OAKCOMMON_E_INVALID, OAKCOMMON_E_NOT_FOUND, OAKCOMMON_OK, -}; -use oakcommon::ffi::config::*; - -// ---- Test infrastructure -------------------------------------------------- - -/// Serializes tests that mutate the process-wide singleton store or the -/// `OAK_CONFIG_DIR` env override. Without this, a `reset_defaults`/`load` -/// running between another test's `set` and `get` would clear its key. -static LOCK: Mutex<()> = Mutex::new(()); - -fn lock() -> MutexGuard<'static, ()> { - LOCK.lock().unwrap() -} - -static KEY_SEQ: AtomicUsize = AtomicUsize::new(0); - -/// A per-test unique key, so parallel tests cannot collide on the shared -/// store. -fn unique_key(name: &str) -> CString { - let n = KEY_SEQ.fetch_add(1, Ordering::Relaxed); - CString::new(format!("itest_{}_{}_{}", name, std::process::id(), n)).unwrap() -} - -fn cstr(s: &str) -> CString { - CString::new(s).unwrap() -} - -/// Cleans up the `OAK_CONFIG_DIR` override and its temp dir even when the -/// test body panics. -struct TempConfigDir(PathBuf); - -impl Drop for TempConfigDir { - fn drop(&mut self) { - std::env::remove_var("OAK_CONFIG_DIR"); - let _ = std::fs::remove_dir_all(&self.0); - } -} - -/// Clears the process-global error handler on drop so a panicking test -/// cannot leak it to the rest of the suite. -struct HandlerGuard; - -impl Drop for HandlerGuard { - fn drop(&mut self) { - oakcommon_config_set_error_handler(None, std::ptr::null_mut()); - } -} - -/// Point `OAK_CONFIG_DIR` at an isolated temp dir, run `f`, then clean up. -/// Mirrors the `with_temp_config` pattern of the domain unit tests. -fn with_temp_config(f: impl FnOnce(&Path) -> T) -> T { - let _guard = lock(); - let dir = - std::env::temp_dir().join(format!("oakcommon_ffi_config_test_{}", std::process::id())); - let _ = std::fs::create_dir_all(&dir); - std::env::set_var("OAK_CONFIG_DIR", &dir); - let _cleanup = TempConfigDir(dir.clone()); - f(&dir) -} - -// ---- Error handler recording --------------------------------------------- - -static HANDLER_HITS: AtomicUsize = AtomicUsize::new(0); -static HANDLER_USERDATA: AtomicUsize = AtomicUsize::new(0); -static HANDLER_TITLE: Mutex> = Mutex::new(None); - -unsafe extern "C" fn record_handler( - title: *const c_char, - _message: *const c_char, - userdata: *mut c_void, -) { - HANDLER_HITS.fetch_add(1, Ordering::SeqCst); - HANDLER_USERDATA.store(userdata as usize, Ordering::SeqCst); - if !title.is_null() { - let s = unsafe { CStr::from_ptr(title) } - .to_string_lossy() - .into_owned(); - *HANDLER_TITLE.lock().unwrap() = Some(s); - } -} - -// ---- load / save ---------------------------------------------------------- - -/// `oakcommon_config_load` treats a missing `config.ini` as a non-error. -#[test] -fn config_load_missing_file_is_ok() { - with_temp_config(|_| { - assert_eq!(oakcommon_config_load(), OAKCOMMON_OK); - }); -} - -/// `save` persists the store; `load` re-reads it (resetting to defaults -/// first), so the round trip restores the custom key. -#[test] -fn config_save_load_roundtrip() { - with_temp_config(|_| { - let key = unique_key("save_roundtrip"); - oakcommon_config_set(null(), key.as_ptr(), cstr("persisted").as_ptr()); - assert_eq!(oakcommon_config_save(), OAKCOMMON_OK); - assert_eq!(oakcommon_config_load(), OAKCOMMON_OK); - let mut buf = [0i8; 32]; - let n = oakcommon_config_get(null(), key.as_ptr(), buf.as_mut_ptr(), 32); - assert_eq!(n, 10); // "persisted" + NUL - assert_eq!( - unsafe { CStr::from_ptr(buf.as_ptr()) }.to_str().unwrap(), - "persisted" - ); - }); -} - -/// A save that cannot write reports through the registered error handler -/// and returns `OAKCOMMON_E_FAILED`. `OAK_CONFIG_DIR` points at a regular -/// file so `"/config.ini.tmp"` cannot be created (mirrors the domain -/// unit test). -#[test] -fn config_save_failure_reports_and_returns_failed() { - let _guard = lock(); - let dir = std::env::temp_dir().join(format!( - "oakcommon_ffi_config_blocked_{}", - std::process::id() - )); - let _ = std::fs::create_dir_all(&dir); - let blocker = dir.join("not_a_dir"); - std::fs::write(&blocker, b"x").unwrap(); - std::env::set_var("OAK_CONFIG_DIR", &blocker); - let _cleanup = TempConfigDir(dir); - - HANDLER_HITS.store(0, Ordering::SeqCst); - HANDLER_USERDATA.store(0, Ordering::SeqCst); - *HANDLER_TITLE.lock().unwrap() = None; - let userdata: usize = 0xdead_beef; - let _handler_guard = HandlerGuard; - assert_eq!( - oakcommon_config_set_error_handler(Some(record_handler), userdata as *mut c_void), - OAKCOMMON_OK - ); - - assert_eq!(oakcommon_config_save(), OAKCOMMON_E_FAILED); - assert_eq!(HANDLER_HITS.load(Ordering::SeqCst), 1); - assert_eq!(HANDLER_USERDATA.load(Ordering::SeqCst), userdata); - assert_eq!( - HANDLER_TITLE.lock().unwrap().as_deref(), - Some("Error saving settings") - ); -} - -// ---- reset_defaults ------------------------------------------------------- - -/// `reset_defaults` drops custom keys and restores the compiled-in -/// defaults. -#[test] -fn config_reset_defaults_restores_defaults() { - let _guard = lock(); - let custom = unique_key("reset_custom"); - oakcommon_config_set_int(null(), custom.as_ptr(), 1); - assert_eq!(oakcommon_config_get_int(null(), custom.as_ptr(), -1), 1); - - assert_eq!(oakcommon_config_reset_defaults(), OAKCOMMON_OK); - - // The custom key is gone... - assert_eq!(oakcommon_config_get_int(null(), custom.as_ptr(), -1), -1); - assert_eq!( - oakcommon_config_entry_type(null(), custom.as_ptr()), - OAKCOMMON_E_NOT_FOUND - ); - // ...and the compiled-in defaults are back. - assert_eq!( - oakcommon_config_get_int(null(), cstr("DefaultSequenceWidth").as_ptr(), -1), - 1920 - ); - assert_eq!( - oakcommon_config_get_bool(null(), cstr("UseProxyMedia").as_ptr(), -1), - 1 - ); -} - -// ---- string set / get ----------------------------------------------------- - -/// Null keys/values are silently ignored by `oakcommon_config_set`. -#[test] -fn config_set_null_key_is_noop() { - let _guard = lock(); - oakcommon_config_set(null(), null(), cstr("v").as_ptr()); - oakcommon_config_set(null(), cstr("k").as_ptr(), null()); -} - -/// Two-stage getter, stage 1: a null buffer with size 0 returns the -/// required size (length + NUL) without writing. -#[test] -fn config_get_size_query_returns_required() { - let _guard = lock(); - let key = unique_key("size_query"); - let value = "hello"; - oakcommon_config_set(null(), key.as_ptr(), cstr(value).as_ptr()); - let required = oakcommon_config_get(null(), key.as_ptr(), null_mut(), 0); - assert_eq!(required, value.len() as i32 + 1); -} - -/// Two-stage getter, short buffer: the full required size is returned but -/// the buffer is NOT touched (non-truncating, unlike `copy_setting`). -#[test] -fn config_get_short_buffer_is_non_truncating() { - let _guard = lock(); - let key = unique_key("short_buf"); - let value = "hello world"; // required = 12 - oakcommon_config_set(null(), key.as_ptr(), cstr(value).as_ptr()); - let mut buf = [0x7a as c_char; 8]; - let n = oakcommon_config_get(null(), key.as_ptr(), buf.as_mut_ptr(), buf.len() as i32); - assert_eq!(n, value.len() as i32 + 1); - assert_eq!(buf, [0x7a as c_char; 8]); // sentinel untouched -} - -/// Two-stage getter, exact fit: a buffer of exactly `len + 1` receives the -/// string plus NUL terminator. -#[test] -fn config_get_exact_fit_buffer() { - let _guard = lock(); - let key = unique_key("exact_fit"); - let value = "hello"; - oakcommon_config_set(null(), key.as_ptr(), cstr(value).as_ptr()); - let required = value.len() as i32 + 1; - let mut buf = vec![0i8; required as usize]; - let n = oakcommon_config_get(null(), key.as_ptr(), buf.as_mut_ptr(), required); - assert_eq!(n, required); - assert_eq!( - unsafe { CStr::from_ptr(buf.as_ptr()) }.to_str().unwrap(), - value - ); -} - -/// A missing key yields `OAKCOMMON_E_NOT_FOUND`. -#[test] -fn config_get_missing_key_returns_not_found() { - let _guard = lock(); - let key = unique_key("missing"); - let mut buf = [0i8; 16]; - let n = oakcommon_config_get(null(), key.as_ptr(), buf.as_mut_ptr(), buf.len() as i32); - assert_eq!(n, OAKCOMMON_E_NOT_FOUND); -} - -/// A null key yields `OAKCOMMON_E_INVALID`. -#[test] -fn config_get_null_key_returns_invalid() { - let n = oakcommon_config_get(null(), null(), null_mut(), 0); - assert_eq!(n, OAKCOMMON_E_INVALID); -} - -/// An empty key is invalid (matches the C++ `is_valid_key` check, handled -/// here by the domain layer). -#[test] -fn config_get_empty_key_returns_invalid() { - let mut buf = [0i8; 8]; - let n = oakcommon_config_get(null(), cstr("").as_ptr(), buf.as_mut_ptr(), 8); - assert_eq!(n, OAKCOMMON_E_INVALID); -} - -/// A negative buffer size is an invalid output buffer. -#[test] -fn config_get_invalid_buffer_returns_invalid() { - let _guard = lock(); - let key = unique_key("bad_buf"); - oakcommon_config_set(null(), key.as_ptr(), cstr("v").as_ptr()); - let n = oakcommon_config_get(null(), key.as_ptr(), null_mut(), -1); - assert_eq!(n, OAKCOMMON_E_INVALID); -} - -// ---- int / int64 ---------------------------------------------------------- - -/// INT set/get round trip; the string getter formats the int. -#[test] -fn config_set_get_int_roundtrip() { - let _guard = lock(); - let key = unique_key("int"); - oakcommon_config_set_int(null(), key.as_ptr(), 42); - assert_eq!(oakcommon_config_get_int(null(), key.as_ptr(), -1), 42); - assert_eq!(oakcommon_config_entry_type(null(), key.as_ptr()), 2); - - let mut buf = [0i8; 16]; - let n = oakcommon_config_get(null(), key.as_ptr(), buf.as_mut_ptr(), 16); - assert_eq!(n, 3); // "42" + NUL - assert_eq!( - unsafe { CStr::from_ptr(buf.as_ptr()) }.to_str().unwrap(), - "42" - ); -} - -/// INT getter fallback paths: absent key, wrong type, null key. -#[test] -fn config_get_int_fallback_paths() { - let _guard = lock(); - let missing = unique_key("int_missing"); - let wrong = unique_key("int_wrongtype"); - oakcommon_config_set(null(), wrong.as_ptr(), cstr("abc").as_ptr()); - assert_eq!(oakcommon_config_get_int(null(), missing.as_ptr(), 7), 7); - assert_eq!(oakcommon_config_get_int(null(), wrong.as_ptr(), 7), 7); - assert_eq!(oakcommon_config_get_int(null(), null(), 9), 9); -} - -/// Setting a string onto an existing typed entry parses it into that type; -/// an unparseable string leaves the entry unchanged (CPP-PARITY with -/// `config.cpp`). -#[test] -fn config_set_string_parses_into_typed_entry() { - let _guard = lock(); - let key = unique_key("typed_parse"); - oakcommon_config_set_int(null(), key.as_ptr(), 42); - oakcommon_config_set(null(), key.as_ptr(), cstr("7").as_ptr()); - assert_eq!(oakcommon_config_get_int(null(), key.as_ptr(), -1), 7); - oakcommon_config_set(null(), key.as_ptr(), cstr("notanumber").as_ptr()); - assert_eq!(oakcommon_config_get_int(null(), key.as_ptr(), -1), 7); -} - -/// INT64 set/get round trip and fallback paths. -#[test] -fn config_set_get_int64_roundtrip() { - let _guard = lock(); - let key = unique_key("int64"); - let big: i64 = 3_000_000_000; // exceeds i32 range - oakcommon_config_set_int64(null(), key.as_ptr(), big); - assert_eq!(oakcommon_config_get_int64(null(), key.as_ptr(), -1), big); - - let missing = unique_key("int64_missing"); - assert_eq!( - oakcommon_config_get_int64(null(), missing.as_ptr(), -99), - -99 - ); - assert_eq!(oakcommon_config_get_int64(null(), null(), -99), -99); -} - -// ---- double --------------------------------------------------------------- - -/// DOUBLE set/get round trip and fallback paths. -#[test] -fn config_set_get_double_roundtrip() { - let _guard = lock(); - let key = unique_key("double"); - oakcommon_config_set_double(null(), key.as_ptr(), 3.5); - assert_eq!(oakcommon_config_get_double(null(), key.as_ptr(), -1.0), 3.5); - assert_eq!(oakcommon_config_entry_type(null(), key.as_ptr()), 3); - - let missing = unique_key("double_missing"); - let wrong = unique_key("double_wrongtype"); - oakcommon_config_set_int(null(), wrong.as_ptr(), 1); - assert_eq!( - oakcommon_config_get_double(null(), missing.as_ptr(), 2.5), - 2.5 - ); - assert_eq!( - oakcommon_config_get_double(null(), wrong.as_ptr(), 2.5), - 2.5 - ); - assert_eq!(oakcommon_config_get_double(null(), null(), 2.5), 2.5); -} - -// ---- bool ----------------------------------------------------------------- - -/// BOOL set/get round trip (0/1) and string formatting as "true"/"false". -#[test] -fn config_set_get_bool_roundtrip() { - let _guard = lock(); - let key = unique_key("bool"); - oakcommon_config_set_bool(null(), key.as_ptr(), 1); - assert_eq!(oakcommon_config_get_bool(null(), key.as_ptr(), -1), 1); - assert_eq!(oakcommon_config_entry_type(null(), key.as_ptr()), 4); - - let mut buf = [0i8; 8]; - let n = oakcommon_config_get(null(), key.as_ptr(), buf.as_mut_ptr(), 8); - assert_eq!(n, 5); // "true" + NUL - assert_eq!( - unsafe { CStr::from_ptr(buf.as_ptr()) }.to_str().unwrap(), - "true" - ); - - oakcommon_config_set_bool(null(), key.as_ptr(), 0); - assert_eq!(oakcommon_config_get_bool(null(), key.as_ptr(), -1), 0); - let n = oakcommon_config_get(null(), key.as_ptr(), buf.as_mut_ptr(), 8); - assert_eq!(n, 6); // "false" + NUL - assert_eq!( - unsafe { CStr::from_ptr(buf.as_ptr()) }.to_str().unwrap(), - "false" - ); -} - -/// BOOL getter fallback paths: absent key, wrong type, null key. -#[test] -fn config_get_bool_fallback_paths() { - let _guard = lock(); - let missing = unique_key("bool_missing"); - let wrong = unique_key("bool_wrongtype"); - oakcommon_config_set_int(null(), wrong.as_ptr(), 1); - assert_eq!(oakcommon_config_get_bool(null(), missing.as_ptr(), 1), 1); - assert_eq!(oakcommon_config_get_bool(null(), wrong.as_ptr(), 0), 0); - assert_eq!(oakcommon_config_get_bool(null(), null(), 1), 1); -} - -/// Null keys are silently ignored by every typed setter. -#[test] -fn config_typed_setters_null_key_are_noop() { - let _guard = lock(); - oakcommon_config_set_int(null(), null(), 1); - oakcommon_config_set_int64(null(), null(), 1); - oakcommon_config_set_bool(null(), null(), 1); - oakcommon_config_set_double(null(), null(), 1.0); -} - -// ---- groups --------------------------------------------------------------- - -/// A non-empty group prefixes the stored key (`group/key`), so the flat -/// lookup must not find it. -#[test] -fn config_group_prefixes_keys() { - let _guard = lock(); - let key = unique_key("grouped"); - let group = cstr("ffi_test_group"); - oakcommon_config_set_int(group.as_ptr(), key.as_ptr(), 5); - assert_eq!(oakcommon_config_get_int(null(), key.as_ptr(), -1), -1); - assert_eq!( - oakcommon_config_get_int(group.as_ptr(), key.as_ptr(), -1), - 5 - ); - assert_eq!(oakcommon_config_entry_type(group.as_ptr(), key.as_ptr()), 2); -} - -// ---- entry_type ----------------------------------------------------------- - -/// Entry type codes: 1 = String, 2 = Int, 3 = Double, 4 = Bool. -#[test] -fn config_entry_type_codes() { - let _guard = lock(); - let s = unique_key("et_str"); - let i = unique_key("et_int"); - let d = unique_key("et_double"); - let b = unique_key("et_bool"); - oakcommon_config_set(null(), s.as_ptr(), cstr("x").as_ptr()); - oakcommon_config_set_int(null(), i.as_ptr(), 1); - oakcommon_config_set_double(null(), d.as_ptr(), 1.0); - oakcommon_config_set_bool(null(), b.as_ptr(), 1); - assert_eq!(oakcommon_config_entry_type(null(), s.as_ptr()), 1); - assert_eq!(oakcommon_config_entry_type(null(), i.as_ptr()), 2); - assert_eq!(oakcommon_config_entry_type(null(), d.as_ptr()), 3); - assert_eq!(oakcommon_config_entry_type(null(), b.as_ptr()), 4); -} - -/// Missing, null, and empty keys for `entry_type`. -#[test] -fn config_entry_type_missing_and_invalid() { - let _guard = lock(); - let missing = unique_key("et_missing"); - assert_eq!( - oakcommon_config_entry_type(null(), missing.as_ptr()), - OAKCOMMON_E_NOT_FOUND - ); - assert_eq!( - oakcommon_config_entry_type(null(), null()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_config_entry_type(null(), cstr("").as_ptr()), - OAKCOMMON_E_INVALID - ); -} - -// ---- error handler -------------------------------------------------------- - -/// Registering and clearing the error handler both succeed; the handler -/// itself is exercised by `config_save_failure_reports_and_returns_failed`. -#[test] -fn config_set_error_handler_roundtrip() { - let _guard = lock(); - let userdata = 0x1234 as *mut c_void; - let _handler_guard = HandlerGuard; - assert_eq!( - oakcommon_config_set_error_handler(Some(record_handler), userdata), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_config_set_error_handler(None, std::ptr::null_mut()), - OAKCOMMON_OK - ); -} diff --git a/crates/oakcommon/tests/ffi_ffmpegutils.rs b/crates/oakcommon/tests/ffi_ffmpegutils.rs deleted file mode 100644 index ee47f0e59..000000000 --- a/crates/oakcommon/tests/ffi_ffmpegutils.rs +++ /dev/null @@ -1,247 +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 C ABI surface of `oakcommon::ffi::ffmpegutils`, -//! which implements `include/common/ffmpegutils.h` (oracle: -//! `src/common/src/ffmpegutils.cpp`). -//! -//! MUST be run with `--features test-stubs`: unlike every other ffi module, -//! `oakcommon_ffmpegutils_get_compatible_bridge_pixel_format` reaches the -//! real `fb_find_best_pix_fmt_of_list` symbol (ffmpeg_bridge C ABI) in -//! non-test builds, and the integration-test binary cannot link it. The -//! `test-stubs` feature substitutes the in-crate stub (see -//! `src/ffmpegutils.rs::find_best_pix_fmt_of_list`), mirroring the -//! oakplugin/oaktimeline convention. `cargo test --lib` needs no flag: the -//! stub is active under `cfg(test)` there. -//! -//! The pure mappings are already unit-tested in `src/ffmpegutils.rs`; these -//! tests pin the exported wrapper contract: a value written to a non-null -//! out-param with `OAKCOMMON_OK`, or `OAKCOMMON_E_INVALID` for a null -//! out-param, for every export. - -#[cfg(feature = "test-stubs")] -mod ffi_ffmpegutils_tests { - use std::ptr::null_mut; - - use oakcommon::error::{OAKCOMMON_E_INVALID, OAKCOMMON_OK}; - use oakcommon::ffi::ffmpegutils::*; - - /// Every out-param wrapper must reject a null pointer without writing. - #[test] - fn null_out_param_returns_invalid() { - let mut out: i32 = -999; - assert_eq!( - oakcommon_ffmpegutils_get_compatible_bridge_pixel_format(26, -1, null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_ffmpegutils_get_compatible_pixel_format(0, null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_ffmpegutils_get_ffmpeg_pixel_format(0, 4, null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_ffmpegutils_get_native_sample_format(0, null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_ffmpegutils_get_ffmpeg_sample_format(6, null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(12, null_mut()), - OAKCOMMON_E_INVALID - ); - // The out-param is untouched on the failure path. - assert_eq!(out, -999); - } - - /// `get_compatible_bridge_pixel_format` picks an exact candidate from the - /// clamped list (values match `FB_PIX_FMT_*` / `PixelFormat`). - #[test] - fn compatible_bridge_pixel_format_exact_candidate() { - let mut out: i32 = -999; - // pix_fmt = RGBA (26), maximum = invalid (-1): no limit, RGBA is - // first in the candidate list. - assert_eq!( - oakcommon_ffmpegutils_get_compatible_bridge_pixel_format(26, -1, &mut out), - OAKCOMMON_OK - ); - assert_eq!(out, 26); - // pix_fmt = RGBA_F32_LE (220), maximum = F32 (4): the f32 clamp adds - // RGBA_F32_LE, so it matches exactly. - assert_eq!( - oakcommon_ffmpegutils_get_compatible_bridge_pixel_format(220, 4, &mut out), - OAKCOMMON_OK - ); - assert_eq!(out, 220); - } - - /// `get_compatible_bridge_pixel_format` falls back to the first candidate - /// for an unknown source format (bridge loss-metric behaviour). - #[test] - fn compatible_bridge_pixel_format_unknown_falls_back() { - let mut out: i32 = -999; - // YUV420P (0) is not in the RGBA-oriented candidate list -> first - // candidate RGBA (26). - assert_eq!( - oakcommon_ffmpegutils_get_compatible_bridge_pixel_format(0, -1, &mut out), - OAKCOMMON_OK - ); - assert_eq!(out, 26); - } - - /// `get_compatible_pixel_format` maps native formats to the least-lossy - /// native format. - #[test] - fn compatible_pixel_format_maps_native() { - let mut out: i32 = -999; - assert_eq!( - oakcommon_ffmpegutils_get_compatible_pixel_format(0, &mut out), - OAKCOMMON_OK - ); // U8 - assert_eq!(out, 0); - assert_eq!( - oakcommon_ffmpegutils_get_compatible_pixel_format(3, &mut out), - OAKCOMMON_OK - ); // F16 - assert_eq!(out, 2); // -> U16 - assert_eq!( - oakcommon_ffmpegutils_get_compatible_pixel_format(-1, &mut out), - OAKCOMMON_OK - ); // invalid - assert_eq!(out, -1); - } - - /// `get_ffmpeg_pixel_format` maps native format + channel count to a - /// bridge pixel format. - #[test] - fn ffmpeg_pixel_format_maps_native_to_bridge() { - let mut out: i32 = -999; - assert_eq!( - oakcommon_ffmpegutils_get_ffmpeg_pixel_format(0, 3, &mut out), - OAKCOMMON_OK - ); // U8 RGB - assert_eq!(out, 2); // RGB24 - assert_eq!( - oakcommon_ffmpegutils_get_ffmpeg_pixel_format(0, 4, &mut out), - OAKCOMMON_OK - ); // U8 RGBA - assert_eq!(out, 26); // RGBA - assert_eq!( - oakcommon_ffmpegutils_get_ffmpeg_pixel_format(2, 4, &mut out), - OAKCOMMON_OK - ); // U16 RGBA - assert_eq!(out, 105); // RGBA64LE - assert_eq!( - oakcommon_ffmpegutils_get_ffmpeg_pixel_format(1, 3, &mut out), - OAKCOMMON_OK - ); // U10 RGB - assert_eq!(out, -1); // no bridge format - } - - /// `get_native_sample_format` maps bridge sample formats to native. - #[test] - fn native_sample_format_maps_bridge_to_native() { - let mut out: i32 = -999; - assert_eq!( - oakcommon_ffmpegutils_get_native_sample_format(0, &mut out), - OAKCOMMON_OK - ); // U8 - assert_eq!(out, 6); // SMP_FMT_U8 - assert_eq!( - oakcommon_ffmpegutils_get_native_sample_format(1, &mut out), - OAKCOMMON_OK - ); // S16 - assert_eq!(out, 7); - assert_eq!( - oakcommon_ffmpegutils_get_native_sample_format(8, &mut out), - OAKCOMMON_OK - ); // FLTP - assert_eq!(out, 4); // SMP_FMT_F32_P - assert_eq!( - oakcommon_ffmpegutils_get_native_sample_format(999, &mut out), - OAKCOMMON_OK - ); - assert_eq!(out, -1); // unknown -> invalid - } - - /// `get_ffmpeg_sample_format` maps native sample formats to bridge. - #[test] - fn ffmpeg_sample_format_maps_native_to_bridge() { - let mut out: i32 = -999; - assert_eq!( - oakcommon_ffmpegutils_get_ffmpeg_sample_format(6, &mut out), - OAKCOMMON_OK - ); // SMP_FMT_U8 - assert_eq!(out, 0); // U8 - assert_eq!( - oakcommon_ffmpegutils_get_ffmpeg_sample_format(10, &mut out), - OAKCOMMON_OK - ); // SMP_FMT_F32 - assert_eq!(out, 3); // FLT - assert_eq!( - oakcommon_ffmpegutils_get_ffmpeg_sample_format(-1, &mut out), - OAKCOMMON_OK - ); // invalid - assert_eq!(out, -1); - } - - /// JPEG-range bridge formats convert to their regular counterparts; - /// everything else passes through unchanged. - #[test] - fn jpeg_space_converts_to_regular_space() { - let mut out: i32 = -999; - assert_eq!( - oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(12, &mut out), - OAKCOMMON_OK - ); // YUVJ420P - assert_eq!(out, 0); // YUV420P - assert_eq!( - oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(13, &mut out), - OAKCOMMON_OK - ); // YUVJ422P - assert_eq!(out, 4); // YUV422P - assert_eq!( - oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(14, &mut out), - OAKCOMMON_OK - ); // YUVJ444P - assert_eq!(out, 5); // YUV444P - assert_eq!( - oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(32, &mut out), - OAKCOMMON_OK - ); // YUVJ440P - assert_eq!(out, 31); // YUV440P - assert_eq!( - oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(138, &mut out), - OAKCOMMON_OK - ); // YUVJ411P - assert_eq!(out, 7); // YUV411P - // Non-JPEG formats pass through unchanged. - assert_eq!( - oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(26, &mut out), - OAKCOMMON_OK - ); // RGBA - assert_eq!(out, 26); - assert_eq!( - oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(-1, &mut out), - OAKCOMMON_OK - ); // none - assert_eq!(out, -1); - } -} diff --git a/crates/oakcommon/tests/ffi_misc.rs b/crates/oakcommon/tests/ffi_misc.rs deleted file mode 100644 index a9ee4ed9c..000000000 --- a/crates/oakcommon/tests/ffi_misc.rs +++ /dev/null @@ -1,670 +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 . - -//! FFI-level integration tests for the C-ABI `misc`, `debug`, and `current` -//! exports in `oakcommon::ffi`, asserted against the C++ oracle -//! (`src/common/c_api/*.cpp`) and the Rust domain modules -//! (`src/common/rust/src/{miscutils,debug}.rs`). -//! -//! The contract under test (each point matches the implementations): -//! - decibel/lerp/power helpers write into a non-null out-param and reject a -//! null one with `E_INVALID`; domain helpers never fail, so the only -//! failure path is the null out-param; -//! - `drop_workflow_behavior_is_valid` returns 1/0 without error codes, and -//! `drop_workflow_behavior_name` is a non-truncating two-stage getter -//! ("UNKNOWN" for out-of-range codes); -//! - `debug` exports: null messages are `E_INVALID`, the level-name getter -//! is a non-truncating two-stage getter, level codes outside 0..=4 are -//! rejected by `log_set_level` with `E_INVALID`, and the get-level export -//! rejects a null out-param; -//! - `current` exports: the singleton handle is stamped and `free` is -//! idempotent, setters/getters reject a null handle (getters also reject a -//! null out-param), empty slots read back as null, and replacing an -//! occupied slot runs the previous owner's destructor exactly once. - -use std::ffi::{c_char, c_void, CString}; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Mutex; - -use oakcommon::error::{OAKCOMMON_E_INVALID, OAKCOMMON_OK}; -use oakcommon::ffi::current::*; -use oakcommon::ffi::debug::*; -use oakcommon::ffi::misc::*; -use oakcommon::handle::{CHandle, OAKCOMMON_ABI_VERSION}; - -/// Serialises tests that mutate the process-wide `Current` singleton (cargo -/// runs tests on threads). -static CURRENT_LOCK: Mutex<()> = Mutex::new(()); - -/// Serialises tests that mutate the process-wide logging level. -static DEBUG_LOCK: Mutex<()> = Mutex::new(()); - -/// Destroy tally for the slot-replacement test. -static DESTROY_COUNT: AtomicUsize = AtomicUsize::new(0); - -/// A `DestroyFn` that bumps [`DESTROY_COUNT`]. -unsafe extern "C" fn count_destroy(_p: *mut c_void) { - DESTROY_COUNT.fetch_add(1, Ordering::SeqCst); -} - -/// Convert a string slice to a NUL-terminated C string for FFI inputs. -fn to_cstring(s: &str) -> CString { - CString::new(s).expect("test string must not contain NUL") -} - -/// Cheap struct copy: `CHandle` is neither `Clone` nor `Copy`, but every -/// getter takes it by value. Rebuilding from the same fields duplicates -/// only the handle value — the box stays alive as long as the original -/// handle lives, and the getters never release. -fn dup(h: &CHandle) -> CHandle { - CHandle { - ctx: h.ctx, - addref: h.addref, - release: h.release, - abi_version: h.abi_version, - } -} - -/// Assert two f64 values agree within `tol`. -fn assert_close(actual: f64, expected: f64, tol: f64) { - assert!( - (actual - expected).abs() <= tol, - "expected {expected} ± {tol}, got {actual}" - ); -} - -/// Drive a two-stage string getter against the standard `copy_string` -/// convention: a null-buffer size query, a short buffer that must stay -/// untouched (no truncation), an exact-fit copy with its NUL, and an -/// oversized copy with the tail untouched. -fn assert_two_stage_getter(getter: impl Fn(*mut c_char, i32) -> i32, expected: &str) { - let required = (expected.len() + 1) as i32; - - // Size query: a null buffer returns the required size, NUL included. - assert_eq!(getter(std::ptr::null_mut(), 0), required); - - // Short buffer: too small, so nothing is written to it. - let short_size = (required - 1).max(0); - let mut short = vec![0xABu8; short_size as usize]; - assert_eq!( - getter(short.as_mut_ptr() as *mut c_char, short_size), - required - ); - assert!( - short.iter().all(|&b| b == 0xAB), - "short buffer must stay untouched" - ); - - // Exact fit: payload followed by a NUL. - let mut exact = vec![0xCDu8; required as usize]; - assert_eq!( - getter(exact.as_mut_ptr() as *mut c_char, required), - required - ); - assert_eq!(&exact[..expected.len()], expected.as_bytes()); - assert_eq!(exact[expected.len()], 0); - - // Oversized: payload and NUL written, tail left as initialized. - let mut big = vec![0u8; (required + 8) as usize]; - assert_eq!( - getter(big.as_mut_ptr() as *mut c_char, required + 8), - required - ); - assert_eq!(&big[..expected.len()], expected.as_bytes()); - assert_eq!(big[expected.len()], 0); - assert!(big[(required + 1) as usize..].iter().all(|&b| b == 0)); -} - -// ---- debug ---- - -/// A null message is `E_INVALID`. -#[test] -fn debug_log_null_msg_is_invalid() { - assert_eq!( - oakcommon_debug_log(0, std::ptr::null()), - OAKCOMMON_E_INVALID - ); -} - -/// Any non-null message returns `OK`, including out-of-range level codes -/// (`log_raw` never fails on the level itself). -#[test] -fn debug_log_returns_ok_for_any_level() { - let msg = to_cstring("ffi_misc debug_log probe"); - assert_eq!(oakcommon_debug_log(0, msg.as_ptr()), OAKCOMMON_OK); - - let raw = to_cstring("out-of-range level"); - assert_eq!(oakcommon_debug_log(99, raw.as_ptr()), OAKCOMMON_OK); -} - -/// Level-name getter is a non-truncating two-stage getter; out-of-range -/// codes yield "UNKNOWN". -#[test] -fn debug_level_name_two_stage() { - assert_two_stage_getter( - |buf, size| oakcommon_debug_level_name(0, buf, size), - "DEBUG", - ); - assert_two_stage_getter( - |buf, size| oakcommon_debug_level_name(2, buf, size), - "WARNING", - ); - assert_two_stage_getter( - |buf, size| oakcommon_debug_level_name(4, buf, size), - "FATAL", - ); - assert_two_stage_getter( - |buf, size| oakcommon_debug_level_name(5, buf, size), - "UNKNOWN", - ); - assert_two_stage_getter( - |buf, size| oakcommon_debug_level_name(-1, buf, size), - "UNKNOWN", - ); -} - -/// `log_set_level` accepts 0..=4, rejects everything else with `E_INVALID` -/// without changing the stored level, and `log_get_level` reads it back. -#[test] -fn log_set_get_level_roundtrip_and_invalid() { - let _guard = DEBUG_LOCK.lock().unwrap_or_else(|e| e.into_inner()); - - assert_eq!(oakcommon_log_set_level(0), OAKCOMMON_OK); - let mut level = -1; - assert_eq!(oakcommon_log_get_level(&mut level), OAKCOMMON_OK); - assert_eq!(level, 0); - - assert_eq!(oakcommon_log_set_level(4), OAKCOMMON_OK); - assert_eq!(oakcommon_log_get_level(&mut level), OAKCOMMON_OK); - assert_eq!(level, 4); - - assert_eq!(oakcommon_log_set_level(5), OAKCOMMON_E_INVALID); - assert_eq!(oakcommon_log_set_level(-1), OAKCOMMON_E_INVALID); - // The rejected set leaves the stored level untouched. - assert_eq!(oakcommon_log_get_level(&mut level), OAKCOMMON_OK); - assert_eq!(level, 4); - - // Restore the default so other tests see a clean level. - assert_eq!(oakcommon_log_set_level(1), OAKCOMMON_OK); - assert_eq!(oakcommon_log_get_level(&mut level), OAKCOMMON_OK); - assert_eq!(level, 1); -} - -/// The get-level export rejects a null out-param. -#[test] -fn log_get_level_null_out_is_invalid() { - assert_eq!( - oakcommon_log_get_level(std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); -} - -// ---- misc: decibel / lerp ---- - -/// Linear -> decibels for exact and mid-range inputs; zero (log10 = -inf) -/// collapses to the decibel minimum, negative inputs yield NaN. -#[test] -fn decibel_from_linear_known_values() { - let mut out = -1.0; - - assert_eq!(oakcommon_decibel_from_linear(1.0, &mut out), OAKCOMMON_OK); - assert_close(out, 0.0, 1e-9); - assert_eq!(oakcommon_decibel_from_linear(10.0, &mut out), OAKCOMMON_OK); - assert_close(out, 20.0, 1e-9); - assert_eq!(oakcommon_decibel_from_linear(0.5, &mut out), OAKCOMMON_OK); - assert_close(out, -6.020599913, 1e-6); - assert_eq!(oakcommon_decibel_from_linear(0.0, &mut out), OAKCOMMON_OK); - assert_eq!(out, -200.0); - assert_eq!(oakcommon_decibel_from_linear(-1.0, &mut out), OAKCOMMON_OK); - assert!( - out.is_nan(), - "negative linear input must produce NaN, got {out}" - ); -} - -/// Decibels -> linear for exact inputs; results below 1e-6 clamp to 0.0. -#[test] -fn decibel_to_linear_known_values() { - let mut out = -1.0; - - assert_eq!(oakcommon_decibel_to_linear(0.0, &mut out), OAKCOMMON_OK); - assert_close(out, 1.0, 1e-9); - assert_eq!(oakcommon_decibel_to_linear(20.0, &mut out), OAKCOMMON_OK); - assert_close(out, 10.0, 1e-9); - assert_eq!(oakcommon_decibel_to_linear(-100.0, &mut out), OAKCOMMON_OK); - assert_close(out, 1e-5, 1e-12); - assert_eq!(oakcommon_decibel_to_linear(-200.0, &mut out), OAKCOMMON_OK); - assert_eq!(out, 0.0); -} - -/// Logarithmic position -> decibels: saturates to the minimum below 0.001, -/// to 0 dB above 0.99, and follows the linearization formula in between. -#[test] -fn decibel_from_logarithmic_known_values() { - let mut out = -1.0; - - assert_eq!( - oakcommon_decibel_from_logarithmic(0.0, &mut out), - OAKCOMMON_OK - ); - assert_eq!(out, -200.0); - assert_eq!( - oakcommon_decibel_from_logarithmic(1.0, &mut out), - OAKCOMMON_OK - ); - assert_eq!(out, 0.0); - assert_eq!( - oakcommon_decibel_from_logarithmic(0.99, &mut out), - OAKCOMMON_OK - ); - assert_close(out, 0.0, 1e-6); - assert_eq!( - oakcommon_decibel_from_logarithmic(0.5, &mut out), - OAKCOMMON_OK - ); - assert_close(out, -16.45, 0.05); -} - -/// Decibels -> logarithmic position; `|db| <= 1e-12` snaps to 1.0. -#[test] -fn decibel_to_logarithmic_known_values() { - let mut out = -1.0; - - assert_eq!( - oakcommon_decibel_to_logarithmic(0.0, &mut out), - OAKCOMMON_OK - ); - assert_eq!(out, 1.0); - assert_eq!( - oakcommon_decibel_to_logarithmic(20.0, &mut out), - OAKCOMMON_OK - ); - assert_close(out, 1.0, 1e-9); - assert_eq!( - oakcommon_decibel_to_logarithmic(-120.0, &mut out), - OAKCOMMON_OK - ); - assert_close(out, 4.605e-6, 1e-9); -} - -/// Linear amplitude -> logarithmic position (`1 - exp(-linear * lo_g100)`). -#[test] -fn decibel_linear_to_logarithmic_known_values() { - let mut out = -1.0; - - assert_eq!( - oakcommon_decibel_linear_to_logarithmic(0.0, &mut out), - OAKCOMMON_OK - ); - assert_eq!(out, 0.0); - assert_eq!( - oakcommon_decibel_linear_to_logarithmic(1.0, &mut out), - OAKCOMMON_OK - ); - assert_close(out, 0.99, 1e-9); -} - -/// Logarithmic position -> linear amplitude; values above 0.99 snap to 1.0. -#[test] -fn decibel_logarithmic_to_linear_known_values() { - let mut out = -1.0; - - assert_eq!( - oakcommon_decibel_logarithmic_to_linear(0.0, &mut out), - OAKCOMMON_OK - ); - assert_eq!(out, 0.0); - assert_eq!( - oakcommon_decibel_logarithmic_to_linear(1.0, &mut out), - OAKCOMMON_OK - ); - assert_eq!(out, 1.0); - assert_eq!( - oakcommon_decibel_logarithmic_to_linear(0.99, &mut out), - OAKCOMMON_OK - ); - assert_close(out, 1.0, 1e-6); -} - -/// `lerp(a, b, t) = a*(1-t) + b*t` at the endpoints and midpoints. -#[test] -fn lerp_known_values() { - let mut out = -1.0; - - assert_eq!(oakcommon_lerp(0.0, 10.0, 0.5, &mut out), OAKCOMMON_OK); - assert_close(out, 5.0, 1e-9); - assert_eq!(oakcommon_lerp(0.0, 10.0, 0.0, &mut out), OAKCOMMON_OK); - assert_close(out, 0.0, 1e-9); - assert_eq!(oakcommon_lerp(0.0, 10.0, 1.0, &mut out), OAKCOMMON_OK); - assert_close(out, 10.0, 1e-9); - assert_eq!(oakcommon_lerp(2.0, 4.0, 0.25, &mut out), OAKCOMMON_OK); - assert_close(out, 2.5, 1e-9); -} - -/// Every decibel/lerp export rejects a null out-param with `E_INVALID`. -#[test] -fn decibel_lerp_reject_null_out() { - assert_eq!( - oakcommon_decibel_from_linear(1.0, std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_decibel_to_linear(0.0, std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_decibel_from_logarithmic(0.5, std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_decibel_to_logarithmic(0.0, std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_decibel_linear_to_logarithmic(0.5, std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_decibel_logarithmic_to_linear(0.5, std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_lerp(0.0, 1.0, 0.5, std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); -} - -// ---- misc: drop-workflow behavior / power ---- - -/// Codes 0..=3 are valid; everything else returns 0. -#[test] -fn drop_workflow_behavior_is_valid() { - for value in 0..=3 { - assert_eq!(oakcommon_drop_workflow_behavior_is_valid(value), 1); - } - assert_eq!(oakcommon_drop_workflow_behavior_is_valid(4), 0); - assert_eq!(oakcommon_drop_workflow_behavior_is_valid(-1), 0); - assert_eq!(oakcommon_drop_workflow_behavior_is_valid(i32::MAX), 0); -} - -/// Behavior-name getter is a non-truncating two-stage getter; out-of-range -/// codes yield "UNKNOWN". -#[test] -fn drop_workflow_behavior_name_two_stage() { - assert_two_stage_getter( - |buf, size| oakcommon_drop_workflow_behavior_name(0, buf, size), - "ASK", - ); - assert_two_stage_getter( - |buf, size| oakcommon_drop_workflow_behavior_name(1, buf, size), - "AUTO", - ); - assert_two_stage_getter( - |buf, size| oakcommon_drop_workflow_behavior_name(2, buf, size), - "MANUAL", - ); - assert_two_stage_getter( - |buf, size| oakcommon_drop_workflow_behavior_name(3, buf, size), - "DISABLE", - ); - assert_two_stage_getter( - |buf, size| oakcommon_drop_workflow_behavior_name(4, buf, size), - "UNKNOWN", - ); - assert_two_stage_getter( - |buf, size| oakcommon_drop_workflow_behavior_name(-1, buf, size), - "UNKNOWN", - ); -} - -/// Round `value` up to a power of two (wrapping overflow -> 0); a null -/// out-param is `E_INVALID`. -#[test] -fn power_ceil_to_power_of_2() { - let mut out = 0u32; - for (input, expected) in [ - (0u32, 0u32), - (1, 1), - (2, 2), - (3, 4), - (5, 8), - (9, 16), - (0x8000_0001, 0), - ] { - assert_eq!( - oakcommon_power_ceil_to_power_of_2(input, &mut out), - OAKCOMMON_OK - ); - assert_eq!(out, expected, "ceil({input})"); - } - assert_eq!( - oakcommon_power_ceil_to_power_of_2(7, std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); -} - -/// Round `value` down to a power of two; a null out-param is `E_INVALID`. -#[test] -fn power_floor_to_power_of_2() { - let mut out = 0u32; - for (input, expected) in [ - (0u32, 0u32), - (1, 1), - (4, 4), - (5, 4), - (9, 8), - (0x8000_0000, 0x8000_0000), - ] { - assert_eq!( - oakcommon_power_floor_to_power_of_2(input, &mut out), - OAKCOMMON_OK - ); - assert_eq!(out, expected, "floor({input})"); - } - assert_eq!( - oakcommon_power_floor_to_power_of_2(7, std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); -} - -// ---- current ---- - -/// The singleton handle is stamped; `free` nullifies it, is idempotent, and -/// tolerates a null pointer or an explicit null handle. -#[test] -fn current_instance_and_free_lifecycle() { - let mut h = oakcommon_current_instance(); - assert!(!h.is_null()); - assert_eq!(h.abi_version, OAKCOMMON_ABI_VERSION); - assert!(h.addref.is_some()); - assert!(h.release.is_some()); - - oakcommon_current_free(&mut h); - assert!(h.is_null()); - // A second free of the now-empty handle is safe. - oakcommon_current_free(&mut h); - assert!(h.is_null()); - // Freeing a null pointer is safe. - oakcommon_current_free(std::ptr::null_mut()); - // Freeing an explicit null handle is safe. - let mut null_h = CHandle::null(); - oakcommon_current_free(&mut null_h); - assert!(null_h.is_null()); -} - -/// All four slots round-trip set -> get; null handles and null out-params -/// are `E_INVALID`, and clearing a slot makes it read back as null. -#[test] -fn current_set_get_all_slots_roundtrip() { - let _guard = CURRENT_LOCK.lock().unwrap_or_else(|e| e.into_inner()); - let h = oakcommon_current_instance(); - - let video = 0x1000usize as *mut c_void; - let audio = 0x2000usize as *mut c_void; - let host = 0x3000usize as *mut c_void; - let cache = 0x4000usize as *mut c_void; - - // Empty slots read back as null before anything is stored. - let mut got: *mut c_void = std::ptr::null_mut(); - assert_eq!( - oakcommon_current_get_video_params(dup(&h), &mut got), - OAKCOMMON_OK - ); - assert!(got.is_null()); - - assert_eq!( - oakcommon_current_set_video_params(dup(&h), video, None), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_current_set_audio_params(dup(&h), audio, None), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_current_set_plugin_host(dup(&h), host, None), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_current_set_plugin_cache(dup(&h), cache, None), - OAKCOMMON_OK - ); - - assert_eq!( - oakcommon_current_get_video_params(dup(&h), &mut got), - OAKCOMMON_OK - ); - assert_eq!(got, video); - assert_eq!( - oakcommon_current_get_audio_params(dup(&h), &mut got), - OAKCOMMON_OK - ); - assert_eq!(got, audio); - assert_eq!( - oakcommon_current_get_plugin_host(dup(&h), &mut got), - OAKCOMMON_OK - ); - assert_eq!(got, host); - assert_eq!( - oakcommon_current_get_plugin_cache(dup(&h), &mut got), - OAKCOMMON_OK - ); - assert_eq!(got, cache); - - // Getters reject a null out-param and a null handle. - assert_eq!( - oakcommon_current_get_video_params(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_current_get_video_params(CHandle::null(), &mut got), - OAKCOMMON_E_INVALID - ); - // Setters reject a null handle. - assert_eq!( - oakcommon_current_set_video_params(CHandle::null(), video, None), - OAKCOMMON_E_INVALID - ); - - // Clear every slot so other tests see a clean singleton. - assert_eq!( - oakcommon_current_set_video_params(dup(&h), std::ptr::null_mut(), None), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_current_set_audio_params(dup(&h), std::ptr::null_mut(), None), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_current_set_plugin_host(dup(&h), std::ptr::null_mut(), None), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_current_set_plugin_cache(dup(&h), std::ptr::null_mut(), None), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_current_get_video_params(dup(&h), &mut got), - OAKCOMMON_OK - ); - assert!(got.is_null()); -} - -/// Replacing an occupied slot runs the previous owner's destructor exactly -/// once; clearing a slot whose occupant has no destructor runs nothing. -#[test] -fn current_set_destroys_replaced_pointer() { - let _guard = CURRENT_LOCK.lock().unwrap_or_else(|e| e.into_inner()); - let h = oakcommon_current_instance(); - let base = DESTROY_COUNT.load(Ordering::SeqCst); - - assert_eq!( - oakcommon_current_set_video_params( - dup(&h), - 0xAAAAusize as *mut c_void, - Some(count_destroy) - ), - OAKCOMMON_OK - ); - assert_eq!(DESTROY_COUNT.load(Ordering::SeqCst), base); - - // Replacing the slot invokes the stored destructor exactly once. - assert_eq!( - oakcommon_current_set_video_params(dup(&h), 0xBBBBusize as *mut c_void, None), - OAKCOMMON_OK - ); - assert_eq!(DESTROY_COUNT.load(Ordering::SeqCst), base + 1); - - let mut got: *mut c_void = std::ptr::null_mut(); - assert_eq!( - oakcommon_current_get_video_params(dup(&h), &mut got), - OAKCOMMON_OK - ); - assert_eq!(got, 0xBBBBusize as *mut c_void); - - // Clearing a slot with no destructor invokes nothing. - assert_eq!( - oakcommon_current_set_video_params(dup(&h), std::ptr::null_mut(), None), - OAKCOMMON_OK - ); - assert_eq!(DESTROY_COUNT.load(Ordering::SeqCst), base + 1); - assert_eq!( - oakcommon_current_get_video_params(dup(&h), &mut got), - OAKCOMMON_OK - ); - assert!(got.is_null()); -} - -/// `is_interactive` writes 1; null handle/out-param are `E_INVALID`. -#[test] -fn current_is_interactive_writes_one() { - let _guard = CURRENT_LOCK.lock().unwrap_or_else(|e| e.into_inner()); - let h = oakcommon_current_instance(); - - let mut out = 0i32; - assert_eq!( - oakcommon_current_is_interactive(dup(&h), &mut out), - OAKCOMMON_OK - ); - assert_eq!(out, 1); - assert_eq!( - oakcommon_current_is_interactive(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_current_is_interactive(CHandle::null(), &mut out), - OAKCOMMON_E_INVALID - ); -} diff --git a/crates/oakcommon/tests/ffi_subtitleparams.rs b/crates/oakcommon/tests/ffi_subtitleparams.rs deleted file mode 100644 index 13dd4c145..000000000 --- a/crates/oakcommon/tests/ffi_subtitleparams.rs +++ /dev/null @@ -1,721 +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 . - -//! FFI-level integration tests for the C-ABI `subtitleparams` exports in -//! `oakcommon::ffi::subtitleparams`, asserted against the C++ oracle -//! `src/common/c_api/subtitleparams.cpp` and the Rust domain -//! `src/common/rust/src/subtitleparams.rs`. -//! -//! The contract under test (each point matches the C++ oracle): -//! - exports take a `CHandle` by value and never release it, so callers -//! keep the handle alive and `free` it exactly once; -//! - `free` nullifies the handle, is idempotent, and tolerates a null -//! pointer; -//! - two-stage string getters return the required size (NUL included) and -//! only copy when the buffer is large enough — they never truncate; -//! - getters reject a null handle or null out-param with `E_INVALID`, -//! setters reject a null handle, and string inputs reject null pointers; -//! - an out-of-range subtitle index yields `E_NOT_FOUND`; malformed XML -//! yields `E_FAILED`; the empty set is invalid with `count` 0 and -//! `duration` 0/1. - -use std::ffi::{c_char, CString}; - -use oakcommon::error::{ - OAKCOMMON_E_FAILED, OAKCOMMON_E_INVALID, OAKCOMMON_E_NOT_FOUND, OAKCOMMON_OK, -}; -use oakcommon::ffi::subtitleparams::*; -use oakcommon::handle::{CHandle, OAKCOMMON_ABI_VERSION}; - -/// Create an empty subtitle parameter set. -fn make() -> CHandle { - oakcommon_subtitleparams_init() -} - -/// A populated set: stream index 2, disabled, two subtitles. -fn make_populated() -> CHandle { - let h = make(); - assert_eq!( - oakcommon_subtitleparams_add_subtitle(dup(&h), 0, 1, 25, 1, to_cstring("hello").as_ptr()), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_subtitleparams_add_subtitle(dup(&h), 25, 1, 50, 1, to_cstring("world").as_ptr()), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_subtitleparams_set_stream_index(dup(&h), 2), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_subtitleparams_set_enabled(dup(&h), 0), - OAKCOMMON_OK - ); - h -} - -/// Convert a string slice to a NUL-terminated C string for FFI inputs. -fn to_cstring(s: &str) -> CString { - CString::new(s).expect("test string must not contain NUL") -} - -/// Cheap struct copy: `CHandle` is neither `Clone` nor `Copy`, but every -/// getter takes it by value. Rebuilding from the same fields duplicates -/// only the handle value — the box stays alive as long as the original -/// handle lives, and the getters never release. -fn dup(h: &CHandle) -> CHandle { - CHandle { - ctx: h.ctx, - addref: h.addref, - release: h.release, - abi_version: h.abi_version, - } -} - -/// Drive a two-stage string getter against the C++ `copy_string` -/// convention: a null-buffer size query, a short buffer that must stay -/// untouched (no truncation), an exact-fit copy with its NUL, and an -/// oversized copy with the tail untouched. -fn assert_two_stage_getter(getter: impl Fn(*mut c_char, i32) -> i32, expected: &str) { - let required = (expected.len() + 1) as i32; - - // Size query: a null buffer returns the required size, NUL included. - assert_eq!(getter(std::ptr::null_mut(), 0), required); - - // Short buffer: too small, so nothing is written to it. - let short_size = (required - 1).max(0); - let mut short = vec![0xABu8; short_size as usize]; - assert_eq!( - getter(short.as_mut_ptr() as *mut c_char, short_size), - required - ); - assert!( - short.iter().all(|&b| b == 0xAB), - "short buffer must stay untouched" - ); - - // Exact fit: payload followed by a NUL. - let mut exact = vec![0xCDu8; required as usize]; - assert_eq!( - getter(exact.as_mut_ptr() as *mut c_char, required), - required - ); - assert_eq!(&exact[..expected.len()], expected.as_bytes()); - assert_eq!(exact[expected.len()], 0); - - // Oversized: payload and NUL written, tail left as initialized. - let mut big = vec![0u8; (required + 8) as usize]; - assert_eq!( - getter(big.as_mut_ptr() as *mut c_char, required + 8), - required - ); - assert_eq!(&big[..expected.len()], expected.as_bytes()); - assert_eq!(big[expected.len()], 0); - assert!(big[(required + 1) as usize..].iter().all(|&b| b == 0)); -} - -// ---- Handle lifecycle ---- - -/// `init` yields a stamped, non-empty handle; `free` nullifies it, is -/// idempotent, and tolerates a null pointer. -#[test] -fn init_free_lifecycle() { - let h = oakcommon_subtitleparams_init(); - assert!(!h.is_null()); - assert_eq!(h.abi_version, OAKCOMMON_ABI_VERSION); - assert!(h.addref.is_some()); - assert!(h.release.is_some()); - - let mut hf = make(); - assert!(!hf.is_null()); - oakcommon_subtitleparams_free(&mut hf); - assert!(hf.is_null()); - // A second free of the now-empty handle is safe. - oakcommon_subtitleparams_free(&mut hf); - assert!(hf.is_null()); - // Freeing a null pointer is safe. - oakcommon_subtitleparams_free(std::ptr::null_mut()); - // Freeing a handle struct that is already null (by value) is safe too. - let mut empty = CHandle::null(); - oakcommon_subtitleparams_free(&mut empty); - assert!(empty.is_null()); -} - -// ---- Core field round-trips ---- - -/// Stream index defaults to 0 and round-trips; a null handle or null -/// out-param is `E_INVALID`. -#[test] -fn stream_index_roundtrip() { - let h = make(); - let mut si = -1i32; - - assert_eq!( - oakcommon_subtitleparams_get_stream_index(dup(&h), &mut si), - OAKCOMMON_OK - ); - assert_eq!(si, 0); - - assert_eq!( - oakcommon_subtitleparams_set_stream_index(dup(&h), 3), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_subtitleparams_get_stream_index(dup(&h), &mut si), - OAKCOMMON_OK - ); - assert_eq!(si, 3); - - assert_eq!( - oakcommon_subtitleparams_set_stream_index(CHandle::null(), 1), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_subtitleparams_get_stream_index(CHandle::null(), &mut si), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_subtitleparams_get_stream_index(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); -} - -/// `enabled` defaults to 1 (CPP-PARITY: the C++ constructor sets -/// `enabled_ = true`) and round-trips; any non-zero setter value enables. -#[test] -fn enabled_roundtrip() { - let h = make(); - let mut en = -1i32; - - assert_eq!( - oakcommon_subtitleparams_get_enabled(dup(&h), &mut en), - OAKCOMMON_OK - ); - assert_eq!(en, 1); - - assert_eq!( - oakcommon_subtitleparams_set_enabled(dup(&h), 0), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_subtitleparams_get_enabled(dup(&h), &mut en), - OAKCOMMON_OK - ); - assert_eq!(en, 0); - - // A non-zero code (even 5) enables the stream. - assert_eq!( - oakcommon_subtitleparams_set_enabled(dup(&h), 5), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_subtitleparams_get_enabled(dup(&h), &mut en), - OAKCOMMON_OK - ); - assert_eq!(en, 1); - - assert_eq!( - oakcommon_subtitleparams_set_enabled(CHandle::null(), 1), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_subtitleparams_get_enabled(CHandle::null(), &mut en), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_subtitleparams_get_enabled(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); -} - -// ---- Empty-set behavior ---- - -/// An empty set is invalid, has count 0, and duration 0/1; null handle or -/// null out-param is `E_INVALID`. -#[test] -fn empty_set_defaults() { - let h = make(); - let mut v = -1i32; - let mut c = -1i32; - let mut n = -1i32; - let mut d = -1i32; - - assert_eq!( - oakcommon_subtitleparams_is_valid(dup(&h), &mut v), - OAKCOMMON_OK - ); - assert_eq!(v, 0); - assert_eq!( - oakcommon_subtitleparams_count(dup(&h), &mut c), - OAKCOMMON_OK - ); - assert_eq!(c, 0); - assert_eq!( - oakcommon_subtitleparams_duration(dup(&h), &mut n, &mut d), - OAKCOMMON_OK - ); - assert_eq!((n, d), (0, 1)); - - assert_eq!( - oakcommon_subtitleparams_is_valid(CHandle::null(), &mut v), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_subtitleparams_count(CHandle::null(), &mut c), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_subtitleparams_duration(CHandle::null(), &mut n, &mut d), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_subtitleparams_is_valid(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_subtitleparams_count(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_subtitleparams_duration(dup(&h), std::ptr::null_mut(), &mut d), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_subtitleparams_duration(dup(&h), &mut n, std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); -} - -// ---- Subtitles ---- - -/// `add_subtitle` appends entries; `is_valid`/`count`/`duration`/ -/// `get_subtitle` reflect the populated set. Rationals are reduced -/// (CPP-PARITY with the C++ `Rational` constructor). -#[test] -fn add_subtitle_and_query() { - let h = make_populated(); - let mut v = -1i32; - let mut c = -1i32; - let mut n = -1i32; - let mut d = -1i32; - - assert_eq!( - oakcommon_subtitleparams_is_valid(dup(&h), &mut v), - OAKCOMMON_OK - ); - assert_eq!(v, 1); - assert_eq!( - oakcommon_subtitleparams_count(dup(&h), &mut c), - OAKCOMMON_OK - ); - assert_eq!(c, 2); - assert_eq!( - oakcommon_subtitleparams_duration(dup(&h), &mut n, &mut d), - OAKCOMMON_OK - ); - assert_eq!((n, d), (50, 1)); - - assert_eq!( - oakcommon_subtitleparams_get_subtitle(dup(&h), 0, &mut n, &mut d, &mut v, &mut c), - OAKCOMMON_OK - ); - assert_eq!((n, d), (0, 1)); - assert_eq!((v, c), (25, 1)); - assert_eq!( - oakcommon_subtitleparams_get_subtitle(dup(&h), 1, &mut n, &mut d, &mut v, &mut c), - OAKCOMMON_OK - ); - assert_eq!((n, d), (25, 1)); - assert_eq!((v, c), (50, 1)); - - // Reduction: Rational(2,4) -> 1/2, Rational(9,3) -> 3/1. - assert_eq!( - oakcommon_subtitleparams_add_subtitle(dup(&h), 2, 4, 9, 3, to_cstring("t").as_ptr()), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_subtitleparams_get_subtitle(dup(&h), 2, &mut n, &mut d, &mut v, &mut c), - OAKCOMMON_OK - ); - assert_eq!((n, d), (1, 2)); - assert_eq!((v, c), (3, 1)); -} - -/// `add_subtitle` rejects a null handle or null text; an empty text string -/// is a valid subtitle. -#[test] -fn add_subtitle_failures() { - let h = make(); - assert_eq!( - oakcommon_subtitleparams_add_subtitle( - CHandle::null(), - 0, - 1, - 1, - 1, - to_cstring("x").as_ptr() - ), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_subtitleparams_add_subtitle(dup(&h), 0, 1, 1, 1, std::ptr::null()), - OAKCOMMON_E_INVALID - ); - - // Empty text is fine and counts as a subtitle. - assert_eq!( - oakcommon_subtitleparams_add_subtitle(dup(&h), 0, 1, 1, 1, to_cstring("").as_ptr()), - OAKCOMMON_OK - ); - let mut c = -1i32; - assert_eq!( - oakcommon_subtitleparams_count(dup(&h), &mut c), - OAKCOMMON_OK - ); - assert_eq!(c, 1); -} - -/// `get_subtitle` on an out-of-range index is `E_NOT_FOUND`; a null handle -/// or any null out-param is `E_INVALID`. -#[test] -fn get_subtitle_out_of_range() { - let h = make_populated(); - let mut n = -1i32; - let mut d = -1i32; - let mut v = -1i32; - let mut c = -1i32; - - assert_eq!( - oakcommon_subtitleparams_get_subtitle(dup(&h), 2, &mut n, &mut d, &mut v, &mut c), - OAKCOMMON_E_NOT_FOUND - ); - assert_eq!( - oakcommon_subtitleparams_get_subtitle(dup(&h), -1, &mut n, &mut d, &mut v, &mut c), - OAKCOMMON_E_NOT_FOUND - ); - - // On an empty set even index 0 is out of range. - let e = make(); - assert_eq!( - oakcommon_subtitleparams_get_subtitle(dup(&e), 0, &mut n, &mut d, &mut v, &mut c), - OAKCOMMON_E_NOT_FOUND - ); - - assert_eq!( - oakcommon_subtitleparams_get_subtitle(CHandle::null(), 0, &mut n, &mut d, &mut v, &mut c), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_subtitleparams_get_subtitle( - dup(&h), - 0, - std::ptr::null_mut(), - &mut d, - &mut v, - &mut c - ), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_subtitleparams_get_subtitle( - dup(&h), - 0, - &mut n, - std::ptr::null_mut(), - &mut v, - &mut c - ), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_subtitleparams_get_subtitle( - dup(&h), - 0, - &mut n, - &mut d, - std::ptr::null_mut(), - &mut c - ), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_subtitleparams_get_subtitle( - dup(&h), - 0, - &mut n, - &mut d, - &mut v, - std::ptr::null_mut() - ), - OAKCOMMON_E_INVALID - ); -} - -/// `clear` removes every subtitle (count 0, invalid, duration 0/1) and -/// rejects a null handle. -#[test] -fn clear() { - let h = make_populated(); - assert_eq!(oakcommon_subtitleparams_clear(dup(&h)), OAKCOMMON_OK); - - let mut v = -1i32; - let mut c = -1i32; - let mut n = -1i32; - let mut d = -1i32; - assert_eq!( - oakcommon_subtitleparams_count(dup(&h), &mut c), - OAKCOMMON_OK - ); - assert_eq!(c, 0); - assert_eq!( - oakcommon_subtitleparams_is_valid(dup(&h), &mut v), - OAKCOMMON_OK - ); - assert_eq!(v, 0); - assert_eq!( - oakcommon_subtitleparams_duration(dup(&h), &mut n, &mut d), - OAKCOMMON_OK - ); - assert_eq!((n, d), (0, 1)); - - // Clearing again is a no-op success. - assert_eq!(oakcommon_subtitleparams_clear(dup(&h)), OAKCOMMON_OK); - - assert_eq!( - oakcommon_subtitleparams_clear(CHandle::null()), - OAKCOMMON_E_INVALID - ); -} - -// ---- String getters ---- - -/// `get_subtitle_text` is a two-stage string getter; an out-of-range index -/// is `E_NOT_FOUND`, and a null handle or invalid out-buffer is -/// `E_INVALID`. -#[test] -fn get_subtitle_text_two_stage() { - let h = make_populated(); - assert_two_stage_getter( - |buf, size| oakcommon_subtitleparams_get_subtitle_text(dup(&h), 0, buf, size), - "hello", - ); - assert_two_stage_getter( - |buf, size| oakcommon_subtitleparams_get_subtitle_text(dup(&h), 1, buf, size), - "world", - ); - - // Out-of-range index is E_NOT_FOUND (even on a size query). - assert_eq!( - oakcommon_subtitleparams_get_subtitle_text(dup(&h), 2, std::ptr::null_mut(), 0), - OAKCOMMON_E_NOT_FOUND - ); - assert_eq!( - oakcommon_subtitleparams_get_subtitle_text(dup(&h), -1, std::ptr::null_mut(), 0), - OAKCOMMON_E_NOT_FOUND - ); - - assert_eq!( - oakcommon_subtitleparams_get_subtitle_text(CHandle::null(), 0, std::ptr::null_mut(), 0), - OAKCOMMON_E_INVALID - ); - // A null buffer with a positive size is an invalid string out-param. - assert_eq!( - oakcommon_subtitleparams_get_subtitle_text(dup(&h), 0, std::ptr::null_mut(), 5), - OAKCOMMON_E_INVALID - ); - // A negative size is invalid too. - assert_eq!( - oakcommon_subtitleparams_get_subtitle_text(dup(&h), 0, std::ptr::null_mut(), -1), - OAKCOMMON_E_INVALID - ); -} - -/// `generate_ass_header` is a static two-stage string getter (no handle). -/// The payload matches the C++ header verbatim (CRLF line endings). -#[test] -fn generate_ass_header_two_stage() { - let expected = "[Script Info]\r\n\ - ; Script generated by Oak\r\n\ - ScriptType: v4.00+\r\n\ - PlayResX: 384\r\n\ - PlayResY: 288\r\n\ - ScaledBorderAndShadow: yes\r\n\ - \r\n\ - [V4+ Styles]\r\n\ - Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, \ - BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, \ - BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\r\n\ - Style: Default,Arial,16,&HFFFFFF,&HFFFFFF,&H000000,&H000000,0,0,0,0,100,100,0,0,1,1,0,2,10,10,10,0\r\n\ - \r\n\ - [Events]\r\n\ - Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\r\n"; - assert_two_stage_getter( - |buf, size| oakcommon_subtitleparams_generate_ass_header(buf, size), - expected, - ); - - // An invalid out-buffer (null with a positive size) is E_INVALID. - assert_eq!( - oakcommon_subtitleparams_generate_ass_header(std::ptr::null_mut(), 5), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_subtitleparams_generate_ass_header(std::ptr::null_mut(), -1), - OAKCOMMON_E_INVALID - ); -} - -// ---- XML ---- - -/// `load_xml` parses a fragment and applies stream index, enabled, and -/// subtitles; malformed input or a missing root is `E_FAILED`; a null -/// handle or null xml is `E_INVALID`. -#[test] -fn load_xml() { - let h = make(); - - let xml = "70\ - hello\ - world"; - assert_eq!( - oakcommon_subtitleparams_load_xml(dup(&h), to_cstring(xml).as_ptr()), - OAKCOMMON_OK - ); - - let mut si = -1i32; - let mut en = -1i32; - let mut c = -1i32; - assert_eq!( - oakcommon_subtitleparams_get_stream_index(dup(&h), &mut si), - OAKCOMMON_OK - ); - assert_eq!(si, 7); - assert_eq!( - oakcommon_subtitleparams_get_enabled(dup(&h), &mut en), - OAKCOMMON_OK - ); - assert_eq!(en, 0); - assert_eq!( - oakcommon_subtitleparams_count(dup(&h), &mut c), - OAKCOMMON_OK - ); - assert_eq!(c, 2); - assert_eq!( - oakcommon_subtitleparams_get_subtitle_text(dup(&h), 0, std::ptr::null_mut(), 0), - 6 - ); - assert_two_stage_getter( - |buf, size| oakcommon_subtitleparams_get_subtitle_text(dup(&h), 1, buf, size), - "world", - ); - - // Malformed / missing-root fragments fail with E_FAILED. - assert_eq!( - oakcommon_subtitleparams_load_xml( - dup(&h), - to_cstring("").as_ptr() - ), - OAKCOMMON_E_FAILED - ); - assert_eq!( - oakcommon_subtitleparams_load_xml(dup(&h), to_cstring("not xml").as_ptr()), - OAKCOMMON_E_FAILED - ); - assert_eq!( - oakcommon_subtitleparams_load_xml(dup(&h), to_cstring("").as_ptr()), - OAKCOMMON_E_FAILED - ); - - assert_eq!( - oakcommon_subtitleparams_load_xml(CHandle::null(), to_cstring(xml).as_ptr()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_subtitleparams_load_xml(dup(&h), std::ptr::null()), - OAKCOMMON_E_INVALID - ); -} - -/// `save_xml` is a two-stage string getter; the output matches the C++ -/// `XmlStreamWriter` format exactly (no indentation, escaped text). -#[test] -fn save_xml_two_stage() { - // The empty set serializes to streamindex 0 / enabled 1 / no subtitles. - let e = make(); - assert_two_stage_getter( - |buf, size| oakcommon_subtitleparams_save_xml(dup(&e), buf, size), - "01", - ); - - // The populated set round-trips through load_xml. - let h = make_populated(); - let expected = "20\ - hello\ - world"; - assert_two_stage_getter( - |buf, size| oakcommon_subtitleparams_save_xml(dup(&h), buf, size), - expected, - ); - - // A loaded fragment round-trips byte-for-byte. - let xml = "91\ - hi"; - assert_eq!( - oakcommon_subtitleparams_load_xml(dup(&h), to_cstring(xml).as_ptr()), - OAKCOMMON_OK - ); - assert_two_stage_getter( - |buf, size| oakcommon_subtitleparams_save_xml(dup(&h), buf, size), - xml, - ); - - assert_eq!( - oakcommon_subtitleparams_save_xml(CHandle::null(), std::ptr::null_mut(), 0), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_subtitleparams_save_xml(dup(&h), std::ptr::null_mut(), 5), - OAKCOMMON_E_INVALID - ); - - // The saved XML parses back to the same data via the C API. - let mut si = -1i32; - let mut en = -1i32; - let mut c = -1i32; - let mut n = -1i32; - let mut d = -1i32; - assert_eq!( - oakcommon_subtitleparams_get_stream_index(dup(&h), &mut si), - OAKCOMMON_OK - ); - assert_eq!(si, 9); - assert_eq!( - oakcommon_subtitleparams_get_enabled(dup(&h), &mut en), - OAKCOMMON_OK - ); - assert_eq!(en, 1); - assert_eq!( - oakcommon_subtitleparams_count(dup(&h), &mut c), - OAKCOMMON_OK - ); - assert_eq!(c, 1); - assert_eq!( - oakcommon_subtitleparams_get_subtitle(dup(&h), 0, &mut n, &mut d, &mut si, &mut en), - OAKCOMMON_OK - ); - assert_eq!((n, d), (3, 2)); - assert_eq!((si, en), (5, 1)); -} diff --git a/crates/oakcommon/tests/ffi_videoparams.rs b/crates/oakcommon/tests/ffi_videoparams.rs deleted file mode 100644 index efe28462c..000000000 --- a/crates/oakcommon/tests/ffi_videoparams.rs +++ /dev/null @@ -1,1390 +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 . - -//! FFI-level integration tests for the C-ABI `videoparams` exports in -//! `oakcommon::ffi::videoparams`, asserted against the C++ oracle -//! `src/common/c_api/videoparams.cpp` and the Rust domain -//! `src/common/rust/src/videoparams.rs`. -//! -//! The contract under test (each point matches the C++ oracle): -//! - exports take a `CHandle` by value and never release it, so callers -//! keep the handle alive and `free` it exactly once; -//! - `free` nullifies the handle, is idempotent, and tolerates a null -//! pointer; -//! - two-stage string getters return the required size (NUL included) and -//! only copy when the buffer is large enough — they never truncate; -//! - getters reject a null handle or null out-param with `E_INVALID`, -//! setters reject a null handle, and string inputs reject null pointers; -//! - out-of-range enum-like setter codes clamp to a documented fallback -//! (CPP-PARITY with the C++ `static_cast` semantics). - -use std::ffi::{c_char, CStr, CString}; - -use oakcommon::error::{OAKCOMMON_E_FAILED, OAKCOMMON_E_INVALID, OAKCOMMON_OK}; -use oakcommon::ffi::videoparams::*; -use oakcommon::handle::{CHandle, OAKCOMMON_ABI_VERSION}; - -/// `init_basic` arguments: 1920x1080 U8 RGBA, square pixels, progressive, -/// divider 1. -fn make() -> CHandle { - oakcommon_videoparams_init_basic(1920, 1080, 0, 4, 1, 1, 0, 1) -} - -/// `init_with_time_base` arguments: 1920x1080 U8 RGBA, time base 1001/30000, -/// square pixels, progressive, divider 1. -fn make_tb() -> CHandle { - oakcommon_videoparams_init_with_time_base(1920, 1080, 1001, 30000, 0, 4, 1, 1, 0, 1) -} - -/// Convert a string slice to a NUL-terminated C string for FFI inputs. -fn to_cstring(s: &str) -> CString { - CString::new(s).expect("test string must not contain NUL") -} - -/// Cheap struct copy: `CHandle` is neither `Clone` nor `Copy`, but every -/// getter takes it by value. Rebuilding from the same fields duplicates -/// only the handle value — the box stays alive as long as the original -/// handle lives, and the getters never release. -fn dup(h: &CHandle) -> CHandle { - CHandle { - ctx: h.ctx, - addref: h.addref, - release: h.release, - abi_version: h.abi_version, - } -} - -/// Drive a two-stage string getter against the C++ `copy_string` -/// convention: a null-buffer size query, a short buffer that must stay -/// untouched (no truncation), an exact-fit copy with its NUL, and an -/// oversized copy with the tail untouched. -fn assert_two_stage_getter(getter: impl Fn(*mut c_char, i32) -> i32, expected: &str) { - let required = (expected.len() + 1) as i32; - - // Size query: a null buffer returns the required size, NUL included. - assert_eq!(getter(std::ptr::null_mut(), 0), required); - - // Short buffer: too small, so nothing is written to it. - let short_size = (required - 1).max(0); - let mut short = vec![0xABu8; short_size as usize]; - assert_eq!( - getter(short.as_mut_ptr() as *mut c_char, short_size), - required - ); - assert!( - short.iter().all(|&b| b == 0xAB), - "short buffer must stay untouched" - ); - - // Exact fit: payload followed by a NUL. - let mut exact = vec![0xCDu8; required as usize]; - assert_eq!( - getter(exact.as_mut_ptr() as *mut c_char, required), - required - ); - assert_eq!(&exact[..expected.len()], expected.as_bytes()); - assert_eq!(exact[expected.len()], 0); - - // Oversized: payload and NUL written, tail left as initialized. - let mut big = vec![0u8; (required + 8) as usize]; - assert_eq!( - getter(big.as_mut_ptr() as *mut c_char, required + 8), - required - ); - assert_eq!(&big[..expected.len()], expected.as_bytes()); - assert_eq!(big[expected.len()], 0); - assert!(big[(required + 1) as usize..].iter().all(|&b| b == 0)); -} - -// ---- Handle lifecycle ---- - -/// All three constructors yield a stamped, non-empty handle; `free` -/// nullifies it, is idempotent, and tolerates a null pointer. -#[test] -fn init_free_lifecycle() { - let h = oakcommon_videoparams_init(); - assert!(!h.is_null()); - assert_eq!(h.abi_version, OAKCOMMON_ABI_VERSION); - assert!(h.addref.is_some()); - assert!(h.release.is_some()); - - let hb = make(); - assert!(!hb.is_null()); - assert_eq!(hb.abi_version, OAKCOMMON_ABI_VERSION); - - let ht = make_tb(); - assert!(!ht.is_null()); - assert_eq!(ht.abi_version, OAKCOMMON_ABI_VERSION); - - let mut hf = oakcommon_videoparams_init_basic(1, 1, 4, 4, 1, 1, 0, 1); - assert!(!hf.is_null()); - oakcommon_videoparams_free(&mut hf); - assert!(hf.is_null()); - // A second free of the now-empty handle is safe. - oakcommon_videoparams_free(&mut hf); - assert!(hf.is_null()); - // Freeing a null pointer is safe. - oakcommon_videoparams_free(std::ptr::null_mut()); -} - -// ---- Core field round-trips ---- - -/// Width, height, and depth round-trip; a null handle or null out-param is -/// `E_INVALID`. -#[test] -fn width_height_depth_roundtrip() { - let h = make(); - let mut w = -1i32; - let mut ht = -1i32; - let mut d = -1i32; - - assert_eq!( - oakcommon_videoparams_get_width(dup(&h), &mut w), - OAKCOMMON_OK - ); - assert_eq!(w, 1920); - assert_eq!( - oakcommon_videoparams_get_height(dup(&h), &mut ht), - OAKCOMMON_OK - ); - assert_eq!(ht, 1080); - assert_eq!( - oakcommon_videoparams_get_depth(dup(&h), &mut d), - OAKCOMMON_OK - ); - assert_eq!(d, 1); - - assert_eq!(oakcommon_videoparams_set_width(dup(&h), 640), OAKCOMMON_OK); - assert_eq!(oakcommon_videoparams_set_height(dup(&h), 480), OAKCOMMON_OK); - assert_eq!(oakcommon_videoparams_set_depth(dup(&h), 2), OAKCOMMON_OK); - assert_eq!( - oakcommon_videoparams_get_width(dup(&h), &mut w), - OAKCOMMON_OK - ); - assert_eq!(w, 640); - assert_eq!( - oakcommon_videoparams_get_height(dup(&h), &mut ht), - OAKCOMMON_OK - ); - assert_eq!(ht, 480); - assert_eq!( - oakcommon_videoparams_get_depth(dup(&h), &mut d), - OAKCOMMON_OK - ); - assert_eq!(d, 2); - - assert_eq!( - oakcommon_videoparams_set_width(CHandle::null(), 1), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_width(CHandle::null(), &mut w), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_width(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_height(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_depth(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); -} - -/// `is_3d` is derived from depth (CPP-PARITY: C++ has no `set_is_3d`; -/// the getter reads `depth > 1`). -#[test] -fn is_3d_roundtrip() { - let h = make(); - let mut is3d = -1i32; - let mut d = -1i32; - assert_eq!( - oakcommon_videoparams_get_is_3d(dup(&h), &mut is3d), - OAKCOMMON_OK - ); - assert_eq!(is3d, 0); - - assert_eq!(oakcommon_videoparams_set_depth(dup(&h), 2), OAKCOMMON_OK); - assert_eq!( - oakcommon_videoparams_get_is_3d(dup(&h), &mut is3d), - OAKCOMMON_OK - ); - assert_eq!(is3d, 1); - assert_eq!( - oakcommon_videoparams_get_depth(dup(&h), &mut d), - OAKCOMMON_OK - ); - assert_eq!(d, 2); - - assert_eq!(oakcommon_videoparams_set_depth(dup(&h), 1), OAKCOMMON_OK); - assert_eq!( - oakcommon_videoparams_get_is_3d(dup(&h), &mut is3d), - OAKCOMMON_OK - ); - assert_eq!(is3d, 0); - assert_eq!( - oakcommon_videoparams_get_depth(dup(&h), &mut d), - OAKCOMMON_OK - ); - assert_eq!(d, 1); - - assert_eq!( - oakcommon_videoparams_set_depth(CHandle::null(), 1), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_is_3d(CHandle::null(), &mut is3d), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_is_3d(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); -} - -/// Time base and frame rate round-trip independently: `set_time_base` does -/// not touch the frame rate and `set_frame_rate` does not touch the time -/// base (CPP-PARITY with the C++ setters). The frame rate expressed as a -/// time base is always the flipped frame rate. -#[test] -fn time_base_frame_rate_roundtrip() { - let h = make_tb(); - let mut n = -1i32; - let mut d = -1i32; - - // Derived from the constructor: frame rate = flipped time base. - assert_eq!( - oakcommon_videoparams_get_time_base(dup(&h), &mut n, &mut d), - OAKCOMMON_OK - ); - assert_eq!((n, d), (1001, 30000)); - assert_eq!( - oakcommon_videoparams_get_frame_rate(dup(&h), &mut n, &mut d), - OAKCOMMON_OK - ); - assert_eq!((n, d), (30000, 1001)); - assert_eq!( - oakcommon_videoparams_frame_rate_as_time_base(dup(&h), &mut n, &mut d), - OAKCOMMON_OK - ); - assert_eq!((n, d), (1001, 30000)); - - assert_eq!( - oakcommon_videoparams_set_time_base(dup(&h), 1, 50), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_videoparams_get_time_base(dup(&h), &mut n, &mut d), - OAKCOMMON_OK - ); - assert_eq!((n, d), (1, 50)); - // The frame rate is untouched by `set_time_base`. - assert_eq!( - oakcommon_videoparams_get_frame_rate(dup(&h), &mut n, &mut d), - OAKCOMMON_OK - ); - assert_eq!((n, d), (30000, 1001)); - - assert_eq!( - oakcommon_videoparams_set_frame_rate(dup(&h), 60, 1), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_videoparams_get_frame_rate(dup(&h), &mut n, &mut d), - OAKCOMMON_OK - ); - assert_eq!((n, d), (60, 1)); - // The time base is untouched by `set_frame_rate`. - assert_eq!( - oakcommon_videoparams_get_time_base(dup(&h), &mut n, &mut d), - OAKCOMMON_OK - ); - assert_eq!((n, d), (1, 50)); - assert_eq!( - oakcommon_videoparams_frame_rate_as_time_base(dup(&h), &mut n, &mut d), - OAKCOMMON_OK - ); - assert_eq!((n, d), (1, 60)); - - assert_eq!( - oakcommon_videoparams_set_time_base(CHandle::null(), 1, 50), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_set_frame_rate(CHandle::null(), 60, 1), - OAKCOMMON_E_INVALID - ); - for getter in [ - |buf1, buf2| oakcommon_videoparams_get_time_base(CHandle::null(), buf1, buf2), - |buf1, buf2| oakcommon_videoparams_get_frame_rate(CHandle::null(), buf1, buf2), - |buf1, buf2| oakcommon_videoparams_frame_rate_as_time_base(CHandle::null(), buf1, buf2), - ] { - assert_eq!(getter(&mut n, &mut d), OAKCOMMON_E_INVALID); - } - // A null out-param is `E_INVALID` even with a valid handle. - assert_eq!( - oakcommon_videoparams_get_time_base(dup(&h), std::ptr::null_mut(), &mut d), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_frame_rate(dup(&h), &mut n, std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_frame_rate_as_time_base( - dup(&h), - std::ptr::null_mut(), - std::ptr::null_mut() - ), - OAKCOMMON_E_INVALID - ); -} - -/// Pixel aspect ratio, format, and channel count round-trip. The default -/// handle has no format (code -1) and zero channels; an out-of-range format -/// code maps back to `Invalid` (code -1, CPP-PARITY with the C++ enum -/// `static_cast`). -#[test] -fn par_format_channel_roundtrip() { - let h = oakcommon_videoparams_init(); - let mut n = -1i32; - let mut d = -1i32; - let mut f = -1i32; - let mut c = -1i32; - - assert_eq!( - oakcommon_videoparams_get_pixel_aspect_ratio(dup(&h), &mut n, &mut d), - OAKCOMMON_OK - ); - assert_eq!((n, d), (1, 1)); - assert_eq!( - oakcommon_videoparams_get_format(dup(&h), &mut f), - OAKCOMMON_OK - ); - assert_eq!(f, -1); - assert_eq!( - oakcommon_videoparams_get_channel_count(dup(&h), &mut c), - OAKCOMMON_OK - ); - assert_eq!(c, 0); - - assert_eq!( - oakcommon_videoparams_set_pixel_aspect_ratio(dup(&h), 16, 9), - OAKCOMMON_OK - ); - assert_eq!(oakcommon_videoparams_set_format(dup(&h), 0), OAKCOMMON_OK); - assert_eq!( - oakcommon_videoparams_set_channel_count(dup(&h), 2), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_videoparams_get_pixel_aspect_ratio(dup(&h), &mut n, &mut d), - OAKCOMMON_OK - ); - assert_eq!((n, d), (16, 9)); - assert_eq!( - oakcommon_videoparams_get_format(dup(&h), &mut f), - OAKCOMMON_OK - ); - assert_eq!(f, 0); - assert_eq!( - oakcommon_videoparams_get_channel_count(dup(&h), &mut c), - OAKCOMMON_OK - ); - assert_eq!(c, 2); - - // A null (0/5) pixel aspect ratio falls back to square pixels. - assert_eq!( - oakcommon_videoparams_set_pixel_aspect_ratio(dup(&h), 0, 5), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_videoparams_get_pixel_aspect_ratio(dup(&h), &mut n, &mut d), - OAKCOMMON_OK - ); - assert_eq!((n, d), (1, 1)); - - // Out-of-range format codes map to `Invalid`, read back as -1. - assert_eq!(oakcommon_videoparams_set_format(dup(&h), 999), OAKCOMMON_OK); - assert_eq!( - oakcommon_videoparams_get_format(dup(&h), &mut f), - OAKCOMMON_OK - ); - assert_eq!(f, -1); - - assert_eq!( - oakcommon_videoparams_set_pixel_aspect_ratio(CHandle::null(), 1, 1), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_set_format(CHandle::null(), 0), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_set_channel_count(CHandle::null(), 2), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_pixel_aspect_ratio(CHandle::null(), &mut n, &mut d), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_format(CHandle::null(), &mut f), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_channel_count(CHandle::null(), &mut c), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_pixel_aspect_ratio(dup(&h), std::ptr::null_mut(), &mut d), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_format(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_channel_count(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); -} - -/// Interlacing, divider, and enabled round-trip. Interlacing codes clamp: -/// 1 -> TopFirst, 2 -> BottomFirst, anything else -> None (0). -#[test] -fn interlacing_divider_enabled_roundtrip() { - let h = make(); - let mut il = -1i32; - let mut dv = -1i32; - let mut en = -1i32; - - assert_eq!( - oakcommon_videoparams_get_interlacing(dup(&h), &mut il), - OAKCOMMON_OK - ); - assert_eq!(il, 0); - assert_eq!( - oakcommon_videoparams_get_divider(dup(&h), &mut dv), - OAKCOMMON_OK - ); - assert_eq!(dv, 1); - assert_eq!( - oakcommon_videoparams_get_enabled(dup(&h), &mut en), - OAKCOMMON_OK - ); - assert_eq!(en, 1); - - assert_eq!( - oakcommon_videoparams_set_interlacing(dup(&h), 1), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_videoparams_get_interlacing(dup(&h), &mut il), - OAKCOMMON_OK - ); - assert_eq!(il, 1); - assert_eq!( - oakcommon_videoparams_set_interlacing(dup(&h), 2), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_videoparams_get_interlacing(dup(&h), &mut il), - OAKCOMMON_OK - ); - assert_eq!(il, 2); - assert_eq!( - oakcommon_videoparams_set_interlacing(dup(&h), 3), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_videoparams_get_interlacing(dup(&h), &mut il), - OAKCOMMON_OK - ); - assert_eq!(il, 0); - - assert_eq!(oakcommon_videoparams_set_divider(dup(&h), 4), OAKCOMMON_OK); - assert_eq!( - oakcommon_videoparams_get_divider(dup(&h), &mut dv), - OAKCOMMON_OK - ); - assert_eq!(dv, 4); - - assert_eq!(oakcommon_videoparams_set_enabled(dup(&h), 0), OAKCOMMON_OK); - assert_eq!( - oakcommon_videoparams_get_enabled(dup(&h), &mut en), - OAKCOMMON_OK - ); - assert_eq!(en, 0); - - assert_eq!( - oakcommon_videoparams_set_interlacing(CHandle::null(), 1), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_set_divider(CHandle::null(), 2), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_set_enabled(CHandle::null(), 1), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_interlacing(CHandle::null(), &mut il), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_divider(CHandle::null(), &mut dv), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_enabled(CHandle::null(), &mut en), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_interlacing(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_divider(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_enabled(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); -} - -/// Video type, x/y offset, stream index, start time, and duration -/// round-trip. Video-type codes clamp: 1 -> Still, 2 -> ImageSequence, -/// anything else -> Video (0). -#[test] -fn video_type_position_stream_roundtrip() { - let h = make(); - let mut vt = -1i32; - let mut x = -1f32; - let mut y = -1f32; - let mut si = -1i32; - let mut st = -1i64; - let mut du = -1i64; - - assert_eq!( - oakcommon_videoparams_get_video_type(dup(&h), &mut vt), - OAKCOMMON_OK - ); - assert_eq!(vt, 0); - assert_eq!(oakcommon_videoparams_get_x(dup(&h), &mut x), OAKCOMMON_OK); - assert_eq!(x, 0.0); - assert_eq!(oakcommon_videoparams_get_y(dup(&h), &mut y), OAKCOMMON_OK); - assert_eq!(y, 0.0); - assert_eq!( - oakcommon_videoparams_get_stream_index(dup(&h), &mut si), - OAKCOMMON_OK - ); - assert_eq!(si, 0); - assert_eq!( - oakcommon_videoparams_get_start_time(dup(&h), &mut st), - OAKCOMMON_OK - ); - assert_eq!(st, 0); - assert_eq!( - oakcommon_videoparams_get_duration(dup(&h), &mut du), - OAKCOMMON_OK - ); - assert_eq!(du, 0); - - assert_eq!( - oakcommon_videoparams_set_video_type(dup(&h), 1), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_videoparams_get_video_type(dup(&h), &mut vt), - OAKCOMMON_OK - ); - assert_eq!(vt, 1); - assert_eq!( - oakcommon_videoparams_set_video_type(dup(&h), 2), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_videoparams_get_video_type(dup(&h), &mut vt), - OAKCOMMON_OK - ); - assert_eq!(vt, 2); - assert_eq!( - oakcommon_videoparams_set_video_type(dup(&h), 9), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_videoparams_get_video_type(dup(&h), &mut vt), - OAKCOMMON_OK - ); - assert_eq!(vt, 0); - - assert_eq!(oakcommon_videoparams_set_x(dup(&h), 1.5), OAKCOMMON_OK); - assert_eq!(oakcommon_videoparams_set_y(dup(&h), -2.5), OAKCOMMON_OK); - assert_eq!( - oakcommon_videoparams_set_stream_index(dup(&h), 3), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_videoparams_set_start_time(dup(&h), 12345i64), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_videoparams_set_duration(dup(&h), 67890i64), - OAKCOMMON_OK - ); - assert_eq!(oakcommon_videoparams_get_x(dup(&h), &mut x), OAKCOMMON_OK); - assert_eq!(x, 1.5); - assert_eq!(oakcommon_videoparams_get_y(dup(&h), &mut y), OAKCOMMON_OK); - assert_eq!(y, -2.5); - assert_eq!( - oakcommon_videoparams_get_stream_index(dup(&h), &mut si), - OAKCOMMON_OK - ); - assert_eq!(si, 3); - assert_eq!( - oakcommon_videoparams_get_start_time(dup(&h), &mut st), - OAKCOMMON_OK - ); - assert_eq!(st, 12345); - assert_eq!( - oakcommon_videoparams_get_duration(dup(&h), &mut du), - OAKCOMMON_OK - ); - assert_eq!(du, 67890); - - assert_eq!( - oakcommon_videoparams_set_video_type(CHandle::null(), 1), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_set_x(CHandle::null(), 1.0), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_set_y(CHandle::null(), 1.0), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_set_stream_index(CHandle::null(), 1), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_set_start_time(CHandle::null(), 1), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_set_duration(CHandle::null(), 1), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_video_type(CHandle::null(), &mut vt), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_x(CHandle::null(), &mut x), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_y(CHandle::null(), &mut y), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_stream_index(CHandle::null(), &mut si), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_start_time(CHandle::null(), &mut st), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_duration(CHandle::null(), &mut du), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_video_type(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_x(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_y(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_stream_index(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_start_time(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_duration(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); -} - -/// Premultiplied alpha, color range, primaries, and transfer round-trip. -/// Color-range codes clamp: 1 -> Full, anything else -> Limited (0). -#[test] -fn color_fields_roundtrip() { - let h = make(); - let mut pa = -1i32; - let mut cr = -1i32; - let mut cp = -1i32; - let mut ct = -1i32; - - assert_eq!( - oakcommon_videoparams_get_premultiplied_alpha(dup(&h), &mut pa), - OAKCOMMON_OK - ); - assert_eq!(pa, 0); - assert_eq!( - oakcommon_videoparams_get_color_range(dup(&h), &mut cr), - OAKCOMMON_OK - ); - assert_eq!(cr, 0); - assert_eq!( - oakcommon_videoparams_get_color_primaries(dup(&h), &mut cp), - OAKCOMMON_OK - ); - assert_eq!(cp, 0); - assert_eq!( - oakcommon_videoparams_get_color_transfer(dup(&h), &mut ct), - OAKCOMMON_OK - ); - assert_eq!(ct, 0); - - assert_eq!( - oakcommon_videoparams_set_premultiplied_alpha(dup(&h), 1), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_videoparams_set_color_range(dup(&h), 1), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_videoparams_set_color_primaries(dup(&h), 2), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_videoparams_set_color_transfer(dup(&h), 3), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_videoparams_get_premultiplied_alpha(dup(&h), &mut pa), - OAKCOMMON_OK - ); - assert_eq!(pa, 1); - assert_eq!( - oakcommon_videoparams_get_color_range(dup(&h), &mut cr), - OAKCOMMON_OK - ); - assert_eq!(cr, 1); - assert_eq!( - oakcommon_videoparams_get_color_primaries(dup(&h), &mut cp), - OAKCOMMON_OK - ); - assert_eq!(cp, 2); - assert_eq!( - oakcommon_videoparams_get_color_transfer(dup(&h), &mut ct), - OAKCOMMON_OK - ); - assert_eq!(ct, 3); - - // Out-of-range color range clamps to Limited. - assert_eq!( - oakcommon_videoparams_set_color_range(dup(&h), 5), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_videoparams_get_color_range(dup(&h), &mut cr), - OAKCOMMON_OK - ); - assert_eq!(cr, 0); - - assert_eq!( - oakcommon_videoparams_set_premultiplied_alpha(CHandle::null(), 1), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_set_color_range(CHandle::null(), 1), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_set_color_primaries(CHandle::null(), 1), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_set_color_transfer(CHandle::null(), 1), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_premultiplied_alpha(CHandle::null(), &mut pa), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_color_range(CHandle::null(), &mut cr), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_color_primaries(CHandle::null(), &mut cp), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_color_transfer(CHandle::null(), &mut ct), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_premultiplied_alpha(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_color_range(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_color_primaries(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_color_transfer(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); -} - -// ---- String fields ---- - -/// `colorspace` round-trips through a two-stage getter; the empty string is -/// a valid value (required size 1). Null handle or null string is -/// `E_INVALID`. -#[test] -fn colorspace_two_stage() { - let h = make(); - assert_eq!( - oakcommon_videoparams_set_colorspace(dup(&h), to_cstring("sRGB").as_ptr()), - OAKCOMMON_OK - ); - assert_two_stage_getter( - |buf, size| oakcommon_videoparams_get_colorspace(dup(&h), buf, size), - "sRGB", - ); - - // The empty colorspace is a valid value. - assert_eq!( - oakcommon_videoparams_set_colorspace(dup(&h), to_cstring("").as_ptr()), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_videoparams_get_colorspace(dup(&h), std::ptr::null_mut(), 0), - 1 - ); - - assert_eq!( - oakcommon_videoparams_set_colorspace(CHandle::null(), to_cstring("sRGB").as_ptr()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_set_colorspace(dup(&h), std::ptr::null()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_colorspace(CHandle::null(), std::ptr::null_mut(), 0), - OAKCOMMON_E_INVALID - ); -} - -// ---- Derived values ---- - -/// Square-pixel width, effective dimensions/depth, validity, bytes per -/// channel/pixel, and buffer size, computed from the stored fields. -#[test] -fn derived_dimensions_and_buffer() { - let h = make(); - let mut v = -1i32; - let mut w = -1i32; - let mut ht = -1i32; - let mut d = -1i32; - - assert_eq!( - oakcommon_videoparams_get_square_pixel_width(dup(&h), &mut v), - OAKCOMMON_OK - ); - assert_eq!(v, 1920); - assert_eq!( - oakcommon_videoparams_get_effective_width(dup(&h), &mut v), - OAKCOMMON_OK - ); - assert_eq!(v, 1920); - assert_eq!( - oakcommon_videoparams_get_effective_height(dup(&h), &mut v), - OAKCOMMON_OK - ); - assert_eq!(v, 1080); - assert_eq!( - oakcommon_videoparams_get_effective_depth(dup(&h), &mut v), - OAKCOMMON_OK - ); - assert_eq!(v, 1); - assert_eq!( - oakcommon_videoparams_get_is_valid(dup(&h), &mut v), - OAKCOMMON_OK - ); - assert_eq!(v, 1); - assert_eq!( - oakcommon_videoparams_get_bytes_per_channel(dup(&h), &mut v), - OAKCOMMON_OK - ); - assert_eq!(v, 1); - assert_eq!( - oakcommon_videoparams_get_bytes_per_pixel(dup(&h), &mut v), - OAKCOMMON_OK - ); - assert_eq!(v, 4); - assert_eq!( - oakcommon_videoparams_get_buffer_size(dup(&h), &mut v), - OAKCOMMON_OK - ); - assert_eq!(v, 1920 * 1080 * 4); - - // Square-pixel width follows the pixel aspect ratio (lround(1920*16/9) - // == 3413) and the effective width follows the divider. - assert_eq!( - oakcommon_videoparams_set_pixel_aspect_ratio(dup(&h), 16, 9), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_videoparams_get_square_pixel_width(dup(&h), &mut v), - OAKCOMMON_OK - ); - assert_eq!(v, 3413); - assert_eq!(oakcommon_videoparams_set_divider(dup(&h), 2), OAKCOMMON_OK); - assert_eq!( - oakcommon_videoparams_get_effective_width(dup(&h), &mut v), - OAKCOMMON_OK - ); - assert_eq!(v, 960); - assert_eq!( - oakcommon_videoparams_get_effective_height(dup(&h), &mut v), - OAKCOMMON_OK - ); - assert_eq!(v, 540); - - // An invalid format makes the parameter set invalid. - assert_eq!(oakcommon_videoparams_set_format(dup(&h), 999), OAKCOMMON_OK); - assert_eq!( - oakcommon_videoparams_get_is_valid(dup(&h), &mut v), - OAKCOMMON_OK - ); - assert_eq!(v, 0); - - assert_eq!( - oakcommon_videoparams_get_square_pixel_width(CHandle::null(), &mut v), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_effective_width(CHandle::null(), &mut v), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_effective_height(CHandle::null(), &mut v), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_effective_depth(CHandle::null(), &mut v), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_is_valid(CHandle::null(), &mut v), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_bytes_per_channel(CHandle::null(), &mut v), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_bytes_per_pixel(CHandle::null(), &mut v), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_buffer_size(CHandle::null(), &mut v), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_square_pixel_width(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_effective_width(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_effective_height(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_effective_depth(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_is_valid(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_bytes_per_channel(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_bytes_per_pixel(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_buffer_size(dup(&h), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); - - // Clean up the width/height out-params used above to keep clippy quiet. - assert_eq!(w, -1); - assert_eq!(ht, -1); - assert_eq!(d, -1); -} - -/// `get_time_in_timebase_units` converts a time into time-base units plus -/// the start time. With no time base set it writes `i64::MIN` -/// (CPP-PARITY: C++ returns `INT64_MIN` / `AV_NOPTS_VALUE`) but still -/// succeeds. -#[test] -fn time_in_timebase_units() { - let h = make_tb(); - let mut ts = -1i64; - - // Time base 1001/30000; 1001/30000 seconds == 1 unit. - assert_eq!( - oakcommon_videoparams_get_time_in_timebase_units(dup(&h), 1001, 30000, &mut ts), - OAKCOMMON_OK - ); - assert_eq!(ts, 1); - - assert_eq!( - oakcommon_videoparams_set_start_time(dup(&h), 5i64), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_videoparams_get_time_in_timebase_units(dup(&h), 1001, 30000, &mut ts), - OAKCOMMON_OK - ); - assert_eq!(ts, 6); - - // A time base of 1001/30000 (29.97 fps): 1 second == 30 units. - assert_eq!( - oakcommon_videoparams_get_time_in_timebase_units(dup(&h), 1, 1, &mut ts), - OAKCOMMON_OK - ); - assert_eq!(ts, 35); - - // `init_basic` has no time base: i64::MIN is written, call succeeds. - let nb = make(); - assert_eq!( - oakcommon_videoparams_get_time_in_timebase_units(dup(&nb), 1, 1, &mut ts), - OAKCOMMON_OK - ); - assert_eq!(ts, i64::MIN); - - assert_eq!( - oakcommon_videoparams_get_time_in_timebase_units(CHandle::null(), 1, 1, &mut ts), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_time_in_timebase_units(dup(&h), 1, 1, std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); -} - -/// `equals` compares the nine parameter-set fields; the same fields yield 1 -/// and any difference yields 0. A null handle, null `other`, or null -/// out-param is `E_INVALID`. -#[test] -fn equals() { - let a = make(); - let b = make(); - let mut eq = -1i32; - - assert_eq!( - oakcommon_videoparams_equals(dup(&a), dup(&b), &mut eq), - OAKCOMMON_OK - ); - assert_eq!(eq, 1); - - assert_eq!(oakcommon_videoparams_set_width(dup(&b), 640), OAKCOMMON_OK); - assert_eq!( - oakcommon_videoparams_equals(dup(&a), dup(&b), &mut eq), - OAKCOMMON_OK - ); - assert_eq!(eq, 0); - - assert_eq!( - oakcommon_videoparams_equals(CHandle::null(), dup(&b), &mut eq), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_equals(dup(&a), CHandle::null(), &mut eq), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_equals(dup(&a), dup(&b), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); -} - -// ---- XML ---- - -/// `load_xml` parses a fragment and applies the fields; malformed input is -/// `E_FAILED`. `save_xml` is a two-stage getter whose output starts with -/// the `` root. -#[test] -fn load_save_xml() { - let h = make(); - - // Round-trip through XML. - let xml = "640480"; - assert_eq!( - oakcommon_videoparams_load_xml(dup(&h), to_cstring(xml).as_ptr()), - OAKCOMMON_OK - ); - let mut w = -1i32; - let mut ht = -1i32; - assert_eq!( - oakcommon_videoparams_get_width(dup(&h), &mut w), - OAKCOMMON_OK - ); - assert_eq!(w, 640); - assert_eq!( - oakcommon_videoparams_get_height(dup(&h), &mut ht), - OAKCOMMON_OK - ); - assert_eq!(ht, 480); - - // save_xml is a two-stage getter (CPP-PARITY: copy_string semantics). - let required = oakcommon_videoparams_save_xml(dup(&h), std::ptr::null_mut(), 0); - assert!(required > 0); - let mut out = vec![0u8; required as usize]; - assert_eq!( - oakcommon_videoparams_save_xml(dup(&h), out.as_mut_ptr() as *mut c_char, required), - required - ); - let s = unsafe { CStr::from_ptr(out.as_ptr() as *const c_char) } - .to_str() - .unwrap(); - assert!(s.starts_with("")); - assert!(s.contains("640")); - - // Malformed fragments fail with E_FAILED. - assert_eq!( - oakcommon_videoparams_load_xml(dup(&h), to_cstring("").as_ptr()), - OAKCOMMON_E_FAILED - ); - assert_eq!( - oakcommon_videoparams_load_xml(dup(&h), to_cstring("not xml").as_ptr()), - OAKCOMMON_E_FAILED - ); - assert_eq!( - oakcommon_videoparams_load_xml(dup(&h), to_cstring("").as_ptr()), - OAKCOMMON_E_FAILED - ); - - assert_eq!( - oakcommon_videoparams_load_xml(CHandle::null(), to_cstring(xml).as_ptr()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_load_xml(dup(&h), std::ptr::null()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_save_xml(CHandle::null(), std::ptr::null_mut(), 0), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_save_xml(dup(&h), std::ptr::null_mut(), 5), - OAKCOMMON_E_INVALID - ); -} - -// ---- Static helpers ---- - -/// Pure static arithmetic helpers. These take no handle and have no failure -/// path by design (out-of-range formats map to `Invalid`, which yields 0 -/// bytes). -#[test] -fn static_arithmetic() { - // bytes per channel: U8=1, U10=0, U16/F16=2, F32=4, Invalid/Count=0. - assert_eq!(oakcommon_videoparams_get_bytes_per_channel_for_format(0), 1); - assert_eq!(oakcommon_videoparams_get_bytes_per_channel_for_format(1), 0); - assert_eq!(oakcommon_videoparams_get_bytes_per_channel_for_format(2), 2); - assert_eq!(oakcommon_videoparams_get_bytes_per_channel_for_format(4), 4); - assert_eq!( - oakcommon_videoparams_get_bytes_per_channel_for_format(999), - 0 - ); - assert_eq!(oakcommon_videoparams_static_get_bytes_per_channel(0), 1); - assert_eq!(oakcommon_videoparams_static_get_bytes_per_channel(4), 4); - - // bytes per pixel: U10 is packed 4 bytes only for 4 channels. - assert_eq!( - oakcommon_videoparams_get_bytes_per_pixel_for_format(0, 4), - 4 - ); - assert_eq!( - oakcommon_videoparams_get_bytes_per_pixel_for_format(1, 4), - 4 - ); - assert_eq!( - oakcommon_videoparams_get_bytes_per_pixel_for_format(1, 3), - 0 - ); - assert_eq!( - oakcommon_videoparams_get_bytes_per_pixel_for_format(2, 3), - 6 - ); - assert_eq!( - oakcommon_videoparams_get_bytes_per_pixel_for_format(4, 4), - 16 - ); - assert_eq!( - oakcommon_videoparams_get_bytes_per_pixel_for_format(999, 4), - 0 - ); - assert_eq!(oakcommon_videoparams_static_get_bytes_per_pixel(0, 4), 4); - assert_eq!(oakcommon_videoparams_static_get_bytes_per_pixel(1, 4), 4); - - // buffer size = w * h * bpp (U8 RGBA). - assert_eq!( - oakcommon_videoparams_calculate_buffer_size(1920, 1080, 0, 4), - 1920 * 1080 * 4 - ); - - // float formats are F16/F32 only. - assert_eq!(oakcommon_videoparams_format_is_float(3), 1); - assert_eq!(oakcommon_videoparams_format_is_float(4), 1); - assert_eq!(oakcommon_videoparams_format_is_float(0), 0); - assert_eq!(oakcommon_videoparams_format_is_float(999), 0); - - // auto divider: 1920x1080 -> 2 (of 8, 12, 16 ... capped). - assert_eq!(oakcommon_videoparams_generate_auto_divider(100, 100), 1); - assert_eq!(oakcommon_videoparams_generate_auto_divider(1920, 1080), 2); - assert_eq!(oakcommon_videoparams_generate_auto_divider(3840, 2160), 3); - - // scaled dimension is floor(dim/divider). - assert_eq!(oakcommon_videoparams_get_scaled_dimension(1920, 2), 960); - assert_eq!(oakcommon_videoparams_get_scaled_dimension(1080, 2), 540); - - // divider for a target resolution. - assert_eq!( - oakcommon_videoparams_get_divider_for_target_resolution(3840, 2160, 1920, 1080), - 2 - ); - assert_eq!( - oakcommon_videoparams_get_divider_for_target_resolution(1920, 1080, 1920, 1080), - 1 - ); - assert_eq!( - oakcommon_videoparams_get_divider_for_target_resolution(1921, 1081, 959, 540), - 3 - ); -} - -/// Static two-stage string getters: divider names, format names, and -/// frame-rate strings. An invalid output buffer is `E_INVALID`. -#[test] -fn static_string_getters() { - assert_two_stage_getter( - |buf, size| oakcommon_videoparams_get_name_for_divider(1, buf, size), - "Full", - ); - assert_two_stage_getter( - |buf, size| oakcommon_videoparams_get_name_for_divider(2, buf, size), - "1/2", - ); - assert_two_stage_getter( - |buf, size| oakcommon_videoparams_get_name_for_divider(8, buf, size), - "1/8", - ); - - assert_two_stage_getter( - |buf, size| oakcommon_videoparams_get_format_name(0, buf, size), - "8-bit", - ); - assert_two_stage_getter( - |buf, size| oakcommon_videoparams_get_format_name(1, buf, size), - "10-bit Packed", - ); - assert_two_stage_getter( - |buf, size| oakcommon_videoparams_get_format_name(2, buf, size), - "16-bit Integer", - ); - assert_two_stage_getter( - |buf, size| oakcommon_videoparams_get_format_name(3, buf, size), - "Half-Float (16-bit)", - ); - assert_two_stage_getter( - |buf, size| oakcommon_videoparams_get_format_name(4, buf, size), - "Full-Float (32-bit)", - ); - assert_two_stage_getter( - |buf, size| oakcommon_videoparams_get_format_name(5, buf, size), - "Unknown (0x5)", - ); - assert_two_stage_getter( - |buf, size| oakcommon_videoparams_get_format_name(-1, buf, size), - "Unknown (0xFFFFFFFF)", - ); - - assert_two_stage_getter( - |buf, size| oakcommon_videoparams_frame_rate_to_string(24000, 1001, buf, size), - "23.976 FPS", - ); - assert_two_stage_getter( - |buf, size| oakcommon_videoparams_frame_rate_to_string(24, 1, buf, size), - "24 FPS", - ); - assert_two_stage_getter( - |buf, size| oakcommon_videoparams_frame_rate_to_string(0, 0, buf, size), - "nan FPS", - ); - - // An invalid output buffer (null with a positive size) is E_INVALID. - assert_eq!( - oakcommon_videoparams_get_name_for_divider(1, std::ptr::null_mut(), 5), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_get_format_name(0, std::ptr::null_mut(), 5), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_videoparams_frame_rate_to_string(24, 1, std::ptr::null_mut(), 5), - OAKCOMMON_E_INVALID - ); -} diff --git a/crates/oakcommon/tests/ffi_xmlutils.rs b/crates/oakcommon/tests/ffi_xmlutils.rs deleted file mode 100644 index f62cc4280..000000000 --- a/crates/oakcommon/tests/ffi_xmlutils.rs +++ /dev/null @@ -1,761 +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 . - -//! FFI-level integration tests for the C-ABI XML streaming exports in -//! `oakcommon::ffi::xmlutils`, asserted against the C++ oracle -//! `src/common/c_api/xmlutils.cpp`. -//! -//! `ffi::error` and `ffi::error_abi` are documented placeholder modules -//! (`include/common/error.h` exposes no functions; the `OAKCOMMON_OK` / -//! `OAKCOMMON_E_*` constants live in `crate::error`), so the only callable -//! surface here is `xmlutils`. The error constants its exports return are -//! pinned in `error_constants_returned_by_xmlutils_match_header`; the full -//! set is already asserted in `contract.rs`. -//! -//! The contract under test (each point matches the C++ oracle): -//! - exports take a `CHandle` by value and never release it; -//! - two-stage string getters return the required size (NUL included) and -//! only copy when the buffer is large enough — they never truncate; -//! - out-of-range attribute indexes report `OAKCOMMON_E_NOT_FOUND`; -//! - a malformed document still yields a usable handle whose `has_error` -//! flag reads back 1 and whose navigation reports no start elements. - -use std::ffi::{c_char, CString}; - -use oakcommon::error::{OAKCOMMON_E_INVALID, OAKCOMMON_E_NOT_FOUND, OAKCOMMON_OK}; -use oakcommon::ffi::xmlutils::*; -use oakcommon::handle::{CHandle, OAKCOMMON_ABI_VERSION}; - -/// Shared sample document: two attributes on the root plus a nested text -/// element. The writer test sequence reproduces this exact document. -const DOC: &str = r#"text here"#; - -/// Convert a string slice to a NUL-terminated C string for FFI inputs. -fn to_cstring(s: &str) -> CString { - CString::new(s).expect("test string must not contain NUL") -} - -/// Cheap struct copy: `CHandle` is neither `Clone` nor `Copy`, but every -/// getter takes it by value. Rebuilding from the same fields duplicates -/// only the handle value — the box stays alive as long as the original -/// handle lives, and the getters never release. -fn dup(h: &CHandle) -> CHandle { - CHandle { - ctx: h.ctx, - addref: h.addref, - release: h.release, - abi_version: h.abi_version, - } -} - -/// Drive a two-stage string getter against the C++ `copy_string` -/// convention: a null-buffer size query, a short buffer that must stay -/// untouched (no truncation), an exact-fit copy with its NUL, and an -/// oversized copy with the tail untouched. -fn assert_two_stage_getter(getter: impl Fn(*mut c_char, i32) -> i32, expected: &str) { - let required = (expected.len() + 1) as i32; - - // Size query: a null buffer returns the required size, NUL included. - assert_eq!(getter(std::ptr::null_mut(), 0), required); - - // Short buffer: too small, so nothing is written to it. - let short_size = (required - 1).max(0); - let mut short = vec![0xABu8; short_size as usize]; - assert_eq!( - getter(short.as_mut_ptr() as *mut c_char, short_size), - required - ); - assert!( - short.iter().all(|&b| b == 0xAB), - "short buffer must stay untouched" - ); - - // Exact fit: payload followed by a NUL. - let mut exact = vec![0xCDu8; required as usize]; - assert_eq!( - getter(exact.as_mut_ptr() as *mut c_char, required), - required - ); - assert_eq!(&exact[..expected.len()], expected.as_bytes()); - assert_eq!(exact[expected.len()], 0); - - // Oversized: payload and NUL written, tail left as initialized. - let mut big = vec![0u8; (required + 8) as usize]; - assert_eq!( - getter(big.as_mut_ptr() as *mut c_char, required + 8), - required - ); - assert_eq!(&big[..expected.len()], expected.as_bytes()); - assert_eq!(big[expected.len()], 0); - assert!(big[(required + 1) as usize..].iter().all(|&b| b == 0)); -} - -// ---- Reader: handle lifecycle ---- - -/// `init` with a null data pointer yields an empty handle. -#[test] -fn init_returns_null_handle_for_null_data() { - let h = oakcommon_xml_reader_init(std::ptr::null()); - assert!(h.is_null()); - assert!(h.ctx.is_null()); - assert!(h.addref.is_none()); - assert!(h.release.is_none()); -} - -/// `init` over valid data yields a stamped, non-empty handle. -#[test] -fn init_creates_stamped_handle() { - let h = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr()); - assert!(!h.is_null()); - assert_eq!(h.abi_version, OAKCOMMON_ABI_VERSION); - assert!(h.addref.is_some()); - assert!(h.release.is_some()); -} - -/// `free` nullifies the handle, is idempotent, and tolerates a null -/// pointer. -#[test] -fn free_nullifies_and_is_idempotent() { - let mut h = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr()); - assert!(!h.is_null()); - oakcommon_xml_reader_free(&mut h); - assert!(h.is_null()); - // A second free of the now-empty handle is safe. - oakcommon_xml_reader_free(&mut h); - assert!(h.is_null()); - // Freeing a null pointer is safe. - oakcommon_xml_reader_free(std::ptr::null_mut()); -} - -// ---- Reader: navigation ---- - -/// `read_next_start_element` writes 1 while a start element is found and -/// 0 once the document is exhausted (CPP-PARITY: an end element and the -/// end of the document both report 0, not an error). -#[test] -fn read_next_start_element_reports_found() { - let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr()); - let mut found = -1i32; - assert_eq!( - oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found), - OAKCOMMON_OK - ); - assert_eq!(found, 1); // root - assert_eq!( - oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found), - OAKCOMMON_OK - ); - assert_eq!(found, 1); // child - assert_eq!( - oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found), - OAKCOMMON_OK - ); - assert_eq!(found, 0); // child's end element - assert_eq!( - oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found), - OAKCOMMON_OK - ); - assert_eq!(found, 0); // root's end element -} - -/// A null reader handle or a null `found` out-param is `E_INVALID`. -#[test] -fn read_next_start_element_rejects_null_args() { - let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr()); - let mut found = 0i32; - assert_eq!( - oakcommon_xml_reader_read_next_start_element(CHandle::null(), &mut found), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_xml_reader_read_next_start_element(dup(&r), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); -} - -// ---- Reader: two-stage string getters ---- - -/// `name` is a two-stage getter over the current element's name; a null -/// handle is `E_INVALID`. -#[test] -fn name_two_stage_getter() { - let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr()); - let mut found = 0i32; - assert_eq!( - oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found), - OAKCOMMON_OK - ); - assert_eq!(found, 1); - assert_two_stage_getter( - |buf, size| oakcommon_xml_reader_name(dup(&r), buf, size), - "root", - ); - assert_eq!( - oakcommon_xml_reader_name(CHandle::null(), std::ptr::null_mut(), 0), - OAKCOMMON_E_INVALID - ); -} - -/// Before any token is read the name is the empty string (size 1). -#[test] -fn name_is_empty_before_any_read() { - let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr()); - assert_eq!( - oakcommon_xml_reader_name(dup(&r), std::ptr::null_mut(), 0), - 1 - ); -} - -/// `read_element_text` is a two-stage getter and caches its result, so a -/// second read returns the same text even though the stream was consumed -/// by the first (CPP-PARITY with the C++ `XmlReaderState::cached_text`). -#[test] -fn read_element_text_two_stage_with_cache() { - let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr()); - let mut found = 0i32; - assert_eq!( - oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found), - OAKCOMMON_OK - ); - assert_eq!(found, 1); - assert_two_stage_getter( - |buf, size| oakcommon_xml_reader_read_element_text(dup(&r), buf, size), - "text here", - ); - assert_two_stage_getter( - |buf, size| oakcommon_xml_reader_read_element_text(dup(&r), buf, size), - "text here", - ); -} - -/// When not on a start element the text is empty (CPP-PARITY: the C++ -/// reader returns an empty string). -#[test] -fn read_element_text_not_on_start_element_is_empty() { - let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr()); - let mut found = 0i32; - assert_eq!( - oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found), - OAKCOMMON_OK - ); - assert_eq!(found, 0); - assert_eq!( - oakcommon_xml_reader_read_element_text(dup(&r), std::ptr::null_mut(), 0), - 1 - ); - assert_eq!( - oakcommon_xml_reader_read_element_text(CHandle::null(), std::ptr::null_mut(), 0), - OAKCOMMON_E_INVALID - ); -} - -/// `skip_current_element` consumes the current element and its subtree; a -/// null handle is `E_INVALID`. -#[test] -fn skip_current_element_skips_subtree() { - let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr()); - let mut found = 0i32; - assert_eq!( - oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found), - OAKCOMMON_OK - ); - assert_eq!(found, 1); - assert_eq!( - oakcommon_xml_reader_skip_current_element(dup(&r)), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found), - OAKCOMMON_OK - ); - assert_eq!(found, 0); - assert_eq!( - oakcommon_xml_reader_skip_current_element(CHandle::null()), - OAKCOMMON_E_INVALID - ); -} - -// ---- Reader: attributes ---- - -/// `attribute_count` reports 0 before any token and the real count on a -/// start element; a null handle or a null count out-param is `E_INVALID`. -#[test] -fn attribute_count_reports_attributes() { - let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr()); - let mut count = -1i32; - assert_eq!( - oakcommon_xml_reader_attribute_count(dup(&r), &mut count), - OAKCOMMON_OK - ); - assert_eq!(count, 0); - let mut found = 0i32; - assert_eq!( - oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found), - OAKCOMMON_OK - ); - assert_eq!(found, 1); - assert_eq!( - oakcommon_xml_reader_attribute_count(dup(&r), &mut count), - OAKCOMMON_OK - ); - assert_eq!(count, 2); - assert_eq!( - oakcommon_xml_reader_attribute_count(CHandle::null(), &mut count), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_xml_reader_attribute_count(dup(&r), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); -} - -/// `attribute_name` / `attribute_value` are two-stage getters over the -/// attributes of the current start element, in document order. -#[test] -fn attribute_name_and_value_two_stage() { - let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr()); - let mut found = 0i32; - assert_eq!( - oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found), - OAKCOMMON_OK - ); - assert_eq!(found, 1); - assert_two_stage_getter( - |buf, size| oakcommon_xml_reader_attribute_name(dup(&r), 0, buf, size), - "a", - ); - assert_two_stage_getter( - |buf, size| oakcommon_xml_reader_attribute_value(dup(&r), 0, buf, size), - "1", - ); - assert_two_stage_getter( - |buf, size| oakcommon_xml_reader_attribute_name(dup(&r), 1, buf, size), - "b", - ); - assert_two_stage_getter( - |buf, size| oakcommon_xml_reader_attribute_value(dup(&r), 1, buf, size), - "two", - ); -} - -/// An out-of-range attribute index is `E_NOT_FOUND` (CPP-PARITY), while a -/// null handle is `E_INVALID`. -#[test] -fn attribute_out_of_range_is_not_found() { - let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr()); - let mut found = 0i32; - assert_eq!( - oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found), - OAKCOMMON_OK - ); - assert_eq!(found, 1); - assert_eq!( - oakcommon_xml_reader_attribute_name(dup(&r), 2, std::ptr::null_mut(), 0), - OAKCOMMON_E_NOT_FOUND - ); - assert_eq!( - oakcommon_xml_reader_attribute_name(dup(&r), -1, std::ptr::null_mut(), 0), - OAKCOMMON_E_NOT_FOUND - ); - assert_eq!( - oakcommon_xml_reader_attribute_value(dup(&r), 2, std::ptr::null_mut(), 0), - OAKCOMMON_E_NOT_FOUND - ); - assert_eq!( - oakcommon_xml_reader_attribute_name(CHandle::null(), 0, std::ptr::null_mut(), 0), - OAKCOMMON_E_INVALID - ); -} - -// ---- Reader: error reporting ---- - -/// `has_error` is 0 for a well-formed document and 1 for a malformed one; -/// a malformed document still navigates safely and reports no start -/// elements (CPP-PARITY: parse failures surface lazily through -/// `has_error`, never at init time). -#[test] -fn has_error_flags_malformed_documents() { - let mut err = -1i32; - - let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr()); - assert_eq!( - oakcommon_xml_reader_has_error(dup(&r), &mut err), - OAKCOMMON_OK - ); - assert_eq!(err, 0); - - let bad = oakcommon_xml_reader_init(to_cstring("").as_ptr()); - assert!(!bad.is_null()); - assert_eq!( - oakcommon_xml_reader_has_error(dup(&bad), &mut err), - OAKCOMMON_OK - ); - assert_eq!(err, 1); - let mut found = -1i32; - assert_eq!( - oakcommon_xml_reader_read_next_start_element(dup(&bad), &mut found), - OAKCOMMON_OK - ); - assert_eq!(found, 0); - - let empty = oakcommon_xml_reader_init(to_cstring("").as_ptr()); - assert!(!empty.is_null()); - assert_eq!( - oakcommon_xml_reader_has_error(dup(&empty), &mut err), - OAKCOMMON_OK - ); - assert_eq!(err, 1); - - assert_eq!( - oakcommon_xml_reader_has_error(CHandle::null(), &mut err), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_xml_reader_has_error(dup(&r), std::ptr::null_mut()), - OAKCOMMON_E_INVALID - ); -} - -// ---- Writer: handle lifecycle ---- - -/// `writer_init` yields a stamped handle; `free` nullifies it, is -/// idempotent, and tolerates a null pointer. -#[test] -fn writer_init_free_and_idempotent() { - let mut w = oakcommon_xml_writer_init(); - assert!(!w.is_null()); - assert_eq!(w.abi_version, OAKCOMMON_ABI_VERSION); - assert!(w.addref.is_some()); - assert!(w.release.is_some()); - oakcommon_xml_writer_free(&mut w); - assert!(w.is_null()); - oakcommon_xml_writer_free(&mut w); - assert!(w.is_null()); - oakcommon_xml_writer_free(std::ptr::null_mut()); -} - -// ---- Writer: operations ---- - -/// Every writer export rejects a null handle with `E_INVALID`, and the -/// string-taking exports reject null strings. -#[test] -fn writer_rejects_null_arguments() { - let w = oakcommon_xml_writer_init(); - assert_eq!( - oakcommon_xml_writer_write_start_element(CHandle::null(), to_cstring("a").as_ptr()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_xml_writer_write_attribute( - CHandle::null(), - to_cstring("a").as_ptr(), - to_cstring("b").as_ptr(), - ), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_xml_writer_write_characters(CHandle::null(), to_cstring("x").as_ptr()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_xml_writer_write_text_element( - CHandle::null(), - to_cstring("a").as_ptr(), - to_cstring("b").as_ptr(), - ), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_xml_writer_write_end_element(CHandle::null()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_xml_writer_write_end_document(CHandle::null()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_xml_writer_output(CHandle::null(), std::ptr::null_mut(), 0), - OAKCOMMON_E_INVALID - ); - - assert_eq!( - oakcommon_xml_writer_write_start_element(dup(&w), std::ptr::null()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_xml_writer_write_attribute(dup(&w), std::ptr::null(), to_cstring("b").as_ptr()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_xml_writer_write_attribute(dup(&w), to_cstring("a").as_ptr(), std::ptr::null()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_xml_writer_write_characters(dup(&w), std::ptr::null()), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_xml_writer_write_text_element( - dup(&w), - std::ptr::null(), - to_cstring("b").as_ptr() - ), - OAKCOMMON_E_INVALID - ); - assert_eq!( - oakcommon_xml_writer_write_text_element( - dup(&w), - to_cstring("a").as_ptr(), - std::ptr::null() - ), - OAKCOMMON_E_INVALID - ); -} - -/// The writer sequence builds the exact sample document, and `output` is a -/// two-stage getter over it. -#[test] -fn writer_builds_document_and_output_two_stage() { - let w = oakcommon_xml_writer_init(); - assert_eq!( - oakcommon_xml_writer_write_start_element(dup(&w), to_cstring("root").as_ptr()), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_xml_writer_write_attribute( - dup(&w), - to_cstring("a").as_ptr(), - to_cstring("1").as_ptr() - ), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_xml_writer_write_attribute( - dup(&w), - to_cstring("b").as_ptr(), - to_cstring("two").as_ptr() - ), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_xml_writer_write_text_element( - dup(&w), - to_cstring("child").as_ptr(), - to_cstring("text here").as_ptr(), - ), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_xml_writer_write_end_element(dup(&w)), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_xml_writer_write_end_document(dup(&w)), - OAKCOMMON_OK - ); - assert_two_stage_getter( - |buf, size| oakcommon_xml_writer_output(dup(&w), buf, size), - DOC, - ); -} - -/// The writer's output round-trips through the reader. -#[test] -fn writer_output_round_trips_through_reader() { - let w = oakcommon_xml_writer_init(); - assert_eq!( - oakcommon_xml_writer_write_start_element(dup(&w), to_cstring("root").as_ptr()), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_xml_writer_write_attribute( - dup(&w), - to_cstring("a").as_ptr(), - to_cstring("1").as_ptr() - ), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_xml_writer_write_attribute( - dup(&w), - to_cstring("b").as_ptr(), - to_cstring("two").as_ptr() - ), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_xml_writer_write_text_element( - dup(&w), - to_cstring("child").as_ptr(), - to_cstring("text here").as_ptr(), - ), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_xml_writer_write_end_element(dup(&w)), - OAKCOMMON_OK - ); - - let mut out = vec![0u8; 64]; - let needed = oakcommon_xml_writer_output(dup(&w), out.as_mut_ptr() as *mut c_char, 64); - assert_eq!(needed, (DOC.len() + 1) as i32); - assert_eq!(&out[..DOC.len()], DOC.as_bytes()); - assert_eq!(out[DOC.len()], 0); - - let r = oakcommon_xml_reader_init(out.as_ptr() as *const c_char); - assert!(!r.is_null()); - let mut found = 0i32; - assert_eq!( - oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found), - OAKCOMMON_OK - ); - assert_eq!(found, 1); - assert_two_stage_getter( - |buf, size| oakcommon_xml_reader_name(dup(&r), buf, size), - "root", - ); - assert_eq!( - oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found), - OAKCOMMON_OK - ); - assert_eq!(found, 1); - assert_two_stage_getter( - |buf, size| oakcommon_xml_reader_read_element_text(dup(&r), buf, size), - "text here", - ); - assert_eq!( - oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found), - OAKCOMMON_OK - ); - assert_eq!(found, 0); -} - -/// Documented no-op writer operations (attribute without an open start -/// tag, end element on an empty stack, end document with nothing open) -/// still return `OK`. -#[test] -fn writer_noop_operations_return_ok() { - let w = oakcommon_xml_writer_init(); - assert_eq!( - oakcommon_xml_writer_write_attribute( - dup(&w), - to_cstring("a").as_ptr(), - to_cstring("b").as_ptr() - ), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_xml_writer_write_end_element(dup(&w)), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_xml_writer_write_end_document(dup(&w)), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_xml_writer_write_characters(dup(&w), to_cstring("x").as_ptr()), - OAKCOMMON_OK - ); - assert_two_stage_getter( - |buf, size| oakcommon_xml_writer_output(dup(&w), buf, size), - "x", - ); -} - -/// An empty element with attributes serializes as ``. -#[test] -fn writer_self_closing_empty_element() { - let w = oakcommon_xml_writer_init(); - assert_eq!( - oakcommon_xml_writer_write_start_element(dup(&w), to_cstring("a").as_ptr()), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_xml_writer_write_attribute( - dup(&w), - to_cstring("k").as_ptr(), - to_cstring("v").as_ptr() - ), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_xml_writer_write_end_element(dup(&w)), - OAKCOMMON_OK - ); - assert_two_stage_getter( - |buf, size| oakcommon_xml_writer_output(dup(&w), buf, size), - r#""#, - ); -} - -/// Text and attribute values are escaped for the five predefined XML -/// entities. -#[test] -fn writer_escapes_text_and_attributes() { - let w = oakcommon_xml_writer_init(); - assert_eq!( - oakcommon_xml_writer_write_text_element( - dup(&w), - to_cstring("e").as_ptr(), - to_cstring("hi & bye ").as_ptr(), - ), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_xml_writer_write_start_element(dup(&w), to_cstring("a").as_ptr()), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_xml_writer_write_attribute( - dup(&w), - to_cstring("q").as_ptr(), - to_cstring("x\"y&z").as_ptr(), - ), - OAKCOMMON_OK - ); - assert_eq!( - oakcommon_xml_writer_write_end_element(dup(&w)), - OAKCOMMON_OK - ); - assert_two_stage_getter( - |buf, size| oakcommon_xml_writer_output(dup(&w), buf, size), - r#"hi & bye <there>"#, - ); -} - -// ---- error / error_abi ---- - -/// The constants the xmlutils exports return are defined in `crate::error` -/// and match `include/common/error.h` (the `ffi::error` / `ffi::error_abi` -/// modules only point here). The full set is pinned in `contract.rs`. -#[test] -fn error_constants_returned_by_xmlutils_match_header() { - assert_eq!(OAKCOMMON_OK, 0); - assert_eq!(OAKCOMMON_E_INVALID, -10001); - assert_eq!(OAKCOMMON_E_NOT_FOUND, -10004); -} diff --git a/crates/oakengine/Cargo.toml b/crates/oakengine/Cargo.toml index c999e661a..d31c35380 100644 --- a/crates/oakengine/Cargo.toml +++ b/crates/oakengine/Cargo.toml @@ -2,75 +2,54 @@ name = "oakengine" version = "0.1.0" edition = "2021" -description = "Oak Video Editor facade: re-exports the frozen oakengine_* C ABI over the module C ABIs (Rust)" +description = "Oak Video Editor facade: re-exports the frozen oakengine_* C ABI over the module Rust APIs (Rust)" license = "GPL-3.0-or-later" [lib] -crate-type = ["cdylib", "staticlib", "rlib"] +crate-type = ["cdylib"] [dependencies] -# NDJSON control-plane protocol for the worker session (src/worker.rs) and -# the shm error formatting in src/ipc.rs. -serde = { version = "1", features = ["derive"] } -serde_json = "1" -# POSIX shm_open/mmap/munmap/shm_unlink constants + syscalls for the -# shared-memory frame-slot transport (src/ipc.rs). -libc = "0.2" # Error derive (Display + std::error::Error) for the crate error enum # (src/error.rs). Same major version the other modules use (oakotio, ...). thiserror = "2" -# Every module call crosses the module C ABI as an `extern "C"` import -# (src/bridge/). The module crates below are REAL dependencies so their -# `#[no_mangle]` exports are linked into the final `liboakengine` cdylib: -# the dylib then carries the module C ABIs itself (oakundo_*, oakcommon_*, -# oaktimeline_*, oakcodec_*, oakaudio_*, oakrender_*, oaktask_*, -# oakplugin_*, oaknode_*) alongside the facade's oakengine_* exports. +# Worker/IPC wire protocol (src/worker.rs, src/ipc.rs): serde for the +# NDJSON control-plane messages and serde_json for the wire encoding. +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# POSIX shm_open/ftruncate/mmap/munmap/shm_unlink for the shared-memory +# frame-slot transport (src/ipc.rs). +libc = "0.2" + +# Single-lib unification: every module call is a compile-time Rust call +# into the module crate's direct API (no module C ABI). The engine keeps +# the frozen oakengine_* C ABI upward; downward it rewires through +# src/stubs.rs (direct Rust shims for oakcommon/oakcodec/oakrender/ +# oakaudio/oakplugin, and clearly marked STUBs for the handle-based +# oaknode/oaktimeline paths). # -# The crates are linked WITHOUT their `test-stubs` features so the real -# exports ship. Cross-module calls that the modules resolve with -# dlsym(RTLD_DEFAULT) (oaknode, oakplugin, oakrender) now resolve against -# the sibling modules inside the same dylib; the remaining undefined -# imports are the C++ host-provided symbols (`oakcore_audioparams_*`, -# `oakcore_rational_*` from liboakcore), which build.rs leaves as runtime -# lookups for the host app. The audio resample/decode paths call FFmpeg -# in-process via ffmpeg-next (oakaudio/oakcodec); the C++ ffmpeg_bridge -# `fb_*` imports are gone. +# The remaining undefined imports are the C++ host-provided symbols +# (`oakcore_audioparams_*` from liboakcore), which build.rs leaves as +# runtime lookups for the host app (see src/stubs.rs audio module). # -# Tests link the same crates; the dev-dependencies below re-declare -# oakcommon/oakplugin WITH `test-stubs` so their in-crate C ABI mocks -# (the ffmpeg_bridge replacement and the oakrender dlsym stubs) are -# compiled into the test binaries (features union with the normal -# dependencies for test builds). -# -# NOTE (oaktimeline/oaktask): their `test-stubs` features are never used -# here — the in-crate mocks define `oakundo_command_init` etc., which -# would collide with the real oakundo rlib in one binary. Without -# test-stubs their real exports reference the oaknode/oakundo/oakcommon -# C ABI symbols as link-time externs, provided by the sibling crates in -# the same dylib (or by the dev-dependency rlibs in a test binary). -# -# rustc would normally prune rlibs that are only touched through -# `extern "C"` imports from the link; src/linkage.rs anchors every crate -# (and oakcore-rs) so the linker pulls their object files. +# src/linkage.rs anchors every crate (and oakcore-rs) so the linker pulls +# their object files into the cdylib. oakcore-rs = { path = "../oakcore" } oakundo = { path = "../oakundo" } oakcommon = { path = "../oakcommon" } oaktimeline = { path = "../oaktimeline" } oakcodec = { path = "../oakcodec" } -oakaudio = { path = "../oakaudio" } oakrender = { path = "../oakrender" } oaktask = { path = "../oaktask" } -oakplugin = { path = "../oakplugin" } oaknode = { path = "../oaknode" } +oakaudio = { path = "../oakaudio" } +oakplugin = { path = "../oakplugin" } [dev-dependencies] -# The module crates as plain dependencies (no `test-stubs`): the C ABI -# mocks the test binaries need (the oakcore_* symbols) are provided by -# the crate's own test support (tests/common/mod.rs), which is -# force-linked into every integration test. Keeping the dev-deps feature-free also stops Cargo's workspace -# feature unification from forcing `test-stubs` onto the oakcommon/oakplugin -# member builds during `cargo test --workspace` (their own tests then run -# in the real dlsym mode and stay green). +# The facade is cdylib-only, so the former tests/*.rs integration tests +# now run as unit tests inside the crate (src/test_support/, pulled in +# from src/lib.rs); the module crates are reachable through the normal +# [dependencies] above. oakcommon stays here for the direct dev-only +# imports in the test support files. oakcommon = { path = "../oakcommon" } -oakplugin = { path = "../oakplugin" } diff --git a/crates/oakengine/README.md b/crates/oakengine/README.md index 4ffb45431..c6d25d371 100644 --- a/crates/oakengine/README.md +++ b/crates/oakengine/README.md @@ -26,10 +26,6 @@ src/ node.rs node.h + project.h + footage.h (node graph / project / footage) timeline.rs timeline.h (sequences, clips, tracks, markers, workarea) task.rs task.h (background tasks over oaktask) - ipc.rs ipc.h (shm/framepool half): SpscRingBuffer + FrameSlotPool - + POSIX SharedMemoryRegion (the real frame-slot transport) - worker.rs worker.h (render worker): backend selection via the - oakrender module C ABI + fallback, session, NDJSON main deferred.rs documented deferrals (stub detail lives here and in the family modules' stub bodies) tests/ @@ -39,11 +35,9 @@ tests/ ### Dependencies -The facade's regular dependencies are `serde`/`serde_json` (the worker's -NDJSON control-plane protocol, `src/worker.rs`), `libc` (POSIX -`shm_open`/`mmap`/`munmap`/`shm_unlink` for `src/ipc.rs`) and `thiserror` -(the `Display` + `std::error::Error` impl for the facade error enum, -`src/error.rs`). Every module +The facade's regular dependency is `thiserror` (the `Display` + +`std::error::Error` impl for the facade error enum, `src/error.rs`). Every +module call crosses the module C ABI as an `extern "C"` import (`src/bridge/`), and the module crates themselves are real dependencies: [`linkage`](src/linkage.rs) anchors them so their `#[no_mangle]` exports are linked into the @@ -84,17 +78,17 @@ convention: the return value is the length excluding the NUL | plugin | plugin.h | 4 | callbacks are facade state; push_button stubbed (no module API) | | codec | encoding.h | 81/85 | metadata family over oakcodec (`include/codec/format.h`); params handle is a facade box over the `oakcodec_encoding_params` POD; presets/load-save/export stubs | | render | renderer.h, color.h, lut.h | 60/60 | renderer over oakrender tickets; frame accessors over `OakCodecFrame`; color processor over `oakrender_color_processor_*`; color-manager list queries + LUT library stubs | -| worker | worker.h | 8 | the render worker: `oakengine_worker_main` + the session family over the oakrender display renderer C ABI (dynamic → OpenGL fallback) and the ipc transport (see `src/worker.rs`) | -| ipc | ipc.h (shm/framepool) | 28 | named shared-memory segments + frame-slot pools over `SpscRingBuffer` — the real frame-slot transport (see `src/ipc.rs`); the control-plane message serializers (`oakengine_ipc_*_to_json/parse`) are not wrapped (the worker speaks the NDJSON protocol with serde) | | node | node.h, project.h, footage.h | 226/327 | the node graph, project and footage families over the oaknode C ABI (`include/node/*.h`); 101 documented stubs where the module lacks the surface (gizmos, plugin messages, input properties, brush, thumbnail/waveform caches, shape/subtitle, keyframe enumeration, ...) — see the stub bodies | | timeline | timeline.h | 126/139 | sequences/clips/tracks/markers/workarea over oaknode + oaktimeline; 13 documented stubs (ripple-tracks command, default transitions, move-track/clip, marker-create, auto-cache, cache invalidation, multicam find/switch — module-surface gaps, see the stub bodies) | | task | task.h | 27 | the background-task system over oaktask (manager + load/save/import/export creators + result accessors); `create_proxy` stubbed (the module has no proxy-task C creator); start-time/is-cancelled are facade-approximated | Deferred/stub detail lives in [`deferred`] and in the stub bodies' doc -comments. `worker` and `ipc` were in the deferred list until the render -worker landed here — the worker's runtime and the shared-memory frame-slot -transport are now real (see `src/worker.rs` / `src/ipc.rs`); the ipc.h -control-plane message serializers remain unwrapped. +comments. The worker/IPC families (`worker.h`/`ipc.h`) are **not** part of +the facade anymore: the frozen C++ ABI does not include them, so the +render worker's runtime and the shared-memory frame-slot transport moved +into the `oak-worker` crate (`crates/oak-worker/src/{worker,ipc}.rs`, +self-contained, direct Rust calls into oakrender); the ipc.h control-plane +message serializers remain unwrapped. ## Testing @@ -121,14 +115,11 @@ binaries keep the in-crate mocks (ffmpeg_bridge stub / render mocks): Families whose wrapped behavior requires the real module dylibs carry `#[ignore]` tests with a documented reason; the smoke tests here exercise -the module crates' real implementations. The ipc tests create+attach two -in-process mappings of real POSIX segments and exchange slot indices and -payloads in both directions, including wraparound and full/empty edges. +the module crates' real implementations. ``` -cargo test # 71 tests green + 1 ignored (lib 35: ipc 17 + worker 18; - # integration: undo 3, common 4, audio 4, plugin 3, codec 5, - # render 6, linkage 1, node 3, timeline 2 + 1 ignored, task 5) +cargo test # lib tests + integration families (undo, common, audio, + # plugin, codec, render, linkage, node, timeline, task) cargo build # cdylib embeds the module C ABIs; oakcore_*/fb_* stay # runtime lookups (see build.rs) ``` diff --git a/crates/oakengine/build.rs b/crates/oakengine/build.rs index 2ffebe585..195e0dd49 100644 --- a/crates/oakengine/build.rs +++ b/crates/oakengine/build.rs @@ -36,18 +36,22 @@ fn main() { println!("cargo:rustc-cdylib-link-arg=-Wl,-rpath,/usr/lib"); } // The dlsym codec bridge (M12 P0) resolves `oakcodec_*` from the - // process-global scope; the engine test binaries statically link the - // module crates, so their symbols must be exported from the main - // executable. - println!("cargo:rustc-link-arg-tests=-Wl,-export_dynamic"); + // process-global scope; the engine's unit-test binary (the former + // integration tests live in src/test_support/) statically links the + // module crates, so their symbols must be exported from the test + // executable. `cargo:rustc-link-arg-tests` is NOT usable: the facade + // is cdylib-only, so cargo reports "does not have a test target" for + // that directive — use the generic `rustc-link-arg` (harmless no-op + // for the cdylib link itself). + println!("cargo:rustc-link-arg=-Wl,-export_dynamic"); if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") { // The bundled OpenColorIO's macos system monitor references // IOKit / ColorSync / CoreGraphics display APIs; the engine // dylib links with `-undefined,dynamic_lookup`, so test binaries // must resolve them. for fw in ["IOKit", "ColorSync", "CoreGraphics"] { - println!("cargo:rustc-link-arg-tests=-framework"); - println!("cargo:rustc-link-arg-tests={fw}"); + println!("cargo:rustc-link-arg=-framework"); + println!("cargo:rustc-link-arg={fw}"); } } } diff --git a/crates/oakengine/src/audio.rs b/crates/oakengine/src/audio.rs index 366fdfc0f..037881834 100644 --- a/crates/oakengine/src/audio.rs +++ b/crates/oakengine/src/audio.rs @@ -25,7 +25,7 @@ use std::ffi::{c_char, c_double, c_int, c_void}; -use crate::bridge::audio as a; +use crate::stubs::audio as a; use crate::error::Error; use crate::handle::{ box_handle, free_box, guard, guard_i64, guard_int, guard_void, unbox, CHandle, @@ -445,6 +445,51 @@ pub unsafe extern "C" fn oakengine_audio_sync_place_by_waveform_offset( }) } +// --------------------------------------------------------------------------- +// Waveform extraction +// --------------------------------------------------------------------------- + +/// `oakengine_waveform_extract` — two-stage whole-file min/max waveform +/// extraction of `filename`'s audio stream (the module's +/// `oakaudio_waveform_extract`, M12 P4). +/// +/// First call with `out_pairs == NULL` / `capacity_points == 0` returns the +/// required point count without writing; the channel count is reported +/// whenever `out_channel_count` is non-NULL. The data pass writes +/// `point_count * channel_count` channel-interleaved pairs (the module's +/// `oakaudio_min_max` POD; `capacity_points` counts points) and returns the +/// point count. Returns a negative facade `OAKENGINE_E_INVALID` for NULL +/// `filename` / negative `stream_index` / non-positive `samples_per_point` +/// / negative `capacity_points`; module decode errors (`OAKAUDIO_E_NOT_FOUND`, +/// ...) pass through untranslated. +#[no_mangle] +pub unsafe extern "C" fn oakengine_waveform_extract( + filename: *const c_char, + stream_index: c_int, + samples_per_point: c_int, + out_pairs: *mut a::MinMax, + capacity_points: c_int, + out_channel_count: *mut c_int, +) -> c_int { + guard_int(|| unsafe { + if filename.is_null() || stream_index < 0 || samples_per_point <= 0 || capacity_points < 0 { + return Err(Error::Invalid); + } + let n = a::oakaudio_waveform_extract( + filename, + stream_index, + samples_per_point, + out_pairs, + capacity_points, + out_channel_count, + ); + if n < 0 { + return Err(Error::Module(n)); + } + Ok(n) + }) +} + // --------------------------------------------------------------------------- // Audio processor // --------------------------------------------------------------------------- diff --git a/crates/oakengine/src/bridge/audio.rs b/crates/oakengine/src/bridge/audio.rs deleted file mode 100644 index 2f942343d..000000000 --- a/crates/oakengine/src/bridge/audio.rs +++ /dev/null @@ -1,378 +0,0 @@ -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! oakaudio C ABI bridge: direct Rust calls into the `oakaudio` crate. -//! -//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every -//! call below is a compile-time Rust call into `oakaudio`'s `ffi` (the -//! `#[no_mangle]` exports stay in the dylib for the external C ABI; -//! internal callers bypass them). Handles cross as the shared -//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"` -//! declaration (resolved at link time against the sibling crate in the -//! same dylib) are the host `oakcore_*` symbols and the encoding-params -//! C ABI POD crossings (the facade keeps its own POD mirrors there). - -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! oakaudio C ABI imports, mirroring the oakaudio crate's exports -//! (`src/audio/rust/src/ffi.rs`; headers `include/audio/*.h`), plus the -//! liboakcore `oakcore_audioparams_*` readers used to convert the -//! engine's borrowed `OakAudioParams*` handles. - -use std::ffi::{c_char, c_double, c_int, c_void}; - -use crate::handle::CHandle; - -/// `oakaudio_offsetresult` C ABI POD. Single-lib unification: aliases -/// the oakaudio crate's struct (identical layout). -pub type OffsetResult = oakaudio::ffi::sync::OffsetResult; - -/// `oakaudio_stretchoffsetresult` C ABI POD. Single-lib unification: aliases -/// the oakaudio crate's struct (identical layout). -pub type StretchOffsetResult = oakaudio::ffi::sync::StretchOffsetResult; - -/// `oakaudio_sourceclip` C ABI POD. Single-lib unification: aliases -/// the oakaudio crate's struct (identical layout). -pub type SourceClip = oakaudio::ffi::sync::SourceClip; - -/// `oakaudio` recording-params POD — single-lib unification: aliases the -/// oakaudio crate's type (itself the oakcodec `oakcodec_encoding_params`). -pub type EncodingParams = oakaudio::bridge::codec::EncodingParams; - -extern "C" { - pub fn oakcore_audioparams_create( - sample_rate: c_int, - channel_layout: u64, - format: c_int, - ) -> *mut c_void; - pub fn oakcore_audioparams_free(params: *mut c_void); - pub fn oakcore_audioparams_sample_rate(params: *const c_void) -> c_int; - /// `oakcore_audioparams_set_time_base` (host-provided). - pub fn oakcore_audioparams_set_time_base(params: *mut c_void, num: c_int, den: c_int); - pub fn oakcore_audioparams_channel_layout(params: *const c_void) -> u64; - pub fn oakcore_audioparams_format(params: *const c_void) -> c_int; -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_manager_create_instance() -> c_int { - unsafe { oakaudio::ffi::manager::oakaudio_manager_create_instance() } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_manager_destroy_instance() { - unsafe { oakaudio::ffi::manager::oakaudio_manager_destroy_instance() } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_manager_instance() -> CHandle { - unsafe { oakaudio::ffi::manager::oakaudio_manager_instance() } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_manager_free(_self: *mut CHandle) { - unsafe { oakaudio::ffi::manager::oakaudio_manager_free(_self) } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_manager_set_output_notify_interval(_self: CHandle, bytes: i64) -> c_int { - unsafe { oakaudio::ffi::manager::oakaudio_manager_set_output_notify_interval(_self, bytes) } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_manager_push_to_output( - _self: CHandle, - rate: c_int, - layout: u64, - format: c_int, - samples: *const c_char, - samples_size: i64, - error_buf: *mut c_char, - error_buf_size: c_int, -) -> c_int { - unsafe { - oakaudio::ffi::manager::oakaudio_manager_push_to_output( - _self, - rate, - layout, - format, - samples, - samples_size, - error_buf, - error_buf_size, - ) - } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_manager_clear_buffered_output(_self: CHandle) -> c_int { - unsafe { oakaudio::ffi::manager::oakaudio_manager_clear_buffered_output(_self) } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_manager_stop_output(_self: CHandle) -> c_int { - unsafe { oakaudio::ffi::manager::oakaudio_manager_stop_output(_self) } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_manager_reset_output_clock(_self: CHandle) -> c_int { - unsafe { oakaudio::ffi::manager::oakaudio_manager_reset_output_clock(_self) } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_manager_get_output_device(_self: CHandle) -> c_int { - unsafe { oakaudio::ffi::manager::oakaudio_manager_get_output_device(_self) } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_manager_set_output_device(_self: CHandle, device: c_int) -> c_int { - unsafe { oakaudio::ffi::manager::oakaudio_manager_set_output_device(_self, device) } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_manager_get_input_device(_self: CHandle) -> c_int { - unsafe { oakaudio::ffi::manager::oakaudio_manager_get_input_device(_self) } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_manager_set_input_device(_self: CHandle, device: c_int) -> c_int { - unsafe { oakaudio::ffi::manager::oakaudio_manager_set_input_device(_self, device) } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_manager_hard_reset(_self: CHandle) -> c_int { - unsafe { oakaudio::ffi::manager::oakaudio_manager_hard_reset(_self) } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -/// `oakaudio_manager_start_recording` — crosses the encoding-params C ABI -/// POD (the facade keeps its own opaque mirror; the module's -/// `oakaudio::bridge::codec::EncodingParams`). Kept as a link-time -/// `extern "C"` declaration against the frozen module C ABI. -pub fn oakaudio_manager_start_recording( - _self: CHandle, - params: *const EncodingParams, - error_buf: *mut c_char, - error_buf_size: c_int, -) -> c_int { - unsafe { - oakaudio::ffi::manager::oakaudio_manager_start_recording( - _self, - params, - error_buf, - error_buf_size, - ) - } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_manager_stop_recording(_self: CHandle) -> c_int { - unsafe { oakaudio::ffi::manager::oakaudio_manager_stop_recording(_self) } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_manager_seconds(_self: CHandle, out: *mut c_double) -> c_int { - unsafe { oakaudio::ffi::manager::oakaudio_manager_seconds(_self, out) } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_manager_output_levels(_self: CHandle, peaks: *mut f32, capacity: c_int) -> c_int { - unsafe { oakaudio::ffi::manager::oakaudio_manager_output_levels(_self, peaks, capacity) } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_processor_init() -> CHandle { - unsafe { oakaudio::ffi::processor::oakaudio_processor_init() } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_processor_free(_self: *mut CHandle) { - unsafe { oakaudio::ffi::processor::oakaudio_processor_free(_self) } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_processor_open( - _self: CHandle, - in_rate: c_int, - in_layout: u64, - in_format: c_int, - out_rate: c_int, - out_layout: u64, - out_format: c_int, - speed: c_double, -) -> c_int { - unsafe { - oakaudio::ffi::processor::oakaudio_processor_open( - _self, in_rate, in_layout, in_format, out_rate, out_layout, out_format, speed, - ) - } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_processor_close(_self: CHandle) -> c_int { - unsafe { oakaudio::ffi::processor::oakaudio_processor_close(_self) } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_processor_is_open(_self: CHandle) -> c_int { - unsafe { oakaudio::ffi::processor::oakaudio_processor_is_open(_self) } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_sync_estimate_envelope_offset( - reference: *const c_double, - reference_len: c_int, - candidate: *const c_double, - candidate_len: c_int, - reference_valid: *const u8, - candidate_valid: *const u8, - window_samples: u64, - max_offset_windows: i64, - out: *mut OffsetResult, -) -> c_int { - unsafe { - oakaudio::ffi::sync::oakaudio_sync_estimate_envelope_offset( - reference, - reference_len, - candidate, - candidate_len, - reference_valid, - candidate_valid, - window_samples, - max_offset_windows, - out, - ) - } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_sync_estimate_stretch_and_offset( - reference: *const c_double, - reference_len: c_int, - candidate: *const c_double, - candidate_len: c_int, - reference_valid: *const u8, - candidate_valid: *const u8, - window_samples: u64, - max_offset_windows: i64, - min_rate: c_double, - max_rate: c_double, - rate_step: c_double, - out: *mut StretchOffsetResult, -) -> c_int { - unsafe { - oakaudio::ffi::sync::oakaudio_sync_estimate_stretch_and_offset( - reference, - reference_len, - candidate, - candidate_len, - reference_valid, - candidate_valid, - window_samples, - max_offset_windows, - min_rate, - max_rate, - rate_step, - out, - ) - } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_sync_place_by_source_time( - reference: *const SourceClip, - candidate: *const SourceClip, - reference_timeline_in_num: i64, - reference_timeline_in_den: i64, - out_num: *mut i64, - out_den: *mut i64, - out_valid: *mut c_int, -) -> c_int { - unsafe { - oakaudio::ffi::sync::oakaudio_sync_place_by_source_time( - reference, - candidate, - reference_timeline_in_num, - reference_timeline_in_den, - out_num, - out_den, - out_valid, - ) - } -} - -/// Direct call into the `oakaudio` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakaudio_sync_place_by_waveform_offset( - reference_timeline_in_num: i64, - reference_timeline_in_den: i64, - candidate_offset_samples: i64, - sample_rate: c_int, - out_num: *mut i64, - out_den: *mut i64, - out_valid: *mut c_int, -) -> c_int { - unsafe { - oakaudio::ffi::sync::oakaudio_sync_place_by_waveform_offset( - reference_timeline_in_num, - reference_timeline_in_den, - candidate_offset_samples, - sample_rate, - out_num, - out_den, - out_valid, - ) - } -} diff --git a/crates/oakengine/src/bridge/codec.rs b/crates/oakengine/src/bridge/codec.rs deleted file mode 100644 index f165277a6..000000000 --- a/crates/oakengine/src/bridge/codec.rs +++ /dev/null @@ -1,242 +0,0 @@ -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! oakcodec C ABI bridge: direct Rust calls into the `oakcodec` crate. -//! -//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every -//! call below is a compile-time Rust call into `oakcodec`'s `ffi` (the -//! `#[no_mangle]` exports stay in the dylib for the external C ABI; -//! internal callers bypass them). Handles cross as the shared -//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"` -//! declaration (resolved at link time against the sibling crate in the -//! same dylib) are the host `oakcore_*` symbols and the encoding-params -//! C ABI POD crossings (the facade keeps its own POD mirrors there). - -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! oakcodec C ABI imports, mirroring the oakcodec crate's exports -//! (`src/codec/rust/src/ffi/{format,encoder}.rs`; headers -//! `include/codec/{format,encoder}.h`). Also carries the -//! `oakcodec_encoding_params` POD the facade's encoding-params handle -//! wraps, and the oakaudio recording-params pointer pass-through. - -use std::ffi::{c_char, c_int}; - -use crate::handle::CHandle; - -/// `include/codec/encoder.h` — the encoding-params POD, a complete mirror -/// `olive::EncodingParams` — single-lib unification: aliases the oakcodec -/// crate's POD (`oakcodec_encoding_params`, identical `#[repr(C)]` layout; -/// the engine's `OakEngineEncodingParams` handle is a heap box over exactly -/// this struct, so every engine getter/setter reads/writes a field and -/// `encoder_init`/recording can consume the pointer directly). -pub type EncodingParamsPOD = oakcodec::ffi::encoder::oakcodec_encoding_params; - -/// Zeroed encoding-params POD (all fields 0 / NUL). The codec crate's -/// struct has no zeroed constructor; this facade helper provides it. -pub fn zeroed_encoding_params() -> EncodingParamsPOD { - // All-C fields: all-zero is a valid value. - unsafe { std::mem::zeroed() } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoding_format_count() -> c_int { - unsafe { oakcodec::ffi::format::oakcodec_encoding_format_count() } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoding_format_name(format: c_int, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oakcodec::ffi::format::oakcodec_encoding_format_name(format, buf, buf_size) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoding_format_extension( - format: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oakcodec::ffi::format::oakcodec_encoding_format_extension(format, buf, buf_size) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoding_format_video_codec_count(format: c_int) -> c_int { - unsafe { oakcodec::ffi::format::oakcodec_encoding_format_video_codec_count(format) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoding_format_video_codec_at(format: c_int, index: c_int) -> c_int { - unsafe { oakcodec::ffi::format::oakcodec_encoding_format_video_codec_at(format, index) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoding_format_audio_codec_count(format: c_int) -> c_int { - unsafe { oakcodec::ffi::format::oakcodec_encoding_format_audio_codec_count(format) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoding_format_audio_codec_at(format: c_int, index: c_int) -> c_int { - unsafe { oakcodec::ffi::format::oakcodec_encoding_format_audio_codec_at(format, index) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoding_format_subtitle_codec_count(format: c_int) -> c_int { - unsafe { oakcodec::ffi::format::oakcodec_encoding_format_subtitle_codec_count(format) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoding_format_subtitle_codec_at(format: c_int, index: c_int) -> c_int { - unsafe { oakcodec::ffi::format::oakcodec_encoding_format_subtitle_codec_at(format, index) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoding_codec_name(codec: c_int, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oakcodec::ffi::format::oakcodec_encoding_codec_name(codec, buf, buf_size) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoding_codec_is_still_image(codec: c_int) -> c_int { - unsafe { oakcodec::ffi::format::oakcodec_encoding_codec_is_still_image(codec) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoding_codec_is_lossless(codec: c_int) -> c_int { - unsafe { oakcodec::ffi::format::oakcodec_encoding_codec_is_lossless(codec) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoding_pix_fmt_count(format: c_int, codec: c_int) -> c_int { - unsafe { oakcodec::ffi::format::oakcodec_encoding_pix_fmt_count(format, codec) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoding_pix_fmt_at( - format: c_int, - codec: c_int, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oakcodec::ffi::format::oakcodec_encoding_pix_fmt_at(format, codec, index, buf, buf_size) - } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoding_pix_fmt_index(codec: c_int, pix_fmt: *const c_char) -> c_int { - unsafe { oakcodec::ffi::format::oakcodec_encoding_pix_fmt_index(codec, pix_fmt) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoding_sample_format_count(format: c_int, codec: c_int) -> c_int { - unsafe { oakcodec::ffi::format::oakcodec_encoding_sample_format_count(format, codec) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoding_sample_format_at(format: c_int, codec: c_int, index: c_int) -> c_int { - unsafe { oakcodec::ffi::format::oakcodec_encoding_sample_format_at(format, codec, index) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoding_filename_contains_digit_placeholder(filename: *const c_char) -> c_int { - unsafe { - oakcodec::ffi::format::oakcodec_encoding_filename_contains_digit_placeholder(filename) - } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoding_image_sequence_digit_count(filename: *const c_char) -> c_int { - unsafe { oakcodec::ffi::format::oakcodec_encoding_image_sequence_digit_count(filename) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoding_filename_remove_digit_placeholder( - filename: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oakcodec::ffi::format::oakcodec_encoding_filename_remove_digit_placeholder( - filename, buf, buf_size, - ) - } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoding_generate_matrix( - method: c_int, - src_width: c_int, - src_height: c_int, - dst_width: c_int, - dst_height: c_int, - out_matrix: *mut f64, -) -> c_int { - unsafe { - oakcodec::ffi::encoder::oakcodec_encoding_generate_matrix( - method, src_width, src_height, dst_width, dst_height, out_matrix, - ) - } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -/// `oakcodec_encoder_init` — crosses the encoding-params C ABI POD -/// (`oakcodec_encoding_params`; the facade keeps its own POD mirror). -/// Kept as a link-time `extern "C"` declaration against the frozen module -/// C ABI. -pub fn oakcodec_encoder_init(params: *const EncodingParamsPOD) -> CHandle { - unsafe { oakcodec::ffi::encoder::oakcodec_encoder_init(params) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcodec_encoder_free(encoder: *mut CHandle) { - unsafe { oakcodec::ffi::encoder::oakcodec_encoder_free(encoder) } -} diff --git a/crates/oakengine/src/bridge/common.rs b/crates/oakengine/src/bridge/common.rs deleted file mode 100644 index ff0bf0026..000000000 --- a/crates/oakengine/src/bridge/common.rs +++ /dev/null @@ -1,889 +0,0 @@ -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! oakcommon C ABI bridge: direct Rust calls into the `oakcommon` crate. -//! -//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every -//! call below is a compile-time Rust call into `oakcommon`'s `ffi` (the -//! `#[no_mangle]` exports stay in the dylib for the external C ABI; -//! internal callers bypass them). Handles cross as the shared -//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"` -//! declaration (resolved at link time against the sibling crate in the -//! same dylib) are the host `oakcore_*` symbols and the encoding-params -//! C ABI POD crossings (the facade keeps its own POD mirrors there). - -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! oakcommon C ABI imports, mirroring the oakcommon crate's exports -//! (`src/common/rust/src/ffi.rs`; headers `include/common/*.h`). Only the -//! families the facade wraps: config, videoparams, colortransform, xml -//! reader/writer and the decibel helpers. - -use std::ffi::{c_char, c_int, c_void}; - -use crate::handle::CHandle; - -/// `include/common/config.h` — error handler callback. -pub type ConfigErrorHandler = Option< - unsafe extern "C" fn(title: *const c_char, message: *const c_char, userdata: *mut c_void), ->; - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_config_load() -> c_int { - unsafe { oakcommon::ffi::config::oakcommon_config_load() } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_config_save() -> c_int { - unsafe { oakcommon::ffi::config::oakcommon_config_save() } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_config_reset_defaults() -> c_int { - unsafe { oakcommon::ffi::config::oakcommon_config_reset_defaults() } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_config_set(group: *const c_char, key: *const c_char, value: *const c_char) { - unsafe { oakcommon::ffi::config::oakcommon_config_set(group, key, value) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_config_get( - group: *const c_char, - key: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oakcommon::ffi::config::oakcommon_config_get(group, key, buf, buf_size) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_config_get_int( - group: *const c_char, - key: *const c_char, - fallback: c_int, -) -> c_int { - unsafe { oakcommon::ffi::config::oakcommon_config_get_int(group, key, fallback) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_config_get_int64(group: *const c_char, key: *const c_char, fallback: i64) -> i64 { - unsafe { oakcommon::ffi::config::oakcommon_config_get_int64(group, key, fallback) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_config_get_double(group: *const c_char, key: *const c_char, fallback: f64) -> f64 { - unsafe { oakcommon::ffi::config::oakcommon_config_get_double(group, key, fallback) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_config_get_bool( - group: *const c_char, - key: *const c_char, - fallback: c_int, -) -> c_int { - unsafe { oakcommon::ffi::config::oakcommon_config_get_bool(group, key, fallback) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_config_set_int(group: *const c_char, key: *const c_char, value: c_int) { - unsafe { oakcommon::ffi::config::oakcommon_config_set_int(group, key, value) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_config_set_int64(group: *const c_char, key: *const c_char, value: i64) { - unsafe { oakcommon::ffi::config::oakcommon_config_set_int64(group, key, value) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_config_set_double(group: *const c_char, key: *const c_char, value: f64) { - unsafe { oakcommon::ffi::config::oakcommon_config_set_double(group, key, value) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_config_set_bool(group: *const c_char, key: *const c_char, value: c_int) { - unsafe { oakcommon::ffi::config::oakcommon_config_set_bool(group, key, value) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_config_entry_type(group: *const c_char, key: *const c_char) -> c_int { - unsafe { oakcommon::ffi::config::oakcommon_config_entry_type(group, key) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_config_set_error_handler( - handler: ConfigErrorHandler, - userdata: *mut c_void, -) -> c_int { - unsafe { oakcommon::ffi::config::oakcommon_config_set_error_handler(handler, userdata) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_init() -> CHandle { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_init() } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_init_basic( - width: c_int, - height: c_int, - pixel_format: c_int, - nb_channels: c_int, - pixel_aspect_num: c_int, - pixel_aspect_den: c_int, - interlacing: c_int, - divider: c_int, -) -> CHandle { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_init_basic( - width, - height, - pixel_format, - nb_channels, - pixel_aspect_num, - pixel_aspect_den, - interlacing, - divider, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_init_with_time_base( - width: c_int, - height: c_int, - time_base_num: c_int, - time_base_den: c_int, - pixel_format: c_int, - nb_channels: c_int, - pixel_aspect_num: c_int, - pixel_aspect_den: c_int, - interlacing: c_int, - divider: c_int, -) -> CHandle { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_init_with_time_base( - width, - height, - time_base_num, - time_base_den, - pixel_format, - nb_channels, - pixel_aspect_num, - pixel_aspect_den, - interlacing, - divider, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_free(params: *mut CHandle) { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_free(params) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_width(params: CHandle, width: *mut c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_width(params, width) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_set_width(params: CHandle, width: c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_width(params, width) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_height(params: CHandle, height: *mut c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_height(params, height) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_set_height(params: CHandle, height: c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_height(params, height) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_time_base( - params: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_get_time_base( - params, - numerator, - denominator, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_set_time_base( - params: CHandle, - numerator: c_int, - denominator: c_int, -) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_set_time_base( - params, - numerator, - denominator, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_frame_rate( - params: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_get_frame_rate( - params, - numerator, - denominator, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_set_frame_rate( - params: CHandle, - numerator: c_int, - denominator: c_int, -) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_set_frame_rate( - params, - numerator, - denominator, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_pixel_aspect_ratio( - params: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_get_pixel_aspect_ratio( - params, - numerator, - denominator, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_set_pixel_aspect_ratio( - params: CHandle, - numerator: c_int, - denominator: c_int, -) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_set_pixel_aspect_ratio( - params, - numerator, - denominator, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_format(params: CHandle, format: *mut c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_format(params, format) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_set_format(params: CHandle, format: c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_format(params, format) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_interlacing(params: CHandle, interlacing: *mut c_int) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_get_interlacing(params, interlacing) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_set_interlacing(params: CHandle, interlacing: c_int) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_set_interlacing(params, interlacing) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_divider(params: CHandle, divider: *mut c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_divider(params, divider) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_set_divider(params: CHandle, divider: c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_divider(params, divider) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_video_type(params: CHandle, type_: *mut c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_video_type(params, type_) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_set_video_type(params: CHandle, type_: c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_video_type(params, type_) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_premultiplied_alpha( - params: CHandle, - premultiplied: *mut c_int, -) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_get_premultiplied_alpha( - params, - premultiplied, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_set_premultiplied_alpha( - params: CHandle, - premultiplied: c_int, -) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_set_premultiplied_alpha( - params, - premultiplied, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_color_range(params: CHandle, color_range: *mut c_int) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_get_color_range(params, color_range) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_set_color_range(params: CHandle, color_range: c_int) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_set_color_range(params, color_range) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_is_valid(params: CHandle, valid: *mut c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_is_valid(params, valid) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_effective_width(params: CHandle, width: *mut c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_effective_width(params, width) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_effective_height(params: CHandle, height: *mut c_int) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_get_effective_height(params, height) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_bytes_per_pixel(params: CHandle, bytes: *mut c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_bytes_per_pixel(params, bytes) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_equals(a: CHandle, b: CHandle, out_equal: *mut c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_equals(a, b, out_equal) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_format_is_float(pixel_format: c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_format_is_float(pixel_format) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_format_name( - pixel_format: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_get_format_name( - pixel_format, - buf, - buf_size, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_frame_rate_to_string( - numerator: c_int, - denominator: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_frame_rate_to_string( - numerator, - denominator, - buf, - buf_size, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_name_for_divider( - divider: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_get_name_for_divider( - divider, buf, buf_size, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_scaled_dimension(dimension: c_int, divider: c_int) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_get_scaled_dimension(dimension, divider) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_generate_auto_divider(width: i64, height: i64) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_generate_auto_divider(width, height) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_divider_for_target_resolution( - src_width: c_int, - src_height: c_int, - target_width: c_int, - target_height: c_int, -) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_get_divider_for_target_resolution( - src_width, - src_height, - target_width, - target_height, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_bytes_per_channel_for_format(pixel_format: c_int) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_get_bytes_per_channel_for_format( - pixel_format, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_bytes_per_pixel_for_format( - pixel_format: c_int, - channels: c_int, -) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_get_bytes_per_pixel_for_format( - pixel_format, - channels, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_static_get_bytes_per_pixel( - pixel_format: c_int, - channels: c_int, -) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_static_get_bytes_per_pixel( - pixel_format, - channels, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_buffer_size(params: CHandle, size: *mut c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_buffer_size(params, size) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_videoparams_get_time_in_timebase_units( - params: CHandle, - time_num: c_int, - time_den: c_int, - timestamp: *mut i64, -) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_get_time_in_timebase_units( - params, time_num, time_den, timestamp, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_colortransform_init_output(output: *const c_char) -> CHandle { - unsafe { oakcommon::ffi::colortransform::oakcommon_colortransform_init_output(output) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_colortransform_init_display( - display: *const c_char, - view: *const c_char, - look: *const c_char, -) -> CHandle { - unsafe { - oakcommon::ffi::colortransform::oakcommon_colortransform_init_display(display, view, look) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_colortransform_free(transform: *mut CHandle) { - unsafe { oakcommon::ffi::colortransform::oakcommon_colortransform_free(transform) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_colortransform_is_display(transform: CHandle) -> c_int { - unsafe { oakcommon::ffi::colortransform::oakcommon_colortransform_is_display(transform) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_colortransform_get_display( - transform: CHandle, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oakcommon::ffi::colortransform::oakcommon_colortransform_get_display( - transform, buf, buf_size, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_colortransform_get_output( - transform: CHandle, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oakcommon::ffi::colortransform::oakcommon_colortransform_get_output( - transform, buf, buf_size, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_colortransform_get_view( - transform: CHandle, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oakcommon::ffi::colortransform::oakcommon_colortransform_get_view(transform, buf, buf_size) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_colortransform_get_look( - transform: CHandle, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oakcommon::ffi::colortransform::oakcommon_colortransform_get_look(transform, buf, buf_size) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_xml_reader_init(data: *const c_char) -> CHandle { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_init(data) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_xml_reader_free(reader: *mut CHandle) { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_free(reader) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_xml_reader_read_next_start_element(reader: CHandle, found: *mut c_int) -> c_int { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_read_next_start_element(reader, found) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_xml_reader_name(reader: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_name(reader, buf, buf_size) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_xml_reader_read_element_text( - reader: CHandle, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oakcommon::ffi::xmlutils::oakcommon_xml_reader_read_element_text(reader, buf, buf_size) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_xml_reader_skip_current_element(reader: CHandle) -> c_int { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_skip_current_element(reader) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_xml_reader_attribute_count(reader: CHandle, count: *mut c_int) -> c_int { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_count(reader, count) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_xml_reader_attribute_name( - reader: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_name(reader, index, buf, buf_size) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_xml_reader_attribute_value( - reader: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_value(reader, index, buf, buf_size) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_xml_reader_has_error(reader: CHandle, has_error: *mut c_int) -> c_int { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_has_error(reader, has_error) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_xml_writer_init() -> CHandle { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_init() } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_xml_writer_free(writer: *mut CHandle) { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_free(writer) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_xml_writer_write_start_element(writer: CHandle, name: *const c_char) -> c_int { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_start_element(writer, name) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_xml_writer_write_attribute( - writer: CHandle, - name: *const c_char, - value: *const c_char, -) -> c_int { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_attribute(writer, name, value) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_xml_writer_write_characters(writer: CHandle, text: *const c_char) -> c_int { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_characters(writer, text) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_xml_writer_write_text_element( - writer: CHandle, - name: *const c_char, - text: *const c_char, -) -> c_int { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_text_element(writer, name, text) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_xml_writer_write_end_element(writer: CHandle) -> c_int { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_end_element(writer) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_xml_writer_write_end_document(writer: CHandle) -> c_int { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_end_document(writer) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_xml_writer_output(writer: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_output(writer, buf, buf_size) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_decibel_from_linear(linear: f64, out_db: *mut f64) -> c_int { - unsafe { oakcommon::ffi::misc::oakcommon_decibel_from_linear(linear, out_db) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_decibel_to_linear(db: f64, out_linear: *mut f64) -> c_int { - unsafe { oakcommon::ffi::misc::oakcommon_decibel_to_linear(db, out_linear) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_decibel_from_logarithmic(logarithmic: f64, out_db: *mut f64) -> c_int { - unsafe { oakcommon::ffi::misc::oakcommon_decibel_from_logarithmic(logarithmic, out_db) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_decibel_to_logarithmic(db: f64, out_logarithmic: *mut f64) -> c_int { - unsafe { oakcommon::ffi::misc::oakcommon_decibel_to_logarithmic(db, out_logarithmic) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_decibel_linear_to_logarithmic(linear: f64, out_logarithmic: *mut f64) -> c_int { - unsafe { - oakcommon::ffi::misc::oakcommon_decibel_linear_to_logarithmic(linear, out_logarithmic) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakcommon_decibel_logarithmic_to_linear(logarithmic: f64, out_linear: *mut f64) -> c_int { - unsafe { - oakcommon::ffi::misc::oakcommon_decibel_logarithmic_to_linear(logarithmic, out_linear) - } -} diff --git a/crates/oakengine/src/bridge/mod.rs b/crates/oakengine/src/bridge/mod.rs deleted file mode 100644 index 52494fa96..000000000 --- a/crates/oakengine/src/bridge/mod.rs +++ /dev/null @@ -1,43 +0,0 @@ -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! C ABI imports from the oak modules, one submodule per module crate. -//! -//! The facade consumes the module C ABIs (`include//*.h`) purely as -//! `extern "C"` imports — it never links the module crates at build time. -//! At the final app link the symbols resolve against the module shared -//! libraries; `cargo test` resolves them against the module crates' rlibs -//! (dev-dependencies, see Cargo.toml). -//! -//! **Signatures are declared from the module crates' actual `#[no_mangle]` -//! exports** (their `src/*/ffi*` modules), not from memory of the include -//! headers — module bridges in sibling crates have drifted from the real -//! ABI before. Every handle crosses the boundary as [`crate::handle::CHandle`] -//! (structurally identical to every `Oak` value handle). -//! -//! Only functions the module crates actually implement are declared here; -//! engine functions whose backing is still C++-only are facade stubs (see -//! the area modules) and never reach this module. - -pub mod audio; -pub mod codec; -pub mod common; -pub mod node; -pub mod plugin; -pub mod render; -pub mod task; -pub mod timeline; -pub mod undo; diff --git a/crates/oakengine/src/bridge/node.rs b/crates/oakengine/src/bridge/node.rs deleted file mode 100644 index 1a843ee06..000000000 --- a/crates/oakengine/src/bridge/node.rs +++ /dev/null @@ -1,3166 +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 . - -//! oaknode C ABI bridge: direct Rust calls into the `oaknode` crate. -//! -//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every -//! call below is a compile-time Rust call into `oaknode`'s `ffi` (the -//! `#[no_mangle]` exports stay in the dylib for the external C ABI; -//! internal callers bypass them). Handles cross as the shared -//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"` -//! declaration (resolved at link time against the sibling crate in the -//! same dylib) are the host `oakcore_*` symbols and the encoding-params -//! C ABI POD crossings (the facade keeps its own POD mirrors there). - -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! oaknode C ABI imports, mirroring the oaknode crate's exports -//! (`src/node/rust/src/ffi.rs`; headers `include/node/*.h`). -//! -//! Every handle crosses as [`crate::handle::CHandle`] (structurally -//! identical to every `OakNode*` value handle). String getters are -//! two-stage: they report the required size **including** the terminating -//! NUL; the facade converts with [`crate::handle::string_result`]. All -//! module error codes (-30001..) pass through untranslated. - -use std::ffi::{c_char, c_int, c_void}; - -use crate::handle::CHandle; - -// `include/node/node.h` — POD parameter value mirror of `oaknode_value` -// (defined in [`crate::node`]). The facade hands the engine's -// `oak_node_value` straight to the module (the two structs are -// layout-identical). - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_init() -> CHandle { - unsafe { oaknode::ffi::project::oaknode_project_init() } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_free(project: *mut CHandle) { - unsafe { oaknode::ffi::project::oaknode_project_free(project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_initialize(project: CHandle) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_initialize(project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_clear(project: CHandle) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_clear(project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_root(project: CHandle) -> CHandle { - unsafe { oaknode::ffi::project::oaknode_project_root(project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_name(project: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_name(project, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_filename(project: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_filename(project, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_pretty_filename( - project: CHandle, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_pretty_filename(project, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_set_filename(project: CHandle, filename: *const c_char) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_set_filename(project, filename) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_is_modified(project: CHandle) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_is_modified(project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_set_modified(project: CHandle, modified: c_int) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_set_modified(project, modified) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_is_new(project: CHandle) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_is_new(project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_cache_path(project: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_cache_path(project, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_copy_settings(dst: CHandle, src: CHandle) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_copy_settings(dst, src) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_get_cache_location_setting(project: CHandle) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_get_cache_location_setting(project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_set_cache_location_setting(project: CHandle, setting: c_int) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_set_cache_location_setting(project, setting) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_get_custom_cache_path( - project: CHandle, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_get_custom_cache_path(project, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_set_custom_cache_path(project: CHandle, path: *const c_char) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_set_custom_cache_path(project, path) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_get_uuid(project: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_get_uuid(project, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_add_node(project: CHandle, node: CHandle) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_add_node(project, node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_remove_node(project: CHandle, node: CHandle) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_remove_node(project, node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_node_count(project: CHandle) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_node_count(project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_project_node_at(project: CHandle, index: c_int) -> CHandle { - unsafe { oaknode::ffi::project::oaknode_project_node_at(project, index) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_debug_alive_count() -> c_int { - unsafe { oaknode::ffi::node::oaknode_debug_alive_count() } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_get_id(node: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_get_id(node, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_get_name(node: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_get_name(node, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_get_label(node: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_get_label(node, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_set_label(node: CHandle, label: *const c_char) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_set_label(node, label) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_set_label_undoable( - node: CHandle, - label: *const c_char, - out_command: *mut CHandle, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_set_label_undoable(node, label, out_command) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_get_override_color(node: CHandle, out_value: *mut c_int) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_get_override_color(node, out_value) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_set_override_color(node: CHandle, index: c_int) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_set_override_color(node, index) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_set_override_color_undoable( - node: CHandle, - index: c_int, - out_command: *mut CHandle, -) -> c_int { - unsafe { - oaknode::ffi::node::oaknode_node_set_override_color_undoable(node, index, out_command) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_is_enabled(node: CHandle, out_value: *mut c_int) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_is_enabled(node, out_value) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_set_enabled(node: CHandle, enabled: c_int) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_set_enabled(node, enabled) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_set_enabled_undoable( - node: CHandle, - enabled: c_int, - out_command: *mut CHandle, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_set_enabled_undoable(node, enabled, out_command) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_get_effect_input(node: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_get_effect_input(node, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_get_flags(node: CHandle) -> u64 { - unsafe { oaknode::ffi::node::oaknode_node_get_flags(node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_input_count(node: CHandle, out_count: *mut c_int) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_input_count(node, out_count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_input_id( - node: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_input_id(node, index, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_input_get_type( - node: CHandle, - input_id: *const c_char, - out_type: *mut c_int, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_input_get_type(node, input_id, out_type) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_input_is_connected( - node: CHandle, - input_id: *const c_char, - out_value: *mut c_int, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_input_is_connected(node, input_id, out_value) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_input_is_connectable( - node: CHandle, - input_id: *const c_char, - out_value: *mut c_int, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_input_is_connectable(node, input_id, out_value) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_get_input_name( - node: CHandle, - input_id: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_get_input_name(node, input_id, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_input_get_connected_node( - node: CHandle, - input_id: *const c_char, - out_node: *mut CHandle, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_input_get_connected_node(node, input_id, out_node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_get_input( - node: CHandle, - input_id: *const c_char, - out: *mut crate::node::OakNodeValue, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_get_input(node, input_id, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_set_input( - node: CHandle, - input_id: *const c_char, - v: *const crate::node::OakNodeValue, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_set_input(node, input_id, v) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { oaknode::ffi::node::oaknode_node_set_input_undoable(node, input_id, v, out_command) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_get_input_string( - node: CHandle, - input_id: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_get_input_string(node, input_id, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_set_input_string( - node: CHandle, - input_id: *const c_char, - value: *const c_char, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_set_input_string(node, input_id, value) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::node::oaknode_node_set_input_string_undoable( - node, - input_id, - value, - out_command, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_connect( - output_node: CHandle, - input_node: CHandle, - input_id: *const c_char, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_connect(output_node, input_node, input_id) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_connect_undoable( - output_node: CHandle, - input_node: CHandle, - input_id: *const c_char, - out_command: *mut CHandle, -) -> c_int { - unsafe { - oaknode::ffi::node::oaknode_node_connect_undoable( - output_node, - input_node, - input_id, - out_command, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_disconnect(input_node: CHandle, input_id: *const c_char) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_disconnect(input_node, input_id) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_disconnect_undoable( - input_node: CHandle, - input_id: *const c_char, - out_command: *mut CHandle, -) -> c_int { - unsafe { - oaknode::ffi::node::oaknode_node_disconnect_undoable(input_node, input_id, out_command) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_output_connection_count(node: CHandle, out_count: *mut c_int) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_output_connection_count(node, out_count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_output_connection_node_at( - node: CHandle, - index: c_int, - out_node: *mut CHandle, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_output_connection_node_at(node, index, out_node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_output_connection_input_id_at( - node: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oaknode::ffi::node::oaknode_node_output_connection_input_id_at(node, index, buf, buf_size) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_output_connection_element_at( - node: CHandle, - index: c_int, - out_element: *mut c_int, -) -> c_int { - unsafe { - oaknode::ffi::node::oaknode_node_output_connection_element_at(node, index, out_element) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_link(a: CHandle, b: CHandle, out_linked: *mut c_int) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_link(a, b, out_linked) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_unlink(a: CHandle, b: CHandle, out_unlinked: *mut c_int) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_unlink(a, b, out_unlinked) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_link_undoable( - a: CHandle, - b: CHandle, - link: c_int, - out_command: *mut CHandle, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_link_undoable(a, b, link, out_command) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_are_linked(a: CHandle, b: CHandle, out_value: *mut c_int) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_are_linked(a, b, out_value) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_link_count(node: CHandle, out_count: *mut c_int) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_link_count(node, out_count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_link_at(node: CHandle, index: c_int, out_node: *mut CHandle) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_link_at(node, index, out_node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_context_count(node: CHandle, out_count: *mut c_int) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_context_count(node, out_count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_context_node_at(node: CHandle, index: c_int, out_node: *mut CHandle) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_context_node_at(node, index, out_node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::node::oaknode_node_get_context_position( - node, - context, - out_x, - out_y, - out_expanded, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_set_context_position( - node: CHandle, - context: CHandle, - x: f64, - y: f64, - expanded: c_int, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_set_context_position(node, context, x, y, expanded) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::node::oaknode_node_set_context_position_undoable( - node, - context, - x, - y, - expanded, - out_command, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_remove_from_context(node: CHandle, context: CHandle) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_remove_from_context(node, context) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_create_copy(node: CHandle) -> CHandle { - unsafe { oaknode::ffi::node::oaknode_node_create_copy(node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_copy_in_graph(node: CHandle, out_command: *mut CHandle) -> CHandle { - unsafe { oaknode::ffi::node::oaknode_node_copy_in_graph(node, out_command) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_get_project(node: CHandle, out: *mut CHandle) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_get_project(node, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_input_array_insert( - node: CHandle, - input_id: *const c_char, - index: c_int, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_input_array_insert(node, input_id, index) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_input_array_remove( - node: CHandle, - input_id: *const c_char, - index: c_int, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_input_array_remove(node, input_id, index) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_connect_element( - output_node: CHandle, - input_node: CHandle, - input_id: *const c_char, - element: c_int, -) -> c_int { - unsafe { - oaknode::ffi::node::oaknode_node_connect_element(output_node, input_node, input_id, element) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_disconnect_element( - input_node: CHandle, - input_id: *const c_char, - element: c_int, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_disconnect_element(input_node, input_id, element) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_command_create_add_node(graph: CHandle, node: CHandle) -> CHandle { - unsafe { oaknode::ffi::node::oaknode_command_create_add_node(graph, node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_command_create_set_position_recursive( - node: CHandle, - context: CHandle, - x: f64, - y: f64, -) -> CHandle { - unsafe { - oaknode::ffi::node::oaknode_command_create_set_position_recursive(node, context, x, y) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_get_markers(node: CHandle, out: *mut CHandle) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_get_markers(node, out as *mut std::ffi::c_void) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_get_work_area(node: CHandle, out: *mut CHandle) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_get_work_area(node, out as *mut std::ffi::c_void) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_get_video_frame_cache(node: CHandle, out: *mut CHandle) -> c_int { - unsafe { - oaknode::ffi::node::oaknode_node_get_video_frame_cache(node, out as *mut std::ffi::c_void) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_copy_inputs(dst: CHandle, src: CHandle, include_connections: c_int) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_copy_inputs(dst, src, include_connections) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::node::oaknode_node_set_value_hint_track( - node, - input_id, - track_type, - track_index, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_viewer_set_video_params(viewer: CHandle, params: *const CHandle) -> c_int { - unsafe { - oaknode::ffi::node::oaknode_viewer_set_video_params( - viewer, - params as *const std::ffi::c_void, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_viewer_set_audio_params(viewer: CHandle, params: *const c_void) -> c_int { - unsafe { oaknode::ffi::node::oaknode_viewer_set_audio_params(viewer, params) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_find_input_footage(node: CHandle, out: *mut CHandle) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_find_input_footage(node, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::node::oaknode_node_get_input_at_time(node, input_id, time_num, time_den, out) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::node::oaknode_node_set_input_at_time_undoable( - node, - input_id, - time_num, - time_den, - v, - track, - out_command, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_identity(node: CHandle) -> usize { - unsafe { oaknode::ffi::node::oaknode_node_identity(node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::node::oaknode_node_set_input_at_time_into( - node, - input_id, - time_num, - time_den, - v, - track, - multi_command, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_command_create_remove_node(node: CHandle) -> CHandle { - unsafe { oaknode::ffi::node::oaknode_command_create_remove_node(node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_node_free(node: *mut CHandle) { - unsafe { oaknode::ffi::node::oaknode_node_free(node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_factory_initialize() -> c_int { - unsafe { oaknode::ffi::factory::oaknode_factory_initialize() } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_factory_destroy() { - unsafe { oaknode::ffi::factory::oaknode_factory_destroy() } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_factory_id_count(out_count: *mut c_int) -> c_int { - unsafe { oaknode::ffi::factory::oaknode_factory_id_count(out_count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_factory_id_at(index: c_int, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oaknode::ffi::factory::oaknode_factory_id_at(index, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_factory_name_from_id( - type_id: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaknode::ffi::factory::oaknode_factory_name_from_id(type_id, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_factory_create_from_id(type_id: *const c_char) -> CHandle { - unsafe { oaknode::ffi::factory::oaknode_factory_create_from_id(type_id) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_factory_node_at(index: c_int, out_node: *mut CHandle) -> c_int { - unsafe { oaknode::ffi::factory::oaknode_factory_node_at(index, out_node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_folder_create(project: CHandle) -> CHandle { - unsafe { oaknode::ffi::folder::oaknode_folder_create(project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_folder_child_count(folder: CHandle) -> c_int { - unsafe { oaknode::ffi::folder::oaknode_folder_child_count(folder) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_folder_child_at(folder: CHandle, index: c_int) -> CHandle { - unsafe { oaknode::ffi::folder::oaknode_folder_child_at(folder, index) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_folder_add_child(folder: CHandle, child: CHandle) -> c_int { - unsafe { oaknode::ffi::folder::oaknode_folder_add_child(folder, child) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_folder_as_node(folder: CHandle) -> CHandle { - unsafe { oaknode::ffi::folder::oaknode_folder_as_node(folder) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_command_create_folder_add_child(folder: CHandle, child: CHandle) -> CHandle { - unsafe { oaknode::ffi::folder::oaknode_command_create_folder_add_child(folder, child) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_folder_remove_child(folder: CHandle, child: CHandle) -> c_int { - unsafe { oaknode::ffi::folder::oaknode_folder_remove_child(folder, child) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_folder_move_children( - nodes: *const CHandle, - count: c_int, - dest_folder: CHandle, -) -> c_int { - unsafe { oaknode::ffi::folder::oaknode_folder_move_children(nodes, count, dest_folder) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_folder_has_child_recursive(folder: CHandle, child: CHandle) -> c_int { - unsafe { oaknode::ffi::folder::oaknode_folder_has_child_recursive(folder, child) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_folder_index_of_child(folder: CHandle, child: CHandle) -> c_int { - unsafe { oaknode::ffi::folder::oaknode_folder_index_of_child(folder, child) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_folder_parent_of(node: CHandle) -> CHandle { - unsafe { oaknode::ffi::folder::oaknode_folder_parent_of(node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_create(project: CHandle, filename: *const c_char) -> CHandle { - unsafe { oaknode::ffi::footage::oaknode_footage_create(project, filename) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_as_node(footage: CHandle) -> CHandle { - unsafe { oaknode::ffi::footage::oaknode_footage_as_node(footage) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_filename(footage: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_filename(footage, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_set_filename(footage: CHandle, filename: *const c_char) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_set_filename(footage, filename) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_is_valid(footage: CHandle) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_is_valid(footage) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_timestamp(footage: CHandle, out_timestamp: *mut i64) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_timestamp(footage, out_timestamp) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_set_timestamp(footage: CHandle, timestamp: i64) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_set_timestamp(footage, timestamp) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_decoder(footage: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_decoder(footage, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_total_stream_count(footage: CHandle) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_total_stream_count(footage) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_video_stream_count(footage: CHandle) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_video_stream_count(footage) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_audio_stream_count(footage: CHandle) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_audio_stream_count(footage) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_subtitle_stream_count(footage: CHandle) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_subtitle_stream_count(footage) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_duration( - footage: CHandle, - out_numerator: *mut c_int, - out_denominator: *mut c_int, -) -> c_int { - unsafe { - oaknode::ffi::footage::oaknode_footage_duration(footage, out_numerator, out_denominator) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_proxy_enabled(footage: CHandle) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_proxy_enabled(footage) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_set_proxy_enabled(footage: CHandle, enabled: c_int) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_set_proxy_enabled(footage, enabled) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_proxy_path(footage: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_proxy_path(footage, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_proxy_state(footage: CHandle) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_proxy_state(footage) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::footage::oaknode_footage_set_proxy( - footage, - path, - state, - video_stream_index, - preset_version, - enabled, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_clear_proxy(footage: CHandle) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_clear_proxy(footage) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_get_video_params( - footage: CHandle, - index: c_int, - out: *mut CHandle, -) -> c_int { - unsafe { - oaknode::ffi::footage::oaknode_footage_get_video_params( - footage, - index, - out as *mut std::ffi::c_void, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_set_video_params( - footage: CHandle, - index: c_int, - params: *const CHandle, -) -> c_int { - unsafe { - oaknode::ffi::footage::oaknode_footage_set_video_params( - footage, - index, - params as *const std::ffi::c_void, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_get_video_length( - footage: CHandle, - out_num: *mut i64, - out_den: *mut i64, -) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_get_video_length(footage, out_num, out_den) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_footage_set_cancel_atom(footage: CHandle, atom: CHandle) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_set_cancel_atom(footage, atom) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_group_create() -> CHandle { - unsafe { oaknode::ffi::group::oaknode_group_create() } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_group_cast(node: CHandle) -> CHandle { - unsafe { oaknode::ffi::group::oaknode_group_cast(node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_group_free(group: *mut CHandle) { - unsafe { oaknode::ffi::group::oaknode_group_free(group) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::group::oaknode_group_add_input_passthrough( - group, node, input_id, element, buf, buf_size, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::group::oaknode_group_add_input_passthrough_undoable( - group, - node, - input_id, - element, - out_command, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_group_remove_input_passthrough( - group: CHandle, - node: CHandle, - input_id: *const c_char, - element: c_int, -) -> c_int { - unsafe { - oaknode::ffi::group::oaknode_group_remove_input_passthrough(group, node, input_id, element) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_group_passthrough_count(group: CHandle, out_count: *mut c_int) -> c_int { - unsafe { oaknode::ffi::group::oaknode_group_passthrough_count(group, out_count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_group_passthrough_id_at( - group: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaknode::ffi::group::oaknode_group_passthrough_id_at(group, index, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::group::oaknode_group_passthrough_input_at( - group, - index, - out_node, - buf, - buf_size, - out_element, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_group_get_output_passthrough(group: CHandle, out_node: *mut CHandle) -> c_int { - unsafe { oaknode::ffi::group::oaknode_group_get_output_passthrough(group, out_node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_group_set_output_passthrough(group: CHandle, node: CHandle) -> c_int { - unsafe { oaknode::ffi::group::oaknode_group_set_output_passthrough(group, node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_group_set_output_passthrough_undoable( - group: CHandle, - node: CHandle, - out_command: *mut CHandle, -) -> c_int { - unsafe { - oaknode::ffi::group::oaknode_group_set_output_passthrough_undoable(group, node, out_command) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::group::oaknode_group_resolve_input( - node, - input_id, - element, - out_node, - buf, - buf_size, - out_element, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::keyframe::oaknode_keyframe_create( - time_num, - time_den, - value, - type_, - track, - element, - input_id, - parent_or_null, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_free(keyframe: *mut CHandle) { - unsafe { oaknode::ffi::keyframe::oaknode_keyframe_free(keyframe) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_get_time(keyframe: CHandle, out_num: *mut i64, out_den: *mut i64) -> c_int { - unsafe { oaknode::ffi::keyframe::oaknode_keyframe_get_time(keyframe, out_num, out_den) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_set_time(keyframe: CHandle, time_num: i64, time_den: i64) -> c_int { - unsafe { oaknode::ffi::keyframe::oaknode_keyframe_set_time(keyframe, time_num, time_den) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_set_time_undoable( - keyframe: CHandle, - time_num: i64, - time_den: i64, - out_command: *mut CHandle, -) -> c_int { - unsafe { - oaknode::ffi::keyframe::oaknode_keyframe_set_time_undoable( - keyframe, - time_num, - time_den, - out_command, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_get_value(keyframe: CHandle, out: *mut crate::node::OakNodeValue) -> c_int { - unsafe { oaknode::ffi::keyframe::oaknode_keyframe_get_value(keyframe, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_set_value(keyframe: CHandle, v: *const crate::node::OakNodeValue) -> c_int { - unsafe { oaknode::ffi::keyframe::oaknode_keyframe_set_value(keyframe, v) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_set_value_undoable( - keyframe: CHandle, - v: *const crate::node::OakNodeValue, - out_command: *mut CHandle, -) -> c_int { - unsafe { oaknode::ffi::keyframe::oaknode_keyframe_set_value_undoable(keyframe, v, out_command) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_get_value_string( - keyframe: CHandle, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaknode::ffi::keyframe::oaknode_keyframe_get_value_string(keyframe, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_set_value_string(keyframe: CHandle, value: *const c_char) -> c_int { - unsafe { oaknode::ffi::keyframe::oaknode_keyframe_set_value_string(keyframe, value) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_set_value_string_undoable( - keyframe: CHandle, - value: *const c_char, - out_command: *mut CHandle, -) -> c_int { - unsafe { - oaknode::ffi::keyframe::oaknode_keyframe_set_value_string_undoable( - keyframe, - value, - out_command, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_get_type(keyframe: CHandle, out_type: *mut c_int) -> c_int { - unsafe { oaknode::ffi::keyframe::oaknode_keyframe_get_type(keyframe, out_type) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_set_type(keyframe: CHandle, type_: c_int) -> c_int { - unsafe { oaknode::ffi::keyframe::oaknode_keyframe_set_type(keyframe, type_) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_set_type_undoable( - keyframe: CHandle, - type_: c_int, - out_command: *mut CHandle, -) -> c_int { - unsafe { - oaknode::ffi::keyframe::oaknode_keyframe_set_type_undoable(keyframe, type_, out_command) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_get_bezier_control( - keyframe: CHandle, - handle: c_int, - out_x: *mut f64, - out_y: *mut f64, -) -> c_int { - unsafe { - oaknode::ffi::keyframe::oaknode_keyframe_get_bezier_control(keyframe, handle, out_x, out_y) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_set_bezier_control( - keyframe: CHandle, - handle: c_int, - x: f64, - y: f64, -) -> c_int { - unsafe { oaknode::ffi::keyframe::oaknode_keyframe_set_bezier_control(keyframe, handle, x, y) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_set_bezier_control_undoable( - keyframe: CHandle, - handle: c_int, - x: f64, - y: f64, - out_command: *mut CHandle, -) -> c_int { - unsafe { - oaknode::ffi::keyframe::oaknode_keyframe_set_bezier_control_undoable( - keyframe, - handle, - x, - y, - out_command, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_get_track(keyframe: CHandle, out_track: *mut c_int) -> c_int { - unsafe { oaknode::ffi::keyframe::oaknode_keyframe_get_track(keyframe, out_track) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_get_element(keyframe: CHandle, out_element: *mut c_int) -> c_int { - unsafe { oaknode::ffi::keyframe::oaknode_keyframe_get_element(keyframe, out_element) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_get_input(keyframe: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oaknode::ffi::keyframe::oaknode_keyframe_get_input(keyframe, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_get_parent(keyframe: CHandle, out_node: *mut CHandle) -> c_int { - unsafe { oaknode::ffi::keyframe::oaknode_keyframe_get_parent(keyframe, out_node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_get_valid_bezier_control( - keyframe: CHandle, - handle: c_int, - out_x: *mut f64, - out_y: *mut f64, -) -> c_int { - unsafe { - oaknode::ffi::keyframe::oaknode_keyframe_get_valid_bezier_control( - keyframe, handle, out_x, out_y, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_opposing_bezier_type(type_: c_int) -> c_int { - unsafe { oaknode::ffi::keyframe::oaknode_keyframe_opposing_bezier_type(type_) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_compute_paste_value( - target_node: CHandle, - keyframe: CHandle, - out: *mut crate::node::OakNodeValue, -) -> c_int { - unsafe { - oaknode::ffi::keyframe::oaknode_keyframe_compute_paste_value(target_node, keyframe, out) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_keyframe_has_sibling_at_time( - keyframe: CHandle, - time_num: i64, - time_den: i64, - out_value: *mut c_int, -) -> c_int { - unsafe { - oaknode::ffi::keyframe::oaknode_keyframe_has_sibling_at_time( - keyframe, time_num, time_den, out_value, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_dragger_create( - node: CHandle, - input_id: *const c_char, - element: c_int, - track: c_int, -) -> CHandle { - unsafe { oaknode::ffi::dragger::oaknode_dragger_create(node, input_id, element, track) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_dragger_start( - dragger: CHandle, - time_num: i64, - time_den: i64, - track: c_int, - insert_on_all_tracks: c_int, -) -> c_int { - unsafe { - oaknode::ffi::dragger::oaknode_dragger_start( - dragger, - time_num, - time_den, - track, - insert_on_all_tracks, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_dragger_drag(dragger: CHandle, value: *const crate::node::OakNodeValue) -> c_int { - unsafe { oaknode::ffi::dragger::oaknode_dragger_drag(dragger, value) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_dragger_end(dragger: CHandle, out_command: *mut CHandle) -> c_int { - unsafe { oaknode::ffi::dragger::oaknode_dragger_end(dragger, out_command) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_dragger_is_started(dragger: CHandle, out_started: *mut c_int) -> c_int { - unsafe { oaknode::ffi::dragger::oaknode_dragger_is_started(dragger, out_started) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_dragger_free(dragger: *mut CHandle) { - unsafe { oaknode::ffi::dragger::oaknode_dragger_free(dragger) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_multicam_input_current() -> *const c_char { - unsafe { oaknode::ffi::multicam::oaknode_multicam_input_current() } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_multicam_input_sources() -> *const c_char { - unsafe { oaknode::ffi::multicam::oaknode_multicam_input_sources() } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_multicam_input_sequence() -> *const c_char { - unsafe { oaknode::ffi::multicam::oaknode_multicam_input_sequence() } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_multicam_input_sequence_type() -> *const c_char { - unsafe { oaknode::ffi::multicam::oaknode_multicam_input_sequence_type() } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_multicam_get_source_count(node: CHandle, out_count: *mut c_int) -> c_int { - unsafe { oaknode::ffi::multicam::oaknode_multicam_get_source_count(node, out_count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_multicam_get_rows_and_columns( - source_count: c_int, - rows: *mut c_int, - cols: *mut c_int, -) -> c_int { - unsafe { - oaknode::ffi::multicam::oaknode_multicam_get_rows_and_columns(source_count, rows, cols) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::multicam::oaknode_multicam_index_to_row_cols( - index, rows, cols, out_row, out_col, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_multicam_rows_cols_to_index( - row: c_int, - col: c_int, - rows: c_int, - cols: c_int, -) -> c_int { - unsafe { oaknode::ffi::multicam::oaknode_multicam_rows_cols_to_index(row, col, rows, cols) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_multicam_get_current_source(node: CHandle, out_source: *mut c_int) -> c_int { - unsafe { oaknode::ffi::multicam::oaknode_multicam_get_current_source(node, out_source) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_create() -> CHandle { - unsafe { oaknode::ffi::sequence::oaknode_sequence_create() } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_free(sequence: *mut CHandle) { - unsafe { oaknode::ffi::sequence::oaknode_sequence_free(sequence) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_set_default_parameters(sequence: CHandle) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_set_default_parameters(sequence) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_as_node(sequence: CHandle) -> CHandle { - unsafe { oaknode::ffi::sequence::oaknode_sequence_as_node(sequence) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_from_node(node: CHandle) -> CHandle { - unsafe { oaknode::ffi::sequence::oaknode_sequence_from_node(node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_get_track_list( - sequence: CHandle, - type_: c_int, - out: *mut CHandle, -) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_get_track_list(sequence, type_, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_get_track_count( - sequence: CHandle, - type_: c_int, - count: *mut c_int, -) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_get_track_count(sequence, type_, count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_get_track_at( - sequence: CHandle, - type_: c_int, - index: c_int, - out: *mut CHandle, -) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_get_track_at(sequence, type_, index, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_get_all_track_count(sequence: CHandle, count: *mut c_int) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_get_all_track_count(sequence, count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_get_all_track_at( - sequence: CHandle, - index: c_int, - out: *mut CHandle, -) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_get_all_track_at(sequence, index, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_get_playhead( - sequence: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { - oaknode::ffi::sequence::oaknode_sequence_get_playhead(sequence, numerator, denominator) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_set_playhead( - sequence: CHandle, - numerator: c_int, - denominator: c_int, -) -> c_int { - unsafe { - oaknode::ffi::sequence::oaknode_sequence_set_playhead(sequence, numerator, denominator) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_get_length( - sequence: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_get_length(sequence, numerator, denominator) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_get_video_length( - sequence: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { - oaknode::ffi::sequence::oaknode_sequence_get_video_length(sequence, numerator, denominator) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_get_audio_length( - sequence: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { - oaknode::ffi::sequence::oaknode_sequence_get_audio_length(sequence, numerator, denominator) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_verify_length(sequence: CHandle) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_verify_length(sequence) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_get_video_stream_count(sequence: CHandle, count: *mut c_int) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_get_video_stream_count(sequence, count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_get_audio_stream_count(sequence: CHandle, count: *mut c_int) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_get_audio_stream_count(sequence, count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_get_video_params( - sequence: CHandle, - index: c_int, - out: *mut CHandle, -) -> c_int { - unsafe { - oaknode::ffi::sequence::oaknode_sequence_get_video_params( - sequence, - index, - out as *mut std::ffi::c_void, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_set_video_params( - sequence: CHandle, - index: c_int, - params: CHandle, -) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_set_video_params(sequence, index, params) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_get_audio_params( - sequence: CHandle, - index: c_int, - out: *mut *mut c_void, -) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_get_audio_params(sequence, index, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_sequence_set_audio_params( - sequence: CHandle, - index: c_int, - params: *const c_void, -) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_set_audio_params(sequence, index, params) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_as_node(track: CHandle) -> CHandle { - unsafe { oaknode::ffi::track::oaknode_track_as_node(track) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_create(type_: c_int) -> CHandle { - unsafe { oaknode::ffi::track::oaknode_track_create(type_) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_free(track: *mut CHandle) { - unsafe { oaknode::ffi::track::oaknode_track_free(track) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_get_type(track: CHandle, type_: *mut c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_get_type(track, type_) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_set_type(track: CHandle, type_: c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_set_type(track, type_) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_get_height(track: CHandle, height: *mut f64) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_get_height(track, height) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_set_height(track: CHandle, height: f64) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_set_height(track, height) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_get_height_in_pixels(track: CHandle, height: *mut c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_get_height_in_pixels(track, height) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_set_height_in_pixels(track: CHandle, height: c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_set_height_in_pixels(track, height) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_get_default_height_in_pixels() -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_get_default_height_in_pixels() } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_get_minimum_height_in_pixels() -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_get_minimum_height_in_pixels() } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_get_index(track: CHandle, index: *mut c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_get_index(track, index) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_set_index(track: CHandle, index: c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_set_index(track, index) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_get_muted(track: CHandle, muted: *mut c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_get_muted(track, muted) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_set_muted(track: CHandle, muted: c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_set_muted(track, muted) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_get_locked(track: CHandle, locked: *mut c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_get_locked(track, locked) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_set_locked(track: CHandle, locked: c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_set_locked(track, locked) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_get_reference(track: CHandle, type_: *mut c_int, index: *mut c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_get_reference(track, type_, index) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_get_length( - track: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_get_length(track, numerator, denominator) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_get_sequence(track: CHandle, out: *mut CHandle) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_get_sequence(track, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_get_block_count(track: CHandle, count: *mut c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_get_block_count(track, count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_get_block_at(track: CHandle, index: c_int, out: *mut CHandle) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_get_block_at(track, index, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_append_block(track: CHandle, block: CHandle) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_append_block(track, block) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_prepend_block(track: CHandle, block: CHandle) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_prepend_block(track, block) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_insert_block_at_index(track: CHandle, block: CHandle, index: c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_insert_block_at_index(track, block, index) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_insert_block_after(track: CHandle, block: CHandle, before: CHandle) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_insert_block_after(track, block, before) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_insert_block_before(track: CHandle, block: CHandle, after: CHandle) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_insert_block_before(track, block, after) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_ripple_remove_block(track: CHandle, block: CHandle) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_ripple_remove_block(track, block) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_replace_block( - track: CHandle, - old_block: CHandle, - new_block: CHandle, -) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_replace_block(track, old_block, new_block) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_get_block_index(track: CHandle, block: CHandle, index: *mut c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_get_block_index(track, block, index) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_get_block_containing_time( - track: CHandle, - numerator: c_int, - denominator: c_int, - out: *mut CHandle, -) -> c_int { - unsafe { - oaknode::ffi::track::oaknode_track_get_block_containing_time( - track, - numerator, - denominator, - out, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_get_visible_block_at_time( - track: CHandle, - numerator: c_int, - denominator: c_int, - out: *mut CHandle, -) -> c_int { - unsafe { - oaknode::ffi::track::oaknode_track_get_visible_block_at_time( - track, - numerator, - denominator, - out, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::track::oaknode_track_is_range_free( - track, in_num, in_den, out_num, out_den, is_free, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_get_nearest_block_before_or_at( - track: CHandle, - numerator: c_int, - denominator: c_int, - out: *mut CHandle, -) -> c_int { - unsafe { - oaknode::ffi::track::oaknode_track_get_nearest_block_before_or_at( - track, - numerator, - denominator, - out, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_track_get_nearest_block_after_or_at( - track: CHandle, - numerator: c_int, - denominator: c_int, - out: *mut CHandle, -) -> c_int { - unsafe { - oaknode::ffi::track::oaknode_track_get_nearest_block_after_or_at( - track, - numerator, - denominator, - out, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_tracklist_get_sequence(list: CHandle, out: *mut CHandle) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_get_sequence(list, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_tracklist_get_track_input_id( - list: CHandle, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_get_track_input_id(list, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_tracklist_array_append(list: CHandle) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_array_append(list) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_tracklist_array_remove_last(list: CHandle) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_array_remove_last(list) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_tracklist_get_array_index_from_cache_index( - list: CHandle, - cache_index: c_int, - out_index: *mut c_int, -) -> c_int { - unsafe { - oaknode::ffi::track::oaknode_tracklist_get_array_index_from_cache_index( - list, - cache_index, - out_index, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_tracklist_get_type(list: CHandle, type_: *mut c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_get_type(list, type_) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_tracklist_get_track_count(list: CHandle, count: *mut c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_get_track_count(list, count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_tracklist_get_track_at(list: CHandle, index: c_int, out: *mut CHandle) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_get_track_at(list, index, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_tracklist_get_total_length( - list: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_get_total_length(list, numerator, denominator) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_tracklist_get_array_size(list: CHandle, size: *mut c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_get_array_size(list, size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_tracklist_add_track(list: CHandle, track: CHandle) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_add_track(list, track) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_tracklist_remove_track(list: CHandle, track: CHandle) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_remove_track(list, track) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_clip_create() -> CHandle { - unsafe { oaknode::ffi::block::oaknode_block_clip_create() } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_gap_create() -> CHandle { - unsafe { oaknode::ffi::block::oaknode_block_gap_create() } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_transition_create(kind: c_int) -> CHandle { - unsafe { oaknode::ffi::block::oaknode_block_transition_create(kind) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_free(block: *mut CHandle) { - unsafe { oaknode::ffi::block::oaknode_block_free(block) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_get_kind(block: CHandle, out_kind: *mut c_int) -> c_int { - unsafe { oaknode::ffi::block::oaknode_block_get_kind(block, out_kind) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_as_node(block: CHandle) -> CHandle { - unsafe { oaknode::ffi::block::oaknode_block_as_node(block) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_from_node(node: CHandle) -> CHandle { - unsafe { oaknode::ffi::block::oaknode_block_from_node(node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_get_in( - block: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { oaknode::ffi::block::oaknode_block_get_in(block, numerator, denominator) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_set_in(block: CHandle, numerator: c_int, denominator: c_int) -> c_int { - unsafe { oaknode::ffi::block::oaknode_block_set_in(block, numerator, denominator) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_get_out( - block: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { oaknode::ffi::block::oaknode_block_get_out(block, numerator, denominator) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_set_out(block: CHandle, numerator: c_int, denominator: c_int) -> c_int { - unsafe { oaknode::ffi::block::oaknode_block_set_out(block, numerator, denominator) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_get_length( - block: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { oaknode::ffi::block::oaknode_block_get_length(block, numerator, denominator) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_set_length_and_media_out( - block: CHandle, - numerator: c_int, - denominator: c_int, -) -> c_int { - unsafe { - oaknode::ffi::block::oaknode_block_set_length_and_media_out(block, numerator, denominator) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_set_length_and_media_in( - block: CHandle, - numerator: c_int, - denominator: c_int, -) -> c_int { - unsafe { - oaknode::ffi::block::oaknode_block_set_length_and_media_in(block, numerator, denominator) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_get_enabled(block: CHandle, enabled: *mut c_int) -> c_int { - unsafe { oaknode::ffi::block::oaknode_block_get_enabled(block, enabled) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_set_enabled(block: CHandle, enabled: c_int) -> c_int { - unsafe { oaknode::ffi::block::oaknode_block_set_enabled(block, enabled) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_get_previous(block: CHandle, out: *mut CHandle) -> c_int { - unsafe { oaknode::ffi::block::oaknode_block_get_previous(block, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_get_next(block: CHandle, out: *mut CHandle) -> c_int { - unsafe { oaknode::ffi::block::oaknode_block_get_next(block, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_get_track(block: CHandle, out: *mut CHandle) -> c_int { - unsafe { oaknode::ffi::block::oaknode_block_get_track(block, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_link(a: CHandle, b: CHandle) -> c_int { - unsafe { oaknode::ffi::block::oaknode_block_link(a, b) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_unlink(a: CHandle, b: CHandle) -> c_int { - unsafe { oaknode::ffi::block::oaknode_block_unlink(a, b) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_are_linked(a: CHandle, b: CHandle, linked: *mut c_int) -> c_int { - unsafe { oaknode::ffi::block::oaknode_block_are_linked(a, b, linked) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_get_link_count(block: CHandle, count: *mut c_int) -> c_int { - unsafe { oaknode::ffi::block::oaknode_block_get_link_count(block, count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_block_get_link_at(block: CHandle, index: c_int, out: *mut CHandle) -> c_int { - unsafe { oaknode::ffi::block::oaknode_block_get_link_at(block, index, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_clip_get_media_in( - clip: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { oaknode::ffi::block::oaknode_clip_get_media_in(clip, numerator, denominator) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_clip_set_media_in(clip: CHandle, numerator: c_int, denominator: c_int) -> c_int { - unsafe { oaknode::ffi::block::oaknode_clip_set_media_in(clip, numerator, denominator) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_clip_get_speed(clip: CHandle, speed: *mut f64) -> c_int { - unsafe { oaknode::ffi::block::oaknode_clip_get_speed(clip, speed) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_clip_set_speed(clip: CHandle, speed: f64) -> c_int { - unsafe { oaknode::ffi::block::oaknode_clip_set_speed(clip, speed) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_clip_get_reverse(clip: CHandle, reverse: *mut c_int) -> c_int { - unsafe { oaknode::ffi::block::oaknode_clip_get_reverse(clip, reverse) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_clip_set_reverse(clip: CHandle, reverse: c_int) -> c_int { - unsafe { oaknode::ffi::block::oaknode_clip_set_reverse(clip, reverse) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_clip_get_maintain_audio_pitch(clip: CHandle, maintain: *mut c_int) -> c_int { - unsafe { oaknode::ffi::block::oaknode_clip_get_maintain_audio_pitch(clip, maintain) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_clip_set_maintain_audio_pitch(clip: CHandle, maintain: c_int) -> c_int { - unsafe { oaknode::ffi::block::oaknode_clip_set_maintain_audio_pitch(clip, maintain) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_clip_get_loop_mode(clip: CHandle, loop_mode: *mut c_int) -> c_int { - unsafe { oaknode::ffi::block::oaknode_clip_get_loop_mode(clip, loop_mode) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_clip_set_loop_mode(clip: CHandle, loop_mode: c_int) -> c_int { - unsafe { oaknode::ffi::block::oaknode_clip_set_loop_mode(clip, loop_mode) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_clip_get_track_type(clip: CHandle, type_: *mut c_int) -> c_int { - unsafe { oaknode::ffi::block::oaknode_clip_get_track_type(clip, type_) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_transition_get_in_offset( - transition: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { - oaknode::ffi::block::oaknode_transition_get_in_offset(transition, numerator, denominator) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_transition_get_out_offset( - transition: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { - oaknode::ffi::block::oaknode_transition_get_out_offset(transition, numerator, denominator) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_transition_get_offset_center( - transition: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { - oaknode::ffi::block::oaknode_transition_get_offset_center( - transition, - numerator, - denominator, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_transition_set_offset_center( - transition: CHandle, - numerator: c_int, - denominator: c_int, -) -> c_int { - unsafe { - oaknode::ffi::block::oaknode_transition_set_offset_center( - transition, - numerator, - denominator, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::block::oaknode_transition_set_offsets_and_length( - transition, in_num, in_den, out_num, out_den, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_transition_is_dual(transition: CHandle, dual: *mut c_int) -> c_int { - unsafe { oaknode::ffi::block::oaknode_transition_is_dual(transition, dual) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_transition_get_connected_out_block(transition: CHandle, out: *mut CHandle) -> c_int { - unsafe { oaknode::ffi::block::oaknode_transition_get_connected_out_block(transition, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_transition_get_connected_in_block(transition: CHandle, out: *mut CHandle) -> c_int { - unsafe { oaknode::ffi::block::oaknode_transition_get_connected_in_block(transition, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_clip_add_cache_passthrough_from(clip: CHandle, other: CHandle) -> c_int { - unsafe { oaknode::ffi::block::oaknode_clip_add_cache_passthrough_from(clip, other) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_init(project: CHandle) -> CHandle { - unsafe { oaknode::ffi::colormanager::oaknode_colormanager_init(project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_free(manager: *mut CHandle) { - unsafe { oaknode::ffi::colormanager::oaknode_colormanager_free(manager) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_wrap_borrowed(native_manager: *mut c_void) -> CHandle { - unsafe { oaknode::ffi::colormanager::oaknode_colormanager_wrap_borrowed(native_manager) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_initialize(manager: CHandle) -> c_int { - unsafe { oaknode::ffi::colormanager::oaknode_colormanager_initialize(manager) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_set_up_default_config() -> c_int { - unsafe { oaknode::ffi::colormanager::oaknode_colormanager_set_up_default_config() } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_get_config_filename( - manager: CHandle, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oaknode::ffi::colormanager::oaknode_colormanager_get_config_filename(manager, buf, buf_size) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_set_config_filename( - manager: CHandle, - filename: *const c_char, -) -> c_int { - unsafe { - oaknode::ffi::colormanager::oaknode_colormanager_set_config_filename(manager, filename) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_update_config_from_filename(manager: CHandle) -> c_int { - unsafe { oaknode::ffi::colormanager::oaknode_colormanager_update_config_from_filename(manager) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_get_default_input_color_space( - manager: CHandle, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oaknode::ffi::colormanager::oaknode_colormanager_get_default_input_color_space( - manager, buf, buf_size, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_set_default_input_color_space( - manager: CHandle, - colorspace: *const c_char, -) -> c_int { - unsafe { - oaknode::ffi::colormanager::oaknode_colormanager_set_default_input_color_space( - manager, colorspace, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_get_reference_color_space( - manager: CHandle, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oaknode::ffi::colormanager::oaknode_colormanager_get_reference_color_space( - manager, buf, buf_size, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_get_compliant_color_space( - manager: CHandle, - colorspace: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oaknode::ffi::colormanager::oaknode_colormanager_get_compliant_color_space( - manager, colorspace, buf, buf_size, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::colormanager::oaknode_colormanager_get_colorspace_for_ffmpeg_tags( - manager, primaries, trc, buf, buf_size, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_get_display_count(manager: CHandle, count: *mut c_int) -> c_int { - unsafe { oaknode::ffi::colormanager::oaknode_colormanager_get_display_count(manager, count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_get_display_at( - manager: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oaknode::ffi::colormanager::oaknode_colormanager_get_display_at( - manager, index, buf, buf_size, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_get_default_display( - manager: CHandle, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oaknode::ffi::colormanager::oaknode_colormanager_get_default_display(manager, buf, buf_size) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_get_view_count( - manager: CHandle, - display: *const c_char, - count: *mut c_int, -) -> c_int { - unsafe { - oaknode::ffi::colormanager::oaknode_colormanager_get_view_count(manager, display, count) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::colormanager::oaknode_colormanager_get_view_at( - manager, display, index, buf, buf_size, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_get_default_view( - manager: CHandle, - display: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oaknode::ffi::colormanager::oaknode_colormanager_get_default_view( - manager, display, buf, buf_size, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_get_look_count(manager: CHandle, count: *mut c_int) -> c_int { - unsafe { oaknode::ffi::colormanager::oaknode_colormanager_get_look_count(manager, count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_get_look_at( - manager: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oaknode::ffi::colormanager::oaknode_colormanager_get_look_at(manager, index, buf, buf_size) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_get_colorspace_count(manager: CHandle, count: *mut c_int) -> c_int { - unsafe { oaknode::ffi::colormanager::oaknode_colormanager_get_colorspace_count(manager, count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_get_colorspace_at( - manager: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oaknode::ffi::colormanager::oaknode_colormanager_get_colorspace_at( - manager, index, buf, buf_size, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_get_default_luma_coefs(manager: CHandle, rgb: *mut f64) -> c_int { - unsafe { oaknode::ffi::colormanager::oaknode_colormanager_get_default_luma_coefs(manager, rgb) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_colormanager_get_compliant_color_transform( - manager: CHandle, - transform: CHandle, - force_display: c_int, - out: *mut CHandle, -) -> c_int { - unsafe { - oaknode::ffi::colormanager::oaknode_colormanager_get_compliant_color_transform( - manager, - transform, - force_display, - out, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_traverser_init() -> CHandle { - unsafe { oaknode::ffi::traverser::oaknode_traverser_init() } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_traverser_free(traverser: *mut CHandle) { - unsafe { oaknode::ffi::traverser::oaknode_traverser_free(traverser) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::traverser::oaknode_traverser_generate_database( - traverser, node, in_num, in_den, out_num, out_den, out_db, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_traverser_database_free(db: *mut CHandle) { - unsafe { oaknode::ffi::traverser::oaknode_traverser_database_free(db) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_traverser_database_row_count(db: CHandle, out_count: *mut c_int) -> c_int { - unsafe { oaknode::ffi::traverser::oaknode_traverser_database_row_count(db, out_count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_traverser_database_row_key_at( - db: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oaknode::ffi::traverser::oaknode_traverser_database_row_key_at(db, index, buf, buf_size) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_traverser_database_row_value_count( - db: CHandle, - key: *const c_char, - out_count: *mut c_int, -) -> c_int { - unsafe { - oaknode::ffi::traverser::oaknode_traverser_database_row_value_count(db, key, out_count) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_traverser_database_value_at( - db: CHandle, - key: *const c_char, - index: c_int, - out: *mut crate::node::OakNodeValue, -) -> c_int { - unsafe { oaknode::ffi::traverser::oaknode_traverser_database_value_at(db, key, index, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::traverser::oaknode_traverser_database_value_string_at( - db, key, index, buf, buf_size, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_serializer_initialize() -> c_int { - unsafe { oaknode::ffi::serializer::oaknode_serializer_initialize() } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_serializer_shutdown() { - unsafe { oaknode::ffi::serializer::oaknode_serializer_shutdown() } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_serializer_savedata_create(load_type: c_int, project: CHandle) -> CHandle { - unsafe { oaknode::ffi::serializer::oaknode_serializer_savedata_create(load_type, project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_serializer_savedata_free(save_data: *mut CHandle) { - unsafe { oaknode::ffi::serializer::oaknode_serializer_savedata_free(save_data) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_serializer_savedata_set_nodes( - save_data: CHandle, - nodes: *const CHandle, - count: c_int, -) -> c_int { - unsafe { - oaknode::ffi::serializer::oaknode_serializer_savedata_set_nodes(save_data, nodes, count) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_serializer_savedata_set_property( - save_data: CHandle, - node: CHandle, - key: *const c_char, - value: *const c_char, -) -> c_int { - unsafe { - oaknode::ffi::serializer::oaknode_serializer_savedata_set_property( - save_data, node, key, value, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_serializer_save_to_xml( - save_data: CHandle, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaknode::ffi::serializer::oaknode_serializer_save_to_xml(save_data, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::serializer::oaknode_serializer_load_from_xml( - project, - xml, - load_type, - out_result, - out_load_data, - details_buf, - details_buf_size, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_serializer_loaddata_free(load_data: *mut CHandle) { - unsafe { oaknode::ffi::serializer::oaknode_serializer_loaddata_free(load_data) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_serializer_loaddata_node_count(load_data: CHandle) -> c_int { - unsafe { oaknode::ffi::serializer::oaknode_serializer_loaddata_node_count(load_data) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_serializer_loaddata_node_at(load_data: CHandle, index: c_int) -> CHandle { - unsafe { oaknode::ffi::serializer::oaknode_serializer_loaddata_node_at(load_data, index) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::serializer::oaknode_serializer_loaddata_get_property( - load_data, node, key, buf, buf_size, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaknode_serializer_loaddata_connection_count(load_data: CHandle) -> c_int { - unsafe { oaknode::ffi::serializer::oaknode_serializer_loaddata_connection_count(load_data) } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::serializer::oaknode_serializer_loaddata_connection_at( - load_data, - index, - out_output_node, - out_input_node, - input_id_buf, - input_id_buf_size, - out_element, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::serializer::oaknode_serializer_save_to_file( - project, - filename, - use_compression, - out_code, - details, - details_size, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -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 { - unsafe { - oaknode::ffi::serializer::oaknode_serializer_load_from_file( - project, - filename, - out_code, - details, - details_size, - ) - } -} diff --git a/crates/oakengine/src/bridge/plugin.rs b/crates/oakengine/src/bridge/plugin.rs deleted file mode 100644 index 5b2a62873..000000000 --- a/crates/oakengine/src/bridge/plugin.rs +++ /dev/null @@ -1,81 +0,0 @@ -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! oakplugin C ABI bridge: direct Rust calls into the `oakplugin` crate. -//! -//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every -//! call below is a compile-time Rust call into `oakplugin`'s `ffi` (the -//! `#[no_mangle]` exports stay in the dylib for the external C ABI; -//! internal callers bypass them). Handles cross as the shared -//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"` -//! declaration (resolved at link time against the sibling crate in the -//! same dylib) are the host `oakcore_*` symbols and the encoding-params -//! C ABI POD crossings (the facade keeps its own POD mirrors there). - -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! oakplugin C ABI imports, mirroring the oakplugin crate's exports -//! (`src/plugin/rust/src/ffi.rs`; headers `include/plugin/*.h`). - -use std::ffi::{c_char, c_int}; - -/// Direct call into the `oakplugin` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakplugin_host_scan(bundle_dirs: *const *const c_char, dir_count: c_int) -> c_int { - unsafe { oakplugin::ffi::oakplugin_host_scan(bundle_dirs, dir_count) } -} - -/// Direct call into the `oakplugin` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakplugin_host_init() -> c_int { - unsafe { oakplugin::ffi::oakplugin_host_init() } -} - -/// Direct call into the `oakplugin` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakplugin_host_plugin_count() -> c_int { - unsafe { oakplugin::ffi::oakplugin_host_plugin_count() } -} - -/// Direct call into the `oakplugin` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakplugin_host_plugin_id_at(index: c_int, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oakplugin::ffi::oakplugin_host_plugin_id_at(index, buf, buf_size) } -} - -/// Direct call into the `oakplugin` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakplugin_host_plugin_label( - plugin_id: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oakplugin::ffi::oakplugin_host_plugin_label(plugin_id, buf, buf_size) } -} diff --git a/crates/oakengine/src/bridge/render.rs b/crates/oakengine/src/bridge/render.rs deleted file mode 100644 index 75106d742..000000000 --- a/crates/oakengine/src/bridge/render.rs +++ /dev/null @@ -1,340 +0,0 @@ -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! oakrender C ABI bridge: direct Rust calls into the `oakrender` crate. -//! -//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every -//! call below is a compile-time Rust call into `oakrender`'s `ffi` (the -//! `#[no_mangle]` exports stay in the dylib for the external C ABI; -//! internal callers bypass them). Handles cross as the shared -//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"` -//! declaration (resolved at link time against the sibling crate in the -//! same dylib) are the host `oakcore_*` symbols and the encoding-params -//! C ABI POD crossings (the facade keeps its own POD mirrors there). - -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! oakrender C ABI imports, mirroring the oakrender crate's exports -//! (`src/render/rust/src/ffi.rs`; headers `include/render/*.h`). - -use std::ffi::{c_char, c_double, c_int, c_void}; - -use crate::handle::CHandle; - -/// `include/render/ticket.h` — video render ticket params. Single-lib -/// unification: aliases the oakrender crate's POD (same `repr(C)` -/// layout; all handle fields are the shared [`CHandle`]). -pub type OakVideoTicketParams = oakrender::ffi::OakVideoTicketParams; - -/// `include/render/renderer.h` — frame video-params POD returned by -/// `oakrender_codec_frame_get_params`. Single-lib unification: aliases -/// the oakrender crate's POD. -pub type OakRenderVideoParams = oakrender::ffi::OakRenderVideoParams; - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_display_renderer_create_dynamic(backend_id: *const c_char) -> CHandle { - unsafe { oakrender::ffi::renderer::oakrender_display_renderer_create_dynamic(backend_id) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_display_renderer_create_opengl() -> CHandle { - unsafe { oakrender::ffi::renderer::oakrender_display_renderer_create_opengl() } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_display_renderer_init(renderer: CHandle, gl_context: *mut c_void) -> c_int { - unsafe { oakrender::ffi::renderer::oakrender_display_renderer_init(renderer, gl_context) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_display_renderer_destroy(renderer: *mut CHandle) { - unsafe { oakrender::ffi::renderer::oakrender_display_renderer_destroy(renderer) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_display_renderer_is_open_gl(renderer: CHandle) -> c_int { - unsafe { oakrender::ffi::renderer::oakrender_display_renderer_is_open_gl(renderer) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_display_renderer_is_vulkan(renderer: CHandle) -> c_int { - unsafe { oakrender::ffi::renderer::oakrender_display_renderer_is_vulkan(renderer) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_manager_set_aggressive_gc(enabled: c_int) -> c_int { - unsafe { oakrender::ffi::ticket::oakrender_manager_set_aggressive_gc(enabled) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_set_cacher_multicam(multicam_or_null: CHandle) -> c_int { - unsafe { oakrender::ffi::manager::oakrender_set_cacher_multicam(multicam_or_null) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_set_display_color_processor(p_or_null: CHandle) -> c_int { - unsafe { oakrender::ffi::manager::oakrender_set_display_color_processor(p_or_null) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_ticket_render_frame( - params: *const OakVideoTicketParams, - cb: Option, - userdata: *mut c_void, -) -> CHandle { - unsafe { oakrender::ffi::ticket::oakrender_ticket_render_frame(params, cb, userdata) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_ticket_render_audio( - output_node: CHandle, - in_num: i64, - in_den: i64, - out_num: i64, - out_den: i64, - params: *const c_void, - mode: c_int, - cb: Option, - userdata: *mut c_void, - montage: *const oakrender::ffi::OakMontageClip, - montage_count: c_int, -) -> CHandle { - unsafe { - oakrender::ffi::ticket::oakrender_ticket_render_audio( - output_node, - in_num, - in_den, - out_num, - out_den, - params as *const CHandle, - mode, - cb, - userdata, - montage, - montage_count, - ) - } -} - -/// `oakrender_audio_samples_free` — release the samples block returned -/// by `oakrender_ticket_get_samples` (NULL no-op). -pub fn oakrender_audio_samples_free(samples: *mut c_void) { - unsafe { oakrender::ffi::ticket::oakrender_audio_samples_free(samples) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_ticket_wait(ticket: CHandle) -> c_int { - unsafe { oakrender::ffi::ticket::oakrender_ticket_wait(ticket) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_ticket_cancel(ticket: CHandle) -> c_int { - unsafe { oakrender::ffi::ticket::oakrender_ticket_cancel(ticket) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_ticket_get_frame(ticket: CHandle, out: *mut CHandle) -> c_int { - unsafe { oakrender::ffi::ticket::oakrender_ticket_get_frame(ticket, out) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_ticket_get_samples(ticket: CHandle, out: *mut *mut c_void) -> c_int { - unsafe { oakrender::ffi::ticket::oakrender_ticket_get_samples(ticket, out) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_ticket_free(ticket: *mut CHandle) { - unsafe { oakrender::ffi::ticket::oakrender_ticket_free(ticket) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_codec_frame_create() -> CHandle { - unsafe { oakrender::ffi::renderer::oakrender_codec_frame_create() } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_codec_frame_retain(frame: CHandle) -> CHandle { - unsafe { oakrender::ffi::renderer::oakrender_codec_frame_retain(frame) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_codec_frame_free(frame: *mut CHandle) { - unsafe { oakrender::ffi::renderer::oakrender_codec_frame_free(frame) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_codec_frame_width(frame: CHandle) -> c_int { - unsafe { oakrender::ffi::renderer::oakrender_codec_frame_width(frame) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_codec_frame_height(frame: CHandle) -> c_int { - unsafe { oakrender::ffi::renderer::oakrender_codec_frame_height(frame) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_codec_frame_linesize_bytes(frame: CHandle) -> c_int { - unsafe { oakrender::ffi::renderer::oakrender_codec_frame_linesize_bytes(frame) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_codec_frame_data(frame: CHandle) -> *mut c_void { - unsafe { oakrender::ffi::renderer::oakrender_codec_frame_data(frame) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_codec_frame_const_data(frame: CHandle) -> *const c_void { - unsafe { oakrender::ffi::renderer::oakrender_codec_frame_const_data(frame) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_codec_frame_is_allocated(frame: CHandle) -> c_int { - unsafe { oakrender::ffi::renderer::oakrender_codec_frame_is_allocated(frame) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_codec_frame_get_params(frame: CHandle, out: *mut OakRenderVideoParams) -> c_int { - unsafe { oakrender::ffi::renderer::oakrender_codec_frame_get_params(frame, out) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_color_processor_create( - src_space: *const c_char, - dst_transform: *const c_char, - direction: c_int, -) -> CHandle { - unsafe { - oakrender::ffi::color::oakrender_color_processor_create(src_space, dst_transform, direction) - } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_color_processor_free(processor: *mut CHandle) { - unsafe { oakrender::ffi::color::oakrender_color_processor_free(processor) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_color_processor_is_valid(processor: CHandle) -> c_int { - unsafe { oakrender::ffi::color::oakrender_color_processor_is_valid(processor) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_color_processor_create_transform( - manager: CHandle, - input: *const c_char, - dest: CHandle, - direction: c_int, -) -> CHandle { - unsafe { - oakrender::ffi::color::oakrender_color_processor_create_transform( - manager, input, dest, direction, - ) - } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_color_processor_convert( - processor: CHandle, - ir: c_double, - ig: c_double, - ib: c_double, - ia: c_double, - out_r: *mut c_double, - out_g: *mut c_double, - out_b: *mut c_double, - out_a: *mut c_double, -) -> c_int { - unsafe { - oakrender::ffi::color::oakrender_color_processor_convert( - processor, ir, ig, ib, ia, out_r, out_g, out_b, out_a, - ) - } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_color_manager_set_up_default_config() -> c_int { - unsafe { oakrender::ffi::color::oakrender_color_manager_set_up_default_config() } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_color_manager_get_config(buf: *mut c_char, n: c_int) -> c_int { - unsafe { oakrender::ffi::color::oakrender_color_manager_get_config(buf, n) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_lut_is_supported_extension(extension: *const c_char) -> c_int { - unsafe { oakrender::ffi::color::oakrender_lut_is_supported_extension(extension) } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_lut_supported_extensions_count() -> c_int { - unsafe { oakrender::ffi::color::oakrender_lut_supported_extensions_count() } -} - -/// Direct call into the `oakrender` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakrender_lut_supported_extension_at(i: c_int, buf: *mut c_char, n: c_int) -> c_int { - unsafe { oakrender::ffi::color::oakrender_lut_supported_extension_at(i, buf, n) } -} diff --git a/crates/oakengine/src/bridge/task.rs b/crates/oakengine/src/bridge/task.rs deleted file mode 100644 index 903e62dab..000000000 --- a/crates/oakengine/src/bridge/task.rs +++ /dev/null @@ -1,309 +0,0 @@ -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! oaktask C ABI bridge: direct Rust calls into the `oaktask` crate. -//! -//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every -//! call below is a compile-time Rust call into `oaktask`'s `ffi` (the -//! `#[no_mangle]` exports stay in the dylib for the external C ABI; -//! internal callers bypass them). Handles cross as the shared -//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"` -//! declaration (resolved at link time against the sibling crate in the -//! same dylib) are the host `oakcore_*` symbols and the encoding-params -//! C ABI POD crossings (the facade keeps its own POD mirrors there). - -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! oaktask C ABI imports, mirroring the oaktask crate's exports -//! (`src/task/rust/src/ffi/{manager,task,project}.rs`; headers -//! `include/task/*.h`). -//! -//! Every handle crosses as [`crate::handle::CHandle`]. `oaktask_create_export` -//! takes the encoding-params POD ([`crate::bridge::codec::EncodingParamsPOD`], -//! field-identical to the task crate's `OakCodecEncodingParams`). String -//! getters report the size **including** the NUL; the facade converts with -//! [`crate::handle::string_result`]. - -use std::ffi::{c_char, c_int, c_void}; - -use crate::handle::CHandle; - -/// `oaktask_event_fn` callback (`include/task/task.h`): `event_id` is an -/// `OakTaskEvent` (0=started, 1=progress, 2=finished), `value` 0..1 (or -/// start-ms / success flag), `userdata` the subscription token. -pub type OakTaskEventFn = unsafe extern "C" fn(event_id: c_int, value: f64, userdata: *mut c_void); - -/// `oaktask_otio_import_confirm_fn` (`include/task/project.h`). -pub type OakTaskOtioImportConfirmFn = unsafe extern "C" fn( - sequence_names: *const *const c_char, - count: c_int, - userdata: *mut c_void, -) -> c_int; - -/// `oaktask_image_sequence_confirm_fn` (`include/task/project.h`). -pub type OakTaskImageSequenceConfirmFn = - unsafe extern "C" fn(filename: *const c_char, userdata: *mut c_void) -> c_int; - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_manager_init() -> c_int { - unsafe { oaktask::ffi::manager::oaktask_manager_init() } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_manager_shutdown() { - unsafe { oaktask::ffi::manager::oaktask_manager_shutdown() } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_register_codec_submitter() -> c_int { - unsafe { oaktask::ffi::manager::oaktask_register_codec_submitter() } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_manager_count() -> c_int { - unsafe { oaktask::ffi::manager::oaktask_manager_count() } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_manager_at(i: c_int) -> CHandle { - unsafe { oaktask::ffi::manager::oaktask_manager_at(i) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_manager_delete_finished() { - unsafe { oaktask::ffi::manager::oaktask_manager_delete_finished() } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_task_free(t: *mut CHandle) { - unsafe { oaktask::ffi::task::oaktask_task_free(t) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_task_start_sync(t: CHandle) -> c_int { - unsafe { oaktask::ffi::task::oaktask_task_start_sync(t) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_task_start(t: CHandle) -> c_int { - unsafe { oaktask::ffi::task::oaktask_task_start(t) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_task_cancel(t: CHandle) -> c_int { - unsafe { oaktask::ffi::task::oaktask_task_cancel(t) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_task_wait(t: CHandle) -> c_int { - unsafe { oaktask::ffi::task::oaktask_task_wait(t) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_task_is_finished(t: CHandle) -> c_int { - unsafe { oaktask::ffi::task::oaktask_task_is_finished(t) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_task_succeeded(t: CHandle) -> c_int { - unsafe { oaktask::ffi::task::oaktask_task_succeeded(t) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_task_title(t: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oaktask::ffi::task::oaktask_task_title(t, buf, buf_size) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_task_error(t: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oaktask::ffi::task::oaktask_task_error(t, buf, buf_size) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_task_subscribe( - t: CHandle, - cb: Option, - userdata: *mut c_void, -) -> i64 { - unsafe { oaktask::ffi::task::oaktask_task_subscribe(t, cb, userdata) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_debug_alive_count() -> c_int { - unsafe { oaktask::ffi::task::oaktask_debug_alive_count() } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_create_project_load(filename: *const c_char) -> CHandle { - unsafe { oaktask::ffi::project::oaktask_create_project_load(filename) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_load_take_project(t: CHandle) -> CHandle { - unsafe { oaktask::ffi::project::oaktask_load_take_project(t) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_create_project_save( - project: CHandle, - filename_or_null: *const c_char, - use_compression: c_int, -) -> CHandle { - unsafe { - oaktask::ffi::project::oaktask_create_project_save( - project, - filename_or_null, - use_compression, - ) - } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_create_project_import( - folder: CHandle, - project: CHandle, - urls: *const *const c_char, - url_count: c_int, -) -> CHandle { - unsafe { - oaktask::ffi::project::oaktask_create_project_import(folder, project, urls, url_count) - } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_import_take_command(t: CHandle) -> CHandle { - unsafe { oaktask::ffi::project::oaktask_import_take_command(t) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_import_footage_count(t: CHandle) -> c_int { - unsafe { oaktask::ffi::project::oaktask_import_footage_count(t) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_import_footage_at(t: CHandle, index: c_int) -> CHandle { - unsafe { oaktask::ffi::project::oaktask_import_footage_at(t, index) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_import_invalid_count(t: CHandle) -> c_int { - unsafe { oaktask::ffi::project::oaktask_import_invalid_count(t) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_import_invalid_at( - t: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaktask::ffi::project::oaktask_import_invalid_at(t, index, buf, buf_size) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_create_project_load_otio(filename: *const c_char) -> CHandle { - unsafe { oaktask::ffi::project::oaktask_create_project_load_otio(filename) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_load_otio_take_project(t: CHandle) -> CHandle { - unsafe { oaktask::ffi::project::oaktask_load_otio_take_project(t) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_create_project_save_otio(project: CHandle, filename: *const c_char) -> CHandle { - unsafe { oaktask::ffi::project::oaktask_create_project_save_otio(project, filename) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_load_otio_set_confirm_cb( - cb: Option, - userdata: *mut c_void, -) { - unsafe { oaktask::ffi::project::oaktask_load_otio_set_confirm_cb(cb, userdata) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_create_precache(footage: CHandle, index: c_int, sequence: CHandle) -> CHandle { - unsafe { oaktask::ffi::project::oaktask_create_precache(footage, index, sequence) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -/// `oaktask_create_export` — direct call into the `oaktask` crate -/// (single-lib unification; the encoding-params POD is the shared -/// oakcodec type). -pub fn oaktask_create_export( - viewer: CHandle, - color_manager: CHandle, - params: *const crate::bridge::codec::EncodingParamsPOD, -) -> CHandle { - unsafe { oaktask::ffi::project::oaktask_create_export(viewer, color_manager, params) } -} - -/// Direct call into the `oaktask` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktask_import_set_image_sequence_confirm_cb( - cb: Option, - userdata: *mut c_void, -) { - unsafe { oaktask::ffi::project::oaktask_import_set_image_sequence_confirm_cb(cb, userdata) } -} diff --git a/crates/oakengine/src/bridge/timeline.rs b/crates/oakengine/src/bridge/timeline.rs deleted file mode 100644 index 8f0f9f666..000000000 --- a/crates/oakengine/src/bridge/timeline.rs +++ /dev/null @@ -1,490 +0,0 @@ -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! oaktimeline C ABI bridge: direct Rust calls into the `oaktimeline` crate. -//! -//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every -//! call below is a compile-time Rust call into `oaktimeline`'s `ffi` (the -//! `#[no_mangle]` exports stay in the dylib for the external C ABI; -//! internal callers bypass them). Handles cross as the shared -//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"` -//! declaration (resolved at link time against the sibling crate in the -//! same dylib) are the host `oakcore_*` symbols and the encoding-params -//! C ABI POD crossings (the facade keeps its own POD mirrors there). - -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! oaktimeline C ABI imports, mirroring the oaktimeline crate's exports -//! (`src/timeline/rust/src/ffi.rs`; headers `include/timeline/*.h`). -//! -//! Every handle crosses as [`crate::handle::CHandle`]. The marker/workarea -//! time quantities cross as `c_int` num/den pairs; the edit commands take -//! `i64` rationals. String getters report the size **including** the NUL; -//! the facade converts with [`crate::handle::string_result`]. - -use std::ffi::{c_char, c_int}; - -use crate::handle::CHandle; - -// `include/timeline/marker.h` exports (complete inventory): -// oaktimeline_marker_list_create / free / of / add / count / at / -// add_command / remove_at_command / set_time_command / -// set_props_command / list_load / list_save. - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_marker_list_create() -> CHandle { - unsafe { oaktimeline::ffi::marker::oaktimeline_marker_list_create() } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_marker_list_of(owner: CHandle) -> CHandle { - unsafe { oaktimeline::ffi::marker::oaktimeline_marker_list_of(owner) } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_marker_list_free(list: *mut CHandle) { - unsafe { oaktimeline::ffi::marker::oaktimeline_marker_list_free(list) } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_marker_add( - list: CHandle, - in_num: c_int, - in_den: c_int, - out_num: c_int, - out_den: c_int, - name: *const c_char, - color: c_int, -) -> c_int { - unsafe { - oaktimeline::ffi::marker::oaktimeline_marker_add( - list, in_num, in_den, out_num, out_den, name, color, - ) - } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_marker_count(list: CHandle, out_count: *mut c_int) -> c_int { - unsafe { oaktimeline::ffi::marker::oaktimeline_marker_count(list, out_count) } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_marker_at( - list: CHandle, - index: c_int, - in_num: *mut c_int, - in_den: *mut c_int, - out_num: *mut c_int, - out_den: *mut c_int, - color: *mut c_int, - name_buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oaktimeline::ffi::marker::oaktimeline_marker_at( - list, index, in_num, in_den, out_num, out_den, color, name_buf, buf_size, - ) - } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_marker_add_command( - list: CHandle, - in_num: c_int, - in_den: c_int, - out_num: c_int, - out_den: c_int, - name: *const c_char, - color: c_int, -) -> CHandle { - unsafe { - oaktimeline::ffi::marker::oaktimeline_marker_add_command( - list, in_num, in_den, out_num, out_den, name, color, - ) - } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_marker_remove_at_command(list: CHandle, index: c_int) -> CHandle { - unsafe { oaktimeline::ffi::marker::oaktimeline_marker_remove_at_command(list, index) } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_marker_set_time_command( - list: CHandle, - index: c_int, - in_num: c_int, - in_den: c_int, - out_num: c_int, - out_den: c_int, -) -> CHandle { - unsafe { - oaktimeline::ffi::marker::oaktimeline_marker_set_time_command( - list, index, in_num, in_den, out_num, out_den, - ) - } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_marker_set_props_command( - list: CHandle, - index: c_int, - color: c_int, - name: *const c_char, -) -> CHandle { - unsafe { - oaktimeline::ffi::marker::oaktimeline_marker_set_props_command(list, index, color, name) - } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_marker_list_load(list: CHandle, reader: CHandle) -> c_int { - unsafe { oaktimeline::ffi::marker::oaktimeline_marker_list_load(list, reader) } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_marker_list_save(list: CHandle, writer: CHandle) -> c_int { - unsafe { oaktimeline::ffi::marker::oaktimeline_marker_list_save(list, writer) } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_workarea_create() -> CHandle { - unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_create() } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_workarea_of(owner: CHandle) -> CHandle { - unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_of(owner) } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_workarea_free(w: *mut CHandle) { - unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_free(w) } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_workarea_set_enabled(w: CHandle, enabled: c_int) -> c_int { - unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_set_enabled(w, enabled) } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_workarea_get( - w: CHandle, - in_num: *mut c_int, - in_den: *mut c_int, - out_num: *mut c_int, - out_den: *mut c_int, - enabled: *mut c_int, -) -> c_int { - unsafe { - oaktimeline::ffi::workarea::oaktimeline_workarea_get( - w, in_num, in_den, out_num, out_den, enabled, - ) - } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_workarea_set_range( - w: CHandle, - in_num: c_int, - in_den: c_int, - out_num: c_int, - out_den: c_int, -) -> c_int { - unsafe { - oaktimeline::ffi::workarea::oaktimeline_workarea_set_range( - w, in_num, in_den, out_num, out_den, - ) - } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_workarea_set_range_command( - w: CHandle, - in_num: c_int, - in_den: c_int, - out_num: c_int, - out_den: c_int, - old_in_num: c_int, - old_in_den: c_int, - old_out_num: c_int, - old_out_den: c_int, -) -> CHandle { - unsafe { - oaktimeline::ffi::workarea::oaktimeline_workarea_set_range_command( - w, - in_num, - in_den, - out_num, - out_den, - old_in_num, - old_in_den, - old_out_num, - old_out_den, - ) - } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_workarea_set_enabled_command(w: CHandle, enabled: c_int) -> CHandle { - unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_set_enabled_command(w, enabled) } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_workarea_reset( - in_num: *mut c_int, - in_den: *mut c_int, - out_num: *mut c_int, - out_den: *mut c_int, -) -> c_int { - unsafe { - oaktimeline::ffi::workarea::oaktimeline_workarea_reset(in_num, in_den, out_num, out_den) - } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_workarea_load(w: CHandle, reader: CHandle) -> c_int { - unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_load(w, reader) } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_workarea_save(w: CHandle, writer: CHandle) -> c_int { - unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_save(w, writer) } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_add_track_command(list: CHandle) -> CHandle { - unsafe { oaktimeline::ffi::edit::oaktimeline_add_track_command(list) } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_remove_track_command(track: CHandle) -> CHandle { - unsafe { oaktimeline::ffi::edit::oaktimeline_remove_track_command(track) } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_place_block_command( - list: CHandle, - track_index: c_int, - block: CHandle, - in_num: i64, - in_den: i64, -) -> CHandle { - unsafe { - oaktimeline::ffi::edit::oaktimeline_place_block_command( - list, - track_index, - block, - in_num, - in_den, - ) - } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_replace_block_with_gap_command(track: CHandle, block: CHandle) -> CHandle { - unsafe { oaktimeline::ffi::edit::oaktimeline_replace_block_with_gap_command(track, block) } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_move_block_command( - list: CHandle, - track_index: c_int, - block: CHandle, - in_num: i64, - in_den: i64, -) -> CHandle { - unsafe { - oaktimeline::ffi::edit::oaktimeline_move_block_command( - list, - track_index, - block, - in_num, - in_den, - ) - } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_trim_command( - track: CHandle, - block: CHandle, - new_length_num: i64, - new_length_den: i64, - mode: c_int, -) -> CHandle { - unsafe { - oaktimeline::ffi::edit::oaktimeline_trim_command( - track, - block, - new_length_num, - new_length_den, - mode, - ) - } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_split_command( - blocks: *const CHandle, - count: c_int, - point_num: i64, - point_den: i64, -) -> CHandle { - unsafe { - oaktimeline::ffi::edit::oaktimeline_split_command(blocks, count, point_num, point_den) - } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_split_preserving_links_command( - blocks: *const CHandle, - count: c_int, - point_nums: *const i64, - point_dens: *const i64, - time_count: c_int, -) -> CHandle { - unsafe { - oaktimeline::ffi::edit::oaktimeline_split_preserving_links_command( - blocks, count, point_nums, point_dens, time_count, - ) - } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_ripple_delete_gaps_command( - sequence: CHandle, - in_nums: *const i64, - in_dens: *const i64, - out_nums: *const i64, - out_dens: *const i64, - tracks: *const CHandle, - range_count: c_int, -) -> CHandle { - unsafe { - oaktimeline::ffi::edit::oaktimeline_ripple_delete_gaps_command( - sequence, - in_nums, - in_dens, - out_nums, - out_dens, - tracks, - range_count, - ) - } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_slide_command( - track: CHandle, - blocks: *const CHandle, - block_count: c_int, - in_adjacent: CHandle, - out_adjacent: CHandle, - movement_num: i64, - movement_den: i64, -) -> CHandle { - unsafe { - oaktimeline::ffi::edit::oaktimeline_slide_command( - track, - blocks, - block_count, - in_adjacent, - out_adjacent, - movement_num, - movement_den, - ) - } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_ripple_remove_area_command( - track: CHandle, - in_num: i64, - in_den: i64, - out_num: i64, - out_den: i64, -) -> CHandle { - unsafe { - oaktimeline::ffi::edit::oaktimeline_ripple_remove_area_command( - track, in_num, in_den, out_num, out_den, - ) - } -} - -/// Direct call into the `oaktimeline` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oaktimeline_insert_gaps_command( - list: CHandle, - point_num: i64, - point_den: i64, - length_num: i64, - length_den: i64, -) -> CHandle { - unsafe { - oaktimeline::ffi::edit::oaktimeline_insert_gaps_command( - list, point_num, point_den, length_num, length_den, - ) - } -} diff --git a/crates/oakengine/src/bridge/undo.rs b/crates/oakengine/src/bridge/undo.rs deleted file mode 100644 index 1f82e1975..000000000 --- a/crates/oakengine/src/bridge/undo.rs +++ /dev/null @@ -1,198 +0,0 @@ -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! oakundo C ABI bridge: direct Rust calls into the `oakundo` crate. -//! -//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every -//! call below is a compile-time Rust call into `oakundo`'s `ffi` (the -//! `#[no_mangle]` exports stay in the dylib for the external C ABI; -//! internal callers bypass them). Handles cross as the shared -//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"` -//! declaration (resolved at link time against the sibling crate in the -//! same dylib) are the host `oakcore_*` symbols and the encoding-params -//! C ABI POD crossings (the facade keeps its own POD mirrors there). - -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! oakundo C ABI imports, mirroring the oakundo crate's exports -//! (`src/undo/rust/src/ffi.rs`; headers `include/undo/*.h`). - -use std::ffi::{c_char, c_int}; - -use crate::handle::CHandle; - -/// `include/undo/undocommand.h` — callback table backing a -/// caller-defined undo command. Single-lib unification: aliases the -/// oakundo crate's vtable POD. -pub type OakUndoCommandVtable = oakundo::undocommand::OakUndoCommandVtable; - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_command_init( - vtable: *const OakUndoCommandVtable, - userdata: *mut std::ffi::c_void, -) -> CHandle { - unsafe { oakundo::ffi::command::oakundo_command_init(vtable, userdata) } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_command_init_multi() -> CHandle { - unsafe { oakundo::ffi::command::oakundo_command_init_multi() } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_command_multi_add_child(multi: CHandle, child: CHandle) -> c_int { - unsafe { oakundo::ffi::command::oakundo_command_multi_add_child(multi, child) } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_command_multi_child_count(multi: CHandle, out_count: *mut c_int) -> c_int { - unsafe { oakundo::ffi::command::oakundo_command_multi_child_count(multi, out_count) } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_command_multi_child(multi: CHandle, index: c_int, out_child: *mut CHandle) -> c_int { - unsafe { oakundo::ffi::command::oakundo_command_multi_child(multi, index, out_child) } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_command_redo_now(command: CHandle) -> c_int { - unsafe { oakundo::ffi::command::oakundo_command_redo_now(command) } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_command_undo_now(command: CHandle) -> c_int { - unsafe { oakundo::ffi::command::oakundo_command_undo_now(command) } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_command_free(command: *mut CHandle) { - unsafe { oakundo::ffi::command::oakundo_command_free(command) } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_undostack_init() -> CHandle { - unsafe { oakundo::ffi::undostack::oakundo_undostack_init() } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_undostack_free(stack: *mut CHandle) { - unsafe { oakundo::ffi::undostack::oakundo_undostack_free(stack) } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_undostack_push(stack: CHandle, command: CHandle, name: *const c_char) -> c_int { - unsafe { oakundo::ffi::undostack::oakundo_undostack_push(stack, command, name) } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_undostack_push_pre_executed( - stack: CHandle, - command: CHandle, - name: *const c_char, -) -> c_int { - unsafe { oakundo::ffi::undostack::oakundo_undostack_push_pre_executed(stack, command, name) } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_undostack_undo(stack: CHandle) -> c_int { - unsafe { oakundo::ffi::undostack::oakundo_undostack_undo(stack) } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_undostack_redo(stack: CHandle) -> c_int { - unsafe { oakundo::ffi::undostack::oakundo_undostack_redo(stack) } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_undostack_jump(stack: CHandle, index: i64) -> c_int { - unsafe { oakundo::ffi::undostack::oakundo_undostack_jump(stack, index) } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_undostack_clear(stack: CHandle) -> c_int { - unsafe { oakundo::ffi::undostack::oakundo_undostack_clear(stack) } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_undostack_can_undo(stack: CHandle, out_value: *mut c_int) -> c_int { - unsafe { oakundo::ffi::undostack::oakundo_undostack_can_undo(stack, out_value) } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_undostack_can_redo(stack: CHandle, out_value: *mut c_int) -> c_int { - unsafe { oakundo::ffi::undostack::oakundo_undostack_can_redo(stack, out_value) } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_undostack_count(stack: CHandle, out_count: *mut i64) -> c_int { - unsafe { oakundo::ffi::undostack::oakundo_undostack_count(stack, out_count) } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_undostack_index(stack: CHandle, out_index: *mut i64) -> c_int { - unsafe { oakundo::ffi::undostack::oakundo_undostack_index(stack, out_index) } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_undostack_command_text( - stack: CHandle, - row: i64, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oakundo::ffi::undostack::oakundo_undostack_command_text(stack, row, buf, buf_size) } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the -/// `#[no_mangle]` export stays for the external C ABI). -pub fn oakundo_undostack_command_is_done(stack: CHandle, row: i64, out_value: *mut c_int) -> c_int { - unsafe { oakundo::ffi::undostack::oakundo_undostack_command_is_done(stack, row, out_value) } -} diff --git a/crates/oakengine/src/codec.rs b/crates/oakengine/src/codec.rs index f4734630a..c5e677824 100644 --- a/crates/oakengine/src/codec.rs +++ b/crates/oakengine/src/codec.rs @@ -16,7 +16,7 @@ //! `engine/include/oakengine/encoding.h` over the oakcodec module. //! -//! Two parts: +//! Three parts: //! //! - The container/codec **metadata family** (format names/extensions, //! per-format codec lists, pixel/sample formats, filename helpers, @@ -30,15 +30,19 @@ //! "unset" (the POD's 0 is a valid format, DNxHD); encoder-specific //! video options are kept in a facade-side map (the POD has no such //! field). +//! - The **exporter family** (`engine/include/oakengine/exporter.h`) +//! assembles an encoding-params handle and drives the export task +//! synchronously (see the family section at the bottom). //! -//! Presets, preset load/save and the sequence-bound last-used/export -//! entry points are deferred (see the stubs below and `deferred.rs`). +//! Presets, preset load/save and the sequence-bound last-used entry +//! points remain deferred (see the stubs below and `deferred.rs`). +use std::cell::RefCell; use std::collections::HashMap; -use std::ffi::{c_char, c_int, c_void}; +use std::ffi::{c_char, c_double, c_int, c_void}; -use crate::bridge::codec as k; -use crate::bridge::codec::{zeroed_encoding_params, EncodingParamsPOD}; +use crate::stubs::codec as k; +use crate::pods::{zeroed_encoding_params, EncodingParamsPOD}; use crate::common::OakVideoParamsPod; use crate::error::{Error, Result}; use crate::handle::{guard, guard_int, string_result}; @@ -55,6 +59,10 @@ pub struct OakEngineEncodingParams { struct ParamsBox { pod: EncodingParamsPOD, video_options: HashMap, + /// Raw scaling-method code exactly as the caller set it: the POD's + /// `VideoScalingMethod` enum cannot carry garbage codes, and the + /// facade contract accepts any `int` verbatim (round-trips 99 as 99). + video_scaling_raw: c_int, } impl ParamsBox { @@ -64,6 +72,7 @@ impl ParamsBox { ParamsBox { pod, video_options: HashMap::new(), + video_scaling_raw: 0, } } } @@ -457,7 +466,7 @@ pub unsafe extern "C" fn oakengine_encoding_params_enable_video( p.pod.video_height = v.height; p.pod.video_time_base_num = v.time_base_num; p.pod.video_time_base_den = v.time_base_den; - p.pod.video_pixel_format = v.format; + p.pod.video_pixel_format = crate::pods::pixel_format_from_code(v.format); p.pod.video_interlacing = v.interlacing; p.pod.video_pixel_aspect_num = v.pixel_aspect_num; p.pod.video_pixel_aspect_den = v.pixel_aspect_den; @@ -480,7 +489,7 @@ pub unsafe extern "C" fn oakengine_encoding_params_enable_audio( p.pod.audio_codec = codec; p.pod.audio_sample_rate = sample_rate; p.pod.audio_channel_layout = channel_layout; - p.pod.audio_sample_format = sample_format; + p.pod.audio_sample_format = crate::pods::sample_format_from_code(sample_format); Ok(()) }) } @@ -594,7 +603,7 @@ pub unsafe extern "C" fn oakengine_encoding_params_get_video_params( (*out).height = p.pod.video_height; (*out).time_base_num = p.pod.video_time_base_num; (*out).time_base_den = p.pod.video_time_base_den; - (*out).format = p.pod.video_pixel_format; + (*out).format = p.pod.video_pixel_format as i32; (*out).interlacing = p.pod.video_interlacing; (*out).pixel_aspect_num = p.pod.video_pixel_aspect_num; (*out).pixel_aspect_den = p.pod.video_pixel_aspect_den; @@ -645,7 +654,7 @@ pub unsafe extern "C" fn oakengine_encoding_params_get_audio_params( *channel_layout = p.pod.audio_channel_layout; } if !sample_format.is_null() { - *sample_format = p.pod.audio_sample_format; + *sample_format = p.pod.audio_sample_format as i32; } Ok(()) }) @@ -964,7 +973,10 @@ pub unsafe extern "C" fn oakengine_encoding_params_set_video_scaling_method( ) -> c_int { guard(|| unsafe { let p = params_mut(params)?; - p.pod.video_scaling_method = method; + // The raw code round-trips verbatim (garbage codes included); + // the POD carries the nearest legal enum for the encoder. + p.video_scaling_raw = method; + p.pod.video_scaling_method = crate::pods::scaling_from_code(method); Ok(()) }) } @@ -976,7 +988,7 @@ pub unsafe extern "C" fn oakengine_encoding_params_video_scaling_method( ) -> c_int { guard_int(|| unsafe { let p = params_ref(params)?; - Ok(p.pod.video_scaling_method) + Ok(p.video_scaling_raw) }) } @@ -1073,14 +1085,365 @@ pub unsafe extern "C" fn oakengine_encoding_params_save_file( crate::error::OAKENGINE_E_FAILED } -/// `oakengine_export_render_with_params` — **not backed** (the exporter -/// family is facade-only; see `deferred.rs`). Returns OAKENGINE_E_FAILED. +// --------------------------------------------------------------------------- +// Exporter family (exporter.h) +// --------------------------------------------------------------------------- + +// Thread-local reason for the last failed export on this thread (the C++ +// `g_last_error`, `engine/src/capi/export.cpp`). Cleared at the start of +// every export call; read by [`oakengine_export_last_error`]. +thread_local! { + static EXPORT_LAST_ERROR: RefCell = const { RefCell::new(String::new()) }; +} + +// Thread-local progress callback installed by +// [`oakengine_export_set_progress_callback`] (the C++ `g_progress_fn` / +// `g_progress_userdata`). Per-thread like the C++: the synchronous export +// runs on the installing thread, so the module task events arrive there. +thread_local! { + static EXPORT_PROGRESS: RefCell< + Option<(unsafe extern "C" fn(c_double, *mut c_void), *mut c_void)>, + > = const { RefCell::new(None) }; +} + +/// The module task progress event id (`OAKTASK_EVENT_PROGRESS`, see +/// `oakengine_task_subscribe`). +const EXPORT_EVENT_PROGRESS: c_int = 1; + +fn export_last_error_set(msg: String) { + EXPORT_LAST_ERROR.with(|e| *e.borrow_mut() = msg); +} + +/// Forward the progress events of a running export to the installed +/// callback. Installed as the task subscription only while a callback is +/// set; the module passes the callback's own `userdata` through. +unsafe extern "C" fn export_progress_event(event_id: c_int, value: f64, userdata: *mut c_void) { + if event_id != EXPORT_EVENT_PROGRESS { + return; + } + EXPORT_PROGRESS.with(|slot| { + if let Some((cb, _)) = *slot.borrow() { + // SAFETY: the callback + userdata follow the installer's + // contract; the task emits on its running thread. + unsafe { cb(value, userdata) }; + } + }); +} + +/// Run an export task synchronously on the calling thread — the shared +/// tail of every exporter-family entry point: create the task (taking +/// ownership of `params`), subscribe the installed progress callback, run +/// through [`oakengine_task_start_sync`], read the task error into the +/// thread-local last-error slot, and free the task. +/// +/// Returns OAKENGINE_OK on success, OAKENGINE_E_FAILED otherwise. On the +/// task-creation failure path `params` ownership stays with the caller +/// (mirroring [`oakengine_task_create_export`]). +fn export_run_sync(seq: *mut crate::handle::OakEngineSequence, params: *mut OakEngineEncodingParams) -> c_int { + let task = unsafe { crate::task::oakengine_task_create_export(seq, params) }; + if task.is_null() { + export_last_error_set("failed to create the export task".into()); + return crate::error::OAKENGINE_E_FAILED; + } + // Progress events through the same module subscription the app's + // `start_export` uses (`oakengine_task_subscribe`). + EXPORT_PROGRESS.with(|slot| { + if let Some((_, userdata)) = *slot.borrow() { + unsafe { + crate::task::oakengine_task_subscribe(task, Some(export_progress_event), userdata); + } + } + }); + let ok = unsafe { crate::task::oakengine_task_start_sync(task) }; + let rc = if ok == 1 { + crate::error::OAKENGINE_OK + } else { + let err = export_task_error(task); + export_last_error_set(if err.is_empty() { + "export failed".into() + } else { + err + }); + crate::error::OAKENGINE_E_FAILED + }; + unsafe { crate::task::oakengine_task_free(task) }; + rc +} + +/// Two-stage read of a task's error string (empty when none). +fn export_task_error(task: *mut crate::handle::OakEngineTask) -> String { + unsafe { + let needed = crate::task::oakengine_task_error(task, std::ptr::null_mut(), 0); + if needed <= 0 { + return String::new(); + } + let mut buf = vec![0 as c_char; needed as usize + 1]; + let n = crate::task::oakengine_task_error(task, buf.as_mut_ptr(), buf.len() as c_int); + if n < 0 { + return String::new(); + } + let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len()); + String::from_utf8_lossy(unsafe { + std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) + }) + .into_owned() + } +} + +/// `oakengine_export_render_with_params` — render `seq` to the file the +/// encoding params describe, through the same synchronous export path the +/// app's `start_export` drives (`oakengine_task_create_export` + +/// `oakengine_task_start_sync` + free). +/// +/// Takes ownership of `params` on success (destroyed with the export +/// task, mirroring [`oakengine_task_create_export`]); on the +/// task-creation failure path the caller keeps ownership. The sequence +/// handle is validated for non-NULL only — the C++ "handle is a sequence +/// of the active project" walk has no Rust analogue (created sequences +/// live in their own scratch project, see `oakengine_sequence_new`). +/// +/// Returns OAKENGINE_OK on success; OAKENGINE_E_INVALID for NULL +/// arguments; OAKENGINE_E_FAILED for creation/run failures (see +/// [`oakengine_export_last_error`]). #[no_mangle] pub unsafe extern "C" fn oakengine_export_render_with_params( - _seq: *mut crate::handle::OakEngineSequence, - _params: *const OakEngineEncodingParams, + seq: *mut crate::handle::OakEngineSequence, + params: *const OakEngineEncodingParams, ) -> c_int { - crate::error::OAKENGINE_E_FAILED + guard(|| unsafe { + export_last_error_set(String::new()); + if seq.is_null() || params.is_null() { + export_last_error_set("invalid arguments".into()); + return Err(Error::Invalid); + } + if export_run_sync(seq, params as *mut OakEngineEncodingParams) == crate::error::OAKENGINE_OK { + Ok(()) + } else { + Err(Error::Failed("export failed".into())) + } + }) +} + +/// `oakengine_export_render` — render `seq`'s [in_ts, out_ts) range +/// offline and encode it to `path`. +/// +/// `in_ts`/`out_ts` are frame timestamps in the sequence's frame-rate +/// timebase (the export frame rate is the sequence frame rate). `width`/ +/// `height` <= 0 fall back to the sequence's video dimensions; when they +/// differ the frames are scaled to fit. Video is encoded with the +/// options' codec (default H.264 in an MP4 container), audio with the +/// options' codec (default AAC) at the requested rate/layout (defaults: +/// 48 kHz stereo — the engine has no sequence-audio getter, so the +/// header's "sequence rate/layout" fallback mirrors the app's export +/// dialog instead). The options' codec fields carry the exporter.h +/// `OAKENGINE_EXPORT_VIDEO_*` / `OAKENGINE_EXPORT_AUDIO_*` values, +/// mapped here onto the engine's `ExportFormat` / `ExportCodec` ids. +/// +/// The call blocks until the export finishes; progress is reported +/// through the callback set with [`oakengine_export_set_progress_callback`]. +/// +/// Deviations from the C++ header: no `OAKENGINE_INIT_RENDER` +/// requirement (the Rust render path is CPU-only and self-contained, see +/// `oakengine_render_manager_init`) and no "sequence is part of a +/// project" check (created sequences live in a scratch project). +/// +/// Returns OAKENGINE_OK on success; OAKENGINE_E_INVALID for bad +/// arguments; OAKENGINE_E_FAILED for render/encode failures (see +/// [`oakengine_export_last_error`]). +#[no_mangle] +pub unsafe extern "C" fn oakengine_export_render( + seq: *mut crate::handle::OakEngineSequence, + path: *const c_char, + in_ts: i64, + out_ts: i64, + width: c_int, + height: c_int, + opts: *const crate::pods::OakExportOptions, +) -> c_int { + guard(|| unsafe { + export_last_error_set(String::new()); + if seq.is_null() || path.is_null() || in_ts < 0 || out_ts <= in_ts { + export_last_error_set("invalid arguments".into()); + return Err(Error::Invalid); + } + let o = if opts.is_null() { + crate::pods::OakExportOptions { + video_codec: crate::pods::OAKENGINE_EXPORT_VIDEO_H264, + audio_codec: crate::pods::OAKENGINE_EXPORT_AUDIO_AAC, + video_bit_rate: 0, + audio_sample_rate: 0, + audio_channel_count: 0, + } + } else { + *opts + }; + + // Map the exporter.h codec ids onto the engine's enum ids. + let (format, vcodec) = match o.video_codec { + crate::pods::OAKENGINE_EXPORT_VIDEO_H264 => ( + oakcodec::exportformat::Format::MPEG4Video as i32, + oakcodec::exportcodec::Codec::H264 as i32, + ), + crate::pods::OAKENGINE_EXPORT_VIDEO_H265 => ( + oakcodec::exportformat::Format::MPEG4Video as i32, + oakcodec::exportcodec::Codec::H265 as i32, + ), + crate::pods::OAKENGINE_EXPORT_VIDEO_PNG_SEQUENCE => ( + oakcodec::exportformat::Format::PNG as i32, + oakcodec::exportcodec::Codec::PNG as i32, + ), + _ => { + export_last_error_set(format!("unknown video codec {}", o.video_codec)); + return Err(Error::Invalid); + } + }; + let audio_enabled = o.audio_codec != crate::pods::OAKENGINE_EXPORT_AUDIO_NONE; + let acodec = if audio_enabled { + match o.audio_codec { + crate::pods::OAKENGINE_EXPORT_AUDIO_AAC => oakcodec::exportcodec::Codec::AAC as i32, + crate::pods::OAKENGINE_EXPORT_AUDIO_PCM => oakcodec::exportcodec::Codec::PCM as i32, + _ => { + export_last_error_set(format!("unknown audio codec {}", o.audio_codec)); + return Err(Error::Invalid); + } + } + } else { + 0 + }; + + // Sequence geometry + frame rate (the export frame rate is the + // sequence's). + let mut sw: c_int = 0; + let mut sh: c_int = 0; + let mut par_num: c_int = 1; + let mut par_den: c_int = 1; + Error::from_module(crate::timeline::oakengine_sequence_get_video_params( + seq, + &mut sw, + &mut sh, + &mut par_num, + &mut par_den, + ))?; + let mut rate_num: c_int = 0; + let mut rate_den: c_int = 1; + Error::from_module(crate::timeline::oakengine_sequence_get_frame_rate( + seq, + &mut rate_num, + &mut rate_den, + ))?; + if rate_num <= 0 || rate_den <= 0 { + export_last_error_set("sequence has no valid frame rate".into()); + return Err(Error::Invalid); + } + let out_w = if width > 0 { width } else { sw }; + let out_h = if height > 0 { height } else { sh }; + if out_w <= 0 || out_h <= 0 { + export_last_error_set("sequence has no valid video dimensions".into()); + return Err(Error::Invalid); + } + let sample_rate = if o.audio_sample_rate > 0 { o.audio_sample_rate } else { 48000 }; + let layout: u64 = if o.audio_channel_count > 0 { + match o.audio_channel_count { + 1 => 0x4, // AV_CH_LAYOUT_MONO + 2 => 0x3, // AV_CH_LAYOUT_STEREO + n => { + export_last_error_set(format!( + "unsupported audio channel count {n} (1 = mono, 2 = stereo)" + )); + return Err(Error::Invalid); + } + } + } else { + 0x3 + }; + + // Assemble the encoding params through the public setters (the same + // path the app's `start_export` uses); the task consumes the handle + // once created. + let params = oakengine_encoding_params_create(); + if params.is_null() { + export_last_error_set("failed to create encoding params".into()); + return Err(Error::Failed("failed to create encoding params".into())); + } + let fail = |msg: &str| -> Result<()> { + oakengine_encoding_params_destroy(params); + export_last_error_set(msg.into()); + Err(Error::Failed(msg.into())) + }; + let cpath = std::ffi::CString::new(crate::handle::read_cstr(path)) + .map_err(|_| Error::Failed("invalid path (NUL byte)".into()))?; + if oakengine_encoding_params_set_filename(params, cpath.as_ptr()) != 0 { + return fail("failed to set the export filename"); + } + if oakengine_encoding_params_set_format(params, format) != 0 { + return fail("failed to set the export format"); + } + let pod = OakVideoParamsPod { + width: out_w, + height: out_h, + time_base_num: rate_den, + time_base_den: rate_num, + format: 0, + pixel_aspect_num: par_num.max(1), + pixel_aspect_den: par_den.max(1), + interlacing: 0, + color_range: 0, + divider: 1, + video_type: 0, + premultiplied_alpha: 0, + }; + if oakengine_encoding_params_enable_video(params, &pod, vcodec) != 0 { + return fail("failed to enable video"); + } + if audio_enabled && oakengine_encoding_params_enable_audio(params, sample_rate, layout, 0, acodec) != 0 { + return fail("failed to enable audio"); + } + if o.video_bit_rate > 0 { + oakengine_encoding_params_set_video_bit_rate(params, o.video_bit_rate); + } + // Fit scaling (the header's documented behavior when the output + // size differs from the sequence's). + if oakengine_encoding_params_set_video_scaling_method(params, 0) != 0 { + return fail("failed to set the video scaling method"); + } + // Export range as seconds rationals: frame timestamps in the + // sequence's frame-rate timebase (frame duration = rate_den/rate_num). + let tb_num = i64::from(rate_den); + let tb_den = i64::from(rate_num); + oakengine_encoding_params_set_custom_range(params, in_ts * tb_num, tb_den, out_ts * tb_num, tb_den); + oakengine_encoding_params_set_export_length(params, ((out_ts - in_ts) * tb_num) as c_int, rate_num); + + if export_run_sync(seq, params) == crate::error::OAKENGINE_OK { + Ok(()) + } else { + Err(Error::Failed("export failed".into())) + } + }) +} + +/// `oakengine_export_last_error` — the reason for the last failed export +/// on this thread (buf/size; empty after a successful export). +#[no_mangle] +pub unsafe extern "C" fn oakengine_export_last_error(buf: *mut c_char, buf_size: c_int) -> c_int { + guard_int(|| { + let err = EXPORT_LAST_ERROR.with(|e| e.borrow().clone()); + Ok(unsafe { crate::handle::write_string(&err, buf, buf_size) }) + }) +} + +/// `oakengine_export_set_progress_callback` — install the progress +/// callback used by subsequent [`oakengine_export_render`] / +/// [`oakengine_export_render_with_params`] calls on this thread (NULL +/// disables). The callback receives `fraction` in [0, 1] and is invoked +/// on the exporting thread during the synchronous run. +#[no_mangle] +pub unsafe extern "C" fn oakengine_export_set_progress_callback( + f: Option, + userdata: *mut c_void, +) { + crate::handle::guard_void(|| { + EXPORT_PROGRESS.with(|slot| *slot.borrow_mut() = f.map(|cb| (cb, userdata))); + }); } /// `oakengine_encoding_params_get_last_used` — **not backed** (sequence @@ -1115,9 +1478,9 @@ pub unsafe extern "C" fn oakengine_encoding_start_audio_recording( if m.is_null() { return Err(Error::State); } - let rc = crate::bridge::audio::oakaudio_manager_start_recording( + let rc = crate::stubs::audio::oakaudio_manager_start_recording( m, - &p.pod as *const oakaudio::bridge::codec::EncodingParams, + &p.pod as *const crate::pods::EncodingParamsPOD, errbuf, errbuf_size, ); diff --git a/crates/oakengine/src/common.rs b/crates/oakengine/src/common.rs index 8b2514d3d..390a53a8d 100644 --- a/crates/oakengine/src/common.rs +++ b/crates/oakengine/src/common.rs @@ -30,7 +30,7 @@ use std::ffi::{c_char, c_int, c_void}; use std::sync::{Mutex, OnceLock}; -use crate::bridge::common as c; +use crate::stubs::common as c; use crate::error::Error; use crate::handle::{ box_handle, free_box, guard, guard_int, guard_void, string_result, OakEngineClipboard, diff --git a/crates/oakengine/src/deferred.rs b/crates/oakengine/src/deferred.rs index 59afd2e39..35fcfced2 100644 --- a/crates/oakengine/src/deferred.rs +++ b/crates/oakengine/src/deferred.rs @@ -49,14 +49,15 @@ //! crates lack the C ABI surface — each stub returns its header's //! documented failure value: //! -//! - **codec** (encoding.h, 81/85 wrapped): the preset path/count/name, -//! preset load/save and the sequence-bound export/last-used entry -//! points (`oakengine_encoding_preset_*`, +//! - **codec** (encoding.h, 82/85 wrapped): the preset path/count/name, +//! preset load/save and the sequence-bound last-used entry points +//! (`oakengine_encoding_preset_*`, //! `oakengine_encoding_params_load_file/save_file`, -//! `oakengine_export_render_with_params`, //! `oakengine_encoding_params_get/set_last_used`) are stubs — the -//! oakcodec crate has no preset API and those entry points need the -//! exporter/sequence families. +//! oakcodec crate has no preset API and the last-used pair needs the +//! deferred node/timeline families. The exporter entry point +//! (`oakengine_export_render_with_params`) is backed since M12 (see +//! `crate::codec`, "Exporter family"). //! - **render color** (color.h, 19/31 wrapped): the color-manager list //! queries (colorspace/display/view/look/compliant/luma), the //! standalone config handle and `color_processor_id` / diff --git a/crates/oakengine/src/handle.rs b/crates/oakengine/src/handle.rs index 67186b1d9..fc0aa4e6e 100644 --- a/crates/oakengine/src/handle.rs +++ b/crates/oakengine/src/handle.rs @@ -35,7 +35,7 @@ //! [`write_string`]): the return value is the required length including //! the terminating NUL; negative values are error codes. -use std::ffi::{c_char, c_int, c_void}; +use std::ffi::{c_char, c_int}; use std::panic::{catch_unwind, AssertUnwindSafe}; use crate::error::{Error, Result}; @@ -48,6 +48,95 @@ use crate::error::{Error, Result}; /// `Clone + Copy + Send + Sync` come from the shared type. pub use oakcore_rs::handle::CHandle; +/// Engine-side boxed payloads holding the oaknode domain (single-lib +/// unification). Every `oakengine_*` node-family handle ultimately wraps +/// one of these behind a [`CHandle`]: +/// +/// - projects box [`domain::ProjectArc`] (`Arc>`); +/// - 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`]). diff --git a/crates/oakengine/src/ipc.rs b/crates/oakengine/src/ipc.rs index 59e9e30a9..cb5dfac16 100644 --- a/crates/oakengine/src/ipc.rs +++ b/crates/oakengine/src/ipc.rs @@ -14,22 +14,33 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//! Render-worker IPC: named shared memory holding the frame-slot pools — -//! the Rust port of `engine/render/ipc/` (`sharedmemoryregion.cpp`, -//! `oliveimpl/render/ipc/frameslotpool.cpp`) behind the frozen C ABI in -//! `engine/include/oakengine/ipc.h`. +//! Render-worker IPC: the control-plane NDJSON protocol and the +//! shared-memory frame-slot transport, owned by the oakengine facade and +//! exported through the frozen `oakengine_ipc_*` C ABI +//! (`engine/include/oakengine/ipc.h`); the `oak-worker` binary consumes it +//! purely through the C ABI. The transport is the Rust port of +//! `engine/render/ipc/` + `ipcmessage.cpp`. //! -//! Two pieces, mirroring the C++ exactly: +//! Two halves: //! -//! - [`SharedMemoryRegion`]: a named POSIX segment (`shm_open` + `mmap`, -//! `munmap` + `shm_unlink` on close). One process creates the segment -//! (owner, unlinks on close); the peer attaches to it by key. -//! - [`FrameSlotPool`]: a fixed pool of equal-sized frame slots laid out -//! inside a region, with lock-free hand-off through two +//! - **Control plane.** One compact JSON object per line on the stdio +//! pipes (worker.cpp / ipcmessage.cpp `write_message`/`read_message`). +//! Every message carries a `"type"` string; the field names below are +//! the ones the C++ serializers actually emit +//! (`engine/render/ipc/ipcmessage.cpp`): note `ticket` / `node` / +//! `channels` / `slot` — the longer names (`ticket_id`, `node_uuid`, +//! `channel_count`, `output_slot`) exist only on the C POD structs in +//! `ipc.h`. [`write_message`]/[`error_message`] build the wire lines. +//! - **Data plane.** Named shared memory holding the frame-slot pools — +//! the port of `engine/render/ipc/` (`sharedmemoryregion.cpp`, +//! `frameslotpool.cpp`): [`SharedMemoryRegion`] maps a named POSIX +//! segment (`shm_open` + `mmap`, `munmap` + `shm_unlink` on close), +//! and [`FrameSlotPool`] lays out a fixed pool of equal-sized frame +//! slots inside it with lock-free hand-off through two //! [`SpscRingBuffer`]s of slot indices (free + ready). Each ring is a //! single-producer/single-consumer structure; the filler owns -//! `free.pop` + `ready.push`, the drainer owns `ready.pop` + `free.push`, -//! so no mutex is ever taken. +//! `free.pop` + `ready.push`, the drainer owns `ready.pop` + +//! `free.push`, so no mutex is ever taken. //! //! **The in-memory layout is the version-1 wire protocol** the app and the //! render worker share, and it never changes: the byte offsets below are @@ -40,15 +51,178 @@ //! //! This module is deliberately unsafe-heavy and self-contained: it touches //! raw shared memory and raw POSIX syscalls, and everything else in the -//! facade reaches it through the safe wrapper methods and the C ABI exports -//! at the bottom. +//! crate reaches it through the safe wrapper methods. +//! +//! Message types (M = main/editor, W = worker): +//! handshake M<->W negotiate protocol version + announce shm geometry +//! load_graph M ->W path to a temp file holding the serialized graph +//! render_frame M ->W request a frame render (ticket, node, time, params) +//! frame_ready W ->M a rendered frame is published (slot + ticket) +//! cancel M ->W abandon an in-flight ticket +//! graph_update M ->W reserved (no payload struct yet) +//! shutdown M ->W finish current work and exit cleanly +//! error W ->M worker-side failure report ("message" field) +//! +//! Items the worker does not emit yet (frame_ready, graph_update, +//! `FrameReadyMsg`) and message ids it ignores (`cancel`) are kept as the +//! documented protocol surface; `dead_code` until the frame-slot transport +//! is driven by a real graph (see [`crate::worker`]). + +#![allow(dead_code)] use std::ffi::{c_char, c_int, c_void}; +use std::io::{self, Write}; use std::ptr; use std::sync::atomic::{AtomicU32, Ordering}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; + use crate::handle::{guard_int, guard_ptr}; +/// `"handshake"`. +pub const TYPE_HANDSHAKE: &str = "handshake"; +/// `"load_graph"`. +pub const TYPE_LOAD_GRAPH: &str = "load_graph"; +/// `"render_frame"`. +pub const TYPE_RENDER_FRAME: &str = "render_frame"; +/// `"frame_ready"`. +pub const TYPE_FRAME_READY: &str = "frame_ready"; +/// `"cancel"`. +pub const TYPE_CANCEL: &str = "cancel"; +/// `"graph_update"`. +pub const TYPE_GRAPH_UPDATE: &str = "graph_update"; +/// `"shutdown"`. +pub const TYPE_SHUTDOWN: &str = "shutdown"; +/// `"error"`. +pub const TYPE_ERROR: &str = "error"; + +/// `handshake` — field-for-field equivalent of `oak_ipc_handshake` +/// (ipc.h). Wire field names match the C++ serializer. +#[derive(Serialize, Deserialize, Default, Debug, Clone)] +#[serde(default)] +pub struct HandshakeMsg { + /// Protocol version. + pub protocol_version: i32, + /// Worker->main output shared-memory segment key. + pub shm_key: String, + /// Main->worker input shared-memory segment key (optional). + pub input_shm_key: String, + /// Number of main->worker input frame slots. + pub input_slots: i32, + /// Number of worker->main output frame slots. + pub output_slots: i32, + /// Per-output-slot pixel block size. + pub slot_data_bytes: i64, + /// Per-input-slot pixel block size. + pub input_slot_data_bytes: i64, +} + +impl HandshakeMsg { + /// The worker's startup handshake (`worker.cpp startup_handshake()`). + pub fn to_json(&self) -> Value { + json!({ + "type": TYPE_HANDSHAKE, + "protocol_version": self.protocol_version, + "shm_key": self.shm_key, + "input_shm_key": self.input_shm_key, + "input_slots": self.input_slots, + "output_slots": self.output_slots, + "slot_data_bytes": self.slot_data_bytes, + "input_slot_data_bytes": self.input_slot_data_bytes, + }) + } +} + +/// `render_frame` — request a frame render. Wire names per ipcmessage.cpp: +/// `ticket`, `node`, `channels` (not the ipc.h POD names). +#[derive(Serialize, Deserialize, Default, Debug, Clone)] +#[serde(default)] +pub struct RenderFrameMsg { + /// Correlates with the eventual frame_ready. + pub ticket: i64, + /// Viewer node stable uuid in the loaded graph. + pub node: String, + /// Frame timestamp numerator. + pub time_num: i64, + /// Frame timestamp denominator. + pub time_den: i64, + /// Forced output size (0 = graph default). + pub width: i32, + /// Forced output height (0 = graph default). + pub height: i32, + /// Forced PixelFormat (-1 = default). + pub format: i32, + /// Channel count (0 = default). + pub channels: i32, + /// RenderMode. + pub mode: i32, + /// Optional decoded input slot (-1 = none). + pub input_slot: i32, + /// Ordered decoded input slots. + pub input_slots: Vec, + /// 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`. @@ -77,6 +251,8 @@ pub enum ShmMode { } impl ShmMode { + /// Map the C ABI mode integer (`OAK_IPC_SHM_MODE_CREATE` = 0, + /// `OAK_IPC_SHM_MODE_ATTACH` = 1) back to the enum. fn from_c(v: c_int) -> ShmMode { match v { 0 => ShmMode::Create, @@ -748,7 +924,7 @@ impl SharedMemoryRegion { /// Unmap and (if owner) unlink the segment. Also called by `Drop`. pub fn close(&mut self) { if !self.data.is_null() { - unsafe { libc::munmap(self.data as *mut c_void, self.size) }; + unsafe { libc::munmap(self.data as *mut std::ffi::c_void, self.size) }; self.data = ptr::null_mut(); } if self.fd >= 0 { @@ -804,7 +980,503 @@ impl Drop for SharedMemoryRegion { } } -// --------------------------------------------------------------------------- +#[cfg(test)] +mod tests { + use super::*; + + // ---- Control-plane protocol ------------------------------------------ + + #[test] + fn handshake_wire_format_matches_cpp_field_names() { + let hs = HandshakeMsg { + protocol_version: 1, + shm_key: "olive-rw-1234-0-out".into(), + input_shm_key: "".into(), + input_slots: 0, + output_slots: 6, + slot_data_bytes: 4096, + input_slot_data_bytes: 0, + }; + let value = hs.to_json(); + // Key order is not part of the contract (JSON objects; the C++ + // QJsonObject is hash-ordered too), but the names must match the + // C++ serializer exactly. + assert_eq!(value["type"], "handshake"); + assert_eq!(value["protocol_version"], 1); + assert_eq!(value["shm_key"], "olive-rw-1234-0-out"); + assert_eq!(value["input_shm_key"], ""); + assert_eq!(value["input_slots"], 0); + assert_eq!(value["output_slots"], 6); + assert_eq!(value["slot_data_bytes"], 4096); + assert_eq!(value["input_slot_data_bytes"], 0); + // And the serialized line must parse back to the same object. + let round: serde_json::Value = + serde_json::from_str(&serde_json::to_string(&value).unwrap()).unwrap(); + assert_eq!(round, value); + } + + #[test] + fn render_frame_parse_accepts_cpp_field_names() { + let json = r#"{"type":"render_frame","ticket":42,"node":"abcd","time_num":1,"time_den":24,"width":1920,"height":1080,"format":-1,"channels":0,"mode":0,"input_slot":-1,"input_slots":[],"has_color_transform":false,"color_output":"","color_view":"","color_look":""}"#; + let m: RenderFrameMsg = serde_json::from_str(json).unwrap(); + assert_eq!(m.ticket, 42); + assert_eq!(m.node, "abcd"); + assert_eq!(m.time_num, 1); + assert_eq!(m.time_den, 24); + assert_eq!(m.width, 1920); + assert_eq!(m.input_slot, -1); + } + + #[test] + fn render_frame_defaults_on_missing_fields() { + // The C++ parser defaults missing fields (QJsonValue defaults); + // serde(default) mirrors that. + let m: RenderFrameMsg = + serde_json::from_str(r#"{"type":"render_frame","ticket":7}"#).unwrap(); + assert_eq!(m.ticket, 7); + assert_eq!(m.time_den, 0); + assert!(m.node.is_empty()); + assert!(!m.has_color_transform); + } + + #[test] + fn error_message_carries_ticket_only_when_nonzero() { + assert_eq!( + error_message("boom", None), + json!({ "type": "error", "message": "boom" }) + ); + assert_eq!( + error_message("boom", Some(0)), + json!({ "type": "error", "message": "boom" }) + ); + assert_eq!( + error_message("boom", Some(9)), + json!({ "type": "error", "message": "boom", "ticket": 9 }) + ); + } + + #[test] + fn write_message_emits_one_json_line() { + let mut buf = Vec::new(); + write_message(&mut buf, &json!({ "type": "shutdown" })).unwrap(); + assert_eq!(String::from_utf8(buf).unwrap(), "{\"type\":\"shutdown\"}\n"); + } + + // ---- Shared-memory transport ----------------------------------------- + + /// A unique, temporary POSIX segment key for a test (pid + counter), so + /// parallel test runs never collide. + fn test_key(name: &str) -> String { + static COUNTER: AtomicU32 = AtomicU32::new(0); + let n = COUNTER.fetch_add(1, Ordering::Relaxed); + SharedMemoryRegion::make_key(i64::from(std::process::id()), (n & 0x7FFF) as i32) + + &format!("-{name}") + } + + /// Create one segment and map it a second time — the in-process + /// equivalent of two processes sharing a segment. Returns + /// `(owner_region, peer_region)`; both must be kept alive for the + /// whole test (the peer is an attach that does not unlink). + fn two_mappings(key: &str, size: usize) -> (SharedMemoryRegion, SharedMemoryRegion) { + let mut owner = SharedMemoryRegion::new(); + assert!( + owner.open(key, size, ShmMode::Create), + "create failed: {}", + owner.error() + ); + let mut peer = SharedMemoryRegion::new(); + assert!( + peer.open(key, size, ShmMode::Attach), + "attach failed: {}", + peer.error() + ); + (owner, peer) + } + + // ---- SpscRingBuffer ------------------------------------------------- + + #[test] + fn ring_bytes_needed_matches_cpp_layout() { + // 12 header bytes + capacity * 4. + assert_eq!(SpscRingBuffer::bytes_needed(4), 12 + 16); + assert_eq!(SpscRingBuffer::bytes_needed(5), 12 + 20); + assert_eq!(SpscRingBuffer::bytes_needed(0), 12); + } + + #[test] + fn ring_empty_full_and_single_entry() { + let key = test_key("ring-empty"); + let size = SpscRingBuffer::bytes_needed(4); + let (owner, peer) = two_mappings(&key, size); + // SAFETY: both mappings are live and at least `size` bytes. + let prod = unsafe { SpscRingBuffer::create(owner.data(), 4) }; + let cons = unsafe { SpscRingBuffer::attach(peer.data()) }; + + assert!(unsafe { cons.is_empty_approx() }); + let mut v = 99; + assert!(!unsafe { cons.pop(&mut v) }); + assert_eq!(v, 99); + + assert!(unsafe { prod.push(7) }); + assert!(!unsafe { cons.is_empty_approx() }); + assert_eq!(unsafe { cons.size_approx() }, 1); + assert!(unsafe { cons.pop(&mut v) }); + assert_eq!(v, 7); + assert!(unsafe { cons.is_empty_approx() }); + } + + #[test] + fn ring_capacity_minus_one_live_entries() { + // A ring of capacity N holds at most N-1 entries (one slot is + // always left empty to tell full from empty). + let key = test_key("ring-cap"); + let size = SpscRingBuffer::bytes_needed(4); + let (owner, peer) = two_mappings(&key, size); + // SAFETY: live mappings. + let prod = unsafe { SpscRingBuffer::create(owner.data(), 4) }; + let cons = unsafe { SpscRingBuffer::attach(peer.data()) }; + + for i in 0..3 { + assert!(unsafe { prod.push(i) }); + } + // The 4th push must fail: head would collide with tail. + assert!(!unsafe { prod.push(99) }); + + let mut v = 0; + for expected in 0..3 { + assert!(unsafe { cons.pop(&mut v) }); + assert_eq!(v, expected); + } + assert!(!unsafe { cons.pop(&mut v) }); + } + + #[test] + fn ring_wraparound_preserves_order() { + // Fill, drain, then wrap past the end of the slot array: cursors + // are modulo-capacity, order must be preserved across the wrap. + let key = test_key("ring-wrap"); + let size = SpscRingBuffer::bytes_needed(4); + let (owner, peer) = two_mappings(&key, size); + // SAFETY: live mappings. + let prod = unsafe { SpscRingBuffer::create(owner.data(), 4) }; + let cons = unsafe { SpscRingBuffer::attach(peer.data()) }; + + for i in 0..3 { + assert!(unsafe { prod.push(i) }); + } + let mut v = 0; + for _ in 0..3 { + assert!(unsafe { cons.pop(&mut v) }); + } + // Ring is empty again; push past the wrap point. + for i in 3..6 { + assert!(unsafe { prod.push(i) }); + } + for expected in 3..6 { + assert!(unsafe { cons.pop(&mut v) }); + assert_eq!(v, expected); + } + } + + // ---- FrameSlotPool -------------------------------------------------- + + #[test] + fn framepool_bytes_needed_matches_cpp_offsets() { + // Recompute by hand with the C++ layout: header 64, each ring + // align_up(12 + 4*(n+1), 64), meta align_up(176*n, 64), data + // align_up(slot_bytes, 64) * n. + let check = |n: u32, slot: usize| { + let ring = align_up(12 + 4 * (n as usize + 1), 64); + let expected = + 64 + ring + ring + align_up(176 * n as usize, 64) + align_up(slot, 64) * n as usize; + assert_eq!(FrameSlotPool::bytes_needed(n, slot), expected); + }; + check(4, 4096); + check(6, 1_000_000); + check(1, 64); + check(3, 100); + } + + #[test] + fn framepool_create_attach_two_processes_both_directions() { + // "Two processes": two mappings of the same segment. Owner creates + // the pool; the peer attaches. A filler on one side and a drainer + // on the other exchange slots in both directions. + let key = test_key("pool-bidi"); + let slots = 4u32; + let slot_bytes = 64usize; + let size = FrameSlotPool::bytes_needed(slots, slot_bytes); + let (owner, peer) = two_mappings(&key, size); + + // SAFETY: both mappings are live and sized by bytes_needed. + let filler = unsafe { FrameSlotPool::create(owner.data(), slots, slot_bytes) }; + let drainer = unsafe { FrameSlotPool::attach(peer.data()) }; + + assert!(filler.is_valid()); + assert!(drainer.is_valid()); + assert_eq!(drainer.slot_count(), slots); + assert_eq!(drainer.slot_data_bytes(), slot_bytes); + + // Filler acquires every slot exactly once (seeded free ring), then + // the free ring is empty. + let mut got = Vec::new(); + for _ in 0..slots { + let mut s = 0; + assert!(unsafe { filler.acquire(&mut s) }); + got.push(s); + } + got.sort_unstable(); + assert_eq!(got, vec![0, 1, 2, 3]); + let mut extra = 0; + assert!(!unsafe { filler.acquire(&mut extra) }); + // Drainer sees nothing ready yet. + assert!(!unsafe { drainer.consume(&mut extra) }); + + // Filler writes pixels + meta into two slots and publishes them. + for (i, slot) in [0u32, 2u32].iter().enumerate() { + // SAFETY: `slot` was acquired above. + let data = unsafe { filler.slot_data(*slot) }; + unsafe { ptr::write_bytes(data, (i * 40 + 1) as u8, slot_bytes) }; + // SAFETY: slot in range. + let meta = unsafe { &mut *filler.meta(*slot) }; + meta.id = 100 + *slot as i64; + meta.width = 8; + meta.height = 8; + meta.data_size = slot_bytes as i32; + assert!(unsafe { filler.publish(*slot) }); + } + + // Drainer consumes them through its own mapping and sees the same + // payloads and metadata. + let mut consumed = Vec::new(); + for _ in 0..2 { + let mut s = 0; + assert!(unsafe { drainer.consume(&mut s) }); + // SAFETY: s was consumed. + let data = unsafe { drainer.slot_data_const(s) }; + let meta = unsafe { &*drainer.meta_const(s) }; + assert_eq!(meta.id, 100 + s as i64); + assert_eq!(meta.width, 8); + assert_eq!(meta.data_size, slot_bytes as i32); + // SAFETY: slot_bytes readable in the slot block. + let first = unsafe { *data }; + assert_eq!(first, ((s as usize / 2) * 40 + 1) as u8); + consumed.push(s); + } + consumed.sort_unstable(); + assert_eq!(consumed, vec![0, 2]); + assert!(!unsafe { drainer.consume(&mut extra) }); + + // Drainer releases the slots back; the filler can acquire them + // again — the full round trip through both rings. + for s in consumed { + assert!(unsafe { drainer.release(s) }); + } + let mut s = 0; + assert!(unsafe { filler.acquire(&mut s) }); + assert_eq!(s, 0); + } + + #[test] + fn framepool_wraparound_and_full_edges() { + // Small pool: cycle every slot many times, verifying the rings' + // modulo behavior end to end. + let key = test_key("pool-wrap"); + let slots = 3u32; + let slot_bytes = 32usize; + let size = FrameSlotPool::bytes_needed(slots, slot_bytes); + let (owner, peer) = two_mappings(&key, size); + + // SAFETY: live mappings. + let filler = unsafe { FrameSlotPool::create(owner.data(), slots, slot_bytes) }; + let drainer = unsafe { FrameSlotPool::attach(peer.data()) }; + + for cycle in 0..4u32 { + let mut published = Vec::new(); + for _ in 0..slots { + let mut s = 0; + assert!(unsafe { filler.acquire(&mut s) }, "cycle {cycle}"); + // SAFETY: acquired slot. + unsafe { ptr::write_bytes(filler.slot_data(s), cycle as u8, slot_bytes) }; + // SAFETY: slot in range. + let meta = unsafe { &mut *filler.meta(s) }; + meta.id = i64::from(cycle * 100 + s); + assert!(unsafe { filler.publish(s) }); + published.push(s); + } + // Pool is full on the filler side. + let mut x = 0; + assert!(!unsafe { filler.acquire(&mut x) }); + + // Drain everything on the drainer side. + let mut consumed = Vec::new(); + for _ in 0..slots { + let mut s = 0; + assert!(unsafe { drainer.consume(&mut s) }); + // SAFETY: consumed slot. + let meta = unsafe { &*drainer.meta_const(s) }; + assert_eq!(meta.id, i64::from(cycle * 100 + s)); + // SAFETY: 1 byte readable. + assert_eq!(unsafe { *drainer.slot_data_const(s) }, cycle as u8); + consumed.push(s); + } + assert!(!unsafe { drainer.consume(&mut x) }); + consumed.sort_unstable(); + assert_eq!(consumed, vec![0, 1, 2]); + + for s in consumed { + assert!(unsafe { drainer.release(s) }); + } + } + } + + #[test] + fn framepool_attach_rejects_wrong_magic() { + let key = test_key("pool-badmagic"); + let size = FrameSlotPool::bytes_needed(2, 16); + let (owner, _peer) = two_mappings(&key, size); + // Overwrite the header area with garbage — no pool magic. + // SAFETY: owner mapping is live. + unsafe { ptr::write_bytes(owner.data(), 0xAB, 64) }; + // SAFETY: buffer is live. + let pool = unsafe { FrameSlotPool::attach(owner.data()) }; + assert!(!pool.is_valid()); + assert_eq!(pool.slot_count(), 0); + assert_eq!(pool.slot_data_bytes(), 0); + } + + #[test] + fn framepool_pool_over_reused_segment_is_consistent() { + // A pool that has been cycled fully and then attached fresh reports + // the same geometry as bytes_needed computed it. + let key = test_key("pool-geometry"); + let slots = 5u32; + let slot_bytes = 1000usize; + let size = FrameSlotPool::bytes_needed(slots, slot_bytes); + let (owner, peer) = two_mappings(&key, size); + // SAFETY: live mappings. + let _ = unsafe { FrameSlotPool::create(owner.data(), slots, slot_bytes) }; + let attached = unsafe { FrameSlotPool::attach(peer.data()) }; + assert!(attached.is_valid()); + assert_eq!(attached.slot_count(), slots); + assert_eq!(attached.slot_data_bytes(), slot_bytes); + // Slot stride is 64-aligned (matches the C++ data layout). + // SAFETY: valid pool. + let s0 = unsafe { attached.slot_data(0) }; + let s1 = unsafe { attached.slot_data(1) }; + assert_eq!(s1 as usize - s0 as usize, align_up(slot_bytes, K_ALIGN)); + } + + // ---- SharedMemoryRegion --------------------------------------------- + + #[test] + fn region_create_attach_write_visibility() { + let key = test_key("region-vis"); + let size = 4096usize; + let (mut owner, mut peer) = two_mappings(&key, size); + assert!(owner.is_valid()); + assert!(peer.is_valid()); + assert_eq!(owner.size(), size); + assert_eq!(peer.size(), size); + assert_eq!(owner.key(), key); + assert_eq!(peer.key(), key); + + // Owner writes; peer sees it through its own mapping. + // SAFETY: both mappings are live with `size` bytes. + unsafe { + let dst = owner.data() as *mut u32; + *dst = 0xDEADBEEF; + } + // SAFETY: peer mapping live. + let seen = unsafe { *(peer.data() as *const u32) }; + assert_eq!(seen, 0xDEADBEEF); + + // Peer writes back; owner sees it. + // SAFETY: peer mapping live. + unsafe { + let dst = peer.data() as *mut u32; + *dst = 0x12345678; + } + // SAFETY: owner mapping live. + assert_eq!(unsafe { *(owner.data() as *const u32) }, 0x12345678); + + // Closing the ATTACH side does not unlink: while the owner lives, + // a third mapping can still open the name. + peer.close(); + assert!(!peer.is_valid()); + let mut third = SharedMemoryRegion::new(); + assert!(third.open(&key, size, ShmMode::Attach), "{}", third.error()); + assert!(third.is_valid()); + third.close(); + + // Closing the OWNER unlinks the segment; further attaches fail. + owner.close(); + assert!(!owner.is_valid()); + let mut fourth = SharedMemoryRegion::new(); + assert!(!fourth.open(&key, size, ShmMode::Attach)); + } + + #[test] + fn region_create_replaces_stale_segment() { + // Mirrors the C++: Create unlinks any stale segment with the same + // name first (crash cleanup), so a second Create SUCCEEDS and owns + // a fresh, zeroed segment. + let key = test_key("region-exists"); + let size = 128usize; + let (mut owner, _peer) = two_mappings(&key, size); + assert!(owner.is_valid()); + // SAFETY: owner mapping live. + unsafe { *(owner.data() as *mut u32) = 0xCAFEBABE }; + + let mut second = SharedMemoryRegion::new(); + assert!( + second.open(&key, size, ShmMode::Create), + "{}", + second.error() + ); + assert!(second.is_valid()); + // The replacement segment is fresh (zeroed by create). + // SAFETY: second mapping live. + assert_eq!(unsafe { *(second.data() as *const u32) }, 0); + } + + #[test] + fn region_attach_fails_when_segment_too_small() { + // macOS rounds shm segment sizes up to a 16 KiB minimum, so use + // sizes above that to exercise the size check. + let key = test_key("region-small"); + let (owner, _peer) = two_mappings(&key, 4096); + assert!(owner.is_valid()); + + // Attaching with a larger size than the segment must fail (the + // fstat check, mirroring the C++). + let mut big = SharedMemoryRegion::new(); + assert!(!big.open(&key, 65536, ShmMode::Attach)); + assert!(!big.is_valid()); + assert!(!big.error().is_empty()); + } + + #[test] + fn region_make_key_format() { + assert_eq!(SharedMemoryRegion::make_key(4242, 3), "olive-rw-4242-3"); + assert_eq!(SharedMemoryRegion::make_key(1, 0), "olive-rw-1-0"); + } + + #[test] + fn region_keys_are_isolation_safe() { + // Keys with slashes are flattened to a single-slash POSIX name. + let key = "a/b/c"; + let size = 64usize; + let (mut owner, mut peer) = two_mappings(key, size); + assert!(owner.is_valid()); + assert!(peer.is_valid()); + // The actual POSIX name is "/a_b_c". + // SAFETY: mapping live. + unsafe { *(owner.data() as *mut u32) = 7 }; + // SAFETY: peer mapping live. + assert_eq!(unsafe { *(peer.data() as *const u32) }, 7); + } +} // C ABI exports (engine/include/oakengine/ipc.h) // --------------------------------------------------------------------------- @@ -1219,7 +1891,7 @@ pub unsafe extern "C" fn oakengine_ipc_framepool_release( // --------------------------------------------------------------------------- #[cfg(test)] -mod tests { +mod cabi_tests { use super::*; /// A unique, temporary POSIX segment key for a test (pid + counter), so diff --git a/crates/oakengine/src/lib.rs b/crates/oakengine/src/lib.rs index 941596688..bc96ee650 100644 --- a/crates/oakengine/src/lib.rs +++ b/crates/oakengine/src/lib.rs @@ -66,7 +66,6 @@ pub use oaknode; pub mod audio; -pub mod bridge; pub mod codec; pub mod common; pub mod deferred; @@ -77,28 +76,35 @@ pub mod ipc; pub mod linkage; pub mod node; pub mod plugin; +pub mod pods; pub mod render; +pub mod stubs; pub mod task; pub mod testmedia; pub mod timeline; pub mod undo; pub mod worker; +/// The former `tests/*.rs` integration tests, now unit tests (the facade +/// is cdylib-only, so integration tests cannot link it as an rlib crate; +/// see `test_support/mod.rs`). +#[cfg(test)] +#[path = "test_support/mod.rs"] +mod tests; #[cfg(test)] mod test_link { // The lib's own unit-test binary must link the module crates' rlibs to - // satisfy the facade's `extern "C"` imports that the unit tests compile - // in — e.g. the worker session's oakrender display renderer (src/worker.rs). + // satisfy the facade's imports that the unit tests compile in — e.g. + // the render family's oakrender display renderer (src/render.rs). // The integration tests do the same through tests/common/mod.rs // `force_link()`; this covers the `cargo test` unit-test binary. #![allow(dead_code)] fn force_link() -> usize { let fns: [usize; 4] = [ - oakrender::ffi::renderer::oakrender_display_renderer_create_opengl as *const () - as usize, - oaknode::ffi::project::oaknode_project_init as *const () as usize, - oaktimeline::ffi::marker::oaktimeline_marker_list_create as *const () as usize, - oaktask::ffi::manager::oaktask_manager_init as *const () as usize, + oakrender::backend::DisplayRenderer::new as *const () as usize, + oaknode::project::Project::new as *const () as usize, + oaktimeline::marker::TimelineMarkerList::new as *const () as usize, + oaktask::manager::TaskManager::init as *const () as usize, ]; fns.iter().sum() } diff --git a/crates/oakengine/src/linkage.rs b/crates/oakengine/src/linkage.rs index d40e63f9b..b05e2fa1a 100644 --- a/crates/oakengine/src/linkage.rs +++ b/crates/oakengine/src/linkage.rs @@ -39,23 +39,24 @@ /// runtime via dlsym(RTLD_DEFAULT) and they must be present in the dylib /// for that lookup to succeed. fn force_link() -> usize { - let fns: [usize; 13] = [ + let fns: [usize; 11] = [ // oakcore-rs (pure value types; referenced so its rlib is linked). oakcore_rs::Rational::new(1, 2).numerator() as usize, - // One exported C ABI symbol per module crate. - oakundo::ffi::undostack::oakundo_undostack_init as usize, - oakcommon::ffi::config::oakcommon_config_get_int as usize, - oaktimeline::ffi::marker::oaktimeline_marker_list_create as usize, - oakcodec::ffi::format::oakcodec_encoding_format_count as usize, - oakaudio::ffi::waveform::oakaudio_waveform_length as usize, - oakrender::ffi::cache::oakrender_cache_indicator_height as usize, - oaktask::ffi::manager::oaktask_manager_init as usize, - oakplugin::ffi::oakplugin_host_plugin_count as usize, - oaknode::ffi::project::oaknode_project_init as usize, - // oaknode's dlsym(RTLD_DEFAULT) targets (see tests/common/mod.rs). - oakcommon::ffi::xmlutils::oakcommon_xml_writer_init as usize, - oakcommon::ffi::xmlutils::oakcommon_xml_reader_init as usize, - oakundo::ffi::command::oakundo_command_init as usize, + // One public direct-Rust symbol per module crate. oakundo/oakcommon + // no longer export a C ABI; their handle-level Rust API functions + // serve as the link anchors. + oakundo::undostack::undostack_init as usize, + oakcommon::configstore::ConfigStore::instance as usize, + oaktimeline::marker::TimelineMarkerList::new as usize, + oakcodec::exportformat::Format::get_name as usize, + oakrender::manager::RenderManager::init as usize, + oaktask::manager::TaskManager::init as usize, + oaknode::project::Project::new as usize, + // oaknode's serializer resolves oakcommon XML/undo symbols at + // runtime; anchors for the dylib. + oakcommon::xmlutils::XmlWriter::new as usize, + oakcommon::xmlutils::XmlReader::new as usize, + oakundo::undocommand::command_init as usize, ]; fns.iter().sum() } diff --git a/crates/oakengine/src/node.rs b/crates/oakengine/src/node.rs index 4a9f565ee..c9176567b 100644 --- a/crates/oakengine/src/node.rs +++ b/crates/oakengine/src/node.rs @@ -19,8 +19,8 @@ use std::ffi::{c_char, c_int, c_void}; -use crate::bridge::common as c; -use crate::bridge::node as n; +use crate::stubs::common as c; +use crate::stubs::node as n; use crate::common::OakVideoParamsPod; use crate::error::{Error, Result}; use crate::handle::{ @@ -2518,13 +2518,16 @@ pub unsafe extern "C" fn oakengine_node_input_is_keyframed( self_: *const OakEngineNode, input_id: *const c_char, ) -> c_int { - // Stub: the oaknode module has no keyframing-enabled query. guard_int(|| unsafe { if self_.is_null() || input_id.is_null() { return Ok(0); } - let _ = unbox(self_)?; - Ok(0) + let rc = n::oaknode_node_is_input_keyframing(unbox(self_)?, input_id); + if rc < 0 { + Err(Error::Module(rc)) + } else { + Ok(rc) + } }) } @@ -2534,13 +2537,16 @@ pub unsafe extern "C" fn oakengine_node_keyframe_count( self_: *const OakEngineNode, input_id: *const c_char, ) -> c_int { - // Stub: the oaknode module has no keyframe enumeration C ABI. guard_int(|| unsafe { if self_.is_null() || input_id.is_null() { return Ok(0); } - let _ = unbox(self_)?; - Ok(0) + let rc = n::oaknode_node_keyframe_count(unbox(self_)?, input_id); + if rc < 0 { + Err(Error::Module(rc)) + } else { + Ok(rc) + } }) } @@ -2553,16 +2559,20 @@ pub unsafe extern "C" fn oakengine_node_keyframe_at( time_ts: *mut i64, value: *mut OakNodeValue, ) -> c_int { - // Stub: see `oakengine_node_keyframe_count`. guard(|| unsafe { - if self_.is_null() || input_id.is_null() { + if self_.is_null() || input_id.is_null() || time_ts.is_null() || value.is_null() { return Err(Error::Invalid); } - let _ = unbox(self_)?; - let _ = index; - let _ = time_ts; - let _ = value; - Err(Error::NotFound) + let h = unbox(self_)?; + let tb = time_base_for(h); + let mut num: i64 = 0; + let mut den: i64 = 0; + let rc = n::oaknode_node_keyframe_at(h, input_id, index, &mut num, &mut den, value); + Error::from_module(rc)?; + // Rational seconds -> frame timestamp in the project time base. + let ts = num as i128 * tb.1 as i128 / den as i128 / tb.0 as i128; + *time_ts = ts as i64; + Ok(()) }) } @@ -2578,14 +2588,38 @@ pub unsafe extern "C" fn oakengine_node_keyframe_get_easing( y2: *mut f32, type_: *mut c_int, ) -> c_int { - // Stub: see `oakengine_node_keyframe_count`. guard(|| unsafe { - if self_.is_null() || input_id.is_null() { + if self_.is_null() || input_id.is_null() || type_.is_null() { return Err(Error::Invalid); } - let _ = unbox(self_)?; - let _ = (index, x1, y1, x2, y2, type_); - Err(Error::NotFound) + let h = unbox(self_)?; + // Locate the key's time through the count/at pair. + let mut num: i64 = 0; + let mut den: i64 = 0; + let mut dummy = OakNodeValue::none(); + Error::from_module(n::oaknode_node_keyframe_at( + h, input_id, index, &mut num, &mut den, &mut dummy, + ))?; + Error::from_module(n::oaknode_node_keyframe_type_at( + h, input_id, num, den, type_, + ))?; + if !x1.is_null() { + let mut bx: f64 = 0.0; + let mut by: f64 = 0.0; + if n::oaknode_node_keyframe_bezier_at(h, input_id, num, den, 0, &mut bx, &mut by) == 0 { + *x1 = bx as f32; + *y1 = by as f32; + } + } + if !x2.is_null() { + let mut bx: f64 = 0.0; + let mut by: f64 = 0.0; + if n::oaknode_node_keyframe_bezier_at(h, input_id, num, den, 1, &mut bx, &mut by) == 0 { + *x2 = bx as f32; + *y2 = by as f32; + } + } + Ok(()) }) } @@ -2634,14 +2668,14 @@ pub unsafe extern "C" fn oakengine_node_keyframe_remove( input_id: *const c_char, time_ts: i64, ) -> c_int { - // Stub: the oaknode module has no remove-keyframe C ABI. guard(|| unsafe { if self_.is_null() || input_id.is_null() { return Err(Error::Invalid); } - let _ = unbox(self_)?; - let _ = time_ts; - Err(Error::NotFound) + let h = unbox(self_)?; + let tb = time_base_for(h); + let (t_num, t_den) = ts_to_time(time_ts, tb); + Error::from_module(n::oaknode_node_remove_keyframe(h, input_id, t_num, t_den)) }) } @@ -2685,11 +2719,31 @@ pub unsafe extern "C" fn oakengine_node_insert_keyframe_command( pub unsafe extern "C" fn oakengine_node_remove_keyframe_command( keyframe: *mut OakEngineKeyframe, ) -> *mut c_void { - // Stub: the module has no remove-keyframe command creator (the module - // keyframe handles are detached values, not track members). guard_ptr(|| unsafe { - let _ = unbox(keyframe); - Ok(std::ptr::null_mut()) + let h = unbox(keyframe)?; + let mut num: i64 = 0; + let mut den: i64 = 0; + Error::from_module(n::oaknode_keyframe_get_time(h, &mut num, &mut den))?; + let mut input_buf = [0 as c_char; 256]; + let rc = n::oaknode_keyframe_get_input(h, input_buf.as_mut_ptr(), 256); + if rc < 0 { + return Ok(std::ptr::null_mut()); + } + let mut parent = CHandle::null(); + Error::from_module(n::oaknode_keyframe_get_parent(h, &mut parent))?; + // Build the undo command as a closure over the remove path (the + // module has no remove-key command creator; the closure carries + // the same semantics). + let cmd = crate::stubs::node::box_keyframe_remove_command( + parent, + input_buf.as_ptr(), + num, + den, + ); + if cmd.ctx.is_null() { + return Ok(std::ptr::null_mut()); + } + Ok(command_box(cmd)?.cast()) }) } @@ -2746,17 +2800,52 @@ pub unsafe extern "C" fn oakengine_node_keyframe_set_easing( x2: f32, y2: f32, ) -> c_int { - // Stub: see `oakengine_node_keyframe_count` (no easing accessors). guard(|| unsafe { if self_.is_null() || input_id.is_null() { return Err(Error::Invalid); } - let _ = unbox(self_)?; - let _ = (time_ts, type_, x1, y1, x2, y2); - Err(Error::NotFound) + let h = unbox(self_)?; + let tb = time_base_for(h); + let (t_num, t_den) = ts_to_time(time_ts, tb); + Error::from_module(n::oaknode_node_keyframe_set_type( + h, input_id, t_num, t_den, type_, + ))?; + Error::from_module(n::oaknode_node_keyframe_set_bezier( + h, input_id, t_num, t_den, 0, x1 as f64, y1 as f64, + ))?; + Error::from_module(n::oaknode_node_keyframe_set_bezier( + h, input_id, t_num, t_den, 1, x2 as f64, y2 as f64, + )) }) } + +/// Apply a value-at-time write WITHOUT pushing an undo row (the +/// `*_many` keyframe editors apply live writes; documented deviation +/// from the C++ multi commands). +/// +/// # Safety +/// `h` must be a live module node handle; `input_id` a valid +/// NUL-terminated string; `v` a live POD. +unsafe fn live_set_value_at_time( + h: CHandle, + input_id: *const c_char, + t_num: i64, + t_den: i64, + v: *const OakNodeValue, +) -> Result<()> { + unsafe { + let mut cmd: CHandle = CHandle::null(); + Error::from_module(n::oaknode_node_set_input_at_time_undoable( + h, input_id, t_num, t_den, v, 0, &mut cmd, + ))?; + let rc = oakundo::undocommand::command_redo_now(cmd); + let mut cmd_h = cmd; + oakundo::undocommand::command_free(&mut cmd_h); + Error::from_module(rc) + } +} + /// `oakengine_node_keyframes_set_type_many`. #[no_mangle] pub unsafe extern "C" fn oakengine_node_keyframes_set_type_many( @@ -2768,14 +2857,23 @@ pub unsafe extern "C" fn oakengine_node_keyframes_set_type_many( count: c_int, type_: c_int, ) -> c_int { - // Stub: see `oakengine_node_keyframe_count`. - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { + // Live per-key type writes (the C++ grouped them into one command; + // documented deviation — no undo row is created). + guard(|| unsafe { + if self_.is_null() || input_id.is_null() || count < 0 || (count > 0 && times_ts.is_null()) { return Err(Error::Invalid); } - let _ = unbox(self_)?; - let _ = (element, times_ts, tracks, count, type_); - Err(Error::NotFound) + let h = unbox(self_)?; + let tb = time_base_for(h); + let _ = (element, tracks); + for i in 0..count as usize { + let ts = *times_ts.add(i); + let (t_num, t_den) = ts_to_time(ts, tb); + Error::from_module(n::oaknode_node_keyframe_set_type( + h, input_id, t_num, t_den, type_, + ))?; + } + Ok(()) }) } @@ -2790,14 +2888,29 @@ pub unsafe extern "C" fn oakengine_node_keyframes_set_time_many( count: c_int, new_time_ts: i64, ) -> c_int { - // Stub: see `oakengine_node_keyframe_count`. - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { + // Live per-key re-times: each key is removed from its old time and + // re-inserted at the new one with its value (no undo row; documented). + guard(|| unsafe { + if self_.is_null() || input_id.is_null() || count < 0 || (count > 0 && old_times_ts.is_null()) + { return Err(Error::Invalid); } - let _ = unbox(self_)?; - let _ = (element, old_times_ts, tracks, count, new_time_ts); - Err(Error::NotFound) + let h = unbox(self_)?; + let tb = time_base_for(h); + let (new_num, new_den) = ts_to_time(new_time_ts, tb); + let _ = (element, tracks); + for i in 0..count as usize { + let old_ts = *old_times_ts.add(i); + let (old_num, old_den) = ts_to_time(old_ts, tb); + let mut v = OakNodeValue::none(); + let rc = n::oaknode_node_get_input_at_time(h, input_id, old_num, old_den, &mut v); + if rc != 0 { + return Err(Error::Module(rc)); + } + Error::from_module(n::oaknode_node_remove_keyframe(h, input_id, old_num, old_den))?; + live_set_value_at_time(h, input_id, new_num, new_den, &v)?; + } + Ok(()) }) } @@ -2813,14 +2926,25 @@ pub unsafe extern "C" fn oakengine_node_keyframes_set_value_many( values: *const OakNodeValue, old_values: *const OakNodeValue, ) -> c_int { - // Stub: see `oakengine_node_keyframe_count`. - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { + // Live per-key value writes (no undo row; the C++ grouped them — + // documented deviation). + guard(|| unsafe { + if self_.is_null() + || input_id.is_null() + || count < 0 + || (count > 0 && (times_ts.is_null() || values.is_null())) + { return Err(Error::Invalid); } - let _ = unbox(self_)?; - let _ = (element, times_ts, tracks, count, values, old_values); - Err(Error::NotFound) + let h = unbox(self_)?; + let tb = time_base_for(h); + let _ = (element, tracks, old_values); + for i in 0..count as usize { + let ts = *times_ts.add(i); + let (t_num, t_den) = ts_to_time(ts, tb); + live_set_value_at_time(h, input_id, t_num, t_den, values.add(i))?; + } + Ok(()) }) } @@ -2838,14 +2962,25 @@ pub unsafe extern "C" fn oakengine_node_keyframes_set_bezier_many( out_x: f64, out_y: f64, ) -> c_int { - // Stub: see `oakengine_node_keyframe_count`. - guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { + // Live per-key bezier writes (no undo row; documented deviation). + guard(|| unsafe { + if self_.is_null() || input_id.is_null() || count < 0 || (count > 0 && times_ts.is_null()) { return Err(Error::Invalid); } - let _ = unbox(self_)?; - let _ = (element, times_ts, tracks, count, in_x, in_y, out_x, out_y); - Err(Error::NotFound) + let h = unbox(self_)?; + let tb = time_base_for(h); + let _ = (element, tracks); + for i in 0..count as usize { + let ts = *times_ts.add(i); + let (t_num, t_den) = ts_to_time(ts, tb); + Error::from_module(n::oaknode_node_keyframe_set_bezier( + h, input_id, t_num, t_den, 0, in_x, in_y, + ))?; + Error::from_module(n::oaknode_node_keyframe_set_bezier( + h, input_id, t_num, t_den, 1, out_x, out_y, + ))?; + } + Ok(()) }) } @@ -2863,14 +2998,17 @@ pub unsafe extern "C" fn oakengine_node_keyframe_set_bezier_point( old_x: f64, old_y: f64, ) -> c_int { - // Stub: see `oakengine_node_keyframe_count`. guard(|| unsafe { - if self_.is_null() || input_id.is_null() { + if self_.is_null() || input_id.is_null() || (point_index != 0 && point_index != 1) { return Err(Error::Invalid); } - let _ = unbox(self_)?; - let _ = (element, time_ts, track, point_index, x, y, old_x, old_y); - Err(Error::NotFound) + let h = unbox(self_)?; + let tb = time_base_for(h); + let (t_num, t_den) = ts_to_time(time_ts, tb); + let _ = (element, track, old_x, old_y); + Error::from_module(n::oaknode_node_keyframe_set_bezier( + h, input_id, t_num, t_den, point_index, x, y, + )) }) } @@ -2881,14 +3019,11 @@ pub unsafe extern "C" fn oakengine_node_keyframes_clear( self_: *mut OakEngineNode, input_id: *const c_char, ) -> c_int { - // Stub: the module has no clear-keyframes command; since it also has - // no keyframes, the documented no-op result is returned. guard(|| unsafe { if self_.is_null() || input_id.is_null() { return Err(Error::Invalid); } - let _ = unbox(self_)?; - Ok(()) + Error::from_module(n::oaknode_node_clear_keyframes(unbox(self_)?, input_id)) }) } @@ -3040,14 +3175,19 @@ pub unsafe extern "C" fn oakengine_node_input_is_keyframed_ex( input_id: *const c_char, element: c_int, ) -> c_int { - // Stub: see `oakengine_node_input_is_keyframed`. + // Whole-value tracks: the element selector is recorded but the track + // query covers the (input, -1) track (documented deviation). guard_int(|| unsafe { if self_.is_null() || input_id.is_null() { return Ok(0); } - let _ = unbox(self_)?; + let rc = n::oaknode_node_is_input_keyframing(unbox(self_)?, input_id); let _ = element; - Ok(0) + if rc < 0 { + Err(Error::Module(rc)) + } else { + Ok(rc) + } }) } @@ -3101,14 +3241,29 @@ pub unsafe extern "C" fn oakengine_node_input_get_default_value( track: c_int, out: *mut OakNodeValue, ) -> c_int { - // Stub: the oaknode module has no default-value export. guard(|| unsafe { if self_.is_null() || input_id.is_null() || out.is_null() { return Err(Error::Invalid); } - let _ = unbox(self_)?; let _ = track; - Err(Error::NotFound) + // The declared type's default maps back into the POD (same + // conversion as the getter family). + let mut ty: c_int = 0; + Error::from_module(n::oaknode_node_input_get_type( + unbox(self_)?, + input_id, + &mut ty, + ))?; + // Best-effort: the default of the enabled input is Boolean(true); + // other inputs report the None POD (documented deviation). + let default = OakNodeValue { + kind: ty, + num: if ty == value_type::BOOL { 1 } else { 0 }, + den: 0, + f: [0.0; 4], + }; + *out = default; + Ok(()) }) } @@ -5271,14 +5426,19 @@ pub unsafe extern "C" fn oakengine_node_keyframe_count_on_track( element: c_int, track: c_int, ) -> c_int { - // Stub: see `oakengine_node_keyframe_count`. + // Whole-value tracks: the element/track selectors are recorded but + // the count covers the (input, -1) track (documented deviation). guard_int(|| unsafe { if self_.is_null() || input_id.is_null() { return Ok(0); } - let _ = unbox(self_)?; + let rc = n::oaknode_node_keyframe_count(unbox(self_)?, input_id); let _ = (element, track); - Ok(0) + if rc < 0 { + Err(Error::Module(rc)) + } else { + Ok(rc) + } }) } @@ -5293,14 +5453,35 @@ pub unsafe extern "C" fn oakengine_node_keyframes_toggle_at_time( on: c_int, undo_name: *const c_char, ) -> c_int { - // Stub: see `oakengine_node_keyframe_count`. guard(|| unsafe { if self_.is_null() || input_id.is_null() { return Err(Error::Invalid); } - let _ = unbox(self_)?; - let _ = (element, time_ts, track, on, undo_name); - Err(Error::NotFound) + let h = unbox(self_)?; + let tb = time_base_for(h); + let (t_num, t_den) = ts_to_time(time_ts, tb); + let has = n::oaknode_node_has_keyframe_at_time(h, input_id, t_num, t_den); + if has < 0 { + return Err(Error::Module(has)); + } + let _ = (element, track); + if on != 0 && has == 0 { + // Insert a key at the time carrying the current value. + let mut v = OakNodeValue::none(); + let rc = n::oaknode_node_get_input_at_time(h, input_id, t_num, t_den, &mut v); + if rc != 0 { + return Err(Error::Module(rc)); + } + let mut cmd: CHandle = CHandle::null(); + Error::from_module(n::oaknode_node_set_input_at_time_undoable( + h, input_id, t_num, t_den, &v, 0, &mut cmd, + ))?; + push_command(cmd, if undo_name.is_null() { "Toggle Keyframe" } else { "" }) + } else if on == 0 && has == 1 { + Error::from_module(n::oaknode_node_remove_keyframe(h, input_id, t_num, t_den)) + } else { + Ok(()) + } }) } @@ -5313,17 +5494,70 @@ pub unsafe extern "C" fn oakengine_node_has_keyframe_at_time( time_ts: i64, track: c_int, ) -> c_int { - // Stub: see `oakengine_node_keyframe_count`. guard_int(|| unsafe { if self_.is_null() || input_id.is_null() { return Ok(0); } - let _ = unbox(self_)?; - let _ = (element, time_ts, track); - Ok(0) + let h = unbox(self_)?; + let tb = time_base_for(h); + let (t_num, t_den) = ts_to_time(time_ts, tb); + let _ = (element, track); + let rc = n::oaknode_node_has_keyframe_at_time(h, input_id, t_num, t_den); + if rc < 0 { + Err(Error::Module(rc)) + } else { + Ok(rc) + } }) } + +/// First (earliest) or last (latest) keyframe time of an input, written +/// as a rational-seconds pair (0/1 when the input has no keys). +/// +/// # Safety +/// `self_` must be a live engine node handle; `input_id` a valid +/// NUL-terminated string. +unsafe fn keyframe_extreme_time( + self_: *const OakEngineNode, + input_id: *const c_char, + earliest: bool, + num: *mut i64, + den: *mut i64, +) -> Result { + unsafe { + if self_.is_null() || input_id.is_null() { + return Ok(0); + } + let h = unbox(self_)?; + let count = n::oaknode_node_keyframe_count(h, input_id); + if count <= 0 { + if !num.is_null() { + *num = 0; + } + if !den.is_null() { + *den = 1; + } + return Ok(0); + } + let index = if earliest { 0 } else { count - 1 }; + let mut k_num: i64 = 0; + let mut k_den: i64 = 0; + let mut dummy = OakNodeValue::none(); + let rc = n::oaknode_node_keyframe_at(h, input_id, index, &mut k_num, &mut k_den, &mut dummy); + if rc != 0 { + return Err(Error::Module(rc)); + } + if !num.is_null() { + *num = k_num; + } + if !den.is_null() { + *den = k_den; + } + Ok(1) + } +} + /// `oakengine_node_keyframe_earliest_time`. #[no_mangle] pub unsafe extern "C" fn oakengine_node_keyframe_earliest_time( @@ -5333,21 +5567,9 @@ pub unsafe extern "C" fn oakengine_node_keyframe_earliest_time( num: *mut i64, den: *mut i64, ) -> c_int { - // Stub: see `oakengine_node_keyframe_count`. guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - set_node_error("invalid arguments"); - return Ok(0); - } - let _ = unbox(self_)?; let _ = element; - if !num.is_null() { - *num = 0; - } - if !den.is_null() { - *den = 1; - } - Ok(0) + keyframe_extreme_time(self_, input_id, true, num, den) }) } @@ -5360,22 +5582,85 @@ pub unsafe extern "C" fn oakengine_node_keyframe_latest_time( num: *mut i64, den: *mut i64, ) -> c_int { - // Stub: see `oakengine_node_keyframe_count`. guard_int(|| unsafe { + let _ = element; + keyframe_extreme_time(self_, input_id, false, num, den) + }) +} + + +/// Closest keyframe time strictly before (or at/after) a frame timestamp, +/// written as rational seconds (0/1 when none — the documented neutral +/// result). +/// +/// # Safety +/// `self_` must be a live engine node handle; `input_id` a valid +/// NUL-terminated string. +unsafe fn closest_keyframe_time( + self_: *const OakEngineNode, + input_id: *const c_char, + element: c_int, + time_ts: i64, + track: c_int, + before: bool, + num: *mut i64, + den: *mut i64, +) -> Result { + unsafe { if self_.is_null() || input_id.is_null() { - set_node_error("invalid arguments"); return Ok(0); } - let _ = unbox(self_)?; - let _ = element; - if !num.is_null() { - *num = 0; + let h = unbox(self_)?; + let tb = time_base_for(h); + let (t_num, t_den) = ts_to_time(time_ts, tb); + let target = t_num as f64 / t_den as f64; + let _ = (element, track); + let count = n::oaknode_node_keyframe_count(h, input_id); + let mut best: Option<(i64, i64)> = None; + for i in 0..count { + let mut k_num: i64 = 0; + let mut k_den: i64 = 0; + let mut dummy = OakNodeValue::none(); + if n::oaknode_node_keyframe_at(h, input_id, i, &mut k_num, &mut k_den, &mut dummy) != 0 { + continue; + } + let k = k_num as f64 / k_den as f64; + let matches = if before { k < target } else { k >= target }; + if !matches { + continue; + } + match best { + Some((bn, bd)) => { + let b = bn as f64 / bd as f64; + let closer = if before { k > b } else { k < b }; + if closer { + best = Some((k_num, k_den)); + } + } + None => best = Some((k_num, k_den)), + } } - if !den.is_null() { - *den = 1; + match best { + Some((n_, d_)) => { + if !num.is_null() { + *num = n_; + } + if !den.is_null() { + *den = d_; + } + Ok(1) + } + None => { + if !num.is_null() { + *num = 0; + } + if !den.is_null() { + *den = 1; + } + Ok(0) + } } - Ok(0) - }) + } } /// `oakengine_node_keyframe_closest_time_before`. @@ -5389,21 +5674,8 @@ pub unsafe extern "C" fn oakengine_node_keyframe_closest_time_before( num: *mut i64, den: *mut i64, ) -> c_int { - // Stub: see `oakengine_node_keyframe_count`. guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - set_node_error("invalid arguments"); - return Ok(0); - } - let _ = unbox(self_)?; - let _ = (element, time_ts, track); - if !num.is_null() { - *num = 0; - } - if !den.is_null() { - *den = 1; - } - Ok(0) + closest_keyframe_time(self_, input_id, element, time_ts, track, true, num, den) }) } @@ -5418,21 +5690,8 @@ pub unsafe extern "C" fn oakengine_node_keyframe_closest_time_after( num: *mut i64, den: *mut i64, ) -> c_int { - // Stub: see `oakengine_node_keyframe_count`. guard_int(|| unsafe { - if self_.is_null() || input_id.is_null() { - set_node_error("invalid arguments"); - return Ok(0); - } - let _ = unbox(self_)?; - let _ = (element, time_ts, track); - if !num.is_null() { - *num = 0; - } - if !den.is_null() { - *den = 1; - } - Ok(0) + closest_keyframe_time(self_, input_id, element, time_ts, track, false, num, den) }) } @@ -5445,10 +5704,23 @@ pub unsafe extern "C" fn oakengine_node_keyframe_handle_on_track( track: c_int, index: c_int, ) -> *mut OakEngineKeyframe { - // Stub: see `oakengine_node_keyframe_count`. - guard_ptr(|| { - let _ = (self_, input_id, element, track, index); - Ok(std::ptr::null_mut()) + guard_ptr(|| unsafe { + if self_.is_null() || input_id.is_null() || index < 0 { + return Ok(std::ptr::null_mut()); + } + let h = unbox(self_)?; + let mut num: i64 = 0; + let mut den: i64 = 0; + let mut dummy = OakNodeValue::none(); + let rc = n::oaknode_node_keyframe_at(h, input_id, index, &mut num, &mut den, &mut dummy); + if rc != 0 { + return Ok(std::ptr::null_mut()); + } + let kf = n::oaknode_keyframe_create(num, den, &dummy, 0, track, element, input_id, h); + if kf.ctx.is_null() { + return Ok(std::ptr::null_mut()); + } + Ok(box_handle::(kf)) }) } @@ -5462,10 +5734,24 @@ pub unsafe extern "C" fn oakengine_node_keyframe_handle_at_time( time_num: i64, time_den: i64, ) -> *mut OakEngineKeyframe { - // Stub: see `oakengine_node_keyframe_count`. - guard_ptr(|| { - let _ = (self_, input_id, element, track, time_num, time_den); - Ok(std::ptr::null_mut()) + guard_ptr(|| unsafe { + if self_.is_null() || input_id.is_null() || time_den == 0 { + return Ok(std::ptr::null_mut()); + } + let h = unbox(self_)?; + let rc = n::oaknode_node_has_keyframe_at_time(h, input_id, time_num, time_den); + if rc != 1 { + return Ok(std::ptr::null_mut()); + } + let mut dummy = OakNodeValue::none(); + if n::oaknode_node_get_input_at_time(h, input_id, time_num, time_den, &mut dummy) != 0 { + return Ok(std::ptr::null_mut()); + } + let kf = n::oaknode_keyframe_create(time_num, time_den, &dummy, 0, track, element, input_id, h); + if kf.ctx.is_null() { + return Ok(std::ptr::null_mut()); + } + Ok(box_handle::(kf)) }) } @@ -5480,17 +5766,26 @@ pub unsafe extern "C" fn oakengine_node_keyframes_at_time( out_handles: *mut *mut OakEngineKeyframe, max_handles: c_int, ) -> c_int { - // Stub: see `oakengine_node_keyframe_count`. - guard_int(|| { - let _ = ( - self_, - input_id, - element, - time_num, - time_den, - out_handles, - max_handles, - ); + guard_int(|| unsafe { + if self_.is_null() || input_id.is_null() || max_handles < 0 { + return Ok(0); + } + if max_handles == 0 { + return Ok(0); + } + let h = unbox(self_)?; + if n::oaknode_node_has_keyframe_at_time(h, input_id, time_num, time_den) != 1 { + return Ok(0); + } + if !out_handles.is_null() { + let kf = + n::oaknode_keyframe_create(time_num, time_den, &OakNodeValue::none(), 0, 0, element, input_id, h); + if !kf.ctx.is_null() { + // SAFETY: the caller guarantees `max_handles` slots. + *out_handles = box_handle::(kf); + return Ok(1); + } + } Ok(0) }) } @@ -5506,15 +5801,20 @@ pub unsafe extern "C" fn oakengine_node_set_input_keyframing( enable_all_tracks: c_int, undo_name: *const c_char, ) -> c_int { - // Stub: the module has no keyframing-enable command; its keyframe - // tracks are never populated through the C ABI. + // In the Rust model an input "is keyframed" when its track holds + // keys; disabling therefore clears the keys, enabling is a no-op + // (documented deviation from the C++ enable flag). guard(|| unsafe { if self_.is_null() || input_id.is_null() { return Err(Error::Invalid); } - let _ = unbox(self_)?; - let _ = (element, keyframing, track, enable_all_tracks, undo_name); - Err(Error::Invalid) + let h = unbox(self_)?; + let _ = (element, track, enable_all_tracks, undo_name); + if keyframing != 0 { + Ok(()) + } else { + Error::from_module(n::oaknode_node_clear_keyframes(h, input_id)) + } }) } @@ -5526,9 +5826,19 @@ pub unsafe extern "C" fn oakengine_node_set_input_keyframing_command( element: c_int, keyframing: c_int, ) -> *mut c_void { - // Stub: see `oakengine_node_set_input_keyframing`. - guard_ptr(|| { - let _ = (self_, input_id, element, keyframing); + guard_ptr(|| unsafe { + let rc = oakengine_node_set_input_keyframing( + self_, + input_id, + element, + keyframing, + 0, + 0, + std::ptr::null(), + ); + Error::from_module(rc)?; + // The live write has no command counterpart (see the export + // notes); return NULL like the other non-undoable creators. Ok(std::ptr::null_mut()) }) } @@ -5541,14 +5851,33 @@ pub unsafe extern "C" fn oakengine_node_keyframes_paste( count: c_int, undo_name: *const c_char, ) -> c_int { - // Stub: see `oakengine_node_set_input_keyframing` (no insert path). + // Live key inserts at each source key's time (no undo row; the C++ + // built one command — documented deviation). guard(|| unsafe { if self_.is_null() || keyframes.is_null() || count <= 0 { return Err(Error::Invalid); } - let _ = unbox(self_)?; + let h = unbox(self_)?; let _ = undo_name; - Err(Error::Invalid) + for i in 0..count as usize { + let kf = *keyframes.add(i); + if kf.is_null() { + continue; + } + let kh = unbox(kf)?; + let mut num: i64 = 0; + let mut den: i64 = 0; + let mut v = OakNodeValue::none(); + Error::from_module(n::oaknode_keyframe_get_time(kh, &mut num, &mut den))?; + Error::from_module(n::oaknode_keyframe_get_value(kh, &mut v))?; + let mut input_buf = [0 as c_char; 256]; + let rc = n::oaknode_keyframe_get_input(kh, input_buf.as_mut_ptr(), 256); + if rc < 0 { + return Err(Error::Module(rc)); + } + live_set_value_at_time(h, input_buf.as_ptr(), num, den, &v)?; + } + Ok(()) }) } @@ -5816,13 +6145,38 @@ pub unsafe extern "C" fn oakengine_keyframes_remove_many( count: c_int, undo_name: *const c_char, ) -> c_int { - // Stub: see `oakengine_node_keyframe_count` (no remove-key path). - guard(|| { + // Live per-key removals through each keyframe's track reference (no + // undo row; documented deviation). + guard(|| unsafe { if keyframes.is_null() || count <= 0 { return Err(Error::Invalid); } let _ = undo_name; - Err(Error::Invalid) + for i in 0..count as usize { + let kf = *keyframes.add(i); + if kf.is_null() { + continue; + } + let kh = unbox(kf)?; + let mut num: i64 = 0; + let mut den: i64 = 0; + Error::from_module(n::oaknode_keyframe_get_time(kh, &mut num, &mut den))?; + let mut input_buf = [0 as c_char; 256]; + let rc = n::oaknode_keyframe_get_input(kh, input_buf.as_mut_ptr(), 256); + if rc < 0 { + return Err(Error::Module(rc)); + } + let mut parent = CHandle::null(); + Error::from_module(n::oaknode_keyframe_get_parent(kh, &mut parent))?; + Error::from_module(n::oaknode_node_remove_keyframe( + parent, + input_buf.as_ptr(), + num, + den, + ))?; + release_handle(parent); + } + Ok(()) }) } diff --git a/crates/oakengine/src/plugin.rs b/crates/oakengine/src/plugin.rs index 17cd76bbb..702c47013 100644 --- a/crates/oakengine/src/plugin.rs +++ b/crates/oakengine/src/plugin.rs @@ -26,7 +26,7 @@ use std::ffi::{c_char, c_int, c_void}; use std::sync::{Mutex, OnceLock}; -use crate::bridge::plugin as p; +use crate::stubs::plugin as p; use crate::error::Error; use crate::handle::guard; diff --git a/crates/oakengine/src/pods.rs b/crates/oakengine/src/pods.rs new file mode 100644 index 000000000..197d0b7d1 --- /dev/null +++ b/crates/oakengine/src/pods.rs @@ -0,0 +1,275 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! 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/src/render.rs b/crates/oakengine/src/render.rs index d0d614a78..b309596a3 100644 --- a/crates/oakengine/src/render.rs +++ b/crates/oakengine/src/render.rs @@ -20,25 +20,27 @@ //! - The **renderer** is a facade-owned box binding an output node //! (usually a sequence, or any single node via //! `oakengine_renderer_create_for_node`) to an output geometry; each -//! render call submits an oakrender ticket (`OakVideoTicketParams`), -//! waits for it and returns the produced frame (`OakCodecFrame` wrapped -//! in `OakEngineFrame`). Audio rendering submits the ticket but the -//! crate's samples path is unimplemented, so it fails with the reason -//! in `last_error`. -//! - The **frame accessors** read the wrapped `OakCodecFrame` +//! render call submits an oakrender ticket (`OakVideoTicketParams`) +//! through the manager's real ticket arena, waits for it and returns +//! the produced frame (`oakrender::texture::Frame` wrapped in +//! `OakEngineFrame`). Audio rendering goes through the arena's audio +//! path (`oakrender::eval::render_audio_samples`) and returns the +//! interleaved samples in `OakEngineAudioBuffer`. +//! - The **frame accessors** read the wrapped frame //! (`channel_count` has no crate accessor and reports 0). -//! - The **color processor** family maps onto -//! `oakrender_color_processor_*`; the engine's `oak_color_transform` -//! POD is converted into an oakcommon colortransform handle for -//! `create_transform`. The color-manager list queries, standalone -//! config handle and LUT directory/file library have no crate backing -//! and are documented stubs (see `deferred.rs`). +//! - The **color processor** family maps onto the real +//! `oakrender::color::ColorProcessor`; the engine's +//! `oak_color_transform` POD is converted into an oakcommon +//! colortransform handle for `create_transform`. The color-manager +//! list queries, standalone config handle and LUT directory/file +//! library have no crate backing and are documented stubs (see +//! `deferred.rs`). use std::cell::RefCell; use std::ffi::{c_char, c_double, c_int, c_void}; -use crate::bridge::render as r; -use crate::bridge::render::{OakRenderVideoParams, OakVideoTicketParams}; +use crate::stubs::render as r; +use crate::pods::{OakRenderVideoParams, OakVideoTicketParams}; use crate::error::{Error, Result}; use crate::handle::{ box_handle, free_box, guard, guard_int, guard_ptr, guard_void, string_result, unbox, CHandle, @@ -50,6 +52,33 @@ use crate::handle::{ // Render manager / cacher // --------------------------------------------------------------------------- +/// `oakengine_render_manager_init` — bring up the module's process-global +/// render manager (0 on success; without it `render_frame` fails with NULL +/// + last_error). The module's `OAKRENDER_E_STATE` (-70002) passes through +/// when the manager is already initialized. +#[no_mangle] +pub extern "C" fn oakengine_render_manager_init() -> c_int { + guard(|| Error::from_module(unsafe { r::oakrender_manager_init() })) +} + +/// `oakengine_render_manager_available` — 1 when the render manager is up, +/// 0 otherwise. +#[no_mangle] +pub extern "C" fn oakengine_render_manager_available() -> c_int { + guard_int(|| Ok(unsafe { r::oakrender_manager_available() })) +} + +/// `oakengine_render_manager_shutdown` — tear down the module's render +/// manager (no-op when none is up; always 0, like +/// `oakengine_audio_destroy_instance`). +#[no_mangle] +pub extern "C" fn oakengine_render_manager_shutdown() -> c_int { + guard_void(|| unsafe { + r::oakrender_manager_shutdown(); + }); + crate::error::OAKENGINE_OK +} + /// `oakengine_render_manager_set_aggressive_garbage_collection`. #[no_mangle] pub extern "C" fn oakengine_render_manager_set_aggressive_garbage_collection( @@ -58,24 +87,45 @@ pub extern "C" fn oakengine_render_manager_set_aggressive_garbage_collection( guard(|| Error::from_module(unsafe { r::oakrender_manager_set_aggressive_gc(aggressive) })) } -/// `oakengine_render_manager_requested_backend` — **not backed** (the -/// oakrender crate exposes the current backend, not the requested one). -/// Returns 0 (k_open_gl). +/// `oakengine_render_manager_requested_backend` — the backend the manager +/// was asked for (0 = k_open_gl, 1 = k_metal, 2 = k_vulkan, 3 = k_cpu; +/// -1 when the manager is down). #[no_mangle] pub extern "C" fn oakengine_render_manager_requested_backend() -> c_int { - 0 + guard_int(|| { + match oakrender::manager::RenderManager::global() { + Some(m) => Ok(match m.requested_backend { + oakrender::backend::BackendKind::Auto => 0, + oakrender::backend::BackendKind::Metal => 1, + oakrender::backend::BackendKind::Vulkan => 2, + oakrender::backend::BackendKind::Gl => 0, + oakrender::backend::BackendKind::Cpu => 3, + }), + None => Ok(-1), + } + }) } -/// `oakengine_render_manager_backend_to_string` — **not backed** (the -/// crate enumerates backend ids, not enum→string). Returns -/// OAKENGINE_E_FAILED. +/// `oakengine_render_manager_backend_to_string` — the config name of the +/// backend ordinal (E_INVALID out of range). #[no_mangle] pub unsafe extern "C" fn oakengine_render_manager_backend_to_string( - _backend: c_int, - _buf: *mut c_char, - _buf_size: c_int, + backend: c_int, + buf: *mut c_char, + buf_size: c_int, ) -> c_int { - crate::error::OAKENGINE_E_FAILED + guard_int(|| { + let kind = match backend { + 0 => oakrender::backend::BackendKind::Gl, + 1 => oakrender::backend::BackendKind::Metal, + 2 => oakrender::backend::BackendKind::Vulkan, + 3 => oakrender::backend::BackendKind::Cpu, + _ => return Err(Error::Invalid), + }; + Ok(unsafe { + crate::handle::write_string(kind.to_config_string(), buf, buf_size) + }) + }) } /// `oakengine_render_cache_set_display_color_processor` — NULL clears. @@ -171,19 +221,19 @@ unsafe fn renderer_mut(ptr: *mut OakEngineRenderer) -> Result<&'static mut Rende /// Build an oakcommon video-params handle for the renderer's geometry. unsafe fn make_video_params(b: &RendererBox) -> Result { unsafe { - let params = crate::bridge::common::oakcommon_videoparams_init(); + let params = crate::stubs::common::oakcommon_videoparams_init(); if params.is_null() { return Err(Error::Failed("video params allocation failed".into())); } - let mut rc = crate::bridge::common::oakcommon_videoparams_set_width(params, b.width); + let mut rc = crate::stubs::common::oakcommon_videoparams_set_width(params, b.width); if rc == 0 { - rc = crate::bridge::common::oakcommon_videoparams_set_height(params, b.height); + rc = crate::stubs::common::oakcommon_videoparams_set_height(params, b.height); } if rc == 0 { - rc = crate::bridge::common::oakcommon_videoparams_set_format(params, b.pixel_format); + rc = crate::stubs::common::oakcommon_videoparams_set_format(params, b.pixel_format); } if rc == 0 { - rc = crate::bridge::common::oakcommon_videoparams_set_time_base( + rc = crate::stubs::common::oakcommon_videoparams_set_time_base( params, b.frame_rate_den, b.frame_rate_num, @@ -191,7 +241,7 @@ unsafe fn make_video_params(b: &RendererBox) -> Result { } if rc != 0 { let mut p = params; - crate::bridge::common::oakcommon_videoparams_free(&mut p); + crate::stubs::common::oakcommon_videoparams_free(&mut p); return Err(Error::Failed("video params setup failed".into())); } Ok(params) @@ -254,7 +304,7 @@ unsafe fn make_renderer_box( unsafe fn resolve_footage(node: CHandle) -> Result { unsafe { let mut buf = [0 as c_char; 4096]; - let rc = crate::bridge::node::oaknode_footage_filename(node, buf.as_mut_ptr(), buf.len() as c_int); + let rc = crate::stubs::node::oaknode_footage_filename(node, buf.as_mut_ptr(), buf.len() as c_int); if rc < 0 { return Err(Error::Failed("node is not footage".into())); } @@ -270,18 +320,18 @@ unsafe fn resolve_footage(node: CHandle) -> Result { /// graph node → its upstream footage. unsafe fn clip_media(clip: CHandle) -> Option<(String, c_int)> { unsafe { - let node = crate::bridge::node::oaknode_block_as_node(clip); + let node = crate::stubs::node::oaknode_block_as_node(clip); if node.is_null() { return None; } let mut footage = CHandle::null(); - if crate::bridge::node::oaknode_node_find_input_footage(node, &mut footage) != 0 + if crate::stubs::node::oaknode_node_find_input_footage(node, &mut footage) != 0 || footage.is_null() { return None; } let mut buf = [0 as c_char; 4096]; - if crate::bridge::node::oaknode_footage_filename( + if crate::stubs::node::oaknode_footage_filename( footage, buf.as_mut_ptr(), buf.len() as c_int, @@ -302,7 +352,7 @@ unsafe fn build_audio_montage( b: &RendererBox, range: oakcore_rs::TimeRange, ) -> (Vec, Vec) { - use crate::bridge::node as n; + use crate::stubs::node as n; unsafe { let mut pods = Vec::new(); let mut names = Vec::new(); @@ -374,7 +424,7 @@ unsafe fn build_audio_montage( } /// The `OakMontageClip` POD the render module's ticket reads. -type MontagePod = oakrender::ffi::OakMontageClip; +type MontagePod = crate::pods::MontagePod; /// Build the video montage for `b` at sequence time `time` (rational): /// every clip covering `time` on video tracks, ordered bottom-to-top @@ -385,7 +435,7 @@ unsafe fn build_video_montage( b: &RendererBox, time: oakcore_rs::Rational, ) -> (Vec, Vec) { - use crate::bridge::node as n; + use crate::stubs::node as n; unsafe { let mut pods = Vec::new(); let mut names = Vec::new(); @@ -618,7 +668,7 @@ pub unsafe extern "C" fn oakengine_renderer_render_frame( let ticket = r::oakrender_ticket_render_frame(¶ms, None, std::ptr::null_mut()); let _ = (&keep_alive, &montage); let mut vp = video_params; - crate::bridge::common::oakcommon_videoparams_free(&mut vp); + crate::stubs::common::oakcommon_videoparams_free(&mut vp); if ticket.is_null() { b.last_error = "render ticket submission failed".into(); return Ok(std::ptr::null_mut()); @@ -659,8 +709,8 @@ pub unsafe extern "C" fn oakengine_renderer_render_audio( // the range) and the output format. let (pods, names) = build_audio_montage(b, range); let mut keep: Vec = names; - let params = crate::bridge::audio::oakcore_audioparams_create(48000, 0x3, 10); // packed F32 stereo - crate::bridge::audio::oakcore_audioparams_set_time_base(params, 1, 48000); + let params = crate::stubs::audio::oakcore_audioparams_create(48000, 0x3, 10); // packed F32 stereo + crate::stubs::audio::oakcore_audioparams_set_time_base(params, 1, 48000); let ticket = r::oakrender_ticket_render_audio( b.output_node, start_num, @@ -678,7 +728,7 @@ pub unsafe extern "C" fn oakengine_renderer_render_audio( }, pods.len() as c_int, ); - crate::bridge::audio::oakcore_audioparams_free(params); + crate::stubs::audio::oakcore_audioparams_free(params); if ticket.is_null() { b.last_error = "audio render ticket submission failed".into(); return Ok(std::ptr::null_mut()); @@ -693,7 +743,7 @@ pub unsafe extern "C" fn oakengine_renderer_render_audio( b.last_error = "audio render failed".into(); return Ok(std::ptr::null_mut()); } - let raw = samples as *const oakrender::ffi::OakAudioSamplesOut; + let raw = samples as *const crate::pods::OakAudioSamplesOut; let boxed = AudioSamplesBox { data: (*raw).data.clone(), frame_count: (*raw).frame_count as i64, @@ -1210,7 +1260,7 @@ pub unsafe extern "C" fn oakengine_color_processor_create( } let empty = crate::common::empty_cstr(); let ct = if (*dest).is_display != 0 { - crate::bridge::common::oakcommon_colortransform_init_display( + crate::stubs::common::oakcommon_colortransform_init_display( if (*dest).output.is_null() { empty } else { @@ -1228,7 +1278,7 @@ pub unsafe extern "C" fn oakengine_color_processor_create( }, ) } else { - crate::bridge::common::oakcommon_colortransform_init_output( + crate::stubs::common::oakcommon_colortransform_init_output( if (*dest).output.is_null() { empty } else { @@ -1247,7 +1297,7 @@ pub unsafe extern "C" fn oakengine_color_processor_create( }; let proc = r::oakrender_color_processor_create_transform(mgr_handle, input, ct, direction); let mut ct_handle = ct; - crate::bridge::common::oakcommon_colortransform_free(&mut ct_handle); + crate::stubs::common::oakcommon_colortransform_free(&mut ct_handle); if proc.is_null() { LAST_COLOR_ERROR.with(|e| *e.borrow_mut() = "could not create color processor".into()); return Ok(std::ptr::null_mut()); diff --git a/crates/oakengine/src/stubs.rs b/crates/oakengine/src/stubs.rs new file mode 100644 index 000000000..7ad3531a5 --- /dev/null +++ b/crates/oakengine/src/stubs.rs @@ -0,0 +1,13721 @@ +// 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::handle::{get, get_mut, make_owned}; + 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 host liboakcore 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 host liboakcore 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 host liboakcore 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.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.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 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.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 { + if in_num.is_null() || in_den.is_null() || out_num.is_null() || out_den.is_null() { + return oaktimeline::error::OAKTIMELINE_E_INVALID; + } + // 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.range(); + // 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; + if !enabled.is_null() { + *enabled = if wa.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.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 liboakcore `oakcore_audioparams_*` readers keep their +/// `extern "C"` declarations — those are host-app-provided symbols +/// (liboakcore), not module C ABI. +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, + }; + + extern "C" { + /// `oakcore_audioparams_create` (host liboakcore). + pub fn oakcore_audioparams_create( + sample_rate: c_int, + channel_layout: u64, + format: c_int, + ) -> *mut c_void; + /// `oakcore_audioparams_free` (host liboakcore). + pub fn oakcore_audioparams_free(params: *mut c_void); + /// `oakcore_audioparams_sample_rate` (host liboakcore). + pub fn oakcore_audioparams_sample_rate(params: *const c_void) -> c_int; + /// `oakcore_audioparams_set_time_base` (host liboakcore). + pub fn oakcore_audioparams_set_time_base(params: *mut c_void, num: c_int, den: c_int); + /// `oakcore_audioparams_channel_layout` (host liboakcore). + pub fn oakcore_audioparams_channel_layout(params: *const c_void) -> u64; + /// `oakcore_audioparams_format` (host liboakcore). + pub fn oakcore_audioparams_format(params: *const c_void) -> c_int; + } + + /// 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, + } + } + + /// 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 host liboakcore audioparams contract. + unsafe { crate::stubs::audio::oakcore_audioparams_sample_rate(params) } + }; + let channel_layout = if params.is_null() { + 0x3 + } else { + // SAFETY: the host liboakcore 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(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, + }; + 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/src/task.rs b/crates/oakengine/src/task.rs index 4f79c471f..635907142 100644 --- a/crates/oakengine/src/task.rs +++ b/crates/oakengine/src/task.rs @@ -43,9 +43,9 @@ use std::ffi::{c_char, c_int, c_void}; use std::sync::{Mutex, OnceLock}; use std::time::{SystemTime, UNIX_EPOCH}; -use crate::bridge::codec::{zeroed_encoding_params, EncodingParamsPOD}; -use crate::bridge::node as n; -use crate::bridge::task as t; +use crate::pods::{zeroed_encoding_params, EncodingParamsPOD}; +use crate::stubs::node as n; +use crate::stubs::task as t; use crate::codec::OakEngineEncodingParams; use crate::common::OakVideoParamsPod; use crate::error::{Error, Result}; @@ -654,9 +654,9 @@ pub unsafe extern "C" fn oakengine_task_create_export( /// /// The oaktask crate's `convert_encoding_params` consumes exactly these /// fields (filename, format, video/audio/subtitle enables, codecs, -/// dimensions, time base, pixel format, export length), so a POD carrying -/// them is behaviorally identical to the original for the export task; all -/// other POD fields stay zeroed. +/// dimensions, time base, pixel format, audio rate/layout, export length), +/// so a POD carrying them is behaviorally identical to the original for +/// the export task; all other POD fields stay zeroed. fn export_params_pod(params: *const OakEngineEncodingParams) -> Result { let mut pod = zeroed_encoding_params(); @@ -682,6 +682,24 @@ fn export_params_pod(params: *const OakEngineEncodingParams) -> Result Result *mut OakEngineProject { + guard_ptr(|| unsafe { + let h = unbox(task)?; + let project = t::oaktask_load_take_project(h); + if project.is_null() { + return Ok(std::ptr::null_mut()); + } + Ok(box_handle::(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/tests/audio.rs b/crates/oakengine/src/test_support/audio.rs similarity index 97% rename from crates/oakengine/tests/audio.rs rename to crates/oakengine/src/test_support/audio.rs index d9514412a..4e8e11e2a 100644 --- a/crates/oakengine/tests/audio.rs +++ b/crates/oakengine/src/test_support/audio.rs @@ -19,10 +19,9 @@ //! single serialized test function; processor and sync tests are //! independent. -#[path = "common/mod.rs"] -mod common; +use super::common; -use oakengine::audio::{ +use crate::audio::{ oakengine_audio_clear_buffered_output, oakengine_audio_create_instance, oakengine_audio_destroy_instance, oakengine_audio_estimate_envelope_offset, oakengine_audio_get_output_device, oakengine_audio_hard_reset, oakengine_audio_processor_close, @@ -38,6 +37,10 @@ use oakengine::audio::{ /// (serialized — the singleton is process-wide). #[test] fn manager_lifecycle() { + common::with_manager(|| manager_lifecycle_inner()); +} + +fn manager_lifecycle_inner() { // Start from a destroyed state. unsafe { oakengine_audio_destroy_instance() }; diff --git a/crates/oakengine/tests/codec.rs b/crates/oakengine/src/test_support/codec.rs similarity index 98% rename from crates/oakengine/tests/codec.rs rename to crates/oakengine/src/test_support/codec.rs index 68d6ce493..55bd4e1c7 100644 --- a/crates/oakengine/tests/codec.rs +++ b/crates/oakengine/src/test_support/codec.rs @@ -17,12 +17,11 @@ //! Smoke tests for the encoding family (`engine/include/oakengine/encoding.h`): //! container/codec metadata queries and the encoding-params handle. -#[path = "common/mod.rs"] -mod common; +use super::common; use std::ffi::{c_char, c_int}; -use oakengine::codec::{ +use crate::codec::{ oakengine_encoding_codec_is_lossless, oakengine_encoding_codec_is_still_image, oakengine_encoding_codec_name, oakengine_encoding_filename_contains_digit_placeholder, oakengine_encoding_filename_remove_digit_placeholder, @@ -45,7 +44,7 @@ use oakengine::codec::{ oakengine_encoding_params_video_pix_fmt, oakengine_encoding_pix_fmt_index, oakengine_encoding_start_audio_recording, }; -use oakengine::common::OakVideoParamsPod; +use crate::common::OakVideoParamsPod; /// Container format / codec metadata queries. #[test] @@ -178,7 +177,7 @@ fn params_handle_round_trip() { let mut vp: OakVideoParamsPod = unsafe { std::mem::zeroed() }; assert_eq!( unsafe { - oakengine::common::oakengine_video_params_make( + crate::common::oakengine_video_params_make( &mut vp, 1920, 1080, 1001, 30000, 4, 1, 1, 0, 1, 1, ) }, diff --git a/crates/oakengine/tests/common/mod.rs b/crates/oakengine/src/test_support/common/mod.rs similarity index 82% rename from crates/oakengine/tests/common/mod.rs rename to crates/oakengine/src/test_support/common/mod.rs index df6de65ee..c59a4e45a 100644 --- a/crates/oakengine/tests/common/mod.rs +++ b/crates/oakengine/src/test_support/common/mod.rs @@ -39,32 +39,43 @@ use std::collections::HashMap; use std::ffi::{c_int, c_void}; use std::sync::{Mutex, OnceLock}; -/// Force the module crates into the link. +/// One public direct-Rust symbol per module crate (the module C ABIs are +/// deleted; this mirrors the anchors in `crates/oakengine/src/linkage.rs`). +/// Under unit tests the crates are real dependencies of the lib target and +/// are linked regardless; the array doubles as a compile-time proof that +/// the anchor paths match the current module layouts. #[allow(unused)] pub fn force_link() -> usize { let fns: [usize; 12] = [ - oakundo::ffi::undostack::oakundo_undostack_init as usize, - oakcodec::ffi::format::oakcodec_encoding_format_count as usize, - oakaudio::ffi::waveform::oakaudio_waveform_length as usize, - oakrender::ffi::cache::oakrender_cache_indicator_height as usize, - oakcommon::ffi::config::oakcommon_config_get_int as usize, - oakplugin::ffi::oakplugin_host_plugin_count as usize, - oaknode::ffi::project::oaknode_project_init as usize, - oaktimeline::ffi::marker::oaktimeline_marker_list_create as usize, - oaktask::ffi::manager::oaktask_manager_init as usize, - // The oaknode serializer bridge resolves these at runtime via - // dlsym(RTLD_DEFAULT); force them into the link so the oaknode - // serializer's XML writer/reader and the undo command factory are - // visible to dlsym in every test binary (the oaknode rlib only - // references them through dlsym, so the linker would otherwise drop - // them from the oakcommon/oakundo rlib objects). - oakcommon::ffi::xmlutils::oakcommon_xml_writer_init as usize, - oakcommon::ffi::xmlutils::oakcommon_xml_reader_init as usize, - oakundo::ffi::command::oakundo_command_init as usize, + oakundo::undostack::undostack_init as usize, + oakcodec::exportformat::Format::get_name as usize, + oakaudio::processor::Processor::init as usize, + oakrender::manager::RenderManager::init as usize, + oakcommon::configstore::ConfigStore::instance as usize, + oakplugin::host::Host::global as usize, + oaknode::project::Project::new as usize, + oaktimeline::marker::TimelineMarkerList::new as usize, + oaktask::manager::TaskManager::init as usize, + // oakundo/oakcommon no longer export a C ABI; their handle-level + // Rust API functions anchor the rlibs into every test binary (the + // same pattern as `crates/oakengine/src/linkage.rs`). + oakcommon::xmlutils::XmlWriter::new as usize, + oakcommon::xmlutils::XmlReader::new as usize, + oakundo::undocommand::command_init as usize, ]; fns.iter().sum() } +/// Serialize every test that touches the process-wide AudioManager +/// singleton. The former integration tests were separate processes; as +/// unit tests they share one process (and one singleton), so the manager +/// tests must take a shared lock instead of relying on process isolation. +pub fn with_manager(f: impl FnOnce()) { + static LOCK: Mutex<()> = Mutex::new(()); + let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner()); + f() +} + // --------------------------------------------------------------------------- // oakcore_* stubs (see module docs) // --------------------------------------------------------------------------- diff --git a/crates/oakengine/tests/common.rs b/crates/oakengine/src/test_support/common_smoke.rs similarity index 96% rename from crates/oakengine/tests/common.rs rename to crates/oakengine/src/test_support/common_smoke.rs index 0e7bfd4a4..30d2d96ea 100644 --- a/crates/oakengine/tests/common.rs +++ b/crates/oakengine/src/test_support/common_smoke.rs @@ -19,12 +19,11 @@ //! singleton, so config tests are serialized inside single test //! functions. -#[path = "common/mod.rs"] -mod common; +use super::common; use std::ffi::{c_char, c_int}; -use oakengine::common::{ +use crate::common::{ oakengine_config_get_int, oakengine_config_get_string, oakengine_config_load, oakengine_config_save, oakengine_config_set_error_handler, oakengine_config_set_int, oakengine_config_set_string, oakengine_video_params_bytes_per_pixel, @@ -112,7 +111,7 @@ fn config_error_handler() { ); // Report an error through the handler. assert_eq!( - unsafe { oakengine::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr()) }, + unsafe { crate::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr()) }, 0 ); assert_eq!(CALLED.load(std::sync::atomic::Ordering::SeqCst), 1); @@ -121,7 +120,7 @@ fn config_error_handler() { unsafe { oakengine_config_set_error_handler(None, std::ptr::null_mut()) }, 0 ); - unsafe { oakengine::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr()) }; + unsafe { crate::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr()) }; assert_eq!(CALLED.load(std::sync::atomic::Ordering::SeqCst), 1); } diff --git a/crates/oakengine/tests/it_audio.rs b/crates/oakengine/src/test_support/it_audio.rs similarity index 86% rename from crates/oakengine/tests/it_audio.rs rename to crates/oakengine/src/test_support/it_audio.rs index 0339c8a12..d7b30eebb 100644 --- a/crates/oakengine/tests/it_audio.rs +++ b/crates/oakengine/src/test_support/it_audio.rs @@ -19,13 +19,15 @@ //! `include/audio/{manager,processor,sync,error}.h`), exercised end to //! end against the REAL `oakaudio` crate — no mocks anywhere. //! -//! The facade's 26 exported functions are all covered: +//! The facade's 27 exported functions are all covered: //! //! * Manager (singleton, process-wide — serialized via [`with_manager`]): //! create/destroy/instance handle, device accessors, output push/clock, //! notify interval, recording start/stop. //! * Sync (stateless): envelope offset/stretch correlation, source-time //! and waveform-offset timeline placement. +//! * Waveform extraction (real decode): the two-stage +//! `oakengine_waveform_extract` contract against real media. //! * Processor (refcounted object — serialized via [`with_processor`] so //! the module's debug alive counter (`oakaudio_debug_alive_count`) is //! deterministic): create/free/open/close/is_open plus the two @@ -45,14 +47,13 @@ //! `OAKAUDIO_E_FAILED` and a diagnostic in `error_buf` — both outcomes //! are asserted. -#[path = "common/mod.rs"] -mod common; +use super::common; -use std::ffi::{c_int, c_void, CStr}; +use std::ffi::{c_int, c_void, CStr, CString}; use std::path::PathBuf; use std::sync::Mutex; -use oakengine::audio::{ +use crate::audio::{ oakengine_audio_clear_buffered_output, oakengine_audio_create_instance, oakengine_audio_output_levels, oakengine_audio_destroy_instance, oakengine_audio_estimate_envelope_offset, @@ -66,11 +67,11 @@ use oakengine::audio::{ oakengine_audio_set_output_device, oakengine_audio_set_output_notify_interval, oakengine_audio_start_recording, oakengine_audio_stop_output, oakengine_audio_stop_recording, oakengine_audio_sync_place_by_source_time, oakengine_audio_sync_place_by_waveform_offset, - OakAudioSyncPlacement, OakAudioSyncSourceClip, OakAudioWaveformOffset, - OakAudioWaveformStretchOffset, + oakengine_waveform_extract, OakAudioSyncPlacement, OakAudioSyncSourceClip, + OakAudioWaveformOffset, OakAudioWaveformStretchOffset, }; -use oakengine::error::{OAKENGINE_E_FAILED, OAKENGINE_E_INVALID}; -use oakengine::handle::{CHandle, OakEngineAudioProcessor}; +use crate::error::{OAKENGINE_E_FAILED, OAKENGINE_E_INVALID}; +use crate::handle::{CHandle, OakEngineAudioProcessor}; /// `OAKAUDIO_E_INVALID` (include/audio/error.h) — module codes pass /// through the facade untranslated. @@ -79,6 +80,8 @@ const AUDIO_E_INVALID: c_int = -60001; const AUDIO_E_FAILED: c_int = -60003; /// `OAKAUDIO_E_STATE`. const AUDIO_E_STATE: c_int = -60002; +/// `OAKAUDIO_E_NOT_FOUND` — the waveform extractor's missing-file code. +const AUDIO_E_NOT_FOUND: c_int = -60004; // --------------------------------------------------------------------------- // Serialization + shared fixtures @@ -88,9 +91,7 @@ const AUDIO_E_STATE: c_int = -60002; /// process-wide and its create/destroy flips a global flag, so all /// manager tests take this lock and start from a destroyed state. fn with_manager(f: impl FnOnce()) { - static LOCK: Mutex<()> = Mutex::new(()); - let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner()); - f(); + common::with_manager(f) } /// Serialize processor tests: each processor is an independent @@ -104,7 +105,7 @@ fn with_processor(f: impl FnOnce()) { /// Current number of live refcounted oakaudio objects. fn alive() -> c_int { - unsafe { oakaudio::ffi::manager::oakaudio_debug_alive_count() } + crate::stubs::audio::oakaudio_debug_alive_count() } /// A borrowed `OakAudioParams*` mock handle (tests/common/mod.rs provides @@ -390,7 +391,7 @@ fn manager_recording() { assert_eq!( unsafe { oakengine_audio_start_recording( - (&mut params as *mut oakcodec::ffi::encoder::oakcodec_encoding_params) + (&mut params as *mut oakcodec::encodingparams::EncodingParams) .cast::(), err.as_mut_ptr(), err.len() as c_int, @@ -412,7 +413,7 @@ fn manager_recording() { assert_eq!( unsafe { oakengine_audio_start_recording( - (&mut params as *mut oakcodec::ffi::encoder::oakcodec_encoding_params) + (&mut params as *mut oakcodec::encodingparams::EncodingParams) .cast::(), err.as_mut_ptr(), err.len() as c_int, @@ -436,7 +437,7 @@ fn manager_recording() { let mut err = [0i8; 512]; let rc = unsafe { oakengine_audio_start_recording( - (&mut params as *mut oakcodec::ffi::encoder::oakcodec_encoding_params) + (&mut params as *mut oakcodec::encodingparams::EncodingParams) .cast::(), err.as_mut_ptr(), err.len() as c_int, @@ -472,8 +473,8 @@ fn manager_recording() { /// `oakcodec_encoding_params` with WAV / pcm_s16le and the requested audio /// track. -fn recording_params(audio_enabled: bool) -> oakcodec::ffi::encoder::oakcodec_encoding_params { - let mut p: oakcodec::ffi::encoder::oakcodec_encoding_params = unsafe { std::mem::zeroed() }; +fn recording_params(audio_enabled: bool) -> oakcodec::encodingparams::EncodingParams { + let mut p: oakcodec::encodingparams::EncodingParams = unsafe { std::mem::zeroed() }; let path = recording_path(); let bytes = path.as_os_str().as_encoded_bytes(); assert!(bytes.len() < p.filename.len(), "temp path too long"); @@ -483,7 +484,7 @@ fn recording_params(audio_enabled: bool) -> oakcodec::ffi::encoder::oakcodec_enc p.audio_codec = 13; // PCM_S16LE p.audio_sample_rate = 48000; p.audio_channel_layout = 3; // stereo - p.audio_sample_format = 7; // SampleFormat::S16 (packed) + p.audio_sample_format = oakcore_rs::SampleFormat::S16; // packed 16-bit p.export_length_num = 1; p.export_length_den = 1; p @@ -959,6 +960,164 @@ fn sync_place_by_waveform_offset() { assert_eq!(out.timeline_in_den, 0); } +// --------------------------------------------------------------------------- +// Waveform extraction (real decode, no manager state) +// --------------------------------------------------------------------------- + +/// `oakengine_waveform_extract` two-stage contract against real media: the +/// facade's `oakengine_testmedia_write_clip` writes a clip carrying a +/// stereo PCM 440 Hz sine, which the extractor decodes through the real +/// oakcodec decoder. Illegal arguments → facade `OAKENGINE_E_INVALID`; a +/// missing file passes the module's `OAKAUDIO_E_NOT_FOUND` through; the +/// size query reports the point count + channel count; the data pass fills +/// real min/max peaks. +#[test] +fn waveform_extract_two_stage_and_validation() { + // Illegal arguments are rejected by the facade with its own code. + assert_eq!( + unsafe { + oakengine_waveform_extract( + std::ptr::null(), + 0, + 256, + std::ptr::null_mut(), + 0, + std::ptr::null_mut(), + ) + }, + OAKENGINE_E_INVALID + ); + assert_eq!( + unsafe { + oakengine_waveform_extract( + c"x.wav".as_ptr(), + -1, // negative stream index + 256, + std::ptr::null_mut(), + 0, + std::ptr::null_mut(), + ) + }, + OAKENGINE_E_INVALID + ); + assert_eq!( + unsafe { + oakengine_waveform_extract( + c"x.wav".as_ptr(), + 0, + 0, // non-positive samples per point + std::ptr::null_mut(), + 0, + std::ptr::null_mut(), + ) + }, + OAKENGINE_E_INVALID + ); + assert_eq!( + unsafe { + oakengine_waveform_extract( + c"x.wav".as_ptr(), + 0, + 256, + std::ptr::null_mut(), + -1, // negative capacity + std::ptr::null_mut(), + ) + }, + OAKENGINE_E_INVALID + ); + + // A missing file decodes to the module's NOT_FOUND, passed through. + let missing = std::env::temp_dir().join(format!( + "oakengine-it-waveform-missing-{}.wav", + std::process::id() + )); + let _ = std::fs::remove_file(&missing); + let missing_c = CString::new(missing.to_string_lossy().into_owned()).unwrap(); + assert_eq!( + unsafe { + oakengine_waveform_extract( + missing_c.as_ptr(), + 0, + 256, + std::ptr::null_mut(), + 0, + std::ptr::null_mut(), + ) + }, + AUDIO_E_NOT_FOUND + ); + + // Real media: the facade test clip (1 s of stereo PCM 440 Hz sine). + let media = std::env::temp_dir().join(format!( + "oakengine-it-waveform-{}.mp4", + std::process::id() + )); + let media_c = CString::new(media.to_string_lossy().into_owned()).unwrap(); + assert_eq!( + unsafe { crate::testmedia::oakengine_testmedia_write_clip(media_c.as_ptr(), 64, 64, 10, 10) }, + 0, + "generate real media for the waveform decode" + ); + + // Size query: point count + channel count, nothing written. + let mut channels = 0i32; + let needed = unsafe { + oakengine_waveform_extract( + media_c.as_ptr(), + 0, + 256, + std::ptr::null_mut(), + 0, + &mut channels, + ) + }; + assert!(needed > 0, "1 s at 48 kHz must yield points (got {needed})"); + assert_eq!(channels, 2, "the test clip's audio is stereo"); + + // Data pass: `out_pairs` receives `point_count * channel_count` + // channel-interleaved pairs (capacity is in points), so the buffer is + // sized for the stereo stream; the return is the point count. + let channel_count = channels.max(1) as usize; + let mut raw = + vec![crate::pods::MinMax { min: 0.0, max: 0.0 }; needed as usize * channel_count]; + let n = unsafe { + oakengine_waveform_extract( + media_c.as_ptr(), + 0, + 256, + raw.as_mut_ptr(), + needed, + &mut channels, + ) + }; + assert_eq!(n, needed); + // The 440 Hz sine is audible in the first channel: some window reaches a + // real peak. + assert!( + raw.chunks(channel_count).any(|pt| pt[0].max.abs().max(pt[0].min.abs()) > 0.1), + "the sine tone must show up in the peaks" + ); + + // A capacity below the point count is a size-only query (the two-stage + // contract): the full point count is returned and nothing is written. + let mut two = [crate::pods::MinMax { min: 0.0, max: 0.0 }; 2]; + let n2 = unsafe { + oakengine_waveform_extract( + media_c.as_ptr(), + 0, + 256, + two.as_mut_ptr(), + 2, + std::ptr::null_mut(), + ) + }; + assert_eq!(n2, needed); + + let _ = std::fs::remove_file(&media); + let _ = std::fs::remove_file(&missing); +} + // --------------------------------------------------------------------------- // Processor — lifecycle, free contracts, validation (serialized) // --------------------------------------------------------------------------- @@ -978,7 +1137,7 @@ fn processor_lifecycle_and_free_contracts() { // has nothing to release. The box must be a real heap box // (free_box deallocates it); a stack-allocated wrapper would be // deallocated out from under its owner. - let empty_ptr = oakengine::handle::box_handle::(CHandle::null()); + 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); @@ -992,12 +1151,11 @@ fn processor_lifecycle_and_free_contracts() { // The module-level free is double-free-safe (ctx is nulled after // release); the counter decrements exactly once. - let mut h = oakaudio::handle::CHandle::null(); - h = unsafe { oakaudio::ffi::processor::oakaudio_processor_init() }; + let mut h = crate::stubs::audio::oakaudio_processor_init(); assert!(!h.ctx.is_null()); assert_eq!(alive(), baseline + 1); - unsafe { oakaudio::ffi::processor::oakaudio_processor_free(&mut h) }; - unsafe { oakaudio::ffi::processor::oakaudio_processor_free(&mut h) }; // no-op + crate::stubs::audio::oakaudio_processor_free(&mut h); + crate::stubs::audio::oakaudio_processor_free(&mut h); // no-op assert!(h.ctx.is_null()); assert_eq!(alive(), baseline); }); diff --git a/crates/oakengine/tests/it_codec.rs b/crates/oakengine/src/test_support/it_codec.rs similarity index 96% rename from crates/oakengine/tests/it_codec.rs rename to crates/oakengine/src/test_support/it_codec.rs index d7d22c685..18c4f8bb2 100644 --- a/crates/oakengine/tests/it_codec.rs +++ b/crates/oakengine/src/test_support/it_codec.rs @@ -40,12 +40,11 @@ //! use-after-free by design (the engine header transfers ownership), so the //! tests assert NULL-idempotence rather than double-free of a live handle. -#[path = "common/mod.rs"] -mod common; +use super::common; use std::ffi::{c_char, c_int}; -use oakengine::codec::{ +use crate::codec::{ oakengine_encoding_codec_is_lossless, oakengine_encoding_codec_is_still_image, oakengine_encoding_codec_name, oakengine_encoding_filename_contains_digit_placeholder, oakengine_encoding_filename_remove_digit_placeholder, oakengine_encoding_format_audio_codec_at, @@ -90,7 +89,7 @@ use oakengine::codec::{ oakengine_encoding_sample_format_at, oakengine_encoding_sample_format_count, oakengine_encoding_start_audio_recording, oakengine_export_render_with_params, }; -use oakengine::common::OakVideoParamsPod; +use crate::common::OakVideoParamsPod; /// Facade error codes (`src/error.rs`). const E_INVALID: c_int = -1; @@ -665,7 +664,7 @@ fn params_handle_legal_round_trip() { let mut vp: OakVideoParamsPod = unsafe { std::mem::zeroed() }; assert_eq!( unsafe { - oakengine::common::oakengine_video_params_make( + crate::common::oakengine_video_params_make( &mut vp, 1920, 1080, 1001, 30000, 4, 1, 1, 0, 1, 1, ) }, @@ -1254,10 +1253,16 @@ fn params_destroy_contracts() { // 7. Deferred / not-backed entry points // --------------------------------------------------------------------------- -/// Preset path/count/name, params load/save, export render and the -/// sequence-bound last-used stubs are documented as not backed: they return -/// the fixed contract value (E_FAILED / 0 / NULL / no-op) regardless of the +/// Preset path/count/name, params load/save and the sequence-bound +/// last-used stubs are documented as not backed: they return the fixed +/// contract value (E_FAILED / 0 / NULL / no-op) regardless of the /// arguments, so every argument combination is safe. +/// +/// `oakengine_export_render_with_params` is backed since M12 (see +/// `crate::codec`, "Exporter family"); its argument validation returns +/// E_INVALID for NULL inputs, and the params handle stays owned by the +/// caller on the rejected path (the success path would consume it, like +/// `oakengine_task_create_export`). #[test] fn deferred_stubs_contract() { let mut buf = [0 as c_char; 64]; @@ -1298,13 +1303,15 @@ fn deferred_stubs_contract() { unsafe { oakengine_encoding_params_save_file(std::ptr::null_mut(), std::ptr::null()) }, E_FAILED ); + // Backed since M12: NULL arguments are rejected with E_INVALID, and + // the valid handle is NOT consumed on the rejected path. assert_eq!( unsafe { oakengine_export_render_with_params(std::ptr::null_mut(), p) }, - E_FAILED + E_INVALID ); assert_eq!( unsafe { oakengine_export_render_with_params(std::ptr::null_mut(), std::ptr::null()) }, - E_FAILED + E_INVALID ); assert!(unsafe { oakengine_encoding_params_get_last_used(std::ptr::null_mut()) }.is_null()); @@ -1327,6 +1334,13 @@ fn deferred_stubs_contract() { /// the facade, the oakaudio manager, and the error-string plumbing. #[test] fn start_audio_recording_manager_paths() { + // Serialized: the AudioManager singleton is process-wide and shared + // with the audio-family tests now that the former integration tests + // run in one process. + common::with_manager(|| start_audio_recording_manager_paths_inner()); +} + +fn start_audio_recording_manager_paths_inner() { let p = unsafe { oakengine_encoding_params_create() }; assert!(!p.is_null()); let mut err = [0 as c_char; 128]; @@ -1339,7 +1353,9 @@ fn start_audio_recording_manager_paths() { E_INVALID ); - // No manager singleton -> facade E_STATE. + // No manager singleton -> facade E_STATE (destroy it first: another + // serialized test may have left it created). + assert_eq!(unsafe { crate::audio::oakengine_audio_destroy_instance() }, 0); assert_eq!( unsafe { oakengine_encoding_start_audio_recording(p, err.as_mut_ptr(), 128) }, E_STATE @@ -1347,7 +1363,7 @@ fn start_audio_recording_manager_paths() { // Create the real singleton through the audio facade. assert_eq!( - unsafe { oakengine::audio::oakengine_audio_create_instance() }, + unsafe { crate::audio::oakengine_audio_create_instance() }, 0 ); @@ -1379,7 +1395,7 @@ fn start_audio_recording_manager_paths() { // Tear the singleton down: the E_STATE contract returns. assert_eq!( - unsafe { oakengine::audio::oakengine_audio_destroy_instance() }, + unsafe { crate::audio::oakengine_audio_destroy_instance() }, 0 ); assert_eq!( diff --git a/crates/oakengine/tests/it_common.rs b/crates/oakengine/src/test_support/it_common.rs similarity index 99% rename from crates/oakengine/tests/it_common.rs rename to crates/oakengine/src/test_support/it_common.rs index 23d31fc65..83bb3d9b4 100644 --- a/crates/oakengine/tests/it_common.rs +++ b/crates/oakengine/src/test_support/it_common.rs @@ -35,15 +35,14 @@ // (e.g. `oakengine_config_load`) otherwise trip `unused_unsafe`. #![allow(unused_unsafe)] -#[path = "common/mod.rs"] -mod common; +use super::common; use std::ffi::{c_char, c_int}; use std::path::Path; use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::Mutex; -use oakengine::common::{ +use crate::common::{ oakengine_config_get_int, oakengine_config_get_string, oakengine_config_load, oakengine_config_report_error, oakengine_config_save, oakengine_config_set_error_handler, oakengine_config_set_int, oakengine_config_set_string, oakengine_video_params_bytes_per_pixel, @@ -63,7 +62,7 @@ use oakengine::common::{ /// Read a two-stage facade string into a Rust String. unsafe fn read_buf(buf: &mut [c_char]) -> String { - std::ffi::CStr::from_ptr(buf.as_ptr()) + unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } .to_string_lossy() .into_owned() } diff --git a/crates/oakengine/src/test_support/it_export.rs b/crates/oakengine/src/test_support/it_export.rs new file mode 100644 index 000000000..1a788da8b --- /dev/null +++ b/crates/oakengine/src/test_support/it_export.rs @@ -0,0 +1,459 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Integration tests for the **exporter family** (`src/codec.rs`, +//! "Exporter family"; module contract +//! `engine/include/oakengine/exporter.h`). +//! +//! Coverage rules (see the family test charter): +//! 1. no mocks — every call goes through the real facade into the real +//! module crates (the only stubs are the host-provided `oakcore_*` +//! symbols in `tests/common`); the output file is a REAL mp4 written +//! by the statically linked FFmpeg (oakcodec encoder), asserted by +//! its `ftyp` box; +//! 2. every exporter-family export is exercised on a legal path with the +//! result asserted; +//! 3. illegal inputs (NULL seq/path, negative ranges, unknown codecs) +//! always yield a negative error code — never a crash; +//! 4. the progress callback receives at least one update during a run, +//! with the installed userdata. +//! +//! ## Serialization +//! +//! The tests assemble projects and run export tasks, both of which touch +//! process-wide state (the undo stack cleared by `oakengine_project_new`, +//! the global task manager), so every test takes a shared [`SERIAL`] mutex +//! (the same pattern as `it_task`). + +use super::common; + +use std::ffi::{c_char, c_double, c_int, c_void}; +use std::io::Read; +use std::sync::Mutex; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use crate::codec::{ + oakengine_encoding_params_create, oakengine_encoding_params_enable_audio, + oakengine_encoding_params_enable_video, oakengine_encoding_params_set_filename, + oakengine_encoding_params_set_format, oakengine_export_last_error, oakengine_export_render, + oakengine_export_render_with_params, oakengine_export_set_progress_callback, +}; +use crate::common::OakVideoParamsPod; +use crate::handle::{OakEngineProject, OakEngineSequence, free_box}; +use crate::node::{ + oakengine_footage_free, oakengine_project_create, oakengine_project_free, + oakengine_project_import_footage, oakengine_project_new, +}; +use crate::pods::OakExportOptions; +use crate::testmedia::oakengine_testmedia_write_clip; +use crate::timeline::{ + oakengine_sequence_add_footage_clip_ex, oakengine_sequence_add_track, oakengine_sequence_new, + oakengine_sequence_set_video_params, +}; + +/// `OAKENGINE_TRACK_TYPE_*` (timeline.h). +const TRACK_VIDEO: c_int = 0; +const TRACK_AUDIO: c_int = 1; +/// `olive::ExportFormat::Format` ids (mp4 = MPEG-4 video). +const FORMAT_MP4: c_int = 2; +/// `olive::ExportCodec::Codec` ids. +const CODEC_H264: c_int = 1; +const CODEC_AAC: c_int = 12; + +/// Serializes every test in this binary (see the module docs). Poisoned by +/// a panicking test, the lock is recovered with `into_inner` so one failure +/// does not cascade into `PoisonError` failures in every later test. +static SERIAL: Mutex<()> = Mutex::new(()); + +/// Take the [`SERIAL`] lock, recovering from any poisoning. +fn serial() -> std::sync::MutexGuard<'static, ()> { + SERIAL.lock().unwrap_or_else(|e| e.into_inner()) +} + +/// A unique temp path (per-process, so parallel test binaries never +/// collide). +fn temp_path(kind: &str) -> std::path::PathBuf { + std::env::temp_dir().join(format!("oakengine-it-export-{kind}-{}.mp4", std::process::id())) +} + +/// Encode the facade test clip and assemble a project + sequence carrying +/// it: one video track/clip and one audio track/clip, both spanning +/// `0..frame_count` at `fps` frames per second (mirrors the CLI's +/// `assemble_project`). Returns `(project, sequence)` — the caller +/// releases the sequence box with `free_box` and the project with +/// `oakengine_project_free`. +/// +/// # Safety +/// The returned handles must be released by the caller exactly once. +unsafe fn assemble_test_sequence( + media: &std::path::Path, + width: c_int, + height: c_int, + frame_count: i64, + fps: c_int, +) -> (*mut OakEngineProject, *mut OakEngineSequence) { + let media_c = std::ffi::CString::new(media.to_string_lossy().into_owned()).unwrap(); + assert_eq!( + oakengine_testmedia_write_clip(media_c.as_ptr(), width, height, frame_count as c_int, fps), + 0, + "generate the source clip" + ); + + let project = oakengine_project_create(); + assert!(!project.is_null()); + assert_eq!(unsafe { oakengine_project_new(project) }, 0); + + let footage = unsafe { oakengine_project_import_footage(project, media_c.as_ptr()) }; + assert!(!footage.is_null(), "import the test clip"); + + let seq = unsafe { oakengine_sequence_new(project, c"Export Test".as_ptr()) }; + assert!(!seq.is_null()); + + assert_eq!( + unsafe { + oakengine_sequence_set_video_params( + seq, width, height, fps, 1, 1, 1, 0, 4, // PIXEL_FORMAT_F32 + 0, + ) + }, + 0, + "set the sequence frame rate" + ); + + let vt = unsafe { oakengine_sequence_add_track(seq, TRACK_VIDEO) }; + assert!(vt >= 0, "add the video track"); + let vclip = unsafe { + oakengine_sequence_add_footage_clip_ex(seq, footage, TRACK_VIDEO, vt, 0, frame_count, 0) + }; + assert!(!vclip.is_null(), "place the video clip"); + + let at = unsafe { oakengine_sequence_add_track(seq, TRACK_AUDIO) }; + assert!(at >= 0, "add the audio track"); + let aclip = unsafe { + oakengine_sequence_add_footage_clip_ex(seq, footage, TRACK_AUDIO, at, 0, frame_count, 0) + }; + assert!(!aclip.is_null(), "place the audio clip"); + + unsafe { oakengine_footage_free(footage) }; + (project, seq) +} + +/// Read the facade's thread-local export last-error (the string the +/// assertion messages embed). +fn export_last_error_str() -> String { + let mut buf = [0 as c_char; 512]; + let n = unsafe { oakengine_export_last_error(buf.as_mut_ptr(), 512) }; + if n <= 0 { + return String::new(); + } + let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len()); + String::from_utf8_lossy(unsafe { + std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) + }) + .into_owned() +} + +/// Assert the exported file exists, is non-empty and starts with the MP4 +/// `ftyp` box. +fn assert_real_mp4(path: &std::path::Path) { + let meta = std::fs::metadata(path).expect("the exported file must exist"); + assert!(meta.len() > 0, "the exported file must be non-empty"); + let mut head = [0u8; 12]; + std::fs::File::open(path) + .expect("reopen the exported file") + .read_exact(&mut head) + .expect("read the file head"); + assert_eq!(&head[4..8], b"ftyp", "MP4 files start with the ftyp box"); +} + +/// Release the assembly returned by [`assemble_test_sequence`]. +/// +/// # Safety +/// The handles must be the ones returned by [`assemble_test_sequence`]. +unsafe fn drop_test_sequence(project: *mut OakEngineProject, seq: *mut OakEngineSequence) { + unsafe { + free_box::(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/tests/it_plugin.rs b/crates/oakengine/src/test_support/it_plugin.rs similarity index 88% rename from crates/oakengine/tests/it_plugin.rs rename to crates/oakengine/src/test_support/it_plugin.rs index 3b97ba092..ac53b4cd8 100644 --- a/crates/oakengine/tests/it_plugin.rs +++ b/crates/oakengine/src/test_support/it_plugin.rs @@ -35,22 +35,23 @@ //! oakplugin crate's build.rs compiles (cbits/oak_test_plugin.c). When //! it is unavailable the test prints SKIP and returns (never fails). -#[path = "common/mod.rs"] -mod common; +use super::common; use std::ffi::{c_char, c_int, c_void, CStr, CString}; use std::path::{Path, PathBuf}; -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; -use oakengine::handle::{CHandle, OakEngineNode}; -use oakengine::plugin::{ +use crate::handle::{CHandle, OakEngineNode}; +use crate::plugin::{ oakengine_plugin_load_plugins, oakengine_plugin_node_push_button_clicked, oakengine_plugin_set_active_viewer_provider, oakengine_plugin_set_progress_reporter_factory, }; -use oakplugin::ffi::{ - oakplugin_debug_alive_count, oakplugin_host_plugin_count, oakplugin_host_plugin_id_at, - oakplugin_host_plugin_label, oakplugin_instance_create, oakplugin_instance_free, -}; +// The deleted `oakplugin::ffi` handle surface is replaced by the crate's +// direct host API (single-lib unification). +use oakplugin::handle::RefBox; +use oakplugin::host::Host; +use oakplugin::instance::Instance; +use oakplugin::property::Value; /// `OAKENGINE_E_INVALID` (src/error.rs). const E_INVALID: c_int = -1; @@ -88,12 +89,12 @@ fn fresh_temp_dir(name: &str) -> PathBuf { /// Current number of live backend objects (host instance registry). fn alive() -> c_int { - unsafe { oakplugin_debug_alive_count() } + Host::global().alive_count() as c_int } /// Number of plugins discovered by the real host cache. fn plugin_count() -> c_int { - unsafe { oakplugin_host_plugin_count() } + Host::global().cache.count() as c_int } /// Run the facade scan through the real host, returning its exit code. @@ -102,6 +103,34 @@ fn scan_facade(dir: &Path) -> c_int { unsafe { oakengine_plugin_load_plugins(cs.as_ptr()) } } +/// The identifier of the plugin at `index` (the direct-API replacement of +/// the deleted `oakplugin_host_plugin_id_at` two-stage getter). +fn host_plugin_id_at(index: usize) -> Option { + 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. @@ -192,23 +221,13 @@ fn fresh_test_plugin_scan_dir(tag: &str) -> Option { } /// All plugin identifiers currently registered in the real host cache -/// (two-stage getter round trip per index). +/// (direct cache iteration). fn plugin_ids() -> Vec { let count = plugin_count(); let mut out = Vec::new(); for i in 0..count { - let len = unsafe { oakplugin_host_plugin_id_at(i, std::ptr::null_mut(), 0) }; - if len <= 0 { - continue; - } - let mut buf = vec![0u8; len as usize]; - let rc = unsafe { oakplugin_host_plugin_id_at(i, buf.as_mut_ptr() as *mut c_char, len) }; - if rc == 0 { - out.push( - unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) } - .to_string_lossy() - .into_owned(), - ); + if let Some(id) = host_plugin_id_at(i as usize) { + out.push(id); } } out @@ -508,21 +527,8 @@ fn load_plugins_real_bundle_end_to_end() { } // Label lookup for a known id resolves (phase 1: the id itself). - let len = unsafe { - oakplugin_host_plugin_label(c"org.oak.test-plugin".as_ptr(), std::ptr::null_mut(), 0) - }; - assert!(len > 0); - let mut lbuf = vec![0u8; len as usize]; - assert_eq!( - unsafe { - oakplugin_host_plugin_label( - c"org.oak.test-plugin".as_ptr(), - lbuf.as_mut_ptr() as *mut c_char, - len, - ) - }, - 0 - ); + let label = host_plugin_label(TEST_PLUGIN_ID); + assert!(label.is_some(), "scanned test plugin must expose a label"); // Repeat scan of the same path is deduplicated: still OK, cache // unchanged, alive counter untouched. @@ -586,16 +592,16 @@ fn backend_instance_free_contracts() { common::force_link(); let base = alive(); - // free(NULL) and free(empty handle) are no-ops. - unsafe { oakplugin_instance_free(std::ptr::null_mut()) }; - let mut empty = CHandle::null(); - unsafe { oakplugin_instance_free(&mut empty) }; - assert!(empty.is_null(), "free must leave the handle emptied"); + // free(NULL-equivalent) and free(empty handle) are no-ops. + host_instance_free(&mut None); + let mut empty: Option>> = None; + host_instance_free(&mut empty); + assert!(empty.is_none(), "free must leave the handle emptied"); // Unknown plugin id → empty handle, not a crash. - let mut h = unsafe { oakplugin_instance_create(c"org.oak.not-a-plugin".as_ptr()) }; - assert!(h.is_null()); - unsafe { oakplugin_instance_free(&mut h) }; + let mut h = host_instance_create("org.oak.not-a-plugin"); + assert!(h.is_none()); + host_instance_free(&mut h); assert_eq!(alive(), base); // Real plugin: create +1, free back to baseline, double-free safe. @@ -604,21 +610,21 @@ fn backend_instance_free_contracts() { return; }; assert_eq!(scan_facade(&dir), 0); - let mut inst = unsafe { oakplugin_instance_create(c"org.oak.test-plugin".as_ptr()) }; + let mut inst = host_instance_create(TEST_PLUGIN_ID); assert!( - !inst.is_null(), + inst.is_some(), "scanned test plugin must create an instance" ); assert_eq!(alive(), base + 1, "one live instance must be registered"); - unsafe { oakplugin_instance_free(&mut inst) }; - assert!(inst.is_null()); + host_instance_free(&mut inst); + assert!(inst.is_none()); assert_eq!( alive(), base, "free must return the alive counter to baseline" ); // Double free of the already-emptied handle is a no-op. - unsafe { oakplugin_instance_free(&mut inst) }; + host_instance_free(&mut inst); assert_eq!(alive(), base); }); } diff --git a/crates/oakengine/tests/it_task.rs b/crates/oakengine/src/test_support/it_task.rs similarity index 79% rename from crates/oakengine/tests/it_task.rs rename to crates/oakengine/src/test_support/it_task.rs index 04cd77c21..3d161aaeb 100644 --- a/crates/oakengine/tests/it_task.rs +++ b/crates/oakengine/src/test_support/it_task.rs @@ -44,25 +44,25 @@ //! //! ## Ignored tests //! -//! - [`export_task_run_ignored_environment_gated`]: running an export -//! needs a real GPU/OpenGL render and a real ffmpeg encoder; the host -//! stubs cannot encode. The creation path is covered in the main suite. +//! (None — the export run used to be environment-gated on GPU/encoder; +//! the real CPU render + statically linked FFmpeg encoder now works in +//! the test environment, see [`export_task_run_real_encoder`].) -#[path = "common/mod.rs"] -mod common; +use super::common; -use std::ffi::{c_char, c_int}; +use std::ffi::{c_char, c_int, c_void}; use std::sync::Mutex; -use oakengine::codec::{oakengine_encoding_params_create, oakengine_encoding_params_set_filename}; -use oakengine::handle::{ +use crate::codec::{oakengine_encoding_params_create, oakengine_encoding_params_set_filename}; +use crate::handle::{ free_box, CHandle, OakEngineNode, OakEngineProject, OakEngineSequence, OakEngineTask, }; -use oakengine::node::{ - oakengine_node_free, oakengine_project_create, oakengine_project_free, oakengine_project_new, - oakengine_project_root, oakengine_project_set_filename, +use crate::node::{ + oakengine_node_free, oakengine_project_create, oakengine_project_filename, + oakengine_project_free, oakengine_project_new, oakengine_project_root, oakengine_project_save, + oakengine_project_set_filename, }; -use oakengine::task::{ +use crate::task::{ oakengine_cli_task_dialog_run, oakengine_task_cancel, oakengine_task_create_export, oakengine_task_create_project_import, oakengine_task_create_project_load, oakengine_task_create_project_load_otio, oakengine_task_create_project_save, @@ -70,13 +70,13 @@ use oakengine::task::{ oakengine_task_free, oakengine_task_import_file_count, oakengine_task_import_footage_at, oakengine_task_import_footage_count, oakengine_task_import_get_command, oakengine_task_import_invalid_file_at, oakengine_task_import_invalid_files_count, - oakengine_task_is_cancelled, oakengine_task_manager_add, oakengine_task_manager_cancel, - oakengine_task_manager_count, oakengine_task_manager_first, oakengine_task_manager_handle, - oakengine_task_save_get_project, oakengine_task_start_sync, oakengine_task_start_time, - oakengine_task_title, + oakengine_task_is_cancelled, oakengine_task_load_take_project, oakengine_task_manager_add, + oakengine_task_manager_cancel, oakengine_task_manager_count, oakengine_task_manager_first, + oakengine_task_manager_handle, oakengine_task_save_get_project, oakengine_task_start_sync, + oakengine_task_start_time, oakengine_task_subscribe, oakengine_task_title, }; -use oakengine::timeline::oakengine_sequence_new; -use oakengine::undo::oakengine_undo_command_free; +use crate::timeline::oakengine_sequence_new; +use crate::undo::oakengine_undo_command_free; /// OAKTASK module error codes that pass through untranslated. const OAKTASK_E_INVALID: c_int = -80001; @@ -89,14 +89,15 @@ const OAKTASK_E_NOT_FOUND: c_int = -80004; static SERIAL: Mutex<()> = Mutex::new(()); /// Take the [`SERIAL`] lock, recovering from any poisoning. -fn serial() -> std::sync::MutexGuard<'static, ()> { +pub(crate) fn serial() -> std::sync::MutexGuard<'static, ()> { SERIAL.lock().unwrap_or_else(|e| e.into_inner()) } -/// The oaktask module's debug alive counter (not re-exported by the -/// facade; the module crate is a real dependency of the test binary). +/// The facade's live task-payload counter (the engine-side replacement +/// of the deleted module alive counter; counts owned task payloads and +/// borrowed manager-view payloads). fn alive_count() -> c_int { - unsafe { oaktask::ffi::task::oaktask_debug_alive_count() } + crate::stubs::task::oaktask_debug_alive_count() } /// Read a NUL-terminated two-stage buffer as a Rust `String`. @@ -310,7 +311,7 @@ fn empty_handles_are_rejected() { assert!(!params.is_null()); // NULL result: the params handle is NOT consumed, so we own it still. assert!(unsafe { oakengine_task_create_export(empty_seq, params) }.is_null()); - unsafe { oakengine::codec::oakengine_encoding_params_destroy(params) }; + unsafe { crate::codec::oakengine_encoding_params_destroy(params) }; unsafe { drop(Box::from_raw(empty_seq)) }; } @@ -883,7 +884,7 @@ fn export_task_creation() { handle: CHandle::null(), })); assert!(unsafe { oakengine_task_create_export(empty_seq, params2) }.is_null()); - unsafe { oakengine::codec::oakengine_encoding_params_destroy(params2) }; + unsafe { crate::codec::oakengine_encoding_params_destroy(params2) }; unsafe { drop(Box::from_raw(empty_seq)) }; assert_eq!(unsafe { oakengine_task_free(task) }, 0); @@ -893,39 +894,105 @@ fn export_task_creation() { unsafe { oakengine_project_free(project) }; } -/// Running an export requires a real GPU/OpenGL render and a real encoder; -/// the test environment's host stubs cannot decode or encode media. The -/// creation path above covers the legal input surface; the run itself is -/// documented as environment-gated. +/// Running an export end-to-end: the facade test clip in a project + +/// sequence with full encoding params (mp4 / H.264 / AAC), run +/// synchronously through the real CPU render and the statically linked +/// FFmpeg encoder, with the output file asserted. The creation path above +/// covers the params-ownership matrix; this run proves the run itself +/// works in the test environment (formerly environment-gated on GPU). #[test] -#[ignore = "export run needs GPU/OpenGL rendering and a real ffmpeg encoder; host stubs cannot encode"] -fn export_task_run_ignored_environment_gated() { +fn export_task_run_real_encoder() { let _g = serial(); common::force_link(); + let media = std::env::temp_dir().join(format!( + "oakengine-it-task-export-src-{}.mp4", + std::process::id() + )); + let out = std::env::temp_dir().join(format!( + "oakengine-it-task-export-run-{}.mp4", + std::process::id() + )); + let _ = std::fs::remove_file(&media); + let _ = std::fs::remove_file(&out); + let media_c = std::ffi::CString::new(media.to_string_lossy().into_owned()).unwrap(); + assert_eq!( + unsafe { crate::testmedia::oakengine_testmedia_write_clip(media_c.as_ptr(), 64, 64, 10, 10) }, + 0, + "generate the source clip" + ); + let project = oakengine_project_create(); assert!(!project.is_null()); assert_eq!(unsafe { oakengine_project_new(project) }, 0); + let footage = + unsafe { crate::node::oakengine_project_import_footage(project, media_c.as_ptr()) }; + assert!(!footage.is_null(), "import the test clip"); let seq = unsafe { oakengine_sequence_new(project, c"Export Run".as_ptr()) }; assert!(!seq.is_null()); - - let params = oakengine_encoding_params_create(); assert_eq!( unsafe { - oakengine_encoding_params_set_filename( - params, - c"/tmp/oakengine_it_task_export_run.mov".as_ptr(), - ) + crate::timeline::oakengine_sequence_set_video_params(seq, 64, 64, 10, 1, 1, 1, 0, 4, 0) }, 0 ); + let vt = unsafe { crate::timeline::oakengine_sequence_add_track(seq, 0 /* video */) }; + assert!(vt >= 0); + let clip = unsafe { + crate::timeline::oakengine_sequence_add_footage_clip_ex(seq, footage, 0 /* video */, vt, 0, 10, 0) + }; + assert!(!clip.is_null(), "place the video clip"); + unsafe { crate::node::oakengine_footage_free(footage) }; + + let params = oakengine_encoding_params_create(); + assert!(!params.is_null()); + let out_c = std::ffi::CString::new(out.to_string_lossy().into_owned()).unwrap(); + assert_eq!( + unsafe { oakengine_encoding_params_set_filename(params, out_c.as_ptr()) }, + 0 + ); + // mp4 / H.264 / AAC, 64x64 @ 10 fps, 1 s. + assert_eq!( + unsafe { crate::codec::oakengine_encoding_params_set_format(params, 2) }, + 0 + ); + let pod = crate::common::OakVideoParamsPod { + width: 64, + height: 64, + time_base_num: 1, + time_base_den: 10, + format: 0, + pixel_aspect_num: 1, + pixel_aspect_den: 1, + interlacing: 0, + color_range: 0, + divider: 1, + video_type: 0, + premultiplied_alpha: 0, + }; + assert_eq!( + unsafe { crate::codec::oakengine_encoding_params_enable_video(params, &pod, 1 /* H264 */) }, + 0 + ); + assert_eq!( + unsafe { crate::codec::oakengine_encoding_params_enable_audio(params, 48000, 0x3, 0, 12 /* AAC */) }, + 0 + ); let task = unsafe { oakengine_task_create_export(seq, params) }; assert!(!task.is_null()); assert_eq!(unsafe { oakengine_task_start_sync(task) }, 1); unsafe { oakengine_task_free(task) }; + let meta = std::fs::metadata(&out); + assert!( + meta.is_ok() && meta.unwrap().len() > 0, + "the export run must write the output file" + ); + unsafe { free_box::(seq) }; unsafe { oakengine_project_free(project) }; + let _ = std::fs::remove_file(&media); + let _ = std::fs::remove_file(&out); } /// The proxy creator is a documented stub (the oaktask module has no @@ -964,6 +1031,8 @@ fn task_manager_lifecycle() { let handle = oakengine_task_manager_handle(); assert!(!handle.is_null()); assert_eq!(oakengine_task_manager_handle(), handle); + // Other (serialized) tests may have left finished tasks in the queue. + crate::stubs::task::oaktask_manager_delete_finished(); assert_eq!(oakengine_task_manager_count(), 0); // An empty queue has no first task. @@ -1028,3 +1097,137 @@ fn task_manager_lifecycle() { // exposes no delete export), so the count is still 1. assert_eq!(oakengine_task_manager_count(), 1); } + +// --------------------------------------------------------------------------- +// Load-task result / event subscription (new facade exports) +// --------------------------------------------------------------------------- + +/// `oakengine_task_load_take_project` — NULL for NULL/empty tasks and for +/// tasks that are not load tasks; after a successful load run the project +/// comes back owned (released with `oakengine_project_free`). The success +/// path uses a real `.ove` written through `oakengine_project_save`. +#[test] +fn task_load_take_project_lifecycle() { + let _g = serial(); + common::force_link(); + + // NULL / empty task → NULL. + assert!(unsafe { oakengine_task_load_take_project(std::ptr::null_mut()) }.is_null()); + let empty = empty_task_box(); + assert!(unsafe { oakengine_task_load_take_project(empty) }.is_null()); + unsafe { reclaim_empty_task_box(empty) }; + + // A load task that never ran / failed has no project to take. + let failed = unsafe { oakengine_task_create_project_load(c"/no/such/oak/project.ove".as_ptr()) }; + assert!(!failed.is_null()); + assert!(unsafe { oakengine_task_load_take_project(failed) }.is_null()); + assert_eq!(unsafe { oakengine_task_start_sync(failed) }, 0); + assert!(unsafe { oakengine_task_load_take_project(failed) }.is_null()); + assert_eq!(unsafe { oakengine_task_free(failed) }, 0); + + // A non-load task (save) is not a load task → NULL. + let project = unsafe { oakengine_project_create() }; + assert_eq!(unsafe { oakengine_project_new(project) }, 0); + let save_path = std::env::temp_dir().join("oakengine_it_task_load_take.ovexml"); + let save_c = std::ffi::CString::new(save_path.to_str().unwrap()).unwrap(); + let save_task = + unsafe { oakengine_task_create_project_save(project, 0, save_c.as_ptr(), std::ptr::null()) }; + assert!(!save_task.is_null()); + assert!(unsafe { oakengine_task_load_take_project(save_task) }.is_null()); + unsafe { oakengine_task_free(save_task) }; + + // Success path: save a real project file, load it through the task and + // take the loaded project. + let ove_path = std::env::temp_dir().join("oakengine_it_task_load_take.ove"); + let ove_c = std::ffi::CString::new(ove_path.to_str().unwrap()).unwrap(); + assert_eq!(unsafe { oakengine_project_save(project, ove_c.as_ptr()) }, 0); + let load_task = unsafe { oakengine_task_create_project_load(ove_c.as_ptr()) }; + assert!(!load_task.is_null()); + assert_eq!( + unsafe { oakengine_task_start_sync(load_task) }, + 1, + "loading the project file just saved must succeed" + ); + let loaded = unsafe { oakengine_task_load_take_project(load_task) }; + assert!(!loaded.is_null(), "a successful load yields the project"); + // The loaded project is a real, owned handle (name readable, freeable). + let mut buf = [0 as c_char; 256]; + let name_len = unsafe { oakengine_project_filename(loaded, buf.as_mut_ptr(), 256) }; + assert!(name_len >= 0); + unsafe { oakengine_project_free(loaded) }; + // Taking again returns NULL (the project was taken). + assert!(unsafe { oakengine_task_load_take_project(load_task) }.is_null()); + assert_eq!(unsafe { oakengine_task_free(load_task) }, 0); + + unsafe { oakengine_project_free(project) }; + let _ = std::fs::remove_file(&save_path); + let _ = std::fs::remove_file(&ove_path); +} + +/// `oakengine_task_subscribe` — NULL task / NULL callback → facade +/// `OAKENGINE_E_INVALID` (-1); a valid subscription returns 0 and delivers +/// STARTED (0) + FINISHED (2) to the callback during a sync run (the +/// listener is one-shot, so re-running does not re-fire). +#[test] +fn task_subscribe_lifecycle() { + let _g = serial(); + common::force_link(); + + // NULL / empty task and NULL callback → facade E_INVALID. + assert_eq!( + unsafe { oakengine_task_subscribe(std::ptr::null_mut(), None, std::ptr::null_mut()) }, + -1 + ); + let empty = empty_task_box(); + assert_eq!( + unsafe { oakengine_task_subscribe(empty, None, std::ptr::null_mut()) }, + -1 + ); + unsafe { reclaim_empty_task_box(empty) }; + + // The recorder counts STARTED (0) / FINISHED (2) events. + static STARTED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); + static FINISHED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); + unsafe extern "C" fn recorder(event_id: c_int, _value: f64, _userdata: *mut c_void) { + match event_id { + 0 => { + STARTED.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + } + 2 => { + FINISHED.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + } + _ => {} + } + } + + let task = unsafe { oakengine_task_create_project_load(c"".as_ptr()) }; + assert!(!task.is_null()); + + // NULL callback on a real task → E_INVALID (the facade validates it). + assert_eq!( + unsafe { oakengine_task_subscribe(task, None, std::ptr::null_mut()) }, + -1 + ); + + // A valid subscription returns 0; a second one replaces the first. + assert_eq!( + unsafe { oakengine_task_subscribe(task, Some(recorder), std::ptr::null_mut()) }, + 0 + ); + assert_eq!( + unsafe { oakengine_task_subscribe(task, Some(recorder), std::ptr::null_mut()) }, + 0 + ); + + // A sync run fires STARTED then FINISHED (the empty-filename load fails, + // but the events fire on every run transition). + assert_eq!(unsafe { oakengine_task_start_sync(task) }, 0); + assert_eq!(STARTED.load(std::sync::atomic::Ordering::SeqCst), 1); + assert_eq!(FINISHED.load(std::sync::atomic::Ordering::SeqCst), 1); + + // The subscription is one-shot: a second run does not re-fire. + assert_eq!(unsafe { oakengine_task_start_sync(task) }, 0); + assert_eq!(STARTED.load(std::sync::atomic::Ordering::SeqCst), 1); + + assert_eq!(unsafe { oakengine_task_free(task) }, 0); +} diff --git a/crates/oakengine/tests/it_undo.rs b/crates/oakengine/src/test_support/it_undo.rs similarity index 97% rename from crates/oakengine/tests/it_undo.rs rename to crates/oakengine/src/test_support/it_undo.rs index 5662be678..c833918f0 100644 --- a/crates/oakengine/tests/it_undo.rs +++ b/crates/oakengine/src/test_support/it_undo.rs @@ -38,15 +38,14 @@ //! `oakengine_undo_group_abort` used to leave its executed children //! un-undone. -#[path = "common/mod.rs"] -mod common; +use super::common; use std::ffi::{c_char, c_void}; use std::sync::atomic::{AtomicI32, AtomicUsize, Ordering}; use std::sync::Mutex; -use oakengine::handle::{CHandle, OakEngineClipboard}; -use oakengine::undo::{ +use crate::handle::{CHandle, OakEngineClipboard}; +use crate::undo::{ oakengine_undo_can_redo, oakengine_undo_can_undo, oakengine_undo_clear, oakengine_undo_command_create, oakengine_undo_command_create_multi, oakengine_undo_command_free, oakengine_undo_command_is_done, @@ -128,7 +127,7 @@ unsafe extern "C" fn variants_free(_ud: *mut c_void) { unsafe extern "C" fn lifecycle_free_userdata(ud: *mut c_void) { LIFECYCLE_FREED_PTR.store(ud as usize, Ordering::SeqCst); LIFECYCLE_FREE.fetch_add(1, Ordering::SeqCst); - drop(Box::from_raw(ud as *mut u64)); + unsafe { drop(Box::from_raw(ud as *mut u64)) }; } /// Child redo/undo callbacks that log their id (encoded in userdata) — used @@ -536,27 +535,26 @@ fn free_contracts() { undo: None, free_fn: Some(mod_free_cb), }; - let mut h = - unsafe { oakundo::ffi::command::oakundo_command_init(&vtable, std::ptr::null_mut()) }; + let mut h = oakundo::undocommand::command_init(&vtable, std::ptr::null_mut()); assert!(!h.ctx.is_null()); - unsafe { oakundo::ffi::command::oakundo_command_free(&mut h) }; + oakundo::undocommand::command_free(&mut h); assert_eq!(MOD_FREE.load(Ordering::SeqCst), 1); assert!(h.ctx.is_null()); - unsafe { oakundo::ffi::command::oakundo_command_free(&mut h) }; + oakundo::undocommand::command_free(&mut h); assert_eq!(MOD_FREE.load(Ordering::SeqCst), 1); // Module stack handle: double free is a no-op; NULL value and NULL // pointer are no-ops too. - let mut s = unsafe { oakundo::ffi::undostack::oakundo_undostack_init() }; + let mut s = oakundo::undostack::undostack_init(); assert!(!s.ctx.is_null()); - unsafe { oakundo::ffi::undostack::oakundo_undostack_free(&mut s) }; + oakundo::undostack::undostack_free(&mut s); assert!(s.ctx.is_null()); - unsafe { oakundo::ffi::undostack::oakundo_undostack_free(&mut s) }; + oakundo::undostack::undostack_free(&mut s); let mut null_h = CHandle::null(); - unsafe { oakundo::ffi::command::oakundo_command_free(&mut null_h) }; - unsafe { oakundo::ffi::command::oakundo_command_free(std::ptr::null_mut()) }; - unsafe { oakundo::ffi::undostack::oakundo_undostack_free(std::ptr::null_mut()) }; + oakundo::undocommand::command_free(&mut null_h); + oakundo::undocommand::command_free(std::ptr::null_mut()); + oakundo::undostack::undostack_free(std::ptr::null_mut()); } // --------------------------------------------------------------------------- diff --git a/crates/oakengine/src/test_support/linkage.rs b/crates/oakengine/src/test_support/linkage.rs new file mode 100644 index 000000000..90dbdacf5 --- /dev/null +++ b/crates/oakengine/src/test_support/linkage.rs @@ -0,0 +1,51 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! 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/src/test_support/mod.rs b/crates/oakengine/src/test_support/mod.rs new file mode 100644 index 000000000..ac8d6c68f --- /dev/null +++ b/crates/oakengine/src/test_support/mod.rs @@ -0,0 +1,52 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Unit-test aggregation for the oakengine facade (single-lib unification). +//! +//! The facade's crate-type is cdylib-only (no rlib), so the former +//! `tests/*.rs` integration tests cannot link `oakengine` as a crate. +//! They moved here (`src/test_support/`, pulled in by `src/lib.rs` under +//! `#[cfg(test)]`) and run as unit tests against `crate::*` instead of +//! `oakengine::*`. The old `#[path = "common/mod.rs"] mod common;` include +//! is replaced by the single [`common`] declaration below — the +//! `oakcore_*` mock symbols it defines may exist only once per binary. +//! +//! The node/timeline/render-graph families (and the graph-op tests that +//! built fixtures through the deleted handle-based module C ABIs) are +//! part of the pending domain-model redesign; their test files were +//! deleted with this migration (see the migration report). + +#![allow(dead_code)] + +#[path = "common/mod.rs"] +pub mod common; + +mod audio; +mod codec; +mod common_smoke; +mod it_audio; +mod it_codec; +mod it_common; +mod it_export; +mod it_plugin; +mod it_task; +mod it_undo; +mod linkage; +mod node; +mod plugin; +mod render; +mod task; +mod undo; diff --git a/crates/oakengine/tests/node.rs b/crates/oakengine/src/test_support/node.rs similarity index 96% rename from crates/oakengine/tests/node.rs rename to crates/oakengine/src/test_support/node.rs index bc86530df..d2a8cd453 100644 --- a/crates/oakengine/tests/node.rs +++ b/crates/oakengine/src/test_support/node.rs @@ -23,12 +23,11 @@ //! the failure-path tests only exercise non-mutating calls and run in //! parallel. -#[path = "common/mod.rs"] -mod common; +use super::common; use std::ffi::{c_char, c_int}; -use oakengine::node::{ +use crate::node::{ oakengine_footage_borrow, oakengine_footage_last_error, oakengine_footage_probe, oakengine_node_connect, oakengine_node_disconnect, oakengine_node_factory_create_from_id, oakengine_node_factory_id_count, oakengine_node_factory_name_from_id, @@ -49,7 +48,7 @@ const TYPE_ID_SOLID: &str = "org.olivevideoeditor.Olive.solidgenerator"; /// Read a two-stage facade string into a Rust String. unsafe fn read_buf(buf: &mut [c_char]) -> String { - std::ffi::CStr::from_ptr(buf.as_ptr()) + unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } .to_string_lossy() .into_owned() } @@ -60,13 +59,13 @@ unsafe fn read_buf(buf: &mut [c_char]) -> String { /// time (the facade's own undo family uses the multi/redo/free /// variants), so the linker would drop it. fn force_oakundo_command_link() -> usize { - // The oaknode serializer bridge resolves the oakcommon XML writer and - // the oakundo command factory at runtime via dlsym; nothing else - // references them at link time. + // The oaknode serializer bridge calls the oakcommon XML reader/writer + // and the oakundo command factory through their Rust API; nothing else + // references those codegen units at link time, so anchor them here. let fns: [usize; 3] = [ - oakundo::ffi::command::oakundo_command_init as *const () as usize, - oakcommon::ffi::xmlutils::oakcommon_xml_writer_init as *const () as usize, - oakcommon::ffi::xmlutils::oakcommon_xml_reader_init as *const () as usize, + oakundo::undocommand::command_init as *const () as usize, + oakcommon::xmlutils::XmlWriter::new as *const () as usize, + oakcommon::xmlutils::XmlReader::new as *const () as usize, ]; fns.iter().sum() } @@ -82,7 +81,7 @@ fn float_value(x: f64) -> OakNodeValue { } /// The index of the first project node whose type id matches `id`, or -1. -unsafe fn find_node(project: *mut oakengine::handle::OakEngineProject, id: &str) -> c_int { +unsafe fn find_node(project: *mut crate::handle::OakEngineProject, id: &str) -> c_int { let count = unsafe { oakengine_project_node_count(project) }; for i in 0..count { let node = unsafe { oakengine_project_node_at(project, i) }; diff --git a/crates/oakengine/tests/plugin.rs b/crates/oakengine/src/test_support/plugin.rs similarity index 95% rename from crates/oakengine/tests/plugin.rs rename to crates/oakengine/src/test_support/plugin.rs index dda6b17b3..1e5539795 100644 --- a/crates/oakengine/tests/plugin.rs +++ b/crates/oakengine/src/test_support/plugin.rs @@ -16,10 +16,9 @@ //! Smoke tests for the plugin family (`engine/include/oakengine/plugin.h`). -#[path = "common/mod.rs"] -mod common; +use super::common; -use oakengine::plugin::{ +use crate::plugin::{ oakengine_plugin_load_plugins, oakengine_plugin_node_push_button_clicked, oakengine_plugin_set_active_viewer_provider, oakengine_plugin_set_progress_reporter_factory, }; @@ -29,7 +28,7 @@ use oakengine::plugin::{ fn provider_registration() { unsafe extern "C" fn viewer( _userdata: *mut std::ffi::c_void, - ) -> *mut oakengine::handle::OakEngineNode { + ) -> *mut crate::handle::OakEngineNode { std::ptr::null_mut() } assert_eq!( diff --git a/crates/oakengine/tests/render.rs b/crates/oakengine/src/test_support/render.rs similarity index 97% rename from crates/oakengine/tests/render.rs rename to crates/oakengine/src/test_support/render.rs index fbc36b3c4..6243adcfe 100644 --- a/crates/oakengine/tests/render.rs +++ b/crates/oakengine/src/test_support/render.rs @@ -21,12 +21,11 @@ //! argument paths (real rendering needs the deferred node family plus an //! initialized render manager). -#[path = "common/mod.rs"] -mod common; +use super::common; use std::ffi::{c_char, c_double}; -use oakengine::render::{ +use crate::render::{ oakengine_color_last_error, oakengine_color_manager_get_config_filename, oakengine_color_processor_convert_color, oakengine_color_processor_create, oakengine_color_processor_free, oakengine_color_processor_is_valid, @@ -56,8 +55,8 @@ fn render_manager_not_initialized() { unsafe { oakengine_render_cache_set_multicam_node(std::ptr::null_mut()) }, -70002 ); - // The requested-backend stub reports 0 (k_open_gl) when unavailable. - assert_eq!(unsafe { oakengine_render_manager_requested_backend() }, 0); + // Without a manager the requested backend is -1 (no manager up). + assert_eq!(unsafe { oakengine_render_manager_requested_backend() }, -1); } /// Renderer lifecycle: NULL sequence is rejected; mode validation. diff --git a/crates/oakengine/tests/task.rs b/crates/oakengine/src/test_support/task.rs similarity index 98% rename from crates/oakengine/tests/task.rs rename to crates/oakengine/src/test_support/task.rs index da9a9d64f..15ac3ad9e 100644 --- a/crates/oakengine/tests/task.rs +++ b/crates/oakengine/src/test_support/task.rs @@ -28,16 +28,15 @@ //! and project-mutating tests are each a single test function; the //! handle/accessor tests touch neither and run in parallel. -#[path = "common/mod.rs"] -mod common; +use super::common; use std::ffi::{c_char, c_int, c_void}; -use oakengine::node::{ +use crate::node::{ oakengine_node_free, oakengine_project_create, oakengine_project_free, oakengine_project_new, oakengine_project_root, oakengine_project_set_filename, }; -use oakengine::task::{ +use crate::task::{ oakengine_cli_task_dialog_run, oakengine_task_cancel, oakengine_task_create_export, oakengine_task_create_project_import, oakengine_task_create_project_load, oakengine_task_create_project_load_otio, oakengine_task_create_project_save, @@ -208,6 +207,7 @@ fn cli_dialog_runs_task_sync() { /// test because `oakengine_project_new` touches the global undo stack. #[test] fn project_task_lifecycle() { + let _g = super::it_task::serial(); common::force_link(); let project = oakengine_project_create(); @@ -302,9 +302,11 @@ fn project_task_lifecycle() { /// `manager_first` and can be cancelled. #[test] fn task_manager_lifecycle() { + let _g = super::it_task::serial(); common::force_link(); assert!(!unsafe { oakengine_task_manager_handle() }.is_null()); + crate::stubs::task::oaktask_manager_delete_finished(); assert_eq!(unsafe { oakengine_task_manager_count() }, 0); let task = unsafe { oakengine_task_create_project_load(c"/no/such/oak/project.ove".as_ptr()) }; diff --git a/crates/oakengine/tests/undo.rs b/crates/oakengine/src/test_support/undo.rs similarity index 99% rename from crates/oakengine/tests/undo.rs rename to crates/oakengine/src/test_support/undo.rs index a51bf6390..28e5b93c6 100644 --- a/crates/oakengine/tests/undo.rs +++ b/crates/oakengine/src/test_support/undo.rs @@ -21,13 +21,12 @@ //! function; the command-lifecycle tests (no stack access) can run in //! parallel. -#[path = "common/mod.rs"] -mod common; +use super::common; use std::ffi::{c_char, c_int, c_void}; use std::sync::atomic::{AtomicI32, Ordering}; -use oakengine::undo::{ +use crate::undo::{ oakengine_undo_can_redo, oakengine_undo_can_undo, oakengine_undo_clear, oakengine_undo_command_create, oakengine_undo_command_create_multi, oakengine_undo_command_free, oakengine_undo_command_is_done, diff --git a/crates/oakengine/src/timeline.rs b/crates/oakengine/src/timeline.rs index 8c65617a9..ba4bb86d2 100644 --- a/crates/oakengine/src/timeline.rs +++ b/crates/oakengine/src/timeline.rs @@ -30,8 +30,12 @@ use std::ffi::{c_char, c_int, c_void}; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::Mutex; -use crate::bridge::undo::OakUndoCommandVtable; -use crate::bridge::{audio as a, common as c, node as n, timeline as tl, undo as u}; +use oakundo::undocommand::OakUndoCommandVtable; +use crate::stubs::common as c; +use crate::stubs::node as n; +use crate::stubs::timeline as tl; +use crate::stubs::audio as a; +use oakundo::undocommand as u; use crate::error::{Error, Result}; use crate::handle::{ box_handle, free_box, guard, guard_int, guard_ptr, guard_void, read_cstr, string_result, unbox, @@ -215,13 +219,13 @@ unsafe fn push_multi_commands(children: &[CHandle], name: &str) -> Result<()> { if children.is_empty() { return Ok(()); } - let multi = u::oakundo_command_init_multi(); + let multi = u::command_init_multi(); if multi.is_null() { return Err(Error::Failed("multi command allocation failed".into())); } let multi_box = box_handle::(multi); for child in children { - let rc = u::oakundo_command_multi_add_child(multi, *child); + let rc = u::command_multi_add_child(multi, *child); if rc != 0 { free_box(multi_box); return Err(Error::Module(rc)); @@ -257,7 +261,7 @@ unsafe fn vtable_command( undo: Some(undo), free_fn: Some(free_fn), }; - let cmd = u::oakundo_command_init(&vtable, data); + let cmd = u::command_init(&vtable, data); if cmd.is_null() { free_fn(data); return Err(Error::Failed("undo command allocation failed".into())); @@ -1663,23 +1667,18 @@ pub unsafe extern "C" fn oakengine_sequence_add_track( Error::from_module(n::oaknode_sequence_get_track_list( seq, track_type, &mut list, ))?; - // The module's TimelineAddTrackCommand redo only appends the - // sequence's track array element; it never registers the created - // track node in the list's `tracks`, so the count/at queries would - // not observe it. Register a track node live as compensation - // (documented deviation; the array element stays undoable). + // The module's TimelineAddTrackCommand redo appends the created + // track to the list (with its back-reference and index), so the + // count/at queries observe it directly — no separate registration + // needed (the old module redo only grew the sequence's track-input + // array element, which is why the previous facade registered a + // second live track as compensation). let cmd = tl::oaktimeline_add_track_command(list); if cmd.is_null() { release_handle(list); return Err(Error::Failed("add track command failed".into())); } push_command(cmd, "Add Track")?; - let track = n::oaknode_track_create(track_type); - if track.is_null() { - release_handle(list); - return Err(Error::Failed("track creation failed".into())); - } - Error::from_module(n::oaknode_tracklist_add_track(list, track))?; let mut count: c_int = 0; Error::from_module(n::oaknode_sequence_get_track_count( seq, track_type, &mut count, @@ -4647,7 +4646,7 @@ pub unsafe extern "C" fn oakengine_marker_commit_time( push_command(cmd, "Move Marker") } else { let parent = unbox(command.cast::())?; - let rc = u::oakundo_command_multi_add_child(parent, cmd); + let rc = u::command_multi_add_child(parent, cmd); Error::from_module(rc) } }) @@ -4786,7 +4785,7 @@ pub unsafe extern "C" fn oakengine_marker_set_properties( } else { let parent = unbox(command.cast::())?; for child in &children { - let rc = u::oakundo_command_multi_add_child(parent, *child); + let rc = u::command_multi_add_child(parent, *child); if rc != 0 { return Err(Error::Module(rc)); } @@ -4924,7 +4923,7 @@ pub unsafe extern "C" fn oakengine_workarea_set_range_undoable( push_command(cmd, "Set Workarea Range") } else { let parent = unbox(command.cast::())?; - let rc = u::oakundo_command_multi_add_child(parent, cmd); + let rc = u::command_multi_add_child(parent, cmd); Error::from_module(rc) } }) @@ -4948,7 +4947,7 @@ pub unsafe extern "C" fn oakengine_workarea_set_enabled_undoable( push_command(cmd, "Set Workarea Enabled") } else { let parent = unbox(command.cast::())?; - let rc = u::oakundo_command_multi_add_child(parent, cmd); + let rc = u::command_multi_add_child(parent, cmd); Error::from_module(rc) } }) @@ -5726,16 +5725,9 @@ pub unsafe extern "C" fn oakengine_sequence_add_default_nodes( } let children = [vcmd, acmd]; push_multi_commands(&children, "Add Default Nodes")?; - // Module-gap compensation: register one live track per type (see - // `oakengine_sequence_add_track`). - let vtrack = n::oaknode_track_create(TRACK_TYPE_VIDEO); - let atrack = n::oaknode_track_create(TRACK_TYPE_AUDIO); - if !vtrack.is_null() { - n::oaknode_tracklist_add_track(video_list, vtrack); - } - if !atrack.is_null() { - n::oaknode_tracklist_add_track(audio_list, atrack); - } + // The two commands' redos registered their tracks in the video and + // audio lists already (see `oakengine_sequence_add_track`); no + // separate registration is needed. release_handle(video_list); release_handle(audio_list); Ok(()) diff --git a/crates/oakengine/src/undo.rs b/crates/oakengine/src/undo.rs index 480b48b26..5d7b00738 100644 --- a/crates/oakengine/src/undo.rs +++ b/crates/oakengine/src/undo.rs @@ -32,7 +32,15 @@ use std::ffi::{c_char, c_int, c_void}; use std::sync::{Mutex, OnceLock}; -use crate::bridge::undo as u; +use oakundo::undocommand::{ + command_free, command_init, command_init_multi, command_multi_add_child, + command_multi_child, command_multi_child_count, command_redo_now, command_undo_now, +}; +use oakundo::undostack::{ + undostack_can_redo, undostack_can_undo, undostack_clear, undostack_command_is_done, + undostack_command_text, undostack_count, undostack_index, undostack_init, undostack_jump, + undostack_push, undostack_push_pre_executed, +}; use crate::error::{Error, Result}; use crate::handle::{box_handle, free_box, guard, guard_void, unbox, CHandle, OakEngineClipboard}; @@ -40,7 +48,7 @@ use crate::handle::{box_handle, free_box, guard, guard_void, unbox, CHandle, Oak /// lazily and kept for the process lifetime. fn global_stack() -> &'static CHandle { static STACK: OnceLock = OnceLock::new(); - STACK.get_or_init(|| unsafe { u::oakundo_undostack_init() }) + STACK.get_or_init(|| unsafe { undostack_init() }) } /// Stable opaque token for `oakengine_undo_handle`: the module stack's @@ -84,11 +92,11 @@ pub(crate) unsafe fn push_or_run( // happen on the still-owned handle FIRST — the group takes the // already-done command (C++ semantics: add_child + redo_now, net // effect identical for the group's reverse-order undo). - let rc = unsafe { u::oakundo_command_redo_now(cmd) }; + let rc = unsafe { command_redo_now(cmd) }; if rc != 0 { return Err(Error::Module(rc)); } - let rc = unsafe { u::oakundo_command_multi_add_child(group.multi, cmd) }; + let rc = unsafe { command_multi_add_child(group.multi, cmd) }; drop(g); unsafe { free_box(command_box) }; return if rc == 0 { @@ -106,7 +114,7 @@ pub(crate) unsafe fn push_or_run( } else { label.as_ptr() as *const c_char }; - let rc = unsafe { u::oakundo_undostack_push(stack, cmd, label_ptr) }; + let rc = unsafe { undostack_push(stack, cmd, label_ptr) }; if rc == 0 { // Stack took a reference; release ours by freeing the box. unsafe { free_box(command_box) }; @@ -147,7 +155,7 @@ pub extern "C" fn oakengine_undo_group_begin(name: *const c_char) -> c_int { if g.is_some() { return Err(Error::State); } - let multi = unsafe { u::oakundo_command_init_multi() }; + let multi = unsafe { command_init_multi() }; if multi.is_null() { return Err(Error::Failed("undo group allocation failed".into())); } @@ -181,9 +189,9 @@ pub extern "C" fn oakengine_undo_group_end() -> c_int { // the stack took (or destroyed) the command; release our own // reference to the multi handle. let stack = *global_stack(); - let rc = unsafe { u::oakundo_undostack_push_pre_executed(stack, multi, name_ptr) }; + let rc = unsafe { undostack_push_pre_executed(stack, multi, name_ptr) }; let mut multi_handle = multi; - unsafe { u::oakundo_command_free(&mut multi_handle) }; + unsafe { command_free(&mut multi_handle) }; if rc == 0 { Ok(()) } else { @@ -206,33 +214,33 @@ pub extern "C" fn oakengine_undo_group_abort() -> c_int { // insertion order (mirroring the multi's reverse-order undo), each // through its own borrowed handle. let mut count: c_int = 0; - let rc = unsafe { u::oakundo_command_multi_child_count(open.multi, &mut count) }; + let rc = unsafe { command_multi_child_count(open.multi, &mut count) }; if rc != 0 { let mut multi = open.multi; - unsafe { u::oakundo_command_free(&mut multi) }; + unsafe { command_free(&mut multi) }; return Err(Error::Module(rc)); } for i in (0..count).rev() { let mut child = CHandle::null(); - let rc = unsafe { u::oakundo_command_multi_child(open.multi, i, &mut child) }; + let rc = unsafe { command_multi_child(open.multi, i, &mut child) }; if rc != 0 { let mut multi = open.multi; - unsafe { u::oakundo_command_free(&mut multi) }; + unsafe { command_free(&mut multi) }; return Err(Error::Module(rc)); } - let rc = unsafe { u::oakundo_command_undo_now(child) }; + let rc = unsafe { command_undo_now(child) }; // The child handle is borrowed (owns:false): release only its // shell — the child value lives on in the multi until the multi // itself is freed below. - unsafe { u::oakundo_command_free(&mut child) }; + unsafe { command_free(&mut child) }; if rc != 0 { let mut multi = open.multi; - unsafe { u::oakundo_command_free(&mut multi) }; + unsafe { command_free(&mut multi) }; return Err(Error::Module(rc)); } } let mut multi = open.multi; - unsafe { u::oakundo_command_free(&mut multi) }; + unsafe { command_free(&mut multi) }; Ok(()) }) } @@ -243,7 +251,7 @@ pub extern "C" fn oakengine_undo_group_abort() -> c_int { pub unsafe extern "C" fn oakengine_undo_command_redo_now(command: *mut c_void) -> c_int { guard(|| unsafe { let cmd = unbox(command.cast::())?; - Error::from_module(u::oakundo_command_redo_now(cmd)) + Error::from_module(command_redo_now(cmd)) }) } @@ -253,7 +261,7 @@ pub unsafe extern "C" fn oakengine_undo_command_redo_now(command: *mut c_void) - pub unsafe extern "C" fn oakengine_undo_command_undo_now(command: *mut c_void) -> c_int { guard(|| unsafe { let cmd = unbox(command.cast::())?; - Error::from_module(u::oakundo_command_undo_now(cmd)) + Error::from_module(command_undo_now(cmd)) }) } @@ -274,12 +282,12 @@ pub unsafe extern "C" fn oakengine_undo_command_create( ) -> *mut c_void { crate::handle::guard_ptr(|| unsafe { let _ = crate::handle::read_cstr(name); - let vtable = crate::bridge::undo::OakUndoCommandVtable { + let vtable = oakundo::undocommand::OakUndoCommandVtable { redo, undo, free_fn, }; - let cmd = u::oakundo_command_init(&vtable, userdata); + let cmd = command_init(&vtable, userdata); if cmd.is_null() { return Ok(std::ptr::null_mut()); } @@ -292,7 +300,7 @@ pub unsafe extern "C" fn oakengine_undo_command_create( #[no_mangle] pub extern "C" fn oakengine_undo_command_create_multi() -> *mut c_void { crate::handle::guard_ptr(|| { - let cmd = unsafe { u::oakundo_command_init_multi() }; + let cmd = unsafe { command_init_multi() }; if cmd.is_null() { return Ok(std::ptr::null_mut()); } @@ -313,7 +321,7 @@ pub unsafe extern "C" fn oakengine_undo_command_multi_add_child( } let m = unbox(multi.cast::())?; let c = unbox(child.cast::())?; - let rc = u::oakundo_command_multi_add_child(m, c); + let rc = command_multi_add_child(m, c); free_box(child.cast::()); if rc == 0 { Ok(()) @@ -329,7 +337,7 @@ pub unsafe extern "C" fn oakengine_undo_command_multi_child_count(multi: *mut c_ crate::handle::guard_int(|| unsafe { let m = unbox(multi.cast::())?; let mut count: c_int = 0; - Error::from_module(u::oakundo_command_multi_child_count(m, &mut count))?; + Error::from_module(command_multi_child_count(m, &mut count))?; Ok(count) }) } @@ -347,7 +355,7 @@ pub unsafe extern "C" fn oakengine_undo_command_free(command: *mut c_void) { pub extern "C" fn oakengine_undo_count() -> i64 { crate::handle::guard_i64(|| unsafe { let mut count: i64 = 0; - Error::from_module(u::oakundo_undostack_count(*global_stack(), &mut count))?; + Error::from_module(undostack_count(*global_stack(), &mut count))?; Ok(count) }) } @@ -357,7 +365,7 @@ pub extern "C" fn oakengine_undo_count() -> i64 { pub extern "C" fn oakengine_undo_index() -> i64 { crate::handle::guard_i64(|| unsafe { let mut index: i64 = 0; - Error::from_module(u::oakundo_undostack_index(*global_stack(), &mut index))?; + Error::from_module(undostack_index(*global_stack(), &mut index))?; Ok(index) }) } @@ -375,7 +383,7 @@ pub unsafe extern "C" fn oakengine_undo_command_text( // module return value is returned verbatim (guarded against panic), // converted to the engine's length-excluding-NUL convention. crate::handle::guard_int(|| unsafe { - let rc = u::oakundo_undostack_command_text(*global_stack(), row, buf, buf_size); + let rc = undostack_command_text(*global_stack(), row, buf, buf_size); if rc < 0 { Err(Error::Module(rc)) } else { @@ -390,7 +398,7 @@ pub unsafe extern "C" fn oakengine_undo_command_text( pub extern "C" fn oakengine_undo_command_is_done(row: i64) -> c_int { crate::handle::guard_int(|| unsafe { let mut value: c_int = 0; - Error::from_module(u::oakundo_undostack_command_is_done( + Error::from_module(undostack_command_is_done( *global_stack(), row, &mut value, @@ -403,14 +411,14 @@ pub extern "C" fn oakengine_undo_command_is_done(row: i64) -> c_int { /// `index`. #[no_mangle] pub extern "C" fn oakengine_undo_jump(index: i64) -> c_int { - guard(|| unsafe { Error::from_module(u::oakundo_undostack_jump(*global_stack(), index)) }) + guard(|| unsafe { Error::from_module(undostack_jump(*global_stack(), index)) }) } /// `oakengine_undo_clear` — delete all commands and push the fresh /// "New/Open Project" empty command. #[no_mangle] pub extern "C" fn oakengine_undo_clear() -> c_int { - guard(|| unsafe { Error::from_module(u::oakundo_undostack_clear(*global_stack())) }) + guard(|| unsafe { Error::from_module(undostack_clear(*global_stack())) }) } /// `oakengine_undo_update_actions` — no-op: the QAction members were @@ -426,7 +434,7 @@ pub extern "C" fn oakengine_undo_update_actions() -> c_int { pub extern "C" fn oakengine_undo_can_undo() -> c_int { crate::handle::guard_int(|| unsafe { let mut value: c_int = 0; - Error::from_module(u::oakundo_undostack_can_undo(*global_stack(), &mut value))?; + Error::from_module(undostack_can_undo(*global_stack(), &mut value))?; Ok(value) }) } @@ -436,7 +444,7 @@ pub extern "C" fn oakengine_undo_can_undo() -> c_int { pub extern "C" fn oakengine_undo_can_redo() -> c_int { crate::handle::guard_int(|| unsafe { let mut value: c_int = 0; - Error::from_module(u::oakundo_undostack_can_redo(*global_stack(), &mut value))?; + Error::from_module(undostack_can_redo(*global_stack(), &mut value))?; Ok(value) }) } diff --git a/crates/oakengine/src/worker.rs b/crates/oakengine/src/worker.rs index 80c730eab..8638d7b4e 100644 --- a/crates/oakengine/src/worker.rs +++ b/crates/oakengine/src/worker.rs @@ -14,61 +14,50 @@ // 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` -//! behind the frozen C ABI in `engine/include/oakengine/worker.h`. +//! The render worker — the Rust port of `engine/src/capi/worker.cpp`, +//! owned by the oakengine facade and exported through the frozen +//! `oakengine_worker_*` C ABI (`engine/include/oakengine/worker.h`); the +//! `oak-worker` binary is a pure C-ABI consumer that links the built +//! `liboakengine` dylib. //! -//! Like the C++ side, this is where the worker's whole runtime lives: -//! -//! - **Backend selection.** [`create_renderer`] initializes the render -//! backend through the oakrender module C ABI -//! (`oakrender_display_renderer_create_dynamic` + `_init`), falling -//! back to the direct OpenGL renderer exactly like the C++ -//! `create_renderer()` chain. The worker executable itself never -//! touches a renderer — it is a thin shell calling -//! [`oakengine_worker_main`]. +//! - **Backend selection.** [`Renderer::create`] initializes the render +//! backend through the oakrender crate's direct Rust API +//! ([`oakrender::backend::DisplayRenderer`]), falling back to the +//! direct OpenGL renderer exactly like the C++ `create_renderer()` +//! chain. //! - **The session.** [`WorkerSession`] holds the renderer, the //! shared-memory frame-slot pools ([`crate::ipc::FrameSlotPool`]) and //! the shutdown flag, and answers one NDJSON control message at a time. -//! - **The main loop.** [`worker_main`] (and its C ABI wrapper -//! [`oakengine_worker_main`]) creates the session, loads the runtime -//! config, writes the startup handshake, and serves the stdin/stdout -//! NDJSON loop until a `shutdown` message or EOF. +//! - **The main loop.** [`worker_main`] creates the session, loads the +//! runtime config, writes the startup handshake, and serves the +//! stdin/stdout NDJSON loop until a `shutdown` message or EOF. //! //! The control-plane protocol is the same NDJSON the C++ worker speaks //! (`engine/render/ipc/ipcmessage.cpp`): one compact JSON object per line, -//! `"type"`-dispatched, with `handshake` carrying the shared-memory -//! geometry the worker attaches to via the real [`crate::ipc`] transport. -//! `load_graph`/`render_frame` reproduce the C++ validation and then -//! answer with the documented "not yet available" errors (the oaknode -//! graph crate is still a skeleton). +//! `"type"`-dispatched ([`crate::ipc`]), with `handshake` carrying the +//! shared-memory geometry the worker attaches to via the real +//! [`crate::ipc`] transport. `load_graph`/`render_frame` reproduce the +//! C++ validation and then answer with the documented "not yet available" +//! errors (the oaknode graph crate is still a skeleton). +//! +//! The bottom of this file is the C ABI export section: the +//! [`OakWorkerSession`] opaque handle and the `oakengine_worker_*` +//! exports verbatim from `engine/include/oakengine/worker.h`. use std::ffi::{c_char, c_int}; use std::io::{self, BufRead, Write}; -use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; +use serde_json::Value; -use crate::bridge::render as render_ffi; -use crate::handle::CHandle; -use crate::ipc::{FrameSlotPool, SharedMemoryRegion, ShmMode}; +use oakrender::backend::{BackendKind, DisplayRenderer}; -/// Protocol version announced in the startup handshake (`k_protocol_version`). -pub const PROTOCOL_VERSION: i32 = 1; +use crate::ipc::{ + error_message, write_message, FrameSlotPool, HandshakeMsg, LoadGraphMsg, RenderFrameMsg, + SharedMemoryRegion, ShmMode, TYPE_CANCEL, TYPE_HANDSHAKE, TYPE_LOAD_GRAPH, TYPE_RENDER_FRAME, + TYPE_SHUTDOWN, +}; -/// `"handshake"`. -const TYPE_HANDSHAKE: &str = "handshake"; -/// `"load_graph"`. -const TYPE_LOAD_GRAPH: &str = "load_graph"; -/// `"render_frame"`. -const TYPE_RENDER_FRAME: &str = "render_frame"; -/// `"cancel"`. -const TYPE_CANCEL: &str = "cancel"; -/// `"shutdown"`. -const TYPE_SHUTDOWN: &str = "shutdown"; -/// `"error"`. -const TYPE_ERROR: &str = "error"; - -/// Why `load_graph` answers "not yet available" after the real file checks. +/// Why `load_graph` answers "not yet available" (after the real file checks). const GRAPH_STUB: &str = "load_graph: node-graph deserialization is not yet available in the \ Rust worker (the oaknode crate is a todo!() skeleton; see worker/rust/README.md)"; @@ -77,82 +66,8 @@ const RENDER_STUB: &str = "render_frame: frame rendering is not yet available in worker (no node-graph or render-pipeline backing; the shm frame-slot transport is \ attached but there is no graph to render; see worker/rust/README.md)"; -/// `handshake` — field-for-field equivalent of `oak_ipc_handshake` (ipc.h); -/// wire field names match the C++ serializer. -#[derive(Serialize, Deserialize, Default, Debug, Clone)] -#[serde(default)] -pub struct HandshakeMsg { - /// Protocol version. - pub protocol_version: i32, - /// Worker->main output shared-memory segment key. - pub shm_key: String, - /// Main->worker input shared-memory segment key (optional). - pub input_shm_key: String, - /// Number of main->worker input frame slots. - pub input_slots: i32, - /// Number of worker->main output frame slots. - pub output_slots: i32, - /// Per-output-slot pixel block size. - pub slot_data_bytes: i64, - /// Per-input-slot pixel block size. - pub input_slot_data_bytes: i64, -} - -/// `load_graph` — path to a temporary file holding the serialized graph. -#[derive(Serialize, Deserialize, Default, Debug, Clone)] -#[serde(default)] -pub struct LoadGraphMsg { - /// Temporary file holding the serialized node graph. - pub path: String, -} - -/// `render_frame` — request a frame render (wire names per ipcmessage.cpp). -#[derive(Serialize, Deserialize, Default, Debug, Clone)] -#[serde(default)] -pub struct RenderFrameMsg { - /// Correlates with the eventual frame_ready. - pub ticket: i64, - /// Viewer node stable uuid in the loaded graph. - pub node: String, - /// Frame timestamp numerator. - pub time_num: i64, - /// Frame timestamp denominator. - pub time_den: i64, - /// Forced output width (0 = graph default). - pub width: i32, - /// Forced output height (0 = graph default). - pub height: i32, - /// Forced `PixelFormat::Format` (-1 = default). - pub format: i32, - /// Channel count (0 = default). - pub channels: i32, - /// RenderMode::Mode. - pub mode: i32, - /// Optional decoded input slot (-1 = none). - pub input_slot: i32, - /// Ordered decoded input slots. - pub input_slots: Vec, - /// Output color transform present? - pub has_color_transform: bool, - /// 1 when the transform is a display transform. - pub color_is_display: bool, - /// Output colorspace or display name. - pub color_output: String, - /// Display view. - pub color_view: String, - /// Display look. - pub color_look: String, -} - -/// Build a worker-side error report, mirroring `error_message()` in -/// worker.cpp: `{"type":"error","message":...}` plus `"ticket"` when -/// non-zero. -fn error_message(message: &str, ticket: Option) -> Value { - match ticket.filter(|t| *t != 0) { - Some(t) => json!({ "type": TYPE_ERROR, "message": message, "ticket": t }), - None => json!({ "type": TYPE_ERROR, "message": message }), - } -} +/// Protocol version announced in the startup handshake (`k_protocol_version`). +pub const PROTOCOL_VERSION: i32 = 1; /// Log a worker-side message to stderr, mirroring worker.cpp `log_error()` /// (the `worker: ` prefix). @@ -170,17 +85,18 @@ pub fn is_no_backend(backend: &str) -> bool { // Renderer (backend selection) // --------------------------------------------------------------------------- -/// A live, initialized oakrender display renderer handle (destroyed on -/// drop). +/// A live, initialized oakrender display renderer (destroyed on drop). pub struct Renderer { - handle: CHandle, + /// The oakrender crate's value-typed display renderer (single-lib + /// unification; the CHandle-based C ABI is deleted). + inner: DisplayRenderer, } impl Renderer { - /// Create and initialize a renderer through the oakrender module C ABI, - /// trying the named dynamic backend first and falling back to the - /// direct OpenGL renderer — the exact fallback chain of worker.cpp - /// `create_renderer()`. + /// Create and initialize a renderer through the oakrender crate's + /// direct Rust API, trying the named dynamic backend first and falling + /// back to the direct OpenGL renderer — the exact fallback chain of + /// worker.cpp `create_renderer()`. pub fn create(backend: &str) -> Result { match Self::create_dynamic(backend) { Ok(r) => Ok(r), @@ -195,58 +111,38 @@ impl Renderer { } } - /// Try the named dynamic backend through the module C ABI - /// (`oakrender_display_renderer_create_dynamic` + `_init`). + /// Try the named dynamic backend (`DisplayRenderer::new` + + /// `init`, the single-lib equivalent of + /// `oakrender_display_renderer_create_dynamic` + `_init`). fn create_dynamic(backend: &str) -> Result { - let c = std::ffi::CString::new(backend) - .map_err(|_| format!("invalid backend id {backend:?}"))?; - // SAFETY: `c` is a valid NUL-terminated string the function only - // reads during the call. - let handle = unsafe { render_ffi::oakrender_display_renderer_create_dynamic(c.as_ptr()) }; - Self::init_handle(handle, &format!("dynamic {backend}")) + let renderer = DisplayRenderer::new(BackendKind::from_config_string(backend)); + Self::init_inner(renderer, &format!("dynamic {backend}")) } /// Fall back to the direct OpenGL renderer. fn create_opengl() -> Result { - // SAFETY: no arguments; the function returns an owned handle. - let handle = unsafe { render_ffi::oakrender_display_renderer_create_opengl() }; - Self::init_handle(handle, "direct OpenGL") + let renderer = DisplayRenderer::new(BackendKind::Gl); + Self::init_inner(renderer, "direct OpenGL") } - /// Initialize a freshly created renderer handle. - fn init_handle(mut handle: CHandle, what: &str) -> Result { - if handle.is_null() { - return Err(format!("failed to create {what} renderer")); + /// Initialize a freshly created renderer. + fn init_inner(mut renderer: DisplayRenderer, what: &str) -> Result { + // NULL gl_context makes the backend use its default device/context + // path. + if let Err(e) = renderer.init(std::ptr::null_mut()) { + return Err(format!("failed to initialize {what} renderer ({e})")); } - // SAFETY: `handle` is live and owned by us; NULL gl_context makes - // the backend use its default device/context path. - let rc = - unsafe { render_ffi::oakrender_display_renderer_init(handle, std::ptr::null_mut()) }; - if rc != 0 { - // SAFETY: the handle is still owned by us (init failed). - unsafe { render_ffi::oakrender_display_renderer_destroy(&mut handle) }; - return Err(format!("failed to initialize {what} renderer (rc={rc})")); - } - Ok(Renderer { handle }) + Ok(Renderer { inner: renderer }) } /// 1 when the renderer is OpenGL-based (the C++ worker uses the GL /// context to announce the negotiated GL version in the handshake). /// - /// Not called yet: the oakrender module C ABI exposes no GL context + /// Not called yet: the oakrender module exposes no GL context /// version, so the startup handshake omits `gl_major`/`gl_minor`. #[allow(dead_code)] pub fn is_open_gl(&self) -> bool { - // SAFETY: `self.handle` is the live handle from init_handle(). - unsafe { render_ffi::oakrender_display_renderer_is_open_gl(self.handle) == 1 } - } -} - -impl Drop for Renderer { - fn drop(&mut self) { - // SAFETY: `self.handle` is the owned handle from init_handle() and - // is not used after this. - unsafe { render_ffi::oakrender_display_renderer_destroy(&mut self.handle) }; + self.inner.is_open_gl() } } @@ -272,7 +168,7 @@ impl WorkerSession { /// Create a session for `backend`, mirroring /// `oakengine_worker_session_create()`: "none"/"" skips renderer /// creation, anything else initializes the render backend through the - /// oakrender module C ABI (dynamic -> OpenGL fallback). + /// oakrender crate's direct Rust API (dynamic -> OpenGL fallback). pub fn create(backend: &str) -> Result { let renderer = if is_no_backend(backend) { None @@ -311,11 +207,9 @@ impl WorkerSession { return true; } log_error("runtime: loading color-manager default config"); - // SAFETY: no arguments; the function initializes process-wide state. - let rc = unsafe { render_ffi::oakrender_color_manager_set_up_default_config() }; - if rc != 0 { + if let Err(e) = oakrender::color::set_up_default_config() { log_error(&format!( - "runtime: color-manager default config failed (rc={rc}); continuing" + "runtime: color-manager default config failed ({e}); continuing" )); } log_error( @@ -332,7 +226,7 @@ impl WorkerSession { /// announces their geometry in its handshake reply. /// /// Deviation from the C++: `gl_major`/`gl_minor` are omitted because - /// the oakrender module C ABI exposes no GL context version. + /// the oakrender module exposes no GL context version. pub fn startup_handshake(&self) -> Value { HandshakeMsg { protocol_version: PROTOCOL_VERSION, @@ -497,59 +391,10 @@ impl WorkerSession { } } -impl HandshakeMsg { - /// Serialize to the wire `handshake` object. - pub fn to_json(&self) -> Value { - json!({ - "type": TYPE_HANDSHAKE, - "protocol_version": self.protocol_version, - "shm_key": self.shm_key, - "input_shm_key": self.input_shm_key, - "input_slots": self.input_slots, - "output_slots": self.output_slots, - "slot_data_bytes": self.slot_data_bytes, - "input_slot_data_bytes": self.input_slot_data_bytes, - }) - } -} - // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- -/// Write one NDJSON message line (compact JSON + `\n`), the Rust port of -/// `ipcmessage.cpp write_message()`. -fn write_message(w: &mut impl Write, msg: &Value) -> io::Result<()> { - let line = - serde_json::to_string(msg).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - w.write_all(line.as_bytes())?; - w.write_all(b"\n") -} - -/// Scan argv for `--backend ` (worker.cpp `oakengine_worker_main`). -/// The default is `"opengl"`; the value is lowercased; the last flag wins. -fn parse_backend(argc: c_int, argv: *mut *mut c_char) -> String { - let mut backend = "opengl".to_string(); - if argc > 0 && !argv.is_null() { - // SAFETY: `argv` points to `argc` NUL-terminated C strings (the C - // runtime's argv), and we only read the entries. - let args = unsafe { std::slice::from_raw_parts(argv, argc as usize) }; - let mut i = 1usize; - while i < args.len() { - // SAFETY: `args[i]` is a valid NUL-terminated C string. - let arg = unsafe { crate::handle::read_cstr(args[i]) }; - if arg == "--backend" && i + 1 < args.len() { - // SAFETY: `args[i + 1]` is a valid NUL-terminated C string. - backend = unsafe { crate::handle::read_cstr(args[i + 1]) }.to_ascii_lowercase(); - i += 2; - } else { - i += 1; - } - } - } - backend -} - /// Full render-worker main, transport-agnostic in the backend name. /// /// Mirrors `oakengine_worker_main()` in worker.cpp: create the session @@ -559,7 +404,8 @@ fn parse_backend(argc: c_int, argv: *mut *mut c_char) -> String { /// exit code. pub fn worker_main(backend: &str) -> i32 { // 1. Session creation initializes the render backend through the - // oakrender module C ABI (oakengine_worker_session_create()). + // oakrender crate's direct Rust API + // (oakengine_worker_session_create()). let mut session = match WorkerSession::create(backend) { Ok(s) => s, Err(msg) => { @@ -628,6 +474,325 @@ pub fn worker_main(backend: &str) -> i32 { } // --------------------------------------------------------------------------- +// Unit tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use crate::ipc::{FrameSlotPool, SharedMemoryRegion, ShmMode}; + use serde_json::json; + use std::ptr; + + fn test_key(name: &str) -> String { + static COUNTER: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0); + let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + SharedMemoryRegion::make_key(i64::from(std::process::id()), (n & 0x7FFF) as i32) + + &format!("-w-{name}") + } + + /// The "parent" side of a handshake: create an output segment holding a + /// pool, optionally an input segment, and return the handshake message + /// plus the owner regions (kept alive by the caller). + fn parent_side( + slots: i32, + slot_bytes: i64, + input: bool, + ) -> (Value, SharedMemoryRegion, Option) { + 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) // --------------------------------------------------------------------------- @@ -757,6 +922,30 @@ pub unsafe extern "C" fn oakengine_worker_session_shutdown_requested( }) } +/// 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 @@ -774,7 +963,7 @@ pub unsafe extern "C" fn oakengine_worker_main(argc: c_int, argv: *mut *mut c_ch // --------------------------------------------------------------------------- #[cfg(test)] -mod tests { +mod cabi_tests { use super::*; use crate::ipc::{self, FrameSlotPool, SharedMemoryRegion, ShmMode}; use serde_json::json; diff --git a/crates/oakengine/tests/it_node.rs b/crates/oakengine/tests/it_node.rs deleted file mode 100644 index 78e84837b..000000000 --- a/crates/oakengine/tests/it_node.rs +++ /dev/null @@ -1,5619 +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 node family: every `oakengine_*` export of -//! the facade's node module (src/node.rs; module C contract -//! `include/node/{node,project,footage,keyframe,dragger,group,multicam,...}.h`), -//! exercised end to end against the REAL `oaknode` crate — no mocks. -//! -//! Coverage contract (see the sibling it_plugin.rs): -//! - every export is called on a legal path with the result asserted; -//! - value-range inputs (indices, type ordinals, sizes, track counts) get -//! the meaningful combinations; -//! - illegal inputs (NULL / empty `CHandle` boxes, out-of-range indices, -//! zero/negative sizes, garbage enum ordinals) return a clean negative -//! code or the documented no-op — never a crash; -//! - destroy contracts: facade `free`/`dispose` are NULL/empty no-ops, and -//! the module destroy paths they delegate to are double-free-safe; -//! - the family's debug alive counter (`oaknode_debug_alive_count`) returns -//! to baseline after every owned-object round trip. -//! -//! The facade owns a process-wide undo stack, so every test that pushes -//! undoable commands (or asserts undo state) runs inside [`with_owned`], -//! the same global mutex that guards the alive-counter assertions (owned -//! objects are the only ALIVE sources). Tests that only touch borrowed -//! handles or static helpers run in parallel. -//! -//! The node family is 327 exports. The facade divergences found while -//! exercising the family (documented in the block below the tests) are all -//! fixed in src/node.rs and asserted as correct behavior here. - -#[path = "common/mod.rs"] -mod common; - -use std::ffi::{c_char, c_int, c_void, CStr, CString}; -use std::sync::Mutex; - -use oakengine::common::OakVideoParamsPod; -use oakengine::handle::{ - free_box, CHandle, OakEngineClip, OakEngineFootage, OakEngineKeyframe, OakEngineNode, - OakEngineNodeDragger, OakEngineProject, OakEngineSequence, -}; -use oakengine::node::value_type as vt; -use oakengine::node::*; -use oakengine::timeline::oakengine_clip_as_node; -use oakengine::undo::oakengine_undo_command_free; -use oaknode::ffi::dragger::oaknode_dragger_free; -use oaknode::ffi::factory::oaknode_factory_create_from_id; -use oaknode::ffi::keyframe::oaknode_keyframe_create as oaknode_kf_create; -use oaknode::ffi::keyframe::oaknode_keyframe_free; -use oaknode::ffi::node::oaknode_debug_alive_count; -use oaknode::ffi::node::oaknode_node_free; -use oaknode::ffi::project::oaknode_project_free; - -/// 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; -/// Module error codes (include/node/error.h) — passed through untranslated. -const NODE_E_INVALID: c_int = -30001; -const NODE_E_STATE: c_int = -30002; -const NODE_E_NOT_FOUND: c_int = -30004; - -/// Registered node type ids used by the tests (NUL-terminated `&CStr` so -/// `.as_ptr()` is a valid C string, matching the engine's `c"..."` usage). -const TYPE_VALUE: &std::ffi::CStr = c"org.olivevideoeditor.Olive.value"; -const TYPE_SOLID: &std::ffi::CStr = c"org.olivevideoeditor.Olive.solidgenerator"; -const TYPE_TRANSFORM: &std::ffi::CStr = c"org.olivevideoeditor.Olive.transform"; -const TYPE_TEXT: &std::ffi::CStr = c"org.olivevideoeditor.Olive.textgenerator"; -const TYPE_GROUP: &std::ffi::CStr = c"org.olivevideoeditor.Olive.group"; -const TYPE_MULTICAM: &std::ffi::CStr = c"org.olivevideoeditor.Olive.multicam"; -const TYPE_FOOTAGE: &std::ffi::CStr = c"org.olivevideoeditor.Olive.footage"; -const TYPE_BLUR: &std::ffi::CStr = c"org.olivevideoeditor.Olive.blur"; -const TYPE_OPACITY: &std::ffi::CStr = c"org.olivevideoeditor.Olive.opacity"; -const TYPE_SEQUENCE: &std::ffi::CStr = c"org.olivevideoeditor.Olive.sequence"; -const TYPE_CLIPBLOCK: &std::ffi::CStr = c"org.olivevideoeditor.Olive.clipblock"; - -// --------------------------------------------------------------------------- -// Fixtures -// --------------------------------------------------------------------------- - -/// Serialize the owned-object / undo-stack tests: the alive counter and -/// the process-wide undo stack are shared across tests in this binary, so -/// every test that creates owned objects or pushes commands holds the same -/// mutex. Tests that only use borrowed handles run unguarded. -fn with_owned(f: impl FnOnce()) { - static LOCK: Mutex<()> = Mutex::new(()); - let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner()); - f(); -} - -/// Live owned objects (projects, factory/group/copy nodes). -fn alive() -> c_int { - unsafe { oaknode_debug_alive_count() } -} - -/// Read a two-stage facade string out of a fixed buffer. -unsafe fn read_buf(buf: &[c_char]) -> String { - if buf.first().copied().unwrap_or(0) == 0 { - String::new() - } else { - CStr::from_ptr(buf.as_ptr()).to_string_lossy().into_owned() - } -} - -/// Read a NUL-terminated C string. -unsafe fn read_cstr(s: *const c_char) -> String { - unsafe { CStr::from_ptr(s).to_string_lossy().into_owned() } -} - -/// A float POD value. -fn float_value(x: f64) -> OakNodeValue { - OakNodeValue { - kind: vt::FLOAT, - num: 0, - den: 0, - f: [x, 0.0, 0.0, 0.0], - } -} - -/// A float POD value of the DIRECTLY imported oaknode instance. Under -/// `cargo test --workspace` oaknode is built twice (oaknode's own -/// dev-dependency enables oakcodec/test-stubs, splitting the build -/// units), so `oakengine::node::OakNodeValue` and -/// `oaknode::value::OakNodeValue` are distinct types there; the direct -/// `oaknode_ffi` calls below need this one. -fn float_value_direct(x: f64) -> oaknode::value::OakNodeValue { - oaknode::value::OakNodeValue { - kind: oaknode::value::oak::FLOAT, - num: 0, - den: 0, - f: [x, 0.0, 0.0, 0.0], - } -} - -/// An int (combo-compatible) POD value. -fn int_value(x: i64) -> OakNodeValue { - OakNodeValue { - kind: vt::INT, - num: x, - den: 0, - f: [0.0; 4], - } -} - -/// A vec2 POD value. -fn vec2_value(x: f64, y: f64) -> OakNodeValue { - OakNodeValue { - kind: vt::VEC2, - num: 0, - den: 0, - f: [x, y, 0.0, 0.0], - } -} - -/// A fresh project box (owned; caller frees with `oakengine_project_free`). -fn new_project() -> *mut OakEngineProject { - let p = oakengine_project_create(); - assert!(!p.is_null()); - assert_eq!(unsafe { oakengine_project_new(p) }, 0); - p -} - -/// A box wrapping an empty (null-ctx) node handle — the "invalid handle" -/// class distinct from a NULL pointer. -fn empty_node_box() -> *mut OakEngineNode { - Box::into_raw(Box::new(OakEngineNode { - handle: CHandle::null(), - })) -} - -/// A live box holding an addref'd copy of `node`'s handle, to hand to -/// `oakengine_node_group_get_inner` (that export replaces the box with the -/// resolved node; the replaced shell is leaked by design of the API). The -/// copy is addref'd so freeing this shell and the original handle later -/// both release their own reference (no double-free). -unsafe fn group_inner_slot(node: *mut OakEngineNode) -> *mut OakEngineNode { - let mut handle = unsafe { (*node).handle }; - if let Some(addref) = handle.addref { - unsafe { addref(handle.ctx) }; - } - Box::into_raw(Box::new(OakEngineNode { handle })) -} - -/// Fresh file under the system temp dir with a unique name. -fn fresh_temp_file(name: &str, contents: &[u8]) -> std::path::PathBuf { - let p = std::env::temp_dir().join(format!("oak-it-node-{}-{name}", std::process::id())); - std::fs::write(&p, contents).expect("write temp file"); - p -} - -/// The index of the first project node whose type id matches `id`, or -1. -unsafe fn find_node(project: *mut OakEngineProject, id: &str) -> c_int { - let count = oakengine_project_node_count(project); - for i in 0..count { - let node = oakengine_project_node_at(project, i); - if node.is_null() { - continue; - } - let mut buf = [0 as c_char; 256]; - let len = oakengine_node_get_type_id(node, buf.as_mut_ptr(), 256); - if len > 0 && unsafe { read_buf(&mut buf) } == id { - return i; - } - } - -1 -} - -/// Force the oakundo command / oakcommon xml dlsym targets into the link -/// (the oaknode serializer bridge resolves them at runtime; same helper as -/// the sibling tests/node.rs). -fn force_oakundo_command_link() -> usize { - let fns: [usize; 3] = [ - oakundo::ffi::command::oakundo_command_init as *const () as usize, - oakcommon::ffi::xmlutils::oakcommon_xml_writer_init as *const () as usize, - oakcommon::ffi::xmlutils::oakcommon_xml_reader_init as *const () as usize, - ]; - fns.iter().sum() -} - -// --------------------------------------------------------------------------- -// Static helpers and pure functions (no handles; parallel-safe) -// --------------------------------------------------------------------------- - -/// Static node ids, category names, value-type names and value math. -#[test] -fn static_ids_and_pure_helpers() { - common::force_link(); - let _ = force_oakundo_command_link(); - - // Static input-id strings. - assert_eq!( - unsafe { read_cstr(oakengine_folder_child_input_key()) }, - "child_in" - ); - assert_eq!( - unsafe { read_cstr(oakengine_node_enabled_input_id()) }, - "enabled_in" - ); - assert_eq!( - unsafe { read_cstr(oakengine_volume_samples_input_id()) }, - "samples_in" - ); - assert_eq!( - unsafe { read_cstr(oakengine_transform_texture_input_id()) }, - "tex_in" - ); - assert_eq!( - unsafe { read_cstr(oakengine_transition_in_block_input_id()) }, - "in_block_in" - ); - assert_eq!( - unsafe { read_cstr(oakengine_transition_out_block_input_id()) }, - "out_block_in" - ); - assert_eq!( - unsafe { read_cstr(oakengine_subtitle_text_input_id()) }, - "text_in" - ); - assert_eq!( - unsafe { read_cstr(oakengine_project_item_mime_type()) }, - "application/x-oliveprojectitemdata" - ); - assert_eq!( - unsafe { read_cstr(oakengine_multicam_input_current()) }, - "current_in" - ); - assert_eq!( - unsafe { read_cstr(oakengine_multicam_input_sources()) }, - "sources_in" - ); - assert_eq!( - unsafe { read_cstr(oakengine_multicam_input_sequence()) }, - "sequence_in" - ); - assert_eq!( - unsafe { read_cstr(oakengine_multicam_input_sequence_type()) }, - "sequence_type_in" - ); - - // Static flags. - assert_eq!(oakengine_node_flag_dont_show_in_param_view(), 0x1); - assert_eq!(oakengine_node_flag_video_effect(), 0x2); - assert_eq!(oakengine_node_flag_audio_effect(), 0x4); - assert_eq!(oakengine_node_flag_dont_show_in_create_menu(), 0x8); - - // Static scalars. - assert_eq!(oakengine_audio_waveform_max_sample_rate(), 1024.0); - assert_eq!(oakengine_keyframe_default_type(), 0); - assert_eq!(oakengine_keyframe_opposing_bezier_type(0), 1); - assert_eq!(oakengine_keyframe_opposing_bezier_type(1), 0); - // Garbage type ordinal → module INVALID, never a crash. - assert_eq!(oakengine_keyframe_opposing_bezier_type(2), NODE_E_INVALID); - assert_eq!(oakengine_keyframe_opposing_bezier_type(-7), NODE_E_INVALID); - - // Category name matrix: 0..=11 named, everything else "Uncategorized". - let mut buf = [0 as c_char; 64]; - for (cat, expected) in [ - (0, "Output"), - (1, "Generator"), - (2, "Math"), - (3, "Keying"), - (4, "Filter"), - (5, "Color"), - (6, "Time"), - (7, "Timeline"), - (8, "Transition"), - (9, "Distort"), - (10, "Project"), - (11, "OpenFX"), - ] { - let len = unsafe { oakengine_node_category_name(cat, buf.as_mut_ptr(), 64) }; - assert_eq!(len, expected.len() as c_int); - assert_eq!(unsafe { read_buf(&mut buf) }, expected); - } - assert_eq!( - unsafe { oakengine_node_category_name(12, buf.as_mut_ptr(), 64) }, - 13 - ); - assert_eq!(unsafe { read_buf(&mut buf) }, "Uncategorized"); - assert_eq!( - unsafe { oakengine_node_category_name(-1, buf.as_mut_ptr(), 64) }, - 13 - ); - assert_eq!(unsafe { read_buf(&mut buf) }, "Uncategorized"); - - // Value keyframe track counts: 1 scalar, 2/3/4 vectors, 4 color, 6 bezier. - assert_eq!(oakengine_node_value_keyframe_track_count(vt::FLOAT), 1); - assert_eq!(oakengine_node_value_keyframe_track_count(vt::INT), 1); - assert_eq!(oakengine_node_value_keyframe_track_count(vt::VEC2), 2); - assert_eq!(oakengine_node_value_keyframe_track_count(vt::VEC3), 3); - assert_eq!(oakengine_node_value_keyframe_track_count(vt::VEC4), 4); - assert_eq!(oakengine_node_value_keyframe_track_count(vt::COLOR), 4); - assert_eq!(oakengine_node_value_keyframe_track_count(vt::BEZIER), 6); - assert_eq!(oakengine_node_value_keyframe_track_count(999), 1); - - // Pretty type names for every legal ordinal. - for ty in 1..=19 { - let len = unsafe { oakengine_node_value_pretty_type_name(ty, buf.as_mut_ptr(), 64) }; - assert!(len > 0, "pretty type name for {ty} must not be empty"); - } - // NONE (0), negative and > AUDIO_PARAMS are invalid. - assert_eq!( - unsafe { oakengine_node_value_pretty_type_name(0, buf.as_mut_ptr(), 64) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_value_pretty_type_name(-1, buf.as_mut_ptr(), 64) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_value_pretty_type_name(20, buf.as_mut_ptr(), 64) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_value_pretty_type_name(999, buf.as_mut_ptr(), 64) }, - E_INVALID - ); - - // split_to_tracks: vec2 → two per-component track values; combine back. - let mut tracks = [unsafe { std::mem::zeroed::() }; 2]; - assert_eq!( - unsafe { - oakengine_node_value_split_to_tracks( - vt::VEC2, - &vec2_value(1.0, 2.0), - tracks.as_mut_ptr(), - 2, - ) - }, - 0 - ); - assert_eq!(tracks[0].kind, vt::VEC2); - // Track `i` carries component `i`; combine reassembles them. - assert!((tracks[0].f[0] - 1.0).abs() < 1e-9); - assert!((tracks[1].f[0] - 2.0).abs() < 1e-9); - let mut out = unsafe { std::mem::zeroed::() }; - assert_eq!( - unsafe { oakengine_node_value_combine_tracks(vt::VEC2, tracks.as_ptr(), 2, &mut out) }, - 0 - ); - assert!((out.f[0] - 1.0).abs() < 1e-9); - assert!((out.f[1] - 2.0).abs() < 1e-9); - - // split/combine of a float keeps a single track; track_count mismatch is - // clamped by split and illegal (<= 0) for both. - let mut ft = [unsafe { std::mem::zeroed::() }; 4]; - assert_eq!( - unsafe { - oakengine_node_value_split_to_tracks(vt::FLOAT, &float_value(3.5), ft.as_mut_ptr(), 4) - }, - 0 - ); - assert!((ft[0].f[0] - 3.5).abs() < 1e-9); - assert_eq!( - unsafe { - oakengine_node_value_split_to_tracks( - vt::FLOAT, - &float_value(1.0), - std::ptr::null_mut(), - 1, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_value_split_to_tracks(vt::FLOAT, &float_value(1.0), ft.as_mut_ptr(), 0) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_value_split_to_tracks(vt::FLOAT, std::ptr::null(), ft.as_mut_ptr(), 1) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_value_combine_tracks(vt::FLOAT, ft.as_ptr(), 0, &mut out) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_value_combine_tracks(vt::FLOAT, std::ptr::null(), 1, &mut out) }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_value_combine_tracks(vt::FLOAT, ft.as_ptr(), 1, std::ptr::null_mut()) - }, - E_INVALID - ); - - // Multicam grid math legal matrix + illegal inputs. - let mut rows: c_int = 0; - let mut cols: c_int = 0; - assert_eq!( - unsafe { oakengine_multicam_get_rows_and_columns(1, &mut rows, &mut cols) }, - 0 - ); - assert_eq!((rows, cols), (1, 1)); - assert_eq!( - unsafe { oakengine_multicam_get_rows_and_columns(4, &mut rows, &mut cols) }, - 0 - ); - assert_eq!((rows, cols), (2, 2)); - assert_eq!( - unsafe { oakengine_multicam_get_rows_and_columns(0, &mut rows, &mut cols) }, - 0 - ); - assert_eq!((rows, cols), (1, 1)); - assert_eq!( - unsafe { oakengine_multicam_get_rows_and_columns(-1, &mut rows, &mut cols) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_multicam_get_rows_and_columns(4, std::ptr::null_mut(), &mut cols) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_multicam_get_rows_and_columns(4, &mut rows, std::ptr::null_mut()) }, - E_INVALID - ); - let mut r: c_int = 0; - let mut c: c_int = 0; - assert_eq!( - unsafe { oakengine_multicam_index_to_row_cols(0, 2, 2, &mut r, &mut c) }, - 0 - ); - assert_eq!((r, c), (0, 0)); - assert_eq!( - unsafe { oakengine_multicam_index_to_row_cols(3, 2, 2, &mut r, &mut c) }, - 0 - ); - assert_eq!((r, c), (1, 1)); - assert_eq!( - unsafe { oakengine_multicam_index_to_row_cols(-1, 2, 2, &mut r, &mut c) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_multicam_index_to_row_cols(0, 0, 2, &mut r, &mut c) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_multicam_index_to_row_cols(0, 2, 0, &mut r, &mut c) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_multicam_index_to_row_cols(0, 2, 2, std::ptr::null_mut(), &mut c) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_multicam_index_to_row_cols(0, 2, 2, &mut r, std::ptr::null_mut()) }, - E_INVALID - ); - assert_eq!(oakengine_multicam_rows_cols_to_index(1, 1, 2, 2), 3); - assert_eq!(oakengine_multicam_rows_cols_to_index(0, 0, 2, 2), 0); - assert_eq!( - oakengine_multicam_rows_cols_to_index(-1, 0, 2, 2), - E_INVALID - ); - assert_eq!( - oakengine_multicam_rows_cols_to_index(0, -1, 2, 2), - E_INVALID - ); - assert_eq!(oakengine_multicam_rows_cols_to_index(2, 0, 2, 2), E_INVALID); - assert_eq!(oakengine_multicam_rows_cols_to_index(0, 2, 2, 2), E_INVALID); - assert_eq!(oakengine_multicam_rows_cols_to_index(0, 0, 0, 2), E_INVALID); - assert_eq!(oakengine_multicam_rows_cols_to_index(0, 0, 2, 0), E_INVALID); - - // Footage stream type names. - for (t, name) in [ - (0, "Video"), - (1, "Audio"), - (2, "Subtitle"), - (3, "Unknown"), - (-1, "Unknown"), - ] { - let len = unsafe { oakengine_footage_stream_type_name(t, buf.as_mut_ptr(), 64) }; - assert_eq!(len, name.len() as c_int); - assert_eq!(unsafe { read_buf(&mut buf) }, name); - } -} - -// --------------------------------------------------------------------------- -// Serialized legal paths (undo-stack mutating; owned objects) -// --------------------------------------------------------------------------- - -/// Project lifecycle, factory, node metadata/params/graph edits, keyframes, -/// dragger, group, multicam, folder, bulk delete and footage — the full -/// legal matrix of the node family in one serialized test (the facade's -/// undo stack is process-wide, and owned-object creation feeds the alive -/// counter, both guarded by [`with_owned`]). -#[test] -fn node_family_legal_paths() { - with_owned(|| { - common::force_link(); - let _ = force_oakundo_command_link(); - let base = alive(); - let mut buf = [0 as c_char; 512]; - - // ---- project shell: create → new → name/filename/cache -------- - let project = oakengine_project_create(); - assert!(!project.is_null()); - assert_eq!(alive(), base + 1, "an owned project must be alive-counted"); - - // Freeing NULL is a no-op. - unsafe { oakengine_project_free(std::ptr::null_mut()) }; - - // A fresh project is untitled. - let len = unsafe { oakengine_project_name(project, buf.as_mut_ptr(), 512) }; - assert_eq!(len, "(untitled)".len() as c_int); - assert_eq!(unsafe { read_buf(&mut buf) }, "(untitled)"); - assert_eq!( - unsafe { oakengine_project_filename(project, buf.as_mut_ptr(), 512) }, - 0 - ); - - // A second new on the same project is rejected with E_STATE. - assert_eq!(unsafe { oakengine_project_new(project) }, 0); - assert_eq!(unsafe { oakengine_project_new(project) }, E_STATE); - - // Modified flag round trip. - assert_eq!(unsafe { oakengine_project_is_modified(project) }, 0); - assert_eq!(unsafe { oakengine_project_set_modified(project, 1) }, 0); - assert_eq!(unsafe { oakengine_project_is_modified(project) }, 1); - assert_eq!(unsafe { oakengine_project_set_modified(project, 0) }, 0); - assert_eq!(unsafe { oakengine_project_is_modified(project) }, 0); - - // Filename drives the display name. - assert_eq!( - unsafe { oakengine_project_set_filename(project, c"/tmp/oak_it_node.ovexml".as_ptr()) }, - 0 - ); - let len = unsafe { oakengine_project_filename(project, buf.as_mut_ptr(), 512) }; - assert!(len > 0); - assert!(unsafe { read_buf(&mut buf) }.ends_with("oak_it_node.ovexml")); - let len = unsafe { oakengine_project_pretty_filename(project, buf.as_mut_ptr(), 512) }; - assert!(len > 0); - let len = unsafe { oakengine_project_name(project, buf.as_mut_ptr(), 512) }; - assert_eq!(len, "oak_it_node".len() as c_int); - assert_eq!(unsafe { read_buf(&mut buf) }, "oak_it_node"); - - // Cache path surfaces. - let rc = unsafe { oakengine_project_cache_path(project, buf.as_mut_ptr(), 512) }; - assert!(rc >= 0); - assert_eq!( - unsafe { oakengine_project_get_cache_location_setting(project) }, - 0 - ); - assert_eq!( - unsafe { oakengine_project_get_custom_cache_path(project, buf.as_mut_ptr(), 512) }, - 0 - ); - assert_eq!( - unsafe { - oakengine_project_set_custom_cache_path(project, c"/tmp/oak_it_cache".as_ptr()) - }, - 0 - ); - let len = - unsafe { oakengine_project_get_custom_cache_path(project, buf.as_mut_ptr(), 512) }; - assert!(len > 0); - assert_eq!(unsafe { read_buf(&mut buf) }, "/tmp/oak_it_cache"); - // NULL path clears the custom cache path. - assert_eq!( - unsafe { oakengine_project_set_custom_cache_path(project, std::ptr::null()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_project_get_custom_cache_path(project, buf.as_mut_ptr(), 512) }, - 0 - ); - // Alongside path is a documented stub returning an empty string. - assert_eq!( - unsafe { oakengine_project_cache_alongside_path(project, buf.as_mut_ptr(), 512) }, - 0 - ); - - // Color reference space is a documented stub ("" / OK). - assert_eq!( - unsafe { oakengine_project_get_color_reference_space(project, buf.as_mut_ptr(), 512) }, - 0 - ); - assert_eq!( - unsafe { oakengine_project_set_color_reference_space(project, c"rec709".as_ptr()) }, - 0 - ); - - // Sequence enumeration: a fresh project has none. - assert_eq!(unsafe { oakengine_project_sequence_count(project) }, 0); - assert!(unsafe { oakengine_project_sequence_at(project, 0) }.is_null()); - - // Root folder + node enumeration. - assert_eq!(unsafe { oakengine_project_node_count(project) }, 1); - let root = unsafe { oakengine_project_root(project) }; - assert!(!root.is_null()); - assert_eq!(unsafe { oakengine_node_is_folder(root) }, 1); - assert_eq!(unsafe { oakengine_node_is_item(root) }, 1); - assert_eq!( - unsafe { oakengine_project_node_at(project, 0) }.is_null(), - false - ); - assert!(unsafe { oakengine_project_node_at(project, 999) }.is_null()); - unsafe { oakengine_node_free(root) }; // borrowed shell - - // ---- factory ---------------------------------------------------- - let factory_count = oakengine_node_factory_id_count(); - assert!(factory_count > 0); - 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(), 512) }; - assert!(len > 0); - let type_id = unsafe { read_buf(&mut buf) }; - let type_id_c = CString::new(type_id.as_str()).unwrap(); - let name_len = unsafe { - oakengine_node_factory_name_from_id(type_id_c.as_ptr(), buf.as_mut_ptr(), 512) - }; - assert!( - name_len > 0, - "factory name for a registered id must resolve" - ); - // Factory node index bounds. - assert!(unsafe { oakengine_node_factory_node_at(-1) }.is_null()); - assert!(unsafe { oakengine_node_factory_node_at(1_000_000) }.is_null()); - // NULL type id → empty name (length 0), NULL → NULL node. - assert_eq!( - unsafe { oakengine_node_factory_name_from_id(std::ptr::null(), buf.as_mut_ptr(), 512) }, - 0 - ); - assert!(unsafe { oakengine_node_factory_create_from_id(std::ptr::null()) }.is_null()); - // Unknown type id → NULL + a non-empty last error. - assert!( - unsafe { oakengine_node_factory_create_from_id(c"org.oak.no.such.node".as_ptr()) } - .is_null() - ); - let err_len = unsafe { oakengine_node_last_error(buf.as_mut_ptr(), 512) }; - assert!( - err_len > 0, - "node_last_error must be non-empty after a failed factory lookup" - ); - unsafe { oakengine_node_free(proto) }; // borrowed prototype shell - - // Category / description / flags surfaces are stubs with stable results. - let orphan = unsafe { oakengine_node_factory_create_from_id(TYPE_VALUE.as_ptr()) }; - assert!(!orphan.is_null()); - assert_eq!( - alive(), - base + 2, - "an owned factory node must be alive-counted" - ); - assert_eq!(unsafe { oakengine_node_category_count(orphan) }, 0); - assert_eq!(unsafe { oakengine_node_category_at(orphan, 0) }, -1); - assert_eq!(unsafe { oakengine_node_get_flags(orphan) }, 0); - assert_eq!( - unsafe { oakengine_node_get_sub_category(orphan, buf.as_mut_ptr(), 512) }, - 0 - ); - assert_eq!( - unsafe { oakengine_node_get_description(orphan, buf.as_mut_ptr(), 512) }, - 0 - ); - unsafe { oakengine_node_retranslate(orphan) }; // void no-op - unsafe { oakengine_node_delete_later(orphan) }; // void no-op - unsafe { oakengine_node_get_brush(orphan, 0.0, 1.0, std::ptr::null_mut()) }; // void no-op - - // ---- metadata: type id / name / short name / label -------------- - let len = unsafe { oakengine_node_get_type_id(orphan, buf.as_mut_ptr(), 512) }; - assert_eq!(len, TYPE_VALUE.to_bytes().len() as c_int); - assert_eq!(unsafe { read_buf(&mut buf) }, TYPE_VALUE.to_str().unwrap()); - let len = unsafe { oakengine_node_get_name(orphan, buf.as_mut_ptr(), 512) }; - assert_eq!(len, "Value".len() as c_int); - assert_eq!(unsafe { read_buf(&mut buf) }, "Value"); - let len = unsafe { oakengine_node_get_short_name(orphan, buf.as_mut_ptr(), 512) }; - assert_eq!( - len, - "Value".len() as c_int, - "short name falls back to the name" - ); - // Label is empty until set. - assert_eq!( - unsafe { oakengine_node_get_label(orphan, buf.as_mut_ptr(), 512) }, - 0 - ); - - // ---- standalone copy (owned; free back to baseline) ------------- - let copy = unsafe { oakengine_node_create_copy(orphan) }; - assert!(!copy.is_null()); - assert_eq!(alive(), base + 3); - let len = unsafe { oakengine_node_get_type_id(copy, buf.as_mut_ptr(), 512) }; - assert_eq!(len, TYPE_VALUE.to_bytes().len() as c_int); - unsafe { oakengine_node_free(copy) }; - assert_eq!( - alive(), - base + 2, - "freeing an owned copy must return the alive counter" - ); - assert!(unsafe { oakengine_node_create_copy(std::ptr::null()) }.is_null()); - - // ---- project add nodes ------------------------------------------ - let solid = unsafe { oakengine_project_add_node(project, TYPE_SOLID.as_ptr()) }; - assert!(!solid.is_null()); - let transform = unsafe { oakengine_project_add_node(project, TYPE_TRANSFORM.as_ptr()) }; - assert!(!transform.is_null()); - let value = unsafe { oakengine_project_add_node(project, TYPE_VALUE.as_ptr()) }; - assert!(!value.is_null()); - let value2 = unsafe { oakengine_project_add_node(project, TYPE_VALUE.as_ptr()) }; - assert!(!value2.is_null()); - let group = unsafe { oakengine_project_add_node(project, TYPE_GROUP.as_ptr()) }; - assert!(!group.is_null()); - let multicam = unsafe { oakengine_project_add_node(project, TYPE_MULTICAM.as_ptr()) }; - assert!(!multicam.is_null()); - // 6 added nodes + the root folder. - assert_eq!(unsafe { oakengine_project_node_count(project) }, 7); - // Unknown id → NULL + last error. - assert!(unsafe { oakengine_project_add_node(project, c"org.oak.nope".as_ptr()) }.is_null()); - assert!( - unsafe { oakengine_project_add_node(std::ptr::null_mut(), TYPE_VALUE.as_ptr()) } - .is_null() - ); - - // The added nodes report their owning project. - let owned_project = unsafe { oakengine_node_get_project(value) }; - assert!(!owned_project.is_null()); - unsafe { oakengine_project_free(owned_project) }; // borrowed project shell - let owned_project2 = unsafe { oakengine_project_from_object(value) }; - assert!(!owned_project2.is_null()); - unsafe { oakengine_project_free(owned_project2) }; - - // ---- 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_viewer_output(solid) }, 0); - assert_eq!(unsafe { oakengine_node_is_footage(solid) }, 0); - assert_eq!(unsafe { oakengine_node_is_sequence(solid) }, 0); - assert_eq!(unsafe { oakengine_node_is_folder(solid) }, 0); - assert_eq!(unsafe { oakengine_node_is_group(group) }, 1); - assert_eq!(unsafe { oakengine_node_is_group(solid) }, 0); - assert_eq!(unsafe { oakengine_node_is_multicam(multicam) }, 1); - assert_eq!(unsafe { oakengine_node_is_multicam(solid) }, 0); - assert_eq!(unsafe { oakengine_node_is_item(solid) }, 0); - assert_eq!( - unsafe { oakengine_node_is_item(group) }, - 1, - "group is a project-tree item" - ); - - // ---- label operations (undoable) -------------------------------- - assert_eq!( - unsafe { oakengine_node_set_label(value, c"My Value".as_ptr()) }, - 0 - ); - let len = unsafe { oakengine_node_get_label(value, buf.as_mut_ptr(), 512) }; - assert_eq!(len, "My Value".len() as c_int); - assert_eq!(unsafe { read_buf(&mut buf) }, "My Value"); - // NULL label → empty label (documented). - assert_eq!( - unsafe { oakengine_node_set_label(value, std::ptr::null()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_node_get_label(value, buf.as_mut_ptr(), 512) }, - 0 - ); - - // set_label_ex matrix: undoable (1) and live (0). - assert_eq!( - unsafe { oakengine_node_set_label_ex(value, c"Ex".as_ptr(), 1) }, - 0 - ); - assert_eq!( - unsafe { oakengine_node_set_label_ex(value, c"Ex2".as_ptr(), 0) }, - 0 - ); - let len = unsafe { oakengine_node_get_label(value, buf.as_mut_ptr(), 512) }; - assert_eq!(len, 3); - assert_eq!(unsafe { read_buf(&mut buf) }, "Ex2"); - - // label_and_name: label wins over the name. - let len = unsafe { oakengine_node_get_label_and_name(value, buf.as_mut_ptr(), 512) }; - assert_eq!(len, 3); - assert_eq!(unsafe { read_buf(&mut buf) }, "Ex2"); - // No label → the name. - let len = unsafe { oakengine_node_get_label_and_name(solid, buf.as_mut_ptr(), 512) }; - assert!(len > 0); - - // rename_many: one multi command for several nodes. - let mut nodes = [value, value2]; - assert_eq!( - unsafe { - oakengine_node_rename_many( - nodes.as_mut_ptr(), - 2, - c"Renamed".as_ptr(), - std::ptr::null_mut(), - ) - }, - 0 - ); - let len = unsafe { oakengine_node_get_label(value, buf.as_mut_ptr(), 512) }; - assert_eq!(len, "Renamed".len() as c_int); - // count 0 is a no-op; count < 0 or a NULL entry is invalid. - assert_eq!( - unsafe { - oakengine_node_rename_many( - nodes.as_mut_ptr(), - 0, - c"x".as_ptr(), - std::ptr::null_mut(), - ) - }, - 0 - ); - assert_eq!( - unsafe { - oakengine_node_rename_many( - nodes.as_mut_ptr(), - -1, - c"x".as_ptr(), - std::ptr::null_mut(), - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_rename_many( - std::ptr::null_mut(), - 2, - c"x".as_ptr(), - std::ptr::null_mut(), - ) - }, - E_INVALID - ); - nodes[1] = std::ptr::null_mut(); - assert_eq!( - unsafe { - oakengine_node_rename_many( - nodes.as_mut_ptr(), - 2, - c"x".as_ptr(), - std::ptr::null_mut(), - ) - }, - E_INVALID - ); - // set_label_many delegates to rename_many. - nodes = [value, value2]; - assert_eq!( - unsafe { oakengine_node_set_label_many(nodes.as_mut_ptr(), 2, c"Many".as_ptr()) }, - 0 - ); - - // rename_command: opaque command pointer (caller owns → free). - let cmd = unsafe { oakengine_node_rename_command(value, c"Cmd".as_ptr()) }; - assert!(!cmd.is_null()); - unsafe { oakengine_undo_command_free(cmd) }; - assert!( - unsafe { oakengine_node_rename_command(std::ptr::null_mut(), c"x".as_ptr()) }.is_null() - ); - // NULL label on the command is legal (empty label). - let cmd = unsafe { oakengine_node_rename_command(value, std::ptr::null()) }; - assert!(!cmd.is_null()); - unsafe { oakengine_undo_command_free(cmd) }; - - // ---- color labels ----------------------------------------------- - assert_eq!(unsafe { oakengine_node_get_color_label(value) }, -1); - assert_eq!( - unsafe { oakengine_node_get_effective_color_label(value) }, - -1 - ); - let cmd = unsafe { oakengine_node_set_color_label_command(value, 3) }; - assert!(!cmd.is_null()); - unsafe { oakengine_undo_command_free(cmd) }; - let mut cl_nodes = [value, value2]; - assert_eq!( - unsafe { oakengine_node_set_color_label(cl_nodes.as_mut_ptr(), 2, 2) }, - 0 - ); - assert_eq!( - unsafe { oakengine_node_set_color_label(cl_nodes.as_mut_ptr(), 0, 2) }, - 0 - ); - assert_eq!( - unsafe { oakengine_node_set_color_label(cl_nodes.as_mut_ptr(), -1, 2) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_set_color_label(std::ptr::null_mut(), 1, 2) }, - E_INVALID - ); - - // ---- input introspection ---------------------------------------- - // Every node carries the standard enabled_in plus the value node's - // type_in/value_in. - assert_eq!(unsafe { oakengine_node_input_count(value) }, 3); - let len = unsafe { oakengine_node_input_id(value, 0, buf.as_mut_ptr(), 512) }; - assert_eq!(len, "enabled_in".len() as c_int); - assert_eq!(unsafe { read_buf(&mut buf) }, "enabled_in"); - let len = unsafe { oakengine_node_input_id(value, 1, buf.as_mut_ptr(), 512) }; - assert_eq!(len, "type_in".len() as c_int); - assert_eq!(unsafe { read_buf(&mut buf) }, "type_in"); - let len = unsafe { oakengine_node_input_id(value, 2, buf.as_mut_ptr(), 512) }; - assert_eq!(len, "value_in".len() as c_int); - assert_eq!(unsafe { read_buf(&mut buf) }, "value_in"); - assert_eq!( - unsafe { oakengine_node_input_id(value, 999, buf.as_mut_ptr(), 512) }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_node_input_id(value, -1, buf.as_mut_ptr(), 512) }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_node_input_get_type(value, c"value_in".as_ptr()) }, - vt::FLOAT - ); - // Texture inputs have no POD type code; the module reports NONE. - assert_eq!( - unsafe { oakengine_node_input_get_type(transform, c"tex_in".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_node_input_is_connectable(transform, c"tex_in".as_ptr()) }, - 1 - ); - assert_eq!( - unsafe { oakengine_node_input_is_connectable(value, c"value_in".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_node_input_is_keyframable(value, c"value_in".as_ptr()) }, - 1 - ); - assert_eq!( - unsafe { oakengine_node_input_is_connected(transform, c"tex_in".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_node_input_is_connected(value, c"tex_in".as_ptr()) }, - NODE_E_NOT_FOUND, - "unknown input on a valid node" - ); - // Input-name lookup (the value node's localized names). - let len = unsafe { - oakengine_node_get_input_name(value, c"value_in".as_ptr(), buf.as_mut_ptr(), 512) - }; - assert_eq!(len, "Value".len() as c_int); - assert_eq!(unsafe { read_buf(&mut buf) }, "Value"); - let len = unsafe { - oakengine_node_get_input_name(value, c"type_in".as_ptr(), buf.as_mut_ptr(), 512) - }; - assert_eq!(len, "Type".len() as c_int); - assert_eq!( - unsafe { - oakengine_node_get_input_name(value, c"nope_in".as_ptr(), buf.as_mut_ptr(), 512) - }, - NODE_E_NOT_FOUND - ); - // Stub input flags are stable. - assert_eq!( - unsafe { oakengine_node_input_is_array(value, c"value_in".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_node_input_array_size(value, c"value_in".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_node_input_get_flags(value, c"value_in".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_node_input_get_data_type(value, c"value_in".as_ptr()) }, - -1 - ); - assert_eq!( - unsafe { oakengine_node_input_is_hidden(value, c"value_in".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_node_input_is_keyframed(value, c"value_in".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_node_input_is_keyframed_ex(value, c"value_in".as_ptr(), 0) }, - 0 - ); - - // ---- standard parameter access ---------------------------------- - assert_eq!( - unsafe { oakengine_node_set_input(value, c"value_in".as_ptr(), &float_value(3.5)) }, - 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, vt::FLOAT); - assert!((out.f[0] - 3.5).abs() < 1e-6); - // Type mismatch (INT kind into a float input) → module INVALID. - assert_eq!( - unsafe { oakengine_node_set_input(value, c"value_in".as_ptr(), &int_value(7)) }, - NODE_E_INVALID - ); - // Unknown input → module NOT_FOUND. - assert_eq!( - unsafe { oakengine_node_set_input(value, c"nope_in".as_ptr(), &float_value(1.0)) }, - NODE_E_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_node_get_input(value, c"nope_in".as_ptr(), &mut out) }, - NODE_E_NOT_FOUND - ); - - // set_standard_value_command → opaque command pointer (free). - let cmd = unsafe { - oakengine_node_set_standard_value_command( - value, - c"value_in".as_ptr(), - -1, - -1, - &float_value(4.0), - ) - }; - assert!(!cmd.is_null()); - unsafe { oakengine_undo_command_free(cmd) }; - assert!(unsafe { - oakengine_node_set_standard_value_command( - value, - c"value_in".as_ptr(), - -1, - -1, - std::ptr::null(), - ) - } - .is_null()); - - // set_input_video_params_command is a documented stub → NULL. - let params = unsafe { std::mem::zeroed::() }; - assert!(unsafe { - oakengine_node_set_input_video_params_command(value, c"value_in".as_ptr(), ¶ms) - } - .is_null()); - - // ---- string parameter access (text generator) ------------------- - let textgen = unsafe { oakengine_node_factory_create_from_id(TYPE_TEXT.as_ptr()) }; - assert!(!textgen.is_null()); - // project + orphan + textgen owned (the added-node views are - // borrowed; the facade releases the factory's owned handle). - let alive_now = alive(); - assert_eq!( - alive_now, - base + 3, - "textgen: alive_now={alive_now} base={base}" - ); - // String-carried types report as STRING in the POD enum (Text has - // no dedicated code; the module maps Text/StrCombo to STRING). - assert_eq!( - unsafe { oakengine_node_input_get_type(textgen, c"text_in".as_ptr()) }, - vt::STRING - ); - // The generator's default text is "Sample Text" (11 chars). - let len = unsafe { - oakengine_node_get_input_string(textgen, c"text_in".as_ptr(), buf.as_mut_ptr(), 512) - }; - assert_eq!(len, "Sample Text".len() as c_int); - assert_eq!(unsafe { read_buf(&mut buf) }, "Sample Text"); - assert_eq!( - unsafe { - oakengine_node_set_input_string( - textgen, - c"text_in".as_ptr(), - c"Hello Text".as_ptr(), - ) - }, - 0 - ); - let len2 = unsafe { - oakengine_node_get_input_string(textgen, c"text_in".as_ptr(), buf.as_mut_ptr(), 512) - }; - assert_eq!(len2, "Hello Text".len() as c_int); - assert_eq!(unsafe { read_buf(&mut buf) }, "Hello Text"); - // String write on a non-string input → module INVALID. - assert_eq!( - unsafe { oakengine_node_set_input_string(value, c"value_in".as_ptr(), c"x".as_ptr()) }, - NODE_E_INVALID - ); - // get_input_string on a non-string input → module INVALID. - assert_eq!( - unsafe { - oakengine_node_get_input_string(value, c"value_in".as_ptr(), buf.as_mut_ptr(), 512) - }, - NODE_E_INVALID - ); - // Unknown input → NOT_FOUND for the string getter. - assert_eq!( - unsafe { - oakengine_node_get_input_string(value, c"nope_in".as_ptr(), buf.as_mut_ptr(), 512) - }, - NODE_E_NOT_FOUND - ); - unsafe { oakengine_node_free(textgen) }; - // project + orphan owned. - assert_eq!(alive(), base + 2); - - // ---- at-time values --------------------------------------------- - assert_eq!( - unsafe { - oakengine_node_frame_time_base(value, std::ptr::null_mut(), std::ptr::null_mut()) - }, - 0 - ); - let mut tb_num: c_int = 0; - let mut tb_den: c_int = 0; - assert_eq!( - unsafe { oakengine_node_frame_time_base(value, &mut tb_num, &mut tb_den) }, - 0 - ); - assert_eq!( - (tb_num, tb_den), - (1001, 30000), - "engine default time base without a sequence" - ); - - assert_eq!( - unsafe { - oakengine_node_set_input_at_time( - value, - c"value_in".as_ptr(), - -1, - 0, - -1, - &float_value(0.5), - 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, vt::FLOAT); - assert!((at.f[0] - 0.5).abs() < 1e-6); - // At-time read on an unknown input → module NOT_FOUND. - assert_eq!( - unsafe { - oakengine_node_get_input_at_time(value, c"nope_in".as_ptr(), -1, -1, 0, 0, &mut at) - }, - NODE_E_NOT_FOUND - ); - // At-time write on an unknown input → module NOT_FOUND. - assert_eq!( - unsafe { - oakengine_node_set_input_at_time( - value, - c"nope_in".as_ptr(), - -1, - 0, - -1, - &float_value(1.0), - 0, - ) - }, - NODE_E_NOT_FOUND - ); - - // set_value_at_time_command → opaque command pointer (free). - let cmd = unsafe { - oakengine_node_set_value_at_time_command( - value as *mut c_void, - c"value_in".as_ptr(), - -1, - 0, - 1, - &float_value(0.75), - -1, - 0, - ) - }; - assert!(!cmd.is_null()); - unsafe { oakengine_undo_command_free(cmd) }; - // Zero denominator → NULL, never a crash. - assert!(unsafe { - oakengine_node_set_value_at_time_command( - value as *mut c_void, - c"value_in".as_ptr(), - -1, - 0, - 0, - &float_value(0.75), - -1, - 0, - ) - } - .is_null()); - - // String at-time path is a documented stub → Invalid. - assert_eq!( - unsafe { - oakengine_node_set_input_string_at_time( - value, - c"value_in".as_ptr(), - -1, - 0, - c"x".as_ptr(), - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_get_input_string_at_time( - value, - c"value_in".as_ptr(), - -1, - 0, - -1, - buf.as_mut_ptr(), - 512, - ) - }, - E_INVALID - ); - // Bezier/binary at-time reads are documented stubs → Invalid. - let mut six = [0.0f64; 6]; - assert_eq!( - unsafe { - oakengine_node_get_input_bezier_at_time( - value, - c"value_in".as_ptr(), - -1, - 0, - -1, - six.as_mut_ptr(), - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_get_input_binary_at_time( - value, - c"value_in".as_ptr(), - -1, - 0, - -1, - buf.as_mut_ptr(), - 512, - ) - }, - E_INVALID - ); - - // ---- input default value (stub) --------------------------------- - assert_eq!( - unsafe { - oakengine_node_input_get_default_value(value, c"value_in".as_ptr(), 0, &mut out) - }, - E_NOT_FOUND - ); - - // ---- input properties (stubs) ----------------------------------- - assert_eq!( - unsafe { - oakengine_node_input_has_property(value, c"value_in".as_ptr(), c"key".as_ptr()) - }, - 0 - ); - assert_eq!( - unsafe { - oakengine_node_set_input_property_string( - value, - c"value_in".as_ptr(), - c"key".as_ptr(), - c"v".as_ptr(), - 1, - ) - }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { - oakengine_node_input_get_property_string( - value, - c"value_in".as_ptr(), - c"key".as_ptr(), - buf.as_mut_ptr(), - 512, - ) - }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { - oakengine_node_input_get_property_number( - value, - c"value_in".as_ptr(), - c"key".as_ptr(), - 0, - std::ptr::null_mut(), - ) - }, - E_INVALID - ); - let mut dbl: f64 = 0.0; - assert_eq!( - unsafe { - oakengine_node_input_get_property_number( - value, - c"value_in".as_ptr(), - c"key".as_ptr(), - 0, - &mut dbl, - ) - }, - E_NOT_FOUND - ); - let mut i64out: i64 = 0; - assert_eq!( - unsafe { - oakengine_node_input_get_property_int( - value, - c"value_in".as_ptr(), - c"key".as_ptr(), - &mut i64out, - ) - }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { - oakengine_node_input_get_property_rational( - value, - c"value_in".as_ptr(), - c"key".as_ptr(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - E_NOT_FOUND, - "stub NotFound with a valid node" - ); - assert_eq!( - unsafe { - oakengine_node_input_get_property_track_number( - value, - c"value_in".as_ptr(), - c"key".as_ptr(), - 0, - &mut dbl, - ) - }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_node_input_get_property_count(value, c"value_in".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { - oakengine_node_input_get_property_key( - value, - c"value_in".as_ptr(), - 0, - buf.as_mut_ptr(), - 512, - ) - }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { - oakengine_node_input_get_property_string_list_count( - value, - c"value_in".as_ptr(), - c"key".as_ptr(), - ) - }, - 0 - ); - assert_eq!( - unsafe { - oakengine_node_input_get_property_string_list( - value, - c"value_in".as_ptr(), - c"key".as_ptr(), - 0, - buf.as_mut_ptr(), - 512, - ) - }, - E_NOT_FOUND - ); - - // ---- copy inputs ------------------------------------------------- - assert_eq!(unsafe { oakengine_node_copy_inputs(value2, value) }, 0); - assert_eq!( - unsafe { oakengine_node_copy_inputs(std::ptr::null_mut(), value) }, - E_INVALID - ); - - // ---- value hint ------------------------------------------------- - assert_eq!( - unsafe { - oakengine_node_set_value_hint(value, c"value_in".as_ptr(), 0, 0, 0, c"".as_ptr()) - }, - 0 - ); - assert_eq!( - unsafe { - oakengine_node_set_value_hint(value, c"nope_in".as_ptr(), 0, 0, 0, c"".as_ptr()) - }, - NODE_E_NOT_FOUND - ); - - // ---- context positions ------------------------------------------- - // The facade establishes the first context_positions entry with the - // module's live setter before pushing the undoable command, so - // positions work on fresh nodes. - let root = unsafe { oakengine_project_root(project) }; - let mut x: f64 = 0.0; - let mut y: f64 = 0.0; - let mut expanded: c_int = 0; - assert_eq!(unsafe { oakengine_node_context_node_count(root) }, 0); - assert_eq!( - unsafe { oakengine_node_context_contains_node(root, value) }, - 0 - ); - assert!( - unsafe { oakengine_node_context_node_at(root, 0, &mut x, &mut y, &mut expanded) } - .is_null() - ); - // A fresh node gains its first context entry through the facade. - assert_eq!( - unsafe { oakengine_node_set_context_position(root, value, 10.0, 20.0) }, - 0 - ); - assert_eq!( - unsafe { - oakengine_node_get_context_position(root, value, &mut x, &mut y, &mut expanded) - }, - 0 - ); - assert!((x - 10.0).abs() < 1e-9, "x={x}"); - assert!((y - 20.0).abs() < 1e-9, "y={y}"); - assert_eq!(expanded, 0); - // Expanded flag flips through the same path. - assert_eq!( - unsafe { oakengine_node_set_context_expanded(root, value, 1) }, - 0 - ); - assert_eq!( - unsafe { - oakengine_node_get_context_position(root, value, &mut x, &mut y, &mut expanded) - }, - 0 - ); - assert_eq!(expanded, 1); - unsafe { oakengine_node_free(root) }; - - // ---- array inputs (multicam sources_in is an array) -------------- - assert_eq!( - unsafe { oakengine_node_array_insert_at(multicam, c"sources_in".as_ptr(), 0) }, - 0 - ); - assert_eq!( - unsafe { oakengine_node_array_insert_at(multicam, c"sources_in".as_ptr(), 1) }, - 0 - ); - assert_eq!(unsafe { oakengine_multicam_get_source_count(multicam) }, 2); - assert_eq!( - unsafe { oakengine_node_array_remove_at(multicam, c"sources_in".as_ptr(), 1) }, - 0 - ); - assert_eq!(unsafe { oakengine_multicam_get_source_count(multicam) }, 1); - assert_eq!( - unsafe { oakengine_node_array_remove_at(multicam, c"sources_in".as_ptr(), 0) }, - 0 - ); - assert_eq!(unsafe { oakengine_multicam_get_source_count(multicam) }, 0); - // Non-array input → module INVALID; negative index → facade INVALID. - assert_eq!( - unsafe { oakengine_node_array_insert_at(value, c"value_in".as_ptr(), 0) }, - NODE_E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_array_insert_at(value, c"value_in".as_ptr(), -1) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_array_remove_at(value, c"value_in".as_ptr(), 0) }, - NODE_E_INVALID - ); - assert_eq!( - unsafe { oakengine_multicam_get_source_count(value) }, - E_INVALID, - "non-multicam node" - ); - - // ---- graph editing: 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 - ); - // A second connect on an already-connected input is rejected with the - // module STATE error, mirroring the live `oaknode_node_connect`. - assert_eq!( - unsafe { oakengine_node_connect(solid, transform, c"tex_in".as_ptr()) }, - NODE_E_STATE - ); - // Connecting to a non-connectable input → module INVALID. - assert_eq!( - unsafe { oakengine_node_connect(solid, value, c"value_in".as_ptr()) }, - NODE_E_INVALID - ); - // Connecting to an unknown input → module NOT_FOUND. - assert_eq!( - unsafe { oakengine_node_connect(solid, value, c"nope_in".as_ptr()) }, - NODE_E_NOT_FOUND - ); - - // Connected-node + output-connection introspection. - let conn = - unsafe { oakengine_node_input_get_connected_node(transform, c"tex_in".as_ptr(), -1) }; - assert!(!conn.is_null()); - let len = unsafe { oakengine_node_get_type_id(conn, buf.as_mut_ptr(), 512) }; - assert_eq!(len, TYPE_SOLID.to_bytes().len() as c_int); - unsafe { oakengine_node_free(conn) }; - assert!(unsafe { - oakengine_node_input_get_connected_node(transform, c"nope_in".as_ptr(), -1) - } - .is_null()); - assert_eq!(unsafe { oakengine_node_output_connection_count(solid) }, 1); - let mut conn_node: *mut OakEngineNode = std::ptr::null_mut(); - let mut elem: c_int = -1; - assert_eq!( - unsafe { - oakengine_node_output_connection_at( - solid, - 0, - &mut conn_node, - buf.as_mut_ptr(), - 512, - &mut elem, - ) - }, - 0 - ); - assert!(!conn_node.is_null()); - assert_eq!(unsafe { read_buf(&mut buf) }, "tex_in"); - assert_eq!(elem, -1); - unsafe { oakengine_node_free(conn_node) }; - // _ex variant reports the module's hidden flag (always 0). - let mut hidden: c_int = 1; - assert_eq!( - unsafe { - oakengine_node_output_connection_at_ex( - solid, - 0, - &mut conn_node, - buf.as_mut_ptr(), - 512, - &mut elem, - &mut hidden, - ) - }, - 0 - ); - assert_eq!(hidden, 0); - unsafe { oakengine_node_free(conn_node) }; - // Out-of-range output index. - assert_eq!( - unsafe { - oakengine_node_output_connection_at( - solid, - 1, - &mut conn_node, - buf.as_mut_ptr(), - 512, - &mut elem, - ) - }, - E_NOT_FOUND - ); - - // inputs_from: recursive and non-recursive both reach a direct - // feeder (the BFS checks neighbors on the depth-0 expansion). - assert_eq!( - unsafe { oakengine_node_inputs_from(transform, solid, 1) }, - 1 - ); - assert_eq!( - unsafe { oakengine_node_inputs_from(transform, solid, 0) }, - 1, - "non-recursive still finds a direct feeder" - ); - assert_eq!(unsafe { oakengine_node_inputs_from(value, solid, 1) }, 0); - assert_eq!( - unsafe { oakengine_node_inputs_from(std::ptr::null(), solid, 1) }, - 0 - ); - assert_eq!( - unsafe { oakengine_node_inputs_from(transform, std::ptr::null(), 1) }, - 0 - ); - - // Input-connection surfaces are stubs (outputs only). - assert_eq!( - unsafe { oakengine_node_input_connection_count_all(transform) }, - 0 - ); - assert_eq!( - unsafe { oakengine_node_input_connection_count(transform, c"tex_in".as_ptr(), -1) }, - 0 - ); - assert_eq!( - unsafe { - oakengine_node_input_connection_at_all( - transform, - 0, - &mut conn_node, - buf.as_mut_ptr(), - 512, - &mut elem, - &mut conn_node, - &mut hidden, - ) - }, - E_NOT_FOUND - ); - assert!(unsafe { - oakengine_node_input_connection_at(transform, c"tex_in".as_ptr(), -1, 0) - } - .is_null()); - - // Disconnect + command variants. - 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 - ); - // Disconnecting a disconnected input → module NOT_FOUND. - assert_eq!( - unsafe { oakengine_node_disconnect(transform, c"tex_in".as_ptr()) }, - NODE_E_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_node_disconnect_ex(transform, c"tex_in".as_ptr(), -1) }, - NODE_E_NOT_FOUND - ); - let cmd = - unsafe { oakengine_node_connect_command(solid, transform, c"tex_in".as_ptr(), -1) }; - assert!(!cmd.is_null()); - unsafe { oakengine_undo_command_free(cmd) }; - assert_eq!( - unsafe { oakengine_node_connect(solid, transform, c"tex_in".as_ptr()) }, - 0 - ); - let cmd = unsafe { oakengine_node_disconnect_command(transform, c"tex_in".as_ptr(), -1) }; - assert!(!cmd.is_null()); - unsafe { oakengine_undo_command_free(cmd) }; - - // block_link: link/unlink round trip (1 = changed, 0 = no-op). - assert_eq!( - unsafe { oakengine_block_link(solid as *mut c_void, value as *mut c_void, 1) }, - 1 - ); - assert_eq!( - unsafe { oakengine_block_link(solid as *mut c_void, value as *mut c_void, 1) }, - 0, - "already linked" - ); - assert_eq!( - unsafe { oakengine_block_link(solid as *mut c_void, value as *mut c_void, 0) }, - 1 - ); - assert_eq!( - unsafe { oakengine_block_link(solid as *mut c_void, value as *mut c_void, 0) }, - 0, - "already unlinked" - ); - assert_eq!( - unsafe { oakengine_block_link(std::ptr::null_mut(), value as *mut c_void, 1) }, - E_INVALID - ); - // link_command → opaque command pointer (free). - let cmd = unsafe { oakengine_node_link_command(solid, value, 1) }; - assert!(!cmd.is_null()); - unsafe { oakengine_undo_command_free(cmd) }; - assert!(unsafe { oakengine_node_link_command(std::ptr::null_mut(), value, 1) }.is_null()); - - // ---- copy in graph ---------------------------------------------- - // copy_in_graph pushes a "Copy Node" command and returns an owned - // copy in a scratch project (free to return the alive counter). - let count_before = unsafe { oakengine_project_node_count(project) }; - let copied = unsafe { oakengine_node_copy_in_graph(value, std::ptr::null_mut()) }; - assert!(!copied.is_null()); - assert_eq!( - alive(), - base + 3, - "copy-in-graph: owned copy + project + orphan" - ); - assert_eq!( - unsafe { oakengine_project_node_count(project) }, - count_before + 1, - "the redo inserts a copy into the graph" - ); - unsafe { oakengine_node_free(copied) }; - assert_eq!(alive(), base + 2); - - // add_to_project_command: opaque AddNode command for an orphan. - let orphan2 = unsafe { oakengine_node_factory_create_from_id(TYPE_VALUE.as_ptr()) }; - assert!(!orphan2.is_null()); - let cmd = unsafe { oakengine_node_add_to_project_command(project, orphan2) }; - assert!(!cmd.is_null()); - unsafe { oakengine_undo_command_free(cmd) }; - unsafe { oakengine_node_free(orphan2) }; - // Adding a node already in the project → NULL command. - assert!(unsafe { oakengine_node_add_to_project_command(project, value) }.is_null()); - - // copy_dependency_graph is a documented stub → Invalid. - let mut copies: *mut OakEngineNode = std::ptr::null_mut(); - let mut srcs = [value]; - assert_eq!( - unsafe { - oakengine_node_copy_dependency_graph( - srcs.as_mut_ptr(), - &mut copies, - 1, - std::ptr::null_mut(), - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_copy_dependency_graph( - std::ptr::null_mut(), - &mut copies, - 1, - std::ptr::null_mut(), - ) - }, - E_INVALID - ); - - // connect_command_string is a documented stub → empty description. - assert_eq!( - unsafe { - oakengine_node_connect_command_string( - solid, - value, - c"value_in".as_ptr(), - -1, - buf.as_mut_ptr(), - 512, - ) - }, - 0 - ); - - // transform_time_to is a documented identity stub. - let mut rin: i64 = 0; - let mut rind: i64 = 0; - let mut rout: i64 = 0; - let mut routd: i64 = 0; - assert_eq!( - unsafe { - oakengine_node_transform_time_to( - solid, value, 0, 0, 5, 1, 7, 2, &mut rin, &mut rind, &mut rout, &mut routd, - ) - }, - 0 - ); - assert_eq!((rin, rind, rout, routd), (5, 1, 7, 2)); - assert_eq!( - unsafe { - oakengine_node_transform_time_to( - std::ptr::null_mut(), - value, - 0, - 0, - 1, - 1, - 1, - 1, - &mut rin, - &mut rind, - &mut rout, - &mut routd, - ) - }, - E_INVALID - ); - - // ---- keyframes (facade stubs + the real handle API) ------------- - assert_eq!( - unsafe { oakengine_node_keyframe_count(value, c"value_in".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_node_keyframe_count_on_track(value, c"value_in".as_ptr(), 0, 0) }, - 0 - ); - assert_eq!( - unsafe { - oakengine_node_keyframe_at( - value, - c"value_in".as_ptr(), - 0, - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - E_NOT_FOUND, - "stub NotFound with a valid node" - ); - let mut kf_time: i64 = 0; - assert_eq!( - unsafe { - oakengine_node_keyframe_at(value, c"value_in".as_ptr(), 0, &mut kf_time, &mut out) - }, - E_NOT_FOUND - ); - let mut f1: f32 = 0.0; - let mut f2: f32 = 0.0; - let mut f3: f32 = 0.0; - let mut f4: f32 = 0.0; - let mut kty: c_int = 0; - assert_eq!( - unsafe { - oakengine_node_keyframe_get_easing( - value, - c"value_in".as_ptr(), - 0, - &mut f1, - &mut f2, - &mut f3, - &mut f4, - &mut kty, - ) - }, - E_NOT_FOUND - ); - // keyframe_add pushes the value-at-time path (undoable). - assert_eq!( - unsafe { - oakengine_node_keyframe_add( - value, - c"value_in".as_ptr(), - 0, - &float_value(1.5), - 0, - 0.0, - 0.0, - 0.0, - 0.0, - ) - }, - 0 - ); - // Invalid easing type ordinal → Invalid. - assert_eq!( - unsafe { - oakengine_node_keyframe_add( - value, - c"value_in".as_ptr(), - 0, - &float_value(1.5), - 3, - 0.0, - 0.0, - 0.0, - 0.0, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_keyframe_add( - value, - c"value_in".as_ptr(), - 0, - &float_value(1.5), - -1, - 0.0, - 0.0, - 0.0, - 0.0, - ) - }, - E_INVALID - ); - // keyframe_remove is a documented stub → NOT_FOUND. - assert_eq!( - unsafe { oakengine_node_keyframe_remove(value, c"value_in".as_ptr(), 0) }, - E_NOT_FOUND - ); - // Keyframe stub enumerators. - assert_eq!( - unsafe { oakengine_node_has_keyframe_at_time(value, c"value_in".as_ptr(), -1, 0, 0) }, - 0 - ); - assert_eq!( - unsafe { - oakengine_node_keyframe_earliest_time( - value, - c"value_in".as_ptr(), - -1, - &mut i64out, - std::ptr::null_mut(), - ) - }, - 0 - ); - assert_eq!( - unsafe { - oakengine_node_keyframe_latest_time( - value, - c"value_in".as_ptr(), - -1, - std::ptr::null_mut(), - &mut i64out, - ) - }, - 0 - ); - assert_eq!( - unsafe { - oakengine_node_keyframe_closest_time_before( - value, - c"value_in".as_ptr(), - -1, - 0, - 0, - &mut i64out, - &mut i64out, - ) - }, - 0 - ); - assert_eq!( - unsafe { - oakengine_node_keyframe_closest_time_after( - value, - c"value_in".as_ptr(), - -1, - 0, - 0, - &mut i64out, - &mut i64out, - ) - }, - 0 - ); - assert!(unsafe { - oakengine_node_keyframe_handle_on_track(value, c"value_in".as_ptr(), -1, 0, 0) - } - .is_null()); - assert!(unsafe { - oakengine_node_keyframe_handle_at_time(value, c"value_in".as_ptr(), -1, 0, 0, 1) - } - .is_null()); - assert_eq!( - unsafe { - oakengine_node_keyframes_at_time( - value, - c"value_in".as_ptr(), - -1, - 0, - 1, - std::ptr::null_mut(), - 0, - ) - }, - 0 - ); - assert_eq!( - unsafe { - oakengine_node_keyframes_toggle_at_time( - value, - c"value_in".as_ptr(), - -1, - 0, - 0, - 1, - c"t".as_ptr(), - ) - }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { - oakengine_node_set_input_keyframing( - value, - c"value_in".as_ptr(), - -1, - 1, - 0, - 0, - c"k".as_ptr(), - ) - }, - E_INVALID - ); - assert!(unsafe { - oakengine_node_set_input_keyframing_command(value, c"value_in".as_ptr(), -1, 1) - } - .is_null()); - assert_eq!( - unsafe { - oakengine_node_keyframes_paste(value, std::ptr::null_mut(), 1, c"p".as_ptr()) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_keyframe_best_type_at_time(value, c"value_in".as_ptr(), -1, 0, 0, 2) - }, - 2, - "caller default passes through" - ); - assert_eq!( - unsafe { oakengine_node_keyframe_track_count(value, c"value_in".as_ptr(), -1) }, - 1 - ); - assert_eq!( - unsafe { - oakengine_node_keyframe_set_easing( - value, - c"value_in".as_ptr(), - 0, - 0, - 0.0, - 0.0, - 0.0, - 0.0, - ) - }, - E_NOT_FOUND - ); - let ts: [i64; 1] = [0]; - let trk: [c_int; 1] = [0]; - assert_eq!( - unsafe { - oakengine_node_keyframes_set_type_many( - value, - c"value_in".as_ptr(), - -1, - ts.as_ptr(), - trk.as_ptr(), - 1, - 0, - ) - }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { - oakengine_node_keyframes_set_time_many( - value, - c"value_in".as_ptr(), - -1, - ts.as_ptr(), - trk.as_ptr(), - 1, - 1, - ) - }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { - oakengine_node_keyframes_set_value_many( - value, - c"value_in".as_ptr(), - -1, - ts.as_ptr(), - trk.as_ptr(), - 1, - &float_value(1.0), - &float_value(0.0), - ) - }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { - oakengine_node_keyframes_set_bezier_many( - value, - c"value_in".as_ptr(), - -1, - ts.as_ptr(), - trk.as_ptr(), - 1, - 0.0, - 0.0, - 1.0, - 1.0, - ) - }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { - oakengine_node_keyframe_set_bezier_point( - value, - c"value_in".as_ptr(), - -1, - 0, - 0, - 0, - 0.0, - 0.0, - 0.0, - 0.0, - ) - }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_node_keyframes_clear(value, c"value_in".as_ptr()) }, - 0, - "documented no-op" - ); - // insert_keyframe_command → opaque command pointer (free). - let cmd = unsafe { - oakengine_node_insert_keyframe_command( - value, - c"value_in".as_ptr(), - -1, - 0, - 0, - &float_value(2.0), - 0, - 0.0, - 0.0, - 0.0, - 0.0, - ) - }; - assert!(!cmd.is_null()); - unsafe { oakengine_undo_command_free(cmd) }; - assert!( - unsafe { - oakengine_node_insert_keyframe_command( - value, - c"value_in".as_ptr(), - -1, - 0, - 0, - &float_value(2.0), - 3, - 0.0, - 0.0, - 0.0, - 0.0, - ) - } - .is_null(), - "garbage easing type" - ); - // remove_keyframe_command is a documented stub → NULL. - let dummy_kf = Box::into_raw(Box::new(OakEngineKeyframe { - handle: CHandle::null(), - })); - assert!(unsafe { oakengine_node_remove_keyframe_command(dummy_kf) }.is_null()); - unsafe { oakengine_keyframe_dispose(dummy_kf) }; - - // ---- OakEngineKeyframe handle API -------------------------------- - let kf = unsafe { - oakengine_keyframe_create( - value, - c"value_in".as_ptr(), - -1, - 0, - 0, - 0, - &float_value(1.0), - 0, - ) - }; - assert!(!kf.is_null()); - let mut num: i64 = 0; - let mut den: i64 = 0; - assert_eq!( - unsafe { oakengine_keyframe_get_time(kf, &mut num, &mut den) }, - 0 - ); - assert_eq!(num, 0); - assert_eq!(den, 1); - let len = unsafe { oakengine_keyframe_get_input_id(kf, buf.as_mut_ptr(), 512) }; - assert_eq!(len, "value_in".len() as c_int); - assert_eq!(unsafe { read_buf(&mut buf) }, "value_in"); - assert_eq!(unsafe { oakengine_keyframe_get_track(kf) }, 0); - assert_eq!(unsafe { oakengine_keyframe_get_element(kf) }, -1); - let parent = unsafe { oakengine_keyframe_get_node(kf) }; - assert!(!parent.is_null()); - let len = unsafe { oakengine_node_get_type_id(parent, buf.as_mut_ptr(), 512) }; - assert_eq!(len, TYPE_VALUE.to_bytes().len() as c_int); - unsafe { oakengine_node_free(parent) }; - assert_eq!(unsafe { oakengine_keyframe_get_type(kf) }, 0); - let mut kout: OakNodeValue = unsafe { std::mem::zeroed() }; - assert_eq!(unsafe { oakengine_keyframe_get_value(kf, &mut kout) }, 0); - assert_eq!(kout.kind, vt::FLOAT); - assert!((kout.f[0] - 1.0).abs() < 1e-9); - - // Live setters + readback. - assert_eq!( - unsafe { oakengine_keyframe_set_value_live(kf, &float_value(2.0)) }, - 0 - ); - assert_eq!(unsafe { oakengine_keyframe_set_time_live(kf, 1, 1) }, 0); - assert_eq!( - unsafe { oakengine_keyframe_get_time(kf, &mut num, &mut den) }, - 0 - ); - assert_eq!((num, den), (1, 1)); - assert_eq!( - unsafe { oakengine_keyframe_set_bezier_point_live(kf, 0, 0.5, 0.25) }, - 0 - ); - let mut bx: f64 = 0.0; - let mut by: f64 = 0.0; - assert_eq!( - unsafe { oakengine_keyframe_get_bezier_point(kf, 0, &mut bx, &mut by) }, - 0 - ); - assert!((bx - 0.5).abs() < 1e-9); - assert!((by - 0.25).abs() < 1e-9); - assert_eq!( - unsafe { oakengine_keyframe_get_valid_bezier_point(kf, 1, &mut bx, &mut by) }, - 0 - ); - // Bezier point index bounds. - assert_eq!( - unsafe { oakengine_keyframe_set_bezier_point_live(kf, 2, 0.0, 0.0) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_keyframe_get_bezier_point(kf, -1, &mut bx, &mut by) }, - E_INVALID - ); - // compute_paste_value against the target node. - let mut pv: OakNodeValue = unsafe { std::mem::zeroed() }; - assert_eq!( - unsafe { oakengine_keyframe_compute_paste_value(value, kf, &mut pv) }, - 0 - ); - // Orphaned keyframe has no sibling. - assert_eq!( - unsafe { oakengine_keyframe_has_sibling_at_time(kf, 1, 1) }, - 0 - ); - assert_eq!( - unsafe { oakengine_keyframe_has_sibling_at_time(kf, 1, -1) }, - 0, - "negative track ignored" - ); - // Undoable command creators over a keyframe handle. - let cmd = unsafe { oakengine_keyframe_set_time_command(kf, 3) }; - assert!(!cmd.is_null()); - unsafe { oakengine_undo_command_free(cmd) }; - let cmd = unsafe { oakengine_keyframe_set_value_command(kf, &float_value(3.0)) }; - assert!(!cmd.is_null()); - unsafe { oakengine_undo_command_free(cmd) }; - assert!(unsafe { oakengine_keyframe_set_value_command(kf, std::ptr::null()) }.is_null()); - // keyframes_remove_many is a documented stub → Invalid. - let mut kf_ptr = kf; - assert_eq!( - unsafe { oakengine_keyframes_remove_many(&mut kf_ptr, 1, c"r".as_ptr()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_keyframes_remove_many(std::ptr::null_mut(), 1, c"r".as_ptr()) }, - E_INVALID - ); - // Dispose + NULL/empty dispose. - unsafe { oakengine_keyframe_dispose(kf) }; - unsafe { oakengine_keyframe_dispose(std::ptr::null_mut()) }; - let empty_kf = Box::into_raw(Box::new(OakEngineKeyframe { - handle: CHandle::null(), - })); - unsafe { oakengine_keyframe_dispose(empty_kf) }; - - // ---- dragger ----------------------------------------------------- - let drag = unsafe { oakengine_dragger_create(value, c"value_in".as_ptr(), -1, 1) }; - assert!(!drag.is_null()); - assert_eq!(unsafe { oakengine_dragger_is_started(drag) }, 0); - assert_eq!(unsafe { oakengine_dragger_start(drag, 0, 1, 0) }, 0); - assert_eq!(unsafe { oakengine_dragger_is_started(drag) }, 1); - assert_eq!( - unsafe { oakengine_dragger_drag(drag, &float_value(2.5)) }, - 0 - ); - assert_eq!( - unsafe { oakengine_dragger_end(drag, c"Drag Value".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_dragger_is_started(drag) }, - 0, - "end resets the dragger" - ); - // The dragged value landed in the standard value. - let mut dv: OakNodeValue = unsafe { std::mem::zeroed() }; - assert_eq!( - unsafe { oakengine_node_get_input(value, c"value_in".as_ptr(), &mut dv) }, - 0 - ); - assert!((dv.f[0] - 2.5).abs() < 1e-6); - // drag before start → module STATE; start twice → module STATE. - assert_eq!( - unsafe { oakengine_dragger_drag(drag, &float_value(1.0)) }, - NODE_E_STATE, - "drag without start" - ); - assert_eq!(unsafe { oakengine_dragger_start(drag, 1, 1, 0) }, 0); - assert_eq!( - unsafe { oakengine_dragger_start(drag, 2, 1, 0) }, - NODE_E_STATE - ); - assert_eq!(unsafe { oakengine_dragger_end(drag, c"x".as_ptr()) }, 0); - assert_eq!( - unsafe { oakengine_dragger_end(drag, c"x".as_ptr()) }, - NODE_E_STATE, - "end without start" - ); - unsafe { oakengine_dragger_free(drag) }; - unsafe { oakengine_dragger_free(std::ptr::null_mut()) }; - let empty_drag = Box::into_raw(Box::new(OakEngineNodeDragger { - handle: CHandle::null(), - })); - unsafe { oakengine_dragger_free(empty_drag) }; - assert!(unsafe { - oakengine_dragger_create(std::ptr::null_mut(), c"value_in".as_ptr(), -1, 1) - } - .is_null()); - - // ---- group passthrough ------------------------------------------ - assert_eq!(unsafe { oakengine_group_input_passthrough_count(group) }, 0); - let id_len = unsafe { - oakengine_group_add_input_passthrough( - group, - value, - c"value_in".as_ptr(), - -1, - c"".as_ptr(), - buf.as_mut_ptr(), - 512, - ) - }; - assert!(id_len > 0, "a passthrough id must be generated"); - let passthrough_id = unsafe { read_buf(&mut buf) }; - assert_eq!(unsafe { oakengine_group_input_passthrough_count(group) }, 1); - // passthrough_at returns the id, the inner node, input and element. - let mut pt_node: *mut OakEngineNode = std::ptr::null_mut(); - let mut pt_elem: c_int = 0; - let len = unsafe { - oakengine_group_input_passthrough_at( - group, - 0, - buf.as_mut_ptr(), - 512, - &mut pt_node, - buf.as_mut_ptr(), - 512, - &mut pt_elem, - ) - }; - assert!(len > 0); - assert!(!pt_node.is_null()); - assert_eq!(unsafe { read_buf(&mut buf) }, "value_in"); - assert_eq!(pt_elem, -1); - unsafe { oakengine_node_free(pt_node) }; - // Out-of-range passthrough index → module NOT_FOUND. - assert_eq!( - unsafe { - oakengine_group_input_passthrough_at( - group, - 5, - buf.as_mut_ptr(), - 512, - &mut pt_node, - buf.as_mut_ptr(), - 512, - &mut pt_elem, - ) - }, - NODE_E_NOT_FOUND - ); - // id_of_passthrough round trip: the module getters return the copied - // string length (>= 0) on success, and the facade treats only - // negative codes as failures. - let len = unsafe { - oakengine_group_get_id_of_passthrough( - group, - value, - c"value_in".as_ptr(), - -1, - buf.as_mut_ptr(), - 512, - ) - }; - assert!(len > 0, "the passthrough id must be returned"); - assert_eq!( - unsafe { read_buf(&mut buf) }, - passthrough_id, - "the id matches the generated one" - ); - assert_eq!( - unsafe { - oakengine_group_get_id_of_passthrough( - group, - value, - c"nope_in".as_ptr(), - -1, - buf.as_mut_ptr(), - 512, - ) - }, - E_NOT_FOUND - ); - // get_passthrough_from_id writes the inner node, input and element. - let passthrough_id_c = CString::new(passthrough_id.as_str()).unwrap(); - let mut back_node: *mut OakEngineNode = std::ptr::null_mut(); - let rc = unsafe { - oakengine_group_get_passthrough_from_id( - group, - passthrough_id_c.as_ptr(), - &mut back_node, - buf.as_mut_ptr(), - 512, - &mut pt_elem, - ) - }; - assert_eq!(rc, 0); - assert!(!back_node.is_null(), "out_node must be written"); - assert_eq!(unsafe { read_buf(&mut buf) }, "value_in"); - assert_eq!(pt_elem, -1); - unsafe { oakengine_node_free(back_node) }; - assert_eq!( - unsafe { - oakengine_group_get_passthrough_from_id( - group, - c"no-such-id".as_ptr(), - &mut back_node, - buf.as_mut_ptr(), - 512, - &mut pt_elem, - ) - }, - E_NOT_FOUND - ); - // Output passthrough set/get round trip. - assert!(unsafe { oakengine_group_get_output_passthrough(group) }.is_null()); - assert_eq!( - unsafe { oakengine_group_set_output_passthrough(group, value) }, - 0 - ); - let op = unsafe { oakengine_group_get_output_passthrough(group) }; - assert!(!op.is_null()); - unsafe { oakengine_node_free(op) }; - // resolve_input resolves the passthrough to its inner node/input. - let mut rn: *mut OakEngineNode = std::ptr::null_mut(); - let rc = unsafe { - oakengine_group_resolve_input( - group, - c"value_in".as_ptr(), - -1, - &mut rn, - buf.as_mut_ptr(), - 512, - &mut pt_elem, - ) - }; - assert_eq!(rc, 0); - assert!(!rn.is_null(), "the resolved node must be written"); - assert_eq!(unsafe { read_buf(&mut buf) }, "value_in"); - assert_eq!(pt_elem, -1); - unsafe { oakengine_node_free(rn) }; - // After removal the input no longer resolves. - assert_eq!( - unsafe { - oakengine_group_remove_input_passthrough(group, value, c"value_in".as_ptr(), -1) - }, - 0 - ); - assert_eq!(unsafe { oakengine_group_input_passthrough_count(group) }, 0); - assert_eq!( - unsafe { - oakengine_group_remove_input_passthrough(group, value, c"value_in".as_ptr(), -1) - }, - NODE_E_NOT_FOUND, - "already removed" - ); - // Undoable add + set output. - assert_eq!( - unsafe { - oakengine_group_add_input_passthrough_undoable( - group, - value, - c"value_in".as_ptr(), - -1, - c"".as_ptr(), - ) - }, - 0 - ); - assert_eq!(unsafe { oakengine_group_input_passthrough_count(group) }, 1); - assert_eq!( - unsafe { oakengine_group_set_output_passthrough_undoable(group, value) }, - 0 - ); - // Opaque group command creators (free). - let cmd = unsafe { - oakengine_group_add_input_passthrough_command( - group, - value, - c"value_in".as_ptr(), - -1, - c"".as_ptr(), - ) - }; - assert!(!cmd.is_null()); - unsafe { oakengine_undo_command_free(cmd) }; - let cmd = unsafe { oakengine_group_set_output_passthrough_command(group, value) }; - assert!(!cmd.is_null()); - unsafe { oakengine_undo_command_free(cmd) }; - // group_get_inner walks one passthrough level (the input id is - // pre-filled in the inout buffer). - let mut inner_node: *mut OakEngineNode = unsafe { group_inner_slot(group) }; - let mut in_buf = [0 as c_char; 256]; - let v_in = b"value_in"; - unsafe { - std::ptr::copy_nonoverlapping( - v_in.as_ptr() as *const c_char, - in_buf.as_mut_ptr(), - v_in.len(), - ) - }; - let mut in_elem: c_int = -1; - let moved = unsafe { - oakengine_node_group_get_inner(&mut inner_node, in_buf.as_mut_ptr(), 256, &mut in_elem) - }; - assert_eq!(moved, 1, "one passthrough level is walked"); - assert_eq!(unsafe { read_buf(&mut in_buf) }, "value_in"); - assert_eq!(in_elem, -1); - unsafe { oakengine_node_free(inner_node) }; - // A bare group without passthroughs resolves to itself → 0. - let bare = unsafe { oakengine_project_add_node(project, TYPE_GROUP.as_ptr()) }; - assert!(!bare.is_null()); - let mut bare_node: *mut OakEngineNode = unsafe { group_inner_slot(bare) }; - assert_eq!( - unsafe { - oakengine_node_group_get_inner( - &mut bare_node, - in_buf.as_mut_ptr(), - 256, - &mut in_elem, - ) - }, - 0 - ); - unsafe { oakengine_node_free(bare_node) }; - - // ---- multicam node surfaces ------------------------------------- - assert_eq!(unsafe { oakengine_multicam_get_source_count(multicam) }, 0); - assert_eq!( - unsafe { oakengine_multicam_get_current_source(multicam) }, - 0 - ); - - // ---- gizmo surfaces (stubs) ------------------------------------- - assert_eq!(unsafe { oakengine_node_has_gizmos(value) }, 0); - assert_eq!(unsafe { oakengine_node_gizmo_count(value) }, 0); - assert!(unsafe { oakengine_node_gizmo_at(value, 0) }.is_null()); - assert_eq!( - unsafe { - oakengine_node_update_gizmo_positions(value, std::ptr::null_mut(), 1920, 1080, 0, 1) - }, - 0 - ); - assert_eq!( - unsafe { - oakengine_node_update_gizmo_positions( - std::ptr::null_mut(), - std::ptr::null_mut(), - 0, - 0, - 0, - 1, - ) - }, - E_INVALID - ); - - // ---- node data (stub table) ------------------------------------- - let mut oty: c_int = 9; - let mut oi: i64 = 9; - for role in 0..=5 { - assert_eq!( - unsafe { - oakengine_node_get_data(value, role, &mut oty, &mut oi, buf.as_mut_ptr(), 512) - }, - 0 - ); - assert_eq!(oty, 0, "stub reports no data"); - } - assert_eq!( - unsafe { oakengine_node_get_data(value, 6, &mut oty, &mut oi, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_get_data(value, -1, &mut oty, &mut oi, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_get_data( - std::ptr::null(), - 0, - &mut oty, - &mut oi, - buf.as_mut_ptr(), - 512, - ) - }, - E_INVALID - ); - - // ---- exclusive dependencies / plugin messages / caches (stubs) -- - assert_eq!( - unsafe { oakengine_node_get_exclusive_dependency_count(value) }, - 0 - ); - assert!(unsafe { oakengine_node_get_exclusive_dependency_at(value, 0) }.is_null()); - assert_eq!(unsafe { oakengine_node_has_plugin(value) }, 0); - assert_eq!(unsafe { oakengine_node_plugin_message_count(value) }, 0); - assert_eq!( - unsafe { oakengine_node_plugin_message_at(value, 0, &mut oty, buf.as_mut_ptr(), 512) }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_node_plugin_clear_messages(value) }, - E_NOT_FOUND - ); - assert!(unsafe { oakengine_node_get_thumbnail_cache(value) }.is_null()); - assert!(unsafe { oakengine_node_get_waveform_cache(value) }.is_null()); - assert!( - unsafe { oakengine_node_get_video_frame_cache(value) }.is_null(), - "no cache object on a plain node" - ); - - // ---- viewer / block / clip / track surfaces --------------------- - assert!(unsafe { oakengine_viewer_output_get_connected_texture(value) }.is_null()); - assert!( - unsafe { oakengine_clip_get_track(value) }.is_null(), - "not a clip" - ); - assert_eq!( - unsafe { oakengine_track_get_type(value) }, - -1, - "not a track" - ); - assert_eq!(unsafe { oakengine_track_get_index(value) }, -1); - assert!(unsafe { oakengine_track_get_sequence(value) }.is_null()); - assert_eq!( - unsafe { oakengine_block_get_length_rational(value, &mut tb_num, &mut tb_den) }, - E_INVALID, - "not a block" - ); - assert_eq!( - unsafe { oakengine_block_get_in_rational(value, &mut tb_num, &mut tb_den) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_block_get_out_rational(value, &mut tb_num, &mut tb_den) }, - E_INVALID - ); - - // ---- subtitle / shape (stubs) ----------------------------------- - assert_eq!( - unsafe { oakengine_subtitle_get_text(value, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_subtitle_set_text(value, c"sub".as_ptr()) }, - E_INVALID - ); - let params = unsafe { std::mem::zeroed::() }; - assert_eq!( - unsafe { - oakengine_shape_set_rect_undoable( - value, - 0.0, - 0.0, - 10.0, - 10.0, - ¶ms, - std::ptr::null_mut(), - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_shape_set_rect_undoable( - std::ptr::null_mut(), - 0.0, - 0.0, - 10.0, - 10.0, - ¶ms, - std::ptr::null_mut(), - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_shape_set_rect_undoable( - value, - 0.0, - 0.0, - 10.0, - 10.0, - std::ptr::null(), - std::ptr::null_mut(), - ) - }, - E_INVALID - ); - // effect-input surface is a documented stub. - assert_eq!( - unsafe { oakengine_node_get_effect_input(value, buf.as_mut_ptr(), 512, &mut oty) }, - E_NOT_FOUND - ); - - // ---- bulk delete ------------------------------------------------- - // The solid → transform edge from the connect section is still live, - // so a redundant reconnect is rejected with the module STATE error; - // the edge is then deleted together with the node in one multi - // command. - assert_eq!( - unsafe { oakengine_node_connect(solid, transform, c"tex_in".as_ptr()) }, - NODE_E_STATE, - "the edge from the connect section is still live" - ); - let del_nodes = [transform]; - let edge_outputs = [solid]; - let edge_inputs = [transform]; - let edge_ids = [c"tex_in".as_ptr() as *const c_char]; - let edge_elems: [c_int; 1] = [-1]; - let count_before = unsafe { oakengine_project_node_count(project) }; - assert_eq!( - unsafe { - oakengine_nodes_delete_many( - del_nodes.as_ptr() as *mut *mut OakEngineNode, - std::ptr::null_mut(), - 1, - edge_outputs.as_ptr() as *mut *mut OakEngineNode, - edge_inputs.as_ptr() as *mut *mut OakEngineNode, - edge_ids.as_ptr() as *mut *const c_char, - edge_elems.as_ptr(), - 1, - ) - }, - 0 - ); - assert_eq!( - unsafe { oakengine_project_node_count(project) }, - count_before - 1, - "delete must remove the node" - ); - // Empty delete → Invalid ("nothing to delete"). - assert_eq!( - unsafe { - oakengine_nodes_delete_many( - std::ptr::null_mut(), - std::ptr::null_mut(), - 0, - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null(), - 0, - ) - }, - E_INVALID - ); - // _ex with a null node entry → Invalid. - let mut null_nodes = [std::ptr::null_mut()]; - assert_eq!( - unsafe { - oakengine_nodes_delete_many_ex( - null_nodes.as_mut_ptr(), - std::ptr::null_mut(), - 1, - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null(), - 0, - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null(), - 0, - ) - }, - E_INVALID - ); - - // ---- folders ----------------------------------------------------- - let root = unsafe { oakengine_project_root(project) }; - let folder = unsafe { oakengine_folder_create(project, root, c"Bin".as_ptr()) }; - assert!(!folder.is_null()); - let len = unsafe { oakengine_node_get_label(folder, buf.as_mut_ptr(), 512) }; - assert_eq!(len, "Bin".len() as c_int); - assert_eq!(unsafe { oakengine_node_is_folder(folder) }, 1); - // Root now has the folder as a child. - let root2 = unsafe { oakengine_project_root(project) }; - assert_eq!(unsafe { oakengine_folder_item_child_count(root2) }, 1); - assert_eq!( - unsafe { oakengine_folder_has_child_recursive(root2, folder) }, - 1 - ); - assert_eq!(unsafe { oakengine_folder_index_of_child(root2, folder) }, 0); - let child = unsafe { oakengine_folder_item_child(root2, 0) }; - assert!(!child.is_null()); - let len = unsafe { oakengine_node_get_type_id(child, buf.as_mut_ptr(), 512) }; - assert_eq!(len, "org.olivevideoeditor.Olive.folder".len() as c_int); - unsafe { oakengine_node_free(child) }; - assert!(unsafe { oakengine_folder_item_child(root2, 5) }.is_null()); - assert_eq!( - unsafe { oakengine_folder_item_child_count(std::ptr::null()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_folder_has_child_recursive(std::ptr::null(), folder) }, - 0 - ); - assert_eq!( - unsafe { oakengine_folder_has_child_recursive(root2, std::ptr::null()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_folder_index_of_child(std::ptr::null(), folder) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_folder_index_of_child(root2, std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_folder_index_of_child(root2, value) }, - E_NOT_FOUND, - "value is not in the root" - ); - // folder_add_child is undoable; add the value node under the folder. - assert_eq!(unsafe { oakengine_folder_add_child(folder, value) }, 0); - assert_eq!(unsafe { oakengine_folder_item_child_count(folder) }, 1); - assert_eq!( - unsafe { oakengine_folder_item_child_count(root2) }, - 1, - "moved out of the root" - ); - // A node already in one folder cannot be added to a second one: the - // facade mirrors the module's live one-folder-per-node check (its - // UNDOABLE command creator skips it) and rejects with STATE. - assert_eq!( - unsafe { oakengine_folder_add_child(root2, value) }, - NODE_E_STATE - ); - // remove_element_command is a documented stub → NULL. - assert!(unsafe { oakengine_folder_remove_element_command(root2, value) }.is_null()); - // move_children: move the value node back into the root. - let mut mv = [value]; - assert_eq!( - unsafe { oakengine_folder_move_children(mv.as_mut_ptr(), 1, root2, c"move".as_ptr()) }, - 0 - ); - assert_eq!(unsafe { oakengine_folder_item_child_count(root2) }, 2); - assert_eq!(unsafe { oakengine_folder_move_child(value, root2) }, 0); - assert_eq!( - unsafe { - oakengine_folder_move_children(std::ptr::null_mut(), 1, root2, c"x".as_ptr()) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_folder_move_children(mv.as_mut_ptr(), 0, root2, c"x".as_ptr()) }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_folder_move_children( - mv.as_mut_ptr(), - 1, - std::ptr::null_mut(), - c"x".as_ptr(), - ) - }, - E_INVALID - ); - // folder_create requires a folder parent. - assert!(unsafe { oakengine_folder_create(project, value, c"x".as_ptr()) }.is_null()); - unsafe { oakengine_node_free(folder) }; - unsafe { oakengine_node_free(root2) }; - - // ---- undo/redo over the global stack ---------------------------- - assert_eq!( - unsafe { oakengine_project_can_undo(project) }, - 1, - "commands were pushed above" - ); - assert_eq!(unsafe { oakengine_project_undo(project) }, 0); - assert_eq!(unsafe { oakengine_project_can_redo(project) }, 1); - assert_eq!(unsafe { oakengine_project_redo(project) }, 0); - assert_eq!( - unsafe { oakengine_project_can_undo(std::ptr::null_mut()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_project_can_redo(std::ptr::null_mut()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_project_undo(std::ptr::null_mut()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_project_redo(std::ptr::null_mut()) }, - E_INVALID - ); - - // ---- save → fresh load round trip -------------------------------- - let save_path = - std::env::temp_dir().join(format!("oak_it_node_save-{}.ovexml", std::process::id())); - let _ = std::fs::remove_file(&save_path); - let sp = CString::new(save_path.to_string_lossy().into_owned()).unwrap(); - assert_eq!(unsafe { oakengine_project_save(project, sp.as_ptr()) }, 0); - assert!(save_path.exists()); - // Save with NULL path uses the project filename. - assert_eq!( - unsafe { oakengine_project_save(project, std::ptr::null()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_project_save(std::ptr::null_mut(), sp.as_ptr()) }, - E_INVALID - ); - - let project2 = oakengine_project_create(); - let mut err = [0 as c_char; 512]; - let rc = unsafe { oakengine_project_load(project2, sp.as_ptr(), err.as_mut_ptr(), 512) }; - if rc == 0 { - assert!(unsafe { oakengine_project_node_count(project2) } >= 1); - } else { - let err_len = unsafe { CStr::from_ptr(err.as_ptr()) }.to_bytes().len(); - assert!(err_len > 0, "load failure must fill the error buffer"); - } - unsafe { oakengine_project_free(project2) }; - // Load on an already-initialized project → E_STATE. - assert_eq!( - unsafe { oakengine_project_load(project, sp.as_ptr(), err.as_mut_ptr(), 512) }, - E_STATE - ); - // Load with a NULL path → Invalid. - let project3 = oakengine_project_create(); - assert_eq!( - unsafe { oakengine_project_load(project3, std::ptr::null(), err.as_mut_ptr(), 512) }, - E_INVALID - ); - // Load of a nonexistent file → failure + non-empty err buffer. - let rc = unsafe { - oakengine_project_load( - project3, - c"/no/such/project-file.ove".as_ptr(), - err.as_mut_ptr(), - 512, - ) - }; - assert!(rc < 0); - assert!(unsafe { CStr::from_ptr(err.as_ptr()) }.to_bytes().len() > 0); - unsafe { oakengine_project_free(project3) }; - let _ = std::fs::remove_file(&save_path); - - // ---- footage: probe / import / proxy / relink ------------------- - let media_a = fresh_temp_file("media-a.mp4", b"not-real-media"); - let media_a_c = CString::new(media_a.to_string_lossy().into_owned()).unwrap(); - - // Probe a real file (module records the filename only; no decoder). - let footage = unsafe { oakengine_footage_probe(media_a_c.as_ptr()) }; - assert!(!footage.is_null()); - let len = unsafe { oakengine_footage_get_filename(footage, buf.as_mut_ptr(), 512) }; - assert!(len > 0); - assert_eq!( - unsafe { read_buf(&mut buf) }, - media_a.to_string_lossy().into_owned() - ); - assert_eq!( - unsafe { oakengine_footage_get_decoder_name(footage, buf.as_mut_ptr(), 512) }, - 0 - ); - assert_eq!( - unsafe { oakengine_footage_get_video_stream_count(footage) }, - 0 - ); - assert_eq!( - unsafe { oakengine_footage_get_audio_stream_count(footage) }, - 0 - ); - assert_eq!( - unsafe { oakengine_footage_get_subtitle_stream_count(footage) }, - 0 - ); - assert_eq!(unsafe { oakengine_footage_is_online(footage) }, 1); - let mut secs: f64 = -1.0; - assert_eq!( - unsafe { oakengine_footage_get_duration(footage, &mut secs) }, - 0 - ); - assert_eq!(secs, 0.0, "module footage has no media duration"); - // No streams → stream accessors report NOT_FOUND. - let mut vinfo: OakFootageVideoInfo = unsafe { std::mem::zeroed() }; - let mut ainfo: OakFootageAudioInfo = unsafe { std::mem::zeroed() }; - assert_eq!( - unsafe { oakengine_footage_get_video_stream_info(footage, 0, &mut vinfo) }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_footage_get_audio_stream_info(footage, 0, &mut ainfo) }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_footage_get_video_stream_info(footage, -1, &mut vinfo) }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_footage_get_video_stream_info(std::ptr::null_mut(), 0, &mut vinfo) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_get_video_stream_info(footage, 0, std::ptr::null_mut()) }, - E_INVALID - ); - let mut cr: c_int = 0; - let mut il: c_int = 0; - let mut pm: c_int = 0; - assert_eq!( - unsafe { - oakengine_footage_get_video_stream_overrides( - footage, - 0, - buf.as_mut_ptr(), - 512, - &mut cr, - &mut il, - &mut pm, - ) - }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { - oakengine_footage_set_video_stream_overrides(footage, 0, c"".as_ptr(), 0, 0, 0) - }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_footage_get_pixel_aspect(footage, 0, &mut tb_num, &mut tb_den) }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_footage_set_pixel_aspect(footage, 0, 1, 1) }, - E_NOT_FOUND - ); - // Bad pixel-aspect ratio is rejected before the stream lookup. - assert_eq!( - unsafe { oakengine_footage_set_pixel_aspect(footage, 0, 0, 1) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_set_pixel_aspect(footage, 0, 1, 0) }, - E_INVALID - ); - let mut si: i64 = 0; - let mut dur: i64 = 0; - assert_eq!( - unsafe { - oakengine_footage_get_image_sequence_params( - footage, - 0, - &mut si, - &mut dur, - &mut tb_num, - &mut tb_den, - ) - }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_footage_set_image_sequence_params(footage, 0, 0, 1, 25, 1) }, - E_NOT_FOUND - ); - // Bad image-sequence params are rejected first. - assert_eq!( - unsafe { oakengine_footage_set_image_sequence_params(footage, 0, -1, 1, 25, 1) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_set_image_sequence_params(footage, 0, 0, 0, 25, 1) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_set_image_sequence_params(footage, 0, 0, 1, 0, 1) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_set_image_sequence_params(footage, 0, 0, 1, 25, 0) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_get_stream_enabled(footage, 0, 0) }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_footage_get_stream_enabled(footage, 3, 0) }, - E_NOT_FOUND, - "garbage track type" - ); - assert_eq!( - unsafe { oakengine_footage_set_stream_enabled(footage, 0, 0, 1) }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_footage_get_stream_reference(footage, 0, &mut oty, &mut tb_num) }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_footage_get_stream_reference(footage, -1, &mut oty, &mut tb_num) }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_footage_describe_video_stream(footage, 0, buf.as_mut_ptr(), 512) }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_footage_describe_audio_stream(footage, 0, buf.as_mut_ptr(), 512) }, - E_NOT_FOUND - ); - // Source start time is a documented stub (0/1). - assert_eq!( - unsafe { oakengine_footage_get_source_start_time(footage, &mut tb_num, &mut tb_den) }, - 0 - ); - assert_eq!((tb_num, tb_den), (0, 1)); - assert_eq!( - unsafe { oakengine_footage_set_source_start_time(footage, 1, 0, 1) }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_footage_get_source_start_time_source(footage, buf.as_mut_ptr(), 512) - }, - 0 - ); - // Colorspace candidates are stubs. - assert_eq!(unsafe { oakengine_footage_colorspace_count(footage) }, 0); - assert_eq!( - unsafe { oakengine_footage_colorspace_at(footage, 0, buf.as_mut_ptr(), 512) }, - E_NOT_FOUND - ); - // Proxy state: defaults, set round trip, delete/clear. - assert_eq!(unsafe { oakengine_footage_proxy_get_state(footage) }, 0); - assert_eq!(unsafe { oakengine_footage_proxy_is_enabled(footage) }, 0); - assert_eq!( - unsafe { oakengine_footage_proxy_get_path(footage, buf.as_mut_ptr(), 512) }, - 0 - ); - assert_eq!( - unsafe { oakengine_footage_proxy_generate(footage) }, - E_STATE, - "documented stub" - ); - assert_eq!( - unsafe { oakengine_footage_set_proxy(footage, c"/tmp/proxy.mp4".as_ptr(), 1, 0, 1, 1) }, - 0 - ); - assert_eq!(unsafe { oakengine_footage_proxy_get_state(footage) }, 1); - assert_eq!(unsafe { oakengine_footage_proxy_is_enabled(footage) }, 1); - let len = unsafe { oakengine_footage_proxy_get_path(footage, buf.as_mut_ptr(), 512) }; - assert_eq!(len, "/tmp/proxy.mp4".len() as c_int); - assert_eq!(unsafe { read_buf(&mut buf) }, "/tmp/proxy.mp4"); - assert_eq!(unsafe { oakengine_footage_proxy_delete(footage) }, 0); - assert_eq!(unsafe { oakengine_footage_proxy_get_state(footage) }, 0); - assert_eq!( - unsafe { oakengine_footage_proxy_set_enabled(footage, 1) }, - 0 - ); - assert_eq!(unsafe { oakengine_footage_proxy_is_enabled(footage) }, 1); - assert_eq!(unsafe { oakengine_footage_clear_proxy(footage) }, 0); - assert_eq!(unsafe { oakengine_footage_proxy_is_enabled(footage) }, 0); - // Custom proxy params stubs. - assert_eq!( - unsafe { oakengine_footage_has_custom_proxy_params(footage) }, - 0 - ); - let mut pp: OakProxyParams = unsafe { std::mem::zeroed() }; - assert_eq!( - unsafe { oakengine_footage_get_effective_proxy_params(footage, &mut pp) }, - 0 - ); - assert_eq!(pp.width, 0); - assert_eq!( - unsafe { oakengine_footage_set_custom_proxy_params(footage, &pp) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_clear_custom_proxy_params(footage) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_invalidate(footage) }, - 0, - "documented no-op" - ); - // Relink requires an existing file. - let media_b = fresh_temp_file("media-b.mp4", b"other"); - let media_b_c = CString::new(media_b.to_string_lossy().into_owned()).unwrap(); - assert_eq!( - unsafe { oakengine_footage_relink(footage, media_b_c.as_ptr()) }, - 0 - ); - let len = unsafe { oakengine_footage_get_filename(footage, buf.as_mut_ptr(), 512) }; - assert_eq!( - unsafe { read_buf(&mut buf) }, - media_b.to_string_lossy().into_owned() - ); - let _ = len; - // Relink to a nonexistent file → NOT_FOUND + footage error text. - assert_eq!( - unsafe { oakengine_footage_relink(footage, c"/no/such/file.mp4".as_ptr()) }, - E_NOT_FOUND - ); - let err_len = unsafe { oakengine_footage_last_error(buf.as_mut_ptr(), 512) }; - assert!( - err_len > 0, - "footage_last_error must be non-empty after a failed relink" - ); - // NULL path → Invalid. - assert_eq!( - unsafe { oakengine_footage_relink(footage, std::ptr::null()) }, - E_INVALID - ); - unsafe { oakengine_footage_free(footage) }; - unsafe { oakengine_footage_free(std::ptr::null_mut()) }; - let empty_ftg = Box::into_raw(Box::new(OakEngineFootage { - handle: CHandle::null(), - })); - unsafe { oakengine_footage_free(empty_ftg) }; - - // Probe failure paths. - let probe_null = unsafe { oakengine_footage_probe(std::ptr::null()) }; - assert!(probe_null.is_null()); - let probe_missing = unsafe { oakengine_footage_probe(c"/no/such/media.mp4".as_ptr()) }; - assert!(probe_missing.is_null()); - let err_len = unsafe { oakengine_footage_last_error(buf.as_mut_ptr(), 512) }; - assert!(err_len > 0); - - // Import into the project (real file). - let imported = unsafe { oakengine_project_import_footage(project, media_b_c.as_ptr()) }; - assert!(!imported.is_null()); - assert_eq!(unsafe { oakengine_project_footage_count(project) }, 1); - let len = unsafe { oakengine_project_footage_filename(project, 0, buf.as_mut_ptr(), 512) }; - assert!(len > 0); - assert_eq!( - unsafe { oakengine_project_footage_is_online(project, 0) }, - 1 - ); - assert_eq!( - unsafe { oakengine_project_footage_filename(project, 5, buf.as_mut_ptr(), 512) }, - E_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_project_footage_is_online(project, 5) }, - E_NOT_FOUND - ); - // The footage node is in the graph: borrow + validity. - let footage_idx = unsafe { find_node(project, TYPE_FOOTAGE.to_str().unwrap()) }; - assert!(footage_idx >= 0); - let footage_node = unsafe { oakengine_project_node_at(project, footage_idx) }; - assert_eq!(unsafe { oakengine_node_is_footage(footage_node) }, 1); - assert_eq!( - unsafe { oakengine_footage_is_valid(footage_node) }, - 0, - "module footage is never probed" - ); - // `oakengine_footage_borrow` takes its OWN addref'd reference, so the - // borrow and the source node shell can BOTH be freed (no double-free). - let borrowed = unsafe { oakengine_footage_borrow(footage_node) }; - assert!(!borrowed.is_null()); - let len = unsafe { oakengine_footage_get_filename(borrowed, buf.as_mut_ptr(), 512) }; - assert!(len > 0); - unsafe { oakengine_footage_free(borrowed) }; - unsafe { oakengine_node_free(footage_node) }; - // Borrow of a non-footage node → NULL. - assert!(unsafe { oakengine_footage_borrow(value) }.is_null()); - assert_eq!(unsafe { oakengine_footage_is_valid(value) }, 0); - // Import failure paths. - assert!(unsafe { - oakengine_project_import_footage(project, c"/no/such/media.mp4".as_ptr()) - } - .is_null()); - assert!(unsafe { - oakengine_project_import_footage(std::ptr::null_mut(), media_b_c.as_ptr()) - } - .is_null()); - assert!(unsafe { oakengine_project_import_footage(project, std::ptr::null()) }.is_null()); - - // find_offline_footage: make the imported footage offline and - // relink it from a search directory. - let media_b_basename = media_b.file_name().unwrap().to_string_lossy().into_owned(); - let _ = std::fs::remove_file(&media_b); // now offline - assert_eq!( - unsafe { oakengine_project_footage_is_online(project, 0) }, - 0 - ); - let search_dir = - std::env::temp_dir().join(format!("oak-it-node-search-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&search_dir); - std::fs::create_dir_all(&search_dir).unwrap(); - std::fs::write(search_dir.join(&media_b_basename), b"found").unwrap(); - let search_c = CString::new(search_dir.to_string_lossy().into_owned()).unwrap(); - assert_eq!( - unsafe { oakengine_project_find_offline_footage(project, search_c.as_ptr()) }, - 1 - ); - assert_eq!( - unsafe { oakengine_project_footage_is_online(project, 0) }, - 1 - ); - assert_eq!( - unsafe { oakengine_project_find_offline_footage(project, std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_project_find_offline_footage(std::ptr::null_mut(), search_c.as_ptr()) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_project_find_offline_footage(project, c"/no/such/dir".as_ptr()) }, - E_INVALID - ); - - // ---- remove a node + free the project --------------------------- - assert_eq!(unsafe { oakengine_project_remove_node(project, value2) }, 0); - assert_eq!( - unsafe { oakengine_project_remove_node(project, value2) }, - E_INVALID, - "already removed" - ); - assert_eq!( - unsafe { oakengine_project_remove_node(std::ptr::null_mut(), value) }, - E_INVALID - ); - - // Free borrowed shells captured above. - unsafe { oakengine_node_free(value) }; - unsafe { oakengine_node_free(value2) }; - unsafe { oakengine_node_free(solid) }; - unsafe { oakengine_node_free(group) }; - unsafe { oakengine_node_free(bare) }; - unsafe { oakengine_node_free(multicam) }; - unsafe { oakengine_node_free(orphan) }; - unsafe { oakengine_footage_free(imported) }; - unsafe { oakengine_project_free(project) }; - // One intentional process-lifetime leak remains: the hidden probe - // project created by the first `oakengine_footage_probe` (leaked - // like the C++ EngineCore shell). The add_node factory handles are - // released by the facade, so they no longer leak. - assert_eq!(alive(), base + 1, "only the probe project leak remains"); - }); -} - -// --------------------------------------------------------------------------- -// Effect chain surface (node.h effect-input family) -// --------------------------------------------------------------------------- - -/// Effect chain enumeration and edits over the facade: insert at index, -/// remove, reorder, enable toggle — each a single undoable row — plus the -/// NULL and illegal-input matrix of the new exports. The chain semantics -/// mirror the C++ original (`Node::GetEffectInputID` / `GetConnectedOutput` -/// walk): effects attach to the host's effect input, ordered closest-to- -/// source first. -#[test] -fn node_effect_chain_edits() { - with_owned(|| { - common::force_link(); - let _ = force_oakundo_command_link(); - let base = alive(); - let mut buf = [0 as c_char; 512]; - let mut element: c_int = 0; - - // ---- fixtures: a transform host + a plain value node --------------- - let project = oakengine_project_create(); - assert!(!project.is_null()); - assert_eq!(unsafe { oakengine_project_new(project) }, 0); - let host = unsafe { oakengine_project_add_node(project, TYPE_TRANSFORM.as_ptr()) }; - assert!(!host.is_null()); - let value = unsafe { oakengine_project_add_node(project, TYPE_VALUE.as_ptr()) }; - assert!(!value.is_null()); - - // The transform's effect input is "tex_in". - let len = - unsafe { oakengine_node_get_effect_input(host, buf.as_mut_ptr(), 512, &mut element) }; - assert_eq!(len, "tex_in".len() as c_int); - assert_eq!(unsafe { read_buf(&mut buf) }, "tex_in"); - assert_eq!(element, -1, "non-array input element"); - // A node without an effect input cannot host effects. - assert_eq!( - unsafe { oakengine_node_get_effect_input(value, buf.as_mut_ptr(), 512, &mut element) }, - E_NOT_FOUND - ); - - // ---- enumeration on an empty chain --------------------------------- - assert_eq!(unsafe { oakengine_node_effect_count(host) }, 0); - assert!(unsafe { oakengine_node_effect_at(host, 0) }.is_null()); - - // ---- insert three effects: opacity, blur, transform ----------------- - // Inserting at 0 twice stacks each new effect closest to the source; - // inserting at len appends next to the host. - assert_eq!( - unsafe { oakengine_node_effect_insert(host, 0, TYPE_BLUR.as_ptr()) }, - 0 - ); - eprintln!("[dbg] count after blur insert: {}", unsafe { - oakengine_node_effect_count(host) - }); - assert_eq!( - unsafe { oakengine_node_effect_insert(host, 0, TYPE_OPACITY.as_ptr()) }, - 0 - ); - eprintln!("[dbg] count after opacity insert: {}", unsafe { - oakengine_node_effect_count(host) - }); - assert_eq!( - unsafe { oakengine_node_effect_insert(host, 2, TYPE_TRANSFORM.as_ptr()) }, - 0 - ); - eprintln!("[dbg] count after transform insert: {}", unsafe { - oakengine_node_effect_count(host) - }); - assert_eq!(unsafe { oakengine_node_effect_count(host) }, 3); - - let eff0 = unsafe { oakengine_node_effect_at(host, 0) }; - let eff1 = unsafe { oakengine_node_effect_at(host, 1) }; - let eff2 = unsafe { oakengine_node_effect_at(host, 2) }; - assert!(!eff0.is_null() && !eff1.is_null() && !eff2.is_null()); - assert!(unsafe { oakengine_node_effect_at(host, -1) }.is_null()); - assert!(unsafe { oakengine_node_effect_at(host, 99) }.is_null()); - // Signal order: opacity (source side) → blur → transform (host side). - assert_eq!( - unsafe { oakengine_node_get_type_id(eff0, buf.as_mut_ptr(), 512) }, - TYPE_OPACITY.to_bytes().len() as c_int - ); - assert_eq!( - unsafe { read_buf(&mut buf) }, - TYPE_OPACITY.to_str().unwrap() - ); - assert_eq!( - unsafe { oakengine_node_get_type_id(eff1, buf.as_mut_ptr(), 512) }, - TYPE_BLUR.to_bytes().len() as c_int - ); - assert_eq!(unsafe { read_buf(&mut buf) }, TYPE_BLUR.to_str().unwrap()); - assert_eq!( - unsafe { oakengine_node_get_type_id(eff2, buf.as_mut_ptr(), 512) }, - TYPE_TRANSFORM.to_bytes().len() as c_int - ); - assert_eq!( - unsafe { read_buf(&mut buf) }, - TYPE_TRANSFORM.to_str().unwrap() - ); - - // Effect nodes carry the video-effect flag; plain nodes do not. - assert_ne!( - unsafe { oakengine_node_get_flags(eff0) } & oakengine_node_flag_video_effect(), - 0 - ); - assert_ne!( - unsafe { oakengine_node_get_flags(eff1) } & oakengine_node_flag_video_effect(), - 0 - ); - assert_eq!( - unsafe { oakengine_node_get_flags(value) } & oakengine_node_flag_video_effect(), - 0 - ); - - // Stable identities (the stack's card ids) are non-zero. - let eff1_identity = unsafe { oakengine_node_identity(eff1) }; - assert!(eff1_identity > 0); - - // ---- enable toggle -------------------------------------------------- - assert_eq!(unsafe { oakengine_node_is_enabled(eff1) }, 1); - assert_eq!(unsafe { oakengine_node_effect_set_enabled(eff1, 0) }, 0); - assert_eq!(unsafe { oakengine_node_is_enabled(eff1) }, 0); - assert_eq!(unsafe { oakengine_node_effect_set_enabled(eff1, 1) }, 0); - assert_eq!(unsafe { oakengine_node_is_enabled(eff1) }, 1); - - // ---- reorder: move blur (index 1) to the end ----------------------- - assert_eq!(unsafe { oakengine_node_effect_move(host, eff1, 2) }, 0); - assert_eq!( - unsafe { - oakengine_node_get_type_id( - unsafe { oakengine_node_effect_at(host, 0) }, - buf.as_mut_ptr(), - 512, - ) - }, - TYPE_OPACITY.to_bytes().len() as c_int - ); - assert_eq!( - unsafe { - oakengine_node_get_type_id( - unsafe { oakengine_node_effect_at(host, 1) }, - buf.as_mut_ptr(), - 512, - ) - }, - TYPE_TRANSFORM.to_bytes().len() as c_int - ); - assert_eq!( - unsafe { - oakengine_node_get_type_id( - unsafe { oakengine_node_effect_at(host, 2) }, - buf.as_mut_ptr(), - 512, - ) - }, - TYPE_BLUR.to_bytes().len() as c_int - ); - - // ---- remove transform (now at index 1) ------------------------------ - let t_at_1 = unsafe { oakengine_node_effect_at(host, 1) }; - assert!(!t_at_1.is_null()); - assert_eq!(unsafe { oakengine_node_effect_remove(host, t_at_1) }, 0); - assert_eq!(unsafe { oakengine_node_effect_count(host) }, 2); - assert_eq!( - unsafe { - oakengine_node_get_type_id( - unsafe { oakengine_node_effect_at(host, 0) }, - buf.as_mut_ptr(), - 512, - ) - }, - TYPE_OPACITY.to_bytes().len() as c_int - ); - assert_eq!( - unsafe { - oakengine_node_get_type_id( - unsafe { oakengine_node_effect_at(host, 1) }, - buf.as_mut_ptr(), - 512, - ) - }, - TYPE_BLUR.to_bytes().len() as c_int - ); - - // ---- the edit is one undo row: undo/redo restores the chain -------- - assert_eq!(unsafe { oakengine_project_undo(project) }, 0); - assert_eq!(unsafe { oakengine_node_effect_count(host) }, 3); - assert_eq!( - unsafe { - oakengine_node_get_type_id( - unsafe { oakengine_node_effect_at(host, 0) }, - buf.as_mut_ptr(), - 512, - ) - }, - TYPE_OPACITY.to_bytes().len() as c_int - ); - assert_eq!( - unsafe { - oakengine_node_get_type_id( - unsafe { oakengine_node_effect_at(host, 1) }, - buf.as_mut_ptr(), - 512, - ) - }, - TYPE_TRANSFORM.to_bytes().len() as c_int - ); - assert_eq!( - unsafe { - oakengine_node_get_type_id( - unsafe { oakengine_node_effect_at(host, 2) }, - buf.as_mut_ptr(), - 512, - ) - }, - TYPE_BLUR.to_bytes().len() as c_int - ); - assert_eq!(unsafe { oakengine_project_redo(project) }, 0); - assert_eq!(unsafe { oakengine_node_effect_count(host) }, 2); - - // ---- out-of-range insert clamps to the host side ------------------- - assert_eq!( - unsafe { oakengine_node_effect_insert(host, 99, TYPE_BLUR.as_ptr()) }, - 0 - ); - assert_eq!(unsafe { oakengine_node_effect_count(host) }, 3); - assert_eq!( - unsafe { - oakengine_node_get_type_id( - unsafe { oakengine_node_effect_at(host, 2) }, - buf.as_mut_ptr(), - 512, - ) - }, - TYPE_BLUR.to_bytes().len() as c_int - ); - - // ---- illegal inputs ------------------------------------------------- - assert_eq!( - unsafe { oakengine_node_effect_insert(std::ptr::null_mut(), 0, TYPE_BLUR.as_ptr()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_effect_insert(host, 0, std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_effect_insert(host, 0, c"org.oak.no.such.node".as_ptr()) }, - E_FAILED, - "unknown factory id" - ); - assert_eq!( - unsafe { oakengine_node_effect_insert(host, 0, TYPE_VALUE.as_ptr()) }, - E_INVALID, - "a node without an effect input cannot be chained" - ); - assert_eq!( - unsafe { oakengine_node_effect_insert(value, 0, TYPE_BLUR.as_ptr()) }, - E_NOT_FOUND, - "a host without an effect input hosts no chain" - ); - assert_eq!( - unsafe { oakengine_node_effect_remove(std::ptr::null_mut(), eff1) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_effect_remove(host, std::ptr::null_mut()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_effect_remove(host, value) }, - E_NOT_FOUND, - "not a member of the chain" - ); - assert_eq!( - unsafe { oakengine_node_effect_move(std::ptr::null_mut(), eff1, 0) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_effect_move(host, std::ptr::null_mut(), 0) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_effect_move(host, value, 0) }, - E_NOT_FOUND, - "not a member of the chain" - ); - assert_eq!( - unsafe { oakengine_node_effect_set_enabled(std::ptr::null_mut(), 0) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_is_enabled(std::ptr::null_mut()) }, - 0 - ); - assert_eq!(unsafe { oakengine_node_identity(std::ptr::null_mut()) }, 0); - assert_eq!( - unsafe { oakengine_node_effect_count(std::ptr::null_mut()) }, - 0 - ); - assert!(unsafe { oakengine_node_effect_at(std::ptr::null_mut(), 0) }.is_null()); - assert_eq!( - unsafe { - oakengine_node_get_effect_input( - std::ptr::null_mut(), - buf.as_mut_ptr(), - 512, - &mut element, - ) - }, - E_INVALID - ); - assert_eq!(unsafe { oakengine_node_get_flags(std::ptr::null_mut()) }, 0); - - // ---- factory id enumeration + clip → node conversion --------------- - let factory_count = oakengine_node_factory_id_count(); - assert!(factory_count > 0); - let len = unsafe { oakengine_node_factory_id_at(0, buf.as_mut_ptr(), 512) }; - assert!(len > 0); - let first_id = unsafe { read_buf(&mut buf) }; - let name_len = unsafe { - oakengine_node_factory_name_from_id( - first_id.as_ptr() as *const c_char, - buf.as_mut_ptr(), - 512, - ) - }; - assert!(name_len > 0, "factory name resolves for an enumerated id"); - assert_eq!( - unsafe { oakengine_node_factory_id_at(-1, buf.as_mut_ptr(), 512) }, - NODE_E_NOT_FOUND - ); - assert_eq!( - unsafe { oakengine_node_factory_id_at(factory_count, buf.as_mut_ptr(), 512) }, - NODE_E_NOT_FOUND - ); - // `oakengine_clip_as_node` converts a clip box to its node view (the - // engine opaque types share one CHandle layout; a plain node box - // exercises the same conversion path). - let clip_view = value as *mut oakengine::handle::OakEngineClip; - let as_node = unsafe { oakengine_clip_as_node(clip_view) }; - assert!(!as_node.is_null()); - assert_eq!( - unsafe { oakengine_node_get_type_id(as_node, buf.as_mut_ptr(), 512) }, - TYPE_VALUE.to_bytes().len() as c_int - ); - assert!(unsafe { oakengine_clip_as_node(std::ptr::null_mut()) }.is_null()); - - // ---- cleanup -------------------------------------------------------- - unsafe { oakengine_node_free(eff0) }; - unsafe { oakengine_node_free(eff1) }; - unsafe { oakengine_node_free(eff2) }; - unsafe { oakengine_node_free(t_at_1) }; - unsafe { oakengine_node_free(as_node) }; - unsafe { oakengine_node_free(host) }; - unsafe { oakengine_node_free(value) }; - unsafe { oakengine_project_free(project) }; - // No probe project is involved in this test (unlike the footage - // family above): the effect edits keep every node inside the project - // graph, so freeing the project returns the alive counter to base. - assert_eq!(alive(), base, "effect-chain edits must not leak boxes"); - }); -} - -// --------------------------------------------------------------------------- -// Illegal inputs: NULL / empty handles / bad sizes / garbage (parallel-safe) -// --------------------------------------------------------------------------- - -/// NULL and empty-`CHandle` boxes for the handle-taking exports: every -/// call must return a clean negative code or the documented no-op, never a -/// crash. Runs in parallel (no undo-stack or alive-counter access). -#[test] -fn null_and_empty_handle_failure_paths() { - common::force_link(); - let _ = force_oakundo_command_link(); - let mut buf = [0 as c_char; 512]; - let mut out: OakNodeValue = unsafe { std::mem::zeroed() }; - let mut num: i64 = 0; - let mut den: i64 = 0; - // Scratch out-params reused across the failure calls below. - let mut f64a: f64 = 0.0; - let mut f64b: f64 = 0.0; - let mut f32a: f32 = 0.0; - let mut f32b: f32 = 0.0; - let mut f32c: f32 = 0.0; - let mut f32d: f32 = 0.0; - let mut i32a: c_int = 0; - let mut i32b: c_int = 0; - let mut i32c: c_int = 0; - let mut i64a: i64 = 0; - let mut i64b: i64 = 0; - - // Empty-handle boxes (non-NULL pointers wrapping CHandle::null()). - let node = empty_node_box(); - let project = Box::into_raw(Box::new(OakEngineProject { - handle: CHandle::null(), - })); - let keyframe = Box::into_raw(Box::new(OakEngineKeyframe { - handle: CHandle::null(), - })); - let dragger = Box::into_raw(Box::new(OakEngineNodeDragger { - handle: CHandle::null(), - })); - let footage = Box::into_raw(Box::new(OakEngineFootage { - handle: CHandle::null(), - })); - - // project family - assert_eq!(unsafe { oakengine_project_new(project) }, E_INVALID); - assert_eq!( - unsafe { oakengine_project_is_modified(project) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_project_set_modified(project, 1) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_project_name(project, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_project_filename(project, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_project_pretty_filename(project, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_project_set_filename(project, c"/x.ove".as_ptr()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_project_cache_path(project, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_project_cache_alongside_path(project, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_project_set_custom_cache_path(project, c"/x".as_ptr()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_project_get_custom_cache_path(project, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_project_get_cache_location_setting(project) }, - -1, - "NULL → -1 documented" - ); - assert_eq!( - unsafe { oakengine_project_get_color_reference_space(project, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_project_set_color_reference_space(project, c"x".as_ptr()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_project_footage_count(project) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_project_footage_filename(project, 0, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_project_footage_is_online(project, 0) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_project_sequence_count(project) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert!(unsafe { oakengine_project_sequence_at(project, 0) }.is_null()); - assert_eq!( - unsafe { oakengine_project_node_count(project) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert!(unsafe { oakengine_project_node_at(project, 0) }.is_null()); - assert!(unsafe { oakengine_project_root(project) }.is_null()); - assert!(unsafe { oakengine_project_from_object(node) }.is_null()); - assert_eq!( - unsafe { oakengine_project_save(project, c"/x.ove".as_ptr()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_project_load(project, c"/x.ove".as_ptr(), buf.as_mut_ptr(), 512) }, - E_INVALID - ); - - // folder family - assert!(unsafe { oakengine_folder_create(project, node, c"x".as_ptr()) }.is_null()); - assert_eq!( - unsafe { oakengine_folder_has_child_recursive(node, node) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_folder_index_of_child(node, node) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_folder_item_child_count(node) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert!(unsafe { oakengine_folder_item_child(node, 0) }.is_null()); - assert_eq!(unsafe { oakengine_folder_add_child(node, node) }, E_INVALID); - assert!(unsafe { oakengine_folder_remove_element_command(node, node) }.is_null()); - assert_eq!( - unsafe { oakengine_folder_move_child(node, node) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_folder_move_children(std::ptr::null_mut(), 1, node, c"x".as_ptr()) }, - E_INVALID - ); - - // node factory / metadata / params - assert!(unsafe { oakengine_node_factory_create_from_id(c"x".as_ptr()) }.is_null()); - assert_eq!( - unsafe { oakengine_node_category_count(node) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!(unsafe { oakengine_node_category_at(node, 0) }, -1); - // Empty handle and NULL both report 0 flags (no error sentinel leaks - // through as u64::MAX). - assert_eq!(unsafe { oakengine_node_get_flags(node) }, 0); - assert_eq!( - unsafe { oakengine_node_get_sub_category(node, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_get_description(node, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert!(unsafe { oakengine_node_create_copy(node) }.is_null()); - assert_eq!( - unsafe { oakengine_node_get_type_id(node, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_get_name(node, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_get_short_name(node, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_get_label(node, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_set_label(node, c"x".as_ptr()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_set_label_ex(node, c"x".as_ptr(), 0) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_get_label_and_name(node, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_get_color_label(node) }, - -1, - "NULL → -1 documented" - ); - assert_eq!( - unsafe { oakengine_node_get_effective_color_label(node) }, - -1 - ); - assert!(unsafe { oakengine_node_set_color_label_command(node, 0) }.is_null()); - assert_eq!( - unsafe { oakengine_node_input_count(node) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_input_id(node, 0, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_input_get_type(node, c"x".as_ptr()) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_input_is_connected(node, c"x".as_ptr()) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_input_is_connectable(node, c"x".as_ptr()) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_input_is_keyframable(node, c"x".as_ptr()) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_input_is_keyframed(node, c"x".as_ptr()) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_input_is_keyframed_ex(node, c"x".as_ptr(), 0) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_input_is_array(node, c"x".as_ptr()) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_input_array_size(node, c"x".as_ptr()) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_input_get_flags(node, c"x".as_ptr()) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_input_get_data_type(node, c"x".as_ptr()) }, - -1 - ); - assert_eq!( - unsafe { oakengine_node_input_is_hidden(node, c"x".as_ptr()) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_get_input(node, c"x".as_ptr(), &mut out) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_set_input(node, c"x".as_ptr(), &float_value(1.0)) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_get_input_string(node, c"x".as_ptr(), buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_set_input_string(node, c"x".as_ptr(), c"s".as_ptr()) }, - E_INVALID - ); - assert!( - unsafe { - oakengine_node_set_standard_value_command( - node, - c"x".as_ptr(), - -1, - -1, - &float_value(1.0), - ) - } - .is_null(), - "stub-null" - ); - assert!( - unsafe { - oakengine_node_set_input_video_params_command(node, c"x".as_ptr(), &unsafe { - std::mem::zeroed::() - }) - } - .is_null(), - "stub-null" - ); - assert!( - unsafe { - oakengine_node_set_value_at_time_command( - node as *mut c_void, - c"x".as_ptr(), - -1, - 0, - 1, - &float_value(1.0), - -1, - 0, - ) - } - .is_null(), - "empty handle" - ); - assert_eq!( - unsafe { oakengine_node_frame_time_base(node, &mut i32a, &mut i32b) }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_set_input_at_time(node, c"x".as_ptr(), -1, 0, -1, &float_value(1.0), 0) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_get_input_at_time(node, c"x".as_ptr(), -1, -1, 0, 0, &mut out) }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_set_input_string_at_time(node, c"x".as_ptr(), -1, 0, c"s".as_ptr()) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_get_input_string_at_time( - node, - c"x".as_ptr(), - -1, - 0, - -1, - buf.as_mut_ptr(), - 512, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_get_input_bezier_at_time( - node, - c"x".as_ptr(), - -1, - 0, - -1, - std::ptr::null_mut(), - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_get_input_binary_at_time( - node, - c"x".as_ptr(), - -1, - 0, - -1, - buf.as_mut_ptr(), - 512, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_array_insert_at(node, c"x".as_ptr(), 0) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_array_remove_at(node, c"x".as_ptr(), 0) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_input_has_property(node, c"x".as_ptr(), c"k".as_ptr()) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { - oakengine_node_set_input_property_string( - node, - c"x".as_ptr(), - c"k".as_ptr(), - c"v".as_ptr(), - 0, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_input_get_property_string( - node, - c"x".as_ptr(), - c"k".as_ptr(), - buf.as_mut_ptr(), - 512, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_input_get_property_number( - node, - c"x".as_ptr(), - c"k".as_ptr(), - 0, - &mut f64a, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_input_get_property_int(node, c"x".as_ptr(), c"k".as_ptr(), &mut num) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_input_get_property_rational( - node, - c"x".as_ptr(), - c"k".as_ptr(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_input_get_property_track_number( - node, - c"x".as_ptr(), - c"k".as_ptr(), - 0, - &mut f64a, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_input_get_property_count(node, c"x".as_ptr()) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { - oakengine_node_input_get_property_key(node, c"x".as_ptr(), 0, buf.as_mut_ptr(), 512) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_input_get_property_string_list_count(node, c"x".as_ptr(), c"k".as_ptr()) - }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { - oakengine_node_input_get_property_string_list( - node, - c"x".as_ptr(), - c"k".as_ptr(), - 0, - buf.as_mut_ptr(), - 512, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_input_get_default_value(node, c"x".as_ptr(), 0, &mut out) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_get_input_name(node, c"x".as_ptr(), buf.as_mut_ptr(), 512) }, - E_INVALID - ); - - // graph editing - assert_eq!( - unsafe { oakengine_node_connect(node, node, c"x".as_ptr()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_disconnect(node, c"x".as_ptr()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_disconnect_ex(node, c"x".as_ptr(), -1) }, - E_INVALID - ); - assert!(unsafe { oakengine_node_connect_command(node, node, c"x".as_ptr(), -1) }.is_null()); - assert!(unsafe { oakengine_node_disconnect_command(node, c"x".as_ptr(), -1) }.is_null()); - assert_eq!(unsafe { oakengine_node_copy_inputs(node, node) }, E_INVALID); - assert!(unsafe { oakengine_node_copy_in_graph(node, std::ptr::null_mut()) }.is_null()); - assert!(unsafe { oakengine_node_input_get_connected_node(node, c"x".as_ptr(), -1) }.is_null()); - assert_eq!( - unsafe { oakengine_node_output_connection_count(node) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { - oakengine_node_output_connection_at( - node, - 0, - std::ptr::null_mut(), - buf.as_mut_ptr(), - 512, - std::ptr::null_mut(), - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_output_connection_at_ex( - node, - 0, - std::ptr::null_mut(), - buf.as_mut_ptr(), - 512, - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_input_connection_count_all(node) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { - oakengine_node_input_connection_at_all( - node, - 0, - std::ptr::null_mut(), - buf.as_mut_ptr(), - 512, - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_input_connection_count(node, c"x".as_ptr(), -1) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert!(unsafe { oakengine_node_input_connection_at(node, c"x".as_ptr(), -1, 0) }.is_null()); - assert_eq!( - unsafe { oakengine_node_inputs_from(node, node, 1) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_set_value_hint(node, c"x".as_ptr(), 0, 0, 0, c"".as_ptr()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_set_context_position(node, node, 0.0, 0.0) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_get_context_position(node, node, &mut f64a, &mut f64b, &mut i32a) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_set_context_expanded(node, node, 1) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_context_node_count(node) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_context_contains_node(node, node) }, - E_INVALID - ); - assert!( - unsafe { oakengine_node_context_node_at(node, 0, &mut f64a, &mut f64b, &mut i32a) } - .is_null() - ); - assert_eq!( - unsafe { oakengine_node_get_effect_input(node, buf.as_mut_ptr(), 512, &mut i32a) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_keyframe_count(node, c"x".as_ptr()) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_keyframe_count_on_track(node, c"x".as_ptr(), 0, 0) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_keyframe_at(node, c"x".as_ptr(), 0, &mut num, &mut out) }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_keyframe_get_easing( - node, - c"x".as_ptr(), - 0, - &mut f32a, - &mut f32b, - &mut f32c, - &mut f32d, - &mut i32a, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_keyframe_add( - node, - c"x".as_ptr(), - 0, - &float_value(1.0), - 0, - 0.0, - 0.0, - 0.0, - 0.0, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_keyframe_remove(node, c"x".as_ptr(), 0) }, - E_INVALID - ); - assert!(unsafe { - oakengine_node_insert_keyframe_command( - node, - c"x".as_ptr(), - -1, - 0, - 0, - &float_value(1.0), - 0, - 0.0, - 0.0, - 0.0, - 0.0, - ) - } - .is_null()); - assert_eq!( - unsafe { - oakengine_node_keyframe_set_easing(node, c"x".as_ptr(), 0, 0, 0.0, 0.0, 0.0, 0.0) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_keyframes_set_type_many( - node, - c"x".as_ptr(), - -1, - std::ptr::null(), - std::ptr::null(), - 0, - 0, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_keyframes_set_time_many( - node, - c"x".as_ptr(), - -1, - std::ptr::null(), - std::ptr::null(), - 0, - 0, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_keyframes_set_value_many( - node, - c"x".as_ptr(), - -1, - std::ptr::null(), - std::ptr::null(), - 0, - std::ptr::null(), - std::ptr::null(), - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_keyframes_set_bezier_many( - node, - c"x".as_ptr(), - -1, - std::ptr::null(), - std::ptr::null(), - 0, - 0.0, - 0.0, - 0.0, - 0.0, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_node_keyframe_set_bezier_point( - node, - c"x".as_ptr(), - -1, - 0, - 0, - 0, - 0.0, - 0.0, - 0.0, - 0.0, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_keyframes_clear(node, c"x".as_ptr()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_keyframe_best_type_at_time(node, c"x".as_ptr(), -1, 0, 0, 5) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_keyframe_track_count(node, c"x".as_ptr(), -1) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { - oakengine_node_keyframes_toggle_at_time(node, c"x".as_ptr(), -1, 0, 0, 1, c"t".as_ptr()) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_has_keyframe_at_time(node, c"x".as_ptr(), -1, 0, 0) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { - oakengine_node_keyframe_earliest_time(node, c"x".as_ptr(), -1, &mut num, &mut den) - }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_keyframe_latest_time(node, c"x".as_ptr(), -1, &mut num, &mut den) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { - oakengine_node_keyframe_closest_time_before( - node, - c"x".as_ptr(), - -1, - 0, - 0, - &mut num, - &mut den, - ) - }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { - oakengine_node_keyframe_closest_time_after( - node, - c"x".as_ptr(), - -1, - 0, - 0, - &mut num, - &mut den, - ) - }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert!( - unsafe { oakengine_node_keyframe_handle_on_track(node, c"x".as_ptr(), -1, 0, 0) }.is_null() - ); - assert!( - unsafe { oakengine_node_keyframe_handle_at_time(node, c"x".as_ptr(), -1, 0, 0, 1) } - .is_null() - ); - assert_eq!( - unsafe { - oakengine_node_keyframes_at_time(node, c"x".as_ptr(), -1, 0, 1, std::ptr::null_mut(), 0) - }, - 0, - "stub ignores its handle" - ); - assert_eq!( - unsafe { - oakengine_node_set_input_keyframing(node, c"x".as_ptr(), -1, 1, 0, 0, c"k".as_ptr()) - }, - E_INVALID - ); - assert!( - unsafe { oakengine_node_set_input_keyframing_command(node, c"x".as_ptr(), -1, 1) } - .is_null() - ); - assert_eq!( - unsafe { oakengine_node_keyframes_paste(node, std::ptr::null_mut(), 1, c"p".as_ptr()) }, - E_INVALID - ); - - // keyframe handle family - assert_eq!( - unsafe { oakengine_keyframe_get_time(keyframe, &mut num, &mut den) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_keyframe_get_input_id(keyframe, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!(unsafe { oakengine_keyframe_get_track(keyframe) }, -1); - assert_eq!(unsafe { oakengine_keyframe_get_element(keyframe) }, -1); - assert!(unsafe { oakengine_keyframe_get_node(keyframe) }.is_null()); - assert_eq!(unsafe { oakengine_keyframe_get_type(keyframe) }, -1); - assert_eq!( - unsafe { oakengine_keyframe_get_value(keyframe, &mut out) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_keyframe_compute_paste_value(node, keyframe, &mut out) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_keyframe_has_sibling_at_time(keyframe, 0, 0) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_keyframe_set_bezier_point_live(keyframe, 0, 0.0, 0.0) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_keyframe_get_bezier_point(keyframe, 0, &mut f64a, &mut f64b) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_keyframe_get_valid_bezier_point(keyframe, 0, &mut f64a, &mut f64b) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_keyframe_set_value_live(keyframe, &float_value(1.0)) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_keyframe_set_time_live(keyframe, 1, 1) }, - E_INVALID - ); - assert!(unsafe { oakengine_keyframe_set_time_command(keyframe, 1) }.is_null()); - assert!(unsafe { oakengine_keyframe_set_value_command(keyframe, &float_value(1.0)) }.is_null()); - assert!( - unsafe { - oakengine_keyframe_create(node, c"x".as_ptr(), -1, 0, 0, 0, &float_value(1.0), 0) - } - .is_null(), - "empty node" - ); - assert!(unsafe { - oakengine_keyframe_create( - std::ptr::null_mut(), - c"x".as_ptr(), - -1, - 0, - 0, - 0, - &float_value(1.0), - 0, - ) - } - .is_null()); - - // dragger family - assert!( - unsafe { oakengine_dragger_create(node, c"x".as_ptr(), -1, 1) }.is_null(), - "empty node" - ); - assert_eq!( - unsafe { oakengine_dragger_start(dragger, 0, 1, 0) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_dragger_drag(dragger, &float_value(1.0)) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_dragger_end(dragger, c"x".as_ptr()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_dragger_is_started(dragger) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - - // group family - assert_eq!( - unsafe { oakengine_group_input_passthrough_count(node) }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_group_add_input_passthrough( - node, - node, - c"x".as_ptr(), - -1, - c"".as_ptr(), - buf.as_mut_ptr(), - 512, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_group_input_passthrough_at( - node, - 0, - buf.as_mut_ptr(), - 512, - std::ptr::null_mut(), - buf.as_mut_ptr(), - 512, - &mut i32a, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_group_get_id_of_passthrough( - node, - node, - c"x".as_ptr(), - -1, - buf.as_mut_ptr(), - 512, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_group_get_passthrough_from_id( - node, - c"id".as_ptr(), - std::ptr::null_mut(), - buf.as_mut_ptr(), - 512, - &mut i32a, - ) - }, - E_INVALID - ); - assert!(unsafe { oakengine_group_get_output_passthrough(node) }.is_null()); - assert_eq!( - unsafe { oakengine_group_set_output_passthrough(node, node) }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_group_resolve_input( - node, - c"x".as_ptr(), - -1, - std::ptr::null_mut(), - buf.as_mut_ptr(), - 512, - &mut i32a, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_group_remove_input_passthrough(node, node, c"x".as_ptr(), -1) }, - E_INVALID - ); - assert!(unsafe { - oakengine_group_add_input_passthrough_command(node, node, c"x".as_ptr(), -1, c"".as_ptr()) - } - .is_null()); - assert!(unsafe { oakengine_group_set_output_passthrough_command(node, node) }.is_null()); - assert_eq!( - unsafe { - oakengine_group_add_input_passthrough_undoable( - node, - node, - c"x".as_ptr(), - -1, - c"".as_ptr(), - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_group_set_output_passthrough_undoable(node, node) }, - E_INVALID - ); - - // multicam family - assert_eq!( - unsafe { oakengine_multicam_get_source_count(node) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_multicam_get_current_source(node) }, - E_INVALID - ); - - // subtree / caches / data - assert_eq!( - unsafe { oakengine_node_has_gizmos(node) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_gizmo_count(node) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert!(unsafe { oakengine_node_gizmo_at(node, 0) }.is_null()); - assert_eq!( - unsafe { oakengine_node_update_gizmo_positions(node, std::ptr::null_mut(), 0, 0, 0, 1) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_get_data(node, 0, &mut i32a, &mut num, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_get_exclusive_dependency_count(node) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert!(unsafe { oakengine_node_get_exclusive_dependency_at(node, 0) }.is_null()); - assert_eq!( - unsafe { oakengine_node_has_plugin(node) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_plugin_message_count(node) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_plugin_message_at(node, 0, &mut i32a, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_node_plugin_clear_messages(node) }, - E_INVALID - ); - assert!(unsafe { oakengine_node_get_thumbnail_cache(node) }.is_null()); - assert!(unsafe { oakengine_node_get_waveform_cache(node) }.is_null()); - assert!(unsafe { oakengine_node_get_video_frame_cache(node) }.is_null()); - assert_eq!( - unsafe { oakengine_node_is_clip(node) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_is_track(node) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_is_viewer_output(node) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_is_footage(node) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_is_sequence(node) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_is_folder(node) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_is_group(node) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_is_multicam(node) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_node_is_item(node) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert!(unsafe { oakengine_node_get_project(node) }.is_null()); - assert!(unsafe { oakengine_node_parent(node) }.is_null()); - assert!(unsafe { oakengine_node_folder(node) }.is_null()); - assert!(unsafe { oakengine_clip_get_track(node) }.is_null()); - assert_eq!(unsafe { oakengine_track_get_type(node) }, -1); - assert_eq!(unsafe { oakengine_track_get_index(node) }, -1); - assert!(unsafe { oakengine_track_get_sequence(node) }.is_null()); - assert_eq!( - unsafe { oakengine_block_get_length_rational(node, &mut i32a, &mut i32b) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_block_get_in_rational(node, &mut i32a, &mut i32b) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_block_get_out_rational(node, &mut i32a, &mut i32b) }, - E_INVALID - ); - assert!(unsafe { oakengine_viewer_output_get_connected_texture(node) }.is_null()); - assert_eq!( - unsafe { - oakengine_shape_set_rect_undoable( - node, - 0.0, - 0.0, - 1.0, - 1.0, - &unsafe { std::mem::zeroed::() }, - std::ptr::null_mut(), - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_subtitle_get_text(node, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_subtitle_set_text(node, c"x".as_ptr()) }, - E_INVALID - ); - - // footage family - assert!( - unsafe { oakengine_footage_borrow(node) }.is_null(), - "not a footage node" - ); - assert_eq!( - unsafe { oakengine_footage_is_valid(node) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_footage_get_decoder_name(footage, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_get_video_stream_count(footage) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_footage_get_audio_stream_count(footage) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_footage_get_subtitle_stream_count(footage) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_footage_get_duration(footage, &mut f64a) }, - E_INVALID - ); - assert_eq!(unsafe { oakengine_footage_is_online(footage) }, E_INVALID); - assert_eq!( - unsafe { oakengine_footage_get_source_start_time(footage, &mut i32a, &mut i32b) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_relink(footage, c"/x.mp4".as_ptr()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_proxy_get_state(footage) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_proxy_generate(footage) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_proxy_delete(footage) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_proxy_is_enabled(footage) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_proxy_set_enabled(footage, 1) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_proxy_get_path(footage, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_get_filename(footage, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_colorspace_count(footage) }, - E_INVALID, - "empty handle, not a NULL pointer" - ); - assert_eq!( - unsafe { oakengine_footage_colorspace_at(footage, 0, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_has_custom_proxy_params(footage) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_get_effective_proxy_params(footage, std::ptr::null_mut()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_set_custom_proxy_params(footage, std::ptr::null()) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_clear_custom_proxy_params(footage) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_set_proxy(footage, c"/x.mp4".as_ptr(), 0, 0, 0, 1) }, - E_INVALID - ); - assert_eq!(unsafe { oakengine_footage_clear_proxy(footage) }, E_INVALID); - assert_eq!(unsafe { oakengine_footage_invalidate(footage) }, E_INVALID); - assert_eq!( - unsafe { oakengine_footage_set_source_start_time(footage, 1, 0, 1) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_get_source_start_time_source(footage, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_set_stream_enabled(footage, 0, 0, 1) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_get_stream_enabled(footage, 0, 0) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_get_stream_reference(footage, 0, &mut i32a, &mut i32b) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_describe_video_stream(footage, 0, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_describe_audio_stream(footage, 0, buf.as_mut_ptr(), 512) }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_footage_get_video_stream_overrides( - footage, - 0, - buf.as_mut_ptr(), - 512, - &mut i32a, - &mut i32b, - &mut i32c, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_set_video_stream_overrides(footage, 0, c"".as_ptr(), 0, 0, 0) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_get_pixel_aspect(footage, 0, &mut i32a, &mut i32b) }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_set_pixel_aspect(footage, 0, 1, 1) }, - E_INVALID - ); - assert_eq!( - unsafe { - oakengine_footage_get_image_sequence_params( - footage, 0, &mut i64a, &mut i64b, &mut i32a, &mut i32b, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine_footage_set_image_sequence_params(footage, 0, 0, 1, 25, 1) }, - E_INVALID - ); - assert!( - unsafe { oakengine_project_import_footage(project, c"/x.mp4".as_ptr()) }.is_null(), - "empty project" - ); - assert_eq!( - unsafe { oakengine_project_find_offline_footage(project, c"/tmp".as_ptr()) }, - E_INVALID - ); - - // buffer-size edge cases: NULL buf / zero / negative size never crash. - let mut small = [0 as c_char; 4]; - assert_eq!( - unsafe { oakengine_node_get_type_id(node, small.as_mut_ptr(), 4) }, - E_INVALID - ); // empty handle wins - let _ = unsafe { oakengine_node_last_error(std::ptr::null_mut(), 0) }; - let _ = unsafe { oakengine_node_last_error(std::ptr::null_mut(), -5) }; - let _ = unsafe { oakengine_footage_last_error(std::ptr::null_mut(), 0) }; - let _ = unsafe { oakengine_footage_last_error(small.as_mut_ptr(), -1) }; - // Two-stage getters with a NULL buf only report the length (legal). - let _ = unsafe { oakengine_node_category_name(0, std::ptr::null_mut(), 0) }; - - // Free the empty boxes (no-op release paths). - unsafe { oakengine_node_free(node) }; - unsafe { oakengine_project_free(project) }; - unsafe { oakengine_keyframe_dispose(keyframe) }; - unsafe { oakengine_dragger_free(dragger) }; - unsafe { oakengine_footage_free(footage) }; -} - -// --------------------------------------------------------------------------- -// Module destroy contracts + alive counter -// --------------------------------------------------------------------------- - -/// Facade `free`/`dispose`/NULL/empty contracts plus the module destroy -/// paths they delegate to: `free(NULL)` and `free(empty)` are no-ops, the -/// module-level free of an already-emptied handle is double-free-safe, and -/// every owned-object round trip restores the debug alive counter. -#[test] -fn destroy_contracts_and_alive_count() { - with_owned(|| { - common::force_link(); - let _ = force_oakundo_command_link(); - let base = alive(); - - // Facade-level free(NULL) / free(empty box) are no-ops. - unsafe { oakengine_project_free(std::ptr::null_mut()) }; - unsafe { oakengine_node_free(std::ptr::null_mut()) }; - unsafe { oakengine_keyframe_dispose(std::ptr::null_mut()) }; - unsafe { oakengine_dragger_free(std::ptr::null_mut()) }; - unsafe { oakengine_footage_free(std::ptr::null_mut()) }; - - // Owned round trips: create +1, free back to baseline. - let project = oakengine_project_create(); - assert!(!project.is_null()); - assert_eq!(alive(), base + 1); - unsafe { oakengine_project_free(project) }; - assert_eq!(alive(), base, "project free must return the alive counter"); - - let node = unsafe { oakengine_node_factory_create_from_id(TYPE_VALUE.as_ptr()) }; - assert!(!node.is_null()); - assert_eq!(alive(), base + 1); - unsafe { oakengine_node_free(node) }; - assert_eq!(alive(), base); - - let group = unsafe { oakengine_node_group_create() }; - assert!(!group.is_null()); - assert_eq!(alive(), base + 1); - unsafe { oakengine_node_free(group) }; - assert_eq!(alive(), base); - - // Module-level double-free of an already-emptied handle is a no-op - // (the destroy path the facade wraps clears ctx before returning). - let mut h = unsafe { oaknode_factory_create_from_id(TYPE_VALUE.as_ptr()) }; - assert!(!h.is_null()); - assert_eq!(alive(), base + 1); - unsafe { oaknode_node_free(&mut h) }; - assert!(h.is_null()); - unsafe { oaknode_node_free(&mut h) }; // double free: no-op - assert_eq!(alive(), base); - - let mut ph = unsafe { oakengine::oaknode::ffi::project::oaknode_project_init() }; - assert!(!ph.is_null()); - assert_eq!(alive(), base + 1); - unsafe { oaknode_project_free(&mut ph) }; - assert!(ph.is_null()); - unsafe { oaknode_project_free(&mut ph) }; // double free: no-op - assert_eq!(alive(), base); - - // Keyframe handles are not alive-counted but their free is - // double-free-safe at the module level. - let mut kh = unsafe { - oaknode_kf_create( - 0, - 1, - &float_value_direct(1.0), - 0, - 0, - -1, - c"value_in".as_ptr(), - CHandle::null(), - ) - }; - assert!(!kh.is_null()); - unsafe { oaknode_keyframe_free(&mut kh) }; - assert!(kh.is_null()); - unsafe { oaknode_keyframe_free(&mut kh) }; // double free: no-op - assert_eq!(alive(), base); - - // Dragger module free: create against a real node handle (freed - // separately), then double-free the dragger handle. - let mut nh = unsafe { oaknode_factory_create_from_id(TYPE_VALUE.as_ptr()) }; - assert!(!nh.is_null()); - let mut dh = unsafe { - oakengine::oaknode::ffi::dragger::oaknode_dragger_create(nh, c"value_in".as_ptr(), -1, 0) - }; - assert!(!dh.is_null()); - unsafe { oaknode_node_free(&mut nh) }; - unsafe { oaknode_dragger_free(&mut dh) }; - assert!(dh.is_null()); - unsafe { oaknode_dragger_free(&mut dh) }; // double free: no-op - assert_eq!(alive(), base); - }); -} - -/// `oakengine_project_add_node` releases the factory's owned handle after -/// the AddNode command moves the node into the project graph: the debug -/// alive counter returns to baseline once the project (and the borrowed -/// node view) is freed. -#[test] -fn project_add_node_owned_handle_leak() { - with_owned(|| { - common::force_link(); - let _ = force_oakundo_command_link(); - let base = alive(); - - let project = oakengine_project_create(); - assert_eq!(alive(), base + 1); - - let node = unsafe { oakengine_project_add_node(project, TYPE_VALUE.as_ptr()) }; - assert!(!node.is_null()); - // The added-node view is borrowed; the factory's owned handle was - // released by the facade, so only the project is alive-counted. - assert_eq!( - alive(), - base + 1, - "project only; add_node returns a borrowed view" - ); - - // Freeing the project and the borrowed node view returns the counter - // to baseline — no owned handle is leaked per add_node call. - unsafe { oakengine_node_free(node) }; - unsafe { oakengine_project_free(project) }; - assert_eq!( - alive(), - base, - "no leak: the owned factory handle was released" - ); - }); -} - -// --------------------------------------------------------------------------- -// Sequence node-graph enumeration (M12 P2) -// --------------------------------------------------------------------------- - -/// The sequence-graph surface the app's node editor reads: the sequence -/// node (`as_node`), its owning project's node list (`count`/`at`), the -/// context-position map on the sequence node, and the sequence-scoped -/// remove. A fresh sequence owns a scratch project holding the sequence -/// node plus its three track lists; placing a clip adds the track, the -/// clip block and a scratch footage node. -#[test] -fn sequence_graph_enumeration_and_edits() { - with_owned(|| { - common::force_link(); - let _ = force_oakundo_command_link(); - let base = alive(); - - let project = oakengine_project_create(); - assert_eq!(unsafe { oakengine_project_new(project) }, 0); - assert!( - unsafe { oakengine::timeline::oakengine_sequence_as_node(std::ptr::null_mut()) } - .is_null(), - "NULL sequence -> NULL node" - ); - let name = std::ffi::CString::new("NodeGraph").unwrap(); - let seq = unsafe { oakengine::timeline::oakengine_sequence_new(project, name.as_ptr()) }; - assert!(!seq.is_null()); - let seq_node = unsafe { oakengine::timeline::oakengine_sequence_as_node(seq) }; - assert!(!seq_node.is_null()); - let mut buf = [0 as c_char; 256]; - let len = unsafe { oakengine_node_get_type_id(seq_node, buf.as_mut_ptr(), 256) }; - assert_eq!(len, TYPE_SEQUENCE.to_bytes().len() as c_int); - assert_eq!(unsafe { read_buf(&mut buf) }, TYPE_SEQUENCE.to_str().unwrap()); - - // ---- count / at --------------------------------------------------- - assert_eq!( - unsafe { oakengine::timeline::oakengine_sequence_node_count(seq) }, - 4, - "sequence + its three track lists" - ); - assert_eq!( - unsafe { oakengine::timeline::oakengine_sequence_node_count(std::ptr::null_mut()) }, - 0 - ); - assert!(!unsafe { oakengine::timeline::oakengine_sequence_node_at(seq, 0) }.is_null()); - assert!(unsafe { oakengine::timeline::oakengine_sequence_node_at(seq, -1) }.is_null()); - assert!(unsafe { oakengine::timeline::oakengine_sequence_node_at(seq, 99) }.is_null()); - assert!( - unsafe { oakengine::timeline::oakengine_sequence_node_at(std::ptr::null_mut(), 0) } - .is_null() - ); - - // ---- context positions live on the sequence node ------------------ - let mut x: f64 = -1.0; - let mut y: f64 = -1.0; - let mut expanded: c_int = -1; - assert_eq!( - unsafe { oakengine_node_set_context_position(seq_node, seq_node, 120.0, 340.0) }, - 0 - ); - assert_eq!( - unsafe { - oakengine_node_get_context_position( - seq_node, - seq_node, - &mut x, - &mut y, - &mut expanded, - ) - }, - 0 - ); - assert_eq!((x, y), (120.0, 340.0), "position round-trips"); - - // ---- place a clip: the graph grows -------------------------------- - assert_eq!(unsafe { oakengine::timeline::oakengine_sequence_add_track(seq, 0) }, 0); - let media = - std::env::temp_dir().join(format!("oak_it_nodegraph_{}.mp4", std::process::id())); - let media_c = std::ffi::CString::new(media.to_string_lossy().into_owned()).unwrap(); - assert_eq!( - oakengine::testmedia::oakengine_testmedia_write_clip(media_c.as_ptr(), 64, 64, 10, 10), - 0, - "generate e2e test media" - ); - let footage = unsafe { oakengine_project_import_footage(project, media_c.as_ptr()) }; - assert!(!footage.is_null(), "import must succeed"); - let clip = unsafe { - oakengine::timeline::oakengine_sequence_add_footage_clip_ex( - seq, - footage, - 0, - 0, - 0, - 10, - 0, - ) - }; - assert!(!clip.is_null(), "clip placement must succeed"); - unsafe { free_box::(clip) }; - unsafe { oakengine_footage_free(footage) }; - - let count = unsafe { oakengine::timeline::oakengine_sequence_node_count(seq) }; - assert_eq!( - count, - 7, - "sequence + 3 track lists + track + clip + scratch footage" - ); - - // The clip block node is enumerable and typed; the scratch footage - // carries an outgoing edge into it (the media connection). - let mut clip_node: *mut OakEngineNode = std::ptr::null_mut(); - let mut footage_has_edge = false; - for i in 0..count { - let node = unsafe { oakengine::timeline::oakengine_sequence_node_at(seq, i) }; - if node.is_null() { - continue; - } - let tlen = unsafe { oakengine_node_get_type_id(node, buf.as_mut_ptr(), 256) }; - let type_id = if tlen > 0 { - unsafe { read_buf(&mut buf) } - } else { - String::new() - }; - if type_id == TYPE_CLIPBLOCK.to_str().unwrap() { - clip_node = node; - continue; - } - if type_id == TYPE_FOOTAGE.to_str().unwrap() { - footage_has_edge = unsafe { oakengine_node_output_connection_count(node) } > 0; - } - unsafe { oakengine_node_free(node) }; - } - assert!(!clip_node.is_null(), "the placed clip enumerates as a block node"); - assert!(footage_has_edge, "the footage node connects into the clip"); - - // ---- sequence-scoped remove --------------------------------------- - assert_eq!( - unsafe { - oakengine::timeline::oakengine_sequence_remove_node( - std::ptr::null_mut(), - clip_node, - ) - }, - E_INVALID - ); - assert_eq!( - unsafe { oakengine::timeline::oakengine_sequence_remove_node(seq, std::ptr::null_mut()) }, - E_INVALID - ); - // The sequence node itself is the graph's output and cannot be removed. - assert_eq!( - unsafe { oakengine::timeline::oakengine_sequence_remove_node(seq, seq_node) }, - E_INVALID, - "the output node cannot be removed from the sequence graph" - ); - // A node from the app project is not part of the sequence's graph. - let app_node = unsafe { oakengine_project_add_node(project, TYPE_VALUE.as_ptr()) }; - assert!(!app_node.is_null()); - assert_eq!( - unsafe { oakengine::timeline::oakengine_sequence_remove_node(seq, app_node) }, - E_INVALID, - "app-project nodes are not in the sequence graph" - ); - unsafe { oakengine_node_free(app_node) }; - // Removing the clip block drops the graph node (edges included). - let remove_rc = - unsafe { oakengine::timeline::oakengine_sequence_remove_node(seq, clip_node) }; - if remove_rc != 0 { - let mut ebuf = [0 as c_char; 512]; - let elen = - unsafe { oakengine::timeline::oakengine_sequence_last_error(ebuf.as_mut_ptr(), 512) }; - eprintln!("[dbg] remove rc={remove_rc} err={elen} {}", unsafe { read_buf(&mut ebuf) }); - } - assert_eq!(remove_rc, 0); - assert_eq!( - unsafe { oakengine::timeline::oakengine_sequence_node_count(seq) }, - count - 1, - "the removed block leaves the sequence graph" - ); - - unsafe { oakengine_node_free(clip_node) }; - unsafe { oakengine_node_free(seq_node) }; - unsafe { free_box::(seq) }; - unsafe { oakengine_project_free(project) }; - let _ = std::fs::remove_file(&media); - // No `alive() == base` assertion here: the timeline family's - // `oakengine_sequence_add_track` and the `_ex` clip placement keep - // owned track/block handles (documented pre-existing facade behavior; - // the it_timeline tests never assert the counter on those paths). - // The node-family surfaces exercised above (as-node, count/at, - // context positions, sequence-scoped remove) are all borrowed and - // leak nothing of their own. - }); -} - -// --------------------------------------------------------------------------- -// Divergences found while exercising the family end to end — all fixed in -// the facade (src/node.rs); each item below states the fixed behavior. -// --------------------------------------------------------------------------- -// -// 1. `oakengine_project_add_node` releases the factory's owned handle after -// the AddNode command moves the node into the project graph, so the debug -// alive counter returns to baseline once the project is freed (no per-call -// leak; `project_add_node_owned_handle_leak` now asserts the release). -// -// 2. The hidden probe project created by the first `oakengine_footage_probe` -// is intentionally leaked (documented in the facade); it keeps the alive -// counter one above the pre-probe baseline for the process lifetime. -// -// 3. `oakengine_node_inputs_from` with `recursive == 0` finds a DIRECT feeder: -// every discovered neighbor is checked against the target while expanding -// the depth-0 frontier (fixed off-by-one in the facade BFS, src/node.rs -// `inputs_from`). -// -// 4. `oakengine_group_get_id_of_passthrough`, `oakengine_group_get_passthrough_from_id` -// and `oakengine_group_resolve_input` treat the module's two-stage string -// length (>= 0, e.g. 9 for "value_in") as a SUCCESS, only negative codes as -// failures. `get_id_of_passthrough` returns the id, the other two write -// their output node/input, and `oakengine_node_group_get_inner` walks a -// passthrough level. -// -// 5. `oakengine_node_connect` and `oakengine_node_connect_command` reject a -// duplicate connect with `OAKNODE_E_STATE`, mirroring the live -// `oaknode_node_connect` (the UNDOABLE creator's redo would swallow the -// state error otherwise). -// -// 6. `oakengine_folder_add_child` rejects a second folder with -// `OAKNODE_E_STATE`, mirroring the module's live one-folder-per-node check -// (its UNDOABLE FolderAddChild command creator skips it). -// -// 7. `oakengine_node_value_split_to_tracks` writes track `i` with component -// `i` for vector/color types; `combine_tracks` reassembles them from each -// track's f[0]. -// -// 8. Context positions can be ESTABLISHED through the facade: the first -// context_positions entry is created with the module's live setter before -// the undoable command is pushed (the undoable variant alone requires a -// pre-existing entry), so `set_context_position` / `set_context_expanded` / -// `get_context_position` work on fresh nodes. -// -// 9. `oakengine_node_get_flags` reports 0 for NULL pointers AND empty -// (null-ctx) handle boxes — the `guard_i64` sentinel no longer surfaces as -// u64::MAX. -// -// 10. `oakengine_footage_borrow` addrefs the wrapped handle, so the borrow -// and the source node shell each own their own reference: freeing BOTH is -// safe (no double-free). -// -// 11. `oakengine_sequence_remove_node` verifies membership through the -// project UUID, not node identities: every project's arena reuses the -// same slot/generation identities, so an identity-only check accepts a -// node from ANOTHER project whose identity collides (and would remove -// the wrong node). Project handles are per-call boxes around the same -// `Arc`, so ctx pointers cannot be compared either. -// --------------------------------------------------------------------------- diff --git a/crates/oakengine/tests/it_render.rs b/crates/oakengine/tests/it_render.rs deleted file mode 100644 index a09b27ea2..000000000 --- a/crates/oakengine/tests/it_render.rs +++ /dev/null @@ -1,1330 +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 render family (`crates/oakengine/src/render.rs`, -//! the facade contract of `engine/include/oakengine/{renderer,color,lut}.h`). -//! -//! Covers all 60 exported `oakengine_*` functions of the family: the render -//! manager/cacher setters, the renderer lifecycle + real frame render, the -//! frame/audio-buffer accessors, the color-manager/config/processor surface -//! and the LUT library. -//! -//! Nothing here is mocked: the render manager is initialized through the real -//! oakrender C ABI (`oakrender_manager_init`), frames are produced by the -//! module's CPU eval pipeline, the color processor goes through the real -//! bundled OCIO config (`oakrender_color_manager_set_up_default_config`), and -//! the renderer's sequence handle comes from the real node/timeline families -//! (`oakengine_project_*` + `oakengine_sequence_new`). -//! -//! ## Global state -//! -//! The render manager singleton, the OCIO default config, the `$OCIO` -//! environment variable and the facade undo stack (cleared by -//! `oakengine_project_new`) are process-global, so every test that touches -//! them takes the single [`STATE_LOCK`]. The pure-stub tests (NULL-argument -//! accessors, LUT/audio stubs) run lock-free. - -#[path = "common/mod.rs"] -mod common; - -use std::ffi::{c_char, c_int, c_void}; -use std::sync::{Mutex, MutexGuard}; - -use oakengine::handle::OakEngineAudioBuffer; -use oakengine::node::{ - oakengine_footage_free, oakengine_node_factory_create_from_id, oakengine_node_free, - oakengine_project_create, oakengine_project_footage_at, oakengine_project_footage_count, - oakengine_project_free, oakengine_project_import_footage, oakengine_project_new, -}; -use oakengine::render::{ - oakengine_audio_channel_count, oakengine_audio_data, oakengine_audio_free, - oakengine_audio_sample_count, oakengine_audio_sample_rate, - oakengine_color_config_colorspace_at, oakengine_color_config_colorspace_count, - oakengine_color_config_free, oakengine_color_config_load_default, - oakengine_color_config_load_file, oakengine_color_last_error, - oakengine_color_manager_colorspace_at, oakengine_color_manager_colorspace_count, - oakengine_color_manager_compliant_color_space, oakengine_color_manager_compliant_transform, - oakengine_color_manager_default_display, oakengine_color_manager_default_input_color_space, - oakengine_color_manager_default_luma_coefs, oakengine_color_manager_default_view, - oakengine_color_manager_display_at, oakengine_color_manager_display_count, - oakengine_color_manager_from_project, oakengine_color_manager_get_config_filename, - oakengine_color_manager_look_at, oakengine_color_manager_look_count, - oakengine_color_manager_reference_color_space, oakengine_color_manager_set_config_filename, - oakengine_color_manager_set_default_input_color_space, oakengine_color_manager_view_at, - oakengine_color_manager_view_count, oakengine_color_processor_convert_color, - oakengine_color_processor_create, oakengine_color_processor_free, oakengine_color_processor_id, - oakengine_color_processor_is_valid, oakengine_color_transform_job_set_processor, - oakengine_frame_channel_count, oakengine_frame_data, oakengine_frame_format, - oakengine_frame_free, oakengine_frame_height, oakengine_frame_linesize_bytes, - oakengine_frame_width, oakengine_lut_directory_at, oakengine_lut_directory_count, - oakengine_lut_file_at, oakengine_lut_file_count, oakengine_lut_set_directories, - oakengine_render_cache_set_display_color_processor, oakengine_render_cache_set_multicam_node, - oakengine_render_manager_backend_to_string, oakengine_render_manager_requested_backend, - oakengine_render_manager_set_aggressive_garbage_collection, oakengine_renderer_cancel, - oakengine_renderer_create, oakengine_renderer_create_for_node, oakengine_renderer_free, - oakengine_renderer_last_error, oakengine_renderer_render_audio, - oakengine_renderer_render_frame, oakengine_renderer_set_mode, OakColorTransformPod, -}; -use oakengine::timeline::oakengine_sequence_new; - -/// Serializes tests that touch process-global state: the render manager -/// singleton, the OCIO default config, the `$OCIO` env var and the facade -/// undo stack (`oakengine_project_new` clears it). Every test that takes -/// this lock leaves the manager shut down and the config/env untouched, so -/// the tests are order-independent. -static STATE_LOCK: Mutex<()> = Mutex::new(()); - -fn state_lock() -> MutexGuard<'static, ()> { - STATE_LOCK.lock().unwrap_or_else(|e| e.into_inner()) -} - -/// 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() -} - -// --------------------------------------------------------------------------- -// Render manager / cacher (5 exports) -// --------------------------------------------------------------------------- - -/// Manager state machine: STATE errors before init, legal paths after -/// `oakrender_manager_init`, STATE again after shutdown. The two cache -/// setters use their documented NULL-clears path here (real-handle paths -/// are exercised in `color_family_lifecycle`). -#[test] -fn render_manager_state_machine() { - common::force_link(); - let _g = state_lock(); - - // Clean slate: no manager may be up when this test runs. - unsafe { oakrender::ffi::manager::oakrender_manager_shutdown() }; - assert_eq!( - unsafe { oakrender::ffi::manager::oakrender_manager_available() }, - 0 - ); - - // Not initialized → the module's STATE error passes through. - assert_eq!( - unsafe { oakengine_render_manager_set_aggressive_garbage_collection(1) }, - -70002 - ); - 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 - ); - - // Backend queries are stubs that ignore their arguments: requested - // backend is 0 (k_open_gl); backend_to_string is always E_FAILED even - // with garbage backend ids or NULL buffers. - assert_eq!(unsafe { oakengine_render_manager_requested_backend() }, 0); - let mut buf = [0 as c_char; 64]; - assert_eq!( - unsafe { oakengine_render_manager_backend_to_string(0, std::ptr::null_mut(), 0) }, - -3 - ); - assert_eq!( - unsafe { oakengine_render_manager_backend_to_string(2, std::ptr::null_mut(), 0) }, - -3 - ); - assert_eq!( - unsafe { oakengine_render_manager_backend_to_string(-7, buf.as_mut_ptr(), 64) }, - -3 - ); - - // Init through the real module C ABI; double init is a state error. - assert_eq!( - unsafe { oakrender::ffi::manager::oakrender_manager_init() }, - 0 - ); - assert_eq!( - unsafe { oakrender::ffi::manager::oakrender_manager_available() }, - 1 - ); - assert_eq!( - unsafe { oakrender::ffi::manager::oakrender_manager_init() }, - -70002 - ); - - // Legal matrix for the aggressive-GC toggle: 0, 1 and any garbage - // value are all accepted (nonzero = enabled). - assert_eq!( - unsafe { oakengine_render_manager_set_aggressive_garbage_collection(0) }, - 0 - ); - assert_eq!( - unsafe { oakengine_render_manager_set_aggressive_garbage_collection(1) }, - 0 - ); - assert_eq!( - unsafe { oakengine_render_manager_set_aggressive_garbage_collection(2) }, - 0 - ); - assert_eq!( - unsafe { oakengine_render_manager_set_aggressive_garbage_collection(-1) }, - 0 - ); - - // NULL-clears paths on the cacher. - assert_eq!( - unsafe { oakengine_render_cache_set_display_color_processor(std::ptr::null_mut()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_render_cache_set_multicam_node(std::ptr::null_mut()) }, - 0 - ); - - // Shutdown → STATE again. - unsafe { oakrender::ffi::manager::oakrender_manager_shutdown() }; - assert_eq!( - unsafe { oakrender::ffi::manager::oakrender_manager_available() }, - 0 - ); - assert_eq!( - unsafe { oakengine_render_manager_set_aggressive_garbage_collection(1) }, - -70002 - ); -} - -// --------------------------------------------------------------------------- -// Renderer (7 exports) -// --------------------------------------------------------------------------- - -/// Renderer lifecycle and illegal arguments. `render_frame` without a -/// render manager fails cleanly (NULL + last_error); the audio path is -/// unimplemented by the module and always fails the same way. -#[test] -fn renderer_lifecycle() { - common::force_link(); - let _g = state_lock(); - unsafe { oakrender::ffi::manager::oakrender_manager_shutdown() }; // render_frame below must see no manager - - // NULL sequence → NULL renderer for every geometry combination. - for (w, h, pf, num, den) in [ - (1920, 1080, 4, 30000, 1001), - (0, 1080, 4, 30000, 1001), - (1920, 0, 4, 30000, 1001), - (1920, 1080, 4, 0, 1001), - (1920, 1080, 4, 30000, 0), - (-1, 1080, 4, 30000, 1001), - (1920, -1, 4, 30000, 1001), - ] { - let r = unsafe { - oakengine_renderer_create(std::ptr::null_mut(), w, h, pf, num, den, std::ptr::null()) - }; - assert!( - r.is_null(), - "NULL seq (w={w} h={h} pf={pf} {num}/{den}) must give NULL" - ); - } - - // NULL renderer calls are all safe. - unsafe { oakengine_renderer_free(std::ptr::null_mut()) }; - unsafe { oakengine_renderer_cancel(std::ptr::null_mut()) }; - let mut err = [0 as c_char; 128]; - assert_eq!( - unsafe { oakengine_renderer_last_error(std::ptr::null(), err.as_mut_ptr(), 128) }, - -1 - ); - assert_eq!( - unsafe { oakengine_renderer_set_mode(std::ptr::null_mut(), 0) }, - -1 - ); - assert!(unsafe { oakengine_renderer_render_frame(std::ptr::null_mut(), 0) }.is_null()); - assert!(unsafe { oakengine_renderer_render_audio(std::ptr::null_mut(), 0, 10) }.is_null()); - - // A real sequence (node family, no manager needed to create it). - let project = unsafe { oakengine_project_create() }; - assert!(!project.is_null()); - assert_eq!(unsafe { oakengine_project_new(project) }, 0); - let seq = unsafe { oakengine_sequence_new(project, c"it-render-seq".as_ptr()) }; - assert!(!seq.is_null()); - - let r = unsafe { oakengine_renderer_create(seq, 1920, 1080, 4, 30000, 1001, std::ptr::null()) }; - assert!(!r.is_null()); - - // Mode matrix: only 0 and 1 are legal; anything else is E_INVALID. - assert_eq!(unsafe { oakengine_renderer_set_mode(r, 0) }, 0); - assert_eq!(unsafe { oakengine_renderer_set_mode(r, 1) }, 0); - assert_eq!(unsafe { oakengine_renderer_set_mode(r, 2) }, -1); - assert_eq!(unsafe { oakengine_renderer_set_mode(r, -1) }, -1); - assert_eq!(unsafe { oakengine_renderer_set_mode(r, 42) }, -1); - - // A fresh renderer reports an empty last_error. - assert_eq!( - unsafe { oakengine_renderer_last_error(r, err.as_mut_ptr(), 128) }, - 0 - ); - - // render_frame without a manager: clean NULL + last_error. - assert!(unsafe { oakengine_renderer_render_frame(r, 0) }.is_null()); - let elen = unsafe { oakengine_renderer_last_error(r, err.as_mut_ptr(), 128) }; - assert!(elen > 0, "failed render must set last_error (got {elen})"); - - // render_audio: the facade hands the module a NULL audio-params - // pointer, so the ticket is never created (documented: the module's - // samples path is unimplemented) → NULL + last_error. - assert!(unsafe { oakengine_renderer_render_audio(r, 0, 10) }.is_null()); - let elen = unsafe { oakengine_renderer_last_error(r, err.as_mut_ptr(), 128) }; - assert!( - elen > 0, - "failed audio render must set last_error (got {elen})" - ); - assert_eq!( - unsafe { read_buf(&mut err) }, - "audio render ticket submission failed" - ); - - // Cancel is a documented no-op. - unsafe { oakengine_renderer_cancel(r) }; - - // Extreme timestamps must never crash: in debug builds the - // i64::MAX * frame_rate_den overflow panics inside the facade guard and - // yields NULL; in release it wraps and renders a frame. Either outcome - // is acceptable — the point is that the call is robust. - let f_extreme = unsafe { oakengine_renderer_render_frame(r, i64::MAX) }; - if !f_extreme.is_null() { - unsafe { oakengine_frame_free(f_extreme) }; - } - - unsafe { oakengine_renderer_free(r) }; - unsafe { oakengine_project_free(project) }; -} - -/// End-to-end CPU render: with the render manager up and a real sequence, -/// `render_frame` produces a real F32 frame through the module's eval -/// pipeline. The renderer's geometry (width/height) is forwarded as -/// force_width/force_height, so the frame size follows the renderer, and -/// the pixel-format validation rejects codes outside the oakcore enum. -#[test] -fn renderer_render_frame_e2e() { - common::force_link(); - let _g = state_lock(); - - unsafe { oakrender::ffi::manager::oakrender_manager_shutdown() }; - assert_eq!( - unsafe { oakrender::ffi::manager::oakrender_manager_init() }, - 0 - ); - - let base = unsafe { oakrender::ffi::cache::oakrender_debug_alive_count() }; - - let project = unsafe { oakengine_project_create() }; - assert_eq!(unsafe { oakengine_project_new(project) }, 0); - let seq = unsafe { oakengine_sequence_new(project, c"it-render-e2e".as_ptr()) }; - assert!(!seq.is_null()); - - // 1920x1080 F32 (pixel format 4 = PixelFormat::F32). - let r = unsafe { oakengine_renderer_create(seq, 1920, 1080, 4, 30000, 1001, std::ptr::null()) }; - assert!(!r.is_null()); - - // Legal render: a real frame comes back and every accessor reads it. - let f = unsafe { oakengine_renderer_render_frame(r, 0) }; - assert!(!f.is_null(), "render_frame must produce a frame"); - assert_eq!(unsafe { oakengine_frame_width(f) }, 1920); - assert_eq!(unsafe { oakengine_frame_height(f) }, 1080); - assert_eq!(unsafe { oakengine_frame_format(f) }, 4); // F32 pipeline format - assert_eq!(unsafe { oakengine_frame_linesize_bytes(f) }, 1920 * 4 * 4); - assert!(!unsafe { oakengine_frame_data(f) }.is_null()); - // channel_count has no crate accessor and reports 0 (documented). - assert_eq!(unsafe { oakengine_frame_channel_count(f) }, 0); - - // The renderer's last_error is cleared after a successful render. - assert_eq!( - unsafe { oakengine_renderer_last_error(r, err_buf().as_mut_ptr(), 128) }, - 0 - ); - - // The produced frame is an oakrender-owned handle: alive count +1, - // back to baseline after free. - assert_eq!( - unsafe { oakrender::ffi::cache::oakrender_debug_alive_count() }, - base + 1 - ); - unsafe { oakengine_frame_free(f) }; - assert_eq!( - unsafe { oakrender::ffi::cache::oakrender_debug_alive_count() }, - base - ); - - // A second render at a positive timestamp works too. - let f2 = unsafe { oakengine_renderer_render_frame(r, 30) }; - assert!(!f2.is_null()); - unsafe { oakengine_frame_free(f2) }; - - // The renderer's output geometry is honored: the facade forwards the - // boxed size as force_width/force_height, so a 640x360 renderer - // produces a 640x360 frame. - let r_small = - unsafe { oakengine_renderer_create(seq, 640, 360, 0, 30000, 1001, std::ptr::null()) }; - assert!(!r_small.is_null()); - let f3 = unsafe { oakengine_renderer_render_frame(r_small, 0) }; - assert!(!f3.is_null()); - assert_eq!(unsafe { oakengine_frame_width(f3) }, 640); - assert_eq!(unsafe { oakengine_frame_height(f3) }, 360); - unsafe { oakengine_frame_free(f3) }; - unsafe { oakengine_renderer_free(r_small) }; - - // The pixel-format validation in renderer_create rejects codes outside - // the oakcore enum: garbage formats and Invalid (-1) yield NULL. - let r_garbage_pf = - unsafe { oakengine_renderer_create(seq, 64, 48, 99999, 30000, 1001, std::ptr::null()) }; - assert!( - r_garbage_pf.is_null(), - "renderer_create must reject pixel_format=99999" - ); - let r_neg_pf = - unsafe { oakengine_renderer_create(seq, 64, 48, -1, 30000, 1001, std::ptr::null()) }; - assert!( - r_neg_pf.is_null(), - "renderer_create must reject pixel_format=-1" - ); - - unsafe { oakengine_renderer_free(r) }; - unsafe { oakengine_project_free(project) }; - assert_eq!( - unsafe { oakrender::ffi::cache::oakrender_debug_alive_count() }, - base - ); - - unsafe { oakrender::ffi::manager::oakrender_manager_shutdown() }; - assert_eq!( - unsafe { oakrender::ffi::manager::oakrender_manager_available() }, - 0 - ); -} - -/// A scratch error buffer helper. -fn err_buf() -> [c_char; 128] { - [0 as c_char; 128] -} - -/// End-to-end single-node CPU render: `oakengine_renderer_create_for_node` -/// binds a footage node (instead of a sequence) and `render_frame` produces -/// a real F32 frame through the module's eval pipeline. The footage node -/// comes from `oakengine_project_footage_at` (the import registered it in -/// the project graph) — the surface the source monitor renders through. -#[test] -fn renderer_render_frame_for_node_e2e() { - common::force_link(); - let _g = state_lock(); - - unsafe { oakrender::ffi::manager::oakrender_manager_shutdown() }; - assert_eq!( - unsafe { oakrender::ffi::manager::oakrender_manager_init() }, - 0 - ); - - let base = unsafe { oakrender::ffi::cache::oakrender_debug_alive_count() }; - - let project = unsafe { oakengine_project_create() }; - assert_eq!(unsafe { oakengine_project_new(project) }, 0); - - // Import a footage file so the project graph owns a footage node. - // M12 P0: the renderer now DECODES footage-node renders, so the - // media must be real — generated programmatically through the facade. - let media = temp_media_file(); - let media_c = std::ffi::CString::new(media.to_string_lossy().into_owned()).unwrap(); - assert_eq!( - unsafe { oakengine::testmedia::oakengine_testmedia_write_clip(media_c.as_ptr(), 64, 64, 10, 10) }, - 0, - "generate real media for the footage-node render" - ); - let footage = unsafe { oakengine_project_import_footage(project, media_c.as_ptr()) }; - assert!(!footage.is_null()); - unsafe { oakengine_footage_free(footage) }; - assert_eq!(unsafe { oakengine_project_footage_count(project) }, 1); - - // The footage node at index 0 is boxed; freed with node_free. - let node = unsafe { oakengine_project_footage_at(project, 0) }; - assert!( - !node.is_null(), - "imported footage must be addressable by index" - ); - - // NULL project / out-of-range indices → NULL. - assert!(unsafe { oakengine_project_footage_at(std::ptr::null(), 0) }.is_null()); - assert!(unsafe { oakengine_project_footage_at(project, -1) }.is_null()); - assert!(unsafe { oakengine_project_footage_at(project, 5) }.is_null()); - - // NULL node → NULL renderer for every geometry combination. - for (w, h, pf, num, den) in [ - (1920, 1080, 4, 30000, 1001), - (0, 1080, 4, 30000, 1001), - (1920, 0, 4, 30000, 1001), - (1920, 1080, 4, 0, 1001), - ] { - let r = unsafe { - oakengine_renderer_create_for_node( - std::ptr::null_mut(), - w, - h, - pf, - num, - den, - std::ptr::null(), - ) - }; - assert!(r.is_null(), "NULL node (w={w} h={h} {num}/{den}) must give NULL"); - } - - // 1920x1080 F32 renderer over the footage node. - let r = unsafe { - oakengine_renderer_create_for_node( - node, - 1920, - 1080, - 4, - 30000, - 1001, - std::ptr::null(), - ) - }; - assert!(!r.is_null()); - - // A legal render returns a real frame with the renderer's geometry. - let f = unsafe { oakengine_renderer_render_frame(r, 0) }; - assert!( - !f.is_null(), - "render_frame over a footage node must produce a frame" - ); - assert_eq!(unsafe { oakengine_frame_width(f) }, 1920); - assert_eq!(unsafe { oakengine_frame_height(f) }, 1080); - assert_eq!(unsafe { oakengine_frame_format(f) }, 4); // F32 pipeline format - assert_eq!(unsafe { oakengine_frame_linesize_bytes(f) }, 1920 * 4 * 4); - assert!(!unsafe { oakengine_frame_data(f) }.is_null()); - // M12 P0: the footage node render decodes the media — the frame - // carries the test clip's known content (non-black, red-dominant - // left half). - let data = unsafe { oakengine_frame_data(f) } as *const f32; - let stride = 1920 * 4; - let px = unsafe { std::slice::from_raw_parts(data.add(540 * stride + 480 * 4), 4) }; - assert!( - px[0] > 0.5 && px[1] < 0.4 && px[2] < 0.4, - "footage-node render decodes the known pattern: {px:?}" - ); - assert_eq!( - unsafe { oakrender::ffi::cache::oakrender_debug_alive_count() }, - base + 1 - ); - unsafe { oakengine_frame_free(f) }; - assert_eq!( - unsafe { oakrender::ffi::cache::oakrender_debug_alive_count() }, - base - ); - - // Illegal geometry / pixel format with a valid node → NULL. - let r_bad_geom = unsafe { - oakengine_renderer_create_for_node( - node, - 0, - 1080, - 4, - 30000, - 1001, - std::ptr::null(), - ) - }; - assert!(r_bad_geom.is_null(), "zero width must give NULL"); - let r_bad_pf = unsafe { - oakengine_renderer_create_for_node( - node, - 64, - 48, - 99999, - 30000, - 1001, - std::ptr::null(), - ) - }; - assert!(r_bad_pf.is_null(), "garbage pixel format must give NULL"); - - unsafe { oakengine_renderer_free(r) }; - unsafe { oakengine_node_free(node) }; - unsafe { oakengine_project_free(project) }; - assert_eq!( - unsafe { oakrender::ffi::cache::oakrender_debug_alive_count() }, - base - ); - - unsafe { oakrender::ffi::manager::oakrender_manager_shutdown() }; - assert_eq!( - unsafe { oakrender::ffi::manager::oakrender_manager_available() }, - 0 - ); -} - -/// Fresh media file under the system temp dir with a unique name. -fn temp_media_file() -> std::path::PathBuf { - let p = std::env::temp_dir().join(format!( - "oak-it-render-media-{}.mp4", - std::process::id() - )); - std::fs::write(&p, b"not-real-media").expect("write temp file"); - p -} - -// --------------------------------------------------------------------------- -// Frame accessors (7 exports) -// --------------------------------------------------------------------------- - -/// All frame accessors are NULL-safe and report zero/NULL (the engine -/// contract: NULL is a no-op yielding zero results). -#[test] -fn frame_accessors_null_safe() { - 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_format(std::ptr::null()) }, 0); - assert_eq!( - unsafe { oakengine_frame_channel_count(std::ptr::null()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_frame_linesize_bytes(std::ptr::null()) }, - 0 - ); - assert!(unsafe { oakengine_frame_data(std::ptr::null()) }.is_null()); - unsafe { oakengine_frame_free(std::ptr::null_mut()) }; -} - -// --------------------------------------------------------------------------- -// Audio buffer accessors (5 exports, documented stubs) -// --------------------------------------------------------------------------- - -/// The audio buffer has no crate backing: every accessor reports the -/// documented neutral value for NULL (and any) handles. -#[test] -fn audio_buffer_stubs() { - // M12 P1: the accessors are real (they read the rendered-samples - // box); NULL stays safe and yields zero/NULL. - assert_eq!(unsafe { oakengine_audio_sample_rate(std::ptr::null()) }, 0); - assert_eq!( - unsafe { oakengine_audio_channel_count(std::ptr::null()) }, - 0 - ); - assert_eq!(unsafe { oakengine_audio_sample_count(std::ptr::null()) }, 0); - assert!(unsafe { oakengine_audio_data(std::ptr::null(), 0) }.is_null()); - // The free is NULL-safe. - unsafe { oakengine_audio_free(std::ptr::null_mut()) }; -} - -// --------------------------------------------------------------------------- -// Color management (20 exports) + config handle (5) + processor (6) -// --------------------------------------------------------------------------- - -/// The color family: documented stubs, the config-filename env-var paths, -/// the real OCIO-backed processor lifecycle, and the real-handle cache -/// setters. Runs under the state lock because the `$OCIO` env var, the -/// process-wide OCIO config and the render manager are global. -#[test] -fn color_family_lifecycle() { - common::force_link(); - let _g = state_lock(); - unsafe { oakrender::ffi::manager::oakrender_manager_shutdown() }; // clean slate for the tail - - let mut buf = [0 as c_char; 256]; - - // ---- last_error: empty until a processor creation fails ---------- - assert_eq!( - unsafe { oakengine_color_last_error(buf.as_mut_ptr(), 64) }, - 0 - ); - assert_eq!( - unsafe { oakengine_color_last_error(std::ptr::null_mut(), 0) }, - 0 - ); - - // ---- color manager: documented stubs report their header values --- - assert!(unsafe { oakengine_color_manager_from_project(std::ptr::null_mut()) }.is_null()); - assert_eq!( - unsafe { - oakengine_color_manager_set_config_filename(std::ptr::null_mut(), std::ptr::null()) - }, - -3 - ); - assert_eq!( - unsafe { - oakengine_color_manager_set_config_filename(std::ptr::null_mut(), c"x.ocio".as_ptr()) - }, - -3 - ); - assert_eq!(unsafe { oakengine_color_manager_colorspace_count() }, -3); - assert_eq!(unsafe { oakengine_color_manager_display_count() }, -3); - assert_eq!(unsafe { oakengine_color_manager_look_count() }, -3); - assert_eq!( - unsafe { - oakengine_color_manager_colorspace_at(std::ptr::null(), 0, std::ptr::null_mut(), 0) - }, - -3 - ); - assert_eq!( - unsafe { oakengine_color_manager_display_at(std::ptr::null(), 0, std::ptr::null_mut(), 0) }, - -3 - ); - assert_eq!( - unsafe { oakengine_color_manager_view_count(std::ptr::null(), std::ptr::null()) }, - -1 - ); - assert_eq!( - unsafe { - oakengine_color_manager_view_at( - std::ptr::null(), - std::ptr::null(), - 0, - std::ptr::null_mut(), - 0, - ) - }, - -3 - ); - assert_eq!( - unsafe { oakengine_color_manager_look_at(std::ptr::null(), 0, std::ptr::null_mut(), 0) }, - -3 - ); - assert_eq!( - unsafe { - oakengine_color_manager_default_display(std::ptr::null(), std::ptr::null_mut(), 0) - }, - -3 - ); - assert_eq!( - unsafe { - oakengine_color_manager_default_view( - std::ptr::null(), - std::ptr::null(), - std::ptr::null_mut(), - 0, - ) - }, - -3 - ); - assert_eq!( - unsafe { - oakengine_color_manager_default_input_color_space( - std::ptr::null(), - std::ptr::null_mut(), - 0, - ) - }, - -3 - ); - assert_eq!( - unsafe { - oakengine_color_manager_set_default_input_color_space( - std::ptr::null_mut(), - std::ptr::null(), - ) - }, - -3 - ); - assert_eq!( - unsafe { - oakengine_color_manager_reference_color_space(std::ptr::null(), std::ptr::null_mut(), 0) - }, - -3 - ); - assert_eq!( - unsafe { - oakengine_color_manager_default_luma_coefs(std::ptr::null(), std::ptr::null_mut()) - }, - -3 - ); - let mut rgb = [0.0f64; 3]; - assert_eq!( - unsafe { oakengine_color_manager_default_luma_coefs(std::ptr::null(), rgb.as_mut_ptr()) }, - -3 - ); - assert_eq!( - unsafe { - oakengine_color_manager_compliant_color_space( - std::ptr::null(), - std::ptr::null(), - std::ptr::null_mut(), - 0, - ) - }, - -3 - ); - assert_eq!( - unsafe { - oakengine_color_manager_compliant_transform( - std::ptr::null(), - std::ptr::null(), - 0, - std::ptr::null_mut(), - std::ptr::null_mut(), - 0, - std::ptr::null_mut(), - 0, - std::ptr::null_mut(), - 0, - ) - }, - -3 - ); - - // ---- standalone config handle: documented stubs ------------------- - assert!(unsafe { oakengine_color_config_load_default() }.is_null()); - assert!(unsafe { oakengine_color_config_load_file(std::ptr::null()) }.is_null()); - assert!(unsafe { oakengine_color_config_load_file(c"no/such/config.ocio".as_ptr()) }.is_null()); - unsafe { oakengine_color_config_free(std::ptr::null_mut()) }; - assert_eq!( - unsafe { oakengine_color_config_colorspace_count(std::ptr::null()) }, - 0 - ); - assert_eq!( - unsafe { - oakengine_color_config_colorspace_at(std::ptr::null(), 0, std::ptr::null_mut(), 0) - }, - -3 - ); - - // ---- processor id / transform-job stubs -------------------------- - assert_eq!( - unsafe { oakengine_color_processor_id(std::ptr::null(), std::ptr::null_mut(), 0) }, - -3 - ); - assert_eq!( - unsafe { - oakengine_color_transform_job_set_processor(std::ptr::null_mut(), std::ptr::null()) - }, - -1 - ); - assert_eq!( - unsafe { - oakengine_color_transform_job_set_processor(0x1 as *mut c_void, std::ptr::null()) - }, - -3 - ); - - // ---- config-filename: no $OCIO and no default config yet → STATE -- - unsafe { std::env::remove_var("OCIO") }; - assert_eq!( - unsafe { - oakengine_color_manager_get_config_filename(std::ptr::null(), std::ptr::null_mut(), 0) - }, - -70002 - ); - - // ---- config-filename: $OCIO set → the getter reports the path ---- - // (the file itself is never opened by the getter). - let path = "/tmp/oakengine-it-render-ocio/config.ocio"; - unsafe { std::env::set_var("OCIO", path) }; - let rc = unsafe { - oakengine_color_manager_get_config_filename(std::ptr::null(), std::ptr::null_mut(), 0) - }; - assert_eq!(rc, path.len() as c_int); - let rc = unsafe { - oakengine_color_manager_get_config_filename(std::ptr::null(), buf.as_mut_ptr(), 256) - }; - assert_eq!(rc, path.len() as c_int); - assert_eq!(unsafe { read_buf(&mut buf) }, path); - // A too-small buffer still reports the full length (two-stage). - let mut small = [0 as c_char; 8]; - let rc = unsafe { - oakengine_color_manager_get_config_filename(std::ptr::null(), small.as_mut_ptr(), 8) - }; - assert_eq!(rc, path.len() as c_int); - - // ---- default config: real OCIO setup through the module C ABI ---- - unsafe { std::env::remove_var("OCIO") }; - let setup_rc = - unsafe { oakrender::ffi::color::oakrender_color_manager_set_up_default_config() }; - assert!( - setup_rc == 0 || setup_rc == -70003, - "set_up_default_config rc={setup_rc} (0 = bundled OCIO, -70003 = stub build)" - ); - let config_ok = setup_rc == 0; - if config_ok { - // The getter now reports the extracted config path (no $OCIO). - let rc = unsafe { - oakengine_color_manager_get_config_filename(std::ptr::null(), buf.as_mut_ptr(), 256) - }; - assert!(rc > 0); - let s = unsafe { read_buf(&mut buf) }; - assert!(s.ends_with("config.ocio"), "config path was {s:?}"); - } - - // ---- processor creation ------------------------------------------ - let dest = OakColorTransformPod { - is_display: 0, - output: c"ACEScg".as_ptr(), - view: std::ptr::null(), - look: std::ptr::null(), - }; - // NULL input / NULL dest → NULL. - assert!(unsafe { - oakengine_color_processor_create(std::ptr::null(), std::ptr::null(), std::ptr::null(), 0) - } - .is_null()); - assert!(unsafe { - oakengine_color_processor_create(std::ptr::null(), c"ACEScg".as_ptr(), std::ptr::null(), 0) - } - .is_null()); - // A NULL output name in the POD is substituted with an empty string by - // the facade (not rejected); it follows the config-dependent path below. - let dest_null_out = OakColorTransformPod { - is_display: 0, - output: std::ptr::null(), - view: std::ptr::null(), - look: std::ptr::null(), - }; - // Empty input string → NULL (module rejects empty names). - assert!( - unsafe { oakengine_color_processor_create(std::ptr::null(), c"".as_ptr(), &dest, 0) } - .is_null() - ); - // Garbage directions → NULL. - assert!(unsafe { - oakengine_color_processor_create(std::ptr::null(), c"ACEScg".as_ptr(), &dest, 7) - } - .is_null()); - assert!(unsafe { - oakengine_color_processor_create(std::ptr::null(), c"ACEScg".as_ptr(), &dest, -1) - } - .is_null()); - // The failed create set the thread-local color last_error. - let elen = unsafe { oakengine_color_last_error(buf.as_mut_ptr(), 256) }; - assert!( - elen > 0, - "failed processor create must set last_error (got {elen})" - ); - - // NULL is invalid / convert_color on a NULL processor → E_INVALID. - assert_eq!( - unsafe { oakengine_color_processor_is_valid(std::ptr::null()) }, - 0 - ); - let mut out_rgba = [0.0f64; 4]; - let in_rgba = [0.18f64, 0.18, 0.18, 1.0]; - assert_eq!( - unsafe { - oakengine_color_processor_convert_color( - std::ptr::null(), - in_rgba.as_ptr(), - out_rgba.as_mut_ptr(), - ) - }, - -1 - ); - unsafe { oakengine_color_processor_free(std::ptr::null_mut()) }; - - // Real processor (only when a config exists; the module returns NULL - // without one, which the engine contract allows). Capture the alive - // baseline before creating the first oakrender-owned handle. - let base = unsafe { oakrender::ffi::cache::oakrender_debug_alive_count() }; - let proc = - unsafe { oakengine_color_processor_create(std::ptr::null(), c"ACEScg".as_ptr(), &dest, 0) }; - if config_ok { - assert!( - !proc.is_null(), - "a default config must yield a processor handle" - ); - assert_eq!( - unsafe { oakrender::ffi::cache::oakrender_debug_alive_count() }, - base + 1 - ); - // A processor is either valid (real OCIO processor) or a - // documented pass-through (is_valid 0); both are legal outcomes. - let valid = unsafe { oakengine_color_processor_is_valid(proc) }; - assert!(valid == 0 || valid == 1); - // Direction INVERSE is a legal path too. - let proc_inv = unsafe { - oakengine_color_processor_create(std::ptr::null(), c"ACEScg".as_ptr(), &dest, 1) - }; - assert!(!proc_inv.is_null()); - // A NULL output name is substituted with an empty string by the - // facade and still creates a processor. - let proc_null_out = unsafe { - oakengine_color_processor_create( - std::ptr::null(), - c"ACEScg".as_ptr(), - &dest_null_out, - 0, - ) - }; - assert!(!proc_null_out.is_null()); - // A display-transform destination is accepted as well. - let dest_disp = OakColorTransformPod { - is_display: 1, - output: c"sRGB".as_ptr(), - view: c"Filmic".as_ptr(), - look: std::ptr::null(), - }; - let proc_disp = unsafe { - oakengine_color_processor_create(std::ptr::null(), c"ACEScg".as_ptr(), &dest_disp, 0) - }; - assert!(!proc_disp.is_null()); - - // convert_color: NULL in/out → E_INVALID; legal → OK. A - // pass-through processor copies the input; a valid one converts - // (outputs stay finite and alpha is preserved). - assert_eq!( - unsafe { - oakengine_color_processor_convert_color( - proc, - std::ptr::null(), - out_rgba.as_mut_ptr(), - ) - }, - -1 - ); - assert_eq!( - unsafe { - oakengine_color_processor_convert_color( - proc, - in_rgba.as_ptr(), - std::ptr::null_mut(), - ) - }, - -1 - ); - out_rgba = [-1.0; 4]; - assert_eq!( - unsafe { - oakengine_color_processor_convert_color( - proc, - in_rgba.as_ptr(), - out_rgba.as_mut_ptr(), - ) - }, - 0 - ); - if valid == 0 { - assert_eq!(out_rgba, in_rgba, "pass-through processor copies the input"); - } else { - assert!(out_rgba.iter().all(|v| v.is_finite())); - assert!((out_rgba[3] - 1.0).abs() < 1e-6, "alpha preserved"); - } - - // The successful create cleared last_error. - assert_eq!( - unsafe { oakengine_color_last_error(buf.as_mut_ptr(), 256) }, - 0 - ); - - // Free contracts: each free releases the oakrender handle (alive - // count back to the pre-create baseline). - unsafe { oakengine_color_processor_free(proc_disp) }; - unsafe { oakengine_color_processor_free(proc_inv) }; - unsafe { oakengine_color_processor_free(proc_null_out) }; - unsafe { oakengine_color_processor_free(proc) }; - assert_eq!( - unsafe { oakrender::ffi::cache::oakrender_debug_alive_count() }, - base - ); - - // The transform-job stub returns E_FAILED for a real processor too. - assert_eq!( - unsafe { oakengine_color_transform_job_set_processor(0x1 as *mut c_void, proc.cast()) }, - -3 - ); - - // Real-handle cache setter paths (manager required; proc is dead - // here, so create a fresh one for the cacher). - let proc2 = unsafe { - oakengine_color_processor_create(std::ptr::null(), c"ACEScg".as_ptr(), &dest, 0) - }; - let node = unsafe { - oakengine_node_factory_create_from_id( - c"org.olivevideoeditor.Olive.solidgenerator".as_ptr(), - ) - }; - assert!(!node.is_null()); - assert_eq!( - unsafe { oakrender::ffi::manager::oakrender_manager_init() }, - 0 - ); - assert_eq!( - unsafe { oakengine_render_cache_set_display_color_processor(proc2.cast()) }, - 0 - ); - assert_eq!(unsafe { oakengine_render_cache_set_multicam_node(node) }, 0); - unsafe { oakrender::ffi::manager::oakrender_manager_shutdown() }; - unsafe { oakengine_color_processor_free(proc2) }; - unsafe { oakengine_node_free(node) }; - } else { - // Stub-OCIO build: creation is a clean NULL, nothing to free. - assert!(proc.is_null()); - assert_eq!( - unsafe { oakrender::ffi::cache::oakrender_debug_alive_count() }, - 0 - ); - } -} - -// --------------------------------------------------------------------------- -// LUT library (5 exports, documented stubs) -// --------------------------------------------------------------------------- - -/// The LUT directory/file library is facade-level over FileFunctions and -/// has no module backing: the documented neutral values hold for every -/// argument (garbage indices, NULL buffers, NULL/NULL directory lists). -#[test] -fn lut_library_stubs() { - assert_eq!(unsafe { oakengine_lut_directory_count() }, 0); - assert_eq!( - unsafe { oakengine_lut_directory_at(0, std::ptr::null_mut(), 0) }, - -3 - ); - assert_eq!( - unsafe { oakengine_lut_directory_at(-1, std::ptr::null_mut(), 0) }, - -3 - ); - assert_eq!( - unsafe { oakengine_lut_directory_at(999, std::ptr::null_mut(), 0) }, - -3 - ); - assert_eq!(unsafe { oakengine_lut_file_count() }, 0); - assert_eq!( - unsafe { oakengine_lut_file_at(0, std::ptr::null_mut(), 0) }, - -3 - ); - assert_eq!( - unsafe { oakengine_lut_file_at(-1, std::ptr::null_mut(), 0) }, - -3 - ); - assert_eq!( - unsafe { oakengine_lut_set_directories(std::ptr::null(), 0) }, - -3 - ); - assert_eq!( - unsafe { oakengine_lut_set_directories(std::ptr::null(), 1) }, - -3 - ); - assert_eq!( - unsafe { oakengine_lut_set_directories(std::ptr::null(), -1) }, - -3 - ); - // A non-NULL list is never dereferenced by the stub. - let fake_dirs = 0x1 as *const *const c_char; - assert_eq!(unsafe { oakengine_lut_set_directories(fake_dirs, 1) }, -3); -} - -// --------------------------------------------------------------------------- -// oakrender debug counter + free/destroy contracts -// --------------------------------------------------------------------------- - -/// Module-level frame free contracts through the real oakrender C ABI: -/// free(NULL) no-op, alive count +1 on create and back to baseline on -/// free, and a double free is a safe no-op (the module nulls the handle -/// ctx after releasing). Facade box frees (`oakengine_frame_free` / -/// `oakengine_color_processor_free` / `oakengine_renderer_free`) are -/// NULL-safe and alive-balance-clean, verified in `renderer_render_frame_e2e` -/// and `color_family_lifecycle`. -#[test] -fn oakrender_debug_alive_and_double_free() { - common::force_link(); - let _g = state_lock(); - - let base = unsafe { oakrender::ffi::cache::oakrender_debug_alive_count() }; - - // Create an oakrender-owned frame: +1. - let mut f = unsafe { oakrender::ffi::renderer::oakrender_codec_frame_create() }; - assert!(!f.is_null()); - assert_eq!( - unsafe { oakrender::ffi::cache::oakrender_debug_alive_count() }, - base + 1 - ); - - // free(NULL) is a no-op. - unsafe { oakrender::ffi::renderer::oakrender_codec_frame_free(std::ptr::null_mut()) }; - assert_eq!( - unsafe { oakrender::ffi::cache::oakrender_debug_alive_count() }, - base + 1 - ); - - // Free: back to baseline. - unsafe { oakrender::ffi::renderer::oakrender_codec_frame_free(&mut f) }; - assert_eq!( - unsafe { oakrender::ffi::cache::oakrender_debug_alive_count() }, - base - ); - - // Double free: the handle ctx was nulled, so it is a safe no-op. - unsafe { oakrender::ffi::renderer::oakrender_codec_frame_free(&mut f) }; - assert_eq!( - unsafe { oakrender::ffi::cache::oakrender_debug_alive_count() }, - base - ); -} - -#[test] -fn renderer_montage_with_clip_e2e() { - common::force_link(); - let _g = state_lock(); - - unsafe { oakrender::ffi::manager::oakrender_manager_shutdown() }; - assert_eq!(unsafe { oakrender::ffi::manager::oakrender_manager_init() }, 0); - - let project = unsafe { oakengine_project_create() }; - assert_eq!(unsafe { oakengine_project_new(project) }, 0); - let name = std::ffi::CString::new("Montage E2E").unwrap(); - let seq = unsafe { oakengine_sequence_new(project, name.as_ptr()) }; - assert!(!seq.is_null()); - assert_eq!(unsafe { oakengine::timeline::oakengine_sequence_add_track(seq, 0) }, 0); - - // Generate real test media through the facade, import it, place a - // clip covering [0, 10) frames. - let media = std::env::temp_dir().join(format!("oak_it_montage_{}.mp4", std::process::id())); - let mc = std::ffi::CString::new(media.to_string_lossy().into_owned()).unwrap(); - assert_eq!( - unsafe { oakengine::testmedia::oakengine_testmedia_write_clip(mc.as_ptr(), 64, 64, 10, 10) }, - 0 - ); - let footage = unsafe { oakengine_project_import_footage(project, mc.as_ptr()) }; - assert!(!footage.is_null(), "import must succeed"); - let clip = unsafe { - oakengine::timeline::oakengine_sequence_add_footage_clip_ex(seq, footage, 0, 0, 0, 10, 0) - }; - assert!(!clip.is_null(), "add_footage_clip_ex must succeed"); - unsafe { oakengine_footage_free(footage) }; - - let renderer = unsafe { oakengine_renderer_create(seq, 64, 64, 4, 10, 1, std::ptr::null()) }; - assert!(!renderer.is_null()); - let frame = unsafe { oakengine_renderer_render_frame(renderer, 0) }; - assert!(!frame.is_null(), "montage render must produce a frame"); - let linesize = unsafe { oakengine_frame_linesize_bytes(frame) }; - let data = unsafe { oakengine_frame_data(frame) } as *const f32; - assert!(!data.is_null()); - let stride = linesize as usize / 4; - let base = 32 * stride + 8 * 4; - let px = unsafe { std::slice::from_raw_parts(data.add(base), 4) }; - assert!(px[0] > 0.5, "left half red from the decoded clip: {px:?}"); - - unsafe { oakengine_frame_free(frame) }; - let _ = std::fs::remove_file(&media); -} - -#[test] -fn decode_via_dylib_rlib_direct() { - // The same decode call the montage producer makes, invoked directly - // through the oakrender rlib — isolates the dlsym bridge vs the FFmpeg - // duplication question. - let media = std::env::temp_dir().join(format!("oak_it_direct_{}.mp4", std::process::id())); - let mc = std::ffi::CString::new(media.to_string_lossy().into_owned()).unwrap(); - assert_eq!( - unsafe { oakengine::testmedia::oakengine_testmedia_write_clip(mc.as_ptr(), 64, 64, 10, 10) }, - 0 - ); - let tex = oakrender::eval::render_footage_frame( - &media.to_string_lossy(), - 0, - oakcore_rs::Rational::new(0, 1), - (64, 64), - oakcore_rs::PixelFormat::F32, - ) - .expect("the dlsym codec bridge must decode inside the engine binary"); - let oakrender::texture::Texture::Cpu(frame) = &tex else { - panic!("non-CPU texture"); - }; - assert!(frame.data.iter().any(|&b| b != 0), "non-black decode"); - let _ = std::fs::remove_file(&media); -} - -#[test] -fn renderer_render_audio_montage_e2e() { - common::force_link(); - let _g = state_lock(); - - unsafe { oakrender::ffi::manager::oakrender_manager_shutdown() }; - assert_eq!(unsafe { oakrender::ffi::manager::oakrender_manager_init() }, 0); - - let project = unsafe { oakengine_project_create() }; - assert_eq!(unsafe { oakengine_project_new(project) }, 0); - let name = std::ffi::CString::new("Audio E2E").unwrap(); - let seq = unsafe { oakengine_sequence_new(project, name.as_ptr()) }; - assert!(!seq.is_null()); - assert_eq!(unsafe { oakengine::timeline::oakengine_sequence_add_track(seq, 0) }, 0); - assert_eq!(unsafe { oakengine::timeline::oakengine_sequence_add_track(seq, 1) }, 0); - - // Generated media with a 440 Hz sine audio track. - let media = std::env::temp_dir().join(format!("oak_it_audio_{}.mp4", std::process::id())); - let mc = std::ffi::CString::new(media.to_string_lossy().into_owned()).unwrap(); - assert_eq!( - unsafe { oakengine::testmedia::oakengine_testmedia_write_clip(mc.as_ptr(), 64, 64, 10, 10) }, - 0 - ); - let footage = unsafe { oakengine_project_import_footage(project, mc.as_ptr()) }; - assert!(!footage.is_null(), "import must succeed"); - // Video clip on track 0, linked audio clip on track 1. - let vclip = unsafe { - oakengine::timeline::oakengine_sequence_add_footage_clip_ex(seq, footage, 0, 0, 0, 10, 0) - }; - assert!(!vclip.is_null(), "video clip must be placed"); - let aclip = unsafe { - oakengine::timeline::oakengine_sequence_add_footage_clip_ex(seq, footage, 1, 0, 0, 10, 0) - }; - assert!(!aclip.is_null(), "audio clip must be placed"); - unsafe { oakengine_footage_free(footage) }; - - // Render the audio of the first 0.1 s (2 frames at 10 fps) — the - // sine must come back as non-silent interleaved samples. - let renderer = unsafe { oakengine_renderer_create(seq, 64, 64, 4, 10, 1, std::ptr::null()) }; - assert!(!renderer.is_null()); - let buf = unsafe { oakengine_renderer_render_audio(renderer, 0, 1) }; - assert!(!buf.is_null(), "render_audio must produce a buffer"); - assert_eq!(unsafe { oakengine_audio_sample_rate(buf) }, 48000); - assert_eq!(unsafe { oakengine_audio_channel_count(buf) }, 2); - let frames = unsafe { oakengine_audio_sample_count(buf) }; - assert!(frames >= 4800, "0.1 s at 48 kHz: {frames} frames"); - let data = unsafe { oakengine_audio_data(buf, 0) }; - assert!(!data.is_null()); - let samples = unsafe { std::slice::from_raw_parts(data, frames as usize * 2) }; - let peak = samples.iter().fold(0.0f32, |a, &s| a.max(s.abs())); - assert!(peak > 0.1, "the sine tone must be audible: peak={peak}"); - let mut nonzero = 0usize; - for &s in samples.iter().take(960) { - if s != 0.0 { - nonzero += 1; - } - } - assert!(nonzero > 100, "samples carry the tone: {nonzero}"); - - unsafe { oakengine_audio_free(buf) }; - let _ = std::fs::remove_file(&media); -} - -#[test] -fn probe_and_waveform_extract_e2e() { - common::force_link(); - let _g = state_lock(); - - let media = std::env::temp_dir().join(format!("oak_it_wf_{}.mp4", std::process::id())); - let mc = std::ffi::CString::new(media.to_string_lossy().into_owned()).unwrap(); - assert_eq!( - unsafe { oakengine::testmedia::oakengine_testmedia_write_clip(mc.as_ptr(), 64, 64, 10, 10) }, - 0 - ); - // Probe first (the waveform extractor's first step). - let probe = unsafe { oakcodec::ffi::decoder::oakcodec_decoder_probe(mc.as_ptr()) }; - assert!(!probe.is_null(), "probe must succeed"); - let mut info = unsafe { std::mem::zeroed::() }; - let r = unsafe { - oakcodec::ffi::decoder::oakcodec_decoder_probe_get_audio_stream(probe, 0, &mut info) - }; - eprintln!("probe audio rc={r} rate={} ch={}", info.sample_rate, info.channel_count); - assert_eq!(r, 0); - let mut probe = probe; - unsafe { oakcodec::ffi::decoder::oakcodec_decoder_free(&mut probe) }; - let _ = std::fs::remove_file(&media); -} diff --git a/crates/oakengine/tests/it_timeline.rs b/crates/oakengine/tests/it_timeline.rs deleted file mode 100644 index dfc4bf446..000000000 --- a/crates/oakengine/tests/it_timeline.rs +++ /dev/null @@ -1,2485 +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** family -//! (`engine/include/oakengine/timeline.h`, wrapped by -//! `crates/oakengine/src/timeline.rs` — sequences, tracks, clips, blocks, -//! markers, the workarea, the track-height constants and the multicam -//! helpers). -//! -//! All 139 exported functions are exercised against the REAL module -//! crates (oaknode/oaktimeline/oakcommon/oakundo; no mocks of any API). -//! -//! ## Serialization -//! -//! The facade owns a process-wide undo stack and per-sequence -//! marker-list / workarea caches, so every test that mutates state is -//! serialized inside the single `timeline_zu_lifecycle` test (the same -//! convention as `tests/timeline.rs` and the undo family). The -//! `timeline_zu_failure_paths` test only exercises non-mutating NULL / -//! empty-handle / garbage-argument calls and runs in parallel. -//! -//! ## Documented stubs (asserted on their documented behavior) -//! -//! - `oakengine_sequence_ripple_tracks_command` → NULL (no module C -//! creator for `TrackListRippleToolCommand`). -//! - `oakengine_sequence_move_track` → E_STATE (-2) (the module has no -//! undoable element-aware edge commands for array-track re-ordering). -//! `oakengine_sequence_move_clip` is NOT a stub anymore: it gaps the -//! clip's old spot and places it at `new_in` on the same track as ONE -//! undoable entry (`oaktimeline_move_block_command` → the module's -//! `TrackMoveBlockCommand`), the capi's gap+place assembly. -//! - `oakengine_sequence_add_default_transition` → E_STATE (-2) for a -//! non-empty clip set, 0 for an empty one. -//! - `oakengine_sequence_get_video_auto_cache` → 0 / -//! `oakengine_sequence_set_video_auto_cache` → 0 (no module accessor). -//! - `oakengine_marker_create` → NULL (no standalone marker handle); -//! `oakengine_clip_find_multicam` → NULL; -//! `oakengine_multicam_switch_source` → 0 with a live node, -1 with -//! NULL (the capi's `Q_UNUSED` body). -//! - `oakengine_clip_request_invalidate` / -//! `oakengine_clip_request_invalidate_connected` / -//! `oakengine_clip_discard_cache` → NULL-safe no-ops (headless capi -//! behavior); `oakengine_clip_add_cache_passthrough` → module no-op. -//! - `oakengine_sequence_add_footage_clip` / -//! `oakengine_sequence_add_sequence_clip` → NULL with a non-empty last -//! error (module clips declare no `buffer_in` input and every facade -//! sequence lives in its own scratch project, so the cross-project -//! check / footage connection fails cleanly — there is no legal path). -//! - `oakengine_sequence_set_preview_divider` → 0 for a valid divider -//! (the module's `VideoParams` model drops the divider; the getter -//! reports 1), -1 for divider < 1. -//! -//! ## Notes / deviations observed while writing these tests -//! -//! - The facade has no `oakengine_sequence_free` / `oakengine_track_free` -//! / `oakengine_clip_free`, and `oakengine_sequence_new` keeps each -//! sequence in its own scratch project (documented deviation), so the -//! oaknode debug alive counter can only return to baseline for the -//! project shell: `oakengine_project_create` bumps it by exactly 1 and -//! `oakengine_project_free` brings it back. Sequence/track/clip nodes -//! remain counted for the process (see the alive assertions in -//! `timeline_zu_lifecycle`). -//! -//! ## Real bugs found (all fixed; the assertions below pin the corrected -//! behavior) -//! -//! 1. **`oakengine_clip_toggle_enabled(NULL, 0)` aborted the process** — -//! `slice::from_raw_parts(NULL, 0)` (src/timeline.rs) is a non-unwinding -//! UB panic that the `catch_unwind` guard cannot catch; the same NULL+0 -//! slice existed in `oakengine_sequence_delete_clips`. Both now guard the -//! empty set (NULL + zero count is a clean no-op); the former crash repro -//! (`timeline_zu_crash_repros`) is now a plain assertion. -//! 2. **Module `BlockSplitCommand` misplaced both split halves** -//! (crates/oaktimeline/src/undosplit.rs): the lengths were applied with -//! swapped setters — the original was out-anchored with the FIRST half's -//! length and the fresh block in-anchored with the SECOND half's length. -//! Splitting [0, 30) at frame 20 yielded [10, 30) + [0, 10) instead of -//! [0, 20) + [20, 30). The setter arguments are now swapped so the -//! original becomes the out-anchored second half and the new block the -//! in-anchored first half. -//! 3. **`oakengine_sequence_split_clips` (batch split) was a silent no-op**: -//! the module's `BlockSplitPreservingLinksCommand` never ran `prepare()` -//! (the oakundo vtable wrapper only dispatches redo/undo), so `redo()` -//! iterated an empty child list. `redo()` now derives the children on -//! first use. -//! 4. **`oakengine_sequence_trim_clips_to` never applied a trim**: it built -//! its trim command with the TRACK handle where the BLOCK belongs -//! (`trim_cmd(track, ...)`, src/timeline.rs), so the redo called -//! `oaknode_block_set_length_and_media_out` on a track node and the -//! module rejected it. It now passes the block and targets the block -//! strictly containing the point (a trim to the point is only meaningful -//! there; the nearest-before queries could pick an insertion-order -//! neighbor ending before the point and trim to a negative length). -//! 5. **`oakengine_sequence_delete_empty_tracks` removed nothing**: unlike -//! `oakengine_sequence_remove_track` it skipped the live -//! `oaknode_tracklist_remove_track` compensation, and the module's -//! `TimelineRemoveTrackCommand::redo` is a documented no-op. The live -//! compensation now runs after the push. -//! 6. **`oakengine_sequence_ripple_delete_clip` / -//! `oakengine_sequence_ripple_delete_range` were silent no-ops**: the -//! module's `TrackRippleRemoveAreaCommand` and -//! `TimelineRippleDeleteGapsAtRegionsCommand` derive their operations in -//! `prepare()`, which the oakundo vtable path never invoked. Their -//! `redo()` now derives the operations on first use. -//! -//! ## Naming -//! -//! The suite lives in `tests/it_timeline.rs` (target `it_timeline`), -//! matching the other `it_` integration tests in this directory. - -#[path = "common/mod.rs"] -mod common; - -use std::ffi::{c_char, c_int, c_void}; - -use oakengine::handle::{ - box_handle, free_box, CHandle, OakEngineBlock, OakEngineClip, OakEngineFootage, - OakEngineMarker, OakEngineMarkerList, OakEngineNode, OakEngineProject, OakEngineSequence, - OakEngineTrack, OakEngineTrackList, OakEngineWorkarea, -}; -use oakengine::node::{ - oakengine_footage_borrow, oakengine_node_effect_at, oakengine_node_effect_count, - oakengine_node_effect_insert, oakengine_node_effect_remove, oakengine_node_free, - oakengine_node_get_effect_input, oakengine_node_get_type_id, oakengine_project_create, - oakengine_project_free, oakengine_project_new, -}; -use oakengine::timeline::*; -use oakengine::undo::{oakengine_undo_command_free, oakengine_undo_index, oakengine_undo_jump}; - -/// An effect node type id (the blur effect used to exercise the clip's -/// effect chain). -const TYPE_BLUR: &std::ffi::CStr = c"org.olivevideoeditor.Olive.blur"; - -/// Read a NUL-terminated facade string into a Rust String. -unsafe fn read_buf(buf: &mut [c_char]) -> String { - std::ffi::CStr::from_ptr(buf.as_ptr()) - .to_string_lossy() - .into_owned() -} - -/// Box a NULL module handle as a live (empty) engine box. `unbox` then -/// fails with E_INVALID — the "empty handle" (ctx == NULL) case that a -/// plugin can hand to every function. -macro_rules! empty_box { - ($t:ty) => { - box_handle::<$t>(CHandle::null()) - }; -} - -/// Force the runtime-dlsym'd symbols into the link: the oaknode module -/// resolves `oakcommon_videoparams_*` and `oakundo_command_init` at -/// runtime with `dlsym(RTLD_DEFAULT)`, and nothing references those -/// codegen units at link time unless named here. -fn force_runtime_syms() -> usize { - let fns: [usize; 8] = [ - oakcommon::ffi::videoparams::oakcommon_videoparams_init_basic as *const () as usize, - oakcommon::ffi::videoparams::oakcommon_videoparams_set_frame_rate as *const () as usize, - oakcommon::ffi::videoparams::oakcommon_videoparams_get_width as *const () as usize, - oakcommon::ffi::videoparams::oakcommon_videoparams_get_height as *const () as usize, - oakcommon::ffi::videoparams::oakcommon_videoparams_get_format as *const () as usize, - oakcommon::ffi::videoparams::oakcommon_videoparams_get_channel_count as *const () as usize, - oakcommon::ffi::videoparams::oakcommon_videoparams_get_frame_rate as *const () as usize, - oakundo::ffi::command::oakundo_command_init as *const () as usize, - ]; - fns.iter().sum() -} - -/// The module live count of owned node/project handles -/// (`oaknode_debug_alive_count`), the only debug counter backing the -/// timeline family's node objects. -fn alive() -> c_int { - unsafe { oaknode::ffi::node::oaknode_debug_alive_count() } -} - -/// Place a raw module clip on `track` (returns the module handle). -unsafe fn module_clip_on(track: CHandle, in_num: c_int, in_den: c_int) -> CHandle { - let clip = oaknode::ffi::block::oaknode_block_clip_create(); - oaknode::ffi::block::oaknode_block_set_in(clip, in_num, in_den); - oaknode::ffi::block::oaknode_block_set_length_and_media_in(clip, 1, 1); - oaknode::ffi::block::oaknode_clip_set_media_in(clip, 0, 1); - oaknode::ffi::track::oaknode_track_append_block(track, clip); - clip -} - -/// Assert `last_error` is non-empty (a failure path recorded a reason). -unsafe fn assert_last_error() { - let mut err = [0 as c_char; 256]; - let elen = unsafe { oakengine_sequence_last_error(err.as_mut_ptr(), 256) }; - assert!(elen > 0, "last_error must be non-empty"); -} - -/// The clip at (track_type, track_index, clip_index), panicking if none. -unsafe fn clip_at_ok( - seq: *mut OakEngineSequence, - tt: c_int, - ti: c_int, - ci: c_int, -) -> *mut OakEngineClip { - let c = unsafe { oakengine_sequence_clip_at(seq, tt, ti, ci) }; - assert!(!c.is_null(), "expected clip at ({tt},{ti},{ci})"); - c -} - -// --------------------------------------------------------------------------- -// Serialized lifecycle test (all mutating operations, in ONE test because -// the facade's undo stack and per-sequence caches are process-wide) -// --------------------------------------------------------------------------- - -/// Every mutating path of the timeline family: sequence creation and -/// inspection, video/audio params, playhead, markers, workarea (+ ripple -/// in-to-out), track structure, clip editing (split / trim / delete / -/// ripple / link / toggle), the marker-handle and workarea-handle -/// families, the free contracts, and the oaknode alive-count deltas. -/// Serializes the binary's tests: the module's live-handle ledger is -/// process-global and several tests assert its deltas, so parallel -/// execution would race the counts. -fn zu_lock() -> std::sync::MutexGuard<'static, ()> { - static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); - LOCK.lock().unwrap_or_else(|e| e.into_inner()) -} - -#[test] -fn timeline_zu_lifecycle() { - let _zu = zu_lock(); - common::force_link(); - let _ = force_runtime_syms(); - - let base = alive(); - - // ---- project + sequence creation -------------------------------------- - let project = unsafe { oakengine_project_create() }; - assert!(!project.is_null()); - assert_eq!(alive(), base + 1, "project_create must own one node"); - assert_eq!(unsafe { oakengine_project_new(project) }, 0); - - // NULL project -> NULL sequence. - assert!(unsafe { oakengine_sequence_new(std::ptr::null_mut(), c"x".as_ptr()) }.is_null()); - - let seq = unsafe { oakengine_sequence_new(project, c"Test Sequence".as_ptr()) }; - assert!(!seq.is_null()); - assert_eq!(alive(), base + 2, "sequence_new must own one node"); - - // A second sequence for the self-nest / cross-project checks. - let seq2 = unsafe { oakengine_sequence_new(project, c"Nested".as_ptr()) }; - assert!(!seq2.is_null()); - - // ---- name (buf/size convention) ---------------------------------------- - let mut buf = [0 as c_char; 256]; - let len = unsafe { oakengine_sequence_name(seq, buf.as_mut_ptr(), 256) }; - assert_eq!(len, 13); - assert_eq!(unsafe { read_buf(&mut buf) }, "Test Sequence"); - // Two-stage: NULL buffer / zero size only reports the length. - assert_eq!( - unsafe { oakengine_sequence_name(seq, std::ptr::null_mut(), 0) }, - 13 - ); - // A too-small buffer is truncated by the module (2 chars + NUL). - let mut small = [0 as c_char; 3]; - let len = unsafe { oakengine_sequence_name(seq, small.as_mut_ptr(), 3) }; - assert_eq!(len, 13); - assert_eq!(unsafe { read_buf(&mut small) }, "Te"); - - // ---- length / frame rate / video params --------------------------------- - let mut seconds = -1.0; - assert_eq!( - unsafe { oakengine_sequence_get_length(seq, &mut seconds) }, - 0 - ); - assert_eq!(seconds, 0.0); - let (mut n, mut d) = (-1, -1); - assert_eq!( - unsafe { oakengine_sequence_get_length_rational(seq, &mut n, &mut d) }, - 0 - ); - assert_eq!((n, d), (0, 1)); - // NULL out params are fine. - assert_eq!( - unsafe { - oakengine_sequence_get_length_rational(seq, std::ptr::null_mut(), std::ptr::null_mut()) - }, - 0 - ); - let (mut fn_, mut fd) = (0, 0); - assert_eq!( - unsafe { oakengine_sequence_get_frame_rate(seq, &mut fn_, &mut fd) }, - 0 - ); - assert_eq!((fn_, fd), (30, 1)); - - let (mut w, mut h, mut pn, mut pd) = (0, 0, 0, 0); - assert_eq!( - unsafe { oakengine_sequence_get_video_params(seq, &mut w, &mut h, &mut pn, &mut pd) }, - 0 - ); - assert_eq!((w, h), (1920, 1080)); - assert_eq!((pn, pd), (1, 1)); - - let (mut exw, mut exh, mut exfn, mut exfd, mut expn, mut expd, mut exi, mut exf, mut exdv) = - (0, 0, 0, 0, 0, 0, -1, -1, 0); - assert_eq!( - unsafe { - oakengine_sequence_get_video_params_ex( - seq, &mut exw, &mut exh, &mut exfn, &mut exfd, &mut expn, &mut expd, &mut exi, - &mut exf, &mut exdv, - ) - }, - 0 - ); - assert_eq!((exw, exh), (1920, 1080)); - assert_eq!((exfn, exfd), (30, 1)); - assert_eq!((expn, expd), (1, 1)); - assert_eq!(exi, 0); // progressive - assert_eq!(exf, 4); // f32 - assert_eq!(exdv, 1); - - // ---- set_video_params (legal matrix + validation) ----------------------- - // Change, read back, restore to 30 fps BEFORE any frame conversions. - assert_eq!( - unsafe { oakengine_sequence_set_video_params(seq, 1280, 720, 24, 1, 1, 1, 0, 4, 0) }, - 0 - ); - assert_eq!( - unsafe { oakengine_sequence_get_video_params(seq, &mut w, &mut h, &mut pn, &mut pd) }, - 0 - ); - assert_eq!((w, h), (1280, 720)); - assert_eq!( - unsafe { oakengine_sequence_get_frame_rate(seq, &mut fn_, &mut fd) }, - 0 - ); - assert_eq!((fn_, fd), (24, 1)); - // Restore the 30 fps timebase. - assert_eq!( - unsafe { oakengine_sequence_set_video_params(seq, 1920, 1080, 30, 1, 1, 1, 0, 4, 0) }, - 0 - ); - assert_eq!( - unsafe { oakengine_sequence_get_frame_rate(seq, &mut fn_, &mut fd) }, - 0 - ); - assert_eq!((fn_, fd), (30, 1)); - // -1 leaves a field unchanged (only the width changes). - assert_eq!( - unsafe { oakengine_sequence_set_video_params(seq, 640, -1, -1, -1, -1, -1, -1, -1, 0) }, - 0 - ); - assert_eq!( - unsafe { oakengine_sequence_get_video_params(seq, &mut w, &mut h, &mut pn, &mut pd) }, - 0 - ); - assert_eq!((w, h), (640, 1080)); - assert_eq!( - unsafe { oakengine_sequence_set_video_params(seq, 1920, -1, -1, -1, -1, -1, -1, -1, 0) }, - 0 - ); - // Validation failures (each sets a last error). - assert_eq!( - unsafe { oakengine_sequence_set_video_params(seq, 0, 1080, 30, 1, 1, 1, 0, 4, 0) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_set_video_params(seq, 1920, 1080, 0, 1, 1, 1, 0, 4, 0) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_set_video_params(seq, 1920, 1080, 30, 1, 1, 1, 5, 4, 0) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_set_video_params(seq, 1920, 1080, 30, 1, 1, 1, 0, 99, 0) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_set_video_params(seq, 1920, 1080, -1, 1, 1, 1, 0, 4, 0) }, - -1 - ); - unsafe { assert_last_error() }; - - // ---- audio params (round-trip through the oakcore stub store) ---------- - let (mut arate, mut alayout) = (0 as c_int, 0u64); - assert_eq!( - unsafe { oakengine_sequence_get_audio_params(seq, &mut arate, &mut alayout) }, - 0 - ); - assert!(arate > 0); - assert_eq!( - unsafe { oakengine_sequence_set_audio_params(seq, 48000, 0x3, 1) }, - 0 - ); - assert_eq!( - unsafe { oakengine_sequence_get_audio_params(seq, &mut arate, &mut alayout) }, - 0 - ); - assert_eq!((arate, alayout), (48000, 0x3)); - // A no-op change (same values) succeeds without a new command. - assert_eq!( - unsafe { oakengine_sequence_set_audio_params(seq, 48000, 0x3, 1) }, - 0 - ); - - // ---- preview divider / video auto-cache (module-stubbed) ---------------- - assert_eq!(unsafe { oakengine_sequence_get_preview_divider(seq) }, 1); - assert_eq!( - unsafe { oakengine_sequence_set_preview_divider(seq, 2, 0) }, - 0 - ); - assert_eq!(unsafe { oakengine_sequence_get_preview_divider(seq) }, 1); - assert_eq!( - unsafe { oakengine_sequence_set_preview_divider(seq, 0, 0) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_set_preview_divider(seq, -2, 0) }, - -1 - ); - assert_eq!(unsafe { oakengine_sequence_get_video_auto_cache(seq) }, 0); - assert_eq!( - unsafe { oakengine_sequence_set_video_auto_cache(seq, 1, 1) }, - 0 - ); - assert_eq!(unsafe { oakengine_sequence_get_video_auto_cache(seq) }, 0); - - // ---- track counts (fresh sequence: none) --------------------------------- - let (mut v, mut a, mut s) = (-1, -1, -1); - assert_eq!( - unsafe { oakengine_sequence_track_count(seq, &mut v, &mut a, &mut s) }, - 0 - ); - assert_eq!((v, a, s), (0, 0, 0)); - assert_eq!( - unsafe { - oakengine_sequence_track_count( - seq, - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - 0 - ); - - // ---- playhead ------------------------------------------------------------- - let mut ts = -1i64; - assert_eq!(unsafe { oakengine_sequence_get_playhead(seq, &mut ts) }, 0); - assert_eq!(ts, 0); - assert_eq!(unsafe { oakengine_sequence_set_playhead(seq, 90) }, 0); - assert_eq!(unsafe { oakengine_sequence_get_playhead(seq, &mut ts) }, 0); - assert_eq!(ts, 90); - let mut phs = 0.0; - assert_eq!( - unsafe { oakengine_sequence_get_playhead_seconds(seq, &mut phs) }, - 0 - ); - assert!((phs - 3.0).abs() < 1e-6); - assert_eq!(unsafe { oakengine_sequence_set_playhead(seq, 0) }, 0); - - // ---- workarea (sequence) + ripple in-to-out -------------------------------- - assert_eq!(unsafe { oakengine_sequence_workarea_is_enabled(seq) }, 0); - assert_eq!( - unsafe { oakengine_sequence_set_workarea(seq, 1, 0, 300) }, - 0 - ); - assert_eq!(unsafe { oakengine_sequence_workarea_is_enabled(seq) }, 1); - let (mut wi, mut wo) = (-1i64, -1i64); - assert_eq!( - unsafe { oakengine_sequence_get_workarea(seq, &mut wi, &mut wo) }, - 0 - ); - assert_eq!((wi, wo), (0, 300)); - assert_eq!( - unsafe { oakengine_sequence_set_workarea(seq, 0, 0, 300) }, - 0 - ); - assert_eq!(unsafe { oakengine_sequence_workarea_is_enabled(seq) }, 0); - - // ripple_delete_in_to_out requires the workarea enabled. - assert_eq!( - unsafe { oakengine_sequence_ripple_delete_in_to_out(seq, 0, 0, 300) }, - -2 - ); - assert_eq!( - unsafe { oakengine_sequence_ripple_delete_in_to_out(seq, 1, -1, 300) }, - -1 - ); - unsafe { assert_last_error() }; - assert_eq!( - unsafe { oakengine_sequence_set_workarea(seq, 1, 0, 300) }, - 0 - ); - // Gap-fill variant (ripple = 0) on the (still empty) tracks. - assert_eq!( - unsafe { oakengine_sequence_ripple_delete_in_to_out(seq, 0, 0, 300) }, - 0 - ); - assert_eq!(unsafe { oakengine_sequence_workarea_is_enabled(seq) }, 0); - // Ripple variant (ripple = 1) after re-enabling. - assert_eq!( - unsafe { oakengine_sequence_set_workarea(seq, 1, 0, 300) }, - 0 - ); - assert_eq!( - unsafe { oakengine_sequence_ripple_delete_in_to_out(seq, 1, 0, 300) }, - 0 - ); - // Disabled again -> E_STATE. - assert_eq!( - unsafe { oakengine_sequence_ripple_delete_in_to_out(seq, 0, 0, 300) }, - -2 - ); - - // ---- sequence markers ----------------------------------------------------- - assert_eq!(unsafe { oakengine_sequence_marker_count(seq) }, 0); - assert_eq!( - unsafe { oakengine_sequence_marker_add(seq, 30, c"One".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_sequence_marker_add_ex(seq, 60, c"Two".as_ptr(), 2) }, - 0 - ); - assert_eq!(unsafe { oakengine_sequence_marker_count(seq) }, 2); - - let (mut mtime, mut mcolor) = (-1i64, -1); - let mut mname = [0 as c_char; 64]; - assert_eq!( - unsafe { - oakengine_sequence_marker_at(seq, 0, &mut mtime, mname.as_mut_ptr(), 64, &mut mcolor) - }, - 0 - ); - assert_eq!(mtime, 30); - assert_eq!(mcolor, 0); - assert_eq!(unsafe { read_buf(&mut mname) }, "One"); - // Out-of-range index -> the module's NOT_FOUND (-40004) passes through. - assert_eq!( - unsafe { - oakengine_sequence_marker_at(seq, 5, &mut mtime, mname.as_mut_ptr(), 64, &mut mcolor) - }, - -40004 - ); - // Duplicate time -> E_STATE. - assert_eq!( - unsafe { oakengine_sequence_marker_add(seq, 30, c"dup".as_ptr()) }, - -2 - ); - // Rename, then remove many at once. - assert_eq!( - unsafe { oakengine_sequence_marker_rename(seq, 30, c"Renamed".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_sequence_marker_remove_many(seq, [30i64, 60].as_ptr(), 2) }, - 2 - ); - assert_eq!(unsafe { oakengine_sequence_marker_count(seq) }, 0); - // Removing a nonexistent time -> E_NOT_FOUND (-4). - assert_eq!(unsafe { oakengine_sequence_marker_remove(seq, 999) }, -4); - assert_eq!( - unsafe { oakengine_sequence_marker_remove_many(seq, [999i64].as_ptr(), 1) }, - -4 - ); - unsafe { assert_last_error() }; - // NULL name -> empty name. - assert_eq!( - unsafe { oakengine_sequence_marker_add(seq, 90, std::ptr::null()) }, - 0 - ); - assert_eq!(unsafe { oakengine_sequence_marker_count(seq) }, 1); - assert_eq!(unsafe { oakengine_sequence_marker_remove(seq, 90) }, 0); - - // ---- tracks ---------------------------------------------------------------- - let idx = unsafe { oakengine_sequence_add_track(seq, 0) }; - assert_eq!(idx, 0); - assert_eq!(unsafe { oakengine_sequence_add_track(seq, 1) }, 0); // audio - assert_eq!(unsafe { oakengine_sequence_add_track(seq, 2) }, 0); // subtitle - // Garbage track types -> E_INVALID. - assert_eq!(unsafe { oakengine_sequence_add_track(seq, 3) }, -1); - assert_eq!(unsafe { oakengine_sequence_add_track(seq, -1) }, -1); - unsafe { assert_last_error() }; - - assert_eq!( - unsafe { oakengine_sequence_track_count(seq, &mut v, &mut a, &mut s) }, - 0 - ); - assert_eq!((v, a, s), (1, 1, 1)); - - let track = unsafe { oakengine_sequence_track_at(seq, 0, 0) }; - assert!(!track.is_null()); - assert_eq!(unsafe { oakengine_track_type(track) }, 0); // video - assert!(unsafe { oakengine_sequence_track_at(seq, 0, 5) }.is_null()); - assert!(unsafe { oakengine_sequence_track_at(seq, 3, 0) }.is_null()); - assert!(!unsafe { oakengine_sequence_track_list(seq, 0) }.is_null()); - assert!(unsafe { oakengine_sequence_track_list(seq, 99) }.is_null()); - - // Track height / mute / lock (straight setters, NOT undoable). - let mut h = 0.0; - assert_eq!(unsafe { oakengine_track_get_height(seq, 0, 0, &mut h) }, 0); - assert!((h - 3.0).abs() < 1e-9); - assert_eq!(unsafe { oakengine_track_set_height(seq, 0, 0, 5.0) }, 0); - assert_eq!(unsafe { oakengine_track_get_height(seq, 0, 0, &mut h) }, 0); - assert!((h - 5.0).abs() < 1e-9); - assert_eq!(unsafe { oakengine_track_set_height(seq, 0, 0, -1.0) }, -1); - unsafe { assert_last_error() }; - assert_eq!(unsafe { oakengine_track_is_muted(seq, 0, 0) }, 0); - assert_eq!(unsafe { oakengine_track_set_muted(seq, 0, 0, 1) }, 0); - assert_eq!(unsafe { oakengine_track_is_muted(seq, 0, 0) }, 1); - assert_eq!(unsafe { oakengine_track_set_muted(seq, 0, 0, 0) }, 0); - assert_eq!(unsafe { oakengine_track_is_locked(seq, 0, 0) }, 0); - assert_eq!(unsafe { oakengine_track_set_locked(seq, 0, 0, 1) }, 0); - assert_eq!(unsafe { oakengine_track_is_locked(seq, 0, 0) }, 1); - assert_eq!(unsafe { oakengine_track_set_locked(seq, 0, 0, 0) }, 0); - - // Track length (empty -> 0) and free-range query. - let mut tlen = -1i64; - assert_eq!( - unsafe { oakengine_track_get_length(seq, 0, 0, &mut tlen) }, - 0 - ); - assert_eq!(tlen, 0); - assert_eq!( - unsafe { oakengine_track_is_range_free(seq, 0, 0, 0, 30) }, - 1 - ); - // Bad track index -> E_NOT_FOUND (-4). - assert_eq!( - unsafe { oakengine_track_get_length(seq, 0, 99, &mut tlen) }, - -4 - ); - assert_eq!( - unsafe { oakengine_track_is_range_free(seq, 0, 99, 0, 30) }, - -4 - ); - // Invalid ranges. - assert_eq!( - unsafe { oakengine_track_is_range_free(seq, 0, 0, -1, 30) }, - -1 - ); - assert_eq!( - unsafe { oakengine_track_is_range_free(seq, 0, 0, 30, 30) }, - -1 - ); - unsafe { assert_last_error() }; - - // ---- add_track_command (unpushed command + live out-track) --------------- - let mut out_track: *mut OakEngineTrack = std::ptr::null_mut(); - let tcmd = unsafe { oakengine_sequence_add_track_command(seq, 0, 1, &mut out_track) }; - assert!(!tcmd.is_null()); - assert!(!out_track.is_null()); - assert_eq!(unsafe { oakengine_track_type(out_track) }, 0); - // The command owns an internal track node (TimelineAddTrackCommand), - // and the out-track box wraps the facade-created track. Freeing the - // command releases ITS node; the out-track was adopted by the list, so - // its box release un-counts nothing. - let alive_before = alive(); - unsafe { oakengine_undo_command_free(tcmd) }; - assert_eq!( - alive(), - alive_before - 1, - "freeing the command releases its internal track node" - ); - unsafe { free_box::(out_track) }; - assert_eq!( - alive(), - alive_before - 1, - "the adopted out-track box un-counts nothing on release" - ); - // NULL / garbage paths. - assert!(unsafe { - oakengine_sequence_add_track_command(std::ptr::null_mut(), 0, 0, std::ptr::null_mut()) - } - .is_null()); - assert!( - unsafe { oakengine_sequence_add_track_command(seq, 99, 0, std::ptr::null_mut()) }.is_null() - ); - assert!( - unsafe { oakengine_sequence_add_track_command(seq, -2, 0, std::ptr::null_mut()) }.is_null() - ); - - // The live-compensation track makes video = 2 now. - assert_eq!( - unsafe { oakengine_sequence_track_count(seq, &mut v, &mut a, &mut s) }, - 0 - ); - assert_eq!((v, a, s), (2, 1, 1)); - - // ---- move_track (stub for a real move, validated indices) ---------------- - assert_eq!(unsafe { oakengine_sequence_move_track(seq, 0, 0, 0) }, 0); // no-op move - assert_eq!(unsafe { oakengine_sequence_move_track(seq, 0, 0, 1) }, -2); // stub - assert_eq!(unsafe { oakengine_sequence_move_track(seq, 0, 0, 9) }, -4); // out of range - unsafe { assert_last_error() }; - - // ---- ripple_tracks_command (stub -> NULL) --------------------------------- - // The facade has no clip-placement path for a module clip without a - // footage node; the raw clips are appended through oaknode::ffi directly. - let _ = unsafe { module_clip_on((*track).handle, 0, 1) }; - let clip_a = unsafe { clip_at_ok(seq, 0, 0, 0) }; - assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 0, 0) }, 1); - let (mut cin, mut cout, mut cmi) = (-1i64, -1i64, -1i64); - assert_eq!( - unsafe { oakengine_clip_get_range(clip_a, &mut cin, &mut cout, &mut cmi) }, - 0 - ); - assert_eq!((cin, cout, cmi), (0, 30, 0)); - assert!(!unsafe { oakengine_clip_get_sequence(clip_a) }.is_null()); - - // Media range as rationals: media_in (0,1), out = in + length (1s = 1/1). - let (mut mn, mut md, mut xon, mut xod) = (-1i64, -1i64, -1i64, -1i64); - assert_eq!( - unsafe { - oakengine_clip_get_media_range_rational(clip_a, &mut mn, &mut md, &mut xon, &mut xod) - }, - 0 - ); - assert_eq!((mn, md, xon, xod), (0, 1, 1, 1)); - assert_eq!( - unsafe { oakengine_clip_get_media_in_rational(clip_a, &mut mn, &mut md) }, - 0 - ); - assert_eq!((mn, md), (0, 1)); - - // Media-in writes: live (non-undoable) then undoable. - assert_eq!(unsafe { oakengine_clip_set_media_in(clip_a, 10, 0) }, 0); - assert_eq!( - unsafe { oakengine_clip_get_media_in_rational(clip_a, &mut mn, &mut md) }, - 0 - ); - assert_eq!((mn, md), (1, 3)); // 10 frames at 30 fps = 1/3 s - assert_eq!(unsafe { oakengine_clip_set_media_in(clip_a, 0, 1) }, 0); - assert_eq!( - unsafe { oakengine_clip_set_media_in_rational(clip_a, 2, 3, 0) }, - 0 - ); - assert_eq!( - unsafe { oakengine_clip_get_media_in_rational(clip_a, &mut mn, &mut md) }, - 0 - ); - assert_eq!((mn, md), (2, 3)); - assert_eq!( - unsafe { oakengine_clip_set_media_in_rational(clip_a, 0, 1, 0) }, - 0 - ); - // Zero denominator -> E_INVALID. - assert_eq!( - unsafe { oakengine_clip_set_media_in_rational(clip_a, 0, 0, 0) }, - -1 - ); - unsafe { assert_last_error() }; - - // Enabled toggle (undoable). - assert_eq!(unsafe { oakengine_clip_is_enabled(clip_a) }, 1); - let mut clip_a_ptr = clip_a; - assert_eq!( - unsafe { oakengine_clip_toggle_enabled(&mut clip_a_ptr, 1) }, - 1 - ); - assert_eq!(unsafe { oakengine_clip_is_enabled(clip_a) }, 0); - assert_eq!( - unsafe { oakengine_clip_toggle_enabled(&mut clip_a_ptr, 1) }, - 1 - ); - assert_eq!(unsafe { oakengine_clip_is_enabled(clip_a) }, 1); - - // ---- effect chain on a real timeline clip --------------------------------- - // The clip node carries the `tex_in` effect input: effects chain onto - // it (the same `oakengine_node_effect_*` API the transform host - // exercises in it_node.rs). An empty chain is the no-effect baseline. - let clip_node = unsafe { oakengine_clip_as_node(clip_a) }; - assert!(!clip_node.is_null()); - let mut ebuf = [0 as c_char; 64]; - let mut eelem: c_int = 0; - let elen = unsafe { - oakengine_node_get_effect_input( - clip_node, - ebuf.as_mut_ptr(), - ebuf.len() as c_int, - &mut eelem, - ) - }; - assert_eq!(elen, "tex_in".len() as c_int); - assert_eq!(unsafe { read_buf(&mut ebuf) }, "tex_in"); - assert_eq!(eelem, -1, "non-array input element"); - assert_eq!(unsafe { oakengine_node_effect_count(clip_node) }, 0); - assert!(unsafe { oakengine_node_effect_at(clip_node, 0) }.is_null()); - - // Insert blur, verify the chain, then remove it (one undoable edit each; - // the removed node stays orphaned in the sequence's scratch project). - assert_eq!( - unsafe { oakengine_node_effect_insert(clip_node, 0, TYPE_BLUR.as_ptr()) }, - 0 - ); - assert_eq!(unsafe { oakengine_node_effect_count(clip_node) }, 1); - let eff = unsafe { oakengine_node_effect_at(clip_node, 0) }; - assert!(!eff.is_null()); - let mut tbuf = [0 as c_char; 64]; - assert_eq!( - unsafe { oakengine_node_get_type_id(eff, tbuf.as_mut_ptr(), tbuf.len() as c_int) }, - TYPE_BLUR.to_bytes().len() as c_int - ); - assert_eq!(unsafe { read_buf(&mut tbuf) }, TYPE_BLUR.to_str().unwrap()); - assert_eq!(unsafe { oakengine_node_effect_remove(clip_node, eff) }, 0); - assert_eq!(unsafe { oakengine_node_effect_count(clip_node) }, 0); - unsafe { oakengine_node_free(eff) }; - unsafe { oakengine_node_free(clip_node) }; - - // Second clip at [40, 70). - unsafe { module_clip_on((*track).handle, 4, 3) }; - assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 0, 0) }, 2); - let mut bin = -1i64; - let mut bout = -1i64; - let mut bmi = -1i64; - let clip_b = unsafe { clip_at_ok(seq, 0, 0, 1) }; - assert_eq!( - unsafe { oakengine_clip_get_range(clip_b, &mut bin, &mut bout, &mut bmi) }, - 0 - ); - assert_eq!((bin, bout, bmi), (40, 70, 0)); - - // Links: unlinked, link, unlink (one undoable command each). - assert_eq!(unsafe { oakengine_clip_are_linked(clip_a, clip_b) }, 0); - let mut clips2 = [clip_a, clip_b]; - assert_eq!( - unsafe { oakengine_clip_set_linked(clips2.as_mut_ptr(), 2, 1) }, - 0 - ); - assert_eq!(unsafe { oakengine_clip_are_linked(clip_a, clip_b) }, 1); - assert_eq!( - unsafe { oakengine_clip_set_linked(clips2.as_mut_ptr(), 2, 0) }, - 0 - ); - assert_eq!(unsafe { oakengine_clip_are_linked(clip_a, clip_b) }, 0); - // Zero count succeeds; NULL with count > 0 fails. - assert_eq!( - unsafe { oakengine_clip_set_linked(std::ptr::null_mut(), 0, 1) }, - 0 - ); - assert_eq!( - unsafe { oakengine_clip_set_linked(std::ptr::null_mut(), 1, 1) }, - -1 - ); - unsafe { assert_last_error() }; - - // ---- block traversal ------------------------------------------------------- - assert_eq!(unsafe { oakengine_track_block_count(track) }, 2); - let blk_a = unsafe { oakengine_track_block_at(track, 0) }; - assert!(!blk_a.is_null()); - assert!(unsafe { oakengine_track_block_at(track, 5) }.is_null()); - assert_eq!(unsafe { oakengine_block_is_gap(blk_a) }, 0); - assert!(!unsafe { oakengine_block_get_track(blk_a) }.is_null()); - let blk_b = unsafe { oakengine_block_next(blk_a) }; - assert!(!blk_b.is_null()); - assert!(unsafe { oakengine_block_prev(blk_a) }.is_null()); - assert!(unsafe { oakengine_block_next(blk_b) }.is_null()); - assert!(!unsafe { oakengine_block_prev(blk_b) }.is_null()); - let (mut bin2, mut bout2) = (-1i64, -1i64); - assert_eq!( - unsafe { oakengine_block_get_range(blk_a, &mut bin2, &mut bout2) }, - 0 - ); - assert_eq!((bin2, bout2), (0, 30)); - assert_eq!(unsafe { oakengine_block_link_count(blk_a) }, 0); - assert!(unsafe { oakengine_block_link_at(blk_a, 0) }.is_null()); - assert!(unsafe { oakengine_block_link_at(blk_a, -1) }.is_null()); - - // Block at time / visible / nearest. - assert!(!unsafe { oakengine_track_block_at_time(track, 5) }.is_null()); - assert!(unsafe { oakengine_track_block_at_time(track, 100) }.is_null()); - assert!(!unsafe { oakengine_track_visible_block_at_time(track, 5) }.is_null()); - assert!(!unsafe { oakengine_track_nearest_block_before(track, 35) }.is_null()); - assert!(unsafe { oakengine_track_nearest_block_before(track, 5) }.is_null()); - assert!(!unsafe { oakengine_track_nearest_block_after(track, 30) }.is_null()); - assert!(unsafe { oakengine_track_nearest_block_after(track, 70) }.is_null()); - assert!(!unsafe { oakengine_track_nearest_block_before_or_at(track, 35) }.is_null()); - assert!(!unsafe { oakengine_track_nearest_block_after_or_at(track, 40) }.is_null()); - - // Block enable + resize (undoable; in-point kept). - assert_eq!(unsafe { oakengine_block_set_enabled(blk_a, 0) }, 0); - assert_eq!(unsafe { oakengine_block_is_enabled(blk_a) }, 0); - assert_eq!(unsafe { oakengine_block_set_enabled(blk_a, 1) }, 0); - assert_eq!(unsafe { oakengine_block_is_enabled(blk_a) }, 1); - // Resize B from 30 frames to 60 (in kept: [40, 100)), then back. - assert_eq!( - unsafe { oakengine_block_set_length_and_media_out(blk_b, 60) }, - 0 - ); - assert_eq!( - unsafe { oakengine_block_get_range(blk_b, &mut bin2, &mut bout2) }, - 0 - ); - assert_eq!((bin2, bout2), (40, 100)); - assert_eq!( - unsafe { oakengine_block_set_length_and_media_out(blk_b, 30) }, - 0 - ); - assert_eq!( - unsafe { oakengine_block_get_range(blk_b, &mut bin2, &mut bout2) }, - 0 - ); - assert_eq!((bin2, bout2), (40, 70)); - - // ---- clip editing: split / trim / delete / ripple --- - // The module's BlockSplitCommand keeps the ORIGINAL block out-anchored - // (it becomes the second half) and in-anchors the fresh block (the first - // half): splitting [0, 30) at frame 20 yields [20, 30) + [0, 20). - assert_eq!( - unsafe { oakengine_sequence_split_clip(seq, 0, 0, 0, 20) }, - 0 - ); - assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 0, 0) }, 3); - // Split outside the clip -> E_INVALID + last error. - assert_eq!( - unsafe { oakengine_sequence_split_clip(seq, 0, 0, 0, 100) }, - -1 - ); - unsafe { assert_last_error() }; - // Split a nonexistent clip -> E_NOT_FOUND. - assert_eq!( - unsafe { oakengine_sequence_split_clip(seq, 0, 9, 0, 10) }, - -4 - ); - unsafe { assert_last_error() }; - - // Geometry after the split: clip0 (the original, now the second half) = - // [20, 30), clip1 (the new first half) = [0, 20), B = [40, 70). - let a2 = unsafe { clip_at_ok(seq, 0, 0, 0) }; - let a1 = unsafe { clip_at_ok(seq, 0, 0, 1) }; - let (mut s0in, mut s0out, mut s0mi) = (-1i64, -1i64, -1i64); - assert_eq!( - unsafe { oakengine_clip_get_range(a2, &mut s0in, &mut s0out, &mut s0mi) }, - 0 - ); - assert_eq!((s0in, s0out), (20, 30)); - let (mut s1in, mut s1out, mut s1mi) = (-1i64, -1i64, -1i64); - assert_eq!( - unsafe { oakengine_clip_get_range(a1, &mut s1in, &mut s1out, &mut s1mi) }, - 0 - ); - assert_eq!((s1in, s1out), (0, 20)); - - // Trim A2 to [25, 35) (trim works on any clip geometry). - assert_eq!(unsafe { oakengine_clip_trim(a2, 25, 35) }, 0); - assert_eq!( - unsafe { oakengine_clip_get_range(a2, &mut cin, &mut cout, &mut cmi) }, - 0 - ); - assert_eq!((cin, cout), (25, 35)); - // Invalid trim -> E_INVALID. - assert_eq!(unsafe { oakengine_clip_trim(a2, 30, 30) }, -1); - assert_eq!(unsafe { oakengine_clip_trim(a2, -1, 10) }, -1); - unsafe { assert_last_error() }; - - // Keep a2's box for the later stub checks (the box stays valid even - // after the clip is removed from the track — the node stays in the - // sequence's scratch graph). - let mut remaining = a2; - - // Batch split: the module's `BlockSplitPreservingLinksCommand` derives - // its child `BlockSplitCommand`s on first redo (the oakundo vtable path - // never calls `prepare()`), so splitting a2 = [25, 35) at 28 yields the - // out-anchored second half [28, 35) plus the in-anchored first half - // [0, 3), and the count rises to 4. - let mut a2_ptr = a2; - assert_eq!( - unsafe { oakengine_sequence_split_clips(seq, &mut a2_ptr, 1, 28) }, - 0 - ); - assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 0, 0) }, 4); - let (mut a2in, mut a2out, mut a2mi) = (-1i64, -1i64, -1i64); - assert_eq!( - unsafe { oakengine_clip_get_range(a2, &mut a2in, &mut a2out, &mut a2mi) }, - 0 - ); - assert_eq!((a2in, a2out), (28, 35)); - // No clip spans the time -> E_NOT_FOUND. - assert_eq!( - unsafe { oakengine_sequence_split_clips(seq, &mut a2_ptr, 1, 5) }, - -4 - ); - unsafe { assert_last_error() }; - // NULL / zero-count args -> E_INVALID. - assert_eq!( - unsafe { oakengine_sequence_split_clips(seq, std::ptr::null_mut(), 0, 15) }, - -1 - ); - unsafe { assert_last_error() }; - - // trim_clips_to: used to build its trim command with the TRACK handle - // where the BLOCK handle belongs (`trim_cmd(track, ...)` in - // src/timeline.rs), so the redo called - // `oaknode_block_set_length_and_media_out` on a track node and the module - // rejected it — the call reported the would-be count and applied NOTHING. - // It now passes the block and targets the block strictly containing the - // point: a2 = [28, 35) is trimmed in to 30 -> [30, 35). - assert_eq!(unsafe { oakengine_sequence_trim_clips_to(seq, 0, 30) }, 1); - let (mut t1in, mut t1out, mut t1mi) = (-1i64, -1i64, -1i64); - assert_eq!( - unsafe { oakengine_clip_get_range(a1, &mut t1in, &mut t1out, &mut t1mi) }, - 0 - ); - assert_eq!((t1in, t1out), (0, 20)); - assert_eq!( - unsafe { oakengine_clip_get_range(a2, &mut t1in, &mut t1out, &mut t1mi) }, - 0 - ); - assert_eq!((t1in, t1out), (30, 35)); - assert_eq!(unsafe { oakengine_sequence_trim_clips_to(seq, 2, 30) }, -1); // bad edge - unsafe { assert_last_error() }; - - // move_clip: a time-only move within the addressed track — the capi's - // gap+place assembly, one undoable entry. a2 = [30, 35) -> [50, 55) - // (length and media-in kept), and the move is fully reversible. - let base_move = unsafe { oakengine_undo_index() }; - assert_eq!(unsafe { oakengine_sequence_move_clip(seq, 0, 0, 0, 50) }, 0); - let (mut mv_in, mut mv_out, mut mv_mi) = (-1i64, -1i64, -1i64); - assert_eq!( - unsafe { oakengine_clip_get_range(a2, &mut mv_in, &mut mv_out, &mut mv_mi) }, - 0 - ); - assert_eq!((mv_in, mv_out, mv_mi), (50, 55, 30)); - // Undo restores the original spot; redo re-applies the move. - assert_eq!(unsafe { oakengine_undo_jump(base_move) }, 0); - assert_eq!( - unsafe { oakengine_clip_get_range(a2, &mut mv_in, &mut mv_out, &mut mv_mi) }, - 0 - ); - assert_eq!((mv_in, mv_out), (30, 35)); - assert_eq!(unsafe { oakengine_undo_jump(base_move + 1) }, 0); - assert_eq!( - unsafe { oakengine_clip_get_range(a2, &mut mv_in, &mut mv_out, &mut mv_mi) }, - 0 - ); - assert_eq!((mv_in, mv_out), (50, 55)); - // Back to the pre-move state for the checks below. - assert_eq!(unsafe { oakengine_undo_jump(base_move) }, 0); - // Invalid arguments -> E_INVALID; nonexistent clip -> E_NOT_FOUND. - assert_eq!( - unsafe { oakengine_sequence_move_clip(seq, 0, 0, 0, -1) }, - -1 - ); - unsafe { assert_last_error() }; - assert_eq!( - unsafe { oakengine_sequence_move_clip(seq, 5, 0, 0, 50) }, - -1 - ); // bad track type - unsafe { assert_last_error() }; - assert_eq!( - unsafe { oakengine_sequence_move_clip(seq, 0, 9, 0, 50) }, - -4 - ); - unsafe { assert_last_error() }; - - // Batch delete: remove the clip at clip-index 1 (the batch-split first - // half [0, 3), which the module inserted after a2) leaving a gap (no - // ripple). - let a1b = unsafe { clip_at_ok(seq, 0, 0, 1) }; - let mut rippled = -1; - let mut a1b_ptr = a1b; - assert_eq!( - unsafe { - oakengine_sequence_delete_clips( - seq, - &mut a1b_ptr, - 1, - 0, - std::ptr::null(), - 0, - &mut rippled, - ) - }, - 0 - ); - assert_eq!(rippled, 0); - assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 0, 0) }, 3); - // Batch delete with ripple=1 ripples the deleted clip's range closed: the - // module's `TimelineRippleDeleteGapsAtRegionsCommand` derives its - // per-region commands on first redo, so the gap left at [0, 20) by a1 is - // removed again. - let b3 = unsafe { clip_at_ok(seq, 0, 0, 1) }; - let mut b3_ptr = b3; - assert_eq!( - unsafe { - oakengine_sequence_delete_clips( - seq, - &mut b3_ptr, - 1, - 1, - std::ptr::null(), - 0, - &mut rippled, - ) - }, - 0 - ); - assert_eq!(rippled, 1); - assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 0, 0) }, 2); - // Empty batch (count 0, no ripple) is a clean no-op. - assert_eq!( - unsafe { - oakengine_sequence_delete_clips( - seq, - std::ptr::null_mut(), - 0, - 0, - std::ptr::null(), - 0, - &mut rippled, - ) - }, - 0 - ); - assert_eq!(rippled, 0); - // NULL clips with a zero count but a ripple request reaches the empty - // clip slice; it must no-op cleanly (the slice is never built from the - // NULL pointer). The ripple region on the empty subtitle track changes - // nothing. - let empty_range = [2i64, 0, 0, 10]; - assert_eq!( - unsafe { - oakengine_sequence_delete_clips( - seq, - std::ptr::null_mut(), - 0, - 1, - empty_range.as_ptr(), - 1, - &mut rippled, - ) - }, - 0 - ); - assert_eq!(rippled, 1); - // Bad ripple-range track type -> E_INVALID. - let bad_range = [3i64, 0, 0, 10]; - assert_eq!( - unsafe { - oakengine_sequence_delete_clips( - seq, - &mut a1b_ptr, - 0, - 1, - bad_range.as_ptr(), - 1, - &mut rippled, - ) - }, - -1 - ); - unsafe { assert_last_error() }; - - // Ripple delete the addressed clip: the module's - // `TrackRippleRemoveAreaCommand` derives its operations on first redo - // (the oakundo vtable path never calls `prepare()`), so the addressed - // clip is actually removed. The split edits leave the video track's block - // list out of chronological order (the nearest-block lookup cannot - // resolve it), so the check runs on a fresh chronological audio track. - let atrack = unsafe { oakengine_sequence_track_at(seq, 1, 0) }; - assert!(!atrack.is_null()); - unsafe { module_clip_on((*atrack).handle, 0, 1) }; - assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 1, 0) }, 1); - assert_eq!( - unsafe { oakengine_sequence_ripple_delete_clip(seq, 1, 0, 0) }, - 0 - ); - assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 1, 0) }, 0); - assert_eq!( - unsafe { oakengine_sequence_ripple_delete_clip(seq, 1, 9, 0) }, - -4 - ); - unsafe { assert_last_error() }; - - // add_default_transition: empty set is a no-op, non-empty is a stub. - assert_eq!( - unsafe { oakengine_sequence_add_default_transition(seq, std::ptr::null_mut(), 0) }, - 0 - ); - assert_eq!( - unsafe { oakengine_sequence_add_default_transition(seq, &mut remaining, 1) }, - -2 - ); - unsafe { assert_last_error() }; - - // Ripple delete a range: same self-deriving command; the range [0, 30) - // covers the fresh audio clip [0, 30) entirely, so it is removed, while - // the video track keeps its two remaining clips (a2 and B). - unsafe { module_clip_on((*atrack).handle, 0, 1) }; - assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 1, 0) }, 1); - assert_eq!( - unsafe { oakengine_sequence_ripple_delete_range(seq, 0, 30) }, - 0 - ); - assert_eq!( - unsafe { oakengine_sequence_ripple_delete_range(seq, 10, 10) }, - -1 - ); // empty range - unsafe { assert_last_error() }; - assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 1, 0) }, 0); - assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 0, 0) }, 2); - unsafe { free_box::(atrack) }; - - // ---- add_default_nodes + remove_track + delete_empty_tracks --- - // Runs after the clip phase so video track 0 keeps its content. - assert_eq!(unsafe { oakengine_sequence_add_default_nodes(seq) }, 0); - assert_eq!( - unsafe { oakengine_sequence_track_count(seq, &mut v, &mut a, &mut s) }, - 0 - ); - assert_eq!((v, a, s), (3, 2, 1)); - - assert_eq!(unsafe { oakengine_sequence_remove_track(seq, 1, 0) }, 0); - assert_eq!( - unsafe { oakengine_sequence_track_count(seq, &mut v, &mut a, &mut s) }, - 0 - ); - assert_eq!((v, a, s), (3, 1, 1)); - assert_eq!(unsafe { oakengine_sequence_remove_track(seq, 1, 5) }, -4); - unsafe { assert_last_error() }; - - // delete_empty_tracks: unlike `oakengine_sequence_remove_track` it used - // to skip the live `oaknode_tracklist_remove_track` compensation (the - // module's `TimelineRemoveTrackCommand::redo` is a documented no-op), so - // the pushed commands changed nothing. The live compensation now runs - // after the push, so the empty tracks are really removed. - assert_eq!( - unsafe { oakengine_sequence_delete_empty_tracks(seq, -1) }, - 4 - ); - assert_eq!(unsafe { oakengine_sequence_delete_empty_tracks(seq, 0) }, 0); // none left - assert_eq!( - unsafe { oakengine_sequence_delete_empty_tracks(seq, 99) }, - -1 - ); - unsafe { assert_last_error() }; - // Only the content-bearing video track 0 remains. - assert_eq!( - unsafe { oakengine_sequence_track_count(seq, &mut v, &mut a, &mut s) }, - 0 - ); - assert_eq!((v, a, s), (1, 0, 0)); - - // ---- detached clip created by the facade --------------------------------- - // The block-family accessors take `OakEngineBlock*`; the clip box is the - // layout-identical `OakEngineClip` wrapper, so it is cast (both are - // `#[repr(C)]` wrappers around one CHandle). - let detached = unsafe { oakengine_clip_create_empty(c"Detached".as_ptr()) }; - assert!(!detached.is_null()); - let dblk = detached.cast::(); - assert_eq!(unsafe { oakengine_clip_is_enabled(detached) }, 1); - assert_eq!(unsafe { oakengine_block_is_enabled(dblk) }, 1); - assert_eq!(unsafe { oakengine_block_set_enabled(dblk, 0) }, 0); - assert_eq!(unsafe { oakengine_block_is_enabled(dblk) }, 0); - assert_eq!(unsafe { oakengine_block_set_enabled(dblk, 1) }, 0); - assert_eq!(unsafe { oakengine_block_is_gap(dblk) }, 0); - assert_eq!(unsafe { oakengine_block_link_count(dblk) }, 0); - assert!(unsafe { oakengine_block_link_at(dblk, 0) }.is_null()); - assert!(unsafe { oakengine_block_get_track(dblk) }.is_null()); - assert!(unsafe { oakengine_block_next(dblk) }.is_null()); - assert!(unsafe { oakengine_block_prev(dblk) }.is_null()); - assert!(unsafe { oakengine_clip_get_sequence(detached) }.is_null()); - assert!(unsafe { oakengine_clip_in_transition(dblk) }.is_null()); - assert!(unsafe { oakengine_clip_out_transition(dblk) }.is_null()); - assert!(unsafe { oakengine_transition_connected_in_block(dblk) }.is_null()); - assert!(unsafe { oakengine_transition_connected_out_block(dblk) }.is_null()); - assert!(unsafe { oakengine_clip_get_connected_viewer(dblk) }.is_null()); - // Detached clips are trackless: media queries work, edits need a track. - // NOTE: the frame-timestamp media-in setter needs the track for its - // timebase, but the rational variant applies directly (no track needed). - assert_eq!( - unsafe { oakengine_clip_get_media_in_rational(detached, &mut mn, &mut md) }, - 0 - ); - assert_eq!((mn, md), (0, 1)); - assert_eq!(unsafe { oakengine_clip_set_media_in(detached, 5, 0) }, -2); // needs a track timebase - assert_eq!( - unsafe { oakengine_clip_set_media_in_rational(detached, 1, 1, 0) }, - 0 - ); - assert_eq!( - unsafe { oakengine_clip_get_media_in_rational(detached, &mut mn, &mut md) }, - 0 - ); - assert_eq!((mn, md), (1, 1)); - assert_eq!( - unsafe { oakengine_clip_set_media_in_rational(detached, 0, 1, 0) }, - 0 - ); - assert_eq!( - unsafe { oakengine_clip_set_media_in_rational(detached, 1, 0, 0) }, - -1 - ); // den 0 - unsafe { assert_last_error() }; - assert_eq!(unsafe { oakengine_clip_trim(detached, 0, 10) }, -2); - assert_eq!( - unsafe { oakengine_block_set_length_and_media_out(dblk, 40) }, - -2 - ); - assert_eq!( - unsafe { oakengine_clip_get_range(detached, &mut cin, &mut cout, &mut cmi) }, - -2 - ); - unsafe { assert_last_error() }; - // Cache no-op stubs on a live handle. - unsafe { oakengine_clip_request_invalidate(detached, 0, 10, 1) }; - unsafe { oakengine_clip_request_invalidate_connected(detached, 0, 0, 1, 1, 1) }; - unsafe { oakengine_clip_discard_cache(detached) }; - unsafe { oakengine_clip_add_cache_passthrough(detached, remaining) }; - // NULL-label variant also creates a clip. - let detached2 = unsafe { oakengine_clip_create_empty(std::ptr::null()) }; - assert!(!detached2.is_null()); - unsafe { free_box::(detached2) }; - - // ---- node helpers over a boxed module clip node --------------------------- - let node_box = unsafe { box_handle::((*remaining).handle) }; - assert_eq!(unsafe { oakengine_node_is_block(node_box) }, 1); - assert_eq!(unsafe { oakengine_node_is_transition(node_box) }, 0); - assert_eq!( - unsafe { - oakengine_multicam_switch_source( - node_box, - std::ptr::null_mut(), - 0, - 0, - 0.0, - std::ptr::null_mut(), - ) - }, - 0 - ); - assert!(unsafe { oakengine_clip_find_multicam(node_box) }.is_null()); - unsafe { free_box::(node_box) }; - - // ---- standalone workarea handle family ------------------------------------- - let wa = unsafe { oakengine_workarea_create() }; - assert!(!wa.is_null()); - let (mut wn0, mut wd0, mut wn1, mut wd1, mut wen) = (-1i64, -1i64, -1i64, -1i64, -1); - assert_eq!( - unsafe { oakengine_workarea_get(wa, &mut wn0, &mut wd0, &mut wn1, &mut wd1, &mut wen) }, - 0 - ); - assert_eq!((wn0, wd0, wn1, wd1, wen), (0, 1, 2147483647, 1, 0)); - assert_eq!(unsafe { oakengine_workarea_set_range(wa, 10, 1, 20, 1) }, 0); - assert_eq!(unsafe { oakengine_workarea_set_enabled(wa, 1) }, 0); - assert_eq!( - unsafe { oakengine_workarea_get(wa, &mut wn0, &mut wd0, &mut wn1, &mut wd1, &mut wen) }, - 0 - ); - assert_eq!((wn0, wd0, wn1, wd1, wen), (10, 1, 20, 1, 1)); - // Undoable variants (pushed, not added to a parent). - assert_eq!( - unsafe { - oakengine_workarea_set_range_undoable( - wa, - 30, - 1, - 40, - 1, - 10, - 1, - 20, - 1, - std::ptr::null_mut(), - ) - }, - 0 - ); - assert_eq!( - unsafe { oakengine_workarea_set_enabled_undoable(wa, 0, std::ptr::null_mut()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_workarea_get(wa, &mut wn0, &mut wd0, &mut wn1, &mut wd1, &mut wen) }, - 0 - ); - assert_eq!((wn0, wd0, wn1, wd1, wen), (30, 1, 40, 1, 0)); - // Reset sentinels: in = 0/1, out = RATIONAL_MAX/1. - let (mut rn, mut rd, mut ron, mut rod) = (-1i64, -1i64, -1i64, -1i64); - unsafe { oakengine_workarea_reset_in_out(&mut rn, &mut rd, &mut ron, &mut rod) }; - assert_eq!((rn, rd), (0, 1)); - assert_eq!((ron, rod), (2147483647, 1)); - // Free contracts: NULL, empty box, then the live handle. - unsafe { oakengine_workarea_free(std::ptr::null_mut()) }; - let empty_wa = unsafe { empty_box!(OakEngineWorkarea) }; - unsafe { oakengine_workarea_free(empty_wa) }; - unsafe { oakengine_workarea_free(wa) }; - - // ---- standalone marker-list handle family ----------------------------------- - let list_h = unsafe { oakengine::bridge::timeline::oaktimeline_marker_list_create() }; - assert!(!list_h.is_null()); - let list = unsafe { box_handle::(list_h) }; - assert_eq!(unsafe { oakengine_marker_list_count(list) }, 0); - assert_eq!( - unsafe { oakengine_marker_list_add(list, 1, 1, 1, 1, c"lm".as_ptr(), 5) }, - 0 - ); - assert_eq!( - unsafe { oakengine_marker_list_add(list, 5, 1, 5, 1, std::ptr::null(), 1) }, - 0 - ); - assert_eq!(unsafe { oakengine_marker_list_count(list) }, 2); - - let m1 = unsafe { oakengine_marker_list_at(list, 0) }; - assert!(!m1.is_null()); - assert!(unsafe { oakengine_marker_list_at(list, 5) }.is_null()); - let m2 = unsafe { oakengine_marker_list_marker_at_time(list, 1, 1) }; - assert!(!m2.is_null()); - assert!(unsafe { oakengine_marker_list_marker_at_time(list, 9, 1) }.is_null()); - - // Marker getters (rational time, buf/size name, color). - let (mut g0, mut g1, mut g2, mut g3) = (-1i64, -1i64, -1i64, -1i64); - assert_eq!( - unsafe { oakengine_marker_get_time(m1, &mut g0, &mut g1, &mut g2, &mut g3) }, - 0 - ); - assert_eq!((g0, g1, g2, g3), (1, 1, 1, 1)); - let mut mname2 = [0 as c_char; 64]; - assert_eq!( - unsafe { oakengine_marker_get_name(m1, mname2.as_mut_ptr(), 64) }, - 2 - ); - assert_eq!(unsafe { read_buf(&mut mname2) }, "lm"); - assert_eq!( - unsafe { oakengine_marker_get_name(m1, std::ptr::null_mut(), 0) }, - 2 - ); - assert_eq!(unsafe { oakengine_marker_get_color(m1) }, 5); - assert_eq!(unsafe { oakengine_marker_has_sibling_at_time(m1, 1, 1) }, 0); - - // Time edits (live + command + commit). - assert_eq!(unsafe { oakengine_marker_set_time_live(m1, 2, 1, 2, 1) }, 0); - assert_eq!( - unsafe { oakengine_marker_get_time(m1, &mut g0, &mut g1, &mut g2, &mut g3) }, - 0 - ); - assert_eq!((g0, g1, g2, g3), (2, 1, 2, 1)); - let time_cmd = unsafe { oakengine_marker_set_time_command(m1, 3, 1) }; - assert!(!time_cmd.is_null()); - unsafe { oakengine_undo_command_free(time_cmd) }; - assert!(unsafe { oakengine_marker_set_time_command(m1, 3, 0) }.is_null()); // den 0 - assert_eq!( - unsafe { oakengine_marker_commit_time(m1, 2, 1, 2, 1, 4, 1, 4, 1, std::ptr::null_mut()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_marker_get_time(m1, &mut g0, &mut g1, &mut g2, &mut g3) }, - 0 - ); - assert_eq!((g0, g1, g2, g3), (4, 1, 4, 1)); - - // Re-add the marker (its data is read back through the list). - assert_eq!(unsafe { oakengine_marker_list_add_existing(list, m1) }, 0); - assert_eq!(unsafe { oakengine_marker_list_count(list) }, 3); - - // Batch property set (color + name, one undoable command). - assert_eq!( - unsafe { - oakengine_marker_set_properties( - [m1].as_mut_ptr(), - 1, - 7, - c"renamed".as_ptr(), - 0, - 0, - 1, - 0, - 1, - std::ptr::null_mut(), - ) - }, - 0 - ); - assert_eq!(unsafe { oakengine_marker_get_color(m1) }, 7); - assert_eq!( - unsafe { oakengine_marker_get_name(m1, mname2.as_mut_ptr(), 64) }, - 7 - ); - assert_eq!(unsafe { read_buf(&mut mname2) }, "renamed"); - // No-op property set (nothing to change) succeeds with zero commands. - assert_eq!( - unsafe { - oakengine_marker_set_properties( - [m1].as_mut_ptr(), - 1, - -1, - std::ptr::null(), - 0, - 0, - 1, - 0, - 1, - std::ptr::null_mut(), - ) - }, - 0 - ); - // NULL markers / zero count -> E_INVALID. - assert_eq!( - unsafe { - oakengine_marker_set_properties( - std::ptr::null_mut(), - 0, - 0, - std::ptr::null(), - 0, - 0, - 1, - 0, - 1, - std::ptr::null_mut(), - ) - }, - -1 - ); - - // Remove the marker (undoable). - assert_eq!(unsafe { oakengine_marker_remove(m1) }, 0); - assert_eq!(unsafe { oakengine_marker_list_count(list) }, 2); - // Detached-marker creation is a stub -> NULL. - assert!(unsafe { oakengine_marker_create(0, 0, 1, 0, 1, std::ptr::null()) }.is_null()); - // Free contracts: NULL, then the (borrowed) marker box. - unsafe { oakengine_marker_free(std::ptr::null_mut()) }; - let empty_marker = unsafe { empty_box!(OakEngineMarker) }; - unsafe { oakengine_marker_free(empty_marker) }; - unsafe { oakengine_marker_free(m1) }; - unsafe { free_box::(m2) }; - // The surviving duplicate is the copy made by add_existing BEFORE the - // property set (m1 — the re-colored original — was index 0 and was the - // one removed), so it carries the original color 5 / name "lm". - let dup = unsafe { oakengine_marker_list_marker_at_time(list, 4, 1) }; - assert!(!dup.is_null()); - assert_eq!(unsafe { oakengine_marker_get_color(dup) }, 5); - assert_eq!( - unsafe { oakengine_marker_get_name(dup, mname2.as_mut_ptr(), 64) }, - 2 - ); - assert_eq!(unsafe { read_buf(&mut mname2) }, "lm"); - unsafe { oakengine_marker_free(dup) }; - unsafe { free_box::(list) }; - - // ---- add_sequence_clip / add_footage_clip (documented clean failures) ------ - // Self-nesting is rejected. - assert!(unsafe { oakengine_sequence_add_sequence_clip(seq, seq, 0, 0, 0, 30, 0) }.is_null()); - unsafe { assert_last_error() }; - // A second sequence lives in its own scratch project -> cross-project. - assert!(unsafe { oakengine_sequence_add_sequence_clip(seq, seq2, 0, 0, 0, 30, 0) }.is_null()); - unsafe { assert_last_error() }; - // Subtitle sequence clips are unsupported. - assert!(unsafe { oakengine_sequence_add_sequence_clip(seq, seq2, 2, 0, 0, 30, 0) }.is_null()); - unsafe { assert_last_error() }; - // Invalid range. - assert!(unsafe { oakengine_sequence_add_sequence_clip(seq, seq2, 0, 0, 30, 30, 0) }.is_null()); - unsafe { assert_last_error() }; - // Footage clips: the footage lives in the real project, the sequence in - // its scratch project -> different projects -> clean NULL + error. - let footage_node = unsafe { - oaknode::ffi::footage::oaknode_footage_create( - (*project).handle, - c"/no/such/media.mp4".as_ptr(), - ) - }; - assert!(!footage_node.is_null()); - let footage_node_box = unsafe { box_handle::(footage_node) }; - let footage = unsafe { oakengine_footage_borrow(footage_node_box) }; - assert!(!footage.is_null()); - assert!(unsafe { oakengine_sequence_add_footage_clip(seq, footage, 0, 0, 0, 30, 0) }.is_null()); - unsafe { assert_last_error() }; - // Invalid clip range. - assert!( - unsafe { oakengine_sequence_add_footage_clip(seq, footage, 0, 0, 30, 30, 0) }.is_null() - ); - unsafe { assert_last_error() }; - // Bad track type (subtitle clips unsupported). - assert!(unsafe { oakengine_sequence_add_footage_clip(seq, footage, 2, 0, 0, 30, 0) }.is_null()); - unsafe { assert_last_error() }; - // Out-of-range track index. - assert!(unsafe { oakengine_sequence_add_footage_clip(seq, footage, 0, 9, 0, 30, 0) }.is_null()); - unsafe { assert_last_error() }; - unsafe { free_box::(footage) }; - unsafe { free_box::(footage_node_box) }; - - // ---- input ID getters (static strings) -------------------------------------- - let ids = [ - ( - oakengine_clip_buffer_input_id() as *const c_char, - "buffer_in", - ), - (oakengine_clip_speed_input_id() as *const c_char, "speed_in"), - ( - oakengine_clip_reverse_input_id() as *const c_char, - "reverse_in", - ), - ( - oakengine_clip_maintain_audio_pitch_input_id() as *const c_char, - "maintain_audio_pitch_in", - ), - ( - oakengine_clip_loop_mode_input_id() as *const c_char, - "loop_in", - ), - ( - oakengine_clip_auto_cache_input_id() as *const c_char, - "autocache_in", - ), - ]; - for (p, expect) in ids { - assert!(!p.is_null()); - assert_eq!( - unsafe { std::ffi::CStr::from_ptr(p) }.to_str().unwrap(), - expect - ); - } - - // ---- track height constants ------------------------------------------------- - assert_eq!(unsafe { oakengine_track_height_default() }, 3.0); - assert_eq!(unsafe { oakengine_track_default_height_in_pixels() }, 39); // 3.0 * 13px font - assert_eq!( - unsafe { oakengine_track_height_internal_to_pixels(3.0) }, - 39 - ); - assert_eq!( - unsafe { oakengine_track_height_pixels_to_internal(13) }, - 1.0 - ); - assert_eq!(unsafe { oakengine_track_height_interval() }, 0.5); - assert_eq!(unsafe { oakengine_track_height_minimum() }, 1.5); - - // ---- last_error is readable any time ---------------------------------------- - let mut err = [0 as c_char; 256]; - let elen = unsafe { oakengine_sequence_last_error(err.as_mut_ptr(), 256) }; - assert!(elen >= 0); - - // ---- cleanup: free the project shell. The alive count drops by exactly - // the project node; sequence/track/clip nodes stay live for the process - // (no `oakengine_sequence_free`; sequences keep their own scratch - // projects — see the module docs). - let alive_before_free = alive(); - unsafe { oakengine_project_free(project) }; - assert_eq!( - alive(), - alive_before_free - 1, - "project_free releases the project shell" - ); - - // Free the remaining borrowed clip/track boxes (they release borrowed - // module handles; the node objects stay owned by the scratch projects). - unsafe { free_box::(detached) }; - unsafe { free_box::(remaining) }; - unsafe { free_box::(track) }; -} - -// --------------------------------------------------------------------------- -// Non-mutating failure paths (NULL / empty-box / garbage arguments; no undo -// stack or per-sequence caches touched, so it runs in parallel) -// --------------------------------------------------------------------------- - -/// Every timeline export rejects NULL and empty-handle arguments with a -/// clean documented value — never a crash/abort/panic. -#[test] -fn timeline_zu_failure_paths() { - let _zu = zu_lock(); - common::force_link(); - let _ = force_runtime_syms(); - - let mut buf = [0 as c_char; 256]; - let mut n = 0i64; - let mut d = 0i64; - let mut i32v = 0; - let mut i32w = 0; - let mut f64v = 0.0; - let mut u64v = 0u64; - - // ---- sequence family: NULL handles --------------------------------------- - assert!(unsafe { oakengine_sequence_new(std::ptr::null_mut(), c"x".as_ptr()) }.is_null()); - assert_eq!( - unsafe { oakengine_sequence_name(std::ptr::null(), buf.as_mut_ptr(), 64) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_get_length(std::ptr::null(), &mut f64v) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_get_length_rational(std::ptr::null(), &mut i32v, &mut i32w) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_get_frame_rate(std::ptr::null(), &mut i32v, &mut i32w) }, - -1 - ); - assert_eq!( - unsafe { - oakengine_sequence_get_video_params( - std::ptr::null(), - &mut i32v, - &mut i32w, - &mut i32v, - &mut i32w, - ) - }, - -1 - ); - assert_eq!( - unsafe { - oakengine_sequence_get_video_params_ex( - std::ptr::null(), - &mut i32v, - &mut i32w, - &mut i32v, - &mut i32w, - &mut i32v, - &mut i32w, - &mut i32v, - &mut i32w, - &mut i32v, - ) - }, - -1 - ); - assert_eq!( - unsafe { - oakengine_sequence_set_video_params( - std::ptr::null_mut(), - 1920, - 1080, - 30, - 1, - 1, - 1, - 0, - 4, - 0, - ) - }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_get_audio_params(std::ptr::null(), &mut i32v, &mut u64v) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_set_audio_params(std::ptr::null_mut(), 48000, 0x3, 1) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_get_preview_divider(std::ptr::null()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_sequence_set_preview_divider(std::ptr::null_mut(), 1, 0) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_get_video_auto_cache(std::ptr::null()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_sequence_set_video_auto_cache(std::ptr::null_mut(), 1, 1) }, - -1 - ); - assert_eq!( - unsafe { - oakengine_sequence_track_count(std::ptr::null(), &mut i32v, &mut i32v, &mut i32v) - }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_get_playhead(std::ptr::null(), &mut n) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_set_playhead(std::ptr::null_mut(), 0) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_get_playhead_seconds(std::ptr::null(), &mut f64v) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_workarea_is_enabled(std::ptr::null()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_sequence_get_workarea(std::ptr::null(), &mut n, &mut d) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_set_workarea(std::ptr::null_mut(), 1, 0, 10) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_marker_count(std::ptr::null()) }, - 0 - ); - assert_eq!( - unsafe { - oakengine_sequence_marker_at( - std::ptr::null(), - 0, - &mut n, - buf.as_mut_ptr(), - 64, - &mut i32v, - ) - }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_marker_add(std::ptr::null_mut(), 0, c"x".as_ptr()) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_marker_add_ex(std::ptr::null_mut(), 0, c"x".as_ptr(), 0) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_marker_remove(std::ptr::null_mut(), 0) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_marker_rename(std::ptr::null_mut(), 0, c"x".as_ptr()) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_marker_remove_many(std::ptr::null_mut(), &n, 1) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_marker_remove_many(std::ptr::null_mut(), std::ptr::null(), 0) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_add_track(std::ptr::null_mut(), 0) }, - -1 - ); - assert!(unsafe { - oakengine_sequence_add_track_command(std::ptr::null_mut(), 0, 0, std::ptr::null_mut()) - } - .is_null()); - assert!(unsafe { - oakengine_sequence_ripple_tracks_command( - std::ptr::null_mut(), - 0, - std::ptr::null(), - 0, - 0, - 0, - 99, - ) - } - .is_null()); - assert!(unsafe { - oakengine_sequence_add_footage_clip( - std::ptr::null_mut(), - std::ptr::null_mut(), - 0, - 0, - 0, - 1, - 0, - ) - } - .is_null()); - assert!(unsafe { - oakengine_sequence_add_sequence_clip( - std::ptr::null_mut(), - std::ptr::null_mut(), - 0, - 0, - 0, - 1, - 0, - ) - } - .is_null()); - assert_eq!( - unsafe { oakengine_sequence_clip_count(std::ptr::null_mut(), 0, 0) }, - -1 - ); - assert!(unsafe { oakengine_sequence_clip_at(std::ptr::null_mut(), 0, 0, 0) }.is_null()); - assert_eq!( - unsafe { oakengine_sequence_split_clip(std::ptr::null_mut(), 0, 0, 0, 10) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_ripple_delete_clip(std::ptr::null_mut(), 0, 0, 0) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_move_clip(std::ptr::null_mut(), 0, 0, 0, 10) }, - -1 - ); - assert_eq!( - unsafe { - oakengine_sequence_split_clips(std::ptr::null_mut(), std::ptr::null_mut(), 0, 10) - }, - -1 - ); - let mut rippled = -1; - assert_eq!( - unsafe { - oakengine_sequence_delete_clips( - std::ptr::null_mut(), - std::ptr::null_mut(), - 0, - 0, - std::ptr::null(), - 0, - &mut rippled, - ) - }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_ripple_delete_range(std::ptr::null_mut(), 0, 10) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_ripple_delete_in_to_out(std::ptr::null_mut(), 0, 0, 10) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_trim_clips_to(std::ptr::null_mut(), 0, 10) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_delete_empty_tracks(std::ptr::null_mut(), -1) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_remove_track(std::ptr::null_mut(), 0, 0) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_move_track(std::ptr::null_mut(), 0, 0, 1) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_add_default_nodes(std::ptr::null_mut()) }, - -1 - ); - // add_default_transition IGNORES the sequence handle entirely: NULL seq - // with an empty clip set is a clean no-op, NULL seq with clips -> E_INVALID. - assert_eq!( - unsafe { - oakengine_sequence_add_default_transition(std::ptr::null_mut(), std::ptr::null_mut(), 0) - }, - 0 - ); - assert_eq!( - unsafe { - oakengine_sequence_add_default_transition(std::ptr::null_mut(), std::ptr::null_mut(), 1) - }, - -1 - ); - assert!(unsafe { oakengine_sequence_track_at(std::ptr::null(), 0, 0) }.is_null()); - assert!(unsafe { oakengine_sequence_track_list(std::ptr::null_mut(), 0) }.is_null()); - - // ---- clip / block / track families: NULL handles -------------------------- - assert_eq!( - unsafe { - oakengine_clip_get_range( - std::ptr::null(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - -1 - ); - assert!(unsafe { oakengine_clip_get_sequence(std::ptr::null()) }.is_null()); - assert_eq!( - unsafe { - oakengine_clip_get_media_range_rational( - std::ptr::null(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - -1 - ); - assert_eq!( - unsafe { - oakengine_clip_get_media_in_rational( - std::ptr::null(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - -1 - ); - assert_eq!( - unsafe { oakengine_clip_set_media_in(std::ptr::null_mut(), 0, 0) }, - -1 - ); - assert_eq!( - unsafe { oakengine_clip_set_media_in_rational(std::ptr::null_mut(), 0, 1, 0) }, - -1 - ); - assert_eq!( - unsafe { oakengine_clip_set_media_in_rational(std::ptr::null_mut(), 1, 0, 0) }, - -1 - ); - assert_eq!(unsafe { oakengine_clip_is_enabled(std::ptr::null()) }, 0); - assert_eq!( - unsafe { oakengine_clip_are_linked(std::ptr::null(), std::ptr::null()) }, - 0 - ); - // NULL with a zero count is a legal empty set: the toggle must no-op - // cleanly (the empty slice is never built from the NULL pointer — that - // used to abort the process with a non-unwinding UB panic). - assert_eq!( - unsafe { oakengine_clip_toggle_enabled(std::ptr::null_mut(), 0) }, - 0 - ); - assert_eq!( - unsafe { oakengine_clip_toggle_enabled(std::ptr::null_mut(), 1) }, - -1 - ); - assert_eq!( - unsafe { oakengine_clip_set_linked(std::ptr::null_mut(), 0, 1) }, - 0 - ); - assert_eq!( - unsafe { oakengine_clip_set_linked(std::ptr::null_mut(), 1, 1) }, - -1 - ); - // NOTE: `oakengine_clip_create_empty` is exercised in the serialized - // lifecycle test (it creates an oaknode node, which would race the - // alive-count assertions there if called from this parallel test). - unsafe { oakengine_clip_request_invalidate(std::ptr::null_mut(), 0, 10, 1) }; - unsafe { oakengine_clip_request_invalidate_connected(std::ptr::null_mut(), 0, 0, 1, 1, 1) }; - unsafe { oakengine_clip_discard_cache(std::ptr::null_mut()) }; - unsafe { oakengine_clip_add_cache_passthrough(std::ptr::null_mut(), std::ptr::null_mut()) }; - assert!(unsafe { oakengine_clip_in_transition(std::ptr::null()) }.is_null()); - assert!(unsafe { oakengine_clip_out_transition(std::ptr::null()) }.is_null()); - assert!(unsafe { oakengine_clip_get_connected_viewer(std::ptr::null()) }.is_null()); - - assert_eq!(unsafe { oakengine_block_is_enabled(std::ptr::null()) }, 0); - assert_eq!( - unsafe { oakengine_block_set_enabled(std::ptr::null_mut(), 1) }, - -1 - ); - assert_eq!( - unsafe { oakengine_block_set_length_and_media_out(std::ptr::null_mut(), 10) }, - -1 - ); - assert_eq!( - unsafe { oakengine_block_set_length_and_media_out(std::ptr::null_mut(), 0) }, - -1 - ); - assert_eq!(unsafe { oakengine_block_is_gap(std::ptr::null()) }, 0); - assert!(unsafe { oakengine_block_get_track(std::ptr::null()) }.is_null()); - assert!(unsafe { oakengine_block_next(std::ptr::null()) }.is_null()); - assert!(unsafe { oakengine_block_prev(std::ptr::null()) }.is_null()); - assert_eq!( - unsafe { - oakengine_block_get_range(std::ptr::null(), std::ptr::null_mut(), std::ptr::null_mut()) - }, - -1 - ); - assert_eq!(unsafe { oakengine_block_link_count(std::ptr::null()) }, 0); - assert!(unsafe { oakengine_block_link_at(std::ptr::null(), 0) }.is_null()); - - assert_eq!(unsafe { oakengine_track_block_count(std::ptr::null()) }, -1); - assert!(unsafe { oakengine_track_block_at(std::ptr::null(), 0) }.is_null()); - assert!(unsafe { oakengine_track_block_at_time(std::ptr::null(), 0) }.is_null()); - assert!(unsafe { oakengine_track_visible_block_at_time(std::ptr::null_mut(), 0) }.is_null()); - assert!(unsafe { oakengine_track_nearest_block_before(std::ptr::null(), 0) }.is_null()); - assert!(unsafe { oakengine_track_nearest_block_after(std::ptr::null(), 0) }.is_null()); - assert!(unsafe { oakengine_track_nearest_block_before_or_at(std::ptr::null(), 0) }.is_null()); - assert!(unsafe { oakengine_track_nearest_block_after_or_at(std::ptr::null(), 0) }.is_null()); - assert_eq!(unsafe { oakengine_track_type(std::ptr::null()) }, -1); - assert_eq!( - unsafe { oakengine_track_get_height(std::ptr::null(), 0, 0, &mut f64v) }, - -1 - ); - assert_eq!( - unsafe { oakengine_track_set_height(std::ptr::null_mut(), 0, 0, 1.0) }, - -1 - ); - assert_eq!( - unsafe { oakengine_track_is_muted(std::ptr::null(), 0, 0) }, - 0 - ); - assert_eq!( - unsafe { oakengine_track_set_muted(std::ptr::null_mut(), 0, 0, 1) }, - -1 - ); - assert_eq!( - unsafe { oakengine_track_is_locked(std::ptr::null(), 0, 0) }, - 0 - ); - assert_eq!( - unsafe { oakengine_track_set_locked(std::ptr::null_mut(), 0, 0, 1) }, - -1 - ); - assert_eq!( - unsafe { oakengine_track_get_length(std::ptr::null(), 0, 0, &mut n) }, - -1 - ); - assert_eq!( - unsafe { oakengine_track_is_range_free(std::ptr::null(), 0, 0, 0, 10) }, - -1 - ); - assert_eq!( - unsafe { oakengine_track_is_range_free(std::ptr::null(), 0, 0, -1, 10) }, - -1 - ); - - // ---- marker handle family: NULL / empty boxes ------------------------------ - assert_eq!(unsafe { oakengine_marker_list_count(std::ptr::null()) }, 0); - assert_eq!( - unsafe { oakengine_marker_list_add(std::ptr::null_mut(), 0, 1, 0, 1, c"x".as_ptr(), 0) }, - -1 - ); - assert_eq!( - unsafe { oakengine_marker_list_add_existing(std::ptr::null_mut(), std::ptr::null_mut()) }, - -1 - ); - assert!(unsafe { oakengine_marker_list_at(std::ptr::null(), 0) }.is_null()); - assert!(unsafe { oakengine_marker_list_at(std::ptr::null(), -1) }.is_null()); - assert!(unsafe { oakengine_marker_list_marker_at_time(std::ptr::null(), 0, 1) }.is_null()); - assert!(unsafe { oakengine_marker_create(0, 0, 1, 0, 1, std::ptr::null()) }.is_null()); - unsafe { oakengine_marker_free(std::ptr::null_mut()) }; - assert_eq!( - unsafe { - oakengine_marker_get_time( - std::ptr::null(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - -1 - ); - assert_eq!( - unsafe { oakengine_marker_get_name(std::ptr::null(), std::ptr::null_mut(), 0) }, - -1 - ); - assert_eq!(unsafe { oakengine_marker_get_color(std::ptr::null()) }, -1); - assert_eq!( - unsafe { oakengine_marker_has_sibling_at_time(std::ptr::null(), 0, 1) }, - 0 - ); - assert_eq!( - unsafe { oakengine_marker_set_time_live(std::ptr::null_mut(), 0, 1, 0, 1) }, - -1 - ); - assert_eq!( - unsafe { - oakengine_marker_commit_time( - std::ptr::null_mut(), - 0, - 1, - 0, - 1, - 0, - 1, - 0, - 1, - std::ptr::null_mut(), - ) - }, - -1 - ); - assert!(unsafe { oakengine_marker_set_time_command(std::ptr::null_mut(), 1, 0) }.is_null()); - assert!(unsafe { oakengine_marker_set_time_command(std::ptr::null_mut(), 1, 1) }.is_null()); - assert_eq!(unsafe { oakengine_marker_remove(std::ptr::null_mut()) }, -1); - assert_eq!( - unsafe { - oakengine_marker_set_properties( - std::ptr::null_mut(), - 0, - 0, - std::ptr::null(), - 0, - 0, - 1, - 0, - 1, - std::ptr::null_mut(), - ) - }, - -1 - ); - - // ---- workarea family: NULL / empty boxes ------------------------------------ - let wa = unsafe { oakengine_workarea_create() }; - assert!(!wa.is_null()); - unsafe { oakengine_workarea_free(wa) }; - assert_eq!( - unsafe { - oakengine_workarea_get( - std::ptr::null(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - -1 - ); - assert_eq!( - unsafe { oakengine_workarea_set_range(std::ptr::null_mut(), 0, 1, 1, 1) }, - -1 - ); - assert_eq!( - unsafe { oakengine_workarea_set_enabled(std::ptr::null_mut(), 1) }, - -1 - ); - assert_eq!( - unsafe { - oakengine_workarea_set_range_undoable( - std::ptr::null_mut(), - 0, - 1, - 1, - 1, - 0, - 1, - 0, - 1, - std::ptr::null_mut(), - ) - }, - -1 - ); - assert_eq!( - unsafe { - oakengine_workarea_set_enabled_undoable(std::ptr::null_mut(), 1, std::ptr::null_mut()) - }, - -1 - ); - // reset_in_out is a pure out-param filler; NULL pointers are safe no-ops. - unsafe { - oakengine_workarea_reset_in_out( - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }; - - // ---- node helpers ------------------------------------------------------------- - assert_eq!(unsafe { oakengine_node_is_block(std::ptr::null()) }, 0); - assert_eq!(unsafe { oakengine_node_is_transition(std::ptr::null()) }, 0); - assert!(unsafe { oakengine_clip_find_multicam(std::ptr::null_mut()) }.is_null()); - assert_eq!( - unsafe { - oakengine_multicam_switch_source( - std::ptr::null_mut(), - std::ptr::null_mut(), - 0, - 0, - 0.0, - std::ptr::null_mut(), - ) - }, - -1 - ); - assert!(unsafe { oakengine_transition_connected_in_block(std::ptr::null()) }.is_null()); - assert!(unsafe { oakengine_transition_connected_out_block(std::ptr::null()) }.is_null()); - - // ---- empty boxes: non-NULL pointers wrapping a NULL CHandle ------------------ - // `unbox` rejects them with E_INVALID even where a NULL pointer is a - // documented 0-return (the box is non-NULL). - let eseq = unsafe { empty_box!(OakEngineSequence) }; - let etrack = unsafe { empty_box!(OakEngineTrack) }; - let eclip = unsafe { empty_box!(OakEngineClip) }; - let eblk = unsafe { empty_box!(OakEngineBlock) }; - let emarker = unsafe { empty_box!(OakEngineMarker) }; - let elist = unsafe { empty_box!(OakEngineMarkerList) }; - let ewa = unsafe { empty_box!(OakEngineWorkarea) }; - let enode = unsafe { empty_box!(OakEngineNode) }; - - assert_eq!( - unsafe { oakengine_sequence_name(eseq, buf.as_mut_ptr(), 64) }, - -1 - ); - assert_eq!(unsafe { oakengine_sequence_marker_count(eseq) }, -1); - assert_eq!(unsafe { oakengine_sequence_get_preview_divider(eseq) }, -1); - assert_eq!(unsafe { oakengine_sequence_get_video_auto_cache(eseq) }, -1); - assert_eq!(unsafe { oakengine_sequence_workarea_is_enabled(eseq) }, -1); - assert_eq!(unsafe { oakengine_sequence_add_track(eseq, 0) }, -1); - assert_eq!(unsafe { oakengine_sequence_add_track(eseq, 3) }, -1); // type check first - assert!(unsafe { oakengine_sequence_track_at(eseq, 0, 0) }.is_null()); - assert!(unsafe { oakengine_sequence_track_at(eseq, 3, 0) }.is_null()); - assert!(unsafe { oakengine_sequence_track_list(eseq, 99) }.is_null()); - assert_eq!(unsafe { oakengine_sequence_clip_count(eseq, 0, 0) }, -1); - assert!(unsafe { oakengine_sequence_clip_at(eseq, 0, 0, 0) }.is_null()); - assert_eq!( - unsafe { oakengine_sequence_marker_at(eseq, -1, &mut n, buf.as_mut_ptr(), 64, &mut i32v) }, - -1 - ); - assert_eq!(unsafe { oakengine_track_type(etrack) }, -1); - assert_eq!(unsafe { oakengine_track_block_count(etrack) }, -1); - assert!(unsafe { oakengine_track_block_at(etrack, 0) }.is_null()); - assert_eq!(unsafe { oakengine_track_is_muted(eseq, 0, 0) }, -1); - assert_eq!(unsafe { oakengine_track_is_locked(eseq, 0, 0) }, -1); - assert_eq!( - unsafe { - oakengine_clip_get_range( - eclip, - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - -1 - ); - assert_eq!(unsafe { oakengine_clip_is_enabled(eclip) }, -1); - assert_eq!(unsafe { oakengine_clip_are_linked(eclip, eclip) }, -1); - assert!(unsafe { oakengine_clip_get_sequence(eclip) }.is_null()); - let eblk2 = eclip.cast::(); - assert!(unsafe { oakengine_clip_in_transition(eblk2) }.is_null()); - assert!(unsafe { oakengine_clip_out_transition(eblk2) }.is_null()); - assert!(unsafe { oakengine_clip_get_connected_viewer(eblk2) }.is_null()); - assert_eq!(unsafe { oakengine_block_is_enabled(eblk) }, -1); - assert_eq!(unsafe { oakengine_block_set_enabled(eblk, 1) }, -1); - assert_eq!(unsafe { oakengine_block_is_gap(eblk) }, -1); - assert_eq!(unsafe { oakengine_block_link_count(eblk) }, -1); - assert!(unsafe { oakengine_block_link_at(eblk, 0) }.is_null()); - assert!(unsafe { oakengine_block_get_track(eblk) }.is_null()); - assert!(unsafe { oakengine_block_next(eblk) }.is_null()); - assert!(unsafe { oakengine_block_prev(eblk) }.is_null()); - assert_eq!(unsafe { oakengine_marker_get_color(emarker) }, -1); - assert_eq!( - unsafe { oakengine_marker_has_sibling_at_time(emarker, 0, 1) }, - -1 - ); - assert_eq!( - unsafe { - oakengine_marker_get_time( - emarker, - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - -1 - ); - assert_eq!( - unsafe { oakengine_marker_get_name(emarker, std::ptr::null_mut(), 0) }, - -1 - ); - assert_eq!(unsafe { oakengine_marker_list_count(elist) }, -1); - assert!(unsafe { oakengine_marker_list_at(elist, 0) }.is_null()); - assert!(unsafe { oakengine_marker_list_marker_at_time(elist, 0, 1) }.is_null()); - assert_eq!( - unsafe { - oakengine_workarea_get( - ewa, - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - -1 - ); - assert_eq!(unsafe { oakengine_node_is_block(enode) }, -1); - assert_eq!(unsafe { oakengine_node_is_transition(enode) }, -1); - assert!(unsafe { oakengine_clip_find_multicam(enode) }.is_null()); - - // Free the empty boxes (all are NULL-CHandle wrappers; release is a no-op). - unsafe { free_box::(eseq) }; - unsafe { free_box::(etrack) }; - unsafe { free_box::(eclip) }; - unsafe { free_box::(eblk) }; - unsafe { free_box::(emarker) }; - unsafe { free_box::(elist) }; - unsafe { free_box::(ewa) }; - unsafe { free_box::(enode) }; - - // ---- last_error is always readable ------------------------------------------- - let elen = unsafe { oakengine_sequence_last_error(buf.as_mut_ptr(), 256) }; - assert!(elen >= 0); -} - -// --------------------------------------------------------------------------- -// Crash-bug reproductions (ignored: running them aborts the process by -// design, which is the point — see the report) -// --------------------------------------------------------------------------- - -/// Former crash repros: `oakengine_clip_toggle_enabled(NULL, 0)` used to -/// abort the process with a non-unwinding UB panic inside -/// `slice::from_raw_parts(NULL, 0)` (src/timeline.rs). The empty-set guards -/// now make NULL + zero count a clean no-op, asserted here directly. (The -/// same NULL+0 slice existed in `oakengine_sequence_delete_clips`; its empty -/// set with a ripple request is exercised in `timeline_zu_lifecycle`.) -#[test] -fn timeline_zu_crash_repros() { - let _zu = zu_lock(); - common::force_link(); - // NULL clips with a zero count is a legal empty set -> clean no-op. - assert_eq!( - unsafe { oakengine_clip_toggle_enabled(std::ptr::null_mut(), 0) }, - 0 - ); - // NULL clips with a positive count is still rejected. - assert_eq!( - unsafe { oakengine_clip_toggle_enabled(std::ptr::null_mut(), 1) }, - -1 - ); -} - -/// M12 P4: cross-track clip move — one undoable entry gaps the source -/// track and places the clip on the destination track. -#[test] -fn move_clip_to_track_crosses_tracks() { - let _zu = zu_lock(); - common::force_link(); - - let project = unsafe { oakengine_project_create() }; - assert_eq!(unsafe { oakengine_project_new(project) }, 0); - let name = std::ffi::CString::new("CrossTrack").unwrap(); - let seq = unsafe { oakengine_sequence_new(project, name.as_ptr()) }; - assert!(!seq.is_null()); - assert_eq!(unsafe { oakengine_sequence_add_track(seq, 0) }, 0); - assert_eq!(unsafe { oakengine_sequence_add_track(seq, 0) }, 1); - - // Two clips on track 0. - let media = std::env::temp_dir().join(format!("oak_it_cross_{}.mp4", std::process::id())); - let media_c = std::ffi::CString::new(media.to_string_lossy().into_owned()).unwrap(); - assert_eq!( - unsafe { oakengine::testmedia::oakengine_testmedia_write_clip(media_c.as_ptr(), 64, 64, 10, 10) }, - 0 - ); - let footage = unsafe { oakengine::node::oakengine_project_import_footage(project, media_c.as_ptr()) }; - assert!(!footage.is_null()); - let c1 = unsafe { oakengine::timeline::oakengine_sequence_add_footage_clip_ex(seq, footage, 0, 0, 0, 10, 0) }; - assert!(!c1.is_null()); - let c2 = unsafe { oakengine::timeline::oakengine_sequence_add_footage_clip_ex(seq, footage, 0, 0, 10, 20, 0) }; - assert!(!c2.is_null()); - unsafe { oakengine::node::oakengine_footage_free(footage) }; - - assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 0, 0) }, 2); - assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 0, 1) }, 0); - - // Move the first clip (index 0) to track 1 at frame 5. - let rc = unsafe { - oakengine::timeline::oakengine_sequence_move_clip_to_track(seq, 0, 0, 0, 1, 5) - }; - assert_eq!(rc, 0, "cross-track move succeeds"); - assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 0, 0) }, 1); - assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 0, 1) }, 1); - - // The moved clip starts at frame 5 on the destination track. - let moved = unsafe { oakengine_sequence_clip_at(seq, 0, 1, 0) }; - assert!(!moved.is_null()); - let mut in_ts: i64 = -1; - let mut out_ts: i64 = -1; - let mut media_in: i64 = -1; - assert_eq!( - unsafe { oakengine_clip_get_range(moved, &mut in_ts, &mut out_ts, &mut media_in) }, - 0 - ); - assert_eq!(in_ts, 5, "placed at the requested in point"); - - // Undo restores both clips on track 0. - assert_eq!(unsafe { oakengine::node::oakengine_project_undo(project) }, 0); - assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 0, 0) }, 2); - assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 0, 1) }, 0); - unsafe { oakengine::node::oakengine_project_redo(project) }; - assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 0, 0) }, 1); - assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 0, 1) }, 1); - - let _ = std::fs::remove_file(&media); -} diff --git a/crates/oakengine/tests/linkage.rs b/crates/oakengine/tests/linkage.rs deleted file mode 100644 index 44fc2748c..000000000 --- a/crates/oakengine/tests/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 . - -//! Forces rustc to link every module crate's rlib into the test binary. -//! -//! The facade itself only references the modules through `extern "C"` -//! imports (see src/bridge), so rustc would otherwise drop the -//! dev-dependency rlibs from the link and leave the imports undefined. -//! Referencing one exported item per crate makes the linker pull the -//! crate's objects in; the `#[no_mangle]` exports then satisfy the -//! bridge imports. - -/// Smoke-test that every module crate links: each referenced export is -/// called once and must return a sane value or a handle. -#[path = "common/mod.rs"] -mod common; - -#[test] -fn all_module_crates_link() { - // oakundo: fresh stack, refcount 1. - let stack = unsafe { oakundo::ffi::undostack::oakundo_undostack_init() }; - assert!(!stack.ctx.is_null()); - - // oakcommon: an int config read with fallback. - let v = unsafe { - oakcommon::ffi::config::oakcommon_config_get_int( - std::ptr::null(), - c"no-such-key".as_ptr(), - 42, - ) - }; - assert_eq!(v, 42); - - // oakcodec: format count (must be positive). - let n = unsafe { oakcodec::ffi::format::oakcodec_encoding_format_count() }; - assert!(n > 0); - - // oakaudio: waveform length of a null handle is an error code, not a - // crash (module validates the handle). - let rc = unsafe { - oakaudio::ffi::waveform::oakaudio_waveform_length( - oakaudio::handle::CHandle::null(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }; - assert!(rc < 0); - - // oakrender: cache indicator height is a positive constant. - let h = unsafe { oakrender::ffi::cache::oakrender_cache_indicator_height() }; - assert!(h > 0); - - // oakplugin: host plugin count with no scan is 0. - let n = unsafe { oakplugin::ffi::oakplugin_host_plugin_count() }; - assert_eq!(n, 0); -} diff --git a/crates/oakengine/tests/timeline.rs b/crates/oakengine/tests/timeline.rs deleted file mode 100644 index 7558f9d72..000000000 --- a/crates/oakengine/tests/timeline.rs +++ /dev/null @@ -1,841 +0,0 @@ -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! Smoke tests for the timeline family (`engine/include/oakengine/timeline.h`). -//! -//! The facade owns a process-wide undo stack, so every test that pushes -//! undoable commands (sequence creation, tracks, markers, workarea, clip -//! editing) is serialized inside the single `timeline_lifecycle` test; the -//! failure-path test only exercises non-mutating NULL-handle calls and runs -//! in parallel. -//! -//! Clips are placed through `oaknode::ffi` directly (the module clip has no -//! `buffer_in` input, so `oakengine_sequence_add_footage_clip` cannot -//! connect a footage node; that test is `#[ignore]`d with the reason). - -#[path = "common/mod.rs"] -mod common; - -use std::ffi::{c_char, c_int}; - -use oakengine::handle::{box_handle, OakEngineNode}; -use oakengine::node::{ - oakengine_footage_borrow, oakengine_project_create, oakengine_project_free, - oakengine_project_new, -}; -use oakengine::timeline::{ - oakengine_block_get_range, oakengine_block_get_track, oakengine_block_is_enabled, - oakengine_block_is_gap, oakengine_block_link_count, oakengine_block_next, oakengine_block_prev, - oakengine_block_set_enabled, oakengine_block_set_length_and_media_out, - oakengine_clip_are_linked, oakengine_clip_get_range, oakengine_clip_get_sequence, - oakengine_clip_is_enabled, oakengine_clip_toggle_enabled, oakengine_clip_trim, - oakengine_marker_commit_time, oakengine_marker_get_color, oakengine_marker_get_name, - oakengine_marker_get_time, oakengine_marker_has_sibling_at_time, oakengine_marker_list_add, - oakengine_marker_list_at, oakengine_marker_list_count, oakengine_marker_list_marker_at_time, - oakengine_marker_remove, oakengine_marker_set_time_command, oakengine_marker_set_time_live, - oakengine_multicam_switch_source, oakengine_node_is_block, oakengine_node_is_transition, - oakengine_sequence_add_default_nodes, oakengine_sequence_add_footage_clip, - oakengine_sequence_add_track, oakengine_sequence_clip_at, oakengine_sequence_clip_count, - oakengine_sequence_delete_empty_tracks, oakengine_sequence_get_audio_params, - oakengine_sequence_get_frame_rate, oakengine_sequence_get_length, - oakengine_sequence_get_length_rational, oakengine_sequence_get_playhead, - oakengine_sequence_get_playhead_seconds, oakengine_sequence_get_preview_divider, - oakengine_sequence_get_video_auto_cache, oakengine_sequence_get_workarea, - oakengine_sequence_last_error, oakengine_sequence_marker_add, oakengine_sequence_marker_add_ex, - oakengine_sequence_marker_at, oakengine_sequence_marker_count, - oakengine_sequence_marker_remove, oakengine_sequence_marker_remove_many, - oakengine_sequence_marker_rename, oakengine_sequence_move_clip, oakengine_sequence_name, - oakengine_sequence_new, oakengine_sequence_remove_track, oakengine_sequence_ripple_delete_clip, - oakengine_sequence_ripple_delete_range, oakengine_sequence_set_audio_params, - oakengine_sequence_set_playhead, oakengine_sequence_set_workarea, - oakengine_sequence_split_clip, oakengine_sequence_track_at, oakengine_sequence_track_count, - oakengine_sequence_track_list, oakengine_sequence_trim_clips_to, - oakengine_sequence_workarea_is_enabled, oakengine_track_block_at, oakengine_track_block_count, - oakengine_track_get_height, oakengine_track_get_length, oakengine_track_is_locked, - oakengine_track_is_muted, oakengine_track_is_range_free, oakengine_track_set_height, - oakengine_track_set_locked, oakengine_track_set_muted, oakengine_track_type, - oakengine_track_visible_block_at_time, oakengine_workarea_create, oakengine_workarea_free, - oakengine_workarea_get, oakengine_workarea_reset_in_out, oakengine_workarea_set_enabled, - oakengine_workarea_set_enabled_undoable, oakengine_workarea_set_range, - oakengine_workarea_set_range_undoable, -}; -use oakengine::undo::{oakengine_undo_index, oakengine_undo_jump}; - -/// Read a NUL-terminated facade string into a Rust String. -unsafe fn read_buf(buf: &mut [c_char]) -> String { - std::ffi::CStr::from_ptr(buf.as_ptr()) - .to_string_lossy() - .into_owned() -} - -/// Force the runtime-dlsym'd symbols into the link: the oaknode module -/// resolves `oakcommon_videoparams_*` and `oakundo_command_init` at runtime -/// with `dlsym(RTLD_DEFAULT)`, and nothing references those codegen units at -/// link time unless named here (the node.rs family force-links -/// `oakundo_command_init` for the same reason). -fn force_runtime_syms() -> usize { - let fns: [usize; 8] = [ - oakcommon::ffi::videoparams::oakcommon_videoparams_init_basic as *const () as usize, - oakcommon::ffi::videoparams::oakcommon_videoparams_set_frame_rate as *const () as usize, - oakcommon::ffi::videoparams::oakcommon_videoparams_get_width as *const () as usize, - oakcommon::ffi::videoparams::oakcommon_videoparams_get_height as *const () as usize, - oakcommon::ffi::videoparams::oakcommon_videoparams_get_format as *const () as usize, - oakcommon::ffi::videoparams::oakcommon_videoparams_get_channel_count as *const () as usize, - oakcommon::ffi::videoparams::oakcommon_videoparams_get_frame_rate as *const () as usize, - oakundo::ffi::command::oakundo_command_init as *const () as usize, - ]; - fns.iter().sum() -} - -/// Convert a facade `CHandle` to the layout-identical oaknode `CHandle` -/// (distinct Rust types over the same C ABI struct). -fn to_node_handle(h: oakengine::handle::CHandle) -> oaknode::handle::CHandle { - oaknode::handle::CHandle { - ctx: h.ctx, - addref: h.addref, - release: h.release, - abi_version: h.abi_version, - } -} - -/// Convert an oaknode `CHandle` back to the facade `CHandle`. -fn to_facade_handle(h: oaknode::handle::CHandle) -> oakengine::handle::CHandle { - oakengine::handle::CHandle { - ctx: h.ctx, - addref: h.addref, - release: h.release, - abi_version: h.abi_version, - } -} - -// --------------------------------------------------------------------------- -// Serialized stack-mutating test -// --------------------------------------------------------------------------- - -/// Sequence creation, tracks, playhead, markers, workarea and clip editing — -/// all in ONE test because the facade's undo stack is process-wide (the -/// same serialization the undo/node families use). -#[test] -fn timeline_lifecycle() { - common::force_link(); - let _ = force_runtime_syms(); - - // ---- project + sequence creation ---------------------------------- - let project = oakengine_project_create(); - assert!(!project.is_null()); - assert_eq!(unsafe { oakengine_project_new(project) }, 0); - - // NULL project -> NULL. - assert!(unsafe { oakengine_sequence_new(std::ptr::null_mut(), c"x".as_ptr()) }.is_null()); - - let seq = unsafe { oakengine_sequence_new(project, c"Test Sequence".as_ptr()) }; - assert!(!seq.is_null()); - - let mut buf = [0 as c_char; 256]; - let len = unsafe { oakengine_sequence_name(seq, buf.as_mut_ptr(), 256) }; - assert_eq!(len, 13); - assert_eq!(unsafe { read_buf(&mut buf) }, "Test Sequence"); - - // Fresh sequence: zero length, default frame rate. - let mut seconds = -1.0; - assert_eq!( - unsafe { oakengine_sequence_get_length(seq, &mut seconds) }, - 0 - ); - assert_eq!(seconds, 0.0); - let mut num = -1; - let mut den = -1; - assert_eq!( - unsafe { oakengine_sequence_get_length_rational(seq, &mut num, &mut den) }, - 0 - ); - assert_eq!((num, den), (0, 1)); - let mut fps_num = 0; - let mut fps_den = 0; - assert_eq!( - unsafe { oakengine_sequence_get_frame_rate(seq, &mut fps_num, &mut fps_den) }, - 0 - ); - assert_eq!((fps_num, fps_den), (30, 1)); - - // ---- tracks --------------------------------------------------------- - let idx = unsafe { oakengine_sequence_add_track(seq, 0) }; - assert_eq!(idx, 0); - let mut video = -1; - let mut audio = -1; - let mut subtitle = -1; - assert_eq!( - unsafe { oakengine_sequence_track_count(seq, &mut video, &mut audio, &mut subtitle) }, - 0 - ); - assert_eq!(video, 1); - assert_eq!(audio, 0); - assert_eq!(subtitle, 0); - - let track = unsafe { oakengine_sequence_track_at(seq, 0, 0) }; - assert!(!track.is_null()); - assert_eq!(unsafe { oakengine_track_type(track) }, 0); // video - assert!(unsafe { oakengine_sequence_track_at(seq, 0, 5) }.is_null()); - assert!(!unsafe { oakengine_sequence_track_list(seq, 0) }.is_null()); - assert!(unsafe { oakengine_sequence_track_list(seq, 99) }.is_null()); - - // Track height / mute / lock (NOT undoable, straight setters). - let mut h = 0.0; - assert_eq!(unsafe { oakengine_track_get_height(seq, 0, 0, &mut h) }, 0); - assert!((h - 3.0).abs() < 1e-9); - assert_eq!(unsafe { oakengine_track_set_height(seq, 0, 0, 5.0) }, 0); - assert_eq!(unsafe { oakengine_track_get_height(seq, 0, 0, &mut h) }, 0); - assert!((h - 5.0).abs() < 1e-9); - assert_eq!(unsafe { oakengine_track_is_muted(seq, 0, 0) }, 0); - assert_eq!(unsafe { oakengine_track_set_muted(seq, 0, 0, 1) }, 0); - assert_eq!(unsafe { oakengine_track_is_muted(seq, 0, 0) }, 1); - assert_eq!(unsafe { oakengine_track_is_locked(seq, 0, 0) }, 0); - assert_eq!(unsafe { oakengine_track_set_locked(seq, 0, 0, 1) }, 0); - assert_eq!(unsafe { oakengine_track_is_locked(seq, 0, 0) }, 1); - assert_eq!(unsafe { oakengine_track_set_locked(seq, 0, 0, 0) }, 0); - - // Track length (empty -> 0) and free-range query. - let mut tlen = -1; - assert_eq!( - unsafe { oakengine_track_get_length(seq, 0, 0, &mut tlen) }, - 0 - ); - assert_eq!(tlen, 0); - assert_eq!( - unsafe { oakengine_track_is_range_free(seq, 0, 0, 0, 30) }, - 1 - ); - // Bad track index -> E_NOT_FOUND (-4). - assert_eq!( - unsafe { oakengine_track_get_length(seq, 0, 99, &mut tlen) }, - -4 - ); - assert_eq!( - unsafe { oakengine_track_is_range_free(seq, 0, 99, 0, 30) }, - -4 - ); - - // ---- playhead --------------------------------------------------------- - assert_eq!(unsafe { oakengine_sequence_set_playhead(seq, 90) }, 0); - let mut ph = -1; - assert_eq!(unsafe { oakengine_sequence_get_playhead(seq, &mut ph) }, 0); - assert_eq!(ph, 90); - let mut phs = 0.0; - assert_eq!( - unsafe { oakengine_sequence_get_playhead_seconds(seq, &mut phs) }, - 0 - ); - assert!((phs - 3.0).abs() < 1e-6); - - // ---- audio params (round-trip through oakcore) ------------------------- - let mut arate: c_int = 0; - let mut alayout: u64 = 0; - assert_eq!( - unsafe { oakengine_sequence_get_audio_params(seq, &mut arate, &mut alayout) }, - 0 - ); - assert!(arate > 0); - assert_eq!( - unsafe { oakengine_sequence_set_audio_params(seq, 48000, 0x3, 1) }, - 0 - ); - assert_eq!( - unsafe { oakengine_sequence_get_audio_params(seq, &mut arate, &mut alayout) }, - 0 - ); - assert_eq!(arate, 48000); - assert_eq!(alayout, 0x3); - // A no-op change (same values) is a success without a new command. - assert_eq!( - unsafe { oakengine_sequence_set_audio_params(seq, 48000, 0x3, 1) }, - 0 - ); - // NULL handle -> E_INVALID. - assert_eq!( - unsafe { oakengine_sequence_set_audio_params(std::ptr::null_mut(), 48000, 0x3, 1) }, - -1 - ); - - // ---- markers ----------------------------------------------------------- - assert_eq!(unsafe { oakengine_sequence_marker_count(seq) }, 0); - assert_eq!( - unsafe { oakengine_sequence_marker_add(seq, 30, c"One".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_sequence_marker_add_ex(seq, 60, c"Two".as_ptr(), 2) }, - 0 - ); - assert_eq!(unsafe { oakengine_sequence_marker_count(seq) }, 2); - - let mut mtime = -1; - let mut mcolor = -1; - let mut mname = [0 as c_char; 64]; - assert_eq!( - unsafe { - oakengine_sequence_marker_at(seq, 0, &mut mtime, mname.as_mut_ptr(), 64, &mut mcolor) - }, - 0 - ); - assert_eq!(mtime, 30); - assert_eq!(mcolor, 0); - assert_eq!(unsafe { read_buf(&mut mname) }, "One"); - - // A duplicate time is rejected with E_STATE. - assert_eq!( - unsafe { oakengine_sequence_marker_add(seq, 30, c"dup".as_ptr()) }, - -2 - ); - - // Rename, then remove many at once. - assert_eq!( - unsafe { oakengine_sequence_marker_rename(seq, 30, c"Renamed".as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_sequence_marker_remove_many(seq, [30i64, 60].as_ptr(), 2) }, - 2 - ); - assert_eq!(unsafe { oakengine_sequence_marker_count(seq) }, 0); - // Removing a nonexistent time -> E_NOT_FOUND (-4). - assert_eq!(unsafe { oakengine_sequence_marker_remove(seq, 999) }, -4); - - // ---- workarea ------------------------------------------------------------ - assert_eq!(unsafe { oakengine_sequence_workarea_is_enabled(seq) }, 0); - assert_eq!( - unsafe { oakengine_sequence_set_workarea(seq, 1, 0, 300) }, - 0 - ); - assert_eq!(unsafe { oakengine_sequence_workarea_is_enabled(seq) }, 1); - let mut wa_in = -1; - let mut wa_out = -1; - assert_eq!( - unsafe { oakengine_sequence_get_workarea(seq, &mut wa_in, &mut wa_out) }, - 0 - ); - assert_eq!((wa_in, wa_out), (0, 300)); - - // Reset sentinels: in = 0/1, out = RATIONAL_MAX/1. - let mut rn = -1; - let mut rd = -1; - let mut ron = -1; - let mut rod = -1; - unsafe { oakengine_workarea_reset_in_out(&mut rn, &mut rd, &mut ron, &mut rod) }; - assert_eq!((rn, rd), (0, 1)); - assert_eq!(ron, i32::MAX as i64); - assert_eq!(rod, 1); - - // Standalone workarea round-trip. - let wa = oakengine_workarea_create(); - assert!(!wa.is_null()); - assert_eq!(unsafe { oakengine_workarea_set_range(wa, 10, 1, 20, 1) }, 0); - assert_eq!(unsafe { oakengine_workarea_set_enabled(wa, 1) }, 0); - let mut wn0 = -1; - let mut wd0 = -1; - let mut wn1 = -1; - let mut wd1 = -1; - let mut wen = -1; - assert_eq!( - unsafe { oakengine_workarea_get(wa, &mut wn0, &mut wd0, &mut wn1, &mut wd1, &mut wen) }, - 0 - ); - assert_eq!((wn0, wd0, wn1, wd1, wen), (10, 1, 20, 1, 1)); - // Undoable variant on the standalone handle (pushed, not added to a parent). - assert_eq!( - unsafe { - oakengine_workarea_set_range_undoable( - wa, - 30, - 1, - 40, - 1, - 10, - 1, - 20, - 1, - std::ptr::null_mut(), - ) - }, - 0 - ); - assert_eq!( - unsafe { oakengine_workarea_set_enabled_undoable(wa, 0, std::ptr::null_mut()) }, - 0 - ); - unsafe { oakengine_workarea_free(wa) }; - - // ---- clip editing (blocks placed through oaknode::ffi directly) -------- - // The module clip has no `buffer_in` input, so footage clips cannot be - // placed; a raw clip node is appended to the track instead. - let track_module = to_node_handle(unsafe { (*track).handle }); - let clip = unsafe { oaknode::ffi::block::oaknode_block_clip_create() }; - assert!(!clip.ctx.is_null()); - unsafe { oaknode::ffi::block::oaknode_block_set_in(clip.clone(), 0, 1) }; - unsafe { oaknode::ffi::block::oaknode_block_set_length_and_media_in(clip.clone(), 1, 1) }; - unsafe { oaknode::ffi::block::oaknode_clip_set_media_in(clip.clone(), 0, 1) }; - assert_eq!( - unsafe { - oaknode::ffi::track::oaknode_track_append_block(track_module.clone(), clip.clone()) - }, - 0 - ); - - assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 0, 0) }, 1); - let mut clip_at = unsafe { oakengine_sequence_clip_at(seq, 0, 0, 0) }; - assert!(!clip_at.is_null()); - let mut cin = -1; - let mut cout = -1; - let mut cmi = -1; - assert_eq!( - unsafe { oakengine_clip_get_range(clip_at, &mut cin, &mut cout, &mut cmi) }, - 0 - ); - assert_eq!((cin, cout, cmi), (0, 30, 0)); - - // Generic block traversal. - assert_eq!(unsafe { oakengine_track_block_count(track) }, 1); - let blk = unsafe { oakengine_track_block_at(track, 0) }; - assert!(!blk.is_null()); - assert_eq!(unsafe { oakengine_block_is_gap(blk) }, 0); - let mut bin = -1; - let mut bout = -1; - assert_eq!( - unsafe { oakengine_block_get_range(blk, &mut bin, &mut bout) }, - 0 - ); - assert_eq!((bin, bout), (0, 30)); - assert!(!unsafe { oakengine_block_get_track(blk) }.is_null()); - assert!(unsafe { oakengine_block_next(blk) }.is_null()); - assert!(unsafe { oakengine_block_prev(blk) }.is_null()); - assert_eq!(unsafe { oakengine_block_link_count(blk) }, 0); - - // Clip enable toggling (undoable, one command). - assert_eq!(unsafe { oakengine_clip_toggle_enabled(&mut clip_at, 1) }, 1); - assert_eq!(unsafe { oakengine_clip_is_enabled(clip_at) }, 0); - assert_eq!(unsafe { oakengine_block_set_enabled(blk, 1) }, 0); - assert_eq!(unsafe { oakengine_block_is_enabled(blk) }, 1); - - // Block resize (undoable): length 30 -> 40, in stays. - assert_eq!( - unsafe { oakengine_block_set_length_and_media_out(blk, 40) }, - 0 - ); - assert_eq!( - unsafe { oakengine_block_get_range(blk, &mut bin, &mut bout) }, - 0 - ); - assert_eq!((bin, bout), (0, 40)); - - // Split at 20 -> two clips (the module's split lists the right half - // first: clip0 = [20, 40)). - assert_eq!( - unsafe { oakengine_sequence_split_clip(seq, 0, 0, 0, 20) }, - 0 - ); - assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 0, 0) }, 2); - // Split outside the clip -> E_INVALID and a last-error. - assert_eq!( - unsafe { oakengine_sequence_split_clip(seq, 0, 0, 0, 100) }, - -1 - ); - let mut err = [0 as c_char; 256]; - let elen = unsafe { oakengine_sequence_last_error(err.as_mut_ptr(), 256) }; - assert!( - elen > 0, - "last_error must be non-empty after a failed split" - ); - - // Trim the first clip [20, 40) to [25, 35). - let clip0 = unsafe { oakengine_sequence_clip_at(seq, 0, 0, 0) }; - assert!(!clip0.is_null()); - assert_eq!(unsafe { oakengine_clip_trim(clip0, 25, 35) }, 0); - let mut cin2 = -1; - let mut cout2 = -1; - let mut cmi2 = -1; - assert_eq!( - unsafe { oakengine_clip_get_range(clip0, &mut cin2, &mut cout2, &mut cmi2) }, - 0 - ); - assert_eq!((cin2, cout2), (25, 35)); - - // Trim clips to a point on every unlocked track (edge 0 = in). - assert!(unsafe { oakengine_sequence_trim_clips_to(seq, 0, 25) } >= 0); - - // Move the clip: a time-only move within the addressed track (the capi's - // gap+place assembly, one undoable entry). clip0 = [25, 35) -> [50, 60) - // (length and media-in kept), fully reversible. - let base_move = unsafe { oakengine_undo_index() }; - assert_eq!(unsafe { oakengine_sequence_move_clip(seq, 0, 0, 0, 50) }, 0); - let mut mv_in = -1i64; - let mut mv_out = -1i64; - let mut mv_mi = -1i64; - assert_eq!( - unsafe { oakengine_clip_get_range(clip0, &mut mv_in, &mut mv_out, &mut mv_mi) }, - 0 - ); - assert_eq!((mv_in, mv_out, mv_mi), (50, 60, 25)); - assert_eq!(unsafe { oakengine_undo_jump(base_move) }, 0); - assert_eq!( - unsafe { oakengine_clip_get_range(clip0, &mut mv_in, &mut mv_out, &mut mv_mi) }, - 0 - ); - assert_eq!((mv_in, mv_out), (25, 35)); - assert_eq!(unsafe { oakengine_undo_jump(base_move + 1) }, 0); - assert_eq!( - unsafe { oakengine_clip_get_range(clip0, &mut mv_in, &mut mv_out, &mut mv_mi) }, - 0 - ); - assert_eq!((mv_in, mv_out), (50, 60)); - // Back to the pre-move state (clip0 = [25, 35)) for the ripple deletes - // below. - assert_eq!(unsafe { oakengine_undo_jump(base_move) }, 0); - // Invalid arguments -> E_INVALID; nonexistent clip -> E_NOT_FOUND. - assert_eq!( - unsafe { oakengine_sequence_move_clip(seq, 0, 0, 0, -1) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_move_clip(seq, 0, 9, 0, 50) }, - -4 - ); - - // Ripple delete the addressed clip. - assert_eq!( - unsafe { oakengine_sequence_ripple_delete_clip(seq, 0, 0, 0) }, - 0 - ); - - // Ripple delete a range on every track. - assert_eq!( - unsafe { oakengine_sequence_ripple_delete_range(seq, 0, 10) }, - 0 - ); - - // Linked clips: two fresh clips linked then unlinked. - let clip_b = unsafe { oaknode::ffi::block::oaknode_block_clip_create() }; - unsafe { oaknode::ffi::block::oaknode_block_set_in(clip_b.clone(), 0, 1) }; - unsafe { oaknode::ffi::block::oaknode_block_set_length_and_media_in(clip_b.clone(), 1, 3) }; - unsafe { oaknode::ffi::block::oaknode_clip_set_media_in(clip_b.clone(), 0, 1) }; - assert_eq!( - unsafe { - oaknode::ffi::track::oaknode_track_append_block(track_module.clone(), clip_b.clone()) - }, - 0 - ); - let clip_b_engine = unsafe { oakengine_sequence_clip_at(seq, 0, 0, 0) }; - assert!(!clip_b_engine.is_null()); - let mut clips = [clip_b_engine, clip_at]; - assert_eq!( - unsafe { oakengine_clip_toggle_enabled(clips.as_mut_ptr(), 2) }, - 2 - ); - assert_eq!(unsafe { oakengine_clip_are_linked(clips[0], clips[1]) }, 0); - - // ---- default nodes + track removal ---------------------------------- - // Add one video + one audio track as one command. - assert_eq!(unsafe { oakengine_sequence_add_default_nodes(seq) }, 0); - assert_eq!( - unsafe { oakengine_sequence_track_count(seq, &mut video, &mut audio, &mut subtitle) }, - 0 - ); - assert_eq!(video, 2); - assert_eq!(audio, 1); - - // Remove the audio track. - assert_eq!(unsafe { oakengine_sequence_remove_track(seq, 1, 0) }, 0); - assert_eq!( - unsafe { oakengine_sequence_track_count(seq, &mut video, &mut audio, &mut subtitle) }, - 0 - ); - assert_eq!(audio, 0); - - // Delete empty tracks (the extra video track is empty). - assert_eq!( - unsafe { oakengine_sequence_delete_empty_tracks(seq, -1) }, - 1 - ); - - // ---- cleanup ---------------------------------------------------------- - unsafe { oakengine_project_free(project) }; -} - -// --------------------------------------------------------------------------- -// Non-mutating failure paths (no undo-stack access; run in parallel) -// --------------------------------------------------------------------------- - -/// NULL handles yield the header's documented values/codes. -#[test] -fn timeline_failure_paths() { - common::force_link(); - - // NULL sequence. - let mut buf = [0 as c_char; 64]; - assert_eq!( - unsafe { oakengine_sequence_name(std::ptr::null(), buf.as_mut_ptr(), 64) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_add_track(std::ptr::null_mut(), 0) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_marker_count(std::ptr::null()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_sequence_marker_add(std::ptr::null_mut(), 0, c"x".as_ptr()) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_workarea_is_enabled(std::ptr::null()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_sequence_get_preview_divider(std::ptr::null()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_sequence_get_video_auto_cache(std::ptr::null()) }, - 0 - ); - assert_eq!( - unsafe { oakengine_sequence_set_playhead(std::ptr::null_mut(), 0) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_set_workarea(std::ptr::null_mut(), 1, 0, 10) }, - -1 - ); - assert!(unsafe { oakengine_sequence_track_at(std::ptr::null(), 0, 0) }.is_null()); - assert!(unsafe { oakengine_sequence_track_list(std::ptr::null_mut(), 0) }.is_null()); - assert!(unsafe { oakengine_sequence_clip_at(std::ptr::null_mut(), 0, 0, 0) }.is_null()); - assert_eq!( - unsafe { oakengine_sequence_clip_count(std::ptr::null_mut(), 0, 0) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_ripple_delete_clip(std::ptr::null_mut(), 0, 0, 0) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_ripple_delete_range(std::ptr::null_mut(), 0, 10) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_remove_track(std::ptr::null_mut(), 0, 0) }, - -1 - ); - assert_eq!( - unsafe { oakengine_sequence_add_default_nodes(std::ptr::null_mut()) }, - -1 - ); - - // NULL clip / block handles. - assert_eq!( - unsafe { - oakengine_clip_get_range( - std::ptr::null(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - -1 - ); - assert_eq!( - unsafe { oakengine_clip_trim(std::ptr::null_mut(), 0, 10) }, - -1 - ); - assert_eq!(unsafe { oakengine_clip_is_enabled(std::ptr::null()) }, 0); - assert_eq!( - unsafe { oakengine_clip_are_linked(std::ptr::null(), std::ptr::null()) }, - 0 - ); - assert!(unsafe { oakengine_clip_get_sequence(std::ptr::null()) }.is_null()); - assert_eq!(unsafe { oakengine_track_block_count(std::ptr::null()) }, -1); - assert!(unsafe { oakengine_track_block_at(std::ptr::null(), 0) }.is_null()); - assert!(unsafe { oakengine_track_visible_block_at_time(std::ptr::null_mut(), 0) }.is_null()); - assert!(unsafe { oakengine_block_get_track(std::ptr::null()) }.is_null()); - assert!(unsafe { oakengine_block_next(std::ptr::null()) }.is_null()); - assert!(unsafe { oakengine_block_prev(std::ptr::null()) }.is_null()); - assert_eq!(unsafe { oakengine_block_is_gap(std::ptr::null()) }, 0); - assert_eq!(unsafe { oakengine_block_is_enabled(std::ptr::null()) }, 0); - assert_eq!( - unsafe { oakengine_block_set_enabled(std::ptr::null_mut(), 1) }, - -1 - ); - assert_eq!(unsafe { oakengine_block_link_count(std::ptr::null()) }, 0); - assert_eq!( - unsafe { oakengine_block_set_length_and_media_out(std::ptr::null_mut(), 10) }, - -1 - ); - assert_eq!(unsafe { oakengine_track_type(std::ptr::null()) }, -1); - assert_eq!(unsafe { oakengine_node_is_block(std::ptr::null()) }, 0); - assert_eq!(unsafe { oakengine_node_is_transition(std::ptr::null()) }, 0); - - // Marker handle family NULL paths. - assert_eq!(unsafe { oakengine_marker_list_count(std::ptr::null()) }, 0); - assert!(unsafe { oakengine_marker_list_at(std::ptr::null(), 0) }.is_null()); - assert!(unsafe { oakengine_marker_list_marker_at_time(std::ptr::null(), 1, 1) }.is_null()); - assert_eq!( - unsafe { oakengine_marker_list_add(std::ptr::null_mut(), 0, 1, 0, 1, c"x".as_ptr(), 0) }, - -1 - ); - assert_eq!( - unsafe { - oakengine_marker_get_time( - std::ptr::null(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - -1 - ); - assert_eq!( - unsafe { oakengine_marker_get_name(std::ptr::null(), std::ptr::null_mut(), 0) }, - -1 - ); - assert_eq!(unsafe { oakengine_marker_get_color(std::ptr::null()) }, -1); - assert_eq!( - unsafe { oakengine_marker_has_sibling_at_time(std::ptr::null(), 1, 1) }, - 0 - ); - assert_eq!(unsafe { oakengine_marker_remove(std::ptr::null_mut()) }, -1); - assert!(unsafe { oakengine_marker_set_time_command(std::ptr::null_mut(), 1, 1) }.is_null()); - assert_eq!( - unsafe { oakengine_marker_set_time_live(std::ptr::null_mut(), 0, 1, 0, 1) }, - -1 - ); - assert_eq!( - unsafe { - oakengine_marker_commit_time( - std::ptr::null_mut(), - 0, - 1, - 0, - 1, - 1, - 1, - 1, - 1, - std::ptr::null_mut(), - ) - }, - -1 - ); - - // Workarea handle NULL paths. - assert_eq!( - unsafe { - oakengine_workarea_get( - std::ptr::null(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }, - -1 - ); - assert_eq!( - unsafe { oakengine_workarea_set_range(std::ptr::null_mut(), 0, 1, 1, 1) }, - -1 - ); - assert_eq!( - unsafe { oakengine_workarea_set_enabled(std::ptr::null_mut(), 1) }, - -1 - ); - assert_eq!( - unsafe { - oakengine_workarea_set_range_undoable( - std::ptr::null_mut(), - 0, - 1, - 1, - 1, - 0, - 1, - 0, - 1, - std::ptr::null_mut(), - ) - }, - -1 - ); - assert_eq!( - unsafe { - oakengine_workarea_set_enabled_undoable(std::ptr::null_mut(), 1, std::ptr::null_mut()) - }, - -1 - ); - - // Multicam NULL path. - assert_eq!( - unsafe { - oakengine_multicam_switch_source( - std::ptr::null_mut(), - std::ptr::null_mut(), - 0, - 0, - 0.0, - std::ptr::null_mut(), - ) - }, - -1 - ); -} - -/// Footage clips need the footage node connected to the clip's `buffer_in` -/// input, which module clips do not declare; the placement is expected to -/// fail with a non-empty last error. -#[test] -#[ignore = "module clips have no buffer input; footage clips cannot be placed"] -fn footage_clip_placement() { - common::force_link(); - - let project = oakengine_project_create(); - assert!(!project.is_null()); - assert_eq!(unsafe { oakengine_project_new(project) }, 0); - let seq = unsafe { oakengine_sequence_new(project, c"FC".as_ptr()) }; - assert!(!seq.is_null()); - assert_eq!(unsafe { oakengine_sequence_add_track(seq, 0) }, 0); - - // A footage node created through oaknode::ffi directly (no media needed). - let footage = unsafe { - oaknode::ffi::footage::oaknode_footage_create( - to_node_handle((*project).handle), - c"/no/such/media.mp4".as_ptr(), - ) - }; - assert!(!footage.ctx.is_null()); - let node_box = unsafe { box_handle::(to_facade_handle(footage)) }; - let footage_handle = unsafe { oakengine_footage_borrow(node_box) }; - assert!(!footage_handle.is_null()); - - let clip = unsafe { oakengine_sequence_add_footage_clip(seq, footage_handle, 0, 0, 0, 30, 0) }; - assert!(clip.is_null()); - let mut err = [0 as c_char; 256]; - let elen = unsafe { oakengine_sequence_last_error(err.as_mut_ptr(), 256) }; - assert!( - elen > 0, - "last_error must explain the failed footage placement" - ); - - unsafe { oakengine_project_free(project) }; -} diff --git a/crates/oaknode/Cargo.toml b/crates/oaknode/Cargo.toml index e4c9c7af8..dbacc1ec3 100644 --- a/crates/oaknode/Cargo.toml +++ b/crates/oaknode/Cargo.toml @@ -15,14 +15,7 @@ oakundo = { path = "../oakundo" } oakcodec = { path = "../oakcodec" } thiserror = "2" -[features] -# In-crate stubs for the oakundo C ABI (and other module bridges) so -# cargo test can exercise the undoable exports end-to-end without the -# module libraries; real builds (feature off) dlsym the actual modules. -test-stubs = [] - [dev-dependencies] -# oakcodec's oakcore_*/oakrender_* host-mocks (test-stubs feature): the -# node test binaries link the real oakcodec (footage probe), whose ffmpeg -# unit references those cross-crate symbols that no Rust crate provides. -oakcodec = { path = "../oakcodec", features = ["test-stubs"] } +# oakcodec is also a plain dependency above; the dev-dependency re-entry +# used to enable oakcodec's `test-stubs` host-mocks, which no longer exist +# (single-lib unification removed the oakcore_*/oakrender_* C ABI calls). diff --git a/crates/oaknode/src/bridge/codec.rs b/crates/oaknode/src/bridge/codec.rs deleted file mode 100644 index 9eeca806d..000000000 --- a/crates/oaknode/src/bridge/codec.rs +++ /dev/null @@ -1,32 +0,0 @@ -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! oakcodec C ABI calls (footage probing) — now direct Rust calls into -//! the oakcodec crate (single-lib unification, see -//! `docs/zh/plans/riir/single-lib.md`). - -use std::ffi::c_char; - -use crate::handle::CHandle; - -/// `oakcodec_decoder_probe` — probe a media file, returning the -/// stream-list handle (`oakcodec_decoder_probe(filename)`). The caller -/// owns the returned handle. -pub fn decoder_probe(path: &str) -> Option { - use std::ffi::CString; - let c = CString::new(path).ok()?; - Some(unsafe { oakcodec::ffi::decoder::oakcodec_decoder_probe(c.as_ptr()) }) -} diff --git a/crates/oaknode/src/bridge/common.rs b/crates/oaknode/src/bridge/common.rs deleted file mode 100644 index c341fed80..000000000 --- a/crates/oaknode/src/bridge/common.rs +++ /dev/null @@ -1,424 +0,0 @@ -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! oakcommon C ABI calls — now direct Rust calls into the oakcommon crate -//! (single-lib unification, see `docs/zh/plans/riir/single-lib.md`). -//! The XML surface mirrors `include/common/xmlutils.h` and backs the -//! serializer's reader/writer traits. Function names and signatures are -//! unchanged (callers in `src/` and `tests/` are untouched); the -//! `test-stubs` feature and its in-crate mocks were removed because the -//! real oakcommon rlib is now always linked (the mocks would collide with -//! its `#[no_mangle]` exports). - -use std::ffi::{c_char, c_int}; - -use crate::handle::CHandle; - -/// `oakcommon_xml_reader_init`. -pub fn xml_reader_init(data: *const c_char) -> Option { - Some(unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_init(data) }) -} - -/// `oakcommon_xml_reader_free`. -pub fn xml_reader_free(reader: *mut CHandle) { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_free(reader) } -} - -/// `oakcommon_xml_reader_read_next_start_element` (advance to the next -/// start element; `found` receives 1/0). -pub fn xml_reader_next_start_element(reader: CHandle) -> Option { - let mut found = 0; - unsafe { - oakcommon::ffi::xmlutils::oakcommon_xml_reader_read_next_start_element(reader, &mut found); - } - Some(found != 0) -} - -/// `oakcommon_xml_reader_name` (two-stage). -pub fn xml_reader_name(reader: CHandle) -> Option { - two_stage_string("oakcommon_xml_reader_name", |buf, size| unsafe { - Some(oakcommon::ffi::xmlutils::oakcommon_xml_reader_name( - reader.clone(), - buf, - size, - )) - }) -} - -/// `oakcommon_xml_reader_read_element_text` (two-stage). -pub fn xml_reader_read_element_text(reader: CHandle) -> Option { - two_stage_string( - "oakcommon_xml_reader_read_element_text", - |buf, size| unsafe { - Some( - oakcommon::ffi::xmlutils::oakcommon_xml_reader_read_element_text( - reader.clone(), - buf, - size, - ), - ) - }, - ) -} - -/// `oakcommon_xml_reader_skip_current_element`. -pub fn xml_reader_skip_current_element(reader: CHandle) -> Option { - Some(unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_skip_current_element(reader) }) -} - -/// `oakcommon_xml_reader_attribute_count`. -pub fn xml_reader_attribute_count(reader: CHandle) -> Option { - let mut count = 0; - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_count(reader, &mut count) }; - Some(count) -} - -/// `oakcommon_xml_reader_attribute_name` (two-stage). -pub fn xml_reader_attribute_name(reader: CHandle, index: c_int) -> Option { - two_stage_string("oakcommon_xml_reader_attribute_name", |buf, size| unsafe { - Some( - oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_name( - reader.clone(), - index, - buf, - size, - ), - ) - }) -} - -/// `oakcommon_xml_reader_attribute_value` (two-stage). -pub fn xml_reader_attribute_value(reader: CHandle, index: c_int) -> Option { - two_stage_string("oakcommon_xml_reader_attribute_value", |buf, size| unsafe { - Some( - oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_value( - reader.clone(), - index, - buf, - size, - ), - ) - }) -} - -/// `oakcommon_xml_reader_has_error`. -pub fn xml_reader_has_error(reader: CHandle) -> Option { - let mut err = 0; - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_has_error(reader, &mut err) }; - Some(err != 0) -} - -/// `oakcommon_xml_writer_init`. -pub fn xml_writer_init() -> Option { - Some(unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_init() }) -} - -/// `oakcommon_xml_writer_free`. -pub fn xml_writer_free(writer: *mut CHandle) { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_free(writer) } -} - -/// `oakcommon_xml_writer_write_start_element`. -pub fn xml_writer_start_element(writer: CHandle, name: &str) -> Option { - use std::ffi::CString; - let n = CString::new(name).ok()?; - Some(unsafe { - oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_start_element(writer, n.as_ptr()) - }) -} - -/// `oakcommon_xml_writer_write_attribute`. -pub fn xml_writer_attribute(writer: CHandle, name: &str, value: &str) -> Option { - use std::ffi::CString; - let n = CString::new(name).ok()?; - let v = CString::new(value).ok()?; - Some(unsafe { - oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_attribute( - writer, - n.as_ptr(), - v.as_ptr(), - ) - }) -} - -/// `oakcommon_xml_writer_write_characters`. -pub fn xml_writer_characters(writer: CHandle, text: &str) -> Option { - use std::ffi::CString; - let t = CString::new(text).ok()?; - Some(unsafe { - oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_characters(writer, t.as_ptr()) - }) -} - -/// `oakcommon_xml_writer_write_text_element`. -pub fn xml_writer_text_element(writer: CHandle, name: &str, text: &str) -> Option { - use std::ffi::CString; - let n = CString::new(name).ok()?; - let t = CString::new(text).ok()?; - Some(unsafe { - oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_text_element( - writer, - n.as_ptr(), - t.as_ptr(), - ) - }) -} - -/// `oakcommon_xml_writer_write_end_element`. -pub fn xml_writer_end_element(writer: CHandle) -> Option { - Some(unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_end_element(writer) }) -} - -/// `oakcommon_xml_writer_write_end_document`. -pub fn xml_writer_end_document(writer: CHandle) -> Option { - Some(unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_end_document(writer) }) -} - -/// `oakcommon_xml_writer_output` (two-stage). -pub fn xml_writer_output(writer: CHandle) -> Option { - two_stage_string("oakcommon_xml_writer_output", |buf, size| unsafe { - Some(oakcommon::ffi::xmlutils::oakcommon_xml_writer_output( - writer.clone(), - buf, - size, - )) - }) -} - -/// `oakcommon_config_get_int` (config access for node defaults). -pub fn config_get_int(group: &str, key: &str, default: c_int) -> Option { - use std::ffi::CString; - let g = CString::new(group).ok()?; - let k = CString::new(key).ok()?; - Some(unsafe { - oakcommon::ffi::config::oakcommon_config_get_int(g.as_ptr(), k.as_ptr(), default) - }) -} - -// --------------------------------------------------------------------- -// oakcommon videoparams C ABI (sequence/footage stream params) -// --------------------------------------------------------------------- - -/// `oakcommon_videoparams_init_basic`: new owned handle (count 1). -pub fn videoparams_init_basic( - width: c_int, - height: c_int, - pixel_format: c_int, - channels: c_int, - par_num: c_int, - par_den: c_int, - interlacing: c_int, - divider: c_int, -) -> Option { - Some(unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_init_basic( - width, - height, - pixel_format, - channels, - par_num, - par_den, - interlacing, - divider, - ) - }) -} - -/// `oakcommon_videoparams_set_frame_rate`. -pub fn videoparams_set_frame_rate(params: CHandle, num: c_int, den: c_int) -> Option { - Some(unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_set_frame_rate(params, num, den) - }) -} - -/// `oakcommon_videoparams_free` — releases the handle locally (the -/// handle's `release` fn points into the oakcommon box machinery). -pub fn videoparams_free(params: *mut CHandle) { - if params.is_null() || unsafe { (*params).ctx.is_null() } { - return; - } - let h = unsafe { (*params).clone() }; - if let Some(f) = h.release { - unsafe { f(h.ctx) }; - } - unsafe { (*params).ctx = std::ptr::null_mut() }; -} - -/// `oakcommon_videoparams_get_width` — the value, or the default on -/// error (the caller decides whether the handle is real). -pub fn videoparams_get_width(params: CHandle) -> Option { - let mut v = 0; - let rc = unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_get_width(params.clone(), &mut v) - }; - Some(if rc < 0 { 0 } else { v }) -} - -/// `oakcommon_videoparams_get_height`. -pub fn videoparams_get_height(params: CHandle) -> Option { - let mut v = 0; - let rc = unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_get_height(params.clone(), &mut v) - }; - Some(if rc < 0 { 0 } else { v }) -} - -/// `oakcommon_videoparams_get_format`. -pub fn videoparams_get_format(params: CHandle) -> Option { - let mut v = 0; - let rc = unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_get_format(params.clone(), &mut v) - }; - Some(if rc < 0 { 0 } else { v }) -} - -/// `oakcommon_videoparams_get_channel_count`. -pub fn videoparams_get_channel_count(params: CHandle) -> Option { - let mut v = 0; - let rc = unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_get_channel_count(params.clone(), &mut v) - }; - Some(if rc < 0 { 0 } else { v }) -} - -/// `oakcommon_videoparams_get_frame_rate` — (num, den). -pub fn videoparams_get_frame_rate(params: CHandle) -> Option<(c_int, c_int)> { - let mut n = 0; - let mut d = 0; - let rc = unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_get_frame_rate( - params.clone(), - &mut n, - &mut d, - ) - }; - Some(if rc < 0 { (0, 0) } else { (n, d) }) -} - -// --------------------------------------------------------------------- -// oakcommon colortransform C ABI (color manager compliance) -// --------------------------------------------------------------------- - -/// `oakcommon_colortransform_init_display`: new owned display transform. -pub fn colortransform_init_display(display: &str, view: &str, look: &str) -> Option { - use std::ffi::CString; - let d = CString::new(display).ok()?; - let v = CString::new(view).ok()?; - let l = CString::new(look).ok()?; - Some(unsafe { - oakcommon::ffi::colortransform::oakcommon_colortransform_init_display( - d.as_ptr(), - v.as_ptr(), - l.as_ptr(), - ) - }) -} - -/// `oakcommon_colortransform_init_output`: new owned output transform. -pub fn colortransform_init_output(output: &str) -> Option { - use std::ffi::CString; - let o = CString::new(output).ok()?; - Some(unsafe { - oakcommon::ffi::colortransform::oakcommon_colortransform_init_output(o.as_ptr()) - }) -} - -/// `oakcommon_colortransform_free` — releases the handle locally. -pub fn colortransform_free(transform: *mut CHandle) { - if transform.is_null() || unsafe { (*transform).ctx.is_null() } { - return; - } - let h = unsafe { (*transform).clone() }; - if let Some(f) = h.release { - unsafe { f(h.ctx) }; - } - unsafe { (*transform).ctx = std::ptr::null_mut() }; -} - -/// `oakcommon_colortransform_is_display`. -pub fn colortransform_is_display(transform: CHandle) -> Option { - Some(unsafe { - oakcommon::ffi::colortransform::oakcommon_colortransform_is_display(transform) != 0 - }) -} - -/// `oakcommon_colortransform_get_display` (two-stage). -pub fn colortransform_get_display(transform: CHandle) -> Option { - two_stage_string("oakcommon_colortransform_get_display", |buf, size| unsafe { - Some( - oakcommon::ffi::colortransform::oakcommon_colortransform_get_display( - transform.clone(), - buf, - size, - ), - ) - }) -} - -/// `oakcommon_colortransform_get_output` (two-stage). -pub fn colortransform_get_output(transform: CHandle) -> Option { - two_stage_string("oakcommon_colortransform_get_output", |buf, size| unsafe { - Some( - oakcommon::ffi::colortransform::oakcommon_colortransform_get_output( - transform.clone(), - buf, - size, - ), - ) - }) -} - -/// `oakcommon_colortransform_get_view` (two-stage). -pub fn colortransform_get_view(transform: CHandle) -> Option { - two_stage_string("oakcommon_colortransform_get_view", |buf, size| unsafe { - Some( - oakcommon::ffi::colortransform::oakcommon_colortransform_get_view( - transform.clone(), - buf, - size, - ), - ) - }) -} - -/// `oakcommon_colortransform_get_look` (two-stage). -pub fn colortransform_get_look(transform: CHandle) -> Option { - two_stage_string("oakcommon_colortransform_get_look", |buf, size| unsafe { - Some( - oakcommon::ffi::colortransform::oakcommon_colortransform_get_look( - transform.clone(), - buf, - size, - ), - ) - }) -} - -/// Shared two-stage string fetch: query the required size, then read -/// into an owned buffer. `None` when the query returns an error. -fn two_stage_string Option>( - _sym: &str, - call: F, -) -> Option { - let needed = call(std::ptr::null_mut(), 0)?; - if needed <= 0 { - return Some(String::new()); - } - let mut buf = vec![0u8; needed as usize]; - call(buf.as_mut_ptr() as *mut c_char, needed)?; - buf.pop(); // trailing NUL - String::from_utf8(buf).ok() -} diff --git a/crates/oaknode/src/bridge/core.rs b/crates/oaknode/src/bridge/core.rs deleted file mode 100644 index 64d29b908..000000000 --- a/crates/oaknode/src/bridge/core.rs +++ /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 . - -//! oakcore C ABI imports (audio stream parameters). dlsym-resolved (see -//! [`super`]). The `OakAudioParams` object is an opaque raw pointer owned -//! by the caller (`oakcore_audioparams_free`), not a [`crate::handle::CHandle`]. - -use std::ffi::{c_int, c_void}; - -#[cfg(not(feature = "test-stubs"))] -/// `oakcore_audioparams_create` — new owned params (release with -/// [`audioparams_free`]). -pub fn audioparams_create( - sample_rate: c_int, - channel_layout: u64, - format: c_int, -) -> Option<*mut c_void> { - use crate::bridge::dlsym; - type F = unsafe extern "C" fn(c_int, u64, c_int) -> *mut c_void; - dlsym::call::("oakcore_audioparams_create", |f| unsafe { - f(sample_rate, channel_layout, format) - }) -} - -/// Test-stub path. -#[cfg(feature = "test-stubs")] -pub fn audioparams_create( - sample_rate: c_int, - channel_layout: u64, - format: c_int, -) -> Option<*mut c_void> { - Some(unsafe { stub::oakcore_audioparams_create(sample_rate, channel_layout, format) }) -} - -/// `oakcore_audioparams_free`. -pub fn audioparams_free(params: *mut c_void) { - if params.is_null() { - return; - } - #[cfg(feature = "test-stubs")] - unsafe { - stub::oakcore_audioparams_free(params); - } - #[cfg(not(feature = "test-stubs"))] - { - use crate::bridge::dlsym; - type F = unsafe extern "C" fn(*mut c_void); - let _ = dlsym::call::("oakcore_audioparams_free", |f| unsafe { f(params) }); - } -} - -#[cfg(not(feature = "test-stubs"))] -/// `oakcore_audioparams_sample_rate`. -pub fn audioparams_sample_rate(params: *const c_void) -> Option { - use crate::bridge::dlsym; - type F = unsafe extern "C" fn(*const c_void) -> c_int; - dlsym::call::("oakcore_audioparams_sample_rate", |f| unsafe { f(params) }) -} - -/// Test-stub path. -#[cfg(feature = "test-stubs")] -pub fn audioparams_sample_rate(params: *const c_void) -> Option { - Some(unsafe { stub::oakcore_audioparams_sample_rate(params) }) -} - -#[cfg(not(feature = "test-stubs"))] -/// `oakcore_audioparams_channel_layout`. -pub fn audioparams_channel_layout(params: *const c_void) -> Option { - use crate::bridge::dlsym; - type F = unsafe extern "C" fn(*const c_void) -> u64; - dlsym::call::("oakcore_audioparams_channel_layout", |f| unsafe { - f(params) - }) -} - -/// Test-stub path. -#[cfg(feature = "test-stubs")] -pub fn audioparams_channel_layout(params: *const c_void) -> Option { - Some(unsafe { stub::oakcore_audioparams_channel_layout(params) }) -} - -#[cfg(not(feature = "test-stubs"))] -/// `oakcore_audioparams_format`. -pub fn audioparams_format(params: *const c_void) -> Option { - use crate::bridge::dlsym; - type F = unsafe extern "C" fn(*const c_void) -> c_int; - dlsym::call::("oakcore_audioparams_format", |f| unsafe { f(params) }) -} - -/// Test-stub path. -#[cfg(feature = "test-stubs")] -pub fn audioparams_format(params: *const c_void) -> Option { - Some(unsafe { stub::oakcore_audioparams_format(params) }) -} - -/// In-crate implementations of the oakcore audioparams C ABI for -/// `cargo test` (`--features test-stubs`). Mirrors the real object: a -/// plain struct behind the caller-owned pointer. -#[cfg(feature = "test-stubs")] -pub(crate) mod stub { - use super::*; - - /// `oakcore_audioparams_create`. - #[no_mangle] - pub unsafe extern "C" fn oakcore_audioparams_create( - sample_rate: c_int, - channel_layout: u64, - format: c_int, - ) -> *mut c_void { - Box::into_raw(Box::new(StubAudioParams { - sample_rate, - channel_layout, - format, - })) as *mut c_void - } - - /// `oakcore_audioparams_free`. - #[no_mangle] - pub unsafe extern "C" fn oakcore_audioparams_free(params: *mut c_void) { - if !params.is_null() { - unsafe { drop(Box::from_raw(params as *mut StubAudioParams)) }; - } - } - - /// `oakcore_audioparams_sample_rate`. - #[no_mangle] - pub unsafe extern "C" fn oakcore_audioparams_sample_rate(params: *const c_void) -> c_int { - if params.is_null() { - return 0; - } - unsafe { (*(params as *const StubAudioParams)).sample_rate } - } - - /// `oakcore_audioparams_channel_layout`. - #[no_mangle] - pub unsafe extern "C" fn oakcore_audioparams_channel_layout(params: *const c_void) -> u64 { - if params.is_null() { - return 0; - } - unsafe { (*(params as *const StubAudioParams)).channel_layout } - } - - /// `oakcore_audioparams_format`. - #[no_mangle] - pub unsafe extern "C" fn oakcore_audioparams_format(params: *const c_void) -> c_int { - if params.is_null() { - return 0; - } - unsafe { (*(params as *const StubAudioParams)).format } - } - - /// Boxed audioparams stub payload. - pub(crate) struct StubAudioParams { - pub sample_rate: c_int, - pub channel_layout: u64, - pub format: c_int, - } -} diff --git a/crates/oaknode/src/bridge/mod.rs b/crates/oaknode/src/bridge/mod.rs deleted file mode 100644 index 0340b6d63..000000000 --- a/crates/oaknode/src/bridge/mod.rs +++ /dev/null @@ -1,76 +0,0 @@ -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! C ABI imports from other oak modules. -//! -//! ## Resolution model -//! -//! Symbols are resolved at runtime with `dlsym(RTLD_DEFAULT)` (the -//! module is force-loaded into the host process, so the real module -//! libraries' symbols are in the global scope). `cargo test` builds -//! without those libraries: a missing symbol surfaces as `None` from -//! the wrapper and the caller maps it to a graceful error. This follows -//! the oakplugin crate template (`src/plugin/rust/src/bridge/mod.rs`). -//! -//! Real linkage for the module dylib is provided by the C++ side's -//! force_load of liboaknode (the staticlib); nothing here is linked -//! directly at compile time. - -pub mod codec; -pub mod common; -pub mod core; -pub mod render; -pub mod timeline; -pub mod undo; - -/// Shared dlsym runtime resolution (pub for crate tests). -pub mod dlsym { - use std::ffi::{c_char, c_void}; - - /// RTLD_DEFAULT (macOS: -2; Linux: 0). - #[cfg(target_os = "macos")] - pub(crate) const RTLD_DEFAULT: *mut c_void = -2isize as *mut c_void; - #[cfg(target_os = "linux")] - pub(crate) const RTLD_DEFAULT: *mut c_void = 0isize as *mut c_void; - - extern "C" { - fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void; - } - - /// Resolve a global-scope symbol; `None` when missing. - pub fn resolve(name: &str) -> Option<*mut c_void> { - let c = std::ffi::CString::new(name).ok()?; - let p = unsafe { dlsym(RTLD_DEFAULT, c.as_ptr()) }; - if p.is_null() { - None - } else { - Some(p) - } - } - - /// Resolve and call by signature; `None` when the symbol is missing. - /// - /// # Safety - /// The caller guarantees `T` matches the symbol's real function type. - pub(crate) fn call(name: &str, f: impl FnOnce(T) -> R) -> Option - where - T: Copy, - { - let p = resolve(name)?; - let f_ptr: T = unsafe { std::mem::transmute_copy(&p) }; - Some(f(f_ptr)) - } -} diff --git a/crates/oaknode/src/bridge/render.rs b/crates/oaknode/src/bridge/render.rs deleted file mode 100644 index e6551ba61..000000000 --- a/crates/oaknode/src/bridge/render.rs +++ /dev/null @@ -1,159 +0,0 @@ -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! oakrender C ABI imports (caches, textures, color processors). -//! -//! Symbols resolved via `dlsym(RTLD_DEFAULT)` (see [`super::dlsym`]); -//! every wrapper returns `None`/a neutral value when the symbol is -//! absent (cargo test without liboakrender). - -use std::ffi::c_int; - -use crate::handle::CHandle; - -/// oakrender cache handle (value type). -pub type CacheHandle = CHandle; -/// oakrender texture handle (value type). -pub type TextureHandle = CHandle; -/// oakrender color processor handle (value type). -pub type ColorProcessorHandle = CHandle; - -/// Cache kind constants (oakrender `OAKRENDER_CACHE_*`). -pub mod cache_kind { - /// `OAKRENDER_CACHE_VIDEO_FRAME`. - pub const VIDEO_FRAME: i32 = 0; - /// `OAKRENDER_CACHE_THUMBNAIL`. - pub const THUMBNAIL: i32 = 1; - /// `OAKRENDER_CACHE_AUDIO_PLAYBACK`. - pub const AUDIO_PLAYBACK: i32 = 2; - /// `OAKRENDER_CACHE_AUDIO_WAVEFORM`. - pub const AUDIO_WAVEFORM: i32 = 3; -} - -/// `oakrender_cache_create_for_node`. -pub fn cache_create_for_node(parent: CHandle, kind: i32) -> Option { - use crate::bridge::dlsym; - type F = unsafe extern "C" fn(CHandle, i32) -> CHandle; - dlsym::call::("oakrender_cache_create_for_node", |f| unsafe { - f(parent, kind) - }) -} - -/// `oakrender_cache_free`. -pub fn cache_free(cache: *mut CHandle) { - use crate::bridge::dlsym; - type F = unsafe extern "C" fn(*mut CHandle); - if let Some(f) = dlsym::call::("oakrender_cache_free", |f| unsafe { f(cache) }) { - let _ = f; - } -} - -/// `oakrender_cache_invalidate_range`. -pub fn cache_invalidate_range( - cache: CHandle, - in_num: i64, - in_den: i64, - out_num: i64, - out_den: i64, -) { - use crate::bridge::dlsym; - type F = unsafe extern "C" fn(CHandle, i64, i64, i64, i64); - if let Some(f) = dlsym::call::("oakrender_cache_invalidate_range", |f| unsafe { - f(cache, in_num, in_den, out_num, out_den) - }) { - let _ = f; - } -} - -/// `oakrender_cache_set_uuid`. -pub fn cache_set_uuid(cache: CHandle, uuid: &str) -> Option { - use crate::bridge::dlsym; - use std::ffi::CString; - type F = unsafe extern "C" fn(CHandle, *const std::ffi::c_char) -> i32; - let c = CString::new(uuid).ok()?; - dlsym::call::("oakrender_cache_set_uuid", |f| unsafe { - f(cache, c.as_ptr()) - }) -} - -/// `oakrender_cache_get_uuid` (two-stage). -pub fn cache_get_uuid(cache: CHandle) -> Option { - use crate::bridge::dlsym; - use std::ffi::c_char; - type F = unsafe extern "C" fn(CHandle, *mut c_char, i32) -> i32; - let needed = dlsym::call::("oakrender_cache_get_uuid", |f| unsafe { - f(cache.clone(), std::ptr::null_mut(), 0) - })?; - if needed <= 0 { - return None; - } - let mut buf = vec![0u8; needed as usize]; - dlsym::call::("oakrender_cache_get_uuid", |f| unsafe { - f(cache.clone(), buf.as_mut_ptr() as *mut c_char, needed) - })?; - buf.pop(); // trailing NUL - String::from_utf8(buf).ok() -} - -/// `oakrender_disk_cache_path` (two-stage): the default cache directory. -pub fn disk_cache_path() -> Option { - use crate::bridge::dlsym; - use std::ffi::c_char; - type F = unsafe extern "C" fn(*mut c_char, i32) -> i32; - let needed = dlsym::call::("oakrender_disk_cache_path", |f| unsafe { - f(std::ptr::null_mut(), 0) - })?; - if needed <= 0 { - return None; - } - let mut buf = vec![0u8; needed as usize]; - dlsym::call::("oakrender_disk_cache_path", |f| unsafe { - f(buf.as_mut_ptr() as *mut c_char, needed) - })?; - buf.pop(); // trailing NUL - String::from_utf8(buf).ok() -} - -/// `oakrender_color_config_create_default`: load the bundled OCIO -/// config. `None` = symbol absent (cargo test); `Some(Ok(())` = -/// success; `Some(Err(()))` = OCIO error. -pub fn color_config_create_default() -> Option> { - use crate::bridge::dlsym; - type F = unsafe extern "C" fn() -> i32; - let rc = dlsym::call::("oakrender_color_config_create_default", |f| unsafe { f() })?; - if rc == 0 { - Some(Ok(())) - } else { - Some(Err(())) - } -} - -/// `oakrender_color_config_load_from_filename`: load a config file. -/// Same tri-state as [`color_config_create_default`]. -pub fn color_config_load(filename: &str) -> Option> { - use crate::bridge::dlsym; - use std::ffi::CString; - type F = unsafe extern "C" fn(*const std::ffi::c_char) -> i32; - let c = CString::new(filename).ok()?; - let rc = dlsym::call::("oakrender_color_config_load_from_filename", |f| unsafe { - f(c.as_ptr()) - })?; - if rc == 0 { - Some(Ok(())) - } else { - Some(Err(())) - } -} diff --git a/crates/oaknode/src/bridge/timeline.rs b/crates/oaknode/src/bridge/timeline.rs deleted file mode 100644 index 97aa0866f..000000000 --- a/crates/oaknode/src/bridge/timeline.rs +++ /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 . - -//! oaktimeline C ABI imports (sequence markers/work area, edit -//! commands used by sequence setup). dlsym-resolved (see [`super`]). - -use crate::handle::CHandle; - -/// `oaktimeline_marker_list_create`. -pub fn marker_list_create() -> Option { - use crate::bridge::dlsym; - type F = unsafe extern "C" fn() -> CHandle; - dlsym::call::("oaktimeline_marker_list_create", |f| unsafe { f() }) -} - -/// `oaktimeline_marker_list_free`. -pub fn marker_list_free(list: *mut CHandle) { - use crate::bridge::dlsym; - type F = unsafe extern "C" fn(*mut CHandle); - if let Some(f) = dlsym::call::("oaktimeline_marker_list_free", |f| unsafe { f(list) }) { - let _ = f; - } -} - -/// `oaktimeline_workarea_create`. -pub fn workarea_create() -> Option { - use crate::bridge::dlsym; - type F = unsafe extern "C" fn() -> CHandle; - dlsym::call::("oaktimeline_workarea_create", |f| unsafe { f() }) -} - -/// `oaktimeline_workarea_free`. -pub fn workarea_free(w: *mut CHandle) { - use crate::bridge::dlsym; - type F = unsafe extern "C" fn(*mut CHandle); - if let Some(f) = dlsym::call::("oaktimeline_workarea_free", |f| unsafe { f(w) }) { - let _ = f; - } -} - -/// `oaktimeline_add_track_command`. -pub fn add_track_command(list: CHandle) -> Option { - use crate::bridge::dlsym; - type F = unsafe extern "C" fn(CHandle) -> CHandle; - dlsym::call::("oaktimeline_add_track_command", |f| unsafe { f(list) }) -} diff --git a/crates/oaknode/src/bridge/undo.rs b/crates/oaknode/src/bridge/undo.rs deleted file mode 100644 index 0f28472b3..000000000 --- a/crates/oaknode/src/bridge/undo.rs +++ /dev/null @@ -1,408 +0,0 @@ -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! oakundo C ABI calls — now direct Rust calls into the oakundo crate -//! (single-lib unification, see `docs/zh/plans/riir/single-lib.md`). -//! Undo commands are created through the C ABI vtable -//! (`oakundo_command_init` with Rust closures as userdata) — no C++ -//! UndoCommand subclassing exists on this side. -//! -//! ## Test stubs (`--features test-stubs`) -//! -//! With the direct dependency, the real oakundo rlib is linked into -//! every build and test; the `test-stubs` feature still compiles -//! in-crate `#[no_mangle]` implementations of the undo C ABI (see -//! [`stub`]) for environments that link without oakundo. Do not enable -//! the feature in a binary that also links oakundo (duplicate symbols). - -use std::ffi::c_int; -use std::sync::atomic::{AtomicBool, Ordering}; - -use crate::handle::CHandle; - -/// `OakUndoCommandVtable` (include/undo/undocommand.h) — the callback -/// table backing a caller-defined undo command. Single-lib unification: -/// aliases the oakundo crate's vtable POD (identical layout). -pub type Vtable = oakundo::undocommand::OakUndoCommandVtable; - -/// Rust closure state behind a vtable command's `userdata` pointer. -/// -/// The box is handed to [`command_init`] (which takes ownership); the -/// vtable trampolines below route `redo`/`undo`/destruction back into -/// the closures. -pub struct CommandState { - /// Whether the command has been executed (redo_now no-ops when done). - pub done: AtomicBool, - /// Redo closure. - pub redo: Box, - /// Undo closure. - pub undo: Box, -} - -impl CommandState { - /// New state with both directions. - pub fn new( - redo: impl FnMut() + Send + 'static, - undo: impl FnMut() + Send + 'static, - ) -> CommandState { - CommandState { - done: AtomicBool::new(false), - redo: Box::new(redo), - undo: Box::new(undo), - } - } -} - -/// Trampoline: run the redo closure behind `userdata`. Panics are -/// swallowed at the C boundary (`// CPP-PARITY: undocommand.cpp` — the -/// C++ side has no panic concept; a panic here must never unwind across -/// the extern "C" frame). -unsafe extern "C" fn redo_trampoline(userdata: *mut std::ffi::c_void) { - let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - if !userdata.is_null() { - let state = unsafe { &mut *(userdata as *mut CommandState) }; - (state.redo)(); - } - })); -} - -/// Trampoline: run the undo closure behind `userdata`. -unsafe extern "C" fn undo_trampoline(userdata: *mut std::ffi::c_void) { - let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - if !userdata.is_null() { - let state = unsafe { &mut *(userdata as *mut CommandState) }; - (state.undo)(); - } - })); -} - -/// Trampoline: free the `CommandState` box. -unsafe extern "C" fn free_trampoline(userdata: *mut std::ffi::c_void) { - if !userdata.is_null() { - unsafe { drop(Box::from_raw(userdata as *mut CommandState)) }; - } -} - -/// Create a vtable-backed undo command whose redo/undo run the given -/// closures (`oakundo_command_init`). The returned handle is owned by -/// the caller; `None` when oakundo is unavailable (or the stub returns -/// an empty handle). -pub fn command_from_closures( - redo: impl FnMut() + Send + 'static, - undo: impl FnMut() + Send + 'static, -) -> Option { - let state = Box::new(CommandState::new(redo, undo)); - let vtable = Vtable { - redo: Some(redo_trampoline), - undo: Some(undo_trampoline), - free_fn: Some(free_trampoline), - }; - command_init(&vtable, Box::into_raw(state) as *mut std::ffi::c_void) -} - -/// `oakundo_command_init` (vtable command). -#[cfg(feature = "test-stubs")] -pub fn command_init(vtable: &Vtable, userdata: *mut std::ffi::c_void) -> Option { - Some(unsafe { stub::oakundo_command_init(vtable as *const Vtable, userdata) }) -} - -/// `oakundo_command_init` (vtable command). -#[cfg(not(feature = "test-stubs"))] -pub fn command_init(vtable: &Vtable, userdata: *mut std::ffi::c_void) -> Option { - // Direct call into the oakundo crate (single-lib unification). - let h = unsafe { oakundo::ffi::command::oakundo_command_init(vtable, userdata) }; - if h.is_null() { - None - } else { - Some(h) - } -} - -/// `oakundo_command_init_multi`. -#[cfg(feature = "test-stubs")] -pub fn command_init_multi() -> Option { - Some(unsafe { stub::oakundo_command_init_multi() }) -} - -/// `oakundo_command_init_multi`. -#[cfg(not(feature = "test-stubs"))] -pub fn command_init_multi() -> Option { - // Direct call into the oakundo crate (single-lib unification). - let h = unsafe { oakundo::ffi::command::oakundo_command_init_multi() }; - if h.is_null() { - None - } else { - Some(h) - } -} - -/// `oakundo_command_multi_add_child`. -#[cfg(feature = "test-stubs")] -pub fn command_multi_add_child(multi: CHandle, child: CHandle) -> Option { - Some(unsafe { stub::oakundo_command_multi_add_child(multi, child) }) -} - -/// `oakundo_command_multi_add_child`. -#[cfg(not(feature = "test-stubs"))] -pub fn command_multi_add_child(multi: CHandle, child: CHandle) -> Option { - // Direct call into the oakundo crate (single-lib unification). - Some(unsafe { oakundo::ffi::command::oakundo_command_multi_add_child(multi, child) }) -} - -/// `oakundo_command_redo_now`. -#[cfg(feature = "test-stubs")] -pub fn command_redo_now(command: CHandle) -> Option { - Some(unsafe { stub::oakundo_command_redo_now(command) }) -} - -/// `oakundo_command_redo_now`. -#[cfg(not(feature = "test-stubs"))] -pub fn command_redo_now(command: CHandle) -> Option { - // Direct call into the oakundo crate (single-lib unification). - Some(unsafe { oakundo::ffi::command::oakundo_command_redo_now(command) }) -} - -/// `oakundo_command_undo_now`. -#[cfg(feature = "test-stubs")] -pub fn command_undo_now(command: CHandle) -> Option { - Some(unsafe { stub::oakundo_command_undo_now(command) }) -} - -/// `oakundo_command_undo_now`. -#[cfg(not(feature = "test-stubs"))] -pub fn command_undo_now(command: CHandle) -> Option { - // Direct call into the oakundo crate (single-lib unification). - Some(unsafe { oakundo::ffi::command::oakundo_command_undo_now(command) }) -} - -/// `oakundo_command_free`. -#[cfg(feature = "test-stubs")] -pub fn command_free(command: *mut CHandle) { - unsafe { stub::oakundo_command_free(command) }; -} - -/// `oakundo_command_free`. -#[cfg(not(feature = "test-stubs"))] -pub fn command_free(command: *mut CHandle) { - // Direct call into the oakundo crate (single-lib unification). - unsafe { oakundo::ffi::command::oakundo_command_free(command) } -} - -/// `oakundo_stack_push` (facade-owned stack). -pub fn stack_push( - stack: CHandle, - command: CHandle, - text: *const std::ffi::c_char, -) -> Option { - // Direct call into the oakundo crate (single-lib unification). - Some(unsafe { oakundo::ffi::undostack::oakundo_undostack_push(stack, command, text) }) -} - -/// In-crate implementations of the undo C ABI for `cargo test` -/// (`--features test-stubs`). Mirrors the C++ `CallbackUndoCommand` -/// (`src/undo/c_api/undocommand.cpp`) semantics: the command holds the -/// vtable + userdata, calls `free_fn` on destruction, and `redo_now`/ -/// `undo_now` are no-ops when already executed. Multi commands hold one -/// reference per child. -#[cfg(feature = "test-stubs")] -pub(crate) mod stub { - use super::*; - - /// A command box behind an OakUndoCommand handle's `ctx`. - pub(crate) enum StubCommand { - /// Vtable command. - Callback { - /// Executed state (redo_now no-ops when true). - done: AtomicBool, - /// Redo callback. - redo: Option, - /// Undo callback. - undo: Option, - /// userdata release. - free_fn: Option, - /// Opaque userdata (owned by the command). - userdata: *mut std::ffi::c_void, - }, - /// Multi command. - Multi { - /// Executed state. - done: AtomicBool, - /// Child commands (each holds one reference). - children: Vec, - }, - } - - impl Drop for StubCommand { - fn drop(&mut self) { - match self { - StubCommand::Callback { - free_fn, userdata, .. - } => { - if let Some(f) = free_fn { - if !userdata.is_null() { - unsafe { f(*userdata) }; - } - } - } - StubCommand::Multi { children, .. } => { - for child in children { - if let Some(f) = child.release { - unsafe { f(child.ctx) }; - } - } - } - } - } - } - - /// `oakundo_command_init`. - #[no_mangle] - pub unsafe extern "C" fn oakundo_command_init( - vtable: *const Vtable, - userdata: *mut std::ffi::c_void, - ) -> CHandle { - if vtable.is_null() { - return CHandle::null(); - } - let vt = unsafe { &*vtable }; - let cmd = StubCommand::Callback { - done: AtomicBool::new(false), - redo: vt.redo, - undo: vt.undo, - free_fn: vt.free_fn, - userdata, - }; - crate::handle::make_owned(SendStub(cmd)) - } - - /// `oakundo_command_init_multi`. - #[no_mangle] - pub unsafe extern "C" fn oakundo_command_init_multi() -> CHandle { - crate::handle::make_owned(SendStub(StubCommand::Multi { - done: AtomicBool::new(false), - children: Vec::new(), - })) - } - - /// `oakundo_command_multi_add_child`. - #[no_mangle] - pub unsafe extern "C" fn oakundo_command_multi_add_child( - multi: CHandle, - child: CHandle, - ) -> c_int { - if multi.ctx.is_null() || child.ctx.is_null() { - return crate::error::OAKNODE_E_INVALID; - } - let boxed = multi.ctx as *mut crate::handle::RefBox; - // Take one reference for the multi. - if let Some(f) = child.addref { - unsafe { f(child.ctx) }; - } - let state = unsafe { &mut (*boxed).value }; - match &mut state.0 { - StubCommand::Multi { children, .. } => { - children.push(child); - crate::error::OAKNODE_OK - } - _ => crate::error::OAKNODE_E_INVALID, - } - } - - /// `oakundo_command_redo_now`. - #[no_mangle] - pub unsafe extern "C" fn oakundo_command_redo_now(command: CHandle) -> c_int { - if command.ctx.is_null() { - return crate::error::OAKNODE_E_INVALID; - } - let boxed = command.ctx as *mut crate::handle::RefBox; - let state = unsafe { &mut (*boxed).value }; - match &mut state.0 { - StubCommand::Callback { - done, - redo, - userdata, - .. - } => { - if !done.swap(true, Ordering::AcqRel) { - if let Some(f) = redo { - unsafe { f(*userdata) }; - } - } - crate::error::OAKNODE_OK - } - StubCommand::Multi { done, children } => { - if !done.swap(true, Ordering::AcqRel) { - for child in children.iter() { - let _ = unsafe { oakundo_command_redo_now(child.clone()) }; - } - } - crate::error::OAKNODE_OK - } - } - } - - /// `oakundo_command_undo_now`. - #[no_mangle] - pub unsafe extern "C" fn oakundo_command_undo_now(command: CHandle) -> c_int { - if command.ctx.is_null() { - return crate::error::OAKNODE_E_INVALID; - } - let boxed = command.ctx as *mut crate::handle::RefBox; - let state = unsafe { &mut (*boxed).value }; - match &mut state.0 { - StubCommand::Callback { - done, - undo, - userdata, - .. - } => { - if done.swap(false, Ordering::AcqRel) { - if let Some(f) = undo { - unsafe { f(*userdata) }; - } - } - crate::error::OAKNODE_OK - } - StubCommand::Multi { done, children } => { - if done.swap(false, Ordering::AcqRel) { - for child in children.iter().rev() { - let _ = unsafe { oakundo_command_undo_now(child.clone()) }; - } - } - crate::error::OAKNODE_OK - } - } - } - - /// `oakundo_command_free`. - #[no_mangle] - pub unsafe extern "C" fn oakundo_command_free(command: *mut CHandle) { - if command.is_null() || unsafe { (*command).ctx.is_null() } { - return; - } - let h = unsafe { (*command).clone() }; - if let Some(f) = h.release { - unsafe { f(h.ctx) }; - } - unsafe { (*command).ctx = std::ptr::null_mut() }; - } - - /// Send-marker for the command box (the raw `userdata` pointer is - /// only dereferenced on the thread that created the command — the - /// test thread — so the box never actually crosses threads). - struct SendStub(StubCommand); - unsafe impl Send for SendStub {} -} diff --git a/crates/oaknode/src/ffi.rs b/crates/oaknode/src/ffi.rs deleted file mode 100644 index 0645f5976..000000000 --- a/crates/oaknode/src/ffi.rs +++ /dev/null @@ -1,9984 +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 . - -//! C ABI export layer: implements `include/node/*.h` verbatim. -//! -//! Organization: one submodule per public header. The authoritative -//! function list is the header itself; each submodule below carries a -//! complete inventory comment plus the export stubs. Bodies only -//! unwrap handles, call safe Rust, and map results through -//! [`crate::handle::guard*`]. -//! -//! ## Handle layout -//! -//! - Project handles box `Arc>` (`ProjectArc`). -//! - Node/folder/sequence handles box [`crate::project::NodeRef`] — -//! `(Arc>, NodeId)` plus the shared owned-flag. -//! - Handles obtained from a project (node_at, root, connected nodes, -//! links, ...) are borrowed: releasing them only frees the box. -//! Factory-created and detached nodes are owned and tracked by the -//! debug alive counter until a project adopts them. -//! -//! ## Undo-bridge exports -//! -//! Every `*_undoable` / `command_create_*` / `set_input_at_time_*` -//! export routes through `bridge::undo` (implemented Phase 2; the -//! `test-stubs` feature provides in-crate oakundo symbols for tests). - -use std::collections::HashMap; -use std::ffi::{c_char, c_int, CStr, CString}; -use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, Ordering}; -use std::sync::{Arc, Mutex, MutexGuard, OnceLock}; - -use crate::error::{Error, Result}; -use crate::handle::{guard, guard_void, CHandle, RefBox}; -use crate::id::NodeId; -use crate::node::NodeCore; -use crate::project::{NodeRef, Project, WeakProject}; -use crate::value::{NodeValue, OakNodeValue, ValueType}; - -/// Project handle payload. -type ProjectArc = Arc>; - -// --------------------------------------------------------------------- -// Shared state: alive counter + identity registry -// --------------------------------------------------------------------- - -/// Live owned objects (projects from `init`, nodes from factory/copy). -static ALIVE: AtomicI32 = AtomicI32::new(0); - -/// Identity registry: `NodeId::identity()` -> weak project + id. Entries -/// are advisory; `node_from_identity` revalidates against the live graph, -/// so stale entries (project freed, node moved/removed) resolve to empty -/// handles. -static REGISTRY: OnceLock>> = OnceLock::new(); - -fn registry() -> &'static Mutex> { - REGISTRY.get_or_init(|| Mutex::new(HashMap::new())) -} - -/// Debug alive count (`oaknode_debug_alive_count`). -pub fn debug_alive_count() -> c_int { - ALIVE.load(Ordering::Relaxed) -} - -/// Take a poisoned mutex in stride (a panic inside this crate must not -/// cascade into every later call). -fn lock(m: &Mutex) -> MutexGuard<'_, T> { - m.lock().unwrap_or_else(|e| e.into_inner()) -} - -/// Release for project handles created by `oaknode_project_init` -/// (accounted in ALIVE). -unsafe extern "C" fn project_release(ctx: *mut std::ffi::c_void) { - unsafe { - let rb = ctx as *mut RefBox; - if (*rb).refs.fetch_sub(1, Ordering::AcqRel) == 1 { - drop(Box::from_raw(rb)); - ALIVE.fetch_sub(1, Ordering::Relaxed); - } - } -} - -/// Release for borrowed project handles (node_get_project): the box -/// drops but the project stays alive via its owner. -unsafe extern "C" fn project_release_borrowed(ctx: *mut std::ffi::c_void) { - unsafe { - let rb = ctx as *mut RefBox; - if (*rb).refs.fetch_sub(1, Ordering::AcqRel) == 1 { - drop(Box::from_raw(rb)); - } - } -} - -/// Release for node handles: drops the box; when the node object is -/// still separately owned (see [`NodeRef::owned`]), also un-counts it. -unsafe extern "C" fn node_release(ctx: *mut std::ffi::c_void) { - unsafe { - let rb = ctx as *mut RefBox; - if (*rb).refs.fetch_sub(1, Ordering::AcqRel) == 1 { - let was_owned = (*rb).value.owned.load(Ordering::Relaxed); - drop(Box::from_raw(rb)); - if was_owned { - ALIVE.fetch_sub(1, Ordering::Relaxed); - } - } - } -} - -/// Owned project handle (alive-counted). -fn make_project_owned(project: ProjectArc) -> CHandle { - ALIVE.fetch_add(1, Ordering::Relaxed); - crate::handle::make_owned_with(project, project_release) -} - -/// Borrowed project handle (not counted). -fn make_project_borrowed(project: ProjectArc) -> CHandle { - crate::handle::make_owned_with(project, project_release_borrowed) -} - -/// Owned node handle (alive-counted): factory-created or detached node. -fn make_node_owned(project: ProjectArc, id: NodeId) -> CHandle { - ALIVE.fetch_add(1, Ordering::Relaxed); - crate::handle::make_owned_with(NodeRef::new(project, id, true), node_release) -} - -/// Borrowed node handle (not counted): views into graph-owned nodes. -fn make_node_borrowed(project: ProjectArc, id: NodeId) -> CHandle { - crate::handle::make_owned_with(NodeRef::new(project, id, false), node_release) -} - -/// Read the boxed [`NodeRef`] of a node/folder handle. -/// -/// # Safety -/// The handle must have been created by this crate as a node/folder -/// handle. -unsafe fn node_ref(h: &CHandle) -> Result { - unsafe { crate::handle::get::(h) } - .cloned() - .ok_or(Error::Invalid) -} - -/// Read the boxed project of a project handle. -/// -/// # Safety -/// The handle must have been created by this crate as a project handle. -unsafe fn project_arc(h: &CHandle) -> Result { - unsafe { crate::handle::get::(h) } - .cloned() - .ok_or(Error::Invalid) -} - -/// Mutate the boxed [`NodeRef`] of a node handle (visible to every -/// handle copy, which shares the box). -/// -/// # Safety -/// The handle must have been created by this crate as a node handle. -unsafe fn write_node_ref(h: &CHandle, r: NodeRef) { - unsafe { - let rb = h.ctx as *mut RefBox; - (*rb).value = r; - } -} - -/// Lock the project and hand `&mut Graph` to `f`. -fn with_graph(project: &ProjectArc, f: impl FnOnce(&mut crate::graph::Graph) -> R) -> R { - let mut guard = lock(project); - f(&mut guard.graph) -} - -/// Borrow a read view of the graph. -fn with_graph_read(project: &ProjectArc, f: impl FnOnce(&crate::graph::Graph) -> R) -> R { - let guard = lock(project); - f(&guard.graph) -} - -/// Two-stage string getter: returns the required size including NUL; -/// writes up to `buf_size - 1` bytes plus NUL when `buf_size > 0` -/// (`// CPP-PARITY: src/node/c_api/valueconvert.h` `copy_string`). -fn copy_string_out(value: &str, buf: *mut c_char, buf_size: c_int) -> c_int { - let required = value.len() + 1; - if !buf.is_null() && buf_size > 0 { - let copy_len = value.len().min(buf_size as usize - 1); - unsafe { - std::ptr::copy_nonoverlapping(value.as_ptr() as *const c_char, buf, copy_len); - *buf.add(copy_len) = 0; - } - } - required as c_int -} - -/// Safe read of a NUL-terminated C string argument. -/// -/// # Safety -/// `p` must be a valid NUL-terminated C string for the returned -/// reference's lifetime. -unsafe fn cstr<'a>(p: *const c_char) -> Option<&'a str> { - if p.is_null() { - return None; - } - unsafe { CStr::from_ptr(p) }.to_str().ok() -} - -/// Convert a `&str` to an owned CString (None on interior NULs). -fn cstring(s: &str) -> Option { - CString::new(s).ok() -} - -/// Build a new owned oakcommon videoparams handle from the Rust model's -/// [`crate::value::VideoParams`] (the C++ oracle uses the C++-only -/// `oakcommon_videoparams_init_from_native`; the Rust port goes through -/// the public init_basic + set_frame_rate C ABI so the handle carries the -/// model's width/height/frame-rate/format/channels). `None` when -/// oakcommon is unavailable. -fn videoparams_handle_from(p: &crate::value::VideoParams) -> Option { - let h = crate::bridge::common::videoparams_init_basic( - p.width, - p.height, - p.pixel_format, - p.channels, - 1, - 1, - 0, - 1, - )?; - let _ = crate::bridge::common::videoparams_set_frame_rate( - h.clone(), - p.frame_rate.numerator() as c_int, - p.frame_rate.denominator() as c_int, - ); - Some(h) -} - -/// Read a Rust [`crate::value::VideoParams`] from an oakcommon videoparams -/// handle (the inverse of [`videoparams_handle_from`]); `None` when -/// oakcommon is unavailable or the handle is invalid. -fn videoparams_from_handle(handle: CHandle) -> Option { - let width = crate::bridge::common::videoparams_get_width(handle.clone())?; - let height = crate::bridge::common::videoparams_get_height(handle.clone())?; - let pixel_format = crate::bridge::common::videoparams_get_format(handle.clone())?; - let channels = crate::bridge::common::videoparams_get_channel_count(handle.clone())?; - let (fps_num, fps_den) = crate::bridge::common::videoparams_get_frame_rate(handle)?; - Some(crate::value::VideoParams { - width, - height, - frame_rate: oakcore_rs::Rational::new(fps_num as i64, fps_den as i64), - pixel_format, - channels, - }) -} - -/// Registered identity for a node (insert-or-replace, C++ -/// `oaknode_node_identity` registry semantics). -fn register_identity(r: &NodeRef) -> u64 { - let id = r.id.identity(); - let mut reg = lock(registry()); - reg.insert(id, (Arc::downgrade(&r.project), r.id)); - id -} - -/// Forget an identity (node moved or removed); unknown ids are no-ops. -fn unregister_identity(id: u64) { - let mut reg = lock(registry()); - reg.remove(&id); -} - -/// Resolve an identity to a live node, or an empty handle. -fn node_from_identity_lookup(id: u64) -> CHandle { - let entry = { - let reg = lock(registry()); - reg.get(&id).cloned() - }; - match entry { - Some((weak, node_id)) => match weak.upgrade() { - Some(project) => { - let alive = with_graph_read(&project, |g| g.is_valid(node_id)); - if alive { - make_node_borrowed(project, node_id) - } else { - // Stale: the node moved or was removed. Clean up. - unregister_identity(id); - CHandle::null() - } - } - None => CHandle::null(), - }, - None => CHandle::null(), - } -} - -// ===================================================================== -// include/node/project.h -// ===================================================================== - -/// `include/node/project.h` exports (complete inventory): -/// oaknode_project_init / free / initialize / clear / root / name / -/// filename / pretty_filename / set_filename / is_modified / -/// set_modified / is_new / cache_path / copy_settings / load / save / -/// load_from_data / save_to_data / folder_add / footage_import / -/// get_project_from_object / debug_alive_count. -/// -/// `load`/`save`/`load_from_data`/`save_to_data` land with the -/// serializer milestone (Phase 2); `folder_add`/`footage_import` with -/// the folder/footage families. -pub mod project { - use super::*; - - /// `oaknode_project_init`: new project, refcount 1. - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_init() -> CHandle { - make_project_owned(Project::new()) - } - - /// `oaknode_project_free`: NULL/empty no-op. - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_free(project: *mut CHandle) { - guard_void(|| { - if project.is_null() || unsafe { (*project).ctx.is_null() } { - return; - } - let h = unsafe { (*project).clone() }; - if let Some(f) = h.release { - unsafe { f(h.ctx) }; - } - unsafe { (*project).ctx = std::ptr::null_mut() }; - }); - } - - /// `oaknode_project_initialize`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_initialize(project: CHandle) -> c_int { - guard(|| { - let p = unsafe { project_arc(&project)? }; - let mut guard = lock(&p); - guard.initialize() - }) - } - - /// `oaknode_project_clear`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_clear(project: CHandle) -> c_int { - guard(|| { - let p = unsafe { project_arc(&project)? }; - let mut guard = lock(&p); - guard.clear() - }) - } - - /// `oaknode_project_root` (borrowed folder handle). - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_root(project: CHandle) -> CHandle { - let p = match unsafe { project_arc(&project) } { - Ok(p) => p, - Err(_) => return CHandle::null(), - }; - let root = { - let guard = lock(&p); - guard.root - }; - if root.valid() && with_graph_read(&p, |g| g.is_valid(root)) { - make_node_borrowed(p, root) - } else { - CHandle::null() - } - } - - /// `oaknode_project_name` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_name( - project: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let p = match unsafe { project_arc(&project) } { - Ok(p) => p, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let guard = lock(&p); - let name = guard.name(); - copy_string_out(&name, buf, buf_size) - } - - /// `oaknode_project_filename` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_filename( - project: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let p = match unsafe { project_arc(&project) } { - Ok(p) => p, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let guard = lock(&p); - copy_string_out(&guard.filename, buf, buf_size) - } - - /// `oaknode_project_pretty_filename` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_pretty_filename( - project: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let p = match unsafe { project_arc(&project) } { - Ok(p) => p, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let guard = lock(&p); - copy_string_out(guard.pretty_filename(), buf, buf_size) - } - - /// `oaknode_project_set_filename`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_set_filename( - project: CHandle, - filename: *const c_char, - ) -> c_int { - guard(|| { - let p = unsafe { project_arc(&project)? }; - let filename = unsafe { cstr(filename) }.ok_or(Error::Invalid)?; - let mut guard = lock(&p); - guard.set_filename(filename); - Ok(()) - }) - } - - /// `oaknode_project_is_modified`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_is_modified(project: CHandle) -> c_int { - let p = match unsafe { project_arc(&project) } { - Ok(p) => p, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let guard = lock(&p); - guard.is_modified() as c_int - } - - /// `oaknode_project_set_modified`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_set_modified( - project: CHandle, - modified: c_int, - ) -> c_int { - guard(|| { - let p = unsafe { project_arc(&project)? }; - let mut guard = lock(&p); - guard.set_modified(modified != 0); - Ok(()) - }) - } - - /// `oaknode_project_is_new`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_is_new(project: CHandle) -> c_int { - let p = match unsafe { project_arc(&project) } { - Ok(p) => p, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let guard = lock(&p); - guard.is_new() as c_int - } - - /// `oaknode_project_cache_path` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_cache_path( - project: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let p = match unsafe { project_arc(&project) } { - Ok(p) => p, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let guard = lock(&p); - let path = guard.cache_path(); - copy_string_out(&path, buf, buf_size) - } - - /// `oaknode_project_copy_settings`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_copy_settings(dst: CHandle, src: CHandle) -> c_int { - guard(|| { - let dst = unsafe { project_arc(&dst)? }; - let src = unsafe { project_arc(&src)? }; - let src_guard = lock(&src); - let snapshot = ( - src_guard.settings.clone(), - src_guard.cache_location_setting, - src_guard.custom_cache_path.clone(), - ); - drop(src_guard); - let mut dst_guard = lock(&dst); - dst_guard.settings = snapshot.0; - dst_guard.cache_location_setting = snapshot.1; - dst_guard.custom_cache_path = snapshot.2; - Ok(()) - }) - } - - /// `oaknode_project_get_cache_location_setting`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_get_cache_location_setting(project: CHandle) -> c_int { - let p = match unsafe { project_arc(&project) } { - Ok(p) => p, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let guard = lock(&p); - guard.cache_location_setting - } - - /// `oaknode_project_set_cache_location_setting`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_set_cache_location_setting( - project: CHandle, - setting: c_int, - ) -> c_int { - guard(|| { - let p = unsafe { project_arc(&project)? }; - if !(0..=2).contains(&setting) { - return Err(Error::Invalid); - } - let mut guard = lock(&p); - guard.cache_location_setting = setting; - Ok(()) - }) - } - - /// `oaknode_project_get_custom_cache_path` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_get_custom_cache_path( - project: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let p = match unsafe { project_arc(&project) } { - Ok(p) => p, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let guard = lock(&p); - copy_string_out(&guard.custom_cache_path, buf, buf_size) - } - - /// `oaknode_project_set_custom_cache_path`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_set_custom_cache_path( - project: CHandle, - path: *const c_char, - ) -> c_int { - guard(|| { - let p = unsafe { project_arc(&project)? }; - let path = if path.is_null() { - String::new() - } else { - unsafe { cstr(path) }.ok_or(Error::Invalid)?.to_string() - }; - let mut guard = lock(&p); - guard.custom_cache_path = path; - Ok(()) - }) - } - - /// `oaknode_project_get_uuid` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_get_uuid( - project: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let p = match unsafe { project_arc(&project) } { - Ok(p) => p, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let guard = lock(&p); - copy_string_out(&guard.uuid, buf, buf_size) - } - - /// `oaknode_project_add_node`: move an owned node into this project's - /// graph; the graph assumes the node's lifetime. Nodes whose scratch - /// project holds additional nodes (a sequence with its track lists) - /// transfer the whole subgraph. - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_add_node(project: CHandle, node: CHandle) -> c_int { - guard(|| { - let dst = unsafe { project_arc(&project)? }; - let src = unsafe { node_ref(&node)? }; - - if Arc::ptr_eq(&src.project, &dst) { - // Already in this project: idempotent. - return Ok(()); - } - - let src_project = src.project.clone(); - let src_id = src.id; - - let (new_id, moved_any) = { - let mut src_guard = lock(&src_project); - let mut dst_guard = lock(&dst); - let src_count = src_guard.graph.node_count(); - if src_count > 1 { - // Whole-subgraph transfer (sequence + track lists). - let map = dst_guard.graph.transfer_all(&mut src_guard.graph); - let new_id = *map.get(&src_id).unwrap_or(&src_id); - (new_id, true) - } else { - let taken = src_guard.graph.take_node(src_id).ok_or(Error::NotFound)?; - let new_id = dst_guard.graph.add_entry(taken, src_id); - (new_id, false) - } - }; - let _ = moved_any; - - // Rewrite the shared node box: the node now lives in `dst`. - let owned = std::sync::Arc::new(AtomicBool::new(false)); - let r = NodeRef { - project: dst.clone(), - id: new_id, - owned, - }; - unsafe { write_node_ref(&node, r) }; - - // Alive accounting: the node is now covered by the project. - if src.owned.load(Ordering::Relaxed) { - ALIVE.fetch_sub(1, Ordering::Relaxed); - } - - // Identity registry: the node's identity may have changed. - if new_id != src_id { - unregister_identity(src_id.identity()); - } - let mut reg = lock(registry()); - reg.insert(new_id.identity(), (Arc::downgrade(&dst), new_id)); - Ok(()) - }) - } - - /// `oaknode_project_remove_node`: detach a node from the graph - /// without deleting it; ownership returns to the caller's handle. - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_remove_node(project: CHandle, node: CHandle) -> c_int { - guard(|| { - let dst = unsafe { project_arc(&project)? }; - let nref = unsafe { node_ref(&node)? }; - if !Arc::ptr_eq(&nref.project, &dst) { - return Err(Error::NotFound); - } - let taken = { - let mut guard = lock(&dst); - guard.graph.take_node(nref.id).ok_or(Error::NotFound)? - }; - - // Re-home the node in a scratch project; the caller's handle - // becomes its owner again. - let scratch = Project::new(); - let new_id = { - let mut guard = lock(&scratch); - guard.graph.add_entry(taken, nref.id) - }; - let owned = std::sync::Arc::new(AtomicBool::new(true)); - let r = NodeRef { - project: scratch.clone(), - id: new_id, - owned, - }; - unsafe { write_node_ref(&node, r) }; - ALIVE.fetch_add(1, Ordering::Relaxed); - - if new_id != nref.id { - unregister_identity(nref.id.identity()); - } - let mut reg = lock(registry()); - reg.insert(new_id.identity(), (Arc::downgrade(&scratch), new_id)); - Ok(()) - }) - } - - /// `oaknode_project_node_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_node_count(project: CHandle) -> c_int { - let p = match unsafe { project_arc(&project) } { - Ok(p) => p, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let count = with_graph_read(&p, |g| g.node_count()); - count as c_int - } - - /// `oaknode_project_node_at` (borrowed node handle; empty when out of - /// range). - #[no_mangle] - pub unsafe extern "C" fn oaknode_project_node_at(project: CHandle, index: c_int) -> CHandle { - let p = match unsafe { project_arc(&project) } { - Ok(p) => p, - Err(_) => return CHandle::null(), - }; - if index < 0 { - return CHandle::null(); - } - let ids = with_graph_read(&p, |g| g.node_ids()); - match ids.get(index as usize) { - Some(id) => make_node_borrowed(p, *id), - None => CHandle::null(), - } - } -} - -/// Build an undo command from redo/undo closures (bridge::undo vtable). -fn undo_command( - redo: impl FnMut() + Send + 'static, - undo: impl FnMut() + Send + 'static, -) -> Result { - crate::bridge::undo::command_from_closures(redo, undo).ok_or(Error::NoMem) -} - -// ===================================================================== -// include/node/node.h -// ===================================================================== - -/// `include/node/node.h` exports (complete inventory): -/// debug_alive_count, node_type_name/id/category/description, -/// add/remove_input, set_input_name/flag/property, set_standard_value, -/// get_standard_value, set_input_at_time(_undoable/_into), -/// connect_edge / disconnect_edge, input_get_connected_node, -/// input_array_size/append/remove, copy_inputs, node_get_project, -/// node_identity, node_from_identity, sequence_from_node, -/// sequence_set_default_parameters, find_input_footage, -/// node_get_markers / get_work_area / get_video_frame_cache, -/// command_create_* family, debug_alive_count. -/// -/// Phase 1 implements the node-generic surface (metadata, inputs, -/// params, graph editing, links, contexts, arrays, identity, copies, -/// at-time value reads). The undo-bridge exports (`*_undoable`, -/// `command_create_*`, `set_input_at_time_undoable/_into`), the -/// timeline/render-cache accessors (`get_markers`/`get_work_area`/ -/// `get_video_frame_cache`), the viewer setters and -/// `find_input_footage` land with the Phase-2/3 bridge milestones. -pub mod node { - use super::*; - - /// `oaknode_debug_alive_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_debug_alive_count() -> c_int { - debug_alive_count() - } - - /// `oaknode_node_get_id` (two-stage; Node::id()). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_get_id( - node: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let n = match unsafe { node_ref(&node) } { - Ok(n) => n, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let id = with_graph_read(&n.project, |g| { - g.get(n.id).map(|e| e.behavior.type_id().to_string()) - }); - match id { - Some(id) => copy_string_out(&id, buf, buf_size), - None => crate::error::OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_node_get_name` (two-stage; Node::name()). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_get_name( - node: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let n = match unsafe { node_ref(&node) } { - Ok(n) => n, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let name = with_graph_read(&n.project, |g| { - g.get(n.id).map(|e| e.behavior.name().to_string()) - }); - match name { - Some(name) => copy_string_out(&name, buf, buf_size), - None => crate::error::OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_node_get_label` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_get_label( - node: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let n = match unsafe { node_ref(&node) } { - Ok(n) => n, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let label = with_graph_read(&n.project, |g| g.get(n.id).map(|e| e.core.label.clone())); - match label { - Some(label) => copy_string_out(&label, buf, buf_size), - None => crate::error::OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_node_set_label`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_set_label(node: CHandle, label: *const c_char) -> c_int { - guard(|| { - let n = unsafe { node_ref(&node)? }; - let label = unsafe { cstr(label) }.ok_or(Error::Invalid)?; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - entry.core.label = label.to_string(); - Ok(()) - }) - } - - /// `oaknode_node_set_label_undoable` (olive::NodeRenameCommand). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_set_label_undoable( - node: CHandle, - label: *const c_char, - out_command: *mut CHandle, - ) -> c_int { - guard(|| { - if out_command.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let label = unsafe { cstr(label) }.ok_or(Error::Invalid)?.to_string(); - let project = n.project.clone(); - let id = n.id; - let old = { - let guard = lock(&project); - guard - .graph - .get(id) - .map(|e| e.core.label.clone()) - .ok_or(Error::NotFound)? - }; - let cmd = undo_command( - { - let label = label.clone(); - let project = project.clone(); - move || { - let mut guard = lock(&project); - if let Some(e) = guard.graph.get_mut(id) { - e.core.label = label.clone(); - } - } - }, - move || { - let mut guard = lock(&project); - if let Some(e) = guard.graph.get_mut(id) { - e.core.label = old.clone(); - } - }, - )?; - unsafe { *out_command = cmd }; - Ok(()) - }) - } - - /// `oaknode_node_get_override_color`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_get_override_color( - node: CHandle, - out_value: *mut c_int, - ) -> c_int { - guard(|| { - if out_value.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let v = with_graph_read(&n.project, |g| g.get(n.id).map(|e| e.core.override_color)); - let v = v.ok_or(Error::NotFound)?; - unsafe { *out_value = v }; - Ok(()) - }) - } - - /// `oaknode_node_set_override_color`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_set_override_color(node: CHandle, index: c_int) -> c_int { - guard(|| { - let n = unsafe { node_ref(&node)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - entry.core.override_color = index; - Ok(()) - }) - } - - /// `oaknode_node_set_override_color_undoable`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_set_override_color_undoable( - node: CHandle, - index: c_int, - out_command: *mut CHandle, - ) -> c_int { - guard(|| { - if out_command.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let project = n.project.clone(); - let id = n.id; - let old = { - let guard = lock(&project); - guard - .graph - .get(id) - .map(|e| e.core.override_color) - .ok_or(Error::NotFound)? - }; - let cmd = undo_command( - { - let project = project.clone(); - move || { - let mut guard = lock(&project); - if let Some(e) = guard.graph.get_mut(id) { - e.core.override_color = index; - } - } - }, - move || { - let mut guard = lock(&project); - if let Some(e) = guard.graph.get_mut(id) { - e.core.override_color = old; - } - }, - )?; - unsafe { *out_command = cmd }; - Ok(()) - }) - } - - /// `oaknode_node_is_enabled`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_is_enabled( - node: CHandle, - out_value: *mut c_int, - ) -> c_int { - guard(|| { - if out_value.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let enabled = with_graph_read(&n.project, |g| { - g.get(n.id).map(|e| { - e.core - .standard_value(crate::node::ENABLED_INPUT, -1) - .to_double() != 0.0 - }) - }); - let enabled = enabled.ok_or(Error::NotFound)?; - unsafe { *out_value = enabled as c_int }; - Ok(()) - }) - } - - /// `oaknode_node_set_enabled`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_set_enabled(node: CHandle, enabled: c_int) -> c_int { - guard(|| { - let n = unsafe { node_ref(&node)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - if !entry.core.has_input(crate::node::ENABLED_INPUT) { - return Err(Error::NotFound); - } - entry.core.set_standard_value( - crate::node::ENABLED_INPUT, - -1, - NodeValue::Boolean(enabled != 0), - ); - Ok(()) - }) - } - - /// `oaknode_node_set_enabled_undoable`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_set_enabled_undoable( - node: CHandle, - enabled: c_int, - out_command: *mut CHandle, - ) -> c_int { - guard(|| { - if out_command.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let project = n.project.clone(); - let id = n.id; - let old = { - let guard = lock(&project); - guard - .graph - .get(id) - .map(|e| e.core.standard_value(crate::node::ENABLED_INPUT, -1)) - .ok_or(Error::NotFound)? - }; - let new = NodeValue::Boolean(enabled != 0); - let cmd = undo_command( - { - let new = new.clone(); - let project = project.clone(); - move || { - let mut guard = lock(&project); - if let Some(e) = guard.graph.get_mut(id) { - e.core - .set_standard_value(crate::node::ENABLED_INPUT, -1, new.clone()); - } - } - }, - move || { - let mut guard = lock(&project); - if let Some(e) = guard.graph.get_mut(id) { - e.core - .set_standard_value(crate::node::ENABLED_INPUT, -1, old.clone()); - } - }, - )?; - unsafe { *out_command = cmd }; - Ok(()) - }) - } - - /// `oaknode_node_get_effect_input` (two-stage; `Node::GetEffectInputID()`). - /// - /// The id of the input the effect chain attaches to (empty when the node - /// cannot host effects — the C++ contract: "If this is empty, effects - /// cannot attach to this node"). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_get_effect_input( - node: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let n = match unsafe { node_ref(&node) } { - Ok(n) => n, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let effect_input = with_graph_read(&n.project, |g| { - g.get(n.id).map(|e| e.core.effect_input.clone()) - }); - match effect_input { - Some(id) => copy_string_out(&id, buf, buf_size), - None => crate::error::OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_node_get_flags` — the node's flags bitmask (`Node::flags_`). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_get_flags(node: CHandle) -> u64 { - let n = match unsafe { node_ref(&node) } { - Ok(n) => n, - Err(_) => return 0, - }; - with_graph_read(&n.project, |g| g.get(n.id).map(|e| e.core.flags)).unwrap_or(0) - } - - // ---- Input introspection ------------------------------------------ - - /// `oaknode_node_input_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_input_count( - node: CHandle, - out_count: *mut c_int, - ) -> c_int { - guard(|| { - if out_count.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let count = with_graph_read(&n.project, |g| g.get(n.id).map(|e| e.core.inputs.len())); - let count = count.ok_or(Error::NotFound)?; - unsafe { *out_count = count as c_int }; - Ok(()) - }) - } - - /// `oaknode_node_input_id` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_input_id( - node: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let n = match unsafe { node_ref(&node) } { - Ok(n) => n, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - if index < 0 { - return crate::error::OAKNODE_E_NOT_FOUND; - } - let id = with_graph_read(&n.project, |g| { - g.get(n.id) - .and_then(|e| e.core.inputs.get(index as usize).map(|i| i.id.clone())) - }); - match id { - Some(id) => copy_string_out(&id, buf, buf_size), - None => crate::error::OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_node_input_get_type`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_input_get_type( - node: CHandle, - input_id: *const c_char, - out_type: *mut c_int, - ) -> c_int { - guard(|| { - if out_type.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let input_id = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?; - let ty = with_graph_read(&n.project, |g| { - g.get(n.id).and_then(|e| e.core.input_data_type(input_id)) - }); - let ty = ty.ok_or(Error::NotFound)?; - unsafe { *out_type = ty.to_oak() }; - Ok(()) - }) - } - - /// `oaknode_node_input_is_connected`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_input_is_connected( - node: CHandle, - input_id: *const c_char, - out_value: *mut c_int, - ) -> c_int { - guard(|| { - if out_value.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let input_id = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?; - let connected = with_graph_read(&n.project, |g| { - let has = g - .get(n.id) - .map(|e| e.core.has_input(input_id)) - .unwrap_or(false); - has.then(|| g.is_input_connected(n.id, input_id, -1)) - }); - match connected { - Some(v) => { - unsafe { *out_value = v as c_int }; - Ok(()) - } - None => Err(Error::NotFound), - } - }) - } - - /// `oaknode_node_input_is_connectable`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_input_is_connectable( - node: CHandle, - input_id: *const c_char, - out_value: *mut c_int, - ) -> c_int { - guard(|| { - if out_value.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let input_id = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?; - let connectable = with_graph_read(&n.project, |g| { - g.get(n.id) - .and_then(|e| e.core.get_input(input_id)) - .map(|i| i.is_connectable()) - }); - match connectable { - Some(v) => { - unsafe { *out_value = v as c_int }; - Ok(()) - } - None => Err(Error::NotFound), - } - }) - } - - /// `oaknode_node_get_input_name` (two-stage; virtual). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_get_input_name( - node: CHandle, - input_id: *const c_char, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let n = match unsafe { node_ref(&node) } { - Ok(n) => n, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let input_id = match unsafe { cstr(input_id) } { - Some(s) => s, - None => return crate::error::OAKNODE_E_INVALID, - }; - let name = with_graph_read(&n.project, |g| { - g.get(n.id).and_then(|e| { - e.core.has_input(input_id).then(|| { - // enabled_in displays as "Enabled" on every node - // (`// CPP-PARITY: node.cpp:155` retranslate); node - // overrides name their own inputs. - if input_id == crate::node::ENABLED_INPUT { - "Enabled".to_string() - } else { - e.behavior.input_name(input_id).to_string() - } - }) - }) - }); - match name { - Some(name) => copy_string_out(&name, buf, buf_size), - None => crate::error::OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_node_input_get_connected_node` (borrowed handle). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_input_get_connected_node( - node: CHandle, - input_id: *const c_char, - out_node: *mut CHandle, - ) -> c_int { - guard(|| { - if out_node.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let input_id = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?; - let result = with_graph_read(&n.project, |g| { - let has = g - .get(n.id) - .map(|e| e.core.has_input(input_id)) - .unwrap_or(false); - if !has { - return None; - } - Some(g.connected_output(n.id, input_id, -1)) - }); - let source = result.ok_or(Error::NotFound)?; - unsafe { - *out_node = match source { - Some(from) => make_node_borrowed(n.project.clone(), from), - None => CHandle::null(), - }; - } - Ok(()) - }) - } - - // ---- Parameter access ---------------------------------------------- - - /// `oaknode_node_get_input`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_get_input( - node: CHandle, - input_id: *const c_char, - out: *mut OakNodeValue, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let input_id = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?; - let (declared, value) = with_graph_read(&n.project, |g| { - let entry = g.get(n.id)?; - let declared = entry.core.input_data_type(input_id)?; - let value = entry.core.standard_value(input_id, -1); - Some((declared, value)) - }) - .ok_or(Error::NotFound)?; - let pod = OakNodeValue::from_node_value(declared, &value)?; - unsafe { *out = pod }; - Ok(()) - }) - } - - /// `oaknode_node_set_input`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_set_input( - node: CHandle, - input_id: *const c_char, - v: *const OakNodeValue, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&node)? }; - let input_id = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?; - let v = if v.is_null() { - return Err(Error::Invalid); - } else { - unsafe { *v } - }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let declared = entry - .core - .input_data_type(input_id) - .ok_or(Error::NotFound)?; - // POD type must match the declared input type (C++ - // `variant_for_input`; INT and COMBO are interchangeable, and - // string-carried inputs are rejected here). - let kind_ok = match declared { - ValueType::Int | ValueType::Combo => { - v.kind == crate::value::oak::INT || v.kind == crate::value::oak::COMBO - } - _ => v.kind == declared.to_oak(), - }; - if declared.is_string() || !kind_ok { - return Err(Error::Invalid); - } - let value = v.to_node_value(declared)?; - entry.core.set_standard_value(input_id, -1, value); - Ok(()) - }) - } - - /// `oaknode_node_set_input_undoable`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_set_input_undoable( - node: CHandle, - input_id: *const c_char, - v: *const OakNodeValue, - out_command: *mut CHandle, - ) -> c_int { - guard(|| { - if out_command.is_null() || v.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let input_id = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?; - let v = unsafe { *v }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let declared = entry - .core - .input_data_type(input_id) - .ok_or(Error::NotFound)?; - let kind_ok = match declared { - ValueType::Int | ValueType::Combo => { - v.kind == crate::value::oak::INT || v.kind == crate::value::oak::COMBO - } - _ => v.kind == declared.to_oak(), - }; - if declared.is_string() || !kind_ok { - return Err(Error::Invalid); - } - let value = v.to_node_value(declared)?; - let old = entry.core.standard_value(input_id, -1); - let project = n.project.clone(); - let id = n.id; - let input_id = input_id.to_string(); - let cmd = undo_command( - { - let value = value.clone(); - let input_id = input_id.clone(); - let project = project.clone(); - move || { - let mut guard = lock(&project); - if let Some(e) = guard.graph.get_mut(id) { - e.core.set_standard_value(&input_id, -1, value.clone()); - } - } - }, - move || { - let mut guard = lock(&project); - if let Some(e) = guard.graph.get_mut(id) { - e.core.set_standard_value(&input_id, -1, old.clone()); - } - }, - )?; - unsafe { *out_command = cmd }; - Ok(()) - }) - } - - /// `oaknode_node_get_input_string` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_get_input_string( - node: CHandle, - input_id: *const c_char, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let n = match unsafe { node_ref(&node) } { - Ok(n) => n, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let input_id = match unsafe { cstr(input_id) } { - Some(s) => s, - None => return crate::error::OAKNODE_E_INVALID, - }; - let value = with_graph_read(&n.project, |g| { - let entry = g.get(n.id)?; - let declared = entry.core.input_data_type(input_id)?; - if !declared.is_string() { - return None; - } - let v = entry.core.standard_value(input_id, -1); - let s = match &v { - NodeValue::Text(s) => s.clone(), - NodeValue::StrCombo(s) => s.clone(), - _ => v.to_double().to_string(), - }; - Some(s) - }); - match value { - Some(s) => copy_string_out(&s, buf, buf_size), - // Distinguish unknown input (NOT_FOUND) from non-string - // input (INVALID): re-probe. - None => { - let has = with_graph_read(&n.project, |g| { - g.get(n.id) - .map(|e| e.core.has_input(input_id)) - .unwrap_or(false) - }); - if has { - crate::error::OAKNODE_E_INVALID - } else { - crate::error::OAKNODE_E_NOT_FOUND - } - } - } - } - - /// `oaknode_node_set_input_string`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_set_input_string( - node: CHandle, - input_id: *const c_char, - value: *const c_char, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&node)? }; - let input_id = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?; - let value = unsafe { cstr(value) }.ok_or(Error::Invalid)?; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let declared = entry - .core - .input_data_type(input_id) - .ok_or(Error::NotFound)?; - if !declared.is_string() { - return Err(Error::Invalid); - } - let v = match declared { - ValueType::StrCombo => NodeValue::StrCombo(value.to_string()), - _ => NodeValue::Text(value.to_string()), - }; - entry.core.set_standard_value(input_id, -1, v); - Ok(()) - }) - } - - /// `oaknode_node_set_input_string_undoable`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_set_input_string_undoable( - node: CHandle, - input_id: *const c_char, - value: *const c_char, - out_command: *mut CHandle, - ) -> c_int { - guard(|| { - if out_command.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let input_id = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?; - let value = unsafe { cstr(value) }.ok_or(Error::Invalid)?; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let declared = entry - .core - .input_data_type(input_id) - .ok_or(Error::NotFound)?; - if !declared.is_string() { - return Err(Error::Invalid); - } - let old = entry.core.standard_value(input_id, -1); - let new = match declared { - ValueType::StrCombo => NodeValue::StrCombo(value.to_string()), - _ => NodeValue::Text(value.to_string()), - }; - let project = n.project.clone(); - let id = n.id; - let input_id = input_id.to_string(); - let cmd = undo_command( - { - let new = new.clone(); - let input_id = input_id.clone(); - let project = project.clone(); - move || { - let mut guard = lock(&project); - if let Some(e) = guard.graph.get_mut(id) { - e.core.set_standard_value(&input_id, -1, new.clone()); - } - } - }, - move || { - let mut guard = lock(&project); - if let Some(e) = guard.graph.get_mut(id) { - e.core.set_standard_value(&input_id, -1, old.clone()); - } - }, - )?; - unsafe { *out_command = cmd }; - Ok(()) - }) - } - - // ---- Graph editing ------------------------------------------------- - - /// `oaknode_node_connect`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_connect( - output_node: CHandle, - input_node: CHandle, - input_id: *const c_char, - ) -> c_int { - guard(|| { - let from = unsafe { node_ref(&output_node)? }; - let to = unsafe { node_ref(&input_node)? }; - let input_id = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?; - if !Arc::ptr_eq(&from.project, &to.project) { - return Err(Error::State); - } - let mut guard = lock(&from.project); - guard.graph.connect(from.id, to.id, input_id, -1) - }) - } - - /// `oaknode_node_connect_undoable` (olive::NodeEdgeAddCommand). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_connect_undoable( - output_node: CHandle, - input_node: CHandle, - input_id: *const c_char, - out_command: *mut CHandle, - ) -> c_int { - guard(|| { - if out_command.is_null() { - return Err(Error::Invalid); - } - let from = unsafe { node_ref(&output_node)? }; - let to = unsafe { node_ref(&input_node)? }; - let input_id = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?; - let mut guard = lock(&from.project); - // Validate like the live variant (without executing). - let entry = guard.graph.get(to.id).ok_or(Error::NotFound)?; - let input = entry.core.get_input(input_id).ok_or(Error::NotFound)?; - if !input.is_connectable() { - return Err(Error::Invalid); - } - let project = from.project.clone(); - let from_id = from.id; - let to_id = to.id; - let input_id = input_id.to_string(); - let cmd = undo_command( - { - let input_id = input_id.clone(); - let project = project.clone(); - move || { - let mut guard = lock(&project); - guard.graph.connect(from_id, to_id, &input_id, -1).ok(); - } - }, - move || { - let mut guard = lock(&project); - guard.graph.disconnect(from_id, to_id, &input_id, -1); - }, - )?; - unsafe { *out_command = cmd }; - Ok(()) - }) - } - - /// `oaknode_node_disconnect`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_disconnect( - input_node: CHandle, - input_id: *const c_char, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&input_node)? }; - let input_id = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?; - let mut guard = lock(&n.project); - let has = guard - .graph - .get(n.id) - .map(|e| e.core.has_input(input_id)) - .unwrap_or(false); - if !has { - return Err(Error::NotFound); - } - match guard.graph.disconnect_input(n.id, input_id, -1) { - Some(_) => Ok(()), - None => Err(Error::NotFound), - } - }) - } - - /// `oaknode_node_disconnect_undoable` (olive::NodeEdgeRemoveCommand). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_disconnect_undoable( - input_node: CHandle, - input_id: *const c_char, - out_command: *mut CHandle, - ) -> c_int { - guard(|| { - if out_command.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&input_node)? }; - let input_id = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?; - let guard = lock(&n.project); - let from = guard - .graph - .connected_output(n.id, input_id, -1) - .ok_or(Error::NotFound)?; - let project = n.project.clone(); - let from_id = from; - let to_id = n.id; - let input_id = input_id.to_string(); - let cmd = undo_command( - { - let input_id = input_id.clone(); - let project = project.clone(); - move || { - let mut guard = lock(&project); - guard.graph.disconnect(from_id, to_id, &input_id, -1); - } - }, - move || { - let mut guard = lock(&project); - guard.graph.connect(from_id, to_id, &input_id, -1).ok(); - }, - )?; - unsafe { *out_command = cmd }; - Ok(()) - }) - } - - /// `oaknode_node_output_connection_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_output_connection_count( - node: CHandle, - out_count: *mut c_int, - ) -> c_int { - guard(|| { - if out_count.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let count = with_graph_read(&n.project, |g| { - g.get(n.id).map(|_| g.output_connections(n.id).len()) - }); - let count = count.ok_or(Error::NotFound)?; - unsafe { *out_count = count as c_int }; - Ok(()) - }) - } - - /// `oaknode_node_output_connection_node_at` (borrowed handle). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_output_connection_node_at( - node: CHandle, - index: c_int, - out_node: *mut CHandle, - ) -> c_int { - guard(|| { - if out_node.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - if index < 0 { - return Err(Error::NotFound); - } - let conns = with_graph_read(&n.project, |g| g.output_connections(n.id)); - let target = conns - .get(index as usize) - .map(|(to, _, _)| *to) - .ok_or(Error::NotFound)?; - unsafe { *out_node = make_node_borrowed(n.project.clone(), target) }; - Ok(()) - }) - } - - /// `oaknode_node_output_connection_input_id_at` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_output_connection_input_id_at( - node: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let n = match unsafe { node_ref(&node) } { - Ok(n) => n, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - if index < 0 { - return crate::error::OAKNODE_E_NOT_FOUND; - } - let conns = with_graph_read(&n.project, |g| g.output_connections(n.id)); - match conns.get(index as usize) { - Some((_, input, _)) => copy_string_out(input, buf, buf_size), - None => crate::error::OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_node_output_connection_element_at`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_output_connection_element_at( - node: CHandle, - index: c_int, - out_element: *mut c_int, - ) -> c_int { - guard(|| { - if out_element.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - if index < 0 { - return Err(Error::NotFound); - } - let conns = with_graph_read(&n.project, |g| g.output_connections(n.id)); - let element = conns - .get(index as usize) - .map(|(_, _, e)| *e) - .ok_or(Error::NotFound)?; - unsafe { *out_element = element }; - Ok(()) - }) - } - - // ---- Links --------------------------------------------------------- - - /// `oaknode_node_link`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_link( - a: CHandle, - b: CHandle, - out_linked: *mut c_int, - ) -> c_int { - guard(|| { - let a = unsafe { node_ref(&a)? }; - let b = unsafe { node_ref(&b)? }; - if !Arc::ptr_eq(&a.project, &b.project) { - if !out_linked.is_null() { - unsafe { *out_linked = 0 }; - } - return Ok(()); - } - let mut guard = lock(&a.project); - let linked = guard.graph.link(a.id, b.id); - if !out_linked.is_null() { - unsafe { *out_linked = linked as c_int }; - } - Ok(()) - }) - } - - /// `oaknode_node_unlink`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_unlink( - a: CHandle, - b: CHandle, - out_unlinked: *mut c_int, - ) -> c_int { - guard(|| { - let a = unsafe { node_ref(&a)? }; - let b = unsafe { node_ref(&b)? }; - if !Arc::ptr_eq(&a.project, &b.project) { - if !out_unlinked.is_null() { - unsafe { *out_unlinked = 0 }; - } - return Ok(()); - } - let mut guard = lock(&a.project); - let unlinked = guard.graph.unlink(a.id, b.id); - if !out_unlinked.is_null() { - unsafe { *out_unlinked = unlinked as c_int }; - } - Ok(()) - }) - } - - /// `oaknode_node_link_undoable` (olive::NodeLinkCommand). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_link_undoable( - a: CHandle, - b: CHandle, - link: c_int, - out_command: *mut CHandle, - ) -> c_int { - guard(|| { - if out_command.is_null() { - return Err(Error::Invalid); - } - let a = unsafe { node_ref(&a)? }; - let b = unsafe { node_ref(&b)? }; - if !Arc::ptr_eq(&a.project, &b.project) { - return Err(Error::Invalid); - } - let project = a.project.clone(); - let a_id = a.id; - let b_id = b.id; - let cmd = if link != 0 { - undo_command( - { - let project = project.clone(); - move || { - let mut guard = lock(&project); - guard.graph.link(a_id, b_id); - } - }, - move || { - let mut guard = lock(&project); - guard.graph.unlink(a_id, b_id); - }, - )? - } else { - undo_command( - { - let project = project.clone(); - move || { - let mut guard = lock(&project); - guard.graph.unlink(a_id, b_id); - } - }, - move || { - let mut guard = lock(&project); - guard.graph.link(a_id, b_id); - }, - )? - }; - unsafe { *out_command = cmd }; - Ok(()) - }) - } - - /// `oaknode_node_are_linked`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_are_linked( - a: CHandle, - b: CHandle, - out_value: *mut c_int, - ) -> c_int { - guard(|| { - if out_value.is_null() { - return Err(Error::Invalid); - } - let a = unsafe { node_ref(&a)? }; - let b = unsafe { node_ref(&b)? }; - let linked = with_graph_read(&a.project, |g| g.are_linked(a.id, b.id)); - unsafe { *out_value = linked as c_int }; - Ok(()) - }) - } - - /// `oaknode_node_link_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_link_count( - node: CHandle, - out_count: *mut c_int, - ) -> c_int { - guard(|| { - if out_count.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let count = with_graph_read(&n.project, |g| g.get(n.id).map(|e| e.core.links.len())); - let count = count.ok_or(Error::NotFound)?; - unsafe { *out_count = count as c_int }; - Ok(()) - }) - } - - /// `oaknode_node_link_at` (borrowed handle). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_link_at( - node: CHandle, - index: c_int, - out_node: *mut CHandle, - ) -> c_int { - guard(|| { - if out_node.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - if index < 0 { - return Err(Error::NotFound); - } - let links = with_graph_read(&n.project, |g| g.links_of(n.id)); - let target = links.get(index as usize).copied().ok_or(Error::NotFound)?; - unsafe { *out_node = make_node_borrowed(n.project.clone(), target) }; - Ok(()) - }) - } - - // ---- Context positions --------------------------------------------- - - /// `oaknode_node_context_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_context_count( - node: CHandle, - out_count: *mut c_int, - ) -> c_int { - guard(|| { - if out_count.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let count = with_graph_read(&n.project, |g| { - g.get(n.id).map(|e| e.core.context_positions.len()) - }); - let count = count.ok_or(Error::NotFound)?; - unsafe { *out_count = count as c_int }; - Ok(()) - }) - } - - /// `oaknode_node_context_node_at` (borrowed handle). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_context_node_at( - node: CHandle, - index: c_int, - out_node: *mut CHandle, - ) -> c_int { - guard(|| { - if out_node.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - if index < 0 { - return Err(Error::NotFound); - } - let context = with_graph_read(&n.project, |g| { - g.get(n.id).and_then(|e| { - e.core - .context_positions - .get(index as usize) - .map(|(c, _, _)| *c) - }) - }); - let context = context.ok_or(Error::NotFound)?; - unsafe { *out_node = make_node_borrowed(n.project.clone(), context) }; - Ok(()) - }) - } - - /// `oaknode_node_get_context_position`. - #[no_mangle] - pub unsafe extern "C" 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 { - guard(|| { - let n = unsafe { node_ref(&node)? }; - let ctx = unsafe { node_ref(&context)? }; - if !Arc::ptr_eq(&n.project, &ctx.project) { - return Err(Error::Invalid); - } - let pos = with_graph_read(&n.project, |g| { - g.get(n.id).and_then(|e| { - e.core - .context_positions - .iter() - .find(|(c, _, _)| *c == ctx.id) - .map(|(_, p, exp)| (*p, *exp)) - }) - }); - match pos { - Some(((x, y), expanded)) => { - if !out_x.is_null() { - unsafe { *out_x = x }; - } - if !out_y.is_null() { - unsafe { *out_y = y }; - } - if !out_expanded.is_null() { - unsafe { *out_expanded = expanded as c_int }; - } - Ok(()) - } - None => Err(Error::NotFound), - } - }) - } - - /// `oaknode_node_set_context_position`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_set_context_position( - node: CHandle, - context: CHandle, - x: f64, - y: f64, - expanded: c_int, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&node)? }; - let ctx = unsafe { node_ref(&context)? }; - if !Arc::ptr_eq(&n.project, &ctx.project) { - return Err(Error::Invalid); - } - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - entry.core.set_context_position(ctx.id, x, y, expanded != 0); - Ok(()) - }) - } - - /// `oaknode_node_set_context_position_undoable`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_set_context_position_undoable( - node: CHandle, - context: CHandle, - x: f64, - y: f64, - expanded: c_int, - out_command: *mut CHandle, - ) -> c_int { - guard(|| { - if out_command.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let ctx = unsafe { node_ref(&context)? }; - if !Arc::ptr_eq(&n.project, &ctx.project) { - return Err(Error::Invalid); - } - let project = n.project.clone(); - let id = n.id; - let ctx_id = ctx.id; - let old = { - let guard = lock(&project); - guard - .graph - .get(id) - .and_then(|e| { - e.core - .context_positions - .iter() - .find(|(c, _, _)| *c == ctx_id) - .map(|(_, p, exp)| (*p, *exp)) - }) - .ok_or(Error::NotFound)? - }; - let cmd = undo_command( - { - let project = project.clone(); - move || { - let mut guard = lock(&project); - if let Some(e) = guard.graph.get_mut(id) { - e.core.set_context_position(ctx_id, x, y, expanded != 0); - } - } - }, - move || { - let mut guard = lock(&project); - if let Some(e) = guard.graph.get_mut(id) { - e.core - .set_context_position(ctx_id, old.0 .0, old.0 .1, old.1); - } - }, - )?; - unsafe { *out_command = cmd }; - Ok(()) - }) - } - - /// `oaknode_node_remove_from_context`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_remove_from_context( - node: CHandle, - context: CHandle, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&node)? }; - let ctx = unsafe { node_ref(&context)? }; - if !Arc::ptr_eq(&n.project, &ctx.project) { - return Err(Error::Invalid); - } - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - if entry.core.remove_from_context(ctx.id) { - Ok(()) - } else { - Err(Error::NotFound) - } - }) - } - - // ---- Lifetime ------------------------------------------------------ - - /// `oaknode_node_create_copy` (owned orphan copy). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_create_copy(node: CHandle) -> CHandle { - let n = match unsafe { node_ref(&node) } { - Ok(n) => n, - Err(_) => return CHandle::null(), - }; - let copied: Option<(crate::node::NodeCore, Box)> = - with_graph_read(&n.project, |g| { - g.get(n.id).map(|e| { - let core = e.core.clone(); - let behavior = e - .behavior - .duplicate(&core) - .unwrap_or_else(|| Box::new(crate::nodes::EmptyBehavior)); - (core, behavior) - }) - }); - match copied { - Some((core, behavior)) => { - let scratch = Project::new(); - let id = { - let mut guard = lock(&scratch); - guard.graph.add_node(core, behavior) - }; - make_node_owned(scratch, id) - } - None => CHandle::null(), - } - } - - /// `oaknode_node_copy_in_graph`: copy a node inside its graph; the - /// returned command inserts the copy on redo and removes it on undo. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_copy_in_graph( - node: CHandle, - out_command: *mut CHandle, - ) -> CHandle { - let n = match unsafe { node_ref(&node) } { - Ok(n) => n, - Err(_) => return CHandle::null(), - }; - if out_command.is_null() { - return CHandle::null(); - } - let project = n.project.clone(); - let project_redo = project.clone(); - let project_undo = project.clone(); - let src_id = n.id; - let inserted: std::sync::Arc>> = - std::sync::Arc::new(std::sync::Mutex::new(None)); - let cmd = match undo_command( - { - let project = project_redo; - let inserted = inserted.clone(); - move || { - let mut g = lock(&project); - if let Some(e) = g.graph.get(src_id) { - let core = e.core.clone(); - let behavior = e - .behavior - .duplicate(&core) - .unwrap_or_else(|| Box::new(crate::nodes::EmptyBehavior)); - let id = g.graph.add_node(core, behavior); - *inserted.lock().unwrap() = Some(id); - } - } - }, - move || { - let mut g = lock(&project_undo); - if let Some(id) = inserted.lock().unwrap().take() { - g.graph.remove_node(id); - } - }, - ) { - Ok(h) => h, - Err(_) => return CHandle::null(), - }; - unsafe { *out_command = cmd }; - // The returned handle is the (not-yet-inserted) copy in a fresh - // scratch project, owned by the caller. - let scratch = Project::new(); - let copied: Option<(crate::node::NodeCore, Box)> = - with_graph_read(&project, |g| { - g.get(src_id).map(|e| { - let core = e.core.clone(); - let behavior = e - .behavior - .duplicate(&core) - .unwrap_or_else(|| Box::new(crate::nodes::EmptyBehavior)); - (core, behavior) - }) - }); - let (core, behavior) = match copied { - Some(pair) => pair, - None => return CHandle::null(), - }; - let copy_id = { - let mut g = lock(&scratch); - g.graph.add_node(core, behavior) - }; - make_node_owned(scratch, copy_id) - } - - /// `oaknode_node_get_project` (borrowed project handle). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_get_project(node: CHandle, out: *mut CHandle) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - unsafe { *out = make_project_borrowed(n.project.clone()) }; - Ok(()) - }) - } - - /// `oaknode_node_input_array_insert`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_input_array_insert( - node: CHandle, - input_id: *const c_char, - index: c_int, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&node)? }; - let input_id = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?; - let mut guard = lock(&n.project); - guard.graph.input_array_insert(n.id, input_id, index) - }) - } - - /// `oaknode_node_input_array_remove`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_input_array_remove( - node: CHandle, - input_id: *const c_char, - index: c_int, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&node)? }; - let input_id = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?; - let mut guard = lock(&n.project); - guard.graph.input_array_remove(n.id, input_id, index) - }) - } - - /// `oaknode_node_connect_element`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_connect_element( - output_node: CHandle, - input_node: CHandle, - input_id: *const c_char, - element: c_int, - ) -> c_int { - guard(|| { - let from = unsafe { node_ref(&output_node)? }; - let to = unsafe { node_ref(&input_node)? }; - let input_id = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?; - if !Arc::ptr_eq(&from.project, &to.project) { - return Err(Error::State); - } - let mut guard = lock(&from.project); - guard.graph.connect(from.id, to.id, input_id, element) - }) - } - - /// `oaknode_node_disconnect_element`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_disconnect_element( - input_node: CHandle, - input_id: *const c_char, - element: c_int, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&input_node)? }; - let input_id = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?; - let mut guard = lock(&n.project); - let has = guard - .graph - .get(n.id) - .map(|e| e.core.has_input(input_id)) - .unwrap_or(false); - if !has { - return Err(Error::NotFound); - } - match guard.graph.disconnect_input(n.id, input_id, element) { - Some(_) => Ok(()), - None => Err(Error::NotFound), - } - }) - } - - /// `oaknode_command_create_add_node` (olive::NodeAddCommand). - #[no_mangle] - pub unsafe extern "C" fn oaknode_command_create_add_node( - graph: CHandle, - node: CHandle, - ) -> CHandle { - let dst = match unsafe { project_arc(&graph) } { - Ok(p) => p, - Err(_) => return CHandle::null(), - }; - let src = match unsafe { node_ref(&node) } { - Ok(n) => n, - Err(_) => return CHandle::null(), - }; - if Arc::ptr_eq(&src.project, &dst) { - return CHandle::null(); - } - let src_project = src.project.clone(); - let src_id = src.id; - match undo_command( - { - let src_project = src_project.clone(); - let dst = dst.clone(); - move || { - let entry = { - let mut g = lock(&src_project); - g.graph.take_node(src_id) - }; - if let Some(entry) = entry { - let mut g = lock(&dst); - g.graph.add_entry(entry, src_id); - } - } - }, - move || { - let mut g = lock(&dst); - g.graph.take_node(src_id); - }, - ) { - Ok(h) => h, - Err(_) => CHandle::null(), - } - } - - /// `oaknode_command_create_set_position_recursive`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_command_create_set_position_recursive( - node: CHandle, - context: CHandle, - x: f64, - y: f64, - ) -> CHandle { - let n = match unsafe { node_ref(&node) } { - Ok(n) => n, - Err(_) => return CHandle::null(), - }; - let ctx = match unsafe { node_ref(&context) } { - Ok(c) => c, - Err(_) => return CHandle::null(), - }; - if !Arc::ptr_eq(&n.project, &ctx.project) { - return CHandle::null(); - } - let project = n.project.clone(); - let id = n.id; - let ctx_id = ctx.id; - match undo_command( - { - let project = project.clone(); - move || { - let mut g = lock(&project); - if let Some(e) = g.graph.get_mut(id) { - e.core.set_context_position(ctx_id, x, y, true); - } - } - }, - move || { - let mut g = lock(&project); - if let Some(e) = g.graph.get_mut(id) { - e.core.remove_from_context(ctx_id); - } - }, - ) { - Ok(h) => h, - Err(_) => CHandle::null(), - } - } - - /// `oaknode_node_get_markers`: the viewer node's marker-list handle - /// (addref'd; empty when the node is not a viewer or has none — - /// `// CPP-PARITY: src/node/c_api/node.cpp:1285`). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_get_markers( - node: CHandle, - out: *mut std::ffi::c_void, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let markers = with_graph_read(&n.project, |g| { - g.get(n.id).and_then(|e| { - e.behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .map(|s| s.markers.clone()) - }) - }); - let mut h = markers.unwrap_or_else(crate::handle::CHandle::null); - if !h.ctx.is_null() { - if let Some(f) = h.addref { - unsafe { f(h.ctx) }; - } - } - unsafe { *(out as *mut crate::handle::CHandle) = h }; - Ok(()) - }) - } - - /// `oaknode_node_get_work_area`: the viewer node's work-area handle - /// (addref'd; empty when the node is not a viewer or has none). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_get_work_area( - node: CHandle, - out: *mut std::ffi::c_void, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let workarea = with_graph_read(&n.project, |g| { - g.get(n.id).and_then(|e| { - e.behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .map(|s| s.workarea.clone()) - }) - }); - let mut h = workarea.unwrap_or_else(crate::handle::CHandle::null); - if !h.ctx.is_null() { - if let Some(f) = h.addref { - unsafe { f(h.ctx) }; - } - } - unsafe { *(out as *mut crate::handle::CHandle) = h }; - Ok(()) - }) - } - - /// `oaknode_node_get_video_frame_cache`: the node's video cache handle - /// (addref'd; empty when none — `// CPP-PARITY: node.cpp:1323`). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_get_video_frame_cache( - node: CHandle, - out: *mut std::ffi::c_void, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let cache = with_graph_read(&n.project, |g| { - g.get(n.id).map(|e| e.core.caches.video.clone()) - }); - let mut h = cache.unwrap_or_else(crate::handle::CHandle::null); - if !h.ctx.is_null() { - if let Some(f) = h.addref { - unsafe { f(h.ctx) }; - } - } - unsafe { *(out as *mut crate::handle::CHandle) = h }; - Ok(()) - }) - } - - /// `oaknode_node_copy_inputs`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_copy_inputs( - dst: CHandle, - src: CHandle, - include_connections: c_int, - ) -> c_int { - guard(|| { - let dst = unsafe { node_ref(&dst)? }; - let src = unsafe { node_ref(&src)? }; - if !Arc::ptr_eq(&dst.project, &src.project) { - return Err(Error::Invalid); - } - let mut guard = lock(&dst.project); - crate::ops::copy_inputs(&mut guard.graph, src.id, dst.id, include_connections != 0) - }) - } - - /// `oaknode_node_set_value_hint_track`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_set_value_hint_track( - node: CHandle, - input_id: *const c_char, - track_type: c_int, - track_index: c_int, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&node)? }; - let input_id = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - if !entry.core.has_input(input_id) { - return Err(Error::NotFound); - } - // Track reference tag: ":" (C++ - // `Track::Reference::to_string` shape; the exact separator is - // pinned when the track family lands — `// CPP-PARITY: - // src/node/src/output/track/track.h`). - let hint = crate::input::ValueHint { - types: vec![ValueType::Texture], - index: track_index, - tag: format!("{}:{}", track_type, track_index), - }; - entry.core.set_value_hint(input_id, -1, hint); - Ok(()) - }) - } - - /// `oaknode_viewer_set_video_params`: replace a viewer node's video - /// params (stream 0) from an oakcommon handle - /// (`// CPP-PARITY: node.cpp:1380`). The params are read through the - /// oakcommon videoparams getters; when oakcommon is unavailable the - /// handle cannot be read and `OAKNODE_E_INVALID` is returned (the - /// C++ `get_native` NULL path — documented degradation). - #[no_mangle] - pub unsafe extern "C" fn oaknode_viewer_set_video_params( - viewer: CHandle, - params: *const std::ffi::c_void, - ) -> c_int { - guard(|| { - if params.is_null() { - return Err(Error::Invalid); - } - let handle = unsafe { (*(params as *const crate::handle::CHandle)).clone() }; - if handle.ctx.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&viewer)? }; - let read = super::videoparams_from_handle(handle).ok_or(Error::Invalid)?; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let seq = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::Invalid)?; - if seq.video_params.is_empty() { - seq.video_params.push(read); - } else { - seq.video_params[0] = read; - } - Ok(()) - }) - } - - /// `oaknode_viewer_set_audio_params`: replace a viewer node's audio - /// params (stream 0) from an oakcore handle - /// (`// CPP-PARITY: node.cpp:1407`). Read through the oakcore - /// audioparams getters; `OAKNODE_E_INVALID` when oakcore is - /// unavailable (documented degradation). - #[no_mangle] - pub unsafe extern "C" fn oaknode_viewer_set_audio_params( - viewer: CHandle, - params: *const std::ffi::c_void, - ) -> c_int { - guard(|| { - if params.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&viewer)? }; - let read = { - let sample_rate = - crate::bridge::core::audioparams_sample_rate(params).ok_or(Error::Invalid)?; - let channel_layout = crate::bridge::core::audioparams_channel_layout(params) - .ok_or(Error::Invalid)?; - let format = - crate::bridge::core::audioparams_format(params).ok_or(Error::Invalid)?; - crate::value::AudioParams { - sample_rate, - channel_layout, - format, - } - }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let seq = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::Invalid)?; - if seq.audio_params.is_empty() { - seq.audio_params.push(read); - } else { - seq.audio_params[0] = read; - } - Ok(()) - }) - } - - /// `oaknode_node_find_input_footage`: first footage node feeding this - /// node's inputs, transitively (C++ - /// `Node::find_input_nodes()`; `// CPP-PARITY: node.cpp:1429`). - /// `out` receives a borrowed handle (empty when none). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_find_input_footage( - node: CHandle, - out: *mut CHandle, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let found = with_graph_read(&n.project, |g| { - let mut visited = std::collections::HashSet::new(); - let mut queue: Vec = g.upstream(n.id); - while let Some(id) = queue.pop() { - if !visited.insert(id) { - continue; - } - if let Some(entry) = g.get(id) { - let is_footage = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .is_some(); - if is_footage { - return Some(id); - } - } - for up in g.upstream(id) { - if !visited.contains(&up) { - queue.push(up); - } - } - } - None - }); - match found { - Some(id) => unsafe { *out = make_node_borrowed(n.project.clone(), id) }, - None => unsafe { *out = CHandle::null() }, - } - Ok(()) - }) - } - - /// `oaknode_node_get_input_at_time`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_get_input_at_time( - node: CHandle, - input_id: *const c_char, - time_num: i64, - time_den: i64, - out: *mut OakNodeValue, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let input_id = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?; - let time = oakcore_rs::Rational::new(time_num, time_den); - let (declared, value) = with_graph_read(&n.project, |g| { - let entry = g.get(n.id)?; - let declared = entry.core.input_data_type(input_id)?; - let value = entry.core.value_at_time(input_id, -1, time); - Some((declared, value)) - }) - .ok_or(Error::NotFound)?; - let pod = OakNodeValue::from_node_value(declared, &value)?; - unsafe { *out = pod }; - Ok(()) - }) - } - - /// `oaknode_node_set_input_at_time_undoable`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_set_input_at_time_undoable( - node: CHandle, - input_id: *const c_char, - time_num: i64, - time_den: i64, - v: *const OakNodeValue, - track: c_int, - out_command: *mut CHandle, - ) -> c_int { - guard(|| { - if out_command.is_null() || v.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let input_id = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?; - let v = unsafe { *v }; - let time = oakcore_rs::Rational::new(time_num, time_den); - let project = n.project.clone(); - let id = n.id; - let input_id_owned = input_id.to_string(); - // Validate the input + build the value. - let declared = { - let guard = lock(&project); - guard - .graph - .get(id) - .and_then(|e| e.core.input_data_type(input_id)) - .ok_or(Error::NotFound)? - }; - let value = v.to_node_value(declared)?; - let cmd = { - let mut guard = lock(&project); - crate::ops::set_value_at_time_command( - &project, - &guard.graph, - id, - &input_id_owned, - -1, - time, - &value, - )? - }; - let _ = track; - unsafe { *out_command = cmd }; - Ok(()) - }) - } - - /// `oaknode_node_identity`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_identity(node: CHandle) -> usize { - match unsafe { node_ref(&node) } { - Ok(n) => register_identity(&n) as usize, - Err(_) => 0, - } - } - - /// `oaknode_node_from_identity` (registry lookup). - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_from_identity(id: usize) -> CHandle { - node_from_identity_lookup(id as u64) - } - - /// `oaknode_node_set_input_at_time_into`: batch a value-at-time set - /// into an existing multi command. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_set_input_at_time_into( - node: CHandle, - input_id: *const c_char, - time_num: i64, - time_den: i64, - v: *const OakNodeValue, - track: c_int, - multi_command: CHandle, - ) -> c_int { - guard(|| { - if v.is_null() || multi_command.ctx.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let input_id = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?; - let v = unsafe { *v }; - let time = oakcore_rs::Rational::new(time_num, time_den); - let project = n.project.clone(); - let id = n.id; - let input_id_owned = input_id.to_string(); - let declared = { - let guard = lock(&project); - guard - .graph - .get(id) - .and_then(|e| e.core.input_data_type(input_id)) - .ok_or(Error::NotFound)? - }; - let value = v.to_node_value(declared)?; - let cmd = { - let mut guard = lock(&project); - crate::ops::set_value_at_time_command( - &project, - &guard.graph, - id, - &input_id_owned, - -1, - time, - &value, - )? - }; - let _ = track; - match crate::bridge::undo::command_multi_add_child(multi_command, cmd) { - Some(rc) if rc == 0 => Ok(()), - _ => Err(Error::Failed( - "failed to append to the multi command".to_string(), - )), - } - }) - } - - /// `oaknode_command_create_remove_node`: removes the node and - /// disconnects its edges (undo restores it). - #[no_mangle] - pub unsafe extern "C" fn oaknode_command_create_remove_node(node: CHandle) -> CHandle { - let n = match unsafe { node_ref(&node) } { - Ok(n) => n, - Err(_) => return CHandle::null(), - }; - let project = n.project.clone(); - let id = n.id; - match undo_command( - { - let project = project.clone(); - move || { - let mut g = lock(&project); - g.graph.take_node(id); - } - }, - move || { - let _ = project; - // Re-insertion is the caller's responsibility (the entry - // was dropped); undo restores nothing. - }, - ) { - Ok(h) => h, - Err(_) => CHandle::null(), - } - } - - /// `oaknode_node_free`: NULL/empty no-op. - #[no_mangle] - pub unsafe extern "C" fn oaknode_node_free(node: *mut CHandle) { - guard_void(|| { - if node.is_null() || unsafe { (*node).ctx.is_null() } { - return; - } - let h = unsafe { (*node).clone() }; - if let Some(f) = h.release { - unsafe { f(h.ctx) }; - } - unsafe { (*node).ctx = std::ptr::null_mut() }; - }); - } -} - -// ===================================================================== -// include/node/sequence.h -// ===================================================================== - -// ===================================================================== -// include/node/keyframe.h -// ===================================================================== - -/// `include/node/keyframe.h` exports (complete inventory): create/free, -/// get/set time (+_undoable), get/set value (+_undoable), -/// get/set value_string (+_undoable), get/set type (+_undoable), -/// get/set bezier_control (+_undoable), get track/element/input/parent, -/// get_valid_bezier_control, opposing_bezier_type, -/// compute_paste_value, has_sibling_at_time. -/// -/// Handles box a [`KfBox`]: a snapshot of the keyframe plus its -/// (optional) parent location. Every keyframe this ABI hands out is -/// standalone — like the C++ object it mirrors it owns its own -/// time/value/type; the parent reference is informational -/// (`get_parent`, and `has_sibling_at_time`/`compute_paste_value` -/// consult the parent's live track). Undoable setters create -/// `NodeParamSetKeyframe*Command`-equivalent commands whose closures -/// write through to the same box (the command holds a reference for its -/// whole lifetime). -pub mod keyframe { - use super::*; - use crate::keyframe::{Interpolation, Keyframe}; - use oakcore_rs::Rational; - use std::sync::Mutex; - - /// Keyframe handle payload (`// CPP-PARITY: src/node/src/keyframe.h` - /// `NodeKeyframe`). Behind a `Mutex` so undo-command closures can - /// write through to the same box the handle reads. - pub struct KfBox { - /// Parent project (Some when the keyframe was created with a - /// parent node). - pub project: Option, - /// Parent node id. - pub node: Option, - /// The input the keyframe belongs to (C++ `input_`). - pub input: String, - /// Array element (C++ `element_`). - pub element: i32, - /// Component track (C++ `track_`). - pub track: i32, - /// The keyframe data (time/value/interpolation/bezier). - pub data: Keyframe, - } - - /// RAII reference to a keyframe box held by an undo command's - /// closures: keeps the box alive for the command's lifetime and - /// releases it exactly once when the command is destroyed (dropping - /// the closure drops this guard). - struct KfKeep(CHandle); - - impl Drop for KfKeep { - fn drop(&mut self) { - if let Some(f) = self.0.release { - unsafe { f(self.0.ctx) }; - } - self.0.ctx = std::ptr::null_mut(); - } - } - - /// An addref'd copy of `h` for command closures. - fn kf_keep(h: &CHandle) -> KfKeep { - let mut c = h.clone(); - if let Some(f) = c.addref { - unsafe { f(c.ctx) }; - } - KfKeep(c) - } - - /// Read the boxed keyframe. - unsafe fn kf_get(h: &CHandle) -> Option<&Mutex> { - unsafe { crate::handle::get::>(h) } - } - - /// Lock the boxed keyframe. - unsafe fn kf_lock(h: &CHandle) -> Option> { - let m = unsafe { kf_get(h)? }; - Some(m.lock().unwrap_or_else(|e| e.into_inner())) - } - - /// Map an `oaknode_keyframe_type` to [`Interpolation`]. - fn interp_from_c(t: c_int) -> Result { - match t { - 0 => Ok(Interpolation::Linear), - 1 => Ok(Interpolation::Hold), - 2 => Ok(Interpolation::Bezier), - _ => Err(Error::Invalid), - } - } - - /// [`Interpolation`] -> `oaknode_keyframe_type`. - fn interp_to_c(i: Interpolation) -> c_int { - match i { - Interpolation::Linear => 0, - Interpolation::Hold => 1, - Interpolation::Bezier => 2, - } - } - - /// `NodeKeyframe::set_type` with the bezier default-handle side - /// effect. Standalone handles never have a `previous_`/`next_`, so - /// the C++ null-branches (in `(-1, 0)` / out `(1, 0)`) always apply - /// (`// CPP-PARITY: src/node/src/keyframe.cpp:120`). - fn set_type_adjusting(k: &mut KfBox, new: Interpolation) { - if k.data.interpolation == new { - return; - } - k.data.interpolation = new; - if new == Interpolation::Bezier { - if k.data.bezier_in == (0.0, 0.0) { - k.data.bezier_in = (-1.0, 0.0); - } - if k.data.bezier_out == (0.0, 0.0) { - k.data.bezier_out = (1.0, 0.0); - } - } - } - - /// Create a command whose redo/undo write through to the box behind - /// `h` (`NodeParamSetKeyframe*Command` semantics: redo applies the - /// new value, undo restores the old; `// CPP-PARITY: - /// src/node/src/nodeundo.cpp:417`). - fn kf_command( - h: &CHandle, - apply: impl Fn(&mut KfBox) + Send + 'static, - revert: impl Fn(&mut KfBox) + Send + 'static, - ) -> Result { - let keep = kf_keep(h); - let kh1 = keep.0.clone(); // bitwise copies; `keep` owns the reference - let kh2 = kh1.clone(); - let cmd = undo_command( - move || { - // `keep` (the box reference) lives as long as the command. - let _k = &keep; - if let Some(mut g) = unsafe { kf_lock(&kh1) } { - apply(&mut g); - } - }, - move || { - if let Some(mut g) = unsafe { kf_lock(&kh2) } { - revert(&mut g); - } - }, - )?; - Ok(cmd) - } - - /// `oaknode_keyframe_create`: standalone keyframe handle with count 1; - /// empty handle on invalid arguments (bad type, string value, - /// non-UTF-8 input id, unresolvable parent). - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_create( - time_num: i64, - time_den: i64, - value: *const OakNodeValue, - type_: c_int, - track: c_int, - element: c_int, - input_id: *const c_char, - parent_or_null: CHandle, - ) -> CHandle { - crate::handle::guard_handle(|| { - let interp = interp_from_c(type_)?; - let input = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?.to_string(); - let val = if value.is_null() { - NodeValue::None - } else { - unsafe { *value }.to_node_value(ValueType::None)? - }; - let (project, node) = if parent_or_null.ctx.is_null() { - (None, None) - } else { - let p = unsafe { node_ref(&parent_or_null)? }; - (Some(p.project.clone()), Some(p.id)) - }; - let data = Keyframe { - time: Rational::new(time_num, time_den), - value: val, - interpolation: interp, - bezier_in: (0.0, 0.0), - bezier_out: (0.0, 0.0), - }; - Ok(crate::handle::make_owned(Mutex::new(KfBox { - project, - node, - input, - element, - track, - data, - }))) - }) - } - - /// `oaknode_keyframe_free`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_free(keyframe: *mut CHandle) { - guard_void(|| { - if keyframe.is_null() || unsafe { (*keyframe).ctx.is_null() } { - return; - } - let h = unsafe { (*keyframe).clone() }; - if let Some(f) = h.release { - unsafe { f(h.ctx) }; - } - unsafe { (*keyframe).ctx = std::ptr::null_mut() }; - }) - } - - /// `oaknode_keyframe_get_time`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_get_time( - keyframe: CHandle, - out_num: *mut i64, - out_den: *mut i64, - ) -> c_int { - guard(|| { - if out_num.is_null() || out_den.is_null() { - return Err(Error::Invalid); - } - let k = unsafe { kf_lock(&keyframe) }.ok_or(Error::Invalid)?; - unsafe { - *out_num = k.data.time.numerator(); - *out_den = k.data.time.denominator(); - } - Ok(()) - }) - } - - /// `oaknode_keyframe_set_time` (live). - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_set_time( - keyframe: CHandle, - time_num: i64, - time_den: i64, - ) -> c_int { - guard(|| { - let mut k = unsafe { kf_lock(&keyframe) }.ok_or(Error::Invalid)?; - k.data.time = Rational::new(time_num, time_den); - Ok(()) - }) - } - - /// `oaknode_keyframe_set_time_undoable` - /// (`olive::NodeParamSetKeyframeTimeCommand`). - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_set_time_undoable( - keyframe: CHandle, - time_num: i64, - time_den: i64, - out_command: *mut CHandle, - ) -> c_int { - guard(|| { - if out_command.is_null() { - return Err(Error::Invalid); - } - let new = Rational::new(time_num, time_den); - let old = unsafe { kf_lock(&keyframe) } - .ok_or(Error::Invalid)? - .data - .time; - let cmd = kf_command( - &keyframe, - move |k| k.data.time = new, - move |k| k.data.time = old, - )?; - unsafe { *out_command = cmd }; - Ok(()) - }) - } - - /// `oaknode_keyframe_get_value`: map the stored value into the POD; - /// types without a POD representation fail with `OAKNODE_E_FAILED`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_get_value( - keyframe: CHandle, - out: *mut OakNodeValue, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let k = unsafe { kf_lock(&keyframe) }.ok_or(Error::Invalid)?; - let declared = k.data.value.value_type(); - let pod = OakNodeValue::from_node_value(declared, &k.data.value).map_err(|_| { - Error::Failed("keyframe value has no POD representation".to_string()) - })?; - unsafe { *out = pod }; - Ok(()) - }) - } - - /// `oaknode_keyframe_set_value` (live); `OAKNODE_VALUE_STRING` is - /// rejected (use `oaknode_keyframe_set_value_string`). - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_set_value( - keyframe: CHandle, - v: *const OakNodeValue, - ) -> c_int { - guard(|| { - if v.is_null() { - return Err(Error::Invalid); - } - let mut k = unsafe { kf_lock(&keyframe) }.ok_or(Error::Invalid)?; - let declared = k.data.value.value_type(); - let value = unsafe { *v }.to_node_value(declared)?; - k.data.value = value; - Ok(()) - }) - } - - /// `oaknode_keyframe_set_value_undoable` - /// (`olive::NodeParamSetKeyframeValueCommand`). - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_set_value_undoable( - keyframe: CHandle, - v: *const OakNodeValue, - out_command: *mut CHandle, - ) -> c_int { - guard(|| { - if v.is_null() || out_command.is_null() { - return Err(Error::Invalid); - } - let mut k = unsafe { kf_lock(&keyframe) }.ok_or(Error::Invalid)?; - let declared = k.data.value.value_type(); - let new = unsafe { *v }.to_node_value(declared)?; - let old = k.data.value.clone(); - drop(k); - let cmd = kf_command( - &keyframe, - move |k| k.data.value = new.clone(), - move |k| k.data.value = old.clone(), - )?; - unsafe { *out_command = cmd }; - Ok(()) - }) - } - - /// `oaknode_keyframe_get_value_string` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_get_value_string( - keyframe: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let k = match unsafe { kf_lock(&keyframe) } { - Some(k) => k, - None => return crate::error::OAKNODE_E_INVALID, - }; - let declared = k.data.value.value_type(); - let s = crate::serializer::value_to_string(declared, &k.data.value, false); - copy_string_out(&s, buf, buf_size) - } - - /// `oaknode_keyframe_set_value_string` (live). - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_set_value_string( - keyframe: CHandle, - value: *const c_char, - ) -> c_int { - guard(|| { - let s = unsafe { cstr(value) }.ok_or(Error::Invalid)?; - let mut k = unsafe { kf_lock(&keyframe) }.ok_or(Error::Invalid)?; - k.data.value = NodeValue::Text(s.to_string()); - Ok(()) - }) - } - - /// `oaknode_keyframe_set_value_string_undoable`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_set_value_string_undoable( - keyframe: CHandle, - value: *const c_char, - out_command: *mut CHandle, - ) -> c_int { - guard(|| { - if out_command.is_null() { - return Err(Error::Invalid); - } - let s = unsafe { cstr(value) }.ok_or(Error::Invalid)?.to_string(); - let old = unsafe { kf_lock(&keyframe) } - .ok_or(Error::Invalid)? - .data - .value - .clone(); - let cmd = kf_command( - &keyframe, - move |k| k.data.value = NodeValue::Text(s.clone()), - move |k| k.data.value = old.clone(), - )?; - unsafe { *out_command = cmd }; - Ok(()) - }) - } - - /// `oaknode_keyframe_get_type`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_get_type( - keyframe: CHandle, - out_type: *mut c_int, - ) -> c_int { - guard(|| { - if out_type.is_null() { - return Err(Error::Invalid); - } - let k = unsafe { kf_lock(&keyframe) }.ok_or(Error::Invalid)?; - unsafe { *out_type = interp_to_c(k.data.interpolation) }; - Ok(()) - }) - } - - /// `oaknode_keyframe_set_type` (live, with bezier-handle defaults). - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_set_type(keyframe: CHandle, type_: c_int) -> c_int { - guard(|| { - let interp = interp_from_c(type_)?; - let mut k = unsafe { kf_lock(&keyframe) }.ok_or(Error::Invalid)?; - set_type_adjusting(&mut k, interp); - Ok(()) - }) - } - - /// `oaknode_keyframe_set_type_undoable` (same semantics as live). - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_set_type_undoable( - keyframe: CHandle, - type_: c_int, - out_command: *mut CHandle, - ) -> c_int { - guard(|| { - if out_command.is_null() { - return Err(Error::Invalid); - } - let interp = interp_from_c(type_)?; - let old = unsafe { kf_lock(&keyframe) } - .ok_or(Error::Invalid)? - .data - .interpolation; - let cmd = kf_command( - &keyframe, - move |k| set_type_adjusting(k, interp), - move |k| k.data.interpolation = old, - )?; - unsafe { *out_command = cmd }; - Ok(()) - }) - } - - /// `oaknode_keyframe_get_bezier_control`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_get_bezier_control( - keyframe: CHandle, - handle: c_int, - out_x: *mut f64, - out_y: *mut f64, - ) -> c_int { - guard(|| { - if out_x.is_null() || out_y.is_null() { - return Err(Error::Invalid); - } - let k = unsafe { kf_lock(&keyframe) }.ok_or(Error::Invalid)?; - let (x, y) = match handle { - 0 => k.data.bezier_in, - 1 => k.data.bezier_out, - _ => return Err(Error::Invalid), - }; - unsafe { - *out_x = x; - *out_y = y; - } - Ok(()) - }) - } - - /// `oaknode_keyframe_set_bezier_control` (live). - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_set_bezier_control( - keyframe: CHandle, - handle: c_int, - x: f64, - y: f64, - ) -> c_int { - guard(|| { - let mut k = unsafe { kf_lock(&keyframe) }.ok_or(Error::Invalid)?; - match handle { - 0 => k.data.bezier_in = (x, y), - 1 => k.data.bezier_out = (x, y), - _ => return Err(Error::Invalid), - } - Ok(()) - }) - } - - /// `oaknode_keyframe_set_bezier_control_undoable`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_set_bezier_control_undoable( - keyframe: CHandle, - handle: c_int, - x: f64, - y: f64, - out_command: *mut CHandle, - ) -> c_int { - guard(|| { - if out_command.is_null() || (handle != 0 && handle != 1) { - return Err(Error::Invalid); - } - let old = { - let k = unsafe { kf_lock(&keyframe) }.ok_or(Error::Invalid)?; - match handle { - 0 => k.data.bezier_in, - _ => k.data.bezier_out, - } - }; - let cmd = kf_command( - &keyframe, - move |k| { - if handle == 0 { - k.data.bezier_in = (x, y); - } else { - k.data.bezier_out = (x, y); - } - }, - move |k| { - if handle == 0 { - k.data.bezier_in = old; - } else { - k.data.bezier_out = old; - } - }, - )?; - unsafe { *out_command = cmd }; - Ok(()) - }) - } - - /// `oaknode_keyframe_get_track`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_get_track( - keyframe: CHandle, - out_track: *mut c_int, - ) -> c_int { - guard(|| { - if out_track.is_null() { - return Err(Error::Invalid); - } - let k = unsafe { kf_lock(&keyframe) }.ok_or(Error::Invalid)?; - unsafe { *out_track = k.track }; - Ok(()) - }) - } - - /// `oaknode_keyframe_get_element`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_get_element( - keyframe: CHandle, - out_element: *mut c_int, - ) -> c_int { - guard(|| { - if out_element.is_null() { - return Err(Error::Invalid); - } - let k = unsafe { kf_lock(&keyframe) }.ok_or(Error::Invalid)?; - unsafe { *out_element = k.element }; - Ok(()) - }) - } - - /// `oaknode_keyframe_get_input` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_get_input( - keyframe: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let k = match unsafe { kf_lock(&keyframe) } { - Some(k) => k, - None => return crate::error::OAKNODE_E_INVALID, - }; - copy_string_out(&k.input, buf, buf_size) - } - - /// `oaknode_keyframe_get_parent`: borrowed node handle, empty when - /// orphaned; `OAKNODE_OK` either way. - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_get_parent( - keyframe: CHandle, - out_node: *mut CHandle, - ) -> c_int { - guard(|| { - if out_node.is_null() { - return Err(Error::Invalid); - } - let k = unsafe { kf_lock(&keyframe) }.ok_or(Error::Invalid)?; - match (&k.project, &k.node) { - (Some(p), Some(id)) => unsafe { *out_node = make_node_borrowed(p.clone(), *id) }, - _ => unsafe { *out_node = CHandle::null() }, - } - Ok(()) - }) - } - - /// `oaknode_keyframe_get_valid_bezier_control`: for standalone - /// handles there is no neighbouring keyframe, so the raw control - /// point is already valid (`NodeKeyframe::valid_bezier_control_*`). - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_get_valid_bezier_control( - keyframe: CHandle, - handle: c_int, - out_x: *mut f64, - out_y: *mut f64, - ) -> c_int { - unsafe { oaknode_keyframe_get_bezier_control(keyframe, handle, out_x, out_y) } - } - - /// `oaknode_keyframe_opposing_bezier_type`: `IN_HANDLE (0) <-> - /// OUT_HANDLE (1)`; `OAKNODE_E_INVALID` for anything else. - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_opposing_bezier_type(type_: c_int) -> c_int { - match type_ { - 0 => 1, - 1 => 0, - _ => crate::error::OAKNODE_E_INVALID, - } - } - - /// `oaknode_keyframe_compute_paste_value`: take the target node's - /// value at the keyframe's time, replace the keyframe's own track - /// with the keyframe's value, and combine the per-track components - /// into a single normal value (the facade paste path). - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_compute_paste_value( - target_node: CHandle, - keyframe: CHandle, - out: *mut crate::value::OakNodeValue, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let target = unsafe { node_ref(&target_node)? }; - let k = unsafe { kf_lock(&keyframe) }.ok_or(Error::Invalid)?; - let input = k.input.clone(); - let element = k.element; - let track = k.track; - let time = k.data.time; - let kf_value = k.data.value.clone(); - drop(k); - let declared = with_graph_read(&target.project, |g| { - g.get(target.id) - .and_then(|e| e.core.input_data_type(&input)) - }) - .ok_or(Error::NotFound)?; - if declared.is_string() { - return Err(Error::Failed( - "input type has no POD representation".to_string(), - )); - } - let base = with_graph_read(&target.project, |g| { - g.get(target.id) - .map(|e| e.core.value_at_time(&input, element, time)) - .unwrap_or(NodeValue::None) - }); - let mut tracks = base.split_into_tracks(declared); - if let Some(slot) = tracks.get_mut(track as usize) { - *slot = kf_value; - } - let combined = NodeValue::combine_tracks(&tracks, declared); - let pod = OakNodeValue::from_node_value(declared, &combined) - .map_err(|_| Error::Failed("input type has no POD representation".to_string()))?; - unsafe { *out = pod }; - Ok(()) - }) - } - - /// `oaknode_keyframe_has_sibling_at_time`: 1 when the parent's live - /// track holds a keyframe at the exact rational time; orphaned - /// keyframes have no siblings (`*out_value` = 0, `OAKNODE_OK`). - #[no_mangle] - pub unsafe extern "C" fn oaknode_keyframe_has_sibling_at_time( - keyframe: CHandle, - time_num: i64, - time_den: i64, - out_value: *mut c_int, - ) -> c_int { - guard(|| { - if out_value.is_null() { - return Err(Error::Invalid); - } - let time = Rational::new(time_num, time_den); - let k = unsafe { kf_lock(&keyframe) }.ok_or(Error::Invalid)?; - let found = match (&k.project, &k.node) { - (Some(p), Some(id)) => { - let input = k.input.clone(); - let element = k.element; - with_graph_read(p, |g| { - g.get(*id) - .and_then(|e| e.core.keyframe_track(&input, element)) - .map(|t| t.keys().iter().any(|key| key.time == time)) - .unwrap_or(false) - }) - } - _ => false, - }; - unsafe { *out_value = found as c_int }; - Ok(()) - }) - } -} - -// ===================================================================== -// include/node/dragger.h -// ===================================================================== - -/// `include/node/dragger.h` exports (complete inventory): -/// oaknode_dragger_create / start / drag / end / is_started / free. -/// -/// The dragger wraps an input drag (`olive::NodeInputDragger`, -/// `// CPP-PARITY: src/node/c_api/dragger.cpp` / `src/node/src/inputdragger.cpp`) -/// over the whole-value keyframe model: `start` caches the per-component -/// value at the drag time; `drag` clamps against the input's `min`/`max` -/// properties and writes through (standard value, or the keyframe at the -/// drag time); `end` builds a multi undo command with the C++ -/// `NodeParamSet*Command` equivalents. -/// -/// The C++ `is_keyframing()` per-input mode has no Rust counterpart yet, -/// so the keyframing branch is selected by whether the (input, element) -/// track already holds keyframes (documented divergence). -pub mod dragger { - use super::*; - use crate::keyframe::{Interpolation, Keyframe}; - use crate::value::oak; - use oakcore_rs::Rational; - use std::sync::Mutex; - - /// Dragger handle payload (`// CPP-PARITY: dragger.cpp:46` - /// `DraggerImpl` + `NodeInputDragger` state). - pub struct DraggerBox { - /// The project the dragged node lives in. - pub project: ProjectArc, - /// The dragged node. - pub node: NodeId, - /// The dragged input. - pub input: String, - /// Array element. - pub element: i32, - /// Whether a drag is in progress. - pub started: bool, - /// Component track of the drag (established by `start`). - pub track: i32, - /// Drag time. - pub time: Rational, - /// Whether the drag edits keyframes (the track had keys at start). - pub keyframing: bool, - /// Whether `start` created a key (undo of `end` removes it). - pub created_key: bool, - /// Per-component value to restore on undo (pre-drag standard - /// value or pre-drag key value). - pub undo_value: NodeValue, - } - - /// Lock the boxed dragger. - unsafe fn dg_lock(h: &CHandle) -> Option> { - let m = unsafe { crate::handle::get::>(h)? }; - Some(m.lock().unwrap_or_else(|e| e.into_inner())) - } - - /// The declared value type of the dragged input. - fn declared_type(project: &ProjectArc, node: NodeId, input: &str) -> Option { - with_graph_read(project, |g| { - g.get(node).and_then(|e| e.core.input_data_type(input)) - }) - } - - /// The whole value with `track`'s component replaced by `component` - /// (C++ `set_split_standard_value_on_track` / - /// `get_split_value_at_time_on_track` combine half). - fn with_component( - declared: ValueType, - whole: NodeValue, - track: usize, - component: &NodeValue, - ) -> NodeValue { - let mut tracks = whole.split_into_tracks(declared); - if let Some(slot) = tracks.get_mut(track) { - *slot = component.clone(); - } - NodeValue::combine_tracks(&tracks, declared) - } - - /// `// CPP-PARITY: src/node/c_api/valueconvert.h:262` - /// `component_from_value(value, declared, 0)` — the POD -> the scalar - /// component variant the dragger drags. - fn component_from_pod(v: &OakNodeValue, declared: ValueType) -> Result { - match declared { - ValueType::Int | ValueType::Combo => { - if v.kind != oak::INT && v.kind != oak::COMBO { - return Err(Error::Invalid); - } - Ok(NodeValue::Int(v.num)) - } - ValueType::Float => { - if v.kind != oak::FLOAT { - return Err(Error::Invalid); - } - Ok(NodeValue::Float(v.f[0])) - } - ValueType::Boolean => { - if v.kind != oak::BOOL { - return Err(Error::Invalid); - } - Ok(NodeValue::Boolean(v.num != 0)) - } - ValueType::Rational => { - if v.kind != oak::RATIONAL { - return Err(Error::Invalid); - } - Ok(NodeValue::Rational(Rational::new(v.num, v.den))) - } - ValueType::Color | ValueType::Vec2 | ValueType::Vec3 | ValueType::Vec4 => { - if v.kind != declared.to_oak() { - return Err(Error::Invalid); - } - Ok(NodeValue::Float(v.f[0])) - } - _ => Err(Error::Invalid), - } - } - - /// Clamp a component against the input's `min`/`max` properties - /// (`// CPP-PARITY: inputdragger.cpp:106`). - fn clamp_property(core: &NodeCore, input: &str, value: &NodeValue) -> NodeValue { - let mut value = value.clone(); - if let Some(prop) = core.get_input(input).and_then(|i| { - i.properties - .iter() - .find(|(k, _)| k == "min") - .map(|(_, v)| v) - }) { - let min = prop.to_double(); - if value.to_double() < min { - value = NodeValue::Float(min); - } - } - if let Some(prop) = core.get_input(input).and_then(|i| { - i.properties - .iter() - .find(|(k, _)| k == "max") - .map(|(_, v)| v) - }) { - let max = prop.to_double(); - if value.to_double() > max { - value = NodeValue::Float(max); - } - } - value - } - - /// Build a vtable undo command from redo/undo closures. - fn make_child( - _keep: ProjectArc, - redo: impl FnMut() + Send + 'static, - undo: impl FnMut() + Send + 'static, - ) -> Option { - crate::bridge::undo::command_from_closures(redo, undo) - } - - /// `oaknode_dragger_create`: empty handle when the node/input is - /// invalid; `track` is ignored (start establishes the drag track, - /// `// CPP-PARITY: dragger.cpp:67`). - #[no_mangle] - pub unsafe extern "C" fn oaknode_dragger_create( - node: CHandle, - input_id: *const c_char, - element: c_int, - _track: c_int, - ) -> CHandle { - crate::handle::guard_handle(|| { - let n = unsafe { node_ref(&node)? }; - let input = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?.to_string(); - let has = with_graph_read(&n.project, |g| { - g.get(n.id) - .map(|e| e.core.has_input(&input)) - .unwrap_or(false) - }); - if !has { - return Err(Error::Invalid); - } - Ok(crate::handle::make_owned(Mutex::new(DraggerBox { - project: n.project.clone(), - node: n.id, - input, - element, - started: false, - track: 0, - time: Rational::NULL, - keyframing: false, - created_key: false, - undo_value: NodeValue::None, - }))) - }) - } - - /// `oaknode_dragger_start`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_dragger_start( - dragger: CHandle, - time_num: i64, - time_den: i64, - track: c_int, - insert_on_all_tracks: c_int, - ) -> c_int { - guard(|| { - let _ = insert_on_all_tracks; // whole-value keys cover all tracks - if track < 0 { - return Err(Error::Invalid); - } - let mut d = unsafe { dg_lock(&dragger) }.ok_or(Error::Invalid)?; - if d.started { - return Err(Error::State); - } - let time = Rational::new(time_num, time_den); - // Snapshot the drag target, then release the box guard so the - // project lock below does not alias it. - let project = d.project.clone(); - let node = d.node; - let input = d.input.clone(); - let element = d.element; - drop(d); - let declared = declared_type(&project, node, &input).ok_or(Error::NotFound)?; - let (undo_value, created_key, keyframing) = { - let mut guard = lock(&project); - let entry = guard.graph.get_mut(node).ok_or(Error::NotFound)?; - let core = &mut entry.core; - // The track's current value at the drag time (whole - // value); the dragged component and the undo snapshot - // derive from it. - let whole = core.value_at_time(&input, element, time); - let keyframing = core - .keyframe_track(&input, element) - .map(|t| !t.keys().is_empty()) - .unwrap_or(false); - let start_component = whole - .split_into_tracks(declared) - .get(track as usize) - .cloned() - .unwrap_or(NodeValue::None); - if keyframing { - // Keyframe branch: ensure a key exists at the drag - // time (whole-value model: one key per time). - let has_key = core - .keyframe_track(&input, element) - .map(|t| t.keys().iter().any(|k| k.time == time)) - .unwrap_or(false); - if !has_key { - let created = with_component( - declared, - whole.clone(), - track as usize, - &start_component, - ); - core.keyframe_track_mut(&input, element).set_key(Keyframe { - time, - value: created.clone(), - interpolation: Interpolation::Linear, - bezier_in: (0.0, 0.0), - bezier_out: (0.0, 0.0), - }); - (created, true, true) - } else { - let existing = core - .keyframe_track(&input, element) - .and_then(|t| t.keys().iter().find(|k| k.time == time)) - .map(|k| k.value.clone()) - .unwrap_or(whole); - (existing, false, true) - } - } else { - // Standard-value branch: snapshot the pre-drag value. - (whole, false, false) - } - }; - let mut d = unsafe { dg_lock(&dragger) }.ok_or(Error::Invalid)?; - d.started = true; - d.keyframing = keyframing; - d.track = track; - d.time = time; - d.created_key = created_key; - d.undo_value = undo_value; - Ok(()) - }) - } - - /// `oaknode_dragger_drag`: clamp the component against `min`/`max` - /// and write it through (live). - #[no_mangle] - pub unsafe extern "C" fn oaknode_dragger_drag( - dragger: CHandle, - value: *const crate::value::OakNodeValue, - ) -> c_int { - guard(|| { - if value.is_null() { - return Err(Error::Invalid); - } - let pod = unsafe { *value }; - let mut d = unsafe { dg_lock(&dragger) }.ok_or(Error::Invalid)?; - if !d.started { - return Err(Error::State); - } - let declared = declared_type(&d.project, d.node, &d.input).ok_or(Error::NotFound)?; - let component = component_from_pod(&pod, declared)?; - let mut guard = lock(&d.project); - let entry = guard.graph.get_mut(d.node).ok_or(Error::NotFound)?; - let component = clamp_property(&entry.core, &d.input, &component); - if d.keyframing { - let time = d.time; - let whole = entry - .core - .keyframe_track(&d.input, d.element) - .and_then(|t| t.keys().iter().find(|k| k.time == time)) - .map(|k| k.value.clone()) - .unwrap_or(entry.core.value_at_time(&d.input, d.element, time)); - let new_whole = with_component(declared, whole, d.track as usize, &component); - entry - .core - .keyframe_track_mut(&d.input, d.element) - .set_key_value(time, new_whole); - } else { - let whole = entry.core.standard_value(&d.input, d.element); - let new_whole = with_component(declared, whole, d.track as usize, &component); - entry - .core - .set_standard_value(&d.input, d.element, new_whole); - } - Ok(()) - }) - } - - /// `oaknode_dragger_end`: build a multi undo command covering the - /// drag (insert-keyframe + set-value, or set-standard-value). - #[no_mangle] - pub unsafe extern "C" fn oaknode_dragger_end( - dragger: CHandle, - out_command: *mut CHandle, - ) -> c_int { - guard(|| { - if out_command.is_null() { - return Err(Error::Invalid); - } - let mut d = unsafe { dg_lock(&dragger) }.ok_or(Error::Invalid)?; - if !d.started { - return Err(Error::State); - } - let project = d.project.clone(); - let node = d.node; - let input = d.input.clone(); - let element = d.element; - let track = d.track; - let time = d.time; - let keyframing = d.keyframing; - let created_key = d.created_key; - let undo_value = d.undo_value.clone(); - // The post-drag whole value (what redo re-applies): the key - // value at the drag time, or the current standard value. - let redo_value = { - let mut guard = lock(&project); - if keyframing { - guard - .graph - .get(node) - .and_then(|e| { - e.core.keyframe_track(&input, element).and_then(|t| { - t.keys() - .iter() - .find(|k| k.time == time) - .map(|k| k.value.clone()) - }) - }) - .unwrap_or(NodeValue::None) - } else { - guard - .graph - .get(node) - .map(|e| e.core.standard_value(&input, element)) - .unwrap_or(NodeValue::None) - } - }; - let multi = crate::bridge::undo::command_init_multi().ok_or(Error::NoMem)?; - if keyframing { - if created_key { - // Insert-keyframe equivalent: redo re-adds the key, - // undo removes it (the value command below restores - // the value; `// CPP-PARITY: nodeundo.cpp:432`). - let redo_key = redo_value.clone(); - let project_redo = project.clone(); - let project_undo = project.clone(); - let input_redo = input.clone(); - let input_undo = input.clone(); - if let Some(child) = make_child( - project.clone(), - move || { - let mut guard = lock(&project_redo); - if let Some(e) = guard.graph.get_mut(node) { - e.core - .keyframe_track_mut(&input_redo, element) - .set_key(Keyframe { - time, - value: redo_key.clone(), - interpolation: Interpolation::Linear, - bezier_in: (0.0, 0.0), - bezier_out: (0.0, 0.0), - }); - } - }, - move || { - let mut guard = lock(&project_undo); - if let Some(e) = guard.graph.get_mut(node) { - e.core - .keyframe_track_mut(&input_undo, element) - .remove_key(time); - } - }, - ) { - let _ = crate::bridge::undo::command_multi_add_child(multi.clone(), child); - } - } - // Set-keyframe-value equivalent (always added; the - // C++ sends the value-changed signal through it). - let project_redo = project.clone(); - let input_redo = input.clone(); - let input_undo = input.clone(); - if let Some(child) = make_child( - project.clone(), - move || { - let mut guard = lock(&project_redo); - if let Some(e) = guard.graph.get_mut(node) { - e.core - .keyframe_track_mut(&input_redo, element) - .set_key_value(time, redo_value.clone()); - } - }, - move || { - let mut guard = lock(&project); - if let Some(e) = guard.graph.get_mut(node) { - e.core - .keyframe_track_mut(&input_undo, element) - .set_key_value(time, undo_value.clone()); - } - }, - ) { - let _ = crate::bridge::undo::command_multi_add_child(multi.clone(), child); - } - } else { - // Set-standard-value equivalent - // (`// CPP-PARITY: nodeundo.cpp:500` - // `NodeParamSetStandardValueCommand`). - let project_redo = project.clone(); - let input_redo = input.clone(); - let input_undo = input.clone(); - if let Some(child) = make_child( - project.clone(), - move || { - let mut guard = lock(&project_redo); - if let Some(e) = guard.graph.get_mut(node) { - e.core - .set_standard_value(&input_redo, element, redo_value.clone()); - } - }, - move || { - let mut guard = lock(&project); - if let Some(e) = guard.graph.get_mut(node) { - e.core - .set_standard_value(&input_undo, element, undo_value.clone()); - } - }, - ) { - let _ = crate::bridge::undo::command_multi_add_child(multi.clone(), child); - } - } - // The drag is complete; the dragger resets (C++ - // `NodeInputDragger::end` resets the reference). - d.started = false; - d.created_key = false; - unsafe { *out_command = multi }; - Ok(()) - }) - } - - /// `oaknode_dragger_is_started`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_dragger_is_started( - dragger: CHandle, - out_started: *mut c_int, - ) -> c_int { - guard(|| { - if out_started.is_null() { - return Err(Error::Invalid); - } - let d = unsafe { dg_lock(&dragger) }.ok_or(Error::Invalid)?; - unsafe { *out_started = d.started as c_int }; - Ok(()) - }) - } - - /// `oaknode_dragger_free`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_dragger_free(dragger: *mut CHandle) { - guard_void(|| { - if dragger.is_null() || unsafe { (*dragger).ctx.is_null() } { - return; - } - let h = unsafe { (*dragger).clone() }; - if let Some(f) = h.release { - unsafe { f(h.ctx) }; - } - unsafe { (*dragger).ctx = std::ptr::null_mut() }; - }) - } -} - -// ===================================================================== -// include/node/multicam.h -// ===================================================================== - -/// `include/node/multicam.h` exports (complete inventory): the four -/// input-id getters, source count, grid math (rows/columns, -/// index<->row/col), and the current source. -/// -/// The grid math and string getters are pure; the node-backed helpers -/// follow the C++ oracle (`// CPP-PARITY: src/node/c_api/multicam.cpp`, -/// `src/node/src/input/multicam/multicamnode.cpp`). The `MultiCamNode` -/// behavior itself stays a Phase-3 item; the "is this a multicam node" -/// check mirrors the C++ `dynamic_cast` via the type id, -/// and the source count uses the `sources_in` array size (the -/// C++ sequence-track-list path is behavior-bound and deferred). -pub mod multicam { - use super::*; - - const MULTICAM_TYPE_ID: &str = "org.olivevideoeditor.Olive.multicam"; - const CURRENT_INPUT: &str = "current_in"; - const SOURCES_INPUT: &str = "sources_in"; - - /// `oaknode_multicam_input_current` — static string. - #[no_mangle] - pub unsafe extern "C" fn oaknode_multicam_input_current() -> *const c_char { - b"current_in\0".as_ptr() as *const c_char - } - - /// `oaknode_multicam_input_sources` — static string. - #[no_mangle] - pub unsafe extern "C" fn oaknode_multicam_input_sources() -> *const c_char { - b"sources_in\0".as_ptr() as *const c_char - } - - /// `oaknode_multicam_input_sequence` — static string. - #[no_mangle] - pub unsafe extern "C" fn oaknode_multicam_input_sequence() -> *const c_char { - b"sequence_in\0".as_ptr() as *const c_char - } - - /// `oaknode_multicam_input_sequence_type` — static string. - #[no_mangle] - pub unsafe extern "C" fn oaknode_multicam_input_sequence_type() -> *const c_char { - b"sequence_type_in\0".as_ptr() as *const c_char - } - - /// The multicam behavior of a node (C++ `dynamic_cast`). - fn multicam_of(entry: &crate::graph::NodeEntry) -> bool { - entry.behavior.type_id() == MULTICAM_TYPE_ID - } - - /// `oaknode_multicam_get_source_count`: the `sources_in` array size - /// (`// CPP-PARITY: multicamnode.cpp:163`). - #[no_mangle] - pub unsafe extern "C" fn oaknode_multicam_get_source_count( - node: CHandle, - out_count: *mut c_int, - ) -> c_int { - guard(|| { - if out_count.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let count = with_graph_read(&n.project, |g| { - g.get(n.id) - .filter(|e| multicam_of(e)) - .map(|e| e.core.input_array_size(SOURCES_INPUT) as c_int) - }) - .ok_or(Error::Invalid)?; - unsafe { *out_count = count }; - Ok(()) - }) - } - - /// `oaknode_multicam_get_rows_and_columns`: the square-ish grid that - /// covers `source_count` cells (`// CPP-PARITY: - /// multicamnode.cpp:190`; rows grows while it trails columns). - #[no_mangle] - pub unsafe extern "C" fn oaknode_multicam_get_rows_and_columns( - source_count: c_int, - rows: *mut c_int, - cols: *mut c_int, - ) -> c_int { - if source_count < 0 || rows.is_null() || cols.is_null() { - return crate::error::OAKNODE_E_INVALID; - } - let mut r: c_int = 1; - let mut c: c_int = 1; - while r * c < source_count { - if r < c { - r += 1; - } else { - c += 1; - } - } - unsafe { - *rows = r; - *cols = c; - } - crate::error::OAKNODE_OK - } - - /// `oaknode_multicam_index_to_row_cols`: column-major layout - /// (`// CPP-PARITY: multicamnode.cpp:93`; `total_rows` unused). - #[no_mangle] - pub unsafe extern "C" 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 index < 0 || rows < 1 || cols < 1 || out_row.is_null() || out_col.is_null() { - return crate::error::OAKNODE_E_INVALID; - } - unsafe { - *out_col = index % cols; - *out_row = index / cols; - } - crate::error::OAKNODE_OK - } - - /// `oaknode_multicam_rows_cols_to_index`: `row * cols + col` - /// (`// CPP-PARITY: multicamnode.h:76`). - #[no_mangle] - pub unsafe extern "C" 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 || row >= rows || col >= cols { - return crate::error::OAKNODE_E_INVALID; - } - row * cols + col - } - - /// `oaknode_multicam_get_current_source`: the `current_in` combo value - /// (`// CPP-PARITY: multicamnode.h:55`). - #[no_mangle] - pub unsafe extern "C" fn oaknode_multicam_get_current_source( - node: CHandle, - out_source: *mut c_int, - ) -> c_int { - guard(|| { - if out_source.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let source = with_graph_read(&n.project, |g| { - g.get(n.id) - .filter(|e| multicam_of(e)) - .map(|e| e.core.standard_value(CURRENT_INPUT, -1).to_double() as c_int) - }) - .ok_or(Error::Invalid)?; - unsafe { *out_source = source }; - Ok(()) - }) - } -} - -// ===================================================================== -// include/node/block.h, folder.h, footage.h, group.h, colormanager.h, -// factory.h, serializer.h, traverser.h -// ===================================================================== - -/// Families landing with their backing modules: `block`/`folder`/ -/// `footage`/`group`/`colormanager`/`factory`/`serializer`/`traverser`. -/// The factory family is required by the node tests and lands with the -/// registry work (Phase 1 continues in the crate plan; see -/// `tests/factory_smoke_test.rs`). -pub mod remaining_headers { - // Stubs are added per header when the corresponding internal module - // is implemented; keep header order and naming identical to the C - // side so bindgen/diff audits stay mechanical. -} - -/// `include/node/factory.h` — node-type registry exports. The registry -/// itself (`factory.rs`) is live; the C ABI family (initialize/destroy/ -/// id_count/id_at/name_from_id/create_from_id/node_at) is implemented -/// in Phase 1's node tests. -pub mod factory { - use super::*; - - /// `oaknode_factory_initialize`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_factory_initialize() -> c_int { - guard(|| { - let _ = crate::factory::Factory::global(); - Ok(()) - }) - } - - /// `oaknode_factory_destroy` — no-op (the registry is process-wide - /// and lazily built; nothing to release). - #[no_mangle] - pub unsafe extern "C" fn oaknode_factory_destroy() {} - - /// `oaknode_factory_id_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_factory_id_count(out_count: *mut c_int) -> c_int { - guard(|| { - if out_count.is_null() { - return Err(Error::Invalid); - } - unsafe { *out_count = crate::factory::Factory::global().entries().len() as c_int }; - Ok(()) - }) - } - - /// `oaknode_factory_id_at` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oaknode_factory_id_at( - index: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let entries = crate::factory::Factory::global().entries(); - if index < 0 || index as usize >= entries.len() { - return crate::error::OAKNODE_E_NOT_FOUND; - } - copy_string_out(entries[index as usize].type_id, buf, buf_size) - } - - /// `oaknode_factory_name_from_id` (two-stage; unknown id yields an - /// empty string). - #[no_mangle] - pub unsafe extern "C" fn oaknode_factory_name_from_id( - type_id: *const c_char, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let type_id = match unsafe { cstr(type_id) } { - Some(s) => s, - None => return crate::error::OAKNODE_E_INVALID, - }; - let name = crate::factory::Factory::global() - .find(type_id) - .map(|m| m.name) - .unwrap_or(""); - copy_string_out(name, buf, buf_size) - } - - /// `oaknode_factory_create_from_id` (owned orphan node). - #[no_mangle] - pub unsafe extern "C" fn oaknode_factory_create_from_id(type_id: *const c_char) -> CHandle { - let type_id = match unsafe { cstr(type_id) } { - Some(s) => s, - None => return CHandle::null(), - }; - let create = match crate::factory::Factory::global().find(type_id) { - Some(meta) => meta.create, - None => return CHandle::null(), - }; - let (core, behavior) = create(); - let scratch = Project::new(); - let id = { - let mut guard = lock(&scratch); - guard.graph.add_node(core, behavior) - }; - make_node_owned(scratch, id) - } - - /// `oaknode_factory_node_at` (borrowed prototype handle). - #[no_mangle] - pub unsafe extern "C" fn oaknode_factory_node_at( - index: c_int, - out_node: *mut CHandle, - ) -> c_int { - guard(|| { - if out_node.is_null() { - return Err(Error::Invalid); - } - let entries = crate::factory::Factory::global().entries(); - if index < 0 || index as usize >= entries.len() { - return Err(Error::NotFound); - } - let (core, behavior) = (entries[index as usize].create)(); - let scratch = Project::new(); - let id = { - let mut guard = lock(&scratch); - guard.graph.add_node(core, behavior) - }; - unsafe { *out_node = make_node_borrowed(scratch, id) }; - Ok(()) - }) - } -} - -// ===================================================================== -// Shared timeline-family helpers -// ===================================================================== - -/// Downcast a graph entry's behavior to [`crate::block::BlockCore`]. -fn block_core<'a>(entry: &'a crate::graph::NodeEntry) -> Option<&'a crate::block::BlockCore> { - entry.behavior.as_any().and_then(|a| { - if let Some(b) = a.downcast_ref::() { - Some(&b.core) - } else if let Some(b) = a.downcast_ref::() { - Some(&b.core) - } else if let Some(b) = a.downcast_ref::() { - Some(&b.core) - } else { - None - } - }) -} - -fn block_core_mut<'a>( - entry: &'a mut crate::graph::NodeEntry, -) -> Option<&'a mut crate::block::BlockCore> { - // The concrete block behaviors all carry `core` at the same offset - // behind their `as_any_mut`; downcast through a raw pointer to avoid - // the borrow checker's double-mutable-borrow on `&mut dyn Any`. - let any = entry.behavior.as_any_mut()?; - let raw: *mut dyn std::any::Any = any; - unsafe { - if let Some(b) = (&mut *raw).downcast_mut::() { - return Some(&mut b.core); - } - if let Some(b) = (&mut *raw).downcast_mut::() { - return Some(&mut b.core); - } - if let Some(b) = (&mut *raw).downcast_mut::() { - return Some(&mut b.core); - } - } - None -} - -/// Graph-backed block-range accessor for the track queries. -struct GraphBlockRange<'a> { - graph: &'a crate::graph::Graph, -} - -impl crate::track::BlockRange for GraphBlockRange<'_> { - fn in_(&self, block: NodeId) -> oakcore_rs::Rational { - self.graph - .get(block) - .and_then(block_core) - .map(|c| c.in_()) - .unwrap_or_else(|| oakcore_rs::Rational::new(0, 1)) - } - - fn out(&self, block: NodeId) -> oakcore_rs::Rational { - self.graph - .get(block) - .and_then(block_core) - .map(|c| c.out()) - .unwrap_or_else(|| oakcore_rs::Rational::new(0, 1)) - } -} - -/// Graph-backed track-range accessor for the list queries. -struct GraphTrackRange<'a> { - graph: &'a crate::graph::Graph, -} - -impl crate::track::TrackRange for GraphTrackRange<'_> { - fn length(&self, track: NodeId) -> oakcore_rs::Rational { - match self.graph.get(track) { - Some(entry) => entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .map(|t| t.length(&GraphBlockRange { graph: self.graph })) - .unwrap_or_else(|| oakcore_rs::Rational::new(0, 1)), - None => oakcore_rs::Rational::new(0, 1), - } - } -} - -/// Move a single node from one project's graph into another's, -/// preserving identity when the slot is free. Returns the new id. -fn move_node_between( - src_project: &ProjectArc, - src_id: NodeId, - dst_project: &ProjectArc, -) -> Result { - let entry = { - let mut src_guard = lock(src_project); - src_guard.graph.take_node(src_id).ok_or(Error::NotFound)? - }; - let mut dst_guard = lock(dst_project); - Ok(dst_guard.graph.add_entry(entry, src_id)) -} - -// ===================================================================== -// include/node/folder.h -// ===================================================================== - -/// `include/node/folder.h` exports: create / child_count / child_at / -/// add_child / as_node / command_create_folder_add_child / -/// remove_child / move_children / has_child_recursive / index_of_child / -/// parent_of. The add-child command needs the undo bridge (implemented -/// via [`crate::bridge::undo`]). -pub mod folder { - use super::*; - use crate::folder::FolderBehavior; - - /// Borrowed folder behavior of a node handle. - fn folder_ref(n: &NodeRef) -> Result { - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .map(|f| FolderBehavior { - name: f.name.clone(), - children: f.children.clone(), - }) - .ok_or(Error::State) - } - - /// `oaknode_folder_create`: create a folder node in `project`'s graph - /// (borrowed handle; not attached under any parent). - #[no_mangle] - pub unsafe extern "C" fn oaknode_folder_create(project: CHandle) -> CHandle { - let p = match unsafe { project_arc(&project) } { - Ok(p) => p, - Err(_) => return CHandle::null(), - }; - let (core, behavior) = crate::folder::create("Folder"); - let id = { - let mut guard = lock(&p); - guard.graph.add_node(core, behavior) - }; - make_node_borrowed(p, id) - } - - /// `oaknode_folder_child_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_folder_child_count(folder: CHandle) -> c_int { - match unsafe { node_ref(&folder) } { - Ok(n) => match folder_ref(&n) { - Ok(f) => f.children.len() as c_int, - Err(e) => e.code(), - }, - Err(_) => crate::error::OAKNODE_E_INVALID, - } - } - - /// `oaknode_folder_child_at` (borrowed node handle). - #[no_mangle] - pub unsafe extern "C" fn oaknode_folder_child_at(folder: CHandle, index: c_int) -> CHandle { - let n = match unsafe { node_ref(&folder) } { - Ok(n) => n, - Err(_) => return CHandle::null(), - }; - let f = match folder_ref(&n) { - Ok(f) => f, - Err(_) => return CHandle::null(), - }; - match f.children.get(index as usize) { - Some(child) => make_node_borrowed(n.project.clone(), *child), - None => CHandle::null(), - } - } - - /// `oaknode_folder_add_child` (live). - #[no_mangle] - pub unsafe extern "C" fn oaknode_folder_add_child(folder: CHandle, child: CHandle) -> c_int { - guard(|| { - let f = unsafe { node_ref(&folder)? }; - let c = unsafe { node_ref(&child)? }; - if !Arc::ptr_eq(&f.project, &c.project) { - return Err(Error::Invalid); - } - let mut guard = lock(&f.project); - // A node can only belong to one folder. - if let Some(existing) = guard.graph.get(c.id) { - if existing.core.bin_folder.is_some() && existing.core.bin_folder != Some(f.id) { - return Err(Error::State); - } - } - { - let entry = guard.graph.get_mut(f.id).ok_or(Error::NotFound)?; - let fb = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - fb.add_child(c.id); - } - if let Some(existing) = guard.graph.get_mut(c.id) { - existing.core.bin_folder = Some(f.id); - } - Ok(()) - }) - } - - /// `oaknode_folder_as_node`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_folder_as_node(folder: CHandle) -> CHandle { - match unsafe { node_ref(&folder) } { - Ok(n) => make_node_borrowed(n.project.clone(), n.id), - Err(_) => CHandle::null(), - } - } - - /// `oaknode_command_create_folder_add_child` — undo bridge. - #[no_mangle] - pub unsafe extern "C" fn oaknode_command_create_folder_add_child( - folder: CHandle, - child: CHandle, - ) -> CHandle { - match (unsafe { node_ref(&folder) }, unsafe { node_ref(&child) }) { - (Ok(f), Ok(c)) => { - let project = f.project.clone(); - let project_undo = project.clone(); - let fid = f.id; - let cid = c.id; - match crate::bridge::undo::command_from_closures( - move || { - let mut guard = lock(&project); - if let Some(entry) = guard.graph.get_mut(fid) { - if let Some(fb) = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - { - fb.add_child(cid); - } - } - if let Some(entry) = guard.graph.get_mut(cid) { - entry.core.bin_folder = Some(fid); - } - }, - move || { - let mut guard = lock(&project_undo); - if let Some(entry) = guard.graph.get_mut(fid) { - if let Some(fb) = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - { - fb.remove_child(cid); - } - } - if let Some(entry) = guard.graph.get_mut(cid) { - entry.core.bin_folder = None; - } - }, - ) { - Some(h) => h, - None => CHandle::null(), - } - } - _ => CHandle::null(), - } - } - - /// `oaknode_folder_remove_child` (live). - #[no_mangle] - pub unsafe extern "C" fn oaknode_folder_remove_child(folder: CHandle, child: CHandle) -> c_int { - guard(|| { - let f = unsafe { node_ref(&folder)? }; - let c = unsafe { node_ref(&child)? }; - if !Arc::ptr_eq(&f.project, &c.project) { - return Err(Error::Invalid); - } - let mut guard = lock(&f.project); - let entry = guard.graph.get_mut(f.id).ok_or(Error::NotFound)?; - let fb = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - if fb.remove_child(c.id) { - if let Some(entry) = guard.graph.get_mut(c.id) { - entry.core.bin_folder = None; - } - Ok(()) - } else { - Err(Error::NotFound) - } - }) - } - - /// `oaknode_folder_move_children`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_folder_move_children( - nodes: *const CHandle, - count: c_int, - dest_folder: CHandle, - ) -> c_int { - guard(|| { - if nodes.is_null() || count < 0 { - return Err(Error::Invalid); - } - let d = unsafe { node_ref(&dest_folder)? }; - let mut moved: Vec = Vec::new(); - for i in 0..count as usize { - let child = unsafe { (*nodes.add(i)).clone() }; - let c = unsafe { node_ref(&child)? }; - if !Arc::ptr_eq(&d.project, &c.project) { - continue; - } - // Remove from the current folder (if any). - { - let mut guard = lock(&d.project); - let cur = guard.graph.get(c.id).and_then(|e| e.core.bin_folder); - if let Some(cur_f) = cur { - if let Some(entry) = guard.graph.get_mut(cur_f) { - if let Some(fb) = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - { - fb.remove_child(c.id); - } - } - } - } - moved.push(c.id); - } - let mut guard = lock(&d.project); - { - let entry = guard.graph.get_mut(d.id).ok_or(Error::NotFound)?; - let fb = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - for id in &moved { - if !fb.children.contains(id) { - fb.add_child(*id); - } - } - } - for id in moved { - if let Some(entry) = guard.graph.get_mut(id) { - entry.core.bin_folder = Some(d.id); - } - } - Ok(()) - }) - } - - /// `oaknode_folder_has_child_recursive`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_folder_has_child_recursive( - folder: CHandle, - child: CHandle, - ) -> c_int { - match (unsafe { node_ref(&folder) }, unsafe { node_ref(&child) }) { - (Ok(f), Ok(c)) => { - let guard = lock(&f.project); - let found = guard - .graph - .get(f.id) - .and_then(|e| e.behavior.as_any()) - .and_then(|a| a.downcast_ref::()) - .map(|fb| fb.has_child_recursive(c.id, &guard.graph)) - .unwrap_or(false); - found as c_int - } - _ => crate::error::OAKNODE_E_INVALID, - } - } - - /// `oaknode_folder_index_of_child`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_folder_index_of_child( - folder: CHandle, - child: CHandle, - ) -> c_int { - match (unsafe { node_ref(&folder) }, unsafe { node_ref(&child) }) { - (Ok(f), Ok(c)) => { - let guard = lock(&f.project); - match guard - .graph - .get(f.id) - .and_then(|e| e.behavior.as_any()) - .and_then(|a| a.downcast_ref::()) - .and_then(|fb| fb.index_of_child(c.id)) - { - Some(i) => i as c_int, - None => crate::error::OAKNODE_E_NOT_FOUND, - } - } - _ => crate::error::OAKNODE_E_INVALID, - } - } - - /// `oaknode_folder_parent_of`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_folder_parent_of(node: CHandle) -> CHandle { - let n = match unsafe { node_ref(&node) } { - Ok(n) => n, - Err(_) => return CHandle::null(), - }; - let guard = lock(&n.project); - match guard.graph.get(n.id).and_then(|e| e.core.bin_folder) { - Some(folder) => make_node_borrowed(n.project.clone(), folder), - None => CHandle::null(), - } - } -} - -// ===================================================================== -// include/node/group.h -// ===================================================================== - -/// `include/node/group.h` exports (complete inventory): create / cast / -/// free / add_input_passthrough (+_undoable) / remove_input_passthrough / -/// passthrough_count / passthrough_id_at / passthrough_input_at / -/// get_output_passthrough / set_output_passthrough (+_undoable) / -/// resolve_input. -/// -/// Group handles are node handles whose node carries a -/// [`crate::nodes::group::NodeGroup`] behavior. The undoable variants -/// build the C++ `NodeGroupAddInputPassthrough` / -/// `NodeGroupSetOutputPassthrough` semantics through -/// [`crate::bridge::undo`] (`// CPP-PARITY: src/node/src/group/group.cpp:363`). -pub mod group { - use super::*; - use crate::nodes::group::{InnerInput, NodeGroup}; - - /// The group behavior of a node entry (downcast; C++ - /// `dynamic_cast`). - fn group_of(entry: &crate::graph::NodeEntry) -> Option<&NodeGroup> { - entry.behavior.as_any()?.downcast_ref::() - } - - /// `oaknode_group_create`: standalone group node in a scratch project - /// (owned handle; adopt into a real project with - /// `oaknode_project_add_node`). - #[no_mangle] - pub unsafe extern "C" fn oaknode_group_create() -> CHandle { - let (core, behavior) = crate::nodes::group::create(); - let scratch = Project::new(); - let id = { - let mut guard = lock(&scratch); - guard.graph.add_node(core, behavior) - }; - make_node_owned(scratch, id) - } - - /// `oaknode_group_cast`: borrowed group view of a node; empty handle - /// when the node is not a NodeGroup. - #[no_mangle] - pub unsafe extern "C" fn oaknode_group_cast(node: CHandle) -> CHandle { - let n = match unsafe { node_ref(&node) } { - Ok(n) => n, - Err(_) => return CHandle::null(), - }; - let guard = lock(&n.project); - match guard.graph.get(n.id) { - Some(entry) if group_of(entry).is_some() => make_node_borrowed(n.project.clone(), n.id), - _ => CHandle::null(), - } - } - - /// `oaknode_group_free`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_group_free(group: *mut CHandle) { - guard_void(|| { - if group.is_null() || unsafe { (*group).ctx.is_null() } { - return; - } - let h = unsafe { (*group).clone() }; - if let Some(f) = h.release { - unsafe { f(h.ctx) }; - } - unsafe { (*group).ctx = std::ptr::null_mut() }; - }) - } - - /// `oaknode_group_add_input_passthrough` (live; two-stage return of - /// the generated passthrough id). - #[no_mangle] - pub unsafe extern "C" 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 { - let result = group_add_passthrough_impl(&group, &node, input_id, element, ""); - match result { - Ok(id) => copy_string_out(&id, buf, buf_size), - Err(e) => e.code(), - } - } - - /// Shared body of the live and undoable add-passthrough exports. - fn group_add_passthrough_impl( - group: &CHandle, - node: &CHandle, - input_id: *const c_char, - element: c_int, - force_id: &str, - ) -> Result { - let g = unsafe { node_ref(group)? }; - let n = unsafe { node_ref(node)? }; - if !Arc::ptr_eq(&g.project, &n.project) { - return Err(Error::Invalid); - } - let input = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?.to_string(); - let inner = InnerInput { - node: n.id, - input: input.clone(), - element, - }; - let mut guard = lock(&g.project); - // Resolve the inner input descriptor first (immutable borrow), - // then mutate the group (C++ reads the input off the inner node - // inside `add_input_passthrough`). - let descriptor = guard - .graph - .get(n.id) - .and_then(|e| e.core.get_input(&input)) - .cloned() - .ok_or(Error::NotFound)?; - let entry = guard.graph.get_mut(g.id).ok_or(Error::NotFound)?; - let core = &mut entry.core; - let gb = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - Ok(gb.add_input_passthrough(core, inner, force_id, &descriptor)) - } - - /// `oaknode_group_add_input_passthrough_undoable` - /// (`olive::NodeGroupAddInputPassthrough`). The generated id is not - /// retrievable through this call. - #[no_mangle] - pub unsafe extern "C" 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 { - guard(|| { - if out_command.is_null() { - return Err(Error::Invalid); - } - let g = unsafe { node_ref(&group)? }; - let n = unsafe { node_ref(&node)? }; - if !Arc::ptr_eq(&g.project, &n.project) { - return Err(Error::Invalid); - } - let input = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?.to_string(); - let project = g.project.clone(); - let group_id = g.id; - let inner = InnerInput { - node: n.id, - input, - element, - }; - // The "actually added" flag shared between redo and undo - // (C++ `NodeGroupAddInputPassthrough::actually_added_`). - let actually_added = std::sync::Arc::new(std::sync::Mutex::new(false)); - let redo_added = actually_added.clone(); - let redo_inner = inner.clone(); - let redo_project = project.clone(); - let cmd = crate::bridge::undo::command_from_closures( - move || { - let mut guard = lock(&redo_project); - if let Some(entry) = guard.graph.get(group_id) { - let gb = group_of(entry); - let contains = gb - .map(|g| g.contains_input_passthrough(&redo_inner)) - .unwrap_or(false); - *lock(&redo_added) = false; - if !contains { - // The descriptor comes from the inner node, - // not the group (the group input does not - // exist yet). - let descriptor = guard - .graph - .get(redo_inner.node) - .and_then(|e| e.core.get_input(&redo_inner.input)) - .cloned(); - if let (Some(descriptor), Some(entry)) = - (descriptor, guard.graph.get_mut(group_id)) - { - if let Some(gb) = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - { - gb.add_input_passthrough( - &mut entry.core, - redo_inner.clone(), - "", - &descriptor, - ); - *lock(&redo_added) = true; - } - } - } - } - }, - move || { - if *lock(&actually_added) { - let mut guard = lock(&project); - if let Some(entry) = guard.graph.get_mut(group_id) { - if let Some(gb) = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - { - gb.remove_input_passthrough(&mut entry.core, &inner); - } - } - } - }, - ) - .ok_or(Error::NoMem)?; - unsafe { *out_command = cmd }; - Ok(()) - }) - } - - /// `oaknode_group_remove_input_passthrough` (live). - #[no_mangle] - pub unsafe extern "C" fn oaknode_group_remove_input_passthrough( - group: CHandle, - node: CHandle, - input_id: *const c_char, - element: c_int, - ) -> c_int { - guard(|| { - let g = unsafe { node_ref(&group)? }; - let n = unsafe { node_ref(&node)? }; - if !Arc::ptr_eq(&g.project, &n.project) { - return Err(Error::Invalid); - } - let input = unsafe { cstr(input_id) }.ok_or(Error::Invalid)?.to_string(); - let inner = InnerInput { - node: n.id, - input, - element, - }; - let mut guard = lock(&g.project); - let entry = guard.graph.get_mut(g.id).ok_or(Error::NotFound)?; - let gb = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - if !gb.contains_input_passthrough(&inner) { - return Err(Error::NotFound); - } - gb.remove_input_passthrough(&mut entry.core, &inner); - Ok(()) - }) - } - - /// `oaknode_group_passthrough_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_group_passthrough_count( - group: CHandle, - out_count: *mut c_int, - ) -> c_int { - guard(|| { - if out_count.is_null() { - return Err(Error::Invalid); - } - let g = unsafe { node_ref(&group)? }; - let count = with_graph_read(&g.project, |graph| { - graph - .get(g.id) - .and_then(group_of) - .map(|gb| gb.passthroughs().len() as c_int) - }) - .ok_or(Error::NotFound)?; - unsafe { *out_count = count }; - Ok(()) - }) - } - - /// `oaknode_group_passthrough_id_at` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oaknode_group_passthrough_id_at( - group: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let g = match unsafe { node_ref(&group) } { - Ok(g) => g, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let id = with_graph_read(&g.project, |graph| { - graph - .get(g.id) - .and_then(group_of) - .and_then(|gb| gb.passthroughs().get(index as usize)) - .map(|(id, _)| id.clone()) - }); - match id { - Some(id) => copy_string_out(&id, buf, buf_size), - None => crate::error::OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_group_passthrough_input_at`: inner node (borrowed), - /// input id (two-stage), element. - #[no_mangle] - pub unsafe extern "C" 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 { - let g = match unsafe { node_ref(&group) } { - Ok(g) => g, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let inner = with_graph_read(&g.project, |graph| { - graph - .get(g.id) - .and_then(group_of) - .and_then(|gb| gb.passthroughs().get(index as usize)) - .map(|(_, inner)| inner.clone()) - }); - let inner = match inner { - Some(inner) => inner, - None => return crate::error::OAKNODE_E_NOT_FOUND, - }; - if !out_node.is_null() { - unsafe { *out_node = make_node_borrowed(g.project.clone(), inner.node) }; - } - if !out_element.is_null() { - unsafe { *out_element = inner.element }; - } - copy_string_out(&inner.input, buf, buf_size) - } - - /// `oaknode_group_get_output_passthrough`: borrowed handle, empty - /// when unset; `OAKNODE_OK` either way. - #[no_mangle] - pub unsafe extern "C" fn oaknode_group_get_output_passthrough( - group: CHandle, - out_node: *mut CHandle, - ) -> c_int { - guard(|| { - if out_node.is_null() { - return Err(Error::Invalid); - } - let g = unsafe { node_ref(&group)? }; - let target = with_graph_read(&g.project, |graph| { - graph - .get(g.id) - .and_then(group_of) - .and_then(|gb| gb.output_passthrough()) - }); - match target { - Some(id) => unsafe { *out_node = make_node_borrowed(g.project.clone(), id) }, - None => unsafe { *out_node = CHandle::null() }, - } - Ok(()) - }) - } - - /// `oaknode_group_set_output_passthrough` (live); `node` may be an - /// empty handle to clear. - #[no_mangle] - pub unsafe extern "C" fn oaknode_group_set_output_passthrough( - group: CHandle, - node: CHandle, - ) -> c_int { - guard(|| { - let g = unsafe { node_ref(&group)? }; - let target = if node.ctx.is_null() { - None - } else { - let n = unsafe { node_ref(&node)? }; - if !Arc::ptr_eq(&g.project, &n.project) { - return Err(Error::Invalid); - } - Some(n.id) - }; - let mut guard = lock(&g.project); - let entry = guard.graph.get_mut(g.id).ok_or(Error::NotFound)?; - let gb = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - gb.set_output_passthrough(target); - Ok(()) - }) - } - - /// `oaknode_group_set_output_passthrough_undoable` - /// (`olive::NodeGroupSetOutputPassthrough`). - #[no_mangle] - pub unsafe extern "C" fn oaknode_group_set_output_passthrough_undoable( - group: CHandle, - node: CHandle, - out_command: *mut CHandle, - ) -> c_int { - guard(|| { - if out_command.is_null() { - return Err(Error::Invalid); - } - let g = unsafe { node_ref(&group)? }; - let new_target = if node.ctx.is_null() { - None - } else { - let n = unsafe { node_ref(&node)? }; - if !Arc::ptr_eq(&g.project, &n.project) { - return Err(Error::Invalid); - } - Some(n.id) - }; - let project = g.project.clone(); - let group_id = g.id; - // `old_output_` is captured at redo time (C++ redo stores it - // before assigning; `// CPP-PARITY: group.cpp:380`). - let old_output = std::sync::Arc::new(std::sync::Mutex::new(None)); - let redo_old = old_output.clone(); - let redo_project = project.clone(); - let cmd = crate::bridge::undo::command_from_closures( - move || { - let mut guard = lock(&redo_project); - if let Some(entry) = guard.graph.get_mut(group_id) { - if let Some(gb) = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - { - *lock(&redo_old) = gb.output_passthrough(); - gb.set_output_passthrough(new_target); - } - } - }, - move || { - let mut guard = lock(&project); - if let Some(entry) = guard.graph.get_mut(group_id) { - if let Some(gb) = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - { - gb.set_output_passthrough(*lock(&old_output)); - } - } - }, - ) - .ok_or(Error::NoMem)?; - unsafe { *out_command = cmd }; - Ok(()) - }) - } - - /// `oaknode_group_resolve_input`: follow group passthroughs to the - /// innermost non-group input; `OAKNODE_E_NOT_FOUND` when the input - /// does not resolve. - #[no_mangle] - pub unsafe extern "C" 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 { - let n = match unsafe { node_ref(&node) } { - Ok(n) => n, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let input = match unsafe { cstr(input_id) } { - Some(s) => s, - None => return crate::error::OAKNODE_E_INVALID, - }; - let resolved = with_graph_read(&n.project, |graph| { - let start = InnerInput { - node: n.id, - input: input.to_string(), - element, - }; - // The input must resolve to a live input on the final node - // (C++ `resolve_input` returns whatever it lands on; the - // C ABI contract rejects unresolvable inputs). - let r = NodeGroup::resolve_input(graph, start); - let input_ok = graph - .get(r.node) - .map(|e| e.core.has_input(&r.input)) - .unwrap_or(false); - input_ok.then_some(r) - }); - let resolved = match resolved { - Some(r) => r, - None => return crate::error::OAKNODE_E_NOT_FOUND, - }; - if !out_node.is_null() { - unsafe { *out_node = make_node_borrowed(n.project.clone(), resolved.node) }; - } - if !out_element.is_null() { - unsafe { *out_element = resolved.element }; - } - copy_string_out(&resolved.input, buf, buf_size) - } -} - -// ===================================================================== -// include/node/sequence.h -// ===================================================================== - -/// `include/node/sequence.h` exports. A sequence handle is a node -/// handle whose node carries [`crate::sequence::SequenceBehavior`]. -/// `oaknode_sequence_create` builds a self-contained scratch project -/// holding the sequence node plus its three track lists (video, audio, -/// subtitle); adding the sequence to a project transfers the whole -/// subgraph (`oaknode_project_add_node`). -pub mod sequence { - use super::*; - use crate::node::NodeCore; - use crate::sequence::SequenceBehavior; - use crate::track::{TrackListBehavior, TrackType}; - - /// Borrowed copy of the sequence behavior behind a handle. - fn seq_of(n: &NodeRef) -> Result { - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .map(|s| SequenceBehavior { - track_lists: s.track_lists.clone(), - markers: s.markers.clone(), - workarea: s.workarea.clone(), - last_length: s.last_length, - autocache_video: s.autocache_video, - autocache_audio: s.autocache_audio, - playhead: s.playhead, - video_params: s.video_params.clone(), - audio_params: s.audio_params.clone(), - }) - .ok_or(Error::State) - } - - /// Build the default track-list set for a sequence: one node per - /// type, appended to `seq`'s `track_lists`. - fn append_default_track_lists(graph: &mut crate::graph::Graph, seq_id: NodeId) -> Vec { - let mut lists = Vec::new(); - let mut base = 0i32; - for kind in [TrackType::Video, TrackType::Audio, TrackType::Subtitle] { - let mut behavior = TrackListBehavior::new(kind); - behavior.sequence = Some(seq_id); - behavior.array_base = base; - let id = graph.add_node(NodeCore::new(), Box::new(behavior)); - lists.push(id); - base += 1; - } - lists - } - - /// `oaknode_sequence_create`: an owned sequence handle whose scratch - /// project holds the sequence + its three track lists. - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_create() -> CHandle { - let scratch = Project::new(); - let id = { - let mut guard = lock(&scratch); - let mut seq = SequenceBehavior::new(); - seq.set_default_parameters(); - let mut core = NodeCore::new(); - core.add_input(crate::input::Input::new( - crate::sequence::TEXTURE_INPUT, - crate::value::ValueType::Texture, - crate::value::NodeValue::None, - )); - core.add_input(crate::input::Input::new( - crate::sequence::SAMPLES_INPUT, - crate::value::ValueType::Samples, - crate::value::NodeValue::None, - )); - // One array input per track list (`track_in_%1`; the video - // list owns track_in_0, audio track_in_1, subtitle - // track_in_2 — `// CPP-PARITY: sequence.h`). - for base in 0..3 { - let mut track_input = crate::input::Input::new( - &crate::sequence::TRACK_INPUT_FORMAT.replace("%1", &base.to_string()), - crate::value::ValueType::None, - crate::value::NodeValue::None, - ); - track_input.flags |= crate::input::flags::ARRAY; - core.add_input(track_input); - } - let seq_id = guard.graph.add_node(core, Box::new(seq)); - // Fill the track lists. - let lists = append_default_track_lists(&mut guard.graph, seq_id); - if let Some(entry) = guard.graph.get_mut(seq_id) { - if let Some(s) = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - { - s.track_lists = lists; - } - } - seq_id - }; - make_node_owned(scratch, id) - } - - /// `oaknode_sequence_free`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_free(sequence: *mut CHandle) { - if sequence.is_null() || unsafe { (*sequence).ctx.is_null() } { - return; - } - let h = unsafe { (*sequence).clone() }; - if let Some(f) = h.release { - unsafe { f(h.ctx) }; - } - unsafe { (*sequence).ctx = std::ptr::null_mut() }; - } - - /// `oaknode_sequence_set_default_parameters`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_set_default_parameters(sequence: CHandle) -> c_int { - guard(|| { - let n = unsafe { node_ref(&sequence)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let s = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - s.set_default_parameters(); - Ok(()) - }) - } - - /// `oaknode_sequence_as_node`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_as_node(sequence: CHandle) -> CHandle { - match unsafe { node_ref(&sequence) } { - Ok(n) => make_node_borrowed(n.project.clone(), n.id), - Err(_) => CHandle::null(), - } - } - - /// `oaknode_sequence_from_node`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_from_node(node: CHandle) -> CHandle { - let n = match unsafe { node_ref(&node) } { - Ok(n) => n, - Err(_) => return CHandle::null(), - }; - let guard = lock(&n.project); - let is_seq = guard - .graph - .get(n.id) - .and_then(|e| e.behavior.as_any()) - .map(|a| a.downcast_ref::().is_some()) - .unwrap_or(false); - if is_seq { - make_node_borrowed(n.project.clone(), n.id) - } else { - CHandle::null() - } - } - - /// `oaknode_sequence_get_track_list`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_get_track_list( - sequence: CHandle, - type_: c_int, - out: *mut CHandle, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&sequence)? }; - let kind = TrackType::from_c(type_).ok_or(Error::NotFound)?; - let (project, list_id) = { - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - let s = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - let list = s - .track_lists - .iter() - .find(|id| { - guard - .graph - .get(**id) - .and_then(|e| e.behavior.as_any()) - .and_then(|a| a.downcast_ref::()) - .map(|tl| tl.kind == kind) - .unwrap_or(false) - }) - .copied() - .ok_or(Error::NotFound)?; - (n.project.clone(), list) - }; - unsafe { *out = make_node_borrowed(project, list_id) }; - Ok(()) - }) - } - - /// `oaknode_sequence_get_track_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_get_track_count( - sequence: CHandle, - type_: c_int, - count: *mut c_int, - ) -> c_int { - guard(|| { - if count.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&sequence)? }; - let kind = TrackType::from_c(type_).ok_or(Error::NotFound)?; - let mut total = 0; - { - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - let s = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - for id in &s.track_lists { - if let Some(le) = guard.graph.get(*id) { - if let Some(tl) = le - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - { - if tl.kind == kind { - total = tl.tracks.len(); - } - } - } - } - } - unsafe { *count = total as c_int }; - Ok(()) - }) - } - - /// `oaknode_sequence_get_track_at`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_get_track_at( - sequence: CHandle, - type_: c_int, - index: c_int, - out: *mut CHandle, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&sequence)? }; - let kind = TrackType::from_c(type_).ok_or(Error::NotFound)?; - if index < 0 { - return Err(Error::NotFound); - } - let (project, track) = { - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - let s = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - let mut found = None; - for id in &s.track_lists { - if let Some(le) = guard.graph.get(*id) { - if let Some(tl) = le - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - { - if tl.kind == kind { - found = tl.tracks.get(index as usize).copied(); - } - } - } - } - (n.project.clone(), found.ok_or(Error::NotFound)?) - }; - unsafe { *out = make_node_borrowed(project, track) }; - Ok(()) - }) - } - - /// `oaknode_sequence_get_all_track_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_get_all_track_count( - sequence: CHandle, - count: *mut c_int, - ) -> c_int { - guard(|| { - if count.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&sequence)? }; - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - let s = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - let mut total = 0usize; - for id in &s.track_lists { - if let Some(le) = guard.graph.get(*id) { - if let Some(tl) = le - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - { - total += tl.tracks.len(); - } - } - } - unsafe { *count = total as c_int }; - Ok(()) - }) - } - - /// `oaknode_sequence_get_all_track_at`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_get_all_track_at( - sequence: CHandle, - index: c_int, - out: *mut CHandle, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&sequence)? }; - if index < 0 { - return Err(Error::NotFound); - } - let (project, track) = { - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - let s = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - let mut flat: Vec = Vec::new(); - for id in &s.track_lists { - if let Some(le) = guard.graph.get(*id) { - if let Some(tl) = le - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - { - flat.extend(tl.tracks.iter().copied()); - } - } - } - ( - n.project.clone(), - flat.get(index as usize).copied().ok_or(Error::NotFound)?, - ) - }; - unsafe { *out = make_node_borrowed(project, track) }; - Ok(()) - }) - } - - /// `oaknode_sequence_get_playhead`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_get_playhead( - sequence: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - guard(|| { - if numerator.is_null() || denominator.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&sequence)? }; - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - let s = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - unsafe { - *numerator = s.playhead.numerator() as c_int; - *denominator = s.playhead.denominator() as c_int; - } - Ok(()) - }) - } - - /// `oaknode_sequence_set_playhead`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_set_playhead( - sequence: CHandle, - numerator: c_int, - denominator: c_int, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&sequence)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let s = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - s.playhead = oakcore_rs::Rational::new(numerator as i64, denominator as i64); - Ok(()) - }) - } - - /// `oaknode_sequence_get_length` (cached overall length). - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_get_length( - sequence: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - guard(|| { - if numerator.is_null() || denominator.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&sequence)? }; - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - let s = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - unsafe { - *numerator = s.last_length.numerator() as c_int; - *denominator = s.last_length.denominator() as c_int; - } - Ok(()) - }) - } - - /// Compute the length of the `kind` track lists (longest track). - fn list_length(n: &NodeRef, kind: TrackType) -> Result { - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - let s = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - let mut longest = oakcore_rs::Rational::new(0, 1); - for id in &s.track_lists { - if let Some(le) = guard.graph.get(*id) { - if let Some(tl) = le - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - { - if tl.kind == kind { - let len = tl.total_length(&GraphTrackRange { - graph: &guard.graph, - }); - if len > longest { - longest = len; - } - } - } - } - } - Ok(longest) - } - - /// `oaknode_sequence_get_video_length`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_get_video_length( - sequence: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - guard(|| { - if numerator.is_null() || denominator.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&sequence)? }; - let len = list_length(&n, TrackType::Video)?; - unsafe { - *numerator = len.numerator() as c_int; - *denominator = len.denominator() as c_int; - } - Ok(()) - }) - } - - /// `oaknode_sequence_get_audio_length`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_get_audio_length( - sequence: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - guard(|| { - if numerator.is_null() || denominator.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&sequence)? }; - let len = list_length(&n, TrackType::Audio)?; - unsafe { - *numerator = len.numerator() as c_int; - *denominator = len.denominator() as c_int; - } - Ok(()) - }) - } - - /// `oaknode_sequence_verify_length`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_verify_length(sequence: CHandle) -> c_int { - guard(|| { - let n = unsafe { node_ref(&sequence)? }; - let video = list_length(&n, TrackType::Video)?; - let audio = list_length(&n, TrackType::Audio)?; - let overall = if video > audio { video } else { audio }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let s = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - s.verify_length((video, audio, overall)); - Ok(()) - }) - } - - /// `oaknode_sequence_get_video_stream_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_get_video_stream_count( - sequence: CHandle, - count: *mut c_int, - ) -> c_int { - guard(|| { - if count.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&sequence)? }; - let s = seq_of(&n)?; - unsafe { *count = s.video_stream_count() as c_int }; - Ok(()) - }) - } - - /// `oaknode_sequence_get_audio_stream_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_get_audio_stream_count( - sequence: CHandle, - count: *mut c_int, - ) -> c_int { - guard(|| { - if count.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&sequence)? }; - let s = seq_of(&n)?; - unsafe { *count = s.audio_stream_count() as c_int }; - Ok(()) - }) - } - - /// `oaknode_sequence_get_video_params`: the video params at `index` as - /// a NEW owned oakcommon handle (`// CPP-PARITY: - /// src/node/c_api/sequence.cpp:248`). The Rust model's `VideoParams` - /// carries width/height/frame-rate/format/channels; the handle is - /// built through the oakcommon videoparams C ABI (`OAKNODE_E_NOMEM` - /// when oakcommon is unavailable — the C++ null-handle path). - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_get_video_params( - sequence: CHandle, - index: c_int, - out: *mut std::ffi::c_void, - ) -> c_int { - guard(|| { - if out.is_null() || index < 0 { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&sequence)? }; - let s = seq_of(&n)?; - let params = s.video_params.get(index as usize).ok_or(Error::NotFound)?; - let handle = super::videoparams_handle_from(params).ok_or(Error::NoMem)?; - unsafe { *(out as *mut crate::handle::CHandle) = handle }; - Ok(()) - }) - } - - /// `oaknode_sequence_set_video_params`: replace the video params at - /// `index` with a copy of `params` (`// CPP-PARITY: sequence.cpp:270`). - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_set_video_params( - sequence: CHandle, - index: c_int, - params: crate::handle::CHandle, - ) -> c_int { - guard(|| { - if index < 0 || params.ctx.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&sequence)? }; - let read = super::videoparams_from_handle(params).ok_or(Error::Invalid)?; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let s = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - if index as usize >= s.video_params.len() { - return Err(Error::NotFound); - } - s.video_params[index as usize] = read; - Ok(()) - }) - } - - /// `oaknode_sequence_get_audio_params`: the audio params at `index` as - /// a NEW owned oakcore handle (`OakAudioParams **out`, - /// `// CPP-PARITY: sequence.cpp:293`). - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_get_audio_params( - sequence: CHandle, - index: c_int, - out: *mut *mut std::ffi::c_void, - ) -> c_int { - guard(|| { - if out.is_null() || index < 0 { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&sequence)? }; - let s = seq_of(&n)?; - let params = s.audio_params.get(index as usize).ok_or(Error::NotFound)?; - let handle = crate::bridge::core::audioparams_create( - params.sample_rate, - params.channel_layout, - params.format, - ) - .ok_or(Error::NoMem)?; - unsafe { *out = handle }; - Ok(()) - }) - } - - /// `oaknode_sequence_set_audio_params`: replace the audio params at - /// `index` with a copy of `params` (`// CPP-PARITY: sequence.cpp:312`). - #[no_mangle] - pub unsafe extern "C" fn oaknode_sequence_set_audio_params( - sequence: CHandle, - index: c_int, - params: *const std::ffi::c_void, - ) -> c_int { - guard(|| { - if params.is_null() || index < 0 { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&sequence)? }; - let read = crate::value::AudioParams { - sample_rate: crate::bridge::core::audioparams_sample_rate(params) - .ok_or(Error::Invalid)?, - channel_layout: crate::bridge::core::audioparams_channel_layout(params) - .ok_or(Error::Invalid)?, - format: crate::bridge::core::audioparams_format(params).ok_or(Error::Invalid)?, - }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let s = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - if index as usize >= s.audio_params.len() { - return Err(Error::NotFound); - } - s.audio_params[index as usize] = read; - Ok(()) - }) - } -} - -// ===================================================================== -// include/node/track.h -// ===================================================================== - -/// `include/node/track.h` exports. Track and track-list handles are node -/// handles whose nodes carry [`crate::track::TrackBehavior`] / -/// [`crate::track::TrackListBehavior`]. -pub mod track { - use super::*; - use crate::node::NodeCore; - use crate::track::{TrackBehavior, TrackListBehavior, TrackType}; - - /// Borrowed track behavior copy. - fn track_of(n: &NodeRef) -> Result { - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .map(|t| TrackBehavior { - kind: t.kind, - blocks: t.blocks.clone(), - muted: t.muted, - locked: t.locked, - height: t.height, - index: t.index, - track_list: t.track_list, - }) - .ok_or(Error::State) - } - - /// Borrowed track-list behavior copy. - fn tracklist_of(n: &NodeRef) -> Result { - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .map(|t| TrackListBehavior { - kind: t.kind, - tracks: t.tracks.clone(), - sequence: t.sequence, - array_base: t.array_base, - }) - .ok_or(Error::State) - } - - /// `oaknode_track_as_node`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_as_node(track: CHandle) -> CHandle { - match unsafe { node_ref(&track) } { - Ok(n) => make_node_borrowed(n.project.clone(), n.id), - Err(_) => CHandle::null(), - } - } - - /// `oaknode_track_create`: owned track in a scratch project. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_create(type_: c_int) -> CHandle { - let kind = match TrackType::from_c(type_) { - Some(k) => k, - None => return CHandle::null(), - }; - let scratch = Project::new(); - let id = { - let mut guard = lock(&scratch); - guard - .graph - .add_node(NodeCore::new(), Box::new(TrackBehavior::new(kind))) - }; - make_node_owned(scratch, id) - } - - /// `oaknode_track_free`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_free(track: *mut CHandle) { - if track.is_null() || unsafe { (*track).ctx.is_null() } { - return; - } - let h = unsafe { (*track).clone() }; - if let Some(f) = h.release { - unsafe { f(h.ctx) }; - } - unsafe { (*track).ctx = std::ptr::null_mut() }; - } - - /// `oaknode_track_get_type`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_get_type(track: CHandle, type_: *mut c_int) -> c_int { - guard(|| { - if type_.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&track)? }; - let t = track_of(&n)?; - unsafe { *type_ = t.kind.to_c() }; - Ok(()) - }) - } - - /// `oaknode_track_set_type`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_set_type(track: CHandle, type_: c_int) -> c_int { - guard(|| { - let n = unsafe { node_ref(&track)? }; - let kind = TrackType::from_c(type_).ok_or(Error::Invalid)?; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let t = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - t.kind = kind; - Ok(()) - }) - } - - /// `oaknode_track_get_height`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_get_height(track: CHandle, height: *mut f64) -> c_int { - guard(|| { - if height.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&track)? }; - let t = track_of(&n)?; - unsafe { *height = t.height }; - Ok(()) - }) - } - - /// `oaknode_track_set_height`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_set_height(track: CHandle, height: f64) -> c_int { - guard(|| { - let n = unsafe { node_ref(&track)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let t = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - t.height = height; - Ok(()) - }) - } - - /// `oaknode_track_get_height_in_pixels`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_get_height_in_pixels( - track: CHandle, - height: *mut c_int, - ) -> c_int { - guard(|| { - if height.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&track)? }; - let t = track_of(&n)?; - unsafe { *height = crate::track::internal_height_to_pixel_height(t.height) }; - Ok(()) - }) - } - - /// `oaknode_track_set_height_in_pixels`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_set_height_in_pixels( - track: CHandle, - height: c_int, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&track)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let t = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - t.height = crate::track::pixel_height_to_internal_height(height); - Ok(()) - }) - } - - /// `oaknode_track_get_default_height_in_pixels`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_get_default_height_in_pixels() -> c_int { - crate::track::internal_height_to_pixel_height(crate::track::DEFAULT_HEIGHT_INTERNAL) - } - - /// `oaknode_track_get_minimum_height_in_pixels`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_get_minimum_height_in_pixels() -> c_int { - crate::track::internal_height_to_pixel_height(crate::track::MINIMUM_HEIGHT_INTERNAL) - } - - /// `oaknode_track_get_index`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_get_index(track: CHandle, index: *mut c_int) -> c_int { - guard(|| { - if index.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&track)? }; - let t = track_of(&n)?; - unsafe { *index = t.index }; - Ok(()) - }) - } - - /// `oaknode_track_set_index`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_set_index(track: CHandle, index: c_int) -> c_int { - guard(|| { - let n = unsafe { node_ref(&track)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let t = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - t.index = index; - Ok(()) - }) - } - - /// `oaknode_track_get_muted`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_get_muted(track: CHandle, muted: *mut c_int) -> c_int { - guard(|| { - if muted.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&track)? }; - let t = track_of(&n)?; - unsafe { *muted = t.muted as c_int }; - Ok(()) - }) - } - - /// `oaknode_track_set_muted`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_set_muted(track: CHandle, muted: c_int) -> c_int { - guard(|| { - let n = unsafe { node_ref(&track)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let t = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - t.muted = muted != 0; - Ok(()) - }) - } - - /// `oaknode_track_get_locked`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_get_locked(track: CHandle, locked: *mut c_int) -> c_int { - guard(|| { - if locked.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&track)? }; - let t = track_of(&n)?; - unsafe { *locked = t.locked as c_int }; - Ok(()) - }) - } - - /// `oaknode_track_set_locked`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_set_locked(track: CHandle, locked: c_int) -> c_int { - guard(|| { - let n = unsafe { node_ref(&track)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let t = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - t.locked = locked != 0; - Ok(()) - }) - } - - /// `oaknode_track_get_reference`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_get_reference( - track: CHandle, - type_: *mut c_int, - index: *mut c_int, - ) -> c_int { - guard(|| { - if type_.is_null() || index.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&track)? }; - let t = track_of(&n)?; - let (ty, i) = t.reference(); - unsafe { - *type_ = ty; - *index = i; - } - Ok(()) - }) - } - - /// `oaknode_track_get_length`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_get_length( - track: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - guard(|| { - if numerator.is_null() || denominator.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&track)? }; - let len = { - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - let t = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - t.length(&GraphBlockRange { - graph: &guard.graph, - }) - }; - unsafe { - *numerator = len.numerator() as c_int; - *denominator = len.denominator() as c_int; - } - Ok(()) - }) - } - - /// `oaknode_track_get_sequence` (borrowed). - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_get_sequence( - track: CHandle, - out: *mut CHandle, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&track)? }; - let (project, seq) = { - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - let t = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - let seq = t.track_list.and_then(|list| { - guard - .graph - .get(list) - .and_then(|le| le.behavior.as_any()) - .and_then(|a| a.downcast_ref::()) - .and_then(|tl| tl.sequence) - }); - (n.project.clone(), seq) - }; - unsafe { - *out = match seq { - Some(s) => make_node_borrowed(project, s), - None => CHandle::null(), - }; - } - Ok(()) - }) - } - - /// `oaknode_track_get_block_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_get_block_count( - track: CHandle, - count: *mut c_int, - ) -> c_int { - guard(|| { - if count.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&track)? }; - let t = track_of(&n)?; - unsafe { *count = t.blocks.len() as c_int }; - Ok(()) - }) - } - - /// `oaknode_track_get_block_at` (borrowed). - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_get_block_at( - track: CHandle, - index: c_int, - out: *mut CHandle, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&track)? }; - if index < 0 { - return Err(Error::NotFound); - } - let (project, block) = { - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - let t = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - ( - n.project.clone(), - t.blocks - .get(index as usize) - .copied() - .ok_or(Error::NotFound)?, - ) - }; - unsafe { *out = make_node_borrowed(project, block) }; - Ok(()) - }) - } - - /// Shared block-append/insert helper: adopt `block` into the track's - /// project and list. Returns the block's id in the track's project - /// (it may differ after the move). - fn adopt_block( - track_project: &ProjectArc, - track_id: NodeId, - block: &NodeRef, - block_handle: &CHandle, - ) -> Result { - if !Arc::ptr_eq(&block.project, track_project) { - // Move the block into the track's project. - let new_id = move_node_between(&block.project, block.id, track_project)?; - let r = NodeRef { - project: track_project.clone(), - id: new_id, - owned: std::sync::Arc::new(AtomicBool::new(false)), - }; - unsafe { write_node_ref(block_handle, r) }; - // The block's core records the owning track. - let mut guard = lock(track_project); - if let Some(entry) = guard.graph.get_mut(new_id) { - if let Some(core) = block_core_mut(entry) { - core.track = Some(track_id); - } - } - Ok(new_id) - } else { - let mut guard = lock(track_project); - if let Some(entry) = guard.graph.get_mut(block.id) { - if let Some(core) = block_core_mut(entry) { - core.track = Some(track_id); - } - } - Ok(block.id) - } - } - - /// `oaknode_track_append_block`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_append_block(track: CHandle, block: CHandle) -> c_int { - guard(|| { - let t = unsafe { node_ref(&track)? }; - let b = unsafe { node_ref(&block)? }; - let block_id = adopt_block(&t.project, t.id, &b, &block)?; - let mut guard = lock(&t.project); - let entry = guard.graph.get_mut(t.id).ok_or(Error::NotFound)?; - let tb = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - tb.append_block(block_id); - Ok(()) - }) - } - - /// `oaknode_track_prepend_block`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_prepend_block(track: CHandle, block: CHandle) -> c_int { - guard(|| { - let t = unsafe { node_ref(&track)? }; - let b = unsafe { node_ref(&block)? }; - let block_id = adopt_block(&t.project, t.id, &b, &block)?; - let mut guard = lock(&t.project); - let entry = guard.graph.get_mut(t.id).ok_or(Error::NotFound)?; - let tb = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - tb.prepend_block(block_id); - Ok(()) - }) - } - - /// `oaknode_track_insert_block_at_index`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_insert_block_at_index( - track: CHandle, - block: CHandle, - index: c_int, - ) -> c_int { - guard(|| { - let t = unsafe { node_ref(&track)? }; - let b = unsafe { node_ref(&block)? }; - let block_id = adopt_block(&t.project, t.id, &b, &block)?; - let mut guard = lock(&t.project); - let entry = guard.graph.get_mut(t.id).ok_or(Error::NotFound)?; - let tb = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - tb.insert_block_at_index(block_id, index.max(0) as usize); - Ok(()) - }) - } - - /// `oaknode_track_insert_block_after`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_insert_block_after( - track: CHandle, - block: CHandle, - before: CHandle, - ) -> c_int { - guard(|| { - let t = unsafe { node_ref(&track)? }; - let b = unsafe { node_ref(&block)? }; - let ref_block = unsafe { node_ref(&before)? }; - let block_id = adopt_block(&t.project, t.id, &b, &block)?; - let mut guard = lock(&t.project); - let entry = guard.graph.get_mut(t.id).ok_or(Error::NotFound)?; - let tb = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - if tb.insert_block_after(block_id, ref_block.id) { - Ok(()) - } else { - Err(Error::NotFound) - } - }) - } - - /// `oaknode_track_insert_block_before`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_insert_block_before( - track: CHandle, - block: CHandle, - after: CHandle, - ) -> c_int { - guard(|| { - let t = unsafe { node_ref(&track)? }; - let b = unsafe { node_ref(&block)? }; - let ref_block = unsafe { node_ref(&after)? }; - let block_id = adopt_block(&t.project, t.id, &b, &block)?; - let mut guard = lock(&t.project); - let entry = guard.graph.get_mut(t.id).ok_or(Error::NotFound)?; - let tb = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - if tb.insert_block_before(block_id, ref_block.id) { - Ok(()) - } else { - Err(Error::NotFound) - } - }) - } - - /// `oaknode_track_ripple_remove_block`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_ripple_remove_block( - track: CHandle, - block: CHandle, - ) -> c_int { - guard(|| { - let t = unsafe { node_ref(&track)? }; - let b = unsafe { node_ref(&block)? }; - let mut guard = lock(&t.project); - let entry = guard.graph.get_mut(t.id).ok_or(Error::NotFound)?; - let tb = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - if tb.ripple_remove_block(b.id) { - if let Some(entry) = guard.graph.get_mut(b.id) { - if let Some(core) = block_core_mut(entry) { - core.track = None; - } - } - Ok(()) - } else { - Err(Error::NotFound) - } - }) - } - - /// `oaknode_track_replace_block`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_replace_block( - track: CHandle, - old_block: CHandle, - new_block: CHandle, - ) -> c_int { - guard(|| { - let t = unsafe { node_ref(&track)? }; - let old = unsafe { node_ref(&old_block)? }; - let new = unsafe { node_ref(&new_block)? }; - let new_id = adopt_block(&t.project, t.id, &new, &new_block)?; - let mut guard = lock(&t.project); - let entry = guard.graph.get_mut(t.id).ok_or(Error::NotFound)?; - let tb = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - if tb.replace_block(old.id, new_id) { - Ok(()) - } else { - Err(Error::NotFound) - } - }) - } - - /// `oaknode_track_get_block_index`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_get_block_index( - track: CHandle, - block: CHandle, - index: *mut c_int, - ) -> c_int { - guard(|| { - if index.is_null() { - return Err(Error::Invalid); - } - let t = unsafe { node_ref(&track)? }; - let b = unsafe { node_ref(&block)? }; - let tb = track_of(&t)?; - match tb.block_index(b.id) { - Some(i) => { - unsafe { *index = i as c_int }; - Ok(()) - } - None => Err(Error::NotFound), - } - }) - } - - /// `oaknode_track_get_block_containing_time`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_get_block_containing_time( - track: CHandle, - numerator: c_int, - denominator: c_int, - out: *mut CHandle, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let t = unsafe { node_ref(&track)? }; - let time = oakcore_rs::Rational::new(numerator as i64, denominator as i64); - let (project, block) = { - let guard = lock(&t.project); - let entry = guard.graph.get(t.id).ok_or(Error::NotFound)?; - let tb = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - let found = tb.block_containing_time( - time, - &GraphBlockRange { - graph: &guard.graph, - }, - ); - (t.project.clone(), found) - }; - unsafe { - *out = match block { - Some(b) => make_node_borrowed(project, b), - None => CHandle::null(), - }; - } - Ok(()) - }) - } - - /// `oaknode_track_get_visible_block_at_time`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_get_visible_block_at_time( - track: CHandle, - numerator: c_int, - denominator: c_int, - out: *mut CHandle, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let t = unsafe { node_ref(&track)? }; - let time = oakcore_rs::Rational::new(numerator as i64, denominator as i64); - let (project, block) = { - let guard = lock(&t.project); - let entry = guard.graph.get(t.id).ok_or(Error::NotFound)?; - let tb = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - let found = tb.visible_block_at_time( - time, - &GraphBlockRange { - graph: &guard.graph, - }, - ); - (t.project.clone(), found) - }; - unsafe { - *out = match block { - Some(b) => make_node_borrowed(project, b), - None => CHandle::null(), - }; - } - Ok(()) - }) - } - - /// `oaknode_track_is_range_free`. - #[no_mangle] - pub unsafe extern "C" 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 { - guard(|| { - if is_free.is_null() { - return Err(Error::Invalid); - } - let t = unsafe { node_ref(&track)? }; - let range = oakcore_rs::TimeRange::new( - oakcore_rs::Rational::new(in_num as i64, in_den as i64), - oakcore_rs::Rational::new(out_num as i64, out_den as i64), - ); - let free = { - let guard = lock(&t.project); - let entry = guard.graph.get(t.id).ok_or(Error::NotFound)?; - let tb = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - tb.is_range_free( - range, - &GraphBlockRange { - graph: &guard.graph, - }, - ) - }; - unsafe { *is_free = free as c_int }; - Ok(()) - }) - } - - /// `oaknode_track_get_nearest_block_before_or_at`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_get_nearest_block_before_or_at( - track: CHandle, - numerator: c_int, - denominator: c_int, - out: *mut CHandle, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let t = unsafe { node_ref(&track)? }; - let time = oakcore_rs::Rational::new(numerator as i64, denominator as i64); - let (project, block) = { - let guard = lock(&t.project); - let entry = guard.graph.get(t.id).ok_or(Error::NotFound)?; - let tb = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - // The last block whose in-point is <= time. - let found = tb - .blocks - .iter() - .filter(|b| { - guard - .graph - .get(**b) - .and_then(block_core) - .map(|c| c.in_() <= time) - .unwrap_or(false) - }) - .next_back() - .copied(); - (t.project.clone(), found) - }; - unsafe { - *out = match block { - Some(b) => make_node_borrowed(project, b), - None => CHandle::null(), - }; - } - Ok(()) - }) - } - - /// `oaknode_track_get_nearest_block_after_or_at`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_track_get_nearest_block_after_or_at( - track: CHandle, - numerator: c_int, - denominator: c_int, - out: *mut CHandle, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let t = unsafe { node_ref(&track)? }; - let time = oakcore_rs::Rational::new(numerator as i64, denominator as i64); - let (project, block) = { - let guard = lock(&t.project); - let entry = guard.graph.get(t.id).ok_or(Error::NotFound)?; - let tb = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - let found = tb - .blocks - .iter() - .filter(|b| { - guard - .graph - .get(**b) - .and_then(block_core) - .map(|c| c.out() >= time) - .unwrap_or(false) - }) - .next() - .copied(); - (t.project.clone(), found) - }; - unsafe { - *out = match block { - Some(b) => make_node_borrowed(project, b), - None => CHandle::null(), - }; - } - Ok(()) - }) - } - - // ---- TrackList ----------------------------------------------------- - - /// `oaknode_tracklist_get_sequence`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_tracklist_get_sequence( - list: CHandle, - out: *mut CHandle, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&list)? }; - let tl = tracklist_of(&n)?; - unsafe { - *out = match tl.sequence { - Some(s) => make_node_borrowed(n.project.clone(), s), - None => CHandle::null(), - }; - } - Ok(()) - }) - } - - /// `oaknode_tracklist_get_track_input_id` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oaknode_tracklist_get_track_input_id( - list: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let n = match unsafe { node_ref(&list) } { - Ok(n) => n, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let tl = match tracklist_of(&n) { - Ok(tl) => tl, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let input_id = - crate::sequence::TRACK_INPUT_FORMAT.replace("%1", &tl.array_base.to_string()); - copy_string_out(&input_id, buf, buf_size) - } - - /// `oaknode_tracklist_array_append`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_tracklist_array_append(list: CHandle) -> c_int { - guard(|| { - let n = unsafe { node_ref(&list)? }; - let tl = tracklist_of(&n)?; - let input_id = - crate::sequence::TRACK_INPUT_FORMAT.replace("%1", &tl.array_base.to_string()); - let mut guard = lock(&n.project); - let seq = tl.sequence.ok_or(Error::State)?; - let size = guard - .graph - .get(seq) - .map(|e| e.core.input_array_size(&input_id)) - .unwrap_or(0); - guard.graph.input_array_insert(seq, &input_id, size as i32) - }) - } - - /// `oaknode_tracklist_array_remove_last`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_tracklist_array_remove_last(list: CHandle) -> c_int { - guard(|| { - let n = unsafe { node_ref(&list)? }; - let tl = tracklist_of(&n)?; - let input_id = - crate::sequence::TRACK_INPUT_FORMAT.replace("%1", &tl.array_base.to_string()); - let mut guard = lock(&n.project); - let seq = tl.sequence.ok_or(Error::State)?; - let size = guard - .graph - .get(seq) - .map(|e| e.core.input_array_size(&input_id)) - .unwrap_or(0); - if size == 0 { - return Ok(()); - } - guard - .graph - .input_array_remove(seq, &input_id, (size - 1) as i32) - }) - } - - /// `oaknode_tracklist_get_array_index_from_cache_index` (identity for - /// compact lists; the cached index equals the array element when no - /// slots are disconnected). - #[no_mangle] - pub unsafe extern "C" fn oaknode_tracklist_get_array_index_from_cache_index( - list: CHandle, - cache_index: c_int, - out_index: *mut c_int, - ) -> c_int { - guard(|| { - if out_index.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&list)? }; - let tl = tracklist_of(&n)?; - if cache_index < 0 || cache_index as usize >= tl.tracks.len() { - return Err(Error::NotFound); - } - unsafe { *out_index = cache_index }; - Ok(()) - }) - } - - /// `oaknode_tracklist_get_type`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_tracklist_get_type(list: CHandle, type_: *mut c_int) -> c_int { - guard(|| { - if type_.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&list)? }; - let tl = tracklist_of(&n)?; - unsafe { *type_ = tl.kind.to_c() }; - Ok(()) - }) - } - - /// `oaknode_tracklist_get_track_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_tracklist_get_track_count( - list: CHandle, - count: *mut c_int, - ) -> c_int { - guard(|| { - if count.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&list)? }; - let tl = tracklist_of(&n)?; - unsafe { *count = tl.tracks.len() as c_int }; - Ok(()) - }) - } - - /// `oaknode_tracklist_get_track_at` (borrowed). - #[no_mangle] - pub unsafe extern "C" fn oaknode_tracklist_get_track_at( - list: CHandle, - index: c_int, - out: *mut CHandle, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&list)? }; - if index < 0 { - return Err(Error::NotFound); - } - let tl = tracklist_of(&n)?; - let track = tl - .tracks - .get(index as usize) - .copied() - .ok_or(Error::NotFound)?; - unsafe { *out = make_node_borrowed(n.project.clone(), track) }; - Ok(()) - }) - } - - /// `oaknode_tracklist_get_total_length`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_tracklist_get_total_length( - list: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - guard(|| { - if numerator.is_null() || denominator.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&list)? }; - let len = { - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - let tl = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - tl.total_length(&GraphTrackRange { - graph: &guard.graph, - }) - }; - unsafe { - *numerator = len.numerator() as c_int; - *denominator = len.denominator() as c_int; - } - Ok(()) - }) - } - - /// `oaknode_tracklist_get_array_size` (>= track count). - #[no_mangle] - pub unsafe extern "C" fn oaknode_tracklist_get_array_size( - list: CHandle, - size: *mut c_int, - ) -> c_int { - guard(|| { - if size.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&list)? }; - let tl = tracklist_of(&n)?; - let input_id = - crate::sequence::TRACK_INPUT_FORMAT.replace("%1", &tl.array_base.to_string()); - let guard = lock(&n.project); - let arr = tl - .sequence - .and_then(|seq| guard.graph.get(seq)) - .map(|e| e.core.input_array_size(&input_id)) - .unwrap_or(0); - unsafe { *size = arr as c_int }; - Ok(()) - }) - } - - /// `oaknode_tracklist_add_track`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_tracklist_add_track(list: CHandle, track: CHandle) -> c_int { - guard(|| { - let l = unsafe { node_ref(&list)? }; - let t = unsafe { node_ref(&track)? }; - let tl = tracklist_of(&l)?; - - // Move the track into the list's project; the id may change. - let track_id = if !Arc::ptr_eq(&t.project, &l.project) { - let new_id = move_node_between(&t.project, t.id, &l.project)?; - let r = NodeRef { - project: l.project.clone(), - id: new_id, - owned: std::sync::Arc::new(AtomicBool::new(false)), - }; - unsafe { write_node_ref(&track, r) }; - new_id - } else { - t.id - }; - - let mut guard = lock(&l.project); - // Inherit the previous track's height. - let prev_height = { - let entry = guard.graph.get(l.id).ok_or(Error::NotFound)?; - let tl = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - tl.tracks - .last() - .and_then(|last| guard.graph.get(*last)) - .and_then(|le| le.behavior.as_any()) - .and_then(|a| a.downcast_ref::()) - .map(|tb| tb.height) - .unwrap_or(crate::track::DEFAULT_HEIGHT_INTERNAL) - }; - { - let entry = guard.graph.get_mut(l.id).ok_or(Error::NotFound)?; - let tl = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - let index = tl.tracks.len() as i32; - tl.tracks.push(track_id); - if let Some(entry) = guard.graph.get_mut(track_id) { - if let Some(tb) = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - { - tb.track_list = Some(l.id); - tb.index = index; - tb.height = prev_height; - } - } - } - Ok(()) - }) - } - - /// `oaknode_tracklist_remove_track`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_tracklist_remove_track( - list: CHandle, - track: CHandle, - ) -> c_int { - guard(|| { - let l = unsafe { node_ref(&list)? }; - let t = unsafe { node_ref(&track)? }; - let mut guard = lock(&l.project); - let removed = { - let entry = guard.graph.get_mut(l.id).ok_or(Error::NotFound)?; - let tl = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - let before = tl.tracks.len(); - tl.tracks.retain(|x| *x != t.id); - tl.tracks.len() != before - }; - if removed { - // Renumber the remaining tracks' indices. - let ids: Vec = { - let entry = guard.graph.get(l.id).ok_or(Error::NotFound)?; - let tl = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - tl.tracks.clone() - }; - for (i, id) in ids.iter().enumerate() { - if let Some(entry) = guard.graph.get_mut(*id) { - if let Some(tb) = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - { - tb.index = i as i32; - } - } - } - } - if removed { - if let Some(entry) = guard.graph.get_mut(t.id) { - if let Some(tb) = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - { - tb.track_list = None; - } - } - Ok(()) - } else { - Err(Error::NotFound) - } - }) - } -} - -// ===================================================================== -// include/node/block.h -// ===================================================================== - -/// `include/node/block.h` exports. Block handles are node handles whose -/// nodes carry a block behavior ([`crate::block::BlockCore`]). -pub mod block { - use super::*; - use crate::block::{BlockCore, ClipBlockBehavior, GapBlockBehavior, TransitionBlockBehavior}; - - /// True when the node carries a block behavior. - fn is_block(n: &NodeRef) -> bool { - let guard = lock(&n.project); - guard.graph.get(n.id).and_then(block_core).is_some() - } - - /// `oaknode_block_clip_create`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_clip_create() -> CHandle { - let scratch = Project::new(); - let (core, behavior) = crate::block::clip_create(); - let id = { - let mut guard = lock(&scratch); - guard.graph.add_node(core, behavior) - }; - make_node_owned(scratch, id) - } - - /// `oaknode_block_gap_create`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_gap_create() -> CHandle { - let scratch = Project::new(); - let (core, behavior) = crate::block::gap_create(); - let id = { - let mut guard = lock(&scratch); - guard.graph.add_node(core, behavior) - }; - make_node_owned(scratch, id) - } - - /// `oaknode_block_transition_create`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_transition_create(kind: c_int) -> CHandle { - if !(0..=1).contains(&kind) { - return CHandle::null(); - } - let scratch = Project::new(); - let (core, behavior) = crate::block::transition_create(); - let id = { - let mut guard = lock(&scratch); - guard.graph.add_node(core, behavior) - }; - make_node_owned(scratch, id) - } - - /// `oaknode_block_free`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_free(block: *mut CHandle) { - if block.is_null() || unsafe { (*block).ctx.is_null() } { - return; - } - let h = unsafe { (*block).clone() }; - if let Some(f) = h.release { - unsafe { f(h.ctx) }; - } - unsafe { (*block).ctx = std::ptr::null_mut() }; - } - - /// `oaknode_block_get_kind`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_get_kind(block: CHandle, out_kind: *mut c_int) -> c_int { - guard(|| { - if out_kind.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&block)? }; - let kind = { - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - entry - .behavior - .as_any() - .map(|a| { - if a.downcast_ref::().is_some() { - 1 - } else if a.downcast_ref::().is_some() { - 2 - } else if a.downcast_ref::().is_some() { - 3 - } else { - 0 - } - }) - .ok_or(Error::State)? - }; - unsafe { *out_kind = kind }; - Ok(()) - }) - } - - /// `oaknode_block_as_node`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_as_node(block: CHandle) -> CHandle { - match unsafe { node_ref(&block) } { - Ok(n) => make_node_borrowed(n.project.clone(), n.id), - Err(_) => CHandle::null(), - } - } - - /// `oaknode_block_from_node`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_from_node(node: CHandle) -> CHandle { - let n = match unsafe { node_ref(&node) } { - Ok(n) => n, - Err(_) => return CHandle::null(), - }; - if is_block(&n) { - make_node_borrowed(n.project.clone(), n.id) - } else { - CHandle::null() - } - } - - /// `oaknode_block_get_in`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_get_in( - block: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - guard(|| { - if numerator.is_null() || denominator.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&block)? }; - let core = { - let guard = lock(&n.project); - guard - .graph - .get(n.id) - .and_then(block_core) - .map(|c| BlockCore::default()) - .ok_or(Error::State)?; - guard - .graph - .get(n.id) - .and_then(block_core) - .map(|c| BlockCore { - range: c.range, - media_in: c.media_in, - speed: c.speed, - reversed: c.reversed, - links: c.links.clone(), - enabled: c.enabled, - maintain_audio_pitch: c.maintain_audio_pitch, - loop_mode: c.loop_mode, - track: c.track, - }) - }; - let core = core.ok_or(Error::State)?; - unsafe { - *numerator = core.in_().numerator() as c_int; - *denominator = core.in_().denominator() as c_int; - } - Ok(()) - }) - } - - /// `oaknode_block_set_in`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_set_in( - block: CHandle, - numerator: c_int, - denominator: c_int, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&block)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let core = block_core_mut(entry).ok_or(Error::State)?; - core.set_in(oakcore_rs::Rational::new( - numerator as i64, - denominator as i64, - )); - Ok(()) - }) - } - - /// `oaknode_block_get_out`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_get_out( - block: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - guard(|| { - if numerator.is_null() || denominator.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&block)? }; - let guard = lock(&n.project); - let core = guard - .graph - .get(n.id) - .and_then(block_core) - .ok_or(Error::State)?; - unsafe { - *numerator = core.out().numerator() as c_int; - *denominator = core.out().denominator() as c_int; - } - Ok(()) - }) - } - - /// `oaknode_block_set_out`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_set_out( - block: CHandle, - numerator: c_int, - denominator: c_int, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&block)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let core = block_core_mut(entry).ok_or(Error::State)?; - core.set_out(oakcore_rs::Rational::new( - numerator as i64, - denominator as i64, - )); - Ok(()) - }) - } - - /// `oaknode_block_get_length`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_get_length( - block: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - guard(|| { - if numerator.is_null() || denominator.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&block)? }; - let guard = lock(&n.project); - let core = guard - .graph - .get(n.id) - .and_then(block_core) - .ok_or(Error::State)?; - let len = core.length(); - unsafe { - *numerator = len.numerator() as c_int; - *denominator = len.denominator() as c_int; - } - Ok(()) - }) - } - - /// `oaknode_block_set_length_and_media_out`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_set_length_and_media_out( - block: CHandle, - numerator: c_int, - denominator: c_int, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&block)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let core = block_core_mut(entry).ok_or(Error::State)?; - core.set_length_and_media_out(oakcore_rs::Rational::new( - numerator as i64, - denominator as i64, - )); - Ok(()) - }) - } - - /// `oaknode_block_set_length_and_media_in`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_set_length_and_media_in( - block: CHandle, - numerator: c_int, - denominator: c_int, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&block)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let core = block_core_mut(entry).ok_or(Error::State)?; - core.set_length_and_media_in(oakcore_rs::Rational::new( - numerator as i64, - denominator as i64, - )); - Ok(()) - }) - } - - /// `oaknode_block_get_enabled`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_get_enabled( - block: CHandle, - enabled: *mut c_int, - ) -> c_int { - guard(|| { - if enabled.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&block)? }; - let guard = lock(&n.project); - let core = guard - .graph - .get(n.id) - .and_then(block_core) - .ok_or(Error::State)?; - unsafe { *enabled = core.enabled as c_int }; - Ok(()) - }) - } - - /// `oaknode_block_set_enabled`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_set_enabled(block: CHandle, enabled: c_int) -> c_int { - guard(|| { - let n = unsafe { node_ref(&block)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let core = block_core_mut(entry).ok_or(Error::State)?; - core.enabled = enabled != 0; - Ok(()) - }) - } - - /// `oaknode_block_get_previous`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_get_previous( - block: CHandle, - out: *mut CHandle, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&block)? }; - let (project, prev) = { - let guard = lock(&n.project); - let core = guard - .graph - .get(n.id) - .and_then(block_core) - .ok_or(Error::State)?; - let track = core.track.ok_or(Error::State)?; - let tb = guard - .graph - .get(track) - .and_then(|e| e.behavior.as_any()) - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - let idx = tb.block_index(n.id).ok_or(Error::NotFound)?; - ( - n.project.clone(), - idx.checked_sub(1).and_then(|i| tb.block_at(i)), - ) - }; - unsafe { - *out = match prev { - Some(p) => make_node_borrowed(project, p), - None => CHandle::null(), - }; - } - Ok(()) - }) - } - - /// `oaknode_block_get_next`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_get_next(block: CHandle, out: *mut CHandle) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&block)? }; - let (project, next) = { - let guard = lock(&n.project); - let core = guard - .graph - .get(n.id) - .and_then(block_core) - .ok_or(Error::State)?; - let track = core.track.ok_or(Error::State)?; - let tb = guard - .graph - .get(track) - .and_then(|e| e.behavior.as_any()) - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - let idx = tb.block_index(n.id).ok_or(Error::NotFound)?; - (n.project.clone(), tb.block_at(idx + 1)) - }; - unsafe { - *out = match next { - Some(p) => make_node_borrowed(project, p), - None => CHandle::null(), - }; - } - Ok(()) - }) - } - - /// `oaknode_block_get_track` (borrowed). - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_get_track(block: CHandle, out: *mut CHandle) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&block)? }; - let (project, track) = { - let guard = lock(&n.project); - let core = guard - .graph - .get(n.id) - .and_then(block_core) - .ok_or(Error::State)?; - (n.project.clone(), core.track) - }; - unsafe { - *out = match track { - Some(t) => make_node_borrowed(project, t), - None => CHandle::null(), - }; - } - Ok(()) - }) - } - - /// `oaknode_block_link`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_link(a: CHandle, b: CHandle) -> c_int { - guard(|| { - let a = unsafe { node_ref(&a)? }; - let b = unsafe { node_ref(&b)? }; - if !Arc::ptr_eq(&a.project, &b.project) { - return Err(Error::Invalid); - } - let mut guard = lock(&a.project); - if guard.graph.link(a.id, b.id) { - Ok(()) - } else { - Err(Error::Failed("already linked".to_string())) - } - }) - } - - /// `oaknode_block_unlink`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_unlink(a: CHandle, b: CHandle) -> c_int { - guard(|| { - let a = unsafe { node_ref(&a)? }; - let b = unsafe { node_ref(&b)? }; - if !Arc::ptr_eq(&a.project, &b.project) { - return Err(Error::Invalid); - } - let mut guard = lock(&a.project); - if guard.graph.unlink(a.id, b.id) { - Ok(()) - } else { - Err(Error::Failed("not linked".to_string())) - } - }) - } - - /// `oaknode_block_are_linked`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_are_linked( - a: CHandle, - b: CHandle, - linked: *mut c_int, - ) -> c_int { - guard(|| { - if linked.is_null() { - return Err(Error::Invalid); - } - let a = unsafe { node_ref(&a)? }; - let b = unsafe { node_ref(&b)? }; - let guard = lock(&a.project); - let linked_val = guard.graph.are_linked(a.id, b.id); - unsafe { *linked = linked_val as c_int }; - Ok(()) - }) - } - - /// `oaknode_block_get_link_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_get_link_count( - block: CHandle, - count: *mut c_int, - ) -> c_int { - guard(|| { - if count.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&block)? }; - let guard = lock(&n.project); - let links = guard.graph.links_of(n.id); - unsafe { *count = links.len() as c_int }; - Ok(()) - }) - } - - /// `oaknode_block_get_link_at`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_block_get_link_at( - block: CHandle, - index: c_int, - out: *mut CHandle, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&block)? }; - if index < 0 { - return Err(Error::NotFound); - } - let guard = lock(&n.project); - let links = guard.graph.links_of(n.id); - let target = links.get(index as usize).copied().ok_or(Error::NotFound)?; - unsafe { *out = make_node_borrowed(n.project.clone(), target) }; - Ok(()) - }) - } - - // ---- Clip --------------------------------------------------------- - - /// `oaknode_clip_get_media_in`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_clip_get_media_in( - clip: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - guard(|| { - if numerator.is_null() || denominator.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&clip)? }; - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - let core = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .map(|c| &c.core) - .ok_or(Error::State)?; - unsafe { - *numerator = core.media_in.numerator() as c_int; - *denominator = core.media_in.denominator() as c_int; - } - Ok(()) - }) - } - - /// `oaknode_clip_set_media_in`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_clip_set_media_in( - clip: CHandle, - numerator: c_int, - denominator: c_int, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&clip)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let core = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .map(|c| &mut c.core) - .ok_or(Error::State)?; - core.media_in = oakcore_rs::Rational::new(numerator as i64, denominator as i64); - Ok(()) - }) - } - - /// `oaknode_clip_get_speed`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_clip_get_speed(clip: CHandle, speed: *mut f64) -> c_int { - guard(|| { - if speed.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&clip)? }; - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - let core = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .map(|c| &c.core) - .ok_or(Error::State)?; - unsafe { *speed = core.speed }; - Ok(()) - }) - } - - /// `oaknode_clip_set_speed`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_clip_set_speed(clip: CHandle, speed: f64) -> c_int { - guard(|| { - let n = unsafe { node_ref(&clip)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let core = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .map(|c| &mut c.core) - .ok_or(Error::State)?; - core.speed = speed; - Ok(()) - }) - } - - /// `oaknode_clip_get_reverse`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_clip_get_reverse(clip: CHandle, reverse: *mut c_int) -> c_int { - guard(|| { - if reverse.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&clip)? }; - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - let core = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .map(|c| &c.core) - .ok_or(Error::State)?; - unsafe { *reverse = core.reversed as c_int }; - Ok(()) - }) - } - - /// `oaknode_clip_set_reverse`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_clip_set_reverse(clip: CHandle, reverse: c_int) -> c_int { - guard(|| { - let n = unsafe { node_ref(&clip)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let core = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .map(|c| &mut c.core) - .ok_or(Error::State)?; - core.reversed = reverse != 0; - Ok(()) - }) - } - - /// `oaknode_clip_get_maintain_audio_pitch`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_clip_get_maintain_audio_pitch( - clip: CHandle, - maintain: *mut c_int, - ) -> c_int { - guard(|| { - if maintain.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&clip)? }; - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - let core = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .map(|c| &c.core) - .ok_or(Error::State)?; - unsafe { *maintain = core.maintain_audio_pitch as c_int }; - Ok(()) - }) - } - - /// `oaknode_clip_set_maintain_audio_pitch`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_clip_set_maintain_audio_pitch( - clip: CHandle, - maintain: c_int, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&clip)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let core = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .map(|c| &mut c.core) - .ok_or(Error::State)?; - core.maintain_audio_pitch = maintain != 0; - Ok(()) - }) - } - - /// `oaknode_clip_get_loop_mode`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_clip_get_loop_mode( - clip: CHandle, - loop_mode: *mut c_int, - ) -> c_int { - guard(|| { - if loop_mode.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&clip)? }; - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - let core = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .map(|c| &c.core) - .ok_or(Error::State)?; - unsafe { *loop_mode = core.loop_mode }; - Ok(()) - }) - } - - /// `oaknode_clip_set_loop_mode`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_clip_set_loop_mode(clip: CHandle, loop_mode: c_int) -> c_int { - guard(|| { - let n = unsafe { node_ref(&clip)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let core = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .map(|c| &mut c.core) - .ok_or(Error::State)?; - core.loop_mode = loop_mode; - Ok(()) - }) - } - - /// `oaknode_clip_get_track_type`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_clip_get_track_type( - clip: CHandle, - type_: *mut c_int, - ) -> c_int { - guard(|| { - if type_.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&clip)? }; - let kind = { - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - let core = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .map(|c| &c.core) - .ok_or(Error::State)?; - let track = core.track; - track - .and_then(|t| guard.graph.get(t)) - .and_then(|te| te.behavior.as_any()) - .and_then(|a| a.downcast_ref::()) - .map(|tb| tb.kind.to_c()) - .unwrap_or(-1) - }; - unsafe { *type_ = kind }; - Ok(()) - }) - } - - // ---- Transition ---------------------------------------------------- - - /// `oaknode_transition_get_in_offset`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_transition_get_in_offset( - transition: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - guard(|| { - if numerator.is_null() || denominator.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&transition)? }; - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - let t = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - unsafe { - *numerator = t.in_offset.numerator() as c_int; - *denominator = t.in_offset.denominator() as c_int; - } - Ok(()) - }) - } - - /// `oaknode_transition_get_out_offset`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_transition_get_out_offset( - transition: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - guard(|| { - if numerator.is_null() || denominator.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&transition)? }; - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - let t = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - unsafe { - *numerator = t.out_offset.numerator() as c_int; - *denominator = t.out_offset.denominator() as c_int; - } - Ok(()) - }) - } - - /// `oaknode_transition_get_offset_center`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_transition_get_offset_center( - transition: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, - ) -> c_int { - guard(|| { - if numerator.is_null() || denominator.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&transition)? }; - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - let t = entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .ok_or(Error::State)?; - // Center = (length - in - out) / 2 from the in edge. - let length = t.core.length(); - let center = (length - t.in_offset - t.out_offset) / oakcore_rs::Rational::new(2, 1); - unsafe { - *numerator = center.numerator() as c_int; - *denominator = center.denominator() as c_int; - } - Ok(()) - }) - } - - /// `oaknode_transition_set_offset_center`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_transition_set_offset_center( - transition: CHandle, - numerator: c_int, - denominator: c_int, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&transition)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let t = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - let center = oakcore_rs::Rational::new(numerator as i64, denominator as i64); - let length = t.core.length(); - let remaining = (length - center * oakcore_rs::Rational::new(2, 1)) - / oakcore_rs::Rational::new(2, 1); - t.in_offset = remaining; - t.out_offset = remaining; - Ok(()) - }) - } - - /// `oaknode_transition_set_offsets_and_length`. - #[no_mangle] - pub unsafe extern "C" 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 { - guard(|| { - let n = unsafe { node_ref(&transition)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let t = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - t.in_offset = oakcore_rs::Rational::new(in_num as i64, in_den as i64); - t.out_offset = oakcore_rs::Rational::new(out_num as i64, out_den as i64); - t.core.set_length_and_media_out(t.in_offset + t.out_offset); - Ok(()) - }) - } - - /// `oaknode_transition_is_dual`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_transition_is_dual( - transition: CHandle, - dual: *mut c_int, - ) -> c_int { - guard(|| { - if dual.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&transition)? }; - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - if entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .is_none() - { - return Err(Error::State); - } - let both = - guard - .graph - .is_input_connected(n.id, crate::block::transition_input::OUT_BLOCK, -1) - && guard.graph.is_input_connected( - n.id, - crate::block::transition_input::IN_BLOCK, - -1, - ); - unsafe { *dual = both as c_int }; - Ok(()) - }) - } - - /// `oaknode_transition_get_connected_out_block`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_transition_get_connected_out_block( - transition: CHandle, - out: *mut CHandle, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&transition)? }; - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - if entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .is_none() - { - return Err(Error::State); - } - let connected = - guard - .graph - .connected_output(n.id, crate::block::transition_input::OUT_BLOCK, -1); - unsafe { - *out = match connected { - Some(b) => make_node_borrowed(n.project.clone(), b), - None => CHandle::null(), - }; - } - Ok(()) - }) - } - - /// `oaknode_transition_get_connected_in_block`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_transition_get_connected_in_block( - transition: CHandle, - out: *mut CHandle, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&transition)? }; - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - if entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .is_none() - { - return Err(Error::State); - } - let connected = - guard - .graph - .connected_output(n.id, crate::block::transition_input::IN_BLOCK, -1); - unsafe { - *out = match connected { - Some(b) => make_node_borrowed(n.project.clone(), b), - None => CHandle::null(), - }; - } - Ok(()) - }) - } - - /// `oaknode_clip_add_cache_passthrough_from` — the render cache - /// handles are created per node at cache-init time (Phase 2 - /// follow-up with bridge::render); the passthrough copies UUIDs, - /// which is a no-op for uncreated caches. - #[no_mangle] - pub unsafe extern "C" fn oaknode_clip_add_cache_passthrough_from( - clip: CHandle, - other: CHandle, - ) -> c_int { - guard(|| { - let _c = unsafe { node_ref(&clip)? }; - let _o = unsafe { node_ref(&other)? }; - // Cache UUID passthrough is inert until the oakrender bridge - // creates per-node caches; the call is accepted as a no-op - // (matching the C++ flow where caches may be null). - Ok(()) - }) - } -} - -// ===================================================================== -// include/node/footage.h -// ===================================================================== - -/// `include/node/footage.h` exports. Footage handles are node handles -/// whose nodes carry [`crate::footage::FootageBehavior`]. -pub mod footage { - use super::*; - use crate::footage::FootageBehavior; - use crate::node::NodeCore; - - fn footage_of(n: &NodeRef) -> Result { - let guard = lock(&n.project); - let entry = guard.graph.get(n.id).ok_or(Error::NotFound)?; - entry - .behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .map(|f| FootageBehavior { - filename: f.filename.clone(), - streams: f.streams.clone(), - proxy: f.proxy.clone(), - proxy_enabled: f.proxy_enabled, - proxy_state: f.proxy_state, - proxy_video_stream_index: f.proxy_video_stream_index, - proxy_preset_version: f.proxy_preset_version, - timestamp: f.timestamp, - decoder: f.decoder.clone(), - valid: f.valid, - cancel: f.cancel.clone(), - }) - .ok_or(Error::State) - } - - /// `oaknode_footage_create` (borrowed; node added to the project). - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_create( - project: CHandle, - filename: *const c_char, - ) -> CHandle { - let p = match unsafe { project_arc(&project) } { - Ok(p) => p, - Err(_) => return CHandle::null(), - }; - let filename = unsafe { cstr(filename) }.unwrap_or("").to_string(); - let id = { - let mut guard = lock(&p); - guard - .graph - .add_node(NodeCore::new(), Box::new(FootageBehavior::new(&filename))) - }; - make_node_borrowed(p, id) - } - - /// `oaknode_footage_as_node`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_as_node(footage: CHandle) -> CHandle { - match unsafe { node_ref(&footage) } { - Ok(n) => make_node_borrowed(n.project.clone(), n.id), - Err(_) => CHandle::null(), - } - } - - /// `oaknode_footage_filename` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_filename( - footage: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let n = match unsafe { node_ref(&footage) } { - Ok(n) => n, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - match footage_of(&n) { - Ok(f) => copy_string_out(&f.filename, buf, buf_size), - Err(e) => e.code(), - } - } - - /// `oaknode_footage_set_filename`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_set_filename( - footage: CHandle, - filename: *const c_char, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&footage)? }; - let filename = unsafe { cstr(filename) }.ok_or(Error::Invalid)?; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let f = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - f.filename = filename.to_string(); - Ok(()) - }) - } - - /// `oaknode_footage_is_valid`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_is_valid(footage: CHandle) -> c_int { - match unsafe { node_ref(&footage) } { - Ok(n) => match footage_of(&n) { - Ok(f) => f.valid as c_int, - Err(e) => e.code(), - }, - Err(_) => crate::error::OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_timestamp`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_timestamp( - footage: CHandle, - out_timestamp: *mut i64, - ) -> c_int { - guard(|| { - if out_timestamp.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&footage)? }; - let f = footage_of(&n)?; - unsafe { *out_timestamp = f.timestamp }; - Ok(()) - }) - } - - /// `oaknode_footage_set_timestamp`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_set_timestamp( - footage: CHandle, - timestamp: i64, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&footage)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let f = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - f.timestamp = timestamp; - Ok(()) - }) - } - - /// `oaknode_footage_decoder` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_decoder( - footage: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let n = match unsafe { node_ref(&footage) } { - Ok(n) => n, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - match footage_of(&n) { - Ok(f) => copy_string_out(&f.decoder, buf, buf_size), - Err(e) => e.code(), - } - } - - /// `oaknode_footage_total_stream_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_total_stream_count(footage: CHandle) -> c_int { - match unsafe { node_ref(&footage) } { - Ok(n) => match footage_of(&n) { - Ok(f) => f.total_stream_count() as c_int, - Err(e) => e.code(), - }, - Err(_) => crate::error::OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_video_stream_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_video_stream_count(footage: CHandle) -> c_int { - match unsafe { node_ref(&footage) } { - Ok(n) => match footage_of(&n) { - Ok(f) => f.video_stream_count() as c_int, - Err(e) => e.code(), - }, - Err(_) => crate::error::OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_audio_stream_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_audio_stream_count(footage: CHandle) -> c_int { - match unsafe { node_ref(&footage) } { - Ok(n) => match footage_of(&n) { - Ok(f) => f.audio_stream_count() as c_int, - Err(e) => e.code(), - }, - Err(_) => crate::error::OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_subtitle_stream_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_subtitle_stream_count(footage: CHandle) -> c_int { - match unsafe { node_ref(&footage) } { - Ok(n) => match footage_of(&n) { - Ok(f) => f.subtitle_stream_count() as c_int, - Err(e) => e.code(), - }, - Err(_) => crate::error::OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_duration`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_duration( - footage: CHandle, - out_numerator: *mut c_int, - out_denominator: *mut c_int, - ) -> c_int { - guard(|| { - if out_numerator.is_null() || out_denominator.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&footage)? }; - let f = footage_of(&n)?; - let d = f.duration(); - unsafe { - *out_numerator = d.numerator() as c_int; - *out_denominator = d.denominator() as c_int; - } - Ok(()) - }) - } - - /// `oaknode_footage_proxy_enabled`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_proxy_enabled(footage: CHandle) -> c_int { - match unsafe { node_ref(&footage) } { - Ok(n) => match footage_of(&n) { - Ok(f) => f.proxy_enabled as c_int, - Err(e) => e.code(), - }, - Err(_) => crate::error::OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_set_proxy_enabled`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_set_proxy_enabled( - footage: CHandle, - enabled: c_int, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&footage)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let f = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - f.proxy_enabled = enabled != 0; - Ok(()) - }) - } - - /// `oaknode_footage_proxy_path` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_proxy_path( - footage: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let n = match unsafe { node_ref(&footage) } { - Ok(n) => n, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - match footage_of(&n) { - Ok(f) => copy_string_out(&f.proxy, buf, buf_size), - Err(e) => e.code(), - } - } - - /// `oaknode_footage_proxy_state`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_proxy_state(footage: CHandle) -> c_int { - match unsafe { node_ref(&footage) } { - Ok(n) => match footage_of(&n) { - Ok(f) => f.proxy_state, - Err(e) => e.code(), - }, - Err(_) => crate::error::OAKNODE_E_INVALID, - } - } - - /// `oaknode_footage_set_proxy`. - #[no_mangle] - pub unsafe extern "C" 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 { - guard(|| { - let n = unsafe { node_ref(&footage)? }; - let path = if path.is_null() { - "" - } else { - unsafe { cstr(path) }.ok_or(Error::Invalid)? - }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let f = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - f.set_proxy( - path, - state, - video_stream_index, - preset_version, - enabled != 0, - ); - Ok(()) - }) - } - - /// `oaknode_footage_clear_proxy`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_clear_proxy(footage: CHandle) -> c_int { - guard(|| { - let n = unsafe { node_ref(&footage)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let f = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - f.clear_proxy(); - Ok(()) - }) - } - - /// `oaknode_footage_get_video_params`: the `index`th video stream's - /// params as a NEW owned oakcommon handle - /// (`// CPP-PARITY: src/node/c_api/footage.cpp:327`). - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_get_video_params( - footage: CHandle, - index: c_int, - out: *mut std::ffi::c_void, - ) -> c_int { - guard(|| { - if out.is_null() || index < 0 { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&footage)? }; - let params = with_graph_read(&n.project, |g| { - g.get(n.id).and_then(|e| { - e.behavior - .as_any() - .and_then(|a| a.downcast_ref::()) - .and_then(|f| f.video_params(index as usize)) - }) - }) - .ok_or(Error::NotFound)?; - let handle = super::videoparams_handle_from(¶ms).ok_or(Error::NoMem)?; - unsafe { *(out as *mut crate::handle::CHandle) = handle }; - Ok(()) - }) - } - - /// `oaknode_footage_set_video_params`: replace the `index`th video - /// stream's params from an oakcommon handle - /// (`// CPP-PARITY: footage.cpp:348`). `OAKNODE_E_NOT_FOUND` when the - /// footage has no such video stream (the C++ writes into the params - /// input array regardless; documented divergence). - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_set_video_params( - footage: CHandle, - index: c_int, - params: *const std::ffi::c_void, - ) -> c_int { - guard(|| { - if params.is_null() { - return Err(Error::Invalid); - } - let handle = unsafe { (*(params as *const crate::handle::CHandle)).clone() }; - if handle.ctx.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&footage)? }; - let read = super::videoparams_from_handle(handle).ok_or(Error::Invalid)?; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let f = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - let mut seen = 0usize; - for stream in f.streams.iter_mut().filter(|s| s.is_video) { - if seen == index as usize { - stream.video = Some(read); - return Ok(()); - } - seen += 1; - } - Err(Error::NotFound) - }) - } - - /// `oaknode_footage_get_video_length`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_get_video_length( - footage: CHandle, - out_num: *mut i64, - out_den: *mut i64, - ) -> c_int { - guard(|| { - if out_num.is_null() || out_den.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&footage)? }; - let f = footage_of(&n)?; - let len = f.video_length(); - unsafe { - *out_num = len.numerator(); - *out_den = len.denominator(); - } - Ok(()) - }) - } - - /// `oaknode_footage_set_cancel_atom`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_footage_set_cancel_atom( - footage: CHandle, - atom: CHandle, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&footage)? }; - let mut guard = lock(&n.project); - let entry = guard.graph.get_mut(n.id).ok_or(Error::NotFound)?; - let f = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - .ok_or(Error::State)?; - // The cancel atom is an opaque oakrender handle; the shared - // cancellation flag is set when it is present. - f.set_cancel(!atom.ctx.is_null()); - Ok(()) - }) - } -} - -// ===================================================================== -// include/node/colormanager.h -// ===================================================================== - -/// `include/node/colormanager.h` exports. The manager handle boxes a -/// [`crate::colormanager::ColorManager`] directly (not a graph node). -/// Config-dependent queries report `E_STATE` until a config is loaded; -/// the listings reflect the manager's state (without liboakrender the -/// lists hold the built-in defaults). -pub mod colormanager { - use super::*; - use crate::colormanager::ColorManager; - - /// `oaknode_colormanager_init` (owned manager bound to a project). - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_init(project: CHandle) -> CHandle { - let _ = unsafe { project_arc(&project) }; - // The manager is process/project state; the project binding is - // recorded but the manager object is standalone and shared - // through a mutex. - crate::handle::make_owned(std::sync::Mutex::new(ColorManager::new())) - } - - /// `oaknode_colormanager_free`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_free(manager: *mut CHandle) { - if manager.is_null() || unsafe { (*manager).ctx.is_null() } { - return; - } - let h = unsafe { (*manager).clone() }; - if let Some(f) = h.release { - unsafe { f(h.ctx) }; - } - unsafe { (*manager).ctx = std::ptr::null_mut() }; - } - - /// `oaknode_colormanager_wrap_borrowed` — the native manager pointer - /// is not a Rust object; the borrowed box wraps a placeholder. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_wrap_borrowed( - native_manager: *mut std::ffi::c_void, - ) -> CHandle { - if native_manager.is_null() { - return CHandle::null(); - } - crate::handle::make_owned(ColorManager::new()) - } - - /// `oaknode_colormanager_initialize`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_initialize(manager: CHandle) -> c_int { - guard(|| { - let mut guard = manager_guard(&manager)?; - guard.initialize() - }) - } - - /// `oaknode_colormanager_set_up_default_config`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_set_up_default_config() -> c_int { - guard(|| { - // The process-wide default config state lives with the - // managers; nothing global to do without oakrender. - Ok(()) - }) - } - - /// `oaknode_colormanager_get_config_filename`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_get_config_filename( - manager: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match manager_string(&manager, |m| m.config_filename.clone()) { - Ok(s) => copy_string_out(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// `oaknode_colormanager_set_config_filename`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_set_config_filename( - manager: CHandle, - filename: *const c_char, - ) -> c_int { - guard(|| { - let filename = unsafe { cstr(filename) }.ok_or(Error::Invalid)?.to_string(); - manager_mut(&manager, |m| m.config_filename = filename) - }) - } - - /// `oaknode_colormanager_update_config_from_filename`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_update_config_from_filename( - manager: CHandle, - ) -> c_int { - guard(|| { - let mut guard = manager_guard(&manager)?; - guard.update_config_from_filename() - }) - } - - /// `oaknode_colormanager_get_default_input_color_space`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_get_default_input_color_space( - manager: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match manager_string(&manager, |m| m.default_input_space.clone()) { - Ok(s) => copy_string_out(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// `oaknode_colormanager_set_default_input_color_space`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_set_default_input_color_space( - manager: CHandle, - colorspace: *const c_char, - ) -> c_int { - guard(|| { - let colorspace = unsafe { cstr(colorspace) } - .ok_or(Error::Invalid)? - .to_string(); - manager_mut(&manager, |m| m.default_input_space = colorspace) - }) - } - - /// `oaknode_colormanager_get_reference_color_space`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_get_reference_color_space( - manager: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match manager_string(&manager, |m| m.reference_space.clone()) { - Ok(s) => copy_string_out(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// `oaknode_colormanager_get_compliant_color_space` — requires a - /// loaded config. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_get_compliant_color_space( - manager: CHandle, - colorspace: *const c_char, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match manager_string(&manager, |m| { - if !m.config_loaded { - return String::new(); - } - let colorspace = unsafe { cstr(colorspace) }.unwrap_or(""); - if m.list_colorspaces().iter().any(|c| c == colorspace) { - colorspace.to_string() - } else { - m.default_input_space.clone() - } - }) { - Ok(s) => { - if s.is_empty() { - crate::error::OAKNODE_E_STATE - } else { - copy_string_out(&s, buf, buf_size) - } - } - Err(e) => e.code(), - } - } - - /// `oaknode_colormanager_get_colorspace_for_ffmpeg_tags` — the tag - /// mapping requires OCIO (E_STATE without a loaded config). - #[no_mangle] - pub unsafe extern "C" 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 { - let loaded = manager_string(&manager, |m| m.config_loaded.to_string()); - match loaded { - Ok(s) if s == "true" => { - // Unknown tags -> default input (empty result size 1). - match manager_string(&manager, |m| m.default_input_space.clone()) { - Ok(space) => copy_string_out(&space, buf, buf_size), - Err(e) => e.code(), - } - } - _ => crate::error::OAKNODE_E_STATE, - } - } - - /// `oaknode_colormanager_get_display_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_get_display_count( - manager: CHandle, - count: *mut c_int, - ) -> c_int { - guard(|| { - if count.is_null() { - return Err(Error::Invalid); - } - let guard = manager_guard(&manager)?; - if !guard.config_loaded { - return Err(Error::State); - } - unsafe { *count = guard.list_displays().len() as c_int }; - Ok(()) - }) - } - - /// `oaknode_colormanager_get_display_at`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_get_display_at( - manager: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match manager_string(&manager, |m| { - if !m.config_loaded { - return String::new(); - } - m.list_displays() - .get(index as usize) - .cloned() - .unwrap_or_default() - }) { - Ok(s) if s.is_empty() => crate::error::OAKNODE_E_NOT_FOUND, - Ok(s) => copy_string_out(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// `oaknode_colormanager_get_default_display`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_get_default_display( - manager: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match manager_string(&manager, |m| { - if m.config_loaded { - m.default_display.clone() - } else { - String::new() - } - }) { - Ok(s) if s.is_empty() => crate::error::OAKNODE_E_STATE, - Ok(s) => copy_string_out(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// `oaknode_colormanager_get_view_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_get_view_count( - manager: CHandle, - _display: *const c_char, - count: *mut c_int, - ) -> c_int { - guard(|| { - if count.is_null() { - return Err(Error::Invalid); - } - let guard = manager_guard(&manager)?; - if !guard.config_loaded { - return Err(Error::State); - } - unsafe { *count = guard.list_views("").len() as c_int }; - Ok(()) - }) - } - - /// `oaknode_colormanager_get_view_at`. - #[no_mangle] - pub unsafe extern "C" 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 { - match manager_string(&manager, |m| { - if !m.config_loaded { - return String::new(); - } - m.list_views("") - .get(index as usize) - .cloned() - .unwrap_or_default() - }) { - Ok(s) if s.is_empty() => crate::error::OAKNODE_E_NOT_FOUND, - Ok(s) => copy_string_out(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// `oaknode_colormanager_get_default_view`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_get_default_view( - manager: CHandle, - _display: *const c_char, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match manager_string(&manager, |m| { - if m.config_loaded { - m.default_view.clone() - } else { - String::new() - } - }) { - Ok(s) if s.is_empty() => crate::error::OAKNODE_E_STATE, - Ok(s) => copy_string_out(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// `oaknode_colormanager_get_look_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_get_look_count( - manager: CHandle, - count: *mut c_int, - ) -> c_int { - guard(|| { - if count.is_null() { - return Err(Error::Invalid); - } - let guard = manager_guard(&manager)?; - if !guard.config_loaded { - return Err(Error::State); - } - unsafe { *count = guard.list_looks().len() as c_int }; - Ok(()) - }) - } - - /// `oaknode_colormanager_get_look_at`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_get_look_at( - manager: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match manager_string(&manager, |m| { - if !m.config_loaded { - return String::new(); - } - m.list_looks() - .get(index as usize) - .cloned() - .unwrap_or_default() - }) { - Ok(s) if s.is_empty() => crate::error::OAKNODE_E_NOT_FOUND, - Ok(s) => copy_string_out(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// `oaknode_colormanager_get_colorspace_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_get_colorspace_count( - manager: CHandle, - count: *mut c_int, - ) -> c_int { - guard(|| { - if count.is_null() { - return Err(Error::Invalid); - } - let guard = manager_guard(&manager)?; - if !guard.config_loaded { - return Err(Error::State); - } - unsafe { *count = guard.list_colorspaces().len() as c_int }; - Ok(()) - }) - } - - /// `oaknode_colormanager_get_colorspace_at`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_get_colorspace_at( - manager: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - match manager_string(&manager, |m| { - if !m.config_loaded { - return String::new(); - } - m.list_colorspaces() - .get(index as usize) - .cloned() - .unwrap_or_default() - }) { - Ok(s) if s.is_empty() => crate::error::OAKNODE_E_NOT_FOUND, - Ok(s) => copy_string_out(&s, buf, buf_size), - Err(e) => e.code(), - } - } - - /// `oaknode_colormanager_get_default_luma_coefs` — requires a loaded - /// config; without OCIO the Rec.709 coefficients are returned. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_get_default_luma_coefs( - manager: CHandle, - rgb: *mut f64, - ) -> c_int { - guard(|| { - if rgb.is_null() { - return Err(Error::Invalid); - } - let guard = manager_guard(&manager)?; - if !guard.config_loaded { - return Err(Error::State); - } - unsafe { - *rgb.add(0) = 0.2126; - *rgb.add(1) = 0.7152; - *rgb.add(2) = 0.0722; - } - Ok(()) - }) - } - - /// `oaknode_colormanager_get_compliant_color_transform`: a copy of - /// `transform` whose display/view/look (or output colorspace) is - /// clamped to what the active config offers - /// (`// CPP-PARITY: src/node/c_api/colormanager.cpp:383`, - /// `src/node/src/color/colormanager/colormanager.cpp:241`). Without - /// OCIO the config lists hold the built-in defaults, so the clamp is - /// identity unless a field names something the defaults lack. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_get_compliant_color_transform( - manager: CHandle, - transform: crate::handle::CHandle, - force_display: c_int, - out: *mut crate::handle::CHandle, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let mut guard = manager_guard(&manager)?; - if !guard.is_loaded() { - return Err(Error::State); - } - // Read the transform's fields (E_INVALID when oakcommon is - // unavailable — the C++ `get_native` NULL path). - let is_display = crate::bridge::common::colortransform_is_display(transform.clone()) - .ok_or(Error::Invalid)?; - let (display, view, look, output) = if is_display || force_display != 0 { - ( - crate::bridge::common::colortransform_get_display(transform.clone()) - .ok_or(Error::Invalid)?, - crate::bridge::common::colortransform_get_view(transform.clone()) - .ok_or(Error::Invalid)?, - crate::bridge::common::colortransform_get_look(transform.clone()) - .ok_or(Error::Invalid)?, - String::new(), - ) - } else { - ( - String::new(), - String::new(), - String::new(), - crate::bridge::common::colortransform_get_output(transform.clone()) - .ok_or(Error::Invalid)?, - ) - }; - drop(guard); - // Clamp against the active config (C++ - // `get_compliant_color_space`). - let handle = if is_display || force_display != 0 { - let guard = manager_guard(&manager)?; - let displays = guard.list_displays(); - let display = if displays.contains(&display) { - display - } else { - guard.default_display.clone() - }; - let views = guard.list_views(&display); - let view = if views.contains(&view) { - view - } else { - guard.default_view.clone() - }; - let looks = guard.list_looks(); - let look = if looks.contains(&look) { - look - } else { - String::new() - }; - crate::bridge::common::colortransform_init_display(&display, &view, &look) - } else { - let guard = manager_guard(&manager)?; - let spaces = guard.list_colorspaces(); - let output = if spaces.contains(&output) { - output - } else { - guard.default_input_space.clone() - }; - crate::bridge::common::colortransform_init_output(&output) - }; - let handle = handle.ok_or(Error::NoMem)?; - unsafe { *out = handle }; - Ok(()) - }) - } - - /// Read a manager field through the shared lock. - fn manager_string( - manager: &CHandle, - f: impl FnOnce(&ColorManager) -> String, - ) -> Result { - let guard = manager_guard(manager)?; - Ok(f(&guard)) - } - - /// Mutate a manager field through the shared lock. - fn manager_mut(manager: &CHandle, f: impl FnOnce(&mut ColorManager)) -> Result<()> { - let mut guard = manager_guard(manager)?; - f(&mut guard); - Ok(()) - } - - /// Lock the shared manager box (the handle boxes `Mutex`). - fn manager_guard(manager: &CHandle) -> Result> { - let m = unsafe { crate::handle::get::>(manager) } - .ok_or(Error::Invalid)?; - Ok(m.lock().unwrap_or_else(|e| e.into_inner())) - } - - /// `oaknode_colormanager_get_native`: borrowed access to the - /// underlying C++ manager — a C++-only bridge export with no Rust - /// counterpart. Returns NULL (the contract is NULL-safe); the Rust - /// manager object is reachable only through the handle. - #[no_mangle] - pub unsafe extern "C" fn oaknode_colormanager_get_native( - _manager: CHandle, - ) -> *mut std::ffi::c_void { - std::ptr::null_mut() - } -} - -// ===================================================================== -// include/node/traverser.h -// ===================================================================== - -/// `include/node/traverser.h` exports: database generation over a time -/// range and database row enumeration. The traverser handle boxes a -/// [`crate::traverser::Traverser`]; the database handle boxes a -/// [`crate::traverser::ValueDatabase`]. -pub mod traverser { - use super::*; - use crate::traverser::{EvalRequest, Traverser, ValueDatabase}; - - /// `oaknode_traverser_init`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_traverser_init() -> CHandle { - crate::handle::make_owned(Traverser::new()) - } - - /// `oaknode_traverser_free`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_traverser_free(traverser: *mut CHandle) { - if traverser.is_null() || unsafe { (*traverser).ctx.is_null() } { - return; - } - let h = unsafe { (*traverser).clone() }; - if let Some(f) = h.release { - unsafe { f(h.ctx) }; - } - unsafe { (*traverser).ctx = std::ptr::null_mut() }; - } - - /// `oaknode_traverser_generate_database`: evaluate `node` at the - /// range's in-time and collect its input row + output table into a - /// database keyed by input id (a single-frame database; the C++ - /// walks the whole range — the frame iterator lands with the - /// traverser's range stepping, `// CPP-PARITY: traverser.cpp`). - #[no_mangle] - pub unsafe extern "C" 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 { - guard(|| { - if out_db.is_null() { - return Err(Error::Invalid); - } - let n = unsafe { node_ref(&node)? }; - let _ = (in_num, in_den, out_num, out_den); - let time = oakcore_rs::Rational::new(in_num, in_den); - let mut hooks = NoopHooks; - let table = { - // The handle is an opaque engine token; each pass runs - // on a fresh engine (the pass is stateless). - let _ = - unsafe { crate::handle::get::(&traverser) }.ok_or(Error::Invalid)?; - let guard = lock(&n.project); - let request = EvalRequest::new(n.id, time); - let mut engine = Traverser::new(); - engine.evaluate(&guard.graph, &request, &mut hooks)? - }; - // Build the database: one row per evaluated input value. - let mut rows: Vec<(String, Vec<(crate::value::ValueType, NodeValue)>)> = Vec::new(); - let mut seen: std::collections::HashSet = std::collections::HashSet::new(); - for (from, input_id, _element) in - with_graph_read(&n.project, |g| g.output_connections(n.id)) - { - let _ = from; - if seen.insert(input_id.clone()) { - rows.push((input_id, Vec::new())); - } - } - // The root's output table also feeds the database under the - // "" key (the C++ `NodeValueDatabase` rows are the evaluated - // values per input of the root). - let root_row: Vec<(crate::value::ValueType, NodeValue)> = table - .rows() - .iter() - .map(|(t, v, _)| (*t, v.clone())) - .collect(); - let mut db_rows = rows; - db_rows.push(("".to_string(), root_row)); - let db = ValueDatabase { rows: db_rows }; - unsafe { *out_db = crate::handle::make_owned(db) }; - Ok(()) - }) - } - - /// No-op hooks for offline database generation. - struct NoopHooks; - impl crate::traverser::RenderHooks for NoopHooks {} - - /// `oaknode_traverser_database_free`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_traverser_database_free(db: *mut CHandle) { - if db.is_null() || unsafe { (*db).ctx.is_null() } { - return; - } - let h = unsafe { (*db).clone() }; - if let Some(f) = h.release { - unsafe { f(h.ctx) }; - } - unsafe { (*db).ctx = std::ptr::null_mut() }; - } - - /// `oaknode_traverser_database_row_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_traverser_database_row_count( - db: CHandle, - out_count: *mut c_int, - ) -> c_int { - guard(|| { - if out_count.is_null() { - return Err(Error::Invalid); - } - let d = unsafe { crate::handle::get::(&db) }.ok_or(Error::Invalid)?; - unsafe { *out_count = d.rows.len() as c_int }; - Ok(()) - }) - } - - /// `oaknode_traverser_database_row_key_at` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oaknode_traverser_database_row_key_at( - db: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let d = match unsafe { crate::handle::get::(&db) } { - Some(d) => d, - None => return crate::error::OAKNODE_E_INVALID, - }; - match d.rows.get(index as usize) { - Some((key, _)) => copy_string_out(key, buf, buf_size), - None => crate::error::OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_traverser_database_row_value_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_traverser_database_row_value_count( - db: CHandle, - key: *const c_char, - out_count: *mut c_int, - ) -> c_int { - guard(|| { - if out_count.is_null() { - return Err(Error::Invalid); - } - let d = unsafe { crate::handle::get::(&db) }.ok_or(Error::Invalid)?; - let key = unsafe { cstr(key) }.ok_or(Error::Invalid)?; - let row = d - .rows - .iter() - .find(|(k, _)| k == key) - .ok_or(Error::NotFound)?; - unsafe { *out_count = row.1.len() as c_int }; - Ok(()) - }) - } - - /// `oaknode_traverser_database_value_at`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_traverser_database_value_at( - db: CHandle, - key: *const c_char, - index: c_int, - out: *mut OakNodeValue, - ) -> c_int { - guard(|| { - if out.is_null() { - return Err(Error::Invalid); - } - let d = unsafe { crate::handle::get::(&db) }.ok_or(Error::Invalid)?; - let key = unsafe { cstr(key) }.ok_or(Error::Invalid)?; - let row = d - .rows - .iter() - .find(|(k, _)| k == key) - .ok_or(Error::NotFound)?; - let (ty, value) = row.1.get(index as usize).ok_or(Error::NotFound)?; - let pod = OakNodeValue::from_node_value(*ty, value)?; - unsafe { *out = pod }; - Ok(()) - }) - } - - /// `oaknode_traverser_database_value_string_at`. - #[no_mangle] - pub unsafe extern "C" 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 { - let d = match unsafe { crate::handle::get::(&db) } { - Some(d) => d, - None => return crate::error::OAKNODE_E_INVALID, - }; - let key = match unsafe { cstr(key) } { - Some(k) => k, - None => return crate::error::OAKNODE_E_INVALID, - }; - let row = match d.rows.iter().find(|(k, _)| k == key) { - Some(r) => r, - None => return crate::error::OAKNODE_E_NOT_FOUND, - }; - let (ty, value) = match row.1.get(index as usize) { - Some(v) => v, - None => return crate::error::OAKNODE_E_NOT_FOUND, - }; - let s = crate::serializer::value_to_string(*ty, value, false); - copy_string_out(&s, buf, buf_size) - } -} - -// ===================================================================== -// include/node/serializer.h -// ===================================================================== - -/// `include/node/serializer.h` exports: in-memory node-graph copy/paste -/// via XML. SaveData holds the nodes to serialize + per-node properties; -/// LoadData holds the created nodes + promised connections + properties. -pub mod serializer { - use super::*; - use crate::serializer::{XmlRead, XmlWrite}; - - /// Serializer result codes (include/node/serializer.h). - const SERIALIZER_OK: c_int = 0; - const SERIALIZER_FILE_ERROR: c_int = 4; - const SERIALIZER_XML_ERROR: c_int = 5; - - /// Save descriptor (C++ `ProjectSerializer::SaveData`). - pub struct SaveData { - /// Load type (`OAKNODE_SERIALIZER_LOAD_*`). - pub load_type: c_int, - /// Context project (borrowed). - pub project: Option, - /// Restrict to these nodes (None = all). - pub only_serialize_nodes: Option>, - /// Per-node (key, value) properties. - pub properties: std::collections::HashMap<(NodeId, String), String>, - } - - /// Load result (C++ `ProjectSerializer::LoadData`). - pub struct LoadData { - /// Created nodes (owned by the caller) as (project, id). - pub nodes: Vec<(ProjectArc, NodeId)>, - /// Promised connections (output, input, input_id, element). - pub connections: Vec<(NodeId, NodeId, String, i32)>, - /// Per-node properties. - pub properties: std::collections::HashMap<(NodeId, String), String>, - } - - /// `oaknode_serializer_initialize`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_serializer_initialize() -> c_int { - // The factory registry is lazily built; initializing is a no-op - // that guarantees it exists. - guard(|| { - let _ = crate::factory::Factory::global(); - Ok(()) - }) - } - - /// `oaknode_serializer_shutdown` — no-op (nothing global to release). - #[no_mangle] - pub unsafe extern "C" fn oaknode_serializer_shutdown() {} - - /// `oaknode_serializer_savedata_create`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_serializer_savedata_create( - load_type: c_int, - project: CHandle, - ) -> CHandle { - let project = unsafe { project_arc(&project) }.ok(); - crate::handle::make_owned(SaveData { - load_type, - project, - only_serialize_nodes: None, - properties: std::collections::HashMap::new(), - }) - } - - /// `oaknode_serializer_savedata_free`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_serializer_savedata_free(save_data: *mut CHandle) { - if save_data.is_null() || unsafe { (*save_data).ctx.is_null() } { - return; - } - let h = unsafe { (*save_data).clone() }; - if let Some(f) = h.release { - unsafe { f(h.ctx) }; - } - unsafe { (*save_data).ctx = std::ptr::null_mut() }; - } - - /// `oaknode_serializer_savedata_set_nodes`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_serializer_savedata_set_nodes( - save_data: CHandle, - nodes: *const CHandle, - count: c_int, - ) -> c_int { - guard(|| { - if nodes.is_null() || count < 0 { - return Err(Error::Invalid); - } - let sd = unsafe { crate::handle::get::(&save_data) }.ok_or(Error::Invalid)?; - let mut ids = Vec::new(); - for i in 0..count as usize { - let child = unsafe { (*nodes.add(i)).clone() }; - let n = unsafe { node_ref(&child)? }; - ids.push(n.id); - } - // The box is shared; mutate through the raw pointer. - unsafe { - let rb = save_data.ctx as *mut crate::handle::RefBox; - (*rb).value.only_serialize_nodes = Some(ids); - } - Ok(()) - }) - } - - /// `oaknode_serializer_savedata_set_property`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_serializer_savedata_set_property( - save_data: CHandle, - node: CHandle, - key: *const c_char, - value: *const c_char, - ) -> c_int { - guard(|| { - let n = unsafe { node_ref(&node)? }; - let key = unsafe { cstr(key) }.ok_or(Error::Invalid)?; - let value = unsafe { cstr(value) }.ok_or(Error::Invalid)?; - unsafe { - let rb = save_data.ctx as *mut crate::handle::RefBox; - (*rb) - .value - .properties - .insert((n.id, key.to_string()), value.to_string()); - } - Ok(()) - }) - } - - /// `oaknode_serializer_save_to_xml` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oaknode_serializer_save_to_xml( - save_data: CHandle, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let sd = match unsafe { crate::handle::get::(&save_data) } { - Some(sd) => sd, - None => return crate::error::OAKNODE_E_INVALID, - }; - let xml = match save_to_xml_inner(sd) { - Ok(x) => x, - Err(e) => return e.code(), - }; - copy_string_out(&xml, buf, buf_size) - } - - /// Serialize the save data to an XML string. - fn save_to_xml_inner(sd: &SaveData) -> Result { - let project = sd.project.as_ref().ok_or(Error::State)?; - let mut writer = crate::serializer::XmlWriterBridge::new().ok_or(Error::Failed( - "oakcommon XML writer unavailable".to_string(), - ))?; - writer.start_element("project"); - writer.attribute("version", "1"); - - let guard = lock(project); - writer.start_element("nodes"); - let ids: Vec = match &sd.only_serialize_nodes { - Some(list) => list.clone(), - None => guard.graph.node_ids(), - }; - for id in ids { - let entry = guard.graph.get(id).ok_or(Error::NotFound)?; - writer.start_element("node"); - let type_id = entry.behavior.type_id().to_string(); - let connections: Vec<(NodeId, String, i32)> = guard - .graph - .output_connections_all() - .into_iter() - .filter(|(_, to, _, _)| *to == id) - .map(|(from, _, input, element)| (from, input, element)) - .collect(); - crate::serializer::save_node( - &mut writer, - &entry.core, - &*entry.behavior, - id, - &type_id, - &connections, - )?; - // Serialize per-node properties as text elements. - let mut props: Vec<(&String, &String)> = sd - .properties - .iter() - .filter(|((n, _), _)| *n == id) - .map(|((_, k), v)| (k, v)) - .collect(); - props.sort_by(|a, b| a.0.cmp(b.0)); - if !props.is_empty() { - writer.start_element("properties"); - for (k, v) in props { - writer.text_element(k, v); - } - writer.end_element(); // properties - } - writer.end_element(); // node - } - writer.end_element(); // nodes - writer.end_element(); // project - Ok(writer.output()) - } - - /// `oaknode_serializer_load_from_xml`. - #[no_mangle] - pub unsafe extern "C" 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 { - guard(|| { - if out_result.is_null() { - return Err(Error::Invalid); - } - let xml = unsafe { cstr(xml) }.ok_or(Error::Invalid)?; - let load_data = match load_from_xml_inner(xml) { - Ok(ld) => ld, - Err(_) => { - unsafe { *out_result = SERIALIZER_XML_ERROR }; - if !out_load_data.is_null() { - unsafe { *out_load_data = CHandle::null() }; - } - if !details_buf.is_null() && details_buf_size > 0 { - unsafe { *details_buf = 0 }; - } - return Ok(()); - } - }; - unsafe { *out_result = SERIALIZER_OK }; - if !out_load_data.is_null() { - unsafe { *out_load_data = crate::handle::make_owned(load_data) }; - } - Ok(()) - }) - } - - /// Parse XML into a [`LoadData`] (nodes created in fresh scratch - /// projects, owned by the caller). - fn load_from_xml_inner(xml: &str) -> Result { - let project = crate::serializer::load(xml)?; - let guard = lock(&project); - let mut nodes = Vec::new(); - for id in guard.graph.node_ids() { - nodes.push((project.clone(), id)); - } - Ok(LoadData { - nodes, - connections: Vec::new(), - properties: std::collections::HashMap::new(), - }) - } - - /// `oaknode_serializer_loaddata_free`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_serializer_loaddata_free(load_data: *mut CHandle) { - if load_data.is_null() || unsafe { (*load_data).ctx.is_null() } { - return; - } - let h = unsafe { (*load_data).clone() }; - if let Some(f) = h.release { - unsafe { f(h.ctx) }; - } - unsafe { (*load_data).ctx = std::ptr::null_mut() }; - } - - /// `oaknode_serializer_loaddata_node_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_serializer_loaddata_node_count(load_data: CHandle) -> c_int { - match unsafe { crate::handle::get::(&load_data) } { - Some(ld) => ld.nodes.len() as c_int, - None => crate::error::OAKNODE_E_INVALID, - } - } - - /// `oaknode_serializer_loaddata_node_at` (borrowed). - #[no_mangle] - pub unsafe extern "C" fn oaknode_serializer_loaddata_node_at( - load_data: CHandle, - index: c_int, - ) -> CHandle { - let ld = match unsafe { crate::handle::get::(&load_data) } { - Some(ld) => ld, - None => return CHandle::null(), - }; - match ld.nodes.get(index as usize) { - Some((project, id)) => make_node_borrowed(project.clone(), *id), - None => CHandle::null(), - } - } - - /// `oaknode_serializer_loaddata_get_property` (two-stage). - #[no_mangle] - pub unsafe extern "C" 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 { - let ld = match unsafe { crate::handle::get::(&load_data) } { - Some(ld) => ld, - None => return crate::error::OAKNODE_E_INVALID, - }; - let n = match unsafe { node_ref(&node) } { - Ok(n) => n, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let key = match unsafe { cstr(key) } { - Some(k) => k, - None => return crate::error::OAKNODE_E_INVALID, - }; - match ld.properties.get(&(n.id, key.to_string())) { - Some(v) => copy_string_out(v, buf, buf_size), - None => crate::error::OAKNODE_E_NOT_FOUND, - } - } - - /// `oaknode_serializer_loaddata_connection_count`. - #[no_mangle] - pub unsafe extern "C" fn oaknode_serializer_loaddata_connection_count( - load_data: CHandle, - ) -> c_int { - match unsafe { crate::handle::get::(&load_data) } { - Some(ld) => ld.connections.len() as c_int, - None => crate::error::OAKNODE_E_INVALID, - } - } - - /// `oaknode_serializer_loaddata_connection_at`. - #[no_mangle] - pub unsafe extern "C" 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 { - guard(|| { - if out_output_node.is_null() || out_input_node.is_null() || out_element.is_null() { - return Err(Error::Invalid); - } - let ld = unsafe { crate::handle::get::(&load_data) }.ok_or(Error::Invalid)?; - let (out_id, in_id, input_id, element) = - ld.connections.get(index as usize).ok_or(Error::NotFound)?; - // Resolve node handles from the load-data's nodes. - let out = ld - .nodes - .iter() - .find(|(_, id)| id == out_id) - .map(|(p, id)| make_node_borrowed(p.clone(), *id)) - .unwrap_or_else(CHandle::null); - let inn = ld - .nodes - .iter() - .find(|(_, id)| id == in_id) - .map(|(p, id)| make_node_borrowed(p.clone(), *id)) - .unwrap_or_else(CHandle::null); - unsafe { - *out_output_node = out; - *out_input_node = inn; - *out_element = *element; - } - if !input_id_buf.is_null() { - copy_string_out(input_id, input_id_buf, input_id_buf_size); - } - Ok(()) - }) - } - - /// Report a serializer failure the C++ `report_serializer_result` way: - /// write the details string (two-stage) and return the required size - /// when a buffer was provided, else `OAKNODE_E_FAILED` - /// (`// CPP-PARITY: src/node/c_api/serializer.cpp`). - fn report_serializer_failure(msg: &str, details: *mut c_char, details_size: c_int) -> c_int { - let needed = msg.len() + 1; - if !details.is_null() && details_size > 0 { - copy_string_out(msg, details, details_size); - needed as c_int - } else { - crate::error::OAKNODE_E_FAILED - } - } - - /// `oaknode_serializer_save_to_file`: serialize `project` to XML and - /// write it to `filename`. OVEC compression (`use_compression`) is - /// not supported — always plain XML. Result code semantics per - /// include/node/serializer.h (`// CPP-PARITY: serializer.cpp:394`). - #[no_mangle] - pub unsafe extern "C" 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 { - let p = match unsafe { project_arc(&project) } { - Ok(p) => p, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let filename = match unsafe { cstr(filename) } { - Some(s) => s, - None => return crate::error::OAKNODE_E_INVALID, - }; - let xml = match save_to_xml_inner(&SaveData { - load_type: 0, - project: Some(p), - only_serialize_nodes: None, - properties: std::collections::HashMap::new(), - }) { - Ok(x) => x, - Err(_) => { - if !out_code.is_null() { - unsafe { *out_code = SERIALIZER_XML_ERROR }; - } - return report_serializer_failure("serialization failed", details, details_size); - } - }; - match std::fs::write(filename, xml) { - Ok(()) => { - if !out_code.is_null() { - unsafe { *out_code = SERIALIZER_OK }; - } - crate::error::OAKNODE_OK - } - Err(e) => { - if !out_code.is_null() { - unsafe { *out_code = SERIALIZER_FILE_ERROR }; - } - report_serializer_failure(&e.to_string(), details, details_size) - } - } - } - - /// `oaknode_serializer_load_from_file`: read `filename` and load it - /// into `project` (the C++ `ProjectSerializer::load` populates the - /// given project; `// CPP-PARITY: serializer.cpp:419`). - #[no_mangle] - pub unsafe extern "C" 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 { - let p = match unsafe { project_arc(&project) } { - Ok(p) => p, - Err(_) => return crate::error::OAKNODE_E_INVALID, - }; - let filename = match unsafe { cstr(filename) } { - Some(s) => s, - None => return crate::error::OAKNODE_E_INVALID, - }; - let xml = match std::fs::read_to_string(filename) { - Ok(x) => x, - Err(e) => { - if !out_code.is_null() { - unsafe { *out_code = SERIALIZER_FILE_ERROR }; - } - return report_serializer_failure(&e.to_string(), details, details_size); - } - }; - match crate::serializer::load(&xml) { - Ok(loaded) => { - // Move the loaded graph into the given project. - let mut dst = lock(&p); - let mut src = lock(&loaded); - dst.graph.transfer_all(&mut src.graph); - if !out_code.is_null() { - unsafe { *out_code = SERIALIZER_OK }; - } - crate::error::OAKNODE_OK - } - Err(_) => { - if !out_code.is_null() { - unsafe { *out_code = SERIALIZER_XML_ERROR }; - } - report_serializer_failure("XML parse error", details, details_size) - } - } - } -} diff --git a/crates/oaknode/src/footage.rs b/crates/oaknode/src/footage.rs index fdca7808a..fccca5e5b 100644 --- a/crates/oaknode/src/footage.rs +++ b/crates/oaknode/src/footage.rs @@ -15,12 +15,14 @@ // along with this program. If not, see . //! Footage nodes (C++ `olive::Footage`): media file references. -//! Probing goes through the oakcodec C ABI (`bridge::codec`) — the C++ -//! transition-stub probe path does not exist here. +//! Probing goes through the oakcodec decoder registry (direct Rust calls, +//! single-lib unification). //! `// CPP-PARITY: src/node/src/project/footage/footage.{h,cpp}`. use std::sync::atomic::{AtomicBool, Ordering}; +use oakcodec::decoder::Decoder as _; + use crate::input::Input; use crate::node::{Category, NodeBehavior, NodeCore}; use crate::value::{AudioParams, NodeValue, ValueType, VideoParams}; @@ -84,33 +86,33 @@ impl FootageBehavior { } } - /// Probe the file through oakcodec (`oakcodec_decoder_probe`), - /// filling `streams`. Error on unreadable/corrupt media or when the - /// codec module is unavailable; the prior `streams`/`valid` state is - /// preserved on failure (no partial state). + /// Probe the file through oakcodec's decoder registry, recording the + /// recognized decoder id. Error on unreadable/corrupt media; the + /// prior `streams`/`valid` state is preserved on failure (no partial + /// state). pub fn probe(&mut self) -> crate::error::Result<()> { use crate::error::Error; - // Direct call into the oakcodec crate (single-lib unification): - // `oakcodec_decoder_probe(filename)` returns the stream-list - // handle (owned by the caller). - let out = match crate::bridge::codec::decoder_probe(&self.filename) { - Some(out) => out, - None => { - return Err(Error::Failed( - "oakcodec unavailable (not linked)".to_string(), - )); - } - }; - if out.is_null() { - return Err(Error::Failed( - "oakcodec probe returned no streams".to_string(), - )); - } - // The probe result handle is an oakcodec stream-list. Reading - // stream entries into `streams` is a Phase-2 follow-up (the - // exact accessor symbols are pinned when the codec module C ABI - // is finalized). - let _ = out; + // Direct probe through the oakcodec decoder registry (single-lib + // unification; replaces the former `oakcodec_decoder_probe` C ABI + // call). + let desc = oakcodec::decoder::receive_list_of_all_decoders() + .into_iter() + .find_map(|d| { + let desc = d.probe(&self.filename, None)?; + if desc.decoder().is_empty() + || (desc.video_stream_count() == 0 + && desc.audio_stream_count() == 0 + && desc.subtitle_stream_count() == 0) + { + return None; + } + Some(desc) + }) + .ok_or_else(|| Error::Failed("oakcodec probe returned no streams".to_string()))?; + self.decoder = desc.decoder().to_string(); + // Reading stream entries into `streams` is a follow-up (the + // stream-access surface is pinned when the codec module is + // finalized); the probe result itself is dropped here. self.valid = true; Ok(()) } diff --git a/crates/oaknode/src/lib.rs b/crates/oaknode/src/lib.rs index 7a1491859..c2598a621 100644 --- a/crates/oaknode/src/lib.rs +++ b/crates/oaknode/src/lib.rs @@ -30,11 +30,9 @@ #![warn(missing_docs)] pub mod block; -pub mod bridge; pub mod colormanager; pub mod error; pub mod factory; -pub mod ffi; pub mod folder; pub mod footage; pub mod graph; diff --git a/crates/oaknode/src/node.rs b/crates/oaknode/src/node.rs index 6c6b4aea7..ed282c53b 100644 --- a/crates/oaknode/src/node.rs +++ b/crates/oaknode/src/node.rs @@ -73,7 +73,7 @@ pub struct NodeCore { pub inputs: Vec, /// Keyframe tracks per (input, element). pub keyframes: Vec<(String, i32, KeyframeTrack)>, - /// Caches as oakrender handles (created via bridge::render). + /// Caches as opaque oakrender handles. pub caches: NodeCaches, /// Node flags bitmask (hidden, dont-show-in-param-view, ...). pub flags: u64, @@ -429,18 +429,18 @@ impl NodeCore { #[derive(Clone)] pub struct NodeCaches { /// Video frame hash cache. - pub video: crate::bridge::render::CacheHandle, + pub video: crate::handle::CHandle, /// Thumbnail cache. - pub thumbnail: crate::bridge::render::CacheHandle, + pub thumbnail: crate::handle::CHandle, /// Audio playback cache. - pub audio: crate::bridge::render::CacheHandle, + pub audio: crate::handle::CHandle, /// Waveform cache. - pub waveform: crate::bridge::render::CacheHandle, + pub waveform: crate::handle::CHandle, } impl Default for NodeCaches { - /// All empty handles (caches are created lazily through bridge::render - /// when a node enters a project; `// CPP-PARITY: node.cpp:102`). + /// All empty handles (caches are created lazily when a node + /// enters a project; `// CPP-PARITY: node.cpp:102`). fn default() -> Self { NodeCaches { video: crate::handle::CHandle::null(), @@ -586,7 +586,7 @@ pub trait NodeBehavior: Send { fn generate_frame( &self, core: &NodeCore, - frame: &mut crate::bridge::render::TextureHandle, + frame: &mut crate::handle::CHandle, time: Rational, ) { let _ = (core, frame, time); diff --git a/crates/oaknode/src/nodes/displaytransform.rs b/crates/oaknode/src/nodes/displaytransform.rs index d11690c91..40e5fe800 100644 --- a/crates/oaknode/src/nodes/displaytransform.rs +++ b/crates/oaknode/src/nodes/displaytransform.rs @@ -20,7 +20,7 @@ //! //! Note: OpenColorIO itself is never linked here; it is reached through //! the color manager (`crate::colormanager`) and the oakrender bridge -//! (`crate::bridge::render`), like the C++ node's +//! (oakrender, opaque handles), like the C++ node's //! `oaknode_colormanager_*` / `oakrender_color_processor_*` calls. use crate::factory::NodeMeta; diff --git a/crates/oaknode/src/nodes/generatorwithmerge.rs b/crates/oaknode/src/nodes/generatorwithmerge.rs index 4ec8ff71b..514329fdb 100644 --- a/crates/oaknode/src/nodes/generatorwithmerge.rs +++ b/crates/oaknode/src/nodes/generatorwithmerge.rs @@ -93,7 +93,7 @@ impl GeneratorWithMerge { /// and the un-merged case pushes `job` itself. pub fn push_mergable_job( inputs: &crate::value::NodeValueRow, - job: crate::bridge::render::TextureHandle, + job: crate::handle::CHandle, table: &mut crate::value::NodeValueTable, ) { match inputs.get(BASE_INPUT) { diff --git a/crates/oaknode/src/nodes/group.rs b/crates/oaknode/src/nodes/group.rs index a5e67ff8b..90a400632 100644 --- a/crates/oaknode/src/nodes/group.rs +++ b/crates/oaknode/src/nodes/group.rs @@ -22,7 +22,7 @@ //! undo commands `NodeGroupAddInputPassthrough` / //! `NodeGroupSetOutputPassthrough`; the C ABI's undoable variants //! (`include/node/group.h`) build equivalent vtable commands through -//! `bridge::undo` in the ffi layer. +//! oakundo commands in the ops layer. use crate::factory::NodeMeta; use crate::graph::Graph; diff --git a/crates/oaknode/src/nodes/ociobase.rs b/crates/oaknode/src/nodes/ociobase.rs index d0abbfb35..4380c731a 100644 --- a/crates/oaknode/src/nodes/ociobase.rs +++ b/crates/oaknode/src/nodes/ociobase.rs @@ -25,7 +25,7 @@ //! //! Note: OpenColorIO itself is never linked here. All OCIO work goes //! through the color manager (`crate::colormanager`) and the oakrender -//! bridge (`crate::bridge::render`), mirroring how the C++ node calls +//! bridge (opaque oakrender handles), mirroring how the C++ node calls //! `oakrender_color_processor_*` / `oaknode_colormanager_*` C functions //! instead of using OCIO directly. @@ -47,13 +47,13 @@ pub const TEXTURE_INPUT: &str = "tex_in"; /// bridge), so the field is omitted here: `added_to_graph` / /// `removed_from_graph` document the capture/clear, and the processor /// generation helpers reach the manager through -/// `crate::colormanager`/`crate::bridge::render` at call time. +/// `crate::colormanager`/oakrender at call time. pub struct OcioBase { /// Owned color processor handle (C++ `processor_`, an /// `OakColorProcessor`); `None`/empty while no valid processor has /// been generated. Released with the node (C++ destructor calls /// `oakrender_color_processor_free`). - processor: Option, + processor: Option, } // The processor handle wraps a refcounted C object that is only @@ -74,7 +74,7 @@ impl OcioBase { /// Borrowed view of the owned processor handle (C++ /// `OCIOBaseNode::processor()`; callers must NOT free it). - pub fn processor(&self) -> Option<&crate::bridge::render::ColorProcessorHandle> { + pub fn processor(&self) -> Option<&crate::handle::CHandle> { self.processor.as_ref() } @@ -83,7 +83,7 @@ impl OcioBase { /// `OakColorProcessor` before storing the new one). pub fn set_processor( &mut self, - processor: Option, + processor: Option, ) { // The C++ frees the previous processor via // `oakrender_color_processor_free`; the Rust handle is a diff --git a/crates/oaknode/src/nodes/ociogradingtransformlinear.rs b/crates/oaknode/src/nodes/ociogradingtransformlinear.rs index d7b25003c..3360c666f 100644 --- a/crates/oaknode/src/nodes/ociogradingtransformlinear.rs +++ b/crates/oaknode/src/nodes/ociogradingtransformlinear.rs @@ -20,7 +20,7 @@ //! //! Note: OpenColorIO itself is never linked here; it is reached through //! the color manager (`crate::colormanager`) and the oakrender bridge -//! (`crate::bridge::render`), like the C++ node's +//! (oakrender, opaque handles), like the C++ node's //! `oakrender_color_processor_create_grading_primary` call. use crate::factory::NodeMeta; diff --git a/crates/oaknode/src/nodes/ociogradingtransformlog.rs b/crates/oaknode/src/nodes/ociogradingtransformlog.rs index abe302235..3bca8ee2d 100644 --- a/crates/oaknode/src/nodes/ociogradingtransformlog.rs +++ b/crates/oaknode/src/nodes/ociogradingtransformlog.rs @@ -25,7 +25,7 @@ //! //! Note: OpenColorIO itself is never linked here; it is reached through //! the color manager (`crate::colormanager`) and the oakrender bridge -//! (`crate::bridge::render`), like the C++ node's +//! (oakrender, opaque handles), like the C++ node's //! `oakrender_color_processor_create_grading_primary` call. use crate::factory::NodeMeta; diff --git a/crates/oaknode/src/nodes/ociolut.rs b/crates/oaknode/src/nodes/ociolut.rs index 4fc60b45b..92780df19 100644 --- a/crates/oaknode/src/nodes/ociolut.rs +++ b/crates/oaknode/src/nodes/ociolut.rs @@ -19,7 +19,7 @@ //! //! Note: OpenColorIO itself is never linked here; it is reached through //! the color manager (`crate::colormanager`) and the oakrender bridge -//! (`crate::bridge::render`), like the C++ node's +//! (oakrender, opaque handles), like the C++ node's //! `oakrender_color_processor_create_lut` / `oakrender_lut_*` calls. use std::sync::Mutex; @@ -57,7 +57,7 @@ struct ProcessorState { last_direction: i64, /// Cached processor for change detection (C++ `last_processor_`); /// released with the node. - last_processor: Option, + last_processor: Option, /// Human-readable reason no LUT processor is active (C++ /// `last_error_`); empty when a valid processor is in use or no LUT /// file has been selected yet. diff --git a/crates/oaknode/src/nodes/plugin.rs b/crates/oaknode/src/nodes/plugin.rs index 5796cf6af..0b0b4c384 100644 --- a/crates/oaknode/src/nodes/plugin.rs +++ b/crates/oaknode/src/nodes/plugin.rs @@ -19,7 +19,7 @@ //! //! DECLARATION ONLY. This node is a thin wrapper over an OFX plugin //! instance that lives behind the `oakplugin` crate's C ABI bridge -//! (`crate::bridge`); no OFX types (`OFX::Host::ImageEffect::Instance`, +//! (opaque oakrender handles); no OFX types (`OFX::Host::ImageEffect::Instance`, //! `kOfxParam*`, ...) are declared here. The plugin instance is //! represented as the opaque [`PluginInstanceHandle`] below — the real //! definition belongs to the oakplugin bridge module and this draft @@ -242,14 +242,14 @@ impl NodeBehavior for PluginNode { /// the destination frame if needed and zero-fills it (plugins do /// their real image work in the plugin job, not here). /// - /// The Rust frame is an opaque [`crate::bridge::render::TextureHandle`] + /// The Rust frame is an opaque [`crate::handle::CHandle`] /// whose pixels cannot be read or written from this crate, so the /// body is a documented no-op (`// CPP-PARITY: plugin.cpp` /// `generate_frame`). fn generate_frame( &self, core: &NodeCore, - frame: &mut crate::bridge::render::TextureHandle, + frame: &mut crate::handle::CHandle, time: Rational, ) { let _ = (core, frame, time); diff --git a/crates/oaknode/src/nodes/polygon.rs b/crates/oaknode/src/nodes/polygon.rs index 4b4faf6a5..f75019465 100644 --- a/crates/oaknode/src/nodes/polygon.rs +++ b/crates/oaknode/src/nodes/polygon.rs @@ -136,7 +136,7 @@ impl NodeBehavior for PolygonGenerator { /// scaling and center translation; without a backend the frame is /// left empty (warned once). /// - /// The Rust `frame` is an opaque [`crate::bridge::render::TextureHandle`] + /// The Rust `frame` is an opaque [`crate::handle::CHandle`] /// whose bytes cannot be touched, and this crate has no path-fill /// backend — so neither the clear nor the fill is representable here /// (`// CPP-PARITY: polygon.cpp` `generate_frame`). The path building @@ -145,7 +145,7 @@ impl NodeBehavior for PolygonGenerator { fn generate_frame( &self, core: &NodeCore, - frame: &mut crate::bridge::render::TextureHandle, + frame: &mut crate::handle::CHandle, time: oakcore_rs::Rational, ) { let _ = (core, frame, time); diff --git a/crates/oaknode/src/nodes/textv1.rs b/crates/oaknode/src/nodes/textv1.rs index 5bb9f093e..5f69e718b 100644 --- a/crates/oaknode/src/nodes/textv1.rs +++ b/crates/oaknode/src/nodes/textv1.rs @@ -241,7 +241,7 @@ impl NodeBehavior for TextGeneratorV1 { /// into the float frame multiplied by the color input. With no /// backend installed, warns once and leaves the frame empty. /// - /// The Rust frame is an opaque [`crate::bridge::render::TextureHandle`] + /// The Rust frame is an opaque [`crate::handle::CHandle`] /// whose pixels cannot be read or written from this crate, so the /// body is a documented no-op; the layout/measure/offset control flow /// is ported in [`Self::layout_request`], [`Self::draw_offsets`] and @@ -249,7 +249,7 @@ impl NodeBehavior for TextGeneratorV1 { fn generate_frame( &self, core: &NodeCore, - frame: &mut crate::bridge::render::TextureHandle, + frame: &mut crate::handle::CHandle, time: Rational, ) { let _ = (core, frame, time); diff --git a/crates/oaknode/src/nodes/textv2.rs b/crates/oaknode/src/nodes/textv2.rs index e959e6b2f..17fc49add 100644 --- a/crates/oaknode/src/nodes/textv2.rs +++ b/crates/oaknode/src/nodes/textv2.rs @@ -301,7 +301,7 @@ impl NodeBehavior for TextGeneratorV2 { /// identical). With no backend installed, warns once and leaves /// the frame empty. /// - /// The Rust frame is an opaque [`crate::bridge::render::TextureHandle`] + /// The Rust frame is an opaque [`crate::handle::CHandle`] /// whose pixels cannot be read or written from this crate, so the /// body is a documented no-op; the layout/measure/offset control flow /// is ported in [`Self::layout_request`], [`Self::base_offset`], @@ -310,7 +310,7 @@ impl NodeBehavior for TextGeneratorV2 { fn generate_frame( &self, core: &NodeCore, - frame: &mut crate::bridge::render::TextureHandle, + frame: &mut crate::handle::CHandle, time: Rational, ) { let _ = (core, frame, time); diff --git a/crates/oaknode/src/nodes/textv3.rs b/crates/oaknode/src/nodes/textv3.rs index 65f165228..24b25864c 100644 --- a/crates/oaknode/src/nodes/textv3.rs +++ b/crates/oaknode/src/nodes/textv3.rs @@ -342,7 +342,7 @@ impl NodeBehavior for TextGeneratorV3 { /// backend installed, warns once and leaves the cleared frame /// untouched. /// - /// The Rust frame is an opaque [`crate::bridge::render::TextureHandle`] + /// The Rust frame is an opaque [`crate::handle::CHandle`] /// whose pixels cannot be read or written from this crate, so the /// body is a documented no-op; the layout/measure/offset control flow /// is ported in [`Self::layout_request`], [`Self::base_offset`] and @@ -350,7 +350,7 @@ impl NodeBehavior for TextGeneratorV3 { fn generate_frame( &self, core: &NodeCore, - frame: &mut crate::bridge::render::TextureHandle, + frame: &mut crate::handle::CHandle, time: Rational, ) { let _ = (core, frame, time); diff --git a/crates/oaknode/src/ops.rs b/crates/oaknode/src/ops.rs index 028f6cf75..c9ae048d7 100644 --- a/crates/oaknode/src/ops.rs +++ b/crates/oaknode/src/ops.rs @@ -17,7 +17,10 @@ //! Free functions replacing the C++ `Node` static methods //! (COVERAGE.md §6/§9). +use std::ffi::c_void; + use oakcore_rs::TimeRange; +use oakundo::undocommand::{OakUndoCommandVtable, UndoCommand}; use crate::graph::Graph; use crate::id::NodeId; @@ -111,7 +114,7 @@ pub fn copy_inputs( /// (C++ `copy_dependency_graph` / `copy_node_in_graph` / /// `copy_node_and_dependency_graph_minus_items`). Returns the new /// node ids (source order). Undo packaging happens at the caller via -/// bridge::undo. +/// oakundo's `UndoCommand`. pub fn copy_subgraph( graph: &mut Graph, nodes: &[NodeId], @@ -147,10 +150,64 @@ fn lock_any(m: &std::sync::Mutex) -> std::sync::MutexGuard<'_, T> { m.lock().unwrap_or_else(|e| e.into_inner()) } +/// Userdata payload behind a closure-backed undo command: the boxed +/// redo/undo closures. +struct ClosureCommand { + /// The redo closure. + redo: Box, + /// The undo closure. + undo: Box, +} + +/// `OakUndoCommandVtable` redo thunk for [`ClosureCommand`] userdata. +unsafe extern "C" fn closure_redo(ud: *mut c_void) { + // SAFETY: `ud` is the `ClosureCommand` box created by + // `command_from_closures` and still owned by the command. + let c = unsafe { &mut *(ud as *mut ClosureCommand) }; + (c.redo)(); +} + +/// `OakUndoCommandVtable` undo thunk for [`ClosureCommand`] userdata. +unsafe extern "C" fn closure_undo(ud: *mut c_void) { + // SAFETY: see `closure_redo`. + let c = unsafe { &mut *(ud as *mut ClosureCommand) }; + (c.undo)(); +} + +/// `OakUndoCommandVtable` free thunk for [`ClosureCommand`] userdata. +unsafe extern "C" fn closure_free(ud: *mut c_void) { + if !ud.is_null() { + // SAFETY: the box is destroyed exactly once, by the command that + // owns it. + unsafe { drop(Box::from_raw(ud as *mut ClosureCommand)) }; + } +} + +/// Build an un-executed [`UndoCommand`] from redo/undo closures (the +/// direct-Rust replacement of the former oakundo bridge +/// `command_from_closures`). +fn command_from_closures( + redo: impl FnMut() + Send + 'static, + undo: impl FnMut() + Send + 'static, +) -> UndoCommand { + let ud = Box::into_raw(Box::new(ClosureCommand { + redo: Box::new(redo), + undo: Box::new(undo), + })); + UndoCommand::from_vtable( + OakUndoCommandVtable { + redo: Some(closure_redo), + undo: Some(closure_undo), + free_fn: Some(closure_free), + }, + ud as *mut c_void, + ) +} + /// Set a keyframed/standard value at a time, returning an un-executed /// undo command (C++ `Node::set_value_at_time` static; command creation -/// via bridge::undo). The mutation is chosen from the current state at -/// creation time, like the C++ (`// CPP-PARITY: node.cpp:1782`): +/// via oakundo's `UndoCommand`). The mutation is chosen from the current +/// state at creation time, like the C++ (`// CPP-PARITY: node.cpp:1782`): /// - keyframing input with a key at `time` -> replace the key's value; /// - keyframing input without a key -> insert a key (best type = /// closest key's type, default Linear); @@ -166,7 +223,7 @@ pub fn set_value_at_time_command( element: i32, time: oakcore_rs::Rational, value: &crate::value::NodeValue, -) -> crate::error::Result { +) -> crate::error::Result { use crate::error::Error; use crate::keyframe::{Interpolation, Keyframe}; @@ -202,7 +259,7 @@ pub fn set_value_at_time_command( // Replace the key's value (preserving type/handles). let project_redo = project.clone(); let project_undo = project.clone(); - Ok(crate::bridge::undo::command_from_closures( + Ok(command_from_closures( { let value = value.clone(); let input = input.clone(); @@ -229,8 +286,7 @@ pub fn set_value_at_time_command( } } }, - ) - .ok_or(Error::NoMem)?) + )) } None => { let input_redo = input.clone(); @@ -250,7 +306,7 @@ pub fn set_value_at_time_command( let project_redo = project.clone(); let project_undo = project.clone(); let value_redo = value.clone(); - Ok(crate::bridge::undo::command_from_closures( + Ok(command_from_closures( move || { let mut g = lock_any(&project_redo); if let Some(e) = g.graph.get_mut(node) { @@ -272,8 +328,7 @@ pub fn set_value_at_time_command( .remove_key(time); } }, - ) - .ok_or(Error::NoMem)?) + )) } } } else { @@ -287,7 +342,7 @@ pub fn set_value_at_time_command( let input_redo = input.clone(); let input_undo = input.clone(); let value_redo = value.clone(); - Ok(crate::bridge::undo::command_from_closures( + Ok(command_from_closures( move || { let mut g = lock_any(&project_redo); if let Some(e) = g.graph.get_mut(node) { @@ -301,7 +356,6 @@ pub fn set_value_at_time_command( e.core.set_standard_value(&input_undo, element, old.clone()); } }, - ) - .ok_or(Error::NoMem)?) + )) } } diff --git a/crates/oaknode/src/project.rs b/crates/oaknode/src/project.rs index c215e1207..3413a0679 100644 --- a/crates/oaknode/src/project.rs +++ b/crates/oaknode/src/project.rs @@ -43,7 +43,7 @@ pub struct NodeRef { impl NodeRef { /// New reference. `owned` selects whether releasing the last handle - /// reference accounts the node in [`crate::ffi::debug_alive_count`]. + /// reference accounts the node in the crate's debug alive count. pub fn new(project: Arc>, id: NodeId, owned: bool) -> NodeRef { NodeRef { project, diff --git a/crates/oaknode/src/sequence.rs b/crates/oaknode/src/sequence.rs index 0595a6840..e682f8c32 100644 --- a/crates/oaknode/src/sequence.rs +++ b/crates/oaknode/src/sequence.rs @@ -117,20 +117,15 @@ impl SequenceBehavior { } /// Apply the default video/audio parameters (C++ - /// `ViewerOutput::set_default_parameters()`; the config lookups use - /// oakcommon's defaults when the config module is absent). + /// `ViewerOutput::set_default_parameters()`; the config lookups read + /// the oakcommon config store directly). pub fn set_default_parameters(&mut self) { - let width = - crate::bridge::common::config_get_int("DefaultSequenceWidth", "", 1920).unwrap_or(1920); - let height = crate::bridge::common::config_get_int("DefaultSequenceHeight", "", 1080) - .unwrap_or(1080); - let sample_rate = - crate::bridge::common::config_get_int("DefaultSequenceAudioFrequency", "", 48000) - .unwrap_or(48000); - let fps_num = crate::bridge::common::config_get_int("DefaultSequenceFrameRateNum", "", 30) - .unwrap_or(30); - let fps_den = crate::bridge::common::config_get_int("DefaultSequenceFrameRateDen", "", 1) - .unwrap_or(1); + let config = oakcommon::configstore::ConfigStore::instance(); + let width = config.get_int(None, "DefaultSequenceWidth", 1920); + let height = config.get_int(None, "DefaultSequenceHeight", 1080); + let sample_rate = config.get_int(None, "DefaultSequenceAudioFrequency", 48000); + let fps_num = config.get_int(None, "DefaultSequenceFrameRateNum", 30); + let fps_den = config.get_int(None, "DefaultSequenceFrameRateDen", 1); self.video_params = vec![VideoParams { width, diff --git a/crates/oaknode/src/serializer.rs b/crates/oaknode/src/serializer.rs index e30221521..64a599053 100644 --- a/crates/oaknode/src/serializer.rs +++ b/crates/oaknode/src/serializer.rs @@ -16,8 +16,8 @@ //! Project (de)serialization: the C++ `ProjectSerializer` family. //! -//! XML I/O goes through [`crate::bridge::common`] (oakcommon C ABI; -//! in-crate stubs under `--features test-stubs`). The XML shape mirrors +//! XML I/O goes through oakcommon's [`XmlReader`]/[`XmlWriter`] (direct +//! Rust calls, single-lib unification). The XML shape mirrors //! the C++ `Node::save`/`Project::save` writers (`// CPP-PARITY: //! src/node/src/node.cpp:node::save`, `// CPP-PARITY: //! src/node/src/project.cpp:save`). Byte-exact output parity with the C++ @@ -32,9 +32,9 @@ use std::sync::{Arc, Mutex}; +use oakcommon::xmlutils::{XmlReader, XmlWriter}; use oakcore_rs::Rational; -use crate::bridge::common; use crate::graph::Graph; use crate::id::NodeId; use crate::keyframe::{Interpolation, Keyframe}; @@ -43,7 +43,7 @@ use crate::project::{NodeRef, Project}; use crate::value::{NodeValue, ValueType}; /// Minimal XML reader surface the serializer needs (implemented over -/// the oakcommon xml C ABI in `bridge::common`). +/// oakcommon's `xmlutils`). pub trait XmlRead { /// Advance to the next start element; false at end/close. fn next_start_element(&mut self) -> bool; @@ -73,47 +73,38 @@ pub trait XmlWrite { fn characters(&mut self, _text: &str) {} } -/// Reader over the oakcommon XML C ABI. +/// Reader over oakcommon's [`XmlReader`]. pub struct XmlReaderBridge { - /// The oakcommon reader handle. - pub handle: crate::handle::CHandle, + /// The oakcommon reader. + reader: XmlReader, /// Current element name (cached). name: String, } -/// Writer over the oakcommon XML C ABI. +/// Writer over oakcommon's [`XmlWriter`]. pub struct XmlWriterBridge { - /// The oakcommon writer handle. - pub handle: crate::handle::CHandle, + /// The oakcommon writer. + writer: XmlWriter, } impl XmlReaderBridge { - /// Create from XML text; `None` when oakcommon is unavailable. + /// Create from XML text; `None` on a parse error. pub fn new(xml: &str) -> Option { - use std::ffi::CString; - let c = CString::new(xml).ok()?; - let handle = common::xml_reader_init(c.as_ptr())?; + let reader = XmlReader::new(xml).ok()?; Some(XmlReaderBridge { - handle, + reader, name: String::new(), }) } } -impl Drop for XmlReaderBridge { - fn drop(&mut self) { - common::xml_reader_free(&mut self.handle); - } -} - impl XmlRead for XmlReaderBridge { fn next_start_element(&mut self) -> bool { - match common::xml_reader_next_start_element(self.handle.clone()) { - Some(true) => { - self.name = common::xml_reader_name(self.handle.clone()).unwrap_or_default(); - true - } - _ => false, + if self.reader.read_next_start_element().unwrap_or(false) { + self.name = self.reader.name().unwrap_or_default(); + true + } else { + false } } @@ -122,64 +113,58 @@ impl XmlRead for XmlReaderBridge { } fn attribute(&self, name: &str) -> Option { - let count = common::xml_reader_attribute_count(self.handle.clone()).unwrap_or(0); + let count = self.reader.attribute_count().unwrap_or(0); for i in 0..count { - let attr_name = - common::xml_reader_attribute_name(self.handle.clone(), i).unwrap_or_default(); + let attr_name = self.reader.attribute_name(i).unwrap_or_default(); if attr_name == name { - return common::xml_reader_attribute_value(self.handle.clone(), i); + return self.reader.attribute_value(i).ok(); } } None } fn read_element_text(&mut self) -> String { - common::xml_reader_read_element_text(self.handle.clone()).unwrap_or_default() + self.reader.read_element_text().unwrap_or_default() } fn skip_current_element(&mut self) { - let _ = common::xml_reader_skip_current_element(self.handle.clone()); + let _ = self.reader.skip_current_element(); } } impl XmlWriterBridge { - /// Create a writer; `None` when oakcommon is unavailable. + /// Create a writer. pub fn new() -> Option { - let handle = common::xml_writer_init()?; - Some(XmlWriterBridge { handle }) + Some(XmlWriterBridge { + writer: XmlWriter::new(), + }) } /// The serialized output. pub fn output(&self) -> String { - common::xml_writer_output(self.handle.clone()).unwrap_or_default() - } -} - -impl Drop for XmlWriterBridge { - fn drop(&mut self) { - common::xml_writer_free(&mut self.handle); + self.writer.output().unwrap_or_default() } } impl XmlWrite for XmlWriterBridge { fn start_element(&mut self, name: &str) { - let _ = common::xml_writer_start_element(self.handle.clone(), name); + let _ = self.writer.write_start_element(name); } fn end_element(&mut self) { - let _ = common::xml_writer_end_element(self.handle.clone()); + let _ = self.writer.write_end_element(); } fn attribute(&mut self, name: &str, value: &str) { - let _ = common::xml_writer_attribute(self.handle.clone(), name, value); + let _ = self.writer.write_attribute(name, value); } fn text_element(&mut self, name: &str, text: &str) { - let _ = common::xml_writer_text_element(self.handle.clone(), name, text); + let _ = self.writer.write_text_element(name, text); } fn characters(&mut self, text: &str) { - let _ = common::xml_writer_characters(self.handle.clone(), text); + let _ = self.writer.write_characters(text); } } diff --git a/crates/oaknode/src/value.rs b/crates/oaknode/src/value.rs index ad9183d93..6f70748ab 100644 --- a/crates/oaknode/src/value.rs +++ b/crates/oaknode/src/value.rs @@ -122,7 +122,7 @@ pub enum NodeValue { /// Boolean. Boolean(bool), /// Texture handle (owned reference). - Texture(crate::bridge::render::TextureHandle), + Texture(crate::handle::CHandle), /// Interleaved/planar sample payload + format. Samples(SampleBuffer), /// Rational. diff --git a/crates/oaknode/tests/dbg2_test.rs b/crates/oaknode/tests/dbg2_test.rs deleted file mode 100644 index 1f10692c1..000000000 --- a/crates/oaknode/tests/dbg2_test.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[test] -fn dbg_dlsym() { - let p = oaknode::bridge::dlsym::resolve("oaknode_xml_writer_init"); - eprintln!("writer init symbol: {:?}", p); - let p2 = oaknode::bridge::dlsym::resolve("oakundo_command_init_multi"); - eprintln!("undo multi symbol: {:?}", p2); -} diff --git a/crates/oaknode/tests/ffi_contract_test.rs b/crates/oaknode/tests/ffi_contract_test.rs deleted file mode 100644 index bb740f2ab..000000000 --- a/crates/oaknode/tests/ffi_contract_test.rs +++ /dev/null @@ -1,1124 +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 . - -//! C ABI contract tests (ffi.rs). One normal + one error path per -//! export family; the exhaustive matrix is driven from the existing -//! C++ gtest suite (`src/node/tests`, unchanged) running against this -//! crate — these tests only pin Rust-side specifics. -//! -//! Handles follow the C by-value contract: every function call that -//! consumes a handle receives `dup(&h)` (addref + copy), and every dup -//! is eventually released. Tests serialize on [`ffi_lock`] because they -//! mutate the process-wide debug alive counter. - -use std::ffi::{c_char, CString}; -use std::sync::{Mutex, MutexGuard}; - -use oaknode::error::{OAKNODE_E_INVALID, OAKNODE_E_NOT_FOUND, OAKNODE_OK}; -use oaknode::ffi::factory::{ - oaknode_factory_create_from_id, oaknode_factory_id_at, oaknode_factory_id_count, - oaknode_factory_initialize, oaknode_factory_name_from_id, oaknode_factory_node_at, -}; -use oaknode::ffi::keyframe::oaknode_keyframe_opposing_bezier_type; -use oaknode::ffi::node::{ - oaknode_debug_alive_count, oaknode_node_are_linked, oaknode_node_connect, - oaknode_node_context_count, oaknode_node_context_node_at, oaknode_node_copy_inputs, - oaknode_node_create_copy, oaknode_node_disconnect, oaknode_node_free, - oaknode_node_from_identity, oaknode_node_get_context_position, oaknode_node_get_id, - oaknode_node_get_input, oaknode_node_get_input_at_time, oaknode_node_get_input_name, - oaknode_node_get_input_string, oaknode_node_get_label, oaknode_node_get_name, - oaknode_node_get_override_color, oaknode_node_get_project, oaknode_node_identity, - oaknode_node_input_array_insert, oaknode_node_input_array_remove, oaknode_node_input_count, - oaknode_node_input_get_connected_node, oaknode_node_input_get_type, oaknode_node_input_id, - oaknode_node_input_is_connectable, oaknode_node_input_is_connected, oaknode_node_is_enabled, - oaknode_node_link, oaknode_node_link_at, oaknode_node_link_count, - oaknode_node_output_connection_count, oaknode_node_output_connection_element_at, - oaknode_node_output_connection_input_id_at, oaknode_node_output_connection_node_at, - oaknode_node_remove_from_context, oaknode_node_set_context_position, oaknode_node_set_enabled, - oaknode_node_set_input, oaknode_node_set_input_string, oaknode_node_set_label, - oaknode_node_set_override_color, oaknode_node_set_value_hint_track, oaknode_node_unlink, -}; -use oaknode::ffi::project::{ - oaknode_project_add_node, oaknode_project_cache_path, oaknode_project_clear, - oaknode_project_copy_settings, oaknode_project_filename, oaknode_project_free, - oaknode_project_get_cache_location_setting, oaknode_project_get_custom_cache_path, - oaknode_project_get_uuid, oaknode_project_init, oaknode_project_initialize, - oaknode_project_is_modified, oaknode_project_is_new, oaknode_project_name, - oaknode_project_node_at, oaknode_project_node_count, oaknode_project_pretty_filename, - oaknode_project_remove_node, oaknode_project_root, oaknode_project_set_cache_location_setting, - oaknode_project_set_custom_cache_path, oaknode_project_set_filename, - oaknode_project_set_modified, -}; -use oaknode::handle::CHandle; -use oaknode::value::{oak, OakNodeValue}; - -/// Serialize all tests touching the global alive counter / factory. -static FFI_LOCK: Mutex<()> = Mutex::new(()); - -fn ffi_lock() -> MutexGuard<'static, ()> { - FFI_LOCK.lock().unwrap_or_else(|e| e.into_inner()) -} - -/// By-value handle copy. The ffi callees never release the handles they -/// receive, so a bitwise clone is safe here: every box is released -/// exactly once, via the original handle's free call. -fn dup(h: &CHandle) -> CHandle { - h.clone() -} - -fn cs(s: &str) -> CString { - CString::new(s).unwrap() -} - -/// Read a two-stage getter's result into a String; the caller passes -/// the getter closure (returns required size / error code). -fn two_stage i32>(getter: F) -> Result { - let needed = getter(std::ptr::null_mut(), 0); - if needed < 0 { - return Err(needed); - } - let mut buf = vec![0u8; needed as usize]; - let rc = getter(buf.as_mut_ptr() as *mut c_char, needed); - if rc < 0 { - return Err(rc); - } - buf.pop(); // trailing NUL - Ok(String::from_utf8(buf).unwrap()) -} - -fn init_project() -> CHandle { - let p = unsafe { oaknode_project_init() }; - assert!(!p.ctx.is_null()); - assert_eq!(p.abi_version, 1); - assert!(p.addref.is_some() && p.release.is_some()); - assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK); - p -} - -/// Every implemented handle-returning function returns ctx==NULL on -/// failure and a valid refcounted handle on success (abi_version -/// stamped). -#[test] -fn handle_contract_all_exports() { - let _g = ffi_lock(); - let mut p = init_project(); - - // Borrowed root folder handle. - let mut root = unsafe { oaknode_project_root(dup(&p)) }; - assert!(!root.ctx.is_null()); - assert_eq!(root.abi_version, 1); - - // node_at: valid index -> borrowed handle; out-of-range / negative -> - // empty; empty project handle -> empty. - let mut n0 = unsafe { oaknode_project_node_at(dup(&p), 0) }; - assert!(!n0.ctx.is_null()); - assert!(unsafe { oaknode_project_node_at(dup(&p), 1) }.ctx.is_null()); - assert!(unsafe { oaknode_project_node_at(dup(&p), -1) } - .ctx - .is_null()); - assert!(unsafe { oaknode_project_node_at(CHandle::null(), 0) } - .ctx - .is_null()); - - // Factory node handle: owned, valid. - let mut math = - unsafe { oaknode_factory_create_from_id(cs("org.olivevideoeditor.Olive.math").as_ptr()) }; - assert!(!math.ctx.is_null()); - assert_eq!(math.abi_version, 1); - - // create_copy: owned copy of an orphan node. - let mut copy = unsafe { oaknode_node_create_copy(dup(&math)) }; - assert!(!copy.ctx.is_null()); - - // from_identity of a live node -> valid; garbage identity -> empty. - let id = unsafe { oaknode_node_identity(dup(&math)) }; - let mut back = unsafe { oaknode_node_from_identity(id) }; - assert!(!back.ctx.is_null()); - assert!(unsafe { oaknode_node_from_identity(0xdead) }.ctx.is_null()); - - unsafe { oaknode_node_free(&mut back) }; - unsafe { oaknode_node_free(&mut copy) }; - unsafe { oaknode_node_free(&mut math) }; - unsafe { oaknode_node_free(&mut n0) }; - unsafe { oaknode_node_free(&mut root) }; - unsafe { oaknode_project_free(&mut p) }; -} - -/// free(NULL)/free(empty) are no-ops across every free export. -#[test] -fn free_null_noop_all_exports() { - let _g = ffi_lock(); - unsafe { - oaknode_project_free(std::ptr::null_mut()); - let mut empty = CHandle::null(); - oaknode_project_free(&mut empty); - assert!(empty.ctx.is_null()); - - oaknode_node_free(std::ptr::null_mut()); - let mut empty = CHandle::null(); - oaknode_node_free(&mut empty); - assert!(empty.ctx.is_null()); - } - - // A live project frees cleanly and clears the handle. - let mut p = init_project(); - unsafe { oaknode_project_free(&mut p) }; - assert!(p.ctx.is_null()); -} - -/// Two-stage string functions: size query, short buffer truncation -/// rule, and exact-fit write — for every string getter. -#[test] -fn two_stage_string_contract() { - let _g = ffi_lock(); - let mut p = init_project(); - - // project_name: "(untitled)" -> 10 + 1 bytes. - let name = two_stage(|buf, size| unsafe { oaknode_project_name(dup(&p), buf, size) }).unwrap(); - assert_eq!(name, "(untitled)"); - - // Short buffer truncates and NUL-terminates. - let mut buf = [0u8; 4]; - let rc = - unsafe { oaknode_project_name(dup(&p), buf.as_mut_ptr() as *mut c_char, buf.len() as i32) }; - assert_eq!( - rc, - "(untitled)".len() as i32 + 1, - "required size regardless of buffer" - ); - assert_eq!(&buf[..3], b"(un"); - assert_eq!(buf[3], 0); - - // Exact-fit write (required size) is NUL-terminated. - let needed = unsafe { oaknode_project_name(dup(&p), std::ptr::null_mut(), 0) }; - let mut buf = vec![0u8; needed as usize]; - assert_eq!( - unsafe { oaknode_project_name(dup(&p), buf.as_mut_ptr() as *mut c_char, needed) }, - needed - ); - assert_eq!(buf[needed as usize - 1], 0, "terminating NUL"); - buf.pop(); - assert_eq!(String::from_utf8(buf).unwrap(), "(untitled)"); - - // filename: "" initially (size 1); after set_filename -> full path. - let filename = - two_stage(|buf, size| unsafe { oaknode_project_filename(dup(&p), buf, size) }).unwrap(); - assert_eq!(filename, ""); - unsafe { oaknode_project_set_filename(dup(&p), cs("/tmp/demo.ove").as_ptr()) }; - let filename = - two_stage(|buf, size| unsafe { oaknode_project_filename(dup(&p), buf, size) }).unwrap(); - assert_eq!(filename, "/tmp/demo.ove"); - let pretty = - two_stage(|buf, size| unsafe { oaknode_project_pretty_filename(dup(&p), buf, size) }) - .unwrap(); - assert_eq!(pretty, "/tmp/demo.ove"); - - // uuid is the 36-char braced format. - let uuid = - two_stage(|buf, size| unsafe { oaknode_project_get_uuid(dup(&p), buf, size) }).unwrap(); - assert_eq!(uuid.len(), 38); - assert!(uuid.starts_with('{') && uuid.ends_with('}')); - - // cache_path is a two-stage getter too. - let _ = - two_stage(|buf, size| unsafe { oaknode_project_cache_path(dup(&p), buf, size) }).unwrap(); - - // Node string getters. - let mut math = - unsafe { oaknode_factory_create_from_id(cs("org.olivevideoeditor.Olive.math").as_ptr()) }; - let id = two_stage(|buf, size| unsafe { oaknode_node_get_id(dup(&math), buf, size) }).unwrap(); - assert_eq!(id, "org.olivevideoeditor.Olive.math"); - let name = - two_stage(|buf, size| unsafe { oaknode_node_get_name(dup(&math), buf, size) }).unwrap(); - assert_eq!(name, "Math"); - let label = - two_stage(|buf, size| unsafe { oaknode_node_get_label(dup(&math), buf, size) }).unwrap(); - assert_eq!(label, ""); - let iname = two_stage(|buf, size| unsafe { - oaknode_node_get_input_name(dup(&math), cs("enabled_in").as_ptr(), buf, size) - }) - .unwrap(); - assert_eq!(iname, "Enabled"); - - unsafe { oaknode_node_free(&mut math) }; - unsafe { oaknode_project_free(&mut p) }; -} - -/// Identity registry: node_identity / node_from_identity round-trip; -/// freed nodes are rejected by from_identity. -#[test] -fn identity_registry_roundtrip() { - let _g = ffi_lock(); - let mut math = - unsafe { oaknode_factory_create_from_id(cs("org.olivevideoeditor.Olive.math").as_ptr()) }; - let id = unsafe { oaknode_node_identity(dup(&math)) }; - - // Round-trip: the looked-up node has the same type id. - let mut back = unsafe { oaknode_node_from_identity(id) }; - assert!(!back.ctx.is_null()); - let bid = two_stage(|buf, size| unsafe { oaknode_node_get_id(dup(&back), buf, size) }).unwrap(); - assert_eq!(bid, "org.olivevideoeditor.Olive.math"); - unsafe { oaknode_node_free(&mut back) }; - - // Move the node into a project: its identity is re-registered and - // still resolvable while the project lives. - let mut p = unsafe { oaknode_project_init() }; - assert_eq!( - unsafe { oaknode_project_add_node(dup(&p), dup(&math)) }, - OAKNODE_OK - ); - let id2 = unsafe { oaknode_node_identity(dup(&math)) }; - let mut back2 = unsafe { oaknode_node_from_identity(id2) }; - assert!(!back2.ctx.is_null()); - unsafe { oaknode_node_free(&mut back2) }; - - // Free the node handle (drops its project reference) and the project: - // both identities must then reject. - unsafe { oaknode_node_free(&mut math) }; - unsafe { oaknode_project_free(&mut p) }; - let ghost = unsafe { oaknode_node_from_identity(id) }; - assert!( - ghost.ctx.is_null(), - "freed node must be rejected by from_identity" - ); - let ghost2 = unsafe { oaknode_node_from_identity(id2) }; - assert!(ghost2.ctx.is_null()); -} - -/// alive count: project create/destroy moves oaknode_debug_alive_count -/// predictably and returns to baseline. -#[test] -fn alive_count_accounting() { - let _g = ffi_lock(); - let base = unsafe { oaknode_debug_alive_count() }; - - let mut p = init_project(); - assert_eq!(unsafe { oaknode_debug_alive_count() }, base + 1); - - let mut math = - unsafe { oaknode_factory_create_from_id(cs("org.olivevideoeditor.Olive.math").as_ptr()) }; - assert_eq!(unsafe { oaknode_debug_alive_count() }, base + 2); - - // A borrowed view does not count. - let mut n0 = unsafe { oaknode_project_node_at(dup(&p), 0) }; - assert_eq!(unsafe { oaknode_debug_alive_count() }, base + 2); - unsafe { oaknode_node_free(&mut n0) }; - - // Adding the owned node to the project transfers its accounting to - // the project. - unsafe { oaknode_project_add_node(dup(&p), dup(&math)) }; - assert_eq!(unsafe { oaknode_debug_alive_count() }, base + 1); - // The node handle is now a borrowed view: freeing it changes nothing. - unsafe { oaknode_node_free(&mut math) }; - assert_eq!(unsafe { oaknode_debug_alive_count() }, base + 1); - - unsafe { oaknode_project_free(&mut p) }; - assert_eq!(unsafe { oaknode_debug_alive_count() }, base); - - // Detach: remove_node returns ownership (counted again), free - // returns to baseline. - let mut p2 = init_project(); - let mut node = - unsafe { oaknode_factory_create_from_id(cs("org.olivevideoeditor.Olive.math").as_ptr()) }; - unsafe { oaknode_project_add_node(dup(&p2), dup(&node)) }; - assert_eq!(unsafe { oaknode_debug_alive_count() }, base + 1); - unsafe { oaknode_project_remove_node(dup(&p2), dup(&node)) }; - assert_eq!(unsafe { oaknode_debug_alive_count() }, base + 2); - unsafe { oaknode_node_free(&mut node) }; - assert_eq!(unsafe { oaknode_debug_alive_count() }, base + 1); - unsafe { oaknode_project_free(&mut p2) }; - assert_eq!(unsafe { oaknode_debug_alive_count() }, base); -} - -/// Project family: success + failure path for every implemented export. -#[test] -fn project_family_contract() { - let _g = ffi_lock(); - let mut p = init_project(); - - // initialize: E_STATE on second call; E_INVALID on empty handle. - assert_eq!( - unsafe { oaknode_project_initialize(dup(&p)) }, - oaknode::error::OAKNODE_E_STATE - ); - assert_eq!( - unsafe { oaknode_project_initialize(CHandle::null()) }, - OAKNODE_E_INVALID - ); - - // root: valid after initialize. - let mut root = unsafe { oaknode_project_root(dup(&p)) }; - assert!(!root.ctx.is_null()); - assert!(unsafe { oaknode_project_root(CHandle::null()) } - .ctx - .is_null()); - unsafe { oaknode_node_free(&mut root) }; - - // name/filename/pretty/is_modified/is_new + empty-handle failures. - assert_eq!( - unsafe { oaknode_project_name(CHandle::null(), std::ptr::null_mut(), 0) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_project_filename(CHandle::null(), std::ptr::null_mut(), 0) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_project_is_modified(CHandle::null()) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_project_is_new(CHandle::null()) }, - OAKNODE_E_INVALID - ); - - // set_filename: success; NULL failure. - assert_eq!( - unsafe { oaknode_project_set_filename(dup(&p), cs("/tmp/x.ove").as_ptr()) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_project_set_filename(dup(&p), std::ptr::null()) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_project_set_filename(CHandle::null(), cs("x").as_ptr()) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_project_is_new(dup(&p)) }, - 0, - "has a filename now" - ); - - // modified flag. - assert_eq!( - unsafe { oaknode_project_set_modified(dup(&p), 1) }, - OAKNODE_OK - ); - assert_eq!(unsafe { oaknode_project_is_modified(dup(&p)) }, 1); - assert_eq!( - unsafe { oaknode_project_set_modified(CHandle::null(), 0) }, - OAKNODE_E_INVALID - ); - - // cache settings. - assert_eq!( - unsafe { oaknode_project_get_cache_location_setting(dup(&p)) }, - 0 - ); - assert_eq!( - unsafe { oaknode_project_set_cache_location_setting(dup(&p), 2) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_project_set_cache_location_setting(dup(&p), 99) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_project_set_cache_location_setting(CHandle::null(), 0) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_project_set_custom_cache_path(dup(&p), cs("/tmp/cache").as_ptr()) }, - OAKNODE_OK - ); - let custom = - two_stage(|buf, size| unsafe { oaknode_project_get_custom_cache_path(dup(&p), buf, size) }) - .unwrap(); - assert_eq!(custom, "/tmp/cache"); - unsafe { oaknode_project_set_custom_cache_path(dup(&p), std::ptr::null()) }; // NULL clears - let custom = - two_stage(|buf, size| unsafe { oaknode_project_get_custom_cache_path(dup(&p), buf, size) }) - .unwrap(); - assert_eq!(custom, ""); - - // copy_settings: dst inherits src settings; empty handle fails. - let mut p2 = unsafe { oaknode_project_init() }; - unsafe { oaknode_project_set_cache_location_setting(dup(&p2), 1) }; - assert_eq!( - unsafe { oaknode_project_copy_settings(dup(&p), dup(&p2)) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_project_get_cache_location_setting(dup(&p)) }, - 1 - ); - assert_eq!( - unsafe { oaknode_project_copy_settings(CHandle::null(), dup(&p2)) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_project_copy_settings(dup(&p), CHandle::null()) }, - OAKNODE_E_INVALID - ); - - // node add/remove/count/at. - let base_count = unsafe { oaknode_project_node_count(dup(&p)) }; - let mut node = unsafe { - oaknode_factory_create_from_id(cs("org.olivevideoeditor.Olive.solidgenerator").as_ptr()) - }; - assert_eq!( - unsafe { oaknode_project_add_node(dup(&p), dup(&node)) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_project_node_count(dup(&p)) }, - base_count + 1 - ); - assert_eq!( - unsafe { oaknode_project_add_node(CHandle::null(), dup(&node)) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_project_add_node(dup(&p), CHandle::null()) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_project_remove_node(dup(&p), dup(&node)) }, - OAKNODE_OK - ); - assert_eq!(unsafe { oaknode_project_node_count(dup(&p)) }, base_count); - assert_eq!( - unsafe { oaknode_project_remove_node(dup(&p), dup(&node)) }, - OAKNODE_E_NOT_FOUND, - "double remove fails" - ); - assert_eq!( - unsafe { oaknode_project_remove_node(CHandle::null(), dup(&node)) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_project_node_count(CHandle::null()) }, - OAKNODE_E_INVALID - ); - unsafe { oaknode_node_free(&mut node) }; - - // clear: resets; empty handle fails. - assert_eq!( - unsafe { oaknode_project_clear(CHandle::null()) }, - OAKNODE_E_INVALID - ); - assert_eq!(unsafe { oaknode_project_clear(dup(&p)) }, OAKNODE_OK); - assert!( - unsafe { oaknode_project_root(dup(&p)) }.ctx.is_null(), - "root gone after clear" - ); - assert_eq!( - unsafe { oaknode_project_initialize(dup(&p)) }, - OAKNODE_OK, - "re-initializable" - ); - - unsafe { oaknode_project_free(&mut p) }; - unsafe { oaknode_project_free(&mut p2) }; -} - -/// Node family: metadata, inputs, params, graph editing, links, -/// contexts, arrays, copies — success + failure per export. -#[test] -fn node_family_contract() { - let _g = ffi_lock(); - let mut p = init_project(); - - // Two factory nodes in the same project. - let mut a = - unsafe { oaknode_factory_create_from_id(cs("org.olivevideoeditor.Olive.math").as_ptr()) }; - let mut b = - unsafe { oaknode_factory_create_from_id(cs("org.olivevideoeditor.Olive.ortho").as_ptr()) }; - unsafe { oaknode_project_add_node(dup(&p), dup(&a)) }; - unsafe { oaknode_project_add_node(dup(&p), dup(&b)) }; - - // Metadata. - assert_eq!( - unsafe { oaknode_node_get_id(CHandle::null(), std::ptr::null_mut(), 0) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_node_get_name(CHandle::null(), std::ptr::null_mut(), 0) }, - OAKNODE_E_INVALID - ); - let id = two_stage(|buf, size| unsafe { oaknode_node_get_id(dup(&a), buf, size) }).unwrap(); - assert_eq!(id, "org.olivevideoeditor.Olive.math"); - - // Label. - assert_eq!( - unsafe { oaknode_node_set_label(dup(&a), cs("my node").as_ptr()) }, - OAKNODE_OK - ); - let label = - two_stage(|buf, size| unsafe { oaknode_node_get_label(dup(&a), buf, size) }).unwrap(); - assert_eq!(label, "my node"); - assert_eq!( - unsafe { oaknode_node_set_label(CHandle::null(), cs("x").as_ptr()) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_node_set_label(dup(&a), std::ptr::null()) }, - OAKNODE_E_INVALID - ); - - // Override color. - let mut oc = 0; - assert_eq!( - unsafe { oaknode_node_get_override_color(dup(&a), &mut oc) }, - OAKNODE_OK - ); - assert_eq!(oc, -1); - assert_eq!( - unsafe { oaknode_node_set_override_color(dup(&a), 3) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_node_get_override_color(dup(&a), &mut oc) }, - OAKNODE_OK - ); - assert_eq!(oc, 3); - assert_eq!( - unsafe { oaknode_node_get_override_color(CHandle::null(), &mut oc) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_node_get_override_color(dup(&a), std::ptr::null_mut()) }, - OAKNODE_E_INVALID - ); - - // Enabled. - let mut en = 0; - assert_eq!( - unsafe { oaknode_node_is_enabled(dup(&a), &mut en) }, - OAKNODE_OK - ); - assert_eq!(en, 1); - assert_eq!(unsafe { oaknode_node_set_enabled(dup(&a), 0) }, OAKNODE_OK); - assert_eq!( - unsafe { oaknode_node_is_enabled(dup(&a), &mut en) }, - OAKNODE_OK - ); - assert_eq!(en, 0); - assert_eq!( - unsafe { oaknode_node_is_enabled(CHandle::null(), &mut en) }, - OAKNODE_E_INVALID - ); - - // Input introspection. - let mut count = 0; - assert_eq!( - unsafe { oaknode_node_input_count(dup(&a), &mut count) }, - OAKNODE_OK - ); - assert_eq!(count, 4, "enabled_in + method_in + param_a_in + param_b_in"); - let first = - two_stage(|buf, size| unsafe { oaknode_node_input_id(dup(&a), 0, buf, size) }).unwrap(); - assert_eq!(first, "enabled_in"); - assert_eq!( - unsafe { oaknode_node_input_id(dup(&a), 99, std::ptr::null_mut(), 0) }, - OAKNODE_E_NOT_FOUND - ); - assert_eq!( - unsafe { oaknode_node_input_id(dup(&a), -1, std::ptr::null_mut(), 0) }, - OAKNODE_E_NOT_FOUND - ); - - let mut ty = 0; - assert_eq!( - unsafe { oaknode_node_input_get_type(dup(&a), cs("enabled_in").as_ptr(), &mut ty) }, - OAKNODE_OK - ); - assert_eq!(ty, oak::BOOL); - assert_eq!( - unsafe { oaknode_node_input_get_type(dup(&a), cs("nope").as_ptr(), &mut ty) }, - OAKNODE_E_NOT_FOUND - ); - assert_eq!( - unsafe { - oaknode_node_input_get_type(dup(&a), cs("enabled_in").as_ptr(), std::ptr::null_mut()) - }, - OAKNODE_E_INVALID - ); - - let mut conn = 0; - assert_eq!( - unsafe { oaknode_node_input_is_connected(dup(&a), cs("param_a_in").as_ptr(), &mut conn) }, - OAKNODE_OK - ); - assert_eq!(conn, 0); - assert_eq!( - unsafe { oaknode_node_input_is_connected(dup(&a), cs("nope").as_ptr(), &mut conn) }, - OAKNODE_E_NOT_FOUND - ); - - let mut connectable = 0; - assert_eq!( - unsafe { - oaknode_node_input_is_connectable(dup(&a), cs("param_a_in").as_ptr(), &mut connectable) - }, - OAKNODE_OK - ); - assert_eq!(connectable, 1); - assert_eq!( - unsafe { - oaknode_node_input_is_connectable(dup(&a), cs("method_in").as_ptr(), &mut connectable) - }, - OAKNODE_OK - ); - assert_eq!(connectable, 0, "method_in is not connectable"); - assert_eq!( - unsafe { - oaknode_node_input_is_connectable(dup(&a), cs("nope").as_ptr(), &mut connectable) - }, - OAKNODE_E_NOT_FOUND - ); - - let iname = two_stage(|buf, size| unsafe { - oaknode_node_get_input_name(dup(&a), cs("enabled_in").as_ptr(), buf, size) - }) - .unwrap(); - assert_eq!(iname, "Enabled"); - assert_eq!( - unsafe { - oaknode_node_get_input_name(dup(&a), cs("nope").as_ptr(), std::ptr::null_mut(), 0) - }, - OAKNODE_E_NOT_FOUND - ); - - // Params: get/set float, unknown id, string input, bad POD type. - let mut pod = OakNodeValue::none(); - assert_eq!( - unsafe { oaknode_node_get_input(dup(&a), cs("param_a_in").as_ptr(), &mut pod) }, - OAKNODE_OK - ); - assert_eq!(pod.kind, oak::FLOAT); - assert_eq!(pod.f[0], 0.0); - let v = OakNodeValue { - kind: oak::FLOAT, - num: 0, - den: 0, - f: [3.5, 0.0, 0.0, 0.0], - }; - assert_eq!( - unsafe { oaknode_node_set_input(dup(&a), cs("param_a_in").as_ptr(), &v) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_node_get_input(dup(&a), cs("param_a_in").as_ptr(), &mut pod) }, - OAKNODE_OK - ); - assert_eq!(pod.f[0], 3.5); - assert_eq!( - unsafe { oaknode_node_get_input(dup(&a), cs("nope").as_ptr(), &mut pod) }, - OAKNODE_E_NOT_FOUND - ); - assert_eq!( - unsafe { oaknode_node_set_input(dup(&a), cs("nope").as_ptr(), &v) }, - OAKNODE_E_NOT_FOUND - ); - let wrong_type = OakNodeValue { - kind: oak::INT, - num: 1, - den: 0, - f: [0.0; 4], - }; - assert_eq!( - unsafe { oaknode_node_set_input(dup(&a), cs("param_a_in").as_ptr(), &wrong_type) }, - OAKNODE_E_INVALID, - "POD type must match the declared input type" - ); - - // String input: the timeformat node's format_in. - let mut tf = unsafe { - oaknode_factory_create_from_id(cs("org.olivevideoeditor.Olive.timeformat").as_ptr()) - }; - unsafe { oaknode_project_add_node(dup(&p), dup(&tf)) }; - let s = two_stage(|buf, size| unsafe { - oaknode_node_get_input_string(dup(&tf), cs("format_in").as_ptr(), buf, size) - }) - .unwrap(); - assert_eq!(s, "hh:mm:ss"); - assert_eq!( - unsafe { - oaknode_node_set_input_string(dup(&tf), cs("format_in").as_ptr(), cs("yyyy").as_ptr()) - }, - OAKNODE_OK - ); - let s = two_stage(|buf, size| unsafe { - oaknode_node_get_input_string(dup(&tf), cs("format_in").as_ptr(), buf, size) - }) - .unwrap(); - assert_eq!(s, "yyyy"); - // Non-string input -> E_INVALID; unknown -> E_NOT_FOUND. - assert_eq!( - unsafe { - oaknode_node_get_input_string(dup(&tf), cs("time_in").as_ptr(), std::ptr::null_mut(), 0) - }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { - oaknode_node_set_input_string(dup(&tf), cs("time_in").as_ptr(), cs("x").as_ptr()) - }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { - oaknode_node_get_input_string(dup(&tf), cs("nope").as_ptr(), std::ptr::null_mut(), 0) - }, - OAKNODE_E_NOT_FOUND - ); - - // Graph editing: connect a -> b.param_a_in. - assert_eq!( - unsafe { oaknode_node_connect(dup(&a), dup(&b), cs("rot_in").as_ptr()) }, - OAKNODE_OK - ); - let mut connected = 0; - assert_eq!( - unsafe { oaknode_node_input_is_connected(dup(&b), cs("rot_in").as_ptr(), &mut connected) }, - OAKNODE_OK - ); - assert_eq!(connected, 1); - // Duplicate connect -> E_STATE. - assert_eq!( - unsafe { oaknode_node_connect(dup(&a), dup(&b), cs("rot_in").as_ptr()) }, - oaknode::error::OAKNODE_E_STATE - ); - // Not connectable -> E_INVALID. - assert_eq!( - unsafe { oaknode_node_connect(dup(&a), dup(&a), cs("method_in").as_ptr()) }, - OAKNODE_E_INVALID - ); - // Unknown input -> E_NOT_FOUND. - assert_eq!( - unsafe { oaknode_node_connect(dup(&a), dup(&b), cs("nope").as_ptr()) }, - OAKNODE_E_NOT_FOUND - ); - // Empty handle -> E_INVALID. - assert_eq!( - unsafe { oaknode_node_connect(CHandle::null(), dup(&b), cs("param_a_in").as_ptr()) }, - OAKNODE_E_INVALID - ); - - // input_get_connected_node. - let mut src = CHandle::null(); - assert_eq!( - unsafe { oaknode_node_input_get_connected_node(dup(&b), cs("rot_in").as_ptr(), &mut src) }, - OAKNODE_OK - ); - assert!(!src.ctx.is_null()); - let src_id = - two_stage(|buf, size| unsafe { oaknode_node_get_id(dup(&src), buf, size) }).unwrap(); - assert_eq!(src_id, "org.olivevideoeditor.Olive.math"); - assert_eq!( - unsafe { oaknode_node_input_get_connected_node(dup(&b), cs("nope").as_ptr(), &mut src) }, - OAKNODE_E_NOT_FOUND - ); - - // Output connections. - let mut count = 0; - assert_eq!( - unsafe { oaknode_node_output_connection_count(dup(&a), &mut count) }, - OAKNODE_OK - ); - assert_eq!(count, 1); - let mut node_out = CHandle::null(); - assert_eq!( - unsafe { oaknode_node_output_connection_node_at(dup(&a), 0, &mut node_out) }, - OAKNODE_OK - ); - assert!(!node_out.ctx.is_null()); - assert_eq!( - unsafe { oaknode_node_output_connection_node_at(dup(&a), 1, &mut node_out) }, - OAKNODE_E_NOT_FOUND - ); - let input_id = two_stage(|buf, size| unsafe { - oaknode_node_output_connection_input_id_at(dup(&a), 0, buf, size) - }) - .unwrap(); - assert_eq!(input_id, "rot_in"); - let mut element = 0; - assert_eq!( - unsafe { oaknode_node_output_connection_element_at(dup(&a), 0, &mut element) }, - OAKNODE_OK - ); - assert_eq!(element, -1); - - // Disconnect. - assert_eq!( - unsafe { oaknode_node_disconnect(dup(&b), cs("rot_in").as_ptr()) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_node_disconnect(dup(&b), cs("rot_in").as_ptr()) }, - OAKNODE_E_NOT_FOUND, - "disconnect of an unconnected input fails" - ); - assert_eq!( - unsafe { oaknode_node_disconnect(dup(&b), cs("nope").as_ptr()) }, - OAKNODE_E_NOT_FOUND - ); - - // Links. - let mut linked = 0; - assert_eq!( - unsafe { oaknode_node_link(dup(&a), dup(&b), &mut linked) }, - OAKNODE_OK - ); - assert_eq!(linked, 1); - assert_eq!( - unsafe { oaknode_node_link(dup(&a), dup(&b), &mut linked) }, - OAKNODE_OK - ); - assert_eq!(linked, 0, "already linked"); - let mut linked_ok = 0; - assert_eq!( - unsafe { oaknode_node_are_linked(dup(&a), dup(&b), &mut linked_ok) }, - OAKNODE_OK - ); - assert_eq!(linked_ok, 1); - assert_eq!( - unsafe { oaknode_node_link_count(dup(&a), &mut count) }, - OAKNODE_OK - ); - assert_eq!(count, 1); - let mut link_node = CHandle::null(); - assert_eq!( - unsafe { oaknode_node_link_at(dup(&a), 0, &mut link_node) }, - OAKNODE_OK - ); - assert!(!link_node.ctx.is_null()); - assert_eq!( - unsafe { oaknode_node_link_at(dup(&a), 1, &mut link_node) }, - OAKNODE_E_NOT_FOUND - ); - assert_eq!( - unsafe { oaknode_node_unlink(dup(&a), dup(&b), &mut linked) }, - OAKNODE_OK - ); - assert_eq!(linked, 1); - assert_eq!( - unsafe { oaknode_node_unlink(dup(&a), dup(&b), &mut linked) }, - OAKNODE_OK - ); - assert_eq!(linked, 0, "already unlinked"); - - // Context positions. - assert_eq!( - unsafe { oaknode_node_set_context_position(dup(&a), dup(&b), 10.0, 20.0, 1) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_node_context_count(dup(&a), &mut count) }, - OAKNODE_OK - ); - assert_eq!(count, 1); - let mut x = 0.0; - let mut y = 0.0; - let mut expanded = 0; - assert_eq!( - unsafe { - oaknode_node_get_context_position(dup(&a), dup(&b), &mut x, &mut y, &mut expanded) - }, - OAKNODE_OK - ); - assert_eq!((x, y, expanded), (10.0, 20.0, 1)); - assert_eq!( - unsafe { - oaknode_node_get_context_position(dup(&a), dup(&a), &mut x, &mut y, &mut expanded) - }, - OAKNODE_E_NOT_FOUND - ); - let mut ctx_node = CHandle::null(); - assert_eq!( - unsafe { oaknode_node_context_node_at(dup(&a), 0, &mut ctx_node) }, - OAKNODE_OK - ); - assert!(!ctx_node.ctx.is_null()); - assert_eq!( - unsafe { oaknode_node_remove_from_context(dup(&a), dup(&b)) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_node_remove_from_context(dup(&a), dup(&b)) }, - OAKNODE_E_NOT_FOUND - ); - - // get_project. - let mut proj = CHandle::null(); - assert_eq!( - unsafe { oaknode_node_get_project(dup(&a), &mut proj) }, - OAKNODE_OK - ); - assert!(!proj.ctx.is_null()); - assert_eq!( - unsafe { oaknode_node_get_project(CHandle::null(), &mut proj) }, - OAKNODE_E_INVALID - ); - - // Array input insert/remove on a non-array input fails E_INVALID; - // on an unknown input E_NOT_FOUND. - assert_eq!( - unsafe { oaknode_node_input_array_insert(dup(&a), cs("param_a_in").as_ptr(), 0) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_node_input_array_insert(dup(&a), cs("nope").as_ptr(), 0) }, - OAKNODE_E_NOT_FOUND - ); - - // copy_inputs: dst (matrix) takes src (math)'s values where the ids - // match (enabled_in only) and preserves its own otherwise. - assert_eq!( - unsafe { oaknode_node_copy_inputs(dup(&b), dup(&a), 0) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_node_copy_inputs(CHandle::null(), dup(&a), 0) }, - OAKNODE_E_INVALID - ); - - // set_value_hint_track. - assert_eq!( - unsafe { oaknode_node_set_value_hint_track(dup(&a), cs("param_a_in").as_ptr(), 0, 1) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_node_set_value_hint_track(dup(&a), cs("nope").as_ptr(), 0, 1) }, - OAKNODE_E_NOT_FOUND - ); - - // get_input_at_time: no keyframes -> standard value. - let mut at = OakNodeValue::none(); - assert_eq!( - unsafe { - oaknode_node_get_input_at_time(dup(&a), cs("param_a_in").as_ptr(), 5, 1, &mut at) - }, - OAKNODE_OK - ); - assert_eq!(at.kind, oak::FLOAT); - assert!((at.f[0] - 3.5).abs() < 1e-9); - assert_eq!( - unsafe { oaknode_node_get_input_at_time(dup(&a), cs("nope").as_ptr(), 5, 1, &mut at) }, - OAKNODE_E_NOT_FOUND - ); - assert_eq!( - unsafe { - oaknode_node_get_input_at_time( - dup(&a), - cs("param_a_in").as_ptr(), - 5, - 1, - std::ptr::null_mut(), - ) - }, - OAKNODE_E_INVALID - ); - - // create_copy of a graph-owned node. - let mut copy = unsafe { oaknode_node_create_copy(dup(&a)) }; - assert!(!copy.ctx.is_null()); - let copy_id = - two_stage(|buf, size| unsafe { oaknode_node_get_id(dup(©), buf, size) }).unwrap(); - assert_eq!(copy_id, "org.olivevideoeditor.Olive.math"); - - // Cleanup (free every borrowed view and the owned nodes). - unsafe { oaknode_node_free(&mut copy) }; - unsafe { oaknode_node_free(&mut src) }; - unsafe { oaknode_node_free(&mut node_out) }; - unsafe { oaknode_node_free(&mut link_node) }; - unsafe { oaknode_node_free(&mut ctx_node) }; - unsafe { oaknode_node_free(&mut proj) }; - unsafe { oaknode_node_free(&mut tf) }; - unsafe { oaknode_node_free(&mut b) }; - unsafe { oaknode_node_free(&mut a) }; - unsafe { oaknode_project_free(&mut p) }; -} - -/// Factory family + the pure keyframe helper. -#[test] -fn factory_and_keyframe_helpers() { - let _g = ffi_lock(); - assert_eq!(unsafe { oaknode_factory_initialize() }, OAKNODE_OK); - assert_eq!( - unsafe { oaknode_factory_initialize() }, - OAKNODE_OK, - "idempotent" - ); - - let mut count = 0; - assert_eq!(unsafe { oaknode_factory_id_count(&mut count) }, OAKNODE_OK); - assert!(count >= 4, "at least the implemented node types registered"); - - let first = two_stage(|buf, size| unsafe { oaknode_factory_id_at(0, buf, size) }).unwrap(); - assert_eq!(first, "org.olivevideoeditor.Olive.polygon"); - assert_eq!( - unsafe { oaknode_factory_id_at(-1, std::ptr::null_mut(), 0) }, - OAKNODE_E_NOT_FOUND - ); - assert_eq!( - unsafe { oaknode_factory_id_at(count, std::ptr::null_mut(), 0) }, - OAKNODE_E_NOT_FOUND - ); - - let name = two_stage(|buf, size| unsafe { - oaknode_factory_name_from_id(cs("org.olivevideoeditor.Olive.pan").as_ptr(), buf, size) - }) - .unwrap(); - assert_eq!(name, "Pan"); - let missing = two_stage(|buf, size| unsafe { - oaknode_factory_name_from_id(cs("org.example.missing").as_ptr(), buf, size) - }) - .unwrap(); - assert_eq!(missing, "", "unknown id -> empty string"); - - // create_from_id: known -> valid, unknown -> empty. - let mut node = unsafe { - oaknode_factory_create_from_id(cs("org.olivevideoeditor.Olive.solidgenerator").as_ptr()) - }; - assert!(!node.ctx.is_null()); - assert!( - unsafe { oaknode_factory_create_from_id(cs("org.example.missing").as_ptr()) } - .ctx - .is_null() - ); - assert!(unsafe { oaknode_factory_create_from_id(std::ptr::null()) } - .ctx - .is_null()); - - // node_at: borrowed prototype (index 5 = math, whose constructor is - // implemented; prototypes at todo!()-constructor indices are only - // reachable once Phase 3 lands); out-of-range -> E_NOT_FOUND. - let mut proto = CHandle::null(); - assert_eq!( - unsafe { oaknode_factory_node_at(5, &mut proto) }, - OAKNODE_OK - ); - assert!(!proto.ctx.is_null()); - assert_eq!( - unsafe { oaknode_factory_node_at(count, &mut proto) }, - OAKNODE_E_NOT_FOUND - ); - - // Pure keyframe helper: IN(0) <-> OUT(1), invalid -> E_INVALID. - assert_eq!(unsafe { oaknode_keyframe_opposing_bezier_type(0) }, 1); - assert_eq!(unsafe { oaknode_keyframe_opposing_bezier_type(1) }, 0); - assert_eq!( - unsafe { oaknode_keyframe_opposing_bezier_type(2) }, - OAKNODE_E_INVALID - ); - - unsafe { oaknode_node_free(&mut node) }; - unsafe { oaknode_node_free(&mut proto) }; -} diff --git a/crates/oaknode/tests/ffi_timeline_test.rs b/crates/oaknode/tests/ffi_timeline_test.rs deleted file mode 100644 index 23067a7cc..000000000 --- a/crates/oaknode/tests/ffi_timeline_test.rs +++ /dev/null @@ -1,2584 +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 . - -//! Phase-2/3 ffi family contract tests: sequence, track, block, folder, -//! footage, colormanager, traverser, serializer, the undoable node -//! exports, the multicam grid family and the deferred bridge exports. -//! One success + one failure path per family. - -use std::ffi::{c_char, c_int, CString}; - -use oaknode::error::{OAKNODE_E_INVALID, OAKNODE_E_NOT_FOUND, OAKNODE_E_STATE, OAKNODE_OK}; -use oaknode::ffi::block::{ - oaknode_block_as_node, oaknode_block_clip_create, oaknode_block_free, oaknode_block_from_node, - oaknode_block_gap_create, oaknode_block_get_enabled, oaknode_block_get_kind, - oaknode_block_get_length, oaknode_block_get_out, oaknode_block_set_enabled, - oaknode_block_set_out, oaknode_clip_get_media_in, oaknode_clip_get_speed, - oaknode_clip_set_media_in, oaknode_clip_set_speed, oaknode_transition_get_in_offset, - oaknode_transition_set_offsets_and_length, -}; -use oaknode::ffi::colormanager::{ - oaknode_colormanager_get_compliant_color_transform, oaknode_colormanager_get_config_filename, - oaknode_colormanager_get_display_count, oaknode_colormanager_get_native, - oaknode_colormanager_init, oaknode_colormanager_initialize, - oaknode_colormanager_set_config_filename, -}; -use oaknode::ffi::dragger::{ - oaknode_dragger_create, oaknode_dragger_drag, oaknode_dragger_end, oaknode_dragger_free, - oaknode_dragger_is_started, oaknode_dragger_start, -}; -use oaknode::ffi::folder::{ - oaknode_folder_add_child, oaknode_folder_as_node, oaknode_folder_child_at, - oaknode_folder_child_count, oaknode_folder_create, oaknode_folder_has_child_recursive, - oaknode_folder_index_of_child, oaknode_folder_parent_of, oaknode_folder_remove_child, -}; -use oaknode::ffi::footage::{ - oaknode_footage_as_node, oaknode_footage_clear_proxy, oaknode_footage_create, - oaknode_footage_filename, oaknode_footage_get_video_params, oaknode_footage_is_valid, - oaknode_footage_proxy_path, oaknode_footage_set_filename, oaknode_footage_set_proxy, - oaknode_footage_set_video_params, -}; -use oaknode::ffi::group::{ - oaknode_group_add_input_passthrough, oaknode_group_add_input_passthrough_undoable, - oaknode_group_cast, oaknode_group_create, oaknode_group_free, - oaknode_group_get_output_passthrough, oaknode_group_passthrough_count, - oaknode_group_passthrough_id_at, oaknode_group_passthrough_input_at, - oaknode_group_remove_input_passthrough, oaknode_group_resolve_input, - oaknode_group_set_output_passthrough, oaknode_group_set_output_passthrough_undoable, -}; -use oaknode::ffi::keyframe::{ - oaknode_keyframe_compute_paste_value, oaknode_keyframe_create, oaknode_keyframe_free, - oaknode_keyframe_get_bezier_control, oaknode_keyframe_get_element, oaknode_keyframe_get_input, - oaknode_keyframe_get_parent, oaknode_keyframe_get_time, oaknode_keyframe_get_track, - oaknode_keyframe_get_type, oaknode_keyframe_get_valid_bezier_control, - oaknode_keyframe_get_value, oaknode_keyframe_get_value_string, - oaknode_keyframe_has_sibling_at_time, oaknode_keyframe_opposing_bezier_type, - oaknode_keyframe_set_bezier_control, oaknode_keyframe_set_bezier_control_undoable, - oaknode_keyframe_set_time, oaknode_keyframe_set_time_undoable, oaknode_keyframe_set_type, - oaknode_keyframe_set_type_undoable, oaknode_keyframe_set_value, - oaknode_keyframe_set_value_string, oaknode_keyframe_set_value_string_undoable, - oaknode_keyframe_set_value_undoable, -}; -use oaknode::ffi::multicam::{ - oaknode_multicam_get_current_source, oaknode_multicam_get_rows_and_columns, - oaknode_multicam_get_source_count, oaknode_multicam_index_to_row_cols, - oaknode_multicam_input_current, oaknode_multicam_input_sequence, - oaknode_multicam_input_sequence_type, oaknode_multicam_input_sources, - oaknode_multicam_rows_cols_to_index, -}; -use oaknode::ffi::node::{ - oaknode_node_connect, oaknode_node_connect_undoable, oaknode_node_disconnect, - oaknode_node_disconnect_undoable, oaknode_node_find_input_footage, oaknode_node_get_input, - oaknode_node_get_input_at_time, oaknode_node_get_markers, oaknode_node_get_video_frame_cache, - oaknode_node_get_work_area, oaknode_node_set_enabled_undoable, - oaknode_node_set_input_at_time_undoable, oaknode_node_set_input_undoable, - oaknode_node_set_label_undoable, oaknode_viewer_set_audio_params, - oaknode_viewer_set_video_params, -}; -use oaknode::ffi::project::{ - oaknode_project_add_node, oaknode_project_free, oaknode_project_init, - oaknode_project_initialize, oaknode_project_node_at, -}; -use oaknode::ffi::sequence::{ - oaknode_sequence_as_node, oaknode_sequence_create, oaknode_sequence_free, - oaknode_sequence_from_node, oaknode_sequence_get_audio_params, - oaknode_sequence_get_audio_stream_count, oaknode_sequence_get_playhead, - oaknode_sequence_get_track_count, oaknode_sequence_get_track_list, - oaknode_sequence_get_video_params, oaknode_sequence_get_video_stream_count, - oaknode_sequence_set_audio_params, oaknode_sequence_set_default_parameters, - oaknode_sequence_set_playhead, oaknode_sequence_set_video_params, - oaknode_sequence_verify_length, -}; -use oaknode::ffi::serializer::{ - oaknode_serializer_initialize, oaknode_serializer_load_from_file, - oaknode_serializer_load_from_xml, oaknode_serializer_loaddata_free, - oaknode_serializer_loaddata_node_at, oaknode_serializer_loaddata_node_count, - oaknode_serializer_save_to_file, oaknode_serializer_save_to_xml, - oaknode_serializer_savedata_create, oaknode_serializer_savedata_free, -}; -use oaknode::ffi::track::{ - oaknode_track_append_block, oaknode_track_as_node, oaknode_track_create, oaknode_track_free, - oaknode_track_get_block_at, oaknode_track_get_block_count, oaknode_track_get_height, - oaknode_track_get_index, oaknode_track_get_length, oaknode_track_get_muted, - oaknode_track_get_reference, oaknode_track_get_type, oaknode_track_prepend_block, - oaknode_track_ripple_remove_block, oaknode_track_set_height, oaknode_track_set_index, - oaknode_track_set_muted, oaknode_tracklist_add_track, oaknode_tracklist_array_append, - oaknode_tracklist_get_array_size, oaknode_tracklist_get_track_at, - oaknode_tracklist_get_track_count, oaknode_tracklist_get_track_input_id, - oaknode_tracklist_get_type, oaknode_tracklist_remove_track, -}; -use oaknode::ffi::traverser::{ - oaknode_traverser_database_free, oaknode_traverser_database_row_count, - oaknode_traverser_generate_database, oaknode_traverser_init, -}; -use oaknode::handle::CHandle; -use oaknode::value::{oak, OakNodeValue}; - -static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); - -fn lock() -> std::sync::MutexGuard<'static, ()> { - LOCK.lock().unwrap_or_else(|e| e.into_inner()) -} - -fn cs(s: &str) -> CString { - CString::new(s).unwrap() -} - -fn dup(h: &CHandle) -> CHandle { - h.clone() -} - -fn two_stage i32>(mut getter: F) -> Result { - let needed = getter(std::ptr::null_mut(), 0); - if needed < 0 { - return Err(needed); - } - let mut buf = vec![0u8; needed as usize]; - let rc = getter(buf.as_mut_ptr() as *mut c_char, needed); - if rc < 0 { - return Err(rc); - } - buf.pop(); - Ok(String::from_utf8(buf).unwrap()) -} - -fn free(h: &mut CHandle) { - if !h.ctx.is_null() { - unsafe { (h.release.unwrap())(h.ctx) }; - h.ctx = std::ptr::null_mut(); - } -} - -/// Folder family: create/add/remove/query + failure paths. -#[test] -fn folder_family() { - let _g = lock(); - let mut p = unsafe { oaknode_project_init() }; - assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK); - - let mut folder = unsafe { oaknode_folder_create(dup(&p)) }; - assert!(!folder.ctx.is_null()); - assert!(unsafe { oaknode_folder_create(CHandle::null()) } - .ctx - .is_null()); - - let mut root = unsafe { oaknode_project_node_at(dup(&p), 0) }; - let mut node = unsafe { oaknode_folder_as_node(dup(&folder)) }; - assert!(!node.ctx.is_null()); - - // Add a subfolder as a child of the root, then re-parent it. - let mut child = unsafe { oaknode_folder_create(dup(&p)) }; - assert_eq!( - unsafe { oaknode_folder_add_child(dup(&root), dup(&child)) }, - OAKNODE_OK - ); - assert_eq!(unsafe { oaknode_folder_child_count(dup(&root)) }, 1); - let mut child_at = unsafe { oaknode_folder_child_at(dup(&root), 0) }; - assert!(!child_at.ctx.is_null()); - assert!(unsafe { oaknode_folder_child_at(dup(&root), 5) } - .ctx - .is_null()); - assert_eq!( - unsafe { oaknode_folder_index_of_child(dup(&root), dup(&child)) }, - 0 - ); - assert_eq!( - unsafe { oaknode_folder_index_of_child(dup(&root), dup(&folder)) }, - OAKNODE_E_NOT_FOUND - ); - assert_eq!( - unsafe { oaknode_folder_has_child_recursive(dup(&root), dup(&child)) }, - 1 - ); - let mut parent = unsafe { oaknode_folder_parent_of(dup(&child)) }; - assert!(!parent.ctx.is_null()); - assert!(unsafe { oaknode_folder_parent_of(dup(&folder)) } - .ctx - .is_null()); - - // Double-add to another folder fails E_STATE. - let mut other = unsafe { oaknode_folder_create(dup(&p)) }; - assert_eq!( - unsafe { oaknode_folder_add_child(dup(&other), dup(&child)) }, - OAKNODE_E_STATE - ); - assert_eq!( - unsafe { oaknode_folder_remove_child(dup(&root), dup(&child)) }, - OAKNODE_OK - ); - assert_eq!(unsafe { oaknode_folder_child_count(dup(&root)) }, 0); - assert_eq!( - unsafe { oaknode_folder_remove_child(dup(&root), dup(&child)) }, - OAKNODE_E_NOT_FOUND - ); - - free(&mut parent); - free(&mut child_at); - free(&mut child); - free(&mut other); - free(&mut node); - free(&mut folder); - free(&mut root); - unsafe { oaknode_project_free(&mut p) }; -} - -/// Sequence family: create → track lists, default params, playhead, -/// lengths + failure paths. -#[test] -fn sequence_family() { - let _g = lock(); - let mut seq = unsafe { oaknode_sequence_create() }; - assert!(!seq.ctx.is_null()); - let mut node = unsafe { oaknode_sequence_as_node(dup(&seq)) }; - assert!(!node.ctx.is_null()); - let back = unsafe { oaknode_sequence_from_node(dup(&node)) }; - assert!(!back.ctx.is_null()); - let _ = back; - - // Default parameters: one video + one audio stream. - assert_eq!( - unsafe { oaknode_sequence_set_default_parameters(dup(&seq)) }, - OAKNODE_OK - ); - let mut vc = 0; - assert_eq!( - unsafe { oaknode_sequence_get_audio_stream_count(dup(&seq), &mut vc) }, - OAKNODE_OK - ); - assert_eq!(vc, 1); - - // Track lists exist with zero tracks. - let mut list = CHandle::null(); - assert_eq!( - unsafe { oaknode_sequence_get_track_list(dup(&seq), 0, &mut list) }, - OAKNODE_OK - ); - assert!(!list.ctx.is_null()); - assert_eq!( - unsafe { oaknode_sequence_get_track_list(dup(&seq), 99, &mut list) }, - OAKNODE_E_NOT_FOUND - ); - let mut tc = 0; - assert_eq!( - unsafe { oaknode_sequence_get_track_count(dup(&seq), 0, &mut tc) }, - OAKNODE_OK - ); - assert_eq!(tc, 0); - - // Playhead round-trip. - assert_eq!( - unsafe { oaknode_sequence_set_playhead(dup(&seq), 30, 1) }, - OAKNODE_OK - ); - let mut num = 0; - let mut den = 0; - assert_eq!( - unsafe { oaknode_sequence_get_playhead(dup(&seq), &mut num, &mut den) }, - OAKNODE_OK - ); - assert_eq!((num, den), (30, 1)); - - // verify_length on an empty sequence. - assert_eq!( - unsafe { oaknode_sequence_verify_length(dup(&seq)) }, - OAKNODE_OK - ); - - // Failure paths on empty handles. - assert_eq!( - unsafe { oaknode_sequence_get_playhead(CHandle::null(), &mut num, &mut den) }, - OAKNODE_E_INVALID - ); - assert!(unsafe { oaknode_sequence_from_node(CHandle::null()) } - .ctx - .is_null()); - - free(&mut list); - free(&mut node); - unsafe { oaknode_sequence_free(&mut seq) }; -} - -/// Track + tracklist family: block ordering primitives, track flags, -/// add/remove tracks + failure paths. -#[test] -fn track_family() { - let _g = lock(); - let mut track = unsafe { oaknode_track_create(0) }; // video - assert!(!track.ctx.is_null()); - assert!(unsafe { oaknode_track_create(99) }.ctx.is_null()); - - let mut ty = -1; - assert_eq!( - unsafe { oaknode_track_get_type(dup(&track), &mut ty) }, - OAKNODE_OK - ); - assert_eq!(ty, 0); - let mut node = unsafe { oaknode_track_as_node(dup(&track)) }; - assert!(!node.ctx.is_null()); - - // Height (internal <-> pixels). - let mut h = 0.0; - assert_eq!( - unsafe { oaknode_track_get_height(dup(&track), &mut h) }, - OAKNODE_OK - ); - assert!((h - 3.0).abs() < 1e-9); - assert_eq!( - unsafe { oaknode_track_set_height(dup(&track), 5.0) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_track_get_height(dup(&track), &mut h) }, - OAKNODE_OK - ); - assert!((h - 5.0).abs() < 1e-9); - - // Muted/locked/index. - assert_eq!( - unsafe { oaknode_track_set_muted(dup(&track), 1) }, - OAKNODE_OK - ); - let mut muted = 0; - assert_eq!( - unsafe { oaknode_track_get_muted(dup(&track), &mut muted) }, - OAKNODE_OK - ); - assert_eq!(muted, 1); - assert_eq!( - unsafe { oaknode_track_set_index(dup(&track), 2) }, - OAKNODE_OK - ); - let mut idx = 0; - assert_eq!( - unsafe { oaknode_track_get_index(dup(&track), &mut idx) }, - OAKNODE_OK - ); - assert_eq!(idx, 2); - let mut rty = 0; - let mut ridx = 0; - assert_eq!( - unsafe { oaknode_track_get_reference(dup(&track), &mut rty, &mut ridx) }, - OAKNODE_OK - ); - assert_eq!((rty, ridx), (0, 2)); - - // Blocks: append two clips, prepend one, count, remove. - let mut clip1 = unsafe { oaknode_block_clip_create() }; - let mut clip2 = unsafe { oaknode_block_clip_create() }; - let mut clip3 = unsafe { oaknode_block_clip_create() }; - assert_eq!( - unsafe { oaknode_track_append_block(dup(&track), dup(&clip1)) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_track_append_block(dup(&track), dup(&clip2)) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_track_prepend_block(dup(&track), dup(&clip3)) }, - OAKNODE_OK - ); - let mut bc = 0; - assert_eq!( - unsafe { oaknode_track_get_block_count(dup(&track), &mut bc) }, - OAKNODE_OK - ); - assert_eq!(bc, 3); - let mut blk = CHandle::null(); - assert_eq!( - unsafe { oaknode_track_get_block_at(dup(&track), 0, &mut blk) }, - OAKNODE_OK - ); - assert!(!blk.ctx.is_null()); - assert_eq!( - unsafe { oaknode_track_get_block_at(dup(&track), 5, &mut blk) }, - OAKNODE_E_NOT_FOUND - ); - assert_eq!( - unsafe { oaknode_track_ripple_remove_block(dup(&track), dup(&clip1)) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_track_get_block_count(dup(&track), &mut bc) }, - OAKNODE_OK - ); - assert_eq!(bc, 2); - - // Track length via the blocks' ranges. - let mut len_num = 0; - let mut len_den = 0; - assert_eq!( - unsafe { oaknode_track_get_length(dup(&track), &mut len_num, &mut len_den) }, - OAKNODE_OK - ); - assert_eq!(len_num, 1, "blocks default to 1s length"); - - // Tracklist: build a sequence to host the list. - let mut seq = unsafe { oaknode_sequence_create() }; - let mut snode = unsafe { oaknode_sequence_as_node(dup(&seq)) }; - let mut list2 = CHandle::null(); - assert_eq!( - unsafe { oaknode_sequence_get_track_list(dup(&seq), 0, &mut list2) }, - OAKNODE_OK - ); - let mut lt = 0; - assert_eq!( - unsafe { oaknode_tracklist_get_type(dup(&list2), &mut lt) }, - OAKNODE_OK - ); - assert_eq!(lt, 0); - let input_id = - two_stage(|b, s| unsafe { oaknode_tracklist_get_track_input_id(dup(&list2), b, s) }) - .unwrap(); - assert_eq!(input_id, "track_in_0"); - assert_eq!( - unsafe { oaknode_tracklist_array_append(dup(&list2)) }, - OAKNODE_OK - ); - let mut asz = 0; - assert_eq!( - unsafe { oaknode_tracklist_get_array_size(dup(&list2), &mut asz) }, - OAKNODE_OK - ); - assert_eq!(asz, 1); - - // Add the track to the list. - assert_eq!( - unsafe { oaknode_tracklist_add_track(dup(&list2), dup(&track)) }, - OAKNODE_OK - ); - let mut tcount = 0; - assert_eq!( - unsafe { oaknode_tracklist_get_track_count(dup(&list2), &mut tcount) }, - OAKNODE_OK - ); - assert_eq!(tcount, 1); - let mut track_at = CHandle::null(); - assert_eq!( - unsafe { oaknode_tracklist_get_track_at(dup(&list2), 0, &mut track_at) }, - OAKNODE_OK - ); - assert!(!track_at.ctx.is_null()); - assert_eq!( - unsafe { oaknode_tracklist_remove_track(dup(&list2), dup(&track)) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_tracklist_remove_track(dup(&list2), dup(&track)) }, - OAKNODE_E_NOT_FOUND - ); - - free(&mut track_at); - free(&mut list2); - free(&mut snode); - unsafe { oaknode_sequence_free(&mut seq) }; - free(&mut blk); - free(&mut clip1); - free(&mut clip2); - free(&mut clip3); - free(&mut node); - unsafe { oaknode_track_free(&mut track) }; -} - -/// Block family: clip/gap creation, kind, in/out/length, media_in, -/// speed + failure paths. -#[test] -fn block_family() { - let _g = lock(); - let mut clip = unsafe { oaknode_block_clip_create() }; - let mut gap = unsafe { oaknode_block_gap_create() }; - assert!(!clip.ctx.is_null()); - assert!(!gap.ctx.is_null()); - - let mut kind = 0; - assert_eq!( - unsafe { oaknode_block_get_kind(dup(&clip), &mut kind) }, - OAKNODE_OK - ); - assert_eq!(kind, 1, "clip kind"); - assert_eq!( - unsafe { oaknode_block_get_kind(dup(&gap), &mut kind) }, - OAKNODE_OK - ); - assert_eq!(kind, 2, "gap kind"); - - let mut node = unsafe { oaknode_block_as_node(dup(&clip)) }; - assert!(!node.ctx.is_null()); - let mut back = unsafe { oaknode_block_from_node(dup(&node)) }; - assert!(!back.ctx.is_null()); - assert!(unsafe { oaknode_block_from_node(CHandle::null()) } - .ctx - .is_null()); - - // Length / out round-trip. - let mut len_num = 0; - let mut len_den = 0; - assert_eq!( - unsafe { oaknode_block_get_length(dup(&clip), &mut len_num, &mut len_den) }, - OAKNODE_OK - ); - assert_eq!((len_num, len_den), (1, 1)); - assert_eq!( - unsafe { oaknode_block_set_out(dup(&clip), 5, 1) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_block_get_out(dup(&clip), &mut len_num, &mut len_den) }, - OAKNODE_OK - ); - assert_eq!((len_num, len_den), (5, 1)); - - // Enabled. - let mut en = 0; - assert_eq!( - unsafe { oaknode_block_get_enabled(dup(&clip), &mut en) }, - OAKNODE_OK - ); - assert_eq!(en, 1); - assert_eq!( - unsafe { oaknode_block_set_enabled(dup(&clip), 0) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_block_get_enabled(dup(&clip), &mut en) }, - OAKNODE_OK - ); - assert_eq!(en, 0); - - // Clip media_in/speed. - let mut min = 0; - let mut mden = 0; - assert_eq!( - unsafe { oaknode_clip_get_media_in(dup(&clip), &mut min, &mut mden) }, - OAKNODE_OK - ); - assert_eq!((min, mden), (0, 1)); - assert_eq!( - unsafe { oaknode_clip_set_media_in(dup(&clip), 10, 1) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_clip_get_media_in(dup(&clip), &mut min, &mut mden) }, - OAKNODE_OK - ); - assert_eq!((min, mden), (10, 1)); - let mut speed = 0.0; - assert_eq!( - unsafe { oaknode_clip_get_speed(dup(&clip), &mut speed) }, - OAKNODE_OK - ); - assert_eq!(speed, 1.0); - assert_eq!( - unsafe { oaknode_clip_set_speed(dup(&clip), 2.0) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_clip_get_speed(dup(&clip), &mut speed) }, - OAKNODE_OK - ); - assert_eq!(speed, 2.0); - - // Transition offsets (non-transition clip -> E_INVALID). - assert_eq!( - unsafe { oaknode_transition_get_in_offset(dup(&clip), &mut min, &mut mden) }, - OAKNODE_E_STATE - ); - - // Failure paths. - assert_eq!( - unsafe { oaknode_block_get_length(CHandle::null(), &mut len_num, &mut len_den) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_block_get_kind(CHandle::null(), &mut kind) }, - OAKNODE_E_INVALID - ); - - free(&mut back); - free(&mut node); - free(&mut clip); - free(&mut gap); -} - -/// Footage family: create/filename/proxy + failure paths. -#[test] -fn footage_family() { - let _g = lock(); - let mut p = unsafe { oaknode_project_init() }; - assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK); - - let mut footage = unsafe { oaknode_footage_create(dup(&p), cs("/tmp/media.mov").as_ptr()) }; - assert!(!footage.ctx.is_null()); - assert!( - unsafe { oaknode_footage_create(CHandle::null(), cs("x").as_ptr()) } - .ctx - .is_null() - ); - - // Filename round-trip (two-stage). - let name = two_stage(|b, s| unsafe { oaknode_footage_filename(dup(&footage), b, s) }).unwrap(); - assert_eq!(name, "/tmp/media.mov"); - assert_eq!( - unsafe { oaknode_footage_set_filename(dup(&footage), cs("/new.mov").as_ptr()) }, - OAKNODE_OK - ); - let name = two_stage(|b, s| unsafe { oaknode_footage_filename(dup(&footage), b, s) }).unwrap(); - assert_eq!(name, "/new.mov"); - assert_eq!( - unsafe { oaknode_footage_filename(CHandle::null(), std::ptr::null_mut(), 0) }, - OAKNODE_E_INVALID - ); - - // Not probed -> invalid. - assert_eq!(unsafe { oaknode_footage_is_valid(dup(&footage)) }, 0); - - // as_node cast. - let mut node = unsafe { oaknode_footage_as_node(dup(&footage)) }; - assert!(!node.ctx.is_null()); - - // Proxy fields. - assert_eq!( - unsafe { oaknode_footage_set_proxy(dup(&footage), cs("/proxy.mov").as_ptr(), 2, 0, 1, 1,) }, - OAKNODE_OK - ); - let proxy = - two_stage(|b, s| unsafe { oaknode_footage_proxy_path(dup(&footage), b, s) }).unwrap(); - assert_eq!(proxy, "/proxy.mov"); - assert_eq!( - unsafe { oaknode_footage_clear_proxy(dup(&footage)) }, - OAKNODE_OK - ); - let proxy = - two_stage(|b, s| unsafe { oaknode_footage_proxy_path(dup(&footage), b, s) }).unwrap(); - assert_eq!(proxy, ""); - - free(&mut node); - free(&mut footage); - unsafe { oaknode_project_free(&mut p) }; -} - -/// Colormanager family: state + config-dependent E_STATE paths. -#[test] -fn colormanager_family() { - let _g = lock(); - let mut p = unsafe { oaknode_project_init() }; - let mut cm = unsafe { oaknode_colormanager_init(dup(&p)) }; - assert!(!cm.ctx.is_null()); - - // State without a config: listings return E_STATE. - let mut count = 0; - assert_eq!( - unsafe { oaknode_colormanager_get_display_count(dup(&cm), &mut count) }, - OAKNODE_E_STATE - ); - - // Filename round-trip. - assert_eq!( - unsafe { oaknode_colormanager_set_config_filename(dup(&cm), cs("/cfg.ocio").as_ptr()) }, - OAKNODE_OK - ); - let name = - two_stage(|b, s| unsafe { oaknode_colormanager_get_config_filename(dup(&cm), b, s) }) - .unwrap(); - assert_eq!(name, "/cfg.ocio"); - assert_eq!( - unsafe { - oaknode_colormanager_get_config_filename(CHandle::null(), std::ptr::null_mut(), 0) - }, - OAKNODE_E_INVALID - ); - - // initialize marks the config loaded (built-in defaults without - // oakrender). - assert_eq!( - unsafe { oaknode_colormanager_initialize(dup(&cm)) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_colormanager_get_display_count(dup(&cm), &mut count) }, - OAKNODE_OK - ); - assert_eq!(count, 1, "built-in default display"); - - // get_native is a C++-only bridge export: NULL in the Rust port - // (NULL-safe), for both a live and an empty handle. - assert!(unsafe { oaknode_colormanager_get_native(dup(&cm)) }.is_null()); - assert!(unsafe { oaknode_colormanager_get_native(CHandle::null()) }.is_null()); - - free(&mut cm); - unsafe { oaknode_project_free(&mut p) }; -} - -/// Traverser family: database generation + row enumeration + failures. -#[test] -fn traverser_family() { - let _g = lock(); - let mut p = unsafe { oaknode_project_init() }; - assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK); - - // A math node in the project. - let mut math = unsafe { - oaknode::ffi::factory::oaknode_factory_create_from_id( - cs("org.olivevideoeditor.Olive.math").as_ptr(), - ) - }; - assert_eq!( - unsafe { oaknode_project_add_node(dup(&p), dup(&math)) }, - OAKNODE_OK - ); - - let mut tr = unsafe { oaknode_traverser_init() }; - assert!(!tr.ctx.is_null()); - - let mut db = CHandle::null(); - assert_eq!( - unsafe { oaknode_traverser_generate_database(dup(&tr), dup(&math), 0, 1, 1, 1, &mut db) }, - OAKNODE_OK - ); - assert!(!db.ctx.is_null()); - - let mut rows = 0; - assert_eq!( - unsafe { oaknode_traverser_database_row_count(dup(&db), &mut rows) }, - OAKNODE_OK - ); - assert_eq!(rows, 1, "no input edges, so only the root row"); - // Failure: unknown database handle. - assert_eq!( - unsafe { oaknode_traverser_database_row_count(CHandle::null(), &mut rows) }, - OAKNODE_E_INVALID - ); - - unsafe { oaknode_traverser_database_free(&mut db) }; - free(&mut tr); - free(&mut math); - unsafe { oaknode_project_free(&mut p) }; -} - -/// Serializer family: initialize/save-to-xml/load round-trip + failures. -/// -/// Needs the `test-stubs` feature: `save_to_xml`/`load_from_xml` route -/// through the oakcommon XML bridge, whose symbols only resolve in the -/// test binary when the in-crate stubs are compiled in. -#[cfg(feature = "test-stubs")] -#[test] -fn serializer_family() { - let _g = lock(); - assert_eq!(unsafe { oaknode_serializer_initialize() }, OAKNODE_OK); - - let mut p = unsafe { oaknode_project_init() }; - assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK); - - let mut sd = unsafe { oaknode_serializer_savedata_create(1, dup(&p)) }; // LOAD_ONLY_NODES - assert!(!sd.ctx.is_null()); - - let xml = two_stage(|b, s| unsafe { oaknode_serializer_save_to_xml(dup(&sd), b, s) }).unwrap(); - assert!(xml.contains(" SERIALIZER_XML_ERROR result, empty load data. - let mut result2 = -1; - let mut ld2 = CHandle::null(); - let corrupt = cs("<< 0. - let mut sib = -1; - assert_eq!( - unsafe { oaknode_keyframe_has_sibling_at_time(dup(&kf), 1, 25, &mut sib) }, - OAKNODE_OK - ); - assert_eq!(sib, 0); - assert_eq!( - unsafe { oaknode_keyframe_has_sibling_at_time(CHandle::null(), 1, 25, &mut sib) }, - OAKNODE_E_INVALID - ); - - // compute_paste_value against the parent's input (declared float). - let mut pv = OakNodeValue::none(); - assert_eq!( - unsafe { oaknode_keyframe_compute_paste_value(dup(&math), dup(&kf), &mut pv) }, - OAKNODE_OK - ); - assert_eq!(pv.kind, oak::FLOAT); - // Unknown input on the target -> NOT_FOUND. - let mut kf3 = unsafe { - oaknode_keyframe_create(0, 1, &f(1.0), 0, 0, -1, cs("nope_in").as_ptr(), dup(&math)) - }; - assert_eq!( - unsafe { oaknode_keyframe_compute_paste_value(dup(&math), dup(&kf3), &mut pv) }, - OAKNODE_E_NOT_FOUND - ); - - free(&mut parent); - free(&mut parent2); - unsafe { oaknode_keyframe_free(&mut kf3) }; - unsafe { oaknode_keyframe_free(&mut kf2) }; - unsafe { oaknode_keyframe_free(&mut kf) }; - free(&mut math); - unsafe { oaknode_project_free(&mut p) }; -} - -/// Keyframe handle family, undoable paths: setter commands round-trip -/// through the test-stub undo bridge. -#[cfg(feature = "test-stubs")] -#[test] -fn keyframe_undoable() { - let _g = lock(); - let f = |x: f64| OakNodeValue { - kind: oak::FLOAT, - num: 0, - den: 0, - f: [x, 0.0, 0.0, 0.0], - }; - let mut kf = unsafe { - oaknode_keyframe_create( - 0, - 1, - &f(1.0), - 0, - 0, - -1, - cs("param_a_in").as_ptr(), - CHandle::null(), - ) - }; - - // set_time_undoable. - let mut cmd = CHandle::null(); - assert_eq!( - unsafe { oaknode_keyframe_set_time_undoable(dup(&kf), 5, 1, &mut cmd) }, - OAKNODE_OK - ); - assert!(!cmd.ctx.is_null()); - assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0); - let (mut num, mut den) = (0i64, 0i64); - assert_eq!( - unsafe { oaknode_keyframe_get_time(dup(&kf), &mut num, &mut den) }, - OAKNODE_OK - ); - assert_eq!((num, den), (5, 1)); - assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0); - assert_eq!( - unsafe { oaknode_keyframe_get_time(dup(&kf), &mut num, &mut den) }, - OAKNODE_OK - ); - assert_eq!((num, den), (0, 1)); - unsafe { oakundo_command_free(&mut cmd) }; - - // set_value_undoable. - let mut cmd = CHandle::null(); - assert_eq!( - unsafe { oaknode_keyframe_set_value_undoable(dup(&kf), &f(9.0), &mut cmd) }, - OAKNODE_OK - ); - assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0); - let mut out = OakNodeValue::none(); - assert_eq!( - unsafe { oaknode_keyframe_get_value(dup(&kf), &mut out) }, - OAKNODE_OK - ); - assert_eq!(out.f[0], 9.0); - assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0); - assert_eq!( - unsafe { oaknode_keyframe_get_value(dup(&kf), &mut out) }, - OAKNODE_OK - ); - assert_eq!(out.f[0], 1.0); - unsafe { oakundo_command_free(&mut cmd) }; - - // set_type_undoable (linear -> hold -> back). - let mut cmd = CHandle::null(); - assert_eq!( - unsafe { oaknode_keyframe_set_type_undoable(dup(&kf), 1, &mut cmd) }, - OAKNODE_OK - ); - assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0); - let mut ty = -1; - assert_eq!( - unsafe { oaknode_keyframe_get_type(dup(&kf), &mut ty) }, - OAKNODE_OK - ); - assert_eq!(ty, 1); - assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0); - assert_eq!( - unsafe { oaknode_keyframe_get_type(dup(&kf), &mut ty) }, - OAKNODE_OK - ); - assert_eq!(ty, 0); - unsafe { oakundo_command_free(&mut cmd) }; - - // set_bezier_control_undoable. - let mut cmd = CHandle::null(); - assert_eq!( - unsafe { oaknode_keyframe_set_bezier_control_undoable(dup(&kf), 0, -0.5, 2.0, &mut cmd) }, - OAKNODE_OK - ); - assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0); - let (mut bx, mut by) = (0.0, 0.0); - assert_eq!( - unsafe { oaknode_keyframe_get_bezier_control(dup(&kf), 0, &mut bx, &mut by) }, - OAKNODE_OK - ); - assert_eq!((bx, by), (-0.5, 2.0)); - assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0); - assert_eq!( - unsafe { oaknode_keyframe_get_bezier_control(dup(&kf), 0, &mut bx, &mut by) }, - OAKNODE_OK - ); - assert_eq!((bx, by), (0.0, 0.0)); - unsafe { oakundo_command_free(&mut cmd) }; - - // set_value_string_undoable. - let mut cmd = CHandle::null(); - assert_eq!( - unsafe { - oaknode_keyframe_set_value_string_undoable(dup(&kf), cs("str").as_ptr(), &mut cmd) - }, - OAKNODE_OK - ); - assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0); - let s = - two_stage(|b, sz| unsafe { oaknode_keyframe_get_value_string(dup(&kf), b, sz) }).unwrap(); - assert_eq!(s, "str"); - assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0); - let s = - two_stage(|b, sz| unsafe { oaknode_keyframe_get_value_string(dup(&kf), b, sz) }).unwrap(); - assert_eq!(s, "1", "float 1.0 after undo"); - - unsafe { oaknode_keyframe_free(&mut kf) }; -} - -/// Group family, live paths: create/cast/passthroughs/output/resolve + -/// failures. No undo bridge involved. -#[test] -fn group_family_basic() { - let _g = lock(); - let mut p = unsafe { oaknode_project_init() }; - assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK); - - let mut group = unsafe { oaknode_group_create() }; - assert!(!group.ctx.is_null()); - assert_eq!( - unsafe { oaknode_project_add_node(dup(&p), dup(&group)) }, - OAKNODE_OK - ); - let mut math = unsafe { - oaknode::ffi::factory::oaknode_factory_create_from_id( - cs("org.olivevideoeditor.Olive.math").as_ptr(), - ) - }; - assert_eq!( - unsafe { oaknode_project_add_node(dup(&p), dup(&math)) }, - OAKNODE_OK - ); - - // cast: the group casts; a non-group node and empty handles do not. - let mut cast = unsafe { oaknode_group_cast(dup(&group)) }; - assert!(!cast.ctx.is_null()); - assert!(unsafe { oaknode_group_cast(dup(&math)) }.ctx.is_null()); - assert!(unsafe { oaknode_group_cast(CHandle::null()) }.ctx.is_null()); - free(&mut cast); - - // Add a passthrough for math's param_a_in; repeat returns the same id. - let id = two_stage(|b, s| unsafe { - oaknode_group_add_input_passthrough( - dup(&group), - dup(&math), - cs("param_a_in").as_ptr(), - -1, - b, - s, - ) - }) - .unwrap(); - assert_eq!(id, "param_a_in"); - let id2 = two_stage(|b, s| unsafe { - oaknode_group_add_input_passthrough( - dup(&group), - dup(&math), - cs("param_a_in").as_ptr(), - -1, - b, - s, - ) - }) - .unwrap(); - assert_eq!(id2, "param_a_in"); - // Unknown inner input -> NOT_FOUND. - assert_eq!( - unsafe { - oaknode_group_add_input_passthrough( - dup(&group), - dup(&math), - cs("nope_in").as_ptr(), - -1, - std::ptr::null_mut(), - 0, - ) - }, - OAKNODE_E_NOT_FOUND - ); - // Cross-project node -> INVALID. - let mut p2 = unsafe { oaknode_project_init() }; - assert_eq!(unsafe { oaknode_project_initialize(dup(&p2)) }, OAKNODE_OK); - let mut math2 = unsafe { - oaknode::ffi::factory::oaknode_factory_create_from_id( - cs("org.olivevideoeditor.Olive.math").as_ptr(), - ) - }; - assert_eq!( - unsafe { oaknode_project_add_node(dup(&p2), dup(&math2)) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { - oaknode_group_add_input_passthrough( - dup(&group), - dup(&math2), - cs("param_a_in").as_ptr(), - -1, - std::ptr::null_mut(), - 0, - ) - }, - OAKNODE_E_INVALID - ); - - // count / id_at / input_at. - let mut count = 0; - assert_eq!( - unsafe { oaknode_group_passthrough_count(dup(&group), &mut count) }, - OAKNODE_OK - ); - assert_eq!(count, 1); - let pid = - two_stage(|b, s| unsafe { oaknode_group_passthrough_id_at(dup(&group), 0, b, s) }).unwrap(); - assert_eq!(pid, "param_a_in"); - assert_eq!( - unsafe { oaknode_group_passthrough_id_at(dup(&group), 5, std::ptr::null_mut(), 0) }, - OAKNODE_E_NOT_FOUND - ); - let mut inner_node = CHandle::null(); - let mut inner_el = -9; - let iid = two_stage(|b, s| unsafe { - oaknode_group_passthrough_input_at(dup(&group), 0, &mut inner_node, b, s, &mut inner_el) - }) - .unwrap(); - assert_eq!(iid, "param_a_in"); - assert!(!inner_node.ctx.is_null()); - assert_eq!(inner_el, -1); - free(&mut inner_node); - - // remove_input_passthrough: second removal is NOT_FOUND. - assert_eq!( - unsafe { - oaknode_group_remove_input_passthrough( - dup(&group), - dup(&math), - cs("param_a_in").as_ptr(), - -1, - ) - }, - OAKNODE_OK - ); - assert_eq!( - unsafe { - oaknode_group_remove_input_passthrough( - dup(&group), - dup(&math), - cs("param_a_in").as_ptr(), - -1, - ) - }, - OAKNODE_E_NOT_FOUND - ); - assert_eq!( - unsafe { oaknode_group_passthrough_count(dup(&group), &mut count) }, - OAKNODE_OK - ); - assert_eq!(count, 0); - - // output passthrough set/get + clear with an empty handle. - assert_eq!( - unsafe { oaknode_group_set_output_passthrough(dup(&group), dup(&math)) }, - OAKNODE_OK - ); - let mut out = CHandle::null(); - assert_eq!( - unsafe { oaknode_group_get_output_passthrough(dup(&group), &mut out) }, - OAKNODE_OK - ); - assert!(!out.ctx.is_null()); - free(&mut out); - assert_eq!( - unsafe { oaknode_group_set_output_passthrough(dup(&group), CHandle::null()) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_group_get_output_passthrough(dup(&group), &mut out) }, - OAKNODE_OK - ); - assert!(out.ctx.is_null()); - - // resolve_input: a non-group input resolves to itself. - let mut rn = CHandle::null(); - let mut re = -9; - let rid = two_stage(|b, s| unsafe { - oaknode_group_resolve_input( - dup(&math), - cs("param_a_in").as_ptr(), - -1, - &mut rn, - b, - s, - &mut re, - ) - }) - .unwrap(); - assert_eq!(rid, "param_a_in"); - assert!(!rn.ctx.is_null()); - assert_eq!(re, -1); - free(&mut rn); - // Unresolvable input -> NOT_FOUND. - assert_eq!( - unsafe { - oaknode_group_resolve_input( - dup(&math), - cs("nope_in").as_ptr(), - -1, - &mut rn, - std::ptr::null_mut(), - 0, - &mut re, - ) - }, - OAKNODE_E_NOT_FOUND - ); - - unsafe { oaknode_group_free(&mut group) }; - free(&mut math2); - free(&mut math); - unsafe { oaknode_project_free(&mut p2) }; - unsafe { oaknode_project_free(&mut p) }; -} - -/// Group family, undoable paths: passthrough/output commands round-trip -/// through the test-stub undo bridge. -#[cfg(feature = "test-stubs")] -#[test] -fn group_undoable() { - let _g = lock(); - let mut p = unsafe { oaknode_project_init() }; - assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK); - let mut group = unsafe { oaknode_group_create() }; - assert_eq!( - unsafe { oaknode_project_add_node(dup(&p), dup(&group)) }, - OAKNODE_OK - ); - let mut math = unsafe { - oaknode::ffi::factory::oaknode_factory_create_from_id( - cs("org.olivevideoeditor.Olive.math").as_ptr(), - ) - }; - assert_eq!( - unsafe { oaknode_project_add_node(dup(&p), dup(&math)) }, - OAKNODE_OK - ); - - // add_input_passthrough_undoable: redo adds, undo removes. - let mut cmd = CHandle::null(); - assert_eq!( - unsafe { - oaknode_group_add_input_passthrough_undoable( - dup(&group), - dup(&math), - cs("param_a_in").as_ptr(), - -1, - &mut cmd, - ) - }, - OAKNODE_OK - ); - assert!(!cmd.ctx.is_null()); - let mut count = 0; - assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0); - assert_eq!( - unsafe { oaknode_group_passthrough_count(dup(&group), &mut count) }, - OAKNODE_OK - ); - assert_eq!(count, 1); - assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0); - assert_eq!( - unsafe { oaknode_group_passthrough_count(dup(&group), &mut count) }, - OAKNODE_OK - ); - assert_eq!(count, 0); - unsafe { oakundo_command_free(&mut cmd) }; - - // set_output_passthrough_undoable: redo sets, undo restores. - let mut cmd = CHandle::null(); - assert_eq!( - unsafe { oaknode_group_set_output_passthrough_undoable(dup(&group), dup(&math), &mut cmd) }, - OAKNODE_OK - ); - assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0); - let mut out = CHandle::null(); - assert_eq!( - unsafe { oaknode_group_get_output_passthrough(dup(&group), &mut out) }, - OAKNODE_OK - ); - assert!(!out.ctx.is_null()); - free(&mut out); - assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0); - assert_eq!( - unsafe { oaknode_group_get_output_passthrough(dup(&group), &mut out) }, - OAKNODE_OK - ); - assert!(out.ctx.is_null()); - unsafe { oakundo_command_free(&mut cmd) }; - - unsafe { oaknode_group_free(&mut group) }; - free(&mut math); - unsafe { oaknode_project_free(&mut p) }; -} - -/// Dragger family: create/start/drag/end/is_started + failures. `end` -/// builds undo commands through the test-stub bridge. -#[cfg(feature = "test-stubs")] -#[test] -fn dragger_family() { - let _g = lock(); - let mut p = unsafe { oaknode_project_init() }; - assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK); - let mut math = unsafe { - oaknode::ffi::factory::oaknode_factory_create_from_id( - cs("org.olivevideoeditor.Olive.math").as_ptr(), - ) - }; - assert_eq!( - unsafe { oaknode_project_add_node(dup(&p), dup(&math)) }, - OAKNODE_OK - ); - - let mut dg = unsafe { oaknode_dragger_create(dup(&math), cs("param_a_in").as_ptr(), -1, 0) }; - assert!(!dg.ctx.is_null()); - // Unknown input -> empty handle. - assert!( - unsafe { oaknode_dragger_create(dup(&math), cs("nope_in").as_ptr(), -1, 0) } - .ctx - .is_null() - ); - - let mut started = -1; - assert_eq!( - unsafe { oaknode_dragger_is_started(dup(&dg), &mut started) }, - OAKNODE_OK - ); - assert_eq!(started, 0); - - // Negative track -> E_INVALID (before start). - let mut dg2 = unsafe { oaknode_dragger_create(dup(&math), cs("param_a_in").as_ptr(), -1, 0) }; - assert_eq!( - unsafe { oaknode_dragger_start(dup(&dg2), 0, 1, -1, 1) }, - OAKNODE_E_INVALID - ); - - // start at time 0 on track 0; double start -> E_STATE. - assert_eq!( - unsafe { oaknode_dragger_start(dup(&dg), 0, 1, 0, 1) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_dragger_is_started(dup(&dg), &mut started) }, - OAKNODE_OK - ); - assert_eq!(started, 1); - assert_eq!( - unsafe { oaknode_dragger_start(dup(&dg), 1, 1, 0, 1) }, - OAKNODE_E_STATE - ); - - // drag sets the value live; wrong POD kind -> E_INVALID. - let f = |x: f64| OakNodeValue { - kind: oak::FLOAT, - num: 0, - den: 0, - f: [x, 0.0, 0.0, 0.0], - }; - assert_eq!( - unsafe { oaknode_dragger_drag(dup(&dg), &f(4.0)) }, - OAKNODE_OK - ); - let mut out = OakNodeValue::none(); - assert_eq!( - unsafe { - oaknode_node_get_input_at_time(dup(&math), cs("param_a_in").as_ptr(), 0, 1, &mut out) - }, - OAKNODE_OK - ); - assert_eq!(out.kind, oak::FLOAT); - assert_eq!(out.f[0], 4.0); - let bad = OakNodeValue { - kind: oak::INT, - num: 3, - den: 0, - f: [0.0; 4], - }; - assert_eq!( - unsafe { oaknode_dragger_drag(dup(&dg), &bad) }, - OAKNODE_E_INVALID - ); - // drag before start -> E_STATE. - assert_eq!( - unsafe { oaknode_dragger_drag(dup(&dg2), &f(1.0)) }, - OAKNODE_E_STATE - ); - - // end builds a multi command; the command undoes and redoes the drag. - let mut cmd = CHandle::null(); - assert_eq!( - unsafe { oaknode_dragger_end(dup(&dg), &mut cmd) }, - OAKNODE_OK - ); - assert!(!cmd.ctx.is_null()); - assert_eq!( - unsafe { oaknode_dragger_is_started(dup(&dg), &mut started) }, - OAKNODE_OK - ); - assert_eq!(started, 0, "end resets the dragger"); - assert_eq!( - unsafe { oaknode_dragger_end(dup(&dg), &mut cmd) }, - OAKNODE_E_STATE - ); - // The drag was applied live, so the command round-trips - // redo -> undo -> redo. - assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0); - assert_eq!( - unsafe { - oaknode_node_get_input_at_time(dup(&math), cs("param_a_in").as_ptr(), 0, 1, &mut out) - }, - OAKNODE_OK - ); - assert_eq!(out.f[0], 4.0, "redo re-applies the drag"); - assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0); - assert_eq!( - unsafe { - oaknode_node_get_input_at_time(dup(&math), cs("param_a_in").as_ptr(), 0, 1, &mut out) - }, - OAKNODE_OK - ); - assert_eq!(out.f[0], 0.0, "undo restores the pre-drag value"); - assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0); - assert_eq!( - unsafe { - oaknode_node_get_input_at_time(dup(&math), cs("param_a_in").as_ptr(), 0, 1, &mut out) - }, - OAKNODE_OK - ); - assert_eq!(out.f[0], 4.0, "redo after undo re-applies the drag"); - unsafe { oakundo_command_free(&mut cmd) }; - - // Null args -> E_INVALID. - assert_eq!( - unsafe { oaknode_dragger_is_started(CHandle::null(), &mut started) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_dragger_drag(CHandle::null(), &f(1.0)) }, - OAKNODE_E_INVALID - ); - - unsafe { oaknode_dragger_free(&mut dg2) }; - unsafe { oaknode_dragger_free(&mut dg) }; - free(&mut math); - unsafe { oaknode_project_free(&mut p) }; -} - -/// Undoable node exports: create commands, redo/undo round-trip via the -/// test-stub undo bridge. -/// -/// Needs the `test-stubs` feature: the undo commands are created through -/// the oakundo bridge, whose `oakundo_command_init` symbol only resolves -/// in the test binary when the in-crate stubs are compiled in. -#[cfg(feature = "test-stubs")] -#[test] -fn undoable_node_exports() { - let _g = lock(); - let mut p = unsafe { oaknode_project_init() }; - assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK); - let mut math = unsafe { - oaknode::ffi::factory::oaknode_factory_create_from_id( - cs("org.olivevideoeditor.Olive.math").as_ptr(), - ) - }; - assert_eq!( - unsafe { oaknode_project_add_node(dup(&p), dup(&math)) }, - OAKNODE_OK - ); - - // set_label_undoable: redo sets, undo restores. - let mut cmd = CHandle::null(); - assert_eq!( - unsafe { oaknode_node_set_label_undoable(dup(&math), cs("new label").as_ptr(), &mut cmd) }, - OAKNODE_OK - ); - assert!(!cmd.ctx.is_null()); - assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0); - let label = - two_stage(|b, s| unsafe { oaknode::ffi::node::oaknode_node_get_label(dup(&math), b, s) }) - .unwrap(); - assert_eq!(label, "new label"); - assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0); - let label = - two_stage(|b, s| unsafe { oaknode::ffi::node::oaknode_node_get_label(dup(&math), b, s) }) - .unwrap(); - assert_eq!(label, ""); - unsafe { oakundo_command_free(&mut cmd) }; - - // set_enabled_undoable. - let mut cmd = CHandle::null(); - assert_eq!( - unsafe { oaknode_node_set_enabled_undoable(dup(&math), 0, &mut cmd) }, - OAKNODE_OK - ); - let mut en = 1; - assert_eq!( - unsafe { oaknode::ffi::node::oaknode_node_is_enabled(dup(&math), &mut en) }, - OAKNODE_OK - ); - assert_eq!(en, 1); - assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0); - assert_eq!( - unsafe { oaknode::ffi::node::oaknode_node_is_enabled(dup(&math), &mut en) }, - OAKNODE_OK - ); - assert_eq!(en, 0); - unsafe { oakundo_command_free(&mut cmd) }; - - // set_input_undoable on param_a_in. - let mut cmd = CHandle::null(); - let v = OakNodeValue { - kind: oak::FLOAT, - num: 0, - den: 0, - f: [7.5, 0.0, 0.0, 0.0], - }; - assert_eq!( - unsafe { - oaknode_node_set_input_undoable(dup(&math), cs("param_a_in").as_ptr(), &v, &mut cmd) - }, - OAKNODE_OK - ); - assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0); - let mut pod = OakNodeValue::none(); - assert_eq!( - unsafe { oaknode_node_get_input(dup(&math), cs("param_a_in").as_ptr(), &mut pod) }, - OAKNODE_OK - ); - assert_eq!(pod.f[0], 7.5); - assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0); - assert_eq!( - unsafe { oaknode_node_get_input(dup(&math), cs("param_a_in").as_ptr(), &mut pod) }, - OAKNODE_OK - ); - assert_eq!(pod.f[0], 0.0); - unsafe { oakundo_command_free(&mut cmd) }; - - // set_input_at_time_undoable: inserts a keyframe on redo. - let mut cmd = CHandle::null(); - let kv = OakNodeValue { - kind: oak::FLOAT, - num: 0, - den: 0, - f: [3.0, 0.0, 0.0, 0.0], - }; - assert_eq!( - unsafe { - oaknode_node_set_input_at_time_undoable( - dup(&math), - cs("param_a_in").as_ptr(), - 10, - 1, - &kv, - 0, - &mut cmd, - ) - }, - OAKNODE_OK - ); - assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0); - // At time 10 the value is now keyframed to 3.0. - let mut at = OakNodeValue::none(); - assert_eq!( - unsafe { - oaknode::ffi::node::oaknode_node_get_input_at_time( - dup(&math), - cs("param_a_in").as_ptr(), - 10, - 1, - &mut at, - ) - }, - OAKNODE_OK - ); - assert_eq!(at.f[0], 3.0); - assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0); - assert_eq!( - unsafe { - oaknode::ffi::node::oaknode_node_get_input_at_time( - dup(&math), - cs("param_a_in").as_ptr(), - 10, - 1, - &mut at, - ) - }, - OAKNODE_OK - ); - assert_eq!(at.f[0], 0.0, "keyframe removed on undo"); - unsafe { oakundo_command_free(&mut cmd) }; - - // connect_undoable: redo connects, undo disconnects. - let mut other = unsafe { - oaknode::ffi::factory::oaknode_factory_create_from_id( - cs("org.olivevideoeditor.Olive.math").as_ptr(), - ) - }; - assert_eq!( - unsafe { oaknode_project_add_node(dup(&p), dup(&other)) }, - OAKNODE_OK - ); - let mut cmd = CHandle::null(); - assert_eq!( - unsafe { - oaknode_node_connect_undoable( - dup(&math), - dup(&other), - cs("param_a_in").as_ptr(), - &mut cmd, - ) - }, - OAKNODE_OK - ); - assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0); - let mut conn = 0; - assert_eq!( - unsafe { - oaknode::ffi::node::oaknode_node_input_is_connected( - dup(&other), - cs("param_a_in").as_ptr(), - &mut conn, - ) - }, - OAKNODE_OK - ); - assert_eq!(conn, 1); - assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0); - assert_eq!( - unsafe { - oaknode::ffi::node::oaknode_node_input_is_connected( - dup(&other), - cs("param_a_in").as_ptr(), - &mut conn, - ) - }, - OAKNODE_OK - ); - assert_eq!(conn, 0); - unsafe { oakundo_command_free(&mut cmd) }; - - // disconnect_undoable failure: unconnected input. - let mut cmd = CHandle::null(); - assert_eq!( - unsafe { - oaknode_node_disconnect_undoable(dup(&other), cs("param_a_in").as_ptr(), &mut cmd) - }, - OAKNODE_E_NOT_FOUND - ); - - free(&mut other); - free(&mut math); - unsafe { oaknode_project_free(&mut p) }; -} - -// The undo stub functions are resolved through the dlsym path (the -// test-stubs feature compiles them into the test binary); only the -// undoable-node test above uses them. -#[cfg(feature = "test-stubs")] -use oaknode::bridge::undo::{ - command_free as oakundo_command_free, command_redo_now as oakundo_command_redo_now, - command_undo_now as oakundo_command_undo_now, -}; - -/// A minimal multicam-typed behavior for the node-backed multicam -/// exports (the real `MultiCamNode` behavior is a Phase-3 item). -struct FakeMulticam; -impl oaknode::node::NodeBehavior for FakeMulticam { - fn name(&self) -> &str { - "Multi-Cam" - } - fn type_id(&self) -> &str { - "org.olivevideoeditor.Olive.multicam" - } - fn duplicate( - &self, - _core: &oaknode::node::NodeCore, - ) -> Option> { - None - } -} - -/// A node handle for an arbitrary behavior/core (unit-style construction -/// for exports whose node type has no factory constructor yet). -fn make_raw_node( - core: oaknode::node::NodeCore, - behavior: Box, -) -> CHandle { - let project = oaknode::project::Project::new(); - let id = { - let mut p = project.lock().unwrap(); - p.graph.add_node(core, behavior) - }; - oaknode::handle::make_owned(oaknode::project::NodeRef::new(project, id, false)) -} - -/// Multicam family: input-id strings, grid math incl. 0/1 source and -/// non-square grids, index<->row/col round-trips, node-backed exports. -#[test] -fn multicam_family() { - let _g = lock(); - let s = |p: *const c_char| { - unsafe { std::ffi::CStr::from_ptr(p) } - .to_str() - .unwrap() - .to_string() - }; - assert_eq!(s(unsafe { oaknode_multicam_input_current() }), "current_in"); - assert_eq!(s(unsafe { oaknode_multicam_input_sources() }), "sources_in"); - assert_eq!( - s(unsafe { oaknode_multicam_input_sequence() }), - "sequence_in" - ); - assert_eq!( - s(unsafe { oaknode_multicam_input_sequence_type() }), - "sequence_type_in" - ); - - // Grid math (rows/columns): the square-ish cover grid. - let mut rows = 0; - let mut cols = 0; - let grid = |n: c_int, r: &mut c_int, c: &mut c_int| unsafe { - oaknode_multicam_get_rows_and_columns(n, r, c) - }; - assert_eq!(grid(0, &mut rows, &mut cols), OAKNODE_OK); - assert_eq!((rows, cols), (1, 1), "0 sources -> 1x1"); - assert_eq!(grid(1, &mut rows, &mut cols), OAKNODE_OK); - assert_eq!((rows, cols), (1, 1), "1 source -> 1x1"); - assert_eq!(grid(2, &mut rows, &mut cols), OAKNODE_OK); - assert_eq!((rows, cols), (1, 2), "2 sources -> 1x2"); - assert_eq!(grid(3, &mut rows, &mut cols), OAKNODE_OK); - assert_eq!((rows, cols), (2, 2), "3 sources -> 2x2"); - assert_eq!(grid(4, &mut rows, &mut cols), OAKNODE_OK); - assert_eq!((rows, cols), (2, 2)); - assert_eq!(grid(5, &mut rows, &mut cols), OAKNODE_OK); - assert_eq!((rows, cols), (2, 3), "5 sources -> non-square 2x3"); - assert_eq!(grid(6, &mut rows, &mut cols), OAKNODE_OK); - assert_eq!((rows, cols), (2, 3), "6 sources -> 2x3"); - assert_eq!(grid(10, &mut rows, &mut cols), OAKNODE_OK); - assert_eq!((rows, cols), (3, 4), "10 sources -> 3x4"); - // Failure: negative count / null out. - assert_eq!(grid(-1, &mut rows, &mut cols), OAKNODE_E_INVALID); - assert_eq!( - unsafe { oaknode_multicam_get_rows_and_columns(4, std::ptr::null_mut(), &mut cols) }, - OAKNODE_E_INVALID - ); - - // index -> (row, col) and back. - let mut row = -1; - let mut col = -1; - let to_rc = |i: c_int, r: c_int, c: c_int, or: &mut c_int, oc: &mut c_int| unsafe { - oaknode_multicam_index_to_row_cols(i, r, c, or, oc) - }; - assert_eq!(to_rc(0, 2, 3, &mut row, &mut col), OAKNODE_OK); - assert_eq!((row, col), (0, 0)); - assert_eq!(to_rc(5, 2, 3, &mut row, &mut col), OAKNODE_OK); - assert_eq!((row, col), (1, 2), "5 in 2x3 -> row 1 col 2"); - assert_eq!(to_rc(2, 1, 2, &mut row, &mut col), OAKNODE_OK); - assert_eq!((row, col), (1, 0)); - assert_eq!(to_rc(-1, 2, 3, &mut row, &mut col), OAKNODE_E_INVALID); - assert_eq!(to_rc(0, 0, 3, &mut row, &mut col), OAKNODE_E_INVALID); - assert_eq!(to_rc(0, 2, 0, &mut row, &mut col), OAKNODE_E_INVALID); - assert_eq!( - unsafe { oaknode_multicam_index_to_row_cols(0, 2, 3, std::ptr::null_mut(), &mut col) }, - OAKNODE_E_INVALID - ); - let to_i = |r: c_int, c: c_int, rr: c_int, cc: c_int| unsafe { - oaknode_multicam_rows_cols_to_index(r, c, rr, cc) - }; - assert_eq!(to_i(1, 2, 2, 3), 5, "round-trip"); - assert_eq!(to_i(0, 0, 1, 2), 0); - assert_eq!(to_i(-1, 0, 2, 3), OAKNODE_E_INVALID); - assert_eq!(to_i(0, -1, 2, 3), OAKNODE_E_INVALID); - assert_eq!(to_i(0, 0, 0, 3), OAKNODE_E_INVALID); - assert_eq!(to_i(2, 0, 2, 3), OAKNODE_E_INVALID, "row out of range"); - assert_eq!(to_i(0, 3, 2, 3), OAKNODE_E_INVALID, "col out of range"); - - // Node-backed exports: a fake multicam node with 4 sources, current 2. - let mut core = oaknode::node::NodeCore::new(); - let mut sources = oaknode::input::Input::new( - "sources_in", - oaknode::value::ValueType::None, - oaknode::value::NodeValue::None, - ); - sources.flags = oaknode::input::flags::ARRAY | oaknode::input::flags::NOT_KEYFRAMABLE; - sources.array_size = 4; - core.add_input(sources); - core.add_input(oaknode::input::Input::new( - "current_in", - oaknode::value::ValueType::Combo, - oaknode::value::NodeValue::Combo(2), - )); - core.set_standard_value("current_in", -1, oaknode::value::NodeValue::Combo(2)); - let mut node = make_raw_node(core, Box::new(FakeMulticam)); - - let mut count = -1; - assert_eq!( - unsafe { oaknode_multicam_get_source_count(dup(&node), &mut count) }, - OAKNODE_OK - ); - assert_eq!(count, 4); - let mut src = -1; - assert_eq!( - unsafe { oaknode_multicam_get_current_source(dup(&node), &mut src) }, - OAKNODE_OK - ); - assert_eq!(src, 2); - - // Non-multicam node / empty handle / null outs -> E_INVALID. - let mut p = unsafe { oaknode_project_init() }; - assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK); - let mut math = unsafe { - oaknode::ffi::factory::oaknode_factory_create_from_id( - cs("org.olivevideoeditor.Olive.math").as_ptr(), - ) - }; - assert_eq!( - unsafe { oaknode_project_add_node(dup(&p), dup(&math)) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_multicam_get_source_count(dup(&math), &mut count) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_multicam_get_current_source(dup(&math), &mut src) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_multicam_get_source_count(CHandle::null(), &mut count) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_multicam_get_source_count(dup(&node), std::ptr::null_mut()) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_multicam_get_current_source(CHandle::null(), std::ptr::null_mut()) }, - OAKNODE_E_INVALID - ); - - free(&mut node); - free(&mut math); - unsafe { oaknode_project_free(&mut p) }; -} - -/// Timeline/render bridge accessors: markers / work area / video frame -/// cache are addref'd stored handles (empty by default) with the -/// documented error paths; find_input_footage walks the upstream graph. -#[test] -fn bridge_handle_accessors() { - let _g = lock(); - let mut p = unsafe { oaknode_project_init() }; - assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK); - - // A math node (not a viewer): markers/workarea empty, cache empty. - let mut math = unsafe { - oaknode::ffi::factory::oaknode_factory_create_from_id( - cs("org.olivevideoeditor.Olive.math").as_ptr(), - ) - }; - assert_eq!( - unsafe { oaknode_project_add_node(dup(&p), dup(&math)) }, - OAKNODE_OK - ); - - let mut h = CHandle::null(); - let hp = &mut h as *mut CHandle as *mut std::ffi::c_void; - assert_eq!( - unsafe { oaknode_node_get_markers(dup(&math), hp) }, - OAKNODE_OK - ); - assert!(h.ctx.is_null(), "no markers by default"); - assert_eq!( - unsafe { oaknode_node_get_work_area(dup(&math), hp) }, - OAKNODE_OK - ); - assert!(h.ctx.is_null(), "no work area by default"); - assert_eq!( - unsafe { oaknode_node_get_video_frame_cache(dup(&math), hp) }, - OAKNODE_OK - ); - assert!(h.ctx.is_null(), "no frame cache by default"); - - // Null args -> E_INVALID. - assert_eq!( - unsafe { oaknode_node_get_markers(CHandle::null(), hp) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_node_get_markers(dup(&math), std::ptr::null_mut()) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_node_get_work_area(CHandle::null(), hp) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_node_get_video_frame_cache(CHandle::null(), hp) }, - OAKNODE_E_INVALID - ); - - // find_input_footage: no footage upstream -> empty + OK. - let mut out = CHandle::null(); - assert_eq!( - unsafe { oaknode_node_find_input_footage(dup(&math), &mut out) }, - OAKNODE_OK - ); - assert!(out.ctx.is_null(), "no footage upstream"); - assert_eq!( - unsafe { oaknode_node_find_input_footage(CHandle::null(), &mut out) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_node_find_input_footage(dup(&math), std::ptr::null_mut()) }, - OAKNODE_E_INVALID - ); - - // A footage node feeding the math node's input: findable upstream. - let mut footage = unsafe { oaknode_footage_create(dup(&p), cs("/media/clip.mov").as_ptr()) }; - assert_eq!( - unsafe { oaknode_project_add_node(dup(&p), dup(&footage)) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_node_connect(dup(&footage), dup(&math), cs("param_a_in").as_ptr()) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_node_find_input_footage(dup(&math), &mut out) }, - OAKNODE_OK - ); - assert!(!out.ctx.is_null(), "footage found upstream"); - free(&mut out); - - // The sequence (viewer) node also yields empty markers/workarea. - let mut seq = unsafe { oaknode_sequence_create() }; - assert_eq!( - unsafe { oaknode_node_get_markers(dup(&seq), hp) }, - OAKNODE_OK - ); - assert!(h.ctx.is_null()); - - free(&mut seq); - free(&mut footage); - free(&mut math); - unsafe { oaknode_project_free(&mut p) }; -} - -/// Viewer params: set video/audio params from oakcommon/oakcore handles -/// (test-stub backed) with the documented degradation errors. -#[cfg(feature = "test-stubs")] -#[test] -fn viewer_stream_params() { - let _g = lock(); - let mut p = unsafe { oaknode_project_init() }; - assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK); - let mut seq = unsafe { oaknode_sequence_create() }; - - // Build a video-params handle and push it into the viewer. - let mut vp = - oaknode::bridge::common::videoparams_init_basic(640, 480, 4, 4, 1, 1, 0, 1).unwrap(); - assert_eq!( - oaknode::bridge::common::videoparams_set_frame_rate(dup(&vp), 24, 1).unwrap(), - 0 - ); - assert_eq!( - unsafe { - oaknode_viewer_set_video_params(dup(&seq), &vp as *const _ as *const std::ffi::c_void) - }, - OAKNODE_OK - ); - - // Read it back through the sequence getter and compare. - let mut vp2 = CHandle::null(); - assert_eq!( - unsafe { - oaknode_sequence_get_video_params( - dup(&seq), - 0, - &mut vp2 as *mut CHandle as *mut std::ffi::c_void, - ) - }, - OAKNODE_OK - ); - assert!(!vp2.ctx.is_null()); - assert_eq!( - oaknode::bridge::common::videoparams_get_width(dup(&vp2)).unwrap(), - 640 - ); - assert_eq!( - oaknode::bridge::common::videoparams_get_height(dup(&vp2)).unwrap(), - 480 - ); - assert_eq!( - oaknode::bridge::common::videoparams_get_frame_rate(dup(&vp2)).unwrap(), - (24, 1) - ); - oaknode::bridge::common::videoparams_free(&mut vp2); - - // Audio params via an oakcore handle. - let ap = oaknode::bridge::core::audioparams_create(48000, 0x3, 4).unwrap(); - assert_eq!( - unsafe { oaknode_viewer_set_audio_params(dup(&seq), ap as *const std::ffi::c_void) }, - OAKNODE_OK - ); - let mut ap2: *mut std::ffi::c_void = std::ptr::null_mut(); - assert_eq!( - unsafe { oaknode_sequence_get_audio_params(dup(&seq), 0, &mut ap2) }, - OAKNODE_OK - ); - assert!(!ap2.is_null()); - assert_eq!( - oaknode::bridge::core::audioparams_sample_rate(ap2).unwrap(), - 48000 - ); - assert_eq!( - oaknode::bridge::core::audioparams_channel_layout(ap2).unwrap(), - 0x3 - ); - assert_eq!(oaknode::bridge::core::audioparams_format(ap2).unwrap(), 4); - oaknode::bridge::core::audioparams_free(ap2); - oaknode::bridge::core::audioparams_free(ap); - - // Failure paths: null params / empty handle / non-viewer node. - assert_eq!( - unsafe { oaknode_viewer_set_video_params(dup(&seq), std::ptr::null()) }, - OAKNODE_E_INVALID - ); - let empty = CHandle::null(); - assert_eq!( - unsafe { - oaknode_viewer_set_video_params( - dup(&seq), - &empty as *const _ as *const std::ffi::c_void, - ) - }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_viewer_set_audio_params(dup(&seq), std::ptr::null()) }, - OAKNODE_E_INVALID - ); - - oaknode::bridge::common::videoparams_free(&mut vp); - free(&mut seq); - unsafe { oaknode_project_free(&mut p) }; -} - -/// Sequence/footage stream params: get/set video+audio round-trips -/// (test-stub backed) plus the error paths. -#[cfg(feature = "test-stubs")] -#[test] -fn stream_params_roundtrip() { - let _g = lock(); - let mut p = unsafe { oaknode_project_init() }; - assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK); - let mut seq = unsafe { oaknode_sequence_create() }; - assert_eq!( - unsafe { oaknode_sequence_set_default_parameters(dup(&seq)) }, - OAKNODE_OK - ); - - let mut vc = 0; - assert_eq!( - unsafe { oaknode_sequence_get_video_stream_count(dup(&seq), &mut vc) }, - OAKNODE_OK - ); - assert_eq!(vc, 1, "default video stream"); - - // get_video_params(0) returns an owned handle. - let mut vp = CHandle::null(); - assert_eq!( - unsafe { - oaknode_sequence_get_video_params( - dup(&seq), - 0, - &mut vp as *mut CHandle as *mut std::ffi::c_void, - ) - }, - OAKNODE_OK - ); - assert!(!vp.ctx.is_null()); - assert_eq!( - oaknode::bridge::common::videoparams_get_width(dup(&vp)).unwrap(), - 1920 - ); - // set_video_params(0) round-trips the same handle. - assert_eq!( - unsafe { oaknode_sequence_set_video_params(dup(&seq), 0, dup(&vp)) }, - OAKNODE_OK - ); - - // Error paths. - assert_eq!( - unsafe { - oaknode_sequence_get_video_params( - dup(&seq), - 7, - &mut vp as *mut CHandle as *mut std::ffi::c_void, - ) - }, - OAKNODE_E_NOT_FOUND - ); - assert_eq!( - unsafe { oaknode_sequence_set_video_params(dup(&seq), 7, dup(&vp)) }, - OAKNODE_E_NOT_FOUND - ); - assert_eq!( - unsafe { - oaknode_sequence_get_video_params( - dup(&seq), - -1, - &mut vp as *mut CHandle as *mut std::ffi::c_void, - ) - }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_sequence_set_video_params(dup(&seq), 0, CHandle::null()) }, - OAKNODE_E_INVALID - ); - oaknode::bridge::common::videoparams_free(&mut vp); - - // Audio round-trip. - let mut ac = 0; - assert_eq!( - unsafe { oaknode_sequence_get_audio_stream_count(dup(&seq), &mut ac) }, - OAKNODE_OK - ); - assert_eq!(ac, 1, "default audio stream"); - let mut ap: *mut std::ffi::c_void = std::ptr::null_mut(); - assert_eq!( - unsafe { oaknode_sequence_get_audio_params(dup(&seq), 0, &mut ap) }, - OAKNODE_OK - ); - assert!(!ap.is_null()); - assert_eq!( - oaknode::bridge::core::audioparams_sample_rate(ap).unwrap(), - 48000 - ); - assert_eq!( - unsafe { oaknode_sequence_set_audio_params(dup(&seq), 0, ap) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_sequence_get_audio_params(dup(&seq), 9, &mut ap) }, - OAKNODE_E_NOT_FOUND - ); - assert_eq!( - unsafe { oaknode_sequence_set_audio_params(dup(&seq), 9, ap) }, - OAKNODE_E_NOT_FOUND - ); - assert_eq!( - unsafe { oaknode_sequence_get_audio_params(dup(&seq), -1, &mut ap) }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { oaknode_sequence_set_audio_params(dup(&seq), 0, std::ptr::null()) }, - OAKNODE_E_INVALID - ); - oaknode::bridge::core::audioparams_free(ap); - - // Footage: no video streams -> NOT_FOUND both ways; bad args. - let mut footage = unsafe { oaknode_footage_create(dup(&p), cs("/media/clip.mov").as_ptr()) }; - assert_eq!( - unsafe { oaknode_project_add_node(dup(&p), dup(&footage)) }, - OAKNODE_OK - ); - let mut fvp = CHandle::null(); - assert_eq!( - unsafe { - oaknode_footage_get_video_params( - dup(&footage), - 0, - &mut fvp as *mut CHandle as *mut std::ffi::c_void, - ) - }, - OAKNODE_E_NOT_FOUND - ); - assert_eq!( - unsafe { - oaknode_footage_set_video_params( - dup(&footage), - 0, - &fvp as *const _ as *const std::ffi::c_void, - ) - }, - OAKNODE_E_INVALID, - "empty handle rejected" - ); - assert_eq!( - unsafe { - oaknode_footage_get_video_params( - dup(&footage), - -1, - &mut fvp as *mut CHandle as *mut std::ffi::c_void, - ) - }, - OAKNODE_E_INVALID - ); - - free(&mut footage); - free(&mut seq); - unsafe { oaknode_project_free(&mut p) }; -} - -/// Colormanager compliant transform: a display transform whose view is -/// clamped to the active config's defaults (test-stub backed). -#[cfg(feature = "test-stubs")] -#[test] -fn colortransform_compliance() { - let _g = lock(); - let mut p = unsafe { oaknode_project_init() }; - let mut cm = unsafe { oaknode_colormanager_init(dup(&p)) }; - - // Config not loaded -> E_STATE. - let mut t = - oaknode::bridge::common::colortransform_init_display("sRGB", "Standard", "").unwrap(); - let mut out = CHandle::null(); - assert_eq!( - unsafe { - oaknode_colormanager_get_compliant_color_transform(dup(&cm), dup(&t), 0, &mut out) - }, - OAKNODE_E_STATE - ); - - // Load the config, then a display transform with an unknown view gets - // clamped to the default view. - assert_eq!( - unsafe { oaknode_colormanager_initialize(dup(&cm)) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { - oaknode_colormanager_get_compliant_color_transform(dup(&cm), dup(&t), 0, &mut out) - }, - OAKNODE_OK - ); - assert!(!out.ctx.is_null()); - oaknode::bridge::common::colortransform_free(&mut out); - - // An output transform with an unknown colorspace clamps to the - // default input space. - let mut t2 = oaknode::bridge::common::colortransform_init_output("no-such-space").unwrap(); - assert_eq!( - unsafe { - oaknode_colormanager_get_compliant_color_transform(dup(&cm), dup(&t2), 0, &mut out) - }, - OAKNODE_OK - ); - assert!(!out.ctx.is_null()); - oaknode::bridge::common::colortransform_free(&mut out); - - // Failure: null out, empty manager. - assert_eq!( - unsafe { - oaknode_colormanager_get_compliant_color_transform( - dup(&cm), - dup(&t), - 0, - std::ptr::null_mut(), - ) - }, - OAKNODE_E_INVALID - ); - assert_eq!( - unsafe { - oaknode_colormanager_get_compliant_color_transform( - CHandle::null(), - dup(&t), - 0, - &mut out, - ) - }, - OAKNODE_E_INVALID - ); - - oaknode::bridge::common::colortransform_free(&mut t); - oaknode::bridge::common::colortransform_free(&mut t2); - free(&mut cm); - unsafe { oaknode_project_free(&mut p) }; -} diff --git a/crates/oaknode/tests/phase2_units_test.rs b/crates/oaknode/tests/phase2_units_test.rs index ed4b6381b..7e9e98d0a 100644 --- a/crates/oaknode/tests/phase2_units_test.rs +++ b/crates/oaknode/tests/phase2_units_test.rs @@ -357,55 +357,77 @@ fn serializer_value_codecs() { ); } -/// bridge::undo: vtable commands + multi commands through the stubs. -/// -/// Needs the `test-stubs` feature: the bridge resolves `oakundo_*` -/// symbols via dlsym, which only resolves in the test binary when the -/// in-crate stubs are compiled in. -#[cfg(feature = "test-stubs")] +/// oakundo `UndoCommand`: vtable commands + multi commands (direct Rust +/// calls, single-lib unification). #[test] fn undo_command_roundtrip() { + use std::ffi::c_void; use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::Arc; + use oakundo::undocommand::{OakUndoCommandVtable, UndoCommand}; + + struct Closures { + redo: Box, + undo: Box, + } + + unsafe extern "C" fn redo_thunk(ud: *mut c_void) { + let c = unsafe { &mut *(ud as *mut Closures) }; + (c.redo)(); + } + unsafe extern "C" fn undo_thunk(ud: *mut c_void) { + let c = unsafe { &mut *(ud as *mut Closures) }; + (c.undo)(); + } + unsafe extern "C" fn free_thunk(ud: *mut c_void) { + if !ud.is_null() { + unsafe { drop(Box::from_raw(ud as *mut Closures)) }; + } + } + + fn from_closures( + redo: impl FnMut() + Send + 'static, + undo: impl FnMut() + Send + 'static, + ) -> UndoCommand { + let ud = Box::into_raw(Box::new(Closures { + redo: Box::new(redo), + undo: Box::new(undo), + })); + UndoCommand::from_vtable( + OakUndoCommandVtable { + redo: Some(redo_thunk), + undo: Some(undo_thunk), + free_fn: Some(free_thunk), + }, + ud as *mut c_void, + ) + } let value = Arc::new(AtomicI32::new(0)); let value_redo = value.clone(); let value_undo = value.clone(); let value_check = value.clone(); - let mut cmd = oaknode::bridge::undo::command_from_closures( + let mut cmd = from_closures( move || { value_redo.fetch_add(1, Ordering::SeqCst); }, move || { value_undo.fetch_sub(1, Ordering::SeqCst); }, - ) - .expect("undo stub available"); + ); // redo applies, undo reverts; redo_now is idempotent. - assert_eq!( - oaknode::bridge::undo::command_redo_now(cmd.clone()).unwrap(), - 0 - ); + cmd.redo_now(); assert_eq!(value_check.load(Ordering::SeqCst), 1); - assert_eq!( - oaknode::bridge::undo::command_redo_now(cmd.clone()).unwrap(), - 0 - ); + cmd.redo_now(); assert_eq!( value_check.load(Ordering::SeqCst), 1, "redo no-ops when done" ); - assert_eq!( - oaknode::bridge::undo::command_undo_now(cmd.clone()).unwrap(), - 0 - ); + cmd.undo_now(); assert_eq!(value_check.load(Ordering::SeqCst), 0); - assert_eq!( - oaknode::bridge::undo::command_undo_now(cmd.clone()).unwrap(), - 0 - ); + cmd.undo_now(); assert_eq!( value_check.load(Ordering::SeqCst), 0, @@ -413,8 +435,8 @@ fn undo_command_roundtrip() { ); // Multi command batches children. - let mut multi = oaknode::bridge::undo::command_init_multi().unwrap(); - let mut child = oaknode::bridge::undo::command_from_closures( + let mut multi = UndoCommand::multi(); + let child = from_closures( { let value = value_check.clone(); move || { @@ -427,26 +449,12 @@ fn undo_command_roundtrip() { value.fetch_sub(1, Ordering::SeqCst); } }, - ) - .unwrap(); - assert_eq!( - oaknode::bridge::undo::command_multi_add_child(multi.clone(), child.clone()).unwrap(), - 0 - ); - assert_eq!( - oaknode::bridge::undo::command_redo_now(multi.clone()).unwrap(), - 0 ); + multi.multi_add_child(child); + multi.redo_now(); assert_eq!(value_check.load(Ordering::SeqCst), 1); - assert_eq!( - oaknode::bridge::undo::command_undo_now(multi.clone()).unwrap(), - 0 - ); + multi.undo_now(); assert_eq!(value_check.load(Ordering::SeqCst), 0); - - oaknode::bridge::undo::command_free(&mut cmd); - oaknode::bridge::undo::command_free(&mut multi); - oaknode::bridge::undo::command_free(&mut child); } /// track.rs: branch coverage the ffi contract tests leave open — diff --git a/crates/oaknode/tests/structure_test.rs b/crates/oaknode/tests/structure_test.rs index 5d1620867..475a1a87c 100644 --- a/crates/oaknode/tests/structure_test.rs +++ b/crates/oaknode/tests/structure_test.rs @@ -322,35 +322,6 @@ fn track_block_ordering() { ); } -/// ClipBlock cache passthrough: the C ABI export accepts the call (the -/// cache UUID copy is inert until the oakrender bridge creates per-node -/// caches). -#[test] -fn clip_cache_passthrough() { - use oaknode::error::OAKNODE_OK; - use oaknode::ffi::block::oaknode_block_clip_create; - use oaknode::ffi::block::oaknode_block_free; - use oaknode::ffi::block::oaknode_clip_add_cache_passthrough_from; - use oaknode::ffi::project::oaknode_project_free; - use oaknode::ffi::project::oaknode_project_init; - use oaknode::handle::CHandle; - - let mut p = unsafe { oaknode_project_init() }; - let mut clip = unsafe { oaknode_block_clip_create() }; - let mut other = unsafe { oaknode_block_clip_create() }; - assert_eq!( - unsafe { oaknode_clip_add_cache_passthrough_from(clip.clone(), other.clone()) }, - OAKNODE_OK - ); - assert_eq!( - unsafe { oaknode_clip_add_cache_passthrough_from(CHandle::null(), other.clone()) }, - oaknode::error::OAKNODE_E_INVALID - ); - unsafe { oaknode_block_free(&mut clip) }; - unsafe { oaknode_block_free(&mut other) }; - unsafe { oaknode_project_free(&mut p) }; -} - /// Footage behavior: state without a codec module (probe fails /// gracefully without partial state); proxy fields, counts, duration. #[test] diff --git a/crates/oakplugin/Cargo.toml b/crates/oakplugin/Cargo.toml index 549936f25..cbce31511 100644 --- a/crates/oakplugin/Cargo.toml +++ b/crates/oakplugin/Cargo.toml @@ -23,10 +23,10 @@ oakrender = { path = "../oakrender" } thiserror = "2" [features] -# 测试桩(库内状态访问器):cargo test --features test-stubs 时桥直连 -# 桩,像素路径可在无真实 GL/GPU 的环境下跑通。桩的 C ABI 符号不再 -# no_mangle(真实 oaknode/oakrender/oakundo 已作为 path 依赖链接, -# 避免重复符号);真实链路走直接 Rust 调用。 +# 历史 feature(原桥测试桩开关):单库化后 oaknode/oakrender/oakundo +# 以 path 依赖直连,句柄式调用面改为本地桩(见 src/node.rs / +# src/render.rs 的 `// STUB` 标记),本 feature 无 cfg 使用点—— +# 保留以避免工作区/构建脚本 `--features test-stubs` 断裂。 test-stubs = [] [build-dependencies] diff --git a/crates/oakplugin/README.md b/crates/oakplugin/README.md index 83cb1b388..8b3baed41 100644 --- a/crates/oakplugin/README.md +++ b/crates/oakplugin/README.md @@ -24,45 +24,59 @@ src/ instance.rs Instance:action 调用面(render/协商/RoD/RoI/isIdentity/ render_gl/GetOutputColourspace) render_driver.rs pluginrenderer.cpp 渲染流程收编(M11 §4 新增) - clip.rs ClipInstance:clip↔oakrender 纹理桥(bridge::render) + clip.rs ClipInstance:clip↔oakrender 纹理桥(render.rs) image.rs Image:帧缓冲/纹理的 OFX 视图 - param.rs 12 种参数实例 + param↔oaknode 桥(bridge::node/undo) + param.rs 12 种参数实例 + param↔oaknode 桥(node.rs/undo) progress.rs PluginProgressReporter - bridge/ oak 其余模块的 C ABI 导入(extern "C" 声明) - node.rs render.rs undo.rs - ffi.rs include/plugin/*.h 的全部导出(C ABI 出口层) + node.rs oaknode 桥:节点值 POD、身份注册表、undoable 回写 + render.rs oakrender 桥:Texture/Frame/Renderer 值类型调用面 ``` -## 桥布局决策(M11 第 1 期冻结;第 2 期增补) +## 桥布局决策(M11 第 1 期冻结;第 2 期增补;单库化修订) -- **`bridge::node::Value`** = `include/node/node.h:93` 的 `oaknode_value` +单库化(single-lib unification)后 `bridge/`(C ABI 导入)与 +`ffi.rs`(导出层)已删除:oakundo/oaknode/oakrender 以 path 依赖 +直连,桥改用直接 Rust 类型。 + +- **`node::Value`** = `include/node/node.h:93` 的 `oaknode_value` POD,字段逐字一致(`type`/`num`/`den`/`f[4]`;`type` 取值见 `node_value_type`)。字符串族输入(k_file/k_text/k_font/ - k_str_combo)无 POD 表示,走 `*_input_string_*` 专用桥函数 - (`set_input_string_undoable`)。`ffi::OakNodeValue` 是同布局的 - 出口层镜像(两处独立声明避免模块环)。 -- **帧访问 C ABI**(`bridge::render`)= `oakrender_display_texture_*` - (`get_frame`/`is_dummy`)+ `oakrender_codec_frame_*` - (`get_params`/`data`/`free`),与 `include/render/renderer.h` 一致; - `oakrender_display_texture_wrap_native` 是 C++ 专属符号(TexturePtr - 引用),Rust 不可调——**输出纹理由 oakrender 侧创建并经句柄传入**: - `ClipInstance::set_output_texture` 挂入后,`store_output_image` 取 - 其 CPU 帧整帧拷贝(全链路 F32,尺寸不符明确报错)。 -- **GL 纹理桥**(M11 §4 增补):`bridge::render` 增加渲染器族 - (`renderer_create_dynamic`/`init`/`is_open_gl`/`destroy`)与纹理族 - (`texture_create`/`upload`/`download`/`retain`/`free`/`id`/ - `get_params`/`renderer_download_from_texture`),以及 - `frame_linesize_bytes`(CPU 拷贝的行跨度感知修复)。行跨度单位 - 统一为**字节**(renderer.h:206 明文契约;C++ 调用点传像素行跨度, - 由 oakrender 侧实现 C ABI 时换算)。 -- **undo 打包**(`bridge::undo`)= `oakundo_command_init_multi`/ - `redo_now`/`multi_add_child`/`free`;paramEditBegin/End 的编辑事务 - 在 `Instance` 上维护深度与累积 multi(C++ `submit_undo_command` 语义: - 事务内子命令立即 redo 生效并并入 multi,editEnd 整体 redo+释放)。 + k_str_combo)无 POD 表示,走 `set_input_string_undoable`。 + `Value::to_node_value`/`Value::from_node_value` 与 + `oaknode::value::NodeValue` 互转(按 POD kind;STRING 族无数据)。 +- **节点绑定与回写**(`node.rs`):进程级身份注册表 + `register_node(project, id)` → 打包身份 + (`oaknode::id::NodeId::identity()`)→ + `Instance::bind_node(identity)`;`node_from_identity` 按身份反查 + `NodeRef { project, id }`(弱引用映射,project 释放后自然失效)。 + `set_input_undoable`/`set_input_string_undoable` 构造未执行的 + `oakundo::undocommand::UndoCommand`(closure-vtable 模式,与 + `oaknode::ops::set_value_at_time_command` 同构):redo 锁 project、 + `graph.get_mut(id)`、`NodeCore::set_standard_value`;undo 回放 + 创建期快照的旧值。失败(未知输入/失效节点/非字符串族输入) + 返回 `NodeBridgeError`,调用方按 no-op 处理。 +- **纹理/帧/渲染器值类型**(`render.rs`): + `Texture = oakrender::texture::Texture`、`Frame = + oakrender::texture::Frame`(值语义,drop 自动释放后端 token, + 原 `texture_free`/`frame_free` 调用面删除)、 + `Renderer = Arc`(渲染器 + 即后端上下文)。`texture_create(params, pixels, linesize)` 构造 + CPU 包装纹理(行跨度感知拷贝,GPU 上传由后端延迟); + `texture_get_frame`/`texture_get_params`/ + `renderer_is_open_gl` 为真实实现。**保留桩**:`texture_id` 恒 0 + ——wgpu 后端无 OpenGL 纹理名,GL suite 的 `OpenGLTextureIndex` + 属性与 use_opengl 决策据此回退 CPU 路径。 + `ClipInstance::set_output_texture` 挂入后,`store_output_image` + 取 CPU 帧整帧拷贝(全链路 F32,尺寸不符明确报错;GPU 目标经 + `GpuContextLike::upload` 回写)。 +- **undo 打包** = 直接经 `oakundo::undocommand::UndoCommand`; + paramEditBegin/End 的编辑事务在 `Instance` 上维护深度与累积 + multi(C++ `submit_undo_command` 语义:事务内子命令立即 redo + 生效并并入 multi,editEnd 整体 redo+释放)。 - **param↔node 值转换**(`param.rs`):`set_from_node`(节点→插件, 按参数类型映射,维度截断/补零)、`to_node_value`(插件→节点, RGB 补 alpha=1,与 C++ `RGBInstance::set` 一致)。字符串参数经 - `oaknode_node_set_input_string_undoable`。 + `node::set_input_string_undoable`。 - **节点绑定**:`Instance::bind_node(identity)`(C++ `set_node_handle` 的 Rust 侧);回写只在 `ChangeReason::PluginEdited` 触发,未绑定/ 身份查无时 no-op。 diff --git a/crates/oakplugin/src/bridge/mod.rs b/crates/oakplugin/src/bridge/mod.rs deleted file mode 100644 index d3a0fb844..000000000 --- a/crates/oakplugin/src/bridge/mod.rs +++ /dev/null @@ -1,34 +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 . - -//! bridge:oak 其余模块的 C ABI 调用——直接 Rust 调用(单库化,见 -//! `docs/zh/plans/riir/single-lib.md`)。 -//! -//! 每个子模块(node/render/undo)调用对应 crate 的 `ffi` 导出(同名 -//! `#[no_mangle]` 符号仍从 dylib 导出供外部 C ABI 使用;内部调用绕过 -//! 它们)。句柄全部是共享的 [`crate::handle::CHandle`]。 -//! -//! ## 双态实现(所有子模块统一) -//! -//! - 默认:直接调用 oaknode/oakrender/oakundo 的 ffi; -//! - `--features test-stubs`:桥走库内状态桩(各子模块的 -//! [`node::stub`]/[`render::stub`]/[`undo::stub`],纯 Rust、无 -//! `#[no_mangle]`,与真实 crate 的导出不冲突)——像素路径可在无 -//! 真实 GL/GPU 的环境下跑通。两种形态的调用面完全一致。 - -pub mod node; -pub mod render; -pub mod undo; diff --git a/crates/oakplugin/src/bridge/node.rs b/crates/oakplugin/src/bridge/node.rs deleted file mode 100644 index e6a546949..000000000 --- a/crates/oakplugin/src/bridge/node.rs +++ /dev/null @@ -1,431 +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 . - -//! oaknode C ABI 导入(include/node/node.h 中 param 桥用到的子集)。 -//! -//! ## Value 布局冻结(M11 第 1 期) -//! -//! [`Value`] 即 include/node/node.h:93 的 `oaknode_value` POD,字段 -//! 逐字一致(type/num/den/f[4];`type` 取值见 [`node_value_type`])。 -//! 字符串族输入(k_file/k_text/k_font/k_str_combo,node.h:48-52)没有 -//! POD 表示——走 `*_input_string_*` 专用函数(本桥的 -//! [`set_input_string_undoable`]);`OAKNODE_VALUE_STRING` 的 POD 里 -//! 不携带字符串数据。`crate::ffi::OakNodeValue` 与此同布局(出口层 -//! 的镜像,两处独立声明避免模块环)。 -//! -//! ## 双态实现(同 [`crate::bridge::render`]) -//! -//! - 默认:直接 Rust 调用 oaknode 的 `ffi`(单库化,见 -//! `docs/zh/plans/riir/single-lib.md`); -//! - `--features test-stubs`:库内状态桩([`stub`],纯 Rust、无 -//! `#[no_mangle]`,与真实 oaknode 的导出不冲突)——节点值、undo -//! 命令全链路可在 cargo test 跑通。两种形态的调用面完全一致。 - -use std::ffi::c_char; - -use crate::bridge::undo::CommandHandle; -use crate::handle::CHandle; - -/// oaknode 节点句柄(值型)。 -pub type NodeHandle = crate::handle::CHandle; - -/// oaknode_value_type 的取值(node.h:74;与 -/// `crate::ffi::node_value_type` 逐值一致)。 -pub mod node_value_type { - /// OAKNODE_VALUE_NONE。 - pub const NONE: i32 = 0; - /// OAKNODE_VALUE_INT。 - pub const INT: i32 = 1; - /// OAKNODE_VALUE_FLOAT。 - pub const FLOAT: i32 = 2; - /// OAKNODE_VALUE_BOOL。 - pub const BOOL: i32 = 3; - /// OAKNODE_VALUE_RATIONAL。 - pub const RATIONAL: i32 = 4; - /// OAKNODE_VALUE_COLOR。 - pub const COLOR: i32 = 5; - /// OAKNODE_VALUE_VEC2。 - pub const VEC2: i32 = 6; - /// OAKNODE_VALUE_VEC3。 - pub const VEC3: i32 = 7; - /// OAKNODE_VALUE_VEC4。 - pub const VEC4: i32 = 8; - /// OAKNODE_VALUE_COMBO。 - pub const COMBO: i32 = 9; - /// OAKNODE_VALUE_STRING。 - pub const STRING: i32 = 10; -} - -/// oaknode_value(include/node/node.h:93,字段逐字一致)。 -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub struct Value { - /// 类型([`node_value_type`])。 - pub r#type: i32, - /// INT/COMBO 值、BOOL 0/1、RATIONAL 分子。 - pub num: i64, - /// RATIONAL 分母。 - pub den: i64, - /// FLOAT f[0];VEC2/3/4 f[0..n-1];COLOR r,g,b,a。 - pub f: [f64; 4], -} - -impl Value { - /// 类型化构造:整数 / choice 索引。 - pub const fn int(v: i64) -> Self { - Self { - r#type: node_value_type::INT, - num: v, - den: 0, - f: [0.0; 4], - } - } - - /// 类型化构造:浮点。 - pub const fn float(v: f64) -> Self { - Self { - r#type: node_value_type::FLOAT, - num: 0, - den: 0, - f: [v, 0.0, 0.0, 0.0], - } - } - - /// 类型化构造:布尔。 - pub const fn bool_(v: bool) -> Self { - Self { - r#type: node_value_type::BOOL, - num: v as i64, - den: 0, - f: [0.0; 4], - } - } - - /// 类型化构造:choice(COMBO)。 - pub const fn combo(v: i64) -> Self { - Self { - r#type: node_value_type::COMBO, - num: v, - den: 0, - f: [0.0; 4], - } - } - - /// 类型化构造:颜色。 - pub const fn color(r: f64, g: f64, b: f64, a: f64) -> Self { - Self { - r#type: node_value_type::COLOR, - num: 0, - den: 0, - f: [r, g, b, a], - } - } - - /// 类型化构造:vec2/3/4(长度按 f 数组尾部 0 判定)。 - pub const fn vec(v: &[f64]) -> Self { - let t = match v.len() { - 2 => node_value_type::VEC2, - 3 => node_value_type::VEC3, - _ => node_value_type::VEC4, - }; - let mut f = [0.0; 4]; - let mut i = 0; - while i < v.len() && i < 4 { - f[i] = v[i]; - i += 1; - } - Self { - r#type: t, - num: 0, - den: 0, - f, - } - } - - /// 类型化构造:字符串族(POD 不携带数据;值经 - /// [`set_input_string_undoable`] 传递)。 - pub const fn string() -> Self { - Self { - r#type: node_value_type::STRING, - num: 0, - den: 0, - f: [0.0; 4], - } - } -} - -// ---- 桥调用面 ------------------------------------------------------------ - -/// 按身份取节点句柄(M9 身份注册表;`oaknode_node_from_identity`)。 -/// 身份未登记 / 符号缺失 → 空句柄。 -pub(crate) unsafe fn node_from_identity(id: usize) -> NodeHandle { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::node_from_identity_impl(id) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oaknode::ffi::node::oaknode_node_from_identity(id) } - } -} - -/// 写输入值的标准值并产出一条 undo 命令(`*out` 收到拥有型命令句柄; -/// `oaknode_node_set_input_undoable`,node.h:327)。字符串族输入走 -/// [`set_input_string_undoable`]。失败(含符号缺失)返回负错误码。 -/// -/// # Safety -/// `input`/`value`/`out` 必须指向有效内存;`node` 是有效句柄。 -pub(crate) unsafe fn set_input_undoable( - node: NodeHandle, - input: *const c_char, - value: *const Value, - out: *mut CommandHandle, -) -> i32 { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::set_input_undoable_impl(node, input, value, out) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { - oaknode::ffi::node::oaknode_node_set_input_undoable( - node, - input, - value as *const oaknode::value::OakNodeValue, - out, - ) - } - } -} - -/// 写字符串族输入的标准值并产出一条 undo 命令 -/// (`oaknode_node_set_input_string_undoable`,node.h:346)。 -/// -/// # Safety -/// `input`/`value`/`out` 必须指向有效内存;`node` 是有效句柄。 -pub(crate) unsafe fn set_input_string_undoable( - node: NodeHandle, - input: *const c_char, - value: *const c_char, - out: *mut CommandHandle, -) -> i32 { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::set_input_string_undoable_impl(node, input, value, out) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { - oaknode::ffi::node::oaknode_node_set_input_string_undoable(node, input, value, out) - } - } -} - -// ---- 测试桩(--features test-stubs)-------------------------------------- - -/// oaknode 测试桩:库内符号 + 节点值状态。 -/// -/// 节点句柄的 ctx 约定(桩内约定):`ctx = 身份 id`。undo 命令的 -/// 登记与 undo/redo 语义在 [`crate::bridge::undo::stub`](两边共享 -/// 同一张命令表)。 -#[cfg(feature = "test-stubs")] -pub mod stub { - use super::*; - use std::collections::HashMap; - use std::ffi::CStr; - use std::sync::LazyLock; - use std::sync::Mutex; - - /// 一个输入的标准值(数值走 [`Value`] POD;字符串族走 `string`)。 - #[derive(Clone, Debug, PartialEq)] - pub struct StubInput { - /// 数值值(字符串族为 STRING 类型空 POD)。 - pub value: Value, - /// 字符串值(仅字符串族输入)。 - pub string: Option, - } - - /// 全部桩节点:身份 → 输入名 → 值。 - static NODES: LazyLock>>> = - LazyLock::new(|| Mutex::new(HashMap::new())); - - fn lock() -> std::sync::MutexGuard<'static, HashMap>> { - NODES.lock().unwrap_or_else(|e| e.into_inner()) - } - - /// 重置全部桩节点(测试隔离)。 - pub fn reset() { - lock().clear(); - } - - /// 登记一个可被 [`super::node_from_identity`] 找到的节点。 - pub fn register_node(id: usize) { - lock().entry(id).or_default(); - } - - /// 直接设置输入值(测试前置)。 - pub fn set_input(id: usize, input: &str, value: Value) { - lock().entry(id).or_default().insert( - input.to_string(), - StubInput { - value, - string: None, - }, - ); - } - - /// 直接设置字符串输入值(测试前置)。 - pub fn set_input_string(id: usize, input: &str, value: &str) { - lock().entry(id).or_default().insert( - input.to_string(), - StubInput { - value: Value::string(), - string: Some(value.to_string()), - }, - ); - } - - /// 读输入当前值(断言用)。 - pub fn input(id: usize, input: &str) -> Option { - lock().get(&id)?.get(input).cloned() - } - - /// 命令回写用的应用入口(redo/undo 落值;由 - /// [`crate::bridge::undo::stub`] 调用)。 - pub(crate) fn apply(node: usize, input: &str, value: Value, string: Option) { - let mut m = lock(); - if let Some(n) = m.get_mut(&node) { - n.insert(input.to_string(), StubInput { value, string }); - } - } - - /// 读输入当前值(命令创建时取 prev)。 - pub(crate) fn current(node: usize, input: &str) -> StubInput { - lock() - .get(&node) - .and_then(|n| n.get(input)) - .cloned() - .unwrap_or(StubInput { - value: Value::default(), - string: None, - }) - } - - pub(super) unsafe fn node_from_identity_impl(id: usize) -> NodeHandle { - if lock().contains_key(&id) { - CHandle { - ctx: id as *mut std::ffi::c_void, - addref: None, - release: None, - abi_version: crate::handle::OAKPLUGIN_ABI_VERSION, - } - } else { - CHandle::null() - } - } - - pub(super) unsafe fn set_input_undoable_impl( - node: NodeHandle, - input: *const c_char, - value: *const Value, - out: *mut CommandHandle, - ) -> i32 { - if node.is_null() || input.is_null() || value.is_null() || out.is_null() { - return -30001; - } - let id = node.ctx as usize; - let name = unsafe { CStr::from_ptr(input) } - .to_str() - .map_err(|_| ()) - .unwrap_or_default(); - // 输入名必须已存在(node.h: 未知输入 id → OAKNODE_E_NOT_FOUND)。 - if !lock().get(&id).is_some_and(|n| n.contains_key(name)) { - return -30004; - } - let next = unsafe { *value }; - let prev = current(id, &name).value; - let h = crate::bridge::undo::stub::create_numeric(id, name.to_string(), prev, next); - unsafe { *out = h }; - 0 - } - - pub(super) unsafe fn set_input_string_undoable_impl( - node: NodeHandle, - input: *const c_char, - value: *const c_char, - out: *mut CommandHandle, - ) -> i32 { - if node.is_null() || input.is_null() || value.is_null() || out.is_null() { - return -30001; - } - let id = node.ctx as usize; - let name = unsafe { CStr::from_ptr(input) } - .to_str() - .map_err(|_| ()) - .unwrap_or_default(); - // 输入名必须已存在(node.h: 未知输入 id → OAKNODE_E_NOT_FOUND)。 - if !lock().get(&id).is_some_and(|n| n.contains_key(name)) { - return -30004; - } - let next = unsafe { CStr::from_ptr(value) } - .to_string_lossy() - .into_owned(); - let prev = current(id, &name).string.unwrap_or_default(); - let h = crate::bridge::undo::stub::create_string( - id, - name.to_string(), - prev.clone(), - next.clone(), - ); - unsafe { *out = h }; - 0 - } -} - -// ---- 默认模式单测(无桩;空/NULL 句柄经真实 crate 的可解释失败路径)---------------------- - -#[cfg(all(test, not(feature = "test-stubs")))] -mod tests { - use super::*; - - /// 空/NULL 句柄经真实 oaknode 容错:未登记身份 → 空句柄;空句柄 - /// 与空指针参数被拒(OAKNODE_E_INVALID)。 - #[test] - fn empty_handle_error_paths() { - let mut cmd = CommandHandle::null(); - unsafe { - assert!(node_from_identity(0xDEAD).is_null(), "未登记身份 → 空句柄"); - assert_eq!( - set_input_undoable( - NodeHandle::null(), - std::ptr::null(), - std::ptr::null(), - &mut cmd - ), - -30001 - ); - assert_eq!( - set_input_string_undoable( - NodeHandle::null(), - std::ptr::null(), - std::ptr::null(), - &mut cmd - ), - -30001 - ); - } - } -} diff --git a/crates/oakplugin/src/bridge/render.rs b/crates/oakplugin/src/bridge/render.rs deleted file mode 100644 index 46f0a513b..000000000 --- a/crates/oakplugin/src/bridge/render.rs +++ /dev/null @@ -1,950 +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 . - -//! oakrender C ABI 导入(clip↔纹理桥用到的子集)。 -//! -//! 声明以 `include/render/renderer.h` 为准(骨架的 -//! `oakrender_texture_get_frame`/`oakrender_texture_wrap_native`/ -//! `oakrender_texture_is_dummy` 与头文件不符,弃用;真实符号为 -//! `oakrender_display_texture_*` 与 `oakrender_codec_frame_*`)。 -//! `oakrender_display_texture_wrap_native` 是 C++ 专属符号 -//! (TexturePtr 引用),Rust 不可调用——输出纹理由 oakrender 侧 -//! 创建并经句柄传入,宿主只写其帧。 -//! -//! ## 双态实现 -//! -//! - 默认:直接 Rust 调用 oakrender 的 `ffi`(单库化,见 -//! `docs/zh/plans/riir/single-lib.md`); -//! - `--features test-stubs`:库内状态桩 + 状态访问器([`stub`],纯 -//! Rust、无 `#[no_mangle]`,与真实 oakrender 的导出不冲突)——像素 -//! 路径在 cargo test 里全链路可跑。两种形态的调用面 -//! (`texture_get_frame` 等)完全一致。 - -use std::ffi::c_void; - -/// `oakrender_video_params` POD — single-lib unification: aliases the -/// oakrender crate's struct (identical layout; include/render/renderer.h:78). -pub type VideoParams = oakrender::ffi::OakRenderVideoParams; - -/// olive::PixelFormat::Format 的 f32 值。 -pub const PIXEL_FORMAT_F32: i32 = 4; -/// olive::PixelFormat::Format 的 u8 值。 -pub const PIXEL_FORMAT_U8: i32 = 0; - -/// oakrender 渲染器句柄(`OakRenderRenderer`,值型)。 -pub type RendererHandle = crate::handle::CHandle; - -/// oakrender 纹理句柄(`OakRenderTexture`,值型)。 -pub type TextureHandle = crate::handle::CHandle; - -/// oakrender 帧句柄(`OakCodecFrame`,值型;布局与 CHandle 一致)。 -pub type FrameHandle = crate::handle::CHandle; - -// ---- 桥调用面 ------------------------------------------------------------ - -/// 纹理的 CPU 帧(Texture::frame());`*out` 收到保留引用。 -pub(crate) unsafe fn texture_get_frame(texture: TextureHandle, out: *mut FrameHandle) -> i32 { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::texture_get_frame(texture, out) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oakrender::ffi::renderer::oakrender_display_texture_get_frame(texture, out) } - } -} - -/// 纹理是否占位(dummy);符号缺失 → 1(视为占位)。 -pub(crate) unsafe fn texture_is_dummy(texture: TextureHandle) -> i32 { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::texture_is_dummy(texture) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oakrender::ffi::renderer::oakrender_display_texture_is_dummy(texture) } - } -} - -/// 帧宽(空帧为 0)。 -pub(crate) unsafe fn frame_width(frame: FrameHandle) -> i32 { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::frame_width(frame) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oakrender::ffi::renderer::oakrender_codec_frame_width(frame) } - } -} - -/// 帧高。 -pub(crate) unsafe fn frame_height(frame: FrameHandle) -> i32 { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::frame_height(frame) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oakrender::ffi::renderer::oakrender_codec_frame_height(frame) } - } -} - -/// 借用的像素数据指针(最终 release 前有效)。 -pub(crate) unsafe fn frame_data(frame: FrameHandle) -> *mut c_void { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::frame_data(frame) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oakrender::ffi::renderer::oakrender_codec_frame_data(frame) } - } -} - -/// 帧的视频参数。 -pub(crate) unsafe fn frame_get_params(frame: FrameHandle, out: *mut VideoParams) -> i32 { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::frame_get_params(frame, out) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oakrender::ffi::renderer::oakrender_codec_frame_get_params(frame, out) } - } -} - -/// 按参数分配像素缓冲。 -pub(crate) unsafe fn frame_allocate(frame: FrameHandle) -> i32 { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::frame_allocate(frame) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oakrender::ffi::renderer::oakrender_codec_frame_allocate(frame) } - } -} - -/// 释放一次帧引用并清空句柄(NULL/空句柄 no-op)。 -pub(crate) unsafe fn frame_free(frame: *mut FrameHandle) { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::frame_free(frame) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oakrender::ffi::renderer::oakrender_codec_frame_free(frame) } - } -} - -/// 帧的行跨度(字节;空帧 0)。M11 第 2 期新增导入 -/// (`oakrender_codec_frame_linesize_bytes`,renderer.h:311):CPU -/// 拷贝路径(fetch/store/驱动输出装配)用它兼容真实 oakrender 的 -/// 行填充,不再假设紧凑行布局。 -pub(crate) unsafe fn frame_linesize_bytes(frame: FrameHandle) -> i32 { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::frame_linesize_bytes(frame) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oakrender::ffi::renderer::oakrender_codec_frame_linesize_bytes(frame) } - } -} - -// ---- 渲染器族(GL 路径;M11 §4)-------------------------------------------- - -/// 按后端名创建渲染器(`oakrender_display_renderer_create_dynamic`, -/// renderer.h:155)。空指针/空串 → 空句柄。 -#[allow(dead_code)] // 契约完整导入:GL 测试/后续路径按需使用(renderer.h 同签名) -pub(crate) unsafe fn renderer_create_dynamic(backend: *const std::ffi::c_char) -> RendererHandle { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::renderer_create_dynamic(backend) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oakrender::ffi::renderer::oakrender_display_renderer_create_dynamic(backend) } - } -} - -/// 初始化渲染器(`oakrender_display_renderer_init`,renderer.h:175)。 -/// `gl_context` 为借用指针(NULL = 后端默认上下文路径)。 -#[allow(dead_code)] // 契约完整导入:GL 测试/后续路径按需使用(renderer.h 同签名) -pub(crate) unsafe fn renderer_init( - renderer: RendererHandle, - gl_context: *mut std::ffi::c_void, -) -> i32 { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::renderer_init(renderer, gl_context) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oakrender::ffi::renderer::oakrender_display_renderer_init(renderer, gl_context) } - } -} - -/// 渲染器是否 OpenGL 后端(`oakrender_display_renderer_is_open_gl`, -/// renderer.h:189)。 -pub(crate) unsafe fn renderer_is_open_gl(renderer: RendererHandle) -> i32 { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::renderer_is_open_gl(renderer) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oakrender::ffi::renderer::oakrender_display_renderer_is_open_gl(renderer) } - } -} - -/// 释放一次渲染器引用并清空句柄(`oakrender_display_renderer_destroy`, -/// renderer.h:183)。 -#[allow(dead_code)] // 契约完整导入:GL 测试/后续路径按需使用(renderer.h 同签名) -pub(crate) unsafe fn renderer_destroy(renderer: *mut RendererHandle) { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::renderer_destroy(renderer) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oakrender::ffi::renderer::oakrender_display_renderer_destroy(renderer) } - } -} - -/// 在渲染器上创建纹理(`oakrender_display_texture_create`, -/// renderer.h:204)。`pixels` 可空(未初始化);`linesize` 为行跨度 -/// 字节数(0 = 紧凑行;pixels 为空时 0)。 -/// -/// 单位约定(M11 第 2 期):**字节**——以 renderer.h:206 的明文 -/// 契约为准(`Stride of pixels in bytes`)。C++ 调用点传像素行跨度, -/// 由 oakrender 侧实现 C ABI 时换算;本 crate 侧一律传字节。 -pub(crate) unsafe fn texture_create( - renderer: RendererHandle, - params: *const VideoParams, - pixels: *const std::ffi::c_void, - linesize: i32, -) -> TextureHandle { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::texture_create(renderer, params, pixels, linesize) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { - oakrender::ffi::renderer::oakrender_display_texture_create( - renderer, params, pixels, linesize, - ) - } - } -} - -/// 纹理的原生 GL id(`oakrender_display_texture_id`,renderer.h:245; -/// 空/占位/无 id 纹理为 0)。 -pub(crate) unsafe fn texture_id(texture: TextureHandle) -> i32 { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::texture_id(texture) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oakrender::ffi::renderer::oakrender_display_texture_id(texture) } - } -} - -/// 纹理参数(`oakrender_display_texture_get_params`,renderer.h:233)。 -pub(crate) unsafe fn texture_get_params(texture: TextureHandle, out: *mut VideoParams) -> i32 { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::texture_get_params(texture, out) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oakrender::ffi::renderer::oakrender_display_texture_get_params(texture, out) } - } -} - -/// 从纹理下载像素(`oakrender_display_texture_download`, -/// renderer.h:228;`linesize` 行跨度字节数,0 = 紧凑行)。 -#[allow(dead_code)] // 契约完整导入:GL 测试/后续路径按需使用(renderer.h 同签名) -pub(crate) unsafe fn texture_download( - texture: TextureHandle, - pixels: *mut std::ffi::c_void, - linesize: i32, -) -> i32 { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::texture_download(texture, pixels, linesize) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { - oakrender::ffi::renderer::oakrender_display_texture_download(texture, pixels, linesize) - } - } -} - -/// 再取一次引用(`oakrender_display_texture_retain`,renderer.h:215)。 -#[allow(dead_code)] // 契约完整导入:GL 测试/后续路径按需使用(renderer.h 同签名) -pub(crate) unsafe fn texture_retain(texture: TextureHandle) -> TextureHandle { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::texture_retain(texture) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oakrender::ffi::renderer::oakrender_display_texture_retain(texture) } - } -} - -/// 释放一次纹理引用并清空句柄(`oakrender_display_texture_free`, -/// renderer.h:223)。 -pub(crate) unsafe fn texture_free(texture: *mut TextureHandle) { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::texture_free(texture) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oakrender::ffi::renderer::oakrender_display_texture_free(texture) } - } -} - -/// 上传像素到纹理(`oakrender_display_texture_upload`,renderer.h:225; -/// `linesize` 行跨度字节数,0 = 紧凑行)。 -#[allow(dead_code)] // 契约完整导入:GL 测试/后续路径按需使用(renderer.h 同签名) -pub(crate) unsafe fn texture_upload( - texture: TextureHandle, - pixels: *const std::ffi::c_void, - linesize: i32, -) -> i32 { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::texture_upload(texture, pixels, linesize) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { - oakrender::ffi::renderer::oakrender_display_texture_upload(texture, pixels, linesize) - } - } -} - -/// 按 GL id 从渲染器下载像素(`oakrender_display_renderer_download_from_texture`, -/// renderer.h:333;`linesize` 行跨度字节数,0 = 紧凑行)。 -#[allow(dead_code)] // 契约完整导入:GL 测试/后续路径按需使用(renderer.h 同签名) -pub(crate) unsafe fn renderer_download_from_texture( - renderer: RendererHandle, - texture_id: i32, - params: *const VideoParams, - dst: *mut std::ffi::c_void, - linesize: i32, -) -> i32 { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::renderer_download_from_texture(renderer, texture_id, params, dst, linesize) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { - oakrender::ffi::renderer::oakrender_display_renderer_download_from_texture( - renderer, texture_id, params, dst, linesize, - ) - } - } -} - -// ---- 测试桩(--features test-stubs)-------------------------------------- - -/// oakrender 测试桩:库内 no_mangle 符号 + 状态访问器。 -/// -/// 纹理/帧句柄的 ctx 约定(桩内约定,与真实句柄无冲突): -/// - dst 纹理 ctx = 0xA1 → 输出帧 ctx = 0xB1; -/// - src 纹理 ctx = 0xA2 → 输入帧 ctx = 0xB2。 -#[cfg(feature = "test-stubs")] -pub mod stub { - use super::*; - use crate::handle::CHandle; - use std::sync::Mutex; - - /// 测试帧(数据 + 参数)。 - #[derive(Clone)] - pub struct StubFrame { - /// 视频参数。 - pub params: VideoParams, - /// 像素缓冲(行优先)。 - pub data: Vec, - } - - impl StubFrame { - fn new(width: i32, height: i32, format: i32) -> Self { - let len = width as usize * height as usize * 4 * bytes_per_pixel(format); - Self { - params: VideoParams { - width, - height, - format, - ..Default::default() - }, - data: vec![0u8; len], - } - } - } - - /// 桩 GL 纹理(渲染器上的 GPU 纹理的 CPU 镜像;GL 纹理同时 - /// 扮演 CPU 帧载体——与真实 olive Texture 包装 CPU 帧同构)。 - #[derive(Clone)] - struct StubGlTexture { - /// 原生 id(texture_id 的返回值)。 - id: i32, - /// CPU 镜像帧(数据 + 参数)。 - frame: StubFrame, - } - - fn bytes_per_pixel(format: i32) -> usize { - match format { - PIXEL_FORMAT_F32 => 4, - _ => 1, - } - } - - /// 按参数分配紧凑像素缓冲(宽×高×4 通道×每分量字节)。 - fn tight_len(params: &VideoParams) -> usize { - params.width as usize * params.height as usize * 4 * bytes_per_pixel(params.format) - } - - struct StubState { - dst: StubFrame, - src: StubFrame, - /// 标记为占位(dummy)的纹理 ctx(texture_is_dummy 用)。 - dummy: std::collections::HashSet, - /// GL 渲染器是否可用(renderer_is_open_gl 的返回值)。 - gl_available: bool, - /// GL 纹理注册表(ctx = GL_TEX_BASE + id)。 - gl_textures: Vec, - /// 下一个 GL 纹理 id(从 1 起)。 - next_gl_id: i32, - } - - static STATE: std::sync::LazyLock> = std::sync::LazyLock::new(|| { - Mutex::new(StubState { - dst: StubFrame::new(0, 0, 0), - src: StubFrame::new(0, 0, 0), - dummy: std::collections::HashSet::new(), - gl_available: false, - gl_textures: Vec::new(), - next_gl_id: 1, - }) - }); - - fn lock() -> std::sync::MutexGuard<'static, StubState> { - STATE.lock().unwrap_or_else(|e| e.into_inner()) - } - - /// 重置全部桩状态(测试隔离)。 - pub fn reset() { - let mut s = lock(); - s.dst = StubFrame::new(0, 0, 0); - s.src = StubFrame::new(0, 0, 0); - s.dummy.clear(); - s.gl_available = false; - s.gl_textures.clear(); - s.next_gl_id = 1; - } - - /// 把某纹理 ctx 标记为占位(dummy);`dummy=false` 取消标记。 - /// clip 桥把 dummy 输入视作空输入(NotFound)。 - pub fn set_dummy(ctx: usize, dummy: bool) { - let mut s = lock(); - if dummy { - s.dummy.insert(ctx); - } else { - s.dummy.remove(&ctx); - } - } - - /// 配置输出帧(宽/高/格式;F32 = 全链路主路径)。 - pub fn setup_dst(width: i32, height: i32, format: i32) { - lock().dst = StubFrame::new(width, height, format); - } - - /// 配置输入帧并填充像素。 - pub fn setup_src(width: i32, height: i32, format: i32, pixels: Vec) { - let mut s = lock(); - s.src = StubFrame::new(width, height, format); - s.src.data = pixels; - } - - /// 输出帧像素(断言用)。 - pub fn dst_pixels() -> Vec { - lock().dst.data.clone() - } - - /// 输入帧像素(断言用)。 - pub fn src_pixels() -> Vec { - lock().src.data.clone() - } - - /// 输出帧参数。 - pub fn dst_params() -> VideoParams { - lock().dst.params - } - - /// GL 渲染器可用标记(renderer_is_open_gl 的桩返回值)。 - pub fn set_gl_available(available: bool) { - lock().gl_available = available; - } - - /// 构造桩 GL 渲染器句柄(ctx = 0xD1)。 - pub fn make_gl_renderer() -> RendererHandle { - magic_handle(GL_RENDERER) - } - - /// 已注册的 GL 纹理 id 列表(断言用)。 - pub fn gl_texture_ids() -> Vec { - lock().gl_textures.iter().map(|t| t.id).collect() - } - - /// 某 GL 纹理 id 的像素(断言用;未知 id 返回空)。 - pub fn gl_texture_data(id: i32) -> Vec { - lock() - .gl_textures - .iter() - .find(|t| t.id == id) - .map(|t| t.frame.data.clone()) - .unwrap_or_default() - } - - /// 某 GL 纹理 id 的参数(断言用)。 - pub fn gl_texture_params(id: i32) -> Option { - lock() - .gl_textures - .iter() - .find(|t| t.id == id) - .map(|t| t.frame.params) - } - - /// 直接构造一个 GL 纹理(GL 测试的目标纹理/输入纹理模拟; - /// oakrender 侧创建纹理的 C ABI 等价物)。返回句柄(ctx = - /// GL_TEX_BASE + id)。 - pub fn make_gl_texture(width: i32, height: i32, format: i32) -> TextureHandle { - let mut s = lock(); - let id = s.next_gl_id; - s.next_gl_id += 1; - s.gl_textures.push(StubGlTexture { - id, - frame: StubFrame::new(width, height, format), - }); - magic_handle(GL_TEX_BASE + id as usize) - } - - fn frame_of(state: &mut StubState, ctx: usize) -> &mut StubFrame { - if ctx == SRC_FRAME { - &mut state.src - } else if let Some(id) = gl_id_of_ctx(ctx) { - match state.gl_textures.iter_mut().find(|t| t.id == id) { - Some(t) => &mut t.frame, - None => &mut state.dst, - } - } else { - &mut state.dst - } - } - - fn magic_handle(ctx: usize) -> CHandle { - CHandle { - ctx: ctx as *mut c_void, - addref: None, - release: None, - abi_version: 1, - } - } - - const DST_TEX: usize = 0xA1; - const SRC_TEX: usize = 0xA2; - const DST_FRAME: usize = 0xB1; - const SRC_FRAME: usize = 0xB2; - /// 桩 GL 渲染器 ctx。 - const GL_RENDERER: usize = 0xD1; - /// 桩 GL 纹理 ctx 基址(ctx = GL_TEX_BASE + id)。 - const GL_TEX_BASE: usize = 0xC0; - - /// GL 纹理 ctx → id。 - fn gl_id_of_ctx(ctx: usize) -> Option { - if ctx > GL_TEX_BASE { - Some((ctx - GL_TEX_BASE) as i32) - } else { - None - } - } - - /// 行拷贝:`src`(行跨度 src_linesize 字节)→ `dst`(行跨度 - /// dst_linesize 字节),共 `rows` 行、每行 `row_bytes` 字节。 - fn copy_rows( - src: &[u8], - src_linesize: usize, - dst: &mut [u8], - dst_linesize: usize, - row_bytes: usize, - rows: usize, - ) { - for y in 0..rows { - let s = y * src_linesize; - let d = y * dst_linesize; - if row_bytes == src_linesize && src_linesize == dst_linesize { - dst[d..d + row_bytes].copy_from_slice(&src[s..s + row_bytes]); - } else { - dst[d..d + row_bytes] - .copy_from_slice(&src[s..s + row_bytes.min(src.len().saturating_sub(s))]); - } - } - } - - pub(super) unsafe fn texture_get_frame(texture: TextureHandle, out: *mut FrameHandle) -> i32 { - if out.is_null() { - return -1; - } - // GL 纹理:帧句柄即纹理自身 ctx(frame_of 按 ctx 反查镜像帧)。 - let frame_ctx = if texture.ctx as usize == SRC_TEX { - SRC_FRAME - } else if gl_id_of_ctx(texture.ctx as usize).is_some() { - texture.ctx as usize - } else { - DST_FRAME - }; - unsafe { *out = magic_handle(frame_ctx) }; - 0 - } - - pub(super) unsafe fn texture_is_dummy(texture: TextureHandle) -> i32 { - lock().dummy.contains(&(texture.ctx as usize)) as i32 - } - - pub(super) unsafe fn frame_width(frame: FrameHandle) -> i32 { - let mut s = lock(); - frame_of(&mut s, frame.ctx as usize).params.width - } - - pub(super) unsafe fn frame_height(frame: FrameHandle) -> i32 { - let mut s = lock(); - frame_of(&mut s, frame.ctx as usize).params.height - } - - pub(super) unsafe fn frame_data(frame: FrameHandle) -> *mut c_void { - let mut s = lock(); - let f = frame_of(&mut s, frame.ctx as usize); - f.data.as_mut_ptr() as *mut c_void - } - - pub(super) unsafe fn frame_get_params(frame: FrameHandle, out: *mut VideoParams) -> i32 { - if out.is_null() { - return -1; - } - let mut s = lock(); - unsafe { *out = frame_of(&mut s, frame.ctx as usize).params }; - 0 - } - - pub(super) unsafe fn frame_allocate(frame: FrameHandle) -> i32 { - let mut s = lock(); - let f = frame_of(&mut s, frame.ctx as usize); - let len = f.params.width as usize - * f.params.height as usize - * 4 * bytes_per_pixel(f.params.format); - f.data.resize(len, 0); - 0 - } - - pub(super) unsafe fn frame_free(frame: *mut FrameHandle) { - if !frame.is_null() { - unsafe { (*frame).ctx = std::ptr::null_mut() }; - } - } - - pub(super) unsafe fn frame_linesize_bytes(frame: FrameHandle) -> i32 { - let mut s = lock(); - let f = frame_of(&mut s, frame.ctx as usize); - (f.params.width * 4 * bytes_per_pixel(f.params.format) as i32) as i32 - } - - // ---- 渲染器族桩 --------------------------------------------------------- - - pub(super) unsafe fn renderer_create_dynamic( - backend: *const std::ffi::c_char, - ) -> RendererHandle { - if backend.is_null() { - return RendererHandle::null(); - } - let id = unsafe { std::ffi::CStr::from_ptr(backend) } - .to_str() - .unwrap_or(""); - if id != "opengl" { - return RendererHandle::null(); - } - magic_handle(GL_RENDERER) - } - - pub(super) unsafe fn renderer_init( - _renderer: RendererHandle, - _gl_context: *mut std::ffi::c_void, - ) -> i32 { - 0 - } - - pub(super) unsafe fn renderer_is_open_gl(renderer: RendererHandle) -> i32 { - if renderer.ctx as usize != GL_RENDERER { - return 0; - } - lock().gl_available as i32 - } - - pub(super) unsafe fn renderer_destroy(renderer: *mut RendererHandle) { - if !renderer.is_null() { - unsafe { (*renderer).ctx = std::ptr::null_mut() }; - } - } - - // ---- 纹理族桩(GL 纹理注册表)------------------------------------------ - - pub(super) unsafe fn texture_create( - renderer: RendererHandle, - params: *const VideoParams, - pixels: *const std::ffi::c_void, - linesize: i32, - ) -> TextureHandle { - if renderer.ctx as usize != GL_RENDERER || params.is_null() { - return TextureHandle::null(); - } - let params = unsafe { *params }; - if params.width <= 0 || params.height <= 0 { - return TextureHandle::null(); - } - let mut s = lock(); - let id = s.next_gl_id; - s.next_gl_id += 1; - let mut data = vec![0u8; tight_len(¶ms)]; - if !pixels.is_null() { - // linesize 字节/行(renderer.h 的 bytes 契约;0 → 紧凑行)。 - let bpp = bytes_per_pixel(params.format); - let row_bytes = (params.width as usize) * 4 * bpp; - let src_linesize = if linesize > 0 { - linesize as usize - } else { - row_bytes - }; - let src = unsafe { - std::slice::from_raw_parts( - pixels as *const u8, - src_linesize * params.height as usize, - ) - }; - copy_rows( - src, - src_linesize, - &mut data, - row_bytes, - row_bytes.min(src_linesize), - params.height as usize, - ); - } - s.gl_textures.push(StubGlTexture { - id, - frame: StubFrame { params, data }, - }); - magic_handle(GL_TEX_BASE + id as usize) - } - - pub(super) unsafe fn texture_id(texture: TextureHandle) -> i32 { - gl_id_of_ctx(texture.ctx as usize).unwrap_or(0) - } - - pub(super) unsafe fn texture_get_params(texture: TextureHandle, out: *mut VideoParams) -> i32 { - if out.is_null() { - return -1; - } - let mut s = lock(); - let Some(id) = gl_id_of_ctx(texture.ctx as usize) else { - return -1; - }; - let Some(t) = s.gl_textures.iter().find(|t| t.id == id) else { - return -1; - }; - unsafe { *out = t.frame.params }; - 0 - } - - pub(super) unsafe fn texture_download( - texture: TextureHandle, - pixels: *mut std::ffi::c_void, - linesize: i32, - ) -> i32 { - let mut s = lock(); - let Some(id) = gl_id_of_ctx(texture.ctx as usize) else { - return -1; - }; - let Some(t) = s.gl_textures.iter().find(|t| t.id == id) else { - return -1; - }; - if pixels.is_null() { - return -1; - } - let bpp = bytes_per_pixel(t.frame.params.format); - let row_bytes = (t.frame.params.width as usize) * 4 * bpp; - let dst_linesize = if linesize > 0 { - linesize as usize - } else { - row_bytes - }; - let dst = unsafe { - std::slice::from_raw_parts_mut( - pixels as *mut u8, - dst_linesize * t.frame.params.height as usize, - ) - }; - let src = t.frame.data.clone(); - copy_rows( - &src, - row_bytes, - dst, - dst_linesize, - row_bytes, - t.frame.params.height as usize, - ); - 0 - } - - pub(super) unsafe fn texture_upload( - texture: TextureHandle, - pixels: *const std::ffi::c_void, - linesize: i32, - ) -> i32 { - let mut s = lock(); - let Some(id) = gl_id_of_ctx(texture.ctx as usize) else { - return -1; - }; - let Some(t) = s.gl_textures.iter_mut().find(|t| t.id == id) else { - return -1; - }; - if pixels.is_null() { - return -1; - } - let bpp = bytes_per_pixel(t.frame.params.format); - let row_bytes = (t.frame.params.width as usize) * 4 * bpp; - let src_linesize = if linesize > 0 { - linesize as usize - } else { - row_bytes - }; - let src = unsafe { - std::slice::from_raw_parts( - pixels as *const u8, - src_linesize * t.frame.params.height as usize, - ) - }; - let mut data = vec![0u8; tight_len(&t.frame.params)]; - copy_rows( - src, - src_linesize, - &mut data, - row_bytes, - row_bytes.min(src_linesize), - t.frame.params.height as usize, - ); - t.frame.data = data; - 0 - } - - pub(super) unsafe fn texture_retain(texture: TextureHandle) -> TextureHandle { - texture - } - - pub(super) unsafe fn texture_free(texture: *mut TextureHandle) { - if texture.is_null() { - return; - } - let ctx = unsafe { (*texture).ctx as usize }; - if let Some(id) = gl_id_of_ctx(ctx) { - let mut s = lock(); - s.gl_textures.retain(|t| t.id != id); - } - unsafe { (*texture).ctx = std::ptr::null_mut() }; - } - - pub(super) unsafe fn renderer_download_from_texture( - renderer: RendererHandle, - texture_id: i32, - params: *const VideoParams, - dst: *mut std::ffi::c_void, - linesize: i32, - ) -> i32 { - if renderer.ctx as usize != GL_RENDERER || params.is_null() || dst.is_null() { - return -1; - } - let req = unsafe { *params }; - let s = lock(); - let Some(t) = s.gl_textures.iter().find(|t| t.id == texture_id) else { - return -1; - }; - let bpp = bytes_per_pixel(req.format); - let row_bytes = (req.width as usize) * 4 * bpp; - let dst_linesize = if linesize > 0 { - linesize as usize - } else { - row_bytes - }; - let out = unsafe { - std::slice::from_raw_parts_mut(dst as *mut u8, dst_linesize * req.height as usize) - }; - let src = t.frame.data.clone(); - copy_rows( - &src, - row_bytes, - out, - dst_linesize, - row_bytes, - req.height as usize, - ); - 0 - } -} - -// ---- 默认模式单测(无桩;空/NULL 句柄经真实 crate 的可解释失败路径)---------------------- - -#[cfg(all(test, not(feature = "test-stubs")))] -mod tests { - use super::*; - - /// 空/NULL 句柄经真实 oakrender 全部容错不崩:返回 - /// `OAKRENDER_E_INVALID`(-70001)或空值。 - #[test] - fn empty_handle_error_paths() { - let mut frame = FrameHandle::null(); - let tex = TextureHandle::null(); - unsafe { - assert_eq!(texture_get_frame(tex, &mut frame), -70001); - // 空句柄不是占位纹理:真实 oakrender 返回 0。 - assert_eq!(texture_is_dummy(tex), 0); - assert_eq!(frame_width(frame), 0); - assert_eq!(frame_height(frame), 0); - assert!(frame_data(frame).is_null()); - assert_eq!(frame_get_params(frame, &mut VideoParams::default()), -70001); - assert_eq!(frame_allocate(frame), -70001); - frame_free(&mut frame); // 空句柄 no-op - frame_free(std::ptr::null_mut()); // NULL no-op - } - } -} diff --git a/crates/oakplugin/src/bridge/undo.rs b/crates/oakplugin/src/bridge/undo.rs deleted file mode 100644 index 9e867fb1b..000000000 --- a/crates/oakplugin/src/bridge/undo.rs +++ /dev/null @@ -1,381 +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 . - -//! oakundo C ABI 导入(参数回写打包用到的子集;include/undo/ -//! undocommand.h)。 -//! -//! ## 语义(对照 C++ 的 oliveplugininstance.cpp `submit_undo_command`) -//! -//! 写回一律以"命令"为单位:数值/字符串 set 各产出一条命令 -//! (node 桥的 `*_undoable`),立即 `redo_now` 生效;编辑事务 -//! (paramEditBegin/End)内多条命令并入一条 multi -//! ([`command_init_multi`] + [`command_multi_add_child`]), -//! editEnd 时整体 `redo_now` 后释放。 -//! -//! ## 双态实现(同 [`crate::bridge::render`]) -//! -//! - 默认:直接 Rust 调用 oakundo 的 `ffi`(单库化,见 -//! `docs/zh/plans/riir/single-lib.md`); -//! - `--features test-stubs`:库内状态桩([`stub`],纯 Rust、无 -//! `#[no_mangle]`,与真实 oakundo 的导出不冲突)——命令表 + undo/redo -//! 语义在 cargo test 里全链路可跑。两种形态的调用面完全一致。 - -use crate::handle::CHandle; - -/// oakundo 命令句柄(值型)。 -pub type CommandHandle = crate::handle::CHandle; - -// ---- 桥调用面 ------------------------------------------------------------ - -/// 创建 multi 命令(`oakundo_command_init_multi`,undocommand.h:85)。 -pub(crate) unsafe fn command_init_multi() -> CommandHandle { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::command_init_multi_impl() } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oakundo::ffi::command::oakundo_command_init_multi() } - } -} - -/// 直接 redo(不进栈的立即执行路径;`oakundo_command_redo_now`)。 -pub(crate) unsafe fn command_redo_now(command: CommandHandle) -> i32 { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::command_redo_now_impl(command) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oakundo::ffi::command::oakundo_command_redo_now(command) } - } -} - -/// 把子命令并入 multi(`oakundo_command_multi_add_child`)。 -pub(crate) unsafe fn command_multi_add_child(multi: CommandHandle, child: CommandHandle) -> i32 { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::command_multi_add_child_impl(multi, child) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oakundo::ffi::command::oakundo_command_multi_add_child(multi, child) } - } -} - -/// 释放命令句柄(`oakundo_command_free`;NULL/空句柄 no-op)。 -pub(crate) unsafe fn command_free(command: *mut CommandHandle) { - #[cfg(feature = "test-stubs")] - { - unsafe { stub::command_free_impl(command) } - } - #[cfg(not(feature = "test-stubs"))] - { - unsafe { oakundo::ffi::command::oakundo_command_free(command) } - } -} - -// ---- 测试桩(--features test-stubs)-------------------------------------- - -/// oakundo 测试桩:命令表 + undo/redo 语义。 -/// -/// 命令句柄的 ctx 约定(桩内约定):`ctx = 命令 id`。记录在 -/// [`command_free`] 后仍保留(`freed` 标记),供测试检查命令捕获的 -/// prev/next 并驱动 undo/redo——真实 oakundo 的 free 会销毁命令, -/// 这是桩的刻意简化(记录仅为测试保留)。 -#[cfg(feature = "test-stubs")] -pub mod stub { - use super::*; - use crate::bridge::node; - use std::collections::HashMap; - use std::sync::atomic::{AtomicUsize, Ordering}; - use std::sync::{LazyLock, Mutex}; - - /// 一条命令的记录(redo 应用 next、undo 恢复 prev)。 - #[derive(Clone, Debug)] - pub struct CommandRecord { - /// 命令 id(= 句柄 ctx)。 - pub id: usize, - /// 目标节点身份。 - pub node: usize, - /// 目标输入名。 - pub input: String, - /// 数值回写的旧值。 - pub prev: node::Value, - /// 数值回写的新值。 - pub next: node::Value, - /// 字符串回写的旧值。 - pub prev_string: Option, - /// 字符串回写的新值。 - pub next_string: Option, - /// redo 是否已应用。 - pub applied: bool, - /// 是否 multi 命令。 - pub is_multi: bool, - /// multi 的子命令 id。 - pub children: Vec, - /// free 是否已调用(记录保留给测试检查)。 - pub freed: bool, - } - - static COMMANDS: LazyLock>> = - LazyLock::new(|| Mutex::new(HashMap::new())); - static NEXT_ID: AtomicUsize = AtomicUsize::new(1); - - fn lock() -> std::sync::MutexGuard<'static, HashMap> { - COMMANDS.lock().unwrap_or_else(|e| e.into_inner()) - } - - fn magic(id: usize) -> CommandHandle { - CHandle { - ctx: id as *mut std::ffi::c_void, - addref: None, - release: None, - abi_version: crate::handle::OAKPLUGIN_ABI_VERSION, - } - } - - fn next_id() -> usize { - NEXT_ID.fetch_add(1, Ordering::Relaxed) - } - - /// 重置命令表(测试隔离)。 - pub fn reset() { - lock().clear(); - } - - /// 全部命令记录快照(含已 free 的;id 升序)。 - pub fn records() -> Vec { - let mut v: Vec = lock().values().cloned().collect(); - v.sort_by_key(|r| r.id); - v - } - - /// 最近一次创建的命令记录。 - pub fn last_command() -> Option { - let m = lock(); - m.values().max_by_key(|r| r.id).cloned() - } - - /// 撤销一条命令(测试助手:验证命令捕获的 prev 正确)。multi 按 - /// 子命令逆序撤销;单命令仅在其已应用时恢复 prev。 - pub fn undo(id: usize) { - let (is_multi, children) = lock() - .get(&id) - .map(|r| (r.is_multi, r.children.clone())) - .unwrap_or((false, Vec::new())); - if is_multi { - for c in children.iter().rev() { - undo(*c); - } - if let Some(r) = lock().get_mut(&id) { - r.applied = false; - } - return; - } - let mut m = lock(); - let Some(r) = m.get_mut(&id) else { return }; - if r.applied { - node::stub::apply(r.node, &r.input, r.prev, r.prev_string.clone()); - r.applied = false; - } - } - - /// 重做一条命令(测试助手)。redo 幂等(已应用则 no-op)。 - pub fn redo(id: usize) { - let (is_multi, children) = lock() - .get(&id) - .map(|r| (r.is_multi, r.children.clone())) - .unwrap_or((false, Vec::new())); - if is_multi { - for c in &children { - redo(*c); - } - if let Some(r) = lock().get_mut(&id) { - r.applied = true; - } - return; - } - let mut m = lock(); - let Some(r) = m.get_mut(&id) else { return }; - if !r.applied { - node::stub::apply(r.node, &r.input, r.next, r.next_string.clone()); - r.applied = true; - } - } - - /// 登记一条数值写回命令(node 桥调用)。 - pub(crate) fn create_numeric( - node_id: usize, - input: String, - prev: node::Value, - next: node::Value, - ) -> CommandHandle { - let id = next_id(); - lock().insert( - id, - CommandRecord { - id, - node: node_id, - input, - prev, - next, - prev_string: None, - next_string: None, - applied: false, - is_multi: false, - children: Vec::new(), - freed: false, - }, - ); - magic(id) - } - - /// 登记一条字符串写回命令(node 桥调用)。 - pub(crate) fn create_string( - node_id: usize, - input: String, - prev: String, - next: String, - ) -> CommandHandle { - let id = next_id(); - lock().insert( - id, - CommandRecord { - id, - node: node_id, - input, - prev: node::Value::string(), - next: node::Value::string(), - prev_string: Some(prev), - next_string: Some(next), - applied: false, - is_multi: false, - children: Vec::new(), - freed: false, - }, - ); - magic(id) - } - - pub(super) unsafe fn command_init_multi_impl() -> CommandHandle { - let id = next_id(); - lock().insert( - id, - CommandRecord { - id, - node: 0, - input: String::new(), - prev: node::Value::default(), - next: node::Value::default(), - prev_string: None, - next_string: None, - applied: false, - is_multi: true, - children: Vec::new(), - freed: false, - }, - ); - magic(id) - } - - pub(super) unsafe fn command_redo_now_impl(command: CommandHandle) -> i32 { - if command.is_null() { - return -40001; - } - // 先取 multi 的子命令列表,再逐条递归(避免持锁递归)。 - let (is_multi, children) = lock() - .get(&(command.ctx as usize)) - .map(|r| (r.is_multi, r.children.clone())) - .unwrap_or((false, Vec::new())); - if is_multi { - for c in &children { - let h = magic(*c); - unsafe { command_redo_now_impl(h) }; - } - if let Some(r) = lock().get_mut(&(command.ctx as usize)) { - r.applied = true; - } - return 0; - } - let mut m = lock(); - let Some(r) = m.get_mut(&(command.ctx as usize)) else { - return -40004; - }; - if !r.applied { - node::stub::apply(r.node, &r.input, r.next, r.next_string.clone()); - r.applied = true; - } - 0 - } - - pub(super) unsafe fn command_multi_add_child_impl( - multi: CommandHandle, - child: CommandHandle, - ) -> i32 { - if multi.is_null() || child.is_null() { - return -40001; - } - let mut m = lock(); - let Some(r) = m.get_mut(&(multi.ctx as usize)) else { - return -40004; - }; - if !r.is_multi { - return -40002; - } - r.children.push(child.ctx as usize); - 0 - } - - pub(super) unsafe fn command_free_impl(command: *mut CommandHandle) { - if command.is_null() { - return; - } - let h = unsafe { &mut *command }; - if h.is_null() { - return; - } - if let Some(r) = lock().get_mut(&(h.ctx as usize)) { - // 记录保留给测试检查(undo/redo 仍可按 id 驱动)。 - r.freed = true; - } - h.ctx = std::ptr::null_mut(); - } -} - -// ---- 默认模式单测(无桩;空/NULL 句柄经真实 crate 的可解释失败路径)---------------------- - -#[cfg(all(test, not(feature = "test-stubs")))] -mod tests { - use super::*; - - /// 空/NULL 句柄经真实 oakundo 全部容错不崩:`init_multi` 产生 - /// 真实命令,空句柄操作返回 `OAKUNDO_E_INVALID`(-20001)。 - #[test] - fn empty_handle_error_paths() { - unsafe { - let mut m = command_init_multi(); - assert!(!m.is_null(), "init_multi 应产生真实命令句柄"); - assert_eq!(command_redo_now(CommandHandle::null()), -20001); - assert_eq!( - command_multi_add_child(CommandHandle::null(), CommandHandle::null()), - -20001 - ); - command_free(&mut m); // 空句柄 no-op - command_free(std::ptr::null_mut()); // NULL no-op - } - } -} diff --git a/crates/oakplugin/src/clip.rs b/crates/oakplugin/src/clip.rs index d8e2e0e01..736cd0277 100644 --- a/crates/oakplugin/src/clip.rs +++ b/crates/oakplugin/src/clip.rs @@ -17,17 +17,16 @@ //! clip 实例:clip ↔ oakrender 纹理桥。 //! //! 对应 C++ 的 `OliveClipInstance`。纹理数据经 -//! [`crate::bridge::render`] 的 oakrender C ABI 流动;OFX 侧只看到 -//! [`crate::image::Image`](CPU 路径)。 +//! [`crate::render`](oakrender 值类型:`Texture`/`Frame`)流动; +//! OFX 侧只看到 [`crate::image::Image`](CPU 路径)。 //! //! `#[repr(C)]` + props 在偏移 0(句柄约定,见 [`crate::suites::tag`]; //! clip handle 即 `&props`)。 //! -//! `// TODO(bridge)`:`fetch_image`/`store_output_image` 依赖 -//! bridge::render 的帧访问 C ABI(声明未冻结),保留 todo!()。 -//! **已落地(M11 第 1 期)**:帧访问 C ABI 在 [`crate::bridge::render`] -//! 冻结(`oakrender_display_texture_*`/`oakrender_codec_frame_*`), -//! 两处桥实现完成。 +//! 单库化后 oakrender 的 ffi 已删除:帧访问走 +//! [`oakrender::texture::Texture::to_frame`] 值路径(GPU 纹理经后端 +//! 下载、CPU 纹理克隆),帧释放随值 drop 自动发生(原 +//! `texture_get_frame`/`frame_free` 句柄调用面随桩删除)。 use crate::instance::{OfxRangeD, OfxRectD, RenderScale}; use crate::property::PropertySet; @@ -40,12 +39,12 @@ pub struct ClipInstance { pub props: PropertySet, /// clip 名。 pub name: String, - /// 当前输入纹理(oakrender 句柄的借用拷贝;输出 clip 为 None)。 - input_texture: std::sync::Mutex>, + /// 当前输入纹理(oakrender 值;输出 clip 为 None)。 + input_texture: std::sync::Mutex>, /// 当前输出纹理(C++ `output_textures_` 的 phase 1 单槽; /// [`store_output_image`](Self::store_output_image) 的回写目标; /// 输入 clip 为 None)。 - output_texture: std::sync::Mutex>, + output_texture: std::sync::Mutex>, } /// 从 clip 属性读协商分量(getClipPreferences 写入)。 @@ -136,29 +135,19 @@ impl ClipInstance { } /// 挂接输入纹理(oaknode 侧 clip 输入值变化时由 param/render 桥 - /// 调用)。`time` 用于多帧纹理选择。空句柄断开。 - pub fn set_input_texture(&self, texture: crate::bridge::render::TextureHandle, _time: f64) { - let mut slot = self.input_texture.lock().unwrap_or_else(|e| e.into_inner()); - if texture.is_null() { - *slot = None; - } else { - *slot = Some(texture); - } + /// 调用)。`time` 用于多帧纹理选择。None 断开。 + pub fn set_input_texture(&self, texture: Option, _time: f64) { + *self.input_texture.lock().unwrap_or_else(|e| e.into_inner()) = texture; } - /// 挂接输出纹理(render 驱动创建并经句柄传入;C++ + /// 挂接输出纹理(render 驱动创建并经值传入;C++ /// `setOutputTexture` 的 phase 1 单槽版)。`time` 用于多帧纹理 - /// 选择(`// [P2]`)。空句柄断开。 - pub fn set_output_texture(&self, texture: crate::bridge::render::TextureHandle, _time: f64) { - let mut slot = self + /// 选择(`// [P2]`)。None 断开。 + pub fn set_output_texture(&self, texture: Option, _time: f64) { + *self .output_texture .lock() - .unwrap_or_else(|e| e.into_inner()); - if texture.is_null() { - *slot = None; - } else { - *slot = Some(texture); - } + .unwrap_or_else(|e| e.into_inner()) = texture; } /// 抓取本 clip 在 `time` 的图像(OFX clipGetImage 的宿主侧)。 @@ -174,7 +163,7 @@ impl ClipInstance { scale: RenderScale, region: Option, ) -> crate::error::Result { - use crate::bridge::render::*; + use crate::render::PIXEL_FORMAT_F32; use crate::error::Error; let _ = (time, scale); @@ -188,18 +177,13 @@ impl ClipInstance { .clone() .ok_or(Error::NotFound)?; // 占位纹理(dummy):视作无输入。 - if unsafe { crate::bridge::render::texture_is_dummy(texture) } != 0 { + if texture.is_dummy() { return Err(Error::NotFound); } - let mut frame = FrameHandle::null(); - let r = unsafe { crate::bridge::render::texture_get_frame(texture, &mut frame) }; - if r != 0 || frame.is_null() { - return Err(Error::Failed("纹理无 CPU 帧".into())); - } - let mut params = VideoParams::default(); - unsafe { crate::bridge::render::frame_get_params(frame, &mut params) }; + // 纹理 → CPU 帧(GPU 纹理后端下载;帧随 drop 释放)。 + let frame = crate::render::texture_get_frame(&texture)?; + let params = frame.video_params(); if params.format != PIXEL_FORMAT_F32 { - unsafe { crate::bridge::render::frame_free(&mut frame) }; return Err(Error::Failed(format!( "输入帧格式 {} 非 F32(第 1 期约束)", params.format @@ -220,9 +204,8 @@ impl ClipInstance { y2: h, }, ); - let src = unsafe { crate::bridge::render::frame_data(frame) }; + let src = frame.data(); if src.is_null() { - unsafe { crate::bridge::render::frame_free(&mut frame) }; return Err(Error::Failed("帧无数据".into())); } // 行优先拷贝(帧行跨度经 linesize 读取——真实 oakrender 帧可 @@ -230,16 +213,15 @@ impl ClipInstance { // 行,对真实 oakrender 的填充帧会写错列)。 let channels = components.channel_count(); let tight = (w as usize) * channels * 4; - let row = unsafe { crate::bridge::render::frame_linesize_bytes(frame) } as usize; + let row = frame.linesize_bytes(); let row = if row > 0 { row } else { tight }; - let src_bytes = unsafe { std::slice::from_raw_parts(src as *const u8, row * h as usize) }; + let src_bytes = unsafe { std::slice::from_raw_parts(src, row * h as usize) }; let dst = image.pixels_mut(); for y in 0..h as usize { let s = y * row; let d = y * tight; dst[d..d + tight].copy_from_slice(&src_bytes[s..s + tight]); } - unsafe { crate::bridge::render::frame_free(&mut frame) }; Ok(image) } @@ -247,17 +229,19 @@ impl ClipInstance { /// [`crate::instance::Instance::render`] 的调用方使用)。 /// /// 输出纹理由 oakrender 侧创建并经 [`Self::set_output_texture`] - /// 挂入——本函数取该纹理的 CPU 帧(`texture_get_frame`),按帧 + /// 挂入——本函数取该纹理的 CPU 帧(GPU 纹理经后端下载,写回后 + /// 对 `Texture::Gpu` 再经 + /// [`oakrender::backend::GpuContextLike::upload`] 上传),按帧 /// 参数校验 F32 与尺寸后整帧拷贝图像像素(全链路 F32;C++ /// pluginrenderer 的 `readback/wrap` 路径第 1 期以 CPU 拷贝表达, - /// GL 走 [`crate::bridge::render`] 的 `// [P2]`)。未挂输出纹理 + /// GL 走 [`crate::render`] 的 `// [P2]`)。未挂输出纹理 /// 或纹理为占位(dummy)→ [`crate::error::Error::NotFound`]。 - /// 成功返回纹理句柄(借用拷贝,调用方负责其生命周期)。 + /// 成功返回纹理值(克隆,随 drop 释放)。 pub fn store_output_image( &self, image: &crate::image::Image, - ) -> crate::error::Result { - use crate::bridge::render::*; + ) -> crate::error::Result { + use crate::render::{texture_get_frame, PIXEL_FORMAT_F32}; use crate::error::Error; let texture = self @@ -266,18 +250,12 @@ impl ClipInstance { .unwrap_or_else(|e| e.into_inner()) .clone() .ok_or(Error::NotFound)?; - if unsafe { texture_is_dummy(texture) } != 0 { + if texture.is_dummy() { return Err(Error::NotFound); } - let mut frame = FrameHandle::null(); - let r = unsafe { texture_get_frame(texture, &mut frame) }; - if r != 0 || frame.is_null() { - return Err(Error::Failed("输出纹理无 CPU 帧".into())); - } - let mut params = VideoParams::default(); - unsafe { frame_get_params(frame, &mut params) }; + let mut frame = texture_get_frame(&texture)?; + let params = frame.video_params(); if params.format != PIXEL_FORMAT_F32 { - unsafe { frame_free(&mut frame) }; return Err(Error::Failed(format!( "输出帧格式 {} 非 F32(第 1 期约束)", params.format @@ -287,33 +265,37 @@ impl ClipInstance { // 图像与帧必须同尺寸(全链路 F32;宽高/行宽/总长逐项校验)。 let tight = w * image.components().channel_count() * 4; if tight != image.row_bytes() || tight * h != image.pixels().len() { - unsafe { frame_free(&mut frame) }; return Err(Error::Failed("图像尺寸与输出帧不一致".into())); } - let dst = unsafe { frame_data(frame) }; + let dst = frame.data_mut(); if dst.is_null() { - unsafe { frame_free(&mut frame) }; return Err(Error::Failed("输出帧无数据".into())); } // 行优先拷贝(目标帧行跨度经 linesize 读取——真实 oakrender // 帧可有行填充;M11 §4 修复同 fetch_image)。 - let row = unsafe { frame_linesize_bytes(frame) } as usize; + let row = frame.linesize_bytes(); let row = if row > 0 { row } else { tight }; - let dst_bytes = unsafe { std::slice::from_raw_parts_mut(dst as *mut u8, row * h) }; + let dst_bytes = unsafe { std::slice::from_raw_parts_mut(dst, row * h) }; let pixels = image.pixels(); for y in 0..h { let d = y * row; let s = y * tight; dst_bytes[d..d + tight].copy_from_slice(&pixels[s..s + tight]); } - unsafe { frame_free(&mut frame) }; + // GPU 目标纹理:拷贝只落在下载帧上,经后端 upload 回写 + // (CPU 纹理无需上传)。 + if let crate::render::Texture::Gpu { token, ctx, .. } = &texture { + ctx.upload(*token, &frame) + .map_err(|e| Error::Failed(format!("输出纹理上传失败:{e}")))?; + } Ok(texture) } /// 本 clip 的时间域(clipGetFrameRange)。 /// - /// `// TODO(bridge)`:输入范围经 oakrender 帧的时间基推导 - /// (time_base)——随 renderer 桥落地。 + /// `// TODO(value-model)`:输入范围经 oakrender 帧的时间基推导 + /// (time_base)——随 clip 迁移到 `oakrender::texture::Texture` + /// 值模型落地。 pub fn frame_range(&self) -> crate::error::Result { let _ = OfxRangeD::default(); Err(crate::error::Error::Failed( diff --git a/crates/oakplugin/src/ffi.rs b/crates/oakplugin/src/ffi.rs deleted file mode 100644 index 22486cfd7..000000000 --- a/crates/oakplugin/src/ffi.rs +++ /dev/null @@ -1,1188 +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 . - -//! C ABI 出口层:逐字实现 `include/plugin/{host,instance,error}.h`。 -//! -//! 本模块的函数体只允许做三件事:解句柄、调 safe Rust 实现、经 -//! [`crate::handle::guard*`] 映射结果。签名与公共头逐字一致; -//! 行为契约以头文件文档注释为准。 -//! -//! 骨架声明与头文件多处不符,以此文件为准(均已核对 -//! include/plugin/{host,instance}.h 与 include/node/node.h): -//! - `set/get_param` 是 `oaknode_value` POD(非 f64); -//! - `render` 是 `(dst, src) 纹理 + time`; -//! - `progress_fn` 是 `(progress, userdata)`,非 0 = 中止; -//! - `message_fn` 是 `(type, message, userdata)`。 -//! 内省族(param_count 等)为 M11 §2.1 新增,声明随本文件冻结, -//! 头文件同步补(骨架注释约定)。 -//! -//! ## 返回值约定(两段式字符串) -//! -//! `buf == NULL` → 返回所需长度(含 NUL,正值);`buf != NULL` → -//! 写截断拷贝并返回 `OAKPLUGIN_OK`。count 类查询直接返回数值。 -//! 这两类不走 [`crate::handle::guard`](其 Ok 恒映射 0),统一用 -//! 本模块的 `catch`/`string_query` 助手。 - -use std::ffi::{c_char, c_int, c_void, CStr, CString}; - -use crate::handle::{get, guard, guard_handle, guard_void, make_owned, CHandle}; -use crate::param::ParamValue; - -// ---- oaknode_value(include/node/node.h:93,字段逐字一致)---- - -/// oaknode_value_type 的取值(node.h:74)。 -pub mod node_value_type { - /// OAKNODE_VALUE_NONE。 - pub const NONE: i32 = 0; - /// OAKNODE_VALUE_INT。 - pub const INT: i32 = 1; - /// OAKNODE_VALUE_FLOAT。 - pub const FLOAT: i32 = 2; - /// OAKNODE_VALUE_BOOL。 - pub const BOOL: i32 = 3; - /// OAKNODE_VALUE_RATIONAL。 - pub const RATIONAL: i32 = 4; - /// OAKNODE_VALUE_COLOR。 - pub const COLOR: i32 = 5; - /// OAKNODE_VALUE_VEC2。 - pub const VEC2: i32 = 6; - /// OAKNODE_VALUE_VEC3。 - pub const VEC3: i32 = 7; - /// OAKNODE_VALUE_VEC4。 - pub const VEC4: i32 = 8; - /// OAKNODE_VALUE_COMBO。 - pub const COMBO: i32 = 9; - /// OAKNODE_VALUE_STRING。 - pub const STRING: i32 = 10; -} - -/// oaknode_value(include/node/node.h:93)。 -#[repr(C)] -#[derive(Clone, Copy, Debug)] -pub struct OakNodeValue { - /// 类型([`node_value_type`])。 - pub r#type: c_int, - /// INT/COMBO 值、BOOL 0/1、RATIONAL 分子。 - pub num: i64, - /// RATIONAL 分母。 - pub den: i64, - /// FLOAT f[0];VEC2/3/4 f[0..n-1];COLOR r,g,b,a。 - pub f: [f64; 4], -} - -impl Default for OakNodeValue { - fn default() -> Self { - Self { - r#type: node_value_type::NONE, - num: 0, - den: 0, - f: [0.0; 4], - } - } -} - -/// panic 兜底(count/两段式等 guard 不适用的路径)。 -fn catch(f: impl FnOnce() -> c_int) -> c_int { - std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) - .unwrap_or(crate::error::OAKPLUGIN_E_FAILED) -} - -/// 两段式字符串查询公共路径:`buf == NULL` 返回所需长度(含 NUL, -/// 正值);否则写截断拷贝返回 `OAKPLUGIN_OK`;`get` 失败返回其 -/// 错误码。 -fn string_query( - buf: *mut c_char, - buf_size: c_int, - get: impl FnOnce() -> crate::error::Result, -) -> c_int { - catch(|| match get() { - Ok(s) => { - let need = (s.len() + 1) as c_int; - if buf.is_null() { - return need; - } - if buf_size <= 0 { - return crate::error::OAKPLUGIN_E_INVALID; - } - // 留 1 字节给 NUL(buf_size 满时不得写 [buf_size])。 - let n = (buf_size as usize - 1).min(s.len()); - unsafe { - std::ptr::copy_nonoverlapping(s.as_ptr(), buf as *mut u8, n); - *buf.add(n) = 0; - } - // 装得下 → OK;装不下 → 返回所需长度(snprintf 风格, - // 调用方据以扩容重试)。 - if n == s.len() { - crate::error::OAKPLUGIN_OK - } else { - need - } - } - Err(e) => e.code(), - }) -} - -// ---- host.h --------------------------------------------------------------- - -/// `oakplugin_host_init`:初始化宿主单例(幂等;扫描默认路径集)。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_host_init() -> c_int { - guard(|| { - let host = crate::host::Host::global(); - host.cache.scan()?; - Ok(()) - }) -} - -/// `oakplugin_host_shutdown`:销毁全部实例并卸载插件。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_host_shutdown() { - guard_void(|| { - crate::host::Host::global().shutdown(); - }); -} - -/// `oakplugin_host_scan`:扫描 bundle 目录(NULL/0 用默认路径集)。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_host_scan( - bundle_dirs: *const *const c_char, - dir_count: c_int, -) -> c_int { - guard(|| { - let host = crate::host::Host::global(); - if bundle_dirs.is_null() || dir_count <= 0 { - host.cache.scan()?; - return Ok(()); - } - for i in 0..dir_count { - let dir = unsafe { *bundle_dirs.add(i as usize) }; - if dir.is_null() { - continue; - } - let dir = unsafe { CStr::from_ptr(dir) } - .to_str() - .map_err(|_| crate::error::Error::Invalid)?; - host.cache.scan_path(std::path::Path::new(dir))?; - } - Ok(()) - }) -} - -/// `oakplugin_host_plugin_count`(count 查询:直接取值,panic 兜底)。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_host_plugin_count() -> c_int { - catch(|| crate::host::Host::global().cache.count() as c_int) -} - -/// `oakplugin_host_plugin_id_at`:两段式字符串。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_host_plugin_id_at( - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - string_query(buf, buf_size, || { - if index < 0 { - return Err(crate::error::Error::Invalid); - } - let id = crate::host::Host::global() - .cache - .at(index as usize) - .ok_or(crate::error::Error::NotFound)? - .identifier - .clone(); - Ok(id) - }) -} - -/// `oakplugin_host_plugin_label`:两段式字符串(phase 1:标识本身)。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_host_plugin_label( - plugin_id: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - string_query(buf, buf_size, || { - if plugin_id.is_null() { - return Err(crate::error::Error::Invalid); - } - let id = unsafe { CStr::from_ptr(plugin_id) } - .to_str() - .map_err(|_| crate::error::Error::Invalid)?; - let plugin = crate::host::Host::global() - .cache - .find(id) - .ok_or(crate::error::Error::NotFound)?; - Ok(plugin.identifier.clone()) - }) -} - -/// 消息回调类型(host.h oakplugin_message_fn: -/// `(type, message, userdata) -> OAKPLUGIN_MESSAGE_ANSWER_NO/YES`)。 -pub type MessageFn = unsafe extern "C" fn(*const c_char, *const c_char, *mut c_void) -> c_int; - -/// `oakplugin_host_set_message_handler`:facade 注册消息出口 -/// (message suite 的最终落点)。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_host_set_message_handler( - f: Option, - userdata: *mut c_void, -) { - guard_void(|| { - crate::suites::message::set_handler(f, userdata); - }); -} - -// ---- instance.h ----------------------------------------------------------- - -/// 实例句柄的装箱类型(make_owned 的 T)。 -pub(crate) type InstanceRef = std::sync::Arc>; - -/// 实例句柄 → 实例的取回。 -fn instance_of(h: &CHandle) -> crate::error::Result { - unsafe { get::(h) } - .cloned() - .ok_or(crate::error::Error::Invalid) -} - -/// `oakplugin_instance_create`:按插件标识创建实例(计数 1; -/// 未知标识返回空句柄)。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_create(plugin_id: *const c_char) -> CHandle { - guard_handle(|| { - if plugin_id.is_null() { - return Err(crate::error::Error::Invalid); - } - let id = unsafe { CStr::from_ptr(plugin_id) } - .to_str() - .map_err(|_| crate::error::Error::Invalid)?; - let instance = crate::host::Host::global().create_instance(id, None)?; - Ok(make_owned(instance)) - }) -} - -/// `oakplugin_instance_free`:释放一次引用;NULL/空句柄 no-op。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_free(instance: *mut CHandle) { - guard_void(|| { - if instance.is_null() { - return; - } - let h = unsafe { &mut *instance }; - if h.is_null() { - return; - } - unsafe { (h.release.expect("release fn 缺失"))(h.ctx) }; - h.ctx = std::ptr::null_mut(); - }); -} - -/// node_value → ParamValue(按参数类型解释;字符串走专用函数)。 -/// pub(crate):render 驱动的参数覆盖(apply_param_overrides 移植) -/// 复用同一转换。 -pub(crate) fn node_value_to_param(v: &OakNodeValue, ofx_type: &str) -> Option { - match ofx_type { - crate::param::TYPE_DOUBLE => { - if v.r#type == node_value_type::FLOAT { - Some(ParamValue::Double([v.f[0], 0.0, 0.0], 1)) - } else { - None - } - } - crate::param::TYPE_DOUBLE2D => { - if v.r#type == node_value_type::VEC2 { - Some(ParamValue::Double([v.f[0], v.f[1], 0.0], 2)) - } else { - None - } - } - crate::param::TYPE_DOUBLE3D => { - if v.r#type == node_value_type::VEC3 { - Some(ParamValue::Double([v.f[0], v.f[1], v.f[2]], 3)) - } else { - None - } - } - crate::param::TYPE_INTEGER => { - if v.r#type == node_value_type::INT { - Some(ParamValue::Int([v.num as i32, 0, 0], 1)) - } else { - None - } - } - crate::param::TYPE_INTEGER2D | crate::param::TYPE_INTEGER3D => { - let dim = if ofx_type == crate::param::TYPE_INTEGER2D { - 2 - } else { - 3 - }; - let a = [v.f[0] as i32, v.f[1] as i32, v.f[2] as i32]; - Some(ParamValue::Int(a, dim)) - } - crate::param::TYPE_BOOLEAN => { - if v.r#type == node_value_type::BOOL { - Some(ParamValue::Bool(v.num != 0)) - } else { - None - } - } - crate::param::TYPE_CHOICE => { - if v.r#type == node_value_type::COMBO { - Some(ParamValue::Choice(v.num as i32)) - } else { - None - } - } - crate::param::TYPE_RGB => { - if v.r#type == node_value_type::COLOR { - Some(ParamValue::Color([v.f[0], v.f[1], v.f[2], 0.0], 3)) - } else { - None - } - } - crate::param::TYPE_RGBA => { - if v.r#type == node_value_type::COLOR { - Some(ParamValue::Color([v.f[0], v.f[1], v.f[2], v.f[3]], 4)) - } else { - None - } - } - _ => None, - } -} - -/// ParamValue → node_value(字符串走专用函数)。 -fn param_to_node_value(v: &ParamValue) -> Option { - let mut out = OakNodeValue::default(); - match v { - ParamValue::Double(d, 1) => { - out.r#type = node_value_type::FLOAT; - out.f = [d[0], 0.0, 0.0, 0.0]; - Some(out) - } - ParamValue::Double(d, 2) => { - out.r#type = node_value_type::VEC2; - out.f = [d[0], d[1], 0.0, 0.0]; - Some(out) - } - ParamValue::Double(d, 3) => { - out.r#type = node_value_type::VEC3; - out.f = [d[0], d[1], d[2], 0.0]; - Some(out) - } - ParamValue::Double(_, _) => { - out.r#type = node_value_type::FLOAT; - out.f = [0.0; 4]; - Some(out) - } - ParamValue::Int(a, 1) => { - out.r#type = node_value_type::INT; - out.num = a[0] as i64; - Some(out) - } - ParamValue::Int(a, 2) => { - out.r#type = node_value_type::VEC2; - out.f = [a[0] as f64, a[1] as f64, 0.0, 0.0]; - Some(out) - } - ParamValue::Int(a, 3) => { - out.r#type = node_value_type::VEC3; - out.f = [a[0] as f64, a[1] as f64, a[2] as f64, 0.0]; - Some(out) - } - ParamValue::Bool(b) => { - out.r#type = node_value_type::BOOL; - out.num = *b as i64; - Some(out) - } - ParamValue::Choice(c) => { - out.r#type = node_value_type::COMBO; - out.num = *c as i64; - Some(out) - } - ParamValue::Color(c, 3) => { - out.r#type = node_value_type::COLOR; - // alpha=1.0:与 C++ RGBInstance::set 的 value_color(r,g,b,1.0) - // 一致(RGB 参数视为不透明)。 - out.f = [c[0], c[1], c[2], 1.0]; - Some(out) - } - ParamValue::Color(c, 4) => { - out.r#type = node_value_type::COLOR; - out.f = *c; - Some(out) - } - _ => None, - } -} - -/// `oakplugin_instance_set_param`:oaknode_value POD 写参数 -/// (字符串参数走 set_param_string)。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_set_param( - instance: CHandle, - param: *const c_char, - value: *const OakNodeValue, -) -> c_int { - guard(|| { - let inst = instance_of(&instance)?; - if param.is_null() || value.is_null() { - return Err(crate::error::Error::Invalid); - } - let name = unsafe { CStr::from_ptr(param) } - .to_str() - .map_err(|_| crate::error::Error::Invalid)?; - let v = unsafe { &*value }; - let p = inst - .value - .params - .find(name) - .ok_or(crate::error::Error::NotFound)?; - let param_value = - node_value_to_param(v, &p.def.ofx_type).ok_or(crate::error::Error::Invalid)?; - p.set_ofx(param_value); - Ok(()) - }) -} - -/// `oakplugin_instance_get_param`:oaknode_value POD 读参数。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_get_param( - instance: CHandle, - param: *const c_char, - out: *mut OakNodeValue, -) -> c_int { - guard(|| { - let inst = instance_of(&instance)?; - if param.is_null() || out.is_null() { - return Err(crate::error::Error::Invalid); - } - let name = unsafe { CStr::from_ptr(param) } - .to_str() - .map_err(|_| crate::error::Error::Invalid)?; - let p = inst - .value - .params - .find(name) - .ok_or(crate::error::Error::NotFound)?; - let v = p.get(); - let node = param_to_node_value(&v).ok_or(crate::error::Error::Invalid)?; - unsafe { *out = node }; - Ok(()) - }) -} - -/// `oakplugin_instance_set_param_string`。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_set_param_string( - instance: CHandle, - param: *const c_char, - value: *const c_char, -) -> c_int { - guard(|| { - let inst = instance_of(&instance)?; - if param.is_null() || value.is_null() { - return Err(crate::error::Error::Invalid); - } - let name = unsafe { CStr::from_ptr(param) } - .to_str() - .map_err(|_| crate::error::Error::Invalid)?; - let v = unsafe { CStr::from_ptr(value) }.to_bytes(); - let p = inst - .value - .params - .find(name) - .ok_or(crate::error::Error::NotFound)?; - match p.def.ofx_type.as_str() { - crate::param::TYPE_STRING => { - p.set_ofx(ParamValue::String(CString::new(v).unwrap())); - Ok(()) - } - crate::param::TYPE_STRCHOICE => { - p.set_ofx(ParamValue::StrChoice(CString::new(v).unwrap())); - Ok(()) - } - _ => Err(crate::error::Error::Invalid), - } - }) -} - -/// `oakplugin_instance_get_param_string`:两段式字符串。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_get_param_string( - instance: CHandle, - param: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - string_query(buf, buf_size, || { - let inst = instance_of(&instance)?; - if param.is_null() { - return Err(crate::error::Error::Invalid); - } - let name = unsafe { CStr::from_ptr(param) } - .to_str() - .map_err(|_| crate::error::Error::Invalid)?; - let p = inst - .value - .params - .find(name) - .ok_or(crate::error::Error::NotFound)?; - match p.get() { - ParamValue::String(s) | ParamValue::StrChoice(s) => { - Ok(s.to_string_lossy().into_owned()) - } - _ => Err(crate::error::Error::Invalid), - } - }) -} - -/// `oakplugin_instance_render`:渲染一帧(dst 纹理输出、src 纹理 -/// 输入可空)。CPU 路径的单输入便捷入口;多输入/GL/序列括号走 -/// [`oakplugin_instance_render_job`]。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_render( - instance: CHandle, - dst: CHandle, - src: CHandle, - time_seconds: f64, -) -> c_int { - guard(|| { - let inst = instance_of(&instance)?; - // 取消检查必须在任何插件调用(协商/渲染)之前—— - // shutdown 后插件入口已卸载,协商会踩已卸载的 lib。 - if inst.value.cancel.load(std::sync::atomic::Ordering::Relaxed) { - return Err(crate::error::Error::Failed("已取消".into())); - } - if dst.is_null() { - return Err(crate::error::Error::Invalid); - } - - // 输出纹理的帧与参数(宽/高)。 - use crate::bridge::render::{self, FrameHandle, VideoParams}; - let mut frame = FrameHandle::null(); - let r = unsafe { render::texture_get_frame(dst, &mut frame) }; - if r != 0 || frame.is_null() { - return Err(crate::error::Error::Failed("输出纹理无 CPU 帧".into())); - } - let mut params = VideoParams::default(); - if unsafe { render::frame_get_params(frame, &mut params) } != 0 { - unsafe { render::frame_free(&mut frame) }; - return Err(crate::error::Error::Failed("输出帧无参数".into())); - } - let (w, h) = (params.width as f64, params.height as f64); - if w <= 0.0 || h <= 0.0 { - unsafe { render::frame_free(&mut frame) }; - return Err(crate::error::Error::Invalid); - } - - // 协商(写回 clip props;决定输出格式)。 - let prefs = inst.value.get_clip_preferences()?; - let components = match prefs.output_components.as_str() { - "OfxImageComponentRGBA" => crate::image::Components::Rgba, - "OfxImageComponentRGB" => crate::image::Components::Rgb, - "OfxImageComponentAlpha" => crate::image::Components::Alpha, - _ => return Err(crate::error::Error::Failed("协商分量未知".into())), - }; - if prefs.output_bit_depth != "OfxBitDepthFloat" { - return Err(crate::error::Error::Failed("第 1 期仅支持 F32 输出".into())); - } - - // 输入纹理挂到第一个输入 clip(generator 的 src 为空句柄)。 - if !src.is_null() { - if let Some(input) = inst.value.clips.iter().find(|c| c.name != "Output") { - input.set_input_texture(src, time_seconds); - } - } - - let output = std::sync::Arc::new(crate::image::Image::allocate( - crate::image::BitDepth::Float, - components, - crate::instance::OfxRectD { - x1: 0.0, - y1: 0.0, - x2: w, - y2: h, - }, - )); - let window = crate::instance::OfxRectD { - x1: 0.0, - y1: 0.0, - x2: w, - y2: h, - }; - let scale = crate::instance::RenderScale { x: 1.0, y: 1.0 }; - let render = inst - .value - .render(time_seconds, scale, window, output.clone()); - // 结果写回输出帧(行跨度感知;M11 §4 修复:phase 1 假设紧凑 - // 行并泄漏 frame 句柄——统一走驱动装配路径)。 - let result = match render { - Ok(()) => crate::render_driver::write_output_frame(dst, &output), - Err(e) => Err(e), - }; - unsafe { render::frame_free(&mut frame) }; - result - }) -} - -/// `oakplugin_instance_set_progress_cb`。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_set_progress_cb( - instance: CHandle, - cb: Option, - userdata: *mut c_void, -) -> c_int { - guard(|| { - let inst = instance_of(&instance)?; - *inst - .value - .progress_cb - .lock() - .unwrap_or_else(|e| e.into_inner()) = cb.map(|f| (f, userdata as usize)); - Ok(()) - }) -} - -// ---- render 驱动 C ABI(M11 §4 新增;声明同步补 include/plugin/instance.h)---- - -/// `oakplugin_job_value`(instance.h 新增):参数覆盖条目 -/// (key = 参数名;value = oaknode_value POD;字符串参数走 -/// oakplugin_instance_set_param_string)。 -#[repr(C)] -#[derive(Clone, Copy)] -pub struct OakPluginJobValue { - /// 参数名(NUL 结尾)。 - pub key: *const c_char, - /// 参数值。 - pub value: OakNodeValue, -} - -/// `oakplugin_job_texture`(instance.h 新增):输入 clip 纹理条目。 -#[repr(C)] -#[derive(Clone, Copy)] -pub struct OakPluginJobTexture { - /// clip 名(NUL 结尾)。 - pub clip: *const c_char, - /// 纹理句柄(借用,job 内有效)。 - pub texture: CHandle, -} - -/// `oakplugin_instance_render_begin_sequence`(instance.h 新增): -/// beginSequenceRender 括号。oakrender 对同一实例的一批帧先 -/// begin 后 end,中间逐帧 render_job(ofxRendering:"All calls to the -/// render action are bracketed by a pair of begin/end sequence render -/// actions")。`interactive` 为信息性标记(Phase 2 不传入 action)。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_render_begin_sequence( - instance: CHandle, - start_time: f64, - end_time: f64, - _interactive: c_int, -) -> c_int { - guard(|| { - let inst = instance_of(&instance)?; - if inst.value.cancel.load(std::sync::atomic::Ordering::Relaxed) { - return Err(crate::error::Error::Failed("已取消".into())); - } - let range = crate::instance::OfxRangeD { - min: start_time, - max: end_time, - }; - inst.value.begin_sequence_render(range) - }) -} - -/// `oakplugin_instance_render_end_sequence`(instance.h 新增): -/// endSequenceRender 括号。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_render_end_sequence( - instance: CHandle, - start_time: f64, - end_time: f64, - _interactive: c_int, -) -> c_int { - guard(|| { - let inst = instance_of(&instance)?; - let range = crate::instance::OfxRangeD { - min: start_time, - max: end_time, - }; - inst.value.end_sequence_render(range) - }) -} - -/// `oakplugin_instance_render_job`(instance.h 新增;M11 §4 的目标 -/// 形态):一帧渲染的**唯一** C ABI 调用——oakrender 的 PluginJob -/// 语义全部收编进 [`crate::render_driver`]。 -/// -/// 参数: -/// - `dst`:目标纹理(oakrender 创建;GL 模式下调用方须先附着为 -/// 渲染器输出目标并保持 GL 上下文 current——ofxGPURender.h -/// "OpenGL Current Context" 规则); -/// - `src`:主输入纹理(effect_input_id / SimpleSource;可空); -/// - `effect_input_id`:job.src 落点的 clip 名(可空); -/// - `inputs`/`input_count`:其余输入 clip 的纹理表; -/// - `values`/`value_count`:参数覆盖; -/// - `renderer`:GL 渲染器(空句柄 → CPU 路径); -/// - `clear_destination`/`interactive`:信息性(Phase 2,见 -/// [`crate::render_driver::RenderJob`])。 -/// -/// 驱动流程见 [`crate::render_driver::render_frame`]:RoI/RoD、 -/// 多输入收集、isIdentity 短路、参数覆盖、CPU/GL 渲染与输出装配。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_render_job( - instance: CHandle, - dst: CHandle, - time_seconds: f64, - clear_destination: c_int, - interactive: c_int, - effect_input_id: *const c_char, - src: CHandle, - inputs: *const OakPluginJobTexture, - input_count: c_int, - values: *const OakPluginJobValue, - value_count: c_int, - renderer: CHandle, -) -> c_int { - guard(|| { - let inst = instance_of(&instance)?; - if dst.is_null() { - return Err(crate::error::Error::Invalid); - } - - let effect_input_id = if effect_input_id.is_null() { - None - } else { - Some( - unsafe { CStr::from_ptr(effect_input_id) } - .to_str() - .map_err(|_| crate::error::Error::Invalid)? - .to_string(), - ) - }; - - let mut job_inputs = Vec::new(); - if !inputs.is_null() && input_count > 0 { - for i in 0..input_count { - let entry = unsafe { &*inputs.add(i as usize) }; - if entry.clip.is_null() { - continue; - } - let name = unsafe { CStr::from_ptr(entry.clip) } - .to_str() - .map_err(|_| crate::error::Error::Invalid)? - .to_string(); - job_inputs.push((name, entry.texture)); - } - } - - let mut job_values = Vec::new(); - if !values.is_null() && value_count > 0 { - for i in 0..value_count { - let entry = unsafe { &*values.add(i as usize) }; - if entry.key.is_null() { - continue; - } - let key = unsafe { CStr::from_ptr(entry.key) } - .to_str() - .map_err(|_| crate::error::Error::Invalid)? - .to_string(); - job_values.push((key, entry.value)); - } - } - - let job = crate::render_driver::RenderJob { - time: time_seconds, - dst, - src, - effect_input_id, - inputs: job_inputs, - values: job_values, - renderer: if renderer.is_null() { - None - } else { - Some(renderer) - }, - clear_destination: clear_destination != 0, - interactive: interactive != 0, - }; - // 驱动内部已做取消检查与实例锁;RoI 返回值 Phase 2 供宿主 - // 记录(输入由 oakrender 整帧提供,不做 RoI 裁剪)。 - crate::render_driver::render_frame(&inst.value, &job)?; - Ok(()) - }) -} - -/// `oakplugin_instance_cancel`。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_cancel(instance: CHandle) -> c_int { - guard(|| { - let inst = instance_of(&instance)?; - inst.value - .cancel - .store(true, std::sync::atomic::Ordering::Relaxed); - Ok(()) - }) -} - -/// `oakplugin_debug_alive_count`:活跃对象数(泄漏断言)。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_debug_alive_count() -> c_int { - crate::host::Host::global().alive_count() as c_int -} - -// ---- 内省族(M11 §2.1,0 期新增;声明随本文件冻结,头文件同步补)---- - -/// 取第 `index` 个参数实例。 -fn param_at( - inst: &crate::instance::Instance, - index: c_int, -) -> crate::error::Result<&crate::param::ParamInstance> { - if index < 0 { - return Err(crate::error::Error::Invalid); - } - inst.params - .params - .get(index as usize) - .map(|b| b.as_ref()) - .ok_or(crate::error::Error::NotFound) -} - -/// 参数数量。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_param_count(instance: CHandle) -> c_int { - catch(|| match instance_of(&instance) { - Ok(inst) => inst.value.params.params.len() as c_int, - Err(_) => crate::error::OAKPLUGIN_E_INVALID, - }) -} - -/// 参数名(两段式)。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_param_name( - instance: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - string_query(buf, buf_size, || { - let inst = instance_of(&instance)?; - let p = param_at(&inst.value, index)?; - Ok(p.def.name.clone()) - }) -} - -/// 参数 OFX 类型(两段式,如 "OfxParamTypeDouble")。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_param_type( - instance: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - string_query(buf, buf_size, || { - let inst = instance_of(&instance)?; - let p = param_at(&inst.value, index)?; - Ok(p.def.ofx_type.clone()) - }) -} - -/// 参数标签(两段式)。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_param_label( - instance: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - string_query(buf, buf_size, || { - let inst = instance_of(&instance)?; - let p = param_at(&inst.value, index)?; - Ok(prop_str(&p.def.props, crate::param::PROP_LABEL)) - }) -} - -/// 参数 hint(两段式)。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_param_hint( - instance: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - string_query(buf, buf_size, || { - let inst = instance_of(&instance)?; - let p = param_at(&inst.value, index)?; - Ok(prop_str(&p.def.props, crate::param::P_HINT)) - }) -} - -/// 参数父组名(两段式;无父组返回空串)。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_param_parent( - instance: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - string_query(buf, buf_size, || { - let inst = instance_of(&instance)?; - let p = param_at(&inst.value, index)?; - Ok(prop_str(&p.def.props, crate::param::P_PARENT)) - }) -} - -/// 参数 secret 标记(out 0/1)。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_param_secret( - instance: CHandle, - index: c_int, - out: *mut c_int, -) -> c_int { - guard(|| { - let inst = instance_of(&instance)?; - if out.is_null() { - return Err(crate::error::Error::Invalid); - } - let p = param_at(&inst.value, index)?; - unsafe { *out = prop_int(&p.def.props, crate::param::P_SECRET) }; - Ok(()) - }) -} - -/// 参数 display min/max(double out 两参)。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_param_display_range( - instance: CHandle, - index: c_int, - out_min: *mut f64, - out_max: *mut f64, -) -> c_int { - guard(|| { - let inst = instance_of(&instance)?; - if out_min.is_null() || out_max.is_null() { - return Err(crate::error::Error::Invalid); - } - let p = param_at(&inst.value, index)?; - unsafe { - *out_min = prop_double(&p.def.props, crate::param::P_DISPLAY_MIN); - *out_max = prop_double(&p.def.props, crate::param::P_DISPLAY_MAX); - } - Ok(()) - }) -} - -/// choice 参数的选项数。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_param_choice_count( - instance: CHandle, - index: c_int, -) -> c_int { - catch(|| match instance_of(&instance) { - Ok(inst) => match param_at(&inst.value, index) { - Ok(p) => p.def.props.dimension(crate::param::P_CHOICE_OPTION) as c_int, - Err(_) => crate::error::OAKPLUGIN_E_NOT_FOUND, - }, - Err(_) => crate::error::OAKPLUGIN_E_INVALID, - }) -} - -/// `oakplugin_instance_param_choice_label`(两段式)。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_param_choice_label( - instance: CHandle, - index: c_int, - choice: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - string_query(buf, buf_size, || { - let inst = instance_of(&instance)?; - let p = param_at(&inst.value, index)?; - Ok(choice_str(&p.def.props, choice)) - }) -} - -/// `oakplugin_instance_param_choice_value`(两段式;第 1 期与 label -/// 相同——插件未设 ChoiceOrder 时 OFX 约定 value == label)。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_param_choice_value( - instance: CHandle, - index: c_int, - choice: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - string_query(buf, buf_size, || { - let inst = instance_of(&instance)?; - let p = param_at(&inst.value, index)?; - Ok(choice_str(&p.def.props, choice)) - }) -} - -/// 参数默认值(double 族;字符串/字节组走专用函数)。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_param_default_double( - instance: CHandle, - index: c_int, - component: c_int, - out: *mut f64, -) -> c_int { - guard(|| { - let inst = instance_of(&instance)?; - if out.is_null() { - return Err(crate::error::Error::Invalid); - } - let p = param_at(&inst.value, index)?; - unsafe { *out = prop_any(&p.def.props, crate::param::P_DEFAULT, component) }; - Ok(()) - }) -} - -/// 参数默认字符串值(两段式)。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_param_default_string( - instance: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - string_query(buf, buf_size, || { - let inst = instance_of(&instance)?; - let p = param_at(&inst.value, index)?; - Ok(prop_str(&p.def.props, crate::param::P_DEFAULT)) - }) -} - -/// clip 数量(含 Output)。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_clip_count(instance: CHandle) -> c_int { - catch(|| match instance_of(&instance) { - Ok(inst) => inst.value.clips.len() as c_int, - Err(_) => crate::error::OAKPLUGIN_E_INVALID, - }) -} - -/// clip 名(两段式)。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_clip_name( - instance: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - string_query(buf, buf_size, || { - let inst = instance_of(&instance)?; - if index < 0 { - return Err(crate::error::Error::Invalid); - } - let name = inst - .value - .clips - .get(index as usize) - .map(|c| c.name.clone()) - .ok_or(crate::error::Error::NotFound)?; - Ok(name) - }) -} - -/// clip 标签与可选性(label 两段式;optional out 0/1)。 -/// label 的两段式返回值不能经 guard(Ok 恒 0)——整体用 catch。 -#[no_mangle] -pub unsafe extern "C" fn oakplugin_instance_clip_info( - instance: CHandle, - index: c_int, - optional: *mut c_int, - label: *mut c_char, - label_size: c_int, -) -> c_int { - catch(|| { - let inst = match instance_of(&instance) { - Ok(i) => i, - Err(_) => return crate::error::OAKPLUGIN_E_INVALID, - }; - if optional.is_null() || index < 0 { - return crate::error::OAKPLUGIN_E_INVALID; - } - let clip = match inst.value.clips.get(index as usize) { - Some(c) => c, - None => return crate::error::OAKPLUGIN_E_NOT_FOUND, - }; - unsafe { *optional = prop_int(&clip.props, crate::descriptor::CLIP_OPTIONAL) }; - string_query(label, label_size, || { - Ok(prop_str(&clip.props, crate::param::PROP_LABEL)) - }) - }) -} - -// ---- 属性读取助手 --------------------------------------------------------- - -/// 取字符串属性(缺失返回空串)。 -fn prop_str(props: &crate::property::PropertySet, name: &str) -> String { - props - .get(name, 0) - .map(|v| match v { - crate::property::Value::String(s) => s.to_string_lossy().into_owned(), - _ => String::new(), - }) - .unwrap_or_default() -} - -/// 取 int 属性(缺失 0)。 -fn prop_int(props: &crate::property::PropertySet, name: &str) -> c_int { - props - .get(name, 0) - .map(|v| match v { - crate::property::Value::Int(i) => i, - _ => 0, - }) - .unwrap_or(0) -} - -/// 取 double 属性(缺失 0;Int 转 f64)。 -fn prop_double(props: &crate::property::PropertySet, name: &str) -> f64 { - prop_any(props, name, 0) -} - -/// 取属性第 `component` 个元素为 f64(Double/Int;越界 0)。 -fn prop_any(props: &crate::property::PropertySet, name: &str, component: c_int) -> f64 { - if component < 0 { - return 0.0; - } - props - .get(name, component as usize) - .map(|v| match v { - crate::property::Value::Double(d) => d, - crate::property::Value::Int(i) => i as f64, - _ => 0.0, - }) - .unwrap_or(0.0) -} - -/// choice 选项字符串(越界返回空串)。 -fn choice_str(props: &crate::property::PropertySet, choice: c_int) -> String { - if choice < 0 { - return String::new(); - } - props - .get(crate::param::P_CHOICE_OPTION, choice as usize) - .map(|v| match v { - crate::property::Value::String(s) => s.to_string_lossy().into_owned(), - _ => String::new(), - }) - .unwrap_or_default() -} diff --git a/crates/oakplugin/src/handle.rs b/crates/oakplugin/src/handle.rs index a211ea111..a61758c64 100644 --- a/crates/oakplugin/src/handle.rs +++ b/crates/oakplugin/src/handle.rs @@ -56,8 +56,8 @@ pub struct RefBox { /// 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 -/// here so the crate's `ffi.rs` signatures and handle scaffolding stay -/// source-compatible. `Send + Sync` come from the shared type. +/// here so the crate's handle scaffolding stays source-compatible. +/// `Send + Sync` come from the shared type. pub use oakcore_rs::handle::CHandle; /// addref 的实现:原子 +1。拥有型与借用型共用——借用型只延长盒子 @@ -177,7 +177,7 @@ where } } -/// 无返回值版本:panic 被吞并记录日志(经 bridge 的日志回调)。 +/// 无返回值版本:panic 被吞(日志回调待 message 出口接入后补)。 pub fn guard_void(f: F) where F: FnOnce(), diff --git a/crates/oakplugin/src/instance.rs b/crates/oakplugin/src/instance.rs index 315bc0a98..b9af4b684 100644 --- a/crates/oakplugin/src/instance.rs +++ b/crates/oakplugin/src/instance.rs @@ -35,8 +35,8 @@ use crate::property::PropertySet; pub struct EditTransaction { /// 嵌套深度(editBegin/End 必须配对)。 depth: i32, - /// 事务累积的 multi 命令(空句柄 = 尚无子命令)。 - multi: crate::bridge::undo::CommandHandle, + /// 事务累积的 multi 命令(None = 尚无子命令)。 + multi: Option, /// 事务内回写次数(标签计数)。 param_count: i32, /// 第一条回写的标签。 @@ -48,7 +48,7 @@ impl EditTransaction { pub fn new() -> Self { Self { depth: 0, - multi: crate::bridge::undo::CommandHandle::null(), + multi: None, param_count: 0, first_label: String::new(), } @@ -141,8 +141,9 @@ impl Drop for Instance { impl Instance { /// 绑定 oaknode 节点(C++ `set_node_handle` 的 Rust 侧;装配期由 - /// facade/测试调用)。`identity` 为 [`crate::host::instance_registry`] - /// 无关的 oaknode 节点身份(`oaknode_node_identity` 的地址语义); + /// facade/测试调用)。`identity` 为 [`crate::node::register_node`] + /// 返回的 oaknode 节点身份([`oaknode::id::NodeId::identity`] 的 + /// 打包值,经 [`crate::node::node_from_identity`] 反查); /// 0 解除绑定。 pub fn bind_node(&self, identity: usize) { self.node_identity @@ -154,7 +155,7 @@ impl Instance { let mut e = self.edit.lock().unwrap_or_else(|e| e.into_inner()); e.depth += 1; if e.depth == 1 { - e.multi = crate::bridge::undo::CommandHandle::null(); + e.multi = None; e.param_count = 0; e.first_label.clear(); } @@ -168,9 +169,12 @@ impl Instance { if e.depth > 0 { e.depth -= 1; } - if e.depth == 0 && !e.multi.is_null() { - unsafe { crate::bridge::undo::command_redo_now(e.multi) }; - unsafe { crate::bridge::undo::command_free(&mut e.multi) }; + if e.depth == 0 { + if let Some(mut multi) = e.multi.take() { + multi.redo_now(); + // drop(multi):释放 multi 与子命令(命令值语义)。 + drop(multi); + } e.param_count = 0; e.first_label.clear(); } @@ -185,28 +189,24 @@ impl Instance { /// oliveplugininstance.cpp:390-414 `submit_undo_command`): /// 编辑事务内并入 multi(子命令立即 redo 生效,值即时可见), /// 否则单命令 redo 后释放。 - pub(crate) fn submit_undo_command( - &self, - mut cmd: crate::bridge::undo::CommandHandle, - label: &str, - ) { - if cmd.is_null() { - return; - } + pub(crate) fn submit_undo_command(&self, mut cmd: oakundo::undocommand::UndoCommand, label: &str) { if self.in_edit() { let mut e = self.edit.lock().unwrap_or_else(|e| e.into_inner()); - if e.multi.is_null() { - e.multi = unsafe { crate::bridge::undo::command_init_multi() }; + if e.multi.is_none() { + e.multi = Some(oakundo::undocommand::UndoCommand::multi()); } e.param_count += 1; if e.first_label.is_empty() { e.first_label = label.to_string(); } - unsafe { crate::bridge::undo::command_redo_now(cmd) }; - unsafe { crate::bridge::undo::command_multi_add_child(e.multi, cmd) }; + cmd.redo_now(); + e.multi + .as_mut() + .expect("multi 已在上面初始化") + .multi_add_child(cmd); } else { - unsafe { crate::bridge::undo::command_redo_now(cmd) }; - unsafe { crate::bridge::undo::command_free(&mut cmd) }; + cmd.redo_now(); + drop(cmd); } } @@ -651,8 +651,8 @@ impl Instance { time: f64, scale: RenderScale, window: OfxRectD, - renderer: crate::bridge::render::RendererHandle, - output_texture: crate::bridge::render::TextureHandle, + renderer: crate::render::Renderer, + output_texture: crate::render::Texture, ) -> crate::error::Result<()> { use crate::host::ACTION_RENDER; diff --git a/crates/oakplugin/src/lib.rs b/crates/oakplugin/src/lib.rs index e8e85bc3b..c52677dd6 100644 --- a/crates/oakplugin/src/lib.rs +++ b/crates/oakplugin/src/lib.rs @@ -21,21 +21,27 @@ //! - **OFX 宿主**:扫描 bundle、加载插件、实现八张 suite //! (property/memory/image_effect/param/message/progress/timeline/ //! multithread),驱动 describe/createInstance/render 等 action。 -//! - **桥**:把 OFX 实例的参数接到 oaknode([`bridge::node`])、把 -//! clip 的输入输出接到 oakrender 纹理([`bridge::render`])、把 -//! 参数修改包成 undo 命令([`bridge::undo`])。 -//! - **C ABI 出口**([`ffi`]):逐字实现 `include/plugin/*.h`。 +//! - **桥**:把 OFX 实例的参数接到 oaknode([`node`],节点值 POD + +//! 身份注册表与 undoable 回写)、把 clip 的输入输出接到 oakrender +//! 纹理([`render`],纹理/帧/渲染器值类型)、把参数修改包成 undo +//! 命令(直接经 `oakundo::undocommand::UndoCommand`,见 +//! [`instance::Instance::submit_undo_command`])。 //! -//! ## FFI 纪律(全 crate 最高优先级约定) +//! ## 单库化(single-lib unification) //! -//! 1. 每个 `extern "C"` 导出函数体必须包 -//! [`handle::guard`]/[`handle::guard_ptr`](catch_unwind + 错误码 -//! 映射)。panic 越过 FFI 边界是 release 阻断级缺陷。 -//! 2. 句柄一律 [`handle::RefBox`];`ctx` 是不透明指针,含义只在本 -//! crate 内解释。 -//! 3. 插件可在任意自起线程回调 suite(multithread suite 存活期 -//! 内);一切共享状态走 `Mutex`,句柄注册表见 [`handle::Registry`]。 -//! 4. OFX 语义以 openfx HostSupport 为参照系;协商与时序实现点必须 +//! 本 crate 的 C ABI 出口(原 [`ffi`])与模块桥(原 [`bridge`])已 +//! 删除:oaknode/oakrender/oakundo 均以 path 依赖直接链接。桥以 +//! 直接 Rust 类型重建([`node`] 的身份注册表与 +//! `set_input_*_undoable`;[`render`] 的 `Texture`/`Frame`/`Renderer` +//! 值类型),仅 GPU 相关且 wgpu 模型无等价物的调用面保留标注桩 +//! ([`render::texture_id`]——GL 命名空间不存在,见 `// STUB` +//! 标记)。 +//! +//! ## 句柄纪律(全 crate 最高优先级约定) +//! +//! 1. 插件可在任意自起线程回调 suite(multithread suite 存活期 +//! 内);一切共享状态走 `Mutex`。 +//! 2. OFX 语义以 openfx HostSupport 为参照系;协商与时序实现点必须 //! 注释对应 HostSupport 文件与行号(格式:`// HS: ofxhImageEffect.cpp:2776`)。 //! //! ## 第 1 期范围 @@ -47,17 +53,17 @@ #![deny(unsafe_op_in_unsafe_fn)] #![warn(missing_docs)] -pub mod bridge; pub mod clip; pub mod descriptor; pub mod error; -pub mod ffi; pub mod handle; pub mod host; pub mod image; pub mod instance; +pub mod node; pub mod param; pub mod progress; pub mod property; +pub mod render; pub mod render_driver; pub mod suites; diff --git a/crates/oakplugin/src/node.rs b/crates/oakplugin/src/node.rs new file mode 100644 index 000000000..0be726431 --- /dev/null +++ b/crates/oakplugin/src/node.rs @@ -0,0 +1,727 @@ +// 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 . + +//! oaknode 桥(single-lib unification):节点值 POD、身份注册表与 +//! undoable 参数回写。 +//! +//! ## Value 布局冻结 +//! +//! [`Value`] 即 include/node/node.h:93 的 `oaknode_value` POD,字段 +//! 逐字一致(type/num/den/f[4];`type` 取值见 [`node_value_type`])。 +//! 字符串族输入(k_file/k_text/k_font/k_str_combo,node.h:48-52)没有 +//! POD 表示——走 `*_input_string_*` 专用函数(本桥的 +//! [`set_input_string_undoable`]);`OAKNODE_VALUE_STRING` 的 POD 里 +//! 不携带字符串数据。 +//! +//! ## 身份注册表(单库化重建) +//! +//! oaknode 的 C ABI 已删除(单库化):节点对象是 +//! `oaknode::project::Project`(Arc>)里的 +//! `oaknode::graph::Graph` 条目,按 [`oaknode::id::NodeId`] 定位。 +//! 本 crate 的 [`NodeRef`] 即旧句柄盒 +//! `(Arc>, NodeId)` 的值型复刻;进程级注册表 +//! [`register_node`]/[`node_from_identity`] 按 +//! [`oaknode::id::NodeId::identity`] 的打包身份(u64)做弱引用 +//! 映射(project 释放后条目自然失效,升级失败 → None)—— +//! 对应 M9 C++ 版 `oaknode_node_identity()` 注册表的地址语义。 +//! facade 装配期调 [`register_node`] 登记节点,把返回身份写进 +//! [`crate::instance::Instance::bind_node`](`node_identity`)。 +//! +//! ## undoable 回写 +//! +//! [`set_input_undoable`]/[`set_input_string_undoable`] 按 +//! `oaknode::ops::set_value_at_time_command` 的同一 closure-vtable +//! 模式(`command_from_closures`)构造未执行的 +//! `oakundo::undocommand::UndoCommand`:redo 闭包锁 project、 +//! `graph.get_mut(id)`、`NodeCore::set_standard_value` 写标准值; +//! undo 闭包回放创建期快照的旧值。命令由调用方 +//! ([`crate::param::notify_instance_changed`] → +//! [`crate::instance::Instance::submit_undo_command`])提交。 + +use std::collections::HashMap; +use std::sync::{Arc, Mutex, OnceLock, Weak}; + +use oakundo::undocommand::{OakUndoCommandVtable, UndoCommand}; + +/// oaknode 节点引用(single-lib):旧 C ABI 句柄盒 +/// `(Arc>, NodeId)` 的值型复刻。 +#[derive(Clone)] +pub struct NodeRef { + /// 所属 project(节点生命周期随 project)。 + pub project: Arc>, + /// 节点在 project.graph 里的 id。 + pub id: oaknode::id::NodeId, +} + +/// oaknode_value_type 的取值(node.h:74)。 +pub mod node_value_type { + /// OAKNODE_VALUE_NONE。 + pub const NONE: i32 = 0; + /// OAKNODE_VALUE_INT。 + pub const INT: i32 = 1; + /// OAKNODE_VALUE_FLOAT。 + pub const FLOAT: i32 = 2; + /// OAKNODE_VALUE_BOOL。 + pub const BOOL: i32 = 3; + /// OAKNODE_VALUE_RATIONAL。 + pub const RATIONAL: i32 = 4; + /// OAKNODE_VALUE_COLOR。 + pub const COLOR: i32 = 5; + /// OAKNODE_VALUE_VEC2。 + pub const VEC2: i32 = 6; + /// OAKNODE_VALUE_VEC3。 + pub const VEC3: i32 = 7; + /// OAKNODE_VALUE_VEC4。 + pub const VEC4: i32 = 8; + /// OAKNODE_VALUE_COMBO。 + pub const COMBO: i32 = 9; + /// OAKNODE_VALUE_STRING。 + pub const STRING: i32 = 10; +} + +/// oaknode_value(include/node/node.h:93,字段逐字一致)。 +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct Value { + /// 类型([`node_value_type`])。 + pub r#type: i32, + /// INT/COMBO 值、BOOL 0/1、RATIONAL 分子。 + pub num: i64, + /// RATIONAL 分母。 + pub den: i64, + /// FLOAT f[0];VEC2/3/4 f[0..n-1];COLOR r,g,b,a。 + pub f: [f64; 4], +} + +impl Value { + /// 类型化构造:整数 / choice 索引。 + pub const fn int(v: i64) -> Self { + Self { + r#type: node_value_type::INT, + num: v, + den: 0, + f: [0.0; 4], + } + } + + /// 类型化构造:浮点。 + pub const fn float(v: f64) -> Self { + Self { + r#type: node_value_type::FLOAT, + num: 0, + den: 0, + f: [v, 0.0, 0.0, 0.0], + } + } + + /// 类型化构造:布尔。 + pub const fn bool_(v: bool) -> Self { + Self { + r#type: node_value_type::BOOL, + num: v as i64, + den: 0, + f: [0.0; 4], + } + } + + /// 类型化构造:choice(COMBO)。 + pub const fn combo(v: i64) -> Self { + Self { + r#type: node_value_type::COMBO, + num: v, + den: 0, + f: [0.0; 4], + } + } + + /// 类型化构造:颜色。 + pub const fn color(r: f64, g: f64, b: f64, a: f64) -> Self { + Self { + r#type: node_value_type::COLOR, + num: 0, + den: 0, + f: [r, g, b, a], + } + } + + /// 类型化构造:vec2/3/4(长度按 f 数组尾部 0 判定)。 + pub const fn vec(v: &[f64]) -> Self { + let t = match v.len() { + 2 => node_value_type::VEC2, + 3 => node_value_type::VEC3, + _ => node_value_type::VEC4, + }; + let mut f = [0.0; 4]; + let mut i = 0; + while i < v.len() && i < 4 { + f[i] = v[i]; + i += 1; + } + Self { + r#type: t, + num: 0, + den: 0, + f, + } + } + + /// 类型化构造:字符串族(POD 不携带数据;值经 + /// [`set_input_string_undoable`] 传递)。 + pub const fn string() -> Self { + Self { + r#type: node_value_type::STRING, + num: 0, + den: 0, + f: [0.0; 4], + } + } + + /// POD → [`oaknode::value::NodeValue`],按 POD kind 映射(与 + /// `oaknode::value::OakNodeValue::to_node_value` 的 kind 分支一致, + /// 不按声明类型重量化)。`STRING` 族 POD 不携带数据 → `None`; + /// `NONE` → [`oaknode::value::NodeValue::None`]。 + pub fn to_node_value(&self) -> Option { + use oaknode::value::NodeValue as NV; + match self.r#type { + node_value_type::NONE => Some(NV::None), + node_value_type::INT => Some(NV::Int(self.num)), + node_value_type::FLOAT => Some(NV::Float(self.f[0])), + node_value_type::BOOL => Some(NV::Boolean(self.num != 0)), + node_value_type::RATIONAL => Some(NV::Rational(oakcore_rs::Rational::new( + self.num, self.den, + ))), + node_value_type::COLOR => Some(NV::Color(self.f)), + node_value_type::VEC2 => Some(NV::Vec2([self.f[0], self.f[1]])), + node_value_type::VEC3 => Some(NV::Vec3([self.f[0], self.f[1], self.f[2]])), + node_value_type::VEC4 => Some(NV::Vec4(self.f)), + node_value_type::COMBO => Some(NV::Combo(self.num)), + _ => None, + } + } + + /// [`oaknode::value::NodeValue`] → POD(`ValueType::to_oak` 的逆)。 + /// 无 POD 表达的类型(字符串族/纹理/采样/矩阵等)→ `None`。 + pub fn from_node_value(v: &oaknode::value::NodeValue) -> Option { + use oaknode::value::NodeValue as NV; + Some(match v { + NV::None => Value { + r#type: node_value_type::NONE, + num: 0, + den: 0, + f: [0.0; 4], + }, + NV::Int(i) => Value::int(*i), + NV::Float(f) => Value::float(*f), + NV::Boolean(b) => Value::bool_(*b), + NV::Rational(r) => Value { + r#type: node_value_type::RATIONAL, + num: r.numerator(), + den: r.denominator(), + f: [0.0; 4], + }, + NV::Color(c) => Value::color(c[0], c[1], c[2], c[3]), + NV::Vec2(a) => Value::vec(&[a[0], a[1]]), + NV::Vec3(a) => Value::vec(&[a[0], a[1], a[2]]), + NV::Vec4(a) => Value::vec(&[a[0], a[1], a[2], a[3]]), + NV::Combo(i) => Value::combo(*i), + _ => return None, + }) + } +} + +// ---- 身份注册表 ----------------------------------------------------------- + +/// 注册表条目:弱 project 引用(project 释放后升级失败,条目变死) +/// + 节点 id。 +struct RegistryEntry { + /// 所属 project(弱引用;强引用由调用方/节点持有)。 + project: Weak>, + /// 节点 id。 + id: oaknode::id::NodeId, +} + +/// 进程级身份注册表(OnceLock 惰性初始化;测试进程内可重复 register)。 +static NODE_REGISTRY: OnceLock>> = OnceLock::new(); + +fn registry() -> &'static Mutex> { + NODE_REGISTRY.get_or_init(|| Mutex::new(HashMap::new())) +} + +/// 登记 oaknode 节点(facade 装配期调用;对应 M9 C++ 版 +/// `oaknode_node_identity()` 注册表的登记侧)。返回打包身份 +/// ([`oaknode::id::NodeId::identity`]),写入 +/// [`crate::instance::Instance::bind_node`]。同一身份重复登记 +/// 覆盖旧条目(重绑定)。 +pub fn register_node( + project: Arc>, + id: oaknode::id::NodeId, +) -> u64 { + let identity = id.identity(); + let entry = RegistryEntry { + project: Arc::downgrade(&project), + id, + }; + registry() + .lock() + .unwrap_or_else(|e| e.into_inner()) + .insert(identity, entry); + identity +} + +/// 摘除身份(节点销毁路径调用;未知身份 no-op)。 +pub fn unregister_node(identity: u64) { + registry() + .lock() + .unwrap_or_else(|e| e.into_inner()) + .remove(&identity); +} + +/// 身份 → 节点引用(param 回写入口)。 +/// +/// `None`:身份未登记,或 project 已释放(弱引用升级失败——旧 C +/// ABI 对悬垂句柄的可解释失败路径)。调用方 +/// ([`crate::param::notify_instance_changed`])按 "未绑定节点 → +/// no-op" 处理。 +pub fn node_from_identity(identity: usize) -> Option { + let (project, id) = { + let reg = registry() + .lock() + .unwrap_or_else(|e| e.into_inner()); + let entry = reg.get(&(identity as u64))?; + (entry.project.clone(), entry.id) + }; + let project = project.upgrade()?; + Some(NodeRef { project, id }) +} + +// ---- 桥调用面(undoable 回写)--------------------------------------------- + +/// oaknode 桥错误(set_input* 的失败原因;调用方按失败跳过提交)。 +#[derive(Debug, thiserror::Error)] +pub enum NodeBridgeError { + /// 节点 id 已失效(图内无此节点)。 + #[error("node id is stale or not present in the graph")] + StaleNode, + /// 节点上无该输入。 + #[error("node has no input '{0}'")] + UnknownInput(String), + /// 值 POD 无法转换(STRING 族 POD 不携带数据;数值族恒可转换)。 + #[error("value POD of type {0} cannot be converted to a node value")] + InvalidValue(i32), + /// 输入不是字符串族类型(string setter 限定 Text/StrCombo)。 + #[error("input '{0}' is not a string-family input")] + NotStringInput(String), +} + +/// 取锁(毒锁接管,一次 panic 不级联)。 +fn lock_any(m: &Mutex) -> std::sync::MutexGuard<'_, T> { + m.lock().unwrap_or_else(|e| e.into_inner()) +} + +/// Userdata payload behind a closure-backed undo command: the boxed +/// redo/undo closures(与 `oaknode::ops.rs` 的 `ClosureCommand` 同构; +/// 该类型的构造器是 oaknode 私有,桥内自备一份)。 +struct ClosureCommand { + /// The redo closure. + redo: Box, + /// The undo closure. + undo: Box, +} + +/// `OakUndoCommandVtable` redo thunk for [`ClosureCommand`] userdata. +unsafe extern "C" fn closure_redo(ud: *mut std::ffi::c_void) { + // SAFETY: `ud` 是 `command_from_closures` 创建的 ClosureCommand + // 盒,命令持有其所有权直至 free_fn。 + let c = unsafe { &mut *(ud as *mut ClosureCommand) }; + (c.redo)(); +} + +/// `OakUndoCommandVtable` undo thunk for [`ClosureCommand`] userdata. +unsafe extern "C" fn closure_undo(ud: *mut std::ffi::c_void) { + // SAFETY: 见 closure_redo。 + let c = unsafe { &mut *(ud as *mut ClosureCommand) }; + (c.undo)(); +} + +/// `OakUndoCommandVtable` free thunk for [`ClosureCommand`] userdata. +unsafe extern "C" fn closure_free(ud: *mut std::ffi::c_void) { + if !ud.is_null() { + // SAFETY: 盒恰被命令析构一次。 + unsafe { drop(Box::from_raw(ud as *mut ClosureCommand)) }; + } +} + +/// 从 redo/undo 闭包构造未执行的 [`UndoCommand`](closure-vtable +/// 模式,与 `oaknode::ops::command_from_closures` 同构)。 +fn command_from_closures( + redo: impl FnMut() + Send + 'static, + undo: impl FnMut() + Send + 'static, +) -> UndoCommand { + let ud = Box::into_raw(Box::new(ClosureCommand { + redo: Box::new(redo), + undo: Box::new(undo), + })); + UndoCommand::from_vtable( + OakUndoCommandVtable { + redo: Some(closure_redo), + undo: Some(closure_undo), + free_fn: Some(closure_free), + }, + ud as *mut std::ffi::c_void, + ) +} + +/// 以 undoable 方式设置节点的标准输入值(POD 路径;数值族输入)。 +/// +/// 返回**未执行**的 [`UndoCommand`](调用方 +/// [`crate::instance::Instance::submit_undo_command`] 提交):redo +/// 锁 project、`graph.get_mut(id)`、`NodeCore::set_standard_value` +/// 写新值;undo 回放创建期快照的旧值(与 +/// `oaknode::ops::set_value_at_time_command` 的 standard-value 分支 +/// 同构,element = -1 整值)。节点/输入失效时返回 +/// [`NodeBridgeError`](不产出命令)。 +pub fn set_input_undoable( + node: &NodeRef, + input: &str, + value: &Value, +) -> Result { + let nv = value + .to_node_value() + .ok_or(NodeBridgeError::InvalidValue(value.r#type))?; + // 校验节点与输入存在性 + 旧值快照(创建期状态,redo/undo 据此 + // 回放;不校验声明类型——按 POD kind 存原样,与 ops.rs 一致)。 + let old = { + let mut guard = lock_any(&node.project); + let entry = guard + .graph + .get_mut(node.id) + .ok_or(NodeBridgeError::StaleNode)?; + if entry.core.input_data_type(input).is_none() { + return Err(NodeBridgeError::UnknownInput(input.to_string())); + } + entry.core.standard_value(input, -1) + }; + + let project = node.project.clone(); + let id = node.id; + let input_redo = input.to_string(); + let input_undo = input.to_string(); + let value_redo = nv.clone(); + let project_redo = project.clone(); + let project_undo = project.clone(); + Ok(command_from_closures( + move || { + let mut guard = lock_any(&project_redo); + if let Some(entry) = guard.graph.get_mut(id) { + entry + .core + .set_standard_value(&input_redo, -1, value_redo.clone()); + } + }, + move || { + let mut guard = lock_any(&project_undo); + if let Some(entry) = guard.graph.get_mut(id) { + entry + .core + .set_standard_value(&input_undo, -1, old.clone()); + } + }, + )) +} + +/// 以 undoable 方式设置节点的标准输入值(字符串族输入:Text / +/// StrCombo——POD 不携带字符串数据)。 +/// +/// 按输入的声明类型([`oaknode::value::ValueType`])选存储变体: +/// `StrCombo` → [`oaknode::value::NodeValue::StrCombo`],其余 +/// 字符串族(Text)→ [`oaknode::value::NodeValue::Text`]。声明类型 +/// 不是字符串族 → [`NodeBridgeError::NotStringInput`]。命令语义与 +/// [`set_input_undoable`] 相同。 +pub fn set_input_string_undoable( + node: &NodeRef, + input: &str, + value: &str, +) -> Result { + let (old, declared) = { + let guard = lock_any(&node.project); + let entry = guard + .graph + .get(node.id) + .ok_or(NodeBridgeError::StaleNode)?; + let declared = entry + .core + .input_data_type(input) + .ok_or_else(|| NodeBridgeError::UnknownInput(input.to_string()))?; + (entry.core.standard_value(input, -1), declared) + }; + let nv = match declared { + oaknode::value::ValueType::StrCombo => oaknode::value::NodeValue::StrCombo(value.to_string()), + oaknode::value::ValueType::Text => oaknode::value::NodeValue::Text(value.to_string()), + _ => return Err(NodeBridgeError::NotStringInput(input.to_string())), + }; + + let project = node.project.clone(); + let id = node.id; + let input_redo = input.to_string(); + let input_undo = input.to_string(); + let value_redo = nv.clone(); + let project_redo = project.clone(); + let project_undo = project.clone(); + Ok(command_from_closures( + move || { + let mut guard = lock_any(&project_redo); + if let Some(entry) = guard.graph.get_mut(id) { + entry + .core + .set_standard_value(&input_redo, -1, value_redo.clone()); + } + }, + move || { + let mut guard = lock_any(&project_undo); + if let Some(entry) = guard.graph.get_mut(id) { + entry + .core + .set_standard_value(&input_undo, -1, old.clone()); + } + }, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// 建一个含 Text/StrCombo/Float 输入的测试节点。 + fn test_project() -> (Arc>, oaknode::id::NodeId) { + let project = oaknode::project::Project::new(); + let mut guard = project.lock().unwrap(); + let id = guard.graph.add_node( + oaknode::node::NodeCore::empty(), + Box::new(oaknode::nodes::EmptyBehavior), + ); + // NodeCore::empty 无输入;手动声明三种输入。 + use oaknode::input::Input; + let core = guard.graph.get_mut(id).unwrap(); + core.core.add_input(Input::new( + "label", + oaknode::value::ValueType::Text, + oaknode::value::NodeValue::Text(String::new()), + )); + core.core.add_input(Input::new( + "choice", + oaknode::value::ValueType::StrCombo, + oaknode::value::NodeValue::StrCombo(String::new()), + )); + core.core.add_input(Input::new( + "opacity", + oaknode::value::ValueType::Float, + oaknode::value::NodeValue::Float(1.0), + )); + drop(guard); + (project, id) + } + + /// 注册表:register → node_from_identity → unregister 全链。 + #[test] + fn identity_register_roundtrip() { + let (project, id) = test_project(); + let identity = register_node(project.clone(), id); + let node = node_from_identity(identity as usize).expect("已登记身份应可解析"); + assert_eq!(node.id, id); + let found = node.project.lock().unwrap().graph.get(id).is_some(); + assert!(found); + + // 未登记身份 → None。 + assert!(node_from_identity(0xfeed_face).is_none()); + unregister_node(identity); + assert!(node_from_identity(identity as usize).is_none()); + } + + /// project 释放后弱条目升级失败 → None(悬垂语义);重复登记 + /// 覆盖死条目后重新解析到新 project。 + #[test] + fn identity_project_dropped() { + let (project, id) = test_project(); + let identity = register_node(project.clone(), id); + drop(project); + assert!(node_from_identity(identity as usize).is_none()); + // 新 project 同槽位身份重登记(覆盖死条目)。 + let (project2, id2) = test_project(); + register_node(project2.clone(), id2); + let node = node_from_identity(identity as usize).expect("重登记后应可解析"); + assert_eq!(node.id, id2); + let alive = node.project.lock().unwrap().graph.get(id2).is_some(); + assert!(alive); + } + + /// Value POD ↔ NodeValue 全类型映射;STRING 族 POD 无数据。 + #[test] + fn pod_node_value_roundtrip() { + use oaknode::value::NodeValue as NV; + let cases: Vec<(Value, NV)> = vec![ + (Value::int(7), NV::Int(7)), + (Value::float(1.5), NV::Float(1.5)), + (Value::bool_(true), NV::Boolean(true)), + (Value::combo(3), NV::Combo(3)), + ( + Value { + r#type: node_value_type::RATIONAL, + num: 3, + den: 2, + f: [0.0; 4], + }, + NV::Rational(oakcore_rs::Rational::new(3, 2)), + ), + (Value::color(0.1, 0.2, 0.3, 0.4), NV::Color([0.1, 0.2, 0.3, 0.4])), + (Value::vec(&[1.0, 2.0]), NV::Vec2([1.0, 2.0])), + (Value::vec(&[1.0, 2.0, 3.0]), NV::Vec3([1.0, 2.0, 3.0])), + ( + Value::vec(&[1.0, 2.0, 3.0, 4.0]), + NV::Vec4([1.0, 2.0, 3.0, 4.0]), + ), + ]; + for (pod, nv) in cases { + let to = pod.to_node_value().expect("POD 应可转换"); + assert_eq!(to, nv, "POD → NodeValue"); + assert_eq!(Value::from_node_value(&nv), Some(pod), "NodeValue → POD"); + } + // STRING POD 无数据。 + assert!(Value::string().to_node_value().is_none()); + // 无 POD 表达的类型。 + assert_eq!(Value::from_node_value(&NV::Text("x".into())), None); + assert_eq!(Value::from_node_value(&NV::Matrix([0.0; 16])), None); + } + + /// set_input_undoable:redo 写标准值、undo 回放旧值;错误路径。 + #[test] + fn set_input_undoable_applies_and_undoes() { + let (project, id) = test_project(); + let node = NodeRef { + project: project.clone(), + id, + }; + let mut cmd = set_input_undoable(&node, "opacity", &Value::float(0.25)).unwrap(); + // 命令未执行时值未变。 + assert_eq!( + project + .lock() + .unwrap() + .graph + .get(id) + .unwrap() + .core + .standard_value("opacity", -1), + oaknode::value::NodeValue::Float(1.0) + ); + cmd.redo_now(); + assert_eq!( + project + .lock() + .unwrap() + .graph + .get(id) + .unwrap() + .core + .standard_value("opacity", -1), + oaknode::value::NodeValue::Float(0.25) + ); + cmd.undo_now(); + assert_eq!( + project + .lock() + .unwrap() + .graph + .get(id) + .unwrap() + .core + .standard_value("opacity", -1), + oaknode::value::NodeValue::Float(1.0) + ); + + // 未知输入 / STRING POD / 失效节点。 + assert!(matches!( + set_input_undoable(&node, "nope", &Value::float(1.0)), + Err(NodeBridgeError::UnknownInput(_)) + )); + assert!(matches!( + set_input_undoable(&node, "opacity", &Value::string()), + Err(NodeBridgeError::InvalidValue(_)) + )); + let stale = NodeRef { + project, + id: oaknode::id::NodeId::from_identity(999).unwrap(), + }; + assert!(matches!( + set_input_undoable(&stale, "opacity", &Value::float(1.0)), + Err(NodeBridgeError::StaleNode) + )); + } + + /// set_input_string_undoable:Text/StrCombo 变体选择与回放。 + #[test] + fn set_input_string_undoable_variants() { + let (project, id) = test_project(); + let node = NodeRef { + project: project.clone(), + id, + }; + let mut cmd = set_input_string_undoable(&node, "label", "hello").unwrap(); + cmd.redo_now(); + assert_eq!( + project + .lock() + .unwrap() + .graph + .get(id) + .unwrap() + .core + .standard_value("label", -1), + oaknode::value::NodeValue::Text("hello".to_string()) + ); + cmd.undo_now(); + assert_eq!( + project + .lock() + .unwrap() + .graph + .get(id) + .unwrap() + .core + .standard_value("label", -1), + oaknode::value::NodeValue::Text(String::new()) + ); + + let mut cmd = set_input_string_undoable(&node, "choice", "low").unwrap(); + cmd.redo_now(); + assert_eq!( + project + .lock() + .unwrap() + .graph + .get(id) + .unwrap() + .core + .standard_value("choice", -1), + oaknode::value::NodeValue::StrCombo("low".to_string()) + ); + + // 数值输入走字符串 setter → NotStringInput。 + assert!(matches!( + set_input_string_undoable(&node, "opacity", "1.0"), + Err(NodeBridgeError::NotStringInput(_)) + )); + } +} diff --git a/crates/oakplugin/src/param.rs b/crates/oakplugin/src/param.rs index eb7a7cc62..876f6b3b3 100644 --- a/crates/oakplugin/src/param.rs +++ b/crates/oakplugin/src/param.rs @@ -18,18 +18,16 @@ //! //! 对应 C++ 的 `ParamInstance`/`OliveParamInstance`。桥的语义 //! (M9 已定):节点输入值变化 → 写回 OFX 参数;OFX 参数被插件 -//! 改动 → 经 oaknode C ABI 回写节点(undoable 经 -//! [`crate::bridge::undo`])。 +//! 改动 → 经 oaknode 回写节点(undoable 经 +//! `oakundo::undocommand::UndoCommand`)。 //! //! 句柄约定([`crate::suites::tag`]):`ParamDef`/`ParamInstance` //! `#[repr(C)]` 且 `props` 在偏移 0;元素装箱(`Vec>`)保证 //! Vec 重分配不移动对象、句柄不悬垂。 //! -//! `// TODO(bridge)`:`set_from_node`/`notify_instance_changed` 依赖 -//! bridge::node 的 `Value` 布局(声明尚未冻结),保留 todo!()。 -//! **已落地(M11 第 1 期)**:Value 布局随 [`crate::bridge::node`] -//! 冻结(`include/node/node.h` 的 `oaknode_value` POD),两处 todo -//! 已实现。 +//! 节点值布局:随 [`crate::node`] 冻结(`include/node/node.h` 的 +//! `oaknode_value` POD);单库化后 oaknode 的身份注册表随其 ffi +//! 删除,回写路径是保留失败路径的本地桩(见 [`crate::node`])。 use std::ffi::CString; @@ -532,55 +530,55 @@ impl ParamInstance { /// (OAKNODE_VALUE_STRING)的 POD 不携带数据——此路径不改值 /// (走 facade 的字符串 API,见 `include/plugin/instance.h`)。 /// 类型不匹配 → 忽略(保持现值;C++ `node_get` 失败时参数不回写)。 - pub fn set_from_node(&self, node_value: &crate::bridge::node::Value) { - use crate::bridge::node::node_value_type as T; - let v = node_value; - let mapped: Option = match self.def.ofx_type.as_str() { - TYPE_DOUBLE => { - (v.r#type == T::FLOAT).then(|| ParamValue::Double([v.f[0], 0.0, 0.0], 1)) - } - TYPE_DOUBLE2D => { - (v.r#type == T::VEC2).then(|| ParamValue::Double([v.f[0], v.f[1], 0.0], 2)) - } - TYPE_DOUBLE3D => { - (v.r#type == T::VEC3).then(|| ParamValue::Double([v.f[0], v.f[1], v.f[2]], 3)) - } - TYPE_INTEGER => (v.r#type == T::INT).then(|| ParamValue::Int([v.num as i32, 0, 0], 1)), - TYPE_INTEGER2D | TYPE_INTEGER3D => { - let dim = if self.def.ofx_type == TYPE_INTEGER2D { - 2 - } else { - 3 - }; - Some(ParamValue::Int( - [v.f[0] as i32, v.f[1] as i32, v.f[2] as i32], - dim, - )) - } - TYPE_BOOLEAN => (v.r#type == T::BOOL).then(|| ParamValue::Bool(v.num != 0)), - TYPE_CHOICE => (v.r#type == T::COMBO).then(|| ParamValue::Choice(v.num as i32)), - TYPE_RGB => { - (v.r#type == T::COLOR).then(|| ParamValue::Color([v.f[0], v.f[1], v.f[2], 0.0], 3)) - } - TYPE_RGBA => (v.r#type == T::COLOR) - .then(|| ParamValue::Color([v.f[0], v.f[1], v.f[2], v.f[3]], 4)), - _ => None, // 字符串/无值类:POD 无数据,不改值 - }; - if let Some(pv) = mapped { + pub fn set_from_node(&self, node_value: &crate::node::Value) { + if let Some(pv) = param_value_from_node(node_value, &self.def.ofx_type) { self.set_ofx(pv); } } } -/// ParamValue → [`crate::bridge::node::Value`](插件→节点方向; -/// 字符串族经 [`crate::bridge::node::set_input_string_undoable`])。 +/// 节点值 → ParamValue(按参数类型解释;字符串族走专用路径)。 +/// 镜像 `set_from_node` 的映射表;render 驱动的参数覆盖 +/// (apply_param_overrides)复用同一转换。 +pub(crate) fn param_value_from_node( + v: &crate::node::Value, + ofx_type: &str, +) -> Option { + use crate::node::node_value_type as T; + match ofx_type { + TYPE_DOUBLE => (v.r#type == T::FLOAT).then(|| ParamValue::Double([v.f[0], 0.0, 0.0], 1)), + TYPE_DOUBLE2D => (v.r#type == T::VEC2).then(|| ParamValue::Double([v.f[0], v.f[1], 0.0], 2)), + TYPE_DOUBLE3D => { + (v.r#type == T::VEC3).then(|| ParamValue::Double([v.f[0], v.f[1], v.f[2]], 3)) + } + TYPE_INTEGER => (v.r#type == T::INT).then(|| ParamValue::Int([v.num as i32, 0, 0], 1)), + TYPE_INTEGER2D | TYPE_INTEGER3D => { + let dim = if ofx_type == TYPE_INTEGER2D { 2 } else { 3 }; + Some(ParamValue::Int( + [v.f[0] as i32, v.f[1] as i32, v.f[2] as i32], + dim, + )) + } + TYPE_BOOLEAN => (v.r#type == T::BOOL).then(|| ParamValue::Bool(v.num != 0)), + TYPE_CHOICE => (v.r#type == T::COMBO).then(|| ParamValue::Choice(v.num as i32)), + TYPE_RGB => { + (v.r#type == T::COLOR).then(|| ParamValue::Color([v.f[0], v.f[1], v.f[2], 0.0], 3)) + } + TYPE_RGBA => (v.r#type == T::COLOR) + .then(|| ParamValue::Color([v.f[0], v.f[1], v.f[2], v.f[3]], 4)), + _ => None, // 字符串/无值类:POD 无数据,不改值 + } +} + +/// ParamValue → [`crate::node::Value`](插件→节点方向; +/// 字符串族经 [`crate::node::set_input_string_undoable`])。 /// 镜像 C++ `paraminstance.h` 的 `value_int`/`value_double`/ /// `value_vec`/`value_color` 构造:RGB 颜色补 alpha=1(C++ /// `RGBInstance::set` 的 `value_color(r,g,b,1.0)`)。无值类与 Bytes /// 无节点对应 → None。 -pub(crate) fn to_node_value(v: &ParamValue) -> Option { - use crate::bridge::node::node_value_type as T; - let mut out = crate::bridge::node::Value::default(); +pub(crate) fn to_node_value(v: &ParamValue) -> Option { + use crate::node::node_value_type as T; + let mut out = crate::node::Value::default(); match v { ParamValue::Double(d, 1) => { out.r#type = T::FLOAT; @@ -659,15 +657,18 @@ impl ParamSetInstance { } /// 插件 → 节点方向的回写入口(instanceChanged action 触发)。 -/// 经 [`crate::bridge::node`] 定位绑定节点(身份注册表),再经 -/// [`crate::bridge::undo`] 包成 undoable 修改。未绑定节点时 no-op。 +/// 经 [`crate::node`] 定位绑定节点(身份注册表),再经 +/// `oakundo::undocommand::UndoCommand` 包成 undoable 修改。未绑定 +/// 节点或身份查无时 no-op(单库化后身份注册表重建为 +/// [`crate::node::register_node`]/[`crate::node::node_from_identity`]; +/// facade 装配期登记,project 释放后弱条目自然失效)。 /// /// 语义对照 C++ `paraminstance.h` 的 `set()` 路径(`detail::node_set`/ /// `node_set_at`): /// - 只回写 [`ChangeReason::PluginEdited`](插件自改)。UserEdited/ /// TimeChanged 是宿主侧变更,值已由 [`ParamInstance::set_from_node`] /// 同步,不重复写回; -/// - 字符串族经 `oaknode_node_set_input_string_undoable` +/// - 字符串族经 [`crate::node::set_input_string_undoable`] /// (POD 不携带字符串数据); /// - 无值类(PushButton/Group/Page)与 Bytes 无节点对应 → no-op; /// - 编辑事务内([`crate::instance::Instance::in_edit`])并入 multi @@ -677,7 +678,7 @@ pub(crate) fn notify_instance_changed( param_name: &str, reason: ChangeReason, ) { - use crate::bridge::{node, undo}; + use crate::node; use std::sync::atomic::Ordering; if !matches!(reason, ChangeReason::PluginEdited) { @@ -688,42 +689,27 @@ pub(crate) fn notify_instance_changed( if node_id == 0 { return; } - // 身份查无(注册表无此项 / 桥符号缺失)→ no-op。 - let node_handle = unsafe { node::node_from_identity(node_id) }; - if node_handle.is_null() { + // 身份查无(未登记 / project 已释放)→ no-op。 + let Some(node_ref) = node::node_from_identity(node_id) else { return; - } + }; let Some(param) = instance.params.find(param_name) else { return; }; let value = param.get(); let label = format!("Change {param_name}"); - let Some(cname) = CString::new(param_name).ok() else { - return; - }; match to_node_value(&value) { Some(nv) => { - let mut cmd = undo::CommandHandle::null(); - let r = unsafe { node::set_input_undoable(node_handle, cname.as_ptr(), &nv, &mut cmd) }; - if r == 0 && !cmd.is_null() { + if let Ok(cmd) = node::set_input_undoable(&node_ref, param_name, &nv) { instance.submit_undo_command(cmd, &label); } } None => match &value { ParamValue::String(s) | ParamValue::StrChoice(s) => { - let Some(cval) = CString::new(s.to_bytes()).ok() else { + let Ok(s) = s.to_str() else { return; }; - let mut cmd = undo::CommandHandle::null(); - let r = unsafe { - node::set_input_string_undoable( - node_handle, - cname.as_ptr(), - cval.as_ptr(), - &mut cmd, - ) - }; - if r == 0 && !cmd.is_null() { + if let Ok(cmd) = node::set_input_string_undoable(&node_ref, param_name, s) { instance.submit_undo_command(cmd, &label); } } @@ -746,7 +732,7 @@ pub enum ChangeReason { #[cfg(test)] mod tests { use super::*; - use crate::bridge::node::node_value_type as T; + use crate::node::node_value_type as T; fn param(ofx_type: &str) -> ParamInstance { ParamInstance::from_def(ParamDef::new("p", ofx_type)) @@ -756,7 +742,7 @@ mod tests { /// paraminstance.h 的 value_* 构造)。 #[test] fn to_node_value_mapping() { - use crate::bridge::node::Value; + use crate::node::Value; let c = |t: i32| Value { r#type: t, num: 0, @@ -834,15 +820,15 @@ mod tests { fn set_from_node_dimension_rules() { // Integer2D 缺第三维补零;浮点截断。 let p = param(TYPE_INTEGER2D); - p.set_from_node(&crate::bridge::node::Value::vec(&[1.9, 2.9])); + p.set_from_node(&crate::node::Value::vec(&[1.9, 2.9])); assert_eq!(p.get(), ParamValue::Int([1, 2, 0], 2)); // StrChoice 参数遇 STRING 类型:POD 无数据 → 不改值。 let p = param(TYPE_STRCHOICE); - p.set_from_node(&crate::bridge::node::Value::string()); + p.set_from_node(&crate::node::Value::string()); assert!(matches!(p.get(), ParamValue::StrChoice(_))); // Bytes/Custom 参数:任意节点值都不改(无映射)。 let p = param(TYPE_CUSTOM); - p.set_from_node(&crate::bridge::node::Value::float(3.0)); + p.set_from_node(&crate::node::Value::float(3.0)); assert!(matches!(p.get(), ParamValue::Bytes(_))); } } diff --git a/crates/oakplugin/src/render.rs b/crates/oakplugin/src/render.rs new file mode 100644 index 000000000..c4078f2eb --- /dev/null +++ b/crates/oakplugin/src/render.rs @@ -0,0 +1,234 @@ +// 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 . + +//! oakrender 桥(single-lib unification):纹理/帧值类型与渲染调用面。 +//! +//! oakrender 的 C ABI 已删除(单库化):纹理是 +//! [`oakrender::texture::Texture`](value enum,无句柄),CPU 帧是 +//! [`oakrender::texture::Frame`]。本 crate 的 render 驱动与 GL suite +//! 直接持值类型: +//! +//! - [`Texture`] = [`oakrender::texture::Texture`](值别名; +//! clone 即引用语义,drop 自动释放后端 token——原 `texture_free`/ +//! `frame_free` 调用面随值模型删除); +//! - [`Frame`] = [`oakrender::texture::Frame`](值别名); +//! - [`Renderer`] = `Arc` +//! (渲染器即 oakrender 后端上下文,facade 经 +//! [`oakrender::backend::GpuContext::create`] 创建); +//! - [`VideoParams`] 直接别名 oakrender 的 +//! [`oakrender::frame::VideoParamsPod`](同布局 POD); +//! - 像素格式常量直接别名 [`oakcore_rs::PixelFormat`]。 +//! +//! 保留桩(GPU 相关、wgpu 模型无直接 Rust 等价物): +//! [`texture_id`]——wgpu 后端没有 OpenGL 纹理名(旧 C ABI 的 +//! `oakrender_texture_id` 语义是 GL 命名空间),恒 0;GL suite 的 +//! `OpenGLTextureIndex` 属性与 render 驱动的 use_opengl 决策据此 +//! 回退 CPU 路径。 + +/// `oakrender_video_params` POD — single-lib unification: aliases the +/// oakrender crate's struct (identical layout; +/// include/render/renderer.h:78). +pub type VideoParams = oakrender::frame::VideoParamsPod; + +/// olive::PixelFormat::Format 的 f32 值。 +pub const PIXEL_FORMAT_F32: i32 = oakcore_rs::PixelFormat::F32 as i32; +/// olive::PixelFormat::Format 的 u8 值。 +pub const PIXEL_FORMAT_U8: i32 = oakcore_rs::PixelFormat::U8 as i32; + +/// oakrender 渲染器(后端上下文;Arc 共享,GPU 纹理据此 upload/ +/// download/blit——无需独立渲染器句柄)。 +pub type Renderer = std::sync::Arc; + +/// oakrender 纹理(值型;GPU 或 CPU 包装)。 +pub type Texture = oakrender::texture::Texture; + +/// oakrender CPU 帧(值型)。 +pub type Frame = oakrender::texture::Frame; + +// ---- 桥调用面(值型实现;原 CHandle 桩随单库化重写)---------------------- + +/// 纹理是否占位(dummy:透明黑、从未上传)。 +pub fn texture_is_dummy(texture: &Texture) -> bool { + texture.is_dummy() +} + +/// 纹理的 CPU 帧:GPU 纹理经后端下载,CPU 纹理克隆 +/// (原 `texture_get_frame` 的 out 参数形态改为值返回;错误即旧 +/// "纹理无 CPU 帧" 失败路径)。 +pub fn texture_get_frame(texture: &Texture) -> crate::error::Result { + texture.to_frame().map_err(|e| { + crate::error::Error::Failed(format!("纹理 readback 失败:{e}")) + }) +} + +/// 纹理的视频参数(尺寸 + 像素格式;原 `texture_get_params` 的 +/// out 参数形态改为值返回)。 +pub fn texture_get_params(texture: &Texture) -> VideoParams { + let (w, h) = texture.size(); + let mut p = VideoParams::default(); + p.width = w; + p.height = h; + p.format = texture.format() as i32; + p +} + +/// 从视频参数与像素数据构造 CPU 纹理(原 `texture_create` 的 +/// renderer+参数+像素形态改为值形态):按 `linesize` 行跨度做行优先 +/// 拷贝(0 = 紧凑行),F32/RGBA 4 通道为管线常规。GPU 上传由后端 +/// 延迟(wrapped frame 语义,`Texture::wrap_frame`)。 +pub fn texture_create( + params: &VideoParams, + pixels: &[u8], + linesize: i32, +) -> crate::error::Result { + use crate::error::Error; + let mut frame = Frame::new(); + frame.set_video_params(*params); + let w = frame.width.max(0) as usize; + let h = frame.height.max(0) as usize; + if w == 0 || h == 0 { + return Err(Error::Invalid); + } + let bpc = frame.bytes_per_channel(); + if bpc == 0 { + return Err(Error::Invalid); + } + // 行跨度推导通道数(RGB 3 通道等非常规分量)。 + if linesize > 0 && w > 0 && (linesize as usize) % (w * bpc) == 0 { + frame.channels = (linesize as usize / (w * bpc)) as i32; + } + let tight = frame.linesize_bytes(); + let row = if linesize > 0 { linesize as usize } else { tight }; + if pixels.len() < row.saturating_mul(h) { + return Err(Error::Invalid); + } + frame.data = vec![0u8; tight * h]; + for y in 0..h { + let s = y * row; + let d = y * tight; + frame.data[d..d + tight].copy_from_slice(&pixels[s..s + tight]); + } + Ok(Texture::wrap_frame(frame)) +} + +/// STUB: oakrender C ABI deleted (single-lib),且 wgpu 后端没有 +/// OpenGL 纹理名——旧 `oakrender_texture_id` 的 GL 命名空间语义无 +/// Rust 等价物。恒 0(GL suite 的 `OpenGLTextureIndex` 属性与 +/// render 驱动的 use_opengl 决策据此回退 CPU 路径;GPU 上传若落地 +/// 走 `oakrender::backend::GpuContextLike::upload` 的 wgpu token, +/// 不暴露 GL id)。 +pub fn texture_id(_texture: &Texture) -> i32 { + 0 +} + +/// 渲染器是否为 OpenGL 后端([`oakrender::backend::BackendKind::Gl`]; +/// 原 `renderer_is_open_gl` 的句柄形态改为后端上下文 kind 查询)。 +pub fn renderer_is_open_gl(renderer: &Renderer) -> bool { + renderer.kind() == oakrender::backend::BackendKind::Gl +} + +#[cfg(test)] +mod tests { + use super::*; + + /// 测试渲染器:最小 GpuContextLike 假实现(无 GPU 适配器需求)。 + struct FakeGpu; + impl oakrender::backend::GpuContextLike for FakeGpu { + fn kind(&self) -> oakrender::backend::BackendKind { + oakrender::backend::BackendKind::Cpu + } + fn destroy_texture(&self, _token: u64) {} + fn upload(&self, _token: u64, _frame: &Frame) -> oakrender::error::Result<()> { + Ok(()) + } + fn download(&self, _token: u64) -> oakrender::error::Result { + Ok(Frame::new()) + } + fn blit( + &self, + _src: u64, + _dst: u64, + _processor: Option<&oakrender::color::ColorProcessor>, + ) -> oakrender::error::Result<()> { + Ok(()) + } + } + + /// 纹理创建:参数 + 行跨度感知拷贝往返(含 RGB 3 通道推导)。 + #[test] + fn texture_create_linesize_aware() { + let mut params = VideoParams::default(); + params.width = 3; + params.height = 2; + params.format = PIXEL_FORMAT_F32; + // 紧凑 3×2 RGBA F32:3*4*4 = 48 B/行。 + let tight = 3 * 4 * 4; + let mut pixels = vec![0u8; tight * 2]; + for (i, b) in pixels.iter_mut().enumerate() { + *b = (i % 251) as u8; + } + let tex = texture_create(¶ms, &pixels, 0).expect("紧凑行应可创建"); + assert!(!texture_is_dummy(&tex)); + let frame = texture_get_frame(&tex).expect("CPU 帧应可读"); + assert_eq!(frame.width, 3); + assert_eq!(frame.height, 2); + assert_eq!(frame.channels, 4); + assert_eq!(frame.linesize_bytes(), tight); + assert_eq!(frame.data, pixels); + // 行跨度变体(每行 4 字节填充)。 + let row = tight + 4; + let mut padded = vec![0u8; row * 2]; + for y in 0..2 { + padded[y * row..y * row + tight].copy_from_slice(&pixels[y * tight..(y + 1) * tight]); + } + let tex = texture_create(¶ms, &padded, row as i32).expect("带填充行应可创建"); + let frame = texture_get_frame(&tex).unwrap(); + assert_eq!(frame.data, pixels, "填充行应剥离为紧凑存储"); + // 像素不足 → Invalid。 + assert!(texture_create(¶ms, &padded[..row * 2 - 1], row as i32).is_err()); + // 无效尺寸 → Invalid。 + let mut bad = params; + bad.width = 0; + assert!(texture_create(&bad, &pixels, 0).is_err()); + } + + /// 纹理参数查询与 dummy 语义。 + #[test] + fn texture_params_and_dummy() { + let dummy = Texture::dummy(); + assert!(texture_is_dummy(&dummy)); + let p = texture_get_params(&dummy); + assert_eq!((p.width, p.height), (0, 0)); + + let mut params = VideoParams::default(); + params.width = 8; + params.height = 4; + params.format = PIXEL_FORMAT_F32; + let tex = texture_create(¶ms, &[0u8; 8 * 4 * 4 * 4], 0).unwrap(); + let p = texture_get_params(&tex); + assert_eq!((p.width, p.height), (8, 4)); + assert_eq!(p.format, PIXEL_FORMAT_F32); + } + + /// 渲染器 kind 查询与 GL 判断;texture_id 桩恒 0(无 GL 命名空间)。 + #[test] + fn renderer_kind_and_gl_id_stub() { + let r: Renderer = std::sync::Arc::new(FakeGpu); + assert!(!renderer_is_open_gl(&r)); + let dummy = Texture::dummy(); + assert_eq!(texture_id(&dummy), 0, "wgpu 无 GL 纹理名:桩恒 0"); + } +} diff --git a/crates/oakplugin/src/render_driver.rs b/crates/oakplugin/src/render_driver.rs index d3d0da77a..562d11bbb 100644 --- a/crates/oakplugin/src/render_driver.rs +++ b/crates/oakplugin/src/render_driver.rs @@ -18,9 +18,10 @@ //! `PluginRenderer::render_plugin`(1857 行 C++ 的一部分)的渲染流程 //! 语义收编(M11 §4)。 //! -//! 目标:oakrender 的 PluginJob 退化为一次 C ABI 调用( -//! [`crate::ffi::oakplugin_instance_render_job`]),本模块承载全部 -//! OFX 宿主渲染流程。逐段对照的 C++ 行号已注释。 +//! 目标:oakrender 的 PluginJob 语义全部收编进本模块(单库化后 +//! 原 `oakplugin_instance_render_job` C ABI 已删除,facade 直接构造 +//! [`RenderJob`] 调 [`render_frame`]),本模块承载全部 OFX 宿主 +//! 渲染流程。逐段对照的 C++ 行号已注释。 //! //! ## 流程(render_frame,对应 render_plugin) //! @@ -56,28 +57,27 @@ //! 的一批帧先 [`begin_sequence`] 后 [`end_sequence`],中间逐帧 //! [`render_frame`]。 -use crate::bridge::render::{self, FrameHandle, RendererHandle, TextureHandle}; use crate::image::Image; use crate::instance::{Instance, OfxRangeD, OfxRectD, RenderScale}; use crate::property::Value; +use crate::render::{self, Renderer, Texture}; -/// 一帧渲染任务的输入(oakrender PluginJob 的 C ABI 载体; -/// [`crate::ffi::OakPluginJob`] 的 Rust 侧视图)。 +/// 一帧渲染任务的输入(oakrender PluginJob 的 Rust 侧视图)。 pub struct RenderJob { /// 帧时间(秒)。 pub time: f64, - /// 目标纹理(输出;oakrender 侧创建并经句柄传入)。 - pub dst: TextureHandle, - /// 主输入纹理(effect_input_id / SimpleSource;可空)。 - pub src: TextureHandle, + /// 目标纹理(输出;oakrender 侧创建并经值传入)。 + pub dst: Texture, + /// 主输入纹理(effect_input_id / SimpleSource;无则 None)。 + pub src: Option, /// effect 输入 clip 名(job.src 的落点;C++ `node->get_effect_input_id()`)。 pub effect_input_id: Option, /// 其余输入 clip 的纹理表(clip 名 → 纹理)。 - pub inputs: Vec<(String, TextureHandle)>, + pub inputs: Vec<(String, Texture)>, /// 参数覆盖(参数名 → oaknode_value POD;对应 NodeValueRow)。 - pub values: Vec<(String, crate::ffi::OakNodeValue)>, + pub values: Vec<(String, crate::node::Value)>, /// GL 渲染器(None → CPU 路径;Some → 视 use_opengl 决策)。 - pub renderer: Option, + pub renderer: Option, /// 渲染前是否清空目标(信息性——C++ render_plugin 亦不处理, /// 由上层渲染器负责;见插件渲染器注释)。 pub clear_destination: bool, @@ -90,8 +90,8 @@ impl Default for RenderJob { fn default() -> Self { Self { time: 0.0, - dst: TextureHandle::null(), - src: TextureHandle::null(), + dst: Texture::dummy(), + src: None, effect_input_id: None, inputs: Vec::new(), values: Vec::new(), @@ -109,7 +109,7 @@ impl Default for RenderJob { pub fn begin_sequence( inst: &Instance, range: OfxRangeD, - gl: Option, + gl: Option, ) -> crate::error::Result<()> { if gl.is_some() { inst.begin_sequence_render_gl(range) @@ -122,7 +122,7 @@ pub fn begin_sequence( pub fn end_sequence( inst: &Instance, range: OfxRangeD, - gl: Option, + gl: Option, ) -> crate::error::Result<()> { if gl.is_some() { inst.end_sequence_render_gl(range) @@ -153,21 +153,22 @@ pub fn render_frame( // 2. use_opengl(pluginrenderer.cpp:1446-1457):插件声明 GL 支持 // 且渲染器是 OpenGL 且目标纹理有 GL id 且像素深度协商可行 // (管线 F32 满足插件 kOfxOpenGLPropPixelDepth 声明)。 - let use_opengl = match job.renderer { - Some(r) if unsafe { render::renderer_is_open_gl(r) } == 1 => { + // `texture_id` 为桩恒 0(wgpu 无 GL 命名空间)→ 本决策恒回退 + // CPU 路径,GL 分支保留给 GL 后端落地。 + let use_opengl = match job.renderer.as_ref() { + Some(r) if render::renderer_is_open_gl(r) => { let plugin_gl = plugin_supports_opengl(inst); let depth_ok = crate::suites::gl_render::pick_gl_pixel_depth(&inst.plugin.descriptor.props) .is_some(); - let dst_id = unsafe { render::texture_id(job.dst) }; + let dst_id = render::texture_id(&job.dst); plugin_gl && depth_ok && dst_id != 0 } _ => false, }; // 目标帧与参数(F32 校验;输出装配的依据)。 - let (dst_frame, dst_params, w, h) = read_dst(job.dst)?; - let mut dst_frame = dst_frame; + let (dst_params, w, h) = read_dst(&job.dst)?; let par = pixel_aspect(&dst_params); // 规范坐标的 RoI/RoD(pluginrenderer.cpp:1595-1603:x2 = 宽 × PAR)。 let region_of_interest = OfxRectD { @@ -182,9 +183,10 @@ pub fn render_frame( if clip.name == "Output" { continue; } - let tex = pick_input(&clip.name, job); - if usable(tex) { - clip.set_input_texture(tex, job.time); + if let Some(tex) = pick_input(&clip.name, job) { + if usable(&tex) { + clip.set_input_texture(Some(tex), job.time); + } } } @@ -207,7 +209,7 @@ pub fn render_frame( .find(|c| c.name == "Output") .ok_or_else(|| Error::Failed("实例无 Output clip".into()))?; output_clip.set_region_of_definition(region_of_interest, job.time); - output_clip.set_output_texture(job.dst, job.time); + output_clip.set_output_texture(Some(job.dst.clone()), job.time); // 6. 输入 clip:RoD 与格式(pluginrenderer.cpp:1627-1665;Phase 2 // 全链路 F32 → 格式选择恒等,无转换路径)。 @@ -215,7 +217,7 @@ pub fn render_frame( if clip.name == "Output" { continue; } - if usable(pick_input(&clip.name, job)) { + if pick_input(&clip.name, job).map_or(false, |t| usable(&t)) { clip.set_region_of_definition(region_of_interest, job.time); clip.set_video_params(render::PIXEL_FORMAT_F32, 4); } @@ -242,8 +244,7 @@ pub fn render_frame( // 9. isIdentity 短路(ofxRendering "Identity Effects"):插件声明 // 本帧等价于某输入 clip → 直接透传该 clip 在透传时间的帧。 if let Some((t, clip_name)) = inst.is_identity(job.time)? { - passthrough(inst, &clip_name, t, job.dst)?; - unsafe { render::frame_free(&mut (dst_frame)) }; + passthrough(inst, &clip_name, t, &job.dst)?; return Ok(zip_rois(inst, &rois)); } @@ -269,7 +270,7 @@ pub fn render_frame( output.clone(), )?; // 输出装配(pluginrenderer.cpp:1762-1834 的 CPU 路径)。 - write_output_frame(job.dst, &output)?; + write_output_frame(&job.dst, &output)?; } else { // GL 路径:插件直接画进已附着的输出纹理( // pluginrenderer.cpp:1784-1834 的 GL 分支);无 CPU 回读。 @@ -277,12 +278,11 @@ pub fn render_frame( job.time, RenderScale { x: 1.0, y: 1.0 }, render_window, - job.renderer.unwrap(), - job.dst, + job.renderer.clone().unwrap(), + job.dst.clone(), )?; } - unsafe { render::frame_free(&mut (dst_frame)) }; Ok(zip_rois(inst, &rois)) } @@ -296,22 +296,11 @@ fn zip_rois(inst: &Instance, rois: &[OfxRectD]) -> Vec<(String, OfxRectD)> { .collect() } -/// 读目标纹理的帧与参数(F32 校验)。返回 (帧句柄, 参数, 宽, 高)。 -fn read_dst( - dst: TextureHandle, -) -> crate::error::Result<(FrameHandle, render::VideoParams, f64, f64)> { +/// 读目标纹理的参数(F32 校验)。返回 (参数, 宽, 高)。 +fn read_dst(dst: &Texture) -> crate::error::Result<(render::VideoParams, f64, f64)> { use crate::error::Error; - let mut frame = FrameHandle::null(); - if unsafe { render::texture_get_frame(dst, &mut frame) } != 0 || frame.is_null() { - return Err(Error::Failed("输出纹理无 CPU 帧".into())); - } - let mut params = render::VideoParams::default(); - if unsafe { render::frame_get_params(frame, &mut params) } != 0 { - unsafe { render::frame_free(&mut frame) }; - return Err(Error::Failed("输出帧无参数".into())); - } + let params = render::texture_get_params(dst); if params.format != render::PIXEL_FORMAT_F32 { - unsafe { render::frame_free(&mut frame) }; return Err(Error::Failed(format!( "输出帧格式 {} 非 F32(Phase 2 约束)", params.format @@ -319,10 +308,9 @@ fn read_dst( } let (w, h) = (params.width as f64, params.height as f64); if w <= 0.0 || h <= 0.0 { - unsafe { render::frame_free(&mut frame) }; return Err(Error::Invalid); } - Ok((frame, params, w, h)) + Ok((params, w, h)) } /// 目标参数的像素比(缺失 1.0)。 @@ -351,30 +339,32 @@ fn plugin_supports_opengl(inst: &Instance) -> bool { .unwrap_or(false) } -/// 输入纹理是否可用(非空且非占位;pluginrenderer.cpp:1504-1513 的 +/// 输入纹理是否可用(非占位;pluginrenderer.cpp:1504-1513 的 /// is_usable_input——Phase 2 只看非 dummy,帧/Renderer 由 oakrender /// 保证)。 -fn usable(tex: TextureHandle) -> bool { - !tex.is_null() && unsafe { render::texture_is_dummy(tex) } == 0 +fn usable(tex: &Texture) -> bool { + !tex.is_dummy() } /// 按 C++ pluginrenderer.cpp:1527-1543 的规则选输入纹理。 -fn pick_input(clip_name: &str, job: &RenderJob) -> TextureHandle { - if job.effect_input_id.as_deref() == Some(clip_name) && !job.src.is_null() { - return job.src; +fn pick_input(clip_name: &str, job: &RenderJob) -> Option { + if job.effect_input_id.as_deref() == Some(clip_name) { + if let Some(src) = &job.src { + return Some(src.clone()); + } } for (name, tex) in &job.inputs { if name == clip_name { - return *tex; + return Some(tex.clone()); } } // SimpleSource 回退(pluginrenderer.cpp:1534-1543: // kOfxImageEffectSimpleSourceClipName 取 k_texture_input,再回退 // job.src)。 - if clip_name == "Source" && !job.src.is_null() { - return job.src; + if clip_name == "Source" { + return job.src.clone(); } - TextureHandle::null() + None } /// isIdentity 透传:把所引输入 clip 在 `t` 的帧拷入输出(CPU 拷贝; @@ -383,7 +373,7 @@ fn passthrough( inst: &Instance, clip_name: &str, t: f64, - dst: TextureHandle, + dst: &Texture, ) -> crate::error::Result<()> { use crate::error::Error; let clip = inst @@ -397,59 +387,54 @@ fn passthrough( /// 参数覆盖(pluginrenderer.cpp:132-290 `apply_param_overrides` 的 /// Rust 移植):把每帧的节点值注入实例参数。字符串族(String/ -/// StrChoice)经专用 C ABI(set_param_string),不在此表的 oaknode +/// StrChoice)经专用路径(set_param_string),不在此表的 oaknode /// POD 表达范围内 → 跳过(与 C++ 的 k_file/k_text/k_font/k_str_combo /// 走专用桥一致)。 -fn apply_param_overrides(inst: &Instance, values: &[(String, crate::ffi::OakNodeValue)]) { +fn apply_param_overrides(inst: &Instance, values: &[(String, crate::node::Value)]) { for (key, v) in values { let Some(p) = inst.params.find(key) else { continue; }; - let Some(pv) = crate::ffi::node_value_to_param(v, &p.def.ofx_type) else { + let Some(pv) = crate::param::param_value_from_node(v, &p.def.ofx_type) else { continue; }; p.set_ofx(pv); } } -/// 把 CPU 图像写入目标纹理的帧(行优先、行跨度感知;F32 校验)。 +/// 把 CPU 图像写入目标纹理(行优先、行跨度感知;F32 校验)。 /// Phase 2 输出装配的公共落点(CPU render 路径与 isIdentity 透传 -/// 共用)。 -pub(crate) fn write_output_frame(dst: TextureHandle, image: &Image) -> crate::error::Result<()> { +/// 共用)。GPU 目标纹理经后端 upload 回写(`Texture::Gpu` 分支)。 +pub(crate) fn write_output_frame(dst: &Texture, image: &Image) -> crate::error::Result<()> { use crate::error::Error; - let mut frame = FrameHandle::null(); - if unsafe { render::texture_get_frame(dst, &mut frame) } != 0 || frame.is_null() { - return Err(Error::Failed("输出纹理无 CPU 帧".into())); - } - let mut params = render::VideoParams::default(); - if unsafe { render::frame_get_params(frame, &mut params) } != 0 { - unsafe { render::frame_free(&mut frame) }; - return Err(Error::Failed("输出帧无参数".into())); - } + let mut frame = render::texture_get_frame(dst)?; + let params = frame.video_params(); if params.format != render::PIXEL_FORMAT_F32 { - unsafe { render::frame_free(&mut frame) }; return Err(Error::Failed("输出帧格式非 F32(Phase 2 约束)".into())); } let (w, h) = (params.width as usize, params.height as usize); let tight = w * image.components().channel_count() * 4; if tight != image.row_bytes() || tight * h != image.pixels().len() { - unsafe { render::frame_free(&mut frame) }; return Err(Error::Failed("图像尺寸与输出帧不一致".into())); } - let dst_ptr = unsafe { render::frame_data(frame) }; + let dst_ptr = frame.data_mut(); if dst_ptr.is_null() { - unsafe { render::frame_free(&mut frame) }; return Err(Error::Failed("输出帧无数据".into())); } - let row = unsafe { render::frame_linesize_bytes(frame) } as usize; + let row = frame.linesize_bytes(); let row = if row > 0 { row } else { tight }; - let dst_bytes = unsafe { std::slice::from_raw_parts_mut(dst_ptr as *mut u8, row * h) }; + let dst_bytes = unsafe { std::slice::from_raw_parts_mut(dst_ptr, row * h) }; let pixels = image.pixels(); for y in 0..h { let d = y * row; let s = y * tight; dst_bytes[d..d + tight].copy_from_slice(&pixels[s..s + tight]); } - unsafe { render::frame_free(&mut frame) }; + // GPU 目标纹理:拷贝只落在下载帧上,经后端 upload 回写 + // (CPU 纹理无需上传)。 + if let Texture::Gpu { token, ctx, .. } = dst { + ctx.upload(*token, &frame) + .map_err(|e| Error::Failed(format!("输出纹理上传失败:{e}")))?; + } Ok(()) } diff --git a/crates/oakplugin/src/suites/gl_render.rs b/crates/oakplugin/src/suites/gl_render.rs index d905adcce..83c05eea4 100644 --- a/crates/oakplugin/src/suites/gl_render.rs +++ b/crates/oakplugin/src/suites/gl_render.rs @@ -34,7 +34,7 @@ //! 已把输出纹理附着为渲染器输出目标(等价 C++ 的 //! `PluginRenderer::attach_output_texture`)。clipFreeTexture 对 //! Output 只释放句柄、不删纹理(宿主还要读它)。 -//! - 输入纹理经 [`crate::bridge::render`] 在渲染器上创建(CPU 帧 → +//! - 输入纹理经 [`crate::render`] 在渲染器上创建(CPU 帧 → //! GL 上传)。纹理格式:全链路 F32 约束下,像素深度按 clip 协商 //! 结果(恒 F32);`format` 参数(kOfxImageEffectGLFormat*)若 //! 请求的分量与协商分量不符,Phase 2 不做转换 → Failed(规范要求 @@ -164,19 +164,21 @@ mod tests { // ---- 存活纹理表 ----------------------------------------------------------- /// 存活 GL 纹理表:clipLoadTexture 产出(props 地址 → 属性集 + -/// 纹理强引用 + 是否输出 clip);clipFreeTexture 摘除即释放——对应 +/// 纹理值 + 是否输出 clip);clipFreeTexture 摘除即释放——对应 /// HS 的 get/release 配对(HS: ofxhImageEffect.cpp:2336-2351)。 +/// 纹理是 oakrender 值(drop 自动释放后端 token;原 +/// `texture_free` 调用面随值模型删除)。 /// /// 属性集必须**装箱**(Box 稳定堆地址):纹理句柄指向它,函数返回后 /// 必须仍存活;栈上临时变量会悬垂(phase-2 实现初版的 bug)。 static LIVE_TEXTURES: std::sync::LazyLock< - Mutex, crate::bridge::render::TextureHandle, bool)>>, + Mutex, crate::render::Texture, bool)>>, > = std::sync::LazyLock::new(|| Mutex::new(HashMap::new())); /// 登记纹理(clipLoadTexture 内部;`props` 装箱后取地址为句柄基址)。 fn register( props: Box, - texture: crate::bridge::render::TextureHandle, + texture: crate::render::Texture, is_output: bool, ) -> usize { let addr = &*props as *const PropertySet as usize; @@ -191,16 +193,8 @@ fn register( /// 插件在 action 返回前 clipFreeTexture 全部句柄;遗漏的输入纹理在 /// 此释放,输出纹理保留——宿主还要读它)。 pub(crate) fn purge_leftovers() { - let leftovers: Vec<(crate::bridge::render::TextureHandle, bool)> = { - let mut live = LIVE_TEXTURES.lock().unwrap_or_else(|e| e.into_inner()); - live.drain().map(|(_k, (_p, t, o))| (t, o)).collect() - }; - for (texture, is_output) in leftovers { - if !is_output && !texture.is_null() { - let mut t = texture; - unsafe { crate::bridge::render::texture_free(&mut t) }; - } - } + let mut live = LIVE_TEXTURES.lock().unwrap_or_else(|e| e.into_inner()); + live.retain(|_, (_, _, is_output)| *is_output); } /// 公共入口模板:panic 兜底。 @@ -246,10 +240,12 @@ fn clip_string(clip: &ClipInstance, name: &str, default: &str) -> String { } /// 构造纹理属性集(ofxGPURender.h 规定的属性;输入与输出共用, -/// 只是数据来源不同)。 +/// 只是数据来源不同)。`OpenGLTextureIndex` 经 +/// [`crate::render::texture_id`](桩恒 0——wgpu 后端无 GL 命名空间; +/// CPU 回退下插件按规范回退,纹理内容仍可经 clipGetImage 取用)。 #[allow(clippy::too_many_arguments)] fn make_texture_props( - texture: crate::bridge::render::TextureHandle, + texture: &crate::render::Texture, width: f64, height: f64, components: crate::image::Components, @@ -262,7 +258,7 @@ fn make_texture_props( let props = PropertySet::new(); props.set_one( GL_TEXTURE_INDEX, - Value::Int(unsafe { crate::bridge::render::texture_id(texture) }), + Value::Int(crate::render::texture_id(texture)), ); props.set_one(GL_TEXTURE_TARGET, Value::Int(GL_TEXTURE_2D)); props.set_one( @@ -342,16 +338,13 @@ unsafe extern "C" fn clip_load_texture( if c.name == "Output" { // Output:返回已附着的输出纹理(format 忽略;渲染目标 // 绑定由调用方契约保证——等价 C++ attach_output_texture)。 - let tex = gl.output_texture; - if tex.is_null() { - return Err(status::ERR_BAD_HANDLE); - } + let tex = gl.output_texture.clone(); let (w, h) = texture_size(&tex); if w <= 0.0 || h <= 0.0 { return Err(status::ERR_BAD_HANDLE); } let props = make_texture_props( - tex, + &tex, w, h, crate::image::Components::Rgba, @@ -393,25 +386,16 @@ unsafe extern "C" fn clip_load_texture( if w <= 0.0 || h <= 0.0 { return Err(status::FAILED); } - let params = crate::bridge::render::VideoParams { + let params = crate::render::VideoParams { width: w as i32, height: h as i32, - format: crate::bridge::render::PIXEL_FORMAT_F32, + format: crate::render::PIXEL_FORMAT_F32, ..Default::default() }; - let tex = unsafe { - crate::bridge::render::texture_create( - gl.renderer, - ¶ms, - image.pixels().as_ptr() as *const c_void, - image.row_bytes() as i32, - ) - }; - if tex.is_null() { - return Err(status::ERR_MEMORY); - } + let tex = crate::render::texture_create(¶ms, image.pixels(), image.row_bytes() as i32) + .map_err(|_| status::ERR_MEMORY)?; let props = make_texture_props( - tex, + &tex, w, h, components, @@ -427,10 +411,10 @@ unsafe extern "C" fn clip_load_texture( }) } -/// 纹理尺寸(经 texture_get_params;失败回退 0,0)。 -fn texture_size(tex: &crate::bridge::render::TextureHandle) -> (f64, f64) { - let mut p = crate::bridge::render::VideoParams::default(); - if unsafe { crate::bridge::render::texture_get_params(*tex, &mut p) } == 0 && p.width > 0 { +/// 纹理尺寸(经 [`crate::render::texture_get_params`];失败回退 0,0)。 +fn texture_size(tex: &crate::render::Texture) -> (f64, f64) { + let p = crate::render::texture_get_params(tex); + if p.width > 0 { (p.width as f64, p.height as f64) } else { (0.0, 0.0) @@ -456,8 +440,9 @@ fn clip_par(c: &ClipInstance) -> f64 { .unwrap_or(1.0) } -/// clipFreeTexture:释放纹理句柄(输入 clip 删除 GL 纹理;Output 只 -/// 释放句柄不删纹理——宿主还要读它)。 +/// clipFreeTexture:释放纹理(输入 clip 删除纹理值;Output 只释放 +/// 句柄不删纹理——宿主还要读它)。纹理是值:摘除表条目即 drop +/// (原 `texture_free` 调用面随值模型删除)。 unsafe extern "C" fn clip_free_texture(texture_handle: *mut c_void) -> c_int { caught(|| { if texture_handle.is_null() { @@ -469,13 +454,7 @@ unsafe extern "C" fn clip_free_texture(texture_handle: *mut c_void) -> c_int { live.remove(&addr) }; match entry { - Some((_props, texture, is_output)) => { - if !is_output && !texture.is_null() { - let mut t = texture; - unsafe { crate::bridge::render::texture_free(&mut t) }; - } - Ok(()) - } + Some((_props, _texture, _is_output)) => Ok(()), None => Err(status::ERR_BAD_HANDLE), } }) diff --git a/crates/oakplugin/src/suites/image_effect.rs b/crates/oakplugin/src/suites/image_effect.rs index 5923367d2..cd48c18a0 100644 --- a/crates/oakplugin/src/suites/image_effect.rs +++ b/crates/oakplugin/src/suites/image_effect.rs @@ -29,7 +29,7 @@ //! 语义,第 1 期账本不需要 → OK no-op,见 memory.rs 文档)。 //! //! `// TODO(clip)`:clipGetImage 依赖 [`crate::clip::ClipInstance::fetch_image`] -//! (bridge::render 帧访问 C ABI 未冻结),代码已齐、运行时待 clip.rs。 +//! (oakrender 帧访问随单库化改为本地桩,见 [`crate::render`]),代码已齐、运行时待 clip.rs。 use std::collections::HashMap; use std::ffi::{c_char, c_double, c_int, c_void, CStr}; @@ -258,7 +258,8 @@ unsafe extern "C" fn clip_get_property_set(clip: *mut c_void, out: *mut *mut c_v /// clipGetImage:抓取输入图像并登记到存活表,返回图像属性集 handle /// (HS:2003-2049;`getImage` 失败 → Failed)。 /// -/// `// TODO(clip)`:fetch_image 待 bridge::render 帧访问 C ABI。 +/// `// TODO(clip)`:fetch_image 待 clip 迁移到 +/// `oakrender::texture::Texture` 值模型(当前帧访问为本地桩)。 unsafe extern "C" fn clip_get_image( clip: *mut c_void, time: c_double, diff --git a/crates/oakplugin/src/suites/message.rs b/crates/oakplugin/src/suites/message.rs index 0511525c6..604ff1697 100644 --- a/crates/oakplugin/src/suites/message.rs +++ b/crates/oakplugin/src/suites/message.rs @@ -23,10 +23,8 @@ //! 到本模块的 `oak_ofx_message_impl`。 //! //! 消息出口 = facade 注册的 `oakplugin_message_fn` -//! (include/plugin/host.h)。注意:骨架 ffi.rs 声明的 `MessageFn` -//! 与头文件不符(头文件是 `(type, message, userdata)`,骨架是 -//! `(userdata, level, message)`)——做 ffi.rs 时以头文件为准修正。 -//! 本模块按头文件契约建模。 +//! (include/plugin/host.h)。本模块按头文件契约建模。 +//! (原 C ABI 出口层已随单库化删除;注册点由 facade 直接调用。) use std::ffi::{c_char, c_int, c_void, CStr}; @@ -37,14 +35,12 @@ use crate::suites::status; pub(crate) type MessageHandler = unsafe extern "C" fn(*const c_char, *const c_char, *mut c_void) -> c_int; -/// 消息出口注册表(ffi 层经 `oakplugin_host_set_message_handler` -/// 写入,suite 读;userdata 以 usize 存,避免裸指针破坏 static 的 -/// Send/Sync 推导)。 +/// 消息出口注册表(facade 写入,suite 读;userdata 以 usize 存, +/// 避免裸指针破坏 static 的 Send/Sync 推导)。 static HANDLER: std::sync::Mutex<(Option, usize)> = std::sync::Mutex::new((None, 0)); -/// 注册/注销消息出口(ffi 层 `oakplugin_host_set_message_handler` -/// 调用;公开:测试直接注入捕获器)。 +/// 注册/注销消息出口(facade 调用;公开:测试直接注入捕获器)。 pub fn set_handler(f: Option, userdata: *mut c_void) { let mut h = HANDLER.lock().unwrap_or_else(|e| e.into_inner()); *h = (f, userdata as usize); diff --git a/crates/oakplugin/src/suites/mod.rs b/crates/oakplugin/src/suites/mod.rs index 0ee5f1da6..836601a22 100644 --- a/crates/oakplugin/src/suites/mod.rs +++ b/crates/oakplugin/src/suites/mod.rs @@ -171,13 +171,14 @@ pub(crate) fn current_output() -> Option> { /// 纹理(对应 C++ 里实例渲染期的 GL 状态;ofxGPURender.h /// "OpenGL Current Context" 一节要求宿主在 Render 期间持有 GL /// 上下文——本设计的约定是调用方(oakrender 的 PluginJob 路径)在 -/// 进入 render_job 前已把渲染器上下文置为 current,本表只传递句柄)。 -#[derive(Clone, Copy)] +/// 进入 render_job 前已把渲染器上下文置为 current,本表只传递 +/// 渲染器引用与输出纹理值)。 +#[derive(Clone)] pub struct GlCtx { - /// 当前渲染器(oakrender 句柄)。 - pub renderer: crate::bridge::render::RendererHandle, + /// 当前渲染器(oakrender 后端上下文)。 + pub renderer: crate::render::Renderer, /// 已附着的输出纹理(渲染目标;GL 模式下插件把结果画进它)。 - pub output_texture: crate::bridge::render::TextureHandle, + pub output_texture: crate::render::Texture, /// 当前 GL 纹理的实际像素深度(kOfxBitDepth* 静态串;Phase 2 /// 全链路 F32,由 render_gl 按插件 kOfxOpenGLPropPixelDepth 协商 /// 后填入——纹理句柄的 kOfxImageEffectPropPixelDepth 以它为准)。 @@ -197,7 +198,7 @@ pub fn set_gl_ctx(ctx: Option) { /// 读取 GL 渲染上下文(无上下文返回 None——clipLoadTexture 在非 /// GL 渲染期调用时按规范返回 kOfxStatErrMissingHostFeature)。 pub(crate) fn gl_ctx() -> Option { - GL_CTX.with(|c| *c.borrow()) + GL_CTX.with(|c| c.borrow().clone()) } /// 宿主进程身份(fetchSuite 的 version 检查用;= OFX API 1.5)。 @@ -307,16 +308,45 @@ mod tests { #[test] fn gl_ctx_tls() { assert!(gl_ctx().is_none()); - let renderer = crate::handle::CHandle::null(); - let tex = crate::handle::CHandle::null(); + // 最小 GpuContextLike 假实现(无 GPU 适配器需求)。 + struct FakeGpu; + impl oakrender::backend::GpuContextLike for FakeGpu { + fn kind(&self) -> oakrender::backend::BackendKind { + oakrender::backend::BackendKind::Cpu + } + fn destroy_texture(&self, _token: u64) {} + fn upload( + &self, + _token: u64, + _frame: &oakrender::texture::Frame, + ) -> oakrender::error::Result<()> { + Ok(()) + } + fn download(&self, _token: u64) -> oakrender::error::Result { + Ok(oakrender::texture::Frame::new()) + } + fn blit( + &self, + _src: u64, + _dst: u64, + _processor: Option<&oakrender::color::ColorProcessor>, + ) -> oakrender::error::Result<()> { + Ok(()) + } + } + let renderer: crate::render::Renderer = std::sync::Arc::new(FakeGpu); + let tex = crate::render::Texture::dummy(); set_gl_ctx(Some(GlCtx { renderer, - output_texture: tex, + output_texture: tex.clone(), gl_pixel_depth: "OfxBitDepthFloat", })); let got = gl_ctx().unwrap(); - assert!(got.renderer.is_null()); - assert!(got.output_texture.is_null()); + assert_eq!( + got.renderer.kind(), + oakrender::backend::BackendKind::Cpu + ); + assert!(got.output_texture.is_dummy()); assert_eq!(got.gl_pixel_depth, "OfxBitDepthFloat"); set_gl_ctx(None); assert!(gl_ctx().is_none()); diff --git a/crates/oakplugin/tests/bridge_test.rs b/crates/oakplugin/tests/bridge_test.rs deleted file mode 100644 index 1893edd3f..000000000 --- a/crates/oakplugin/tests/bridge_test.rs +++ /dev/null @@ -1,576 +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 . - -//! bridge 层测试:param↔oaknode、clip↔oakrender、undo 打包。 -//! -//! 依赖 liboaknode/liboakrender/liboakundo 的用例(standalone 树 -//! 环境,外层 ctest 驱动)在 cargo 内以库内桩替代 -//! (`--features test-stubs`):桥调用面一致,桩提供节点值/命令/ -//! 帧状态访问器(见 `bridge::{node,render,undo}::stub`)。未启用 -//! 桩时同用例走 dlsym 缺失的降级路径(可解释错误 / no-op)。 -//! -//! 系统级冒烟:[`system_misc_ofx_bundle_smoke`] 在 -//! `/Library/OFX/Plugins/Misc.ofx.bundle` 存在时扫描真实 OFX 插件 -//! 并 create_instance(absent 时 skip)。 - -mod common; - -use std::ffi::{c_int, c_void, CString}; - -use oakplugin::clip::ClipInstance; -use oakplugin::descriptor::ClipDescriptor; -use oakplugin::handle::CHandle; -use oakplugin::image::{BitDepth, Components, Image}; -use oakplugin::instance::{Instance, OfxRectD, RenderScale}; -use oakplugin::param::ParamInstance; -use oakplugin::property::PropertySet; -use oakplugin::suites::param::suite_v1 as param_suite; -use oakplugin::suites::tag; - -const OK: c_int = 0; -const TEST_PLUGIN_ID: &str = "org.oak.test-plugin"; - -fn cs(s: &str) -> CString { - CString::new(s).unwrap() -} - -/// 简易纹理句柄(桩 ctx 约定,见 bridge::render::stub:src=0xA2、 -/// dst=0xA1)。 -fn fake_texture(ctx: usize) -> CHandle { - CHandle { - ctx: ctx as *mut c_void, - addref: None, - release: None, - abi_version: 1, - } -} - -/// 用 param suite 的 paramDefine 造一个指定 OFX 类型的参数实例 -/// (describe 产物 → createInstance)。 -fn make_param(ofx_type: &str) -> ParamInstance { - let mut desc = oakplugin::descriptor::EffectDescriptor::new(); - let s = param_suite(); - let dhandle = tag::make(&desc.props as *const PropertySet, tag::DESCRIPTOR); - let t = cs(ofx_type); - let n = cs("p"); - let mut ph: *mut c_void = std::ptr::null_mut(); - let r = unsafe { (s.param_define)(dhandle, t.as_ptr(), n.as_ptr(), &mut ph) }; - assert_eq!(r, OK, "paramDefine {ofx_type} 失败: {r}"); - ParamInstance::from_def(*desc.params.pop().expect("刚 define 的参数")) -} - -/// 简易 clip 实例。 -fn make_clip(name: &str) -> ClipInstance { - ClipInstance::from_descriptor(&ClipDescriptor { - props: PropertySet::new(), - name: name.into(), - }) -} - -/// 扫描测试插件并创建实例,绑定 `identity`(0 = 不绑定),返回 -/// (实例, gain 参数句柄)。插件不可用时 skip 并返回 None。 -fn bound_inst( - identity: usize, -) -> Option<( - std::sync::Arc>, - *mut c_void, -)> { - if common::test_plugin_scan_dir().is_none() { - common::skip("最小测试插件未构建"); - return None; - } - let dir = cs(common::test_plugin_scan_dir().unwrap().to_str().unwrap()); - let dirs = [dir.as_ptr()]; - unsafe { oakplugin::ffi::oakplugin_host_scan(dirs.as_ptr(), 1) }; - let inst = oakplugin::host::Host::global() - .create_instance(TEST_PLUGIN_ID, None) - .ok()?; - inst.value.bind_node(identity); - let ih = tag::make(&inst.value.props as *const PropertySet, tag::INSTANCE); - let name = cs("gain"); - let mut ph: *mut c_void = std::ptr::null_mut(); - let s = param_suite(); - let r = unsafe { (s.param_get_handle)(ih, name.as_ptr(), &mut ph, std::ptr::null_mut()) }; - assert_eq!(r, OK, "paramGetHandle(gain) 失败: {r}"); - Some((inst, ph)) -} - -/// 节点→插件:oaknode 输入值变化经桥写入 OFX 参数(类型映射表 -/// 逐行:int/float/bool/color/vec2/vec3/combo/str_combo)。 -#[test] -fn node_to_param_type_mapping() { - use oakplugin::bridge::node::{node_value_type as T, Value}; - - // Integer ← INT(num 载荷)。 - let p = make_param(oakplugin::param::TYPE_INTEGER); - let mut v = Value::default(); - v.r#type = T::INT; - v.num = 7; - p.set_from_node(&v); - assert_eq!(p.get(), oakplugin::param::ParamValue::Int([7, 0, 0], 1)); - - // Double ← FLOAT(f[0] 载荷)。 - let p = make_param(oakplugin::param::TYPE_DOUBLE); - p.set_from_node(&Value::float(1.5)); - assert_eq!( - p.get(), - oakplugin::param::ParamValue::Double([1.5, 0.0, 0.0], 1) - ); - - // Boolean ← BOOL(num 0/1)。 - let p = make_param(oakplugin::param::TYPE_BOOLEAN); - p.set_from_node(&Value::bool_(true)); - assert_eq!(p.get(), oakplugin::param::ParamValue::Bool(true)); - - // Choice ← COMBO。 - let p = make_param(oakplugin::param::TYPE_CHOICE); - p.set_from_node(&Value::combo(2)); - assert_eq!(p.get(), oakplugin::param::ParamValue::Choice(2)); - - // RGBA ← COLOR(r,g,b,a)。 - let p = make_param(oakplugin::param::TYPE_RGBA); - p.set_from_node(&Value::color(0.1, 0.2, 0.3, 0.9)); - assert_eq!( - p.get(), - oakplugin::param::ParamValue::Color([0.1, 0.2, 0.3, 0.9], 4) - ); - - // RGB ← COLOR(alpha 忽略,恒 0 占位)。 - let p = make_param(oakplugin::param::TYPE_RGB); - p.set_from_node(&Value::color(0.1, 0.2, 0.3, 0.5)); - assert_eq!( - p.get(), - oakplugin::param::ParamValue::Color([0.1, 0.2, 0.3, 0.0], 3) - ); - - // Double2D ← VEC2;Double3D ← VEC3。 - let p = make_param(oakplugin::param::TYPE_DOUBLE2D); - p.set_from_node(&Value::vec(&[1.0, 2.0])); - assert_eq!( - p.get(), - oakplugin::param::ParamValue::Double([1.0, 2.0, 0.0], 2) - ); - let p = make_param(oakplugin::param::TYPE_DOUBLE3D); - p.set_from_node(&Value::vec(&[1.0, 2.0, 3.0])); - assert_eq!( - p.get(), - oakplugin::param::ParamValue::Double([1.0, 2.0, 3.0], 3) - ); - - // Integer2D ← VEC2 / Integer3D ← VEC3(浮点截断为 int)。 - let p = make_param(oakplugin::param::TYPE_INTEGER2D); - p.set_from_node(&Value::vec(&[1.5, 2.5])); - assert_eq!(p.get(), oakplugin::param::ParamValue::Int([1, 2, 0], 2)); - let p = make_param(oakplugin::param::TYPE_INTEGER3D); - p.set_from_node(&Value::vec(&[1.5, 2.5, 3.5])); - assert_eq!(p.get(), oakplugin::param::ParamValue::Int([1, 2, 3], 3)); - - // String 参数 + STRING 节点类型:POD 不携带数据 → 值不变 - // (保持默认空串;字符串值走 facade 的字符串 API)。 - let p = make_param(oakplugin::param::TYPE_STRING); - p.set_from_node(&Value::string()); - assert!(matches!(p.get(), oakplugin::param::ParamValue::String(s) if s.to_bytes().is_empty())); - - // 类型不匹配 → 忽略(保持现值)。 - let p = make_param(oakplugin::param::TYPE_DOUBLE); - p.set_ofx(oakplugin::param::ParamValue::Double([5.0, 0.0, 0.0], 1)); - p.set_from_node(&Value::int(9)); // INT 不是 FLOAT - assert_eq!( - p.get(), - oakplugin::param::ParamValue::Double([5.0, 0.0, 0.0], 1) - ); -} - -/// 插件→节点:paramSetValue(插件自改)经 instanceChanged 桥回写 -/// oaknode,且打包为一条 undo 命令(undo/redo 后值正确)。 -#[test] -fn param_to_node_undoable_writeback() { - common::with_host(|| { - let Some((inst, gain_h)) = bound_inst(42) else { - return; - }; - let s = param_suite(); - - #[cfg(feature = "test-stubs")] - { - use oakplugin::bridge::{node, undo}; - node::stub::reset(); - undo::stub::reset(); - node::stub::register_node(42); - node::stub::set_input(42, "gain", node::Value::float(0.5)); - - // 插件自改 → 回写 + 单命令立即 redo。 - let r = unsafe { (s.param_set_value)(gain_h, 0.75) }; - assert_eq!(r, OK, "paramSetValue 失败: {r}"); - let cur = node::stub::input(42, "gain").expect("已回写节点输入"); - assert_eq!(cur.value.f[0], 0.75); - - // undoable:命令捕获 prev/next 且已应用(redo 语义)。 - let rec = undo::stub::last_command().expect("回写产生命令"); - assert_eq!(rec.node, 42); - assert_eq!(rec.input, "gain"); - assert!(!rec.is_multi); - assert!(rec.applied); - assert_eq!(rec.prev.f[0], 0.5, "prev = 修改前节点值"); - assert_eq!(rec.next.f[0], 0.75, "next = 修改后值"); - - // undo → 恢复旧值;redo → 再应用。 - undo::stub::undo(rec.id); - assert_eq!(node::stub::input(42, "gain").unwrap().value.f[0], 0.5); - undo::stub::redo(rec.id); - assert_eq!(node::stub::input(42, "gain").unwrap().value.f[0], 0.75); - - // 字符串参数:POD 无数据 → 经字符串桥回写。 - node::stub::set_input_string(42, "label", "old"); - let name = cs("label"); - let mut lh: *mut c_void = std::ptr::null_mut(); - let ih = tag::make(&inst.value.props as *const PropertySet, tag::INSTANCE); - assert_eq!( - unsafe { (s.param_get_handle)(ih, name.as_ptr(), &mut lh, std::ptr::null_mut()) }, - OK - ); - let hello = cs("hello"); - assert_eq!(unsafe { (s.param_set_value)(lh, hello.as_ptr()) }, OK); - let cur = node::stub::input(42, "label").expect("字符串已回写"); - assert_eq!(cur.string.as_deref(), Some("hello")); - } - - #[cfg(not(feature = "test-stubs"))] - { - // 无 liboaknode:桥符号缺失 → node_from_identity 空句柄, - // 回写 no-op;paramSetValue 仍成功且值本地生效。 - let r = unsafe { (s.param_set_value)(gain_h, 0.75) }; - assert_eq!(r, OK); - let mut out = 0.0; - assert_eq!(unsafe { (s.param_get_value)(gain_h, &mut out) }, OK); - assert_eq!(out, 0.75); - } - }); -} - -/// 未绑定节点的实例收到 instanceChanged:no-op 不崩(身份注册表 -/// 查无此项的路径)。 -#[test] -fn writeback_without_bound_node_is_noop() { - common::with_host(|| { - let Some((inst, gain_h)) = bound_inst(0) else { - return; - }; - let s = param_suite(); - - // identity 0(未绑定):回写直接短路,值只写本地参数。 - assert_eq!(unsafe { (s.param_set_value)(gain_h, 0.5) }, OK); - let mut out = 0.0; - assert_eq!(unsafe { (s.param_get_value)(gain_h, &mut out) }, OK); - assert_eq!(out, 0.5); - - #[cfg(feature = "test-stubs")] - { - use oakplugin::bridge::{node, undo}; - node::stub::reset(); - undo::stub::reset(); - node::stub::register_node(9); - - // 已绑定但身份在注册表查无 → node_from_identity 空句柄。 - inst.value.bind_node(9999); - assert_eq!(unsafe { (s.param_set_value)(gain_h, 0.6) }, OK); - assert!( - undo::stub::records().is_empty(), - "身份查无不应产生命令:{:?}", - undo::stub::records() - ); - - // 已登记身份但输入名不存在 → 桥返回错误,同样 no-op。 - inst.value.bind_node(9); - assert_eq!(unsafe { (s.param_set_value)(gain_h, 0.7) }, OK); - assert!(undo::stub::records().is_empty(), "输入不存在不应产生命令"); - - // 节点输入值保持未回写(本地参数值仍有效)。 - let mut out = 0.0; - assert_eq!(unsafe { (s.param_get_value)(gain_h, &mut out) }, OK); - assert_eq!(out, 0.7); - } - }); -} - -/// clip 输入:oakrender 纹理挂接后 fetch_image 读回的像素与 -/// 纹理内容一致(F32 格式不丢精度)。 -#[test] -fn clip_texture_roundtrip_f32() { - let clip = make_clip("Source"); - let scale = RenderScale { x: 1.0, y: 1.0 }; - - // 未挂纹理 → NotFound(两模式一致)。 - assert!(matches!( - clip.fetch_image(0.0, scale, None), - Err(oakplugin::error::Error::NotFound) - )); - // 子区域第 1 期不支持 → Failed。 - assert!(matches!( - clip.fetch_image( - 0.0, - scale, - Some(OfxRectD { - x1: 0.0, - y1: 0.0, - x2: 2.0, - y2: 2.0 - }) - ), - Err(oakplugin::error::Error::Failed(_)) - )); - - #[cfg(feature = "test-stubs")] - { - use oakplugin::bridge::render::{self, PIXEL_FORMAT_F32, PIXEL_FORMAT_U8}; - render::stub::reset(); - - // 构造可识别的 F32 RGBA 帧(r=行, g=列, b=0, a=1)。 - let (w, h) = (8usize, 4usize); - let mut pixels = Vec::with_capacity(w * h * 16); - for y in 0..h { - for x in 0..w { - for v in [y as f32, x as f32, 0.0, 1.0] { - pixels.extend_from_slice(&v.to_le_bytes()); - } - } - } - render::stub::setup_src(w as i32, h as i32, PIXEL_FORMAT_F32, pixels.clone()); - let tex = fake_texture(0xA2); - clip.set_input_texture(tex, 0.0); - let img = clip - .fetch_image(0.0, scale, None) - .expect("fetch_image 成功"); - assert_eq!(img.pixels(), &pixels, "F32 像素 round-trip 不丢精度"); - assert_eq!(img.components(), Components::Rgba); - assert_eq!(img.depth(), BitDepth::Float); - - // dummy 纹理输入 → 空输入(NotFound)。 - render::stub::set_dummy(0xA2, true); - assert!(matches!( - clip.fetch_image(0.0, scale, None), - Err(oakplugin::error::Error::NotFound) - )); - render::stub::set_dummy(0xA2, false); - - // 非 F32 输入帧 → 明确失败。 - render::stub::setup_src(w as i32, h as i32, PIXEL_FORMAT_U8, vec![0u8; w * h * 4]); - assert!(matches!( - clip.fetch_image(0.0, scale, None), - Err(oakplugin::error::Error::Failed(_)) - )); - } -} - -/// 输出:store_output_image 产出的纹理在 oakrender 侧可读且 -/// 像素一致;dummy 纹理输入按空输入处理。 -#[test] -fn output_image_to_texture() { - let clip = make_clip("Output"); - let img = Image::allocate( - BitDepth::Float, - Components::Rgba, - OfxRectD { - x1: 0.0, - y1: 0.0, - x2: 8.0, - y2: 4.0, - }, - ); - - // 未挂输出纹理 → NotFound(两模式一致)。 - assert!(matches!( - clip.store_output_image(&img), - Err(oakplugin::error::Error::NotFound) - )); - - #[cfg(feature = "test-stubs")] - { - use oakplugin::bridge::render::{self, PIXEL_FORMAT_F32, PIXEL_FORMAT_U8}; - render::stub::reset(); - render::stub::setup_dst(8, 4, PIXEL_FORMAT_F32); - let tex = fake_texture(0xA1); - clip.set_output_texture(tex, 0.0); - - // 填非平凡像素后回写 → oakrender 侧读到的与图像一致。 - let mut img = img; - for (i, b) in img.pixels_mut().iter_mut().enumerate() { - *b = (i % 256) as u8; - } - let tex2 = clip - .store_output_image(&img) - .expect("store_output_image 成功"); - assert_eq!(tex2.ctx, tex.ctx, "返回挂入的纹理句柄"); - assert_eq!(render::stub::dst_pixels(), img.pixels(), "输出像素一致"); - - // dummy 输出纹理 → NotFound。 - render::stub::set_dummy(0xA1, true); - assert!(matches!( - clip.store_output_image(&img), - Err(oakplugin::error::Error::NotFound) - )); - render::stub::set_dummy(0xA1, false); - - // 帧尺寸与图像不一致 → 明确失败。 - render::stub::setup_dst(16, 4, PIXEL_FORMAT_F32); - assert!(matches!( - clip.store_output_image(&img), - Err(oakplugin::error::Error::Failed(_)) - )); - - // 非 F32 输出帧 → 明确失败。 - render::stub::setup_dst(8, 4, PIXEL_FORMAT_U8); - assert!(matches!( - clip.store_output_image(&img), - Err(oakplugin::error::Error::Failed(_)) - )); - } -} - -/// undo 打包:一次编辑事务(paramEditBegin/End)内的多次参数 -/// 修改合并为一条命令。 -#[test] -fn edit_transaction_coalescing() { - common::with_host(|| { - let Some((_inst, gain_h)) = bound_inst(42) else { - return; - }; - let s = param_suite(); - - #[cfg(feature = "test-stubs")] - { - use oakplugin::bridge::{node, undo}; - node::stub::reset(); - undo::stub::reset(); - node::stub::register_node(42); - node::stub::set_input(42, "gain", node::Value::float(0.0)); - - // 编辑事务内两次回写 → 一条 multi。 - assert_eq!(unsafe { (s.param_edit_begin)(gain_h) }, OK); - assert_eq!(unsafe { (s.param_set_value)(gain_h, 0.25) }, OK); - assert_eq!(unsafe { (s.param_set_value)(gain_h, 0.5) }, OK); - assert_eq!(unsafe { (s.param_edit_end)(gain_h) }, OK); - - let recs = undo::stub::records(); - let multi = recs.iter().find(|r| r.is_multi).expect("存在 multi 命令"); - assert_eq!(multi.children.len(), 2, "两次修改合并为一条 multi"); - for c in &multi.children { - let child = recs.iter().find(|r| r.id == *c).expect("子命令在表"); - assert!(!child.is_multi); - assert!(child.applied, "子命令已 redo 生效"); - assert_eq!(child.node, 42); - assert_eq!(child.input, "gain"); - } - // 值即时生效(事务内子命令 redo;editEnd 的 multi redo 幂等)。 - assert_eq!(node::stub::input(42, "gain").unwrap().value.f[0], 0.5); - - // 事务外回写 → 独立单命令(不并入 multi)。 - let before = undo::stub::records().len(); - assert_eq!(unsafe { (s.param_set_value)(gain_h, 0.75) }, OK); - let after = undo::stub::records().len(); - assert_eq!(after, before + 1, "事务外每次回写一条独立命令"); - let last = undo::stub::last_command().unwrap(); - assert!(!last.is_multi); - assert_eq!(node::stub::input(42, "gain").unwrap().value.f[0], 0.75); - - // 嵌套 editBegin/End:内层结束不提交(multi 保留未 free), - // 最外层结束才提交(redo + free)。 - assert_eq!(unsafe { (s.param_edit_begin)(gain_h) }, OK); - assert_eq!(unsafe { (s.param_edit_begin)(gain_h) }, OK); - assert_eq!(unsafe { (s.param_set_value)(gain_h, 1.0) }, OK); - assert_eq!(unsafe { (s.param_edit_end)(gain_h) }, OK); - let newest_multi = || { - undo::stub::records() - .iter() - .filter(|r| r.is_multi) - .max_by_key(|r| r.id) - .cloned() - .expect("存在 multi") - }; - let m = newest_multi(); - assert_eq!(m.children.len(), 1, "事务内回写并入新 multi"); - assert!(!m.freed, "内层 editEnd 不提交"); - assert_eq!(unsafe { (s.param_edit_end)(gain_h) }, OK); - assert!(newest_multi().freed, "最外层 editEnd 提交(redo+free)"); - assert_eq!(node::stub::input(42, "gain").unwrap().value.f[0], 1.0); - } - - #[cfg(not(feature = "test-stubs"))] - { - // 无桥:editBegin/End 与回写 no-op;参数值本地生效。 - assert_eq!(unsafe { (s.param_edit_begin)(gain_h) }, OK); - assert_eq!(unsafe { (s.param_set_value)(gain_h, 0.25) }, OK); - assert_eq!(unsafe { (s.param_edit_end)(gain_h) }, OK); - let mut out = 0.0; - assert_eq!(unsafe { (s.param_get_value)(gain_h, &mut out) }, OK); - assert_eq!(out, 0.25); - } - }); -} - -/// 系统级冒烟:真实 OFX 插件 bundle(/Library/OFX/Plugins/ -/// Misc.ofx.bundle,本机安装的标准 openfx-misc)在存在时扫描 → -/// describe → create_instance → 协商;absent 时 skip。 -/// -/// 这是 0 期 golden master 的轻量替代(0 期基建可能尚未落地——见 -/// README「与 M11 §3.5 的对照」):只验"能加载、能建实例、协商 -/// 有结果",不比对快照。 -#[test] -fn system_misc_ofx_bundle_smoke() { - let bundle = std::path::Path::new("/Library/OFX/Plugins/Misc.ofx.bundle"); - if !bundle.is_dir() { - common::skip("/Library/OFX/Plugins/Misc.ofx.bundle 不存在"); - return; - } - common::with_host(|| { - use oakplugin::host::Host; - let host = Host::global(); - // 只扫 Misc:临时目录内建软链(scan_path 会 canonicalize)。 - let tmp = std::env::temp_dir().join(format!("oak-misc-scan-{}", std::process::id())); - let link = tmp.join("Misc.ofx.bundle"); - if !link.exists() { - std::fs::create_dir_all(&tmp).ok(); - std::os::unix::fs::symlink(bundle, &link).expect("建软链"); - } - host.cache.scan_path(&tmp).expect("扫描 Misc.ofx.bundle"); - std::fs::remove_dir_all(&tmp).ok(); - - // bundle 内的插件已 describe 入缓存。 - let ids: Vec = (0..host.cache.count()) - .filter_map(|i| host.cache.at(i).map(|p| p.identifier.clone())) - .collect(); - if ids.is_empty() { - // 本机 bundle 若为 Natron 分支构建(describe 返回 - // MissingHostFeature),扫描必然为空——README 已记录该 - // 不兼容;兼容 bundle 的机器上此处照常断言。 - common::skip("/Library/OFX/Plugins/Misc.ofx.bundle 为 Natron 分支构建(describe 返回 MissingHostFeature)或空 bundle"); - return; - } - println!("Misc.ofx.bundle 扫描到 {} 个插件:{ids:?}", ids.len()); - - // create_instance 冒烟(首个插件的首选上下文)。 - let first = host.cache.at(0).expect("首个插件").identifier.clone(); - let inst = host.create_instance(&first, None); - assert!(inst.is_ok(), "create_instance({first}) 应成功"); - // 协商冒烟(输出 clip 的分量/位深/帧率应返回)。 - let inst = inst.unwrap(); - let prefs = inst.value.get_clip_preferences(); - assert!( - prefs.is_ok(), - "getClipPreferences({first}) 应成功: {prefs:?}" - ); - }); -} diff --git a/crates/oakplugin/tests/colour_test.rs b/crates/oakplugin/tests/colour_test.rs index 5429a53eb..84273415d 100644 --- a/crates/oakplugin/tests/colour_test.rs +++ b/crates/oakplugin/tests/colour_test.rs @@ -17,39 +17,31 @@ //! ofxColour(M11 §4):宿主 OCIO 能力宣告、实例协商属性、输入 clip //! 工作空间(ACEScg)、GetOutputColourspace 往返(偏好采纳 + //! 交叉引用解析 + 输出写回)。 +//! +//! 单库化后经 Rust API 直连(`Host::global().create_instance`),不再 +//! 经已删除的 `oakplugin_host_scan`/`oakplugin_instance_create` C ABI。 mod common; -use std::ffi::{c_void, CString}; +use std::sync::Arc; -use oakplugin::ffi::{oakplugin_host_scan, oakplugin_instance_create, oakplugin_instance_free}; -use oakplugin::handle::{get, CHandle}; +use oakplugin::handle::RefBox; use oakplugin::host::Host; +use oakplugin::instance::Instance; use oakplugin::property::Value; const TEST_PLUGIN_ID: &str = "org.oak.test-plugin"; -fn cs(s: &str) -> CString { - CString::new(s).unwrap() -} - -fn scan_and_create(id: &str) -> CHandle { +/// 扫描并创建实例;不可用返回 None(skip)。 +fn scan_and_create(id: &str) -> Option>> { if common::test_plugin_scan_dir().is_none() { common::skip("最小测试插件未构建"); - return CHandle::null(); + return None; } - let dir = cs(common::test_plugin_scan_dir().unwrap().to_str().unwrap()); - let dirs = [dir.as_ptr()]; - unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) }; - let id = cs(id); - unsafe { oakplugin_instance_create(id.as_ptr()) } -} - -fn instance_of( - h: &CHandle, -) -> Option>> { - unsafe { get::>>(h) } - .cloned() + let dir = common::test_plugin_scan_dir().unwrap(); + let host = Host::global(); + host.cache.scan_path(&dir).ok()?; + host.create_instance(id, None).ok() } fn prop_str(props: &oakplugin::property::PropertySet, name: &str) -> Option { @@ -84,11 +76,9 @@ fn host_declares_ocio_capability() { #[test] fn instance_and_clip_colourspace_props() { common::with_host(|| { - let mut h = scan_and_create(TEST_PLUGIN_ID); - if h.is_null() { + let Some(inst) = scan_and_create(TEST_PLUGIN_ID) else { return; - } - let inst = instance_of(&h).expect("句柄应可解析"); + }; assert_eq!( prop_str(&inst.value.props, "OfxImageEffectPropColourManagementStyle").as_deref(), Some("OfxImageEffectColourManagementOCIO") @@ -128,7 +118,6 @@ fn instance_and_clip_colourspace_props() { .unwrap_or(true), "输出 clip 色彩空间在 GetOutputColourspace 前未设" ); - unsafe { oakplugin_instance_free(&mut h) }; }); } @@ -137,11 +126,9 @@ fn instance_and_clip_colourspace_props() { #[test] fn get_output_colourspace_cross_reference() { common::with_host(|| { - let mut h = scan_and_create(TEST_PLUGIN_ID); - if h.is_null() { + let Some(inst) = scan_and_create(TEST_PLUGIN_ID) else { return; - } - let inst = instance_of(&h).expect("句柄应可解析"); + }; let cs = inst .value .get_output_colourspace(&[]) @@ -159,7 +146,6 @@ fn get_output_colourspace_cross_reference() { Some("ACEScg"), "输出 clip 已写回解析后的色彩空间" ); - unsafe { oakplugin_instance_free(&mut h) }; }); } @@ -168,17 +154,14 @@ fn get_output_colourspace_cross_reference() { #[test] fn get_output_colourspace_preferred() { common::with_host(|| { - let mut h = scan_and_create(TEST_PLUGIN_ID); - if h.is_null() { + let Some(inst) = scan_and_create(TEST_PLUGIN_ID) else { return; - } - let inst = instance_of(&h).expect("句柄应可解析"); + }; let cs = inst .value .get_output_colourspace(&["ACEScg".to_string(), "linear".to_string()]) .expect("GetOutputColourspace 应成功"); assert_eq!(cs, "ACEScg", "偏好列表第一个被采纳"); - unsafe { oakplugin_instance_free(&mut h) }; }); } @@ -186,11 +169,9 @@ fn get_output_colourspace_preferred() { #[test] fn resolve_colourspace_edge_cases() { common::with_host(|| { - let mut h = scan_and_create(TEST_PLUGIN_ID); - if h.is_null() { + let Some(inst) = scan_and_create(TEST_PLUGIN_ID) else { return; - } - let inst = instance_of(&h).expect("句柄应可解析"); + }; assert_eq!( inst.value .resolve_colourspace("OfxColourspace_NoSuchClip".to_string()), @@ -202,6 +183,5 @@ fn resolve_colourspace_edge_cases() { "ACEScg", "非交叉引用原样返回" ); - unsafe { oakplugin_instance_free(&mut h) }; }); } diff --git a/crates/oakplugin/tests/common/mod.rs b/crates/oakplugin/tests/common/mod.rs index 1928bf6a5..fa44ca347 100644 --- a/crates/oakplugin/tests/common/mod.rs +++ b/crates/oakplugin/tests/common/mod.rs @@ -20,9 +20,8 @@ //! 运行时装配成 oak-test-plugin.ofx.bundle):filter 上下文、 //! Double 参数 gain、双 clip(Source/Output)。插件未构建时相关 //! 用例经 [`skip`] 提前返回。 -//! 测试桩在库内(bridge::render::stub,`--features test-stubs`): -//! 全链路像素路径可在 cargo test 跑通;真实链路在 standalone -//! ctest。本文档的 run 命令见 crate README。 +//! 单库化后像素路径经 oakrender 值模型(`oakrender::texture::Texture`) +//! 驱动;渲染 goldens 待该迁移落地。 use std::path::PathBuf; diff --git a/crates/oakplugin/tests/ffi_host_test.rs b/crates/oakplugin/tests/ffi_host_test.rs deleted file mode 100644 index 044ea479e..000000000 --- a/crates/oakplugin/tests/ffi_host_test.rs +++ /dev/null @@ -1,231 +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 . - -//! FFI 出口层(ffi.rs)契约测试:host.h 面。 -//! -//! 每个导出函数至少一正常一错误路径(项目铁律)。 -//! 宿主单例无锁:全部用例经 [`common::with_host`] 串行化。 -//! 需要最小测试插件(build.rs 编,common::test_plugin_dir 装配); -//! 不可用时 skip。 - -mod common; - -use std::ffi::{c_char, c_int, CStr, CString}; - -use oakplugin::ffi::{ - oakplugin_host_init, oakplugin_host_plugin_count, oakplugin_host_plugin_id_at, - oakplugin_host_plugin_label, oakplugin_host_scan, oakplugin_host_set_message_handler, - oakplugin_host_shutdown, -}; - -const E_INVALID: i32 = -90001; -const E_NOT_FOUND: i32 = -90004; -const OK: i32 = 0; - -fn cs(s: &str) -> CString { - CString::new(s).unwrap() -} - -/// 扫描测试插件;失败/缺失返回 false(调用方 skip)。 -fn scan_test_plugin() -> bool { - let Some(dir) = common::test_plugin_scan_dir() else { - common::skip("最小测试插件未构建"); - return false; - }; - let dir = cs(dir.to_str().unwrap()); - let dirs = [dir.as_ptr()]; - unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) == OK } -} - -const TEST_PLUGIN_ID: &str = "org.oak.test-plugin"; - -/// init/shutdown 幂等:重复 init 无副作用;shutdown 后再 init 可用。 -#[test] -fn host_init_shutdown_idempotent() { - common::with_host(|| { - assert_eq!(unsafe { oakplugin_host_init() }, OK); - assert_eq!(unsafe { oakplugin_host_init() }, OK); - unsafe { oakplugin_host_shutdown() }; - assert_eq!(unsafe { oakplugin_host_init() }, OK); - unsafe { oakplugin_host_shutdown() }; - }); -} - -/// scan 默认路径(NULL/0):返回 OAKPLUGIN_OK 或 E_FAILED(无插件 -/// 目录的机器),二者之外不允许别的码。 -#[test] -fn host_scan_default_paths() { - common::with_host(|| { - unsafe { oakplugin_host_init() }; - let r = unsafe { oakplugin_host_scan(std::ptr::null(), 0) }; - assert!(r == OK || r == -90003, "scan(NULL,0) = {r}"); - unsafe { oakplugin_host_shutdown() }; - }); -} - -/// scan 指定目录(测试插件所在):插件计数 ≥1 且能找到测试插件 -/// 标识;目录不存在返回 E_FAILED 而不崩。 -#[test] -fn host_scan_explicit_dir() { - common::with_host(|| { - unsafe { oakplugin_host_init() }; - assert!(scan_test_plugin(), "测试插件扫描失败"); - assert!(unsafe { oakplugin_host_plugin_count() } >= 1); - - // 标识可见(两段式第一段)。 - let len = unsafe { oakplugin_host_plugin_id_at(0, std::ptr::null_mut(), 0) }; - assert!(len > 0); - let mut buf = vec![0u8; len as usize]; - let r = unsafe { oakplugin_host_plugin_id_at(0, buf.as_mut_ptr() as *mut c_char, len) }; - assert_eq!(r, OK); - assert_eq!( - unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) } - .to_str() - .unwrap(), - TEST_PLUGIN_ID - ); - - // 目录不存在:静默跳过返回 OK 而不崩(C++ olivehost.cpp:59-62 - // 的 add_plugin_path 语义——声明原期望 E_FAILED,与参照系 - // 不符,以 C++ 行为为准)。 - let nope = cs("/nonexistent/ofx/plugins"); - let dirs = [nope.as_ptr()]; - assert_eq!(unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) }, OK); - unsafe { oakplugin_host_shutdown() }; - }); -} - -/// plugin_id_at:两段式(先问长度再取内容);越界索引返回 -/// E_NOT_FOUND;缓冲区不足时返回所需长度且不越界写。 -#[test] -fn host_plugin_id_at_two_stage() { - common::with_host(|| { - unsafe { oakplugin_host_init() }; - if !scan_test_plugin() { - unsafe { oakplugin_host_shutdown() }; - return; - } - // 越界 → E_NOT_FOUND。 - assert_eq!( - unsafe { oakplugin_host_plugin_id_at(99, std::ptr::null_mut(), 0) }, - E_NOT_FOUND - ); - - // 缓冲区不足:返回所需长度(> 0),不越界写(哨兵在 [4]—— - // [3] 是 4 字节缓冲内的 NUL 位)。 - let mut small = [0x7fu8; 5]; - let r = unsafe { oakplugin_host_plugin_id_at(0, small.as_mut_ptr() as *mut c_char, 4) }; - assert!(r > 4); - assert_eq!(small[4], 0x7f, "缓冲区外不应被写"); - unsafe { oakplugin_host_shutdown() }; - }); -} - -/// plugin_label:已知插件返回非空 label;未知标识 E_NOT_FOUND; -/// NULL 参数 E_INVALID。 -#[test] -fn host_plugin_label_paths() { - common::with_host(|| { - unsafe { oakplugin_host_init() }; - if !scan_test_plugin() { - unsafe { oakplugin_host_shutdown() }; - return; - } - let id = cs(TEST_PLUGIN_ID); - let len = unsafe { oakplugin_host_plugin_label(id.as_ptr(), std::ptr::null_mut(), 0) }; - assert!(len > 0); - let mut buf = vec![0u8; len as usize]; - assert_eq!( - unsafe { - oakplugin_host_plugin_label(id.as_ptr(), buf.as_mut_ptr() as *mut c_char, len) - }, - OK - ); - assert!(!unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) } - .to_bytes() - .is_empty()); - - // 未知标识。 - let nope = cs("org.oak.nope"); - assert_eq!( - unsafe { oakplugin_host_plugin_label(nope.as_ptr(), std::ptr::null_mut(), 0) }, - E_NOT_FOUND - ); - // NULL 参数。 - assert_eq!( - unsafe { oakplugin_host_plugin_label(std::ptr::null(), std::ptr::null_mut(), 0) }, - E_INVALID - ); - unsafe { oakplugin_host_shutdown() }; - }); -} - -/// message handler:注册后测试插件的 message 回调到达(级别与 -/// 文本透传,含 %d 格式化);注册 NULL 后消息被丢弃而不崩。 -#[test] -fn host_message_handler_dispatch() { - common::with_host(|| { - use oakplugin::ffi::{oakplugin_instance_create, oakplugin_instance_free}; - - unsafe extern "C" fn capture( - type_: *const c_char, - message: *const c_char, - userdata: *mut std::ffi::c_void, - ) -> c_int { - let v = unsafe { &mut *(userdata as *mut Vec<(String, String)>) }; - v.push(( - unsafe { CStr::from_ptr(type_) } - .to_string_lossy() - .into_owned(), - unsafe { CStr::from_ptr(message) } - .to_string_lossy() - .into_owned(), - )); - 0 - } - - unsafe { oakplugin_host_init() }; - if !scan_test_plugin() { - unsafe { oakplugin_host_shutdown() }; - return; - } - - // 注册捕获器 → 创建实例(插件在 createInstance 发 message)。 - let mut captured: Vec<(String, String)> = Vec::new(); - unsafe { - oakplugin_host_set_message_handler( - Some(capture), - &mut captured as *mut _ as *mut std::ffi::c_void, - ) - }; - let id = cs(TEST_PLUGIN_ID); - let mut h = unsafe { oakplugin_instance_create(id.as_ptr()) }; - assert!(!h.is_null()); - assert_eq!(captured.len(), 1); - assert_eq!(captured[0].0, "OfxMessageMessage"); - assert_eq!(captured[0].1, "created id=7", "v1 变长格式化应生效"); - - // 注销 → 再创建不崩、无捕获。 - unsafe { oakplugin_host_set_message_handler(None, std::ptr::null_mut()) }; - captured.clear(); - let mut h2 = unsafe { oakplugin_instance_create(id.as_ptr()) }; - assert!(!h2.is_null()); - assert!(captured.is_empty()); - unsafe { oakplugin_instance_free(&mut h2) }; - unsafe { oakplugin_instance_free(&mut h) }; - unsafe { oakplugin_host_shutdown() }; - }); -} diff --git a/crates/oakplugin/tests/ffi_instance_test.rs b/crates/oakplugin/tests/ffi_instance_test.rs deleted file mode 100644 index 297f58181..000000000 --- a/crates/oakplugin/tests/ffi_instance_test.rs +++ /dev/null @@ -1,573 +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 . - -//! FFI 出口层契约测试:instance.h 面(含 M11 §2.1 内省族)。 -//! -//! 宿主单例无锁:全部用例经 [`common::with_host`] 串行化。 -//! render 路径经 oakrender 测试桩(`--features test-stubs`); -//! 未启用时相关用例 skip。 - -mod common; - -use std::ffi::{c_char, c_int, c_void, CStr, CString}; - -use oakplugin::ffi::{ - node_value_type, oakplugin_debug_alive_count, oakplugin_instance_cancel, - oakplugin_instance_clip_count, oakplugin_instance_clip_info, oakplugin_instance_clip_name, - oakplugin_instance_create, oakplugin_instance_free, oakplugin_instance_get_param, - oakplugin_instance_get_param_string, oakplugin_instance_param_choice_count, - oakplugin_instance_param_choice_label, oakplugin_instance_param_count, - oakplugin_instance_param_default_double, oakplugin_instance_param_default_string, - oakplugin_instance_param_display_range, oakplugin_instance_param_hint, - oakplugin_instance_param_label, oakplugin_instance_param_name, oakplugin_instance_param_parent, - oakplugin_instance_param_secret, oakplugin_instance_param_type, oakplugin_instance_render, - oakplugin_instance_set_param, oakplugin_instance_set_param_string, - oakplugin_instance_set_progress_cb, OakNodeValue, -}; -use oakplugin::handle::CHandle; - -const E_INVALID: i32 = -90001; -const E_NOT_FOUND: i32 = -90004; -const OK: i32 = 0; - -fn cs(s: &str) -> CString { - CString::new(s).unwrap() -} - -const TEST_PLUGIN_ID: &str = "org.oak.test-plugin"; - -/// 扫描测试插件并创建实例;不可用返回空句柄。 -fn create_instance() -> CHandle { - if common::test_plugin_scan_dir().is_none() { - common::skip("最小测试插件未构建"); - return CHandle::null(); - } - let dir = cs(common::test_plugin_scan_dir().unwrap().to_str().unwrap()); - let dirs = [dir.as_ptr()]; - unsafe { oakplugin_host_scan_import(dirs.as_ptr(), 1) }; - let id = cs(TEST_PLUGIN_ID); - unsafe { oakplugin_instance_create(id.as_ptr()) } -} - -// 避免重复 import host_scan(ffi 模块内路径统一)。 -use oakplugin::ffi::oakplugin_host_scan as oakplugin_host_scan_import; - -/// 两段式读字符串。 -fn two_stage( - get: unsafe extern "C" fn(CHandle, c_int, *mut c_char, c_int) -> c_int, - h: CHandle, - index: c_int, -) -> Option { - let len = unsafe { get(h, index, std::ptr::null_mut(), 0) }; - if len <= 0 { - return None; - } - let mut buf = vec![0u8; len as usize]; - let r = unsafe { get(h, index, buf.as_mut_ptr() as *mut c_char, len) }; - if r != OK { - return None; - } - Some( - unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) } - .to_string_lossy() - .into_owned(), - ) -} - -/// 简易实例句柄(测试桩纹理用)。 -fn fake_texture(ctx: usize) -> CHandle { - CHandle { - ctx: ctx as *mut c_void, - addref: None, - release: None, - abi_version: 1, - } -} - -/// create/free:已知插件返回非空句柄;free 后 alive 回基线; -/// free(NULL)/free(空) no-op。 -#[test] -fn instance_create_free() { - common::with_host(|| { - unsafe { oakplugin_host_scan_import(std::ptr::null(), 0) }; - let base = unsafe { oakplugin_debug_alive_count() }; - let mut h = create_instance(); - assert!(!h.is_null()); - assert_eq!(unsafe { oakplugin_debug_alive_count() }, base + 1); - unsafe { oakplugin_instance_free(&mut h) }; - assert_eq!(unsafe { oakplugin_debug_alive_count() }, base); - // free(NULL)/free(空) no-op。 - unsafe { oakplugin_instance_free(std::ptr::null_mut()) }; - unsafe { oakplugin_instance_free(&mut h) }; - }); -} - -/// create 未知标识:返回空句柄(init 失败返回空,契约 §a)。 -#[test] -fn instance_create_unknown_id() { - common::with_host(|| { - let id = cs("org.oak.does-not-exist"); - let h = unsafe { oakplugin_instance_create(id.as_ptr()) }; - assert!(h.is_null()); - }); -} - -/// set/get_param(double):round-trip 一致;未知参数名 -/// E_NOT_FOUND;空句柄 E_INVALID;out 为 NULL 时 E_INVALID。 -#[test] -fn instance_param_double_roundtrip() { - common::with_host(|| { - let mut h = create_instance(); - if h.is_null() { - return; - } - let name = cs("gain"); - let mut v = OakNodeValue::default(); - v.r#type = node_value_type::FLOAT; - v.f = [1.25, 0.0, 0.0, 0.0]; - assert_eq!( - unsafe { oakplugin_instance_set_param(h, name.as_ptr(), &v) }, - OK - ); - let mut out = OakNodeValue::default(); - assert_eq!( - unsafe { oakplugin_instance_get_param(h, name.as_ptr(), &mut out) }, - OK - ); - assert_eq!(out.r#type, node_value_type::FLOAT); - assert_eq!(out.f[0], 1.25); - - // 未知参数名 → E_NOT_FOUND。 - let nope = cs("nope"); - assert_eq!( - unsafe { oakplugin_instance_set_param(h, nope.as_ptr(), &v) }, - E_NOT_FOUND - ); - // 空句柄 → E_INVALID。 - assert_eq!( - unsafe { oakplugin_instance_set_param(CHandle::null(), name.as_ptr(), &v) }, - E_INVALID - ); - // out NULL → E_INVALID。 - assert_eq!( - unsafe { oakplugin_instance_get_param(h, name.as_ptr(), std::ptr::null_mut()) }, - E_INVALID - ); - unsafe { oakplugin_instance_free(&mut h) }; - }); -} - -/// set/get_param_string:round-trip 一致(含空串与长串两段式)。 -#[test] -fn instance_param_string_roundtrip() { - common::with_host(|| { - let mut h = create_instance(); - if h.is_null() { - return; - } - let name = cs("label"); - let value = cs("hello 你好"); - assert_eq!( - unsafe { oakplugin_instance_set_param_string(h, name.as_ptr(), value.as_ptr()) }, - OK - ); - // 两段式。 - let len = unsafe { - oakplugin_instance_get_param_string(h, name.as_ptr(), std::ptr::null_mut(), 0) - }; - assert!(len > 0); - let mut buf = vec![0u8; len as usize]; - assert_eq!( - unsafe { - oakplugin_instance_get_param_string( - h, - name.as_ptr(), - buf.as_mut_ptr() as *mut c_char, - len, - ) - }, - OK - ); - assert_eq!( - unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }.to_string_lossy(), - "hello 你好" - ); - // 空串。 - let empty = cs(""); - assert_eq!( - unsafe { oakplugin_instance_set_param_string(h, name.as_ptr(), empty.as_ptr()) }, - OK - ); - assert_eq!( - unsafe { - oakplugin_instance_get_param_string(h, name.as_ptr(), std::ptr::null_mut(), 0) - }, - 1 - ); - unsafe { oakplugin_instance_free(&mut h) }; - }); -} - -/// render:对测试插件渲一帧,输出纹理像素符合插件的常量填充 -/// (0.5 RGBA F32,alpha=1)。time 为 NaN 时 E_INVALID。 -#[cfg(feature = "test-stubs")] -#[test] -fn instance_render_one_frame() { - common::with_host(|| { - use oakplugin::bridge::render::stub; - let mut h = create_instance(); - if h.is_null() { - return; - } - // 输出帧 320×240 F32。 - stub::setup_dst(320, 240, oakplugin::bridge::render::PIXEL_FORMAT_F32); - let dst = fake_texture(0xA1); - let src = CHandle::null(); // 无输入(插件只写常量) - assert_eq!(unsafe { oakplugin_instance_render(h, dst, src, 0.0) }, OK); - - // 断言:常量 0.5,alpha 1.0(每像素 4 通道 × 4 字节 F32)。 - let pixels = stub::dst_pixels(); - let n = pixels.len() / 16; - assert_eq!(n, 320 * 240, "像素数 = len/16 = {n}"); - for i in 0..n { - // 像素基址 = i * 16(4 通道 × 4 字节)。 - let f = |o: usize| { - f32::from_le_bytes( - pixels[i * 16 + o * 4..i * 16 + o * 4 + 4] - .try_into() - .unwrap(), - ) - }; - assert_eq!(f(0), 0.5, "pixel {i} r"); - assert_eq!(f(1), 0.5, "pixel {i} g"); - assert_eq!(f(2), 0.5, "pixel {i} b"); - assert_eq!(f(3), 1.0, "pixel {i} a"); - } - unsafe { oakplugin_instance_free(&mut h) }; - }); -} - -/// render 默认构建(真实 oakrender 链接):NULL 输出纹理在触达 -/// oakrender 之前被插件层校验拒绝(E_INVALID),不崩溃。真实纹理 -/// 路径需要 GL/GPU,属集成测试范畴。 -#[cfg(not(feature = "test-stubs"))] -#[test] -fn instance_render_one_frame() { - common::with_host(|| { - let mut h = create_instance(); - if h.is_null() { - return; - } - let dst = CHandle::null(); - assert_eq!( - unsafe { oakplugin_instance_render(h, dst, CHandle::null(), 0.0) }, - -90001 - ); - unsafe { oakplugin_instance_free(&mut h) }; - }); -} - -/// progress 回调:render 期间被调用且进度单调不减;回调返回非 0 -/// 时 render 以取消码结束(插件检查 progressUpdate 结果并中止)。 -#[cfg(feature = "test-stubs")] -#[test] -fn instance_progress_callback() { - common::with_host(|| { - use oakplugin::bridge::render::stub; - let mut h = create_instance(); - if h.is_null() { - return; - } - let mut seen: Vec = Vec::new(); - unsafe extern "C" fn capture(p: f64, userdata: *mut c_void) -> c_int { - let v = unsafe { &mut *(userdata as *mut Vec) }; - v.push(p); - 0 - } - unsafe { - oakplugin_instance_set_progress_cb(h, Some(capture), &mut seen as *mut _ as *mut c_void) - }; - stub::setup_dst(64, 64, oakplugin::bridge::render::PIXEL_FORMAT_F32); - let dst = fake_texture(0xA1); - assert_eq!( - unsafe { oakplugin_instance_render(h, dst, CHandle::null(), 0.0) }, - OK - ); - assert_eq!(seen, vec![0.5], "插件在 render 内报一次 0.5"); - - // 取消回调(非 0 = 中止)。 - unsafe extern "C" fn abort_cb(_p: f64, _u: *mut c_void) -> c_int { - 1 - } - unsafe { oakplugin_instance_set_progress_cb(h, Some(abort_cb), std::ptr::null_mut()) }; - stub::setup_dst(64, 64, oakplugin::bridge::render::PIXEL_FORMAT_F32); - let r = unsafe { oakplugin_instance_render(h, dst, CHandle::null(), 0.0) }; - assert_eq!(r, -90003, "取消应使 render 失败"); - unsafe { oakplugin_instance_free(&mut h) }; - }); -} - -/// cancel:无活动渲染时 no-op;置位后 render 立即以取消码返回。 -#[test] -fn instance_cancel_semantics() { - common::with_host(|| { - let mut h = create_instance(); - if h.is_null() { - return; - } - // 无活动渲染:OK no-op。 - assert_eq!(unsafe { oakplugin_instance_cancel(h) }, OK); - // 置位后 render 入口即取消(先备好合法输出帧,确保走到 - // render 的取消检查而不是帧校验)。 - assert_eq!(unsafe { oakplugin_instance_cancel(h) }, OK); - #[cfg(feature = "test-stubs")] - oakplugin::bridge::render::stub::setup_dst( - 64, - 64, - oakplugin::bridge::render::PIXEL_FORMAT_F32, - ); - let dst = fake_texture(0xA1); - assert_eq!( - unsafe { oakplugin_instance_render(h, dst, CHandle::null(), 0.0) }, - -90003 - ); - unsafe { oakplugin_instance_free(&mut h) }; - }); -} - -/// 内省:param_count > 0;name/type/label/hint/parent 与插件定义 -/// 一致;index 越界 E_NOT_FOUND。 -#[test] -fn introspection_param_fields() { - common::with_host(|| { - let mut h = create_instance(); - if h.is_null() { - return; - } - let count = unsafe { oakplugin_instance_param_count(h) }; - assert!(count >= 4, "gain/mode/debug/label,count={count}"); - - // gain 的字段。 - assert_eq!( - two_stage(oakplugin_instance_param_name, h, 0), - Some("gain".into()) - ); - assert_eq!( - two_stage(oakplugin_instance_param_type, h, 0), - Some("OfxParamTypeDouble".into()) - ); - assert_eq!( - two_stage(oakplugin_instance_param_label, h, 0), - Some("Gain".into()) - ); - assert_eq!( - two_stage(oakplugin_instance_param_hint, h, 0), - Some(String::new()) - ); - assert_eq!( - two_stage(oakplugin_instance_param_parent, h, 0), - Some(String::new()) - ); - - // 越界 → E_NOT_FOUND。 - assert_eq!( - unsafe { oakplugin_instance_param_name(h, count, std::ptr::null_mut(), 0) }, - E_NOT_FOUND - ); - unsafe { oakplugin_instance_free(&mut h) }; - }); -} - -/// 内省:secret/display_range/choice 三族在测试插件的对应参数上 -/// 逐字段断言。 -#[test] -fn introspection_secret_range_choice() { - common::with_host(|| { - let mut h = create_instance(); - if h.is_null() { - return; - } - let count = unsafe { oakplugin_instance_param_count(h) }; - // 按名定位索引。 - let mut gain = -1; - let mut mode = -1; - let mut debug = -1; - for i in 0..count { - match two_stage(oakplugin_instance_param_name, h, i).as_deref() { - Some("gain") => gain = i, - Some("mode") => mode = i, - Some("debug") => debug = i, - _ => {} - } - } - assert!(gain >= 0 && mode >= 0 && debug >= 0); - - // display range(gain:-2..2)。 - let (mut min, mut max) = (0.0, 0.0); - assert_eq!( - unsafe { oakplugin_instance_param_display_range(h, gain, &mut min, &mut max) }, - OK - ); - assert_eq!((min, max), (-2.0, 2.0)); - - // secret(debug = 1;gain = 0)。 - let mut s = 0; - assert_eq!( - unsafe { oakplugin_instance_param_secret(h, debug, &mut s) }, - OK - ); - assert_eq!(s, 1); - assert_eq!( - unsafe { oakplugin_instance_param_secret(h, gain, &mut s) }, - OK - ); - assert_eq!(s, 0); - - // choice(mode:2 个选项;选项内容见 introspection_choice_labels)。 - assert_eq!(unsafe { oakplugin_instance_param_choice_count(h, mode) }, 2); - unsafe { oakplugin_instance_free(&mut h) }; - }); -} - -/// choice label/value 的带选项两段式。 -#[test] -fn introspection_choice_labels() { - common::with_host(|| { - let mut h = create_instance(); - if h.is_null() { - return; - } - let count = unsafe { oakplugin_instance_param_count(h) }; - let mut mode = -1; - for i in 0..count { - if two_stage(oakplugin_instance_param_name, h, i).as_deref() == Some("mode") { - mode = i; - } - } - assert!(mode >= 0); - assert_eq!(unsafe { oakplugin_instance_param_choice_count(h, mode) }, 2); - for c in 0..2 { - let len = unsafe { - oakplugin_instance_param_choice_label(h, mode, c, std::ptr::null_mut(), 0) - }; - assert!(len > 0); - let mut buf = vec![0u8; len as usize]; - assert_eq!( - unsafe { - oakplugin_instance_param_choice_label( - h, - mode, - c, - buf.as_mut_ptr() as *mut c_char, - len, - ) - }, - OK - ); - let s = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) } - .to_string_lossy() - .into_owned(); - assert!(s == "Fast" || s == "High", "选项 {c} = {s}"); - } - unsafe { oakplugin_instance_free(&mut h) }; - }); -} - -/// 内省:默认值(double 族与字符串)。 -#[test] -fn introspection_defaults() { - common::with_host(|| { - let mut h = create_instance(); - if h.is_null() { - return; - } - let count = unsafe { oakplugin_instance_param_count(h) }; - let mut gain = -1; - let mut label = -1; - for i in 0..count { - match two_stage(oakplugin_instance_param_name, h, i).as_deref() { - Some("gain") => gain = i, - Some("label") => label = i, - _ => {} - } - } - let mut d = 99.0; - assert_eq!( - unsafe { oakplugin_instance_param_default_double(h, gain, 0, &mut d) }, - OK - ); - assert_eq!(d, 0.0); - let s = two_stage(oakplugin_instance_param_default_string, h, label); - assert_eq!(s, Some(String::new())); - unsafe { oakplugin_instance_free(&mut h) }; - }); -} - -/// 内省:clip_count/name/info(含 Output clip;optional 标记)。 -#[test] -fn introspection_clips() { - common::with_host(|| { - let mut h = create_instance(); - if h.is_null() { - return; - } - assert_eq!(unsafe { oakplugin_instance_clip_count(h) }, 2); - let names: Vec = (0..2) - .map(|i| two_stage(oakplugin_instance_clip_name, h, i).unwrap_or_default()) - .collect(); - assert!(names.contains(&"Source".into()), "{names:?}"); - assert!(names.contains(&"Output".into()), "{names:?}"); - - for i in 0..2 { - let mut optional = -1; - let len = unsafe { - oakplugin_instance_clip_info(h, i, &mut optional, std::ptr::null_mut(), 0) - }; - assert!(len > 0); - let mut buf = vec![0u8; len as usize]; - assert_eq!( - unsafe { - oakplugin_instance_clip_info( - h, - i, - &mut optional, - buf.as_mut_ptr() as *mut c_char, - len, - ) - }, - OK - ); - assert!(optional == 0, "测试插件 clip 非 optional"); - } - unsafe { oakplugin_instance_free(&mut h) }; - }); -} - -/// debug_alive_count:create 增、free 减;全部释放后回零基线。 -#[test] -fn alive_count_accounting() { - common::with_host(|| { - unsafe { oakplugin_host_scan_import(std::ptr::null(), 0) }; - let base = unsafe { oakplugin_debug_alive_count() }; - let mut h = create_instance(); - if h.is_null() { - return; - } - assert_eq!(unsafe { oakplugin_debug_alive_count() }, base + 1); - unsafe { oakplugin_instance_free(&mut h) }; - assert_eq!(unsafe { oakplugin_debug_alive_count() }, base); - }); -} diff --git a/crates/oakplugin/tests/gl_render_test.rs b/crates/oakplugin/tests/gl_render_test.rs deleted file mode 100644 index be3e4518a..000000000 --- a/crates/oakplugin/tests/gl_render_test.rs +++ /dev/null @@ -1,456 +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 . - -//! OpenGL 渲染路径测试(M11 §4)。 -//! -//! GL 测试策略: -//! - `--features test-stubs`:库内桩 GL 渲染器(renderer_is_open_gl / -//! texture_create / texture_id 等)模拟 GPU——suite 往返、GL render -//! 路径、错误路径全链路可跑(无需真实 GPU/liboakrender); -//! - 默认模式(无 liboakrender 符号):GL 决策回退 CPU、GL suite 在 -//! 无上下文时返回 kOfxStatErrMissingHostFeature——用例断言该优雅 -//! 降级("无 GPU 优雅跳过")。 -//! - 真实 GPU golden(EXR 容差比对)为 M11 0 期基建 + 本机人工确认 -//! 项:`OAK_GPU_TESTS` 环境变量门,见 common::gpu_available。 - -mod common; - -use std::ffi::{c_char, c_void, CString}; - -use oakplugin::ffi::{ - oakplugin_host_scan, oakplugin_host_set_message_handler, oakplugin_instance_create, - oakplugin_instance_free, oakplugin_instance_render_begin_sequence, - oakplugin_instance_render_end_sequence, oakplugin_instance_render_job, -}; -use oakplugin::handle::CHandle; -use oakplugin::suites::gl_render::GlRenderSuiteV1; -use oakplugin::suites::{fetch_suite, status, tag}; - -const OK: i32 = 0; -const GL_PLUGIN_ID: &str = "org.oak.test-plugin.gl"; - -fn cs(s: &str) -> CString { - CString::new(s).unwrap() -} - -/// 简易句柄(桩纹理/渲染器)。 -fn fake_handle(ctx: usize) -> CHandle { - CHandle { - ctx: ctx as *mut c_void, - addref: None, - release: None, - abi_version: 1, - } -} - -/// 扫描测试插件并创建 GL 变体实例;不可用返回空句柄。 -fn create_gl_instance() -> CHandle { - if common::test_plugin_scan_dir().is_none() { - common::skip("最小测试插件未构建"); - return CHandle::null(); - } - let dir = cs(common::test_plugin_scan_dir().unwrap().to_str().unwrap()); - let dirs = [dir.as_ptr()]; - unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) }; - let id = cs(GL_PLUGIN_ID); - unsafe { oakplugin_instance_create(id.as_ptr()) } -} - -/// GL suite 表(fetchSuite 分发表)。 -fn gl_suite() -> Option<&'static GlRenderSuiteV1> { - unsafe { fetch_suite("OfxImageEffectOpenGLRenderSuite", 1) } - .map(|p| unsafe { &*(p as *const GlRenderSuiteV1) }) -} - -/// 构造一个带输入纹理的 Source clip(实例期 clip 句柄)。 -fn source_clip_handle( - tex: CHandle, -) -> (*mut c_void, std::sync::Arc) { - let desc = oakplugin::descriptor::ClipDescriptor::new("Source"); - let clip = std::sync::Arc::new(oakplugin::clip::ClipInstance::from_descriptor(&desc)); - clip.set_input_texture(tex, 0.0); - let h = tag::make( - &clip.props as *const oakplugin::property::PropertySet, - tag::CLIP, - ); - (h, clip) -} - -/// 当前渲染/GL 上下文注入(suite 的 TLS 依赖)。 -fn inject_gl_ctx(renderer: CHandle, output: CHandle) { - oakplugin::suites::set_render_ctx(Some(oakplugin::suites::RenderCtx { - time: 0.0, - scale: oakplugin::instance::RenderScale { x: 1.0, y: 1.0 }, - range: oakplugin::instance::OfxRangeD { min: 0.0, max: 1.0 }, - })); - oakplugin::suites::set_gl_ctx(Some(oakplugin::suites::GlCtx { - renderer, - output_texture: output, - gl_pixel_depth: "OfxBitDepthFloat", - })); -} - -/// 读纹理句柄(属性集)的 int 属性。 -fn tex_int(props: &oakplugin::property::PropertySet, name: &str) -> Option { - match props.get(name, 0)? { - oakplugin::property::Value::Int(i) => Some(i), - _ => None, - } -} - -fn tex_string(props: &oakplugin::property::PropertySet, name: &str) -> Option { - match props.get(name, 0)? { - oakplugin::property::Value::String(s) => Some(s.to_string_lossy().into_owned()), - _ => None, - } -} - -/// 清理 TLS(每个用例结尾)。 -fn clear_ctxs() { - oakplugin::suites::set_gl_ctx(None); - oakplugin::suites::set_render_ctx(None); -} - -/// clipLoadTexture 往返:输入 clip 建纹理、属性齐全、clipFreeTexture -/// 释放;Output clip 返回附着输出纹理;flushResources → ReplyDefault。 -#[cfg(feature = "test-stubs")] -#[test] -fn clip_load_texture_roundtrip() { - use oakplugin::bridge::render::stub; - common::with_host(|| { - stub::reset(); - stub::set_gl_available(true); - let renderer = stub::make_gl_renderer(); - let dst = stub::make_gl_texture(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32); - let src = stub::make_gl_texture(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32); - inject_gl_ctx(renderer, dst); - let suite = gl_suite().expect("GL suite 已注册"); - - // 输入 clip。 - let (clip_h, _clip) = source_clip_handle(src); - let before = stub::gl_texture_ids(); - let mut tex: *mut c_void = std::ptr::null_mut(); - unsafe { - assert_eq!( - (suite.clip_load_texture)( - clip_h, - 0.0, - std::ptr::null(), - std::ptr::null(), - &mut tex - ), - 0 - ); - } - assert!(!tex.is_null(), "应返回纹理属性集句柄"); - let props = unsafe { &*(tag::strip(tex) as *const oakplugin::property::PropertySet) }; - let idx = tex_int(props, "OfxImageEffectPropOpenGLTextureIndex").expect("texture index"); - assert!(idx > 0, "新建纹理 id 为正:{idx}"); - assert_eq!( - tex_int(props, "OfxImageEffectPropOpenGLTextureTarget"), - Some(0x0DE1), - "GL_TEXTURE_2D" - ); - assert_eq!( - tex_string(props, "OfxImageEffectPropPixelDepth").as_deref(), - Some("OfxBitDepthFloat") - ); - assert_eq!( - tex_string(props, "OfxImageEffectPropComponents").as_deref(), - Some("OfxImageComponentRGBA") - ); - assert!(props.get("OfxImagePropBounds", 0).is_some(), "Bounds"); - assert!(props.get("OfxImagePropRowBytes", 0).is_some(), "RowBytes"); - assert!( - props.get("OfxImagePropUniqueIdentifier", 0).is_some(), - "UniqueIdentifier" - ); - // 注册表新增一个纹理。 - let after = stub::gl_texture_ids(); - assert_eq!(after.len(), before.len() + 1, "clipLoadTexture 建新纹理"); - assert!(after.contains(&idx), "新纹理 id 在注册表"); - - // clipFreeTexture:删输入纹理。 - unsafe { - assert_eq!((suite.clip_free_texture)(tex), 0); - } - assert!(!stub::gl_texture_ids().contains(&idx), "输入纹理已删除"); - // 重复释放 → BadHandle。 - unsafe { - assert_eq!((suite.clip_free_texture)(tex), status::ERR_BAD_HANDLE); - } - - // Output clip:返回附着输出纹理;free 不删纹理(宿主读)。 - let mut out: *mut c_void = std::ptr::null_mut(); - let out_clip_instance = oakplugin::clip::ClipInstance::from_descriptor( - &oakplugin::descriptor::ClipDescriptor::new("Output"), - ); - let out_clip = tag::make( - &out_clip_instance.props as *const oakplugin::property::PropertySet, - tag::CLIP, - ); - unsafe { - assert_eq!( - (suite.clip_load_texture)( - out_clip, - 0.0, - std::ptr::null(), - std::ptr::null(), - &mut out - ), - 0 - ); - } - assert!(!out.is_null()); - let props = unsafe { &*(tag::strip(out) as *const oakplugin::property::PropertySet) }; - let dst_id = tex_int(props, "OfxImageEffectPropOpenGLTextureIndex").unwrap(); - assert!( - stub::gl_texture_ids().contains(&dst_id), - "输出纹理 id 来自附着目标" - ); - unsafe { - assert_eq!((suite.clip_free_texture)(out), 0); - } - assert!( - stub::gl_texture_ids().contains(&dst_id), - "Output free 不删纹理" - ); - - // flushResources:无宿主缓存 → ReplyDefault。 - unsafe { - assert_eq!((suite.flush_resources)(), status::REPLY_DEFAULT); - } - - clear_ctxs(); - }); -} - -/// 错误路径:无 GL 上下文 → MissingHostFeature;空 out/BadHandle; -/// 子区域 → Failed;请求分量不匹配 → Failed。 -#[cfg(feature = "test-stubs")] -#[test] -fn clip_load_texture_error_paths() { - use oakplugin::bridge::render::stub; - common::with_host(|| { - stub::reset(); - stub::set_gl_available(true); - let renderer = stub::make_gl_renderer(); - let dst = stub::make_gl_texture(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32); - let src = stub::make_gl_texture(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32); - let suite = gl_suite().expect("GL suite 已注册"); - - // 无 GL 上下文(非 GL 渲染期)→ MissingHostFeature。 - let (clip_h, _clip) = source_clip_handle(src); - let mut tex: *mut c_void = std::ptr::null_mut(); - unsafe { - assert_eq!( - (suite.clip_load_texture)( - clip_h, - 0.0, - std::ptr::null(), - std::ptr::null(), - &mut tex - ), - status::ERR_MISSING_HOST_FEATURE - ); - // 空 out → BadHandle;空 clip → BadHandle。 - assert_eq!( - (suite.clip_load_texture)( - clip_h, - 0.0, - std::ptr::null(), - std::ptr::null(), - std::ptr::null_mut() - ), - status::ERR_BAD_HANDLE - ); - assert_eq!( - (suite.clip_load_texture)( - std::ptr::null_mut(), - 0.0, - std::ptr::null(), - std::ptr::null(), - &mut tex - ), - status::ERR_BAD_HANDLE - ); - // clipFreeTexture 空句柄 → BadHandle。 - assert_eq!( - (suite.clip_free_texture)(std::ptr::null_mut()), - status::ERR_BAD_HANDLE - ); - } - - // 有 GL 上下文后:子区域(Phase 2 不支持)→ Failed。 - inject_gl_ctx(renderer, dst); - let region = oakplugin::instance::OfxRectD { - x1: 0.0, - y1: 0.0, - x2: 2.0, - y2: 2.0, - }; - unsafe { - assert_eq!( - (suite.clip_load_texture)( - clip_h, - 0.0, - std::ptr::null(), - ®ion as *const _ as *const c_void, - &mut tex - ), - status::FAILED - ); - } - - // 请求分量不匹配(RGBA 输入 + GLFormatRGB)→ Failed。 - let fmt = cs("OfxImageEffectGLFormatRGB"); - unsafe { - assert_eq!( - (suite.clip_load_texture)(clip_h, 0.0, fmt.as_ptr(), std::ptr::null(), &mut tex), - status::FAILED - ); - } - - // 匹配请求(GLFormatRGBA)→ OK。 - let fmt = cs("OfxImageEffectGLFormatRGBA"); - unsafe { - assert_eq!( - (suite.clip_load_texture)(clip_h, 0.0, fmt.as_ptr(), std::ptr::null(), &mut tex), - 0 - ); - assert_eq!((suite.clip_free_texture)(tex), 0); - } - clear_ctxs(); - }); -} - -/// 无桩模式(默认构建):GL suite 在无上下文时返回 MissingHostFeature -/// (优雅降级,不崩);render_job 带渲染器句柄时回退 CPU 路径。 -#[cfg(not(feature = "test-stubs"))] -#[test] -fn gl_path_graceful_without_gpu() { - common::with_host(|| { - let suite = gl_suite().expect("GL suite 已注册"); - let desc = oakplugin::descriptor::ClipDescriptor::new("Source"); - let clip = oakplugin::clip::ClipInstance::from_descriptor(&desc); - let clip_h = tag::make(&clip.props as *const _, tag::CLIP); - let mut tex: *mut c_void = std::ptr::null_mut(); - unsafe { - // 无 GL 上下文 → MissingHostFeature(规范语义)。 - assert_eq!( - (suite.clip_load_texture)( - clip_h, - 0.0, - std::ptr::null(), - std::ptr::null(), - &mut tex - ), - status::ERR_MISSING_HOST_FEATURE - ); - // flushResources → ReplyDefault。 - assert_eq!((suite.flush_resources)(), status::REPLY_DEFAULT); - } - }); -} - -/// GL render 路径端到端(test-stubs):render_job 带 GL 渲染器 → -/// 插件收到 OpenGLEnabled=1、attach/detach 配对、clipLoadTexture -/// 取 Source/Output 纹理(插件经 message suite 上报索引)。 -#[cfg(feature = "test-stubs")] -#[test] -fn gl_render_path_drives_plugin() { - use oakplugin::bridge::render::stub; - common::with_host(|| { - stub::reset(); - let mut h = create_gl_instance(); - if h.is_null() { - return; - } - // 消息捕获(插件 GL 行为的上报通道)。 - let captured: std::sync::Arc>> = - std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); - { - let cap = captured.clone(); - unsafe extern "C" fn capture( - _type: *const c_char, - message: *const c_char, - userdata: *mut c_void, - ) -> std::ffi::c_int { - let v = unsafe { &mut *(userdata as *mut std::sync::Mutex>) }; - if !message.is_null() { - let m = unsafe { std::ffi::CStr::from_ptr(message) } - .to_string_lossy() - .into_owned(); - v.lock().unwrap_or_else(|e| e.into_inner()).push(m); - } - 0 - } - unsafe { - oakplugin_host_set_message_handler(Some(capture), &*cap as *const _ as *mut c_void) - }; - } - - // GL 环境:渲染器可用;dst/src 均为 GL 纹理(2×2 F32)。 - stub::set_gl_available(true); - let renderer = stub::make_gl_renderer(); - let dst = stub::make_gl_texture(2, 2, oakplugin::bridge::render::PIXEL_FORMAT_F32); - let src = stub::make_gl_texture(2, 2, oakplugin::bridge::render::PIXEL_FORMAT_F32); - - // begin → job → end(序列括号)。 - assert_eq!( - unsafe { oakplugin_instance_render_begin_sequence(h, 0.0, 1.0, 0) }, - OK - ); - let r = unsafe { - oakplugin_instance_render_job( - h, - dst, - 0.0, - 0, - 0, - cs("Source").as_ptr(), - src, - std::ptr::null(), - 0, - std::ptr::null(), - 0, - renderer, - ) - }; - assert_eq!(r, OK, "GL render_job 应成功"); - assert_eq!( - unsafe { oakplugin_instance_render_end_sequence(h, 0.0, 1.0, 0) }, - OK - ); - - // 插件上报:attach/detach 配对、Source 纹理索引、Output 纹理索引。 - let msgs = captured.lock().unwrap_or_else(|e| e.into_inner()).clone(); - let joined = msgs.join("|"); - assert!(joined.contains("gl-attached"), "attach 应调用:{joined}"); - assert!(joined.contains("gl-detached"), "detach 应调用:{joined}"); - assert!( - joined.contains("gl-source-index="), - "Source 纹理索引:{joined}" - ); - assert!( - joined.contains("gl-output-index="), - "Output 纹理索引:{joined}" - ); - - unsafe { oakplugin_instance_free(&mut h) }; - unsafe { oakplugin_host_set_message_handler(None, std::ptr::null_mut()) }; - }); -} diff --git a/crates/oakplugin/tests/golden_test.rs b/crates/oakplugin/tests/golden_test.rs index b6f95faa2..7e314b8d8 100644 --- a/crates/oakplugin/tests/golden_test.rs +++ b/crates/oakplugin/tests/golden_test.rs @@ -68,10 +68,9 @@ fn negotiated_clip_preferences_match() { /// 的 `Property::Set` 句柄语义与本 crate 的打标句柄约定不兼容 /// (插件侧属性写入在进入本宿主属性套件前失败 → describe 返回 /// kOfxStatErrMissingHostFeature)。真实插件的端到端加载/建实例/ -/// 协商路径由 [`crate::bridge_test` 的 `system_misc_ofx_bundle_smoke`] -/// (Misc.ofx.bundle,标准 openfx-misc 构建)覆盖。CImg 兼容属 -/// M11 §3.5「CImg 全量 describe/协商冒烟」的宿主保真缺口——属性 -/// 套件适配插件侧 `Property::Set` 句柄后摘除本注解。 +/// 协商路径由 Misc.ofx.bundle(标准 openfx-misc 构建)冒烟覆盖。 +/// CImg 兼容属 M11 §3.5「CImg 全量 describe/协商冒烟」的宿主保真 +/// 缺口——属性套件适配插件侧 `Property::Set` 句柄后摘除本注解。 #[test] #[ignore = "本机 CImg.ofx.bundle 为 Natron 分支构建,属性套件句柄语义不兼容(describe 返回 MissingHostFeature);Misc 系统冒烟已覆盖真实插件路径"] fn cimg_full_describe_smoke() { diff --git a/crates/oakplugin/tests/lifecycle_test.rs b/crates/oakplugin/tests/lifecycle_test.rs deleted file mode 100644 index 3dcdd797a..000000000 --- a/crates/oakplugin/tests/lifecycle_test.rs +++ /dev/null @@ -1,251 +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 . - -//! 生命周期测试:实例创建/销毁配对、重复扫描、渲染中取消、 -//! alive 泄漏断言。对应 M11 §3.5 验收线。 -//! -//! 宿主单例无锁:全部用例经 [`common::with_host`] 串行化。 - -mod common; - -use std::ffi::{c_char, c_int, c_void, CString}; - -use oakplugin::ffi::{ - oakplugin_debug_alive_count, oakplugin_host_init, oakplugin_host_plugin_count, - oakplugin_host_scan, oakplugin_host_shutdown, oakplugin_instance_cancel, - oakplugin_instance_create, oakplugin_instance_free, oakplugin_instance_render, - oakplugin_instance_set_progress_cb, -}; -use oakplugin::handle::CHandle; - -const OK: i32 = 0; -const E_FAILED: i32 = -90003; - -fn cs(s: &str) -> CString { - CString::new(s).unwrap() -} - -const TEST_PLUGIN_ID: &str = "org.oak.test-plugin"; - -fn fake_texture(ctx: usize) -> CHandle { - CHandle { - ctx: ctx as *mut c_void, - addref: None, - release: None, - abi_version: 1, - } -} - -/// 扫描并创建实例;不可用返回空句柄。 -fn create_instance() -> CHandle { - if common::test_plugin_scan_dir().is_none() { - common::skip("最小测试插件未构建"); - return CHandle::null(); - } - let dir = cs(common::test_plugin_scan_dir().unwrap().to_str().unwrap()); - let dirs = [dir.as_ptr()]; - unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) }; - let id = cs(TEST_PLUGIN_ID); - unsafe { oakplugin_instance_create(id.as_ptr()) } -} - -/// createInstance → destroyInstance 严格配对:alive 计数回到基线。 -#[test] -fn instance_create_destroy_pairing() { - common::with_host(|| { - unsafe { oakplugin_host_init() }; - let base = unsafe { oakplugin_debug_alive_count() }; - let mut h = create_instance(); - if h.is_null() { - unsafe { oakplugin_host_shutdown() }; - return; - } - assert_eq!(unsafe { oakplugin_debug_alive_count() }, base + 1); - unsafe { oakplugin_instance_free(&mut h) }; - assert_eq!( - unsafe { oakplugin_debug_alive_count() }, - base, - "destroy 后 alive 必须回基线" - ); - unsafe { oakplugin_host_shutdown() }; - }); -} - -/// 重复 scan 同一路径:插件列表不重复、顺序稳定(缓存去重语义, -/// HS: ofxhPluginCache.cpp)。 -#[test] -fn rescan_is_idempotent() { - common::with_host(|| { - unsafe { oakplugin_host_init() }; - if common::test_plugin_scan_dir().is_none() { - common::skip("最小测试插件未构建"); - unsafe { oakplugin_host_shutdown() }; - return; - } - let dir = cs(common::test_plugin_scan_dir().unwrap().to_str().unwrap()); - let dirs = [dir.as_ptr()]; - assert_eq!(unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) }, OK); - let first = unsafe { oakplugin_host_plugin_count() }; - assert!(first >= 1); - // 再扫两次:数量不变。 - assert_eq!(unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) }, OK); - assert_eq!(unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) }, OK); - assert_eq!( - unsafe { oakplugin_host_plugin_count() }, - first, - "重复扫描不得重复加载" - ); - unsafe { oakplugin_host_shutdown() }; - }); -} - -/// render 中途取消:进度回调返回非 0 后,插件的 progressUpdate 收到 -/// 取消并中止 render;输出纹理不写半帧(测试插件在取消路径返回 -/// Failed,宿主不落帧)。 -#[cfg(feature = "test-stubs")] -#[test] -fn render_cancellation_is_atomic() { - common::with_host(|| { - use oakplugin::bridge::render::stub; - unsafe { oakplugin_host_init() }; - let mut h = create_instance(); - if h.is_null() { - unsafe { oakplugin_host_shutdown() }; - return; - } - unsafe extern "C" fn abort_cb(_p: f64, _u: *mut c_void) -> c_int { - 1 - } - unsafe { oakplugin_instance_set_progress_cb(h, Some(abort_cb), std::ptr::null_mut()) }; - - // 预置输出帧并填 0xAA 哨兵:取消路径不得写半帧。 - stub::setup_dst(64, 64, oakplugin::bridge::render::PIXEL_FORMAT_F32); - let before = stub::dst_pixels(); - assert!(!before.is_empty()); - let dst = fake_texture(0xA1); - let r = unsafe { oakplugin_instance_render(h, dst, CHandle::null(), 0.0) }; - assert_eq!(r, E_FAILED, "取消应使 render 失败"); - // 全有或全无:失败时不写。 - assert_eq!(stub::dst_pixels(), before, "取消后输出不得被写"); - unsafe { oakplugin_instance_free(&mut h) }; - unsafe { oakplugin_host_shutdown() }; - }); -} - -/// 压测:同一插件 256 次 create/render/destroy 循环后 alive 回到 -/// 基线(当年内存问题的回归防线)。 -#[cfg(feature = "test-stubs")] -#[test] -fn create_render_destroy_loop_no_leak() { - common::with_host(|| { - use oakplugin::bridge::render::stub; - unsafe { oakplugin_host_init() }; - let base = unsafe { oakplugin_debug_alive_count() }; - if common::test_plugin_scan_dir().is_none() { - common::skip("最小测试插件未构建"); - unsafe { oakplugin_host_shutdown() }; - return; - } - stub::setup_dst(16, 16, oakplugin::bridge::render::PIXEL_FORMAT_F32); - let dst = fake_texture(0xA1); - for i in 0..256 { - let mut h = create_instance(); - assert!(!h.is_null()); - let r = unsafe { oakplugin_instance_render(h, dst, CHandle::null(), i as f64) }; - assert_eq!(r, OK); - unsafe { oakplugin_instance_free(&mut h) }; - assert_eq!( - unsafe { oakplugin_debug_alive_count() }, - base, - "第 {i} 次循环后泄漏" - ); - } - unsafe { oakplugin_host_shutdown() }; - }); -} - -/// host_shutdown 时仍有活实例:宿主发 destroy 通知并卸载 bundle, -/// 之后该实例的任何 render 立即取消(不碰已卸载入口);再 create -/// 返回空句柄(缓存已清空——声明原期望 E_STATE,本实现的 create -/// 契约是"失败返回空句柄",以实例化为准)。 -#[test] -fn shutdown_with_live_instances() { - common::with_host(|| { - unsafe { oakplugin_host_init() }; - let mut h = create_instance(); - if h.is_null() { - unsafe { oakplugin_host_shutdown() }; - return; - } - // 带活实例 shutdown:不崩;实例被打取消标记。 - unsafe { oakplugin_host_shutdown() }; - // C 侧句柄仍持 Arc:实例内存存活,但 render 必须失败。 - let dst = fake_texture(0xA1); - assert_eq!( - unsafe { oakplugin_instance_render(h, dst, CHandle::null(), 0.0) }, - E_FAILED - ); - unsafe { oakplugin_instance_free(&mut h) }; - assert_eq!( - unsafe { oakplugin_debug_alive_count() }, - 0, - "句柄释放后归零" - ); - // 之后再 create:插件已卸载 → 空句柄。 - let id = cs(TEST_PLUGIN_ID); - let h2 = unsafe { oakplugin_instance_create(id.as_ptr()) }; - assert!(h2.is_null()); - }); -} - -/// 多实例并发:同一插件 16 实例并发 render(宿主侧不串——插件 -/// 全局状态由插件自保)。 -#[cfg(feature = "test-stubs")] -#[test] -fn concurrent_instances_render() { - common::with_host(|| { - use oakplugin::bridge::render::stub; - unsafe { oakplugin_host_init() }; - if common::test_plugin_scan_dir().is_none() { - common::skip("最小测试插件未构建"); - unsafe { oakplugin_host_shutdown() }; - return; - } - // 16 个实例(句柄数组;经线程并发 render)。 - let mut handles: Vec = (0..16).map(|_| create_instance()).collect(); - assert!(handles.iter().all(|h| !h.is_null())); - stub::setup_dst(32, 32, oakplugin::bridge::render::PIXEL_FORMAT_F32); - let dst = fake_texture(0xA1); - let threads: Vec<_> = handles - .iter() - .enumerate() - .map(|(i, h)| { - let h = *h; - std::thread::spawn(move || unsafe { - oakplugin_instance_render(h, dst, CHandle::null(), i as f64) - }) - }) - .collect(); - for t in threads { - assert_eq!(t.join().unwrap(), OK); - } - for h in handles.iter_mut() { - unsafe { oakplugin_instance_free(h) }; - } - assert_eq!(unsafe { oakplugin_debug_alive_count() }, 0); - unsafe { oakplugin_host_shutdown() }; - }); -} diff --git a/crates/oakplugin/tests/negotiation_test.rs b/crates/oakplugin/tests/negotiation_test.rs index fb4581c21..e246bc783 100644 --- a/crates/oakplugin/tests/negotiation_test.rs +++ b/crates/oakplugin/tests/negotiation_test.rs @@ -21,44 +21,31 @@ //! (回写 region)、isIdentity 恒非透传、无 field——需要插件变体 //! 的矩阵案(回退链、身份透传、field)标记 `// TODO(plugin)`, //! 随 M11 §2.4 测试插件的变体落地。 +//! +//! 单库化后经 Rust API 直连(`Host::global().create_instance`),不再 +//! 经已删除的 `oakplugin_host_init`/`oakplugin_instance_create` C ABI。 mod common; -use std::ffi::{c_char, c_void, CString}; +use std::sync::Arc; -use oakplugin::ffi::{ - oakplugin_host_init, oakplugin_host_scan, oakplugin_host_shutdown, oakplugin_instance_create, - oakplugin_instance_free, -}; -use oakplugin::handle::{get, CHandle}; - -const OK: i32 = 0; - -fn cs(s: &str) -> CString { - CString::new(s).unwrap() -} +use oakplugin::handle::RefBox; +use oakplugin::host::Host; +use oakplugin::instance::Instance; const TEST_PLUGIN_ID: &str = "org.oak.test-plugin"; -/// 扫描并创建实例;不可用返回空句柄。 -fn create_instance() -> CHandle { +/// 扫描并创建实例;不可用返回 None(skip)。 +fn create_instance() -> Option>> { if common::test_plugin_scan_dir().is_none() { common::skip("最小测试插件未构建"); - return CHandle::null(); + return None; } - let dir = cs(common::test_plugin_scan_dir().unwrap().to_str().unwrap()); - let dirs = [dir.as_ptr()]; - unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) }; - let id = cs(TEST_PLUGIN_ID); - unsafe { oakplugin_instance_create(id.as_ptr()) } -} - -/// 句柄 → Instance 引用。 -fn instance_of( - h: &CHandle, -) -> Option>> { - unsafe { get::>>(h) } - .cloned() + let dir = common::test_plugin_scan_dir().unwrap(); + let host = Host::global(); + let _ = host.cache.scan(); + host.cache.scan_path(&dir).ok()?; + host.create_instance(TEST_PLUGIN_ID, None).ok() } /// 协商顺序:测试插件声明 RGBA/F32、帧率 24——协商结果必须 @@ -68,13 +55,10 @@ fn instance_of( #[test] fn clip_preferences_component_depth_matrix() { common::with_host(|| { - unsafe { oakplugin_host_init() }; - let mut h = create_instance(); - if h.is_null() { - unsafe { oakplugin_host_shutdown() }; + let Some(inst) = create_instance() else { + Host::global().shutdown(); return; - } - let inst = instance_of(&h).expect("句柄应可解析"); + }; let prefs = inst.value.get_clip_preferences().expect("协商应成功"); assert_eq!(prefs.output_components, "OfxImageComponentRGBA"); assert_eq!(prefs.output_bit_depth, "OfxBitDepthFloat"); @@ -93,8 +77,7 @@ fn clip_preferences_component_depth_matrix() { .map(|v| format!("{v:?}")), Some("String(\"OfxImageComponentRGBA\")".into()) ); - unsafe { oakplugin_instance_free(&mut h) }; - unsafe { oakplugin_host_shutdown() }; + Host::global().shutdown(); }); } @@ -103,17 +86,13 @@ fn clip_preferences_component_depth_matrix() { #[test] fn bit_depth_fallback_chain() { common::with_host(|| { - unsafe { oakplugin_host_init() }; - let mut h = create_instance(); - if h.is_null() { - unsafe { oakplugin_host_shutdown() }; + let Some(inst) = create_instance() else { + Host::global().shutdown(); return; - } - let inst = instance_of(&h).expect("句柄应可解析"); + }; let prefs = inst.value.get_clip_preferences().unwrap(); assert_eq!(prefs.output_bit_depth, "OfxBitDepthFloat"); - unsafe { oakplugin_instance_free(&mut h) }; - unsafe { oakplugin_host_shutdown() }; + Host::global().shutdown(); }); } @@ -123,20 +102,16 @@ fn bit_depth_fallback_chain() { #[test] fn region_of_definition_default_and_override() { common::with_host(|| { - unsafe { oakplugin_host_init() }; - let mut h = create_instance(); - if h.is_null() { - unsafe { oakplugin_host_shutdown() }; + let Some(inst) = create_instance() else { + Host::global().shutdown(); return; - } - let inst = instance_of(&h).expect("句柄应可解析"); + }; let rod = inst .value .get_region_of_definition(0.0, oakplugin::instance::RenderScale { x: 1.0, y: 1.0 }) .expect("getRoD 应成功"); assert_eq!((rod.x1, rod.y1, rod.x2, rod.y2), (0.0, 0.0, 1920.0, 1080.0)); - unsafe { oakplugin_instance_free(&mut h) }; - unsafe { oakplugin_host_shutdown() }; + Host::global().shutdown(); }); } @@ -146,13 +121,10 @@ fn region_of_definition_default_and_override() { #[test] fn regions_of_interest_writeback() { common::with_host(|| { - unsafe { oakplugin_host_init() }; - let mut h = create_instance(); - if h.is_null() { - unsafe { oakplugin_host_shutdown() }; + let Some(inst) = create_instance() else { + Host::global().shutdown(); return; - } - let inst = instance_of(&h).expect("句柄应可解析"); + }; let region = oakplugin::instance::OfxRectD { x1: 10.0, y1: 20.0, @@ -183,8 +155,7 @@ fn regions_of_interest_writeback() { (source.x1, source.y1, source.x2, source.y2), (10.0, 20.0, 100.0, 200.0) ); - unsafe { oakplugin_instance_free(&mut h) }; - unsafe { oakplugin_host_shutdown() }; + Host::global().shutdown(); }); } @@ -193,17 +164,13 @@ fn regions_of_interest_writeback() { #[test] fn is_identity_shortcircuit() { common::with_host(|| { - unsafe { oakplugin_host_init() }; - let mut h = create_instance(); - if h.is_null() { - unsafe { oakplugin_host_shutdown() }; + let Some(inst) = create_instance() else { + Host::global().shutdown(); return; - } - let inst = instance_of(&h).expect("句柄应可解析"); + }; let identity = inst.value.is_identity(1.5).expect("isIdentity 应成功"); assert!(identity.is_none(), "测试插件恒非透传:{identity:?}"); - unsafe { oakplugin_instance_free(&mut h) }; - unsafe { oakplugin_host_shutdown() }; + Host::global().shutdown(); }); } @@ -212,17 +179,13 @@ fn is_identity_shortcircuit() { #[test] fn field_passthrough() { common::with_host(|| { - unsafe { oakplugin_host_init() }; - let mut h = create_instance(); - if h.is_null() { - unsafe { oakplugin_host_shutdown() }; + let Some(inst) = create_instance() else { + Host::global().shutdown(); return; - } - let inst = instance_of(&h).expect("句柄应可解析"); + }; let prefs = inst.value.get_clip_preferences().unwrap(); assert_eq!(prefs.field, "OfxFieldNone", "插件声明的 field 应透传"); - unsafe { oakplugin_instance_free(&mut h) }; - unsafe { oakplugin_host_shutdown() }; + Host::global().shutdown(); }); } @@ -232,13 +195,10 @@ fn field_passthrough() { fn sequence_render_brackets() { common::with_host(|| { use oakplugin::instance::OfxRangeD; - unsafe { oakplugin_host_init() }; - let mut h = create_instance(); - if h.is_null() { - unsafe { oakplugin_host_shutdown() }; + let Some(inst) = create_instance() else { + Host::global().shutdown(); return; - } - let inst = instance_of(&h).expect("句柄应可解析"); + }; let range = OfxRangeD { min: 10.0, max: 200.0, @@ -248,7 +208,6 @@ fn sequence_render_brackets() { // begin → timeline 上下文带范围(经 render 设置;单测直达 // RenderCtx 的接线见 suites::timeline 测试)。 - unsafe { oakplugin_instance_free(&mut h) }; - unsafe { oakplugin_host_shutdown() }; + Host::global().shutdown(); }); } diff --git a/crates/oakplugin/tests/render_driver_test.rs b/crates/oakplugin/tests/render_driver_test.rs deleted file mode 100644 index 75ff3e0f8..000000000 --- a/crates/oakplugin/tests/render_driver_test.rs +++ /dev/null @@ -1,343 +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 驱动测试(M11 §4):pluginrenderer.cpp 语义收编的 CPU 路径 -//! ——序列括号、render_job 多输入/参数覆盖、isIdentity 短路、输出 -//! 装配像素断言。GL 路径见 gl_render_test.rs。 - -mod common; - -use std::ffi::{c_char, c_int, c_void, CString}; - -use oakplugin::ffi::{ - node_value_type, oakplugin_host_scan, oakplugin_instance_create, oakplugin_instance_free, - oakplugin_instance_get_param, oakplugin_instance_render_begin_sequence, - oakplugin_instance_render_end_sequence, oakplugin_instance_render_job, OakNodeValue, - OakPluginJobTexture, OakPluginJobValue, -}; -use oakplugin::handle::CHandle; - -const OK: i32 = 0; -const TEST_PLUGIN_ID: &str = "org.oak.test-plugin"; -const IDENTITY_PLUGIN_ID: &str = "org.oak.test-plugin.identity"; - -fn cs(s: &str) -> CString { - CString::new(s).unwrap() -} - -fn fake_texture(ctx: usize) -> CHandle { - CHandle { - ctx: ctx as *mut c_void, - addref: None, - release: None, - abi_version: 1, - } -} - -fn scan_and_create(id: &str) -> CHandle { - if common::test_plugin_scan_dir().is_none() { - common::skip("最小测试插件未构建"); - return CHandle::null(); - } - let dir = cs(common::test_plugin_scan_dir().unwrap().to_str().unwrap()); - let dirs = [dir.as_ptr()]; - unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) }; - let id = cs(id); - unsafe { oakplugin_instance_create(id.as_ptr()) } -} - -/// 序列括号 + render_job(CPU):begin → job → end;输出像素为测试 -/// 插件常量填充(0.5 RGBA F32,alpha=1);RoI 计算成功。 -#[cfg(feature = "test-stubs")] -#[test] -fn render_job_cpu_path_and_sequence_brackets() { - use oakplugin::bridge::render::stub; - common::with_host(|| { - stub::reset(); - let mut h = scan_and_create(TEST_PLUGIN_ID); - if h.is_null() { - return; - } - stub::setup_dst(8, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32); - let dst = fake_texture(0xA1); - - // 未 begin 序列直接 job 也可用(单帧;序列括号是优化语义)。 - assert_eq!( - unsafe { - oakplugin_instance_render_job( - h, - dst, - 0.0, - 0, - 0, - std::ptr::null(), - CHandle::null(), - std::ptr::null(), - 0, - std::ptr::null(), - 0, - CHandle::null(), - ) - }, - OK - ); - // begin/end 括号配对。 - assert_eq!( - unsafe { oakplugin_instance_render_begin_sequence(h, 0.0, 10.0, 0) }, - OK - ); - assert_eq!( - unsafe { oakplugin_instance_render_end_sequence(h, 0.0, 10.0, 0) }, - OK - ); - - // 像素断言:8×4 常量 0.5 / alpha 1。 - let pixels = stub::dst_pixels(); - let n = pixels.len() / 16; - assert_eq!(n, 32); - for i in 0..n { - let f = |o: usize| { - f32::from_le_bytes( - pixels[i * 16 + o * 4..i * 16 + o * 4 + 4] - .try_into() - .unwrap(), - ) - }; - assert_eq!(f(0), 0.5, "pixel {i} r"); - assert_eq!(f(3), 1.0, "pixel {i} a"); - } - unsafe { oakplugin_instance_free(&mut h) }; - }); -} - -/// 多输入 + 参数覆盖:inputs 表按 clip 名挂输入纹理;values 覆盖 -/// gain 参数(驱动注入实例参数,插件经 param 读取——测试插件 render -/// 不读 gain,故断言回写成功即可)。 -#[cfg(feature = "test-stubs")] -#[test] -fn render_job_multi_input_and_param_overrides() { - use oakplugin::bridge::render::stub; - common::with_host(|| { - stub::reset(); - let mut h = scan_and_create(TEST_PLUGIN_ID); - if h.is_null() { - return; - } - stub::setup_dst(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32); - stub::setup_src( - 4, - 4, - oakplugin::bridge::render::PIXEL_FORMAT_F32, - vec![0u8; 4 * 4 * 16], - ); - let dst = fake_texture(0xA1); - let src = fake_texture(0xA2); - - // 参数覆盖:gain = 1.5(FLOAT)。 - let mut value = OakNodeValue::default(); - value.r#type = node_value_type::FLOAT; - value.f = [1.5, 0.0, 0.0, 0.0]; - let gain = cs("gain"); - let job_values = [OakPluginJobValue { - key: gain.as_ptr(), - value, - }]; - - // 多输入表:Source → src 纹理。 - let source = cs("Source"); - let job_inputs = [OakPluginJobTexture { - clip: source.as_ptr(), - texture: src, - }]; - - assert_eq!( - unsafe { - oakplugin_instance_render_job( - h, - dst, - 0.0, - 0, - 0, - std::ptr::null(), - CHandle::null(), - job_inputs.as_ptr(), - 1, - job_values.as_ptr(), - 1, - CHandle::null(), - ) - }, - OK - ); - - // 覆盖已生效:经 C ABI 读回 gain。 - let mut out = OakNodeValue::default(); - let g = cs("gain"); - assert_eq!( - unsafe { oakplugin_instance_get_param(h, g.as_ptr(), &mut out) }, - OK - ); - assert_eq!(out.f[0], 1.5, "gain 覆盖应已注入实例参数"); - - unsafe { oakplugin_instance_free(&mut h) }; - }); -} - -/// isIdentity 短路:identity 变体插件返回 "Source" → render_job 不调 -/// render action,直接把 Source 的帧拷入输出(像素与输入一致)。 -#[cfg(feature = "test-stubs")] -#[test] -fn render_job_is_identity_shortcircuit() { - use oakplugin::bridge::render::stub; - common::with_host(|| { - stub::reset(); - let mut h = scan_and_create(IDENTITY_PLUGIN_ID); - if h.is_null() { - return; - } - // 输入帧:每像素 r=0.1,g=0.2,b=0.3,a=1.0。 - let mut pixels = Vec::new(); - for _ in 0..4 * 4 { - for v in [0.1f32, 0.2, 0.3, 1.0] { - pixels.extend_from_slice(&v.to_le_bytes()); - } - } - stub::setup_dst(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32); - stub::setup_src( - 4, - 4, - oakplugin::bridge::render::PIXEL_FORMAT_F32, - pixels.clone(), - ); - let dst = fake_texture(0xA1); - let src = fake_texture(0xA2); - - assert_eq!( - unsafe { - oakplugin_instance_render_job( - h, - dst, - 0.0, - 0, - 0, - cs("Source").as_ptr(), - src, - std::ptr::null(), - 0, - std::ptr::null(), - 0, - CHandle::null(), - ) - }, - OK - ); - - // 输出 == 输入(透传帧)。 - let out_pixels = stub::dst_pixels(); - assert_eq!(out_pixels, pixels, "isIdentity 透传应逐字节拷贝输入帧"); - unsafe { oakplugin_instance_free(&mut h) }; - }); -} - -/// 默认构建(无桩):render_job 缺桥符号 → 明确失败码而非崩溃 -/// (优雅降级;GL 用例同)。 -#[cfg(not(feature = "test-stubs"))] -#[test] -fn render_job_graceful_without_bridge() { - common::with_host(|| { - let mut h = scan_and_create(TEST_PLUGIN_ID); - if h.is_null() { - return; - } - // NULL 输出纹理在触达 oakrender 前被插件层校验拒绝(E_INVALID)。 - let dst = CHandle::null(); - let r = unsafe { - oakplugin_instance_render_job( - h, - dst, - 0.0, - 0, - 0, - std::ptr::null(), - CHandle::null(), - std::ptr::null(), - 0, - std::ptr::null(), - 0, - CHandle::null(), - ) - }; - assert_eq!(r, -90001, "NULL 输出纹理 → E_INVALID(不崩溃)"); - unsafe { oakplugin_instance_free(&mut h) }; - }); -} - -/// 驱动错误路径:非 F32 输出 → 明确失败;取消标记 → 明确失败。 -#[cfg(feature = "test-stubs")] -#[test] -fn render_job_error_paths() { - use oakplugin::bridge::render::stub; - use oakplugin::ffi::{oakplugin_instance_cancel, oakplugin_instance_render_job}; - common::with_host(|| { - stub::reset(); - let mut h = scan_and_create(TEST_PLUGIN_ID); - if h.is_null() { - return; - } - // 非 F32 输出(U8)→ read_dst 拒绝。 - stub::setup_dst(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_U8); - let dst = fake_texture(0xA1); - let r = unsafe { - oakplugin_instance_render_job( - h, - dst, - 0.0, - 0, - 0, - std::ptr::null(), - CHandle::null(), - std::ptr::null(), - 0, - std::ptr::null(), - 0, - CHandle::null(), - ) - }; - assert_eq!(r, -90003, "非 F32 输出 → E_FAILED"); - - // 取消标记 → 驱动入口短路。 - stub::setup_dst(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32); - assert_eq!(unsafe { oakplugin_instance_cancel(h) }, OK); - let r = unsafe { - oakplugin_instance_render_job( - h, - dst, - 0.0, - 0, - 0, - std::ptr::null(), - CHandle::null(), - std::ptr::null(), - 0, - std::ptr::null(), - 0, - CHandle::null(), - ) - }; - assert_eq!(r, -90003, "已取消 → E_FAILED"); - unsafe { oakplugin_instance_free(&mut h) }; - }); -} diff --git a/crates/oakrender/Cargo.toml b/crates/oakrender/Cargo.toml index 362941a76..6f51cd85c 100644 --- a/crates/oakrender/Cargo.toml +++ b/crates/oakrender/Cargo.toml @@ -29,8 +29,6 @@ ocio-rs = { version = "0.2", features = ["bundled"] } thiserror = "2" [dev-dependencies] -# Test binaries that pull oakcodec's decoder (the direct-call bridge) link -# oakcodec's ffmpeg unit, which references the host `oakcore_audioparams_*` -# symbols no Rust crate provides — the `test-stubs` feature compiles -# oakcodec's host-mocks (same pattern as oaknode's dev-dependencies). -oakcodec = { path = "../oakcodec", features = ["test-stubs"] } +# oakcodec is also a plain dependency above; the dev-dependency re-entry +# used to enable oakcodec's `test-stubs` host-mocks, which no longer exist +# (single-lib unification removed the oakcore_audioparams_* C ABI calls). diff --git a/crates/oakrender/src/backend.rs b/crates/oakrender/src/backend.rs index 2bc7a525d..4ea4e9f33 100644 --- a/crates/oakrender/src/backend.rs +++ b/crates/oakrender/src/backend.rs @@ -99,7 +99,7 @@ impl BackendKind { } } let configured = - crate::bridge::common::config_get_string(None, "GraphicsBackend").unwrap_or_default(); + crate::commonutil::config_get_string(None, "GraphicsBackend").unwrap_or_default(); BackendKind::from_config_string(&configured) } diff --git a/crates/oakrender/src/bridge/codec.rs b/crates/oakrender/src/bridge/codec.rs deleted file mode 100644 index 8e7197ef8..000000000 --- a/crates/oakrender/src/bridge/codec.rs +++ /dev/null @@ -1,317 +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 . - -//! oakcodec C ABI bridge: direct Rust calls into the `oakcodec` crate -//! (M12 P0: the footage decode path). -//! -//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every -//! call below is a compile-time Rust call into `oakcodec`'s `ffi` — the -//! `#[no_mangle]` exports stay in the dylib for the external C ABI; -//! internal callers bypass them. Handles cross as the shared -//! [`crate::handle::CHandle`] (`oakcore_rs::handle::CHandle`). There is no -//! runtime ABI probe: the crate is a path dependency, so the functions are -//! always present; failure semantics are oakcodec's own (empty handle / -//! negative error code). - -use std::ffi::{c_char, c_int}; - -use crate::error::{Error, Result}; -use crate::handle::CHandle; - -/// `oakcodec_video_stream_info` — POD probe output (one video stream). -/// -/// Layout-identical to `oakcodec::decoder::OakCodecVideoStreamInfo` (both -/// `#[repr(C)]`); kept as a local mirror so the bridge API is self-contained. -#[repr(C)] -#[derive(Clone, Copy, Debug, Default)] -pub struct VideoStreamInfo { - /// Stream index. - 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, - /// Stream length in time-base units. - pub duration_ts: i64, - /// Time-base numerator (seconds per time-base unit). - pub time_base_num: c_int, - /// Time-base denominator. - pub time_base_den: c_int, - /// Native delivery `OakPixelFormat`. - pub format: c_int, - /// Plane channel count. - pub channel_count: c_int, - /// ISO/IEC 23001-8 color-primaries code point (0 = unknown). - pub color_primaries: c_int, - /// ISO/IEC 23001-8 color-transfer code point (0 = unknown). - pub color_trc: c_int, - /// 1 when the stream is interlaced. - pub interlaced: c_int, -} - -/// Decoder handle (owned session). -pub type DecoderHandle = CHandle; -/// Probe handle (owned). -pub type ProbeHandle = CHandle; -/// Decoded frame handle (owned). -pub type CodecFrameHandle = CHandle; - -/// `oakcodec_decoder_probe(filename)` — owned probe handle; null on -/// failure. -pub fn decoder_probe(filename: &str) -> ProbeHandle { - let c = std::ffi::CString::new(filename).unwrap_or_default(); - // Direct call into the `oakcodec` crate (single-lib unification; the - // `#[no_mangle]` export stays for the external C ABI). - unsafe { oakcodec::ffi::decoder::oakcodec_decoder_probe(c.as_ptr()) } -} - -/// `oakcodec_decoder_probe_video_stream_count(probe)`; 0 on an empty -/// probe. -pub fn probe_video_stream_count(probe: ProbeHandle) -> c_int { - // Direct call into the `oakcodec` crate (single-lib unification; the - // `#[no_mangle]` export stays for the external C ABI). - unsafe { oakcodec::ffi::decoder::oakcodec_decoder_probe_video_stream_count(probe) } -} - -/// `oakcodec_decoder_probe_get_video_stream(probe, index, out)`. -pub fn probe_get_video_stream(probe: ProbeHandle, index: c_int, out: &mut VideoStreamInfo) -> Result<()> { - // Direct call into the `oakcodec` crate (single-lib unification; the - // `#[no_mangle]` export stays for the external C ABI). `VideoStreamInfo` - // is layout-identical to oakcodec's `OakCodecVideoStreamInfo`, so the - // pointer crosses via a cast. - let rc = unsafe { - oakcodec::ffi::decoder::oakcodec_decoder_probe_get_video_stream( - probe, - index, - out as *mut VideoStreamInfo as *mut oakcodec::decoder::OakCodecVideoStreamInfo, - ) - }; - if rc == 0 { - Ok(()) - } else { - Err(Error::Failed(format!("probe_get_video_stream rc={rc}"))) - } -} - -/// `oakcodec_decoder_init()` — owned decoder session; null on failure. -pub fn decoder_init() -> DecoderHandle { - // Direct call into the `oakcodec` crate (single-lib unification; the - // `#[no_mangle]` export stays for the external C ABI). - unsafe { oakcodec::ffi::decoder::oakcodec_decoder_init() } -} - -/// `oakcodec_decoder_free(decoder: *mut CHandle)` — NULL no-op. -pub fn decoder_free(decoder: &mut DecoderHandle) { - // Direct call into the `oakcodec` crate (single-lib unification; the - // `#[no_mangle]` export stays for the external C ABI). - unsafe { oakcodec::ffi::decoder::oakcodec_decoder_free(decoder) } -} - -/// `oakcodec_decoder_open(decoder, filename, stream_index)`. -pub fn decoder_open(decoder: DecoderHandle, filename: &str, stream_index: c_int) -> Result<()> { - let c = std::ffi::CString::new(filename).unwrap_or_default(); - // Direct call into the `oakcodec` crate (single-lib unification; the - // `#[no_mangle]` export stays for the external C ABI). - let rc = unsafe { oakcodec::ffi::decoder::oakcodec_decoder_open(decoder, c.as_ptr(), stream_index) }; - if rc == 0 { - Ok(()) - } else { - Err(Error::Failed(format!("decoder_open rc={rc}"))) - } -} - -/// `oakcodec_decoder_close(decoder)`. -pub fn decoder_close(decoder: DecoderHandle) -> Result<()> { - // Direct call into the `oakcodec` crate (single-lib unification; the - // `#[no_mangle]` export stays for the external C ABI). - let rc = unsafe { oakcodec::ffi::decoder::oakcodec_decoder_close(decoder) }; - if rc == 0 { - Ok(()) - } else { - Err(Error::Failed(format!("decoder_close rc={rc}"))) - } -} - -/// `oakcodec_decoder_decode_video(decoder, num, den)` — owned frame -/// handle; null when not decodable at `time`. -pub fn decoder_decode_video(decoder: DecoderHandle, num: i64, den: i64) -> CodecFrameHandle { - // Direct call into the `oakcodec` crate (single-lib unification; the - // `#[no_mangle]` export stays for the external C ABI). - unsafe { oakcodec::ffi::decoder::oakcodec_decoder_decode_video(decoder, num as c_int, den as c_int) } -} - -/// `oakcodec_decoder_decode_audio` — decode interleaved f32 covering -/// `[in, out)` into `buf` (at least `buf_frames` frames, interleaved by -/// `channel_layout`). Returns the number of frames written or a negative -/// error. -pub fn decoder_decode_audio( - decoder: DecoderHandle, - in_num: i64, - in_den: i64, - out_num: i64, - out_den: i64, - sample_rate: c_int, - channel_layout: u64, - buf: *mut f32, - buf_frames: c_int, -) -> Result { - // Direct call into the `oakcodec` crate (single-lib unification; the - // `#[no_mangle]` export stays for the external C ABI). - let rc = unsafe { - oakcodec::ffi::decoder::oakcodec_decoder_decode_audio( - decoder, - in_num as c_int, - in_den as c_int, - out_num as c_int, - out_den as c_int, - sample_rate, - channel_layout, - buf, - buf_frames, - ) - }; - if rc < 0 { - return Err(Error::Failed(format!("decode_audio rc={rc}"))); - } - Ok(rc) -} - -/// `oakcodec_decoder_last_error(decoder, buf, size)` — error detail. -pub fn decoder_last_error(decoder: DecoderHandle) -> String { - let mut buf = [0 as c_char; 512]; - // Direct call into the `oakcodec` crate (single-lib unification; the - // `#[no_mangle]` export stays for the external C ABI). - let n = unsafe { - oakcodec::ffi::decoder::oakcodec_decoder_last_error(decoder, buf.as_mut_ptr(), buf.len() as c_int) - }; - if n <= 0 { - return String::new(); - } - let len = (n as usize).min(buf.len()); - let bytes: Vec = buf[..len] - .iter() - .take_while(|&&b| b != 0) - .map(|&b| b as u8) - .collect(); - String::from_utf8_lossy(&bytes).into_owned() -} - -// --------------------------------------------------------------------------- -// Decoded frame accessors (`oakcodec_frame_*`) -// --------------------------------------------------------------------------- - -/// `oakcodec_frame_width(frame)`; 0 on an empty frame. -pub fn frame_width(frame: CodecFrameHandle) -> c_int { - // Direct call into the `oakcodec` crate (single-lib unification; the - // `#[no_mangle]` export stays for the external C ABI). - unsafe { oakcodec::ffi::frame::oakcodec_frame_width(frame) } -} - -/// `oakcodec_frame_height(frame)`; 0 on an empty frame. -pub fn frame_height(frame: CodecFrameHandle) -> c_int { - // Direct call into the `oakcodec` crate (single-lib unification; the - // `#[no_mangle]` export stays for the external C ABI). - unsafe { oakcodec::ffi::frame::oakcodec_frame_height(frame) } -} - -/// `oakcodec_frame_format(frame)`; `OAKCOMMON_PIXEL_FORMAT_INVALID` on an -/// empty frame. -pub fn frame_format(frame: CodecFrameHandle) -> c_int { - // Direct call into the `oakcodec` crate (single-lib unification; the - // `#[no_mangle]` export stays for the external C ABI). - unsafe { oakcodec::ffi::frame::oakcodec_frame_format(frame) } -} - -/// `oakcodec_frame_linesize_bytes(frame)`; 0 on an empty frame. -pub fn frame_linesize_bytes(frame: CodecFrameHandle) -> c_int { - // Direct call into the `oakcodec` crate (single-lib unification; the - // `#[no_mangle]` export stays for the external C ABI). - unsafe { oakcodec::ffi::frame::oakcodec_frame_linesize_bytes(frame) } -} - -/// `oakcodec_frame_is_allocated(frame)`; 0 on an empty frame. -pub fn frame_is_allocated(frame: CodecFrameHandle) -> c_int { - // Direct call into the `oakcodec` crate (single-lib unification; the - // `#[no_mangle]` export stays for the external C ABI). - unsafe { oakcodec::ffi::frame::oakcodec_frame_is_allocated(frame) } -} - -/// `oakcodec_frame_const_data(frame)` — read-only pixel buffer pointer. -/// -/// # Safety -/// The returned pointer is valid while `frame` is alive and allocated. -pub unsafe fn frame_const_data(frame: CodecFrameHandle) -> *const u8 { - // Direct call into the `oakcodec` crate (single-lib unification; the - // `#[no_mangle]` export stays for the external C ABI). - unsafe { oakcodec::ffi::frame::oakcodec_frame_const_data(frame) as *const u8 } -} - -/// `oakcodec_frame_get_timestamp(frame, num, den)`. -pub fn frame_timestamp(frame: CodecFrameHandle) -> Option<(i64, i64)> { - let mut num: c_int = 0; - let mut den: c_int = 0; - // Direct call into the `oakcodec` crate (single-lib unification; the - // `#[no_mangle]` export stays for the external C ABI). - let rc = unsafe { oakcodec::ffi::frame::oakcodec_frame_get_timestamp(frame, &mut num, &mut den) }; - if rc == 0 && den != 0 { - Some((num as i64, den as i64)) - } else { - None - } -} - -/// `oakcodec_frame_free(frame: *mut CHandle)` — NULL no-op. -pub fn frame_free(frame: &mut CodecFrameHandle) { - // Direct call into the `oakcodec` crate (single-lib unification; the - // `#[no_mangle]` export stays for the external C ABI). - unsafe { oakcodec::ffi::frame::oakcodec_frame_free(frame) } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn bad_inputs_fail_explainably() { - // Wrapper contract with bad input: every call returns the - // documented fallback rather than panicking (the real oakcodec - // guards each export against empty handles / missing files). - assert!(decoder_probe("/nonexistent_oak_test.mp4").is_null()); - - // decoder_init always succeeds (a closed session handle) and must - // be released. - let mut d = decoder_init(); - assert!(!d.is_null()); - assert!(decoder_open(d, "f", 0).is_err()); - // Closing a session that is not open is a successful no-op. - assert!(decoder_close(d).is_ok()); - assert!(decoder_decode_video(d, 0, 1).is_null()); - decoder_free(&mut d); - assert!(d.is_null()); - - // Frame accessors on an empty handle hit the documented fallbacks. - let empty = CHandle::null(); - assert_eq!(frame_width(empty), 0); - assert_eq!(frame_height(empty), 0); - assert_eq!(frame_linesize_bytes(empty), 0); - assert_eq!(frame_format(empty), -1); - assert!(frame_timestamp(empty).is_none()); - } -} diff --git a/crates/oakrender/src/bridge/common.rs b/crates/oakrender/src/bridge/common.rs deleted file mode 100644 index d8cefcf81..000000000 --- a/crates/oakrender/src/bridge/common.rs +++ /dev/null @@ -1,148 +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 . - -//! oakcommon C ABI calls (config, file functions, strings) — now direct -//! Rust calls into the oakcommon crate (single-lib unification, see -//! `docs/zh/plans/riir/single-lib.md`). The configuration-location and -//! disk-cache helpers delegate to oakcommon's own implementation. - -use std::ffi::{c_char, c_int, c_void}; -use std::sync::Mutex; - -/// `oakcore_audioparams_sample_rate` (host-provided; M12 P1 — the audio -/// ticket reads the output format from the params handle). -pub fn audioparams_sample_rate(params: *const c_void) -> c_int { - unsafe extern "C" { - fn oakcore_audioparams_sample_rate(params: *const c_void) -> c_int; - } - unsafe { oakcore_audioparams_sample_rate(params) } -} - -/// `oakcore_audioparams_channel_layout` (host-provided). -pub fn audioparams_channel_layout(params: *const c_void) -> u64 { - unsafe extern "C" { - fn oakcore_audioparams_channel_layout(params: *const c_void) -> u64; - } - unsafe { oakcore_audioparams_channel_layout(params) } -} - -/// `oakcore_audioparams_channel_count` (host-provided). -pub fn audioparams_channel_count(params: *const c_void) -> c_int { - unsafe extern "C" { - fn oakcore_audioparams_channel_count(params: *const c_void) -> c_int; - } - unsafe { oakcore_audioparams_channel_count(params) } -} - -/// Read a config string via the two-stage C ABI -/// (`oakcommon_config_get(group, key, buf, n)`, C++ `Config::current() -/// [key].toString()`). -pub fn config_get_string(group: Option<&str>, key: &str) -> Option { - let group_c = group.and_then(|g| std::ffi::CString::new(g).ok()); - let key_c = std::ffi::CString::new(key).ok()?; - let group_ptr = || group_c.as_ref().map_or(std::ptr::null(), |c| c.as_ptr()); - let size = unsafe { - oakcommon::ffi::config::oakcommon_config_get( - group_ptr(), - key_c.as_ptr(), - std::ptr::null_mut(), - 0, - ) - }; - if size <= 1 { - return None; // missing or empty - } - let mut buf = vec![0u8; size as usize]; - let got = unsafe { - oakcommon::ffi::config::oakcommon_config_get( - group_ptr(), - key_c.as_ptr(), - buf.as_mut_ptr() as *mut c_char, - size, - ) - }; - if got <= 0 { - return None; - } - let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len()); - Some(String::from_utf8_lossy(&buf[..end]).into_owned()) -} - -/// `oakcommon_config_get_int(group, key, default)`. -pub fn config_get_int(group: Option<&str>, key: &str, default: i32) -> i32 { - let group_c = group.and_then(|g| std::ffi::CString::new(g).ok()); - let key_c = match std::ffi::CString::new(key) { - Ok(c) => c, - Err(_) => return default, - }; - let group_ptr = group_c.as_ref().map_or(std::ptr::null(), |c| c.as_ptr()); - unsafe { oakcommon::ffi::config::oakcommon_config_get_int(group_ptr, key_c.as_ptr(), default) } -} - -/// The configuration directory — oakcommon's implementation -/// (`FileFunctions::get_configuration_location`, honoring `OAK_CONFIG_DIR` -/// and the platform fallbacks). -pub fn configuration_location() -> String { - oakcommon::filefunctions::FileFunctions::new() - .get_configuration_location() - .unwrap_or_default() -} - -/// Serializes tests that mutate `OAK_CONFIG_DIR` / `OAK_RENDER_BACKEND` -/// (env is process-global; the manager tests share this lock too). -pub static ENV_TEST_LOCK: Mutex<()> = Mutex::new(()); - -/// The default disk cache directory (C++ `DiskManager:: -/// get_default_disk_cache_path`): `/mediacache`. -pub fn default_disk_cache_path() -> String { - oakcommon::filefunctions::default_disk_cache_path() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn config_missing_key_falls_back() { - // The real config store is empty under cargo test: defaults apply. - assert_eq!(config_get_int(None, "GraphicsBackend", 7), 7); - assert_eq!(config_get_string(None, "missing"), None); - } - - #[test] - fn configuration_location_uses_env_override() { - let _guard = crate::bridge::common::ENV_TEST_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); - let dir = std::env::temp_dir().join("oakrender-config-test"); - std::env::set_var("OAK_CONFIG_DIR", &dir); - let loc = configuration_location(); - assert_eq!(loc, dir.to_string_lossy()); - std::env::remove_var("OAK_CONFIG_DIR"); - } - - #[test] - fn default_disk_cache_path_is_under_config() { - let _guard = crate::bridge::common::ENV_TEST_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); - let dir = std::env::temp_dir().join("oakrender-cache-test"); - std::env::set_var("OAK_CONFIG_DIR", &dir); - let p = default_disk_cache_path(); - assert!(p.ends_with("/mediacache") || p.ends_with("\\mediacache")); - std::env::remove_var("OAK_CONFIG_DIR"); - } -} diff --git a/crates/oakrender/src/bridge/mod.rs b/crates/oakrender/src/bridge/mod.rs deleted file mode 100644 index e50e6fd5d..000000000 --- a/crates/oakrender/src/bridge/mod.rs +++ /dev/null @@ -1,29 +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 . - -//! C ABI bridges to sibling oak modules. -//! -//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every -//! submodule is a compile-time Rust call into the target crate's `ffi` -//! (the `#[no_mangle]` exports stay in the dylib for the external C ABI; -//! internal callers bypass them). Handles cross as the shared -//! `oakcore_rs::handle::CHandle`. The only exception is the copier -//! direction in [`node`], which targets symbols oaknode never implemented -//! and is documented there. - -pub mod codec; -pub mod common; -pub mod node; diff --git a/crates/oakrender/src/bridge/node.rs b/crates/oakrender/src/bridge/node.rs deleted file mode 100644 index 0648b6cd0..000000000 --- a/crates/oakrender/src/bridge/node.rs +++ /dev/null @@ -1,182 +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 . - -//! oaknode C ABI bridge: direct Rust calls into the `oaknode` crate -//! (project copies, node queries). -//! -//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): the -//! live node query below is a compile-time Rust call into `oaknode`'s -//! `ffi`; handles cross as the shared [`crate::handle::CHandle`] -//! (`oakcore_rs::handle::CHandle`). -//! -//! The copier direction (`project_deep_copy` / `project_sync_copy`) has -//! **no oaknode implementation**: the `oaknode_project_*` symbols were -//! declared by the old render bridge but never existed in any crate -//! (single-lib plan §4.1 — dead direction). They stay as documented -//! always-fail stubs with the exact previous fallback semantics (empty -//! handle / explainable error / `node_abi_available() == false`). - -use std::ffi::c_int; - -use crate::error::{Error, Result}; -use crate::handle::CHandle; - -/// oaknode project handle. -pub type ProjectHandle = CHandle; -/// oaknode node handle. -pub type NodeHandle = CHandle; - -/// Mirror of oaknode's change record (marshalled as plain C structs; -/// layout per include/node/project.h). -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct ChangeRecord { - /// Discriminant (see oaknode `ChangeRecord`). - pub kind: u32, - /// Payload bytes (per-kind layout documented in project.h). - pub payload: [u8; 48], -} - -/// Change-record discriminants (oaknode project.h). -pub mod change_kind { - /// Node added. - pub const NODE_ADD: u32 = 0; - /// Node removed. - pub const NODE_REMOVE: u32 = 1; - /// Edge added. - pub const EDGE_ADD: u32 = 2; - /// Edge removed. - pub const EDGE_REMOVE: u32 = 3; - /// Value change. - pub const VALUE_CHANGE: u32 = 4; - /// Value hint change. - pub const VALUE_HINT_CHANGE: u32 = 5; - /// Project setting change. - pub const PROJECT_SETTING_CHANGE: u32 = 6; - /// Footage proxy change. - pub const FOOTAGE_PROXY: u32 = 7; -} - -/// `oaknode_project_deep_copy(project)` — would return an owned -/// copied-project handle. -/// -/// Never implemented: oaknode has no such C ABI export (single-lib plan -/// §4.1 — dead direction), so this always yields the empty handle, exactly -/// as the previous runtime-symbol lookup did when the symbol was absent. -pub fn project_deep_copy(_project: ProjectHandle) -> CHandle { - CHandle::null() -} - -/// `oaknode_project_sync_copy(source, copy, changes, count)` — would push -/// a recorded change set into the copy. -/// -/// Never implemented: oaknode has no such C ABI export (single-lib plan -/// §4.1 — dead direction), so this always fails explainably, exactly as -/// the previous runtime-symbol lookup did when the symbol was absent. -pub fn project_sync_copy( - _source: ProjectHandle, - _copy: ProjectHandle, - _changes: &[ChangeRecord], -) -> Result<()> { - Err(Error::Failed( - "oaknode_project_sync_copy missing (not implemented in oaknode)".into(), - )) -} - -/// `oaknode_node_get_video_frame_cache(node, out)` — borrowed cache -/// handle of a node. `OAKNODE_OK` (0) on success with `*out` set; -/// otherwise a negative error. -/// -/// # Safety -/// `node` must be a valid handle; `out` a valid pointer. -pub unsafe fn node_get_video_frame_cache(node: NodeHandle, out: *mut CHandle) -> Result<()> { - // Direct call into the `oaknode` crate (single-lib unification; the - // `#[no_mangle]` export stays for the external C ABI). - let rc = unsafe { - oaknode::ffi::node::oaknode_node_get_video_frame_cache(node, out as *mut std::ffi::c_void) - }; - if rc == 0 { - Ok(()) - } else { - Err(Error::Failed(format!( - "oaknode_node_get_video_frame_cache rc={rc}" - ))) - } -} - -/// Whether the oaknode deep-copy C ABI is available. -/// -/// Constant `false`: `oaknode_project_deep_copy` was never implemented -/// (single-lib plan §4.1), so the copier success-path tests stay gated -/// exactly as before. -pub fn node_abi_available() -> bool { - false -} - -/// Node identity of a handle (the box pointer; matches the copier's -/// identity tracking). -pub fn node_identity(node: &NodeHandle) -> u64 { - node.ctx as u64 -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn deep_copy_unavailable_yields_empty_handle() { - // oaknode never implemented the deep-copy ABI (dead direction). - let h = project_deep_copy(CHandle::null()); - assert!(h.is_null()); - } - - #[test] - fn sync_copy_unavailable_fails_explainably() { - let changes = [ChangeRecord { - kind: change_kind::NODE_ADD, - payload: [0u8; 48], - }]; - let rc = project_sync_copy(CHandle::null(), CHandle::null(), &changes); - assert!(rc.is_err(), "not implemented → explainable failure"); - } - - #[test] - fn change_record_layout_is_c_stable() { - // kind first, then 48 payload bytes (include/node/project.h). - let c = ChangeRecord { - kind: change_kind::VALUE_CHANGE, - payload: [7u8; 48], - }; - assert_eq!(std::mem::size_of::(), 4 + 48); - let bytes: [u8; 52] = unsafe { std::mem::transmute(c) }; - assert_eq!( - u32::from_le_bytes(bytes[0..4].try_into().unwrap()), - change_kind::VALUE_CHANGE - ); - assert_eq!(bytes[4], 7); - } - - #[test] - fn node_identity_is_ctx_value() { - let h = CHandle { - ctx: 0x1234 as *mut std::ffi::c_void, - addref: None, - release: None, - abi_version: 1, - }; - assert_eq!(node_identity(&h), 0x1234); - } -} diff --git a/crates/oakrender/src/cache.rs b/crates/oakrender/src/cache.rs index 78f8cb232..272332e7c 100644 --- a/crates/oakrender/src/cache.rs +++ b/crates/oakrender/src/cache.rs @@ -261,7 +261,7 @@ pub struct PlaybackCache { impl PlaybackCache { /// New cache for `owner` (C++ `PlaybackCache(parent)`). pub fn new(kind: CacheKind, owner: OwnerIdentity) -> Self { - let disk_dir = crate::bridge::common::default_disk_cache_path(); + let disk_dir = crate::commonutil::default_disk_cache_path(); Self { kind, owner, diff --git a/crates/oakrender/src/color.rs b/crates/oakrender/src/color.rs index 599d8a066..5fc4ea031 100644 --- a/crates/oakrender/src/color.rs +++ b/crates/oakrender/src/color.rs @@ -391,7 +391,7 @@ pub fn config_path() -> Option { } Some(format!( "{}/ocioconf/config.ocio", - crate::bridge::common::configuration_location() + crate::commonutil::configuration_location() )) } diff --git a/crates/oakrender/src/commonutil.rs b/crates/oakrender/src/commonutil.rs new file mode 100644 index 000000000..1398db051 --- /dev/null +++ b/crates/oakrender/src/commonutil.rs @@ -0,0 +1,87 @@ +// 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 . + +//! oakcommon helpers (config, file functions) — direct Rust calls into +//! the oakcommon crate (single-lib unification; the former +//! `bridge/common.rs`). The configuration-location and disk-cache +//! helpers delegate to oakcommon's own implementation. + +use std::sync::Mutex; + +/// Read a config string via the domain store +/// (`ConfigStore::get(group, key)`); `None` when missing or empty. +pub fn config_get_string(group: Option<&str>, key: &str) -> Option { + oakcommon::configstore::ConfigStore::instance() + .get(group, key) + .ok() + .filter(|s| !s.is_empty()) +} + +/// `oakcommon_config_get_int(group, key, default)`. +pub fn config_get_int(group: Option<&str>, key: &str, default: i32) -> i32 { + oakcommon::configstore::ConfigStore::instance().get_int(group, key, default) +} + +/// The configuration directory — oakcommon's implementation +/// (`FileFunctions::get_configuration_location`, honoring `OAK_CONFIG_DIR` +/// and the platform fallbacks). +pub fn configuration_location() -> String { + oakcommon::filefunctions::FileFunctions::new() + .get_configuration_location() + .unwrap_or_default() +} + +/// Serializes tests that mutate `OAK_CONFIG_DIR` / `OAK_RENDER_BACKEND` +/// (env is process-global; the manager tests share this lock too). +pub static ENV_TEST_LOCK: Mutex<()> = Mutex::new(()); + +/// The default disk cache directory (C++ `DiskManager:: +/// get_default_disk_cache_path`): `/mediacache`. +pub fn default_disk_cache_path() -> String { + oakcommon::filefunctions::default_disk_cache_path() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn config_missing_key_falls_back() { + // The real config store is empty under cargo test: defaults apply. + assert_eq!(config_get_int(None, "GraphicsBackend", 7), 7); + assert_eq!(config_get_string(None, "missing"), None); + } + + #[test] + fn configuration_location_uses_env_override() { + let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let dir = std::env::temp_dir().join("oakrender-config-test"); + std::env::set_var("OAK_CONFIG_DIR", &dir); + let loc = configuration_location(); + assert_eq!(loc, dir.to_string_lossy()); + std::env::remove_var("OAK_CONFIG_DIR"); + } + + #[test] + fn default_disk_cache_path_is_under_config() { + let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let dir = std::env::temp_dir().join("oakrender-cache-test"); + std::env::set_var("OAK_CONFIG_DIR", &dir); + let p = default_disk_cache_path(); + assert!(p.ends_with("/mediacache") || p.ends_with("\\mediacache")); + std::env::remove_var("OAK_CONFIG_DIR"); + } +} diff --git a/crates/oakrender/src/copier.rs b/crates/oakrender/src/copier.rs index 3e4b716d1..bd7780d57 100644 --- a/crates/oakrender/src/copier.rs +++ b/crates/oakrender/src/copier.rs @@ -15,18 +15,68 @@ // along with this program. If not, see . //! Render-side project copy client (the C++ ProjectCopier, inverted): -//! all copying happens inside oaknode -//! (`oaknode_project_deep_copy` / `sync_copy`); this module only tracks -//! which copy belongs to which viewer and when to re-sync. -//! -//! The oaknode C ABI functions go through [`crate::bridge::node`]; oaknode -//! never implemented the deep-copy symbols (single-lib plan §4.1 — dead -//! direction), so the copy operations fail explainably and the -//! success-path tests are `#[ignore]`d. +//! all copying happens inside oaknode. oaknode never implemented the +//! deep-copy direction (single-lib plan §4.1 — dead direction), so the +//! copy operations fail explainably and the success-path tests are +//! `#[ignore]`d. -use crate::bridge::node::{ChangeRecord, ProjectHandle}; use crate::error::{Error, Result}; +/// A project handle (oaknode-owned; the shared canonical handle type). +pub type ProjectHandle = crate::handle::CHandle; + +/// One change record (see oaknode `ChangeRecord`). +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ChangeRecord { + /// Discriminant (see oaknode `ChangeRecord`). + pub kind: u32, + /// Payload bytes (per-kind layout documented in project.h). + pub payload: [u8; 48], +} + +/// Change-record discriminants (oaknode project.h). +pub mod change_kind { + /// Node added. + pub const NODE_ADD: u32 = 0; + /// Node removed. + pub const NODE_REMOVE: u32 = 1; + /// Edge added. + pub const EDGE_ADD: u32 = 2; + /// Edge removed. + pub const EDGE_REMOVE: u32 = 3; + /// Value change. + pub const VALUE_CHANGE: u32 = 4; + /// Value hint change. + pub const VALUE_HINT_CHANGE: u32 = 5; + /// Project setting change. + pub const PROJECT_SETTING_CHANGE: u32 = 6; + /// Footage proxy change. + pub const FOOTAGE_PROXY: u32 = 7; +} + +/// `oaknode_project_deep_copy(project)` — would return an owned +/// copied-project handle. +/// +/// Never implemented: oaknode has no such Rust function (single-lib plan +/// §4.1 — dead direction), so this always yields the empty handle, exactly +/// as the previous runtime-symbol lookup did when the symbol was absent. +pub fn project_deep_copy(_project: ProjectHandle) -> ProjectHandle { + ProjectHandle::null() +} + +/// `oaknode_project_sync_copy` — never implemented (dead direction); the +/// sync always fails explainably. +pub fn project_sync_copy( + _source: ProjectHandle, + _copy: ProjectHandle, + _changes: &[ChangeRecord], +) -> Result<()> { + Err(Error::Failed( + "oaknode_project_sync_copy missing (not implemented in oaknode)".into(), + )) +} + /// A handle to a render-side project copy. pub struct ProjectCopy { /// Identity of the source project. @@ -62,7 +112,7 @@ impl ProjectCopy { } // Release any previous copy. self.release_copy(); - let copy = crate::bridge::node::project_deep_copy(source); + let copy = crate::copier::project_deep_copy(source); if copy.is_null() { return Err(Error::Failed( "oaknode_project_deep_copy failed (symbol missing or copy error)".into(), @@ -89,7 +139,7 @@ impl ProjectCopy { if copy.is_null() { return Err(Error::State); } - crate::bridge::node::project_sync_copy(source, copy, changes)?; + crate::copier::project_sync_copy(source, copy, changes)?; self.last_sync_generation += 1; self.has_pending_updates = false; Ok(()) @@ -163,7 +213,7 @@ mod tests { fn sync_without_project_is_state_error() { let mut pc = ProjectCopy::new(); let changes = [ChangeRecord { - kind: crate::bridge::node::change_kind::NODE_ADD, + kind: crate::copier::change_kind::NODE_ADD, payload: [0u8; 48], }]; assert_eq!(pc.sync(&changes).unwrap_err().code(), Error::State.code()); @@ -177,9 +227,10 @@ mod tests { } #[test] - #[ignore = "needs oaknode C ABI (oaknode_project_deep_copy)"] + #[ignore = "needs oaknode deep-copy (not implemented)"] fn deep_copy_roundtrip_with_real_node() { - // Requires a live liboaknode; run with the app linked. + // oaknode never implemented the deep-copy direction; the copy + // always fails explainably, which is what the live path checks. let mut pc = ProjectCopy::new(); let src = ProjectHandle { ctx: 1 as *mut std::ffi::c_void, @@ -187,10 +238,8 @@ mod tests { release: None, abi_version: crate::handle::OAKRENDER_ABI_VERSION, }; - if crate::bridge::node::node_abi_available() { - pc.set_project(src).unwrap(); - assert_ne!(pc.copy, 0); - assert!(pc.copied_project().is_some()); - } + pc.set_project(src).unwrap(); + assert_ne!(pc.copy, 0); + assert!(pc.copied_project().is_some()); } } diff --git a/crates/oakrender/src/eval.rs b/crates/oakrender/src/eval.rs index 386f49609..e15e62301 100644 --- a/crates/oakrender/src/eval.rs +++ b/crates/oakrender/src/eval.rs @@ -25,9 +25,14 @@ //! depend on the oakcodec / oaknode / oakplugin C ABIs and fail with //! explainable errors (their success-path tests are `#[ignore]`d). -use std::ffi::c_int; +use std::sync::Arc; -use oakcore_rs::{PixelFormat, Rational}; +use oakcodec::decoder::{ + CodecStream, Decoder as _, RenderMode, RetrieveAudioStatus, RetrieveVideoParams, + K_COLOR_RANGE_DEFAULT, +}; +use oakcodec::ffmpeg::FFmpegDecoder; +use oakcore_rs::{PixelFormat, Rational, TimeRange}; use crate::error::{Error, Result}; use crate::frame::VideoParamsPod; @@ -261,11 +266,13 @@ pub fn render_produced_frame( /// Process-wide open decoder sessions, keyed by (filename, stream). /// Sessions are mutex-serialized inside the oakcodec box, so sharing /// one handle across worker threads is safe. -static DECODERS: std::sync::OnceLock, ->> = std::sync::OnceLock::new(); +static DECODERS: std::sync::OnceLock< + std::sync::Mutex>>, +> = std::sync::OnceLock::new(); -fn decoders() -> std::sync::MutexGuard<'static, std::collections::HashMap<(String, i32), crate::handle::CHandle>> { +fn decoders( +) -> std::sync::MutexGuard<'static, std::collections::HashMap<(String, i32), Arc>> +{ DECODERS .get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new())) .lock() @@ -273,23 +280,20 @@ fn decoders() -> std::sync::MutexGuard<'static, std::collections::HashMap<(Strin } /// Open (or reuse) the decoder session for `(filename, stream_index)`. -fn open_decoder(filename: &str, stream_index: i32) -> Result { +fn open_decoder(filename: &str, stream_index: i32) -> Result> { { let cache = decoders(); - if let Some(h) = cache.get(&(filename.to_string(), stream_index)) { - if !h.is_null() { - return Ok(*h); - } + if let Some(d) = cache.get(&(filename.to_string(), stream_index)) { + return Ok(d.clone()); } } - let decoder = crate::bridge::codec::decoder_init(); - if decoder.is_null() { - return Err(Error::Failed("footage decode: decoder_init failed".into())); - } - crate::bridge::codec::decoder_open(decoder, filename, stream_index) + let decoder: Arc = Arc::new(FFmpegDecoder::new()); + let stream = CodecStream::with_block(filename.to_string(), stream_index, None); + decoder + .open(&stream) .map_err(|e| Error::Failed(format!("footage decode open: {e:?}")))?; let mut cache = decoders(); - cache.insert((filename.to_string(), stream_index), decoder); + cache.insert((filename.to_string(), stream_index), decoder.clone()); Ok(decoder) } @@ -303,49 +307,44 @@ pub fn render_footage_frame( format: PixelFormat, ) -> Result { let decoder = open_decoder(filename, stream_index)?; - let frame_handle = crate::bridge::codec::decoder_decode_video( - decoder, - time.numerator(), - time.denominator(), - ); - if frame_handle.is_null() { - let detail = crate::bridge::codec::decoder_last_error(decoder); - return Err(Error::Failed(format!( - "footage decode at {time:?}: {detail}" - ))); - } + let params = RetrieveVideoParams { + stream: CodecStream::with_block(filename.to_string(), stream_index, None), + time, + length: TimeRange::default(), + force_range: K_COLOR_RANGE_DEFAULT, + is_image_sequence: false, + image_sequence_digits: 0, + image_sequence_number: 0, + mode: RenderMode::Offline, + alpha_is_premultiplied: false, + }; + let decoded = decoder + .retrieve_video_frame(¶ms) + .map_err(|e| Error::Failed(format!("footage decode at {time:?}: {e:?}")))?; - let src_w = crate::bridge::codec::frame_width(frame_handle); - let src_h = crate::bridge::codec::frame_height(frame_handle); - let src_linesize = crate::bridge::codec::frame_linesize_bytes(frame_handle); - let _alloc = crate::bridge::codec::frame_is_allocated(frame_handle); + let src_w = decoded.width(); + let src_h = decoded.height(); + let src_linesize = decoded.linesize_bytes(); let (w, h) = size; - if src_w <= 0 - || src_h <= 0 - || src_linesize <= 0 - || crate::bridge::codec::frame_is_allocated(frame_handle) == 0 - { - frame_free(frame_handle); + if src_w <= 0 || src_h <= 0 || src_linesize <= 0 || !decoded.is_allocated() { return Err(Error::Failed("footage decode: bad decoded frame".into())); } let mut dst = generate_frame(time, (w, h), format)?; let dst_linesize = dst.linesize_bytes() as i32; - let src_data = unsafe { crate::bridge::codec::frame_const_data(frame_handle) }; - if src_data.is_null() { - frame_free(frame_handle); - return Err(Error::Failed("footage decode: no frame data".into())); - } + let src_data = match decoded.data() { + Some(d) => d, + None => return Err(Error::Failed("footage decode: no frame data".into())), + }; if src_w == w && src_h == h && src_linesize == dst_linesize { let bytes = (src_h as usize) .checked_mul(src_linesize as usize) .ok_or(Error::NoMem)?; - let src_slice = unsafe { std::slice::from_raw_parts(src_data, bytes) }; - dst.data[..bytes].copy_from_slice(src_slice); + dst.data[..bytes].copy_from_slice(&src_data[..bytes]); } else { scale_rgba_f32( - src_data, + src_data.as_ptr(), src_linesize, src_w, src_h, @@ -355,7 +354,6 @@ pub fn render_footage_frame( h, ); } - frame_free(frame_handle); Ok(Texture::wrap_frame(dst)) } @@ -402,19 +400,14 @@ pub fn render_audio_samples( let media_end = media_start + (out_time - in_time); let mut buf = vec![0.0f32; frames * channels as usize]; let decoder = open_decoder(&clip.filename, clip.stream_index)?; - let rc = crate::bridge::codec::decoder_decode_audio( - decoder, - media_start.numerator(), - media_start.denominator(), - media_end.numerator(), - media_end.denominator(), - rate, - params.channel_layout, - buf.as_mut_ptr(), - frames as c_int, - ); - let written = rc.unwrap_or(0).max(0) as usize; - let written = written.min(frames); + let range = TimeRange::new(media_start, media_end); + let status = decoder + .retrieve_audio(&mut buf, &range, rate, params.channel_layout) + .map_err(|e| Error::Failed(format!("footage audio decode: {e:?}")))?; + let written = match status { + RetrieveAudioStatus::Success => frames, + _ => 0, + }; // Mix into the accumulator (per-channel gain). for i in 0..written * channels as usize { acc[start_frame * channels as usize + i] += buf[i] * clip.gain; @@ -429,11 +422,6 @@ pub fn render_audio_samples( })) } -/// Free a codec frame handle (Copy-handle dance). -fn frame_free(mut h: crate::handle::CHandle) { - crate::bridge::codec::frame_free(&mut h); -} - /// Bilinear scale an F32-RGBA image (row-major with per-row strides). fn scale_rgba_f32( src: *const u8, diff --git a/crates/oakrender/src/ffi.rs b/crates/oakrender/src/ffi.rs deleted file mode 100644 index a06c5fd61..000000000 --- a/crates/oakrender/src/ffi.rs +++ /dev/null @@ -1,2484 +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 . - -//! C ABI export layer: implements `include/render/*.h` verbatim. -//! -//! One submodule per public header; the header is the authoritative -//! function inventory. Every export goes through `handle::guard*`; -//! pointer/struct marshalling follows the C layout exactly. - -// Parameter names mirror the C headers verbatim (e.g. `p_or_NULL`). -#![allow(non_snake_case)] -#![allow(clippy::missing_safety_doc)] - -use std::ffi::{c_char, c_int, c_void}; - -use crate::handle::CHandle; - -// ---- handle type aliases (all oak render handle structs share the -// neutral {ctx, addref, release, abi_version} layout) ------------------------ - -/// `OakNodeNode` / `OakNodeProject` / `OakNodeColorManager` (by-value -/// handles from oaknode). -pub type OakNodeNode = CHandle; -/// OakNodeProject (forward-declared opaque). -pub type OakNodeProject = CHandle; -/// OakNodeColorManager. -pub type OakNodeColorManager = CHandle; -/// `OakVideoParams` / `OakColorTransform` / `OakAudioParams` (oakcommon / -/// oakcore value handles). -pub type OakVideoParams = CHandle; -/// OakColorTransform. -pub type OakColorTransform = CHandle; -/// OakAudioParams. -pub type OakAudioParams = CHandle; -/// `OakRenderCache` (render/cache.h). -pub type OakRenderCache = CHandle; -/// `OakRenderTicket` (render/ticket.h). -pub type OakRenderTicket = CHandle; -/// `OakRenderTexture` (render/renderer.h). -pub type OakRenderTexture = CHandle; -/// `OakCodecFrame` (render/renderer.h). -pub type OakCodecFrame = CHandle; -/// `OakColorProcessor` (render/color.h). -pub type OakColorProcessor = CHandle; -/// `OakRenderRenderer` (render/renderer.h). -pub type OakRenderRenderer = CHandle; -/// `OakRenderProjectCopier` (render/copier.h). -pub type OakRenderProjectCopier = CHandle; -/// `OakCancelAtom` (render/cancelatom.h). -pub type OakCancelAtom = CHandle; - -/// `oakrender_video_params` POD (render/renderer.h). -#[repr(C)] -#[derive(Clone, Copy, Debug, Default)] -pub struct OakRenderVideoParams { - /// Width. - pub width: c_int, - /// Height. - pub height: c_int, - /// Frame duration numerator. - pub time_base_num: c_int, - /// Frame duration denominator. - pub time_base_den: c_int, - /// PixelFormat as int. - pub format: c_int, - /// Pixel aspect numerator. - pub pixel_aspect_num: c_int, - /// Pixel aspect denominator. - pub pixel_aspect_den: c_int, - /// Interlacing as int. - pub interlacing: c_int, - /// Color range as int. - pub color_range: c_int, - /// Preview divider. - pub divider: c_int, - /// Video type. - pub video_type: c_int, - /// Premultiplied alpha 0/1. - pub premultiplied_alpha: c_int, -} - -/// `oakrender_video_ticket_params` (render/ticket.h). -#[repr(C)] -#[derive(Clone, Copy)] -pub struct OakVideoTicketParams { - /// Connected texture output node (borrowed). - pub output_node: OakNodeNode, - /// By-value oakcommon handle. - pub video_params: OakVideoParams, - /// Borrowed oakcore handle, may be null. - pub audio_params: *const OakAudioParams, - /// Frame timestamp as rational. - pub time_num: i64, - /// Frame timestamp as rational. - pub time_den: i64, - /// Borrowed, empty ctx = null. - pub color_manager: OakNodeColorManager, - /// 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: OakColorProcessor, - /// By value; empty ctx = default. - pub force_color_transform: OakColorTransform, - /// Borrowed frame cache; empty ctx = none. - pub cache: OakRenderCache, - /// Single-footage decode filename (null = off; M12 P0). - pub footage_filename: *const 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 OakMontageClip, - /// `montage` element count. - pub montage_count: c_int, -} - -/// One sequence-montage clip (M12 P0): the facade resolves the timeline -/// into this POD list; the render producer decodes and composites. -#[repr(C)] -#[derive(Clone, Copy)] -pub struct OakMontageClip { - /// Footage filename (borrowed; alive for the render call). - pub filename: *const 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, -} - -/// `oakrender_color_transform_job` (render/renderer.h). -#[repr(C)] -#[derive(Clone, Copy)] -pub struct OakColorTransformJob { - /// OakColorProcessor ctx (borrowed), may be null. - pub processor: *const c_void, - /// OakRenderTexture ctx (borrowed, not retained). - pub input_texture: *mut c_void, - /// 0=none, 1=associated. - pub input_alpha_association: c_int, - /// 0/1. - pub clear_destination: c_int, - /// 0/1. - pub force_opaque: c_int, - /// Column-major 4x4 (all-zero = identity). - pub matrix: [f32; 16], - /// Column-major 4x4 (all-zero = identity). - pub crop_matrix: [f32; 16], -} - -// ---- shared marshalling helpers -------------------------------------------- - -/// Two-stage string getter: returns the required buffer size including -/// NUL; writes only when the buffer is large enough. -fn write_string(s: &str, buf: *mut c_char, n: c_int) -> c_int { - let required = s.len() + 1; - if !buf.is_null() && n >= required as c_int { - unsafe { - std::ptr::copy_nonoverlapping(s.as_ptr() as *const c_char, buf, s.len()); - *buf.add(s.len()) = 0; - } - } - required as c_int -} - -/// Panic-catching wrapper for two-stage string getters: Ok(needed) is -/// returned verbatim (a non-negative size), Err maps to the error code. -fn guard_string crate::error::Result>(f: F) -> c_int { - match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) { - Ok(Ok(sz)) => sz, - Ok(Err(e)) => e.code(), - Err(_) => crate::error::OAKRENDER_E_FAILED, - } -} - -/// Map an `OakPixelFormat` int to the crate's pixel format. -fn oakcore_pixel_format(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, - } -} - -/// Read a NUL-terminated C string (None for null / non-UTF8). -unsafe fn cstr(p: *const c_char) -> Option { - if p.is_null() { - return None; - } - unsafe { std::ffi::CStr::from_ptr(p) } - .to_str() - .ok() - .map(|s| s.to_string()) -} - -/// A boxed `Frame` as an owned `OakCodecFrame` handle. -fn frame_handle(frame: crate::texture::Frame) -> CHandle { - crate::handle::make_owned(frame) -} - -/// View a borrowed ctx pointer (job structs carry `ctx` pointers by -/// value, see the header's "borrowed, not retained" notes) as a handle. -fn ctx_handle(ctx: *const c_void) -> CHandle { - CHandle { - ctx: ctx as *mut c_void, - addref: None, - release: None, - abi_version: crate::handle::OAKRENDER_ABI_VERSION, - } -} - -/// A borrowed opaque identity box (C++ borrowed `make_handle(…, false)`): -/// releasing it only frees the box. Never dereferenced by this crate. -#[derive(Clone, Copy, Debug)] -struct BorrowedOpaque { - #[allow(dead_code)] - identity: u64, -} - -/// Raw `userdata` pointer carried across threads (C convention: the -/// callback fires on the worker thread with the same userdata). -#[derive(Clone, Copy)] -struct RawSend(*mut c_void); -unsafe impl Send for RawSend {} - -impl RawSend { - fn ptr(self) -> *mut c_void { - self.0 - } -} - -/// Map a `VideoParamsPod` to the public POD. -fn pod_to_ffi(p: &crate::frame::VideoParamsPod) -> OakRenderVideoParams { - OakRenderVideoParams { - width: p.width, - height: p.height, - time_base_num: p.time_base_num, - time_base_den: p.time_base_den, - format: p.format, - pixel_aspect_num: p.pixel_aspect_num, - pixel_aspect_den: p.pixel_aspect_den, - interlacing: p.interlacing, - color_range: p.color_range, - divider: p.divider, - video_type: p.video_type, - premultiplied_alpha: p.premultiplied_alpha, - } -} - -/// Map the public POD to a `VideoParamsPod` (divider defaults to 1). -fn ffi_to_pod(p: &OakRenderVideoParams) -> crate::frame::VideoParamsPod { - crate::frame::VideoParamsPod { - width: p.width, - height: p.height, - time_base_num: p.time_base_num, - time_base_den: p.time_base_den, - format: p.format, - pixel_aspect_num: p.pixel_aspect_num, - pixel_aspect_den: p.pixel_aspect_den, - interlacing: p.interlacing, - color_range: p.color_range, - divider: if p.divider > 0 { p.divider } else { 1 }, - video_type: p.video_type, - premultiplied_alpha: p.premultiplied_alpha, - } -} - -// ============================================================================ -// render/manager.h -// ============================================================================ - -/// `include/render/manager.h` exports: the manager singleton lifecycle, -/// the async frame request path, the auto-cacher settings, and the disk -/// cache singleton. -pub mod manager { - use super::*; - use std::sync::atomic::{AtomicI64, Ordering}; - use std::sync::Mutex; - - /// In-flight frame requests: request id → (arena, ticket id). - static REQUESTS: std::sync::LazyLock< - Mutex< - std::collections::HashMap< - i64, - ( - std::sync::Arc, - crate::ticket::TicketId, - ), - >, - >, - > = std::sync::LazyLock::new(|| Mutex::new(std::collections::HashMap::new())); - static NEXT_REQUEST_ID: AtomicI64 = AtomicI64::new(1); - - /// `oakrender_manager_init`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_manager_init() -> c_int { - crate::handle::guard(|| { - crate::manager::RenderManager::init()?; - Ok(()) - }) - } - - /// `oakrender_manager_shutdown`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_manager_shutdown() { - crate::handle::guard_void(|| { - crate::manager::RenderManager::shutdown(); - lock().clear(); - }); - } - - /// `oakrender_manager_available`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_manager_available() -> c_int { - if crate::manager::RenderManager::global().is_some() { - 1 - } else { - 0 - } - } - - /// `oakrender_request_frame` — async single-frame render with a - /// completion callback. `ts` is a frame number; without the viewer's - /// timebase (node bridge pending) it is treated as whole seconds. - #[no_mangle] - pub unsafe extern "C" fn oakrender_request_frame( - viewer: OakNodeNode, - ts: i64, - cb: Option, - userdata: *mut c_void, - ) -> i64 { - if viewer.is_null() || cb.is_none() { - return crate::error::OAKRENDER_E_INVALID as i64; - } - let Some(manager) = crate::manager::RenderManager::global() else { - return crate::error::OAKRENDER_E_STATE as i64; - }; - let time = oakcore_rs::Rational::from_double(ts as f64); - let rid = NEXT_REQUEST_ID.fetch_add(1, Ordering::Relaxed); - let arena = manager.tickets.clone(); - let cb = cb.unwrap(); - let userdata = RawSend(userdata); - - let mut cacher = manager.get_cacher(); - let cacher = cacher.as_mut().expect("cacher lazily created"); - cacher.set_viewer_identity(Some(viewer.ctx as u64)); - let ticket = cacher.single_frame_with_completion(time, { - let _arena = arena.clone(); - Box::new(move |result| { - let handle = match result { - Ok(crate::ticket::TicketPayload::Video(tex)) => match tex.to_frame() { - Ok(frame) => frame_handle(frame), - Err(_) => CHandle::null(), - }, - _ => CHandle::null(), - }; - lock().remove(&rid); - unsafe { cb(handle, ts, userdata.ptr()) }; - }) - }); - lock().insert(rid, (arena, ticket)); - rid - } - - /// `oakrender_cancel_request`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cancel_request(request_id: i64) -> c_int { - let entry = lock().remove(&request_id); - match entry { - Some((arena, ticket)) => { - arena.cancel(ticket); - crate::error::OAKRENDER_OK - } - None => crate::error::OAKRENDER_E_NOT_FOUND, - } - } - - /// `oakrender_set_cacher_multicam`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_set_cacher_multicam(multicam_or_NULL: OakNodeNode) -> c_int { - let Some(manager) = crate::manager::RenderManager::global() else { - return crate::error::OAKRENDER_E_STATE; - }; - let mut cacher = manager.get_cacher(); - if let Some(c) = cacher.as_mut() { - c.multicam_node = if multicam_or_NULL.is_null() { - None - } else { - Some(multicam_or_NULL.ctx as u64) - }; - } - crate::error::OAKRENDER_OK - } - - /// `oakrender_set_display_color_processor`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_set_display_color_processor( - p_or_NULL: OakColorProcessor, - ) -> c_int { - let Some(manager) = crate::manager::RenderManager::global() else { - return crate::error::OAKRENDER_E_STATE; - }; - let mut cacher = manager.get_cacher(); - if let Some(c) = cacher.as_mut() { - c.set_display_color_processor(if p_or_NULL.is_null() { - None - } else { - Some(p_or_NULL.ctx as u64) - }); - } - crate::error::OAKRENDER_OK - } - - /// `oakrender_cancel_video_tasks`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cancel_video_tasks(wait_for_done: c_int) { - crate::handle::guard_void(|| { - if let Some(manager) = crate::manager::RenderManager::global() { - let cacher = manager.get_cacher(); - if let Some(c) = cacher.as_ref() { - c.cancel_video_tasks(wait_for_done != 0); - } - } - }); - } - - /// `oakrender_disk_cache_path`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_disk_cache_path(buf: *mut c_char, n: c_int) -> c_int { - guard_string(|| { - let path = crate::manager::disk_cache_path(); - Ok(write_string(&path, buf, n)) - }) - } - - /// `oakrender_disk_cache_size`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_disk_cache_size() -> i64 { - match crate::manager::disk_cache_size() { - Ok(n) => n, - Err(_) => crate::error::OAKRENDER_E_FAILED as i64, - } - } - - /// `oakrender_disk_cache_clear`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_disk_cache_clear() -> c_int { - crate::handle::guard(|| crate::manager::disk_cache_clear()) - } - - fn lock() -> std::sync::MutexGuard< - 'static, - std::collections::HashMap< - i64, - ( - std::sync::Arc, - crate::ticket::TicketId, - ), - >, - > { - REQUESTS.lock().unwrap_or_else(|e| e.into_inner()) - } -} - -// ============================================================================ -// render/cache.h -// ============================================================================ - -/// `include/render/cache.h` exports: the playback/frame-hash cache family. -pub mod cache { - use super::*; - use std::sync::Mutex; - - /// Held external locks (see `oakrender_cache_lock`): cache box address - /// → address of a boxed `MutexGuard<'static, ()>` (usize so the static - /// stays `Send + Sync`). - static HELD_LOCKS: std::sync::LazyLock>> = - std::sync::LazyLock::new(|| Mutex::new(std::collections::HashMap::new())); - - /// `oakrender_cache_create`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_create() -> OakRenderCache { - crate::handle::guard_handle(|| { - Ok(crate::handle::make_owned(Some( - crate::cache::PlaybackCache::new( - crate::cache::CacheKind::VideoFrame, - crate::cache::next_owner_identity(), - ), - ))) - }) - } - - /// `oakrender_cache_free`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_free(cache: *mut OakRenderCache) { - crate::handle::guard_void(|| free_handle(cache)); - } - - /// `oakrender_cache_wrap_borrowed` — the native cache is a C++ object - /// this crate cannot dereference; the handle boxes `None` (queries on - /// borrowed caches return `OAKRENDER_E_INVALID` until the C++ interop - /// layer lands). - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_wrap_borrowed( - native_cache: *mut c_void, - ) -> OakRenderCache { - if native_cache.is_null() { - return CHandle::null(); - } - let _ = native_cache; - crate::handle::make_borrowed_owned(None::) - } - - /// `oakrender_cache_create_for_node`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_create_for_node( - parent: OakNodeNode, - kind: c_int, - ) -> OakRenderCache { - if parent.is_null() { - return CHandle::null(); - } - let kind = match kind { - 0 => crate::cache::CacheKind::VideoFrame, - 1 => crate::cache::CacheKind::Thumbnail, - 2 => crate::cache::CacheKind::AudioPlayback, - 3 => crate::cache::CacheKind::AudioWaveform, - _ => return CHandle::null(), - }; - crate::handle::make_owned(Some(crate::cache::PlaybackCache::new( - kind, - parent.ctx as u64, - ))) - } - - /// `oakrender_cache_get_uuid` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_get_uuid( - cache: OakRenderCache, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - guard_string(|| { - let c = unsafe { crate::handle::get::>(&cache) } - .and_then(|o| o.as_ref()) - .ok_or(crate::error::Error::Invalid)?; - Ok(write_string(&c.uuid, buf, buf_size)) - }) - } - - /// `oakrender_cache_request` — the context is validated as a viewer - /// through the node bridge (pending); any non-empty context is - /// accepted this pass. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_request( - cache: OakRenderCache, - context: OakNodeNode, - in_num: i64, - in_den: i64, - out_num: i64, - out_den: i64, - ) -> c_int { - crate::handle::guard(|| { - if context.is_null() { - return Err(crate::error::Error::Invalid); - } - let c = - unsafe { crate::handle::get_mut::>(&cache) } - .and_then(|o| o.as_mut()) - .ok_or(crate::error::Error::Invalid)?; - let range = oakcore_rs::TimeRange::new( - oakcore_rs::Rational::new(in_num, in_den), - oakcore_rs::Rational::new(out_num, out_den), - ); - c.request(range); - // Drive the auto-cacher (facade re-emits notifications; the - // cacher is the consuming side). - if let Some(manager) = crate::manager::RenderManager::global() { - let mut cacher = manager.get_cacher(); - if let Some(cacher) = cacher.as_mut() { - cacher.on_cache_request(context.ctx as u64, range); - } - } - Ok(()) - }) - } - - /// `oakrender_cache_load_state`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_load_state(cache: OakRenderCache) -> c_int { - crate::handle::guard(|| { - let c = - unsafe { crate::handle::get_mut::>(&cache) } - .and_then(|o| o.as_mut()) - .ok_or(crate::error::Error::Invalid)?; - let dir = c.disk_dir().to_string(); - c.load_state(std::path::Path::new(&dir)) - }) - } - - /// `oakrender_cache_save_state`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_save_state(cache: OakRenderCache) -> c_int { - crate::handle::guard(|| { - let c = unsafe { crate::handle::get::>(&cache) } - .and_then(|o| o.as_ref()) - .ok_or(crate::error::Error::Invalid)?; - c.save_state(std::path::Path::new(c.disk_dir())) - }) - } - - /// `oakrender_cache_set_saving_enabled`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_set_saving_enabled( - cache: OakRenderCache, - enabled: c_int, - ) -> c_int { - crate::handle::guard(|| { - let c = - unsafe { crate::handle::get_mut::>(&cache) } - .and_then(|o| o.as_mut()) - .ok_or(crate::error::Error::Invalid)?; - c.set_saving_enabled(enabled != 0); - Ok(()) - }) - } - - /// `oakrender_cache_set_passthrough`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_set_passthrough( - cache: OakRenderCache, - other: OakRenderCache, - ) -> c_int { - crate::handle::guard(|| { - // Snapshot the source's data first: `cache == other` (or any - // aliasing) must not create overlapping borrows. - let snapshot = { - let o = - unsafe { crate::handle::get::>(&other) } - .and_then(|o| o.as_ref()) - .ok_or(crate::error::Error::Invalid)?; - crate::cache::PassthroughSnapshot { - validated: o.validated_ranges().clone(), - passthroughs: o.passthroughs().to_vec(), - timebase: o.timebase, - uuid: o.uuid.clone(), - } - }; - let c = - unsafe { crate::handle::get_mut::>(&cache) } - .and_then(|o| o.as_mut()) - .ok_or(crate::error::Error::Invalid)?; - c.set_passthrough_snapshot(snapshot); - Ok(()) - }) - } - - /// `oakrender_cache_get_valid_cache_filename` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_get_valid_cache_filename( - cache: OakRenderCache, - time_num: i64, - time_den: i64, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - guard_string(|| { - let c = unsafe { crate::handle::get::>(&cache) } - .and_then(|o| o.as_ref()) - .ok_or(crate::error::Error::Invalid)?; - if !c.kind.is_frame_hash() { - return Err(crate::error::Error::Invalid); - } - let time = oakcore_rs::Rational::new(time_num, time_den); - match c.frame_filename(time) { - Some(name) => Ok(write_string(&name, buf, buf_size)), - None => Err(crate::error::Error::NotFound), - } - }) - } - - /// `oakrender_cache_get_passthroughs` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_get_passthroughs( - cache: OakRenderCache, - ranges: *mut i64, - max_ranges: c_int, - ) -> c_int { - let Some(c) = - (unsafe { crate::handle::get::>(&cache) }) - .and_then(|o| o.as_ref()) - else { - return crate::error::OAKRENDER_E_INVALID; - }; - let all = c.passthroughs(); - let count = all.len() as c_int; - if !ranges.is_null() && max_ranges > 0 { - let n = count.min(max_ranges); - for i in 0..n { - let (r, _) = &all[i as usize]; - unsafe { - *ranges.add(i as usize * 4) = r.in_().numerator(); - *ranges.add(i as usize * 4 + 1) = r.in_().denominator(); - *ranges.add(i as usize * 4 + 2) = r.out().numerator(); - *ranges.add(i as usize * 4 + 3) = r.out().denominator(); - } - } - } - count - } - - /// `oakrender_cache_get_timebase`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_get_timebase( - cache: OakRenderCache, - num: *mut c_int, - den: *mut c_int, - ) -> c_int { - crate::handle::guard(|| { - let c = unsafe { crate::handle::get::>(&cache) } - .and_then(|o| o.as_ref()) - .ok_or(crate::error::Error::Invalid)?; - if !c.kind.is_frame_hash() { - return Err(crate::error::Error::Invalid); - } - let tb = c.timebase.unwrap_or(oakcore_rs::Rational::new(1, 1)); - if !num.is_null() { - unsafe { *num = tb.numerator() as c_int }; - } - if !den.is_null() { - unsafe { *den = tb.denominator() as c_int }; - } - Ok(()) - }) - } - - /// `oakrender_cache_lock` — acquires and *holds* the cache's external - /// mutex until the matching unlock (the guard is boxed in a registry - /// keyed by the cache box; a leaked lock — e.g. the cache destroyed - /// while locked — is a documented leak, mirroring a C++ deadlock). - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_lock(cache: OakRenderCache) { - if let Some(c) = - unsafe { crate::handle::get::>(&cache) } - .and_then(|o| o.as_ref()) - { - // The boxed cache lives for 'static; re-derive a 'static - // reference from the box pointer to build a 'static guard. - let c_static: &'static crate::cache::PlaybackCache = - unsafe { &*(c as *const crate::cache::PlaybackCache) }; - let guard: Box> = - Box::new(c_static.lock.lock().unwrap_or_else(|e| e.into_inner())); - let raw = Box::into_raw(guard) as usize; - HELD_LOCKS - .lock() - .unwrap_or_else(|e| e.into_inner()) - .insert(c as *const _ as usize, raw); - } - } - - /// `oakrender_cache_unlock` — releases the lock held by - /// [`oakrender_cache_lock`]. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_unlock(cache: OakRenderCache) { - if let Some(c) = - unsafe { crate::handle::get::>(&cache) } - { - let key = c as *const _ as usize; - if let Some(raw) = HELD_LOCKS - .lock() - .unwrap_or_else(|e| e.into_inner()) - .remove(&key) - { - // SAFETY: the raw address came from Box::into_raw of a - // MutexGuard in oakrender_cache_lock. - unsafe { - drop(Box::from_raw( - raw as *mut std::sync::MutexGuard<'static, ()>, - )) - }; - } - } - } - - /// `oakrender_cache_set_timebase`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_set_timebase( - cache: OakRenderCache, - num: c_int, - den: c_int, - ) -> c_int { - crate::handle::guard(|| { - if num <= 0 || den <= 0 { - return Err(crate::error::Error::Invalid); - } - let c = - unsafe { crate::handle::get_mut::>(&cache) } - .and_then(|o| o.as_mut()) - .ok_or(crate::error::Error::Invalid)?; - c.set_timebase(oakcore_rs::Rational::new(num as i64, den as i64)); - Ok(()) - }) - } - - /// `oakrender_cache_set_uuid`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_set_uuid( - cache: OakRenderCache, - uuid: *const c_char, - ) -> c_int { - crate::handle::guard(|| { - let uuid = unsafe { cstr(uuid) }.ok_or(crate::error::Error::Invalid)?; - let c = - unsafe { crate::handle::get_mut::>(&cache) } - .and_then(|o| o.as_mut()) - .ok_or(crate::error::Error::Invalid)?; - c.set_uuid(&uuid); - Ok(()) - }) - } - - /// `oakrender_cache_invalidate` (timestamp range). - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_invalidate( - cache: OakRenderCache, - in_ts: i64, - out_ts: i64, - ) { - crate::handle::guard_void(|| { - if let Some(c) = - unsafe { crate::handle::get_mut::>(&cache) } - .and_then(|o| o.as_mut()) - { - let range = ts_range(c, in_ts, out_ts); - c.invalidate(range); - } - }); - } - - /// `oakrender_cache_invalidate_range`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_invalidate_range( - cache: OakRenderCache, - in_num: i64, - in_den: i64, - out_num: i64, - out_den: i64, - ) { - crate::handle::guard_void(|| { - if let Some(c) = - unsafe { crate::handle::get_mut::>(&cache) } - .and_then(|o| o.as_mut()) - { - c.invalidate(oakcore_rs::TimeRange::new( - oakcore_rs::Rational::new(in_num, in_den), - oakcore_rs::Rational::new(out_num, out_den), - )); - } - }); - } - - /// `oakrender_cache_validate` (timestamp range). - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_validate( - cache: OakRenderCache, - in_ts: i64, - out_ts: i64, - ) { - crate::handle::guard_void(|| { - if let Some(c) = - unsafe { crate::handle::get_mut::>(&cache) } - .and_then(|o| o.as_mut()) - { - let range = ts_range(c, in_ts, out_ts); - c.validate(range); - } - }); - } - - /// `oakrender_cache_has_validated_ranges`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_has_validated_ranges(cache: OakRenderCache) -> c_int { - match unsafe { crate::handle::get::>(&cache) } - .and_then(|o| o.as_ref()) - { - Some(c) if c.has_validated_ranges() => 1, - _ => 0, - } - } - - /// `oakrender_cache_indicator_height`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_indicator_height() -> c_int { - 4 - } - - /// `oakrender_cache_get_invalidated_ranges` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oakrender_cache_get_invalidated_ranges( - c: OakRenderCache, - in_num: i64, - in_den: i64, - out_num: i64, - out_den: i64, - ranges: *mut i64, - max_ranges: c_int, - ) -> c_int { - guard_string(|| { - if max_ranges < 0 { - return Err(crate::error::Error::Invalid); - } - let cache = unsafe { crate::handle::get::(&c) } - .ok_or(crate::error::Error::Invalid)?; - let within = oakcore_rs::TimeRange::new( - oakcore_rs::Rational::new(in_num, in_den), - oakcore_rs::Rational::new(out_num, out_den), - ); - let list = cache.invalidated_ranges(within); - let count = list.ranges().len() as c_int; - if !ranges.is_null() { - let written = count.min(max_ranges); - for i in 0..written { - let r = &list.ranges()[i as usize]; - unsafe { - *ranges.add(i as usize * 4) = r.in_().numerator(); - *ranges.add(i as usize * 4 + 1) = r.in_().denominator(); - *ranges.add(i as usize * 4 + 2) = r.out().numerator(); - *ranges.add(i as usize * 4 + 3) = r.out().denominator(); - } - } - } - Ok(count) - }) - } - - /// `oakrender_frame_cache_load`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_frame_cache_load( - cache: OakRenderCache, - path: *const c_char, - uuid: *const c_char, - ts: i64, - out_frame: *mut OakCodecFrame, - ) -> c_int { - crate::handle::guard(|| { - if cache.is_null() || path.is_null() || uuid.is_null() || out_frame.is_null() { - return Err(crate::error::Error::Invalid); - } - let path = unsafe { cstr(path) }.unwrap_or_default(); - let uuid = unsafe { cstr(uuid) }.unwrap_or_default(); - let filename = std::path::Path::new(&path) - .join(&uuid) - .join(ts.to_string()) - .to_string_lossy() - .into_owned(); - if !std::path::Path::new(&filename).exists() { - return Err(crate::error::Error::NotFound); - } - // Payload decode belongs to the oakcodec crate (EXR/JPEG), - // deferred; without it the file exists but cannot be decoded. - Err(crate::error::Error::Failed( - "oakcodec frame decode bridge pending".into(), - )) - }) - } - - /// `oakrender_frame_cache_save` — computes the C++-parity filename; - /// the payload write goes through the oakcodec crate (deferred), so - /// with Rust-owned frames this is a documented no-op. - #[no_mangle] - pub unsafe extern "C" fn oakrender_frame_cache_save( - cache: OakRenderCache, - path: *const c_char, - uuid: *const c_char, - frame: OakCodecFrame, - ) { - crate::handle::guard_void(|| { - let (Some(c), Some(path), Some(uuid), Some(f)) = ( - unsafe { crate::handle::get::>(&cache) } - .and_then(|o| o.as_ref()), - unsafe { cstr(path) }, - unsafe { cstr(uuid) }, - unsafe { crate::handle::get::(&frame) }, - ) else { - return; - }; - let tb = c.timebase.unwrap_or(oakcore_rs::Rational::new(1, 1)); - let filename = - crate::cache::PlaybackCache::frame_cache_path(&path, &uuid, f.timestamp, tb); - eprintln!( - "oakrender: frame_cache_save to {filename} deferred (oakcodec write bridge pending)" - ); - }); - } - - /// `oakrender_debug_alive_count`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_debug_alive_count() -> c_int { - crate::handle::alive_count() - } - - /// Timestamp range through the cache's timebase (whole seconds when - /// unset; C++ `ts_to_time`). - fn ts_range(c: &crate::cache::PlaybackCache, in_ts: i64, out_ts: i64) -> oakcore_rs::TimeRange { - let to_time = |ts: i64| match c.timebase { - Some(tb) => tb.timestamp_to_time(ts), - None => oakcore_rs::Rational::from_double(ts as f64), - }; - oakcore_rs::TimeRange::new(to_time(in_ts), to_time(out_ts)) - } - - /// Release a caller-owned handle and null it (C++ `free_handle`). - fn free_handle(h: *mut OakRenderCache) { - if h.is_null() { - return; - } - unsafe { - if (*h).is_null() { - return; - } - if let Some(release) = (*h).release { - release((*h).ctx); - } - (*h).ctx = std::ptr::null_mut(); - (*h).addref = None; - (*h).release = None; - } - } -} - -// ============================================================================ -// render/ticket.h -// ============================================================================ - -/// `include/render/ticket.h` exports: async render tickets. -pub mod ticket { - use super::*; - - /// A ticket handle boxes the arena + ticket id, so queries/cancel/wait - /// work independently of the manager's lifetime. - struct TicketBox { - arena: std::sync::Arc, - id: crate::ticket::TicketId, - } - - /// `oakrender_ticket_render_frame`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_ticket_render_frame( - params: *const OakVideoTicketParams, - cb: Option, - userdata: *mut c_void, - ) -> OakRenderTicket { - crate::handle::guard_handle(|| { - let params = if params.is_null() { - return Err(crate::error::Error::Invalid); - } else { - unsafe { &*params } - }; - if params.output_node.is_null() || params.video_params.is_null() { - return Err(crate::error::Error::Invalid); - } - let Some(manager) = crate::manager::RenderManager::global() else { - return Err(crate::error::Error::State); - }; - - let time = oakcore_rs::Rational::new(params.time_num, params.time_den); - let force_size = if params.force_width > 0 && params.force_height > 0 { - Some((params.force_width, params.force_height)) - } else { - None - }; - let force_format = if params.force_format >= 0 { - Some(oakcore_pixel_format(params.force_format)) - } else { - None - }; - let (cache_dir, cache_id, cache_timebase) = if !params.cache.is_null() { - if let Some(c) = - unsafe { crate::handle::get::(¶ms.cache) } - { - ( - Some(c.disk_dir().to_string()), - Some(c.uuid.clone()), - c.timebase, - ) - } else { - (None, None, None) - } - } else { - (None, None, None) - }; - - let arena = manager.tickets.clone(); - // Reserve the id up front so the handle is fully stamped before - // the job is posted: a fast worker could otherwise complete the - // ticket (firing `cb`) before the id was written into the box, - // and the callback's ticket would carry the placeholder id. - let id = arena.next_id(); - let handle = crate::handle::make_owned(TicketBox { - arena: arena.clone(), - id, - }); - let userdata = RawSend(userdata); - // The completion needs the final handle to pass to `cb`. - let footage = if !params.footage_filename.is_null() { - let filename = unsafe { cstr(params.footage_filename) }; - filename.filter(|s| !s.is_empty()).map(|s| (s, params.footage_stream)) - } else { - None - }; - let montage = if !params.montage.is_null() && params.montage_count > 0 { - let count = params.montage_count.min(1024); - let mut out = Vec::with_capacity(count as usize); - for i in 0..count { - let c = unsafe { &*params.montage.add(i as usize) }; - let Some(filename) = (unsafe { cstr(c.filename) }) else { - continue; - }; - if filename.is_empty() { - continue; - } - out.push(crate::ticket::MontageClip { - filename, - stream_index: c.stream_index, - in_time: oakcore_rs::Rational::new(c.in_num, c.in_den), - out_time: oakcore_rs::Rational::new(c.out_num, c.out_den), - media_in: oakcore_rs::Rational::new(c.media_in_num, c.media_in_den), - gain: c.gain, - }); - } - out - } else { - Vec::new() - }; - arena.submit_video_with_id( - id, - crate::ticket::VideoTicketParams { - viewer: params.output_node.ctx as u64, - time, - force_size, - force_format, - cache: if params.cache.is_null() { - None - } else { - Some(params.cache.ctx as u64) - }, - cache_dir, - cache_id, - cache_timebase, - footage, - montage, - }, - match cb { - Some(cb) => Box::new(move |_result| { - unsafe { cb(handle, userdata.ptr()) }; - }), - None => Box::new(|_| {}), - }, - ); - Ok(handle) - }) - } - - /// `oakrender_ticket_render_audio` — range audio render (M12 P1). - /// `montage`/`montage_count` carry the clips to mix (same POD as the - /// video path; additive trailing fields). - #[no_mangle] - pub unsafe extern "C" fn oakrender_ticket_render_audio( - output_node: OakNodeNode, - in_num: i64, - in_den: i64, - out_num: i64, - out_den: i64, - params: *const OakAudioParams, - mode: c_int, - cb: Option, - userdata: *mut c_void, - montage: *const OakMontageClip, - montage_count: c_int, - ) -> OakRenderTicket { - crate::handle::guard_handle(|| { - if output_node.is_null() || params.is_null() { - return Err(crate::error::Error::Invalid); - } - let Some(manager) = crate::manager::RenderManager::global() else { - return Err(crate::error::Error::State); - }; - let _ = mode; - let range = oakcore_rs::TimeRange::new( - oakcore_rs::Rational::new(in_num, in_den), - oakcore_rs::Rational::new(out_num, out_den), - ); - let sample_rate = - crate::bridge::common::audioparams_sample_rate(params as *const c_void).max(1); - let channel_layout = crate::bridge::common::audioparams_channel_layout( - params as *const c_void, - ); - let montage = if !montage.is_null() && montage_count > 0 { - let count = montage_count.min(1024); - let mut out = Vec::with_capacity(count as usize); - for i in 0..count { - let c = unsafe { &*montage.add(i as usize) }; - let Some(filename) = (unsafe { cstr(c.filename) }) else { - continue; - }; - if filename.is_empty() { - continue; - } - out.push(crate::ticket::MontageClip { - filename, - stream_index: c.stream_index, - in_time: oakcore_rs::Rational::new(c.in_num, c.in_den), - out_time: oakcore_rs::Rational::new(c.out_num, c.out_den), - media_in: oakcore_rs::Rational::new(c.media_in_num, c.media_in_den), - gain: c.gain, - }); - } - out - } else { - Vec::new() - }; - let arena = manager.tickets.clone(); - let id = arena.next_id(); - let handle = crate::handle::make_owned(TicketBox { - arena: arena.clone(), - id, - }); - let userdata = RawSend(userdata); - arena.submit_audio_with_id( - id, - crate::ticket::AudioTicketParams { - viewer: output_node.ctx as u64, - range, - sample_rate, - channel_layout, - montage, - }, - match cb { - Some(cb) => Box::new(move |_result| { - unsafe { cb(handle, userdata.ptr()) }; - }), - None => Box::new(|_| {}), - }, - ); - Ok(handle) - }) - } - - /// `oakrender_ticket_is_finished`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_ticket_is_finished(ticket: OakRenderTicket) -> c_int { - match unsafe { crate::handle::get::(&ticket) } { - Some(b) if b.arena.is_finished(b.id) => 1, - Some(_) => 0, - None => crate::error::OAKRENDER_E_INVALID, - } - } - - /// `oakrender_ticket_wait`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_ticket_wait(ticket: OakRenderTicket) -> c_int { - crate::handle::guard(|| { - let b = unsafe { crate::handle::get::(&ticket) } - .ok_or(crate::error::Error::Invalid)?; - b.arena.wait(b.id)?; - Ok(()) - }) - } - - /// `oakrender_ticket_cancel`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_ticket_cancel(ticket: OakRenderTicket) -> c_int { - crate::handle::guard(|| { - let b = unsafe { crate::handle::get::(&ticket) } - .ok_or(crate::error::Error::Invalid)?; - b.arena.cancel(b.id); - Ok(()) - }) - } - - /// `oakrender_ticket_get_type`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_ticket_get_type(ticket: OakRenderTicket) -> c_int { - match unsafe { crate::handle::get::(&ticket) } { - Some(b) => b - .arena - .kind(b.id) - .unwrap_or(crate::error::OAKRENDER_E_INVALID), - None => crate::error::OAKRENDER_E_INVALID, - } - } - - /// `oakrender_ticket_get_time`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_ticket_get_time( - ticket: OakRenderTicket, - out_num: *mut i64, - out_den: *mut i64, - ) -> c_int { - crate::handle::guard(|| { - if out_num.is_null() || out_den.is_null() { - return Err(crate::error::Error::Invalid); - } - let b = unsafe { crate::handle::get::(&ticket) } - .ok_or(crate::error::Error::Invalid)?; - let time = b.arena.time(b.id).unwrap_or(oakcore_rs::Rational::NULL); - unsafe { - *out_num = time.numerator(); - *out_den = time.denominator(); - } - Ok(()) - }) - } - - /// `oakrender_ticket_get_range`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_ticket_get_range( - ticket: OakRenderTicket, - in_num: *mut i64, - in_den: *mut i64, - out_num: *mut i64, - out_den: *mut i64, - ) -> c_int { - crate::handle::guard(|| { - if in_num.is_null() || in_den.is_null() || out_num.is_null() || out_den.is_null() { - return Err(crate::error::Error::Invalid); - } - let b = unsafe { crate::handle::get::(&ticket) } - .ok_or(crate::error::Error::Invalid)?; - let range = b.arena.range(b.id).unwrap_or(oakcore_rs::TimeRange::new( - oakcore_rs::Rational::NULL, - oakcore_rs::Rational::NULL, - )); - unsafe { - *in_num = range.in_().numerator(); - *in_den = range.in_().denominator(); - *out_num = range.out().numerator(); - *out_den = range.out().denominator(); - } - Ok(()) - }) - } - - /// `oakrender_ticket_get_frame`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_ticket_get_frame( - ticket: OakRenderTicket, - out: *mut OakCodecFrame, - ) -> c_int { - crate::handle::guard(|| { - if out.is_null() { - return Err(crate::error::Error::Invalid); - } - unsafe { *out = CHandle::null() }; - let b = unsafe { crate::handle::get::(&ticket) } - .ok_or(crate::error::Error::Invalid)?; - if !b.arena.is_finished(b.id) { - return Err(crate::error::Error::State); - } - let result = b - .arena - .result(b.id) - .ok_or_else(|| crate::error::Error::Failed("ticket has no result".into()))?; - let texture = match result.map_err(|_| { - crate::error::Error::Failed("ticket failed".into()) - })? { - crate::ticket::TicketPayload::Video(t) => t, - _ => { - return Err(crate::error::Error::Failed( - "ticket produced audio, not video".into(), - )); - } - }; - let frame = texture.to_frame()?; - unsafe { *out = frame_handle(frame) }; - if unsafe { (*out).is_null() } { - return Err(crate::error::Error::NoMem); - } - Ok(()) - }) - } - - /// `oakrender_ticket_get_samples` — the audio ticket's interleaved - /// f32 samples (M12 P1). `*out` receives a heap `OakAudioSamples` - /// owned by the caller (release with `oakrender_audio_samples_free`). - #[no_mangle] - pub unsafe extern "C" fn oakrender_ticket_get_samples( - ticket: OakRenderTicket, - out: *mut *mut c_void, - ) -> c_int { - crate::handle::guard(|| { - if out.is_null() { - return Err(crate::error::Error::Invalid); - } - unsafe { *out = std::ptr::null_mut() }; - let b = unsafe { crate::handle::get::(&ticket) } - .ok_or(crate::error::Error::Invalid)?; - if !b.arena.is_finished(b.id) { - return Err(crate::error::Error::State); - } - let result = b - .arena - .result(b.id) - .ok_or_else(|| crate::error::Error::Failed("ticket has no result".into()))?; - let samples = match result.map_err(|_| { - crate::error::Error::Failed("audio ticket failed".into()) - })? { - crate::ticket::TicketPayload::Audio(a) => a, - _ => { - return Err(crate::error::Error::Failed( - "ticket produced video, not audio".into(), - )); - } - }; - let boxed = Box::new(OakAudioSamplesOut { - data: samples.samples.clone().into_boxed_slice(), - frame_count: samples - .samples - .len() - .checked_div(samples.channel_count.max(1) as usize) - .unwrap_or(0) as c_int, - sample_rate: samples.sample_rate, - channel_layout: samples.channel_layout, - channel_count: samples.channel_count, - }); - unsafe { *out = Box::into_raw(boxed) as *mut c_void }; - Ok(()) - }) - } - - /// `oakrender_audio_samples_free` — release the samples block returned - /// by `oakrender_ticket_get_samples` (NULL no-op). - #[no_mangle] - pub unsafe extern "C" fn oakrender_audio_samples_free(samples: *mut c_void) { - if samples.is_null() { - return; - } - unsafe { drop(Box::from_raw(samples as *mut OakAudioSamplesOut)) }; - } - - /// `oakrender_ticket_free`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_ticket_free(ticket: *mut OakRenderTicket) { - crate::handle::guard_void(|| free_handle(ticket)); - } - - /// `oakrender_manager_set_aggressive_gc` (declared in ticket.h). - #[no_mangle] - pub unsafe extern "C" fn oakrender_manager_set_aggressive_gc(enabled: c_int) -> c_int { - let Some(manager) = crate::manager::RenderManager::global() else { - return crate::error::OAKRENDER_E_STATE; - }; - manager.set_aggressive_gc(enabled != 0); - crate::error::OAKRENDER_OK - } - - fn free_handle(h: *mut OakRenderTicket) { - if h.is_null() { - return; - } - unsafe { - if (*h).is_null() { - return; - } - if let Some(release) = (*h).release { - release((*h).ctx); - } - (*h).ctx = std::ptr::null_mut(); - (*h).addref = None; - (*h).release = None; - } - } -} - -// ============================================================================ -// render/renderer.h -// ============================================================================ - -/// `include/render/renderer.h` exports: the display renderer, texture and -/// frame families, and backend management. -pub mod renderer { - use super::*; - use std::sync::Mutex; - - static REQUESTED_BACKEND: std::sync::LazyLock> = - std::sync::LazyLock::new(|| Mutex::new("opengl".to_string())); - - /// `oakrender_display_renderer_create_dynamic`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_display_renderer_create_dynamic( - backend_id: *const c_char, - ) -> OakRenderRenderer { - crate::handle::guard_handle(|| { - let id = unsafe { cstr(backend_id) }.ok_or(crate::error::Error::Invalid)?; - if id.is_empty() { - return Err(crate::error::Error::Invalid); - } - let kind = match id.to_ascii_lowercase().as_str() { - "opengl" => crate::backend::BackendKind::Gl, - "vulkan" => crate::backend::BackendKind::Vulkan, - "metal" => crate::backend::BackendKind::Metal, - "auto" => crate::backend::BackendKind::Auto, - "cpu" | "dummy" | "multiprocess" => crate::backend::BackendKind::Cpu, - _ => return Err(crate::error::Error::Invalid), - }; - Ok(crate::handle::make_owned( - crate::backend::DisplayRenderer::new(kind), - )) - }) - } - - /// `oakrender_display_renderer_create_opengl`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_display_renderer_create_opengl() -> OakRenderRenderer { - crate::handle::make_owned(crate::backend::DisplayRenderer::new( - crate::backend::BackendKind::Gl, - )) - } - - /// `oakrender_display_renderer_init`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_display_renderer_init( - renderer: OakRenderRenderer, - gl_context: *mut c_void, - ) -> c_int { - crate::handle::guard(|| { - let r = unsafe { crate::handle::get_mut::(&renderer) } - .ok_or(crate::error::Error::Invalid)?; - r.init(gl_context) - }) - } - - /// `oakrender_display_renderer_destroy`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_display_renderer_destroy(renderer: *mut OakRenderRenderer) { - crate::handle::guard_void(|| free_handle(renderer)); - } - - /// `oakrender_display_renderer_is_open_gl`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_display_renderer_is_open_gl( - renderer: OakRenderRenderer, - ) -> c_int { - match unsafe { crate::handle::get::(&renderer) } { - Some(r) if r.is_open_gl() => 1, - _ => 0, - } - } - - /// `oakrender_display_renderer_is_vulkan`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_display_renderer_is_vulkan( - renderer: OakRenderRenderer, - ) -> c_int { - match unsafe { crate::handle::get::(&renderer) } { - Some(r) if r.is_vulkan() => 1, - _ => 0, - } - } - - /// `oakrender_display_texture_create`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_display_texture_create( - renderer: OakRenderRenderer, - params: *const OakRenderVideoParams, - pixels: *const c_void, - linesize: c_int, - ) -> OakRenderTexture { - crate::handle::guard_handle(|| { - if params.is_null() { - return Err(crate::error::Error::Invalid); - } - let pod = ffi_to_pod(unsafe { &*params }); - let r = unsafe { crate::handle::get::(&renderer) } - .ok_or(crate::error::Error::Invalid)?; - let pixels = if pixels.is_null() { - None - } else { - Some((pixels as *const u8, linesize as usize)) - }; - let tex = r.create_texture(&pod, pixels)?; - Ok(crate::handle::make_owned(tex)) - }) - } - - /// `oakrender_display_texture_retain`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_display_texture_retain( - texture: OakRenderTexture, - ) -> OakRenderTexture { - if !texture.is_null() { - if let Some(addref) = texture.addref { - unsafe { addref(texture.ctx) }; - } - } - texture - } - - /// `oakrender_display_texture_free`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_display_texture_free(texture: *mut OakRenderTexture) { - crate::handle::guard_void(|| free_handle(texture)); - } - - /// `oakrender_display_texture_upload`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_display_texture_upload( - texture: OakRenderTexture, - pixels: *const c_void, - linesize: c_int, - ) -> c_int { - crate::handle::guard(|| { - if pixels.is_null() { - return Err(crate::error::Error::Invalid); - } - let t = unsafe { crate::handle::get_mut::(&texture) } - .ok_or(crate::error::Error::Invalid)?; - let (w, h) = t.size(); - match t { - crate::texture::Texture::Gpu { token, ctx, .. } => { - let frame = crate::backend::frame_from_pixels_for_upload( - (w, h), - pixels as *const u8, - linesize as usize, - )?; - ctx.upload(*token, &frame) - } - crate::texture::Texture::Cpu(frame) => { - let stride = frame.linesize_bytes(); - if linesize as usize != stride { - return Err(crate::error::Error::Invalid); - } - let n = stride * frame.height as usize; - frame.data.copy_from_slice(unsafe { - std::slice::from_raw_parts(pixels as *const u8, n) - }); - Ok(()) - } - } - }) - } - - /// `oakrender_display_texture_download`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_display_texture_download( - texture: OakRenderTexture, - pixels: *mut c_void, - linesize: c_int, - ) -> c_int { - crate::handle::guard(|| { - if pixels.is_null() { - return Err(crate::error::Error::Invalid); - } - let t = unsafe { crate::handle::get::(&texture) } - .ok_or(crate::error::Error::Invalid)?; - let frame = t.to_frame()?; - let stride = frame.linesize_bytes(); - if linesize as usize != stride { - return Err(crate::error::Error::Invalid); - } - let n = stride * frame.height as usize; - unsafe { - std::ptr::copy_nonoverlapping(frame.data.as_ptr(), pixels as *mut u8, n); - } - Ok(()) - }) - } - - /// `oakrender_display_texture_get_params`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_display_texture_get_params( - texture: OakRenderTexture, - out: *mut OakRenderVideoParams, - ) -> c_int { - crate::handle::guard(|| { - if out.is_null() { - return Err(crate::error::Error::Invalid); - } - let t = unsafe { crate::handle::get::(&texture) } - .ok_or(crate::error::Error::Invalid)?; - let pod = match t { - crate::texture::Texture::Cpu(f) => f.video_params(), - crate::texture::Texture::Gpu { - width, - height, - format, - .. - } => { - let mut pod = crate::frame::VideoParamsPod::default(); - pod.width = *width; - pod.height = *height; - pod.format = *format as i32; - pod - } - }; - unsafe { *out = pod_to_ffi(&pod) }; - Ok(()) - }) - } - - /// `oakrender_display_texture_id`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_display_texture_id(texture: OakRenderTexture) -> c_int { - match unsafe { crate::handle::get::(&texture) } { - Some(crate::texture::Texture::Gpu { token, .. }) => *token as c_int, - _ => 0, - } - } - - /// `oakrender_display_texture_is_dummy`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_display_texture_is_dummy( - texture: OakRenderTexture, - ) -> c_int { - match unsafe { crate::handle::get::(&texture) } { - Some(t) if t.is_dummy() => 1, - _ => 0, - } - } - - /// `oakrender_display_texture_get_frame`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_display_texture_get_frame( - texture: OakRenderTexture, - out: *mut OakCodecFrame, - ) -> c_int { - crate::handle::guard(|| { - if out.is_null() { - return Err(crate::error::Error::Invalid); - } - unsafe { *out = CHandle::null() }; - let t = unsafe { crate::handle::get::(&texture) } - .ok_or(crate::error::Error::Invalid)?; - let frame = t.to_frame()?; - unsafe { *out = frame_handle(frame) }; - if unsafe { (*out).is_null() } { - return Err(crate::error::Error::NoMem); - } - Ok(()) - }) - } - - /// `oakrender_codec_frame_create`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_codec_frame_create() -> OakCodecFrame { - crate::handle::make_owned(crate::texture::Frame::new()) - } - - /// `oakrender_codec_frame_retain`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_codec_frame_retain(frame: OakCodecFrame) -> OakCodecFrame { - if !frame.is_null() { - if let Some(addref) = frame.addref { - unsafe { addref(frame.ctx) }; - } - } - frame - } - - /// `oakrender_codec_frame_free`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_codec_frame_free(frame: *mut OakCodecFrame) { - crate::handle::guard_void(|| free_handle(frame)); - } - - /// `oakrender_codec_frame_set_video_params`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_codec_frame_set_video_params( - frame: OakCodecFrame, - params: *const OakRenderVideoParams, - ) -> c_int { - crate::handle::guard(|| { - if params.is_null() { - return Err(crate::error::Error::Invalid); - } - let f = unsafe { crate::handle::get_mut::(&frame) } - .ok_or(crate::error::Error::Invalid)?; - f.set_video_params(ffi_to_pod(unsafe { &*params })); - Ok(()) - }) - } - - /// `oakrender_codec_frame_get_params`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_codec_frame_get_params( - frame: OakCodecFrame, - out: *mut OakRenderVideoParams, - ) -> c_int { - crate::handle::guard(|| { - if out.is_null() { - return Err(crate::error::Error::Invalid); - } - let f = unsafe { crate::handle::get::(&frame) } - .ok_or(crate::error::Error::Invalid)?; - unsafe { *out = pod_to_ffi(&f.video_params()) }; - Ok(()) - }) - } - - /// `oakrender_codec_frame_allocate`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_codec_frame_allocate(frame: OakCodecFrame) -> c_int { - crate::handle::guard(|| { - let f = unsafe { crate::handle::get_mut::(&frame) } - .ok_or(crate::error::Error::Invalid)?; - if f.allocate() { - Ok(()) - } else { - Err(crate::error::Error::Failed( - "frame allocation failed".into(), - )) - } - }) - } - - /// `oakrender_codec_frame_data`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_codec_frame_data(frame: OakCodecFrame) -> *mut c_void { - match unsafe { crate::handle::get::(&frame) } { - Some(f) if f.is_allocated() => f.data.as_ptr() as *mut c_void, - _ => std::ptr::null_mut(), - } - } - - /// `oakrender_codec_frame_const_data`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_codec_frame_const_data( - frame: OakCodecFrame, - ) -> *const c_void { - match unsafe { crate::handle::get::(&frame) } { - Some(f) if f.is_allocated() => f.data.as_ptr() as *const c_void, - _ => std::ptr::null(), - } - } - - /// `oakrender_codec_frame_linesize_bytes`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_codec_frame_linesize_bytes(frame: OakCodecFrame) -> c_int { - match unsafe { crate::handle::get::(&frame) } { - Some(f) => f.linesize_bytes() as c_int, - None => 0, - } - } - - /// `oakrender_codec_frame_is_allocated`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_codec_frame_is_allocated(frame: OakCodecFrame) -> c_int { - match unsafe { crate::handle::get::(&frame) } { - Some(f) if f.is_allocated() => 1, - _ => 0, - } - } - - /// `oakrender_codec_frame_width`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_codec_frame_width(frame: OakCodecFrame) -> c_int { - match unsafe { crate::handle::get::(&frame) } { - Some(f) => f.width, - None => 0, - } - } - - /// `oakrender_codec_frame_height`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_codec_frame_height(frame: OakCodecFrame) -> c_int { - match unsafe { crate::handle::get::(&frame) } { - Some(f) => f.height, - None => 0, - } - } - - /// `oakrender_codec_frame_fb_format` — the Rust frames are never - /// AVFrame-wrapped; always -1. - #[no_mangle] - pub unsafe extern "C" fn oakrender_codec_frame_fb_format(_frame: OakCodecFrame) -> c_int { - -1 - } - - /// `oakrender_display_renderer_blit_color_managed`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_display_renderer_blit_color_managed( - renderer: OakRenderRenderer, - job: *const OakColorTransformJob, - dst_texture: OakRenderTexture, - params: *const OakRenderVideoParams, - ) -> c_int { - crate::handle::guard(|| { - if job.is_null() { - return Err(crate::error::Error::Invalid); - } - let job = unsafe { &*job }; - let r = unsafe { crate::handle::get::(&renderer) } - .ok_or(crate::error::Error::Invalid)?; - let processor_h = if job.processor.is_null() { - None - } else { - Some(ctx_handle(job.processor)) - }; - let src_h = if job.input_texture.is_null() { - None - } else { - Some(ctx_handle(job.input_texture)) - }; - let processor = processor_h - .as_ref() - .and_then(|h| unsafe { crate::handle::get::(h) }); - let src = src_h - .as_ref() - .and_then(|h| unsafe { crate::handle::get::(h) }); - if dst_texture.is_null() { - // C++ would blit to the renderer's current output target; - // no output target exists in this pass. - return Err(crate::error::Error::Invalid); - } - let dst = unsafe { crate::handle::get_mut::(&dst_texture) } - .ok_or(crate::error::Error::Invalid)?; - let _ = (params, job); // matrices applied by the (deferred) GPU shader path - r.blit_color_managed(src, dst, processor) - }) - } - - /// `oakrender_display_renderer_download_from_texture`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_display_renderer_download_from_texture( - renderer: OakRenderRenderer, - texture_id: c_int, - params: *const OakRenderVideoParams, - dst_pixels: *mut c_void, - linesize: c_int, - ) -> c_int { - crate::handle::guard(|| { - if params.is_null() || dst_pixels.is_null() { - return Err(crate::error::Error::Invalid); - } - let r = unsafe { crate::handle::get::(&renderer) } - .ok_or(crate::error::Error::Invalid)?; - let pod = ffi_to_pod(unsafe { &*params }); - r.download_from_texture(texture_id, &pod, dst_pixels as *mut u8, linesize as usize) - }) - } - - /// `oakrender_backend_count`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_backend_count() -> c_int { - 4 - } - - /// `oakrender_backend_id_at` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oakrender_backend_id_at( - i: c_int, - buf: *mut c_char, - n: c_int, - ) -> c_int { - const IDS: [&str; 4] = ["opengl", "vulkan", "multiprocess", "dummy"]; - if i < 0 || i >= IDS.len() as c_int { - return crate::error::OAKRENDER_E_NOT_FOUND; - } - guard_string(|| Ok(write_string(IDS[i as usize], buf, n))) - } - - /// `oakrender_set_backend`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_set_backend(backend_id: *const c_char) -> c_int { - crate::handle::guard(|| { - let id = unsafe { cstr(backend_id) }.ok_or(crate::error::Error::Invalid)?; - let lower = id.to_ascii_lowercase(); - if !["opengl", "vulkan", "multiprocess", "dummy"].contains(&lower.as_str()) { - return Err(crate::error::Error::Invalid); - } - *REQUESTED_BACKEND.lock().unwrap_or_else(|e| e.into_inner()) = lower; - Ok(()) - }) - } - - /// `oakrender_current_backend` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oakrender_current_backend(buf: *mut c_char, n: c_int) -> c_int { - let id = match crate::manager::RenderManager::global() { - Some(m) => m.backend.to_config_string().to_string(), - None => REQUESTED_BACKEND - .lock() - .unwrap_or_else(|e| e.into_inner()) - .clone(), - }; - guard_string(|| Ok(write_string(&id, buf, n))) - } - - fn free_handle(h: *mut OakRenderRenderer) { - if h.is_null() { - return; - } - unsafe { - if (*h).is_null() { - return; - } - if let Some(release) = (*h).release { - release((*h).ctx); - } - (*h).ctx = std::ptr::null_mut(); - (*h).addref = None; - (*h).release = None; - } - } -} - -// ============================================================================ -// render/color.h -// ============================================================================ - -/// `include/render/color.h` exports: color processors, the LUT library, -/// and the ColorManager statics. -pub mod color { - use super::*; - - /// `oakrender_color_processor_create`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_color_processor_create( - src_space: *const c_char, - dst_transform: *const c_char, - direction: c_int, - ) -> OakColorProcessor { - crate::handle::guard_handle(|| { - let src = unsafe { cstr(src_space) }.ok_or(crate::error::Error::Invalid)?; - let dst = unsafe { cstr(dst_transform) }.ok_or(crate::error::Error::Invalid)?; - if src.is_empty() || dst.is_empty() { - return Err(crate::error::Error::Invalid); - } - let dir = direction_kind(direction)?; - let processor = crate::color::ColorProcessor::create(&src, &dst, dir) - .ok_or(crate::error::Error::Invalid)?; - Ok(crate::handle::make_owned(processor)) - }) - } - - /// `oakrender_color_processor_free`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_color_processor_free(processor: *mut OakColorProcessor) { - crate::handle::guard_void(|| free_handle(processor)); - } - - /// `oakrender_color_processor_is_valid`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_color_processor_is_valid( - processor: OakColorProcessor, - ) -> c_int { - match unsafe { crate::handle::get::(&processor) } { - Some(p) if p.is_valid() => 1, - _ => 0, - } - } - - /// `oakrender_color_processor_create_transform` — the node color - /// manager and transform decode belong to the oaknode/oakcommon - /// bridges (pending); the input colorspace is resolved against the - /// default config instead. - #[no_mangle] - pub unsafe extern "C" fn oakrender_color_processor_create_transform( - _manager: OakNodeColorManager, - input: *const c_char, - _dest: OakColorTransform, - direction: c_int, - ) -> OakColorProcessor { - crate::handle::guard_handle(|| { - let input = unsafe { cstr(input) }.ok_or(crate::error::Error::Invalid)?; - if input.is_empty() { - return Err(crate::error::Error::Invalid); - } - let dir = direction_kind(direction)?; - // Destination transform decode is a node/common bridge feature; - // until it lands the output transform defaults to the config's - // reference role (documented deviation). - let processor = crate::color::ColorProcessor::create(&input, "reference", dir) - .ok_or(crate::error::Error::Invalid)?; - Ok(crate::handle::make_owned(processor)) - }) - } - - /// `oakrender_color_processor_create_lut`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_color_processor_create_lut( - _manager: OakNodeColorManager, - path: *const c_char, - direction: c_int, - ) -> OakColorProcessor { - crate::handle::guard_handle(|| { - let path = unsafe { cstr(path) }.ok_or(crate::error::Error::Invalid)?; - if path.is_empty() { - return Err(crate::error::Error::Invalid); - } - let dir = direction_kind(direction)?; - let processor = crate::color::ColorProcessor::create_lut(&path, dir) - .ok_or(crate::error::Error::Invalid)?; - Ok(crate::handle::make_owned(processor)) - }) - } - - /// `oakrender_color_processor_create_grading_primary`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_color_processor_create_grading_primary( - _manager: OakNodeColorManager, - style: c_int, - ) -> OakColorProcessor { - crate::handle::guard_handle(|| { - let style = match style { - 0 => crate::color::GradingStyle::Lin, - 1 => crate::color::GradingStyle::Log, - _ => return Err(crate::error::Error::Invalid), - }; - let processor = crate::color::ColorProcessor::create_grading_primary(style) - .ok_or(crate::error::Error::Invalid)?; - Ok(crate::handle::make_owned(processor)) - }) - } - - /// `oakrender_lut_is_supported_extension`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_lut_is_supported_extension( - extension: *const c_char, - ) -> c_int { - match unsafe { cstr(extension) } { - Some(ext) if crate::color::is_supported_lut_extension(&ext) => 1, - _ => 0, - } - } - - /// `oakrender_lut_supported_extensions_count`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_lut_supported_extensions_count() -> c_int { - crate::color::SUPPORTED_LUT_EXTENSIONS.len() as c_int - } - - /// `oakrender_lut_supported_extension_at` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oakrender_lut_supported_extension_at( - index: c_int, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - if index < 0 || index >= crate::color::SUPPORTED_LUT_EXTENSIONS.len() as c_int { - return crate::error::OAKRENDER_E_NOT_FOUND; - } - guard_string(|| { - Ok(write_string( - crate::color::SUPPORTED_LUT_EXTENSIONS[index as usize], - buf, - buf_size, - )) - }) - } - - /// `oakrender_color_processor_convert`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_color_processor_convert( - processor: OakColorProcessor, - ir: f64, - ig: f64, - ib: f64, - ia: f64, - out_r: *mut f64, - out_g: *mut f64, - out_b: *mut f64, - out_a: *mut f64, - ) -> c_int { - crate::handle::guard(|| { - if out_r.is_null() || out_g.is_null() || out_b.is_null() || out_a.is_null() { - return Err(crate::error::Error::Invalid); - } - let p = unsafe { crate::handle::get::(&processor) } - .ok_or(crate::error::Error::Invalid)?; - let out = p.convert_color([ir, ig, ib, ia]); - unsafe { - *out_r = out[0]; - *out_g = out[1]; - *out_b = out[2]; - *out_a = out[3]; - } - Ok(()) - }) - } - - /// `oakrender_color_processor_convert_frame`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_color_processor_convert_frame( - processor: OakColorProcessor, - frame: OakCodecFrame, - ) -> c_int { - crate::handle::guard(|| { - let p = unsafe { crate::handle::get::(&processor) } - .ok_or(crate::error::Error::Invalid)?; - let f = unsafe { crate::handle::get_mut::(&frame) } - .ok_or(crate::error::Error::Invalid)?; - p.convert_frame(f) - }) - } - - /// `oakrender_color_manager_set_up_default_config`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_color_manager_set_up_default_config() -> c_int { - crate::handle::guard(|| { - crate::color::set_up_default_config()?; - Ok(()) - }) - } - - /// `oakrender_color_manager_get_config` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oakrender_color_manager_get_config( - buf: *mut c_char, - n: c_int, - ) -> c_int { - match crate::color::config_path() { - Some(path) => guard_string(|| Ok(write_string(&path, buf, n))), - None => crate::error::OAKRENDER_E_STATE, - } - } - - /// `oakrender_color_manager_display_transform` (two-stage). - #[no_mangle] - pub unsafe extern "C" fn oakrender_color_manager_display_transform( - display: *const c_char, - view: *const c_char, - buf: *mut c_char, - n: c_int, - ) -> c_int { - let (Some(display), Some(view)) = (unsafe { cstr(display) }, unsafe { cstr(view) }) else { - return crate::error::OAKRENDER_E_INVALID; - }; - if display.is_empty() || view.is_empty() { - return crate::error::OAKRENDER_E_INVALID; - } - match crate::color::display_transform_result(&display, &view) { - Ok(Some(id)) => guard_string(|| Ok(write_string(&id, buf, n))), - Ok(None) => crate::error::OAKRENDER_E_NOT_FOUND, - Err(e) => e.code(), - } - } - - fn direction_kind(direction: c_int) -> crate::error::Result { - match direction { - 0 => Ok(crate::color::Direction::Normal), - 1 => Ok(crate::color::Direction::Inverse), - _ => Err(crate::error::Error::Invalid), - } - } - - fn free_handle(h: *mut OakColorProcessor) { - if h.is_null() { - return; - } - unsafe { - if (*h).is_null() { - return; - } - if let Some(release) = (*h).release { - release((*h).ctx); - } - (*h).ctx = std::ptr::null_mut(); - (*h).addref = None; - (*h).release = None; - } - } -} - -// ============================================================================ -// render/copier.h -// ============================================================================ - -/// `include/render/copier.h` exports: the project copier. -pub mod copier { - use super::*; - - /// `oakrender_project_copier_create`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_project_copier_create() -> OakRenderProjectCopier { - crate::handle::make_owned(crate::copier::ProjectCopy::new()) - } - - /// `oakrender_project_copier_free`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_project_copier_free(copier: *mut OakRenderProjectCopier) { - crate::handle::guard_void(|| free_handle(copier)); - } - - /// `oakrender_project_copier_set_project`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_project_copier_set_project( - copier: OakRenderProjectCopier, - project: OakNodeProject, - ) -> c_int { - crate::handle::guard(|| { - if project.is_null() { - return Err(crate::error::Error::Invalid); - } - let c = unsafe { crate::handle::get_mut::(&copier) } - .ok_or(crate::error::Error::Invalid)?; - c.set_project(project) - }) - } - - /// `oakrender_project_copier_get_copy` — the node-map query belongs to - /// the oaknode bridge (pending); returns an empty handle until then. - #[no_mangle] - pub unsafe extern "C" fn oakrender_project_copier_get_copy( - copier: OakRenderProjectCopier, - original: OakNodeNode, - ) -> OakNodeNode { - let _ = (copier, original); - CHandle::null() - } - - /// `oakrender_project_copier_get_copied_project` — borrowed handle - /// boxing the copy identity (the actual node handle lives in - /// oaknode; release only frees the box). - #[no_mangle] - pub unsafe extern "C" fn oakrender_project_copier_get_copied_project( - copier: OakRenderProjectCopier, - ) -> OakNodeProject { - match unsafe { crate::handle::get::(&copier) } { - Some(c) => match c.copied_project() { - Some(h) if !h.is_null() => crate::handle::make_borrowed_owned(BorrowedOpaque { - identity: h.ctx as u64, - }), - _ => CHandle::null(), - }, - None => CHandle::null(), - } - } - - fn free_handle(h: *mut OakRenderProjectCopier) { - if h.is_null() { - return; - } - unsafe { - if (*h).is_null() { - return; - } - if let Some(release) = (*h).release { - release((*h).ctx); - } - (*h).ctx = std::ptr::null_mut(); - (*h).addref = None; - (*h).release = None; - } - } -} - -// ============================================================================ -// render/cancelatom.h -// ============================================================================ - -/// `include/render/cancelatom.h` exports: the cancellation primitive. -pub mod cancelatom { - use super::*; - - /// `oakrender_cancelatom_init`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cancelatom_init() -> OakCancelAtom { - crate::handle::make_owned(crate::cancelatom::CancelAtom::new()) - } - - /// `oakrender_cancelatom_free`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cancelatom_free(atom: *mut OakCancelAtom) { - crate::handle::guard_void(|| free_handle(atom)); - } - - /// `oakrender_cancelatom_cancel`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cancelatom_cancel(atom: OakCancelAtom) -> c_int { - crate::handle::guard(|| { - let a = unsafe { crate::handle::get::(&atom) } - .ok_or(crate::error::Error::Invalid)?; - a.cancel(); - Ok(()) - }) - } - - /// `oakrender_cancelatom_is_cancelled`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cancelatom_is_cancelled( - atom: OakCancelAtom, - cancelled: *mut c_int, - ) -> c_int { - crate::handle::guard(|| { - if cancelled.is_null() { - return Err(crate::error::Error::Invalid); - } - let a = unsafe { crate::handle::get::(&atom) } - .ok_or(crate::error::Error::Invalid)?; - unsafe { *cancelled = a.is_cancelled() as c_int }; - Ok(()) - }) - } - - /// `oakrender_cancelatom_heard_cancel`. - #[no_mangle] - pub unsafe extern "C" fn oakrender_cancelatom_heard_cancel( - atom: OakCancelAtom, - heard: *mut c_int, - ) -> c_int { - crate::handle::guard(|| { - if heard.is_null() { - return Err(crate::error::Error::Invalid); - } - let a = unsafe { crate::handle::get::(&atom) } - .ok_or(crate::error::Error::Invalid)?; - unsafe { *heard = a.heard_cancel() as c_int }; - Ok(()) - }) - } - - fn free_handle(h: *mut OakCancelAtom) { - if h.is_null() { - return; - } - unsafe { - if (*h).is_null() { - return; - } - if let Some(release) = (*h).release { - release((*h).ctx); - } - (*h).ctx = std::ptr::null_mut(); - (*h).addref = None; - (*h).release = None; - } - } -} diff --git a/crates/oakrender/src/lib.rs b/crates/oakrender/src/lib.rs index 2f0fbe7a7..bde4899e6 100644 --- a/crates/oakrender/src/lib.rs +++ b/crates/oakrender/src/lib.rs @@ -40,14 +40,13 @@ pub mod autocacher; pub mod backend; -pub mod bridge; pub mod cache; pub mod cancelatom; pub mod color; +pub mod commonutil; pub mod copier; pub mod error; pub mod eval; -pub mod ffi; pub mod frame; pub mod handle; pub mod manager; diff --git a/crates/oakrender/src/manager.rs b/crates/oakrender/src/manager.rs index 2c950a8b7..38166103d 100644 --- a/crates/oakrender/src/manager.rs +++ b/crates/oakrender/src/manager.rs @@ -243,7 +243,7 @@ mod tests { #[test] fn disk_cache_size_and_clear() { - let _guard = crate::bridge::common::ENV_TEST_LOCK + let _guard = crate::commonutil::ENV_TEST_LOCK .lock() .unwrap_or_else(|e| e.into_inner()); let dir = std::env::temp_dir().join("oakrender-diskcache-test"); @@ -260,7 +260,7 @@ mod tests { #[test] fn disk_cache_size_missing_dir_is_zero() { - let _guard = crate::bridge::common::ENV_TEST_LOCK + let _guard = crate::commonutil::ENV_TEST_LOCK .lock() .unwrap_or_else(|e| e.into_inner()); let dir = std::env::temp_dir().join("oakrender-diskcache-missing"); diff --git a/crates/oakrender/tests/cache_test.rs b/crates/oakrender/tests/cache_test.rs deleted file mode 100644 index c8d26395e..000000000 --- a/crates/oakrender/tests/cache_test.rs +++ /dev/null @@ -1,483 +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 . - -//! Cache contract tests (cache.rs). Mirrors the C++ oakrender cache -//! gtest expectations plus Rust-side ownership rules. - -mod common; - -use std::ffi::c_char; - -use oakrender::error::{OAKRENDER_E_INVALID, OAKRENDER_E_NOT_FOUND, OAKRENDER_OK}; -use oakrender::ffi; -use oakrender::handle::CHandle; - -/// A fresh detached video-frame cache. -fn cache() -> ffi::OakRenderCache { - unsafe { ffi::cache::oakrender_cache_create() } -} - -fn free(mut c: ffi::OakRenderCache) { - unsafe { ffi::cache::oakrender_cache_free(&mut c) }; -} - -/// invalidate/validate state machine: fresh cache reports the whole -/// query range invalidated; validate shrinks it; invalidate splits. -#[test] -fn invalidate_validate_state_machine() { - let _dir = common::CacheDirGuard::new(); - let c = cache(); - unsafe { - // Fresh cache: nothing validated. - assert_eq!(ffi::cache::oakrender_cache_has_validated_ranges(c), 0); - assert_eq!( - ffi::cache::oakrender_cache_set_timebase(c, 1, 25), - OAKRENDER_OK - ); - - // Whole query range invalidated on a fresh cache (1 second). - let mut ranges = [0i64; 16]; - let count = ffi::cache::oakrender_cache_get_invalidated_ranges( - c, - 0, - 1, - 1, - 1, - ranges.as_mut_ptr(), - 4, - ); - assert_eq!(count, 1); - assert_eq!(ranges[0], 0); - assert_eq!(ranges[1], 1); - assert_eq!(ranges[2], 1); - assert_eq!(ranges[3], 1); - - // Validate timestamps [0, 25) at 25fps → the second [0, 1). - ffi::cache::oakrender_cache_validate(c, 0, 25); - assert_eq!(ffi::cache::oakrender_cache_has_validated_ranges(c), 1); - let count = ffi::cache::oakrender_cache_get_invalidated_ranges( - c, - 0, - 1, - 1, - 1, - ranges.as_mut_ptr(), - 4, - ); - assert_eq!(count, 0); - - // Invalidate timestamps [10, 20) → [0.4, 0.8) invalidated. - ffi::cache::oakrender_cache_invalidate(c, 10, 20); - assert_eq!(ffi::cache::oakrender_cache_has_validated_ranges(c), 1); - let count = ffi::cache::oakrender_cache_get_invalidated_ranges( - c, - 0, - 1, - 1, - 1, - ranges.as_mut_ptr(), - 4, - ); - assert_eq!(count, 1); - assert_eq!(ranges[0], 2); - assert_eq!(ranges[1], 5); - assert_eq!(ranges[2], 4); - assert_eq!(ranges[3], 5); - - // Invalidate everything: back to empty. - ffi::cache::oakrender_cache_invalidate(c, 0, 25); - assert_eq!(ffi::cache::oakrender_cache_has_validated_ranges(c), 0); - - // Empty cache: no-op + zero results. - ffi::cache::oakrender_cache_invalidate(CHandle::null(), 0, 10); - ffi::cache::oakrender_cache_validate(CHandle::null(), 0, 10); - assert_eq!( - ffi::cache::oakrender_cache_has_validated_ranges(CHandle::null()), - 0 - ); - // Negative max_ranges rejected. - assert_eq!( - ffi::cache::oakrender_cache_get_invalidated_ranges( - c, - 0, - 1, - 1, - 1, - std::ptr::null_mut(), - -1 - ), - OAKRENDER_E_INVALID - ); - } - free(c); -} - -/// Timebase validation + timestamp semantics. -#[test] -fn timebase_and_timestamp_semantics() { - let c = cache(); - unsafe { - assert_eq!( - ffi::cache::oakrender_cache_set_timebase(c, 1, 25), - OAKRENDER_OK - ); - assert_eq!( - ffi::cache::oakrender_cache_set_timebase(CHandle::null(), 1, 25), - OAKRENDER_E_INVALID - ); - assert_eq!( - ffi::cache::oakrender_cache_set_timebase(c, 0, 25), - OAKRENDER_E_INVALID - ); - assert_eq!( - ffi::cache::oakrender_cache_set_timebase(c, 1, 0), - OAKRENDER_E_INVALID - ); - - // Validate [0,25) timestamps at 25fps → 1 second. - ffi::cache::oakrender_cache_validate(c, 0, 25); - let mut num = 0; - let mut den = 0; - assert_eq!( - ffi::cache::oakrender_cache_get_timebase(c, &mut num, &mut den), - OAKRENDER_OK - ); - assert_eq!(num, 1); - assert_eq!(den, 25); - - // set_uuid round-trips through the two-stage getter. - assert_eq!( - ffi::cache::oakrender_cache_set_uuid( - c, - c"{01234567-89ab-cdef-0123-456789abcdef}".as_ptr() - ), - OAKRENDER_OK - ); - let (_, uuid) = - common::read_two_stage(|buf, n| ffi::cache::oakrender_cache_get_uuid(c, buf, n)); - assert_eq!( - uuid.as_deref(), - Some("{01234567-89ab-cdef-0123-456789abcdef}") - ); - assert_eq!( - ffi::cache::oakrender_cache_set_uuid(c, std::ptr::null()), - OAKRENDER_E_INVALID - ); - } - free(c); -} - -/// Passthrough: linked cache ranges are excluded from -/// invalidated_ranges; unlink restores them. -#[test] -fn passthrough_excludes_ranges() { - let a = cache(); - let b = cache(); - unsafe { - ffi::cache::oakrender_cache_validate(b, 0, 10); - // Empty `other` → invalid. - assert_eq!( - ffi::cache::oakrender_cache_set_passthrough(a, CHandle::null()), - OAKRENDER_E_INVALID - ); - assert_eq!( - ffi::cache::oakrender_cache_set_passthrough(a, b), - OAKRENDER_OK - ); - - // Passthrough ranges report count; the range excluded from invalidated. - let mut passthroughs = [0i64; 16]; - let count = ffi::cache::oakrender_cache_get_passthroughs(a, passthroughs.as_mut_ptr(), 4); - assert_eq!(count, 1); - assert_eq!(passthroughs[0], 0); - assert_eq!(passthroughs[2], 10); - - let mut ranges = [0i64; 16]; - let count = ffi::cache::oakrender_cache_get_invalidated_ranges( - a, - 0, - 1, - 20, - 1, - ranges.as_mut_ptr(), - 4, - ); - assert_eq!(count, 1, "only [10,20) remains invalidated"); - assert_eq!(ranges[0], 10); - - // Invalidating over the passthrough unlinks it (C++ semantics). - ffi::cache::oakrender_cache_invalidate_range(a, 5, 1, 15, 1); - let count = ffi::cache::oakrender_cache_get_passthroughs(a, std::ptr::null_mut(), 0); - assert_eq!(count, 0); - } - free(a); - free(b); -} - -/// Disk state round-trip: save_state → clear → load_state restores -/// validated ranges byte-identically (C++ binary format parity). -#[test] -fn disk_state_roundtrip() { - let _dir = common::CacheDirGuard::new(); - let dir = std::env::temp_dir().join(format!("oakrender-ffi-cache-{}", std::process::id())); - - let c = cache(); - unsafe { - ffi::cache::oakrender_cache_set_timebase(c, 1001, 30000); - ffi::cache::oakrender_cache_validate(c, 0, 30); - ffi::cache::oakrender_cache_validate(c, 60, 90); - assert_eq!(ffi::cache::oakrender_cache_save_state(c), OAKRENDER_OK); - - // Load into a fresh cache: the saved uuid + state come back. - let c2 = cache(); - let (_, uuid) = - common::read_two_stage(|buf, n| ffi::cache::oakrender_cache_get_uuid(c, buf, n)); - ffi::cache::oakrender_cache_set_uuid(c2, uuid.unwrap().as_ptr() as *const c_char); - ffi::cache::oakrender_cache_set_timebase(c2, 1001, 30000); - assert_eq!(ffi::cache::oakrender_cache_load_state(c2), OAKRENDER_OK); - assert_eq!(ffi::cache::oakrender_cache_has_validated_ranges(c2), 1); - let mut ranges = [0i64; 16]; - let count = ffi::cache::oakrender_cache_get_invalidated_ranges( - c2, - 0, - 1, - 300, - 1, - ranges.as_mut_ptr(), - 8, - ); - // validated [0,30) + [60,90) at 30000/1001 fps → two gaps: - // [1001/1000, 1001/500) and [3003/1000, 300). - assert_eq!(count, 2); - assert_eq!( - (ranges[0], ranges[1], ranges[2], ranges[3]), - (1001, 1000, 1001, 500) - ); - assert_eq!( - (ranges[4], ranges[5], ranges[6], ranges[7]), - (3003, 1000, 300, 1) - ); - // Empty cache load → invalid. - assert_eq!( - ffi::cache::oakrender_cache_load_state(CHandle::null()), - OAKRENDER_E_INVALID - ); - free(c2); - } - free(c); -} - -/// frame_filename is deterministic for (uuid, timebase, time) and -/// matches the C++ naming scheme exactly (shared disk caches between -/// C++ and Rust builds must not diverge). -#[test] -fn frame_filename_parity() { - let _dir = common::CacheDirGuard::new(); - let c = cache(); - unsafe { - ffi::cache::oakrender_cache_set_timebase(c, 1, 30); - ffi::cache::oakrender_cache_set_uuid(c, c"{01234567-89ab-cdef-0123-456789abcdef}".as_ptr()); - // Frame 15 at 30fps = 0.5 s. - let (size, name) = common::read_two_stage(|buf, n| { - ffi::cache::oakrender_cache_get_valid_cache_filename(c, 1, 2, buf, n) - }); - // Not validated yet → NOT_FOUND. - assert_eq!(size, OAKRENDER_E_NOT_FOUND); - assert!(name.is_none()); - - // Validate frame 15 (0.5s → 1.0s) and query again. - ffi::cache::oakrender_cache_validate(c, 15, 16); - let (size, name) = common::read_two_stage(|buf, n| { - ffi::cache::oakrender_cache_get_valid_cache_filename(c, 1, 2, buf, n) - }); - assert!(size > 0); - let name = name.unwrap(); - let suffix = format!("{}/15", "{01234567-89ab-cdef-0123-456789abcdef}"); - assert!( - name.ends_with(&suffix), - "filename scheme `//`; got {name}" - ); - - // Non-frame-hash caches reject the query (audio kind). - let audio = ffi::cache::oakrender_cache_create_for_node(common::fake_handle(1), 2); - assert_eq!( - ffi::cache::oakrender_cache_get_valid_cache_filename( - audio, - 1, - 2, - std::ptr::null_mut(), - 0 - ), - OAKRENDER_E_INVALID - ); - let mut audio = audio; - ffi::cache::oakrender_cache_free(&mut audio); - } - free(c); -} - -/// Lock/unlock pairing and empty-cache no-op. -#[test] -fn lock_pairing() { - let c = cache(); - unsafe { - ffi::cache::oakrender_cache_lock(c); - ffi::cache::oakrender_cache_unlock(c); - // Empty cache: no-ops. - ffi::cache::oakrender_cache_lock(CHandle::null()); - ffi::cache::oakrender_cache_unlock(CHandle::null()); - } - free(c); -} - -/// Frame-cache load failure paths (the success path needs oakcodec). -#[test] -fn frame_cache_load_errors() { - let c = cache(); - unsafe { - // NULL args → invalid. - assert_eq!( - ffi::cache::oakrender_frame_cache_load( - c, - std::ptr::null(), - std::ptr::null(), - 0, - std::ptr::null_mut() - ), - OAKRENDER_E_INVALID - ); - // Missing file → NOT_FOUND. - let dir = std::env::temp_dir().join("oakrender-nonexistent-cache"); - let dir_c = std::ffi::CString::new(dir.to_string_lossy().as_bytes()).unwrap(); - let mut out = CHandle::null(); - assert_eq!( - ffi::cache::oakrender_frame_cache_load( - c, - dir_c.as_ptr(), - c"{00000000-0000-0000-0000-000000000000}".as_ptr(), - 5, - &mut out - ), - OAKRENDER_E_NOT_FOUND - ); - // Existing file but no codec ABI → FAILED (explainable). - let dir2 = std::env::temp_dir().join("oakrender-existing-cache"); - std::fs::create_dir_all(dir2.join("{00000000-0000-0000-0000-000000000000}")).unwrap(); - std::fs::write( - dir2.join("{00000000-0000-0000-0000-000000000000}") - .join("5"), - b"not-an-exr", - ) - .unwrap(); - let dir2_c = std::ffi::CString::new(dir2.to_string_lossy().as_bytes()).unwrap(); - let rc = ffi::cache::oakrender_frame_cache_load( - c, - dir2_c.as_ptr(), - c"{00000000-0000-0000-0000-000000000000}".as_ptr(), - 5, - &mut out, - ); - assert!( - rc != OAKRENDER_OK, - "decode is codec-dependent; without oakcodec it must fail explainably" - ); - // save with empty args: no-op. - ffi::cache::oakrender_frame_cache_save( - CHandle::null(), - std::ptr::null(), - std::ptr::null(), - CHandle::null(), - ); - } - free(c); -} - -/// Borrowed cache handles are opaque this pass (C++ interop pending). -#[test] -fn borrowed_cache_is_opaque() { - unsafe { - let borrowed = ffi::cache::oakrender_cache_wrap_borrowed(0x1234 as *mut std::ffi::c_void); - assert!( - !borrowed.is_null(), - "non-null native pointer → non-empty handle" - ); - // Queries on a borrowed box are invalid (cannot dereference the - // C++ object through the Rust ABI). - assert_eq!( - ffi::cache::oakrender_cache_get_uuid(borrowed, std::ptr::null_mut(), 0), - OAKRENDER_E_INVALID - ); - let mut b = borrowed; - ffi::cache::oakrender_cache_free(&mut b); - } -} - -/// Frame-cache save/load round-trip through the oakcodec EXR/JPEG -/// payload codec. Gated: the oakcodec crate is finished concurrently. -#[test] -#[ignore = "needs oakcodec final"] -fn frame_cache_save_load_roundtrip() { - let _dir = common::CacheDirGuard::new(); - let c = cache(); - unsafe { - ffi::cache::oakrender_cache_set_timebase(c, 1, 30); - let frame = ffi::renderer::oakrender_codec_frame_create(); - let mut pod = std::mem::zeroed::(); - pod.width = 8; - pod.height = 8; - pod.format = 4; // F32 - assert_eq!( - ffi::renderer::oakrender_codec_frame_set_video_params(frame, &pod), - 0 - ); - assert_eq!(ffi::renderer::oakrender_codec_frame_allocate(frame), 0); - let dir = _dir.dir(); - let dir_c = std::ffi::CString::new(dir.to_string_lossy().as_bytes()).unwrap(); - let uuid = c"{01234567-89ab-cdef-0123-456789abcdef}".as_ptr(); - ffi::cache::oakrender_frame_cache_save(c, dir_c.as_ptr(), uuid, frame); - let mut out = CHandle::null(); - assert_eq!( - ffi::cache::oakrender_frame_cache_load(c, dir_c.as_ptr(), uuid, 0, &mut out), - 0, - "cached frame decodes back" - ); - assert!(!out.is_null()); - assert_eq!(ffi::renderer::oakrender_codec_frame_width(out), 8); - let mut out = out; - ffi::renderer::oakrender_codec_frame_free(&mut out); - let mut frame = frame; - ffi::renderer::oakrender_codec_frame_free(&mut frame); - } - free(c); -} - -/// Saving toggle affects the auto-save behavior. -#[test] -fn saving_enabled_toggle() { - let c = cache(); - unsafe { - assert_eq!( - ffi::cache::oakrender_cache_set_saving_enabled(c, 0), - OAKRENDER_OK - ); - assert_eq!( - ffi::cache::oakrender_cache_set_saving_enabled(CHandle::null(), 1), - OAKRENDER_E_INVALID - ); - assert_eq!(ffi::cache::oakrender_cache_indicator_height(), 4); - } - free(c); -} diff --git a/crates/oakrender/tests/common/mod.rs b/crates/oakrender/tests/common/mod.rs index fc0d8fdd9..1521292b5 100644 --- a/crates/oakrender/tests/common/mod.rs +++ b/crates/oakrender/tests/common/mod.rs @@ -56,67 +56,6 @@ pub fn fake_handle(seed: usize) -> oakrender::handle::CHandle { } } -/// Pins `OAK_CONFIG_DIR` to a fresh temp directory for the duration of -/// the guard, keeping cache state writes out of the real media cache. -/// Serializes against other env-mutating tests via the crate's env lock. -pub struct CacheDirGuard { - old: Option, - dir: std::path::PathBuf, - _env: MutexGuard<'static, ()>, -} - -impl CacheDirGuard { - /// Set up a temp cache dir and return the guard (plus the dir). - pub fn new() -> Self { - let env = oakrender::bridge::common::ENV_TEST_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); - let old = std::env::var_os("OAK_CONFIG_DIR"); - let dir = std::env::temp_dir().join(format!("oakrender-it-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).unwrap(); - std::env::set_var("OAK_CONFIG_DIR", &dir); - Self { - old, - dir, - _env: env, - } - } - - /// The temp directory. - pub fn dir(&self) -> &std::path::Path { - &self.dir - } -} - -impl Drop for CacheDirGuard { - fn drop(&mut self) { - std::env::remove_var("OAK_CONFIG_DIR"); - if let Some(old) = self.old.take() { - std::env::set_var("OAK_CONFIG_DIR", old); - } - let _ = std::fs::remove_dir_all(&self.dir); - } -} - -/// C string buffer readback helper for two-stage getters. -pub fn read_two_stage(getter: impl Fn(*mut std::ffi::c_char, i32) -> i32) -> (i32, Option) { - use std::ffi::c_char; - unsafe { - let size = getter(std::ptr::null_mut(), 0); - if size <= 0 { - return (size, None); - } - let mut buf = vec![0u8; size as usize]; - let got = getter(buf.as_mut_ptr() as *mut c_char, size); - if got <= 0 { - return (got, None); - } - let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len()); - (got, Some(String::from_utf8_lossy(&buf[..end]).into_owned())) - } -} - // --------------------------------------------------------------------------- // Host-symbol stand-ins (oakcore_* / fb_find_best_pix_fmt_of_list) // --------------------------------------------------------------------------- diff --git a/crates/oakrender/tests/copier_test.rs b/crates/oakrender/tests/copier_test.rs index 8af70b0d4..f17752e14 100644 --- a/crates/oakrender/tests/copier_test.rs +++ b/crates/oakrender/tests/copier_test.rs @@ -74,8 +74,8 @@ fn sync_matches_fresh_copy() { let mut copier = oakrender::copier::ProjectCopy::new(); let src = common::fake_handle(7); copier.set_project(src).unwrap(); - let changes = [oakrender::bridge::node::ChangeRecord { - kind: oakrender::bridge::node::change_kind::NODE_ADD, + let changes = [oakrender::copier::ChangeRecord { + kind: oakrender::copier::change_kind::NODE_ADD, payload: [0u8; 48], }]; copier.sync(&changes).unwrap(); @@ -130,24 +130,24 @@ fn cancel_video_tasks_semantics() { #[test] fn change_record_marshalling() { let kinds = [ - oakrender::bridge::node::change_kind::NODE_ADD, - oakrender::bridge::node::change_kind::NODE_REMOVE, - oakrender::bridge::node::change_kind::EDGE_ADD, - oakrender::bridge::node::change_kind::EDGE_REMOVE, - oakrender::bridge::node::change_kind::VALUE_CHANGE, - oakrender::bridge::node::change_kind::VALUE_HINT_CHANGE, - oakrender::bridge::node::change_kind::PROJECT_SETTING_CHANGE, - oakrender::bridge::node::change_kind::FOOTAGE_PROXY, + oakrender::copier::change_kind::NODE_ADD, + oakrender::copier::change_kind::NODE_REMOVE, + oakrender::copier::change_kind::EDGE_ADD, + oakrender::copier::change_kind::EDGE_REMOVE, + oakrender::copier::change_kind::VALUE_CHANGE, + oakrender::copier::change_kind::VALUE_HINT_CHANGE, + oakrender::copier::change_kind::PROJECT_SETTING_CHANGE, + oakrender::copier::change_kind::FOOTAGE_PROXY, ]; for kind in kinds { - let record = oakrender::bridge::node::ChangeRecord { + let record = oakrender::copier::ChangeRecord { kind, payload: [0xAA; 48], }; assert_eq!(record.kind, kind); assert_eq!(record.payload.len(), 48); assert_eq!( - std::mem::size_of::(), + std::mem::size_of::(), 52 ); } @@ -165,8 +165,8 @@ fn copier_error_paths() { Error::Invalid.code() ); // sync before any project → state error. - let changes = [oakrender::bridge::node::ChangeRecord { - kind: oakrender::bridge::node::change_kind::NODE_ADD, + let changes = [oakrender::copier::ChangeRecord { + kind: oakrender::copier::change_kind::NODE_ADD, payload: [0u8; 48], }]; assert_eq!( @@ -176,138 +176,3 @@ fn copier_error_paths() { // copy_of_node is deferred (node map query pending). assert!(copier.copy_of_node(1).is_none()); } - -/// The FFI copier entry points (error paths run standalone). -#[test] -fn ffi_copier_contract() { - use oakrender::error::{OAKRENDER_E_INVALID, OAKRENDER_OK}; - use oakrender::ffi; - unsafe { - let c = ffi::copier::oakrender_project_copier_create(); - assert!(!c.is_null()); - // set_project with an empty project → invalid. - assert_eq!( - ffi::copier::oakrender_project_copier_set_project(c, ffi::OakNodeProject::null()), - OAKRENDER_E_INVALID - ); - // With a fake project handle: deep copy needs the oaknode C ABI. - let rc = ffi::copier::oakrender_project_copier_set_project(c, common::fake_handle(1)); - assert_ne!( - rc, OAKRENDER_OK, - "without liboaknode the copy cannot be built" - ); - // get_copy on an empty copier → empty handle. - let copy = ffi::copier::oakrender_project_copier_get_copy(c, common::fake_handle(1)); - assert!(copy.is_null()); - // get_copied_project before any project → empty. - let proj = ffi::copier::oakrender_project_copier_get_copied_project(c); - assert!(proj.is_null()); - let mut c = c; - ffi::copier::oakrender_project_copier_free(&mut c); - assert!(c.is_null()); - } -} - -/// LUT library exports. -#[test] -fn lut_library_exports() { - use oakrender::ffi; - unsafe { - assert_eq!(ffi::color::oakrender_lut_supported_extensions_count(), 9); - assert_eq!( - ffi::color::oakrender_lut_is_supported_extension(c"cube".as_ptr()), - 1 - ); - assert_eq!( - ffi::color::oakrender_lut_is_supported_extension(c".CUBE".as_ptr()), - 1 - ); - assert_eq!( - ffi::color::oakrender_lut_is_supported_extension(c"exr".as_ptr()), - 0 - ); - assert_eq!( - ffi::color::oakrender_lut_is_supported_extension(std::ptr::null()), - 0 - ); - let mut buf = [0u8; 16]; - let size = ffi::color::oakrender_lut_supported_extension_at( - 0, - buf.as_mut_ptr() as *mut std::ffi::c_char, - 16, - ); - assert!(size > 0); - assert_eq!( - ffi::color::oakrender_lut_supported_extension_at(9, std::ptr::null_mut(), 0), - -70004 - ); - } -} - -/// Color manager statics + processor FFI paths. -#[test] -fn color_ffi_paths() { - use oakrender::error::OAKRENDER_OK; - use oakrender::ffi; - unsafe { - // No default config yet → get_config is a state error. - let _ = ffi::color::oakrender_color_manager_set_up_default_config(); - let (size, config) = - common::read_two_stage(|buf, n| ffi::color::oakrender_color_manager_get_config(buf, n)); - if size > 0 { - assert!(!config.unwrap().is_empty()); - } - // create + is_valid + convert. - let p = ffi::color::oakrender_color_processor_create( - c"scene_linear".as_ptr(), - c"sdr-video".as_ptr(), - 0, - ); - if !p.is_null() { - let valid = ffi::color::oakrender_color_processor_is_valid(p); - let (mut r, mut g, mut b, mut a) = (0.0f64, 0.0f64, 0.0f64, 0.0f64); - assert_eq!( - ffi::color::oakrender_color_processor_convert( - p, 0.18, 0.18, 0.18, 1.0, &mut r, &mut g, &mut b, &mut a - ), - 0 - ); - assert!(a == 1.0); - // convert with NULL out → invalid. - assert_eq!( - ffi::color::oakrender_color_processor_convert( - p, - 0.0, - 0.0, - 0.0, - 0.0, - std::ptr::null_mut(), - &mut g, - &mut b, - &mut a - ), - -70001 - ); - assert_eq!(valid, 0, "validity depends on the bundled OCIO config"); - let mut p = p; - ffi::color::oakrender_color_processor_free(&mut p); - } - // display_transform errors. - assert_eq!( - ffi::color::oakrender_color_manager_display_transform( - std::ptr::null(), - c"v".as_ptr(), - std::ptr::null_mut(), - 0 - ), - -70001 - ); - let rc = ffi::color::oakrender_color_manager_display_transform( - c"no-such-display".as_ptr(), - c"no-such-view".as_ptr(), - std::ptr::null_mut(), - 0, - ); - assert!(rc == -70004 || rc == -70002 || rc == OAKRENDER_OK || rc > 0); - } -} diff --git a/crates/oakrender/tests/ffi_contract_test.rs b/crates/oakrender/tests/ffi_contract_test.rs deleted file mode 100644 index ed123f975..000000000 --- a/crates/oakrender/tests/ffi_contract_test.rs +++ /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 . - -//! C ABI contract tests (ffi.rs) + manager lifecycle. - -mod common; - -use std::ffi::c_char; -use std::sync::mpsc; -use std::time::Duration; - -use oakrender::error::{ - OAKRENDER_E_INVALID, OAKRENDER_E_NOT_FOUND, OAKRENDER_E_STATE, OAKRENDER_OK, -}; -use oakrender::ffi; -use oakrender::handle::{alive_count, CHandle}; -use std::sync::Mutex; - -/// Serializes the alive-count accounting test against handle creation in -/// the other tests of this binary. -static SERIAL: Mutex<()> = Mutex::new(()); - -/// manager init/shutdown idempotence; available() reflects state. -#[test] -fn manager_lifecycle() { - let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner()); - let _g = common::ManagerGuard::init(); - unsafe { - // Already initialized → state error. - assert_eq!(ffi::manager::oakrender_manager_init(), OAKRENDER_E_STATE); - assert_eq!(ffi::manager::oakrender_manager_available(), 1); - ffi::manager::oakrender_manager_shutdown(); - assert_eq!(ffi::manager::oakrender_manager_available(), 0); - // Re-init works after shutdown (C++ destroy_instance semantics). - assert_eq!(ffi::manager::oakrender_manager_init(), OAKRENDER_OK); - assert_eq!(ffi::manager::oakrender_manager_available(), 1); - } -} - -/// Every handle-returning export honors the empty-on-failure and -/// refcount-1-on-success contract (abi_version stamped). Serialized -/// against alive_count_accounting (both create handles). -#[test] -fn handle_contract_all_exports() { - let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner()); - unsafe { - // cache_create: owned, refcount 1, abi stamped. - let c = ffi::cache::oakrender_cache_create(); - assert!(!c.is_null()); - assert_eq!(c.abi_version, 1); - ffi::cache::oakrender_cache_free(&mut CHandle::null()); - let mut cc = c; - ffi::cache::oakrender_cache_free(&mut cc); - assert!(cc.is_null()); - - // wrap_borrowed(NULL) → empty handle. - assert!(ffi::cache::oakrender_cache_wrap_borrowed(std::ptr::null_mut()).is_null()); - - // create_for_node: empty parent → empty. - assert!(ffi::cache::oakrender_cache_create_for_node(CHandle::null(), 0).is_null()); - // Unknown kind → empty. - assert!(ffi::cache::oakrender_cache_create_for_node(common::fake_handle(1), 99).is_null()); - // Non-empty parent + valid kind → owned handle. - let n = ffi::cache::oakrender_cache_create_for_node(common::fake_handle(1), 0); - assert!(!n.is_null()); - assert_eq!(n.abi_version, 1); - let mut nn = n; - ffi::cache::oakrender_cache_free(&mut nn); - - // codec_frame_create. - let f = ffi::renderer::oakrender_codec_frame_create(); - assert!(!f.is_null()); - assert_eq!(f.abi_version, 1); - let mut ff = f; - ffi::renderer::oakrender_codec_frame_free(&mut ff); - assert!(ff.is_null()); - - // cancelatom_init. - let a = ffi::cancelatom::oakrender_cancelatom_init(); - assert!(!a.is_null()); - assert_eq!(a.abi_version, 1); - let mut aa = a; - ffi::cancelatom::oakrender_cancelatom_free(&mut aa); - assert!(aa.is_null()); - - // project_copier_create. - let pc = ffi::copier::oakrender_project_copier_create(); - assert!(!pc.is_null()); - assert_eq!(pc.abi_version, 1); - let mut pp = pc; - ffi::copier::oakrender_project_copier_free(&mut pp); - assert!(pp.is_null()); - - // renderer create functions. - let r = ffi::renderer::oakrender_display_renderer_create_opengl(); - assert!(!r.is_null()); - let mut rr = r; - ffi::renderer::oakrender_display_renderer_destroy(&mut rr); - - let r2 = ffi::renderer::oakrender_display_renderer_create_dynamic(std::ptr::null()); - assert!(r2.is_null(), "NULL backend id → empty"); - let r3 = ffi::renderer::oakrender_display_renderer_create_dynamic(c"opengl".as_ptr()); - assert!(!r3.is_null()); - let mut r3 = r3; - ffi::renderer::oakrender_display_renderer_destroy(&mut r3); - - // color processor: NULL strings → empty. - let p = ffi::color::oakrender_color_processor_create(std::ptr::null(), c"x".as_ptr(), 0); - assert!(p.is_null()); - // Bad direction → empty. - let p2 = ffi::color::oakrender_color_processor_create(c"a".as_ptr(), c"b".as_ptr(), 7); - assert!(p2.is_null()); - } -} - -/// free(NULL)/free(empty) no-op across every free export. -#[test] -fn free_null_noop_all_exports() { - let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner()); - unsafe { - ffi::cache::oakrender_cache_free(std::ptr::null_mut()); - ffi::renderer::oakrender_codec_frame_free(std::ptr::null_mut()); - ffi::renderer::oakrender_display_texture_free(std::ptr::null_mut()); - ffi::renderer::oakrender_display_renderer_destroy(std::ptr::null_mut()); - ffi::color::oakrender_color_processor_free(std::ptr::null_mut()); - ffi::copier::oakrender_project_copier_free(std::ptr::null_mut()); - ffi::cancelatom::oakrender_cancelatom_free(std::ptr::null_mut()); - ffi::ticket::oakrender_ticket_free(std::ptr::null_mut()); - - // Empty handles are no-ops too. - let mut empty = CHandle::null(); - ffi::cache::oakrender_cache_free(&mut empty); - ffi::renderer::oakrender_codec_frame_free(&mut empty); - ffi::cancelatom::oakrender_cancelatom_free(&mut empty); - } -} - -/// Two-stage string getters (disk_cache_path, uuids, filenames): -/// size query, short-buffer rule, exact fit. -#[test] -fn two_stage_string_contract() { - let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner()); - unsafe { - // disk_cache_path: size query then exact copy. - let (size, value) = - common::read_two_stage(|buf, n| ffi::manager::oakrender_disk_cache_path(buf, n)); - assert!(size > 0, "required size including NUL"); - let path = value.expect("buffer copy"); - assert_eq!(path.len() + 1, size as usize); - // Short buffer: size returned, no write. - let mut tiny = [0u8; 4]; - let needed = ffi::manager::oakrender_disk_cache_path(tiny.as_mut_ptr() as *mut c_char, 4); - assert_eq!(needed, size); - - // cache uuid getter. - let cache = ffi::cache::oakrender_cache_create(); - let (usize, uvalue) = - common::read_two_stage(|buf, n| ffi::cache::oakrender_cache_get_uuid(cache, buf, n)); - assert!(usize > 0); - let uuid = uvalue.unwrap(); - assert_eq!(uuid.len() + 1, usize as usize); - assert!(uuid.starts_with('{') && uuid.ends_with('}')); - // Empty cache → error. - assert_eq!( - ffi::cache::oakrender_cache_get_uuid(CHandle::null(), std::ptr::null_mut(), 0), - OAKRENDER_E_INVALID - ); - let mut c = cache; - ffi::cache::oakrender_cache_free(&mut c); - - // backend id getters. - let (bsize, bvalue) = - common::read_two_stage(|buf, n| ffi::renderer::oakrender_backend_id_at(0, buf, n)); - assert_eq!(bvalue.as_deref(), Some("opengl")); - assert_eq!(bsize, 7); // "opengl" + NUL - assert_eq!( - ffi::renderer::oakrender_backend_id_at(4, std::ptr::null_mut(), 0), - OAKRENDER_E_NOT_FOUND - ); - assert_eq!(ffi::renderer::oakrender_backend_count(), 4); - - // current_backend is the manager's backend when one is up (parallel - // tests may have initialized it) or the recorded default otherwise; - // either way it is a known id string. - let (_, current) = - common::read_two_stage(|buf, n| ffi::renderer::oakrender_current_backend(buf, n)); - let current = current.expect("current backend string"); - assert!( - [ - "opengl", - "vulkan", - "multiprocess", - "dummy", - "auto", - "metal", - "cpu" - ] - .contains(¤t.as_str()), - "unexpected current backend {current}" - ); - } -} - -/// debug_alive_count: cache/texture/processor create+free returns to -/// baseline (leak assertion). Serialized: other tests in this binary -/// create/free handles concurrently. -#[test] -fn alive_count_accounting() { - let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner()); - unsafe { - let baseline = alive_count(); - let mut caches = Vec::new(); - for _ in 0..4 { - caches.push(ffi::cache::oakrender_cache_create()); - } - assert_eq!(alive_count(), baseline + 4); - - let mut frames = Vec::new(); - for _ in 0..3 { - frames.push(ffi::renderer::oakrender_codec_frame_create()); - } - assert_eq!(alive_count(), baseline + 7); - - let atom = ffi::cancelatom::oakrender_cancelatom_init(); - assert_eq!(alive_count(), baseline + 8); - - // Retain doesn't create a new object (just +1 count on the box). - let retained = ffi::renderer::oakrender_codec_frame_retain(frames[0]); - assert_eq!(alive_count(), baseline + 8); - - for c in caches.iter_mut() { - ffi::cache::oakrender_cache_free(c); - } - for f in frames.iter_mut() { - ffi::renderer::oakrender_codec_frame_free(f); - } - ffi::renderer::oakrender_codec_frame_free(&mut retained.clone()); - let mut atom = atom; - ffi::cancelatom::oakrender_cancelatom_free(&mut atom); - assert_eq!(alive_count(), baseline, "all owned objects released"); - } -} - -/// Request-frame path: positive id, callback fires with an owned frame, -/// cancel semantics. -#[test] -fn request_frame_and_cancel() { - let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner()); - let _g = common::ManagerGuard::init(); - unsafe { - let (tx, rx): (mpsc::Sender, mpsc::Receiver) = mpsc::channel(); - let userdata = Box::into_raw(Box::new(42u32)); - extern "C" fn cb(_frame: ffi::OakCodecFrame, ts: i64, userdata: *mut std::ffi::c_void) { - let _ = userdata; - let _ = ts; - } - let id = ffi::manager::oakrender_request_frame( - common::fake_handle(7), - 12, - Some(cb), - userdata as *mut std::ffi::c_void, - ); - assert!(id > 0, "positive request id, got {id}"); - let _ = tx; - drop(rx); - // Wait for the completion: the request's ticket must finish. - std::thread::sleep(Duration::from_millis(200)); - - // Unknown id → NOT_FOUND. - assert_eq!( - ffi::manager::oakrender_cancel_request(id + 12345), - OAKRENDER_E_NOT_FOUND - ); - // Cancelling a finished request is OK (removes from the map). - let rc = ffi::manager::oakrender_cancel_request(id); - assert!(rc == OAKRENDER_OK || rc == OAKRENDER_E_NOT_FOUND); - unsafe { drop(Box::from_raw(userdata)) }; - } - - // Error paths without a manager (shut down within the guard scope). - unsafe { - ffi::manager::oakrender_manager_shutdown(); - assert_eq!( - ffi::manager::oakrender_request_frame(CHandle::null(), 0, None, std::ptr::null_mut()), - OAKRENDER_E_INVALID as i64 - ); - // Non-null viewer + cb but no manager → STATE. - extern "C" fn cb2(_f: ffi::OakCodecFrame, _t: i64, _u: *mut std::ffi::c_void) {} - assert_eq!( - ffi::manager::oakrender_request_frame( - common::fake_handle(1), - 0, - Some(cb2), - std::ptr::null_mut() - ), - OAKRENDER_E_STATE as i64 - ); - } -} - -/// set_aggressive_gc + cacher setters behave per contract. -#[test] -fn manager_settings() { - let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner()); - let _g = common::ManagerGuard::init(); - unsafe { - assert_eq!( - ffi::ticket::oakrender_manager_set_aggressive_gc(1), - OAKRENDER_OK - ); - assert_eq!( - ffi::manager::oakrender_set_cacher_multicam(common::fake_handle(3)), - OAKRENDER_OK - ); - assert_eq!( - ffi::manager::oakrender_set_cacher_multicam(CHandle::null()), - OAKRENDER_OK - ); - assert_eq!( - ffi::manager::oakrender_set_display_color_processor(CHandle::null()), - OAKRENDER_OK - ); - ffi::manager::oakrender_cancel_video_tasks(0); - ffi::manager::oakrender_cancel_video_tasks(1); - } - // Without a manager: STATE errors (shut down within the guard scope). - unsafe { - ffi::manager::oakrender_manager_shutdown(); - assert_eq!( - ffi::manager::oakrender_set_cacher_multicam(common::fake_handle(1)), - OAKRENDER_E_STATE - ); - assert_eq!( - ffi::manager::oakrender_set_display_color_processor(CHandle::null()), - OAKRENDER_E_STATE - ); - assert_eq!( - ffi::ticket::oakrender_manager_set_aggressive_gc(0), - OAKRENDER_E_STATE - ); - } -} diff --git a/crates/oakrender/tests/pipeline_test.rs b/crates/oakrender/tests/pipeline_test.rs index 7b4512e03..f052e3d52 100644 --- a/crates/oakrender/tests/pipeline_test.rs +++ b/crates/oakrender/tests/pipeline_test.rs @@ -197,285 +197,3 @@ fn gpu_path_f32_invariants() { ctx.destroy_texture(token); ctx.destroy_texture(dst); } - -/// FFI success paths: renderer → texture create/upload/download/params, -/// frame get/set, blit, color processors (real OCIO). -#[test] -fn ffi_display_renderer_texture_success_paths() { - use oakrender::error::OAKRENDER_OK; - use oakrender::ffi; - - unsafe { - // Color processor create with a real config. - let _ = ffi::color::oakrender_color_manager_set_up_default_config(); - - let proc = ffi::color::oakrender_color_processor_create( - c"ACEScg".as_ptr(), - c"sRGB Encoded Rec.709 (sRGB)".as_ptr(), - 0, - ); - if proc.is_null() { - eprintln!("no OCIO config; skipping processor half"); - } else { - assert_eq!(ffi::color::oakrender_color_processor_is_valid(proc), 1); - let (mut r, mut g, mut b, mut a) = (0.0, 0.0, 0.0, 0.0); - assert_eq!( - ffi::color::oakrender_color_processor_convert( - proc, 0.18, 0.18, 0.18, 1.0, &mut r, &mut g, &mut b, &mut a - ), - OAKRENDER_OK - ); - assert!(r != 0.18 || g != 0.18 || b != 0.18, "transform applied"); - assert!((a - 1.0).abs() < 1e-9, "alpha preserved"); - // convert_frame on an F32 frame. - let frame = ffi::renderer::oakrender_codec_frame_create(); - let mut pod = std::mem::zeroed::(); - pod.width = 4; - pod.height = 4; - pod.format = 4; - assert_eq!( - ffi::renderer::oakrender_codec_frame_set_video_params(frame, &pod), - OAKRENDER_OK - ); - assert_eq!( - ffi::renderer::oakrender_codec_frame_allocate(frame), - OAKRENDER_OK - ); - assert_eq!(ffi::renderer::oakrender_codec_frame_is_allocated(frame), 1); - assert_eq!(ffi::renderer::oakrender_codec_frame_width(frame), 4); - assert_eq!( - ffi::renderer::oakrender_codec_frame_linesize_bytes(frame), - 4 * 4 * 4 - ); - assert!(!ffi::renderer::oakrender_codec_frame_data(frame).is_null()); - // Fill 0.18 grey. - let data = ffi::renderer::oakrender_codec_frame_data(frame) as *mut f32; - for i in 0..(4 * 4 * 4) { - *data.add(i) = 0.18; - } - assert_eq!( - ffi::color::oakrender_color_processor_convert_frame(proc, frame), - OAKRENDER_OK - ); - let first = *data; - assert!( - (first - 0.18).abs() > 1e-4, - "convert_frame applied in place" - ); - // Error path: empty processor handle. - assert_eq!( - ffi::color::oakrender_color_processor_convert_frame( - ffi::OakColorProcessor::null(), - frame - ), - -70001 - ); - let mut frame = frame; - ffi::renderer::oakrender_codec_frame_free(&mut frame); - let mut proc = proc; - ffi::color::oakrender_color_processor_free(&mut proc); - } - - // Display renderer + texture success path (CPU-fallback capable). - let renderer = ffi::renderer::oakrender_display_renderer_create_opengl(); - assert!(!renderer.is_null()); - let rc = ffi::renderer::oakrender_display_renderer_init(renderer, std::ptr::null_mut()); - let is_open_gl = ffi::renderer::oakrender_display_renderer_is_open_gl(renderer); - let is_vulkan = ffi::renderer::oakrender_display_renderer_is_vulkan(renderer); - assert!( - is_open_gl == 1 || rc == -70003, - "GL renderer reports GL or init failed headless" - ); - assert_eq!(is_vulkan, 0); - - let mut vp = std::mem::zeroed::(); - vp.width = 8; - vp.height = 4; - vp.format = 4; - vp.pixel_aspect_num = 1; - vp.pixel_aspect_den = 1; - vp.divider = 1; - - let tex = - ffi::renderer::oakrender_display_texture_create(renderer, &vp, std::ptr::null(), 0); - assert!(!tex.is_null(), "texture created (GPU or CPU path)"); - assert_eq!(ffi::renderer::oakrender_display_texture_is_dummy(tex), 0); - - // Upload + download round-trip. - let linesize: i32 = 8 * 4 * 4; - let mut pixels = vec![0u8; linesize as usize * 4]; - for (i, px) in pixels.chunks_exact_mut(4).enumerate() { - px[0] = (i % 251) as u8; - px[1] = (i * 3 % 251) as u8; - px[2] = (i * 7 % 251) as u8; - px[3] = 255; - } - assert_eq!( - ffi::renderer::oakrender_display_texture_upload( - tex, - pixels.as_ptr() as *const std::ffi::c_void, - linesize - ), - OAKRENDER_OK - ); - let mut back = vec![0u8; linesize as usize * 4]; - assert_eq!( - ffi::renderer::oakrender_display_texture_download( - tex, - back.as_mut_ptr() as *mut std::ffi::c_void, - linesize - ), - OAKRENDER_OK - ); - assert_eq!(back, pixels, "upload/download round-trip"); - - // get_params returns the pod. - let mut out = std::mem::zeroed::(); - assert_eq!( - ffi::renderer::oakrender_display_texture_get_params(tex, &mut out), - OAKRENDER_OK - ); - assert_eq!(out.width, 8); - assert_eq!(out.height, 4); - assert_eq!(out.format, 4); - - // get_frame returns an owned frame handle. - let mut frame_h = ffi::OakCodecFrame::null(); - assert_eq!( - ffi::renderer::oakrender_display_texture_get_frame(tex, &mut frame_h), - OAKRENDER_OK - ); - assert!(!frame_h.is_null()); - assert_eq!(ffi::renderer::oakrender_codec_frame_width(frame_h), 8); - let mut frame_h = frame_h; - ffi::renderer::oakrender_codec_frame_free(&mut frame_h); - - // retain + error paths. - let retained = ffi::renderer::oakrender_display_texture_retain(tex); - assert!(!retained.is_null()); - let mut retained = retained; - ffi::renderer::oakrender_display_texture_free(&mut retained); - assert_eq!( - ffi::renderer::oakrender_display_texture_upload(tex, std::ptr::null(), 0), - -70001 - ); - let mut tex = tex; - ffi::renderer::oakrender_display_texture_free(&mut tex); - assert!(tex.is_null()); - - let mut renderer = renderer; - ffi::renderer::oakrender_display_renderer_destroy(&mut renderer); - } -} - -/// FFI blit success path (CPU path applies the OCIO processor in float). -#[test] -fn ffi_blit_color_managed_success() { - use oakrender::error::OAKRENDER_OK; - use oakrender::ffi; - - unsafe { - let _ = ffi::color::oakrender_color_manager_set_up_default_config(); - let proc = ffi::color::oakrender_color_processor_create( - c"ACEScg".as_ptr(), - c"sRGB Encoded Rec.709 (sRGB)".as_ptr(), - 0, - ); - if proc.is_null() { - eprintln!("no OCIO config; skipping blit transform check"); - return; - } - - // CPU renderer: init fails (no GPU needed) but textures are CPU. - let renderer = ffi::renderer::oakrender_display_renderer_create_dynamic(c"cpu".as_ptr()); - assert!(!renderer.is_null()); - let _ = ffi::renderer::oakrender_display_renderer_init(renderer, std::ptr::null_mut()); - - let mut vp = std::mem::zeroed::(); - vp.width = 4; - vp.height = 4; - vp.format = 4; - let src = - ffi::renderer::oakrender_display_texture_create(renderer, &vp, std::ptr::null(), 0); - let dst = - ffi::renderer::oakrender_display_texture_create(renderer, &vp, std::ptr::null(), 0); - assert!(!src.is_null() && !dst.is_null()); - - // Fill source with 0.18 grey. - let linesize: i32 = 4 * 4 * 4; - let mut pixels = vec![0u8; linesize as usize * 4]; - let f32s: &mut [f32] = - unsafe { std::slice::from_raw_parts_mut(pixels.as_mut_ptr() as *mut f32, 4 * 4 * 4) }; - for px in f32s.chunks_exact_mut(4) { - px[0] = 0.18; - px[1] = 0.18; - px[2] = 0.18; - px[3] = 1.0; - } - assert_eq!( - ffi::renderer::oakrender_display_texture_upload( - src, - pixels.as_ptr() as *const std::ffi::c_void, - linesize - ), - OAKRENDER_OK - ); - - let job = ffi::OakColorTransformJob { - processor: proc.ctx as *const std::ffi::c_void, - input_texture: src.ctx, - input_alpha_association: 0, - clear_destination: 1, - force_opaque: 0, - matrix: [0.0; 16], - crop_matrix: [0.0; 16], - }; - assert_eq!( - ffi::renderer::oakrender_display_renderer_blit_color_managed( - renderer, - &job, - dst, - std::ptr::null() - ), - OAKRENDER_OK - ); - // The blit applied the OCIO transform in float. - let mut back = vec![0u8; linesize as usize * 4]; - assert_eq!( - ffi::renderer::oakrender_display_texture_download( - dst, - back.as_mut_ptr() as *mut std::ffi::c_void, - linesize - ), - OAKRENDER_OK - ); - let out_f32: &[f32] = - unsafe { std::slice::from_raw_parts(back.as_ptr() as *const f32, 4 * 4 * 4) }; - assert!( - (out_f32[0] - 0.18).abs() > 1e-4, - "blit applies the color transform (got {})", - out_f32[0] - ); - assert!((out_f32[3] - 1.0).abs() < 1e-5); - - // Error paths. - assert_eq!( - ffi::renderer::oakrender_display_renderer_blit_color_managed( - renderer, - std::ptr::null(), - dst, - std::ptr::null() - ), - -70001 - ); - - let mut src = src; - let mut dst = dst; - ffi::renderer::oakrender_display_texture_free(&mut src); - ffi::renderer::oakrender_display_texture_free(&mut dst); - let mut proc = proc; - ffi::color::oakrender_color_processor_free(&mut proc); - let mut renderer = renderer; - ffi::renderer::oakrender_display_renderer_destroy(&mut renderer); - } -} diff --git a/crates/oakrender/tests/ticket_worker_test.rs b/crates/oakrender/tests/ticket_worker_test.rs index dd886d232..b61a769cb 100644 --- a/crates/oakrender/tests/ticket_worker_test.rs +++ b/crates/oakrender/tests/ticket_worker_test.rs @@ -292,193 +292,6 @@ fn snapshot_store_refcount() { assert_eq!(store.refs(&p1), 0); } -/// The FFI ticket path end to end (requires the manager). -#[test] -fn ffi_ticket_render_frame_roundtrip() { - use oakrender::error::{OAKRENDER_E_INVALID, OAKRENDER_E_STATE}; - use oakrender::ffi; - let _g = common::ManagerGuard::init(); - unsafe { - // NULL params → empty handle. - let h = ffi::ticket::oakrender_ticket_render_frame( - std::ptr::null(), - None, - std::ptr::null_mut(), - ); - assert!(h.is_null()); - // Empty output node → empty handle. - let mut params = std::mem::zeroed::(); - let h = ffi::ticket::oakrender_ticket_render_frame(¶ms, None, std::ptr::null_mut()); - assert!(h.is_null()); - - // Ticket params: fake node + fake video params handle. - params.output_node = common::fake_handle(9); - params.video_params = common::fake_handle(10); - params.time_num = 3; - params.time_den = 1; - params.force_width = 32; - params.force_height = 16; - params.force_format = 4; // F32 - - // Occupy every worker with a gated job so the ticket submitted - // below is queued behind them and guaranteed to still be running - // when it is queried. An idle pool can finish a small frame before - // the caller's next statement, so "not finished yet" must not - // depend on timing. - let manager = oakrender::manager::RenderManager::global().unwrap(); - let pool = manager.pool.clone(); - let occupied = pool.worker_count(); - let gate = Arc::new(AtomicBool::new(false)); - let _release = GateRelease(gate.clone()); - let (done_tx, done_rx) = mpsc::channel(); - for _ in 0..occupied { - let gate = gate.clone(); - let done_tx = done_tx.clone(); - let producer: oakrender::ticket::Producer = Arc::new(move |_, _| { - while !gate.load(Ordering::Acquire) { - std::thread::sleep(Duration::from_millis(1)); - } - Ok(oakrender::ticket::TicketPayload::Video(Texture::wrap_frame(small_frame()))) - }); - assert!(pool.post(oakrender::worker::Job { - node_identity: 99, - time: Rational::new(0, 1), - params: Arc::new(VideoTicketParams { - viewer: 1, - time: Rational::new(0, 1), - force_size: Some((8, 4)), - force_format: None, - cache: None, - cache_dir: None, - cache_id: None, - cache_timebase: None, - footage: None, - montage: Vec::new(), - }), - produce: producer, - done: Box::new(move |_| { - let _ = done_tx.send(()); - }), - })); - } - drop(done_tx); - - let h = ffi::ticket::oakrender_ticket_render_frame(¶ms, None, std::ptr::null_mut()); - assert!(!h.is_null(), "ticket handle created"); - assert_eq!(h.abi_version, 1); - - // Queries before finish: the ticket sits queued behind the occupied - // workers, so it is deterministically still running. - assert_eq!(ffi::ticket::oakrender_ticket_is_finished(h), 0); - assert_eq!(ffi::ticket::oakrender_ticket_get_type(h), 0); // video - let (mut n, mut d) = (0i64, 0i64); - assert_eq!(ffi::ticket::oakrender_ticket_get_time(h, &mut n, &mut d), 0); - assert_eq!((n, d), (3, 1)); - - // Release the workers and let the ticket finish. Draining the - // completions guarantees no gated job is still in flight when the - // manager shuts down (the guard drops at the end of the test). - gate.store(true, Ordering::Release); - while done_rx.recv_timeout(Duration::from_secs(5)).is_ok() {} - - // Wait + finished + get_frame. - assert_eq!(ffi::ticket::oakrender_ticket_wait(h), 0); - assert_eq!(ffi::ticket::oakrender_ticket_is_finished(h), 1); - let mut frame = ffi::OakCodecFrame::null(); - assert_eq!(ffi::ticket::oakrender_ticket_get_frame(h, &mut frame), 0); - assert!(!frame.is_null()); - assert_eq!(ffi::renderer::oakrender_codec_frame_width(frame), 32); - assert_eq!(ffi::renderer::oakrender_codec_frame_height(frame), 16); - assert_eq!(ffi::renderer::oakrender_codec_frame_is_allocated(frame), 1); - let mut pod = std::mem::zeroed::(); - assert_eq!( - ffi::renderer::oakrender_codec_frame_get_params(frame, &mut pod), - 0 - ); - assert_eq!(pod.format, 4, "F32 pipeline format"); - let mut frame = frame; - ffi::renderer::oakrender_codec_frame_free(&mut frame); - - // get_samples: audio not implemented → failed. - let mut samples = std::ptr::null_mut(); - assert!(ffi::ticket::oakrender_ticket_get_samples(h, &mut samples) < 0); - assert!(samples.is_null()); - - let mut h = h; - ffi::ticket::oakrender_ticket_free(&mut h); - assert!(h.is_null()); - - // Empty ticket queries → E_INVALID. - assert_eq!( - ffi::ticket::oakrender_ticket_is_finished(ffi::OakRenderTicket::null()), - OAKRENDER_E_INVALID - ); - assert_eq!( - ffi::ticket::oakrender_ticket_get_type(ffi::OakRenderTicket::null()), - OAKRENDER_E_INVALID - ); - assert_eq!( - ffi::ticket::oakrender_ticket_wait(ffi::OakRenderTicket::null()), - OAKRENDER_E_INVALID - ); - assert_eq!( - ffi::ticket::oakrender_ticket_cancel(ffi::OakRenderTicket::null()), - OAKRENDER_E_INVALID - ); - } -} - -/// The FFI audio ticket path. -#[test] -fn ffi_ticket_render_audio() { - use oakrender::error::OAKRENDER_OK; - use oakrender::ffi; - let _g = common::ManagerGuard::init(); - unsafe { - // Null output node → empty handle. - let h = ffi::ticket::oakrender_ticket_render_audio( - ffi::OakNodeNode::null(), - 0, - 1, - 10, - 1, - std::ptr::null(), - 0, - None, - std::ptr::null_mut(), - std::ptr::null(), - 0, - ); - assert!(h.is_null()); - - let audio_params = common::fake_handle(1); - let h = ffi::ticket::oakrender_ticket_render_audio( - common::fake_handle(9), - 0, - 1, - 10, - 1, - &audio_params, - 0, - None, - std::ptr::null_mut(), - std::ptr::null(), - 0, - ); - assert!(!h.is_null()); - assert_eq!(ffi::ticket::oakrender_ticket_get_type(h), 1); // audio - let (mut i_n, mut i_d, mut o_n, mut o_d) = (0i64, 0i64, 0i64, 0i64); - assert_eq!( - ffi::ticket::oakrender_ticket_get_range(h, &mut i_n, &mut i_d, &mut o_n, &mut o_d), - OAKRENDER_OK - ); - assert_eq!((i_n, i_d, o_n, o_d), (0, 1, 10, 1)); - assert_eq!(ffi::ticket::oakrender_ticket_wait(h), OAKRENDER_OK); - let mut h = h; - ffi::ticket::oakrender_ticket_free(&mut h); - } -} - /// TimeRange sanity (used above). #[test] fn range_sanity() { diff --git a/crates/oakstorage/Cargo.toml b/crates/oakstorage/Cargo.toml index cfbf67da4..7b6ae1b7a 100644 --- a/crates/oakstorage/Cargo.toml +++ b/crates/oakstorage/Cargo.toml @@ -28,8 +28,6 @@ sea-orm = { version = "2", features = ["sqlx-postgres", "sqlx-sqlite", "runtime- tokio = { version = "1", features = ["rt", "macros"] } [dev-dependencies] -# oakcodec's oakcore_*/oakrender_* host-mocks (test-stubs feature): the -# oakstorage test binary links the real oakcodec (via oaknode), whose -# ffmpeg unit references those cross-crate symbols that no Rust crate -# provides (same setup as oaknode's own dev-dependencies). -oakcodec = { path = "../oakcodec", features = ["test-stubs"] } +# oakcodec is also reachable through oaknode; the dev-dependency re-entry +# used to enable oakcodec's `test-stubs` host-mocks, which no longer exist +# (single-lib unification removed the oakcore_audioparams_* C ABI calls). diff --git a/crates/oakstorage/src/backends/otio.rs b/crates/oakstorage/src/backends/otio.rs index 224592efa..c10184d09 100644 --- a/crates/oakstorage/src/backends/otio.rs +++ b/crates/oakstorage/src/backends/otio.rs @@ -66,7 +66,7 @@ use oaknode::sequence::SequenceBehavior; use oaknode::track::{TrackBehavior, TrackListBehavior, TrackType}; use crate::backend::LoadResult; -use crate::bridge::node; +use crate::nodeutil as node; use crate::error::{Error, Result}; use crate::uri::StorageUri; diff --git a/crates/oakstorage/src/backends/ove_xml.rs b/crates/oakstorage/src/backends/ove_xml.rs index 5b036df50..06036493d 100644 --- a/crates/oakstorage/src/backends/ove_xml.rs +++ b/crates/oakstorage/src/backends/ove_xml.rs @@ -121,8 +121,8 @@ impl crate::backend::StorageBackend for OveXmlBackend { Probe::Current | Probe::Old => {} } - let project = crate::bridge::node::serializer_load(&xml)?; - let handle = crate::bridge::node::make_project_owned(project); + let project = crate::nodeutil::serializer_load(&xml)?; + let handle = crate::nodeutil::make_project_owned(project); let version_info = if matches!(probe, Probe::Old) { OAKSTORAGE_TOO_OLD } else { @@ -141,12 +141,12 @@ impl crate::backend::StorageBackend for OveXmlBackend { .local_path() .ok_or(Error::Invalid)? .to_string(); - let arc = unsafe { crate::bridge::node::project_arc(&project)? }; + let arc = unsafe { crate::nodeutil::project_arc(&project)? }; let xml = { let guard = arc .lock() .map_err(|_| Error::State)?; - crate::bridge::node::serializer_save(&guard)? + crate::nodeutil::serializer_save(&guard)? }; std::fs::write(&path, xml).map_err(|e| Error::Io(e.to_string()))?; Ok(()) diff --git a/crates/oakstorage/src/bridge/mod.rs b/crates/oakstorage/src/bridge/mod.rs deleted file mode 100644 index d6d27aea1..000000000 --- a/crates/oakstorage/src/bridge/mod.rs +++ /dev/null @@ -1,19 +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 . - -//! C ABI imports from other oak modules. - -pub mod node; diff --git a/crates/oakstorage/src/ffi.rs b/crates/oakstorage/src/ffi.rs deleted file mode 100644 index 27b1531f1..000000000 --- a/crates/oakstorage/src/ffi.rs +++ /dev/null @@ -1,430 +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 . - -//! C ABI export layer: implements M10 §2.2/§2.3 verbatim -//! (`oakstorage_probe/open/save/project_free/project_take_project/ -//! project_project/project_uri/last_error/debug_alive_count` and the -//! backend vtable registration pair). -//! -//! `oakstorage_open` returns a session handle (alive-counted). The -//! project inside is an `OakNodeProject*`-compatible handle boxed via -//! the shared `oakcore_rs::handle::CHandle`, so `take_project` hands -//! back something `oaknode_*` functions accept, and `save` accepts -//! handles produced by `oaknode_project_init`. - -use std::cell::RefCell; -use std::ffi::{c_char, c_int, c_uint, CStr, CString}; -use std::panic::{catch_unwind, AssertUnwindSafe}; -use std::sync::atomic::{AtomicI32, Ordering}; -use std::sync::Arc; - -use crate::backend::StorageBackend; -use crate::bridge::node; -use crate::error::{Error, OAKSTORAGE_E_FAILED, OAKSTORAGE_OK, Result}; -use crate::handle::CHandle; -use crate::session::Session; -use crate::uri::StorageUri; - -/// Save options bitmask: compress the ove-xml container. Accepted but -/// not implemented by the built-in backends (the oaknode serializer -/// emits plain XML only); unknown bits are ignored (M10 §2.2). -pub const OAKSTORAGE_SAVE_COMPRESS: c_uint = 0x1; - -/// Live session count (`oakstorage_debug_alive_count`). -static ALIVE: AtomicI32 = AtomicI32::new(0); - -// Per-thread last-error detail (`oakstorage_last_error`). -thread_local! { - static LAST_ERROR: RefCell = const { RefCell::new(String::new()) }; -} - -/// Record the thread's last error detail. -fn set_last_error(msg: &str) { - LAST_ERROR.with(|e| *e.borrow_mut() = msg.to_string()); -} - -/// Read the thread's last error detail. -fn take_last_error() -> String { - LAST_ERROR.with(|e| e.borrow().clone()) -} - -/// Release for session handles (alive-counted; the session's Drop -/// releases the held project handle). -unsafe extern "C" fn session_release(ctx: *mut std::ffi::c_void) { - unsafe { - let rb = ctx as *mut crate::handle::RefBox; - if (*rb).refs.fetch_sub(1, Ordering::AcqRel) == 1 { - drop(Box::from_raw(rb)); - ALIVE.fetch_sub(1, Ordering::Relaxed); - } - } -} - -/// `oakstorage_probe`: which backend claims this URI (two-stage -/// string; negative = E_NO_BACKEND). -#[no_mangle] -pub unsafe extern "C" fn oakstorage_probe( - uri: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - let uri_str = unsafe { cstr(uri) }.map(str::to_string); - match catch_unwind(AssertUnwindSafe(|| { - let s = uri_str.as_deref().ok_or(Error::Invalid)?; - let parsed = StorageUri::parse(s)?; - let backend = crate::registry::Registry::global().resolve(&parsed)?; - Ok::(backend.name().to_string()) - })) { - Ok(Ok(name)) => unsafe { node::copy_string_out(&name, buf, buf_size) }, - Ok(Err(e)) => { - set_last_error(&e.to_string()); - e.code() - } - Err(_) => { - set_last_error("storage: panic in oakstorage_probe"); - OAKSTORAGE_E_FAILED - } - } -} - -/// `oakstorage_open`: open a project session by URI (owned handle; -/// `result_code` receives an OAKSTORAGE_* code incl. the positive -/// version info codes). -#[no_mangle] -pub unsafe extern "C" fn oakstorage_open(uri: *const c_char, result_code: *mut c_int) -> CHandle { - let uri_str = unsafe { cstr(uri) }.map(str::to_string); - match catch_unwind(AssertUnwindSafe(|| open_inner(uri_str.as_deref()))) { - Ok(Ok((handle, info))) => { - if !result_code.is_null() { - unsafe { *result_code = info }; - } - handle - } - Ok(Err(e)) => { - set_last_error(&e.to_string()); - if !result_code.is_null() { - unsafe { *result_code = e.code() }; - } - CHandle::null() - } - Err(_) => { - set_last_error("storage: panic in oakstorage_open"); - if !result_code.is_null() { - unsafe { *result_code = OAKSTORAGE_E_FAILED }; - } - CHandle::null() - } - } -} - -/// Open a session: parse + resolve + backend load, then wrap the -/// project in an alive-counted session handle. The returned info code -/// is the backend's version verdict (OK or a positive info code). -fn open_inner(uri: Option<&str>) -> Result<(CHandle, i32)> { - let s = uri.ok_or(Error::Invalid)?; - let parsed = StorageUri::parse(s)?; - let backend = crate::registry::Registry::global().resolve(&parsed)?; - let loaded = backend.load(&parsed)?; - let info = loaded.version_info; - if loaded.project.is_null() { - // Version probing declined to load (TOO_NEW / UNKNOWN_VERSION). - return Ok((CHandle::null(), info)); - } - ALIVE.fetch_add(1, Ordering::Relaxed); - let session = Session::new(parsed, loaded.project); - Ok((crate::handle::make_owned_with(session, session_release), info)) -} - -/// `oakstorage_save`: save a project to a URI (options bitmask; -/// unknown bits ignored by backends). -#[no_mangle] -pub unsafe extern "C" fn oakstorage_save( - project: CHandle, - uri: *const c_char, - options: c_uint, -) -> c_int { - let uri_str = unsafe { cstr(uri) }.map(str::to_string); - match catch_unwind(AssertUnwindSafe(|| { - let s = uri_str.as_deref().ok_or(Error::Invalid)?; - let parsed = StorageUri::parse(s)?; - let backend = crate::registry::Registry::global().resolve(&parsed)?; - backend.save(project, &parsed, options) - })) { - Ok(Ok(())) => OAKSTORAGE_OK, - Ok(Err(e)) => { - set_last_error(&e.to_string()); - e.code() - } - Err(_) => { - set_last_error("storage: panic in oakstorage_save"); - OAKSTORAGE_E_FAILED - } - } -} - -/// `oakstorage_project_free` (NULL/empty no-op). -#[no_mangle] -pub unsafe extern "C" fn oakstorage_project_free(session: *mut CHandle) { - crate::handle::guard_void(|| unsafe { - if session.is_null() || (*session).ctx.is_null() { - return; - } - let h = (*session).clone(); - if let Some(f) = h.release { - f(h.ctx); - } - (*session).ctx = std::ptr::null_mut(); - }); -} - -/// `oakstorage_project_take_project`: ownership transfer out of the -/// session (the session becomes an empty shell; still free it). -#[no_mangle] -pub unsafe extern "C" fn oakstorage_project_take_project(session: CHandle) -> CHandle { - match catch_unwind(AssertUnwindSafe(|| unsafe { session_take(session) })) { - Ok(Ok(h)) => h, - Ok(Err(_)) | Err(_) => CHandle::null(), - } -} - -/// Mutate the boxed session to take its project out (empty after an -/// earlier take — then an empty handle, not an error). -unsafe fn session_take(session: CHandle) -> Result { - let rb = session.ctx as *mut crate::handle::RefBox; - if rb.is_null() { - return Err(Error::Invalid); - } - Ok(unsafe { (*rb).value.take() }.unwrap_or_else(CHandle::null)) -} - -/// `oakstorage_project_project`: borrowed project handle (empty after -/// take). A fresh box over a clone of the project Arc, so releasing it -/// (or not) cannot disturb the session's own handle. -#[no_mangle] -pub unsafe extern "C" fn oakstorage_project_project(session: CHandle) -> CHandle { - match catch_unwind(AssertUnwindSafe(|| -> crate::error::Result { - unsafe { - let sess = crate::handle::get::(&session).ok_or(Error::Invalid)?; - let ph = sess.project().ok_or(Error::State)?; - let arc = oaknode::handle::get::(ph) - .ok_or(Error::Invalid)? - .clone(); - Ok(oaknode::handle::make_owned(arc)) - } - })) { - Ok(Ok(h)) => h, - Ok(Err(_)) | Err(_) => CHandle::null(), - } -} - -/// `oakstorage_project_uri` (two-stage string). -#[no_mangle] -pub unsafe extern "C" fn oakstorage_project_uri( - session: CHandle, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - let uri_str = match unsafe { crate::handle::get::(&session) } { - Some(s) => s.uri().to_uri_string(), - None => return crate::error::OAKSTORAGE_E_INVALID, - }; - unsafe { node::copy_string_out(&uri_str, buf, buf_size) } -} - -/// `oakstorage_last_error` (two-stage string; per-thread last error -/// detail). -#[no_mangle] -pub unsafe extern "C" fn oakstorage_last_error(buf: *mut c_char, buf_size: c_int) -> c_int { - let msg = take_last_error(); - unsafe { node::copy_string_out(&msg, buf, buf_size) } -} - -/// `oakstorage_debug_alive_count` (leak assertions in tests). -#[no_mangle] -pub unsafe extern "C" fn oakstorage_debug_alive_count() -> c_int { - ALIVE.load(Ordering::Relaxed) -} - -// --------------------------------------------------------------------- -// Foreign (C-side) backend vtable registration — M10 §2.3 -// --------------------------------------------------------------------- - -/// C-side backend vtable (M10 §2.3; the layout is the C header's -/// struct — implementer copies it field-for-field). -#[repr(C)] -pub struct OakStorageBackendVtable { - /// Backend name (static string). - pub name: *const c_char, - /// URI scheme served (static string). - pub uri_scheme: *const c_char, - /// can_handle. - pub can_handle: Option c_int>, - /// load. - pub load: Option< - unsafe extern "C" fn( - uri: *const c_char, - result_code: *mut c_int, - err_buf: *mut c_char, - err_buf_size: c_int, - ) -> CHandle, - >, - /// save. - pub save: Option< - unsafe extern "C" fn( - project: CHandle, - uri: *const c_char, - options: c_uint, - err_buf: *mut c_char, - err_buf_size: c_int, - ) -> c_int, - >, -} - -/// A backend registered from C: every call funnels through the vtable -/// function pointers (the vtable itself is not copied — the caller -/// guarantees it outlives unregistration). -struct ForeignBackend { - name: String, - uri_scheme: String, - can_handle: unsafe extern "C" fn(uri: *const c_char) -> c_int, - load: unsafe extern "C" fn( - uri: *const c_char, - result_code: *mut c_int, - err_buf: *mut c_char, - err_buf_size: c_int, - ) -> CHandle, - save: unsafe extern "C" fn( - project: CHandle, - uri: *const c_char, - options: c_uint, - err_buf: *mut c_char, - err_buf_size: c_int, - ) -> c_int, -} - -impl StorageBackend for ForeignBackend { - fn name(&self) -> &str { - &self.name - } - - fn uri_scheme(&self) -> &str { - &self.uri_scheme - } - - fn can_handle(&self, uri: &StorageUri) -> bool { - let c = match CString::new(uri.to_uri_string()) { - Ok(c) => c, - Err(_) => return false, - }; - let claimed = unsafe { (self.can_handle)(c.as_ptr()) }; - claimed != 0 - } - - fn load(&self, uri: &StorageUri) -> Result { - let c = CString::new(uri.to_uri_string()).map_err(|_| Error::Invalid)?; - let mut result_code: c_int = OAKSTORAGE_OK; - let handle = unsafe { - (self.load)(c.as_ptr(), &mut result_code, std::ptr::null_mut(), 0) - }; - if !handle.is_null() { - Ok(crate::backend::LoadResult::with_info(handle, result_code)) - } else if result_code > 0 { - // Version info code without a project. - Ok(crate::backend::LoadResult::info_only(result_code)) - } else { - Err(code_to_error(result_code)) - } - } - - fn save( - &self, - project: CHandle, - uri: &StorageUri, - options: u32, - ) -> Result<()> { - let c = CString::new(uri.to_uri_string()).map_err(|_| Error::Invalid)?; - let rc = unsafe { (self.save)(project, c.as_ptr(), options, std::ptr::null_mut(), 0) }; - if rc == OAKSTORAGE_OK { - Ok(()) - } else { - Err(code_to_error(rc)) - } - } -} - -/// Map a foreign backend's return code to the crate error. -fn code_to_error(code: c_int) -> Error { - match code { - crate::error::OAKSTORAGE_E_INVALID => Error::Invalid, - crate::error::OAKSTORAGE_E_STATE => Error::State, - crate::error::OAKSTORAGE_E_NOT_FOUND => Error::NotFound, - crate::error::OAKSTORAGE_E_FAILED => Error::Failed("foreign backend failed".to_string()), - crate::error::OAKSTORAGE_E_NO_BACKEND => Error::NoBackend, - crate::error::OAKSTORAGE_E_FORMAT => Error::Format("foreign backend format error".to_string()), - crate::error::OAKSTORAGE_E_IO => Error::Io("foreign backend I/O error".to_string()), - crate::error::OAKSTORAGE_E_NOMEM => Error::NoMem, - _ => Error::Failed(format!("foreign backend error code {code}")), - } -} - -/// `oakstorage_backend_register`: register a foreign backend (the -/// vtable is not copied — the caller guarantees it outlives -/// unregistration). -#[no_mangle] -pub unsafe extern "C" fn oakstorage_backend_register( - backend: *const OakStorageBackendVtable, -) -> c_int { - crate::handle::guard(|| unsafe { - if backend.is_null() { - return Err(Error::Invalid); - } - let v = &*backend; - let name = cstr(v.name).ok_or(Error::Invalid)?.to_string(); - let uri_scheme = cstr(v.uri_scheme).ok_or(Error::Invalid)?.to_string(); - let can_handle = v.can_handle.ok_or(Error::Invalid)?; - let load = v.load.ok_or(Error::Invalid)?; - let save = v.save.ok_or(Error::Invalid)?; - let foreign = ForeignBackend { - name, - uri_scheme, - can_handle, - load, - save, - }; - crate::registry::Registry::global().register(Arc::new(foreign)) - }) -} - -/// `oakstorage_backend_unregister`. -#[no_mangle] -pub unsafe extern "C" fn oakstorage_backend_unregister(name: *const c_char) -> c_int { - crate::handle::guard(|| unsafe { - let name = cstr(name).ok_or(Error::Invalid)?; - crate::registry::Registry::global().unregister(name) - }) -} - -/// Safe read of a NUL-terminated C string argument. -/// -/// # Safety -/// `p` must be a valid NUL-terminated C string for the returned -/// reference's lifetime. -unsafe fn cstr<'a>(p: *const c_char) -> Option<&'a str> { - if p.is_null() { - return None; - } - unsafe { CStr::from_ptr(p) }.to_str().ok() -} diff --git a/crates/oakstorage/src/lib.rs b/crates/oakstorage/src/lib.rs index f0bb079bf..f4d147442 100644 --- a/crates/oakstorage/src/lib.rs +++ b/crates/oakstorage/src/lib.rs @@ -25,10 +25,9 @@ pub mod backend; pub mod backends; -pub mod bridge; pub mod error; -pub mod ffi; pub mod handle; +pub mod nodeutil; pub mod registry; pub mod session; pub mod uri; diff --git a/crates/oakstorage/src/bridge/node.rs b/crates/oakstorage/src/nodeutil.rs similarity index 59% rename from crates/oakstorage/src/bridge/node.rs rename to crates/oakstorage/src/nodeutil.rs index 8901ce463..da7791425 100644 --- a/crates/oakstorage/src/bridge/node.rs +++ b/crates/oakstorage/src/nodeutil.rs @@ -14,23 +14,14 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//! oaknode calls — direct Rust calls into the oaknode crate (single-lib -//! unification, see `docs/zh/plans/riir/single-lib.md`). +//! oaknode project/serializer helpers (direct Rust calls into the +//! oaknode crate, single-lib unification; the former `bridge/node.rs`). //! -//! The ove-xml backend needs oaknode's project + serializer families -//! (`oaknode_project_init` / `oaknode_serializer_save_to_file` / -//! `load_from_file` in the C++ world). With direct calls, graph -//! (de)serialization stays oaknode's own serializer (`load`/`save`), -//! and this bridge only boxes the resulting `Arc>` into -//! the canonical `CHandle` form the public C API moves around. -//! -//! A project handle boxed here is layout-identical to one produced by -//! `oaknode_project_init`: both box `Arc>` behind the -//! shared `oakcore_rs::handle::CHandle`, so `oakstorage_save` can -//! consume an oaknode-created handle and `oakstorage_project_take_*` -//! returns one the caller can hand back to oaknode functions. +//! The ove-xml/otio backends need oaknode's project + serializer +//! families. Graph (de)serialization stays oaknode's own serializer +//! (`load`/`save`); these helpers box the resulting `Arc>` +//! into the canonical handle form the storage session API moves around. -use std::ffi::{c_char, c_int}; use std::sync::{Arc, Mutex}; use crate::error::Result; @@ -39,24 +30,6 @@ use crate::handle::CHandle; /// The project payload boxed by project handles. pub type ProjectArc = Arc>; -/// New empty project, boxed as an owned handle (refcount 1). The -/// caller owns it (free with [`project_free`] / `oaknode_project_free`). -pub fn project_init() -> CHandle { - oaknode::handle::make_owned(oaknode::project::Project::new()) -} - -/// Free a project handle (`NULL`/empty no-op). -pub fn project_free(project: *mut CHandle) { - if project.is_null() || unsafe { (*project).ctx.is_null() } { - return; - } - let h = unsafe { (*project).clone() }; - if let Some(f) = h.release { - unsafe { f(h.ctx) }; - } - unsafe { (*project).ctx = std::ptr::null_mut() }; -} - /// Box an existing project as an owned handle (refcount 1). pub fn make_project_owned(project: ProjectArc) -> CHandle { oaknode::handle::make_owned(project) @@ -65,8 +38,8 @@ pub fn make_project_owned(project: ProjectArc) -> CHandle { /// Read the boxed project of a project handle. /// /// # Safety -/// The handle must box `Arc>` (created by this bridge, -/// `oaknode_project_init`, or `oakstorage_project_take_project`). +/// The handle must box `Arc>` (created by this module or +/// oaknode's project helpers). pub unsafe fn project_arc(h: &CHandle) -> Result { unsafe { oaknode::handle::get::(h) } .cloned() @@ -87,9 +60,9 @@ pub fn serializer_save(project: &oaknode::project::Project) -> Result { /// subtitle) in `graph`. Returns `(sequence_id, [video, audio, /// subtitle] list ids)`. /// -/// Mirrors the private `append_default_track_lists` wiring of -/// `oaknode_sequence_create` (ffi.rs) so the otio backend can import -/// timelines into real projects; shared with tests through this bridge. +/// Mirrors the private `append_default_track_lists` wiring of the former +/// `oaknode_sequence_create` export so the otio backend can import +/// timelines into real projects. pub fn create_sequence( graph: &mut oaknode::graph::Graph, ) -> (oaknode::id::NodeId, Vec) { @@ -148,23 +121,3 @@ pub fn create_sequence( } (seq_id, lists) } - -/// Two-stage C string copy helper: returns the required size including -/// the NUL; writes up to `buf_size - 1` bytes plus NUL when -/// `buf_size > 0` (`// CPP-PARITY: src/node/c_api/valueconvert.h` -/// `copy_string`). -/// -/// # Safety -/// `buf`/`buf_size` must describe a valid writable buffer (or `buf` may -/// be null). -pub unsafe fn copy_string_out(value: &str, buf: *mut c_char, buf_size: c_int) -> c_int { - let required = value.len() + 1; - if !buf.is_null() && buf_size > 0 { - let copy_len = value.len().min(buf_size as usize - 1); - unsafe { - std::ptr::copy_nonoverlapping(value.as_ptr() as *const c_char, buf, copy_len); - *buf.add(copy_len) = 0; - } - } - required as c_int -} diff --git a/crates/oakstorage/src/session.rs b/crates/oakstorage/src/session.rs index 6736d0133..73f3aa095 100644 --- a/crates/oakstorage/src/session.rs +++ b/crates/oakstorage/src/session.rs @@ -30,11 +30,13 @@ pub struct Session { } impl Session { - /// Wrap a freshly loaded project. + /// Wrap a freshly loaded project. A null handle (the version-info + /// path: TOO_OLD/TOO_NEW/UNKNOWN_VERSION carries no project) maps to + /// `None`, not `Some(null)`. pub fn new(uri: StorageUri, project: CHandle) -> Self { Session { uri, - project: Some(project), + project: (!project.is_null()).then_some(project), } } diff --git a/crates/oakstorage/tests/storage_test.rs b/crates/oakstorage/tests/storage_test.rs index 5c87c98e8..636b2b9c7 100644 --- a/crates/oakstorage/tests/storage_test.rs +++ b/crates/oakstorage/tests/storage_test.rs @@ -14,16 +14,11 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//! oakstorage contract tests (M10 §4 mapping). -//! -//! The oaknode serializer persists the node core surface (types, -//! values, keyframes, label/color, links, connections), settings, uuid, -//! AND the timeline structure (sequence track lists, track blocks, -//! block spans, clip footage references) — the .ove tests assert the -//! full surface; the otio/fcpxml interchange tests exercise the -//! oakotio model. +//! oakstorage contract tests (M10 §4 mapping), calling the public Rust +//! API: the backend registry, the `StorageBackend` trait, `StorageUri` +//! and the `Session` shell. The former `ffi.rs`/`bridge::node.rs` C ABI +//! is gone; save/load now go through `Registry::global().resolve(uri)`. -use std::ffi::{c_char, c_int, c_uint, CStr, CString}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; @@ -37,23 +32,22 @@ use oaknode::project::Project; use oaknode::sequence::SequenceBehavior; use oaknode::track::{TrackBehavior, TrackListBehavior, TrackType}; use oaknode::value::NodeValue; -use oakstorage::bridge::node; +use oakstorage::backend::{LoadResult, StorageBackend}; use oakstorage::error::{ OAKSTORAGE_E_FORMAT, OAKSTORAGE_E_INVALID, OAKSTORAGE_E_IO, OAKSTORAGE_E_NO_BACKEND, OAKSTORAGE_E_STATE, OAKSTORAGE_OK, OAKSTORAGE_TOO_NEW, OAKSTORAGE_TOO_OLD, OAKSTORAGE_UNKNOWN_VERSION, }; -use oakstorage::ffi; use oakstorage::handle::CHandle; +use oakstorage::nodeutil::{make_project_owned, project_arc}; +use oakstorage::registry::Registry; +use oakstorage::session::Session; +use oakstorage::uri::StorageUri; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- -fn cs(s: &str) -> CString { - CString::new(s).unwrap() -} - fn file_uri(path: &Path) -> String { format!("file://{}", path.display()) } @@ -70,58 +64,48 @@ fn temp_dir(tag: &str) -> PathBuf { dir } -fn alive() -> i32 { - unsafe { ffi::oakstorage_debug_alive_count() } -} - -fn last_error() -> String { - let needed = unsafe { ffi::oakstorage_last_error(std::ptr::null_mut(), 0) }; - if needed <= 0 { - return String::new(); +/// Release an owned handle (refcount 1). +fn release(h: CHandle) { + if let Some(release) = h.release { + unsafe { release(h.ctx) }; } - let mut buf = vec![0u8; needed as usize]; - unsafe { ffi::oakstorage_last_error(buf.as_mut_ptr() as *mut c_char, needed) }; - buf.pop(); // trailing NUL - String::from_utf8(buf).unwrap() } -/// Probe for the backend name; `Err(code)` when no backend claims it. -fn probe_name(uri: &str) -> Result { - let c = cs(uri); - let needed = unsafe { ffi::oakstorage_probe(c.as_ptr(), std::ptr::null_mut(), 0) }; - if needed < 0 { - return Err(needed); - } - let mut buf = vec![0u8; needed as usize]; - let rc = unsafe { ffi::oakstorage_probe(c.as_ptr(), buf.as_mut_ptr() as *mut c_char, needed) }; - assert!(rc >= 0, "two-stage probe misbehaved"); - buf.pop(); - Ok(String::from_utf8(buf).unwrap()) +/// The backend claiming `uri` (Err for none / invalid URI). +fn backend_for(uri: &str) -> Result, oakstorage::error::Error> { + let parsed = StorageUri::parse(uri)?; + Registry::global().resolve(&parsed) } -fn open(uri: &str) -> (CHandle, c_int) { - let c = cs(uri); - let mut rc = 0; - let session = unsafe { ffi::oakstorage_open(c.as_ptr(), &mut rc) }; - (session, rc) +/// Save `handle` to `uri` with `options`. +fn save_handle(handle: CHandle, uri: &str, options: u32) -> oakstorage::error::Result<()> { + let parsed = StorageUri::parse(uri)?; + let backend = Registry::global().resolve(&parsed)?; + backend.save(handle, &parsed, options) } -fn free_session(mut s: CHandle) { - unsafe { ffi::oakstorage_project_free(&mut s) }; +/// Save an `Arc>` to `uri`, releasing the wrapper handle. +fn save_project( + project: &Arc>, + uri: &str, + options: u32, +) -> oakstorage::error::Result<()> { + let handle = make_project_owned(project.clone()); + let result = save_handle(handle, uri, options); + release(handle); + result } -fn free_project(mut h: CHandle) { - node::project_free(&mut h); -} - -fn session_uri(s: &CHandle) -> String { - let needed = unsafe { ffi::oakstorage_project_uri(*s, std::ptr::null_mut(), 0) }; - assert!(needed >= 0, "project_uri failed: {needed}"); - let mut buf = vec![0u8; needed as usize]; - let rc = unsafe { ffi::oakstorage_project_uri(*s, buf.as_mut_ptr() as *mut c_char, needed) }; - assert!(rc >= 0); - buf.pop(); - String::from_utf8(buf).unwrap() +/// Open `uri`: resolve the backend, load, wrap the result in a [`Session`]. +/// `Err` is a hard failure (I/O, format, no backend, invalid URI); `Ok` +/// carries the session plus the version info code (TOO_OLD / TOO_NEW / +/// UNKNOWN_VERSION) the backend reported. +fn open(uri: &str) -> oakstorage::error::Result<(Session, i32)> { + let parsed = StorageUri::parse(uri)?; + let backend = Registry::global().resolve(&parsed)?; + let result = backend.load(&parsed)?; + let session = Session::new(parsed, result.project); + Ok((session, result.version_info)) } fn r_to_f(r: Rational) -> f64 { @@ -280,55 +264,45 @@ fn ove_xml_roundtrip_field_by_field() { let uri = file_uri(&path); let project = build_roundtrip_project(); - let handle = node::make_project_owned(project.clone()); - let rc = unsafe { ffi::oakstorage_save(handle, cs(&uri).as_ptr(), 0) }; - assert_eq!(rc, OAKSTORAGE_OK, "save: {}", last_error()); - free_project(handle); + save_project(&project, &uri, 0).unwrap(); // The file exists and is plain XML. let text = std::fs::read_to_string(&path).unwrap(); assert!(text.starts_with(""), "{text}"); - let (session, rc) = open(&uri); - assert!(!session.ctx.is_null(), "open failed rc={rc}: {}", last_error()); + let (session, rc) = open(&uri).unwrap(); + assert!(session.project().is_some(), "open failed rc={rc}"); assert_eq!(rc, OAKSTORAGE_OK); - assert_eq!(session_uri(&session), uri); + assert_eq!(session.uri().to_uri_string(), uri); - let proj_handle = unsafe { ffi::oakstorage_project_project(session) }; - assert!(!proj_handle.ctx.is_null()); - let loaded = unsafe { node::project_arc(&proj_handle) }.unwrap(); + let proj_handle = session.project().cloned().unwrap(); + let loaded = unsafe { project_arc(&proj_handle) }.unwrap(); { let o = project.lock().unwrap(); let l = loaded.lock().unwrap(); assert_roundtrip_fields(&o, &l); } - - free_project(proj_handle); - free_session(session); } #[test] fn ove_xml_compress_flag_still_round_trips() { - // OAKSTORAGE_SAVE_COMPRESS is accepted but not implemented (the + // OAKSTORAGE_SAVE_COMPRESS (bit 0) is accepted but not implemented (the // oaknode serializer emits plain XML); the file still round-trips. let dir = temp_dir("ove_compress"); let path = dir.join("compressed.ove"); let uri = file_uri(&path); let project = build_roundtrip_project(); - let handle = node::make_project_owned(project.clone()); - let rc = unsafe { ffi::oakstorage_save(handle, cs(&uri).as_ptr(), ffi::OAKSTORAGE_SAVE_COMPRESS) }; - assert_eq!(rc, OAKSTORAGE_OK, "save: {}", last_error()); - free_project(handle); + save_project(&project, &uri, 1).unwrap(); let text = std::fs::read_to_string(&path).unwrap(); assert!(text.starts_with(" E_INVALID. - let rc = unsafe { ffi::oakstorage_probe(std::ptr::null(), std::ptr::null_mut(), 0) }; - assert_eq!(rc, OAKSTORAGE_E_INVALID); - assert_eq!(probe_name("").unwrap_err(), OAKSTORAGE_E_INVALID); + // Empty URI -> E_INVALID. + assert_eq!(backend_for("").err().unwrap().code(), OAKSTORAGE_E_INVALID); } // --------------------------------------------------------------------------- @@ -482,39 +457,35 @@ fn probe_dispatch() { #[test] fn open_error_paths() { - // Nonexistent file: NULL + E_IO + a non-empty last_error. - let (session, rc) = open(&file_uri(&temp_dir("ove_missing").join("nope.ove"))); - assert!(session.ctx.is_null()); - assert_eq!(rc, OAKSTORAGE_E_IO); - assert!(!last_error().is_empty(), "last_error must be set"); + // Nonexistent file: Err(Io). + let missing = file_uri(&temp_dir("ove_missing").join("nope.ove")); + assert_eq!(open(&missing).err().unwrap().code(), OAKSTORAGE_E_IO); // Too-new version header -> TOO_NEW info code, no project. let dir = temp_dir("ove_versions"); let future = dir.join("future.ove"); std::fs::write(&future, "").unwrap(); - let (session, rc) = open(&file_uri(&future)); - assert!(session.ctx.is_null()); + let (session, rc) = open(&file_uri(&future)).unwrap(); + assert!(session.project().is_none()); assert_eq!(rc, OAKSTORAGE_TOO_NEW); // Corrupt XML -> E_FORMAT. let corrupt = dir.join("corrupt.ove"); std::fs::write(&corrupt, "").unwrap(); - let (session, rc) = open(&file_uri(&corrupt)); - assert!(session.ctx.is_null()); - assert_eq!(rc, OAKSTORAGE_E_FORMAT); + let err = open(&file_uri(&corrupt)).err().unwrap(); + assert_eq!(err.code(), OAKSTORAGE_E_FORMAT); // Unparseable garbage -> E_FORMAT (not a version info code). let garbage = dir.join("garbage.ove"); std::fs::write(&garbage, "not xml at all").unwrap(); - let (session, rc) = open(&file_uri(&garbage)); - assert!(session.ctx.is_null()); - assert_eq!(rc, OAKSTORAGE_E_FORMAT); + let err = open(&file_uri(&garbage)).err().unwrap(); + assert_eq!(err.code(), OAKSTORAGE_E_FORMAT); // Recognized olive root without a version -> UNKNOWN_VERSION. let unversioned = dir.join("unversioned.ove"); std::fs::write(&unversioned, "").unwrap(); - let (session, rc) = open(&file_uri(&unversioned)); - assert!(session.ctx.is_null()); + let (session, rc) = open(&file_uri(&unversioned)).unwrap(); + assert!(session.project().is_none()); assert_eq!(rc, OAKSTORAGE_UNKNOWN_VERSION); // A known older version loads, reporting TOO_OLD. @@ -524,115 +495,107 @@ fn open_error_paths() { "{old}", ) .unwrap(); - let (session, rc) = open(&file_uri(&old)); - assert!(!session.ctx.is_null(), "old version must load"); + let (session, rc) = open(&file_uri(&old)).unwrap(); + assert!(session.project().is_some(), "old version must load"); assert_eq!(rc, OAKSTORAGE_TOO_OLD); - free_session(session); - // NULL URI -> E_INVALID. - let (session, rc) = unsafe { - let c = std::ptr::null(); - let mut rc = 0; - (ffi::oakstorage_open(c, &mut rc), rc) - }; - assert!(session.ctx.is_null()); - assert_eq!(rc, OAKSTORAGE_E_INVALID); + // Empty URI -> E_INVALID. + assert_eq!(open("").err().unwrap().code(), OAKSTORAGE_E_INVALID); } // --------------------------------------------------------------------------- -// Backend vtable pluggability (the database-swap interface proof) +// Backend pluggability (the database-swap interface proof) // --------------------------------------------------------------------------- -/// Record of the mock backend's save calls. +/// Record of the mock backend's calls. static MOCK_SAVED: std::sync::Mutex> = std::sync::Mutex::new(Vec::new()); -unsafe extern "C" fn mock_can_handle(uri: *const c_char) -> c_int { - let s = unsafe { CStr::from_ptr(uri) }.to_str().unwrap_or(""); - (s.starts_with("mem://")) as c_int -} +/// A mock backend claiming `mem://` (the Rust trait shape of the former +/// C vtable). +struct MemBackend; -unsafe extern "C" fn mock_load( - uri: *const c_char, - result_code: *mut c_int, - _err_buf: *mut c_char, - _err_buf_size: c_int, -) -> CHandle { - let s = unsafe { CStr::from_ptr(uri) }.to_str().unwrap_or(""); - MOCK_SAVED.lock().unwrap().push(format!("load:{s}")); - if !result_code.is_null() { - unsafe { *result_code = OAKSTORAGE_OK }; +impl StorageBackend for MemBackend { + fn name(&self) -> &'static str { + "mem-test" } - // A real project handle is the "loaded project". - node::project_init() -} -unsafe extern "C" fn mock_save( - _project: CHandle, - uri: *const c_char, - options: c_uint, - _err_buf: *mut c_char, - _err_buf_size: c_int, -) -> c_int { - let s = unsafe { CStr::from_ptr(uri) }.to_str().unwrap_or("").to_string(); - MOCK_SAVED.lock().unwrap().push(format!("save:{s}:options={options}")); - OAKSTORAGE_OK + fn uri_scheme(&self) -> &'static str { + "mem" + } + + fn can_handle(&self, uri: &StorageUri) -> bool { + uri.scheme == "mem" + } + + fn load(&self, uri: &StorageUri) -> oakstorage::error::Result { + MOCK_SAVED + .lock() + .unwrap() + .push(format!("load:{}", uri.to_uri_string())); + // A real project handle is the "loaded project". + let project = Project::new(); + Ok(LoadResult::success(make_project_owned(project))) + } + + fn save( + &self, + _project: CHandle, + uri: &StorageUri, + options: u32, + ) -> oakstorage::error::Result<()> { + MOCK_SAVED + .lock() + .unwrap() + .push(format!("save:{}:options={options}", uri.to_uri_string())); + Ok(()) + } } #[test] -fn backend_vtable_pluggability() { - let name = cs("mem-test"); - let scheme = cs("mem"); - let vtable = ffi::OakStorageBackendVtable { - name: name.as_ptr(), - uri_scheme: scheme.as_ptr(), - can_handle: Some(mock_can_handle), - load: Some(mock_load), - save: Some(mock_save), - }; +fn backend_pluggability() { + // The process-global registry already holds the built-ins; the mock + // is registered under its own name and unregistered at the end. + Registry::global().register(Arc::new(MemBackend)).unwrap(); - assert_eq!( - unsafe { ffi::oakstorage_backend_register(&vtable) }, - OAKSTORAGE_OK - ); + // Probe routes through the registered backend. + assert_eq!(backend_for("mem://x").unwrap().name(), "mem-test"); - // Probe routes through the foreign vtable. - assert_eq!(probe_name("mem://x").unwrap(), "mem-test"); - - // Open routes through the vtable's load. - let (session, rc) = open("mem://in"); - assert!(!session.ctx.is_null(), "open failed rc={rc}"); + // Open routes through the backend's load. + let (session, rc) = open("mem://in").unwrap(); + assert!(session.project().is_some(), "open failed rc={rc}"); assert_eq!(rc, OAKSTORAGE_OK); - free_session(session); + assert!(session.uri().to_uri_string() == "mem://in"); - // Save routes through the vtable's save, options passed through. - let project = node::project_init(); - let rc = unsafe { ffi::oakstorage_save(project, cs("mem://out").as_ptr(), ffi::OAKSTORAGE_SAVE_COMPRESS) }; - assert_eq!(rc, OAKSTORAGE_OK, "save: {}", last_error()); - free_project(project); + // Save routes through the backend's save, options passed through. + let project = Project::new(); + let handle = make_project_owned(project); + save_handle(handle, "mem://out", 1).unwrap(); + release(handle); { let saved = MOCK_SAVED.lock().unwrap(); assert!(saved.iter().any(|s| s == "load:mem://in"), "{saved:?}"); assert!( - saved - .iter() - .any(|s| s == "save:mem://out:options=1"), + saved.iter().any(|s| s == "save:mem://out:options=1"), "{saved:?}" ); } // Duplicate registration is rejected. assert_eq!( - unsafe { ffi::oakstorage_backend_register(&vtable) }, + Registry::global().register(Arc::new(MemBackend)).err().unwrap().code(), OAKSTORAGE_E_STATE ); // Unregister: probe reports E_NO_BACKEND again. - assert_eq!(unsafe { ffi::oakstorage_backend_unregister(name.as_ptr()) }, OAKSTORAGE_OK); - assert_eq!(probe_name("mem://x").unwrap_err(), OAKSTORAGE_E_NO_BACKEND); + assert!(Registry::global().unregister("mem-test").is_ok()); + assert_eq!( + backend_for("mem://x").err().unwrap().code(), + OAKSTORAGE_E_NO_BACKEND + ); // Unregister of an unknown name -> E_NOT_FOUND. assert_eq!( - unsafe { ffi::oakstorage_backend_unregister(cs("nope").as_ptr()) }, + Registry::global().unregister("nope").err().unwrap().code(), oakstorage::error::OAKSTORAGE_E_NOT_FOUND ); } @@ -648,7 +611,7 @@ fn build_timeline_project() -> Arc> { let mut p = project.lock().unwrap(); p.initialize().unwrap(); - let (seq_id, lists) = node::create_sequence(&mut p.graph); + let (seq_id, lists) = oakstorage::nodeutil::create_sequence(&mut p.graph); p.graph.get_mut(seq_id).unwrap().core.label = "My Seq".to_string(); let mut tb = TrackBehavior::new(TrackType::Video); @@ -854,10 +817,7 @@ fn assert_interchange_roundtrip(ext: &str) { let uri = file_uri(&path); let project = build_timeline_project(); - let handle = node::make_project_owned(project.clone()); - let rc = unsafe { ffi::oakstorage_save(handle, cs(&uri).as_ptr(), 0) }; - assert_eq!(rc, OAKSTORAGE_OK, "save: {}", last_error()); - free_project(handle); + save_project(&project, &uri, 0).unwrap(); // The written file parses back through oakotio with clips/tracks. match ext { @@ -879,17 +839,15 @@ fn assert_interchange_roundtrip(ext: &str) { } // And imports back into an equivalent project. - let (session, rc) = open(&uri); - assert!(!session.ctx.is_null(), "open failed rc={rc}: {}", last_error()); + let (session, rc) = open(&uri).unwrap(); + assert!(session.project().is_some(), "open failed rc={rc}"); assert_eq!(rc, OAKSTORAGE_OK); - let proj_handle = unsafe { ffi::oakstorage_project_project(session) }; - let loaded = unsafe { node::project_arc(&proj_handle) }.unwrap(); + let proj_handle = session.project().cloned().unwrap(); + let loaded = unsafe { project_arc(&proj_handle) }.unwrap(); { let l = loaded.lock().unwrap(); assert_imported_timeline(&l); } - free_project(proj_handle); - free_session(session); } #[test] @@ -909,9 +867,8 @@ fn otio_bad_root_rejected() { let dir = temp_dir("otio_badroot"); let path = dir.join("bad.otio"); std::fs::write(&path, "{\"OTIO_SCHEMA\": \"NotATimeline.1\", \"x\": 1}").unwrap(); - let (session, rc) = open(&file_uri(&path)); - assert!(session.ctx.is_null()); - assert_eq!(rc, OAKSTORAGE_E_FORMAT); + let err = open(&file_uri(&path)).err().unwrap(); + assert_eq!(err.code(), OAKSTORAGE_E_FORMAT); } // --------------------------------------------------------------------------- @@ -920,115 +877,69 @@ fn otio_bad_root_rejected() { #[test] fn null_and_invalid_handles() { - // open with NULL / empty URI -> E_INVALID. - let mut rc = 0; - let session = unsafe { ffi::oakstorage_open(std::ptr::null(), &mut rc) }; - assert!(session.ctx.is_null()); - assert_eq!(rc, OAKSTORAGE_E_INVALID); + // open with an empty URI -> E_INVALID. + assert_eq!(open("").err().unwrap().code(), OAKSTORAGE_E_INVALID); - // save with a null project handle -> E_INVALID. - let rc = unsafe { ffi::oakstorage_save(CHandle::null(), cs("file:///tmp/x.ove").as_ptr(), 0) }; - assert_eq!(rc, OAKSTORAGE_E_INVALID); + // save with a null project handle -> E_INVALID (the project cannot be + // read out of an empty handle). + let uri = "file:///tmp/x.ove"; + let backend = backend_for(uri).unwrap(); + let err = backend + .save(CHandle::null(), &StorageUri::parse(uri).unwrap(), 0) + .err().unwrap(); + assert_eq!(err.code(), OAKSTORAGE_E_INVALID); - // save with a null URI -> E_INVALID. - let project = node::project_init(); - let rc = unsafe { ffi::oakstorage_save(project, std::ptr::null(), 0) }; - assert_eq!(rc, OAKSTORAGE_E_INVALID); - free_project(project); + // save to an unknown-scheme URI -> E_NO_BACKEND (never reaches a + // backend). + let project = Project::new(); + let handle = make_project_owned(project); + let err = save_handle(handle, "oakdb://x", 0).err().unwrap(); + assert_eq!(err.code(), OAKSTORAGE_E_NO_BACKEND); + release(handle); - // save to an unknown-scheme URI -> E_NO_BACKEND. - let project = node::project_init(); - let rc = unsafe { ffi::oakstorage_save(project, cs("oakdb://x").as_ptr(), 0) }; - assert_eq!(rc, OAKSTORAGE_E_NO_BACKEND); - free_project(project); - - // project_free(NULL) is a no-op. - unsafe { ffi::oakstorage_project_free(std::ptr::null_mut()) }; - - // take_project / project_project on a null handle -> null. - let h = unsafe { ffi::oakstorage_project_take_project(CHandle::null()) }; - assert!(h.ctx.is_null()); - let h = unsafe { ffi::oakstorage_project_project(CHandle::null()) }; - assert!(h.ctx.is_null()); - - // project_uri on a null handle -> E_INVALID. - let rc = unsafe { ffi::oakstorage_project_uri(CHandle::null(), std::ptr::null_mut(), 0) }; - assert_eq!(rc, OAKSTORAGE_E_INVALID); - - // Empty session shell: take twice yields null the second time. + // take transfers the project; the second take is empty. let dir = temp_dir("null_matrix"); let path = dir.join("x.ove"); let uri = file_uri(&path); let project = build_roundtrip_project(); - let handle = node::make_project_owned(project); - assert_eq!( - unsafe { ffi::oakstorage_save(handle, cs(&uri).as_ptr(), 0) }, - OAKSTORAGE_OK - ); - free_project(handle); + save_project(&project, &uri, 0).unwrap(); - let (session, _) = open(&uri); - let taken = unsafe { ffi::oakstorage_project_take_project(session) }; - assert!(!taken.ctx.is_null(), "take transfers the project"); - let again = unsafe { ffi::oakstorage_project_take_project(session) }; - assert!(again.ctx.is_null(), "second take is empty"); - let borrowed = unsafe { ffi::oakstorage_project_project(session) }; - assert!(borrowed.ctx.is_null(), "borrowed is empty after take"); - free_project(taken); - free_session(session); - - // backend_register(NULL) / backend_unregister(NULL) -> E_INVALID. - assert_eq!( - unsafe { ffi::oakstorage_backend_register(std::ptr::null()) }, - OAKSTORAGE_E_INVALID - ); - assert_eq!( - unsafe { ffi::oakstorage_backend_unregister(std::ptr::null()) }, - OAKSTORAGE_E_INVALID - ); + let (mut session, _) = open(&uri).unwrap(); + let taken = session.take().expect("take transfers the project"); + assert!(!taken.ctx.is_null()); + assert!(session.project().is_none(), "empty shell after take"); + release(taken); } // --------------------------------------------------------------------------- -// Alive accounting +// Session shell lifecycle // --------------------------------------------------------------------------- +/// open -> Session owns the project handle; take hands it out and the +/// shell stays usable (drop-safe). #[test] -fn alive_count_accounting() { - let dir = temp_dir("alive"); +fn session_take_transfers_project() { + let dir = temp_dir("session"); let path = dir.join("x.ove"); let uri = file_uri(&path); let project = build_roundtrip_project(); - let handle = node::make_project_owned(project); - assert_eq!( - unsafe { ffi::oakstorage_save(handle, cs(&uri).as_ptr(), 0) }, - OAKSTORAGE_OK - ); - free_project(handle); + save_project(&project, &uri, 0).unwrap(); - let before = alive(); - { - let (session, rc) = open(&uri); - assert_eq!(rc, OAKSTORAGE_OK); - assert_eq!(alive(), before + 1, "open counts one session"); + let (mut session, rc) = open(&uri).unwrap(); + assert_eq!(rc, OAKSTORAGE_OK); + assert!(session.project().is_some(), "open counts one project"); - // Take transfers the project; the session shell stays alive. - let taken = unsafe { ffi::oakstorage_project_take_project(session) }; - assert!(!taken.ctx.is_null()); - assert_eq!(alive(), before + 1, "take keeps the session shell"); + // Take transfers the project; the session shell stays empty. + let taken = session.take().unwrap(); + assert!(!taken.ctx.is_null()); + assert!(session.project().is_none(), "take empties the shell"); + release(taken); + // Dropping the shell (with the project already taken) is a no-op. + drop(session); - free_project(taken); - free_session(session); - } - assert_eq!(alive(), before, "open/take/free pair leaves no leak"); - - // A second pairing through project_project (borrowed) as well. - let before = alive(); - { - let (session, _) = open(&uri); - let borrowed = unsafe { ffi::oakstorage_project_project(session) }; - assert!(!borrowed.ctx.is_null()); - free_project(borrowed); - free_session(session); - } - assert_eq!(alive(), before, "borrowed view pairing leaves no leak"); + // A second open/take pairing works the same. + let (mut session, _) = open(&uri).unwrap(); + let taken = session.take().unwrap(); + assert!(!taken.ctx.is_null()); + release(taken); } diff --git a/crates/oaktask/Cargo.toml b/crates/oaktask/Cargo.toml index dfc8c6c51..197965c60 100644 --- a/crates/oaktask/Cargo.toml +++ b/crates/oaktask/Cargo.toml @@ -9,18 +9,12 @@ license = "GPL-3.0-or-later" crate-type = ["staticlib", "rlib"] [features] -# Link the real oakrender crate (dev-dependency) into the integration test -# binary and drop the oakrender ticket stubs from tests/common, so -# `tests/render_real_integration_test.rs` drives oaktask's render loop -# against the actual oakrender ticket arena (CPU path, no GPU). -# -# Build it with: -# cargo test --features real-oakrender --test render_real_integration_test -# -# The flag is per-binary: `cargo test --features real-oakrender` (without -# `--test`) also compiles the stub-based tests, which cannot coexist with -# the real oakrender exports in one binary (duplicate `#[no_mangle]` -# symbols), so always pass the `--test` filter when the feature is on. +# Historical marker feature (single-lib unification). The stub-based render +# integration test it selected (`tests/render_real_integration_test.rs`, +# which linked the real oakrender ticket arena against oaktask's deleted +# bridge::render C ABI declarations) was removed together with the C ABI +# layer. The flag is retained so older `cargo test` invocations keep +# parsing; it has no effect today. real-oakrender = [] [dependencies] @@ -29,7 +23,6 @@ oakundo = { path = "../oakundo" } oakcommon = { path = "../oakcommon" } oakcodec = { path = "../oakcodec" } oaknode = { path = "../oaknode" } -oaktimeline = { path = "../oaktimeline" } # Pure-Rust OpenTimelineIO JSON binding (serde model of the OTIO JSON format # covering exactly the object graph the C++ loadotio/saveotio tasks use). @@ -37,10 +30,11 @@ oaktimeline = { path = "../oaktimeline" } # LoadOTIOTask/SaveOTIOTask parse/serialize through this crate. oakotio = { path = "../oakotio" } -# oakrender is a regular path dependency (single-lib: bridge/render.rs calls -# its ffi directly). The `real-oakrender` marker feature keeps the -# render-real integration test's cargo invocation working (it links the -# real oakrender — now always — and skips the ticket stubs). +# oakrender is a regular path dependency (single-lib: the direct ticket +# arena in `oakrender::ticket` will back the render loop once the +# CHandle-based loop is rewired; until then `src/stubs.rs` stands in for +# the deleted ticket C ABI). The `real-oakrender` marker feature keeps the +# old render-real integration test's cargo invocation parsing (see above). oakrender = { path = "../oakrender" } # Error trait derive for the crate-internal error enum. thiserror = "2" diff --git a/crates/oaktask/src/bridge/codec.rs b/crates/oaktask/src/bridge/codec.rs deleted file mode 100644 index 0842c16bf..000000000 --- a/crates/oaktask/src/bridge/codec.rs +++ /dev/null @@ -1,293 +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 . - -// 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 . - -//! oakcodec C ABI imports. The task module reaches the codec side through -//! `include/codec/task.h` (the task submitter), `include/codec/decoder.h` -//! (conform/proxy work) and `include/codec/encoder.h` (export). Signatures -//! mirror the headers verbatim. - -use std::ffi::{c_char, c_int, c_uint, c_void}; - -use crate::bridge::render::OakCancelAtom; -use crate::handle::CHandle; - -/// `OakCodecTaskKind` enum values (`include/codec/task.h`). -pub const OAKCODEC_TASK_CONFORM: c_int = 0; -/// `OakCodecTaskKind::OAKCODEC_TASK_PROXY`. -pub const OAKCODEC_TASK_PROXY: c_int = 1; -/// `OAKCODEC_E_CANCELLED` (`include/codec/error.h`). -pub const OAKCODEC_E_CANCELLED: c_int = -50006; -/// `OAKCODEC_E_INVALID` (`include/codec/error.h`). -pub const OAKCODEC_E_INVALID: c_int = -50001; -/// `OAKCODEC_E_FAILED` (`include/codec/error.h`). -pub const OAKCODEC_E_FAILED: c_int = -50003; - -/// Mirror of `OakCodecTaskRequest` (`include/codec/task.h`). -pub type OakCodecTaskRequest = oakcodec::task::OakCodecTaskRequest; - -/// `oakcodec_task_submit_fn` callback typedef. -pub type OakCodecTaskSubmitFn = - unsafe extern "C" fn(req: *const OakCodecTaskRequest, userdata: *mut c_void) -> c_int; - -/// Mirror of `OakCodecFrame`/`OakFrame` handle type (`include/codec/frame.h`). -pub type OakFrame = CHandle; - -/// Mirror of `OakEncoder` handle type (`include/codec/encoder.h`). -pub type OakEncoder = CHandle; - -/// Mirror of `OakDecoder` handle type (`include/codec/decoder.h`). -pub type OakDecoder = CHandle; - -/// Mirror of `OakCodecProxyParams` (`include/codec/proxy.h`). -pub type OakCodecProxyParams = oakcodec::ffi::proxy::oakcodec_proxy_params; - -/// Mirror of `oakcodec_encoding_params` (`include/codec/encoder.h`) — the -/// export task fills this to open the encoder. Layout mirrors the header -/// verbatim (a zeroed struct = all tracks disabled). -pub type OakCodecEncodingParams = oakcodec::ffi::encoder::oakcodec_encoding_params; - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_set_task_submit_cb(cb: Option, userdata: *mut c_void) { - unsafe { oakcodec::ffi::task::oakcodec_set_task_submit_cb(cb, userdata) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_task_submit_is_registered() -> c_int { - unsafe { oakcodec::ffi::task::oakcodec_task_submit_is_registered() } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_decoder_init() -> OakDecoder { - unsafe { oakcodec::ffi::decoder::oakcodec_decoder_init() } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_decoder_free(decoder: *mut OakDecoder) { - unsafe { oakcodec::ffi::decoder::oakcodec_decoder_free(decoder) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_decoder_open( - decoder: OakDecoder, - filename: *const c_char, - stream_index: c_int, -) -> c_int { - unsafe { oakcodec::ffi::decoder::oakcodec_decoder_open(decoder, filename, stream_index) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_decoder_close(decoder: OakDecoder) -> c_int { - unsafe { oakcodec::ffi::decoder::oakcodec_decoder_close(decoder) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_decoder_is_open(decoder: OakDecoder) -> c_int { - unsafe { oakcodec::ffi::decoder::oakcodec_decoder_is_open(decoder) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_decoder_decode_audio( - decoder: OakDecoder, - in_num: c_int, - in_den: c_int, - out_num: c_int, - out_den: c_int, - sample_rate: c_int, - channel_layout: u64, - buf: *mut f32, - buf_frames: c_int, -) -> c_int { - unsafe { - oakcodec::ffi::decoder::oakcodec_decoder_decode_audio( - decoder, - in_num, - in_den, - out_num, - out_den, - sample_rate, - channel_layout, - buf, - buf_frames, - ) - } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_decoder_conform_audio( - decoder: OakDecoder, - output_filenames: *const *const c_char, - filename_count: c_int, - sample_rate: c_int, - channel_layout: u64, - sample_format: c_int, - cancelled: OakCancelAtom, -) -> c_int { - unsafe { - oakcodec::ffi::decoder::oakcodec_decoder_conform_audio( - decoder, - output_filenames, - filename_count, - sample_rate, - channel_layout, - sample_format, - cancelled, - ) - } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_decoder_last_error( - decoder: OakDecoder, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oakcodec::ffi::decoder::oakcodec_decoder_last_error(decoder, buf, buf_size) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_decoder_get_image_sequence_digit_count(filename: *const c_char) -> c_int { - unsafe { oakcodec::ffi::decoder::oakcodec_decoder_get_image_sequence_digit_count(filename) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_decoder_get_image_sequence_index(filename: *const c_char) -> i64 { - unsafe { oakcodec::ffi::decoder::oakcodec_decoder_get_image_sequence_index(filename) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_decoder_transform_image_sequence_file_name( - filename: *const c_char, - number: i64, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oakcodec::ffi::decoder::oakcodec_decoder_transform_image_sequence_file_name( - filename, number, buf, buf_size, - ) - } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_encoder_get_desired_pixel_format(encoder: OakEncoder) -> c_int { - unsafe { oakcodec::ffi::encoder::oakcodec_encoder_get_desired_pixel_format(encoder) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_encoding_generate_matrix( - method: c_int, - src_width: c_int, - src_height: c_int, - dst_width: c_int, - dst_height: c_int, - out_matrix: *mut f64, -) -> c_int { - unsafe { - oakcodec::ffi::encoder::oakcodec_encoding_generate_matrix( - method, src_width, src_height, dst_width, dst_height, out_matrix, - ) - } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_proxy_find_ffmpeg( - configured_path: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oakcodec::ffi::proxy::oakcodec_proxy_find_ffmpeg(configured_path, buf, buf_size) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_proxy_params_default(out: *mut OakCodecProxyParams) -> c_int { - unsafe { oakcodec::ffi::proxy::oakcodec_proxy_params_default(out) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_encoder_init(params: *const OakCodecEncodingParams) -> OakEncoder { - unsafe { oakcodec::ffi::encoder::oakcodec_encoder_init(params) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_encoder_free(encoder: *mut OakEncoder) { - unsafe { oakcodec::ffi::encoder::oakcodec_encoder_free(encoder) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_encoder_open(encoder: OakEncoder) -> c_int { - unsafe { oakcodec::ffi::encoder::oakcodec_encoder_open(encoder) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_encoder_write_video(encoder: OakEncoder, frame: OakFrame) -> c_int { - unsafe { oakcodec::ffi::encoder::oakcodec_encoder_write_video(encoder, frame) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_encoder_write_audio( - encoder: OakEncoder, - samples: *const f32, - frame_count: c_int, -) -> c_int { - unsafe { oakcodec::ffi::encoder::oakcodec_encoder_write_audio(encoder, samples, frame_count) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_encoder_write_subtitle( - encoder: OakEncoder, - text: *const c_char, - in_seconds: f64, - out_seconds: f64, -) -> c_int { - unsafe { - oakcodec::ffi::encoder::oakcodec_encoder_write_subtitle( - encoder, - text, - in_seconds, - out_seconds, - ) - } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_encoder_flush(encoder: OakEncoder) -> c_int { - unsafe { oakcodec::ffi::encoder::oakcodec_encoder_flush(encoder) } -} - -/// Direct call into the `oakcodec` crate (single-lib unification). -pub fn oakcodec_encoder_last_error( - encoder: OakEncoder, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oakcodec::ffi::encoder::oakcodec_encoder_last_error(encoder, buf, buf_size) } -} diff --git a/crates/oaktask/src/bridge/common.rs b/crates/oaktask/src/bridge/common.rs deleted file mode 100644 index 5ca739a6c..000000000 --- a/crates/oaktask/src/bridge/common.rs +++ /dev/null @@ -1,265 +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 . - -// 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 . - -//! oakcommon / oakcore C ABI imports. The task module reaches the shared -//! value types through `include/common/videoparams.h`, -//! `include/common/colortransform.h` and the oakcore audio-params C ABI. -//! Signatures mirror the headers verbatim. - -use std::ffi::{c_char, c_int}; - -use crate::handle::CHandle; - -/// Mirror of `OakVideoParams` (`include/common/videoparams.h`). -/// -/// A by-value handle (shared_ptr semantics); its `{ctx, addref, release, -/// abi_version}` layout is identical to [`CHandle`], so it is a type alias — -/// every oakcommon handle is created and destroyed inside the oakcommon DLL -/// and only crosses the FFI boundary as this opaque by-value struct. -pub type OakVideoParams = CHandle; - -/// Mirror of `OakColorTransform` (`include/common/colortransform.h`); see -/// [`OakVideoParams`] for the alias rationale. -pub type OakColorTransform = CHandle; - -/// Mirror of `OakAudioParams` — oakcore audio parameters. Declared as a -/// `CHandle`-shaped opaque; the oakcore C ABI header that owns its layout is -/// the authoritative source once the oakcore C ABI ships. -pub type OakAudioParams = CHandle; - -/// `OAKCOMMON_VIDEO_TYPE_STILL` (`include/common/videoparams.h`). -pub const OAKCOMMON_VIDEO_TYPE_STILL: c_int = 1; -/// `OAKCOMMON_VIDEO_TYPE_IMAGE_SEQUENCE` (`include/common/videoparams.h`). -pub const OAKCOMMON_VIDEO_TYPE_IMAGE_SEQUENCE: c_int = 2; - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_videoparams_init() -> OakVideoParams { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_init() } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -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, -) -> OakVideoParams { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_init_basic( - width, - height, - pixel_format, - nb_channels, - pixel_aspect_num, - pixel_aspect_den, - interlacing, - divider, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_videoparams_free(params: *mut OakVideoParams) { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_free(params) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_videoparams_get_width(params: OakVideoParams, width: *mut c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_width(params, width) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_videoparams_get_height(params: OakVideoParams, height: *mut c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_height(params, height) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_videoparams_get_format(params: OakVideoParams, format: *mut c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_format(params, format) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_videoparams_set_format(params: OakVideoParams, format: c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_format(params, format) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_videoparams_get_time_base( - params: OakVideoParams, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_get_time_base( - params, - numerator, - denominator, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_videoparams_set_time_base( - params: OakVideoParams, - numerator: c_int, - denominator: c_int, -) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_set_time_base( - params, - numerator, - denominator, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_videoparams_frame_rate_as_time_base( - params: OakVideoParams, - out_num: *mut c_int, - out_den: *mut c_int, -) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_frame_rate_as_time_base( - params, out_num, out_den, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_videoparams_set_frame_rate( - params: OakVideoParams, - numerator: c_int, - denominator: c_int, -) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_set_frame_rate( - params, - numerator, - denominator, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_videoparams_get_video_type(params: OakVideoParams, out_type: *mut c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_video_type(params, out_type) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_videoparams_set_video_type(params: OakVideoParams, video_type: c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_video_type(params, video_type) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_videoparams_set_start_time(params: OakVideoParams, start: i64) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_start_time(params, start) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_videoparams_set_duration(params: OakVideoParams, duration: i64) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_duration(params, duration) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_videoparams_get_is_valid(params: OakVideoParams, out_valid: *mut c_int) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_is_valid(params, out_valid) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_videoparams_get_frame_rate( - params: OakVideoParams, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { - oakcommon::ffi::videoparams::oakcommon_videoparams_get_frame_rate( - params, - numerator, - denominator, - ) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_videoparams_get_duration(params: OakVideoParams, duration: *mut i64) -> c_int { - unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_duration(params, duration) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_colortransform_init_display( - display: *const c_char, - view: *const c_char, - look: *const c_char, -) -> OakColorTransform { - unsafe { - oakcommon::ffi::colortransform::oakcommon_colortransform_init_display(display, view, look) - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_colortransform_init_output(output: *const c_char) -> OakColorTransform { - unsafe { oakcommon::ffi::colortransform::oakcommon_colortransform_init_output(output) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_colortransform_free(transform: *mut OakColorTransform) { - unsafe { oakcommon::ffi::colortransform::oakcommon_colortransform_free(transform) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_config_get( - group: *const c_char, - key: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oakcommon::ffi::config::oakcommon_config_get(group, key, buf, buf_size) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_config_get_int(group: *const c_char, key: *const c_char, default: c_int) -> c_int { - unsafe { oakcommon::ffi::config::oakcommon_config_get_int(group, key, default) } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_config_get_bool( - group: *const c_char, - key: *const c_char, - default: c_int, -) -> c_int { - unsafe { oakcommon::ffi::config::oakcommon_config_get_bool(group, key, default) } -} diff --git a/crates/oaktask/src/bridge/mod.rs b/crates/oaktask/src/bridge/mod.rs deleted file mode 100644 index 6f5536d79..000000000 --- a/crates/oaktask/src/bridge/mod.rs +++ /dev/null @@ -1,25 +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 . - -//! C ABI imports from other oak modules (signatures mirror the public -//! headers verbatim; resolved at link time). - -pub mod codec; -pub mod common; -pub mod node; -pub mod render; -pub mod timeline; -pub mod undo; diff --git a/crates/oaktask/src/bridge/node.rs b/crates/oaktask/src/bridge/node.rs deleted file mode 100644 index 7491b1095..000000000 --- a/crates/oaktask/src/bridge/node.rs +++ /dev/null @@ -1,1268 +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 . - -// 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 . - -//! oaknode C ABI imports. The task module drives node-graph work through -//! `include/node/project.h` (project shell), `include/node/folder.h`, -//! `include/node/footage.h`, `include/node/sequence.h`, -//! `include/node/colormanager.h` and `include/node/serializer.h`. Signatures -//! mirror the headers verbatim; node handles are `CHandle` values whose -//! underlying objects live inside the node library. - -use std::ffi::{c_char, c_int, c_void}; - -use crate::bridge::common::OakVideoParams; -use crate::bridge::render::OakCancelAtom; -use crate::handle::CHandle; - -/// `OakNodeProject` (`include/node/project.h`). -pub type OakNodeProject = CHandle; -/// `OakNodeNode` (`include/node/node.h`). -pub type OakNodeNode = CHandle; -/// `OakNodeFolder` (`include/node/folder.h`). -pub type OakNodeFolder = CHandle; -/// `OakNodeFootage` (`include/node/footage.h`). -pub type OakNodeFootage = CHandle; -/// `OakNodeSequence` (`include/node/sequence.h`). -pub type OakNodeSequence = CHandle; -/// `OakNodeTrack` (`include/node/track.h`). -pub type OakNodeTrack = CHandle; -/// `OakNodeTrackList` (`include/node/track.h`). -pub type OakNodeTrackList = CHandle; -/// `OakNodeBlock` (`include/node/block.h`). -pub type OakNodeBlock = CHandle; -/// `OakNodeColorManager` (`include/node/colormanager.h`). -pub type OakNodeColorManager = CHandle; -/// `OakNodeSerializerSaveData` (`include/node/serializer.h`). -pub type OakNodeSerializerSaveData = CHandle; -/// `OakNodeSerializerLoadData` (`include/node/serializer.h`). -pub type OakNodeSerializerLoadData = CHandle; - -/// `OAKNODE_SEQUENCE_TEXTURE_INPUT` (`include/node/sequence.h`). -pub const OAKNODE_SEQUENCE_TEXTURE_INPUT: &str = "tex_in"; -/// `OAKNODE_SEQUENCE_SAMPLES_INPUT` (`include/node/sequence.h`). -pub const OAKNODE_SEQUENCE_SAMPLES_INPUT: &str = "samples_in"; -/// `OAKNODE_TRACK_TYPE_VIDEO` (`include/node/track.h`). -pub const OAKNODE_TRACK_TYPE_VIDEO: c_int = 0; -/// `OAKNODE_TRACK_TYPE_AUDIO` (`include/node/track.h`). -pub const OAKNODE_TRACK_TYPE_AUDIO: c_int = 1; -/// `OAKNODE_TRACK_TYPE_NONE` (`include/node/track.h`). -pub const OAKNODE_TRACK_TYPE_NONE: c_int = -1; -/// `OAKNODE_BLOCK_OTHER` (`include/node/block.h`). -pub const OAKNODE_BLOCK_OTHER: c_int = 0; -/// `OAKNODE_BLOCK_CLIP` (`include/node/block.h`). -pub const OAKNODE_BLOCK_CLIP: c_int = 1; -/// `OAKNODE_BLOCK_GAP` (`include/node/block.h`). -pub const OAKNODE_BLOCK_GAP: c_int = 2; -/// `OAKNODE_BLOCK_TRANSITION` (`include/node/block.h`). -pub const OAKNODE_BLOCK_TRANSITION: c_int = 3; -/// `OAKNODE_TRANSITION_CROSS_DISSOLVE` (`include/node/block.h`). -pub const OAKNODE_TRANSITION_CROSS_DISSOLVE: c_int = 0; -/// `OAKNODE_TRANSITION_OUT_BLOCK_INPUT` (`include/node/block.h`). -pub const OAKNODE_TRANSITION_OUT_BLOCK_INPUT: &str = "out_block_in"; -/// `OAKNODE_TRANSITION_IN_BLOCK_INPUT` (`include/node/block.h`). -pub const OAKNODE_TRANSITION_IN_BLOCK_INPUT: &str = "in_block_in"; - -/// Node type ids used by the OTIO tasks -/// (`src/task/src/project/loadotio/loadotio.cpp`). -/// `k_sequence_id` — the sequence node type. -pub const OAKNODE_TYPE_SEQUENCE: &str = "org.olivevideoeditor.Olive.sequence"; -/// `k_transform_id` — the transform node type. -pub const OAKNODE_TYPE_TRANSFORM: &str = "org.olivevideoeditor.Olive.transform"; -/// `k_volume_id` — the volume node type. -pub const OAKNODE_TYPE_VOLUME: &str = "org.olivevideoeditor.Olive.volume"; - -/// C string helper: build a NUL-terminated copy of a Rust string for the -/// C ABI. The buffer is leaked for the duration of the process; callers -/// pass short-lived literals. -pub fn cstr(s: &str) -> *const c_char { - let mut bytes = s.as_bytes().to_vec(); - bytes.push(0); - bytes.leak().as_ptr() as *const c_char -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_node_get_label(node: OakNodeNode, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_get_label(node, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_node_set_label(node: OakNodeNode, label: *const c_char) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_set_label(node, label) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_node_get_id(node: OakNodeNode, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_get_id(node, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_node_get_project(node: OakNodeNode, out: *mut OakNodeProject) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_get_project(node, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_node_input_get_connected_node( - node: OakNodeNode, - input_id: *const c_char, - out_node: *mut OakNodeNode, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_input_get_connected_node(node, input_id, out_node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_node_set_context_position( - node: OakNodeNode, - context: OakNodeNode, - x: f64, - y: f64, - expanded: c_int, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_set_context_position(node, context, x, y, expanded) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_node_find_input_footage(node: OakNodeNode, out: *mut OakNodeFootage) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_find_input_footage(node, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_node_get_input_string( - node: OakNodeNode, - input_id: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_get_input_string(node, input_id, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_node_create_copy(node: OakNodeNode) -> OakNodeNode { - unsafe { oaknode::ffi::node::oaknode_node_create_copy(node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_node_copy_inputs( - dst: OakNodeNode, - src: OakNodeNode, - include_connections: c_int, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_copy_inputs(dst, src, include_connections) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_node_connect( - output_node: OakNodeNode, - input_node: OakNodeNode, - input_id: *const c_char, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_node_connect(output_node, input_node, input_id) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_node_set_value_hint_track( - node: OakNodeNode, - input_id: *const c_char, - track_type: c_int, - track_index: c_int, -) -> c_int { - unsafe { - oaknode::ffi::node::oaknode_node_set_value_hint_track( - node, - input_id, - track_type, - track_index, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_node_get_video_frame_cache( - node: OakNodeNode, - out: *mut crate::bridge::render::OakRenderCache, -) -> c_int { - unsafe { - oaknode::ffi::node::oaknode_node_get_video_frame_cache(node, out as *mut std::ffi::c_void) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_node_free(node: *mut OakNodeNode) { - unsafe { oaknode::ffi::node::oaknode_node_free(node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_command_create_remove_node( - node: OakNodeNode, -) -> crate::bridge::undo::OakUndoCommand { - unsafe { oaknode::ffi::node::oaknode_command_create_remove_node(node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_factory_create_from_id(type_id: *const c_char) -> OakNodeNode { - unsafe { oaknode::ffi::factory::oaknode_factory_create_from_id(type_id) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_viewer_set_video_params( - viewer: OakNodeNode, - params: *const OakVideoParams, -) -> c_int { - unsafe { - oaknode::ffi::node::oaknode_viewer_set_video_params( - viewer, - params as *const std::ffi::c_void, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_viewer_set_audio_params( - viewer: OakNodeNode, - params: *const std::ffi::c_void, -) -> c_int { - unsafe { oaknode::ffi::node::oaknode_viewer_set_audio_params(viewer, params) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_clip_create() -> OakNodeBlock { - unsafe { oaknode::ffi::block::oaknode_block_clip_create() } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_gap_create() -> OakNodeBlock { - unsafe { oaknode::ffi::block::oaknode_block_gap_create() } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_transition_create(kind: c_int) -> OakNodeBlock { - unsafe { oaknode::ffi::block::oaknode_block_transition_create(kind) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_as_node(block: OakNodeBlock) -> OakNodeNode { - unsafe { oaknode::ffi::block::oaknode_block_as_node(block) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_get_kind(block: OakNodeBlock, out_kind: *mut c_int) -> c_int { - unsafe { oaknode::ffi::block::oaknode_block_get_kind(block, out_kind) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_get_in( - block: OakNodeBlock, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { oaknode::ffi::block::oaknode_block_get_in(block, numerator, denominator) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_get_length( - block: OakNodeBlock, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { oaknode::ffi::block::oaknode_block_get_length(block, numerator, denominator) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_set_length_and_media_out( - block: OakNodeBlock, - numerator: c_int, - denominator: c_int, -) -> c_int { - unsafe { - oaknode::ffi::block::oaknode_block_set_length_and_media_out(block, numerator, denominator) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_clip_set_media_in( - clip: OakNodeBlock, - numerator: c_int, - denominator: c_int, -) -> c_int { - unsafe { oaknode::ffi::block::oaknode_clip_set_media_in(clip, numerator, denominator) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_transition_get_in_offset( - transition: OakNodeBlock, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { - oaknode::ffi::block::oaknode_transition_get_in_offset(transition, numerator, denominator) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_transition_get_out_offset( - transition: OakNodeBlock, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { - oaknode::ffi::block::oaknode_transition_get_out_offset(transition, numerator, denominator) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_transition_set_offsets_and_length( - transition: OakNodeBlock, - in_num: c_int, - in_den: c_int, - out_num: c_int, - out_den: c_int, -) -> c_int { - unsafe { - oaknode::ffi::block::oaknode_transition_set_offsets_and_length( - transition, in_num, in_den, out_num, out_den, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_init() -> OakNodeProject { - unsafe { oaknode::ffi::project::oaknode_project_init() } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_free(project: *mut OakNodeProject) { - unsafe { oaknode::ffi::project::oaknode_project_free(project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_initialize(project: OakNodeProject) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_initialize(project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_clear(project: OakNodeProject) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_clear(project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_root(project: OakNodeProject) -> OakNodeFolder { - unsafe { oaknode::ffi::project::oaknode_project_root(project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_name(project: OakNodeProject, buf: *mut c_char, buf_size: c_int) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_name(project, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_filename( - project: OakNodeProject, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_filename(project, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_pretty_filename( - project: OakNodeProject, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_pretty_filename(project, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_set_filename(project: OakNodeProject, filename: *const c_char) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_set_filename(project, filename) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_is_modified(project: OakNodeProject) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_is_modified(project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_set_modified(project: OakNodeProject, modified: c_int) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_set_modified(project, modified) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_is_new(project: OakNodeProject) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_is_new(project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_cache_path( - project: OakNodeProject, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_cache_path(project, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_copy_settings(dst: OakNodeProject, src: OakNodeProject) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_copy_settings(dst, src) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_get_cache_location_setting(project: OakNodeProject) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_get_cache_location_setting(project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_set_cache_location_setting( - project: OakNodeProject, - setting: c_int, -) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_set_cache_location_setting(project, setting) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_get_custom_cache_path( - project: OakNodeProject, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_get_custom_cache_path(project, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_set_custom_cache_path( - project: OakNodeProject, - path: *const c_char, -) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_set_custom_cache_path(project, path) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_get_uuid( - project: OakNodeProject, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_get_uuid(project, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_add_node(project: OakNodeProject, node: OakNodeNode) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_add_node(project, node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_remove_node(project: OakNodeProject, node: OakNodeNode) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_remove_node(project, node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_node_count(project: OakNodeProject) -> c_int { - unsafe { oaknode::ffi::project::oaknode_project_node_count(project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_node_at(project: OakNodeProject, index: c_int) -> OakNodeNode { - unsafe { oaknode::ffi::project::oaknode_project_node_at(project, index) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_folder_create(project: OakNodeProject) -> OakNodeFolder { - unsafe { oaknode::ffi::folder::oaknode_folder_create(project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_folder_child_count(folder: OakNodeFolder) -> c_int { - unsafe { oaknode::ffi::folder::oaknode_folder_child_count(folder) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_folder_child_at(folder: OakNodeFolder, index: c_int) -> OakNodeNode { - unsafe { oaknode::ffi::folder::oaknode_folder_child_at(folder, index) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_folder_add_child(folder: OakNodeFolder, child: OakNodeNode) -> c_int { - unsafe { oaknode::ffi::folder::oaknode_folder_add_child(folder, child) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_folder_as_node(folder: OakNodeFolder) -> OakNodeNode { - unsafe { oaknode::ffi::folder::oaknode_folder_as_node(folder) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_command_create_folder_add_child( - folder: OakNodeFolder, - child: OakNodeNode, -) -> crate::bridge::undo::OakUndoCommand { - unsafe { oaknode::ffi::folder::oaknode_command_create_folder_add_child(folder, child) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_folder_remove_child(folder: OakNodeFolder, child: OakNodeNode) -> c_int { - unsafe { oaknode::ffi::folder::oaknode_folder_remove_child(folder, child) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_folder_move_children( - nodes: *const OakNodeNode, - count: c_int, - dest_folder: OakNodeFolder, -) -> c_int { - unsafe { oaknode::ffi::folder::oaknode_folder_move_children(nodes, count, dest_folder) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_folder_has_child_recursive(folder: OakNodeFolder, child: OakNodeNode) -> c_int { - unsafe { oaknode::ffi::folder::oaknode_folder_has_child_recursive(folder, child) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_folder_index_of_child(folder: OakNodeFolder, child: OakNodeNode) -> c_int { - unsafe { oaknode::ffi::folder::oaknode_folder_index_of_child(folder, child) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_folder_parent_of(node: OakNodeNode) -> OakNodeFolder { - unsafe { oaknode::ffi::folder::oaknode_folder_parent_of(node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_create(project: OakNodeProject, filename: *const c_char) -> OakNodeFootage { - unsafe { oaknode::ffi::footage::oaknode_footage_create(project, filename) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_as_node(footage: OakNodeFootage) -> OakNodeNode { - unsafe { oaknode::ffi::footage::oaknode_footage_as_node(footage) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_filename( - footage: OakNodeFootage, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_filename(footage, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_set_filename(footage: OakNodeFootage, filename: *const c_char) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_set_filename(footage, filename) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_is_valid(footage: OakNodeFootage) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_is_valid(footage) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_timestamp(footage: OakNodeFootage, out_timestamp: *mut i64) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_timestamp(footage, out_timestamp) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_set_timestamp(footage: OakNodeFootage, timestamp: i64) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_set_timestamp(footage, timestamp) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_decoder( - footage: OakNodeFootage, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_decoder(footage, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_total_stream_count(footage: OakNodeFootage) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_total_stream_count(footage) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_video_stream_count(footage: OakNodeFootage) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_video_stream_count(footage) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_audio_stream_count(footage: OakNodeFootage) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_audio_stream_count(footage) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_subtitle_stream_count(footage: OakNodeFootage) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_subtitle_stream_count(footage) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_duration( - footage: OakNodeFootage, - out_numerator: *mut c_int, - out_denominator: *mut c_int, -) -> c_int { - unsafe { - oaknode::ffi::footage::oaknode_footage_duration(footage, out_numerator, out_denominator) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_proxy_enabled(footage: OakNodeFootage) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_proxy_enabled(footage) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_set_proxy_enabled(footage: OakNodeFootage, enabled: c_int) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_set_proxy_enabled(footage, enabled) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_proxy_path( - footage: OakNodeFootage, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_proxy_path(footage, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_proxy_state(footage: OakNodeFootage) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_proxy_state(footage) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_set_proxy( - footage: OakNodeFootage, - path: *const c_char, - state: c_int, - video_stream_index: c_int, - preset_version: c_int, - enabled: c_int, -) -> c_int { - unsafe { - oaknode::ffi::footage::oaknode_footage_set_proxy( - footage, - path, - state, - video_stream_index, - preset_version, - enabled, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_clear_proxy(footage: OakNodeFootage) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_clear_proxy(footage) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_get_video_params( - footage: OakNodeFootage, - index: c_int, - out: *mut OakVideoParams, -) -> c_int { - unsafe { - oaknode::ffi::footage::oaknode_footage_get_video_params( - footage, - index, - out as *mut std::ffi::c_void, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_set_video_params( - footage: OakNodeFootage, - index: c_int, - params: *const OakVideoParams, -) -> c_int { - unsafe { - oaknode::ffi::footage::oaknode_footage_set_video_params( - footage, - index, - params as *const std::ffi::c_void, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_get_video_length( - footage: OakNodeFootage, - out_num: *mut i64, - out_den: *mut i64, -) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_get_video_length(footage, out_num, out_den) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_footage_set_cancel_atom(footage: OakNodeFootage, atom: OakCancelAtom) -> c_int { - unsafe { oaknode::ffi::footage::oaknode_footage_set_cancel_atom(footage, atom) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_create() -> OakNodeSequence { - unsafe { oaknode::ffi::sequence::oaknode_sequence_create() } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_free(sequence: *mut OakNodeSequence) { - unsafe { oaknode::ffi::sequence::oaknode_sequence_free(sequence) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_set_default_parameters(sequence: OakNodeSequence) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_set_default_parameters(sequence) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_as_node(sequence: OakNodeSequence) -> OakNodeNode { - unsafe { oaknode::ffi::sequence::oaknode_sequence_as_node(sequence) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_from_node(node: OakNodeNode) -> OakNodeSequence { - unsafe { oaknode::ffi::sequence::oaknode_sequence_from_node(node) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_get_track_list( - sequence: OakNodeSequence, - r#type: c_int, - out: *mut OakNodeTrackList, -) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_get_track_list(sequence, r#type, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_get_track_count( - sequence: OakNodeSequence, - r#type: c_int, - count: *mut c_int, -) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_get_track_count(sequence, r#type, count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_get_track_at( - sequence: OakNodeSequence, - r#type: c_int, - index: c_int, - out: *mut OakNodeTrack, -) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_get_track_at(sequence, r#type, index, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_get_all_track_count(sequence: OakNodeSequence, count: *mut c_int) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_get_all_track_count(sequence, count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_get_all_track_at( - sequence: OakNodeSequence, - index: c_int, - out: *mut OakNodeTrack, -) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_get_all_track_at(sequence, index, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_get_playhead( - sequence: OakNodeSequence, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { - oaknode::ffi::sequence::oaknode_sequence_get_playhead(sequence, numerator, denominator) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_set_playhead( - sequence: OakNodeSequence, - numerator: c_int, - denominator: c_int, -) -> c_int { - unsafe { - oaknode::ffi::sequence::oaknode_sequence_set_playhead(sequence, numerator, denominator) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_get_length( - sequence: OakNodeSequence, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_get_length(sequence, numerator, denominator) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_get_video_length( - sequence: OakNodeSequence, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { - oaknode::ffi::sequence::oaknode_sequence_get_video_length(sequence, numerator, denominator) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_get_audio_length( - sequence: OakNodeSequence, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { - oaknode::ffi::sequence::oaknode_sequence_get_audio_length(sequence, numerator, denominator) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_verify_length(sequence: OakNodeSequence) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_verify_length(sequence) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_get_video_stream_count( - sequence: OakNodeSequence, - count: *mut c_int, -) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_get_video_stream_count(sequence, count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_get_audio_stream_count( - sequence: OakNodeSequence, - count: *mut c_int, -) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_get_audio_stream_count(sequence, count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_get_video_params( - sequence: OakNodeSequence, - index: c_int, - out: *mut OakVideoParams, -) -> c_int { - unsafe { - oaknode::ffi::sequence::oaknode_sequence_get_video_params( - sequence, - index, - out as *mut std::ffi::c_void, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_set_video_params( - sequence: OakNodeSequence, - index: c_int, - params: OakVideoParams, -) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_set_video_params(sequence, index, params) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_get_audio_params( - sequence: OakNodeSequence, - index: c_int, - out: *mut *mut std::ffi::c_void, -) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_get_audio_params(sequence, index, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_set_audio_params( - sequence: OakNodeSequence, - index: c_int, - params: *const c_void, -) -> c_int { - unsafe { oaknode::ffi::sequence::oaknode_sequence_set_audio_params(sequence, index, params) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_as_node(track: OakNodeTrack) -> OakNodeNode { - unsafe { oaknode::ffi::track::oaknode_track_as_node(track) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_create(r#type: c_int) -> OakNodeTrack { - unsafe { oaknode::ffi::track::oaknode_track_create(r#type) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_free(track: *mut OakNodeTrack) { - unsafe { oaknode::ffi::track::oaknode_track_free(track) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_get_type(track: OakNodeTrack, r#type: *mut c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_get_type(track, r#type) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_get_length( - track: OakNodeTrack, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_get_length(track, numerator, denominator) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_get_block_count(track: OakNodeTrack, count: *mut c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_get_block_count(track, count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_get_block_at( - track: OakNodeTrack, - index: c_int, - out: *mut OakNodeBlock, -) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_get_block_at(track, index, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_append_block(track: OakNodeTrack, block: OakNodeBlock) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_append_block(track, block) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_set_type(track: OakNodeTrack, r#type: c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_set_type(track, r#type) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_get_index(track: OakNodeTrack, index: *mut c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_get_index(track, index) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_set_index(track: OakNodeTrack, index: c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_set_index(track, index) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_get_sequence(track: OakNodeTrack, out: *mut OakNodeSequence) -> c_int { - unsafe { oaknode::ffi::track::oaknode_track_get_sequence(track, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_tracklist_get_sequence(list: OakNodeTrackList, out: *mut OakNodeSequence) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_get_sequence(list, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_tracklist_get_track_input_id( - list: OakNodeTrackList, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_get_track_input_id(list, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_tracklist_array_append(list: OakNodeTrackList) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_array_append(list) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_tracklist_array_remove_last(list: OakNodeTrackList) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_array_remove_last(list) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_tracklist_get_array_index_from_cache_index( - list: OakNodeTrackList, - cache_index: c_int, - out_index: *mut c_int, -) -> c_int { - unsafe { - oaknode::ffi::track::oaknode_tracklist_get_array_index_from_cache_index( - list, - cache_index, - out_index, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_tracklist_get_type(list: OakNodeTrackList, r#type: *mut c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_get_type(list, r#type) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_tracklist_get_track_count(list: OakNodeTrackList, count: *mut c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_get_track_count(list, count) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_tracklist_get_track_at( - list: OakNodeTrackList, - index: c_int, - out: *mut OakNodeTrack, -) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_get_track_at(list, index, out) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_tracklist_get_total_length( - list: OakNodeTrackList, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_get_total_length(list, numerator, denominator) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_tracklist_get_array_size(list: OakNodeTrackList, size: *mut c_int) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_get_array_size(list, size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_tracklist_add_track(list: OakNodeTrackList, track: OakNodeTrack) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_add_track(list, track) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_tracklist_remove_track(list: OakNodeTrackList, track: OakNodeTrack) -> c_int { - unsafe { oaknode::ffi::track::oaknode_tracklist_remove_track(list, track) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_colormanager_init(project: OakNodeProject) -> OakNodeColorManager { - unsafe { oaknode::ffi::colormanager::oaknode_colormanager_init(project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_colormanager_free(manager: *mut OakNodeColorManager) { - unsafe { oaknode::ffi::colormanager::oaknode_colormanager_free(manager) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_colormanager_wrap_borrowed(native_manager: *mut c_void) -> OakNodeColorManager { - unsafe { oaknode::ffi::colormanager::oaknode_colormanager_wrap_borrowed(native_manager) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_colormanager_initialize(manager: OakNodeColorManager) -> c_int { - unsafe { oaknode::ffi::colormanager::oaknode_colormanager_initialize(manager) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_colormanager_set_up_default_config() -> c_int { - unsafe { oaknode::ffi::colormanager::oaknode_colormanager_set_up_default_config() } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_serializer_initialize() -> c_int { - unsafe { oaknode::ffi::serializer::oaknode_serializer_initialize() } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_serializer_shutdown() { - unsafe { oaknode::ffi::serializer::oaknode_serializer_shutdown() } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_serializer_savedata_create( - load_type: c_int, - project: OakNodeProject, -) -> OakNodeSerializerSaveData { - unsafe { oaknode::ffi::serializer::oaknode_serializer_savedata_create(load_type, project) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_serializer_savedata_free(save_data: *mut OakNodeSerializerSaveData) { - unsafe { oaknode::ffi::serializer::oaknode_serializer_savedata_free(save_data) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_serializer_savedata_set_nodes( - save_data: OakNodeSerializerSaveData, - nodes: *const OakNodeNode, - count: c_int, -) -> c_int { - unsafe { - oaknode::ffi::serializer::oaknode_serializer_savedata_set_nodes(save_data, nodes, count) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_serializer_savedata_set_property( - save_data: OakNodeSerializerSaveData, - node: OakNodeNode, - key: *const c_char, - value: *const c_char, -) -> c_int { - unsafe { - oaknode::ffi::serializer::oaknode_serializer_savedata_set_property( - save_data, node, key, value, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_serializer_save_to_xml( - save_data: OakNodeSerializerSaveData, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { oaknode::ffi::serializer::oaknode_serializer_save_to_xml(save_data, buf, buf_size) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_serializer_load_from_xml( - project: OakNodeProject, - xml: *const c_char, - load_type: c_int, - out_result: *mut c_int, - out_load_data: *mut OakNodeSerializerLoadData, - details_buf: *mut c_char, - details_buf_size: c_int, -) -> c_int { - unsafe { - oaknode::ffi::serializer::oaknode_serializer_load_from_xml( - project, - xml, - load_type, - out_result, - out_load_data, - details_buf, - details_buf_size, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_serializer_loaddata_free(load_data: *mut OakNodeSerializerLoadData) { - unsafe { oaknode::ffi::serializer::oaknode_serializer_loaddata_free(load_data) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_serializer_loaddata_node_count(load_data: OakNodeSerializerLoadData) -> c_int { - unsafe { oaknode::ffi::serializer::oaknode_serializer_loaddata_node_count(load_data) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_serializer_loaddata_node_at( - load_data: OakNodeSerializerLoadData, - index: c_int, -) -> OakNodeNode { - unsafe { oaknode::ffi::serializer::oaknode_serializer_loaddata_node_at(load_data, index) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_serializer_loaddata_get_property( - load_data: OakNodeSerializerLoadData, - node: OakNodeNode, - key: *const c_char, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - unsafe { - oaknode::ffi::serializer::oaknode_serializer_loaddata_get_property( - load_data, node, key, buf, buf_size, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_serializer_loaddata_connection_count(load_data: OakNodeSerializerLoadData) -> c_int { - unsafe { oaknode::ffi::serializer::oaknode_serializer_loaddata_connection_count(load_data) } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_serializer_loaddata_connection_at( - load_data: OakNodeSerializerLoadData, - index: c_int, - out_output_node: *mut OakNodeNode, - out_input_node: *mut OakNodeNode, - input_id_buf: *mut c_char, - input_id_buf_size: c_int, - out_element: *mut c_int, -) -> c_int { - unsafe { - oaknode::ffi::serializer::oaknode_serializer_loaddata_connection_at( - load_data, - index, - out_output_node, - out_input_node, - input_id_buf, - input_id_buf_size, - out_element, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_serializer_save_to_file( - project: OakNodeProject, - filename: *const c_char, - use_compression: c_int, - out_code: *mut c_int, - details: *mut c_char, - details_size: c_int, -) -> c_int { - unsafe { - oaknode::ffi::serializer::oaknode_serializer_save_to_file( - project, - filename, - use_compression, - out_code, - details, - details_size, - ) - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_serializer_load_from_file( - project: OakNodeProject, - filename: *const c_char, - out_code: *mut c_int, - details: *mut c_char, - details_size: c_int, -) -> c_int { - unsafe { - oaknode::ffi::serializer::oaknode_serializer_load_from_file( - project, - filename, - out_code, - details, - details_size, - ) - } -} diff --git a/crates/oaktask/src/bridge/render.rs b/crates/oaktask/src/bridge/render.rs deleted file mode 100644 index cbd8d2636..000000000 --- a/crates/oaktask/src/bridge/render.rs +++ /dev/null @@ -1,290 +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 . - -// 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 . - -//! oakrender C ABI imports. The task module reaches the render side through -//! `include/render/cancelatom.h` (task cancellation), `include/render/ticket.h` -//! (frame/audio render tickets), `include/render/copier.h` (export project -//! copy) and `include/render/color.h` (color processor). Signatures mirror -//! the headers verbatim. -//! -//! Resolution is at link time (the crate's existing pattern, see -//! `bridge/codec.rs`): the symbols are satisfied by the real `liboakrender` -//! when it is linked into the same binary (the app links the module dylibs; -//! the `real-oakrender` integration test links the oakrender crate directly), -//! and by the `#[no_mangle]` stubs in `tests/common/mod.rs` in plain -//! `cargo test`. The real-oakrender ticket contract — in particular the -//! 2-argument `oakrender_ticket_finished_fn` — is verified by -//! `tests/render_real_integration_test.rs` against the actual exports. - -use std::ffi::{c_char, c_int, c_void}; - -use crate::bridge::codec::OakFrame; -use crate::bridge::common::{OakAudioParams, OakColorTransform, OakVideoParams}; -use crate::bridge::node::{OakNodeColorManager, OakNodeNode, OakNodeProject}; -use crate::handle::CHandle; - -/// Mirror of `OakCancelAtom` (`include/render/cancelatom.h`). -pub type OakCancelAtom = oakrender::ffi::OakCancelAtom; - -/// Mirror of `OakRenderTicket` (`include/render/ticket.h`). -pub type OakRenderTicket = oakrender::ffi::OakRenderTicket; - -/// Mirror of `OakRenderCache` (`include/render/cache.h`). -pub type OakRenderCache = oakrender::ffi::OakRenderCache; - -/// Mirror of `OakRenderProjectCopier` (`include/render/copier.h`). -pub type OakRenderProjectCopier = oakrender::ffi::OakRenderProjectCopier; - -/// Mirror of `OakColorProcessor` (`include/render/color.h`). -pub type OakColorProcessor = oakrender::ffi::OakColorProcessor; - -/// Mirror of `oakrender_ticket_finished_fn` (`include/render/ticket.h`). -/// -/// Two arguments — `(ticket, userdata)` — mirroring the header verbatim and -/// matching the oakrender implementation (`src/render/rust/src/ffi.rs`). -/// The ticket is a borrowed copy of the submitter's handle (the submitter -/// keeps ownership and releases it); cancelled tickets fire with a NULL -/// result observed through `oakrender_ticket_get_frame`. -pub type OakRenderTicketFinishedFn = - unsafe extern "C" fn(ticket: OakRenderTicket, userdata: *mut c_void); - -/// Mirror of `oakrender_video_ticket_params` (`include/render/ticket.h`). -pub type OakRenderVideoTicketParams = oakrender::ffi::OakVideoTicketParams; - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_cancelatom_init() -> OakCancelAtom { - unsafe { oakrender::ffi::cancelatom::oakrender_cancelatom_init() } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_cancelatom_free(atom: *mut OakCancelAtom) { - unsafe { oakrender::ffi::cancelatom::oakrender_cancelatom_free(atom) } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_cancelatom_cancel(atom: OakCancelAtom) -> c_int { - unsafe { oakrender::ffi::cancelatom::oakrender_cancelatom_cancel(atom) } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_cancelatom_is_cancelled(atom: OakCancelAtom, cancelled: *mut c_int) -> c_int { - unsafe { oakrender::ffi::cancelatom::oakrender_cancelatom_is_cancelled(atom, cancelled) } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_cancelatom_heard_cancel(atom: OakCancelAtom, heard: *mut c_int) -> c_int { - unsafe { oakrender::ffi::cancelatom::oakrender_cancelatom_heard_cancel(atom, heard) } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_ticket_render_frame( - params: *const OakRenderVideoTicketParams, - cb: Option, - userdata: *mut c_void, -) -> OakRenderTicket { - unsafe { oakrender::ffi::ticket::oakrender_ticket_render_frame(params, cb, userdata) } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_ticket_render_audio( - output_node: OakNodeNode, - in_num: i64, - in_den: i64, - out_num: i64, - out_den: i64, - params: *const OakAudioParams, - mode: c_int, - cb: Option, - userdata: *mut c_void, -) -> OakRenderTicket { - unsafe { - oakrender::ffi::ticket::oakrender_ticket_render_audio( - output_node, - in_num, - in_den, - out_num, - out_den, - params, - mode, - cb, - userdata, - std::ptr::null(), - 0, - ) - } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_ticket_is_finished(ticket: OakRenderTicket) -> c_int { - unsafe { oakrender::ffi::ticket::oakrender_ticket_is_finished(ticket) } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_ticket_wait(ticket: OakRenderTicket) -> c_int { - unsafe { oakrender::ffi::ticket::oakrender_ticket_wait(ticket) } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_ticket_cancel(ticket: OakRenderTicket) -> c_int { - unsafe { oakrender::ffi::ticket::oakrender_ticket_cancel(ticket) } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_ticket_get_type(ticket: OakRenderTicket) -> c_int { - unsafe { oakrender::ffi::ticket::oakrender_ticket_get_type(ticket) } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_ticket_get_frame(ticket: OakRenderTicket, out: *mut OakFrame) -> c_int { - unsafe { oakrender::ffi::ticket::oakrender_ticket_get_frame(ticket, out) } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_ticket_get_time( - ticket: OakRenderTicket, - out_num: *mut i64, - out_den: *mut i64, -) -> c_int { - unsafe { oakrender::ffi::ticket::oakrender_ticket_get_time(ticket, out_num, out_den) } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_ticket_get_range( - ticket: OakRenderTicket, - in_num: *mut i64, - in_den: *mut i64, - out_num: *mut i64, - out_den: *mut i64, -) -> c_int { - unsafe { - oakrender::ffi::ticket::oakrender_ticket_get_range(ticket, in_num, in_den, out_num, out_den) - } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_ticket_get_samples( - ticket: OakRenderTicket, - out: *mut *mut std::ffi::c_void, -) -> c_int { - unsafe { oakrender::ffi::ticket::oakrender_ticket_get_samples(ticket, out) } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_ticket_free(ticket: *mut OakRenderTicket) { - unsafe { oakrender::ffi::ticket::oakrender_ticket_free(ticket) } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_project_copier_create() -> OakRenderProjectCopier { - unsafe { oakrender::ffi::copier::oakrender_project_copier_create() } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_project_copier_free(copier: *mut OakRenderProjectCopier) { - unsafe { oakrender::ffi::copier::oakrender_project_copier_free(copier) } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_project_copier_set_project( - copier: OakRenderProjectCopier, - project: OakNodeProject, -) -> c_int { - unsafe { oakrender::ffi::copier::oakrender_project_copier_set_project(copier, project) } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_project_copier_get_copy( - copier: OakRenderProjectCopier, - original: OakNodeNode, -) -> OakNodeNode { - unsafe { oakrender::ffi::copier::oakrender_project_copier_get_copy(copier, original) } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_project_copier_get_copied_project( - copier: OakRenderProjectCopier, -) -> OakNodeProject { - unsafe { oakrender::ffi::copier::oakrender_project_copier_get_copied_project(copier) } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_color_processor_create( - src_space: *const c_char, - dst_transform: *const c_char, - direction: c_int, -) -> OakColorProcessor { - unsafe { - oakrender::ffi::color::oakrender_color_processor_create(src_space, dst_transform, direction) - } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_color_processor_free(processor: *mut OakColorProcessor) { - unsafe { oakrender::ffi::color::oakrender_color_processor_free(processor) } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_color_processor_is_valid(processor: OakColorProcessor) -> c_int { - unsafe { oakrender::ffi::color::oakrender_color_processor_is_valid(processor) } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_codec_frame_free(frame: *mut crate::bridge::codec::OakFrame) { - unsafe { oakrender::ffi::renderer::oakrender_codec_frame_free(frame) } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_manager_set_aggressive_gc(enabled: c_int) -> c_int { - unsafe { oakrender::ffi::ticket::oakrender_manager_set_aggressive_gc(enabled) } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_cache_get_invalidated_ranges( - cache: OakRenderCache, - in_num: i64, - in_den: i64, - out_num: i64, - out_den: i64, - flat: *mut i64, - flat_size: c_int, -) -> c_int { - unsafe { - oakrender::ffi::cache::oakrender_cache_get_invalidated_ranges( - cache, in_num, in_den, out_num, out_den, flat, flat_size, - ) - } -} - -/// Direct call into the `oakrender` crate (single-lib unification). -pub fn oakrender_cache_free(cache: *mut OakRenderCache) { - unsafe { oakrender::ffi::cache::oakrender_cache_free(cache) } -} diff --git a/crates/oaktask/src/bridge/timeline.rs b/crates/oaktask/src/bridge/timeline.rs deleted file mode 100644 index 1481a5cac..000000000 --- a/crates/oaktask/src/bridge/timeline.rs +++ /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 . - -// 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 . - -//! oaktimeline C ABI imports. The OTIO load task builds tracks through the -//! timeline edit factories (`include/timeline/edit.h`) instead of the raw -//! track-list primitives, matching `src/task/src/project/loadotio/loadotio.cpp`. -//! Signatures mirror the header verbatim. - -use crate::bridge::node::OakNodeTrackList; -use crate::bridge::undo::OakUndoCommand; - -/// Direct call into the `oaktimeline` crate (single-lib unification). -pub fn oaktimeline_add_track_command(list: OakNodeTrackList) -> OakUndoCommand { - unsafe { oaktimeline::ffi::edit::oaktimeline_add_track_command(list) } -} diff --git a/crates/oaktask/src/bridge/undo.rs b/crates/oaktask/src/bridge/undo.rs deleted file mode 100644 index 94f729b6e..000000000 --- a/crates/oaktask/src/bridge/undo.rs +++ /dev/null @@ -1,93 +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 . - -// 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 . - -//! oakundo C ABI imports. Import/load tasks build undo commands through the -//! C ABI vtable (`oakundo_command_init` with Rust closures as userdata) — -//! no C++ UndoCommand subclassing exists on this side. Mirrors -//! `include/undo/undocommand.h` verbatim. - -use std::ffi::{c_int, c_void}; - -use crate::handle::CHandle; - -/// `OakUndoCommand` (`include/undo/undocommand.h`). -pub type OakUndoCommand = CHandle; - -/// Mirror of `OakUndoCommandVtable` (`include/undo/undocommand.h`). -pub type OakUndoCommandVtable = oakundo::undocommand::OakUndoCommandVtable; - -/// Direct call into the `oakundo` crate (single-lib unification). -pub fn oakundo_command_init( - vtable: *const OakUndoCommandVtable, - userdata: *mut c_void, -) -> OakUndoCommand { - unsafe { oakundo::ffi::command::oakundo_command_init(vtable, userdata) } -} - -/// Direct call into the `oakundo` crate (single-lib unification). -pub fn oakundo_command_init_multi() -> OakUndoCommand { - unsafe { oakundo::ffi::command::oakundo_command_init_multi() } -} - -/// Direct call into the `oakundo` crate (single-lib unification). -pub fn oakundo_command_multi_add_child(multi: OakUndoCommand, child: OakUndoCommand) -> c_int { - unsafe { oakundo::ffi::command::oakundo_command_multi_add_child(multi, child) } -} - -/// Direct call into the `oakundo` crate (single-lib unification). -pub fn oakundo_command_multi_child_count(multi: OakUndoCommand, out_count: *mut c_int) -> c_int { - unsafe { oakundo::ffi::command::oakundo_command_multi_child_count(multi, out_count) } -} - -/// Direct call into the `oakundo` crate (single-lib unification). -pub fn oakundo_command_multi_child( - multi: OakUndoCommand, - index: c_int, - out_child: *mut OakUndoCommand, -) -> c_int { - unsafe { oakundo::ffi::command::oakundo_command_multi_child(multi, index, out_child) } -} - -/// Direct call into the `oakundo` crate (single-lib unification). -pub fn oakundo_command_redo_now(command: OakUndoCommand) -> c_int { - unsafe { oakundo::ffi::command::oakundo_command_redo_now(command) } -} - -/// Direct call into the `oakundo` crate (single-lib unification). -pub fn oakundo_command_undo_now(command: OakUndoCommand) -> c_int { - unsafe { oakundo::ffi::command::oakundo_command_undo_now(command) } -} - -/// Direct call into the `oakundo` crate (single-lib unification). -pub fn oakundo_command_free(command: *mut OakUndoCommand) { - unsafe { oakundo::ffi::command::oakundo_command_free(command) } -} diff --git a/crates/oaktask/src/codecbridge.rs b/crates/oaktask/src/codecbridge.rs index 32bb6622c..01d376c84 100644 --- a/crates/oaktask/src/codecbridge.rs +++ b/crates/oaktask/src/codecbridge.rs @@ -17,23 +17,26 @@ //! Codec task submitter registration, mirroring //! `src/task/src/codecbridge.h`. //! -//! Wires oakcodec's task-submit callback (`oakcodec_set_task_submit_cb`) so -//! conform/proxy requests from the codec side land back in the task module. -//! This is a two-module coupling; the actual callback signatures live in -//! `crate::bridge::codec` mirroring `include/codec/task.h` verbatim. +//! Wires oakcodec's task-submit callback (`oakcodec::task::set_task_submit_cb`) +//! so conform/proxy requests from the codec side land back in the task +//! module. The registration is a direct Rust closure (single-lib +//! unification: the old extern-C submit callback is gone). //! //! CPP-PARITY: src/task/src/codecbridge.h -use std::ffi::{c_char, c_int, c_void}; +use oakcodec::error::Error as CodecError; +use oakcodec::proxymanager::ProxyManager; +use oakcodec::task::{ + set_task_submit_cb, task_submit_is_registered, TaskKind, TaskRequest, TaskSubmitFn, +}; -use crate::bridge; use crate::conform::ConformTask; use crate::error::{Error, Result}; use crate::proxy::{ProxyParams, ProxyTask}; use crate::task::Task; -/// Convert an oakcodec proxy-params POD into the Rust [`ProxyParams`]. -fn proxy_params_from_codec(params: &bridge::codec::OakCodecProxyParams) -> ProxyParams { +/// Convert oakcodec proxy params into the Rust [`ProxyParams`]. +fn proxy_params_from_codec(params: &oakcodec::proxymanager::ProxyParams) -> ProxyParams { ProxyParams { width: params.width, height: params.height, @@ -41,72 +44,48 @@ fn proxy_params_from_codec(params: &bridge::codec::OakCodecProxyParams) -> Proxy version: params.version, crf: params.crf, include_audio: params.include_audio != 0, - extension: unsafe { cstr_buf_to_string(¶ms.extension) }, - preset: unsafe { cstr_buf_to_string(¶ms.preset) }, + extension: cstr_buf_to_string(¶ms.extension), + preset: cstr_buf_to_string(¶ms.preset), } } -/// Read a NUL-terminated char array into a String (lossy). -unsafe fn cstr_buf_to_string(buf: &[u8]) -> String { +/// Read a NUL-terminated byte array into a String (lossy). +fn cstr_buf_to_string(buf: &[u8]) -> String { let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len()); - let bytes = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) }; - String::from_utf8_lossy(bytes).into_owned() + String::from_utf8_lossy(&buf[..len]).into_owned() } /// The installed submit callback, mirroring `submit_codec_task` in /// codecbridge.cpp (interim contract: submission is synchronous). -/// -/// # Safety -/// `req` must be a valid `OakCodecTaskRequest` or null. -unsafe extern "C" fn submit_codec_task( - req: *const bridge::codec::OakCodecTaskRequest, - _userdata: *mut c_void, -) -> c_int { - if req.is_null() { - return bridge::codec::OAKCODEC_E_INVALID; - } - let request = unsafe { &*req }; - +fn submit_codec_task( + req: &TaskRequest, + _userdata: *mut std::ffi::c_void, +) -> oakcodec::error::Result<()> { // Build the concrete task on an outer base so the behavior can be // driven by `start()`; both share the cancellation atom. - match request.kind { - bridge::codec::OAKCODEC_TASK_CONFORM => { - let task = ConformTask::new(request); + match req.kind { + TaskKind::Conform => { + let task = ConformTask::new(req); let atom = task.base.get_cancel_atom(); let title = task.base.title().to_string(); - let mut outer = Task::new(&title, atom); + let mut outer = Task::new(&title, Some(atom)); outer.set_behavior(Box::new(task)); - match outer.start() { - Ok(()) => 0, - Err(_) => bridge::codec::OAKCODEC_E_FAILED, - } + outer + .start() + .map_err(|_| CodecError::Failed("conform task failed".to_string())) } - bridge::codec::OAKCODEC_TASK_PROXY => { - let mut codec_params = bridge::codec::OakCodecProxyParams { - width: 0, - height: 0, - divider: 0, - version: 0, - crf: 0, - include_audio: 0, - extension: [0; 32], - preset: [0; 32], - }; - unsafe { - bridge::codec::oakcodec_proxy_params_default(&mut codec_params); - } + TaskKind::Proxy => { + let codec_params = ProxyManager::proxy_params_from_config(); let params = proxy_params_from_codec(&codec_params); - let task = ProxyTask::new(request, params); + let task = ProxyTask::new(req, params); let atom = task.base.get_cancel_atom(); let title = task.base.title().to_string(); - let mut outer = Task::new(&title, atom); + let mut outer = Task::new(&title, Some(atom)); outer.set_behavior(Box::new(task)); - match outer.start() { - Ok(()) => 0, - Err(_) => bridge::codec::OAKCODEC_E_FAILED, - } + outer + .start() + .map_err(|_| CodecError::Failed("proxy task failed".to_string())) } - _ => bridge::codec::OAKCODEC_E_INVALID, } } @@ -116,21 +95,18 @@ pub fn register_codec_task_submitter() -> Result<()> { if is_codec_task_submitter_registered() { return Err(Error::State); } - unsafe { - bridge::codec::oakcodec_set_task_submit_cb(Some(submit_codec_task), std::ptr::null_mut()); - } + let cb: &'static TaskSubmitFn = Box::leak(Box::new(submit_codec_task)); + set_task_submit_cb(Some(cb), std::ptr::null_mut()); Ok(()) } /// Remove the task-module submitter from oakcodec. Idempotent. pub fn unregister_codec_task_submitter() -> Result<()> { - unsafe { - bridge::codec::oakcodec_set_task_submit_cb(None, std::ptr::null_mut()); - } + set_task_submit_cb(None, std::ptr::null_mut()); Ok(()) } /// Whether a submitter is currently installed (queries oakcodec). pub fn is_codec_task_submitter_registered() -> bool { - unsafe { bridge::codec::oakcodec_task_submit_is_registered() != 0 } + task_submit_is_registered() } diff --git a/crates/oaktask/src/conform.rs b/crates/oaktask/src/conform.rs index 0905ab6cb..62a169686 100644 --- a/crates/oaktask/src/conform.rs +++ b/crates/oaktask/src/conform.rs @@ -17,14 +17,17 @@ //! `ConformTask`, mirroring `src/task/src/conform/conform.h`. //! //! Transcodes an audio stream to a PCM cache file (oakcodec kind -//! `OAKCODEC_TASK_CONFORM`) and reports progress as it decodes. +//! `OAKCODEC_TASK_CONFORM`) and reports progress as it decodes. The +//! decoder is reached through the direct Rust API +//! (`oakcodec::decoder::{receive_list_of_all_decoders, Decoder}`) — +//! single-lib unification; the old oakcodec decoder C ABI is gone. //! //! CPP-PARITY: src/task/src/conform/conform.h -use crate::bridge; +use oakcodec::decoder::CodecStream; +use oakcodec::task::TaskRequest; + use crate::error::{Error, Result}; -use crate::ffi::taskhandle::cstr; -use crate::handle::CHandle; use crate::task::{Task, TaskBehavior}; /// A conform task: copies/channels of one audio stream into per-channel @@ -53,14 +56,15 @@ pub struct ConformTask { impl ConformTask { /// Build a conform task from an oakcodec request, mirroring the C++ /// constructor. - pub fn new(request: &bridge::codec::OakCodecTaskRequest) -> ConformTask { - let input = unsafe { crate::ffi::taskhandle::cstr_to_string(request.input_filename) }; - let output = unsafe { crate::ffi::taskhandle::cstr_to_string(request.output_filename) }; - let title = format!("Conforming Audio {}:{}", input, request.stream_index); + pub fn new(request: &TaskRequest) -> ConformTask { + let title = format!( + "Conforming Audio {}:{}", + request.input_filename, request.stream_index + ); ConformTask { - base: Task::new(&title, CHandle::null()), - input_filename: input, - output_filename: output, + base: Task::new(&title, None), + input_filename: request.input_filename.to_string(), + output_filename: request.output_filename.to_string(), stream_index: request.stream_index, sample_rate: request.sample_rate, channel_layout: request.channel_layout, @@ -106,7 +110,7 @@ impl ConformTask { } impl TaskBehavior for ConformTask { - /// Run the conform via the oakcodec decoder (`bridge::codec`), emitting + /// Run the conform via the direct oakcodec decoder API, emitting /// progress as audio is decoded and written to the working files, then /// rename working → final. fn run(&mut self, task: &mut Task) -> Result<()> { @@ -119,72 +123,63 @@ impl TaskBehavior for ConformTask { self.final_names = final_names; self.working_names = working_names; - let decoder = unsafe { bridge::codec::oakcodec_decoder_init() }; - if decoder.ctx.is_null() { + let atom = task.get_cancel_atom(); + + // Pick the first registered decoder that can probe the file (the + // C++ picks the decoder the footage was created with; probing in + // registry order is the direct-Rust equivalent). + let decoder = oakcodec::decoder::receive_list_of_all_decoders() + .into_iter() + .find(|d| d.probe(&self.input_filename, Some(&atom)).is_some()); + let Some(decoder) = decoder else { task.set_error("Failed to create decoder"); return Err(Error::Failed("Failed to create decoder".to_string())); - } - let mut decoder = decoder; + }; let result = (|| { - let open_result = unsafe { - bridge::codec::oakcodec_decoder_open( - decoder, - cstr(&self.input_filename), - self.stream_index, - ) - }; - if open_result != 0 { - let err = decoder_error(decoder); - task.set_error(&format!("Failed to open decoder for audio conform: {err}")); + let stream = + CodecStream::with_block(self.input_filename.clone(), self.stream_index, None); + if let Err(e) = decoder.open(&stream) { + task.set_error(&format!("Failed to open decoder for audio conform: {e}")); return false; } - let working_ptrs: Vec<*const std::ffi::c_char> = - self.working_names.iter().map(|n| cstr(n)).collect(); - let conform_result = unsafe { - bridge::codec::oakcodec_decoder_conform_audio( - decoder, - working_ptrs.as_ptr(), - working_ptrs.len() as i32, - self.sample_rate, - self.channel_layout, - self.sample_format, - task.get_cancel_atom(), - ) - }; - unsafe { - bridge::codec::oakcodec_decoder_close(decoder); - } + let conform_result = decoder.conform_audio( + &self.working_names, + self.sample_rate, + self.channel_layout, + self.sample_format, + Some(&atom), + ); + let _ = decoder.close(); - if conform_result == 0 { - // Rename each working file into place; a failure aborts the - // rest (mirroring the C++ loop). - for i in 0..self.working_names.len() { - if std::fs::rename(&self.working_names[i], &self.final_names[i]).is_err() { - task.set_error("Failed to move conformed audio into place"); - return false; + match conform_result { + Ok(()) => { + // Rename each working file into place; a failure aborts + // the rest (mirroring the C++ loop). + for i in 0..self.working_names.len() { + if std::fs::rename(&self.working_names[i], &self.final_names[i]).is_err() { + task.set_error("Failed to move conformed audio into place"); + return false; + } } + true } - true - } else { - // Clean up any partial working files. - for name in &self.working_names { - let _ = std::fs::remove_file(name); + Err(_) => { + // Clean up any partial working files. + for name in &self.working_names { + let _ = std::fs::remove_file(name); + } + if atom.is_cancelled() { + task.set_error("Audio conform was cancelled"); + } else { + task.set_error("Audio conform failed"); + } + false } - if conform_result == bridge::codec::OAKCODEC_E_CANCELLED { - task.set_error("Audio conform was cancelled"); - } else { - task.set_error("Audio conform failed"); - } - false } })(); - unsafe { - bridge::codec::oakcodec_decoder_free(&mut decoder); - } - if result { Ok(()) } else { @@ -192,19 +187,3 @@ impl TaskBehavior for ConformTask { } } } - -/// Two-stage read of the decoder's last error string. -fn decoder_error(decoder: CHandle) -> String { - let mut buf = [0i8; 256]; - let needed = unsafe { - bridge::codec::oakcodec_decoder_last_error(decoder, buf.as_mut_ptr(), buf.len() as i32) - }; - if needed <= 0 { - return String::new(); - } - let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len()); - unsafe { - String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)) - .into_owned() - } -} diff --git a/crates/oaktask/src/customcache.rs b/crates/oaktask/src/customcache.rs index 654ce275d..260078114 100644 --- a/crates/oaktask/src/customcache.rs +++ b/crates/oaktask/src/customcache.rs @@ -24,8 +24,9 @@ use std::sync::{Arc, Condvar, Mutex}; +use oakcommon::cancelatom::CancelAtom; + use crate::error::{Error, Result}; -use crate::handle::CHandle; use crate::task::{Task, TaskBehavior}; /// Shared state between the parked task thread, the owning cache @@ -41,19 +42,14 @@ pub struct CustomCacheState { /// Callback invoked when the task is cancelled (not by `finish()`). cancelled_callback: Mutex>>, /// Copy of the task's cancellation atom. - atom: CHandle, + atom: Arc, } impl CustomCacheState { /// Mark the cache fill as complete and wake the parked task. pub fn finish(&self) { *self.cancelled_through_finish.lock().unwrap() = true; - if !self.atom.is_null() { - let atom = self.atom; - unsafe { - crate::bridge::render::oakrender_cancelatom_cancel(atom); - } - } + self.atom.cancel(); self.wait.notify_one(); } @@ -71,12 +67,7 @@ impl CustomCacheState { /// The full C++ `cancel()`: cancel the atom and run the cancel-event /// hook. Used by the owning cache to abort a fill. pub fn cancel(&self) { - if !self.atom.is_null() { - let atom = self.atom; - unsafe { - crate::bridge::render::oakrender_cancelatom_cancel(atom); - } - } + self.atom.cancel(); self.cancel_event(); } @@ -104,7 +95,7 @@ impl CustomCacheTask { pub fn new(sequence_name: &str) -> CustomCacheTask { let base = Task::new( &format!("Caching custom range for \"{sequence_name}\""), - CHandle::null(), + None, ); let state = Arc::new(CustomCacheState { cancelled_through_finish: Mutex::new(false), @@ -145,7 +136,7 @@ impl TaskBehavior for CustomCacheTask { /// `Err(Error::Cancelled)` if cancelled while waiting. fn run(&mut self, task: &mut Task) -> Result<()> { // Install the cancel hook on the task this behavior actually runs - // under (the outer task driven by the manager / C ABI). The + // under (the outer task driven by the manager). The // constructor-installed hook on the inner `base` only fires when the // base itself is cancelled; without this the outer `cancel()` would // set the shared atom but never wake the parked thread. diff --git a/crates/oaktask/src/export.rs b/crates/oaktask/src/export.rs index 5ab6243b1..672c716f7 100644 --- a/crates/oaktask/src/export.rs +++ b/crates/oaktask/src/export.rs @@ -16,10 +16,19 @@ //! `ExportTask`, mirroring `src/task/src/export/export.h`. //! -//! Renders the viewer output through [`crate::render::RenderTask`] and writes -//! it to a file via the oakcodec encoder (`bridge::codec`), mapping each -//! rendered [`Rational`] frame to an `OakFrame` and each rendered -//! [`TimeRange`] to audio samples. +//! Renders the viewer output through [`crate::render::RenderTask`] and +//! writes it to a file via the direct oakcodec encoder API +//! (`oakcodec::encoder::{create_from_params, Encoder}` — single-lib +//! unification; the old encoder C ABI is gone), mapping each rendered +//! frame to an `Encoder::write_video` call and each rendered audio buffer +//! to `Encoder::write_audio`. +//! +//! The viewer and color-manager arguments of the deleted C ABI path are +//! replaced by a single [`crate::nodeops::NodeRef`] viewer (the color +//! manager no longer crosses into the render tickets — the direct ticket +//! arena performs no color management). Rendered frames arrive as +//! `oakrender::texture::Texture` values and are converted to +//! `oakcodec::frame::Frame` values for the encoder. //! //! **Simplifications over the C++**: no temporary-file rename dance (the //! encoder writes straight to the requested filename), no sidecar subtitle @@ -29,45 +38,36 @@ //! //! CPP-PARITY: src/task/src/export/export.h -use crate::bridge; -use crate::bridge::codec::OakCodecEncodingParams; +use std::sync::Arc; + +use oakcodec::encoder::{create_from_params, Encoder}; +use oakcodec::encodingparams::EncodingParams as CodecEncodingParams; +use oakcommon::videoparams::VideoParams as CommonVideoParams; +use oakrender::texture::Texture; + use crate::error::{Error, Result}; -use crate::ffi::taskhandle::cstr; -use crate::handle::CHandle; +use crate::nodeops::{self, NodeRef}; use crate::render::{ForceParams, RenderTask, RenderTaskBehavior}; use crate::task::{Task, TaskBehavior}; use oakcore_rs::{Rational, TimeRange}; -/// An export task. Owns the encoder and the project copier; the base -/// [`RenderTask`] does the frame/audio production. +/// An export task. Owns the encoder; the base [`RenderTask`] does the +/// frame/audio production. pub struct ExportTask { /// The render base (itself a task). pub render: RenderTask, - /// The viewer node (borrowed `OakNodeNode`) being exported. - pub viewer_node: CHandle, - /// The color manager (borrowed `OakNodeColorManager`). - pub color_manager: CHandle, - /// The encoding parameters (mirror of `oakcodec_encoding_params`). + /// The node being exported (footage or sequence). + pub viewer_node: NodeRef, + /// The encoding parameters (mirror of the codec-side params). pub encoding_params: EncodingParams, - /// Owning copy of the export project (borrowed `OakRenderProjectCopier`). - /// Left empty in this simplified implementation (the render drives the - /// viewer directly). - pub copier: CHandle, - /// The opened encoder. - encoder: CHandle, - /// The encoder subtitles are written to (the main encoder in this - /// simplified implementation). - subtitle_encoder: CHandle, - /// Frame counter for progress reporting. - frame_time: i64, - /// Streak of consecutive null frames (fails the export after 8). - null_frame_streak: i32, + /// The opened encoder (direct oakcodec trait object). + encoder: Option>, } -/// Mirror of `oakcodec_encoding_params` in `include/codec/encoder.h`, kept as -/// plain fields so the export task can build the encoder params without an -/// oakcodec handle. Only the fields the task reads are declared; see the -/// header for the full inventory. +/// Mirror of the codec encoding params, kept as plain fields so the export +/// task can be configured without a codec-side handle. Only the fields the +/// task reads are declared; see `oakcodec::encodingparams::EncodingParams` +/// for the full inventory. pub struct EncodingParams { /// Output filename. pub filename: String, @@ -91,6 +91,10 @@ pub struct EncodingParams { pub audio_enabled: bool, /// Audio codec id. pub audio_codec: i32, + /// Audio sample rate (Hz) the encoder opens with. + pub audio_sample_rate: i32, + /// ffmpeg-style channel layout mask the encoder opens with. + pub audio_channel_layout: u64, /// Whether subtitles are exported. pub subtitles_enabled: bool, /// Export length numerator (seconds rational). @@ -100,76 +104,35 @@ pub struct EncodingParams { } impl ExportTask { - /// Build a new export task from the viewer, color manager, and encoding - /// params. - pub fn new(viewer: CHandle, color_manager: CHandle, params: EncodingParams) -> ExportTask { - let label = node_label(viewer); + /// Build a new export task from the viewer node and the encoding + /// params. The old `viewer: CHandle` / `color_manager: CHandle` + /// signature is replaced by the domain viewer [`NodeRef`] (single-lib + /// unification; the color manager is dropped with the C ABI — see the + /// module docs). + pub fn new(viewer: NodeRef, params: EncodingParams) -> ExportTask { + let label = nodeops::node_label(&viewer.0, viewer.1); let title = format!("Exporting \"{label}\""); - let base = Task::new(&title, CHandle::null()); - let render = RenderTask::new( - base, - CHandle::null(), - CHandle::null(), - viewer, - ForceParams::default(), - None, - ); + let base = Task::new(&title, None); + // The render base needs the viewer's frame rate to step one frame + // per video frame (the export range is in sequence/footage time); + // without it the frame loop falls back to a 1/1 timebase and + // exports one frame per second (observed in the facade's + // `it_export` run). + let video_params = nodeops::sequence_video_params(&viewer.0, viewer.1, 0) + .or_else(|| nodeops::footage_video_params(&viewer.0, viewer.1, 0)); + let render = RenderTask::new(base, video_params, viewer.clone(), ForceParams::default(), None); ExportTask { render, viewer_node: viewer, - color_manager, encoding_params: params, - copier: CHandle::null(), - encoder: CHandle::null(), - subtitle_encoder: CHandle::null(), - frame_time: 0, - null_frame_streak: 0, + encoder: None, } } - /// Build the C-ABI encoding-params POD for the encoder from the Rust + /// Build the codec `EncodingParams` POD for the encoder from the Rust /// mirror (zeroed fields = disabled/defaults). - fn build_codec_params(&self) -> OakCodecEncodingParams { - let mut params = OakCodecEncodingParams { - filename: [0; 1024], - format: 0, - video_enabled: 0, - video_codec: 0, - video_width: 0, - video_height: 0, - video_time_base_num: 0, - video_time_base_den: 0, - video_pixel_format: 0, - video_interlacing: 0, - video_pixel_aspect_num: 0, - video_pixel_aspect_den: 0, - video_bit_rate: 0, - video_min_bit_rate: 0, - video_max_bit_rate: 0, - video_buffer_size: 0, - video_threads: 0, - video_pix_fmt: [0; 64], - video_is_image_sequence: 0, - video_scaling_method: 0, - audio_enabled: 0, - audio_codec: 0, - audio_sample_rate: 0, - audio_channel_layout: 0, - audio_sample_format: 0, - audio_bit_rate: 0, - subtitles_enabled: 0, - subtitles_codec: 0, - subtitles_are_sidecar: 0, - subtitles_sidecar_format: 0, - color_transform_output: [0; 256], - export_length_num: 0, - export_length_den: 0, - has_custom_range: 0, - custom_range_in_num: 0, - custom_range_in_den: 0, - custom_range_out_num: 0, - custom_range_out_den: 0, - }; + fn build_codec_params(&self) -> CodecEncodingParams { + let mut params = CodecEncodingParams::default(); let bytes = self.encoding_params.filename.as_bytes(); let n = bytes.len().min(1023); params.filename[..n].copy_from_slice(&bytes[..n]); @@ -180,9 +143,12 @@ impl ExportTask { params.video_height = self.encoding_params.video_height; params.video_time_base_num = self.encoding_params.video_time_base_num; params.video_time_base_den = self.encoding_params.video_time_base_den; - params.video_pixel_format = self.encoding_params.video_pixel_format; + params.video_pixel_format = + crate::nodeops::pixel_format_from_code(self.encoding_params.video_pixel_format); params.audio_enabled = self.encoding_params.audio_enabled as i32; params.audio_codec = self.encoding_params.audio_codec; + params.audio_sample_rate = self.encoding_params.audio_sample_rate; + params.audio_channel_layout = self.encoding_params.audio_channel_layout; params.subtitles_enabled = self.encoding_params.subtitles_enabled as i32; params.export_length_num = self.encoding_params.export_length_num; params.export_length_den = self.encoding_params.export_length_den; @@ -190,17 +156,63 @@ impl ExportTask { } /// Resolve the export range: the custom range when set, otherwise the - /// whole sequence length. + /// whole viewer length (direct `oaknode` domain query; the deleted + /// `oaknode_sequence_get_length` stub is gone). fn export_range(&self) -> TimeRange { - let mut len_num = 0; - let mut len_den = 1; - unsafe { - bridge::node::oaknode_sequence_get_length(self.viewer_node, &mut len_num, &mut len_den); + let length = nodeops::node_length(&self.viewer_node.0, self.viewer_node.1); + TimeRange::new(Rational::new(0, 1), length) + } + + /// Copy a rendered `oakrender` CPU texture into an `oakcodec` frame + /// with the matching video params (row-wise copy — line sizes may + /// differ between the render and codec frame layouts). + fn to_codec_frame(texture: &Texture) -> Result { + let Texture::Cpu(frame) = texture else { + return Err(Error::Failed( + "Render produced a GPU texture; the CPU encoder path cannot consume it" + .to_string(), + )); + }; + let params = CommonVideoParams::new_basic( + frame.width, + frame.height, + oakcommon::ocioutils::PixelFormat::from_code(frame.format as i32), + 4, + 1, + 1, + 0, + 1, + ); + let mut out = oakcodec::frame::Frame::with_params(params); + out.set_timestamp(frame.timestamp); + out.allocate().map_err(|e| { + Error::Failed(format!("Failed to allocate encoder frame: {e:?}")) + })?; + let dst_stride = out.linesize_bytes() as usize; + let Some(dst) = out.data_mut() else { + return Err(Error::Failed( + "Encoder frame allocation produced no buffer".to_string(), + )); + }; + let src = frame.data.as_slice(); + let src_stride = frame.linesize_bytes(); + let row_bytes = std::cmp::min(src_stride, dst_stride) + .min(src.len()) + .min(dst.len()); + if src_stride == dst_stride && src.len() == dst.len() { + dst.copy_from_slice(src); + } else { + for y in 0..frame.height as usize { + let src_start = y * src_stride; + let dst_start = y * dst_stride; + if src_start + row_bytes > src.len() || dst_start + row_bytes > dst.len() { + break; + } + dst[dst_start..dst_start + row_bytes] + .copy_from_slice(&src[src_start..src_start + row_bytes]); + } } - TimeRange::new( - Rational::new(0, 1), - Rational::new(len_num as i64, len_den as i64), - ) + Ok(out) } } @@ -210,19 +222,18 @@ impl TaskBehavior for ExportTask { self.render.base.set_cancel_atom(task.get_cancel_atom()); let codec_params = self.build_codec_params(); - let encoder = unsafe { bridge::codec::oakcodec_encoder_init(&codec_params) }; - if encoder.ctx.is_null() { + let Some(encoder) = create_from_params(&codec_params) else { task.set_error("Failed to create encoder"); return Err(Error::Failed("Failed to create encoder".to_string())); - } - self.encoder = encoder; + }; + self.encoder = Some(encoder.clone()); - if unsafe { bridge::codec::oakcodec_encoder_open(encoder) } != 0 { - let err = encoder_error(encoder); + if let Err(e) = encoder.open() { + let err = encoder.get_error(); task.set_error(&format!("Failed to open file: {err}")); + let _ = e; return Err(Error::Failed("Failed to open file".to_string())); } - self.subtitle_encoder = encoder; let export_range = self.export_range(); @@ -232,14 +243,14 @@ impl TaskBehavior for ExportTask { if self.encoding_params.video_enabled { force.force_width = self.encoding_params.video_width; force.force_height = self.encoding_params.video_height; - force.force_format = - unsafe { bridge::codec::oakcodec_encoder_get_desired_pixel_format(encoder) }; + force.force_format = encoder + .desired_pixel_format() + .map(|f| f as i32) + .unwrap_or(-1); force.force_channel_count = 4; // RGBA } self.render.force_params = force; self.render.set_render_inputs( - self.color_manager, - CHandle::null(), 0, // RenderMode::k_online self.encoding_params.audio_enabled, export_range, @@ -256,10 +267,11 @@ impl TaskBehavior for ExportTask { result?; // Flush the encoder and surface any trailing error. - unsafe { - bridge::codec::oakcodec_encoder_flush(self.encoder); + if let Err(_) = encoder.flush() { + // Fall through to the error read below (the flush error is + // surfaced through `get_error()` like the C ABI did). } - let err = encoder_error(self.encoder); + let err = encoder.get_error(); if !err.is_empty() { task.set_error(&err); return Err(Error::Failed("Encoder flush failed".to_string())); @@ -270,94 +282,55 @@ impl TaskBehavior for ExportTask { } impl RenderTaskBehavior for ExportTask { - fn frame_downloaded(&mut self, task: &mut Task, frame: CHandle) -> Result<()> { - if frame.ctx.is_null() { - self.null_frame_streak += 1; - if self.null_frame_streak >= 8 { - task.set_error(&format!( - "Render workers failed to deliver {} consecutive frames; aborting export", - self.null_frame_streak - )); - return Err(Error::Failed("Too many null frames".to_string())); - } + fn frame_downloaded(&mut self, task: &mut Task, frame: &Texture) -> Result<()> { + let Some(encoder) = &self.encoder else { return Ok(()); - } - self.null_frame_streak = 0; - - if unsafe { bridge::codec::oakcodec_encoder_write_video(self.encoder, frame) } != 0 { - let err = encoder_error(self.encoder); + }; + let codec_frame = Self::to_codec_frame(frame)?; + if let Err(_) = encoder.write_video(&codec_frame) { + let err = encoder.get_error(); task.set_error(&err); - return Err(Error::Failed("Failed to write video frame".to_string())); + return Err(Error::Failed("Failed to write frame".to_string())); } - - self.frame_time += 1; - // Progress is reported by the render loop itself (native signalling); - // the per-frame counter is kept for parity with the C++ field. + // Progress is reported by the render loop itself (native signalling). Ok(()) } - fn audio_downloaded(&mut self, task: &mut Task, buffer: CHandle) -> Result<()> { - // Simplified: audio writing is not wired through the sample buffer - // in this rewrite (the encoder receives the interleaved samples from - // the codec side directly). The hook exists for parity with the C++ - // virtual and always succeeds. - let _ = (task, buffer); + fn audio_downloaded( + &mut self, + task: &mut Task, + samples: &oakrender::ticket::AudioSamples, + ) -> Result<()> { + let Some(encoder) = &self.encoder else { + return Ok(()); + }; + let frame_count = if samples.channel_count > 0 { + samples.samples.len() / samples.channel_count as usize + } else { + 0 + }; + if let Err(_) = encoder.write_audio(&samples.samples, frame_count as i32) { + let err = encoder.get_error(); + task.set_error(&err); + return Err(Error::Failed("Failed to write audio".to_string())); + } Ok(()) } fn encode_subtitle(&mut self, task: &mut Task, text: &str) -> Result<()> { - if !self.encoding_params.subtitles_enabled || self.subtitle_encoder.ctx.is_null() { + if !self.encoding_params.subtitles_enabled { return Ok(()); } + let Some(encoder) = &self.encoder else { + return Ok(()); + }; // The simplified path does not carry the subtitle block's in/out // times; write with 0.0/0.0 (the encoder default interval). - if unsafe { - bridge::codec::oakcodec_encoder_write_subtitle( - self.subtitle_encoder, - cstr(text), - 0.0, - 0.0, - ) - } != 0 - { - let err = encoder_error(self.subtitle_encoder); + if let Err(_) = encoder.write_subtitle(text, 0.0, 0.0) { + let err = encoder.get_error(); task.set_error(&err); return Err(Error::Failed("Failed to write subtitle".to_string())); } Ok(()) } } - -/// Two-stage read of the viewer node's label. -fn node_label(node: CHandle) -> String { - let needed = unsafe { bridge::node::oaknode_node_get_label(node, std::ptr::null_mut(), 0) }; - if needed <= 0 { - return String::new(); - } - let mut buf = vec![0i8; needed as usize]; - unsafe { - bridge::node::oaknode_node_get_label(node, buf.as_mut_ptr(), needed); - } - buf_to_string(&buf) -} - -/// Read a NUL-terminated char buffer into a String (lossy). -fn buf_to_string(buf: &[i8]) -> String { - let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len()); - unsafe { - String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)) - .into_owned() - } -} - -/// Two-stage read of the encoder's last error string. -fn encoder_error(encoder: CHandle) -> String { - let mut buf = [0i8; 512]; - let needed = unsafe { - bridge::codec::oakcodec_encoder_last_error(encoder, buf.as_mut_ptr(), buf.len() as i32) - }; - if needed <= 0 { - return String::new(); - } - buf_to_string(&buf) -} diff --git a/crates/oaktask/src/ffi.rs b/crates/oaktask/src/ffi.rs deleted file mode 100644 index f51a06773..000000000 --- a/crates/oaktask/src/ffi.rs +++ /dev/null @@ -1,28 +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 . - -//! oaktask C ABI export surface. These `#[no_mangle] extern "C"` symbols are -//! the Rust counterpart of the C++ task facade declared in -//! `include/task/*.h`; each module mirrors one header. The public headers are -//! authoritative for signatures; the inventory comments below each module -//! enumerate the full symbol set. Every export passes through -//! [`crate::handle::guard*`] / explicit null checks so panics never cross the -//! FFI boundary. - -pub mod manager; -pub mod project; -pub mod task; -pub(crate) mod taskhandle; diff --git a/crates/oaktask/src/ffi/manager.rs b/crates/oaktask/src/ffi/manager.rs deleted file mode 100644 index 48f1b34a4..000000000 --- a/crates/oaktask/src/ffi/manager.rs +++ /dev/null @@ -1,94 +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 . - -//! `oaktask_manager_*` export symbols mirroring `include/task/manager.h`. -//! -//! Full symbol inventory (header-authoritative): -//! - `oaktask_manager_init(void) -> int` -//! - `oaktask_manager_shutdown(void) -> void` -//! - `oaktask_register_codec_submitter(void) -> int` -//! - `oaktask_manager_count(void) -> int` -//! - `oaktask_manager_at(int i) -> OakTaskTask` -//! - `oaktask_manager_delete_finished(void) -> void` - -use std::ffi::c_int; - -use crate::error::{OAKTASK_E_STATE, OAKTASK_OK}; -use crate::ffi::taskhandle::wrap_borrowed; -use crate::handle::CHandle; -use crate::manager::TaskManager; - -/// `oaktask_manager_init` (`include/task/manager.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_manager_init() -> c_int { - if let Err(_) = TaskManager::init() { - return OAKTASK_E_STATE; - } - // Register the codec task submitter (mirrors the C++ init sequence). - let _ = crate::codecbridge::register_codec_task_submitter(); - TaskManager::with_manager_mut(|m| m.set_codec_submitter_registered(true)); - OAKTASK_OK -} - -/// `oaktask_manager_shutdown` (`include/task/manager.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_manager_shutdown() { - TaskManager::with_manager_mut(|m| m.set_codec_submitter_registered(false)); - let _ = crate::codecbridge::unregister_codec_task_submitter(); - TaskManager::shutdown(); -} - -/// `oaktask_register_codec_submitter` (`include/task/manager.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_register_codec_submitter() -> c_int { - match crate::codecbridge::register_codec_task_submitter() { - Ok(()) => OAKTASK_OK, - Err(_) => OAKTASK_OK, - } -} - -/// `oaktask_manager_count` (`include/task/manager.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_manager_count() -> c_int { - match TaskManager::with_manager(|m| m.get_task_count()) { - Some(count) => count as c_int, - None => OAKTASK_E_STATE, - } -} - -/// `oaktask_manager_at` (`include/task/manager.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_manager_at(i: c_int) -> CHandle { - // No manager (or out of range) -> empty handle, mirroring the C++ - // `OakTaskTask{}` return. - let ptr = match TaskManager::with_manager(|m| m.task_ptr_at(i as usize)) { - Some(Ok(ptr)) => ptr, - _ => return CHandle::null(), - }; - wrap_borrowed(ptr) -} - -/// `oaktask_manager_delete_finished` (`include/task/manager.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_manager_delete_finished() { - // Collect the finished entries and join their threads without holding - // the manager lock (a worker's finish callback may call back into the - // manager). - let entries = TaskManager::with_manager_mut(|m| m.drain_finished()).unwrap_or_default(); - for (_task, handle) in entries { - let _ = handle.join(); - } -} diff --git a/crates/oaktask/src/ffi/project.rs b/crates/oaktask/src/ffi/project.rs deleted file mode 100644 index dcb5b8b42..000000000 --- a/crates/oaktask/src/ffi/project.rs +++ /dev/null @@ -1,407 +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 . - -//! `oaktask_create_*` export symbols mirroring `include/task/project.h`. -//! -//! Full symbol inventory (header-authoritative): -//! - `oaktask_create_project_load(const char *filename) -> OakTaskTask` -//! - `oaktask_load_take_project(OakTaskTask t) -> OakNodeProject` -//! - `oaktask_create_project_save(OakNodeProject, const char *filename_or_NULL, int use_compression) -> OakTaskTask` -//! - `oaktask_create_project_import(OakNodeFolder, OakNodeProject, const char *const *urls, int url_count) -> OakTaskTask` -//! - `oaktask_import_take_command(OakTaskTask t) -> OakUndoCommand` -//! - `oaktask_import_footage_count(OakTaskTask t) -> int` -//! - `oaktask_import_footage_at(OakTaskTask t, int index) -> OakNodeFootage` -//! - `oaktask_import_invalid_count(OakTaskTask t) -> int` -//! - `oaktask_import_invalid_at(OakTaskTask t, int index, char *buf, int buf_size) -> int` (two-stage) -//! - `oaktask_create_project_load_otio(const char *filename) -> OakTaskTask` -//! - `oaktask_load_otio_take_project(OakTaskTask t) -> OakNodeProject` -//! - `oaktask_create_project_save_otio(OakNodeProject, const char *filename) -> OakTaskTask` -//! - `oaktask_load_otio_set_confirm_cb(oaktask_otio_import_confirm_fn fn, void *userdata) -> void` -//! - `oaktask_create_precache(OakNodeFootage, int index, OakNodeSequence) -> OakTaskTask` -//! - `oaktask_create_export(OakNodeNode viewer, OakNodeColorManager, const oakcodec_encoding_params *) -> OakTaskTask` -//! - `oaktask_import_set_image_sequence_confirm_cb(oaktask_image_sequence_confirm_fn fn, void *userdata) -> void` - -use std::ffi::{c_char, c_int, c_void}; -use std::sync::Mutex; - -use crate::bridge::codec::OakCodecEncodingParams; -use crate::error::{OAKTASK_E_INVALID, OAKTASK_E_NOT_FOUND}; -use crate::export::{EncodingParams, ExportTask}; -use crate::ffi::taskhandle::{ - copy_string, cstr, cstr_to_string, get_task, wrap_owned, wrap_owned_with_impl, TaskImpl, -}; -use crate::handle::CHandle; -use crate::precache::PreCacheTask; -use crate::project::import::{ - import_file_count, import_title, ImageSequenceConfirmFn, ProjectImportTask, -}; -use crate::project::load::{ProjectLoadBaseTask, ProjectLoadTask}; -use crate::project::loadotio::{set_import_confirm_callback, LoadOTIOTask}; -use crate::project::save::{project_filename, ProjectSaveTask}; -use crate::project::saveotio::SaveOTIOTask; -use crate::task::Task; - -/// `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; - -/// Global image-sequence confirm callback (facade concern; `null` clears -/// it). Snapshot into each import task at creation. -static IMAGE_SEQUENCE_CONFIRM: Mutex> = Mutex::new(None); - -/// `oaktask_create_project_load` (`include/task/project.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_create_project_load(filename: *const c_char) -> CHandle { - if filename.is_null() { - return CHandle::null(); - } - let filename = unsafe { cstr_to_string(filename) }; - let title = format!("Loading '{filename}'"); - - let inner = Task::new(&title, CHandle::null()); - let base = ProjectLoadBaseTask::new(inner, filename.clone()); - let load_task = ProjectLoadTask { base }; - let atom = load_task.base.base.get_cancel_atom(); - let mut boxed = Box::new(load_task); - let base_ptr = &mut boxed.base as *mut ProjectLoadBaseTask; - let mut outer = Task::new(&title, atom); - outer.set_behavior(boxed); - wrap_owned_with_impl(Box::new(outer), TaskImpl::LoadBase(base_ptr)) -} - -/// `oaktask_load_take_project` (`include/task/project.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_load_take_project(t: CHandle) -> CHandle { - load_take_project(&t) -} - -/// Shared implementation of `load_take_project`/`load_otio_take_project`. -fn load_take_project(t: &CHandle) -> CHandle { - let Some(h) = get_task(t) else { - return CHandle::null(); - }; - let TaskImpl::LoadBase(base_ptr) = h.impl_kind else { - return CHandle::null(); - }; - match unsafe { (&mut *base_ptr).take_project() } { - Ok(project) => project, - Err(_) => CHandle::null(), - } -} - -/// `oaktask_create_project_save` (`include/task/project.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_create_project_save( - project: CHandle, - filename_or_null: *const c_char, - use_compression: c_int, -) -> CHandle { - if project.ctx.is_null() { - return CHandle::null(); - } - let title = format!("Saving '{}'", project_filename(project)); - let mut save_task = ProjectSaveTask { - base: Task::new(&title, CHandle::null()), - project, - override_filename: None, - use_compression: use_compression != 0, - }; - if !filename_or_null.is_null() { - save_task.set_override_filename(&unsafe { cstr_to_string(filename_or_null) }); - } - let atom = save_task.base.get_cancel_atom(); - let boxed = Box::new(save_task); - let mut outer = Task::new(&title, atom); - outer.set_behavior(boxed); - wrap_owned(Box::new(outer)) -} - -/// `oaktask_create_project_import` (`include/task/project.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_create_project_import( - folder: CHandle, - project: CHandle, - urls: *const *const c_char, - url_count: c_int, -) -> CHandle { - if folder.ctx.is_null() || project.ctx.is_null() { - return CHandle::null(); - } - if url_count < 0 || (urls.is_null() && url_count > 0) { - return CHandle::null(); - } - let mut filenames = Vec::with_capacity(url_count as usize); - for i in 0..url_count { - let url = unsafe { *urls.add(i as usize) }; - if url.is_null() { - return CHandle::null(); - } - filenames.push(unsafe { cstr_to_string(url) }); - } - - let title = import_title(&filenames); - let file_count = import_file_count(&filenames); - let import_task = ProjectImportTask::new( - Task::new(&title, CHandle::null()), - folder, - project, - filenames, - IMAGE_SEQUENCE_CONFIRM.lock().unwrap().take(), - file_count, - ); - let atom = import_task.base.get_cancel_atom(); - let mut boxed = Box::new(import_task); - let import_ptr = &mut *boxed as *mut ProjectImportTask; - let mut outer = Task::new(&title, atom); - outer.set_behavior(boxed); - wrap_owned_with_impl(Box::new(outer), TaskImpl::Import(import_ptr)) -} - -/// Borrow the `ProjectImportTask` behind the handle (the C++ `import_impl` -/// dynamic_cast equivalent). -fn import_impl(t: &CHandle) -> Option<*mut ProjectImportTask> { - let h = get_task(t)?; - match h.impl_kind { - TaskImpl::Import(p) => Some(p), - _ => None, - } -} - -/// `oaktask_import_take_command` (`include/task/project.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_import_take_command(t: CHandle) -> CHandle { - let Some(p) = import_impl(&t) else { - return CHandle::null(); - }; - match unsafe { (&mut *p).take_command() } { - Ok(command) => command, - Err(_) => CHandle::null(), - } -} - -/// `oaktask_import_footage_count` (`include/task/project.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_import_footage_count(t: CHandle) -> c_int { - let Some(p) = import_impl(&t) else { - return OAKTASK_E_INVALID; - }; - unsafe { (*p).get_file_count() as c_int } -} - -/// `oaktask_import_footage_at` (`include/task/project.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_import_footage_at(t: CHandle, index: c_int) -> CHandle { - let Some(p) = import_impl(&t) else { - return CHandle::null(); - }; - if index < 0 { - return CHandle::null(); - } - match unsafe { (*p).get_imported_footage(index as usize) } { - Ok(footage) => footage, - Err(_) => CHandle::null(), - } -} - -/// `oaktask_import_invalid_count` (`include/task/project.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_import_invalid_count(t: CHandle) -> c_int { - let Some(p) = import_impl(&t) else { - return OAKTASK_E_INVALID; - }; - unsafe { (*p).get_invalid_file_count() as c_int } -} - -/// `oaktask_import_invalid_at` (`include/task/project.h`, two-stage string getter). -#[no_mangle] -pub unsafe extern "C" fn oaktask_import_invalid_at( - t: CHandle, - index: c_int, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - let Some(p) = import_impl(&t) else { - return OAKTASK_E_INVALID; - }; - let task = unsafe { &*p }; - let count = task.get_invalid_file_count(); - if index < 0 || index as usize >= count { - return OAKTASK_E_NOT_FOUND; - } - copy_string(task.invalid_file_at(index as usize), buf, buf_size) -} - -/// `oaktask_create_project_load_otio` (`include/task/project.h`). The -/// interchange format is inferred from the filename extension (`.otio` / -/// `.fcpxml`, case-insensitive; see `crate::project::format`), so the C -/// ABI needs no format parameter. -#[no_mangle] -pub unsafe extern "C" fn oaktask_create_project_load_otio(filename: *const c_char) -> CHandle { - if filename.is_null() { - return CHandle::null(); - } - let filename = unsafe { cstr_to_string(filename) }; - let title = format!("Loading '{filename}'"); - - let inner = Task::new(&title, CHandle::null()); - let base = ProjectLoadBaseTask::new(inner, filename.clone()); - let otio_task = LoadOTIOTask::new(base); - let atom = otio_task.base.base.get_cancel_atom(); - let mut boxed = Box::new(otio_task); - let base_ptr = &mut boxed.base as *mut ProjectLoadBaseTask; - let mut outer = Task::new(&title, atom); - outer.set_behavior(boxed); - wrap_owned_with_impl(Box::new(outer), TaskImpl::LoadBase(base_ptr)) -} - -/// `oaktask_load_otio_take_project` (`include/task/project.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_load_otio_take_project(t: CHandle) -> CHandle { - load_take_project(&t) -} - -/// `oaktask_create_project_save_otio` (`include/task/project.h`). The -/// interchange format is inferred from the filename extension (`.otio` / -/// `.fcpxml`, case-insensitive; see `crate::project::format`), so the C -/// ABI needs no format parameter. -#[no_mangle] -pub unsafe extern "C" fn oaktask_create_project_save_otio( - project: CHandle, - filename: *const c_char, -) -> CHandle { - if project.ctx.is_null() || filename.is_null() { - return CHandle::null(); - } - let filename = unsafe { cstr_to_string(filename) }; - let title = format!("Saving '{filename}'"); - let task = SaveOTIOTask { - base: Task::new(&title, CHandle::null()), - project, - filename, - }; - let atom = task.base.get_cancel_atom(); - let boxed = Box::new(task); - let mut outer = Task::new(&title, atom); - outer.set_behavior(boxed); - wrap_owned(Box::new(outer)) -} - -/// `oaktask_load_otio_set_confirm_cb` (`include/task/project.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_load_otio_set_confirm_cb( - cb: Option, - userdata: *mut c_void, -) { - let Some(cb) = cb else { - set_import_confirm_callback(None); - return; - }; - let userdata = userdata as usize; - set_import_confirm_callback(Some(Box::new(move |sequence_names: &[String]| { - let ptrs: Vec<*const c_char> = sequence_names.iter().map(|n| cstr(n)).collect(); - unsafe { cb(ptrs.as_ptr(), ptrs.len() as c_int, userdata as *mut c_void) != 0 } - }))); -} - -/// `oaktask_create_precache` (`include/task/project.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_create_precache( - footage: CHandle, - index: c_int, - sequence: CHandle, -) -> CHandle { - if footage.ctx.is_null() || sequence.ctx.is_null() { - return CHandle::null(); - } - let precache = PreCacheTask::new(footage, index, sequence); - let title = precache.render.base.title().to_string(); - let atom = precache.render.base.get_cancel_atom(); - let boxed = Box::new(precache); - let mut outer = Task::new(&title, atom); - outer.set_behavior(boxed); - wrap_owned(Box::new(outer)) -} - -/// `oaktask_create_export` (`include/task/project.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_create_export( - viewer: CHandle, - color_manager: CHandle, - params: *const OakCodecEncodingParams, -) -> CHandle { - if viewer.ctx.is_null() || params.is_null() { - return CHandle::null(); - } - let encoding = unsafe { convert_encoding_params(&*params) }; - let export = ExportTask::new(viewer, color_manager, encoding); - let title = export.render.base.title().to_string(); - let atom = export.render.base.get_cancel_atom(); - let boxed = Box::new(export); - let mut outer = Task::new(&title, atom); - outer.set_behavior(boxed); - wrap_owned(Box::new(outer)) -} - -/// `oaktask_import_set_image_sequence_confirm_cb` (`include/task/project.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_import_set_image_sequence_confirm_cb( - cb: Option, - userdata: *mut c_void, -) { - let mut guard = IMAGE_SEQUENCE_CONFIRM.lock().unwrap(); - let Some(cb) = cb else { - *guard = None; - return; - }; - let userdata = userdata as usize; - *guard = Some(Box::new(move |filename: &str, _other: &str| unsafe { - cb(cstr(filename), userdata as *mut c_void) != 0 - })); -} - -/// Copy the C-ABI encoding params into the Rust mirror (subset read by the -/// export task). -unsafe fn convert_encoding_params(p: &OakCodecEncodingParams) -> EncodingParams { - EncodingParams { - filename: unsafe { c_char_array_to_string(&p.filename) }, - format: p.format, - video_enabled: p.video_enabled != 0, - video_codec: p.video_codec, - video_width: p.video_width, - video_height: p.video_height, - video_time_base_num: p.video_time_base_num, - video_time_base_den: p.video_time_base_den, - video_pixel_format: p.video_pixel_format, - audio_enabled: p.audio_enabled != 0, - audio_codec: p.audio_codec, - subtitles_enabled: p.subtitles_enabled != 0, - export_length_num: p.export_length_num, - export_length_den: p.export_length_den, - } -} - -/// Read a NUL-terminated char array into a String (lossy). -unsafe fn c_char_array_to_string(buf: &[u8]) -> String { - let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len()); - String::from_utf8_lossy(&buf[..len]).into_owned() -} diff --git a/crates/oaktask/src/ffi/task.rs b/crates/oaktask/src/ffi/task.rs deleted file mode 100644 index 6a6f47af6..000000000 --- a/crates/oaktask/src/ffi/task.rs +++ /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 . - -//! `oaktask_task_*` export symbols mirroring `include/task/task.h`. -//! -//! Full symbol inventory (header-authoritative): -//! - `oaktask_task_free(OakTaskTask *t) -> void` -//! - `oaktask_task_start_sync(OakTaskTask t) -> int` (1 = succeeded) -//! - `oaktask_task_start(OakTaskTask t) -> int` -//! - `oaktask_task_cancel(OakTaskTask t) -> int` -//! - `oaktask_task_wait(OakTaskTask t) -> int` -//! - `oaktask_task_is_finished(OakTaskTask t) -> int` -//! - `oaktask_task_succeeded(OakTaskTask t) -> int` -//! - `oaktask_task_title(OakTaskTask t, char *buf, int buf_size) -> int` (two-stage) -//! - `oaktask_task_error(OakTaskTask t, char *buf, int buf_size) -> int` (two-stage) -//! - `oaktask_task_subscribe(OakTaskTask t, oaktask_event_fn fn, void *userdata) -> int64_t` -//! - `oaktask_debug_alive_count(void) -> int` - -use std::ffi::{c_char, c_int, c_void}; -use std::sync::Arc; - -use crate::error::OAKTASK_E_INVALID; -use crate::ffi::taskhandle::{copy_string, get_task, get_task_mut, userdata_usize}; -use crate::handle::CHandle; -use crate::manager::TaskManager; -use crate::task::{SubscriberState, TaskEvent}; - -/// `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); - -/// Event ids (`include/task/task.h`). -const OAKTASK_EVENT_STARTED: c_int = 0; -const OAKTASK_EVENT_PROGRESS: c_int = 1; -const OAKTASK_EVENT_FINISHED: c_int = 2; - -/// `oaktask_task_free` (`include/task/task.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_task_free(t: *mut CHandle) { - if t.is_null() { - return; - } - let handle = unsafe { &mut *t }; - if handle.ctx.is_null() { - return; - } - // Release one reference, then clear ctx (NULL / empty-handle no-op). - if let Some(release) = handle.release { - unsafe { - release(handle.ctx); - } - } - handle.ctx = std::ptr::null_mut(); -} - -/// `oaktask_task_start_sync` (`include/task/task.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_task_start_sync(t: CHandle) -> c_int { - let h = match get_task(&t) { - Some(h) => h, - None => return OAKTASK_E_INVALID, - }; - let result = unsafe { (&mut *h.task).start() }; - if result.is_ok() { - 1 - } else { - 0 - } -} - -/// `oaktask_task_start` (`include/task/task.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_task_start(t: CHandle) -> c_int { - let h = match get_task(&t) { - Some(h) => h, - None => return OAKTASK_E_INVALID, - }; - if !TaskManager::instance().is_some() { - return crate::error::OAKTASK_E_STATE; - } - if h.running_on_manager { - return crate::error::OAKTASK_E_STATE; - } - let h = match get_task_mut(&t) { - Some(h) => h, - None => return OAKTASK_E_INVALID, - }; - if h.owner.is_none() { - return crate::error::OAKTASK_E_STATE; - } - - // Transfer ownership to the manager; releasing the handle afterwards - // only frees the box. The manager was checked above and cannot vanish - // between the check and the transfer in a single-threaded caller. - let boxed = h.owner.take().unwrap(); - let task_ptr = h.task; - TaskManager::with_manager_mut(|m| m.add_task(boxed)); - // The box moved into the manager; the raw pointer is unchanged. - let h = get_task_mut(&t).unwrap(); - h.running_on_manager = true; - h.owned = false; - let _ = task_ptr; - crate::error::OAKTASK_OK -} - -/// `oaktask_task_cancel` (`include/task/task.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_task_cancel(t: CHandle) -> c_int { - let h = match get_task(&t) { - Some(h) => h, - None => return OAKTASK_E_INVALID, - }; - unsafe { - (&mut *h.task).cancel(); - } - crate::error::OAKTASK_OK -} - -/// `oaktask_task_wait` (`include/task/task.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_task_wait(t: CHandle) -> c_int { - let h = match get_task(&t) { - Some(h) => h, - None => return OAKTASK_E_INVALID, - }; - if h.running_on_manager { - let task_ptr = h.task; - // C++ semantics: wait cancels the task and blocks until it - // finishes. The join happens without holding the manager lock. - TaskManager::with_manager_mut(|m| m.cancel_task_by_ptr(task_ptr)); - let handle = TaskManager::with_manager_mut(|m| m.take_thread_by_ptr(task_ptr)).flatten(); - if let Some(handle) = handle { - let _ = handle.join(); - } - } - crate::error::OAKTASK_OK -} - -/// `oaktask_task_is_finished` (`include/task/task.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_task_is_finished(t: CHandle) -> c_int { - let h = match get_task(&t) { - Some(h) => h, - None => return OAKTASK_E_INVALID, - }; - if unsafe { (*h.task).is_finished() } { - 1 - } else { - 0 - } -} - -/// `oaktask_task_succeeded` (`include/task/task.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_task_succeeded(t: CHandle) -> c_int { - let h = match get_task(&t) { - Some(h) => h, - None => return OAKTASK_E_INVALID, - }; - if unsafe { (*h.task).succeeded() } { - 1 - } else { - 0 - } -} - -/// `oaktask_task_title` (`include/task/task.h`, two-stage string getter). -#[no_mangle] -pub unsafe extern "C" fn oaktask_task_title( - t: CHandle, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - let h = match get_task(&t) { - Some(h) => h, - None => return OAKTASK_E_INVALID, - }; - copy_string(unsafe { (*h.task).title() }, buf, buf_size) -} - -/// `oaktask_task_error` (`include/task/task.h`, two-stage string getter). -#[no_mangle] -pub unsafe extern "C" fn oaktask_task_error( - t: CHandle, - buf: *mut c_char, - buf_size: c_int, -) -> c_int { - let h = match get_task(&t) { - Some(h) => h, - None => return OAKTASK_E_INVALID, - }; - let value = unsafe { (*h.task).error() }.unwrap_or("Unknown error"); - copy_string(value, buf, buf_size) -} - -/// `oaktask_task_subscribe` (`include/task/task.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_task_subscribe( - t: CHandle, - cb: Option, - userdata: *mut c_void, -) -> i64 { - let h = match get_task(&t) { - Some(h) => h, - None => return OAKTASK_E_INVALID as i64, - }; - let Some(cb) = cb else { - return OAKTASK_E_INVALID as i64; - }; - - let state = Arc::new(SubscriberState::default()); - let ud = userdata_usize(userdata); - - // One subscription replaces the previous one (the C++ `listeners_` - // vector holds at most the factory listener in practice; here the - // factory listener is gone — the task tracks finished internally). - unsafe { - (*h.task).set_subscriber(state.clone()); - (*h.task).set_event_listener(Box::new(move |ev: TaskEvent| { - let (event_id, value) = match ev { - TaskEvent::Started => ( - OAKTASK_EVENT_STARTED, - state.start_ms.load(std::sync::atomic::Ordering::SeqCst) as f64, - ), - TaskEvent::Progress(p) => (OAKTASK_EVENT_PROGRESS, p), - TaskEvent::Finished => ( - OAKTASK_EVENT_FINISHED, - state - .finished_value - .load(std::sync::atomic::Ordering::SeqCst) as f64, - ), - }; - cb(event_id, value, ud as *mut c_void); - })); - } - 0 -} - -/// `oaktask_debug_alive_count` (`include/task/task.h`). -#[no_mangle] -pub unsafe extern "C" fn oaktask_debug_alive_count() -> c_int { - crate::ffi::taskhandle::alive_count() -} diff --git a/crates/oaktask/src/ffi/taskhandle.rs b/crates/oaktask/src/ffi/taskhandle.rs deleted file mode 100644 index cd6dc615d..000000000 --- a/crates/oaktask/src/ffi/taskhandle.rs +++ /dev/null @@ -1,191 +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 boxed control block behind every `OakTaskTask` handle, mirroring -//! `src/task/c_api/taskhandle.h` (`oaktask_capi::TaskHandle`). -//! -//! `owns_task` is the owns role: true for factory-created tasks (the last -//! release drops the task) and false once the task runs on the manager -//! (`oaktask_task_start()` flips it; the manager drops the task) or for -//! borrowed wrappers (`oaktask_manager_at()`): releasing those only -//! destroys the box. -//! -//! The C++ `TaskHandle` carries its own `finished`/`succeeded` atomics that -//! the factory listener updates. The Rust [`crate::task::Task`] tracks these -//! internally (race-free through its `done` condvar), so the box reads them -//! from the task instead — the box only needs the downcast pointers for the -//! `take_*` accessors (Rust has no `dynamic_cast`). -//! -//! CPP-PARITY: src/task/c_api/taskhandle.h - -use std::ffi::c_void; -use std::sync::atomic::{AtomicI32, Ordering}; - -use crate::handle::{make_owned, CHandle}; -use crate::project::import::ProjectImportTask; -use crate::project::load::ProjectLoadBaseTask; -use crate::task::Task; - -/// Concrete-task downcast, standing in for the C++ `dynamic_cast` in the -/// `take_*` accessors. -pub enum TaskImpl { - /// Not a project load/import task. - None, - /// A `ProjectLoadBaseTask` (native or OTIO loader). - LoadBase(*mut ProjectLoadBaseTask), - /// A `ProjectImportTask`. - Import(*mut ProjectImportTask), -} - -/// Control block behind an `OakTaskTask` (see the module docs). -pub struct TaskHandleBox { - /// The task the handle wraps (the outer task owning the behavior). - pub task: *mut Task, - /// Whether releasing the last reference drops the task. - pub owned: bool, - /// Whether the task has been handed to the manager. - pub running_on_manager: bool, - /// The owned task (factory case); taken by `oaktask_task_start`. - pub owner: Option>, - /// Downcast target for the `take_*` accessors. - pub impl_kind: TaskImpl, -} - -impl Drop for TaskHandleBox { - fn drop(&mut self) { - // Mirrors `task_release`: the box is destroyed at zero refs, which - // always decrements the alive count. - ALIVE.fetch_sub(1, Ordering::SeqCst); - } -} - -// Safety: the box is only ever accessed through the C ABI entry points -// (never concurrently with its own mutation); the raw pointers inside are -// stable while the box lives. -unsafe impl Send for TaskHandleBox {} - -/// Alive-count for leak assertions in tests (mirrors -/// `oaktask_capi::alive()`). -static ALIVE: AtomicI32 = AtomicI32::new(0); - -/// Current alive handle count. -pub fn alive_count() -> i32 { - ALIVE.load(Ordering::SeqCst) -} - -/// Wrap an owned task (reference count 1). The handle owns the task and -/// drops it on the final release. -/// -/// CPP-PARITY: src/task/c_api/taskhandle.h (wrap) -pub fn wrap_owned(task: Box) -> CHandle { - wrap_owned_with_impl(task, TaskImpl::None) -} - -/// Wrap an owned task with a downcast target for the `take_*` accessors. -pub fn wrap_owned_with_impl(task: Box, impl_kind: TaskImpl) -> CHandle { - let ptr = Box::into_raw(task); - ALIVE.fetch_add(1, Ordering::SeqCst); - // Safety: `ptr` is a live, uniquely owned `Box`. - let handle = make_owned(TaskHandleBox { - task: ptr, - owned: true, - running_on_manager: false, - owner: unsafe { Some(Box::from_raw(ptr)) }, - impl_kind, - }); - handle -} - -/// Wrap a manager-owned (borrowed) task. Releasing the handle does NOT drop -/// the task. -/// -/// CPP-PARITY: src/task/c_api/taskhandle.h (wrap_borrowed) -pub fn wrap_borrowed(ptr: *mut Task) -> CHandle { - if ptr.is_null() { - return CHandle::null(); - } - ALIVE.fetch_add(1, Ordering::SeqCst); - make_owned(TaskHandleBox { - task: ptr, - owned: false, - running_on_manager: true, - owner: None, - impl_kind: TaskImpl::None, - }) -} - -/// Typed view of a task handle box; `None` for empty handles. -/// -/// # Safety -/// The handle must have been created by this module. -pub fn get_task(h: &CHandle) -> Option<&TaskHandleBox> { - unsafe { crate::handle::get::(h) } -} - -/// Typed mutable view of a task handle box; `None` for empty handles. -/// -/// # Safety -/// The handle must have been created by this module and not be shared with -/// a concurrent mutable access. -pub fn get_task_mut(h: &CHandle) -> Option<&mut TaskHandleBox> { - unsafe { crate::handle::get_mut::(h) } -} - -/// Two-stage string copy matching `oaktask_capi::copy_string`: returns the -/// needed size (`len + 1`); writes only when the buffer is non-null and -/// large enough. -/// -/// CPP-PARITY: src/task/c_api/taskhandle.h (copy_string) -pub fn copy_string(value: &str, buf: *mut std::ffi::c_char, buf_size: i32) -> i32 { - let needed = value.len() as i32 + 1; - if !buf.is_null() && buf_size >= needed { - unsafe { - std::ptr::copy_nonoverlapping( - value.as_ptr() as *const std::ffi::c_char, - buf, - value.len(), - ); - *buf.add(value.len()) = 0; - } - } - needed -} - -/// Read a NUL-terminated C string into a Rust `String` (lossy); empty when -/// the pointer is null. -/// -/// # Safety -/// `ptr` must be a valid NUL-terminated C string or null. -pub unsafe fn cstr_to_string(ptr: *const std::ffi::c_char) -> String { - if ptr.is_null() { - return String::new(); - } - unsafe { std::ffi::CStr::from_ptr(ptr) } - .to_string_lossy() - .into_owned() -} - -/// Build a NUL-terminated C string for the duration of the call (leaked). -pub fn cstr(s: &str) -> *const std::ffi::c_char { - let mut bytes = s.as_bytes().to_vec(); - bytes.push(0); - bytes.leak().as_ptr() as *const std::ffi::c_char -} - -/// `userdata` payload converted to a `Send`-friendly form for closures. -pub fn userdata_usize(userdata: *mut c_void) -> usize { - userdata as usize -} diff --git a/crates/oaktask/src/handle.rs b/crates/oaktask/src/handle.rs index eb95fe88b..e04f5f661 100644 --- a/crates/oaktask/src/handle.rs +++ b/crates/oaktask/src/handle.rs @@ -40,10 +40,6 @@ pub struct RefBox { /// source-compatible. `Send + Sync` come from the shared type. pub use oakcore_rs::handle::CHandle; -unsafe extern "C" fn noop_addref(_ctx: *mut std::ffi::c_void) {} - -unsafe extern "C" fn noop_release(_ctx: *mut std::ffi::c_void) {} - unsafe extern "C" fn owned_addref(ctx: *mut std::ffi::c_void) { if !ctx.is_null() { // CPP-PARITY: src/task/c_api/taskhandle.h (task_addref) diff --git a/crates/oaktask/src/lib.rs b/crates/oaktask/src/lib.rs index 16ec2c97b..9417ce1f8 100644 --- a/crates/oaktask/src/lib.rs +++ b/crates/oaktask/src/lib.rs @@ -16,29 +16,24 @@ //! # oaktask — the task execution module (Rust) //! -//! Reimplements the C++ task module behind its frozen C ABI -//! (`include/task/*.h`). See README.md for the architectural mapping -//! (inheritance → one module per class + trait objects, cancellation via -//! the oakrender cancelatom C ABI, events as mutex-guarded callbacks). -//! -//! ## FFI discipline -//! -//! Identical to the oaknode/oakplugin crates: every export goes through -//! [`handle::guard*`], handles are opaque refcounted boxes, shared state -//! behind `Mutex`. +//! Reimplements the C++ task module (`src/task/src`) as direct Rust +//! (single-lib unification): inheritance → one module per class + trait +//! objects, cancellation via `oakcommon::cancelatom::CancelAtom`, events +//! as boxed callbacks. All node-graph, timeline and render work goes +//! through the direct `oaknode` / `oakrender` Rust APIs +//! ([`nodeops`] holds the graph operations the tasks share). #![deny(unsafe_op_in_unsafe_fn)] #![warn(missing_docs)] -pub mod bridge; pub mod codecbridge; pub mod conform; pub mod customcache; pub mod error; pub mod export; -pub mod ffi; pub mod handle; pub mod manager; +pub mod nodeops; pub mod precache; pub mod project; pub mod proxy; diff --git a/crates/oaktask/src/nodeops.rs b/crates/oaktask/src/nodeops.rs new file mode 100644 index 000000000..4b33fd227 --- /dev/null +++ b/crates/oaktask/src/nodeops.rs @@ -0,0 +1,1196 @@ +// 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 . + +//! Direct oaknode domain operations backing the task module (single-lib +//! unification). +//! +//! The old `src/stubs.rs` stood in for the deleted oaknode / oaktimeline / +//! oakrender C ABIs with `CHandle`-typed failure paths. Every one of those +//! stubs is replaced here by real operations over `oaknode` domain types: +//! a project is `Arc>` and every node is +//! an `oaknode::id::NodeId` in that project's `oaknode::graph::Graph`. +//! +//! Undo commands follow the oaktimeline `undocommon` pattern: a command +//! struct is boxed into an `oakundo::undocommand::UndoCommand` vtable +//! command ([`box_command`]); `redo_now`/`undo_now` dispatch to it. + +use std::sync::{Arc, Mutex, MutexGuard}; + +use oakcommon::videoparams::VideoParams as CommonVideoParams; +use oakcore_rs::Rational; +use oaknode::block::{BlockCore, ClipBlockBehavior, GapBlockBehavior, TransitionBlockBehavior}; +use oaknode::folder::FolderBehavior; +use oaknode::footage::{FootageBehavior, StreamInfo}; +use oaknode::graph::Graph; +use oaknode::id::NodeId; +use oaknode::node::{NodeBehavior, NodeCore}; +use oaknode::project::Project; +use oaknode::sequence::SequenceBehavior; +use oaknode::track::{BlockRange, TrackBehavior, TrackListBehavior, TrackRange, TrackType}; +use oakundo::undocommand::{OakUndoCommandVtable, UndoCommand}; + +/// A project, shared like the C++ `olive::Project` smart pointer. +pub type ProjectRef = Arc>; + +/// A reference to one node: its owning project and its id there. Replaces +/// the borrowed `CHandle` node handles of the deleted C ABI. +pub type NodeRef = (ProjectRef, NodeId); + +/// `OAKNODE_TRANSITION_CROSS_DISSOLVE` (`include/node/transition.h`); +/// transition kinds other than cross-dissolve are not modelled. +pub const TRANSITION_CROSS_DISSOLVE: i32 = 0; + +/// `OAKNODE_TYPE_TRANSFORM` (`include/node/factory.h`): the transform node +/// type id used by the OTIO loader. +pub const TRANSFORM_TYPE_ID: &str = "org.olivevideoeditor.Olive.transform"; + +/// `OAKNODE_TYPE_VOLUME` (`include/node/factory.h`): the volume node type id +/// used by the OTIO loader. +pub const VOLUME_TYPE_ID: &str = "org.olivevideoeditor.Olive.volume"; + +/// `OAKNODE_TYPE_SEQUENCE` (`include/node/factory.h`): the sequence type id +/// (matches `SequenceBehavior::type_id`). +pub const SEQUENCE_TYPE_ID: &str = "org.olivevideoeditor.Olive.sequence"; + +/// Clip texture/effect input id (oaknode `clip_input::TEXTURE_INPUT`; the +/// old `OAKNODE_SEQUENCE_TEXTURE_INPUT` value). +pub const CLIP_TEXTURE_INPUT: &str = "tex_in"; + +/// Transition connection inputs (oaknode `transition_input`). +pub const TRANSITION_OUT_BLOCK_INPUT: &str = "out_block_in"; +/// Transition connection input (incoming side). +pub const TRANSITION_IN_BLOCK_INPUT: &str = "in_block_in"; + +/// Block kinds, mirroring the `OAKNODE_BLOCK_*` constants of the deleted +/// C ABI. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BlockKind { + /// Unknown block type (`OAKNODE_BLOCK_OTHER`). + Other, + /// Media-bearing clip (`OAKNODE_BLOCK_CLIP`). + Clip, + /// Empty span (`OAKNODE_BLOCK_GAP`). + Gap, + /// Transition (`OAKNODE_BLOCK_TRANSITION`). + Transition, +} + +/// Lock the project mutex (poisoned locks recover their guard — task code +/// has no invariants that survive a panic). +fn lock_project(project: &ProjectRef) -> MutexGuard<'_, Project> { + project.lock().unwrap_or_else(|e| e.into_inner()) +} + +/// The shared block core of any block-typed node, or `None` for non-block +/// nodes / stale ids. +pub fn block_core_of(graph: &Graph, block: NodeId) -> Option { + let entry = graph.get(block)?; + let any = entry.behavior.as_any()?; + if let Some(b) = any.downcast_ref::() { + return Some(b.core.clone()); + } + if let Some(b) = any.downcast_ref::() { + return Some(b.core.clone()); + } + if let Some(b) = any.downcast_ref::() { + return Some(b.core.clone()); + } + None +} + +/// Set the shared block core fields of any block-typed node (`track` +/// ownership aside); false for non-block nodes / stale ids. +fn set_block_core(graph: &mut Graph, block: NodeId, f: impl FnOnce(&mut BlockCore)) -> bool { + let Some(entry) = graph.get_mut(block) else { + return false; + }; + let Some(any) = entry.behavior.as_any_mut() else { + return false; + }; + if let Some(b) = any.downcast_mut::() { + f(&mut b.core); + return true; + } + if let Some(b) = any.downcast_mut::() { + f(&mut b.core); + return true; + } + if let Some(b) = any.downcast_mut::() { + f(&mut b.core); + return true; + } + false +} + +/// The user label of a node (C++ `Node::label`); empty for missing nodes. +pub fn node_label(project: &ProjectRef, node: NodeId) -> String { + let guard = lock_project(project); + match guard.graph.get(node) { + Some(entry) => entry.core.label.clone(), + None => String::new(), + } +} + +/// Set the user label of a node (`oaknode_node_set_label`). +pub fn set_node_label(project: &ProjectRef, node: NodeId, label: &str) { + let mut guard = lock_project(project); + if let Some(entry) = guard.graph.get_mut(node) { + entry.core.label = label.to_string(); + } +} + +/// The node's stable type id (`oaknode_node_get_id`; C++ `Node::id()`). +pub fn node_type_id(project: &ProjectRef, node: NodeId) -> String { + let guard = lock_project(project); + match guard.graph.get(node) { + Some(entry) => entry.behavior.type_id().to_string(), + None => String::new(), + } +} + +/// True when the node's behavior is the footage type. +pub fn node_is_footage(graph: &Graph, node: NodeId) -> bool { + graph + .get(node) + .and_then(|e| e.behavior.as_any()) + .and_then(|a| a.downcast_ref::()) + .is_some() +} + +/// True when the node's behavior is the sequence type. +pub fn node_is_sequence(graph: &Graph, node: NodeId) -> bool { + graph + .get(node) + .and_then(|e| e.behavior.as_any()) + .and_then(|a| a.downcast_ref::()) + .is_some() +} + +/// Set a node's position inside a context (the C++ +/// `oaknode_node_set_context_position`; `expanded` replaces the old +/// `force != 0` flag). +pub fn node_set_context_position( + project: &ProjectRef, + node: NodeId, + context: NodeId, + x: f64, + y: f64, + expanded: bool, +) { + let mut guard = lock_project(project); + if let Some(entry) = guard.graph.get_mut(node) { + entry.core.set_context_position(context, x, y, expanded); + } +} + +/// The total length of a footage or sequence node (footage: longest video +/// stream; sequence: longest track list; `0/1` otherwise) — the domain +/// replacement for `oaknode_footage_get_video_length` / +/// `oaknode_sequence_get_length`. +pub fn node_length(project: &ProjectRef, node: NodeId) -> Rational { + let guard = lock_project(project); + let Some(entry) = guard.graph.get(node) else { + return Rational::new(0, 1); + }; + let Some(any) = entry.behavior.as_any() else { + return Rational::new(0, 1); + }; + if let Some(f) = any.downcast_ref::() { + return f.video_length(); + } + if let Some(seq) = any.downcast_ref::() { + let blocks = GraphBlockRange { + graph: &guard.graph, + }; + let tracks = GraphTrackRange { + graph: &guard.graph, + }; + let mut longest = Rational::new(0, 1); + for list_id in seq.track_lists.iter().filter(|i| i.valid()) { + let Some(le) = guard.graph.get(*list_id) else { + continue; + }; + let Some(list) = le + .behavior + .as_any() + .and_then(|a| a.downcast_ref::()) + else { + continue; + }; + let len = list.total_length(&tracks); + if len > longest { + longest = len; + } + } + let _ = blocks; + if longest > Rational::new(0, 1) { + return longest; + } + return seq.last_length; + } + Rational::new(0, 1) +} + +/// Block-range access over the project graph (the adapter [`TrackBehavior`] +/// queries need; `// CPP-PARITY: src/task/...` uses the node graph directly). +pub struct GraphBlockRange<'a> { + /// The graph the blocks live in. + pub graph: &'a Graph, +} + +impl BlockRange for GraphBlockRange<'_> { + fn in_(&self, block: NodeId) -> Rational { + block_core_of(self.graph, block) + .map(|c| c.in_()) + .unwrap_or_else(|| Rational::new(0, 1)) + } + + fn out(&self, block: NodeId) -> Rational { + block_core_of(self.graph, block) + .map(|c| c.out()) + .unwrap_or_else(|| Rational::new(0, 1)) + } +} + +/// Track-range access over the project graph (the adapter +/// [`TrackListBehavior::total_length`] needs). +pub struct GraphTrackRange<'a> { + /// The graph the tracks live in. + pub graph: &'a Graph, +} + +impl TrackRange for GraphTrackRange<'_> { + fn length(&self, track: NodeId) -> Rational { + let Some(entry) = self.graph.get(track) else { + return Rational::new(0, 1); + }; + let Some(t) = entry + .behavior + .as_any() + .and_then(|a| a.downcast_ref::()) + else { + return Rational::new(0, 1); + }; + t.length(&GraphBlockRange { graph: self.graph }) + } +} + +/// Convert an oaknode video-params value to the oakcommon `VideoParams` +/// value type the task code works with (the C ABI marshalled the same +/// fields). +fn common_from_node_video(v: oaknode::value::VideoParams) -> CommonVideoParams { + let mut p = CommonVideoParams::new_basic( + v.width, + v.height, + oakcommon::ocioutils::PixelFormat::from_code(v.pixel_format), + v.channels, + 1, + 1, + 0, + 1, + ); + p.set_frame_rate(v.frame_rate.numerator() as i32, v.frame_rate.denominator() as i32); + p +} + +/// Convert an oakcommon video-params value back to the oaknode value type +/// (the fields that survive the C ABI marshalling; `video_type`, +/// `start_time`/`duration` and color fields have no oaknode counterpart). +fn node_video_from_common(v: &CommonVideoParams) -> oaknode::value::VideoParams { + oaknode::value::VideoParams { + width: v.width(), + height: v.height(), + frame_rate: Rational::new(v.frame_rate().0 as i64, v.frame_rate().1 as i64), + pixel_format: v.format().code(), + channels: v.channel_count(), + } +} + +/// The sequence's `index`th video parameter stream as the oakcommon value +/// type (`oaknode_sequence_get_video_params`). +pub fn sequence_video_params( + project: &ProjectRef, + sequence: NodeId, + index: usize, +) -> Option { + let guard = lock_project(project); + let entry = guard.graph.get(sequence)?; + let seq = entry + .behavior + .as_any()? + .downcast_ref::()?; + seq.video_params.get(index).copied().map(common_from_node_video) +} + +/// The sequence's audio output parameters `(sample_rate, channel_layout)` +/// (defaults to 48000 Hz stereo, the sequence defaults). +pub fn sequence_audio_params(project: &ProjectRef, sequence: NodeId) -> (i32, u64) { + let guard = lock_project(project); + let Some(entry) = guard.graph.get(sequence) else { + return (48000, 0x3); + }; + let Some(seq) = entry + .behavior + .as_any() + .and_then(|a| a.downcast_ref::()) + else { + return (48000, 0x3); + }; + match seq.audio_params.first() { + Some(a) => (a.sample_rate, a.channel_layout), + None => (48000, 0x3), + } +} + +/// Create a new footage node in `project` (optionally pre-set with a +/// filename; unprobed — call [`footage_set_filename`] to probe). `None` is +/// no longer reachable (the old null-handle allocation path); the `Option` +/// keeps the call sites' shape. +pub fn footage_create(project: &ProjectRef, filename: Option<&str>) -> Option { + let mut guard = lock_project(project); + let (core, behavior) = oaknode::footage::FootageBehavior::create(); + let id = guard.graph.add_node(core, behavior); + if let Some(filename) = filename { + if let Some(entry) = guard.graph.get_mut(id) { + if let Some(f) = entry + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + { + f.filename = filename.to_string(); + } + } + } + Some(id) +} + +/// Set the footage cancellation flag (the C++ footage cancel atom; the +/// footage behavior owns its own atom, mirrored from the task's). +pub fn footage_set_cancelled(project: &ProjectRef, footage: NodeId, cancelled: bool) { + let mut guard = lock_project(project); + if let Some(entry) = guard.graph.get_mut(footage) { + if let Some(f) = entry + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + { + f.set_cancel(cancelled); + } + } +} + +/// Set the footage's filename and re-probe it through the oakcodec decoder +/// registry (`oaknode_footage_set_filename`); returns the post-probe +/// validity (the C++ set_filename → Probe() → is_valid chain). +pub fn footage_set_filename(project: &ProjectRef, footage: NodeId, filename: &str) -> bool { + let mut guard = lock_project(project); + let Some(entry) = guard.graph.get_mut(footage) else { + return false; + }; + let Some(f) = entry + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + else { + return false; + }; + f.filename = filename.to_string(); + f.valid = false; + entry + .core + .set_standard_value("file_in", -1, oaknode::value::NodeValue::Text(filename.to_string())); + if f.probe().is_err() { + return false; + } + f.valid +} + +/// Whether the footage's last probe succeeded (`oaknode_footage_is_valid`). +pub fn footage_is_valid(project: &ProjectRef, footage: NodeId) -> bool { + let guard = lock_project(project); + guard + .graph + .get(footage) + .and_then(|e| e.behavior.as_any()) + .and_then(|a| a.downcast_ref::()) + .map(|f| f.valid) + .unwrap_or(false) +} + +/// The footage's absolute filename (`oaknode_footage_filename`). +pub fn footage_filename(project: &ProjectRef, footage: NodeId) -> String { + let guard = lock_project(project); + guard + .graph + .get(footage) + .and_then(|e| e.behavior.as_any()) + .and_then(|a| a.downcast_ref::()) + .map(|f| f.filename.clone()) + .unwrap_or_default() +} + +/// Total stream count of the footage (`oaknode_footage_total_stream_count`). +pub fn footage_total_stream_count(project: &ProjectRef, footage: NodeId) -> usize { + let guard = lock_project(project); + guard + .graph + .get(footage) + .and_then(|e| e.behavior.as_any()) + .and_then(|a| a.downcast_ref::()) + .map(|f| f.total_stream_count()) + .unwrap_or(0) +} + +/// The footage's `index`th video parameter stream as the oakcommon value +/// type (`oaknode_footage_get_video_params`). +pub fn footage_video_params( + project: &ProjectRef, + footage: NodeId, + index: usize, +) -> Option { + let guard = lock_project(project); + let f = guard + .graph + .get(footage)? + .behavior + .as_any()? + .downcast_ref::()?; + f.video_params(index).map(common_from_node_video) +} + +/// Overwrite (or create) the footage's `index`th video stream parameters +/// (`oaknode_footage_set_video_params`). +pub fn footage_set_video_params( + project: &ProjectRef, + footage: NodeId, + index: usize, + params: &CommonVideoParams, +) { + let mut guard = lock_project(project); + let Some(entry) = guard.graph.get_mut(footage) else { + return; + }; + let Some(f) = entry + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + else { + return; + }; + let node_params = node_video_from_common(params); + let mut seen = 0usize; + let mut found = false; + for s in f.streams.iter_mut().filter(|s| s.is_video) { + if seen == index { + s.video = Some(node_params); + found = true; + break; + } + seen += 1; + } + if !found { + for _ in seen..=index { + f.streams.push(StreamInfo { + index: f.streams.len() as i32, + is_video: true, + video: None, + audio: None, + duration: Rational::new(0, 1), + }); + } + if let Some(s) = f.streams.iter_mut().filter(|s| s.is_video).nth(index) { + s.video = Some(node_params); + } + } +} + +/// Create a new bin folder node (`oaknode_folder_create`; `None` keeps the +/// call-site shape of the old null-handle path). +pub fn folder_create(project: &ProjectRef) -> Option { + let mut guard = lock_project(project); + let (core, behavior) = oaknode::folder::create(""); + Some(guard.graph.add_node(core, behavior)) +} + +/// Create a new sequence node with default parameters +/// (`oaknode_sequence_create` + `oaknode_sequence_set_default_parameters`; +/// the domain constructor applies the defaults itself). +pub fn sequence_create(project: &ProjectRef) -> Option { + let mut guard = lock_project(project); + let (core, behavior) = oaknode::sequence::SequenceBehavior::create(); + Some(guard.graph.add_node(core, behavior)) +} + +/// The sequence's track list for `kind`, creating it on first use (the C++ +/// `Sequence::get_track_list`). +pub fn sequence_track_list( + project: &ProjectRef, + sequence: NodeId, + kind: TrackType, +) -> Option { + let base = match kind { + TrackType::Video => 0usize, + TrackType::Audio => 1usize, + TrackType::Subtitle => 2usize, + }; + let mut guard = lock_project(project); + { + let entry = guard.graph.get(sequence)?; + let seq = entry + .behavior + .as_any()? + .downcast_ref::()?; + if let Some(id) = seq.track_lists.get(base).filter(|i| i.valid()) { + return Some(*id); + } + } + let list_id = guard.graph.add_node( + NodeCore::new(), + Box::new(TrackListBehavior::new(kind)), + ); + if let Some(le) = guard.graph.get_mut(list_id) { + if let Some(l) = le + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + { + l.sequence = Some(sequence); + l.array_base = base as i32; + } + } + if let Some(entry) = guard.graph.get_mut(sequence) { + if let Some(seq) = entry + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + { + while seq.track_lists.len() <= base { + seq.track_lists.push(NodeId::INVALID); + } + seq.track_lists[base] = list_id; + } + } + Some(list_id) +} + +/// Number of tracks on the list (`oaknode_tracklist_get_track_count`). +pub fn tracklist_track_count(project: &ProjectRef, list: NodeId) -> usize { + let guard = lock_project(project); + guard + .graph + .get(list) + .and_then(|e| e.behavior.as_any()) + .and_then(|a| a.downcast_ref::()) + .map(|l| l.tracks.len()) + .unwrap_or(0) +} + +/// Track at `index` on the list (`oaknode_tracklist_get_track_at`). +pub fn tracklist_track_at(project: &ProjectRef, list: NodeId, index: usize) -> Option { + let guard = lock_project(project); + guard + .graph + .get(list)? + .behavior + .as_any()? + .downcast_ref::()? + .track_at(index) +} + +/// The track's media type (`oaknode_track_get_type`). +pub fn track_type(project: &ProjectRef, track: NodeId) -> Option { + let guard = lock_project(project); + guard + .graph + .get(track) + .and_then(|e| e.behavior.as_any()) + .and_then(|a| a.downcast_ref::()) + .map(|t| t.kind) +} + +/// Number of blocks on the track (`oaknode_track_get_block_count`). +pub fn track_block_count(project: &ProjectRef, track: NodeId) -> usize { + let guard = lock_project(project); + guard + .graph + .get(track) + .and_then(|e| e.behavior.as_any()) + .and_then(|a| a.downcast_ref::()) + .map(|t| t.blocks.len()) + .unwrap_or(0) +} + +/// Block at `index` on the track (`oaknode_track_get_block_at`). +pub fn track_block_at(project: &ProjectRef, track: NodeId, index: usize) -> Option { + let guard = lock_project(project); + guard + .graph + .get(track)? + .behavior + .as_any()? + .downcast_ref::()? + .block_at(index) +} + +/// The track's total length (end of its last block; `oaknode_track_get_length`). +pub fn track_length(project: &ProjectRef, track: NodeId) -> Rational { + let guard = lock_project(project); + let Some(entry) = guard.graph.get(track) else { + return Rational::new(0, 1); + }; + let Some(t) = entry + .behavior + .as_any() + .and_then(|a| a.downcast_ref::()) + else { + return Rational::new(0, 1); + }; + t.length(&GraphBlockRange { graph: &guard.graph }) +} + +/// Append a block to a track and record the track ownership on the block +/// (`oaknode_track_append_block`); false for stale ids. +pub fn track_append_block(project: &ProjectRef, track: NodeId, block: NodeId) -> bool { + let mut guard = lock_project(project); + let _ = set_block_core(&mut guard.graph, block, |core| core.track = Some(track)); + let Some(entry) = guard.graph.get_mut(track) else { + return false; + }; + let Some(t) = entry + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + else { + return false; + }; + t.append_block(block); + true +} + +/// Create a block node of the given kind (`oaknode_block_clip_create` / +/// `oaknode_block_gap_create` / `oaknode_block_transition_create`; the +/// transition kind argument is ignored — only cross-dissolve is modelled). +pub fn block_create(project: &ProjectRef, kind: BlockKind) -> Option { + let mut guard = lock_project(project); + let (core, behavior): (NodeCore, Box) = match kind { + BlockKind::Clip => oaknode::block::clip_create(), + BlockKind::Gap => oaknode::block::gap_create(), + BlockKind::Transition => oaknode::block::transition_create(), + BlockKind::Other => return None, + }; + Some(guard.graph.add_node(core, behavior)) +} + +/// The block's kind (`oaknode_block_get_kind`). +pub fn block_kind(project: &ProjectRef, block: NodeId) -> BlockKind { + let guard = lock_project(project); + let Some(entry) = guard.graph.get(block) else { + return BlockKind::Other; + }; + let Some(any) = entry.behavior.as_any() else { + return BlockKind::Other; + }; + if any.downcast_ref::().is_some() { + BlockKind::Clip + } else if any.downcast_ref::().is_some() { + BlockKind::Gap + } else if any.downcast_ref::().is_some() { + BlockKind::Transition + } else { + BlockKind::Other + } +} + +/// The block's timeline in point (`oaknode_block_get_in`). +pub fn block_in(project: &ProjectRef, block: NodeId) -> Rational { + let guard = lock_project(project); + block_core_of(&guard.graph, block) + .map(|c| c.in_()) + .unwrap_or_else(|| Rational::new(0, 1)) +} + +/// The block's timeline length (`oaknode_block_get_length`). +pub fn block_length(project: &ProjectRef, block: NodeId) -> Rational { + let guard = lock_project(project); + block_core_of(&guard.graph, block) + .map(|c| c.length()) + .unwrap_or_else(|| Rational::new(0, 1)) +} + +/// The block's media in point (C++ `Block::media_in`). +pub fn block_media_in(project: &ProjectRef, block: NodeId) -> Rational { + let guard = lock_project(project); + block_core_of(&guard.graph, block) + .map(|c| c.media_in) + .unwrap_or_else(|| Rational::new(0, 1)) +} + +/// Set the block's length keeping the media out anchored +/// (`oaknode_block_set_length_and_media_out`). +pub fn block_set_length_and_media_out(project: &ProjectRef, block: NodeId, n: i64, d: i64) { + let mut guard = lock_project(project); + let _ = set_block_core(&mut guard.graph, block, |core| { + core.set_length_and_media_out(Rational::new(n, d)) + }); +} + +/// Set a clip's media in point (`oaknode_clip_set_media_in`). +pub fn clip_set_media_in(project: &ProjectRef, block: NodeId, n: i64, d: i64) { + let mut guard = lock_project(project); + if let Some(entry) = guard.graph.get_mut(block) { + if let Some(c) = entry + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + { + c.core.media_in = Rational::new(n, d); + } + } +} + +/// Record the footage connected to a clip (the domain model keeps the +/// clip-footage link in [`ClipBlockBehavior::footage`]). +pub fn clip_set_footage(project: &ProjectRef, block: NodeId, footage: NodeId) { + let mut guard = lock_project(project); + if let Some(entry) = guard.graph.get_mut(block) { + if let Some(c) = entry + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + { + c.footage = Some(footage); + } + } +} + +/// The clip footage recorded on a clip block, if any. +pub fn clip_footage(project: &ProjectRef, block: NodeId) -> Option { + let guard = lock_project(project); + guard + .graph + .get(block)? + .behavior + .as_any()? + .downcast_ref::()? + .footage +} + +/// A transition's in offset (`oaknode_transition_get_in_offset`). +pub fn transition_in_offset(project: &ProjectRef, block: NodeId) -> Rational { + let guard = lock_project(project); + guard + .graph + .get(block) + .and_then(|e| e.behavior.as_any()) + .and_then(|a| a.downcast_ref::()) + .map(|t| t.in_offset) + .unwrap_or_else(|| Rational::new(0, 1)) +} + +/// A transition's out offset (`oaknode_transition_get_out_offset`). +pub fn transition_out_offset(project: &ProjectRef, block: NodeId) -> Rational { + let guard = lock_project(project); + guard + .graph + .get(block) + .and_then(|e| e.behavior.as_any()) + .and_then(|a| a.downcast_ref::()) + .map(|t| t.out_offset) + .unwrap_or_else(|| Rational::new(0, 1)) +} + +/// Set a transition's offsets and derive its length from them +/// (`oaknode_transition_set_offsets_and_length`). +pub fn transition_set_offsets_and_length( + project: &ProjectRef, + block: NodeId, + in_n: i64, + in_d: i64, + out_n: i64, + out_d: i64, +) { + let mut guard = lock_project(project); + if let Some(entry) = guard.graph.get_mut(block) { + if let Some(t) = entry + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + { + t.in_offset = Rational::new(in_n, in_d); + t.out_offset = Rational::new(out_n, out_d); + let length = t.in_offset + t.out_offset; + t.core.set_length_and_media_out(length); + } + } +} + +/// Connect `from`'s output to `to`'s `input_id` (`oaknode_node_connect`); +/// false on invalid endpoints/inputs or cycles. +pub fn node_connect(project: &ProjectRef, from: NodeId, to: NodeId, input_id: &str) -> bool { + let mut guard = lock_project(project); + guard.graph.connect(from, to, input_id, -1).is_ok() +} + +/// Walk the node's input chain to its footage source (C++ +/// `node_find_input_footage`): footage nodes answer with themselves, clip +/// blocks answer with their recorded footage, other nodes are walked +/// upstream along graph edges. Pure graph query — callers already holding +/// the project lock use this variant. +pub fn find_input_footage(graph: &Graph, node: NodeId) -> Option { + if node_is_footage(graph, node) { + return Some(node); + } + if let Some(clip) = graph + .get(node) + .and_then(|e| e.behavior.as_any()) + .and_then(|a| a.downcast_ref::()) + { + if let Some(f) = clip.footage { + if graph.is_valid(f) { + return Some(f); + } + } + } + let mut stack = vec![node]; + let mut seen = std::collections::HashSet::new(); + while let Some(n) = stack.pop() { + if !seen.insert(n) { + continue; + } + for (from, _, _) in graph.input_connections(n) { + if node_is_footage(graph, from) { + return Some(from); + } + stack.push(from); + } + } + None +} + +/// Locking variant of [`find_input_footage`] +/// (`oaknode_node_find_input_footage`). +pub fn node_find_input_footage(project: &ProjectRef, node: NodeId) -> Option { + let guard = lock_project(project); + find_input_footage(&guard.graph, node) +} + +/// Map an `olive::core::PixelFormat` code to `oakcore_rs::PixelFormat` +/// (unknown codes map to `Invalid`). +pub fn pixel_format_from_code(code: i32) -> oakcore_rs::PixelFormat { + match code { + -1 => oakcore_rs::PixelFormat::Invalid, + 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, + } +} + +// --------------------------------------------------------------------------- +// Undo commands +// --------------------------------------------------------------------------- + +/// Command trait for the task-local undo commands (mirrors the +/// oaktimeline `undocommon::Command` pattern). +trait TaskCommand { + /// Apply the change. + fn redo(&mut self); + /// Revert the change. + fn undo(&mut self); +} + +/// Generic `redo` callback forwarding to [`TaskCommand::redo`]. +/// +/// # Safety +/// `userdata` must be the `Box` produced by [`box_command`]. +unsafe extern "C" fn redo_cb(userdata: *mut std::ffi::c_void) { + if userdata.is_null() { + return; + } + // SAFETY: box_command allocated a `Box`; the command still owns it. + (unsafe { &mut *(userdata as *mut T) }).redo(); +} + +/// Generic `undo` callback forwarding to [`TaskCommand::undo`]. +/// +/// # Safety +/// As `redo_cb`. +unsafe extern "C" fn undo_cb(userdata: *mut std::ffi::c_void) { + if userdata.is_null() { + return; + } + // SAFETY: as `redo_cb`. + (unsafe { &mut *(userdata as *mut T) }).undo(); +} + +/// Generic `free` callback dropping the boxed command. +/// +/// # Safety +/// `userdata` must be the `Box` produced by [`box_command`]. +unsafe extern "C" fn free_cb(userdata: *mut std::ffi::c_void) { + if userdata.is_null() { + return; + } + // SAFETY: the command owns this box; dropping it frees the command. + unsafe { + drop(Box::from_raw(userdata as *mut T)); + } +} + +/// Box a task command into an oakundo vtable command value. +fn box_command(cmd: T) -> UndoCommand { + let userdata = Box::into_raw(Box::new(cmd)) as *mut std::ffi::c_void; + let vtable = OakUndoCommandVtable { + redo: Some(redo_cb::), + undo: Some(undo_cb::), + free_fn: Some(free_cb::), + }; + UndoCommand::from_vtable(vtable, userdata) +} + +/// `FolderAddChildCommand` — add an item to a bin folder +/// (`oaknode_command_create_folder_add_child`). +struct FolderAddChildCommand { + /// The destination folder. + folder: NodeRef, + /// The item (footage/folder/sequence) to add. + child: NodeRef, +} + +impl TaskCommand for FolderAddChildCommand { + fn redo(&mut self) { + let mut guard = lock_project(&self.folder.0); + if let Some(entry) = guard.graph.get_mut(self.folder.1) { + if let Some(f) = entry + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + { + f.add_child(self.child.1); + } + } + if let Some(entry) = guard.graph.get_mut(self.child.1) { + entry.core.bin_folder = Some(self.folder.1); + } + } + + fn undo(&mut self) { + let mut guard = lock_project(&self.folder.0); + if let Some(entry) = guard.graph.get_mut(self.folder.1) { + if let Some(f) = entry + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + { + f.remove_child(self.child.1); + } + } + if let Some(entry) = guard.graph.get_mut(self.child.1) { + entry.core.bin_folder = None; + } + } +} + +/// Build the undo command that adds `child` to `folder` +/// (`oaknode_command_create_folder_add_child`). +pub fn folder_add_child_command(folder: NodeRef, child: NodeRef) -> UndoCommand { + box_command(FolderAddChildCommand { folder, child }) +} + +/// `RemoveNodeCommand` — detach a node from the project graph +/// (`oaknode_command_create_remove_node`). The node's entry (core + +/// behavior + generation) is kept between redo and undo; undo re-inserts +/// it into its original slot. Edges are dropped on redo and not restored +/// on undo (the C++ command's connections survive; this task module only +/// removes unconnected nodes). +struct RemoveNodeCommand { + /// The owning project. + project: ProjectRef, + /// The node to remove. + node: NodeId, + /// The detached entry, captured at redo. + entry: Option, + /// The node's bin folder at redo time. + folder: Option, + /// The node's child index in that folder. + folder_index: Option, +} + +impl TaskCommand for RemoveNodeCommand { + fn redo(&mut self) { + let mut guard = lock_project(&self.project); + let bin_folder = guard + .graph + .get(self.node) + .and_then(|e| e.core.bin_folder); + let index = bin_folder.and_then(|fid| { + guard + .graph + .get(fid) + .and_then(|e| e.behavior.as_any()) + .and_then(|a| a.downcast_ref::()) + .and_then(|f| f.index_of_child(self.node)) + }); + if let (Some(fid), Some(_)) = (bin_folder, index) { + if let Some(entry) = guard.graph.get_mut(fid) { + if let Some(f) = entry + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + { + f.remove_child(self.node); + } + } + } + let entry = guard.graph.take_node(self.node); + self.entry = entry; + self.folder = bin_folder; + self.folder_index = index; + } + + fn undo(&mut self) { + let Some(entry) = self.entry.take() else { + return; + }; + let mut guard = lock_project(&self.project); + guard.graph.add_entry(entry, self.node); + if let (Some(fid), Some(index)) = (self.folder, self.folder_index) { + if let Some(entry) = guard.graph.get_mut(fid) { + if let Some(f) = entry + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + { + let index = index.min(f.children.len()); + f.children.insert(index, self.node); + } + } + if let Some(entry) = guard.graph.get_mut(self.node) { + entry.core.bin_folder = Some(fid); + } + } + } +} + +/// Build the undo command that removes `node` from its project +/// (`oaknode_command_create_remove_node`). +pub fn remove_node_command(project: ProjectRef, node: NodeId) -> UndoCommand { + box_command(RemoveNodeCommand { + project, + node, + entry: None, + folder: None, + folder_index: None, + }) +} + +/// `TimelineAddTrackCommand` — append a track to a track list +/// (`TimelineAddTrackCommand::run_immediately` semantics). +/// +/// Domain replacement for oaktimeline's `TimelineAddTrackCommand`: that +/// crate is mid-migration to `Arc>` + `NodeId` and still +/// only exposes `CHandle`-based constructors (whose internals are local +/// stubs), so oaktask cannot wire domain objects through it yet. This +/// command performs the same work directly over the oaknode domain model; +/// once oaktimeline's domain constructor lands, oaktask should switch to +/// it. The C++ merge-node/automerge branch is not modelled (automerge is +/// disabled by default and the sequence-input connection is implicit in +/// the Rust track-list model). +struct TimelineAddTrackCommand { + /// The owning project. + project: ProjectRef, + /// The target track list. + list: NodeId, + /// The created track (set by redo). + track: Option, + /// The list's media type (copied at redo). + kind: TrackType, +} + +impl TaskCommand for TimelineAddTrackCommand { + fn redo(&mut self) { + let mut guard = lock_project(&self.project); + let track_index = { + let Some(entry) = guard.graph.get(self.list) else { + return; + }; + let Some(list) = entry + .behavior + .as_any() + .and_then(|a| a.downcast_ref::()) + else { + return; + }; + self.kind = list.kind; + list.tracks.len() as i32 + }; + let track_id = guard.graph.add_node( + NodeCore::new(), + Box::new(TrackBehavior::new(self.kind)), + ); + if let Some(te) = guard.graph.get_mut(track_id) { + if let Some(t) = te + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + { + t.index = track_index; + t.track_list = Some(self.list); + } + } + if let Some(le) = guard.graph.get_mut(self.list) { + if let Some(l) = le + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + { + l.tracks.push(track_id); + } + } + self.track = Some(track_id); + } + + fn undo(&mut self) { + let Some(track_id) = self.track.take() else { + return; + }; + let mut guard = lock_project(&self.project); + if let Some(le) = guard.graph.get_mut(self.list) { + if let Some(l) = le + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + { + l.tracks.retain(|t| *t != track_id); + } + } + guard.graph.remove_node(track_id); + } +} + +/// Build the undo command that appends a track to `list` +/// (`oaktimeline_add_track_command`; see [`TimelineAddTrackCommand`] for +/// the oaktimeline-migration note). +pub fn add_track_command(project: ProjectRef, list: NodeId) -> UndoCommand { + box_command(TimelineAddTrackCommand { + project, + list, + track: None, + kind: TrackType::Video, + }) +} diff --git a/crates/oaktask/src/precache.rs b/crates/oaktask/src/precache.rs index a993e0aa2..42b2c57ef 100644 --- a/crates/oaktask/src/precache.rs +++ b/crates/oaktask/src/precache.rs @@ -16,67 +16,68 @@ //! `PreCacheTask`, mirroring `src/task/src/precache/precachetask.h`. //! -//! Renders a footage node (or the whole sequence) through -//! [`crate::render::RenderTask`] to fill the playback cache. Owns a deep copy -//! of the project (`OakNodeProject`) and borrows the source footage -//! (`OakNodeFootage`). +//! Renders a footage node through [`crate::render::RenderTask`] to fill +//! the playback cache. The footage and its sequence are now +//! [`crate::nodeops::NodeRef`] domain references (the deleted oaknode C +//! ABI stubs are gone); video params come from the sequence's parameter +//! streams and the cache range is the full footage video length. //! -//! **Simplifications over the C++**: the deep project copy / viewer wiring -//! is not built (the render drives the given viewer directly) and the -//! timeline work-area intersection is replaced by the full footage length. +//! **Simplifications over the C++**: the deep project copy / viewer +//! wiring is not built (the render drives the given footage directly — +//! the ticket carries the footage filename and the footage node's +//! identity as the cache key) and the timeline work-area intersection is +//! replaced by the full footage length. The direct ticket arena's eval +//! producer does not persist frames to the frame cache yet, so this +//! render pass warms nothing on disk; the plumbing is in place. //! //! CPP-PARITY: src/task/src/precache/precachetask.h -use crate::bridge; +use oakcommon::videoparams::VideoParams; + use crate::error::Result; -use crate::handle::CHandle; +use crate::nodeops::{self, NodeRef}; use crate::render::{RenderTask, RenderTaskBehavior}; use crate::task::{Task, TaskBehavior}; use oakcore_rs::{Rational, TimeRange}; -/// A pre-cache task: renders frames and audio of a footage node into the -/// playback cache without any output file. +/// A pre-cache task: renders frames of a footage node into the playback +/// cache without any output file. pub struct PreCacheTask { /// The render base (itself a task). pub render: RenderTask, - /// Owning deep copy of the project (borrowed `OakNodeProject`). - pub project: CHandle, - /// Borrowed source footage (borrowed `OakNodeFootage`). - pub footage: CHandle, + /// The footage being cached. + pub footage: NodeRef, /// Frame index within the footage being cached. pub index: i32, - /// The sequence node (borrowed `OakNodeSequence`). - pub sequence: CHandle, + /// The sequence context the footage lives in. + pub sequence: NodeRef, } impl PreCacheTask { /// Create a pre-cache task for the given footage at `index` inside - /// `sequence`. - pub fn new(footage: CHandle, index: i32, sequence: CHandle) -> PreCacheTask { - // Video params from the sequence (empty when unavailable). - let mut video_params = CHandle::null(); - unsafe { - bridge::node::oaknode_sequence_get_video_params(sequence, 0, &mut video_params); - } + /// `sequence`. The old `footage: CHandle` / `sequence: CHandle` + /// signature is replaced by domain [`NodeRef`]s (single-lib + /// unification). + pub fn new(footage: NodeRef, index: i32, sequence: NodeRef) -> PreCacheTask { + // The sequence's video params drive the render timebase (the + // deleted `oaknode_sequence_get_video_params` stub is replaced by + // the direct behavior query). + let video_params: Option = + nodeops::sequence_video_params(&sequence.0, sequence.1, 0); - let filename = footage_filename(footage); + // The footage filename labels the task (the deleted + // `oaknode_footage_filename` stub is replaced by the direct + // behavior query). + let filename = nodeops::footage_filename(&footage.0, footage.1); let title = format!("Pre-caching {filename}:{index}"); - let base = Task::new(&title, CHandle::null()); - let render = RenderTask::new( - base, - video_params, - CHandle::null(), - sequence, - Default::default(), - None, - ); + let base = Task::new(&title, None); - // A scratch project for the render color manager (simplified). - let project = unsafe { bridge::node::oaknode_project_init() }; + // The render target is the footage node itself (pre-cache fills + // that footage's frame cache). + let render = RenderTask::new(base, video_params, footage.clone(), Default::default(), None); PreCacheTask { render, - project, footage, index, sequence, @@ -90,59 +91,42 @@ impl TaskBehavior for PreCacheTask { self.render.base.set_cancel_atom(task.get_cancel_atom()); // The full footage length is the cache range (simplified: no - // work-area intersection). - let mut len_num = 0i64; - let mut len_den = 1i64; - unsafe { - bridge::node::oaknode_footage_get_video_length( - self.footage, - &mut len_num, - &mut len_den, - ); - } - let range = TimeRange::new(Rational::new(0, 1), Rational::new(len_num, len_den)); + // work-area intersection). The deleted `oaknode_footage_get_video_length` + // stub is replaced by the direct behavior query. + let length = nodeops::node_length(&self.footage.0, self.footage.1); + let range = TimeRange::new(Rational::new(0, 1), length); - let mut color_manager = unsafe { bridge::node::oaknode_colormanager_init(self.project) }; - let mut cache = bridge::render::OakRenderCache::null(); - unsafe { - bridge::node::oaknode_node_get_video_frame_cache(self.sequence, &mut cache); - } - - self.render - .set_render_inputs(color_manager, cache, 0 /* k_online */, false, range); + // No color manager / frame-cache handles exist on the direct + // ticket path (the arena keys the cache by the footage node + // identity — see `RenderTask::build_video_ticket`). + self.render.set_render_inputs(0 /* k_online */, false, range); // Drive the render with `self` as the subclass behavior (the C++ // virtual dispatch receiver); the render is temporarily moved out to - // avoid a self-referential borrow and put back before the handles are - // released below. + // avoid a self-referential borrow and put back before returning. let mut render = std::mem::replace(&mut self.render, crate::render::RenderTask::placeholder()); let result = render.render(task, self); self.render = render; - if !cache.ctx.is_null() { - unsafe { - bridge::render::oakrender_cache_free(&mut cache); - } - } - if !color_manager.ctx.is_null() { - unsafe { - bridge::node::oaknode_colormanager_free(&mut color_manager); - } - } - result } } impl RenderTaskBehavior for PreCacheTask { - fn frame_downloaded(&mut self, task: &mut Task, frame: CHandle) -> Result<()> { - // Do nothing: pre-cache just fills the frame cache. + fn frame_downloaded(&mut self, task: &mut Task, frame: &oakrender::texture::Texture) -> Result<()> { + // Do nothing: pre-cache just fills the frame cache (the direct + // ticket arena records the render through the ticket's cache + // identity; see the module docs). let _ = (task, frame); Ok(()) } - fn audio_downloaded(&mut self, task: &mut Task, buffer: CHandle) -> Result<()> { + fn audio_downloaded( + &mut self, + task: &mut Task, + buffer: &oakrender::ticket::AudioSamples, + ) -> Result<()> { // Pre-cache doesn't cache any audio. let _ = (task, buffer); Ok(()) @@ -153,21 +137,3 @@ impl RenderTaskBehavior for PreCacheTask { Ok(()) } } - -/// Two-stage read of the footage filename. -fn footage_filename(footage: CHandle) -> String { - let needed = - unsafe { bridge::node::oaknode_footage_filename(footage, std::ptr::null_mut(), 0) }; - if needed <= 0 { - return String::new(); - } - let mut buf = vec![0i8; needed as usize]; - unsafe { - bridge::node::oaknode_footage_filename(footage, buf.as_mut_ptr(), needed); - } - let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len()); - unsafe { - String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)) - .into_owned() - } -} diff --git a/crates/oaktask/src/project.rs b/crates/oaktask/src/project.rs index f04a70eef..226e90714 100644 --- a/crates/oaktask/src/project.rs +++ b/crates/oaktask/src/project.rs @@ -16,9 +16,10 @@ //! Project load/save/import tasks, mirroring `src/task/src/project/*`. //! -//! These tasks move data across the oaknode C ABI: they borrow node handles -//! while running and take ownership only on the `take_*` accessors -//! (architectural decision #5 in README.md). +//! These tasks move data through the direct oaknode domain model +//! (`Arc>` + `oaknode::id::NodeId`); +//! they borrow the project while running and hand ownership over on the +//! `take_*` accessors (architectural decision #5 in README.md). pub mod format; pub mod import; diff --git a/crates/oaktask/src/project/import.rs b/crates/oaktask/src/project/import.rs index c9a3644ce..ed3a8c9ac 100644 --- a/crates/oaktask/src/project/import.rs +++ b/crates/oaktask/src/project/import.rs @@ -17,17 +17,27 @@ //! `ProjectImportTask`, mirroring `src/task/src/project/import/import.h`. //! //! Imports media files into a folder of a project. Produces an undoable -//! `OakUndoCommand` (taken via `take_command()`), tracks per-file import -//! failures, and supports an optional image-sequence confirmation callback. +//! [`oakundo::undocommand::UndoCommand`] (taken via `take_command()`), +//! tracks per-file import failures, and supports an optional image-sequence +//! confirmation callback. +//! +//! **Single-lib note**: the node-graph manipulation went through the +//! deleted oaknode C ABI; it now goes through the direct oaknode domain +//! operations in [`crate::nodeops`] (folder/footage creation, probing via +//! the oakcodec decoder registry, undo command construction). The folder +//! and project are domain references (`Arc>` + `NodeId`) +//! instead of borrowed `CHandle`s. //! //! CPP-PARITY: src/task/src/project/import/import.h use std::path::Path; -use crate::bridge; +use oakcommon::configstore::ConfigStore; +use oakcommon::videoparams::VideoType; +use oakundo::undocommand::UndoCommand; + use crate::error::{Error, Result}; -use crate::ffi::taskhandle::cstr; -use crate::handle::CHandle; +use crate::nodeops::{self, NodeRef, ProjectRef}; use crate::task::{Task, TaskBehavior}; /// Callback used to confirm whether a detected image sequence should be @@ -39,18 +49,18 @@ pub type ImageSequenceConfirmFn = Box bool + Send>; pub struct ProjectImportTask { /// The shared task base. pub base: Task, - /// Destination folder (borrowed `OakNodeFolder`). - pub folder: CHandle, - /// Destination project (borrowed `OakNodeProject`). - pub project: CHandle, + /// Destination folder (project + folder node id). + pub folder: NodeRef, + /// Destination project. + pub project: ProjectRef, /// Media filenames to import. pub filenames: Vec, /// The undo command produced by the task, taken via `take_command`. - command: Option, + command: Option, /// Files that failed to import. invalid_files: Vec, /// Imported footage, taken via `get_imported_footage`. - imported_footage: Vec, + imported_footage: Vec, /// Optional image-sequence confirmation callback. image_sequence_confirm: Option, /// Total number of files to import (directories counted recursively). @@ -59,35 +69,15 @@ pub struct ProjectImportTask { image_sequence_ignore_files: Vec, } -impl Drop for ProjectImportTask { - fn drop(&mut self) { - if let Some(command) = &mut self.command { - if !command.ctx.is_null() { - unsafe { - bridge::undo::oakundo_command_free(command); - } - } - } - // Borrowed footage handles: releasing them only frees the handle - // boxes (the footage nodes are owned by the project). - for footage in &self.imported_footage { - if !footage.ctx.is_null() { - if let Some(release) = footage.release { - unsafe { - release(footage.ctx); - } - } - } - } - } -} - impl ProjectImportTask { /// Create an import task for the given folder/project and filenames. + /// The old `folder: CHandle` / `project: CHandle` signature is replaced + /// by the domain [`NodeRef`] folder and [`ProjectRef`] project + /// (single-lib unification). pub fn new( base: Task, - folder: CHandle, - project: CHandle, + folder: NodeRef, + project: ProjectRef, filenames: Vec, image_sequence_confirm: Option, file_count: usize, @@ -108,7 +98,7 @@ impl ProjectImportTask { /// Take ownership of the produced undo command; `Err(Error::State)` if /// the task has not run yet. - pub fn take_command(&mut self) -> Result { + pub fn take_command(&mut self) -> Result { self.command.take().ok_or(Error::State) } @@ -122,18 +112,11 @@ impl ProjectImportTask { !self.invalid_files.is_empty() } - /// Borrowed handle to the imported footage at `index` (addref'd by the C - /// ABI); `Err(Error::NotFound)` if out of range. - pub fn get_imported_footage(&self, index: usize) -> Result { - let footage = self.imported_footage.get(index).ok_or(Error::NotFound)?; - if !footage.ctx.is_null() { - if let Some(addref) = footage.addref { - unsafe { - addref(footage.ctx); - } - } - } - Ok(*footage) + /// The imported footage at `index` as a domain [`NodeRef`]; + /// `Err(Error::NotFound)` if out of range. (The deleted C ABI handed + /// out addref'd handles; the domain reference is a plain clone.) + pub fn get_imported_footage(&self, index: usize) -> Result { + self.imported_footage.get(index).cloned().ok_or(Error::NotFound) } /// Total number of imported footage entries. @@ -151,19 +134,15 @@ impl ProjectImportTask { self.file_count } - /// The invalid filename at `index` (assumes the index is in range). - pub(crate) fn invalid_file_at(&self, index: usize) -> &str { - &self.invalid_files[index] - } - fn import( &mut self, task: &mut Task, - folder: CHandle, + folder: NodeRef, entries: &mut Vec, counter: &mut usize, - parent_command: CHandle, + parent_command: &mut UndoCommand, ) { + let atom = task.get_cancel_atom(); let mut i = 0; while i < entries.len() { if task.is_cancelled() { @@ -183,24 +162,17 @@ impl ProjectImportTask { }; if !entry_list.is_empty() { - let folder_handle = - unsafe { bridge::node::oaknode_folder_create(self.project) }; - if !folder_handle.ctx.is_null() { - unsafe { - bridge::node::oaknode_node_set_label( - bridge::node::oaknode_folder_as_node(folder_handle), - cstr(&basename_of(file_path)), - ); - } + if let Some(folder_id) = nodeops::folder_create(&self.project) { + nodeops::set_node_label(&self.project, folder_id, &basename_of(file_path)); self.add_item_to_folder( - folder, - unsafe { bridge::node::oaknode_folder_as_node(folder_handle) }, + folder.clone(), + (self.project.clone(), folder_id), parent_command, ); let mut sub_entries = entry_list; self.import( task, - folder_handle, + (self.project.clone(), folder_id), &mut sub_entries, counter, parent_command, @@ -208,62 +180,47 @@ impl ProjectImportTask { } } } else { - let footage = - unsafe { bridge::node::oaknode_footage_create(self.project, std::ptr::null()) }; - if footage.ctx.is_null() { + let Some(footage) = nodeops::footage_create(&self.project, None) else { i += 1; continue; - } + }; - unsafe { - bridge::node::oaknode_footage_set_cancel_atom(footage, task.get_cancel_atom()); - } - let ok = - unsafe { bridge::node::oaknode_footage_set_filename(footage, cstr(file_path)) } - == 0; - unsafe { - bridge::node::oaknode_footage_set_cancel_atom( - footage, - bridge::render::OakCancelAtom::null(), - ); - } + // Mirror the C++ cancel-atom dance around the probe: the + // footage behavior's own cancellation flag tracks the + // task's atom during the probe and is cleared afterwards. + nodeops::footage_set_cancelled(&self.project, footage, true); + nodeops::footage_set_cancelled(&self.project, footage, atom.is_cancelled()); + let ok = nodeops::footage_set_filename(&self.project, footage, file_path); + nodeops::footage_set_cancelled(&self.project, footage, false); - if ok && unsafe { bridge::node::oaknode_footage_is_valid(footage) } != 0 { - unsafe { - bridge::node::oaknode_node_set_label( - bridge::node::oaknode_footage_as_node(footage), - cstr(&basename_of(file_path)), - ); - } + if ok && nodeops::footage_is_valid(&self.project, footage) { + nodeops::set_node_label(&self.project, footage, &basename_of(file_path)); // See if this footage is an image sequence. - self.validate_image_sequence(task, footage, entries, i); + self.validate_image_sequence( + task, + (self.project.clone(), footage), + entries, + i, + ); // Create the undoable command that adds the item. self.add_item_to_folder( - folder, - unsafe { bridge::node::oaknode_footage_as_node(footage) }, + folder.clone(), + (self.project.clone(), footage), parent_command, ); - self.imported_footage.push(footage); + self.imported_footage.push((self.project.clone(), footage)); } else { self.invalid_files.push(file_path.clone()); // Remove the invalid footage from the graph; the remove // command takes ownership on redo and deletes the node // when the command is destroyed. - let mut remove = unsafe { - bridge::node::oaknode_command_create_remove_node( - bridge::node::oaknode_footage_as_node(footage), - ) - }; - if !remove.ctx.is_null() { - unsafe { - bridge::undo::oakundo_command_redo_now(remove); - bridge::undo::oakundo_command_free(&mut remove); - } - } + let mut remove = + nodeops::remove_node_command(self.project.clone(), footage); + remove.redo_now(); } *counter += 1; @@ -273,30 +230,25 @@ impl ProjectImportTask { } } - fn add_item_to_folder(&self, folder: CHandle, item: CHandle, command: CHandle) { - let child = unsafe { bridge::node::oaknode_command_create_folder_add_child(folder, item) }; - if !child.ctx.is_null() { - unsafe { - bridge::undo::oakundo_command_multi_add_child(command, child); - } - } + fn add_item_to_folder(&self, folder: NodeRef, item: NodeRef, command: &mut UndoCommand) { + let child = nodeops::folder_add_child_command(folder, item); + command.multi_add_child(child); } fn validate_image_sequence( &mut self, task: &mut Task, - footage: CHandle, + footage: NodeRef, info_list: &mut Vec, index: usize, ) { - let filename = footage_filename(footage); + let filename = nodeops::footage_filename(&footage.0, footage.1); if filename.is_empty() { return; } - let digit_count = unsafe { - bridge::codec::oakcodec_decoder_get_image_sequence_digit_count(cstr(&filename)) - }; + // Direct oakcodec calls (single-lib unification). + let digit_count = oakcodec::decoder::get_image_sequence_digit_count(&filename); if digit_count <= 0 { return; } @@ -309,41 +261,35 @@ impl ProjectImportTask { return; } - if !self.item_is_still_image_footage_only(footage) { + if !self.item_is_still_image_footage_only(&footage) { return; } - let mut video_stream = CHandle::null(); - if unsafe { bridge::node::oaknode_footage_get_video_params(footage, 0, &mut video_stream) } - != 0 - { + let Some(mut video_stream) = nodeops::footage_video_params(&footage.0, footage.1, 0) + else { return; - } + }; - let mut width = 0; - let mut height = 0; - unsafe { - bridge::common::oakcommon_videoparams_get_width(video_stream, &mut width); - bridge::common::oakcommon_videoparams_get_height(video_stream, &mut height); - } + let width = video_stream.width(); + let height = video_stream.height(); - let seq_index = - unsafe { bridge::codec::oakcodec_decoder_get_image_sequence_index(cstr(&filename)) }; + // Direct oakcodec call (single-lib unification). + let seq_index = oakcodec::decoder::get_image_sequence_index(&filename); let prev_fn = transform_sequence_filename(&filename, seq_index - 1, digit_count); let next_fn = transform_sequence_filename(&filename, seq_index + 1, digit_count); - let previous_file = - unsafe { bridge::node::oaknode_footage_create(self.project, cstr(&prev_fn)) }; - let next_file = - unsafe { bridge::node::oaknode_footage_create(self.project, cstr(&next_fn)) }; + let previous_file = nodeops::footage_create(&self.project, Some(&prev_fn)); + let next_file = nodeops::footage_create(&self.project, Some(&next_fn)); - let prev_matches = !previous_file.ctx.is_null() - && unsafe { bridge::node::oaknode_footage_is_valid(previous_file) } != 0 - && self.compare_still_image_size(previous_file, width, height); - let next_matches = !next_file.ctx.is_null() - && unsafe { bridge::node::oaknode_footage_is_valid(next_file) } != 0 - && self.compare_still_image_size(next_file, width, height); + let prev_matches = previous_file.is_some_and(|f| { + nodeops::footage_set_filename(&self.project, f, &prev_fn) + && self.compare_still_image_size(&(self.project.clone(), f), width, height) + }); + let next_matches = next_file.is_some_and(|f| { + nodeops::footage_set_filename(&self.project, f, &next_fn) + && self.compare_still_image_size(&(self.project.clone(), f), width, height) + }); if prev_matches || next_matches { // Ask the user whether this is really a sequence (default: no). @@ -376,179 +322,86 @@ impl ProjectImportTask { } if is_sequence { - unsafe { - bridge::common::oakcommon_videoparams_set_video_type( - video_stream, - bridge::common::OAKCOMMON_VIDEO_TYPE_IMAGE_SEQUENCE, - ); + video_stream.set_video_type(VideoType::ImageSequence); - let mut rate_buf = [0i8; 64]; - let needed = bridge::common::oakcommon_config_get( - std::ptr::null(), - cstr("DefaultSequenceFrameRate"), - rate_buf.as_mut_ptr(), - rate_buf.len() as i32, - ); - if needed > 0 { - let rate = crate::project::load::buf_to_string(&rate_buf); - if let Some((num, den)) = parse_rational(&rate) { - if den != 0 { - bridge::common::oakcommon_videoparams_set_time_base( - video_stream, - num, - den, - ); - bridge::common::oakcommon_videoparams_set_frame_rate( - video_stream, - den, - num, - ); - } + // Direct config read (single-lib unification). + if let Ok(rate) = ConfigStore::instance().get(None, "DefaultSequenceFrameRate") { + if let Some((num, den)) = parse_rational(&rate) { + if den != 0 { + video_stream.set_time_base(num, den); + video_stream.set_frame_rate(den, num); } } - - bridge::common::oakcommon_videoparams_set_start_time(video_stream, start_index); - bridge::common::oakcommon_videoparams_set_duration( - video_stream, - end_index - start_index + 1, - ); - bridge::node::oaknode_footage_set_video_params(footage, 0, &video_stream); } - } - } - if !video_stream.ctx.is_null() { - unsafe { - bridge::common::oakcommon_videoparams_free(&mut video_stream); + video_stream.set_start_time(start_index); + video_stream.set_duration(end_index - start_index + 1); + nodeops::footage_set_video_params(&footage.0, footage.1, 0, &video_stream); } } // The probe footage was only created for comparison; remove it. - for probe in [previous_file, next_file] { - if !probe.ctx.is_null() { - let mut remove = unsafe { - bridge::node::oaknode_command_create_remove_node( - bridge::node::oaknode_footage_as_node(probe), - ) - }; - if !remove.ctx.is_null() { - unsafe { - bridge::undo::oakundo_command_redo_now(remove); - bridge::undo::oakundo_command_free(&mut remove); - } - } - } + for probe in [previous_file, next_file].into_iter().flatten() { + let mut remove = nodeops::remove_node_command(self.project.clone(), probe); + remove.redo_now(); } let _ = task; } - fn item_is_still_image_footage_only(&self, footage: CHandle) -> bool { - if unsafe { bridge::node::oaknode_footage_total_stream_count(footage) } != 1 { + fn item_is_still_image_footage_only(&self, footage: &NodeRef) -> bool { + // The oaknode domain footage records probed streams; a single + // stream is the closest domain equivalent of the C++ + // `total_stream_count == 1` check. The oaknode video params carry + // no `video_type`, so the `kVideoTypeStill` check collapses to + // "one stream with valid dimensions". + if nodeops::footage_total_stream_count(&footage.0, footage.1) != 1 { return false; } - let mut vp = CHandle::null(); - if unsafe { bridge::node::oaknode_footage_get_video_params(footage, 0, &mut vp) } != 0 { + let Some(vp) = nodeops::footage_video_params(&footage.0, footage.1, 0) else { return false; - } + }; - let mut video_type = 0; - let mut valid = 0; - unsafe { - bridge::common::oakcommon_videoparams_get_video_type(vp, &mut video_type); - bridge::common::oakcommon_videoparams_get_is_valid(vp, &mut valid); - bridge::common::oakcommon_videoparams_free(&mut vp); - } - - valid != 0 && video_type == bridge::common::OAKCOMMON_VIDEO_TYPE_STILL + vp.is_valid() && vp.video_type() == VideoType::Video } - fn compare_still_image_size(&self, footage: CHandle, width: i32, height: i32) -> bool { + fn compare_still_image_size(&self, footage: &NodeRef, width: i32, height: i32) -> bool { if !self.item_is_still_image_footage_only(footage) { return false; } - let mut stream = CHandle::null(); - if unsafe { bridge::node::oaknode_footage_get_video_params(footage, 0, &mut stream) } != 0 { + let Some(stream) = nodeops::footage_video_params(&footage.0, footage.1, 0) else { return false; - } + }; - let mut w = 0; - let mut h = 0; - unsafe { - bridge::common::oakcommon_videoparams_get_width(stream, &mut w); - bridge::common::oakcommon_videoparams_get_height(stream, &mut h); - bridge::common::oakcommon_videoparams_free(&mut stream); - } - - w == width && h == height + stream.width() == width && stream.height() == height } } impl TaskBehavior for ProjectImportTask { - /// Import each filename via the oaknode footage/folder C ABI - /// (`bridge::node`), collecting an undo command, imported footage, and - /// per-file failures. + /// Import each filename via the direct oaknode domain operations + /// ([`crate::nodeops`]), collecting an undo command, imported footage, + /// and per-file failures. fn run(&mut self, task: &mut Task) -> Result<()> { - let mut command = unsafe { bridge::undo::oakundo_command_init_multi() }; - if command.ctx.is_null() { - task.set_error("Failed to create import command"); - return Err(Error::Failed("Failed to create import command".to_string())); - } - self.command = Some(command); + let mut command = UndoCommand::multi(); let mut counter = 0; let mut entries = self.filenames.clone(); - self.import(task, self.folder, &mut entries, &mut counter, command); + let folder = (self.folder.0.clone(), self.folder.1); + self.import(task, folder, &mut entries, &mut counter, &mut command); if task.is_cancelled() { - if !command.ctx.is_null() { - unsafe { - bridge::undo::oakundo_command_free(&mut command); - } - } self.command = None; return Err(Error::Cancelled); } + self.command = Some(command); Ok(()) } } /// Count files recursively (directories recurse; anything else counts 1), /// mirroring `count_files_recursive` in import.cpp. -fn count_files_recursive(paths: &[String]) -> usize { - paths - .iter() - .map(|p| { - let path = Path::new(p); - if path.is_dir() { - count_dir_files(path) - } else { - 1 - } - }) - .sum() -} - -fn count_dir_files(dir: &Path) -> usize { - match std::fs::read_dir(dir) { - Ok(rd) => rd - .filter_map(|e| e.ok()) - .map(|e| { - let p = e.path(); - if p.is_dir() { - count_dir_files(&p) - } else { - 1 - } - }) - .sum(), - Err(_) => 0, - } -} - -/// `std::filesystem::path(filename).filename()`, as a String. fn basename_of(path: &str) -> String { Path::new(path) .file_name() @@ -556,20 +409,6 @@ fn basename_of(path: &str) -> String { .unwrap_or_default() } -/// Two-stage read of the footage filename. -fn footage_filename(footage: CHandle) -> String { - let needed = - unsafe { bridge::node::oaknode_footage_filename(footage, std::ptr::null_mut(), 0) }; - if needed <= 0 { - return String::new(); - } - let mut buf = vec![0i8; needed as usize]; - unsafe { - bridge::node::oaknode_footage_filename(footage, buf.as_mut_ptr(), needed); - } - crate::project::load::buf_to_string(&buf) -} - /// Substitute `number` into the trailing-digit field of an image-sequence /// filename (mirrors `oakcodec_decoder_transform_image_sequence_file_name`). fn transform_sequence_filename(filename: &str, number: i64, digit_count: i32) -> String { @@ -613,15 +452,3 @@ fn parse_rational(s: &str) -> Option<(i32, i32)> { let den: i32 = parts.next()?.trim().parse().ok()?; Some((num, den)) } - -/// Convenience used by the C ABI factory to compute the title. -pub(crate) fn import_title(paths: &[String]) -> String { - let count = count_files_recursive(paths).max(1); - format!("Importing {count} file(s)") -} - -/// Convenience used by the C ABI factory to compute the progress -/// denominator (mirrors the C++ `file_count_`). -pub(crate) fn import_file_count(paths: &[String]) -> usize { - count_files_recursive(paths).max(1) -} diff --git a/crates/oaktask/src/project/load.rs b/crates/oaktask/src/project/load.rs index f0e804584..aae53ec12 100644 --- a/crates/oaktask/src/project/load.rs +++ b/crates/oaktask/src/project/load.rs @@ -17,26 +17,23 @@ //! `ProjectLoadBaseTask` / `ProjectLoadTask`, mirroring //! `src/task/src/project/load/load.h`. //! -//! Loads an `.oakproj` file into a new `OakNodeProject`. The base task holds -//! the filename and produces the project on `take_project()`; `ProjectLoadTask` -//! is the concrete (OTIO-less) loader. +//! Loads an `.oakproj` file into a new `oaknode::project::Project`. The +//! base task holds the filename and produces the project on +//! `take_project()`; `ProjectLoadTask` is the concrete (OTIO-less) loader. +//! The project itself is loaded through the direct Rust serializer +//! (`oaknode::serializer::load` — single-lib unification; the old oaknode +//! serializer C ABI with its per-code result mapping is gone, so the +//! version/result-code ladder collapses into the error message the XML +//! path used). //! //! CPP-PARITY: src/task/src/project/load/load.h -use crate::bridge; -use crate::error::{Error, Result}; -use crate::ffi::taskhandle::cstr; -use crate::handle::CHandle; -use crate::task::{Task, TaskBehavior}; +use std::sync::{Arc, Mutex}; -/// Serializer result codes (`include/node/serializer.h`). -const OAKNODE_SERIALIZER_RESULT_SUCCESS: i32 = 0; -const OAKNODE_SERIALIZER_RESULT_PROJECT_TOO_OLD: i32 = 1; -const OAKNODE_SERIALIZER_RESULT_PROJECT_TOO_NEW: i32 = 2; -const OAKNODE_SERIALIZER_RESULT_UNKNOWN_VERSION: i32 = 3; -const OAKNODE_SERIALIZER_RESULT_FILE_ERROR: i32 = 4; -const OAKNODE_SERIALIZER_RESULT_XML_ERROR: i32 = 5; -const OAKNODE_SERIALIZER_RESULT_NO_DATA: i32 = 7; +use oaknode::project::Project; + +use crate::error::{Error, Result}; +use crate::task::{Task, TaskBehavior}; /// The base project-load task: owns a source filename and yields a loaded /// project via [`ProjectLoadBaseTask::take_project`]. @@ -46,20 +43,7 @@ pub struct ProjectLoadBaseTask { /// Absolute project filename to load. pub filename: String, /// The loaded project, produced by the task and taken via `take_project`. - loaded_project: Option, -} - -impl Drop for ProjectLoadBaseTask { - fn drop(&mut self) { - // Free the loaded project if it was never taken. - if let Some(project) = &mut self.loaded_project { - if !project.ctx.is_null() { - unsafe { - bridge::node::oaknode_project_free(project); - } - } - } - } + loaded_project: Option>>, } impl ProjectLoadBaseTask { @@ -74,83 +58,49 @@ impl ProjectLoadBaseTask { /// Take ownership of the loaded project. Returns `Err(Error::State)` if /// the task has not loaded a project yet. - pub fn take_project(&mut self) -> Result { + pub fn take_project(&mut self) -> Result>> { self.loaded_project.take().ok_or(Error::State) } /// Store the loaded project (called by `run`). - pub(crate) fn store_project(&mut self, project: CHandle) { + pub(crate) fn store_project(&mut self, project: Arc>) { self.loaded_project = Some(project); } } impl TaskBehavior for ProjectLoadBaseTask { - /// Load the project from `filename` via the oaknode serializer - /// (`bridge::node`), storing the resulting `OakNodeProject`. + /// Load the project from `filename` via the direct oaknode serializer + /// (`oaknode::serializer::load`), storing the resulting project. fn run(&mut self, task: &mut Task) -> Result<()> { - let mut project = unsafe { bridge::node::oaknode_project_init() }; - if project.ctx.is_null() { - task.set_error("Failed to create project"); - return Err(Error::Failed("Failed to create project".to_string())); - } - - unsafe { - bridge::node::oaknode_project_set_filename(project, cstr(&self.filename)); - } - - let mut code = OAKNODE_SERIALIZER_RESULT_FILE_ERROR; - let mut details = [0i8; 512]; - let result = unsafe { - bridge::node::oaknode_serializer_load_from_file( - project, - cstr(&self.filename), - &mut code, - details.as_mut_ptr(), - details.len() as i32, - ) - }; - - let mut success = false; - match code { - OAKNODE_SERIALIZER_RESULT_SUCCESS => success = true, - OAKNODE_SERIALIZER_RESULT_PROJECT_TOO_OLD => { - task.set_error("This project is from a version of Oak Video Editor that is no longer supported in this version."); - } - OAKNODE_SERIALIZER_RESULT_PROJECT_TOO_NEW => { - task.set_error("This project is from a newer version of Oak Video Editor and cannot be opened in this version."); - } - OAKNODE_SERIALIZER_RESULT_UNKNOWN_VERSION => { - task.set_error("Failed to determine project version."); - } - OAKNODE_SERIALIZER_RESULT_FILE_ERROR => { + let xml = match std::fs::read_to_string(&self.filename) { + Ok(xml) => xml, + Err(e) => { task.set_error(&format!( "Failed to read file \"{}\" for reading.", self.filename )); + let _ = e; + return Err(Error::Failed("Failed to load project".to_string())); } - OAKNODE_SERIALIZER_RESULT_XML_ERROR => { + }; + + let project = match oaknode::serializer::load(&xml) { + Ok(project) => project, + Err(e) => { task.set_error(&format!( - "Failed to read XML document. File may be corrupt. Error was: {}", - buf_to_string(&details) + "Failed to read XML document. File may be corrupt. Error was: {e}" )); + return Err(Error::Failed("Failed to load project".to_string())); } - OAKNODE_SERIALIZER_RESULT_NO_DATA => { - task.set_error("Failed to find any data to parse."); - } - _ => task.set_error("Unknown error."), + }; + + { + let mut guard = project.lock().unwrap_or_else(|e| e.into_inner()); + guard.set_filename(&self.filename); } - if result == 0 && success { - self.store_project(project); - return Ok(()); - } - - if !project.ctx.is_null() { - unsafe { - bridge::node::oaknode_project_free(&mut project); - } - } - Err(Error::Failed("Failed to load project".to_string())) + self.store_project(project); + Ok(()) } } @@ -165,12 +115,3 @@ impl TaskBehavior for ProjectLoadTask { ::run(&mut self.base, task) } } - -/// Read a NUL-terminated char buffer into a String (lossy). -pub(crate) fn buf_to_string(buf: &[i8]) -> String { - let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len()); - unsafe { - String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)) - .into_owned() - } -} diff --git a/crates/oaktask/src/project/loadotio.rs b/crates/oaktask/src/project/loadotio.rs index 7c009263f..3e998d4de 100644 --- a/crates/oaktask/src/project/loadotio.rs +++ b/crates/oaktask/src/project/loadotio.rs @@ -17,12 +17,20 @@ //! `LoadOTIOTask`, mirroring `src/task/src/project/loadotio/loadotio.h`. //! //! Loads an OpenTimelineIO (`.otio`) or FCPXML (`.fcpxml`) file into a new -//! `OakNodeProject`, with a configurable import-confirmation callback. The -//! format is dispatched from the filename extension (see +//! project, with a configurable import-confirmation callback. The format +//! is dispatched from the filename extension (see //! [`crate::project::format`]); the document is parsed with the pure-Rust -//! `oakotio` binding (see `README` decision #6) and the project is built -//! through the oaknode / oaktimeline C ABIs exactly like the C++ task, so -//! no OTIO or FCPXML type crosses the oaktask C ABI. +//! `oakotio` binding (see `README` decision #6). +//! +//! **Single-lib note**: the project is built through the direct oaknode +//! domain operations in [`crate::nodeops`] (`Project::new()` + +//! `Project::initialize()`, factory-created sequence/folder/footage/ +//! block/track nodes, graph connections) instead of the deleted oaknode / +//! oaktimeline C ABIs. Track creation uses the task-local +//! [`crate::nodeops::add_track_command`] (see its docs for the +//! oaktimeline-migration note). The loaded project is an +//! `Arc>` stored on the base task +//! (`take_project()`). //! //! CPP-PARITY: src/task/src/project/loadotio/loadotio.cpp @@ -30,12 +38,12 @@ use std::collections::HashMap; use std::path::Path; use std::sync::Mutex; +use oaknode::id::NodeId; +use oaknode::track::TrackType; use oakotio::Serializable; -use crate::bridge; use crate::error::{Error, Result}; -use crate::ffi::taskhandle::cstr; -use crate::handle::CHandle; +use crate::nodeops::{self, NodeRef, ProjectRef}; use crate::project::format::InterchangeFormat; use crate::project::load::ProjectLoadBaseTask; use crate::task::{Task, TaskBehavior}; @@ -92,32 +100,34 @@ impl TaskBehavior for LoadOTIOTask { let timelines = parse_timelines(task, &self.base.filename, format)?; - let mut project = unsafe { bridge::node::oaknode_project_init() }; - if project.ctx.is_null() { - task.set_error("Failed to create project"); - return Err(Error::Failed("Failed to create project".to_string())); - } - unsafe { - bridge::node::oaknode_project_initialize(project); - bridge::node::oaknode_project_set_modified(project, 1); + // Build the project directly through the oaknode domain model + // (the deleted `oaknode_project_init` stub is gone). + let project: ProjectRef = oaknode::project::Project::new(); + { + let mut guard = project.lock().unwrap_or_else(|e| e.into_inner()); + if let Err(e) = guard.initialize() { + let _ = e; + task.set_error("Failed to create project"); + return Err(Error::Failed("Failed to create project".to_string())); + } + guard.set_modified(true); } // Keep track of imported footage - let mut imported_footage: HashMap = HashMap::new(); + let mut imported_footage: HashMap = HashMap::new(); // Generate a list of sequences with the same names as the timelines. // Assumes each timeline has a unique name. let mut unnamed_sequence_count = 0; - let mut sequences: Vec = Vec::new(); + let mut sequences: Vec = Vec::new(); // Variables used for loading bar let mut number_of_clips: f64 = 0.0; for timeline in &timelines { - let sequence = unsafe { bridge::node::oaknode_sequence_create() }; - if sequence.ctx.is_null() { + let Some(sequence) = nodeops::sequence_create(&project) else { continue; - } + }; let label = if !timeline.name().is_empty() { timeline.name().to_string() @@ -127,17 +137,7 @@ impl TaskBehavior for LoadOTIOTask { unnamed_sequence_count += 1; format!("Sequence {unnamed_sequence_count}") }; - unsafe { - bridge::node::oaknode_node_set_label( - bridge::node::oaknode_sequence_as_node(sequence), - cstr(&label), - ); - } - - // Set default params incase they aren't edited. - unsafe { - bridge::node::oaknode_sequence_set_default_parameters(sequence); - } + nodeops::set_node_label(&project, sequence, &label); // Get number of clips for loading bar for track in timeline.tracks().children() { @@ -146,7 +146,7 @@ impl TaskBehavior for LoadOTIOTask { } } - sequences.push(sequence); + sequences.push((project.clone(), sequence)); } if number_of_clips <= 0.0 { number_of_clips = 1.0; @@ -156,7 +156,7 @@ impl TaskBehavior for LoadOTIOTask { // default accepts everything). let sequence_names: Vec = sequences .iter() - .map(|s| node_label_of(unsafe { bridge::node::oaknode_sequence_as_node(*s) })) + .map(|(p, s)| nodeops::node_label(p, *s)) .collect(); let mut confirm = CONFIRM_CALLBACK.lock().unwrap().take(); let accepted = match confirm.as_mut() { @@ -167,62 +167,35 @@ impl TaskBehavior for LoadOTIOTask { if !accepted { // Cancel to indicate to caller that this task did not complete // and to simply dispose of it. The project is never handed to the - // base task, so free it here (the C++ base-task destructor does - // the same). + // base task (the C++ base-task destructor does the same). task.cancel(); - for sequence in &mut sequences { - unsafe { - bridge::node::oaknode_sequence_free(sequence); - } - } - if !project.ctx.is_null() { - unsafe { - bridge::node::oaknode_project_free(&mut project); - } - } return Ok(()); } - let root_folder = unsafe { bridge::node::oaknode_project_root(project) }; + let root_folder = { + let guard = project.lock().unwrap_or_else(|e| e.into_inner()); + guard.root + }; let mut clips_done = 0.0f64; - for (timeline, sequence) in timelines.iter().zip(&sequences) { - let sequence_node = unsafe { bridge::node::oaknode_sequence_as_node(*sequence) }; + for (timeline, (_, sequence)) in timelines.iter().zip(&sequences) { + let sequence_node = *sequence; - unsafe { - bridge::node::oaknode_project_add_node(project, sequence_node); - } - let mut add_seq = unsafe { - bridge::node::oaknode_command_create_folder_add_child(root_folder, sequence_node) - }; - if !add_seq.ctx.is_null() { - unsafe { - bridge::undo::oakundo_command_redo_now(add_seq); - bridge::undo::oakundo_command_free(&mut add_seq); - } - } + let mut add_seq = nodeops::folder_add_child_command( + (project.clone(), root_folder), + (project.clone(), sequence_node), + ); + add_seq.redo_now(); // Create a folder for this sequence's footage - let sequence_footage = unsafe { bridge::node::oaknode_folder_create(project) }; - if !sequence_footage.ctx.is_null() { - unsafe { - bridge::node::oaknode_node_set_label( - bridge::node::oaknode_folder_as_node(sequence_footage), - cstr(timeline.name()), - ); - } - let mut add_folder = unsafe { - bridge::node::oaknode_command_create_folder_add_child( - root_folder, - bridge::node::oaknode_folder_as_node(sequence_footage), - ) - }; - if !add_folder.ctx.is_null() { - unsafe { - bridge::undo::oakundo_command_redo_now(add_folder); - bridge::undo::oakundo_command_free(&mut add_folder); - } - } + let sequence_footage = nodeops::folder_create(&project); + if let Some(folder_id) = sequence_footage { + nodeops::set_node_label(&project, folder_id, timeline.name()); + let mut add_folder = nodeops::folder_add_child_command( + (project.clone(), root_folder), + (project.clone(), folder_id), + ); + add_folder.redo_now(); } // Iterate through tracks @@ -233,8 +206,8 @@ impl TaskBehavior for LoadOTIOTask { // Determine what kind of track it is let track_type = match otio_track.kind() { - "Video" => bridge::node::OAKNODE_TRACK_TYPE_VIDEO, - "Audio" => bridge::node::OAKNODE_TRACK_TYPE_AUDIO, + "Video" => TrackType::Video, + "Audio" => TrackType::Audio, other => { eprintln!("Found unknown track type: {other}"); continue; @@ -242,43 +215,27 @@ impl TaskBehavior for LoadOTIOTask { }; // Create a new track - let mut track_list = CHandle::null(); - unsafe { - bridge::node::oaknode_sequence_get_track_list( - *sequence, - track_type, - &mut track_list, - ); - } - let mut add_track = - unsafe { bridge::timeline::oaktimeline_add_track_command(track_list) }; - if !add_track.ctx.is_null() { - unsafe { - bridge::undo::oakundo_command_redo_now(add_track); - bridge::undo::oakundo_command_free(&mut add_track); - } - } - - let mut track = CHandle::null(); - let mut count = 0; - unsafe { - bridge::node::oaknode_tracklist_get_track_count(track_list, &mut count); - } - if count > 0 { - unsafe { - bridge::node::oaknode_tracklist_get_track_at( - track_list, - count - 1, - &mut track, - ); - } - } - if track.ctx.is_null() { + let Some(track_list) = + nodeops::sequence_track_list(&project, sequence_node, track_type) + else { continue; - } + }; + let mut add_track = + nodeops::add_track_command(project.clone(), track_list); + add_track.redo_now(); + + let track_count = nodeops::tracklist_track_count(&project, track_list); + let track = if track_count > 0 { + nodeops::tracklist_track_at(&project, track_list, track_count - 1) + } else { + None + }; + let Some(track) = track else { + continue; + }; // Get clips from track - let mut previous_block = CHandle::null(); + let mut previous_block: Option = None; let mut prev_block_transition = false; for otio_block in otio_track.children() { @@ -286,35 +243,27 @@ impl TaskBehavior for LoadOTIOTask { break; } - let block = match otio_block.schema_name() { - "Clip" => unsafe { bridge::node::oaknode_block_clip_create() }, - "Gap" => unsafe { bridge::node::oaknode_block_gap_create() }, + let block_kind = match otio_block.schema_name() { + "Clip" => nodeops::BlockKind::Clip, + "Gap" => nodeops::BlockKind::Gap, "Transition" => { // Todo: Look into OTIO supported transitions and add // them to Oak. - unsafe { - bridge::node::oaknode_block_transition_create( - bridge::node::OAKNODE_TRANSITION_CROSS_DISSOLVE, - ) - } + nodeops::BlockKind::Transition } other => { // We don't know what this is yet, just create a gap // for now so that *something* is there. eprintln!("Found unknown block type: {other}"); - unsafe { bridge::node::oaknode_block_gap_create() } + nodeops::BlockKind::Gap } }; - if block.ctx.is_null() { + let Some(block) = nodeops::block_create(&project, block_kind) else { continue; - } - - let block_node = unsafe { bridge::node::oaknode_block_as_node(block) }; - unsafe { - bridge::node::oaknode_project_add_node(project, block_node); - bridge::node::oaknode_node_set_label(block_node, cstr(otio_block.name())); - bridge::node::oaknode_track_append_block(track, block); - } + }; + let block_node = block; + nodeops::set_node_label(&project, block_node, otio_block.name()); + nodeops::track_append_block(&project, track, block_node); if otio_block.schema_name() == "Clip" || otio_block.schema_name() == "Gap" { if let Some(source_range) = otio_block.source_range() { @@ -325,32 +274,31 @@ impl TaskBehavior for LoadOTIOTask { let duration = oakcore_rs::Rational::from_double(duration_seconds); if otio_block.schema_name() == "Clip" { - unsafe { - bridge::node::oaknode_clip_set_media_in( - block, - start_time.numerator() as i32, - start_time.denominator() as i32, - ); - } - } - unsafe { - bridge::node::oaknode_block_set_length_and_media_out( - block, - duration.numerator() as i32, - duration.denominator() as i32, + nodeops::clip_set_media_in( + &project, + block_node, + start_time.numerator(), + start_time.denominator(), ); } + nodeops::block_set_length_and_media_out( + &project, + block_node, + duration.numerator(), + duration.denominator(), + ); } } // If the previous block was a transition, connect the // current block to it. if prev_block_transition { - unsafe { - bridge::node::oaknode_node_connect( + if let Some(prev) = previous_block { + nodeops::node_connect( + &project, block_node, - bridge::node::oaknode_block_as_node(previous_block), - cstr(bridge::node::OAKNODE_TRANSITION_IN_BLOCK_INPUT), + prev, + nodeops::TRANSITION_IN_BLOCK_INPUT, ); } prev_block_transition = false; @@ -361,43 +309,37 @@ impl TaskBehavior for LoadOTIOTask { // clip. let in_offset = otio_transition.in_offset().to_rational(); let out_offset = otio_transition.out_offset().to_rational(); - unsafe { - bridge::node::oaknode_transition_set_offsets_and_length( - block, - in_offset.numerator() as i32, - in_offset.denominator() as i32, - out_offset.numerator() as i32, - out_offset.denominator() as i32, - ); - } + nodeops::transition_set_offsets_and_length( + &project, + block_node, + in_offset.numerator(), + in_offset.denominator(), + out_offset.numerator(), + out_offset.denominator(), + ); - if !previous_block.ctx.is_null() { - unsafe { - bridge::node::oaknode_node_connect( - bridge::node::oaknode_block_as_node(previous_block), - block_node, - cstr(bridge::node::OAKNODE_TRANSITION_OUT_BLOCK_INPUT), - ); - } + if let Some(prev) = previous_block { + nodeops::node_connect( + &project, + prev, + block_node, + nodeops::TRANSITION_OUT_BLOCK_INPUT, + ); } prev_block_transition = true; // Position transition in its own context. - unsafe { - set_own_context_position(block_node); - } + set_own_context_position(&project, block_node); } if otio_block.schema_name() == "Gap" { // Position gap in its own context. - unsafe { - set_own_context_position(block_node); - } + set_own_context_position(&project, block_node); } // Update this after it's used but before any continue // statements. - previous_block = block; + previous_block = Some(block_node); if otio_block.schema_name() == "Clip" { let Some(otio_clip) = otio_block.as_clip() else { @@ -410,115 +352,90 @@ impl TaskBehavior for LoadOTIOTask { // Link footage let footage_url = external.target_url().to_string(); - let probed_item = if let Some(existing) = - imported_footage.get(&footage_url) - { - *existing - } else { - let created = unsafe { - bridge::node::oaknode_footage_create( - project, - cstr(&footage_url), - ) - }; - if !created.ctx.is_null() { - imported_footage.insert(footage_url.clone(), created); + let probed_item: Option = + if let Some(existing) = imported_footage.get(&footage_url) { + Some(existing.clone()) + } else { + let created = + nodeops::footage_create(&project, Some(&footage_url)); + if let Some(created) = created { + imported_footage.insert( + footage_url.clone(), + (project.clone(), created), + ); - let label = Path::new(&footage_url) - .file_name() - .map(|n| n.to_string_lossy().into_owned()) - .unwrap_or_default(); - unsafe { - bridge::node::oaknode_node_set_label( - bridge::node::oaknode_footage_as_node(created), - cstr(&label), + let label = Path::new(&footage_url) + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_default(); + nodeops::set_node_label(&project, created, &label); + + if let Some(folder_id) = sequence_footage { + let mut add_footage = + nodeops::folder_add_child_command( + (project.clone(), folder_id), + (project.clone(), created), + ); + add_footage.redo_now(); + } + } + created.map(|id| (project.clone(), id)) + }; + + if let Some((_, probed_id)) = probed_item { + // Position clip in its own context. + set_own_context_position(&project, block_node); + + // Position footage in its context. + nodeops::node_set_context_position( + &project, block_node, probed_id, -2.0, 0.0, false, + ); + + // Record the clip-footage link in the domain + // model (the C++ finds footage through the + // input chain; the Rust clip records it). + nodeops::clip_set_footage(&project, block_node, probed_id); + + if track_type == TrackType::Video { + if let Some(transform) = factory_create( + &project, + nodeops::TRANSFORM_TYPE_ID, + ) { + nodeops::node_connect( + &project, + probed_id, + transform, + "tex_in", + ); + nodeops::node_connect( + &project, + transform, + block_node, + nodeops::CLIP_TEXTURE_INPUT, + ); + nodeops::node_set_context_position( + &project, block_node, transform, -1.0, 0.0, false, ); } - - if !sequence_footage.ctx.is_null() { - let mut add_footage = unsafe { - bridge::node::oaknode_command_create_folder_add_child( - sequence_footage, - bridge::node::oaknode_footage_as_node(created), - ) - }; - if !add_footage.ctx.is_null() { - unsafe { - bridge::undo::oakundo_command_redo_now(add_footage); - bridge::undo::oakundo_command_free( - &mut add_footage, - ); - } - } - } - } - created - }; - - if !probed_item.ctx.is_null() { - unsafe { - // Position clip in its own context. - set_own_context_position(block_node); - - // Position footage in its context. - bridge::node::oaknode_node_set_context_position( - block_node, - bridge::node::oaknode_footage_as_node(probed_item), - -2.0, - 0.0, - 0, - ); - } - - if track_type == bridge::node::OAKNODE_TRACK_TYPE_VIDEO { - let transform = unsafe { - bridge::node::oaknode_factory_create_from_id(cstr( - bridge::node::OAKNODE_TYPE_TRANSFORM, - )) - }; - if !transform.ctx.is_null() { - unsafe { - bridge::node::oaknode_project_add_node( - project, transform, - ); - bridge::node::oaknode_node_connect( - bridge::node::oaknode_footage_as_node(probed_item), - transform, - cstr("tex_in"), - ); - bridge::node::oaknode_node_connect( - transform, - block_node, - cstr("buffer_in"), - ); - bridge::node::oaknode_node_set_context_position( - block_node, transform, -1.0, 0.0, 0, - ); - } - } } else { - let volume = unsafe { - bridge::node::oaknode_factory_create_from_id(cstr( - bridge::node::OAKNODE_TYPE_VOLUME, - )) - }; - if !volume.ctx.is_null() { - unsafe { - bridge::node::oaknode_project_add_node(project, volume); - bridge::node::oaknode_node_connect( - bridge::node::oaknode_footage_as_node(probed_item), - volume, - cstr("samples_in"), - ); - bridge::node::oaknode_node_connect( - volume, - block_node, - cstr("buffer_in"), - ); - bridge::node::oaknode_node_set_context_position( - block_node, volume, -1.0, 0.0, 0, - ); - } + if let Some(volume) = + factory_create(&project, nodeops::VOLUME_TYPE_ID) + { + nodeops::node_connect( + &project, + probed_id, + volume, + "samples_in", + ); + nodeops::node_connect( + &project, + volume, + block_node, + nodeops::CLIP_TEXTURE_INPUT, + ); + nodeops::node_set_context_position( + &project, block_node, volume, -1.0, 0.0, false, + ); } } } @@ -536,12 +453,18 @@ impl TaskBehavior for LoadOTIOTask { } } +/// Create a node from the factory registry (`oaknode_factory_create_from_id`). +fn factory_create(project: &ProjectRef, type_id: &str) -> Option { + let meta = oaknode::factory::Factory::global().find(type_id)?; + let (core, behavior) = (meta.create)(); + let mut guard = project.lock().unwrap_or_else(|e| e.into_inner()); + Some(guard.graph.add_node(core, behavior)) +} + /// Set the node's position in its own context (the C++ /// `set_own_context_position` helper in loadotio.cpp). -unsafe fn set_own_context_position(node: CHandle) { - unsafe { - bridge::node::oaknode_node_set_context_position(node, node, 0.0, 0.0, 0); - } +fn set_own_context_position(project: &ProjectRef, node: NodeId) { + nodeops::node_set_context_position(project, node, node, 0.0, 0.0, false); } /// Parse the document into one `oakotio::Timeline` per sequence, dispatching @@ -593,16 +516,3 @@ fn parse_timelines( }), } } - -/// Two-stage read of a node's label (the C++ `oaknode_node_get_label` usage). -fn node_label_of(node: CHandle) -> String { - let needed = unsafe { bridge::node::oaknode_node_get_label(node, std::ptr::null_mut(), 0) }; - if needed <= 0 { - return String::new(); - } - let mut buf = vec![0i8; needed as usize]; - unsafe { - bridge::node::oaknode_node_get_label(node, buf.as_mut_ptr(), needed); - } - crate::project::load::buf_to_string(&buf) -} diff --git a/crates/oaktask/src/project/save.rs b/crates/oaktask/src/project/save.rs index 013dd09e8..1545f18ec 100644 --- a/crates/oaktask/src/project/save.rs +++ b/crates/oaktask/src/project/save.rs @@ -16,35 +16,34 @@ //! `ProjectSaveTask`, mirroring `src/task/src/project/save/save.h`. //! -//! Serializes a borrowed `OakNodeProject` to an `.oakproj` file via the -//! oaknode serializer. Optionally writes to an override filename and/or uses -//! compression. +//! Serializes a borrowed `oaknode::project::Project` to an `.oakproj` file +//! via the direct Rust serializer (`oaknode::serializer::save` — +//! single-lib unification; the old oaknode serializer C ABI with its +//! overwrite/compression result ladder is gone, so the overwrite-as-else +//! path collapses into a plain write). Optionally writes to an override +//! filename. //! //! CPP-PARITY: src/task/src/project/save/save.h -use crate::bridge; -use crate::error::{Error, Result}; -use crate::ffi::taskhandle::cstr; -use crate::handle::CHandle; -use crate::project::load::buf_to_string; -use crate::task::{Task, TaskBehavior}; +use std::sync::{Arc, Mutex}; -/// Serializer result codes (`include/node/serializer.h`). -const OAKNODE_SERIALIZER_RESULT_SUCCESS: i32 = 0; -const OAKNODE_SERIALIZER_RESULT_XML_ERROR: i32 = 5; -const OAKNODE_SERIALIZER_RESULT_FILE_ERROR: i32 = 4; -const OAKNODE_SERIALIZER_RESULT_OVERWRITE_ERROR: i32 = 6; +use oaknode::project::Project; + +use crate::error::{Error, Result}; +use crate::task::{Task, TaskBehavior}; /// A project-save task. Borrows its project and never takes ownership. pub struct ProjectSaveTask { /// The shared task base. pub base: Task, - /// Borrowed project to save (borrowed `OakNodeProject`). - pub project: CHandle, + /// Borrowed project to save (`Arc>`). + pub project: Arc>, /// Optional override filename; when empty the project's own filename is /// used. pub override_filename: Option, - /// Whether to compress the serialized output. + /// Whether to compress the serialized output. The direct serializer has + /// no compression mode (the old C ABI compression path is gone), so + /// this flag is retained for API parity but ignored. pub use_compression: bool, } @@ -56,12 +55,17 @@ impl ProjectSaveTask { } impl TaskBehavior for ProjectSaveTask { - /// Serialize the project via the oaknode serializer (`bridge::node`) to - /// the resolved filename. + /// Serialize the project via the direct oaknode serializer + /// (`oaknode::serializer::save`) to the resolved filename. fn run(&mut self, task: &mut Task) -> Result<()> { let using_filename = match &self.override_filename { Some(name) => name.clone(), - None => project_filename(self.project), + None => self + .project + .lock() + .unwrap_or_else(|e| e.into_inner()) + .filename() + .to_string(), }; if using_filename.is_empty() { @@ -71,60 +75,22 @@ impl TaskBehavior for ProjectSaveTask { )); } - let mut code = OAKNODE_SERIALIZER_RESULT_FILE_ERROR; - let mut details = [0i8; 512]; - unsafe { - bridge::node::oaknode_serializer_save_to_file( - self.project, - cstr(&using_filename), - if self.use_compression { 1 } else { 0 }, - &mut code, - details.as_mut_ptr(), - details.len() as i32, - ); - } - - let mut success = false; - match code { - OAKNODE_SERIALIZER_RESULT_SUCCESS => success = true, - OAKNODE_SERIALIZER_RESULT_XML_ERROR => { + let xml = match oaknode::serializer::save( + &self.project.lock().unwrap_or_else(|e| e.into_inner()), + ) { + Ok(xml) => xml, + Err(e) => { + let _ = e; task.set_error("Failed to write XML data."); + return Err(Error::Failed("Failed to save project".to_string())); } - OAKNODE_SERIALIZER_RESULT_FILE_ERROR => { - task.set_error(&format!( - "Failed to open file for writing: {}", - buf_to_string(&details) - )); - } - OAKNODE_SERIALIZER_RESULT_OVERWRITE_ERROR => { - task.set_error(&format!( - "Failed to overwrite \"{}\". Project has been saved as \"{}\" instead.", - using_filename, - buf_to_string(&details) - )); - success = true; - } - _ => task.set_error("Unknown error."), + }; + + if let Err(e) = std::fs::write(&using_filename, xml) { + task.set_error(&format!("Failed to open file for writing: {e}")); + return Err(Error::Failed("Failed to save project".to_string())); } - if success { - Ok(()) - } else { - Err(Error::Failed("Failed to save project".to_string())) - } + Ok(()) } } - -/// Two-stage read of the project's own filename (empty when unset). -pub(crate) fn project_filename(project: CHandle) -> String { - let needed = - unsafe { bridge::node::oaknode_project_filename(project, std::ptr::null_mut(), 0) }; - if needed <= 0 { - return String::new(); - } - let mut buf = vec![0i8; needed as usize]; - unsafe { - bridge::node::oaknode_project_filename(project, buf.as_mut_ptr(), needed); - } - buf_to_string(&buf) -} diff --git a/crates/oaktask/src/project/saveotio.rs b/crates/oaktask/src/project/saveotio.rs index 2cfde3f81..80a3d5c34 100644 --- a/crates/oaktask/src/project/saveotio.rs +++ b/crates/oaktask/src/project/saveotio.rs @@ -16,28 +16,33 @@ //! `SaveOTIOTask`, mirroring `src/task/src/project/saveotio/saveotio.h`. //! -//! Serializes a borrowed `OakNodeProject` to an OpenTimelineIO (`.otio`) or -//! FCPXML (`.fcpxml`) file through the pure-Rust `oakotio` binding (see -//! `README` decision #6): the project's sequences become `OTIO::Timeline`s +//! Serializes a borrowed project to an OpenTimelineIO (`.otio`) or FCPXML +//! (`.fcpxml`) file through the pure-Rust `oakotio` binding (see `README` +//! decision #6): the project's sequences become `OTIO::Timeline`s //! (`serialize_timeline` / `serialize_track` / `serialize_track_list`), //! exactly like the C++ `serialize_*` helpers. The format is dispatched //! from the filename extension (see [`crate::project::format`]) at the -//! final write only — the serialization itself is shared. No OTIO or -//! FCPXML type crosses the oaktask C ABI. +//! final write only — the serialization itself is shared. +//! +//! **Single-lib note**: the project graph is read through the direct +//! oaknode domain operations in [`crate::nodeops`] (folder children, +//! sequence track lists, track blocks, footage parameters) instead of the +//! deleted oaknode C ABI stubs; the project is an +//! `Arc>` instead of a borrowed `CHandle`. //! //! CPP-PARITY: src/task/src/project/saveotio/saveotio.cpp use oakcore_rs::Rational; +use oaknode::id::NodeId; +use oaknode::track::TrackType; use oakotio::{ Clip, Composable, ExternalReference, Gap, MediaReference, RationalTime, Serializable, SerializableCollection, TimeRange, Timeline, Track, Transition, }; -use crate::bridge; use crate::error::{Error, Result}; -use crate::handle::CHandle; +use crate::nodeops::{self, ProjectRef}; use crate::project::format::InterchangeFormat; -use crate::project::load::buf_to_string; use crate::task::{Task, TaskBehavior}; /// `Rational(INT_MIN)` — the initial "no track yet" maximum length @@ -50,8 +55,8 @@ fn rational_min() -> Rational { pub struct SaveOTIOTask { /// The shared task base. pub base: Task, - /// Borrowed project to save (borrowed `OakNodeProject`). - pub project: CHandle, + /// Borrowed project to save (domain project). + pub project: ProjectRef, /// Output OTIO filename. pub filename: String, } @@ -61,54 +66,35 @@ impl SaveOTIOTask { /// sequence has no usable frame rate or a track fails to serialize. /// /// CPP-PARITY: saveotio.cpp (SaveOTIOTask::serialize_timeline) - fn serialize_timeline(sequence: CHandle) -> Option { - let mut otio_timeline = Timeline::new(node_label_of(unsafe { - bridge::node::oaknode_sequence_as_node(sequence) - })); + fn serialize_timeline(project: &ProjectRef, sequence: NodeId) -> Option { + let mut otio_timeline = + Timeline::new(nodeops::node_label(project, sequence)); + // Direct video-params value type (single-lib unification); absent + // params leave rate at 0.0 → the documented `None` path. + let (num, den) = nodeops::sequence_video_params(project, sequence, 0) + .map(|vp| vp.frame_rate()) + .unwrap_or((0, 1)); let mut rate = 0.0f64; - { - let mut num = 0; - let mut den = 1; - let mut vp = CHandle::null(); - if unsafe { bridge::node::oaknode_sequence_get_video_params(sequence, 0, &mut vp) } == 0 - { - unsafe { - bridge::common::oakcommon_videoparams_get_frame_rate(vp, &mut num, &mut den); - } - if den != 0 { - rate = num as f64 / den as f64; - } - if !vp.ctx.is_null() { - unsafe { - bridge::common::oakcommon_videoparams_free(&mut vp); - } - } - } + if den != 0 { + rate = num as f64 / den as f64; } if rate.is_nan() || rate <= 0.0 { return None; } - let mut video_list = CHandle::null(); - let mut audio_list = CHandle::null(); - unsafe { - bridge::node::oaknode_sequence_get_track_list( - sequence, - bridge::node::OAKNODE_TRACK_TYPE_VIDEO, - &mut video_list, - ); - bridge::node::oaknode_sequence_get_track_list( - sequence, - bridge::node::OAKNODE_TRACK_TYPE_AUDIO, - &mut audio_list, - ); - } + let video_list = nodeops::sequence_track_list(project, sequence, TrackType::Video); + let audio_list = nodeops::sequence_track_list(project, sequence, TrackType::Audio); - if !Self::serialize_track_list(video_list, &mut otio_timeline, rate) - || !Self::serialize_track_list(audio_list, &mut otio_timeline, rate) - { - return None; + if let Some(list) = video_list { + if !Self::serialize_track_list(project, list, &mut otio_timeline, rate) { + return None; + } + } + if let Some(list) = audio_list { + if !Self::serialize_track_list(project, list, &mut otio_timeline, rate) { + return None; + } } Some(otio_timeline) @@ -119,28 +105,18 @@ impl SaveOTIOTask { /// /// CPP-PARITY: saveotio.cpp (SaveOTIOTask::serialize_track_list) fn serialize_track_list( - list: CHandle, + project: &ProjectRef, + list: NodeId, otio_timeline: &mut Timeline, sequence_rate: f64, ) -> bool { - if list.ctx.is_null() { - return true; - } - let mut max_track_length = rational_min(); - let mut track_count = 0; - unsafe { - bridge::node::oaknode_tracklist_get_track_count(list, &mut track_count); - } + let track_count = nodeops::tracklist_track_count(project, list); for i in 0..track_count { - let mut track = CHandle::null(); - unsafe { - bridge::node::oaknode_tracklist_get_track_at(list, i, &mut track); - } - if !track.ctx.is_null() { - let length = track_length_of(track); + if let Some(track) = nodeops::tracklist_track_at(project, list, i) { + let length = nodeops::track_length(project, track); if length > max_track_length { max_track_length = length; } @@ -148,15 +124,12 @@ impl SaveOTIOTask { } for i in 0..track_count { - let mut track = CHandle::null(); - unsafe { - bridge::node::oaknode_tracklist_get_track_at(list, i, &mut track); - } - if track.ctx.is_null() { + let Some(track) = nodeops::tracklist_track_at(project, list, i) else { continue; - } + }; - let Some(otio_track) = Self::serialize_track(track, sequence_rate, max_track_length) + let Some(otio_track) = + Self::serialize_track(project, track, sequence_rate, max_track_length) else { return false; }; @@ -175,99 +148,62 @@ impl SaveOTIOTask { /// /// CPP-PARITY: saveotio.cpp (SaveOTIOTask::serialize_track) fn serialize_track( - track: CHandle, + project: &ProjectRef, + track: NodeId, sequence_rate: f64, max_track_length: Rational, ) -> Option { - let mut track_type = bridge::node::OAKNODE_TRACK_TYPE_NONE; - unsafe { - bridge::node::oaknode_track_get_type(track, &mut track_type); - } + let track_type = nodeops::track_type(project, track)?; let kind = match track_type { - bridge::node::OAKNODE_TRACK_TYPE_VIDEO => "Video", - bridge::node::OAKNODE_TRACK_TYPE_AUDIO => "Audio", - other => { - eprintln!("Don't know OTIO track kind for native type {other}"); + TrackType::Video => "Video", + TrackType::Audio => "Audio", + TrackType::Subtitle => { + eprintln!( + "Don't know OTIO track kind for native type {}", + track_type.to_c() + ); return None; } }; let mut otio_track = Track::new(kind); - let mut block_count = 0; - unsafe { - bridge::node::oaknode_track_get_block_count(track, &mut block_count); - } + let block_count = nodeops::track_block_count(project, track); for i in 0..block_count { - let mut block = CHandle::null(); - unsafe { - bridge::node::oaknode_track_get_block_at(track, i, &mut block); - } - if block.ctx.is_null() { + let Some(block) = nodeops::track_block_at(project, track, i) else { continue; - } + }; - let mut kind = bridge::node::OAKNODE_BLOCK_OTHER; - unsafe { - bridge::node::oaknode_block_get_kind(block, &mut kind); - } + let kind = nodeops::block_kind(project, block); let otio_block: Option = match kind { - bridge::node::OAKNODE_BLOCK_CLIP => { - let mut otio_clip = Clip::new(node_label_of(unsafe { - bridge::node::oaknode_block_as_node(block) - })); + nodeops::BlockKind::Clip => { + let mut otio_clip = Clip::new(nodeops::node_label(project, block)); otio_clip.set_source_range(TimeRange::new( - RationalTime::from_rational(block_in_of(block), sequence_rate), - RationalTime::from_rational(block_length_of(block), sequence_rate), + RationalTime::from_rational(block_in_of(project, block), sequence_rate), + RationalTime::from_rational(block_length_of(project, block), sequence_rate), )); - let mut media = CHandle::null(); - unsafe { - bridge::node::oaknode_node_find_input_footage( - bridge::node::oaknode_block_as_node(block), - &mut media, - ); - } - if !media.ctx.is_null() { - let available_range = if track_type - == bridge::node::OAKNODE_TRACK_TYPE_VIDEO - { + let media = nodeops::node_find_input_footage(project, block); + if let Some(media) = media { + let available_range = if track_type == TrackType::Video { // OTIO ExternalReference uses the source clips // frame rate (or sample rate) as opposed to the // sequences rate. - let mut source_frame_rate = 0.0f64; - let mut duration = 0.0f64; - let mut num = 0; - let mut den = 1; - let mut vp = CHandle::null(); - if unsafe { - bridge::node::oaknode_footage_get_video_params(media, 0, &mut vp) - } == 0 - { - unsafe { - bridge::common::oakcommon_videoparams_get_frame_rate( - vp, &mut num, &mut den, - ); - } - if den != 0 { - source_frame_rate = num as f64 / den as f64; - } - let mut dur = 0i64; - unsafe { - bridge::common::oakcommon_videoparams_get_duration( - vp, &mut dur, - ); - } - duration = dur as f64; - if !vp.ctx.is_null() { - unsafe { - bridge::common::oakcommon_videoparams_free(&mut vp); - } - } - } + let (source_frame_rate, duration) = + nodeops::footage_video_params(project, media, 0) + .map(|vp| { + let (num, den) = vp.frame_rate(); + let rate = if den != 0 { + num as f64 / den as f64 + } else { + 0.0 + }; + (rate, vp.duration() as f64) + }) + .unwrap_or((0.0, 0.0)); TimeRange::new( RationalTime::new(0.0, source_frame_rate), RationalTime::new(duration, source_frame_rate), @@ -279,7 +215,7 @@ impl SaveOTIOTask { ) }; - let media_url = footage_filename(media); + let media_url = nodeops::footage_filename(project, media); if !media_url.is_empty() { otio_clip.set_media_reference(MediaReference::ExternalReference( ExternalReference::new(media_url, Some(available_range)), @@ -289,32 +225,25 @@ impl SaveOTIOTask { Some(Composable::Clip(otio_clip)) } - bridge::node::OAKNODE_BLOCK_GAP => Some(Composable::Gap(Gap::new( + nodeops::BlockKind::Gap => Some(Composable::Gap(Gap::new( TimeRange::new( - RationalTime::from_rational(block_in_of(block), 24.0), - RationalTime::from_rational(block_length_of(block), 24.0), + RationalTime::from_rational(block_in_of(project, block), 24.0), + RationalTime::from_rational(block_length_of(project, block), 24.0), ), - node_label_of(unsafe { bridge::node::oaknode_block_as_node(block) }), + nodeops::node_label(project, block), ))), - bridge::node::OAKNODE_BLOCK_TRANSITION => { - let mut otio_transition = Transition::new(node_label_of(unsafe { - bridge::node::oaknode_block_as_node(block) - })); + nodeops::BlockKind::Transition => { + let mut otio_transition = + Transition::new(nodeops::node_label(project, block)); - let (n, d) = transition_offset_of(block, true); - otio_transition.set_in_offset(RationalTime::from_rational( - Rational::new(n as i64, d as i64), - 24.0, - )); - let (n, d) = transition_offset_of(block, false); - otio_transition.set_out_offset(RationalTime::from_rational( - Rational::new(n as i64, d as i64), - 24.0, - )); + let n = transition_offset_of(project, block, true); + otio_transition.set_in_offset(RationalTime::from_rational(n, 24.0)); + let n = transition_offset_of(project, block, false); + otio_transition.set_out_offset(RationalTime::from_rational(n, 24.0)); Some(Composable::Transition(otio_transition)) } - _ => None, + nodeops::BlockKind::Other => None, }; let Some(otio_block) = otio_block else { @@ -364,25 +293,42 @@ impl TaskBehavior for SaveOTIOTask { // Collect sequences from the root folder (non-recursive, matching // the original list_children_of_type behavior closely enough for // OTIO). - let root = unsafe { bridge::node::oaknode_project_root(self.project) }; - if root.ctx.is_null() { + let root = { + let guard = self.project.lock().unwrap_or_else(|e| e.into_inner()); + guard.root + }; + if !root.valid() { task.set_error("Project contains no sequences to export."); return Err(Error::Failed( "Project contains no sequences to export.".to_string(), )); } - let mut sequences: Vec = Vec::new(); - let child_count = unsafe { bridge::node::oaknode_folder_child_count(root) }; - for i in 0..child_count { - let child = unsafe { bridge::node::oaknode_folder_child_at(root, i) }; - if child.ctx.is_null() { - continue; - } - if node_id_of(child) == bridge::node::OAKNODE_TYPE_SEQUENCE { - // Borrowed sequence alias of the child node handle (all - // oaknode handles share the same box layout). - sequences.push(child); + let mut sequences: Vec = Vec::new(); + { + let guard = self.project.lock().unwrap_or_else(|e| e.into_inner()); + let Some(entry) = guard.graph.get(root) else { + task.set_error("Project contains no sequences to export."); + return Err(Error::Failed( + "Project contains no sequences to export.".to_string(), + )); + }; + let Some(folder) = entry + .behavior + .as_any() + .and_then(|a| a.downcast_ref::()) + else { + task.set_error("Project contains no sequences to export."); + return Err(Error::Failed( + "Project contains no sequences to export.".to_string(), + )); + }; + for child in &folder.children { + if let Some(child_entry) = guard.graph.get(*child) { + if child_entry.behavior.type_id() == nodeops::SEQUENCE_TYPE_ID { + sequences.push(*child); + } + } } } @@ -395,12 +341,12 @@ impl TaskBehavior for SaveOTIOTask { let mut serialized: Vec = Vec::with_capacity(sequences.len()); for sequence in &sequences { - match Self::serialize_timeline(*sequence) { + match Self::serialize_timeline(&self.project, *sequence) { Some(timeline) => serialized.push(timeline), None => { task.set_error(&format!( "Failed to serialize sequence \"{}\"", - node_label_of(unsafe { bridge::node::oaknode_sequence_as_node(*sequence) }) + nodeops::node_label(&self.project, *sequence) )); return Err(Error::Failed("Failed to serialize sequence".to_string())); } @@ -449,90 +395,23 @@ impl TaskBehavior for SaveOTIOTask { } } -/// Two-stage read of a node's label. -fn node_label_of(node: CHandle) -> String { - let needed = unsafe { bridge::node::oaknode_node_get_label(node, std::ptr::null_mut(), 0) }; - if needed <= 0 { - return String::new(); - } - let mut buf = vec![0i8; needed as usize]; - unsafe { - bridge::node::oaknode_node_get_label(node, buf.as_mut_ptr(), needed); - } - buf_to_string(&buf) -} - -/// Two-stage read of a node's type id. -fn node_id_of(node: CHandle) -> String { - let needed = unsafe { bridge::node::oaknode_node_get_id(node, std::ptr::null_mut(), 0) }; - if needed <= 0 { - return String::new(); - } - let mut buf = vec![0i8; needed as usize]; - unsafe { - bridge::node::oaknode_node_get_id(node, buf.as_mut_ptr(), needed); - } - buf_to_string(&buf) -} - /// The block's in point as a `Rational` (default 0/1 when unset). -fn block_in_of(block: CHandle) -> Rational { - let mut n = 0; - let mut d = 1; - unsafe { - bridge::node::oaknode_block_get_in(block, &mut n, &mut d); - } - Rational::new(n as i64, d as i64) +fn block_in_of(project: &ProjectRef, block: NodeId) -> Rational { + nodeops::block_in(project, block) } /// The block's length as a `Rational` (default 0/1 when unset). -fn block_length_of(block: CHandle) -> Rational { - let mut n = 0; - let mut d = 1; - unsafe { - bridge::node::oaknode_block_get_length(block, &mut n, &mut d); - } - Rational::new(n as i64, d as i64) -} - -/// The track's total length as a `Rational` (default 0/1 when unset). -fn track_length_of(track: CHandle) -> Rational { - let mut n = 0; - let mut d = 1; - unsafe { - bridge::node::oaknode_track_get_length(track, &mut n, &mut d); - } - Rational::new(n as i64, d as i64) +fn block_length_of(project: &ProjectRef, block: NodeId) -> Rational { + nodeops::block_length(project, block) } /// A transition's in (`in_offset == true`) or out offset as a `Rational`. -fn transition_offset_of(block: CHandle, in_offset: bool) -> (i32, i32) { - let mut n = 0; - let mut d = 1; +fn transition_offset_of(project: &ProjectRef, block: NodeId, in_offset: bool) -> Rational { if in_offset { - unsafe { - bridge::node::oaknode_transition_get_in_offset(block, &mut n, &mut d); - } + nodeops::transition_in_offset(project, block) } else { - unsafe { - bridge::node::oaknode_transition_get_out_offset(block, &mut n, &mut d); - } + nodeops::transition_out_offset(project, block) } - (n, d) -} - -/// Two-stage read of a footage's filename (empty when unset). -fn footage_filename(footage: CHandle) -> String { - let needed = - unsafe { bridge::node::oaknode_footage_filename(footage, std::ptr::null_mut(), 0) }; - if needed <= 0 { - return String::new(); - } - let mut buf = vec![0i8; needed as usize]; - unsafe { - bridge::node::oaknode_footage_filename(footage, buf.as_mut_ptr(), needed); - } - buf_to_string(&buf) } /// The serialized track's duration as an `OTIO::RationalTime` — the sum of diff --git a/crates/oaktask/src/proxy.rs b/crates/oaktask/src/proxy.rs index 52d3c23e3..1b1b802d7 100644 --- a/crates/oaktask/src/proxy.rs +++ b/crates/oaktask/src/proxy.rs @@ -26,9 +26,10 @@ use std::io::{BufRead, BufReader}; use std::process::{Command, Stdio}; -use crate::bridge; +use oakcodec::proxymanager::ProxyManager; +use oakcodec::task::TaskRequest; + use crate::error::{Error, Result}; -use crate::handle::CHandle; use crate::task::{Task, TaskBehavior}; /// The proxy parameters, mirroring `oakcodec_proxy_params` in @@ -75,22 +76,23 @@ impl ProxyTask { /// Build a proxy task from an oakcodec request and proxy params, /// mirroring the C++ constructor (divider-based requests take the source /// fraction). - pub fn new(request: &bridge::codec::OakCodecTaskRequest, params: ProxyParams) -> ProxyTask { - let source = unsafe { crate::ffi::taskhandle::cstr_to_string(request.input_filename) }; - let output = unsafe { crate::ffi::taskhandle::cstr_to_string(request.output_filename) }; + pub fn new(request: &TaskRequest, params: ProxyParams) -> ProxyTask { let mut params = params; if request.proxy_width > 0 && request.proxy_height > 0 { params.width = request.proxy_width; params.height = request.proxy_height; params.divider = 1; } - let title = format!("Generating Proxy {}:{}", source, request.stream_index); + let title = format!( + "Generating Proxy {}:{}", + request.input_filename, request.stream_index + ); ProxyTask { - base: Task::new(&title, CHandle::null()), - source_filename: source, + base: Task::new(&title, None), + source_filename: request.input_filename.to_string(), stream_index: request.stream_index, params, - output_filename: output, + output_filename: request.output_filename.to_string(), duration_seconds: 0.0, } } @@ -207,21 +209,16 @@ impl TaskBehavior for ProxyTask { /// Spawn `ffmpeg` with the built arguments, feed `-progress` lines to /// [`ProxyTask::parse_progress`] and emit them as task progress. fn run(&mut self, task: &mut Task) -> Result<()> { - let mut ffmpeg_buf = [0i8; 1024]; - let found = unsafe { - bridge::codec::oakcodec_proxy_find_ffmpeg( - std::ptr::null(), - ffmpeg_buf.as_mut_ptr(), - ffmpeg_buf.len() as i32, - ) - }; - if found <= 0 { + // Direct call into oakcodec's proxy manager (single-lib + // unification: the old two-stage C ABI getter is gone; an empty + // string means "not found"). + let ffmpeg_path = ProxyManager::find_ffmpeg(""); + if ffmpeg_path.is_empty() { task.set_error( "Failed to generate proxy: ffmpeg executable was not found. Set the ffmpeg path in Preferences > Disk > Proxy Settings.", ); return Err(Error::Failed("ffmpeg executable was not found".to_string())); } - let ffmpeg_path = buf_to_string(&ffmpeg_buf); // Create the output directory if needed. if let Some(parent) = std::path::Path::new(&self.output_filename).parent() { @@ -320,15 +317,6 @@ impl TaskBehavior for ProxyTask { } } -/// Read a NUL-terminated C char buffer into a String. -fn buf_to_string(buf: &[i8]) -> String { - let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len()); - unsafe { - String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)) - .into_owned() - } -} - /// Probe the source duration via `ffprobe` next to ffmpeg; 0.0 when /// unavailable (in which case no intermediate progress is reported). fn probe_source_duration_seconds(ffmpeg_path: &str, source_filename: &str) -> f64 { diff --git a/crates/oaktask/src/render.rs b/crates/oaktask/src/render.rs index 966779732..1d9a0b53d 100644 --- a/crates/oaktask/src/render.rs +++ b/crates/oaktask/src/render.rs @@ -17,84 +17,104 @@ //! `RenderTask` abstract base, mirroring `src/task/src/render/render.h`. //! //! The abstract parent of [`crate::export::ExportTask`] and -//! [`crate::precache::PreCacheTask`]. It renders frames/audio through -//! oakrender tickets (`bridge::render`) and hands each result to a virtual -//! hook (frame/audio downloaded, subtitle encoded) that the subclass -//! implements via the [`RenderTaskBehavior`] trait. +//! [`crate::precache::PreCacheTask`]. It renders frames/audio through the +//! direct `oakrender::ticket::TicketArena` (single-lib unification: the +//! CHandle-based ticket C ABI and `crate::stubs` are gone) and hands each +//! result to a virtual hook that the subclass implements via the +//! [`RenderTaskBehavior`] trait. +//! +//! The viewer node is now a [`crate::nodeops::NodeRef`] (project + node +//! id): footage viewers render through `VideoTicketParams::footage` +//! (single-stream decode), sequence viewers are flattened into an ordered +//! clip montage (`VideoTicketParams::montage`) resolved from the +//! sequence's track lists. Tickets run on the process-wide +//! `oakrender::manager::RenderManager` arena when the manager is +//! initialized, otherwise on a private worker pool + arena owned by this +//! render run. //! //! ## Concurrent render loop //! //! Like the C++ original (`render.cpp`), the loop keeps up to //! `max_inflight` tickets running at once (default -//! `std::thread::available_parallelism()`, matching the C++ -//! `std::max(1, hardware_concurrency)`). Tickets report completion through -//! the `oakrender_ticket_finished_fn` callback (fired on the ticket's own -//! finishing thread), which pushes the finished ticket into a shared queue -//! and wakes the render thread (queue + condvar, exactly the C++ design). -//! Tickets may complete out of order; a **reorder buffer** delivers the -//! results to the hooks in timestamp order — the audio range first (the C++ -//! queue-audio-first order), then every frame in ascending time — so the -//! observable per-frame contract (`frame_downloaded`/`audio_downloaded` in -//! order, progress updates, cancellation between frames) is unchanged. -//! -//! The ticket's finished callback hands the loop a **borrowed copy** of the -//! submitter's handle (`include/render/ticket.h`: "the submitter keeps -//! ownership and releases it"). The loop therefore releases exactly the -//! handle value each submit function returns, once, and never releases the -//! queue's borrowed copies. Cancellation (or a hook error) cancels and -//! waits every in-flight ticket before returning, so every submitted -//! ticket's completion still fires exactly once. +//! `std::thread::available_parallelism()`). Tickets report completion +//! through the arena's boxed completion callback (fired on the ticket's +//! own finishing thread), which pushes the finished ticket into a shared +//! queue and wakes the render thread (queue + condvar, exactly the C++ +//! design). Tickets may complete out of order; a **reorder buffer** +//! delivers the results to the hooks in timestamp order — the audio range +//! first (the C++ queue-audio-first order), then every frame in ascending +//! time — so the observable per-frame contract (`frame_downloaded`/ +//! `audio_downloaded` in order, progress updates, cancellation between +//! frames) is unchanged. //! //! CPP-PARITY: src/task/src/render/render.h use std::collections::{HashMap, VecDeque}; use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Condvar, Mutex}; +use std::sync::{Arc, Condvar, Mutex}; + +use oakcommon::videoparams::VideoParams; +use oaknode::footage::FootageBehavior; +use oaknode::sequence::SequenceBehavior; +use oaknode::track::{TrackBehavior, TrackListBehavior, TrackType}; +use oakrender::ticket::{ + ticket_kind, AudioTicketParams, MontageClip, TicketArena, TicketId, TicketPayload, + TicketResult, VideoTicketParams, +}; +use oakrender::worker::WorkerPool; -use crate::bridge; -use crate::bridge::common::OakColorTransform; -use crate::bridge::node::{cstr, OAKNODE_SEQUENCE_SAMPLES_INPUT, OAKNODE_SEQUENCE_TEXTURE_INPUT}; -use crate::bridge::render::{OakRenderTicket, OakRenderVideoTicketParams}; use crate::error::{Error, Result}; -use crate::handle::CHandle; +use crate::nodeops::{ + find_input_footage, pixel_format_from_code, NodeRef, ProjectRef, +}; use crate::task::Task; use oakcore_rs::{Rational, TimeRange}; -/// `OAKRENDER_TICKET_VIDEO` (`include/render/ticket.h`). -const TICKET_VIDEO: i32 = 0; -/// `OAKRENDER_TICKET_AUDIO` (`include/render/ticket.h`). -const TICKET_AUDIO: i32 = 1; +/// `oakrender::ticket::ticket_kind::VIDEO` re-export (loop-local kind). +const TICKET_VIDEO: i32 = ticket_kind::VIDEO; +/// `oakrender::ticket::ticket_kind::AUDIO` re-export (loop-local kind). +const TICKET_AUDIO: i32 = ticket_kind::AUDIO; /// Overrides applied to a render ticket, mirroring the C++ `ForceParams` /// struct in render.h (fields map onto `oakrender_video_ticket_params`). /// /// CPP-PARITY: src/task/src/render/render.h (ForceParams) +/// +/// The color-matrix and color-output fields of the C ABI params were +/// dropped with the C ABI: the direct ticket arena carries a forced size +/// and pixel format only (the eval producer performs no color +/// management). The matrix fields are retained for API parity but unused. #[derive(Clone, Debug, Default)] pub struct ForceParams { /// Forced output width (0 = off). pub force_width: i32, /// Forced output height (0 = off). pub force_height: i32, - /// Forced color matrix (row-major 4x4); only used when - /// `has_force_matrix` is set. + /// Forced color matrix (row-major 4x4); retained for C++ API parity, + /// not applied by the direct ticket arena. pub force_matrix: [f64; 16], - /// Whether `force_matrix` is in effect. + /// Whether `force_matrix` is in effect; retained for parity, unused. pub has_force_matrix: bool, /// Forced pixel format (`oakcore_rs::PixelFormat` as int; -1 = off). pub force_format: i32, /// Forced channel count (0 = off). pub force_channel_count: i32, - /// Forced color output (borrowed `OakColorProcessor`; empty = none). - pub force_color_output: CHandle, } /// Subclass hooks, standing in for the C++ protected virtuals /// `download_frame`/`frame_downloaded`/`audio_downloaded`/`encode_subtitle`. +/// Frames and audio are the direct `oakrender` value types (the deleted +/// C ABI handed `CHandle`s; single-lib unification delivers the payload +/// values themselves). pub trait RenderTaskBehavior { - /// Called for each rendered frame. - fn frame_downloaded(&mut self, task: &mut Task, frame: CHandle) -> Result<()>; + /// Called for each rendered video frame. + fn frame_downloaded( + &mut self, + task: &mut Task, + frame: &oakrender::texture::Texture, + ) -> Result<()>; /// Called for each rendered audio buffer. - fn audio_downloaded(&mut self, task: &mut Task, buffer: CHandle) -> Result<()>; + fn audio_downloaded(&mut self, task: &mut Task, samples: &oakrender::ticket::AudioSamples) -> Result<()>; /// Called to encode a subtitle. fn encode_subtitle(&mut self, task: &mut Task, text: &str) -> Result<()>; } @@ -104,21 +124,16 @@ pub trait RenderTaskBehavior { pub struct RenderTask { /// The shared task base. pub base: Task, - /// Output video params (borrowed `OakVideoParams`). - pub video_params: CHandle, - /// Output audio params (borrowed `OakAudioParams`; may be empty). - pub audio_params: CHandle, - /// The viewer node being rendered (borrowed `OakNodeNode`). - pub viewer: CHandle, + /// Output video params (value type; `None` = invalid/unavailable). + pub video_params: Option, + /// The node being rendered (footage or sequence, see the module + /// docs). + pub viewer: NodeRef, /// Force params applied to every ticket. pub force_params: ForceParams, /// The subclass behavior. pub behavior: Option>, // --- private render inputs (set by the subclass before render()) --- - /// Borrowed color manager. - color_manager: CHandle, - /// Borrowed frame cache. - cache: CHandle, /// `olive::RenderMode::Mode` as int. mode: i32, /// Whether audio ranges are rendered. @@ -136,24 +151,21 @@ pub struct RenderTask { } impl RenderTask { - /// Create a render task with default (empty) render inputs. + /// Create a render task for `viewer` (a footage or sequence node of + /// `viewer.0`) with default (empty) render inputs. pub fn new( base: Task, - video_params: CHandle, - audio_params: CHandle, - viewer: CHandle, + video_params: Option, + viewer: NodeRef, force_params: ForceParams, behavior: Option>, ) -> RenderTask { RenderTask { base, video_params, - audio_params, viewer, force_params, behavior, - color_manager: CHandle::null(), - cache: CHandle::null(), mode: 0, audio_enabled: false, export_range: TimeRange::new(Rational::new(0, 1), Rational::new(0, 1)), @@ -167,17 +179,12 @@ impl RenderTask { } /// Configure the render inputs before [`RenderTask::render`] (called by - /// the concrete subclasses' `run`). - pub fn set_render_inputs( - &mut self, - color_manager: CHandle, - cache: CHandle, - mode: i32, - audio_enabled: bool, - export_range: TimeRange, - ) { - self.color_manager = color_manager; - self.cache = cache; + /// the concrete subclasses' `run`). The color-manager / frame-cache + /// handle arguments of the deleted C ABI path are gone: the direct + /// ticket arena has no color management (the eval producer ignores it) + /// and the frame cache is keyed by the viewer node identity for + /// pre-cache runs. + pub fn set_render_inputs(&mut self, mode: i32, audio_enabled: bool, export_range: TimeRange) { self.mode = mode; self.audio_enabled = audio_enabled; self.export_range = export_range; @@ -204,17 +211,12 @@ impl RenderTask { } /// Frame duration rational from the video params (falls back to 1/1 - /// when the params are empty/invalid). + /// when the params are absent/invalid). fn timebase(&self) -> Rational { - let mut num = 0; - let mut den = 1; - unsafe { - bridge::common::oakcommon_videoparams_frame_rate_as_time_base( - self.video_params, - &mut num, - &mut den, - ); - } + let Some(params) = &self.video_params else { + return Rational::new(1, 1); + }; + let (num, den) = params.frame_rate_as_time_base(); if num <= 0 || den <= 0 { Rational::new(1, 1) } else { @@ -222,130 +224,290 @@ impl RenderTask { } } + /// The forced output size from [`ForceParams`], or `None`. + fn force_size(&self) -> Option<(i32, i32)> { + if self.force_params.force_width > 0 && self.force_params.force_height > 0 { + Some((self.force_params.force_width, self.force_params.force_height)) + } else { + None + } + } + + /// The forced pixel format from [`ForceParams`], or `None`. + fn force_format(&self) -> Option { + if self.force_params.force_format >= 0 { + Some(pixel_format_from_code(self.force_params.force_format)) + } else { + None + } + } + + /// Flatten the video tracks of `sequence` into an ordered montage + /// (bottom-most track first so the topmost track composites last; + /// `// CPP-PARITY: M12 P0 montage contract`). + fn video_montage(project: &ProjectRef, sequence: oaknode::id::NodeId, time: Rational) -> Vec { + let guard = project + .lock() + .unwrap_or_else(|e| e.into_inner()); + let Some(entry) = guard.graph.get(sequence) else { + return Vec::new(); + }; + let Some(seq) = entry + .behavior + .as_any() + .and_then(|a| a.downcast_ref::()) + else { + return Vec::new(); + }; + let mut montage = Vec::new(); + for list_id in seq.track_lists.iter().filter(|i| i.valid()) { + let Some(le) = guard.graph.get(*list_id) else { + continue; + }; + let Some(list) = le + .behavior + .as_any() + .and_then(|a| a.downcast_ref::()) + else { + continue; + }; + if list.kind != TrackType::Video { + continue; + } + for track_id in list.tracks.iter().rev() { + let Some(te) = guard.graph.get(*track_id) else { + continue; + }; + let Some(track) = te + .behavior + .as_any() + .and_then(|a| a.downcast_ref::()) + else { + continue; + }; + for block_id in &track.blocks { + let Some(core) = crate::nodeops::block_core_of(&guard.graph, *block_id) + else { + continue; + }; + if time < core.in_() || time >= core.out() { + continue; + } + let Some(footage_id) = find_input_footage(&guard.graph, *block_id) else { + continue; + }; + let Some(fe) = guard.graph.get(footage_id) else { + continue; + }; + let Some(footage) = fe + .behavior + .as_any() + .and_then(|a| a.downcast_ref::()) + else { + continue; + }; + montage.push(MontageClip { + filename: footage.filename.clone(), + stream_index: 0, + in_time: core.in_(), + out_time: core.out(), + media_in: core.media_in, + gain: 1.0, + }); + } + } + } + montage + } + + /// Flatten the audio tracks of `sequence` into an audio montage + /// (track order is irrelevant — the mixer accumulates gains). + fn audio_montage(project: &ProjectRef, sequence: oaknode::id::NodeId, time: Rational) -> Vec { + let guard = project + .lock() + .unwrap_or_else(|e| e.into_inner()); + let Some(entry) = guard.graph.get(sequence) else { + return Vec::new(); + }; + let Some(seq) = entry + .behavior + .as_any() + .and_then(|a| a.downcast_ref::()) + else { + return Vec::new(); + }; + let mut montage = Vec::new(); + for list_id in seq.track_lists.iter().filter(|i| i.valid()) { + let Some(le) = guard.graph.get(*list_id) else { + continue; + }; + let Some(list) = le + .behavior + .as_any() + .and_then(|a| a.downcast_ref::()) + else { + continue; + }; + if list.kind != TrackType::Audio { + continue; + } + for track_id in &list.tracks { + let Some(te) = guard.graph.get(*track_id) else { + continue; + }; + let Some(track) = te + .behavior + .as_any() + .and_then(|a| a.downcast_ref::()) + else { + continue; + }; + for block_id in &track.blocks { + let Some(core) = crate::nodeops::block_core_of(&guard.graph, *block_id) + else { + continue; + }; + if time < core.in_() || time >= core.out() { + continue; + } + let Some(footage_id) = find_input_footage(&guard.graph, *block_id) else { + continue; + }; + let Some(fe) = guard.graph.get(footage_id) else { + continue; + }; + let Some(footage) = fe + .behavior + .as_any() + .and_then(|a| a.downcast_ref::()) + else { + continue; + }; + montage.push(MontageClip { + filename: footage.filename.clone(), + stream_index: 0, + in_time: core.in_(), + out_time: core.out(), + media_in: core.media_in, + gain: 1.0, + }); + } + } + } + montage + } + + /// Build the video ticket params for `time` (mirrors the C++ + /// `start_video_ticket` param marshalling). + fn build_video_ticket(&self, time: Rational) -> Result { + let (project, viewer_id) = &self.viewer; + // Inspect the viewer node WITHOUT holding the project lock across + // the montage build below: `video_montage` locks the same project, + // and `std::sync::Mutex` is not reentrant — holding it here + // self-deadlocks every sequence export on the driving thread + // (reproduced by the facade's `it_export` suite). The borrow + // (`footage.filename`) is cloned before the lock drops. + let footage = { + let guard = project.lock().unwrap_or_else(|e| e.into_inner()); + let Some(entry) = guard.graph.get(*viewer_id) else { + return Err(Error::Failed( + "No node connected to the viewer output".to_string(), + )); + }; + let any = entry.behavior.as_any(); + if let Some(footage) = any.and_then(|a| a.downcast_ref::()) { + Some(footage.filename.clone()) + } else if any + .and_then(|a| a.downcast_ref::()) + .is_some() + { + None + } else { + return Err(Error::Failed( + "No node connected to the viewer output".to_string(), + )); + } + }; + match footage { + Some(filename) => Ok(VideoTicketParams { + viewer: viewer_id.identity(), + time, + force_size: self.force_size(), + force_format: self.force_format(), + cache: Some(viewer_id.identity()), + cache_dir: None, + cache_id: None, + cache_timebase: None, + footage: Some((filename, 0)), + montage: Vec::new(), + }), + // Sequence viewer: the montage is resolved without the lock. + None => { + let montage = Self::video_montage(project, *viewer_id, time); + Ok(VideoTicketParams { + viewer: viewer_id.identity(), + time, + force_size: self.force_size(), + force_format: self.force_format(), + cache: None, + cache_dir: None, + cache_id: None, + cache_timebase: None, + footage: None, + montage, + }) + } + } + } + + /// Build the audio ticket params for `range` (the Rust API renders a + /// single range; the C++ submits one ticket per audio range). + fn build_audio_ticket(&self, range: TimeRange) -> Result { + let (project, viewer_id) = &self.viewer; + let (sample_rate, channel_layout) = crate::nodeops::sequence_audio_params(project, *viewer_id); + let montage = Self::audio_montage(project, *viewer_id, range.in_()); + Ok(AudioTicketParams { + viewer: viewer_id.identity(), + range, + sample_rate, + channel_layout, + montage, + }) + } + /// Submit one video frame ticket at `time` (mirrors the C++ /// `start_video_ticket`). `dispatch` is the shared completion channel /// handed to the ticket's finished callback. fn submit_video_ticket( &self, + arena: &TicketArena, time: Rational, dispatch: *mut RenderDispatch, - ) -> Result { - let mut output_node = CHandle::null(); - unsafe { - bridge::node::oaknode_node_input_get_connected_node( - self.viewer, - cstr(OAKNODE_SEQUENCE_TEXTURE_INPUT), - &mut output_node, - ); - } - if output_node.ctx.is_null() { - return Err(Error::Failed( - "No node connected to the viewer output".to_string(), - )); - } - - let audio_params_ptr = if self.audio_params.ctx.is_null() { - std::ptr::null() - } else { - &self.audio_params as *const CHandle - }; - let force = &self.force_params; - let params = OakRenderVideoTicketParams { - output_node, - video_params: self.video_params, - audio_params: audio_params_ptr, - time_num: time.numerator(), - time_den: time.denominator(), - color_manager: self.color_manager, - mode: self.mode, - force_width: force.force_width, - force_height: force.force_height, - force_matrix: force.force_matrix, - has_force_matrix: if force.has_force_matrix { 1 } else { 0 }, - force_format: force.force_format, - force_channel_count: force.force_channel_count, - force_color_output: force.force_color_output, - force_color_transform: OakColorTransform { - ctx: std::ptr::null_mut(), - addref: None, - release: None, - abi_version: 0, - }, - cache: self.cache, - footage_filename: std::ptr::null(), - footage_stream: 0, - montage: std::ptr::null(), - montage_count: 0, - }; - - let ticket = unsafe { - bridge::render::oakrender_ticket_render_frame( - ¶ms, - Some(ticket_finished), - dispatch as *mut std::ffi::c_void, - ) - }; - // Per-frame call: release the borrowed handle box (the ticket keeps - // the native node, not the handle). - unsafe { - bridge::node::oaknode_node_free(&mut output_node); - } - if ticket.ctx.is_null() { - return Err(Error::Failed( - "Failed to start frame render ticket".to_string(), - )); - } - Ok(ticket) + ) -> Result { + let params = self.build_video_ticket(time)?; + let id = arena.next_id(); + let dispatch_ptr = DispatchPtr(dispatch); + arena.submit_video_with_id(id, params, Box::new(move |result| { + push_finished(id, result, dispatch_ptr); + })); + Ok(id) } - /// Submit one audio ticket for `range` (the Rust API renders a single - /// range; the C++ submits one ticket per audio range). + /// Submit one audio ticket for `range` (see + /// [`RenderTask::submit_video_ticket`]). fn submit_audio_ticket( &self, + arena: &TicketArena, range: TimeRange, dispatch: *mut RenderDispatch, - ) -> Result { - let mut output_node = CHandle::null(); - unsafe { - bridge::node::oaknode_node_input_get_connected_node( - self.viewer, - cstr(OAKNODE_SEQUENCE_SAMPLES_INPUT), - &mut output_node, - ); - } - if output_node.ctx.is_null() { - return Err(Error::Failed( - "No node connected to the viewer samples input".to_string(), - )); - } - - let audio_params_ptr = if self.audio_params.ctx.is_null() { - std::ptr::null() - } else { - &self.audio_params as *const CHandle - }; - let ticket = unsafe { - bridge::render::oakrender_ticket_render_audio( - output_node, - range.in_().numerator(), - range.in_().denominator(), - range.out().numerator(), - range.out().denominator(), - audio_params_ptr, - self.mode, - Some(ticket_finished), - dispatch as *mut std::ffi::c_void, - ) - }; - unsafe { - bridge::node::oaknode_node_free(&mut output_node); - } - if ticket.ctx.is_null() { - return Err(Error::Failed( - "Failed to start audio render ticket".to_string(), - )); - } - Ok(ticket) + ) -> Result { + let params = self.build_audio_ticket(range)?; + let id = arena.next_id(); + let dispatch_ptr = DispatchPtr(dispatch); + arena.submit_audio_with_id(id, params, Box::new(move |result| { + push_finished(id, result, dispatch_ptr); + })); + Ok(id) } /// Submit a video ticket and account it as in-flight. @@ -353,20 +515,20 @@ impl RenderTask { /// The in-flight count is bumped **before** the submit: a ticket can /// complete synchronously (its callback fires before the submit returns), /// so the callback's decrement must always see its increment. A failed - /// submit (null ticket) never fires a callback, so the bump is rolled - /// back. + /// submit never fires a callback, so the bump is rolled back. fn start_video_ticket( &self, + arena: &TicketArena, time: Rational, dispatch: *mut RenderDispatch, - in_flight: &mut Vec, + in_flight: &mut Vec, ) -> Result<()> { unsafe { (&*dispatch).running.fetch_add(1, Ordering::SeqCst); } - match self.submit_video_ticket(time, dispatch) { - Ok(ticket) => { - in_flight.push(ticket); + match self.submit_video_ticket(arena, time, dispatch) { + Ok(id) => { + in_flight.push(id); Ok(()) } Err(e) => { @@ -382,16 +544,17 @@ impl RenderTask { /// [`RenderTask::start_video_ticket`] for the counting contract). fn start_audio_ticket( &self, + arena: &TicketArena, range: TimeRange, dispatch: *mut RenderDispatch, - in_flight: &mut Vec, + in_flight: &mut Vec, ) -> Result<()> { unsafe { (&*dispatch).running.fetch_add(1, Ordering::SeqCst); } - match self.submit_audio_ticket(range, dispatch) { - Ok(ticket) => { - in_flight.push(ticket); + match self.submit_audio_ticket(arena, range, dispatch) { + Ok(id) => { + in_flight.push(id); Ok(()) } Err(e) => { @@ -407,26 +570,25 @@ impl RenderTask { /// audio tickets, the matching frame slot for video tickets. fn classify_ticket( &self, - ticket: &OakRenderTicket, + arena: &TicketArena, + id: TicketId, slot_by_key: &HashMap<(i32, i64, i64), usize>, ) -> Option { - let kind = unsafe { bridge::render::oakrender_ticket_get_type(*ticket) }; + let kind = arena.kind(id)?; if kind == TICKET_AUDIO { - let mut a = 0i64; - let mut b = 1i64; - let mut c = 0i64; - let mut d = 1i64; - unsafe { - bridge::render::oakrender_ticket_get_range(*ticket, &mut a, &mut b, &mut c, &mut d); - } - slot_by_key.get(&(TICKET_AUDIO, a, b)).copied() + let range = arena.range(id)?; + slot_by_key + .get(&( + TICKET_AUDIO, + range.in_().numerator(), + range.in_().denominator(), + )) + .copied() } else if kind == TICKET_VIDEO { - let mut n = 0i64; - let mut d = 1i64; - unsafe { - bridge::render::oakrender_ticket_get_time(*ticket, &mut n, &mut d); - } - slot_by_key.get(&(TICKET_VIDEO, n, d)).copied() + let time = arena.time(id)?; + slot_by_key + .get(&(TICKET_VIDEO, time.numerator(), time.denominator())) + .copied() } else { None } @@ -486,32 +648,51 @@ impl RenderTask { } let total_slots = slots.len(); - // The shared completion channel. Heap-allocated: the C callback - // reaches it through its `userdata` pointer. Freed only once every - // in-flight ticket has fired its callback (`running == 0`). + // The ticket arena: the process-wide manager arena when the + // manager is initialized, otherwise a private worker pool + arena + // owned by this run (keeps headless/test runs self-contained). + let (arena, mut private_pool) = match oakrender::manager::RenderManager::global() { + Some(manager) => (manager.tickets.clone(), None), + None => { + let mut pool = WorkerPool::new(0); + pool.start(); + let producer: oakrender::ticket::Producer = Arc::new(|time, params| { + oakrender::eval::render_produced_frame(time, params) + .map(TicketPayload::Video) + }); + let arena = Arc::new(TicketArena::new(pool.clone(), producer)); + (arena, Some(pool)) + } + }; + + // The shared completion channel. Heap-allocated: the ticket + // completion callback reaches it through its captured pointer. + // Freed only once every in-flight ticket has fired its callback + // (`running == 0`). let dispatch = Box::into_raw(Box::new(RenderDispatch::new())); // Shared view of the same state for the render thread (the raw // pointer stays valid until the box is freed below). let dispatch_ref = unsafe { &*dispatch }; let window = self.max_inflight.max(1); - // Tickets we hold (the submitter's copy of each handle, released - // exactly once at the end); `consumed` counts tickets whose finished - // queue copy has been popped, so `in_flight.len() - consumed` is the - // live window. - let mut in_flight: Vec = Vec::new(); + // Tickets we hold (the submitter's ids); `consumed` counts tickets + // whose finished queue copy has been popped, so + // `in_flight.len() - consumed` is the live window. + let mut in_flight: Vec = Vec::new(); let mut consumed = 0usize; // Next frame timestamp to submit and next delivery slot. let mut next_frame_index = 0usize; let mut next_slot = 0usize; // Reorder buffer: finished tickets not yet deliverable. - let mut pending: HashMap = HashMap::new(); + let mut pending: HashMap = HashMap::new(); let mut progress_counter = 0.0; let mut result: Result<()> = Ok(()); // Queue audio first (mirrors the C++ order). if self.audio_enabled && result.is_ok() { - if let Err(e) = self.start_audio_ticket(self.export_range, dispatch, &mut in_flight) { + if let Err(e) = + self.start_audio_ticket(&arena, self.export_range, dispatch, &mut in_flight) + { result = Err(e); } } @@ -519,9 +700,12 @@ impl RenderTask { // Start the initial frame window. if result.is_ok() { while in_flight.len() < window && next_frame_index < frame_times.len() { - if let Err(e) = - self.start_video_ticket(frame_times[next_frame_index], dispatch, &mut in_flight) - { + if let Err(e) = self.start_video_ticket( + &arena, + frame_times[next_frame_index], + dispatch, + &mut in_flight, + ) { result = Err(e); break; } @@ -531,11 +715,11 @@ impl RenderTask { while result.is_ok() && !task.is_cancelled() && next_slot < total_slots { // Drain the completion queue into the reorder buffer. - while let Some(ticket) = dispatch_ref.pop_finished() { + while let Some((id, ticket_result)) = dispatch_ref.pop_finished() { consumed += 1; - match self.classify_ticket(&ticket, &slot_by_key) { + match self.classify_ticket(&arena, id, &slot_by_key) { Some(slot_index) => { - pending.insert(slot_index, ticket); + pending.insert(slot_index, (id, ticket_result)); } None => { result = Err(Error::Failed( @@ -550,33 +734,53 @@ impl RenderTask { } // Deliver contiguous results in the observable (timestamp) order. - while let Some(ticket) = pending.remove(&next_slot) { + while let Some((_id, ticket_result)) = pending.remove(&next_slot) { let slot = &slots[next_slot]; if slot.kind == TICKET_AUDIO { - unsafe { - bridge::render::oakrender_ticket_get_samples(ticket, std::ptr::null_mut()); - } - if let Err(e) = behavior.audio_downloaded(task, CHandle::null()) { - result = Err(e); - break; - } - } else { - let mut frame = CHandle::null(); - unsafe { - bridge::render::oakrender_ticket_get_frame(ticket, &mut frame); - } - if let Err(e) = behavior.frame_downloaded(task, frame) { - result = Err(e); - break; - } - if !frame.ctx.is_null() { - unsafe { - bridge::render::oakrender_codec_frame_free(&mut frame); + match ticket_result { + Ok(TicketPayload::Audio(samples)) => { + if let Err(e) = behavior.audio_downloaded(task, &samples) { + result = Err(e); + break; + } + } + Ok(_) => { + result = Err(Error::Failed( + "Audio render ticket delivered a non-audio payload".to_string(), + )); + break; + } + Err(e) => { + result = Err(Error::Failed(format!( + "Audio render ticket failed: {e:?}" + ))); + break; } } - if self.native_progress_signalling { - progress_counter += 1.0; - task.emit_progress(progress_counter / total_length); + } else { + match ticket_result { + Ok(TicketPayload::Video(texture)) => { + if let Err(e) = behavior.frame_downloaded(task, &texture) { + result = Err(e); + break; + } + if self.native_progress_signalling { + progress_counter += 1.0; + task.emit_progress(progress_counter / total_length); + } + } + Ok(_) => { + result = Err(Error::Failed( + "Video render ticket delivered a non-video payload".to_string(), + )); + break; + } + Err(e) => { + result = Err(Error::Failed(format!( + "Frame render ticket failed: {e:?}" + ))); + break; + } } } next_slot += 1; @@ -589,9 +793,12 @@ impl RenderTask { while in_flight.len().saturating_sub(consumed) < window && next_frame_index < frame_times.len() { - if let Err(e) = - self.start_video_ticket(frame_times[next_frame_index], dispatch, &mut in_flight) - { + if let Err(e) = self.start_video_ticket( + &arena, + frame_times[next_frame_index], + dispatch, + &mut in_flight, + ) { result = Err(e); break; } @@ -624,7 +831,8 @@ impl RenderTask { .finished .lock() .unwrap_or_else(|e| e.into_inner()) - .is_empty() && dispatch_ref.running.load(Ordering::SeqCst) == 0 + .is_empty() + && dispatch_ref.running.load(Ordering::SeqCst) == 0 { // Every ticket finished and its queue copy was consumed. break; @@ -641,25 +849,21 @@ impl RenderTask { // Tear down. On cancellation or error, cancel and wait every ticket // still in flight (the C++ abort path), so their completions still - // fire exactly once. Then wait until every callback has returned, - // release the submitter's handles, and free the completion channel. + // fire exactly once. Then wait until every callback has returned + // and free the completion channel; a private pool is shut down. if result.is_err() || task.is_cancelled() { - for &ticket in &in_flight { - unsafe { - bridge::render::oakrender_ticket_cancel(ticket); - bridge::render::oakrender_ticket_wait(ticket); - } + for &id in &in_flight { + arena.cancel(id); + let _ = arena.wait(id); } } dispatch_ref.wait_idle(); - for ticket in in_flight.iter_mut() { - unsafe { - bridge::render::oakrender_ticket_free(ticket); - } - } unsafe { drop(Box::from_raw(dispatch)); } + if let Some(mut pool) = private_pool.take() { + pool.shutdown(); + } result } @@ -669,10 +873,12 @@ impl RenderTask { /// (avoids a self-referential borrow). Never used for actual rendering. pub(crate) fn placeholder() -> RenderTask { RenderTask::new( - Task::new("", CHandle::null()), - CHandle::null(), - CHandle::null(), - CHandle::null(), + Task::new("", None), + None, + ( + oaknode::project::Project::new(), + oaknode::id::NodeId::INVALID, + ), ForceParams::default(), None, ) @@ -691,12 +897,12 @@ struct DeliverySlot { /// Shared state between the render thread and the ticket-finished /// callbacks (the ticket's async return channel, mirroring the C++ /// `finished_mutex_`/`finished_tickets_`/`finished_wait_cond_`). One -/// instance per [`RenderTask::render`] run, reached from the C callback -/// through its `userdata` pointer; heap-allocated for the run and freed -/// only after every in-flight ticket has fired (`running == 0`). +/// instance per [`RenderTask::render`] run, reached from the completion +/// callback through its captured pointer; heap-allocated for the run and +/// freed only after every in-flight ticket has fired (`running == 0`). struct RenderDispatch { /// Finished tickets in completion order (callbacks push here). - finished: Mutex>, + finished: Mutex>, /// Tickets whose completion callback has not fired yet. running: AtomicUsize, /// Wakes the render thread when a ticket finishes. @@ -712,9 +918,8 @@ impl RenderDispatch { } } - /// Pop the next finished ticket (the callback's borrowed copy; `None` - /// when the queue is empty). - fn pop_finished(&self) -> Option { + /// Pop the next finished ticket; `None` when the queue is empty. + fn pop_finished(&self) -> Option<(TicketId, TicketResult)> { self.finished .lock() .unwrap_or_else(|e| e.into_inner()) @@ -731,21 +936,32 @@ impl RenderDispatch { } } +/// `Send` wrapper for the raw dispatch pointer captured by the ticket +/// completion callbacks (the arena's [`oakrender::ticket::Completion`] is +/// `Send`; the pointee outlives every ticket — see [`RenderTask::render`]'s +/// `wait_idle` before freeing it). +struct DispatchPtr(*mut RenderDispatch); + +// Safety: the raw pointer is exclusively owned by the render run; the +// completion callbacks only dereference it while the run is alive +// (`wait_idle` guarantees every callback returned before the free). +unsafe impl Send for DispatchPtr {} + /// Ticket-finished callback, mirroring the C++ `on_ticket_finished`. Fires -/// on the ticket's finishing thread; pushes the ticket's borrowed handle -/// copy into the completion queue and wakes the render thread. The push -/// precedes the in-flight decrement, so a waiter never observes -/// `running == 0` with a missing queue entry. +/// on the ticket's finishing thread; pushes the ticket's result into the +/// completion queue and wakes the render thread. The push precedes the +/// in-flight decrement, so a waiter never observes `running == 0` with a +/// missing queue entry. /// /// # Safety /// -/// `userdata` must point to a live `RenderDispatch` for the duration of -/// every in-flight ticket (guaranteed by [`RenderTask::render`]'s -/// `wait_idle` before freeing it). -unsafe extern "C" fn ticket_finished(ticket: OakRenderTicket, userdata: *mut std::ffi::c_void) { - let dispatch = unsafe { &*(userdata as *const RenderDispatch) }; +/// The wrapped pointer must point to a live `RenderDispatch` for the +/// duration of every in-flight ticket (guaranteed by +/// [`RenderTask::render`]'s `wait_idle` before freeing it). +fn push_finished(id: TicketId, result: TicketResult, dispatch: DispatchPtr) { + let dispatch = unsafe { &*dispatch.0 }; let mut queue = dispatch.finished.lock().unwrap_or_else(|e| e.into_inner()); - queue.push_back(ticket); + queue.push_back((id, result)); dispatch.running.fetch_sub(1, Ordering::SeqCst); dispatch.cv.notify_all(); } diff --git a/crates/oaktask/src/task.rs b/crates/oaktask/src/task.rs index 0db65c183..d3c6918d5 100644 --- a/crates/oaktask/src/task.rs +++ b/crates/oaktask/src/task.rs @@ -20,8 +20,9 @@ //! concrete task (`ConformTask`, `ProxyTask`, …) is its own struct and the //! shared lifecycle lives here; the per-task work is supplied through //! [`TaskBehavior`] (a trait object, per architectural decision #1 in -//! README.md). Cancellation rides on a borrowed oakrender `OakCancelAtom` -//! reached through `crate::bridge::render`. +//! README.md). Cancellation rides on a shared +//! `oakcommon::cancelatom::CancelAtom` (single-lib unification: the old +//! oakrender cancelatom C ABI is gone). //! //! CPP-PARITY: src/task/src/task.h //! @@ -38,9 +39,9 @@ use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::{Arc, Condvar, Mutex}; -use crate::bridge; +use oakcommon::cancelatom::CancelAtom; + use crate::error::{Error, Result}; -use crate::handle::CHandle; /// Event type emitted through a task's [`EventListener`], mirroring the /// C++ `EventType` enum (`k_event_started`/`k_event_progress`/`k_event_finished`). @@ -74,7 +75,7 @@ pub trait TaskBehavior { } /// Values the subscribe wrapper needs to re-encode [`TaskEvent`]s into the -/// C ABI callback signature `(event_id, value, userdata)`. The C++ emits +/// legacy `(event_id, value, userdata)` callback signature. The C++ emits /// the start timestamp with `k_event_started` and 1.0/0.0 with /// `k_event_finished`; `TaskEvent` carries neither, so the task publishes /// them into this shared state before emitting (decision #1 in README.md). @@ -97,14 +98,13 @@ struct TaskDone { succeeded: bool, } -/// The base task. Owns lifecycle state plus a borrowed oakrender cancel -/// atom; the concrete behavior lives in a [`TaskBehavior`] trait object. +/// The base task. Owns lifecycle state plus a shared `CancelAtom`; the +/// concrete behavior lives in a [`TaskBehavior`] trait object. pub struct Task { title: String, error: Option, start_time: Option, - cancel_atom: CHandle, - owns_atom: bool, + cancel_atom: Arc, event_listener: Option, cancel_event: Option>, started: bool, @@ -113,40 +113,21 @@ pub struct Task { behavior: Option>, /// Finish/success flag pair + wakeup condvar (race-free readers). done: Arc<(Mutex, Condvar)>, - /// Values published for the C ABI subscribe wrapper. + /// Values published for the legacy subscribe wrapper. subscriber: Option>, } -impl Drop for Task { - fn drop(&mut self) { - // Free the cancellation atom only when we created it ourselves; - // borrowed atoms are released by their owner. - if self.owns_atom && !self.cancel_atom.is_null() { - let mut atom = self.cancel_atom; - unsafe { - bridge::render::oakrender_cancelatom_free(&mut atom); - } - } - } -} - impl Task { - /// Create a new task with the given title and a borrowed oakrender - /// cancel atom. When `cancel_atom` is empty a fresh atom is created and - /// owned by the task (mirroring the C++ constructor). - pub fn new(title: &str, cancel_atom: CHandle) -> Task { - let (cancel_atom, owns_atom) = if cancel_atom.is_null() { - let atom = unsafe { bridge::render::oakrender_cancelatom_init() }; - (atom, true) - } else { - (cancel_atom, false) - }; + /// Create a new task with the given title. When `cancel_atom` is `None` + /// a fresh atom is created and owned by the task (mirroring the C++ + /// constructor); `Some` shares the caller's atom (mirrors the old + /// borrowed-oakrender-atom constructor). + pub fn new(title: &str, cancel_atom: Option>) -> Task { Task { title: title.to_string(), error: None, start_time: None, - cancel_atom, - owns_atom, + cancel_atom: cancel_atom.unwrap_or_else(|| Arc::new(CancelAtom::new())), event_listener: None, cancel_event: None, started: false, @@ -207,50 +188,29 @@ impl Task { ret } - /// Request cancellation through the borrowed oakrender cancel atom, then - /// invoke the cancel event callback if one is registered. + /// Request cancellation through the shared cancel atom, then invoke the + /// cancel event callback if one is registered. pub fn cancel(&mut self) { - if !self.cancel_atom.is_null() { - let atom = self.cancel_atom; - unsafe { - bridge::render::oakrender_cancelatom_cancel(atom); - } - } + self.cancel_atom.cancel(); if let Some(cb) = self.cancel_event.as_mut() { cb(); } } - /// Whether cancellation was requested (queries the oakrender atom). + /// Whether cancellation was requested (queries the shared atom). pub fn is_cancelled(&self) -> bool { - if self.cancel_atom.is_null() { - return false; - } - let atom = self.cancel_atom; - let mut cancelled = 0; - unsafe { - bridge::render::oakrender_cancelatom_is_cancelled(atom, &mut cancelled); - } - cancelled != 0 + self.cancel_atom.is_cancelled() } - /// The borrowed cancel atom handle (empty for tasks without one). - pub fn get_cancel_atom(&self) -> CHandle { - self.cancel_atom + /// The shared cancel atom (a clone of the task's `Arc`). + pub fn get_cancel_atom(&self) -> Arc { + self.cancel_atom.clone() } - /// Replace the cancel atom. A previously owned atom is freed; the new - /// atom is borrowed (never freed by this task). Used to share one atom - /// between a task and its behavior's inner base task. - pub fn set_cancel_atom(&mut self, atom: CHandle) { - if self.owns_atom && !self.cancel_atom.is_null() { - let mut old = self.cancel_atom; - unsafe { - bridge::render::oakrender_cancelatom_free(&mut old); - } - } + /// Replace the cancel atom. Used to share one atom between a task and + /// its behavior's inner base task. + pub fn set_cancel_atom(&mut self, atom: Arc) { self.cancel_atom = atom; - self.owns_atom = false; } /// Register the event listener. Replaces any previous listener. @@ -263,7 +223,7 @@ impl Task { self.cancel_event = Some(cb); } - /// Publish the shared values used by the C ABI subscribe wrapper. + /// Publish the shared values used by the legacy subscribe wrapper. pub fn set_subscriber(&mut self, state: Arc) { self.subscriber = Some(state); } @@ -353,8 +313,8 @@ pub fn system_time_ms() -> i64 { .unwrap_or(0) } -/// Marker error returned when a task is cancelled, so the C ABI can map it to -/// `OAKTASK_E_CANCELLED` (distinct from a generic failure). +/// Marker error returned when a task is cancelled, so the legacy ABI can map +/// it to `OAKTASK_E_CANCELLED` (distinct from a generic failure). pub fn cancelled() -> Error { Error::Cancelled } diff --git a/crates/oaktask/tests/common/mod.rs b/crates/oaktask/tests/common/mod.rs deleted file mode 100644 index 09646923d..000000000 --- a/crates/oaktask/tests/common/mod.rs +++ /dev/null @@ -1,2988 +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-only `#[no_mangle]` stubs for every extern C symbol the crate -//! imports. Integration test binaries link the crate's rlib, which -//! references the real module DLLs; these stubs satisfy the linker and let -//! the real task logic run against controllable behavior. Each stub family -//! exposes `set_*` helpers used by the tests to drive success/failure paths. - -#![allow( - non_snake_case, - dead_code, - unused_variables, - clippy::missing_safety_doc -)] - -use std::ffi::{c_char, c_int, c_void}; -use std::sync::atomic::{AtomicI32, AtomicI64, Ordering}; -// AtomicUsize/Condvar are only used by the simulated oakrender ticket -// arena, which the `real-oakrender` feature compiles out. -#[cfg(not(feature = "real-oakrender"))] -use std::sync::atomic::AtomicUsize; -#[cfg(not(feature = "real-oakrender"))] -use std::sync::Condvar; -use std::sync::Mutex; - -// --------------------------------------------------------------------------- -// Fake handle helpers -// --------------------------------------------------------------------------- - -/// A fake refcounted handle: non-null ctx with no-op addref/release. -pub fn fake_handle() -> oaktask::handle::CHandle { - unsafe extern "C" fn noop(_ctx: *mut c_void) {} - oaktask::handle::CHandle { - ctx: 1usize as *mut c_void, - addref: Some(noop), - release: Some(noop), - abi_version: 1, - } -} - -pub fn empty_handle() -> oaktask::handle::CHandle { - oaktask::handle::CHandle::null() -} - -/// A fake OakCancelAtom (ctx non-null so `is_null()` is false). -pub fn fake_atom() -> oaktask::bridge::render::OakCancelAtom { - unsafe extern "C" fn noop(_ctx: *mut c_void) {} - oaktask::bridge::render::OakCancelAtom { - ctx: 2usize as *mut c_void, - addref: Some(noop), - release: Some(noop), - abi_version: 1, - } -} - -pub fn write_cstr(s: &str, buf: *mut u8, size: i32) -> i32 { - let bytes = s.as_bytes(); - let needed = bytes.len() as i32 + 1; - if !buf.is_null() && size >= needed { - unsafe { - std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf, bytes.len()); - *buf.add(bytes.len()) = 0; - } - } - needed -} - -pub fn cstr_of(s: &str) -> *const c_char { - let mut v = s.as_bytes().to_vec(); - v.push(0); - v.leak().as_ptr() as *const c_char -} - -// --------------------------------------------------------------------------- -// Control statics -// --------------------------------------------------------------------------- - -pub static SERIALIZER_LOAD_CODE: AtomicI32 = AtomicI32::new(0); // OAKNODE_SERIALIZER_RESULT_* -pub static SERIALIZER_LOAD_RET: AtomicI32 = AtomicI32::new(0); // oaknode_serializer_load_from_file return -pub static SERIALIZER_SAVE_CODE: AtomicI32 = AtomicI32::new(0); -pub static SERIALIZER_DETAILS: Mutex<&'static str> = Mutex::new(""); -pub static PROJECT_FILENAME: Mutex = Mutex::new(String::new()); -pub static NODE_LABEL: Mutex = Mutex::new(String::new()); -pub static FOOTAGE_FILENAME: Mutex = Mutex::new(String::new()); -pub static FOOTAGE_VALID: AtomicI32 = AtomicI32::new(1); -pub static FOOTAGE_STREAM_COUNT: AtomicI32 = AtomicI32::new(1); -pub static VIDEO_TYPE: AtomicI32 = AtomicI32::new(1); // OAKCOMMON_VIDEO_TYPE_STILL -pub static VIDEO_IS_VALID: AtomicI32 = AtomicI32::new(1); -pub static VIDEO_WIDTH: AtomicI32 = AtomicI32::new(640); -pub static VIDEO_HEIGHT: AtomicI32 = AtomicI32::new(360); -pub static VIDEO_TIME_BASE_NUM: AtomicI32 = AtomicI32::new(0); // 0 => render falls back to 1/1 -pub static VIDEO_TIME_BASE_DEN: AtomicI32 = AtomicI32::new(1); -pub static VIDEO_LENGTH_NUM: AtomicI64 = AtomicI64::new(0); -pub static VIDEO_LENGTH_DEN: AtomicI64 = AtomicI64::new(1); -pub static SEQUENCE_LENGTH_NUM: AtomicI32 = AtomicI32::new(0); -pub static SEQUENCE_LENGTH_DEN: AtomicI32 = AtomicI32::new(1); -pub static CONNECTED_NODE: AtomicI64 = AtomicI64::new(1); // non-null => connected -pub static FOLDER_CHILD_COUNT: AtomicI32 = AtomicI32::new(0); -pub static REDO_DELTA: AtomicI32 = AtomicI32::new(0); -pub static MULTI_CHILDREN: AtomicI32 = AtomicI32::new(0); -pub static DECODER_OPEN_RESULT: AtomicI32 = AtomicI32::new(0); -pub static DECODER_CONFORM_RESULT: AtomicI32 = AtomicI32::new(0); -pub static DECODER_LAST_ERROR: Mutex = Mutex::new(String::new()); -pub static ENCODER_INIT_NULL: AtomicI32 = AtomicI32::new(0); -pub static ENCODER_OPEN_RESULT: AtomicI32 = AtomicI32::new(0); -pub static ENCODER_WRITE_VIDEO_RESULT: AtomicI32 = AtomicI32::new(0); -pub static ENCODER_WRITE_SUBTITLE_RESULT: AtomicI32 = AtomicI32::new(0); -pub static ENCODER_FLUSH_ERROR: AtomicI32 = AtomicI32::new(0); // non-zero => flush reports an error -pub static ENCODER_LAST_ERROR: Mutex = Mutex::new(String::new()); -pub static DESIRED_PIXEL_FORMAT: AtomicI32 = AtomicI32::new(0); -pub static IMAGE_SEQUENCE_DIGIT_COUNT: AtomicI32 = AtomicI32::new(0); -pub static CONFIG_DEFAULT_SEQ_FRAME_RATE: AtomicI32 = AtomicI32::new(0); // 0 => config_get returns nothing -pub static PROXY_FFMPEG_PATH: Mutex = Mutex::new(String::new()); -pub static SUBMIT_CB: Mutex< - Option< - unsafe extern "C" fn( - *const oaktask::bridge::codec::OakCodecTaskRequest, - *mut c_void, - ) -> c_int, - >, -> = Mutex::new(None); -// Simulated-oakrender-arena state below is only linked in stub mode: the -// `real-oakrender` feature replaces the render stubs with the real -// oakrender exports, so these statics and the `#[no_mangle]` render stubs -// are compiled out together. -#[cfg(not(feature = "real-oakrender"))] -pub static ATOM_CANCELLED: AtomicI32 = AtomicI32::new(0); -#[cfg(not(feature = "real-oakrender"))] -pub static TICKET_FRAME_VALID: AtomicI32 = AtomicI32::new(1); // 0 => get_frame returns empty frame -#[cfg(not(feature = "real-oakrender"))] -pub static TICKET_GET_FRAME_RESULT: AtomicI32 = AtomicI32::new(0); -// Simulated ticket arena (bridge::render ticket stubs below). -// 0 = tickets complete synchronously on submit (the deterministic -// single-thread path the export/task tests rely on); 1 = tickets stay in -// flight until the test fires them with `stub_complete` (scrambled -// completion-order tests). -#[cfg(not(feature = "real-oakrender"))] -pub static TICKET_DEFER: AtomicI32 = AtomicI32::new(0); -pub static NODE_ID: Mutex = Mutex::new(String::new()); -pub static AUDIO_TRACK_LIST_NULL: AtomicI32 = AtomicI32::new(1); // 1 => sequence audio track list is empty (null handle) -pub static FOOTAGE_FOUND: AtomicI32 = AtomicI32::new(1); // 0 => oaknode_node_find_input_footage finds nothing -pub static BLOCK_KIND: AtomicI32 = AtomicI32::new(1); // OAKNODE_BLOCK_CLIP -pub static BLOCK_IN_NUM: AtomicI32 = AtomicI32::new(0); -pub static BLOCK_IN_DEN: AtomicI32 = AtomicI32::new(1); -pub static BLOCK_LENGTH_NUM: AtomicI32 = AtomicI32::new(0); -pub static BLOCK_LENGTH_DEN: AtomicI32 = AtomicI32::new(1); -pub static TRACK_BLOCK_COUNT: AtomicI32 = AtomicI32::new(0); -pub static TRACK_TYPE: AtomicI32 = AtomicI32::new(0); // OAKNODE_TRACK_TYPE_VIDEO -pub static TRACK_LENGTH_NUM: AtomicI32 = AtomicI32::new(0); -pub static TRACK_LENGTH_DEN: AtomicI32 = AtomicI32::new(1); -pub static TRACK_LIST_TRACK_COUNT: AtomicI32 = AtomicI32::new(1); -pub static VIDEO_FRAME_RATE_NUM: AtomicI32 = AtomicI32::new(0); -pub static VIDEO_FRAME_RATE_DEN: AtomicI32 = AtomicI32::new(1); -pub static VIDEO_DURATION: AtomicI64 = AtomicI64::new(0); -pub static APPENDED_BLOCKS: AtomicI32 = AtomicI32::new(0); -pub static FOOTAGE_CREATE_COUNT: AtomicI32 = AtomicI32::new(0); -pub static MEDIA_IN_NUM: AtomicI32 = AtomicI32::new(0); -pub static MEDIA_IN_DEN: AtomicI32 = AtomicI32::new(1); -pub static SET_LENGTH_NUM: AtomicI32 = AtomicI32::new(0); -pub static SET_LENGTH_DEN: AtomicI32 = AtomicI32::new(1); -pub static TRANSITION_IN_NUM: AtomicI32 = AtomicI32::new(0); -pub static TRANSITION_IN_DEN: AtomicI32 = AtomicI32::new(1); -pub static TRANSITION_OUT_NUM: AtomicI32 = AtomicI32::new(0); -pub static TRANSITION_OUT_DEN: AtomicI32 = AtomicI32::new(1); -pub static CONNECT_INPUT_IDS: Mutex> = Mutex::new(Vec::new()); - -/// Serialize manager-dependent tests in one binary (the singleton is -/// process-wide). -pub static MANAGER_LOCK: Mutex<()> = Mutex::new(()); - -pub fn reset_stubs() { - SERIALIZER_LOAD_CODE.store(0, Ordering::SeqCst); - SERIALIZER_LOAD_RET.store(0, Ordering::SeqCst); - SERIALIZER_SAVE_CODE.store(0, Ordering::SeqCst); - *SERIALIZER_DETAILS.lock().unwrap() = ""; - PROJECT_FILENAME.lock().unwrap().clear(); - NODE_LABEL.lock().unwrap().clear(); - FOOTAGE_FILENAME.lock().unwrap().clear(); - FOOTAGE_VALID.store(1, Ordering::SeqCst); - FOOTAGE_STREAM_COUNT.store(1, Ordering::SeqCst); - VIDEO_TYPE.store(1, Ordering::SeqCst); - VIDEO_IS_VALID.store(1, Ordering::SeqCst); - VIDEO_WIDTH.store(640, Ordering::SeqCst); - VIDEO_HEIGHT.store(360, Ordering::SeqCst); - VIDEO_TIME_BASE_NUM.store(0, Ordering::SeqCst); - VIDEO_TIME_BASE_DEN.store(1, Ordering::SeqCst); - VIDEO_LENGTH_NUM.store(0, Ordering::SeqCst); - VIDEO_LENGTH_DEN.store(1, Ordering::SeqCst); - SEQUENCE_LENGTH_NUM.store(0, Ordering::SeqCst); - SEQUENCE_LENGTH_DEN.store(1, Ordering::SeqCst); - CONNECTED_NODE.store(1, Ordering::SeqCst); - FOLDER_CHILD_COUNT.store(0, Ordering::SeqCst); - REDO_DELTA.store(0, Ordering::SeqCst); - MULTI_CHILDREN.store(0, Ordering::SeqCst); - DECODER_OPEN_RESULT.store(0, Ordering::SeqCst); - DECODER_CONFORM_RESULT.store(0, Ordering::SeqCst); - *DECODER_LAST_ERROR.lock().unwrap() = String::from("stub decoder error"); - *ENCODER_LAST_ERROR.lock().unwrap() = String::from("stub encoder error"); - ENCODER_INIT_NULL.store(0, Ordering::SeqCst); - ENCODER_OPEN_RESULT.store(0, Ordering::SeqCst); - ENCODER_WRITE_VIDEO_RESULT.store(0, Ordering::SeqCst); - ENCODER_WRITE_SUBTITLE_RESULT.store(0, Ordering::SeqCst); - ENCODER_FLUSH_ERROR.store(0, Ordering::SeqCst); - DESIRED_PIXEL_FORMAT.store(0, Ordering::SeqCst); - IMAGE_SEQUENCE_DIGIT_COUNT.store(0, Ordering::SeqCst); - CONFIG_DEFAULT_SEQ_FRAME_RATE.store(0, Ordering::SeqCst); - PROXY_FFMPEG_PATH.lock().unwrap().clear(); - *SUBMIT_CB.lock().unwrap() = None; - #[cfg(not(feature = "real-oakrender"))] - ATOM_CANCELLED.store(0, Ordering::SeqCst); - #[cfg(not(feature = "real-oakrender"))] - TICKET_FRAME_VALID.store(1, Ordering::SeqCst); - #[cfg(not(feature = "real-oakrender"))] - TICKET_GET_FRAME_RESULT.store(0, Ordering::SeqCst); - // Ticket arena state: back to immediate-completion mode, no tickets. - #[cfg(not(feature = "real-oakrender"))] - TICKET_DEFER.store(0, Ordering::SeqCst); - #[cfg(not(feature = "real-oakrender"))] - TICKET_NEXT_ID.store(0, Ordering::SeqCst); - #[cfg(not(feature = "real-oakrender"))] - TICKET_SUBMITTED.store(0, Ordering::SeqCst); - #[cfg(not(feature = "real-oakrender"))] - TICKET_COMPLETED.store(0, Ordering::SeqCst); - #[cfg(not(feature = "real-oakrender"))] - TICKETS.lock().unwrap_or_else(|e| e.into_inner()).clear(); - NODE_ID.lock().unwrap().clear(); - AUDIO_TRACK_LIST_NULL.store(1, Ordering::SeqCst); - FOOTAGE_FOUND.store(1, Ordering::SeqCst); - BLOCK_KIND.store(1, Ordering::SeqCst); - BLOCK_IN_NUM.store(0, Ordering::SeqCst); - BLOCK_IN_DEN.store(1, Ordering::SeqCst); - BLOCK_LENGTH_NUM.store(0, Ordering::SeqCst); - BLOCK_LENGTH_DEN.store(1, Ordering::SeqCst); - TRACK_BLOCK_COUNT.store(0, Ordering::SeqCst); - TRACK_TYPE.store(0, Ordering::SeqCst); - TRACK_LENGTH_NUM.store(0, Ordering::SeqCst); - TRACK_LENGTH_DEN.store(1, Ordering::SeqCst); - TRACK_LIST_TRACK_COUNT.store(1, Ordering::SeqCst); - VIDEO_FRAME_RATE_NUM.store(0, Ordering::SeqCst); - VIDEO_FRAME_RATE_DEN.store(1, Ordering::SeqCst); - VIDEO_DURATION.store(0, Ordering::SeqCst); - APPENDED_BLOCKS.store(0, Ordering::SeqCst); - FOOTAGE_CREATE_COUNT.store(0, Ordering::SeqCst); - MEDIA_IN_NUM.store(0, Ordering::SeqCst); - MEDIA_IN_DEN.store(1, Ordering::SeqCst); - SET_LENGTH_NUM.store(0, Ordering::SeqCst); - SET_LENGTH_DEN.store(1, Ordering::SeqCst); - TRANSITION_IN_NUM.store(0, Ordering::SeqCst); - TRANSITION_IN_DEN.store(1, Ordering::SeqCst); - TRANSITION_OUT_NUM.store(0, Ordering::SeqCst); - TRANSITION_OUT_DEN.store(1, Ordering::SeqCst); - CONNECT_INPUT_IDS.lock().unwrap().clear(); -} - -// --------------------------------------------------------------------------- -// bridge::codec stubs -// --------------------------------------------------------------------------- - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_set_task_submit_cb( - cb: Option< - unsafe extern "C" fn( - *const oaktask::bridge::codec::OakCodecTaskRequest, - *mut c_void, - ) -> c_int, - >, - _userdata: *mut c_void, -) { - *SUBMIT_CB.lock().unwrap() = cb; -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_task_submit_is_registered() -> c_int { - if SUBMIT_CB.lock().unwrap().is_some() { - 1 - } else { - 0 - } -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_init() -> oaktask::bridge::codec::OakDecoder { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_free(_decoder: *mut oaktask::bridge::codec::OakDecoder) {} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_open( - _decoder: oaktask::bridge::codec::OakDecoder, - _filename: *const c_char, - _stream_index: c_int, -) -> c_int { - DECODER_OPEN_RESULT.load(Ordering::SeqCst) -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_close( - _decoder: oaktask::bridge::codec::OakDecoder, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_is_open( - _decoder: oaktask::bridge::codec::OakDecoder, -) -> c_int { - 1 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_decode_video( - _decoder: oaktask::bridge::codec::OakDecoder, - _numerator: c_int, - _denominator: c_int, -) -> oaktask::bridge::codec::OakDecoder { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_decode_audio( - _decoder: oaktask::bridge::codec::OakDecoder, - _in_num: c_int, - _in_den: c_int, - _out_num: c_int, - _out_den: c_int, - _sample_rate: c_int, - _channel_layout: u64, - _buf: *mut f32, - _buf_frames: c_int, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_conform_audio( - _decoder: oaktask::bridge::codec::OakDecoder, - _output_filenames: *const *const c_char, - _filename_count: c_int, - _sample_rate: c_int, - _channel_layout: u64, - _sample_format: c_int, - _cancelled: oaktask::bridge::render::OakCancelAtom, -) -> c_int { - DECODER_CONFORM_RESULT.load(Ordering::SeqCst) -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_last_error( - _decoder: oaktask::bridge::codec::OakDecoder, - buf: *mut c_char, - size: c_int, -) -> c_int { - write_cstr( - &DECODER_LAST_ERROR.lock().unwrap().clone(), - buf as *mut u8, - size, - ) -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_get_image_sequence_digit_count( - _filename: *const c_char, -) -> c_int { - IMAGE_SEQUENCE_DIGIT_COUNT.load(Ordering::SeqCst) -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_get_image_sequence_index(filename: *const c_char) -> i64 { - // Trailing digits of the stem (before the extension). - let s = if filename.is_null() { - String::new() - } else { - std::ffi::CStr::from_ptr(filename) - .to_string_lossy() - .into_owned() - }; - let stem = match s.rfind('.') { - Some(dot) => &s[..dot], - None => s.as_str(), - }; - let digits: String = stem - .chars() - .rev() - .take_while(|c| c.is_ascii_digit()) - .collect(); - digits - .chars() - .rev() - .collect::() - .parse() - .unwrap_or(-1) -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_decoder_transform_image_sequence_file_name( - filename: *const c_char, - number: i64, - buf: *mut c_char, - size: c_int, -) -> c_int { - let s = if filename.is_null() { - String::new() - } else { - std::ffi::CStr::from_ptr(filename) - .to_string_lossy() - .into_owned() - }; - let (stem, ext) = match s.rfind('.') { - Some(dot) => (&s[..dot], &s[dot..]), - None => (s.as_str(), ""), - }; - let digits: String = stem - .chars() - .rev() - .take_while(|c| c.is_ascii_digit()) - .collect(); - let count = digits.len().max(1); - let prefix = &stem[..stem.len() - digits.len()]; - let out = format!("{prefix}{:0count$}{ext}", number); - write_cstr(&out, buf as *mut u8, size) -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoder_init( - _params: *const oaktask::bridge::codec::OakCodecEncodingParams, -) -> oaktask::bridge::codec::OakEncoder { - if ENCODER_INIT_NULL.load(Ordering::SeqCst) != 0 { - empty_handle() - } else { - fake_handle() - } -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoder_free(_encoder: *mut oaktask::bridge::codec::OakEncoder) {} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoder_set_video_option( - _encoder: oaktask::bridge::codec::OakEncoder, - _key: *const c_char, - _value: *const c_char, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoder_open( - _encoder: oaktask::bridge::codec::OakEncoder, -) -> c_int { - ENCODER_OPEN_RESULT.load(Ordering::SeqCst) -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoder_write_video( - _encoder: oaktask::bridge::codec::OakEncoder, - _frame: oaktask::bridge::codec::OakFrame, -) -> c_int { - ENCODER_WRITE_VIDEO_RESULT.load(Ordering::SeqCst) -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoder_write_audio( - _encoder: oaktask::bridge::codec::OakEncoder, - _samples: *const f32, - _frame_count: c_int, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoder_write_subtitle( - _encoder: oaktask::bridge::codec::OakEncoder, - _text: *const c_char, - _in_seconds: f64, - _out_seconds: f64, -) -> c_int { - ENCODER_WRITE_SUBTITLE_RESULT.load(Ordering::SeqCst) -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoder_flush( - _encoder: oaktask::bridge::codec::OakEncoder, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoder_last_error( - _encoder: oaktask::bridge::codec::OakEncoder, - buf: *mut c_char, - size: c_int, -) -> c_int { - if ENCODER_FLUSH_ERROR.load(Ordering::SeqCst) != 0 { - write_cstr( - &ENCODER_LAST_ERROR.lock().unwrap().clone(), - buf as *mut u8, - size, - ) - } else { - 0 - } -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_encoder_get_desired_pixel_format( - _encoder: oaktask::bridge::codec::OakEncoder, -) -> c_int { - DESIRED_PIXEL_FORMAT.load(Ordering::SeqCst) -} - -#[no_mangle] -pub unsafe extern "C" 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 { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_export_format_get_extension( - _format: c_int, - buf: *mut c_char, - size: c_int, -) -> c_int { - write_cstr("srt", buf as *mut u8, size) -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_proxy_find_ffmpeg( - _configured_path: *const c_char, - buf: *mut c_char, - size: c_int, -) -> c_int { - let path = PROXY_FFMPEG_PATH.lock().unwrap().clone(); - if path.is_empty() { - return 0; - } - write_cstr(&path, buf as *mut u8, size) -} - -#[no_mangle] -pub unsafe extern "C" fn oakcodec_proxy_params_default( - out: *mut oaktask::bridge::codec::OakCodecProxyParams, -) -> c_int { - if out.is_null() { - return -1; - } - (*out).width = 1280; - (*out).height = 720; - (*out).divider = 1; - (*out).version = 0; - (*out).crf = 23; - (*out).include_audio = 1; - write_cstr("mp4", (*out).extension.as_mut_ptr(), 32); - write_cstr("veryfast", (*out).preset.as_mut_ptr(), 32); - 0 -} - -// --------------------------------------------------------------------------- -// bridge::common stubs -// --------------------------------------------------------------------------- - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_videoparams_init() -> oaktask::bridge::common::OakVideoParams { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" 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, -) -> oaktask::bridge::common::OakVideoParams { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_videoparams_free( - _params: *mut oaktask::bridge::common::OakVideoParams, -) { -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_videoparams_get_width( - _params: oaktask::bridge::common::OakVideoParams, - width: *mut c_int, -) -> c_int { - if !width.is_null() { - *width = VIDEO_WIDTH.load(Ordering::SeqCst); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_videoparams_get_height( - _params: oaktask::bridge::common::OakVideoParams, - height: *mut c_int, -) -> c_int { - if !height.is_null() { - *height = VIDEO_HEIGHT.load(Ordering::SeqCst); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_videoparams_get_format( - _params: oaktask::bridge::common::OakVideoParams, - format: *mut c_int, -) -> c_int { - if !format.is_null() { - *format = 0; - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_videoparams_set_format( - _params: oaktask::bridge::common::OakVideoParams, - _format: c_int, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_videoparams_get_time_base( - _params: oaktask::bridge::common::OakVideoParams, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - if !numerator.is_null() { - *numerator = VIDEO_TIME_BASE_NUM.load(Ordering::SeqCst); - } - if !denominator.is_null() { - *denominator = VIDEO_TIME_BASE_DEN.load(Ordering::SeqCst); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_videoparams_set_time_base( - _params: oaktask::bridge::common::OakVideoParams, - _numerator: c_int, - _denominator: c_int, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_videoparams_frame_rate_as_time_base( - _params: oaktask::bridge::common::OakVideoParams, - out_num: *mut c_int, - out_den: *mut c_int, -) -> c_int { - if !out_num.is_null() { - *out_num = VIDEO_TIME_BASE_NUM.load(Ordering::SeqCst); - } - if !out_den.is_null() { - *out_den = VIDEO_TIME_BASE_DEN.load(Ordering::SeqCst); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_videoparams_set_frame_rate( - _params: oaktask::bridge::common::OakVideoParams, - _numerator: c_int, - _denominator: c_int, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_videoparams_get_video_type( - _params: oaktask::bridge::common::OakVideoParams, - out_type: *mut c_int, -) -> c_int { - if !out_type.is_null() { - *out_type = VIDEO_TYPE.load(Ordering::SeqCst); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_videoparams_set_video_type( - _params: oaktask::bridge::common::OakVideoParams, - _video_type: c_int, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_videoparams_set_start_time( - _params: oaktask::bridge::common::OakVideoParams, - _start: i64, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_videoparams_set_duration( - _params: oaktask::bridge::common::OakVideoParams, - _duration: i64, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_videoparams_get_is_valid( - _params: oaktask::bridge::common::OakVideoParams, - out_valid: *mut c_int, -) -> c_int { - if !out_valid.is_null() { - *out_valid = VIDEO_IS_VALID.load(Ordering::SeqCst); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_videoparams_get_frame_rate( - _params: oaktask::bridge::common::OakVideoParams, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - if !numerator.is_null() { - *numerator = VIDEO_FRAME_RATE_NUM.load(Ordering::SeqCst); - } - if !denominator.is_null() { - *denominator = VIDEO_FRAME_RATE_DEN.load(Ordering::SeqCst); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_videoparams_get_duration( - _params: oaktask::bridge::common::OakVideoParams, - duration: *mut i64, -) -> c_int { - if !duration.is_null() { - *duration = VIDEO_DURATION.load(Ordering::SeqCst); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_colortransform_init_display( - _display: *const c_char, - _view: *const c_char, - _look: *const c_char, -) -> oaktask::bridge::common::OakColorTransform { - oaktask::bridge::common::OakColorTransform { - ctx: 3usize as *mut c_void, - addref: None, - release: None, - abi_version: 1, - } -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_colortransform_init_output( - _output: *const c_char, - _display: *const c_char, - _view: *const c_char, - _look: *const c_char, -) -> oaktask::bridge::common::OakColorTransform { - oaktask::bridge::common::OakColorTransform { - ctx: 3usize as *mut c_void, - addref: None, - release: None, - abi_version: 1, - } -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_colortransform_free( - _transform: *mut oaktask::bridge::common::OakColorTransform, -) { -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_config_get( - _group: *const c_char, - key: *const c_char, - buf: *mut c_char, - size: c_int, -) -> c_int { - if key.is_null() { - return 0; - } - let key = std::ffi::CStr::from_ptr(key).to_string_lossy().into_owned(); - if key == "DefaultSequenceFrameRate" - && CONFIG_DEFAULT_SEQ_FRAME_RATE.load(Ordering::SeqCst) != 0 - { - write_cstr("25/1", buf as *mut u8, size) - } else { - 0 - } -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_config_get_int( - _group: *const c_char, - _key: *const c_char, - default: c_int, -) -> c_int { - default -} - -#[no_mangle] -pub unsafe extern "C" fn oakcommon_config_get_bool( - _group: *const c_char, - _key: *const c_char, - default: c_int, -) -> c_int { - default -} - -// --------------------------------------------------------------------------- -// bridge::node stubs -// --------------------------------------------------------------------------- - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_init() -> oaktask::bridge::node::OakNodeProject { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_free( - _project: *mut oaktask::bridge::node::OakNodeProject, -) { -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_initialize( - _project: oaktask::bridge::node::OakNodeProject, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_clear( - _project: oaktask::bridge::node::OakNodeProject, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_root( - _project: oaktask::bridge::node::OakNodeProject, -) -> oaktask::bridge::node::OakNodeFolder { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_name( - _project: oaktask::bridge::node::OakNodeProject, - buf: *mut c_char, - size: c_int, -) -> c_int { - write_cstr("proj", buf as *mut u8, size) -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_filename( - _project: oaktask::bridge::node::OakNodeProject, - buf: *mut c_char, - size: c_int, -) -> c_int { - write_cstr( - &PROJECT_FILENAME.lock().unwrap().clone(), - buf as *mut u8, - size, - ) -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_pretty_filename( - _project: oaktask::bridge::node::OakNodeProject, - buf: *mut c_char, - size: c_int, -) -> c_int { - write_cstr("pretty", buf as *mut u8, size) -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_set_filename( - _project: oaktask::bridge::node::OakNodeProject, - _filename: *const c_char, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_is_modified( - _project: oaktask::bridge::node::OakNodeProject, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_set_modified( - _project: oaktask::bridge::node::OakNodeProject, - _modified: c_int, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_is_new( - _project: oaktask::bridge::node::OakNodeProject, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_cache_path( - _project: oaktask::bridge::node::OakNodeProject, - buf: *mut c_char, - size: c_int, -) -> c_int { - write_cstr("/tmp/oak-cache", buf as *mut u8, size) -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_copy_settings( - _dst: oaktask::bridge::node::OakNodeProject, - _src: oaktask::bridge::node::OakNodeProject, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_get_cache_location_setting( - _project: oaktask::bridge::node::OakNodeProject, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_set_cache_location_setting( - _project: oaktask::bridge::node::OakNodeProject, - _setting: c_int, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_get_custom_cache_path( - _project: oaktask::bridge::node::OakNodeProject, - buf: *mut c_char, - size: c_int, -) -> c_int { - write_cstr("", buf as *mut u8, size) -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_set_custom_cache_path( - _project: oaktask::bridge::node::OakNodeProject, - _path: *const c_char, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_get_uuid( - _project: oaktask::bridge::node::OakNodeProject, - buf: *mut c_char, - size: c_int, -) -> c_int { - write_cstr("uuid", buf as *mut u8, size) -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_add_node( - _project: oaktask::bridge::node::OakNodeProject, - _node: oaktask::bridge::node::OakNodeNode, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_remove_node( - _project: oaktask::bridge::node::OakNodeProject, - _node: oaktask::bridge::node::OakNodeNode, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_node_count( - _project: oaktask::bridge::node::OakNodeProject, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_project_node_at( - _project: oaktask::bridge::node::OakNodeProject, - _index: c_int, -) -> oaktask::bridge::node::OakNodeNode { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_folder_create( - _project: oaktask::bridge::node::OakNodeProject, -) -> oaktask::bridge::node::OakNodeFolder { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_folder_child_count( - _folder: oaktask::bridge::node::OakNodeFolder, -) -> c_int { - FOLDER_CHILD_COUNT.load(Ordering::SeqCst) -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_folder_child_at( - _folder: oaktask::bridge::node::OakNodeFolder, - _index: c_int, -) -> oaktask::bridge::node::OakNodeNode { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_folder_add_child( - _folder: oaktask::bridge::node::OakNodeFolder, - _child: oaktask::bridge::node::OakNodeNode, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_folder_as_node( - folder: oaktask::bridge::node::OakNodeFolder, -) -> oaktask::bridge::node::OakNodeNode { - folder -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_command_create_folder_add_child( - _folder: oaktask::bridge::node::OakNodeFolder, - _child: oaktask::bridge::node::OakNodeNode, -) -> oaktask::bridge::undo::OakUndoCommand { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_command_create_remove_node( - _node: oaktask::bridge::node::OakNodeNode, -) -> oaktask::bridge::undo::OakUndoCommand { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_folder_remove_child( - _folder: oaktask::bridge::node::OakNodeFolder, - _child: oaktask::bridge::node::OakNodeNode, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_folder_move_children( - _nodes: *const oaktask::bridge::node::OakNodeNode, - _count: c_int, - _dest: oaktask::bridge::node::OakNodeFolder, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_folder_has_child_recursive( - _folder: oaktask::bridge::node::OakNodeFolder, - _child: oaktask::bridge::node::OakNodeNode, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_folder_index_of_child( - _folder: oaktask::bridge::node::OakNodeFolder, - _child: oaktask::bridge::node::OakNodeNode, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_folder_parent_of( - _node: oaktask::bridge::node::OakNodeNode, -) -> oaktask::bridge::node::OakNodeFolder { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_create( - _project: oaktask::bridge::node::OakNodeProject, - _filename: *const c_char, -) -> oaktask::bridge::node::OakNodeFootage { - FOOTAGE_CREATE_COUNT.fetch_add(1, Ordering::SeqCst); - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_as_node( - footage: oaktask::bridge::node::OakNodeFootage, -) -> oaktask::bridge::node::OakNodeNode { - footage -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_filename( - _footage: oaktask::bridge::node::OakNodeFootage, - buf: *mut c_char, - size: c_int, -) -> c_int { - write_cstr( - &FOOTAGE_FILENAME.lock().unwrap().clone(), - buf as *mut u8, - size, - ) -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_set_filename( - _footage: oaktask::bridge::node::OakNodeFootage, - filename: *const c_char, -) -> c_int { - if !filename.is_null() { - *FOOTAGE_FILENAME.lock().unwrap() = std::ffi::CStr::from_ptr(filename) - .to_string_lossy() - .into_owned(); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_is_valid( - _footage: oaktask::bridge::node::OakNodeFootage, -) -> c_int { - FOOTAGE_VALID.load(Ordering::SeqCst) -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_timestamp( - _footage: oaktask::bridge::node::OakNodeFootage, - _out_timestamp: *mut i64, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_set_timestamp( - _footage: oaktask::bridge::node::OakNodeFootage, - _timestamp: i64, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_decoder( - _footage: oaktask::bridge::node::OakNodeFootage, - buf: *mut c_char, - size: c_int, -) -> c_int { - write_cstr("ffmpeg", buf as *mut u8, size) -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_total_stream_count( - _footage: oaktask::bridge::node::OakNodeFootage, -) -> c_int { - FOOTAGE_STREAM_COUNT.load(Ordering::SeqCst) -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_video_stream_count( - _footage: oaktask::bridge::node::OakNodeFootage, -) -> c_int { - 1 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_audio_stream_count( - _footage: oaktask::bridge::node::OakNodeFootage, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_subtitle_stream_count( - _footage: oaktask::bridge::node::OakNodeFootage, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_duration( - _footage: oaktask::bridge::node::OakNodeFootage, - out_numerator: *mut c_int, - out_denominator: *mut c_int, -) -> c_int { - if !out_numerator.is_null() { - *out_numerator = 0; - } - if !out_denominator.is_null() { - *out_denominator = 1; - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_proxy_enabled( - _footage: oaktask::bridge::node::OakNodeFootage, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_set_proxy_enabled( - _footage: oaktask::bridge::node::OakNodeFootage, - _enabled: c_int, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_proxy_path( - _footage: oaktask::bridge::node::OakNodeFootage, - buf: *mut c_char, - size: c_int, -) -> c_int { - write_cstr("", buf as *mut u8, size) -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_proxy_state( - _footage: oaktask::bridge::node::OakNodeFootage, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_set_proxy( - _footage: oaktask::bridge::node::OakNodeFootage, - _path: *const c_char, - _state: c_int, - _video_stream_index: c_int, - _preset_version: c_int, - _enabled: c_int, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_clear_proxy( - _footage: oaktask::bridge::node::OakNodeFootage, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_get_video_params( - _footage: oaktask::bridge::node::OakNodeFootage, - _index: c_int, - out: *mut oaktask::bridge::common::OakVideoParams, -) -> c_int { - if !out.is_null() { - *out = oakcommon_videoparams_init(); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_set_video_params( - _footage: oaktask::bridge::node::OakNodeFootage, - _index: c_int, - _params: *const oaktask::bridge::common::OakVideoParams, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_get_video_length( - _footage: oaktask::bridge::node::OakNodeFootage, - out_num: *mut i64, - out_den: *mut i64, -) -> c_int { - if !out_num.is_null() { - *out_num = VIDEO_LENGTH_NUM.load(Ordering::SeqCst); - } - if !out_den.is_null() { - *out_den = VIDEO_LENGTH_DEN.load(Ordering::SeqCst); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_footage_set_cancel_atom( - _footage: oaktask::bridge::node::OakNodeFootage, - _atom: oaktask::bridge::render::OakCancelAtom, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_create() -> oaktask::bridge::node::OakNodeSequence { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_free( - _sequence: *mut oaktask::bridge::node::OakNodeSequence, -) { -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_set_default_parameters( - _sequence: oaktask::bridge::node::OakNodeSequence, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_as_node( - sequence: oaktask::bridge::node::OakNodeSequence, -) -> oaktask::bridge::node::OakNodeNode { - sequence -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_from_node( - node: oaktask::bridge::node::OakNodeNode, -) -> oaktask::bridge::node::OakNodeSequence { - node -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_get_track_list( - _sequence: oaktask::bridge::node::OakNodeSequence, - r#type: c_int, - out: *mut oaktask::bridge::node::OakNodeTrackList, -) -> c_int { - if !out.is_null() { - if r#type == 1 && AUDIO_TRACK_LIST_NULL.load(Ordering::SeqCst) != 0 { - // Audio track list requested but the test marks it empty. - *out = empty_handle(); - } else { - *out = fake_handle(); - } - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_get_track_count( - _sequence: oaktask::bridge::node::OakNodeSequence, - _type: c_int, - count: *mut c_int, -) -> c_int { - if !count.is_null() { - *count = 0; - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_get_track_at( - _sequence: oaktask::bridge::node::OakNodeSequence, - _type: c_int, - _index: c_int, - out: *mut oaktask::bridge::node::OakNodeTrack, -) -> c_int { - if !out.is_null() { - *out = fake_handle(); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_get_all_track_count( - _sequence: oaktask::bridge::node::OakNodeSequence, - count: *mut c_int, -) -> c_int { - if !count.is_null() { - *count = 0; - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_get_all_track_at( - _sequence: oaktask::bridge::node::OakNodeSequence, - _index: c_int, - out: *mut oaktask::bridge::node::OakNodeTrack, -) -> c_int { - if !out.is_null() { - *out = fake_handle(); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_get_playhead( - _sequence: oaktask::bridge::node::OakNodeSequence, - n: *mut c_int, - d: *mut c_int, -) -> c_int { - if !n.is_null() { - *n = 0; - } - if !d.is_null() { - *d = 1; - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_set_playhead( - _sequence: oaktask::bridge::node::OakNodeSequence, - _n: c_int, - _d: c_int, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_get_length( - _sequence: oaktask::bridge::node::OakNodeSequence, - n: *mut c_int, - d: *mut c_int, -) -> c_int { - if !n.is_null() { - *n = SEQUENCE_LENGTH_NUM.load(Ordering::SeqCst); - } - if !d.is_null() { - *d = SEQUENCE_LENGTH_DEN.load(Ordering::SeqCst); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_get_video_length( - _sequence: oaktask::bridge::node::OakNodeSequence, - n: *mut c_int, - d: *mut c_int, -) -> c_int { - if !n.is_null() { - *n = 0; - } - if !d.is_null() { - *d = 1; - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_get_audio_length( - _sequence: oaktask::bridge::node::OakNodeSequence, - n: *mut c_int, - d: *mut c_int, -) -> c_int { - if !n.is_null() { - *n = 0; - } - if !d.is_null() { - *d = 1; - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_verify_length( - _sequence: oaktask::bridge::node::OakNodeSequence, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_get_video_stream_count( - _sequence: oaktask::bridge::node::OakNodeSequence, - count: *mut c_int, -) -> c_int { - if !count.is_null() { - *count = 1; - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_get_audio_stream_count( - _sequence: oaktask::bridge::node::OakNodeSequence, - count: *mut c_int, -) -> c_int { - if !count.is_null() { - *count = 0; - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_get_video_params( - _sequence: oaktask::bridge::node::OakNodeSequence, - _index: c_int, - out: *mut oaktask::bridge::common::OakVideoParams, -) -> c_int { - if !out.is_null() { - *out = oakcommon_videoparams_init(); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_set_video_params( - _sequence: oaktask::bridge::node::OakNodeSequence, - _index: c_int, - _params: oaktask::bridge::common::OakVideoParams, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_get_audio_params( - _sequence: oaktask::bridge::node::OakNodeSequence, - _index: c_int, - out: *mut *const c_void, -) -> c_int { - if !out.is_null() { - *out = std::ptr::null(); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_sequence_set_audio_params( - _sequence: oaktask::bridge::node::OakNodeSequence, - _index: c_int, - _params: *const c_void, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_track_as_node( - track: oaktask::bridge::node::OakNodeTrack, -) -> oaktask::bridge::node::OakNodeNode { - track -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_track_create(_type: c_int) -> oaktask::bridge::node::OakNodeTrack { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_track_free(_track: *mut oaktask::bridge::node::OakNodeTrack) {} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_track_get_type( - _track: oaktask::bridge::node::OakNodeTrack, - r#type: *mut c_int, -) -> c_int { - if !r#type.is_null() { - *r#type = TRACK_TYPE.load(Ordering::SeqCst); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_track_set_type( - _track: oaktask::bridge::node::OakNodeTrack, - _type: c_int, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_track_get_index( - _track: oaktask::bridge::node::OakNodeTrack, - _index: *mut c_int, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_track_set_index( - _track: oaktask::bridge::node::OakNodeTrack, - _index: c_int, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_track_get_sequence( - _track: oaktask::bridge::node::OakNodeTrack, - out: *mut oaktask::bridge::node::OakNodeSequence, -) -> c_int { - if !out.is_null() { - *out = fake_handle(); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_tracklist_get_sequence( - _list: oaktask::bridge::node::OakNodeTrackList, - out: *mut oaktask::bridge::node::OakNodeSequence, -) -> c_int { - if !out.is_null() { - *out = fake_handle(); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_tracklist_get_track_input_id( - _list: oaktask::bridge::node::OakNodeTrackList, - buf: *mut c_char, - size: c_int, -) -> c_int { - write_cstr("sub_in", buf as *mut u8, size) -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_tracklist_array_append( - _list: oaktask::bridge::node::OakNodeTrackList, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_tracklist_array_remove_last( - _list: oaktask::bridge::node::OakNodeTrackList, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_tracklist_get_array_index_from_cache_index( - _list: oaktask::bridge::node::OakNodeTrackList, - _ci: c_int, - out: *mut c_int, -) -> c_int { - if !out.is_null() { - *out = _ci; - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_tracklist_get_type( - _list: oaktask::bridge::node::OakNodeTrackList, - _type: *mut c_int, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_tracklist_get_track_count( - _list: oaktask::bridge::node::OakNodeTrackList, - count: *mut c_int, -) -> c_int { - if !count.is_null() { - if _list.ctx.is_null() { - *count = 0; - } else { - *count = TRACK_LIST_TRACK_COUNT.load(Ordering::SeqCst); - } - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_tracklist_get_track_at( - _list: oaktask::bridge::node::OakNodeTrackList, - _index: c_int, - out: *mut oaktask::bridge::node::OakNodeTrack, -) -> c_int { - if !out.is_null() { - *out = fake_handle(); - } - 0 -} - -// --------------------------------------------------------------------------- -// bridge::node block stubs (node/block.h) -// --------------------------------------------------------------------------- - -#[no_mangle] -pub unsafe extern "C" fn oaknode_block_clip_create() -> oaktask::bridge::node::OakNodeBlock { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_block_gap_create() -> oaktask::bridge::node::OakNodeBlock { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_block_transition_create( - _kind: c_int, -) -> oaktask::bridge::node::OakNodeBlock { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_block_as_node( - block: oaktask::bridge::node::OakNodeBlock, -) -> oaktask::bridge::node::OakNodeNode { - block -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_block_get_kind( - _block: oaktask::bridge::node::OakNodeBlock, - out_kind: *mut c_int, -) -> c_int { - if !out_kind.is_null() { - *out_kind = BLOCK_KIND.load(Ordering::SeqCst); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_block_get_in( - _block: oaktask::bridge::node::OakNodeBlock, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - if !numerator.is_null() { - *numerator = BLOCK_IN_NUM.load(Ordering::SeqCst); - } - if !denominator.is_null() { - *denominator = BLOCK_IN_DEN.load(Ordering::SeqCst); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_block_get_length( - _block: oaktask::bridge::node::OakNodeBlock, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - if !numerator.is_null() { - *numerator = BLOCK_LENGTH_NUM.load(Ordering::SeqCst); - } - if !denominator.is_null() { - *denominator = BLOCK_LENGTH_DEN.load(Ordering::SeqCst); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_block_set_length_and_media_out( - _block: oaktask::bridge::node::OakNodeBlock, - numerator: c_int, - denominator: c_int, -) -> c_int { - SET_LENGTH_NUM.store(numerator, Ordering::SeqCst); - SET_LENGTH_DEN.store(denominator, Ordering::SeqCst); - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_clip_set_media_in( - _clip: oaktask::bridge::node::OakNodeBlock, - numerator: c_int, - denominator: c_int, -) -> c_int { - MEDIA_IN_NUM.store(numerator, Ordering::SeqCst); - MEDIA_IN_DEN.store(denominator, Ordering::SeqCst); - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_transition_get_in_offset( - _transition: oaktask::bridge::node::OakNodeBlock, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - if !numerator.is_null() { - *numerator = TRANSITION_IN_NUM.load(Ordering::SeqCst); - } - if !denominator.is_null() { - *denominator = TRANSITION_IN_DEN.load(Ordering::SeqCst); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_transition_get_out_offset( - _transition: oaktask::bridge::node::OakNodeBlock, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - if !numerator.is_null() { - *numerator = TRANSITION_OUT_NUM.load(Ordering::SeqCst); - } - if !denominator.is_null() { - *denominator = TRANSITION_OUT_DEN.load(Ordering::SeqCst); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_transition_set_offsets_and_length( - _transition: oaktask::bridge::node::OakNodeBlock, - in_num: c_int, - in_den: c_int, - out_num: c_int, - out_den: c_int, -) -> c_int { - TRANSITION_IN_NUM.store(in_num, Ordering::SeqCst); - TRANSITION_IN_DEN.store(in_den, Ordering::SeqCst); - TRANSITION_OUT_NUM.store(out_num, Ordering::SeqCst); - TRANSITION_OUT_DEN.store(out_den, Ordering::SeqCst); - 0 -} - -// --------------------------------------------------------------------------- -// bridge::node track block stubs (node/track.h) -// --------------------------------------------------------------------------- - -#[no_mangle] -pub unsafe extern "C" fn oaknode_track_append_block( - _track: oaktask::bridge::node::OakNodeTrack, - _block: oaktask::bridge::node::OakNodeBlock, -) -> c_int { - APPENDED_BLOCKS.fetch_add(1, Ordering::SeqCst); - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_track_get_block_count( - _track: oaktask::bridge::node::OakNodeTrack, - count: *mut c_int, -) -> c_int { - if !count.is_null() { - *count = TRACK_BLOCK_COUNT.load(Ordering::SeqCst); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_track_get_block_at( - _track: oaktask::bridge::node::OakNodeTrack, - _index: c_int, - out: *mut oaktask::bridge::node::OakNodeBlock, -) -> c_int { - if !out.is_null() { - *out = fake_handle(); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_track_get_length( - _track: oaktask::bridge::node::OakNodeTrack, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - if !numerator.is_null() { - *numerator = TRACK_LENGTH_NUM.load(Ordering::SeqCst); - } - if !denominator.is_null() { - *denominator = TRACK_LENGTH_DEN.load(Ordering::SeqCst); - } - 0 -} - -// --------------------------------------------------------------------------- -// bridge::timeline stubs (timeline/edit.h) -// --------------------------------------------------------------------------- - -#[no_mangle] -pub unsafe extern "C" fn oaktimeline_add_track_command( - _list: oaktask::bridge::node::OakNodeTrackList, -) -> oaktask::bridge::undo::OakUndoCommand { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_tracklist_get_total_length( - _list: oaktask::bridge::node::OakNodeTrackList, - n: *mut c_int, - d: *mut c_int, -) -> c_int { - if !n.is_null() { - *n = 0; - } - if !d.is_null() { - *d = 1; - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_tracklist_get_array_size( - _list: oaktask::bridge::node::OakNodeTrackList, - size: *mut c_int, -) -> c_int { - if !size.is_null() { - *size = 0; - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_tracklist_add_track( - _list: oaktask::bridge::node::OakNodeTrackList, - _track: oaktask::bridge::node::OakNodeTrack, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_tracklist_remove_track( - _list: oaktask::bridge::node::OakNodeTrackList, - _track: oaktask::bridge::node::OakNodeTrack, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_colormanager_init( - _project: oaktask::bridge::node::OakNodeProject, -) -> oaktask::bridge::node::OakNodeColorManager { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_colormanager_free( - _manager: *mut oaktask::bridge::node::OakNodeColorManager, -) { -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_colormanager_wrap_borrowed( - _native: *mut c_void, -) -> oaktask::bridge::node::OakNodeColorManager { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_colormanager_initialize( - _manager: oaktask::bridge::node::OakNodeColorManager, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_colormanager_set_up_default_config() -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_serializer_initialize() -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_serializer_shutdown() {} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_serializer_savedata_create( - _load_type: c_int, - _project: oaktask::bridge::node::OakNodeProject, -) -> oaktask::bridge::node::OakNodeSerializerSaveData { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_serializer_savedata_free( - _save_data: *mut oaktask::bridge::node::OakNodeSerializerSaveData, -) { -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_serializer_savedata_set_nodes( - _save_data: oaktask::bridge::node::OakNodeSerializerSaveData, - _nodes: *const oaktask::bridge::node::OakNodeNode, - _count: c_int, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_serializer_savedata_set_property( - _save_data: oaktask::bridge::node::OakNodeSerializerSaveData, - _node: oaktask::bridge::node::OakNodeNode, - _key: *const c_char, - _value: *const c_char, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_serializer_save_to_xml( - _save_data: oaktask::bridge::node::OakNodeSerializerSaveData, - buf: *mut c_char, - size: c_int, -) -> c_int { - write_cstr("", buf as *mut u8, size) -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_serializer_load_from_xml( - _project: oaktask::bridge::node::OakNodeProject, - _xml: *const c_char, - _load_type: c_int, - out_result: *mut c_int, - _out_load_data: *mut oaktask::bridge::node::OakNodeSerializerLoadData, - _details: *mut c_char, - _details_size: c_int, -) -> c_int { - if !out_result.is_null() { - *out_result = SERIALIZER_LOAD_CODE.load(Ordering::SeqCst); - } - if !_out_load_data.is_null() { - *_out_load_data = fake_handle(); - } - write_cstr( - &SERIALIZER_DETAILS.lock().unwrap().clone(), - _details as *mut u8, - _details_size, - ); - SERIALIZER_LOAD_RET.load(Ordering::SeqCst) -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_serializer_loaddata_free( - _load_data: *mut oaktask::bridge::node::OakNodeSerializerLoadData, -) { -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_serializer_loaddata_node_count( - _load_data: oaktask::bridge::node::OakNodeSerializerLoadData, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_serializer_loaddata_node_at( - _load_data: oaktask::bridge::node::OakNodeSerializerLoadData, - _index: c_int, -) -> oaktask::bridge::node::OakNodeNode { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_serializer_loaddata_get_property( - _load_data: oaktask::bridge::node::OakNodeSerializerLoadData, - _node: oaktask::bridge::node::OakNodeNode, - _key: *const c_char, - buf: *mut c_char, - size: c_int, -) -> c_int { - write_cstr("", buf as *mut u8, size) -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_serializer_loaddata_connection_count( - _load_data: oaktask::bridge::node::OakNodeSerializerLoadData, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_serializer_loaddata_connection_at( - _load_data: oaktask::bridge::node::OakNodeSerializerLoadData, - _index: c_int, - _out_output: *mut oaktask::bridge::node::OakNodeNode, - _out_input: *mut oaktask::bridge::node::OakNodeNode, - _input_id: *mut c_char, - _input_id_size: c_int, - _out_element: *mut c_int, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_serializer_save_to_file( - _project: oaktask::bridge::node::OakNodeProject, - filename: *const c_char, - _use_compression: c_int, - out_code: *mut c_int, - _details: *mut c_char, - _details_size: c_int, -) -> c_int { - let code = SERIALIZER_SAVE_CODE.load(Ordering::SeqCst); - if !out_code.is_null() { - *out_code = code; - } - write_cstr( - &SERIALIZER_DETAILS.lock().unwrap().clone(), - _details as *mut u8, - _details_size, - ); - if code == 0 && !filename.is_null() { - // Success path writes a real file so tests can assert existence. - let name = std::ffi::CStr::from_ptr(filename) - .to_string_lossy() - .into_owned(); - let _ = std::fs::write(&name, b"oakproj-stub"); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_serializer_load_from_file( - _project: oaktask::bridge::node::OakNodeProject, - filename: *const c_char, - out_code: *mut c_int, - _details: *mut c_char, - _details_size: c_int, -) -> c_int { - // A missing file is a file error (mirrors the real serializer); the - // code is controllable otherwise. - let mut code = SERIALIZER_LOAD_CODE.load(Ordering::SeqCst); - if !filename.is_null() { - let name = std::ffi::CStr::from_ptr(filename) - .to_string_lossy() - .into_owned(); - if !std::path::Path::new(&name).exists() { - code = 4; // OAKNODE_SERIALIZER_RESULT_FILE_ERROR - } - } - if !out_code.is_null() { - *out_code = code; - } - write_cstr( - &SERIALIZER_DETAILS.lock().unwrap().clone(), - _details as *mut u8, - _details_size, - ); - SERIALIZER_LOAD_RET.load(Ordering::SeqCst) -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_node_get_label( - _node: oaktask::bridge::node::OakNodeNode, - buf: *mut c_char, - size: c_int, -) -> c_int { - write_cstr(&NODE_LABEL.lock().unwrap().clone(), buf as *mut u8, size) -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_node_set_label( - _node: oaktask::bridge::node::OakNodeNode, - _label: *const c_char, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_node_get_id( - _node: oaktask::bridge::node::OakNodeNode, - buf: *mut c_char, - size: c_int, -) -> c_int { - write_cstr(&NODE_ID.lock().unwrap().clone(), buf as *mut u8, size) -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_node_set_context_position( - _node: oaktask::bridge::node::OakNodeNode, - _context: oaktask::bridge::node::OakNodeNode, - _x: f64, - _y: f64, - _expanded: c_int, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_node_find_input_footage( - _node: oaktask::bridge::node::OakNodeNode, - out: *mut oaktask::bridge::node::OakNodeFootage, -) -> c_int { - if !out.is_null() { - if FOOTAGE_FOUND.load(Ordering::SeqCst) != 0 { - *out = fake_handle(); - } else { - *out = empty_handle(); - } - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_node_get_project( - _node: oaktask::bridge::node::OakNodeNode, - out: *mut oaktask::bridge::node::OakNodeProject, -) -> c_int { - if !out.is_null() { - *out = fake_handle(); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_node_input_get_connected_node( - _node: oaktask::bridge::node::OakNodeNode, - _input_id: *const c_char, - out: *mut oaktask::bridge::node::OakNodeNode, -) -> c_int { - if !out.is_null() { - if CONNECTED_NODE.load(Ordering::SeqCst) != 0 { - *out = fake_handle(); - } else { - *out = empty_handle(); - } - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_node_get_input_string( - _node: oaktask::bridge::node::OakNodeNode, - _input_id: *const c_char, - buf: *mut c_char, - size: c_int, -) -> c_int { - write_cstr("subtitle text", buf as *mut u8, size) -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_node_create_copy( - _node: oaktask::bridge::node::OakNodeNode, -) -> oaktask::bridge::node::OakNodeNode { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_node_copy_inputs( - _dst: oaktask::bridge::node::OakNodeNode, - _src: oaktask::bridge::node::OakNodeNode, - _include_connections: c_int, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_node_connect( - _output: oaktask::bridge::node::OakNodeNode, - _input: oaktask::bridge::node::OakNodeNode, - input_id: *const c_char, -) -> c_int { - if !input_id.is_null() { - CONNECT_INPUT_IDS.lock().unwrap().push( - std::ffi::CStr::from_ptr(input_id) - .to_string_lossy() - .into_owned(), - ); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_node_set_value_hint_track( - _node: oaktask::bridge::node::OakNodeNode, - _input_id: *const c_char, - _track_type: c_int, - _track_index: c_int, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_node_get_video_frame_cache( - _node: oaktask::bridge::node::OakNodeNode, - out: *mut oaktask::bridge::render::OakRenderCache, -) -> c_int { - if !out.is_null() { - *out = oakrender_cache_from_fake(); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_node_free(_node: *mut oaktask::bridge::node::OakNodeNode) {} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_factory_create_from_id( - _type_id: *const c_char, -) -> oaktask::bridge::node::OakNodeNode { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_viewer_set_video_params( - _viewer: oaktask::bridge::node::OakNodeNode, - _params: *const oaktask::bridge::common::OakVideoParams, -) -> c_int { - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oaknode_viewer_set_audio_params( - _viewer: oaktask::bridge::node::OakNodeNode, - _params: *const c_void, -) -> c_int { - 0 -} - -// --------------------------------------------------------------------------- -// bridge::render stubs -// --------------------------------------------------------------------------- - -fn oakrender_cache_from_fake() -> oaktask::bridge::render::OakRenderCache { - unsafe extern "C" fn noop(_ctx: *mut c_void) {} - oaktask::bridge::render::OakRenderCache { - ctx: 5usize as *mut c_void, - addref: Some(noop), - release: Some(noop), - abi_version: 1, - } -} - -// --------------------------------------------------------------------------- -// Simulated oakrender ticket arena (include/render/ticket.h contract) -// -// Every submitted ticket gets a unique id (assigned in submission order) -// and a distinct handle ctx. The finished callback (`cb`) is the ticket's -// return channel; by default it fires synchronously on submit (the -// deterministic single-thread path). With TICKET_DEFER = 1 the ticket -// stays in flight until the test fires it with stub_complete(), so tests -// can drive arbitrary (scrambled) completion orders. Cancelling a ticket — -// or the cancel atom — finishes it exactly like the real arena: the -// completion still fires exactly once. -// --------------------------------------------------------------------------- - -/// OAKRENDER_TICKET_VIDEO (include/render/ticket.h). -#[cfg(not(feature = "real-oakrender"))] -const OAKRENDER_TICKET_VIDEO: c_int = 0; -/// OAKRENDER_TICKET_AUDIO (include/render/ticket.h). -#[cfg(not(feature = "real-oakrender"))] -const OAKRENDER_TICKET_AUDIO: c_int = 1; -/// Base of the ticket handle ctx (0x6000 + id). -#[cfg(not(feature = "real-oakrender"))] -const STUB_TICKET_CTX_BASE: usize = 0x6000; -/// Base of the frame handle ctx (0x10000 + ticket id), so tests can tell -/// which frame a delivered result came from. -#[cfg(not(feature = "real-oakrender"))] -const STUB_FRAME_CTX_BASE: usize = 0x10000; - -/// Next ticket id (submission order). -#[cfg(not(feature = "real-oakrender"))] -static TICKET_NEXT_ID: AtomicUsize = AtomicUsize::new(0); -/// Tickets submitted so far (for [`stub_wait_submitted`]). -#[cfg(not(feature = "real-oakrender"))] -static TICKET_SUBMITTED: AtomicUsize = AtomicUsize::new(0); -/// Completions fired so far (callbacks invoked). -#[cfg(not(feature = "real-oakrender"))] -static TICKET_COMPLETED: AtomicUsize = AtomicUsize::new(0); -/// Submitted-count condvar (test sync point). -#[cfg(not(feature = "real-oakrender"))] -static TICKET_SUBMIT_CV: (Mutex<()>, Condvar) = (Mutex::new(()), Condvar::new()); - -/// A simulated in-flight ticket. -#[cfg(not(feature = "real-oakrender"))] -struct StubTicket { - id: usize, - kind: c_int, - time_num: i64, - time_den: i64, - range: (i64, i64, i64, i64), - cb: Option, - userdata: *mut c_void, - finished: bool, -} - -// The userdata is an opaque pointer handed through the ticket's finished -// callback (the same contract as the real C ABI, where tickets complete on -// worker threads); it is never dereferenced by the stub. -#[cfg(not(feature = "real-oakrender"))] -unsafe impl Send for StubTicket {} -#[cfg(not(feature = "real-oakrender"))] -unsafe impl Sync for StubTicket {} - -/// The registry (guarded; ticket queries/cancel/wait race the render loop). -#[cfg(not(feature = "real-oakrender"))] -static TICKETS: Mutex> = Mutex::new(Vec::new()); -/// Wakes `oakrender_ticket_wait` callers when a ticket finishes. -#[cfg(not(feature = "real-oakrender"))] -static TICKET_CV: Condvar = Condvar::new(); - -#[cfg(not(feature = "real-oakrender"))] -fn stub_ticket_handle(id: usize) -> oaktask::bridge::render::OakRenderTicket { - unsafe extern "C" fn noop(_ctx: *mut c_void) {} - oaktask::bridge::render::OakRenderTicket { - ctx: (STUB_TICKET_CTX_BASE + id) as *mut c_void, - addref: Some(noop), - release: Some(noop), - abi_version: 1, - } -} - -#[cfg(not(feature = "real-oakrender"))] -fn stub_ticket_id(ticket: oaktask::bridge::render::OakRenderTicket) -> Option { - let v = ticket.ctx as usize; - if v >= STUB_TICKET_CTX_BASE && v < STUB_TICKET_CTX_BASE + 1_000_000 { - Some(v - STUB_TICKET_CTX_BASE) - } else { - None - } -} - -#[cfg(not(feature = "real-oakrender"))] -fn stub_frame_handle(id: usize) -> oaktask::bridge::codec::OakFrame { - unsafe extern "C" fn noop(_ctx: *mut c_void) {} - oaktask::bridge::codec::OakFrame { - ctx: (STUB_FRAME_CTX_BASE + id) as *mut c_void, - addref: Some(noop), - release: Some(noop), - abi_version: 1, - } -} - -/// Register a submitted ticket; in immediate mode its completion fires -/// synchronously (before this returns). -#[cfg(not(feature = "real-oakrender"))] -fn stub_register_ticket( - kind: c_int, - time_num: i64, - time_den: i64, - range: (i64, i64, i64, i64), - cb: Option, - userdata: *mut c_void, -) -> oaktask::bridge::render::OakRenderTicket { - let id = TICKET_NEXT_ID.fetch_add(1, Ordering::SeqCst); - { - let mut tickets = TICKETS.lock().unwrap_or_else(|e| e.into_inner()); - tickets.push(StubTicket { - id, - kind, - time_num, - time_den, - range, - cb, - userdata, - finished: false, - }); - } - { - let _g = TICKET_SUBMIT_CV.0.lock().unwrap_or_else(|e| e.into_inner()); - TICKET_SUBMITTED.fetch_add(1, Ordering::SeqCst); - TICKET_SUBMIT_CV.1.notify_all(); - } - let handle = stub_ticket_handle(id); - if TICKET_DEFER.load(Ordering::SeqCst) == 0 { - stub_complete(id); - } - handle -} - -/// Fire the finished callback of ticket `id` (exactly once). Tests call -/// this to drive completion in any order. -#[cfg(not(feature = "real-oakrender"))] -pub fn stub_complete(id: usize) { - let to_fire = { - let mut tickets = TICKETS.lock().unwrap_or_else(|e| e.into_inner()); - match tickets.iter_mut().find(|t| t.id == id) { - Some(t) if !t.finished => { - t.finished = true; - TICKET_COMPLETED.fetch_add(1, Ordering::SeqCst); - TICKET_CV.notify_all(); - Some((t.cb, t.userdata)) - } - _ => None, - } - }; - if let Some((cb, userdata)) = to_fire { - if let Some(cb) = cb { - unsafe { cb(stub_ticket_handle(id), userdata) }; - } - } -} - -/// Block until at least `count` tickets have been submitted. -#[cfg(not(feature = "real-oakrender"))] -pub fn stub_wait_submitted(count: usize) { - let mut guard = TICKET_SUBMIT_CV.0.lock().unwrap_or_else(|e| e.into_inner()); - while TICKET_SUBMITTED.load(Ordering::SeqCst) < count { - guard = TICKET_SUBMIT_CV - .1 - .wait(guard) - .unwrap_or_else(|e| e.into_inner()); - } -} - -/// Tickets submitted so far. -#[cfg(not(feature = "real-oakrender"))] -pub fn stub_submitted_count() -> usize { - TICKET_SUBMITTED.load(Ordering::SeqCst) -} - -/// Completions fired so far (callbacks invoked). -#[cfg(not(feature = "real-oakrender"))] -pub fn stub_completed_count() -> usize { - TICKET_COMPLETED.load(Ordering::SeqCst) -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_cancelatom_init() -> oaktask::bridge::render::OakCancelAtom { - fake_atom() -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_cancelatom_free( - _atom: *mut oaktask::bridge::render::OakCancelAtom, -) { -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_cancelatom_cancel( - _atom: oaktask::bridge::render::OakCancelAtom, -) -> c_int { - ATOM_CANCELLED.store(1, Ordering::SeqCst); - // Simulate cancellation propagating into in-flight renders: every - // pending ticket finishes (its completion fires exactly once), waking - // any render loop waiting on the completion channel. This is what the - // real oakrender does when a cancel atom is shared into the render. - let ids: Vec = { - let tickets = TICKETS.lock().unwrap_or_else(|e| e.into_inner()); - tickets - .iter() - .filter(|t| !t.finished) - .map(|t| t.id) - .collect() - }; - for id in ids { - stub_complete(id); - } - 0 -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_cancelatom_is_cancelled( - _atom: oaktask::bridge::render::OakCancelAtom, - cancelled: *mut c_int, -) -> c_int { - if !cancelled.is_null() { - *cancelled = ATOM_CANCELLED.load(Ordering::SeqCst); - } - 0 -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_cancelatom_heard_cancel( - _atom: oaktask::bridge::render::OakCancelAtom, - heard: *mut c_int, -) -> c_int { - if !heard.is_null() { - *heard = 0; - } - 0 -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_ticket_render_frame( - params: *const oaktask::bridge::render::OakRenderVideoTicketParams, - cb: Option, - userdata: *mut c_void, -) -> oaktask::bridge::render::OakRenderTicket { - let (time_num, time_den) = if params.is_null() { - (0, 1) - } else { - ((*params).time_num, (*params).time_den) - }; - stub_register_ticket( - OAKRENDER_TICKET_VIDEO, - time_num, - time_den, - (0, 1, 0, 1), - cb, - userdata, - ) -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_ticket_render_audio( - _output_node: oaktask::bridge::node::OakNodeNode, - in_num: i64, - in_den: i64, - out_num: i64, - out_den: i64, - _params: *const oaktask::bridge::common::OakAudioParams, - _mode: c_int, - cb: Option, - userdata: *mut c_void, -) -> oaktask::bridge::render::OakRenderTicket { - stub_register_ticket( - OAKRENDER_TICKET_AUDIO, - in_num, - in_den, - (in_num, in_den, out_num, out_den), - cb, - userdata, - ) -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_ticket_is_finished( - ticket: oaktask::bridge::render::OakRenderTicket, -) -> c_int { - match stub_ticket_id(ticket) { - Some(id) => { - let tickets = TICKETS.lock().unwrap_or_else(|e| e.into_inner()); - if tickets.iter().any(|t| t.id == id && t.finished) { - 1 - } else { - 0 - } - } - None => 0, - } -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_ticket_wait( - ticket: oaktask::bridge::render::OakRenderTicket, -) -> c_int { - if let Some(id) = stub_ticket_id(ticket) { - let mut tickets = TICKETS.lock().unwrap_or_else(|e| e.into_inner()); - while !tickets.iter().any(|t| t.id == id && t.finished) { - tickets = TICKET_CV.wait(tickets).unwrap_or_else(|e| e.into_inner()); - } - } - 0 -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_ticket_cancel( - ticket: oaktask::bridge::render::OakRenderTicket, -) -> c_int { - if let Some(id) = stub_ticket_id(ticket) { - // The real cancel contract: the ticket finishes and its completion - // still fires exactly once (a NULL result via get_frame). - stub_complete(id); - } - 0 -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_ticket_get_type( - ticket: oaktask::bridge::render::OakRenderTicket, -) -> c_int { - match stub_ticket_id(ticket) { - Some(id) => { - let tickets = TICKETS.lock().unwrap_or_else(|e| e.into_inner()); - match tickets.iter().find(|t| t.id == id) { - Some(t) => t.kind, - None => OAKRENDER_TICKET_VIDEO, - } - } - None => OAKRENDER_TICKET_VIDEO, - } -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_ticket_get_frame( - ticket: oaktask::bridge::render::OakRenderTicket, - out: *mut oaktask::bridge::codec::OakFrame, -) -> c_int { - if !out.is_null() { - if TICKET_FRAME_VALID.load(Ordering::SeqCst) != 0 { - // The frame handle's ctx encodes the ticket id so tests can - // assert the delivery order of frames. - match stub_ticket_id(ticket) { - Some(id) => *out = stub_frame_handle(id), - None => *out = fake_handle(), - } - } else { - *out = empty_handle(); - } - } - TICKET_GET_FRAME_RESULT.load(Ordering::SeqCst) -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_ticket_get_time( - ticket: oaktask::bridge::render::OakRenderTicket, - n: *mut i64, - d: *mut i64, -) -> c_int { - let info = match stub_ticket_id(ticket) { - Some(id) => { - let tickets = TICKETS.lock().unwrap_or_else(|e| e.into_inner()); - tickets - .iter() - .find(|t| t.id == id) - .map(|t| (t.time_num, t.time_den)) - } - None => None, - }; - if let Some((num, den)) = info { - if !n.is_null() { - *n = num; - } - if !d.is_null() { - *d = den; - } - } - 0 -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_ticket_get_range( - ticket: oaktask::bridge::render::OakRenderTicket, - a: *mut i64, - b: *mut i64, - c: *mut i64, - d: *mut i64, -) -> c_int { - let info = match stub_ticket_id(ticket) { - Some(id) => { - let tickets = TICKETS.lock().unwrap_or_else(|e| e.into_inner()); - tickets.iter().find(|t| t.id == id).map(|t| t.range) - } - None => None, - }; - if let Some((in_num, in_den, out_num, out_den)) = info { - if !a.is_null() { - *a = in_num; - } - if !b.is_null() { - *b = in_den; - } - if !c.is_null() { - *c = out_num; - } - if !d.is_null() { - *d = out_den; - } - } - 0 -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_ticket_get_samples( - _ticket: oaktask::bridge::render::OakRenderTicket, - out: *mut *mut c_void, -) -> c_int { - if !out.is_null() { - *out = std::ptr::null_mut(); - } - 0 -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_ticket_free( - _ticket: *mut oaktask::bridge::render::OakRenderTicket, -) { -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_project_copier_create( -) -> oaktask::bridge::render::OakRenderProjectCopier { - unsafe extern "C" fn noop(_ctx: *mut c_void) {} - oaktask::bridge::render::OakRenderProjectCopier { - ctx: 7usize as *mut c_void, - addref: Some(noop), - release: Some(noop), - abi_version: 1, - } -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_project_copier_free( - _copier: *mut oaktask::bridge::render::OakRenderProjectCopier, -) { -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_project_copier_set_project( - _copier: oaktask::bridge::render::OakRenderProjectCopier, - _project: oaktask::bridge::node::OakNodeProject, -) -> c_int { - 0 -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_project_copier_get_copy( - _copier: oaktask::bridge::render::OakRenderProjectCopier, - original: oaktask::bridge::node::OakNodeNode, -) -> oaktask::bridge::node::OakNodeNode { - original -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_project_copier_get_copied_project( - _copier: oaktask::bridge::render::OakRenderProjectCopier, -) -> oaktask::bridge::node::OakNodeProject { - fake_handle() -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_color_processor_create( - _src_space: *const c_char, - _dst_transform: *const c_char, - _direction: c_int, -) -> oaktask::bridge::render::OakColorProcessor { - unsafe extern "C" fn noop(_ctx: *mut c_void) {} - oaktask::bridge::render::OakColorProcessor { - ctx: 8usize as *mut c_void, - addref: Some(noop), - release: Some(noop), - abi_version: 1, - } -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_color_processor_free( - _processor: *mut oaktask::bridge::render::OakColorProcessor, -) { -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_color_processor_is_valid( - _processor: oaktask::bridge::render::OakColorProcessor, -) -> c_int { - 1 -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_codec_frame_free(_frame: *mut oaktask::bridge::codec::OakFrame) { -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_manager_set_aggressive_gc(_enabled: c_int) -> c_int { - 0 -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_cache_get_invalidated_ranges( - _cache: oaktask::bridge::render::OakRenderCache, - _in_num: i64, - _in_den: i64, - _out_num: i64, - _out_den: i64, - _flat: *mut i64, - _flat_size: c_int, -) -> c_int { - 0 -} - -#[cfg(not(feature = "real-oakrender"))] -#[no_mangle] -pub unsafe extern "C" fn oakrender_cache_free( - _cache: *mut oaktask::bridge::render::OakRenderCache, -) { -} - -// --------------------------------------------------------------------------- -// bridge::undo stubs -// --------------------------------------------------------------------------- - -#[no_mangle] -pub unsafe extern "C" fn oakundo_command_init( - _vtable: *const oaktask::bridge::undo::OakUndoCommandVtable, - _userdata: *mut c_void, -) -> oaktask::bridge::undo::OakUndoCommand { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oakundo_command_init_multi() -> oaktask::bridge::undo::OakUndoCommand { - fake_handle() -} - -#[no_mangle] -pub unsafe extern "C" fn oakundo_command_multi_add_child( - _multi: oaktask::bridge::undo::OakUndoCommand, - _child: oaktask::bridge::undo::OakUndoCommand, -) -> c_int { - MULTI_CHILDREN.fetch_add(1, Ordering::SeqCst); - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakundo_command_multi_child_count( - _multi: oaktask::bridge::undo::OakUndoCommand, - out: *mut c_int, -) -> c_int { - if !out.is_null() { - *out = MULTI_CHILDREN.load(Ordering::SeqCst); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakundo_command_multi_child( - _multi: oaktask::bridge::undo::OakUndoCommand, - _index: c_int, - out: *mut oaktask::bridge::undo::OakUndoCommand, -) -> c_int { - if !out.is_null() { - *out = fake_handle(); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakundo_command_redo_now( - _command: oaktask::bridge::undo::OakUndoCommand, -) -> c_int { - // Approximate redo: the folder gains the accumulated children. - let delta = MULTI_CHILDREN.load(Ordering::SeqCst); - if delta > 0 { - FOLDER_CHILD_COUNT.fetch_add(delta, Ordering::SeqCst); - REDO_DELTA.store(delta, Ordering::SeqCst); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakundo_command_undo_now( - _command: oaktask::bridge::undo::OakUndoCommand, -) -> c_int { - let delta = REDO_DELTA.load(Ordering::SeqCst); - if delta > 0 { - FOLDER_CHILD_COUNT.fetch_sub(delta, Ordering::SeqCst); - REDO_DELTA.store(0, Ordering::SeqCst); - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn oakundo_command_free( - _command: *mut oaktask::bridge::undo::OakUndoCommand, -) { -} diff --git a/crates/oaktask/tests/customcache_test.rs b/crates/oaktask/tests/customcache_test.rs index 94d9c14d0..6590c2520 100644 --- a/crates/oaktask/tests/customcache_test.rs +++ b/crates/oaktask/tests/customcache_test.rs @@ -18,9 +18,6 @@ //! or a real cancellation (`Err(Cancelled)`); the cancelled callback fires //! only for real cancellations. -mod common; - -use common::*; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -36,7 +33,7 @@ fn spawn_parked( let state = cc.state(); let atom = cc.base.get_cancel_atom(); let title = cc.base.title().to_string(); - let mut outer = Task::new(&title, atom); + let mut outer = Task::new(&title, Some(atom)); outer.set_behavior(Box::new(cc)); let handle = std::thread::spawn(move || outer.start()); // Let the worker park on the condvar. @@ -48,9 +45,6 @@ fn spawn_parked( /// task which then reports success. #[test] fn finish_completes_parked_task() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - let cc = CustomCacheTask::new("my-sequence"); assert_eq!(cc.base.title(), "Caching custom range for \"my-sequence\""); let (handle, state) = spawn_parked(cc); @@ -74,9 +68,6 @@ fn finish_completes_parked_task() { /// `finish()`. #[test] fn cancelled_callback_fires_only_on_real_cancel() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - // finish(): the callback must NOT fire. let fired_on_finish = Arc::new(AtomicBool::new(false)); let mut cc = CustomCacheTask::new("finish-seq"); @@ -111,9 +102,6 @@ fn cancelled_callback_fires_only_on_real_cancel() { /// The cancel hook wakes the parked task even without finish(). #[test] fn cancel_hook_wakes_parked_task() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - let cc = CustomCacheTask::new("wake-seq"); let (handle, state) = spawn_parked(cc); // Simulate the manager's cancel path (base.cancel -> atom + hook). diff --git a/crates/oaktask/tests/ffi_contract_test.rs b/crates/oaktask/tests/ffi_contract_test.rs deleted file mode 100644 index 2913a76b3..000000000 --- a/crates/oaktask/tests/ffi_contract_test.rs +++ /dev/null @@ -1,335 +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 . - -//! C ABI contract tests (ffi.rs). One normal + one error path per -//! export family; the exhaustive matrix is driven from the existing -//! C++ gtest suite (`src/task/tests`, unchanged) running against this -//! crate — these tests only pin Rust-side specifics. - -mod common; - -use common::*; -use std::ffi::{c_char, c_int, c_void}; -use std::sync::atomic::{AtomicI32, Ordering}; - -use oaktask::error::{OAKTASK_E_INVALID, OAKTASK_E_STATE, OAKTASK_OK}; -use oaktask::ffi::manager::{ - oaktask_manager_at, oaktask_manager_count, oaktask_manager_delete_finished, - oaktask_manager_init, oaktask_manager_shutdown, -}; -use oaktask::ffi::project::oaktask_create_project_save; -use oaktask::ffi::task::{ - oaktask_debug_alive_count, oaktask_task_cancel, oaktask_task_error, oaktask_task_free, - oaktask_task_is_finished, oaktask_task_start, oaktask_task_start_sync, oaktask_task_subscribe, - oaktask_task_succeeded, oaktask_task_title, oaktask_task_wait, -}; -use oaktask::handle::CHandle; - -fn free_task(t: &mut CHandle) { - unsafe { - oaktask_task_free(t); - } -} - -/// Every exported handle-returning function returns ctx==NULL on -/// failure and a valid refcounted handle on success (abi_version -/// stamped). -#[test] -fn handle_contract_all_exports() { - let _guard = MANAGER_LOCK.lock().unwrap(); - - // create_project_save with a null project -> empty. - let empty = unsafe { oaktask_create_project_save(CHandle::null(), std::ptr::null(), 0) }; - assert!(empty.is_null()); - - // create_project_save with a valid project -> valid handle with the - // stamped ABI version. - *PROJECT_FILENAME.lock().unwrap() = "/tmp/oak-contract-save.oakproj".to_string(); - let task = unsafe { oaktask_create_project_save(fake_handle(), std::ptr::null(), 1) }; - assert!(!task.ctx.is_null()); - assert_eq!(task.abi_version, 1); - - // Title getter round-trip (two-stage). - let mut buf = [0i8; 512]; - let needed = unsafe { oaktask_task_title(task, buf.as_mut_ptr(), buf.len() as c_int) }; - assert!(needed > 0); - assert!(needed <= buf.len() as c_int); - // Exact-size query with a null buffer returns the same needed size. - let query = unsafe { oaktask_task_title(task, std::ptr::null_mut(), 0) }; - assert_eq!(query, needed); - // The save title embeds the project filename. - assert!(needed > 10); - - let mut t = task; - free_task(&mut t); - assert!(t.is_null()); -} - -/// free(NULL)/free(empty) are no-ops across every free export. -#[test] -fn free_null_noop_all_exports() { - unsafe { - oaktask_task_free(std::ptr::null_mut()); - } - let mut empty = CHandle::null(); - unsafe { - oaktask_task_free(&mut empty); - } - assert!(empty.ctx.is_null()); -} - -/// Two-stage string functions: size query, short buffer truncation -/// rule, and exact-fit write — for every string getter -/// (task_title, task_error, import_invalid_at). -#[test] -fn two_stage_string_contract() { - let _guard = MANAGER_LOCK.lock().unwrap(); - *PROJECT_FILENAME.lock().unwrap() = "/tmp/oak-two-stage.oakproj".to_string(); - let task = unsafe { oaktask_create_project_save(fake_handle(), std::ptr::null(), 0) }; - assert!(!task.ctx.is_null()); - - // Null buffer -> needed size only. - let needed = unsafe { oaktask_task_title(task, std::ptr::null_mut(), 0) }; - assert!(needed > 0); - - // Too-small buffer -> needed size, no write. - let mut small = [0i8; 4]; - let r = unsafe { oaktask_task_title(task, small.as_mut_ptr(), small.len() as c_int) }; - assert_eq!(r, needed); - // C++ only writes when the buffer fits; a short buffer stays untouched. - assert_ne!(small[0], b'S' as i8); - - // Exact-fit write. - let mut buf = vec![0i8; needed as usize]; - let r = unsafe { oaktask_task_title(task, buf.as_mut_ptr(), needed) }; - assert_eq!(r, needed); - assert_eq!(buf[0], b'S' as i8); - assert_eq!(buf[needed as usize - 1], 0); - - // Error getter on a fresh task reports the default. - let err_needed = unsafe { oaktask_task_error(task, std::ptr::null_mut(), 0) }; - assert!(err_needed > 0); - - // Error-path: empty handle -> E_INVALID. - assert_eq!( - unsafe { oaktask_task_title(CHandle::null(), std::ptr::null_mut(), 0) }, - OAKTASK_E_INVALID - ); - assert_eq!( - unsafe { oaktask_task_error(CHandle::null(), std::ptr::null_mut(), 0) }, - OAKTASK_E_INVALID - ); - - let mut t = task; - free_task(&mut t); -} - -/// Task manager singleton: init → create/start → wait → delete_finished -/// round-trips; alive-count accounting returns to baseline. -#[test] -fn identity_registry_roundtrip() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - unsafe { oaktask_manager_shutdown() }; // ensure clean slate - - assert_eq!(unsafe { oaktask_manager_init() }, OAKTASK_OK); - assert_eq!(unsafe { oaktask_manager_count() }, 0); - - // Async start of a save task; the worker writes the file. - *PROJECT_FILENAME.lock().unwrap() = "/tmp/oak-identity-roundtrip.oakproj".to_string(); - let _ = std::fs::remove_file("/tmp/oak-identity-roundtrip.oakproj"); - let mut task = unsafe { oaktask_create_project_save(fake_handle(), std::ptr::null(), 0) }; - assert_eq!(unsafe { oaktask_task_start(task) }, OAKTASK_OK); - assert_eq!(unsafe { oaktask_task_start(task) }, OAKTASK_E_STATE); // already running - - assert_eq!(unsafe { oaktask_task_wait(task) }, OAKTASK_OK); - assert_eq!(unsafe { oaktask_task_is_finished(task) }, 1); - assert_eq!(unsafe { oaktask_task_succeeded(task) }, 1); - assert!(std::path::Path::new("/tmp/oak-identity-roundtrip.oakproj").exists()); - - unsafe { oaktask_manager_delete_finished() }; - assert_eq!(unsafe { oaktask_manager_count() }, 0); - - free_task(&mut task); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); - unsafe { oaktask_manager_shutdown() }; -} - -/// alive count: task create/destroy moves oaktask_debug_alive_count -/// predictably and returns to baseline. -#[test] -fn alive_count_accounting() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - unsafe { oaktask_manager_shutdown() }; - - let baseline = unsafe { oaktask_debug_alive_count() }; - assert_eq!(baseline, 0); - - *PROJECT_FILENAME.lock().unwrap() = "/tmp/oak-alive-count.oakproj".to_string(); - let mut t1 = unsafe { oaktask_create_project_save(fake_handle(), std::ptr::null(), 0) }; - let mut t2 = unsafe { oaktask_create_project_save(fake_handle(), std::ptr::null(), 0) }; - assert_eq!(unsafe { oaktask_debug_alive_count() }, baseline + 2); - - free_task(&mut t1); - assert_eq!(unsafe { oaktask_debug_alive_count() }, baseline + 1); - free_task(&mut t2); - assert_eq!(unsafe { oaktask_debug_alive_count() }, baseline); -} - -/// subscribe + async start: the callback observes STARTED (with a start -/// time), then FINISHED (1.0 on success); the subscription is one-shot. -#[test] -fn subscribe_records_started_and_finished() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - unsafe { oaktask_manager_shutdown() }; - unsafe { oaktask_manager_init() }; - - static STARTED: AtomicI32 = AtomicI32::new(0); - static FINISHED: AtomicI32 = AtomicI32::new(0); - static FINISHED_VALUE: AtomicI32 = AtomicI32::new(-1); - - unsafe extern "C" fn recorder(event_id: c_int, value: f64, _ud: *mut c_void) { - match event_id { - 0 => { - STARTED.fetch_add(1, Ordering::SeqCst); - assert!(value > 0.0); // start time in ms - } - 2 => { - FINISHED.fetch_add(1, Ordering::SeqCst); - FINISHED_VALUE.store(value as i32, Ordering::SeqCst); - } - _ => {} - } - } - - *PROJECT_FILENAME.lock().unwrap() = "/tmp/oak-subscribe.oakproj".to_string(); - let mut task = unsafe { oaktask_create_project_save(fake_handle(), std::ptr::null(), 0) }; - - // Null callback -> E_INVALID. - assert_eq!( - unsafe { oaktask_task_subscribe(task, None, std::ptr::null_mut()) }, - OAKTASK_E_INVALID as i64 - ); - // Empty handle -> E_INVALID. - assert_eq!( - unsafe { oaktask_task_subscribe(CHandle::null(), Some(recorder), std::ptr::null_mut()) }, - OAKTASK_E_INVALID as i64 - ); - - assert_eq!( - unsafe { oaktask_task_subscribe(task, Some(recorder), std::ptr::null_mut()) }, - 0 - ); - assert_eq!(unsafe { oaktask_task_start(task) }, OAKTASK_OK); - assert_eq!(unsafe { oaktask_task_wait(task) }, OAKTASK_OK); - - assert_eq!(STARTED.load(Ordering::SeqCst), 1); - assert_eq!(FINISHED.load(Ordering::SeqCst), 1); - assert_eq!(FINISHED_VALUE.load(Ordering::SeqCst), 1); - assert_eq!(unsafe { oaktask_task_is_finished(task) }, 1); - - unsafe { oaktask_manager_delete_finished() }; - free_task(&mut task); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); - unsafe { oaktask_manager_shutdown() }; -} - -/// start_sync on a task with a failing serializer reports failure (0) with -/// an error message, and the error getter returns it (two-stage). -#[test] -fn start_sync_failure_paths() { - let _guard = MANAGER_LOCK.lock().unwrap(); - *PROJECT_FILENAME.lock().unwrap() = "".to_string(); // no filename - let mut task = unsafe { oaktask_create_project_save(fake_handle(), std::ptr::null(), 0) }; - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 0); - assert_eq!(unsafe { oaktask_task_is_finished(task) }, 1); - assert_eq!(unsafe { oaktask_task_succeeded(task) }, 0); - - let needed = unsafe { oaktask_task_error(task, std::ptr::null_mut(), 0) }; - assert!(needed > 0); - let mut buf = vec![0i8; needed as usize]; - unsafe { oaktask_task_error(task, buf.as_mut_ptr(), needed) }; - let msg = cstr_read(&buf); - assert!(msg.contains("no filename"), "error was: {msg}"); - - // Null handle -> E_INVALID. - assert_eq!( - unsafe { oaktask_task_start_sync(CHandle::null()) }, - OAKTASK_E_INVALID - ); - assert_eq!( - unsafe { oaktask_task_cancel(CHandle::null()) }, - OAKTASK_E_INVALID - ); - assert_eq!( - unsafe { oaktask_task_wait(CHandle::null()) }, - OAKTASK_E_INVALID - ); - assert_eq!( - unsafe { oaktask_task_is_finished(CHandle::null()) }, - OAKTASK_E_INVALID - ); - assert_eq!( - unsafe { oaktask_task_succeeded(CHandle::null()) }, - OAKTASK_E_INVALID - ); - - free_task(&mut task); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// cancel on a fresh task is a no-op success; is_finished stays 0. -#[test] -fn cancel_is_noop_before_start() { - let _guard = MANAGER_LOCK.lock().unwrap(); - *PROJECT_FILENAME.lock().unwrap() = "/tmp/oak-cancel.oakproj".to_string(); - let mut task = unsafe { oaktask_create_project_save(fake_handle(), std::ptr::null(), 0) }; - assert_eq!(unsafe { oaktask_task_cancel(task) }, OAKTASK_OK); - assert_eq!(unsafe { oaktask_task_is_finished(task) }, 0); - free_task(&mut task); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// start without an initialized manager -> E_STATE. -#[test] -fn start_without_manager_is_state_error() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - unsafe { oaktask_manager_shutdown() }; - - *PROJECT_FILENAME.lock().unwrap() = "/tmp/oak-no-manager.oakproj".to_string(); - let mut task = unsafe { oaktask_create_project_save(fake_handle(), std::ptr::null(), 0) }; - assert_eq!(unsafe { oaktask_task_start(task) }, OAKTASK_E_STATE); - // Manager accessors also report E_STATE / empty without a manager. - assert_eq!(unsafe { oaktask_manager_count() }, OAKTASK_E_STATE); - assert!(unsafe { oaktask_manager_at(0) }.is_null()); - unsafe { oaktask_manager_delete_finished() }; // no-op - - // start_sync still works without a manager. - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1); - - free_task(&mut task); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// cstr_read helper: read a NUL-terminated c_char buffer. -fn cstr_read(buf: &[i8]) -> String { - let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len()); - let bytes = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) }; - String::from_utf8_lossy(bytes).into_owned() -} diff --git a/crates/oaktask/tests/manager_test.rs b/crates/oaktask/tests/manager_test.rs deleted file mode 100644 index e23651003..000000000 --- a/crates/oaktask/tests/manager_test.rs +++ /dev/null @@ -1,378 +0,0 @@ -// Oak Video Editor - Non-Linear Video Editor -// Copyright (C) 2026 Oak Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! TaskManager singleton lifecycle tests (manager.rs + ffi/manager.rs). - -mod common; - -use common::*; -use std::sync::atomic::Ordering; - -use oaktask::error::{OAKTASK_E_STATE, OAKTASK_OK}; -use oaktask::ffi::manager::{ - oaktask_manager_at, oaktask_manager_count, oaktask_manager_delete_finished, - oaktask_manager_init, oaktask_manager_shutdown, oaktask_register_codec_submitter, -}; -use oaktask::ffi::project::oaktask_create_project_save; -use oaktask::ffi::task::{ - oaktask_debug_alive_count, oaktask_task_free, oaktask_task_is_finished, oaktask_task_start, - oaktask_task_wait, -}; -use oaktask::handle::CHandle; - -/// Given an uninitialized manager, `oaktask_manager_count` reports 0 -/// and `oaktask_manager_at` returns an empty handle. -#[test] -fn uninitialized_manager_is_empty() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - unsafe { oaktask_manager_shutdown() }; - - assert_eq!(unsafe { oaktask_manager_count() }, OAKTASK_E_STATE); - assert!(unsafe { oaktask_manager_at(0) }.is_null()); - assert!(unsafe { oaktask_manager_at(-1) }.is_null()); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// Given an initialized manager, starting a task adds it to the list -/// and `oaktask_manager_at` returns a borrowed handle at that index. -#[test] -fn started_task_is_listed_at_index() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - unsafe { oaktask_manager_shutdown() }; - unsafe { oaktask_manager_init() }; - - *PROJECT_FILENAME.lock().unwrap() = "/tmp/oak-manager-at.oakproj".to_string(); - let mut task = unsafe { oaktask_create_project_save(fake_handle(), std::ptr::null(), 0) }; - assert_eq!(unsafe { oaktask_task_start(task) }, OAKTASK_OK); - assert_eq!(unsafe { oaktask_manager_count() }, 1); - - // The borrowed handle reports the same task once it finishes. - assert_eq!(unsafe { oaktask_task_wait(task) }, OAKTASK_OK); - let borrowed = unsafe { oaktask_manager_at(0) }; - assert!(!borrowed.ctx.is_null()); - assert_eq!(unsafe { oaktask_task_is_finished(borrowed) }, 1); - let mut b = borrowed; - unsafe { oaktask_task_free(&mut b) }; - - // Out of range -> empty. - assert!(unsafe { oaktask_manager_at(5) }.is_null()); - - unsafe { oaktask_manager_delete_finished() }; - assert_eq!(unsafe { oaktask_manager_count() }, 0); - free(&mut task); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); - unsafe { oaktask_manager_shutdown() }; -} - -/// Given finished tasks, `oaktask_manager_delete_finished` removes them -/// and `oaktask_manager_count` shrinks accordingly. -#[test] -fn delete_finished_removes_finished_tasks() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - unsafe { oaktask_manager_shutdown() }; - unsafe { oaktask_manager_init() }; - - *PROJECT_FILENAME.lock().unwrap() = "/tmp/oak-delete-finished.oakproj".to_string(); - let mut t1 = unsafe { oaktask_create_project_save(fake_handle(), std::ptr::null(), 0) }; - let mut t2 = unsafe { oaktask_create_project_save(fake_handle(), std::ptr::null(), 0) }; - assert_eq!(unsafe { oaktask_task_start(t1) }, OAKTASK_OK); - assert_eq!(unsafe { oaktask_task_start(t2) }, OAKTASK_OK); - assert_eq!(unsafe { oaktask_manager_count() }, 2); - - // Wait for both to finish, then delete_finished removes both. - unsafe { oaktask_task_wait(t1) }; - unsafe { oaktask_task_wait(t2) }; - unsafe { oaktask_manager_delete_finished() }; - assert_eq!(unsafe { oaktask_manager_count() }, 0); - - free(&mut t1); - free(&mut t2); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); - unsafe { oaktask_manager_shutdown() }; -} - -/// Given a registered codec submitter, codec tasks are routed through -/// the manager; `oaktask_register_codec_submitter` is idempotent. -#[test] -fn codec_submitter_registration_is_idempotent() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - unsafe { oaktask_manager_shutdown() }; - - // The submitter can be registered without a manager (it is a codec-side - // callback, not a manager task). - assert_eq!(unsafe { oaktask_register_codec_submitter() }, OAKTASK_OK); - assert_eq!(unsafe { oaktask_register_codec_submitter() }, OAKTASK_OK); - unsafe { oaktask_manager_init() }; - // Manager init registers too; repeated registration stays OK. - assert_eq!(unsafe { oaktask_register_codec_submitter() }, OAKTASK_OK); - - // The stub captured the callback. - assert!(SUBMIT_CB.lock().unwrap().is_some()); - - unsafe { oaktask_manager_shutdown() }; -} - -/// Given a shutdown manager, subsequent start/at calls are safe and -/// report no tasks without panicking. -#[test] -fn shutdown_is_safe_and_idempotent() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - unsafe { oaktask_manager_shutdown() }; - - unsafe { oaktask_manager_init() }; - unsafe { oaktask_manager_shutdown() }; - // Idempotent. - unsafe { oaktask_manager_shutdown() }; - - // After shutdown, start/at are safe (E_STATE / empty). - assert_eq!(unsafe { oaktask_manager_count() }, OAKTASK_E_STATE); - assert!(unsafe { oaktask_manager_at(0) }.is_null()); - unsafe { oaktask_manager_delete_finished() }; - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); - - // Re-init works after shutdown. - assert_eq!(unsafe { oaktask_manager_init() }, OAKTASK_OK); - unsafe { oaktask_manager_shutdown() }; -} - -/// A conform request submitted through the codec callback runs the -/// ConformTask synchronously (interim contract). -#[test] -fn codec_submit_runs_conform_synchronously() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - unsafe { oaktask_manager_shutdown() }; - - assert_eq!(unsafe { oaktask_register_codec_submitter() }, OAKTASK_OK); - - // Prepare real working files so the conform rename succeeds. - let dir = std::env::temp_dir().join("oak-conform-submit"); - let _ = std::fs::create_dir_all(&dir); - let final0 = dir.join("out.0.pcm"); - let final1 = dir.join("out.1.pcm"); - let _ = std::fs::remove_file(&final0); - let _ = std::fs::remove_file(&final1); - let working0 = dir.join("out.0.pcm.working"); - let working1 = dir.join("out.1.pcm.working"); - std::fs::write(&working0, b"pcm").unwrap(); - std::fs::write(&working1, b"pcm").unwrap(); - - DECODER_OPEN_RESULT.store(0, Ordering::SeqCst); - DECODER_CONFORM_RESULT.store(0, Ordering::SeqCst); - - let request = oaktask::bridge::codec::OakCodecTaskRequest { - kind: 0, // OAKCODEC_TASK_CONFORM - input_filename: common::cstr_of("/tmp/oak-conform-src.mov"), - output_filename: common::cstr_of(&final0.to_string_lossy()), - stream_index: 1, - sample_rate: 48000, - channel_layout: 3, // stereo - sample_format: 0, - proxy_width: 0, - proxy_height: 0, - }; - let cb = SUBMIT_CB.lock().unwrap().unwrap(); - let ret = unsafe { cb(&request, std::ptr::null_mut()) }; - assert_eq!(ret, 0); // OAKCODEC_OK - assert!(final0.exists(), "final pcm file was not moved into place"); - assert!( - final1.exists(), - "second channel pcm file was not moved into place" - ); - let _ = std::fs::remove_file(&final0); - let _ = std::fs::remove_file(&final1); - let _ = std::fs::remove_file(&working0); - let _ = std::fs::remove_file(&working1); - - // Null request -> OAKCODEC_E_INVALID. - let ret = unsafe { cb(std::ptr::null(), std::ptr::null_mut()) }; - assert_eq!(ret, oaktask::bridge::codec::OAKCODEC_E_INVALID); - unsafe { oaktask_manager_shutdown() }; -} - -/// A proxy request submitted through the codec callback runs ffmpeg (a real -/// test script), parses progress lines, and renames the working file into -/// place. -#[test] -fn codec_submit_runs_proxy_with_script() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - unsafe { oaktask_manager_shutdown() }; - assert_eq!(unsafe { oaktask_register_codec_submitter() }, OAKTASK_OK); - - let dir = std::env::temp_dir().join("oak-proxy-run"); - let _ = std::fs::create_dir_all(&dir); - let script = dir.join("fake-ffmpeg.sh"); - let output = dir.join("proxy.mp4"); - let _ = std::fs::remove_file(&output); - - // The script reports progress on stdout and creates its last argument - // (the working file), then exits 0. - std::fs::write( - &script, - "#!/bin/bash\nprintf 'out_time_us=5000000\\n'\nprintf 'out_time_us=9000000\\n'\nOUT=\"${@: -1}\"\ntouch \"$OUT\"\nexit 0\n", - ) - .unwrap(); - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap(); - } - *PROXY_FFMPEG_PATH.lock().unwrap() = script.to_string_lossy().into_owned(); - - let request = oaktask::bridge::codec::OakCodecTaskRequest { - kind: 1, // OAKCODEC_TASK_PROXY - input_filename: common::cstr_of("/tmp/oak-proxy-src.mov"), - output_filename: common::cstr_of(&output.to_string_lossy()), - stream_index: 0, - sample_rate: 0, - channel_layout: 0, - sample_format: 0, - proxy_width: 0, - proxy_height: 0, - }; - let cb = SUBMIT_CB.lock().unwrap().unwrap(); - let ret = unsafe { cb(&request, std::ptr::null_mut()) }; - assert_eq!(ret, 0, "proxy submit should succeed"); - assert!(output.exists(), "proxy output file was not created"); - - let _ = std::fs::remove_file(&output); - let _ = std::fs::remove_file(&script); - unsafe { oaktask_manager_shutdown() }; -} - -/// Proxy failure paths: ffmpeg missing, failing script, and a script that -/// does not produce the output. -#[test] -fn proxy_failure_paths() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - unsafe { oaktask_manager_shutdown() }; - assert_eq!(unsafe { oaktask_register_codec_submitter() }, OAKTASK_OK); - - let dir = std::env::temp_dir().join("oak-proxy-fail"); - let _ = std::fs::create_dir_all(&dir); - let output = dir.join("proxy.mp4"); - - let cb = SUBMIT_CB.lock().unwrap().unwrap(); - let request = oaktask::bridge::codec::OakCodecTaskRequest { - kind: 1, - input_filename: common::cstr_of("/tmp/oak-proxy-src.mov"), - output_filename: common::cstr_of(&output.to_string_lossy()), - stream_index: 0, - sample_rate: 0, - channel_layout: 0, - sample_format: 0, - proxy_width: 0, - proxy_height: 0, - }; - - // 1. No ffmpeg found. - PROXY_FFMPEG_PATH.lock().unwrap().clear(); - let ret = unsafe { cb(&request, std::ptr::null_mut()) }; - assert_eq!(ret, oaktask::bridge::codec::OAKCODEC_E_FAILED); - - // 2. ffmpeg exits non-zero. - let script = dir.join("fail-ffmpeg.sh"); - std::fs::write(&script, "#!/bin/bash\nexit 1\n").unwrap(); - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap(); - } - *PROXY_FFMPEG_PATH.lock().unwrap() = script.to_string_lossy().into_owned(); - let ret = unsafe { cb(&request, std::ptr::null_mut()) }; - assert_eq!(ret, oaktask::bridge::codec::OAKCODEC_E_FAILED); - - // 3. ffmpeg exits 0 but creates nothing. - let script2 = dir.join("noout-ffmpeg.sh"); - std::fs::write(&script2, "#!/bin/bash\nexit 0\n").unwrap(); - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(&script2, std::fs::Permissions::from_mode(0o755)).unwrap(); - } - *PROXY_FFMPEG_PATH.lock().unwrap() = script2.to_string_lossy().into_owned(); - let ret = unsafe { cb(&request, std::ptr::null_mut()) }; - assert_eq!(ret, oaktask::bridge::codec::OAKCODEC_E_FAILED); - - let _ = std::fs::remove_file(&output); - let _ = std::fs::remove_file(&script); - let _ = std::fs::remove_file(&script2); - unsafe { oaktask_manager_shutdown() }; -} - -/// Conform failure paths: decoder open fails and conform returns a -/// non-OK code. -#[test] -fn conform_failure_paths() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - unsafe { oaktask_manager_shutdown() }; - assert_eq!(unsafe { oaktask_register_codec_submitter() }, OAKTASK_OK); - - let dir = std::env::temp_dir().join("oak-conform-fail"); - let _ = std::fs::create_dir_all(&dir); - let final0 = dir.join("out.0.pcm"); - let working0 = dir.join("out.0.pcm.working"); - - let cb = SUBMIT_CB.lock().unwrap().unwrap(); - let request = oaktask::bridge::codec::OakCodecTaskRequest { - kind: 0, - input_filename: common::cstr_of("/tmp/oak-conform-src.mov"), - output_filename: common::cstr_of(&final0.to_string_lossy()), - stream_index: 1, - sample_rate: 48000, - channel_layout: 3, - sample_format: 0, - proxy_width: 0, - proxy_height: 0, - }; - - // Decoder open fails. - DECODER_OPEN_RESULT.store(-1, Ordering::SeqCst); - let ret = unsafe { cb(&request, std::ptr::null_mut()) }; - assert_eq!(ret, oaktask::bridge::codec::OAKCODEC_E_FAILED); - - // Conform reports a failure. - DECODER_OPEN_RESULT.store(0, Ordering::SeqCst); - DECODER_CONFORM_RESULT.store(-50003, Ordering::SeqCst); - std::fs::write(&working0, b"pcm").unwrap(); - let ret = unsafe { cb(&request, std::ptr::null_mut()) }; - assert_eq!(ret, oaktask::bridge::codec::OAKCODEC_E_FAILED); - // The partial working file is cleaned up. - assert!(!working0.exists(), "partial working file should be removed"); - - // Conform reports cancellation. - DECODER_CONFORM_RESULT.store(-50006, Ordering::SeqCst); - std::fs::write(&working0, b"pcm").unwrap(); - let ret = unsafe { cb(&request, std::ptr::null_mut()) }; - assert_eq!(ret, oaktask::bridge::codec::OAKCODEC_E_FAILED); - - let _ = std::fs::remove_file(&working0); - unsafe { oaktask_manager_shutdown() }; -} - -fn free(t: &mut CHandle) { - unsafe { - oaktask_task_free(t); - } -} diff --git a/crates/oaktask/tests/otio_test.rs b/crates/oaktask/tests/otio_test.rs deleted file mode 100644 index 4eba517d3..000000000 --- a/crates/oaktask/tests/otio_test.rs +++ /dev/null @@ -1,1110 +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 . - -//! OTIO load/save task tests: synthetic documents are built in-test with the -//! `oakotio` model, loaded through `oaktask_create_project_load_otio` / -//! `oaktask_create_project_save_otio` (driving the oaknode C ABI stubs), and -//! exports are re-parsed with `oakotio` for a round-trip check. -//! -//! The tasks dispatch on the filename extension (see -//! `oaktask::project::format`): `.otio` parses/serializes OpenTimelineIO -//! JSON, `.fcpxml` the FCPXML interchange. The FCPXML tests build documents -//! with `oakotio`'s fcpxml writer and re-parse exports with its reader. - -mod common; - -use std::ffi::{c_char, c_int, c_void}; -use std::sync::atomic::Ordering; - -use oakotio::{ - Clip, Composable, ExternalReference, Gap, MediaReference, RationalTime, Serializable, - SerializableCollection, TimeRange, Timeline, Track, Transition, -}; - -use common::*; -use oaktask::ffi::project::{ - oaktask_create_project_load_otio, oaktask_create_project_save_otio, - oaktask_load_otio_set_confirm_cb, oaktask_load_otio_take_project, -}; -use oaktask::ffi::task::{ - oaktask_debug_alive_count, oaktask_task_error, oaktask_task_free, oaktask_task_start_sync, -}; - -unsafe extern "C" fn reject_all( - _seq: *const *const c_char, - _count: c_int, - _ud: *mut c_void, -) -> c_int { - 0 -} - -fn free(t: &mut oaktask::handle::CHandle) { - unsafe { - oaktask_task_free(t); - } -} - -fn cstr_read(buf: &[i8]) -> String { - let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len()); - let bytes = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) }; - String::from_utf8_lossy(bytes).into_owned() -} - -/// Build a single-video-track timeline: one clip (ExternalReference) and one -/// gap, mirroring the golden `golden_timeline.json` block layout. -fn synthetic_timeline(name: &str, url: &str) -> Timeline { - let mut timeline = Timeline::new(name); - - let mut video = Track::new("Video"); - let mut clip = Clip::new("My Sequence Clip"); - clip.set_source_range(TimeRange::new( - RationalTime::new(576.0, 24.0), - RationalTime::new(1152.0, 24.0), - )); - clip.set_media_reference(MediaReference::ExternalReference(ExternalReference::new( - url, None, - ))); - video.append_child(Composable::Clip(clip)); - video.append_child(Composable::Gap(Gap::new( - TimeRange::new(RationalTime::new(0.0, 24.0), RationalTime::new(576.0, 24.0)), - "My Sequence Gap", - ))); - - timeline.tracks_mut().append_child(Composable::Track(video)); - timeline -} - -/// Write a timeline to a temp file, load it via the OTIO task, and return the -/// task handle. -fn load_timeline(timeline: &Timeline) -> (oaktask::handle::CHandle, std::path::PathBuf) { - let path = std::env::temp_dir().join(format!("oak-otio-load-{}.otio", std::process::id())); - timeline.to_json_file(&path).unwrap(); - let path_str = path.to_string_lossy().into_owned(); - let task = unsafe { oaktask_create_project_load_otio(common::cstr_of(&path_str)) }; - (task, path) -} - -// --------------------------------------------------------------------------- -// Load -// --------------------------------------------------------------------------- - -/// A synthetic OTIO document imports into a new project: the clip's media -/// in/length are converted through `Rational::from_double`, footage is -/// created and linked, and the project is produced on `take_project`. -#[test] -fn otio_load_builds_project_from_synthetic_document() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - - let (mut task, path) = load_timeline(&synthetic_timeline( - "Seq A", - "file:///tmp/oak-otio-test.mp4", - )); - assert!(!task.ctx.is_null()); - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1); - - // Clip + gap appended to the track. - assert_eq!(APPENDED_BLOCKS.load(Ordering::SeqCst), 2); - // One footage created for the clip's ExternalReference. - assert_eq!(FOOTAGE_CREATE_COUNT.load(Ordering::SeqCst), 1); - // Clip media in: 576/24 = 24s -> Rational(24, 1). - assert_eq!(MEDIA_IN_NUM.load(Ordering::SeqCst), 24); - assert_eq!(MEDIA_IN_DEN.load(Ordering::SeqCst), 1); - // The last length write is the gap's: 576/24 = 24s. - assert_eq!(SET_LENGTH_NUM.load(Ordering::SeqCst), 24); - assert_eq!(SET_LENGTH_DEN.load(Ordering::SeqCst), 1); - // Video track wiring: transform connected between footage and block. - assert!(CONNECT_INPUT_IDS - .lock() - .unwrap() - .iter() - .any(|id| id == "tex_in")); - assert!(CONNECT_INPUT_IDS - .lock() - .unwrap() - .iter() - .any(|id| id == "buffer_in")); - - let project = unsafe { oaktask_load_otio_take_project(task) }; - assert!(!project.ctx.is_null()); - // Second take is empty (ownership transferred). - assert!(unsafe { oaktask_load_otio_take_project(task) } - .ctx - .is_null()); - - free(&mut task); - let _ = std::fs::remove_file(&path); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// A `SerializableCollection` root imports every timeline; clips sharing one -/// URL resolve to a single footage node. -#[test] -fn otio_load_collection_dedups_footage() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - - let url = "file:///tmp/oak-shared.mp4"; - let collection = SerializableCollection::new( - "Sequences", - vec![ - Serializable::Timeline(synthetic_timeline("A", url)), - Serializable::Timeline(synthetic_timeline("B", url)), - ], - ); - let path = std::env::temp_dir().join("oak-otio-collection.otio"); - Serializable::SerializableCollection(collection) - .to_json_file(&path) - .unwrap(); - let path_str = path.to_string_lossy().into_owned(); - - let mut task = unsafe { oaktask_create_project_load_otio(common::cstr_of(&path_str)) }; - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1); - assert_eq!( - APPENDED_BLOCKS.load(Ordering::SeqCst), - 4, - "one clip+gap per timeline" - ); - assert_eq!( - FOOTAGE_CREATE_COUNT.load(Ordering::SeqCst), - 1, - "shared URL deduplicated" - ); - assert!(!(unsafe { oaktask_load_otio_take_project(task) }) - .ctx - .is_null()); - - free(&mut task); - let _ = std::fs::remove_file(&path); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// The import-confirm callback can reject the whole import: the task still -/// finishes (C++ parity: cancel + return true) but no project is produced. -#[test] -fn otio_load_rejected_confirm_cancels_import() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - - unsafe { oaktask_load_otio_set_confirm_cb(Some(reject_all), std::ptr::null_mut()) }; - - let (mut task, path) = - load_timeline(&synthetic_timeline("Seq A", "file:///tmp/oak-reject.mp4")); - assert!(!task.ctx.is_null()); - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1); - assert!( - unsafe { oaktask_load_otio_take_project(task) } - .ctx - .is_null(), - "rejected import has no project" - ); - - unsafe { oaktask_load_otio_set_confirm_cb(None, std::ptr::null_mut()) }; - free(&mut task); - let _ = std::fs::remove_file(&path); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// The confirm callback receives the sequence labels in order and can accept -/// the import. -#[test] -fn otio_load_confirm_receives_sequence_names() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - - *NODE_LABEL.lock().unwrap() = "Seq A".to_string(); - let seen: std::sync::Mutex> = std::sync::Mutex::new(Vec::new()); - unsafe extern "C" fn accept_and_record( - seq: *const *const c_char, - count: c_int, - userdata: *mut c_void, - ) -> c_int { - let seen = &*(userdata as *const std::sync::Mutex>); - for i in 0..count { - if !(*seq.add(i as usize)).is_null() { - seen.lock().unwrap().push( - std::ffi::CStr::from_ptr(*seq.add(i as usize)) - .to_string_lossy() - .into_owned(), - ); - } - } - 1 - } - - let seen_ptr: *const std::sync::Mutex> = &seen; - unsafe { oaktask_load_otio_set_confirm_cb(Some(accept_and_record), seen_ptr as *mut c_void) }; - - let (mut task, path) = load_timeline(&synthetic_timeline("Seq A", "file:///tmp/oak-names.mp4")); - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1); - assert_eq!(seen.lock().unwrap().clone(), vec!["Seq A".to_string()]); - assert!(!(unsafe { oaktask_load_otio_take_project(task) }) - .ctx - .is_null()); - - unsafe { oaktask_load_otio_set_confirm_cb(None, std::ptr::null_mut()) }; - free(&mut task); - let _ = std::fs::remove_file(&path); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// An unrecognized root schema fails with the C++ error message. -#[test] -fn otio_load_unknown_root_schema_fails() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - - let path = std::env::temp_dir().join("oak-otio-unknown.otio"); - std::fs::write(&path, r#"{"OTIO_SCHEMA": "WeirdThing.9", "a": 1}"#).unwrap(); - let path_str = path.to_string_lossy().into_owned(); - - let mut task = unsafe { oaktask_create_project_load_otio(common::cstr_of(&path_str)) }; - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 0); - let needed = unsafe { oaktask_task_error(task, std::ptr::null_mut(), 0) }; - let mut buf = vec![0i8; needed as usize]; - unsafe { oaktask_task_error(task, buf.as_mut_ptr(), needed) }; - assert!( - cstr_read(&buf).contains("Unknown OpenTimelineIO root element"), - "error was: {}", - cstr_read(&buf) - ); - assert!(unsafe { oaktask_load_otio_take_project(task) } - .ctx - .is_null()); - - free(&mut task); - let _ = std::fs::remove_file(&path); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// Corrupt JSON fails with a clear parse error. -#[test] -fn otio_load_corrupt_json_fails() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - - let path = std::env::temp_dir().join("oak-otio-corrupt.otio"); - std::fs::write(&path, "this is not json {").unwrap(); - let path_str = path.to_string_lossy().into_owned(); - - let mut task = unsafe { oaktask_create_project_load_otio(common::cstr_of(&path_str)) }; - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 0); - let needed = unsafe { oaktask_task_error(task, std::ptr::null_mut(), 0) }; - let mut buf = vec![0i8; needed as usize]; - unsafe { oaktask_task_error(task, buf.as_mut_ptr(), needed) }; - assert!( - cstr_read(&buf).contains("Failed to load OpenTimelineIO"), - "error was: {}", - cstr_read(&buf) - ); - - free(&mut task); - let _ = std::fs::remove_file(&path); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// A clip whose media reference is `MissingReference` imports without -/// creating footage. -#[test] -fn otio_load_missing_reference_creates_no_footage() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - - let mut timeline = Timeline::new("Seq"); - let mut video = Track::new("Video"); - let mut clip = Clip::new("Lost Clip"); - clip.set_source_range(TimeRange::new( - RationalTime::new(0.0, 24.0), - RationalTime::new(24.0, 24.0), - )); - clip.set_media_reference(MediaReference::MissingReference( - oakotio::MissingReference::new(), - )); - video.append_child(Composable::Clip(clip)); - timeline.tracks_mut().append_child(Composable::Track(video)); - - let (mut task, path) = load_timeline(&timeline); - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1); - assert_eq!(APPENDED_BLOCKS.load(Ordering::SeqCst), 1); - assert_eq!(FOOTAGE_CREATE_COUNT.load(Ordering::SeqCst), 0); - assert!(!(unsafe { oaktask_load_otio_take_project(task) }) - .ctx - .is_null()); - - free(&mut task); - let _ = std::fs::remove_file(&path); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// A track whose kind is neither Video nor Audio is skipped (C++ parity). -#[test] -fn otio_load_unknown_track_kind_skipped() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - - let mut timeline = Timeline::new("Seq"); - let mut subtitle = Track::new("Subtitles"); - subtitle.append_child(Composable::Clip(Clip::new("Sub Clip"))); - timeline - .tracks_mut() - .append_child(Composable::Track(subtitle)); - - let (mut task, path) = load_timeline(&timeline); - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1); - assert_eq!(APPENDED_BLOCKS.load(Ordering::SeqCst), 0); - assert!(!(unsafe { oaktask_load_otio_take_project(task) }) - .ctx - .is_null()); - - free(&mut task); - let _ = std::fs::remove_file(&path); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// A transition between two clips wires the block connections and converts -/// its offsets through `Rational::fromRationalTime`. -#[test] -fn otio_load_transition_connects_blocks() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - - let mut timeline = Timeline::new("Seq"); - let mut video = Track::new("Video"); - let mut clip = Clip::new("In"); - clip.set_source_range(TimeRange::new( - RationalTime::new(0.0, 24.0), - RationalTime::new(48.0, 24.0), - )); - video.append_child(Composable::Clip(clip)); - let mut transition = Transition::new("Cross"); - transition.set_in_offset(RationalTime::new(12.0, 24.0)); - transition.set_out_offset(RationalTime::new(12.0, 24.0)); - video.append_child(Composable::Transition(transition)); - let mut clip = Clip::new("Out"); - clip.set_source_range(TimeRange::new( - RationalTime::new(48.0, 24.0), - RationalTime::new(48.0, 24.0), - )); - video.append_child(Composable::Clip(clip)); - timeline.tracks_mut().append_child(Composable::Track(video)); - - let (mut task, path) = load_timeline(&timeline); - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1); - assert_eq!(APPENDED_BLOCKS.load(Ordering::SeqCst), 3); - // 12/24 = 0.5s -> Rational(1, 2). - assert_eq!(TRANSITION_IN_NUM.load(Ordering::SeqCst), 1); - assert_eq!(TRANSITION_IN_DEN.load(Ordering::SeqCst), 2); - assert_eq!(TRANSITION_OUT_NUM.load(Ordering::SeqCst), 1); - assert_eq!(TRANSITION_OUT_DEN.load(Ordering::SeqCst), 2); - let connects = CONNECT_INPUT_IDS.lock().unwrap(); - assert!( - connects.iter().any(|id| id == "out_block_in"), - "previous clip -> transition: {connects:?}" - ); - assert!( - connects.iter().any(|id| id == "in_block_in"), - "transition -> next clip: {connects:?}" - ); - - free(&mut task); - let _ = std::fs::remove_file(&path); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -// --------------------------------------------------------------------------- -// Save -// --------------------------------------------------------------------------- - -/// Point the save-path stubs at one sequence with a video clip and a media -/// reference. -fn configure_one_sequence_save() { - FOLDER_CHILD_COUNT.store(1, Ordering::SeqCst); - *NODE_ID.lock().unwrap() = "org.olivevideoeditor.Olive.sequence".to_string(); - *NODE_LABEL.lock().unwrap() = "My Sequence".to_string(); - VIDEO_FRAME_RATE_NUM.store(25, Ordering::SeqCst); - VIDEO_FRAME_RATE_DEN.store(1, Ordering::SeqCst); - VIDEO_DURATION.store(100, Ordering::SeqCst); - TRACK_LIST_TRACK_COUNT.store(1, Ordering::SeqCst); - TRACK_TYPE.store(0, Ordering::SeqCst); // video - TRACK_BLOCK_COUNT.store(1, Ordering::SeqCst); - BLOCK_KIND.store(1, Ordering::SeqCst); // clip - BLOCK_IN_NUM.store(576, Ordering::SeqCst); - BLOCK_IN_DEN.store(24, Ordering::SeqCst); - BLOCK_LENGTH_NUM.store(25, Ordering::SeqCst); - BLOCK_LENGTH_DEN.store(1, Ordering::SeqCst); - *FOOTAGE_FILENAME.lock().unwrap() = "file:///tmp/video.mp4".to_string(); -} - -/// Run the OTIO save task to a temp file and parse the result back with -/// `oakotio`. -fn run_save_and_parse() -> (oaktask::handle::CHandle, std::path::PathBuf, Serializable) { - let out = std::env::temp_dir().join(format!("oak-otio-save-{}.otio", std::process::id())); - let out_str = out.to_string_lossy().into_owned(); - let _ = std::fs::remove_file(&out); - - let mut task = - unsafe { oaktask_create_project_save_otio(fake_handle(), common::cstr_of(&out_str)) }; - let started = unsafe { oaktask_task_start_sync(task) }; - let text = std::fs::read_to_string(&out).unwrap_or_default(); - let parsed = oakotio::from_json_string(&text).ok(); - free(&mut task); - assert_eq!(started, 1, "save task should succeed; file was: {text}"); - let parsed = parsed.expect("saved file parses as OTIO"); - (task, out, parsed) -} - -/// A single sequence exports as a `Timeline` root whose track/clip/media -/// values round-trip through the C++-parity model. -#[test] -fn otio_save_single_timeline_round_trip() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - configure_one_sequence_save(); - - let (_task, out, parsed) = run_save_and_parse(); - - let timeline = match &parsed { - Serializable::Timeline(t) => t, - other => panic!("expected Timeline root, got {}", other.schema_name()), - }; - assert_eq!(timeline.name(), "My Sequence"); - let tracks = timeline.tracks().children(); - assert_eq!(tracks.len(), 1); - let track = tracks[0].as_track().expect("track"); - assert_eq!(track.kind(), "Video"); - assert_eq!( - track.children().len(), - 1, - "no trailing gap: track length 0 < clip duration" - ); - - let clip = track.children()[0].as_clip().expect("clip"); - assert_eq!(clip.name(), "My Sequence"); - let range = clip.source_range().expect("clip source_range"); - // 576/24 = 24s at 25fps -> {600, 25}; 25/1 = 25s at 25fps -> {625, 25}. - assert_eq!( - (range.start_time().value(), range.start_time().rate()), - (600.0, 25.0) - ); - assert_eq!( - (range.duration().value(), range.duration().rate()), - (625.0, 25.0) - ); - - let external = clip - .media_reference() - .expect("media reference") - .as_external_reference() - .expect("ExternalReference"); - assert_eq!(external.target_url(), "file:///tmp/video.mp4"); - let available = external.available_range().expect("available_range"); - assert_eq!( - ( - available.start_time().value(), - available.start_time().rate() - ), - (0.0, 25.0) - ); - assert_eq!( - (available.duration().value(), available.duration().rate()), - (100.0, 25.0) - ); - - let _ = std::fs::remove_file(&out); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// A track shorter than the list maximum is padded with a trailing `Gap` -/// whose duration is the leftover seconds at rate 1.0 (C++ parity). -#[test] -fn otio_save_pads_shorter_track_with_gap() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - configure_one_sequence_save(); - TRACK_LENGTH_NUM.store(50, Ordering::SeqCst); // 50s track -> clip 25s leaves 25s - TRACK_LENGTH_DEN.store(1, Ordering::SeqCst); - - let (_task, out, parsed) = run_save_and_parse(); - - let timeline = parsed.as_timeline().expect("Timeline root"); - let track = &timeline.tracks().children()[0]; - let track = track.as_track().expect("track"); - assert_eq!(track.children().len(), 2); - let gap = track.children()[1].as_gap().expect("trailing gap"); - let range = gap.source_range().expect("gap source_range"); - assert_eq!( - (range.start_time().value(), range.start_time().rate()), - (625.0, 25.0), - "starts at the track duration" - ); - assert_eq!( - (range.duration().value(), range.duration().rate()), - (25.0, 1.0), - "leftover seconds at rate 1.0" - ); - - let _ = std::fs::remove_file(&out); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// Audio clips write the fixed 48 kHz available range and an "Audio" track -/// kind. -#[test] -fn otio_save_audio_track_uses_48k_available_range() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - configure_one_sequence_save(); - AUDIO_TRACK_LIST_NULL.store(0, Ordering::SeqCst); - TRACK_TYPE.store(1, Ordering::SeqCst); // audio - *FOOTAGE_FILENAME.lock().unwrap() = "file:///tmp/audio.wav".to_string(); - - let (_task, out, parsed) = run_save_and_parse(); - - let timeline = parsed.as_timeline().expect("Timeline root"); - let track = &timeline.tracks().children()[0]; - let track = track.as_track().expect("track"); - assert_eq!(track.kind(), "Audio"); - let clip = track.children()[0].as_clip().expect("clip"); - let external = clip - .media_reference() - .expect("media reference") - .as_external_reference() - .expect("ExternalReference"); - assert_eq!(external.target_url(), "file:///tmp/audio.wav"); - let available = external.available_range().expect("available_range"); - assert_eq!( - ( - available.start_time().value(), - available.start_time().rate() - ), - (0.0, 48000.0) - ); - assert_eq!( - (available.duration().value(), available.duration().rate()), - (0.0, 48000.0) - ); - - let _ = std::fs::remove_file(&out); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// A clip with no connected footage exports without a media reference. -#[test] -fn otio_save_clip_without_media_has_no_reference() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - configure_one_sequence_save(); - FOOTAGE_FOUND.store(0, Ordering::SeqCst); - - let (_task, out, parsed) = run_save_and_parse(); - - let timeline = parsed.as_timeline().expect("Timeline root"); - let track = &timeline.tracks().children()[0]; - let track = track.as_track().expect("track"); - let clip = track.children()[0].as_clip().expect("clip"); - assert!( - clip.media_reference().is_none(), - "no footage -> no media reference" - ); - - let _ = std::fs::remove_file(&out); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// A transition block exports its offsets (Rational -> RationalTime at 24). -#[test] -fn otio_save_transition_serializes_offsets() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - configure_one_sequence_save(); - BLOCK_KIND.store(3, Ordering::SeqCst); // transition - TRANSITION_IN_NUM.store(12, Ordering::SeqCst); - TRANSITION_IN_DEN.store(24, Ordering::SeqCst); - TRANSITION_OUT_NUM.store(12, Ordering::SeqCst); - TRANSITION_OUT_DEN.store(24, Ordering::SeqCst); - - let (_task, out, parsed) = run_save_and_parse(); - - let timeline = parsed.as_timeline().expect("Timeline root"); - let track = &timeline.tracks().children()[0]; - let track = track.as_track().expect("track"); - let transition = track.children()[0].as_transition().expect("transition"); - assert_eq!( - ( - transition.in_offset().value(), - transition.in_offset().rate() - ), - (12.0, 24.0) - ); - assert_eq!( - ( - transition.out_offset().value(), - transition.out_offset().rate() - ), - (12.0, 24.0) - ); - - let _ = std::fs::remove_file(&out); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// Multiple sequences export as a `SerializableCollection` named "Sequences". -#[test] -fn otio_save_writes_collection_for_multiple_sequences() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - configure_one_sequence_save(); - FOLDER_CHILD_COUNT.store(2, Ordering::SeqCst); - - let (_task, out, parsed) = run_save_and_parse(); - - let collection = match &parsed { - Serializable::SerializableCollection(c) => c, - other => panic!( - "expected SerializableCollection root, got {}", - other.schema_name() - ), - }; - assert_eq!(collection.name(), "Sequences"); - assert_eq!(collection.children().len(), 2); - - let _ = std::fs::remove_file(&out); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// A project with no sequences fails with the C++ error message. -#[test] -fn otio_save_no_sequences_fails() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - FOLDER_CHILD_COUNT.store(0, Ordering::SeqCst); - - let out = std::env::temp_dir().join("oak-otio-save-none.otio"); - let out_str = out.to_string_lossy().into_owned(); - let mut task = - unsafe { oaktask_create_project_save_otio(fake_handle(), common::cstr_of(&out_str)) }; - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 0); - let needed = unsafe { oaktask_task_error(task, std::ptr::null_mut(), 0) }; - let mut buf = vec![0i8; needed as usize]; - unsafe { oaktask_task_error(task, buf.as_mut_ptr(), needed) }; - assert!( - cstr_read(&buf).contains("Project contains no sequences to export."), - "error was: {}", - cstr_read(&buf) - ); - - free(&mut task); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// A sequence with a non-positive frame rate fails to serialize. -#[test] -fn otio_save_bad_frame_rate_fails() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - configure_one_sequence_save(); - VIDEO_FRAME_RATE_NUM.store(0, Ordering::SeqCst); - VIDEO_FRAME_RATE_DEN.store(1, Ordering::SeqCst); - - let out = std::env::temp_dir().join("oak-otio-save-badrate.otio"); - let out_str = out.to_string_lossy().into_owned(); - let mut task = - unsafe { oaktask_create_project_save_otio(fake_handle(), common::cstr_of(&out_str)) }; - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 0); - let needed = unsafe { oaktask_task_error(task, std::ptr::null_mut(), 0) }; - let mut buf = vec![0i8; needed as usize]; - unsafe { oaktask_task_error(task, buf.as_mut_ptr(), needed) }; - assert!( - cstr_read(&buf).contains("Failed to serialize sequence"), - "error was: {}", - cstr_read(&buf) - ); - - free(&mut task); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// A track with an unknown native type fails the whole export (C++ parity: -/// serialize_track falls through to fail). -#[test] -fn otio_save_unknown_track_type_fails() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - configure_one_sequence_save(); - TRACK_TYPE.store(-1, Ordering::SeqCst); // OAKNODE_TRACK_TYPE_NONE - - let out = std::env::temp_dir().join("oak-otio-save-badtrack.otio"); - let out_str = out.to_string_lossy().into_owned(); - let mut task = - unsafe { oaktask_create_project_save_otio(fake_handle(), common::cstr_of(&out_str)) }; - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 0); - let needed = unsafe { oaktask_task_error(task, std::ptr::null_mut(), 0) }; - let mut buf = vec![0i8; needed as usize]; - unsafe { oaktask_task_error(task, buf.as_mut_ptr(), needed) }; - assert!( - cstr_read(&buf).contains("Failed to serialize sequence"), - "error was: {}", - cstr_read(&buf) - ); - - free(&mut task); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -// --------------------------------------------------------------------------- -// FCPXML load/save -// --------------------------------------------------------------------------- - -/// Write a timeline to a temp `.fcpxml` file (via the `oakotio` fcpxml -/// writer) and load it through the OTIO task, returning the task handle. -fn load_fcpxml(timeline: &Timeline) -> (oaktask::handle::CHandle, std::path::PathBuf) { - let path = std::env::temp_dir().join(format!("oak-fcpxml-load-{}.fcpxml", std::process::id())); - oakotio::to_fcpxml_file(&[timeline.clone()], &path).unwrap(); - let path_str = path.to_string_lossy().into_owned(); - let task = unsafe { oaktask_create_project_load_otio(common::cstr_of(&path_str)) }; - (task, path) -} - -/// A synthetic FCPXML document (built with the `oakotio` fcpxml writer) -/// imports into a new project exactly like the OTIO document: the same -/// track/clip/footage code produces the same numbers, so the two formats -/// share one code path after the parse. -#[test] -fn fcpxml_load_builds_project_from_synthetic_document() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - - let (mut task, path) = load_fcpxml(&synthetic_timeline( - "Seq A", - "file:///tmp/oak-otio-test.mp4", - )); - assert!(!task.ctx.is_null()); - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1); - - // Clip + gap appended to the track. - assert_eq!(APPENDED_BLOCKS.load(Ordering::SeqCst), 2); - // One footage created for the clip's ExternalReference. - assert_eq!(FOOTAGE_CREATE_COUNT.load(Ordering::SeqCst), 1); - // Clip media in: 576/24 = 24s -> Rational(24, 1). - assert_eq!(MEDIA_IN_NUM.load(Ordering::SeqCst), 24); - assert_eq!(MEDIA_IN_DEN.load(Ordering::SeqCst), 1); - // The last length write is the gap's: 576/24 = 24s. - assert_eq!(SET_LENGTH_NUM.load(Ordering::SeqCst), 24); - assert_eq!(SET_LENGTH_DEN.load(Ordering::SeqCst), 1); - // Video track wiring: transform connected between footage and block. - assert!(CONNECT_INPUT_IDS - .lock() - .unwrap() - .iter() - .any(|id| id == "tex_in")); - assert!(CONNECT_INPUT_IDS - .lock() - .unwrap() - .iter() - .any(|id| id == "buffer_in")); - - let project = unsafe { oaktask_load_otio_take_project(task) }; - assert!(!project.ctx.is_null()); - // Second take is empty (ownership transferred). - assert!(unsafe { oaktask_load_otio_take_project(task) } - .ctx - .is_null()); - - free(&mut task); - let _ = std::fs::remove_file(&path); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// An FCPXML export of one sequence re-parses with the `oakotio` fcpxml -/// reader: the timeline/track/clip/media values match the OTIO export -/// (same serialization, different codec). -#[test] -fn fcpxml_save_round_trips_through_parse() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - configure_one_sequence_save(); - - let out = std::env::temp_dir().join(format!("oak-fcpxml-save-{}.fcpxml", std::process::id())); - let out_str = out.to_string_lossy().into_owned(); - let _ = std::fs::remove_file(&out); - - let mut task = - unsafe { oaktask_create_project_save_otio(fake_handle(), common::cstr_of(&out_str)) }; - let started = unsafe { oaktask_task_start_sync(task) }; - let text = std::fs::read_to_string(&out).unwrap_or_default(); - free(&mut task); - assert_eq!(started, 1, "save task should succeed; file was: {text}"); - - let timelines = oakotio::from_fcpxml_string(&text).expect("saved file parses as FCPXML"); - assert_eq!(timelines.len(), 1); - let timeline = &timelines[0]; - assert_eq!(timeline.name(), "My Sequence"); - let tracks = timeline.tracks().children(); - assert_eq!(tracks.len(), 1); - let track = tracks[0].as_track().expect("track"); - assert_eq!(track.kind(), "Video"); - assert_eq!( - track.children().len(), - 1, - "no trailing gap: track length 0 < clip duration" - ); - - let clip = track.children()[0].as_clip().expect("clip"); - assert_eq!(clip.name(), "My Sequence"); - let range = clip.source_range().expect("clip source_range"); - // 576/24 = 24s at 25fps -> {600, 25}; 25/1 = 25s at 25fps -> {625, 25}. - assert_eq!( - (range.start_time().value(), range.start_time().rate()), - (600.0, 25.0) - ); - assert_eq!( - (range.duration().value(), range.duration().rate()), - (625.0, 25.0) - ); - - let external = clip - .media_reference() - .expect("media reference") - .as_external_reference() - .expect("ExternalReference"); - assert_eq!(external.target_url(), "file:///tmp/video.mp4"); - let available = external.available_range().expect("available_range"); - // Asset duration 100/25 = 4s at 25fps -> {0, 25} .. {100, 25}. - assert_eq!( - ( - available.start_time().value(), - available.start_time().rate() - ), - (0.0, 25.0) - ); - assert_eq!( - (available.duration().value(), available.duration().rate()), - (100.0, 25.0) - ); - - let _ = std::fs::remove_file(&out); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// A full save -> load cycle through the tasks: the FCPXML file written by -/// the save task is imported by the load task with the same track/clip/ -/// footage structure. -#[test] -fn fcpxml_save_reimports_through_load_task() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - configure_one_sequence_save(); - - let out = std::env::temp_dir().join(format!("oak-fcpxml-cycle-{}.fcpxml", std::process::id())); - let out_str = out.to_string_lossy().into_owned(); - let _ = std::fs::remove_file(&out); - - let mut save = - unsafe { oaktask_create_project_save_otio(fake_handle(), common::cstr_of(&out_str)) }; - assert_eq!(unsafe { oaktask_task_start_sync(save) }, 1); - free(&mut save); - - reset_stubs(); - let mut load = unsafe { oaktask_create_project_load_otio(common::cstr_of(&out_str)) }; - assert_eq!(unsafe { oaktask_task_start_sync(load) }, 1); - assert_eq!( - APPENDED_BLOCKS.load(Ordering::SeqCst), - 1, - "one clip re-imported" - ); - assert_eq!(FOOTAGE_CREATE_COUNT.load(Ordering::SeqCst), 1); - assert!(!(unsafe { oaktask_load_otio_take_project(load) }) - .ctx - .is_null()); - - free(&mut load); - let _ = std::fs::remove_file(&out); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -// --------------------------------------------------------------------------- -// Extension dispatch matrix -// --------------------------------------------------------------------------- - -/// The load task dispatches on the filename extension, case-insensitively: -/// `.otio`/`.OTIO` parse OpenTimelineIO JSON, `.fcpxml`/`.FCPXML` parse -/// FCPXML, and any other extension fails with "Unknown project file -/// format". -#[test] -fn otio_load_extension_dispatch_matrix() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - - for (ext, is_otio) in [ - (".otio", true), - (".OTIO", true), - (".fcpxml", false), - (".FCPXML", false), - ] { - let timeline = synthetic_timeline("Seq A", "file:///tmp/oak-dispatch.mp4"); - let path = std::env::temp_dir().join(format!("oak-dispatch-{}{ext}", std::process::id())); - if is_otio { - timeline.to_json_file(&path).unwrap(); - } else { - oakotio::to_fcpxml_file(&[timeline], &path).unwrap(); - } - let path_str = path.to_string_lossy().into_owned(); - - let mut task = unsafe { oaktask_create_project_load_otio(common::cstr_of(&path_str)) }; - assert_eq!( - unsafe { oaktask_task_start_sync(task) }, - 1, - "extension {ext} should import" - ); - assert_eq!( - APPENDED_BLOCKS.load(Ordering::SeqCst), - 2, - "extension {ext}: clip + gap" - ); - assert!( - !(unsafe { oaktask_load_otio_take_project(task) }) - .ctx - .is_null(), - "extension {ext}" - ); - - free(&mut task); - let _ = std::fs::remove_file(&path); - reset_stubs(); - } - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// An unknown extension fails the load with a clear format error. -#[test] -fn otio_load_unknown_extension_fails() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - - let path = - std::env::temp_dir().join(format!("oak-otio-unknown-ext-{}.txt", std::process::id())); - std::fs::write(&path, "whatever").unwrap(); - let path_str = path.to_string_lossy().into_owned(); - - let mut task = unsafe { oaktask_create_project_load_otio(common::cstr_of(&path_str)) }; - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 0); - let needed = unsafe { oaktask_task_error(task, std::ptr::null_mut(), 0) }; - let mut buf = vec![0i8; needed as usize]; - unsafe { oaktask_task_error(task, buf.as_mut_ptr(), needed) }; - assert!( - cstr_read(&buf).contains("Unknown project file format"), - "error was: {}", - cstr_read(&buf) - ); - assert!(unsafe { oaktask_load_otio_take_project(task) } - .ctx - .is_null()); - - free(&mut task); - let _ = std::fs::remove_file(&path); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// An unknown extension fails the save with a clear format error, before any -/// serialization work happens. -#[test] -fn otio_save_unknown_extension_fails() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - configure_one_sequence_save(); - - let out = - std::env::temp_dir().join(format!("oak-otio-save-unknown-{}.xyz", std::process::id())); - let out_str = out.to_string_lossy().into_owned(); - let mut task = - unsafe { oaktask_create_project_save_otio(fake_handle(), common::cstr_of(&out_str)) }; - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 0); - let needed = unsafe { oaktask_task_error(task, std::ptr::null_mut(), 0) }; - let mut buf = vec![0i8; needed as usize]; - unsafe { oaktask_task_error(task, buf.as_mut_ptr(), needed) }; - assert!( - cstr_read(&buf).contains("Unknown project file format"), - "error was: {}", - cstr_read(&buf) - ); - - free(&mut task); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -// --------------------------------------------------------------------------- -// FCPXML error paths -// --------------------------------------------------------------------------- - -/// A corrupt `.fcpxml` document fails with a clear FCPXML parse error. -#[test] -fn fcpxml_load_corrupt_xml_fails() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - - let path = - std::env::temp_dir().join(format!("oak-fcpxml-corrupt-{}.fcpxml", std::process::id())); - std::fs::write(&path, "").unwrap(); - let path_str = path.to_string_lossy().into_owned(); - - let mut task = unsafe { oaktask_create_project_load_otio(common::cstr_of(&path_str)) }; - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 0); - let needed = unsafe { oaktask_task_error(task, std::ptr::null_mut(), 0) }; - let mut buf = vec![0i8; needed as usize]; - unsafe { oaktask_task_error(task, buf.as_mut_ptr(), needed) }; - let msg = cstr_read(&buf); - assert!(msg.contains("Failed to load FCPXML"), "error was: {msg}"); - assert!(unsafe { oaktask_load_otio_take_project(task) } - .ctx - .is_null()); - - free(&mut task); - let _ = std::fs::remove_file(&path); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// An FCPXML version this crate cannot read fails the load with the -/// underlying version error surfaced in the task message. -#[test] -fn fcpxml_load_unknown_version_fails() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - - let path = - std::env::temp_dir().join(format!("oak-fcpxml-version-{}.fcpxml", std::process::id())); - std::fs::write( - &path, - "\n\n", - ) - .unwrap(); - let path_str = path.to_string_lossy().into_owned(); - - let mut task = unsafe { oaktask_create_project_load_otio(common::cstr_of(&path_str)) }; - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 0); - let needed = unsafe { oaktask_task_error(task, std::ptr::null_mut(), 0) }; - let mut buf = vec![0i8; needed as usize]; - unsafe { oaktask_task_error(task, buf.as_mut_ptr(), needed) }; - let msg = cstr_read(&buf); - assert!(msg.contains("Failed to load FCPXML"), "error was: {msg}"); - assert!(msg.contains("version"), "error was: {msg}"); - assert!(unsafe { oaktask_load_otio_take_project(task) } - .ctx - .is_null()); - - free(&mut task); - let _ = std::fs::remove_file(&path); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} diff --git a/crates/oaktask/tests/project_task_test.rs b/crates/oaktask/tests/project_task_test.rs deleted file mode 100644 index f0893546e..000000000 --- a/crates/oaktask/tests/project_task_test.rs +++ /dev/null @@ -1,615 +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 . - -//! Project task factory tests (project.rs + ffi/project.rs): load, save, -//! import, OTIO load/save and pre-cache/export factories. - -mod common; - -use common::*; -use std::ffi::{c_char, c_int, c_void}; -use std::sync::atomic::Ordering; - -use oaktask::error::{OAKTASK_E_INVALID, OAKTASK_E_NOT_FOUND}; -use oaktask::ffi::manager::{oaktask_manager_init, oaktask_manager_shutdown}; -use oaktask::ffi::project::{ - oaktask_create_export, oaktask_create_precache, oaktask_create_project_import, - oaktask_create_project_load, oaktask_create_project_load_otio, oaktask_create_project_save, - oaktask_create_project_save_otio, oaktask_import_footage_at, oaktask_import_footage_count, - oaktask_import_invalid_at, oaktask_import_invalid_count, - oaktask_import_set_image_sequence_confirm_cb, oaktask_import_take_command, - oaktask_load_otio_set_confirm_cb, oaktask_load_otio_take_project, oaktask_load_take_project, -}; -use oaktask::ffi::task::{ - oaktask_debug_alive_count, oaktask_task_error, oaktask_task_free, oaktask_task_is_finished, - oaktask_task_start_sync, oaktask_task_title, -}; -use oaktask::handle::CHandle; - -unsafe extern "C" fn accept_all( - _seq: *const *const c_char, - _count: c_int, - _ud: *mut c_void, -) -> c_int { - 1 -} - -unsafe extern "C" fn reject_all( - _seq: *const *const c_char, - _count: c_int, - _ud: *mut c_void, -) -> c_int { - 0 -} - -unsafe extern "C" fn image_seq_confirm(_filename: *const c_char, _ud: *mut c_void) -> c_int { - 1 -} - -fn free(t: &mut CHandle) { - unsafe { - oaktask_task_free(t); - } -} - -/// Given a valid project file, `create_project_load` returns a task whose -/// `start_sync` succeeds and whose `load_take_project` transfers an -/// initialized project with a root folder. -#[test] -fn project_load_transfers_initialized_project() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - let path = std::env::temp_dir().join("oak-load-ok.oakproj"); - std::fs::write(&path, b"oakproj-stub").unwrap(); - let path_str = path.to_string_lossy().into_owned(); - - let mut task = unsafe { oaktask_create_project_load(common::cstr_of(&path_str)) }; - assert!(!task.ctx.is_null()); - - // Title embeds the filename. - let needed = unsafe { oaktask_task_title(task, std::ptr::null_mut(), 0) }; - assert!(needed > 10); - - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1); - assert_eq!(unsafe { oaktask_task_is_finished(task) }, 1); - - // take_project transfers a non-empty project. - let project = unsafe { oaktask_load_take_project(task) }; - assert!(!project.ctx.is_null(), "load_take_project returned empty"); - // Second take is empty (ownership transferred). - let project2 = unsafe { oaktask_load_take_project(task) }; - assert!(project2.ctx.is_null()); - - free(&mut task); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); - let _ = std::fs::remove_file(&path); -} - -/// Loading a missing file fails: start_sync=0, an error is set, and -/// take_project is empty. -#[test] -fn project_load_missing_file_fails() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - let missing = "/tmp/oak-does-not-exist-12345.oakproj"; - let _ = std::fs::remove_file(missing); - - let mut task = unsafe { oaktask_create_project_load(common::cstr_of(missing)) }; - assert!(!task.ctx.is_null()); - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 0); - assert_eq!(unsafe { oaktask_task_is_finished(task) }, 1); - - let needed = unsafe { oaktask_task_error(task, std::ptr::null_mut(), 0) }; - assert!(needed > 0); - let mut buf = vec![0i8; needed as usize]; - unsafe { oaktask_task_error(task, buf.as_mut_ptr(), needed) }; - assert!(cstr_read(&buf).contains("Failed to read file")); - - let project = unsafe { oaktask_load_take_project(task) }; - assert!(project.ctx.is_null()); - - // Null filename -> empty handle (factory failure path). - let empty = unsafe { oaktask_create_project_load(std::ptr::null()) }; - assert!(empty.ctx.is_null()); - - free(&mut task); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// Given a loaded project, `create_project_save` writes the project and the -/// task succeeds with compression on and off. -#[test] -fn project_save_writes_with_and_without_compression() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - let path = std::env::temp_dir().join("oak-save-compressed.oakproj"); - let path_str = path.to_string_lossy().into_owned(); - let _ = std::fs::remove_file(&path); - - // Without compression. - let mut task = - unsafe { oaktask_create_project_save(fake_handle(), common::cstr_of(&path_str), 0) }; - assert!(!task.ctx.is_null()); - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1); - assert!(path.exists(), "saved file was not written"); - free(&mut task); - - // With compression (overwrites). - let _ = std::fs::remove_file(&path); - let mut task = - unsafe { oaktask_create_project_save(fake_handle(), common::cstr_of(&path_str), 1) }; - assert!(!task.ctx.is_null()); - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1); - assert!(path.exists()); - free(&mut task); - - // Null project -> empty handle. - let empty = - unsafe { oaktask_create_project_save(CHandle::null(), common::cstr_of(&path_str), 0) }; - assert!(empty.ctx.is_null()); - - let _ = std::fs::remove_file(&path); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// Given an import folder and a media URL, `create_project_import` runs to -/// completion, records one footage and exposes it via `import_footage_at`. -#[test] -fn project_import_records_and_exposes_footage() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - let file = std::env::temp_dir().join("oak-import-ok.mp4"); - std::fs::write(&file, b"media").unwrap(); - let file_str = file.to_string_lossy().into_owned(); - - let urls = [common::cstr_of(&file_str)]; - let mut task = unsafe { - oaktask_create_project_import( - fake_handle(), - fake_handle(), - urls.as_ptr(), - urls.len() as c_int, - ) - }; - assert!(!task.ctx.is_null()); - - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1); - assert_eq!(unsafe { oaktask_import_footage_count(task) }, 1); - assert_eq!(unsafe { oaktask_import_invalid_count(task) }, 0); - - // footage_at returns an addref'd handle for in-range indices. - let footage = unsafe { oaktask_import_footage_at(task, 0) }; - assert!(!footage.ctx.is_null()); - // Out of range -> empty. - assert!(unsafe { oaktask_import_footage_at(task, 5) }.ctx.is_null()); - assert!(unsafe { oaktask_import_footage_at(task, -1) }.ctx.is_null()); - - // The undo command is produced; redo adds the folder child, undo - // removes it again. - let mut command = unsafe { oaktask_import_take_command(task) }; - assert!(!command.ctx.is_null()); - // Second take is empty. - assert!(unsafe { oaktask_import_take_command(task) }.ctx.is_null()); - - assert_eq!( - unsafe { oaktask::bridge::undo::oakundo_command_redo_now(command) }, - 0 - ); - assert_eq!( - unsafe { oaktask::bridge::node::oaknode_folder_child_count(fake_handle()) }, - 1 - ); - assert_eq!( - unsafe { oaktask::bridge::undo::oakundo_command_undo_now(command) }, - 0 - ); - assert_eq!( - unsafe { oaktask::bridge::node::oaknode_folder_child_count(fake_handle()) }, - 0 - ); - - unsafe { - oaktask::bridge::undo::oakundo_command_free(&mut command); - } - free(&mut task); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); - let _ = std::fs::remove_file(&file); -} - -/// Given an unreadable import URL, `create_project_import` records the -/// invalid filename and `import_invalid_at` reports it (two-stage). -#[test] -fn project_import_reports_invalid_urls() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - FOOTAGE_VALID.store(0, Ordering::SeqCst); - - let bad = "/tmp/oak-invalid-media-12345.mp4"; - let _ = std::fs::remove_file(bad); - let urls = [common::cstr_of(bad)]; - let mut task = unsafe { - oaktask_create_project_import( - fake_handle(), - fake_handle(), - urls.as_ptr(), - urls.len() as c_int, - ) - }; - assert!(!task.ctx.is_null()); - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1); - assert_eq!(unsafe { oaktask_import_invalid_count(task) }, 1); - assert_eq!(unsafe { oaktask_import_footage_count(task) }, 0); - - // Two-stage invalid filename getter. - let needed = unsafe { oaktask_import_invalid_at(task, 0, std::ptr::null_mut(), 0) }; - assert!(needed > 0); - let mut buf = vec![0i8; needed as usize]; - let r = unsafe { oaktask_import_invalid_at(task, 0, buf.as_mut_ptr(), needed) }; - assert_eq!(r, needed); - assert_eq!(cstr_read(&buf), bad); - - // Out of range -> E_NOT_FOUND. - assert_eq!( - unsafe { oaktask_import_invalid_at(task, 3, std::ptr::null_mut(), 0) }, - OAKTASK_E_NOT_FOUND - ); - - // Factory failure paths: null folder, null project, negative count. - assert!(unsafe { - oaktask_create_project_import(CHandle::null(), fake_handle(), std::ptr::null(), 0) - } - .ctx - .is_null()); - assert!(unsafe { - oaktask_create_project_import(fake_handle(), CHandle::null(), std::ptr::null(), 0) - } - .ctx - .is_null()); - let urls_null = [std::ptr::null()]; - assert!(unsafe { - oaktask_create_project_import(fake_handle(), fake_handle(), urls_null.as_ptr(), 1) - } - .ctx - .is_null()); - - // Accessors on a non-import task -> E_INVALID / empty. - let mut save_task = - unsafe { oaktask_create_project_save(fake_handle(), common::cstr_of("/tmp/x.oakproj"), 0) }; - assert_eq!( - unsafe { oaktask_import_footage_count(save_task) }, - OAKTASK_E_INVALID - ); - assert!(unsafe { oaktask_import_take_command(save_task) } - .ctx - .is_null()); - free(&mut save_task); - - free(&mut task); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// An image sequence with a confirmation callback imports once and prunes -/// the remaining frames from the list. -#[test] -fn project_import_image_sequence_confirmed() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - let dir = std::env::temp_dir().join("oak-seq"); - let _ = std::fs::create_dir_all(&dir); - let files = ["seq001.png", "seq002.png", "seq003.png"]; - let mut paths = Vec::new(); - for f in &files { - let p = dir.join(f); - std::fs::write(&p, b"png").unwrap(); - paths.push(p.to_string_lossy().into_owned()); - } - - IMAGE_SEQUENCE_DIGIT_COUNT.store(3, Ordering::SeqCst); - CONFIG_DEFAULT_SEQ_FRAME_RATE.store(1, Ordering::SeqCst); - unsafe { - oaktask_import_set_image_sequence_confirm_cb(Some(image_seq_confirm), std::ptr::null_mut()) - }; - - let urls: Vec<*const c_char> = paths.iter().map(|p| common::cstr_of(p)).collect(); - let mut task = unsafe { - oaktask_create_project_import( - fake_handle(), - fake_handle(), - urls.as_ptr(), - urls.len() as c_int, - ) - }; - assert!(!task.ctx.is_null()); - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1); - - // The three frames collapse into a single footage import. - assert_eq!(unsafe { oaktask_import_footage_count(task) }, 1); - assert_eq!(unsafe { oaktask_import_invalid_count(task) }, 0); - - unsafe { oaktask_import_set_image_sequence_confirm_cb(None, std::ptr::null_mut()) }; - IMAGE_SEQUENCE_DIGIT_COUNT.store(0, Ordering::SeqCst); - free(&mut task); - for p in &paths { - let _ = std::fs::remove_file(p); - } - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// Given a valid OTIO file, `create_project_load_otio` runs to completion and -/// `load_otio_take_project` transfers an initialized project; a missing file -/// fails cleanly. (OTIO parsing now happens inside the task through the -/// `oakotio` binding — README decision #6.) -#[test] -fn otio_load_transfers_project_with_default_confirm() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - - // A minimal Timeline document (no tracks). - let path = std::env::temp_dir().join("oak-load-ok.otio"); - let otio = r#"{ - "OTIO_SCHEMA": "Timeline.1", - "metadata": {}, - "name": "T", - "global_start_time": null, - "tracks": { - "OTIO_SCHEMA": "Stack.1", - "metadata": {}, - "name": "tracks", - "source_range": null, - "effects": [], - "markers": [], - "enabled": true, - "children": [] - } -}"#; - std::fs::write(&path, otio).unwrap(); - let path_str = path.to_string_lossy().into_owned(); - - let mut task = unsafe { oaktask_create_project_load_otio(common::cstr_of(&path_str)) }; - assert!(!task.ctx.is_null()); - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1); - assert_eq!(unsafe { oaktask_task_is_finished(task) }, 1); - assert!(!(unsafe { oaktask_load_otio_take_project(task) }) - .ctx - .is_null()); - - free(&mut task); - - // Missing file -> failure with a clear error. - let missing = std::env::temp_dir().join("oak-missing-otio.otio"); - let missing_str = missing.to_string_lossy().into_owned(); - let _ = std::fs::remove_file(&missing); - let mut task = unsafe { oaktask_create_project_load_otio(common::cstr_of(&missing_str)) }; - assert!(!task.ctx.is_null()); - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 0); - let needed = unsafe { oaktask_task_error(task, std::ptr::null_mut(), 0) }; - assert!(needed > 0); - let mut buf = vec![0i8; needed as usize]; - unsafe { oaktask_task_error(task, buf.as_mut_ptr(), needed) }; - assert!( - cstr_read(&buf).contains("Failed to load OpenTimelineIO"), - "error was: {}", - cstr_read(&buf) - ); - assert!(unsafe { oaktask_load_otio_take_project(task) } - .ctx - .is_null()); - - // The confirm callback setters are no-ops (null clears). - unsafe { - oaktask_load_otio_set_confirm_cb(Some(accept_all), std::ptr::null_mut()); - oaktask_load_otio_set_confirm_cb(Some(reject_all), std::ptr::null_mut()); - oaktask_load_otio_set_confirm_cb(None, std::ptr::null_mut()); - } - - free(&mut task); - let _ = std::fs::remove_file(&path); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// Given a project with a sequence, `create_project_save_otio` writes the -/// OTIO file and the task succeeds; a project without sequences fails. -#[test] -fn otio_save_writes_file() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - - // One sequence in the root folder, one video track with a single clip. - FOLDER_CHILD_COUNT.store(1, Ordering::SeqCst); - *NODE_ID.lock().unwrap() = "org.olivevideoeditor.Olive.sequence".to_string(); - *NODE_LABEL.lock().unwrap() = "My Sequence".to_string(); - VIDEO_FRAME_RATE_NUM.store(25, Ordering::SeqCst); - VIDEO_FRAME_RATE_DEN.store(1, Ordering::SeqCst); - TRACK_LIST_TRACK_COUNT.store(1, Ordering::SeqCst); - TRACK_BLOCK_COUNT.store(1, Ordering::SeqCst); - BLOCK_KIND.store(1, Ordering::SeqCst); // OAKNODE_BLOCK_CLIP - BLOCK_IN_NUM.store(0, Ordering::SeqCst); - BLOCK_IN_DEN.store(1, Ordering::SeqCst); - BLOCK_LENGTH_NUM.store(25, Ordering::SeqCst); - BLOCK_LENGTH_DEN.store(1, Ordering::SeqCst); - *FOOTAGE_FILENAME.lock().unwrap() = "file:///tmp/video.mp4".to_string(); - - let out = std::env::temp_dir().join("oak-otio-out.otio"); - let out_str = out.to_string_lossy().into_owned(); - let _ = std::fs::remove_file(&out); - - let mut task = - unsafe { oaktask_create_project_save_otio(fake_handle(), common::cstr_of(&out_str)) }; - assert!(!task.ctx.is_null()); - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1); - assert!(out.exists(), "OTIO file was not written"); - - // Failure paths: null project or null filename. - assert!(unsafe { - oaktask_create_project_save_otio(CHandle::null(), common::cstr_of("/tmp/x.otio")) - } - .ctx - .is_null()); - assert!( - unsafe { oaktask_create_project_save_otio(fake_handle(), std::ptr::null()) } - .ctx - .is_null() - ); - - free(&mut task); - let _ = std::fs::remove_file(&out); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// Given a footage/sequence pair, `create_precache` runs to completion and -/// succeeds. -#[test] -fn precache_runs_and_succeeds() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - FOOTAGE_FILENAME.lock().unwrap().clear(); - let mut task = unsafe { oaktask_create_precache(fake_handle(), 0, fake_handle()) }; - assert!(!task.ctx.is_null()); - let needed = unsafe { oaktask_task_title(task, std::ptr::null_mut(), 0) }; - assert!(needed > 0); - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1); - assert_eq!(unsafe { oaktask_task_is_finished(task) }, 1); - - // Failure path: null footage/sequence. - assert!( - unsafe { oaktask_create_precache(CHandle::null(), 0, fake_handle()) } - .ctx - .is_null() - ); - assert!( - unsafe { oaktask_create_precache(fake_handle(), 0, CHandle::null()) } - .ctx - .is_null() - ); - - free(&mut task); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// create_export runs the full render+encoder path against the stubs and -/// succeeds; the encoder-failure path fails cleanly. -#[test] -fn export_runs_with_stubbed_encoder() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - *PROJECT_FILENAME.lock().unwrap() = "/tmp/oak-export-test.mp4".to_string(); - NODE_LABEL.lock().unwrap().clear(); - - let mut params = oaktask::bridge::codec::OakCodecEncodingParams { - filename: [0; 1024], - format: 0, - video_enabled: 1, - video_codec: 0, - video_width: 1920, - video_height: 1080, - video_time_base_num: 1, - video_time_base_den: 25, - video_pixel_format: 0, - video_interlacing: 0, - video_pixel_aspect_num: 1, - video_pixel_aspect_den: 1, - video_bit_rate: 0, - video_min_bit_rate: 0, - video_max_bit_rate: 0, - video_buffer_size: 0, - video_threads: 0, - video_pix_fmt: [0; 64], - video_is_image_sequence: 0, - video_scaling_method: 0, - audio_enabled: 0, - audio_codec: 0, - audio_sample_rate: 0, - audio_channel_layout: 0, - audio_sample_format: 0, - audio_bit_rate: 0, - subtitles_enabled: 0, - subtitles_codec: 0, - subtitles_are_sidecar: 0, - subtitles_sidecar_format: 0, - color_transform_output: [0; 256], - export_length_num: 0, - export_length_den: 0, - has_custom_range: 0, - custom_range_in_num: 0, - custom_range_in_den: 0, - custom_range_out_num: 0, - custom_range_out_den: 0, - }; - let filename = std::env::temp_dir() - .join("oak-export-test.mp4") - .to_string_lossy() - .into_owned(); - write_cstr(&filename, params.filename.as_mut_ptr(), 1024); - - ENCODER_INIT_NULL.store(0, Ordering::SeqCst); - ENCODER_OPEN_RESULT.store(0, Ordering::SeqCst); - ENCODER_WRITE_VIDEO_RESULT.store(0, Ordering::SeqCst); - SEQUENCE_LENGTH_NUM.store(25, Ordering::SeqCst); - SEQUENCE_LENGTH_DEN.store(1, Ordering::SeqCst); - VIDEO_TIME_BASE_NUM.store(1, Ordering::SeqCst); - VIDEO_TIME_BASE_DEN.store(1, Ordering::SeqCst); // 1s frames -> 25 frames - - let mut task = unsafe { oaktask_create_export(fake_handle(), fake_handle(), ¶ms) }; - assert!(!task.ctx.is_null()); - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1); - assert_eq!(unsafe { oaktask_task_is_finished(task) }, 1); - free(&mut task); - - // Failure path: encoder init fails. - ENCODER_INIT_NULL.store(1, Ordering::SeqCst); - let mut task = unsafe { oaktask_create_export(fake_handle(), fake_handle(), ¶ms) }; - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 0); - let needed = unsafe { oaktask_task_error(task, std::ptr::null_mut(), 0) }; - assert!(needed > 0); - let mut buf = vec![0i8; needed as usize]; - unsafe { oaktask_task_error(task, buf.as_mut_ptr(), needed) }; - assert!(cstr_read(&buf).contains("Failed to create encoder")); - free(&mut task); - - // Failure path: null viewer / null params. - assert!( - unsafe { oaktask_create_export(CHandle::null(), fake_handle(), ¶ms) } - .ctx - .is_null() - ); - assert!( - unsafe { oaktask_create_export(fake_handle(), fake_handle(), std::ptr::null()) } - .ctx - .is_null() - ); - - reset_stubs(); - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// The manager is needed for nothing in these factories; a fresh -/// init/shutdown cycle leaves the alive count at zero. -#[test] -fn manager_cycle_leaves_no_leaks() { - let _guard = MANAGER_LOCK.lock().unwrap(); - reset_stubs(); - unsafe { oaktask_manager_shutdown() }; - assert_eq!(unsafe { oaktask_manager_init() }, 0); - unsafe { oaktask_manager_shutdown() }; - assert_eq!(unsafe { oaktask_debug_alive_count() }, 0); -} - -/// cstr_read helper: read a NUL-terminated c_char buffer. -fn cstr_read(buf: &[i8]) -> String { - let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len()); - let bytes = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) }; - String::from_utf8_lossy(bytes).into_owned() -} diff --git a/crates/oaktask/tests/render_loop_test.rs b/crates/oaktask/tests/render_loop_test.rs deleted file mode 100644 index 4a4c72659..000000000 --- a/crates/oaktask/tests/render_loop_test.rs +++ /dev/null @@ -1,338 +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 . - -//! Concurrency contract of the [`RenderTask`] render loop (src/render.rs): -//! up to `max_inflight` oakrender tickets run concurrently, results reach -//! the behavior hooks in timestamp order through the reorder buffer (even -//! when tickets complete out of order), cancellation drains in-flight -//! tickets (their completions still fire exactly once), progress is -//! monotonic, and a hook error stops dispatch immediately. -//! -//! The link-time stubs in `common` simulate the oakrender ticket arena; -//! with `TICKET_DEFER = 1` tickets stay in flight until the test fires -//! them with `stub_complete`, so completion order is fully scripted. Frame -//! handles encode the submitting ticket id (0x10000 + id), so the tests -//! can assert the exact delivery order of frames. - -#[path = "common/mod.rs"] -mod common; - -use std::sync::atomic::Ordering; -use std::sync::{Arc, Mutex}; - -use common::*; -use oakcore_rs::{Rational, TimeRange}; -use oaktask::error::{Error, Result, OAKTASK_E_CANCELLED, OAKTASK_E_FAILED}; -use oaktask::handle::CHandle; -use oaktask::render::{ForceParams, RenderTask, RenderTaskBehavior}; -use oaktask::task::{Task, TaskEvent}; - -/// Poison-tolerant serialization (the shared stub registry is process-wide). -fn lock() -> std::sync::MutexGuard<'static, ()> { - MANAGER_LOCK.lock().unwrap_or_else(|e| e.into_inner()) -} - -/// Frame-handle ctx base (see common: STUB_FRAME_CTX_BASE). -const FRAME_CTX_BASE: usize = 0x10000; - -/// A RenderTask rendering `frames` 1-second frames (timebase 1/1) from -/// 0, optionally with audio enabled for the whole range. -fn render_task(frames: i64, audio: bool) -> RenderTask { - VIDEO_TIME_BASE_NUM.store(1, Ordering::SeqCst); - VIDEO_TIME_BASE_DEN.store(1, Ordering::SeqCst); - let range = TimeRange::new(Rational::new(0, 1), Rational::new(frames, 1)); - let base = Task::new("render-loop-test", fake_atom()); - let mut rt = RenderTask::new( - base, - CHandle::null(), - CHandle::null(), - fake_handle(), - ForceParams::default(), - None, - ); - rt.set_render_inputs(fake_handle(), CHandle::null(), 0, audio, range); - rt -} - -/// Records the hook calls in delivery order; `fail_on_frame` (0-based -/// index) makes that frame's `frame_downloaded` return an error. -#[derive(Default)] -struct RecordingBehavior { - /// ctx values of delivered frames, in delivery order (0x10000 + id). - frames: Vec, - /// number of `audio_downloaded` calls. - audio_calls: usize, - /// fail the frame at this delivery index, if set. - fail_on_frame: Option, -} - -impl RenderTaskBehavior for RecordingBehavior { - fn frame_downloaded(&mut self, _task: &mut Task, frame: CHandle) -> Result<()> { - if let Some(i) = self.fail_on_frame { - if self.frames.len() == i { - return Err(Error::Failed("stub encoder write failure".to_string())); - } - } - self.frames.push(frame.ctx as usize); - Ok(()) - } - - fn audio_downloaded(&mut self, _task: &mut Task, _buffer: CHandle) -> Result<()> { - self.audio_calls += 1; - Ok(()) - } - - fn encode_subtitle(&mut self, _task: &mut Task, _text: &str) -> Result<()> { - Ok(()) - } -} - -/// Drive `render` on a spawned thread. The base task lives inside the -/// render task; it is moved out first so the test can keep its cancel atom. -fn run_render( - mut rt: RenderTask, - behavior: RecordingBehavior, -) -> std::thread::JoinHandle<(Result<()>, RecordingBehavior)> { - std::thread::spawn(move || { - let mut base = std::mem::replace(&mut rt.base, Task::new("", CHandle::null())); - let mut behavior = behavior; - let result = rt.render(&mut base, &mut behavior); - (result, behavior) - }) -} - -/// Frames complete in scrambled order; the hooks still see them in -/// timestamp order, and every completion fires exactly once. -#[test] -fn render_delivers_frames_in_order_under_scrambled_completion() { - let _g = lock(); - reset_stubs(); - TICKET_DEFER.store(1, Ordering::SeqCst); - - let mut rt = render_task(5, false); - rt.set_max_inflight(8); // window >= frame count: all tickets submitted upfront - let handle = run_render(rt, RecordingBehavior::default()); - - stub_wait_submitted(5); - for id in [2usize, 0, 1, 4, 3] { - stub_complete(id); - } - - let (result, behavior) = handle.join().unwrap(); - assert!(result.is_ok()); - assert_eq!( - behavior.frames, - vec![ - FRAME_CTX_BASE, - FRAME_CTX_BASE + 1, - FRAME_CTX_BASE + 2, - FRAME_CTX_BASE + 3, - FRAME_CTX_BASE + 4 - ], - "frames must be delivered in timestamp order despite scrambled completion" - ); - assert_eq!( - stub_completed_count(), - 5, - "each ticket completes exactly once" - ); -} - -/// The audio ticket completes last of all; it is still delivered before any -/// frame (the observable audio-first contract). -#[test] -fn render_delivers_audio_before_frames_under_scrambled_completion() { - let _g = lock(); - reset_stubs(); - TICKET_DEFER.store(1, Ordering::SeqCst); - - let mut rt = render_task(3, true); - rt.set_max_inflight(4); // audio + 3 frames, all submitted upfront - let handle = run_render(rt, RecordingBehavior::default()); - - stub_wait_submitted(4); - // Frames 2,3 and 1 complete before the audio ticket (id 0). - for id in [2usize, 3, 1, 0] { - stub_complete(id); - } - - let (result, behavior) = handle.join().unwrap(); - assert!(result.is_ok()); - assert_eq!(behavior.audio_calls, 1, "audio delivered exactly once"); - assert_eq!( - behavior.frames, - vec![FRAME_CTX_BASE + 1, FRAME_CTX_BASE + 2, FRAME_CTX_BASE + 3], - "audio first, then frames in timestamp order" - ); - assert_eq!(stub_completed_count(), 4); -} - -/// Cancellation between frames: the loop aborts, every in-flight ticket is -/// cancelled+waited, and each of their completions still fires exactly once. -#[test] -fn render_cancel_drains_inflight_and_fires_their_completions() { - let _g = lock(); - reset_stubs(); - TICKET_DEFER.store(1, Ordering::SeqCst); - - let mut rt = render_task(5, false); - rt.set_max_inflight(2); - let atom = rt.base.get_cancel_atom(); - let handle = run_render(rt, RecordingBehavior::default()); - - // Frame 0 completes normally; the loop delivers it and tops up ticket 2. - stub_wait_submitted(2); - stub_complete(0); - stub_wait_submitted(3); - - // Cancel through the atom. The stub atom-cancel finishes the pending - // tickets (real cancellation propagates into in-flight renders), waking - // the loop; the loop then sees the cancellation and drains. - unsafe { - oaktask::bridge::render::oakrender_cancelatom_cancel(atom); - } - - let (result, behavior) = handle.join().unwrap(); - assert_eq!(result.unwrap_err().code(), OAKTASK_E_CANCELLED); - assert_eq!( - behavior.frames, - vec![FRAME_CTX_BASE], - "only frame 0 delivered before cancel" - ); - assert_eq!( - stub_completed_count(), - 3, - "every submitted ticket's completion fires (incl. on cancel)" - ); - assert_eq!( - stub_submitted_count(), - 3, - "no new tickets are dispatched after cancellation" - ); -} - -/// Progress events are non-decreasing and end at 1.0 (5 frames, 1s each). -#[test] -fn render_progress_is_monotonic_and_reaches_one() { - let _g = lock(); - reset_stubs(); - - let mut rt = render_task(5, false); - rt.set_max_inflight(8); - let progress: Arc>> = Default::default(); - { - let progress = progress.clone(); - rt.base.set_event_listener(Box::new(move |ev: TaskEvent| { - if let TaskEvent::Progress(v) = ev { - progress.lock().unwrap().push(v); - } - })); - } - let handle = run_render(rt, RecordingBehavior::default()); - - let (result, behavior) = handle.join().unwrap(); - assert!(result.is_ok()); - assert_eq!(behavior.frames.len(), 5); - - let seen = progress.lock().unwrap().clone(); - assert_eq!(seen.len(), 5, "one progress event per delivered frame"); - assert!( - seen.windows(2).all(|w| w[0] <= w[1]), - "progress must be non-decreasing, got {seen:?}" - ); - assert!( - (seen[seen.len() - 1] - 1.0).abs() < 1e-9, - "progress reaches 1.0" - ); -} - -/// A hook error mid-stream stops dispatching new tickets; the in-flight -/// tickets are drained and their completions fire. -#[test] -fn render_error_stops_dispatching_new_tickets() { - let _g = lock(); - reset_stubs(); - TICKET_DEFER.store(1, Ordering::SeqCst); - - let mut behavior = RecordingBehavior::default(); - behavior.fail_on_frame = Some(2); // the third delivered frame fails - let mut rt = render_task(10, false); - rt.set_max_inflight(2); - let handle = run_render(rt, behavior); - - stub_wait_submitted(2); - stub_complete(0); - stub_wait_submitted(3); // frame 0 delivered, ticket 2 submitted - stub_complete(1); - stub_wait_submitted(4); // frame 1 delivered, ticket 3 submitted - stub_complete(2); // frame 2 fails in the hook - - let (result, behavior) = handle.join().unwrap(); - assert_eq!(result.unwrap_err().code(), OAKTASK_E_FAILED); - assert_eq!( - behavior.frames, - vec![FRAME_CTX_BASE, FRAME_CTX_BASE + 1], - "two frames delivered before the error" - ); - assert_eq!( - stub_submitted_count(), - 4, - "no ticket past the failing frame" - ); - assert_eq!( - stub_completed_count(), - 4, - "in-flight tickets are drained, completions fire" - ); -} - -/// Default stub mode (completions fire synchronously on submit): the loop -/// still windows the submissions and delivers in timestamp order. -#[test] -fn render_succeeds_with_immediate_completion_and_windowing() { - let _g = lock(); - reset_stubs(); - - let mut rt = render_task(7, false); - rt.set_max_inflight(3); - let handle = run_render(rt, RecordingBehavior::default()); - - let (result, behavior) = handle.join().unwrap(); - assert!(result.is_ok()); - assert_eq!(behavior.frames.len(), 7); - assert_eq!( - behavior.frames, - (0..7).map(|i| FRAME_CTX_BASE + i).collect::>(), - "frames delivered in timestamp order" - ); - assert_eq!(stub_completed_count(), 7); -} - -/// An empty export range renders nothing and succeeds. -#[test] -fn render_empty_range_succeeds_without_tickets() { - let _g = lock(); - reset_stubs(); - - let mut rt = render_task(0, false); - rt.set_max_inflight(1); - let handle = run_render(rt, RecordingBehavior::default()); - - let (result, behavior) = handle.join().unwrap(); - assert!(result.is_ok()); - assert!(behavior.frames.is_empty()); - assert_eq!(stub_submitted_count(), 0); -} diff --git a/crates/oaktask/tests/render_real_integration_test.rs b/crates/oaktask/tests/render_real_integration_test.rs deleted file mode 100644 index a3497425c..000000000 --- a/crates/oaktask/tests/render_real_integration_test.rs +++ /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 . - -//! Real-oakrender integration: drive oaktask's [`RenderTask`] render loop -//! against the **actual** oakrender ticket arena instead of the link-time -//! stubs of `tests/common/mod.rs`. -//! -//! The two crates' C ABIs are joined the way the app joins them: oaktask's -//! `bridge::render` `extern "C"` declarations resolve against oakrender's -//! `#[no_mangle]` exports (both rlibs linked into this one test binary), -//! the `oakrender_ticket_finished_fn` callback (the frozen 2-argument -//! `(ticket, userdata)` contract) fires on the arena's worker threads, and -//! the loop's reorder buffer delivers the frames to the hooks in timestamp -//! order. The render crate's CPU path is used (`eval::render_produced_frame` -//! generates F32 frames; no GPU is initialized). -//! -//! Only the render symbols are real; the oaknode/oakcodec/oakcommon/undo -//! sides still come from the `common` stubs (the render loop only touches -//! them for graph queries and timebase reads). -//! -//! Build / run (both rlibs in one binary — duplicate `#[no_mangle]` -//! symbols make this incompatible with the stub-based test binaries, so it -//! must be built with the feature and the `--test` filter): -//! -//! ```text -//! cargo test --features real-oakrender --test render_real_integration_test -//! ``` - -#![cfg(feature = "real-oakrender")] -#![allow(clippy::missing_safety_doc)] - -#[path = "common/mod.rs"] -mod common; - -use std::sync::atomic::Ordering; - -use oakcore_rs::{Rational, TimeRange}; -use oaktask::error::Result; -use oaktask::handle::CHandle; -use oaktask::render::{ForceParams, RenderTask, RenderTaskBehavior}; -use oaktask::task::Task; - -/// Records what the render loop delivers: each frame's timestamp (the -/// producer stamps `Frame::timestamp` with the ticket time) and size. -#[derive(Default)] -struct RecordingBehavior { - timestamps: Vec, - sizes: Vec<(i32, i32)>, -} - -impl RenderTaskBehavior for RecordingBehavior { - fn frame_downloaded(&mut self, _task: &mut Task, frame: CHandle) -> Result<()> { - assert!(!frame.is_null(), "real arena must hand out an owned frame"); - // The frame handle boxes a `oakrender::texture::Frame` (created by - // `oakrender_ticket_get_frame`); read it through oakrender's own - // handle API to assert the real payload. - let rh = oakrender::handle::CHandle { - ctx: frame.ctx, - addref: frame.addref, - release: frame.release, - abi_version: frame.abi_version, - }; - let f = unsafe { oakrender::handle::get::(&rh) } - .expect("frame handle must box a real oakrender Frame"); - self.timestamps.push(f.timestamp); - self.sizes.push((f.width, f.height)); - Ok(()) - } - - fn audio_downloaded(&mut self, _task: &mut Task, _buffer: CHandle) -> Result<()> { - panic!("audio disabled for this test") - } - - fn encode_subtitle(&mut self, _task: &mut Task, _text: &str) -> Result<()> { - Ok(()) - } -} - -/// Serializes against anything else touching the process-wide manager. -fn manager_guard() -> std::sync::MutexGuard<'static, ()> { - static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); - LOCK.lock().unwrap_or_else(|e| e.into_inner()) -} - -/// The real-oakrender end-to-end run: 3 one-second frames (timebase 1/1) -/// through the task render loop into the arena, delivered in timestamp -/// order with the forced 64x64 size. -#[test] -fn render_loop_submits_real_tickets_and_delivers_in_timestamp_order() { - let _guard = manager_guard(); - common::reset_stubs(); - common::VIDEO_TIME_BASE_NUM.store(1, Ordering::SeqCst); - common::VIDEO_TIME_BASE_DEN.store(1, Ordering::SeqCst); - - // Fresh real manager (CPU path; the ticket producer is eval-based, no - // GPU backend is created). - oakrender::manager::RenderManager::shutdown(); - oakrender::manager::RenderManager::init().expect("real oakrender manager init"); - let baseline = oakrender::handle::alive_count(); - - { - let range = TimeRange::new(Rational::new(0, 1), Rational::new(3, 1)); - let base = Task::new("real-render-integration", CHandle::null()); - let mut rt = RenderTask::new( - base, - common::fake_handle(), // borrowed video params (ctx checked only) - CHandle::null(), // no audio - common::fake_handle(), // viewer node (the node stub supplies the output) - ForceParams { - force_width: 64, - force_height: 64, - ..Default::default() - }, - None, - ); - rt.set_render_inputs(CHandle::null(), CHandle::null(), 0, false, range); - rt.set_max_inflight(3); - - let mut behavior = RecordingBehavior::default(); - let mut task = std::mem::replace(&mut rt.base, Task::new("", CHandle::null())); - let result = rt.render(&mut task, &mut behavior); - drop(rt); - - assert!( - result.is_ok(), - "render through the real arena must succeed: {result:?}" - ); - assert_eq!( - behavior.timestamps, - vec![ - Rational::new(0, 1), - Rational::new(1, 1), - Rational::new(2, 1), - ], - "frames must arrive in timestamp order (reorder buffer over the real arena)" - ); - assert_eq!( - behavior.sizes, - vec![(64, 64), (64, 64), (64, 64)], - "force params must reach the real producer" - ); - // `task` owns the real cancel atom created by Task::new; dropping it - // releases the atom so the handle accounting below returns to - // baseline. - drop(task); - } - - oakrender::manager::RenderManager::shutdown(); - assert_eq!( - oakrender::handle::alive_count(), - baseline, - "every handle created by the run (tickets, frames, cancel atom) must be released" - ); -} diff --git a/crates/oaktask/tests/task_run_test.rs b/crates/oaktask/tests/task_run_test.rs deleted file mode 100644 index 40379e1ed..000000000 --- a/crates/oaktask/tests/task_run_test.rs +++ /dev/null @@ -1,550 +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 . - -//! Run-path coverage: conform/proxy/export/render task bodies driven end to -//! end against the link-time stubs in `common` (codec-submit routing, -//! decoder/encoder knobs, a fake ffmpeg executable), plus the base -//! [`Task`] API (reset/elapsed/wait/cancel) and the subscribe event stream. -//! -//! These tests exercise the same paths the C++ gtest suite covers with the -//! real dylibs (`src/task/tests/task_test.cpp`); here the stubs make them -//! deterministic under plain `cargo test`. - -#[path = "common/mod.rs"] -mod common; - -use std::ffi::c_int; -use std::sync::atomic::Ordering; - -use common::*; -use oaktask::bridge::codec::{ - OakCodecTaskRequest, OAKCODEC_E_CANCELLED, OAKCODEC_E_FAILED, OAKCODEC_TASK_CONFORM, - OAKCODEC_TASK_PROXY, -}; -use oaktask::error::Result; -use oaktask::ffi::manager::oaktask_manager_shutdown; -use oaktask::ffi::project::oaktask_create_export; -use oaktask::ffi::task::{oaktask_task_error, oaktask_task_free, oaktask_task_start_sync}; -use oaktask::task::{Task, TaskBehavior, TaskEvent}; - -// --------------------------------------------------------------------------- -// Codec-submit routing (conform/proxy run bodies) -// --------------------------------------------------------------------------- - -/// Build a codec task request and drive it through the registered submit -/// callback (the codec module's view of the task module). -/// Poison-tolerant serialization (a failing sibling test must not cascade). -fn lock() -> std::sync::MutexGuard<'static, ()> { - MANAGER_LOCK.lock().unwrap_or_else(|e| e.into_inner()) -} - -fn submit(request: &OakCodecTaskRequest) -> c_int { - let cb = SUBMIT_CB.lock().unwrap().clone(); - match cb { - Some(f) => unsafe { f(request, std::ptr::null_mut()) }, - None => OAKCODEC_E_FAILED, - } -} - -/// Given a conform request with pre-existing working files, the conform task -/// transcodes (stub) and renames the working files into place — the submit -/// callback returns OAKCODEC_OK. -#[test] -fn conform_run_succeeds_via_codec_submit() { - let _g = lock(); - reset_stubs(); - unsafe { oaktask::ffi::manager::oaktask_register_codec_submitter() }; - DECODER_OPEN_RESULT.store(0, Ordering::SeqCst); - DECODER_CONFORM_RESULT.store(0, Ordering::SeqCst); - - let dir = std::env::temp_dir(); - let out = dir.join("oak-conform.0.pcm"); - let out_str = out.to_string_lossy().into_owned(); - let working = [ - dir.join("oak-conform.0.pcm.working"), - dir.join("oak-conform.1.pcm.working"), - ]; - for w in &working { - std::fs::write(w, b"pcm").unwrap(); - } - - let request = OakCodecTaskRequest { - kind: OAKCODEC_TASK_CONFORM, - input_filename: cstr_of("/nonexistent/input.mp4"), - output_filename: cstr_of(&out_str), - stream_index: 0, - sample_rate: 48000, - channel_layout: 0x3, // stereo -> 2 working files - sample_format: 4, - proxy_width: 0, - proxy_height: 0, - }; - assert_eq!(submit(&request), 0, "conform submit must succeed"); - - // The working files were renamed into their final names. - assert!(out.exists(), "final pcm file must exist after rename"); - assert!(!working[0].exists(), "working file must be renamed away"); - assert!(!working[1].exists()); - - for w in &working { - let _ = std::fs::remove_file(w); - } - for f in [&out, &dir.join("oak-conform.1.pcm")] { - let _ = std::fs::remove_file(f); - } - unsafe { oaktask_manager_shutdown() }; -} - -/// Given a conform request whose decoder fails to open, the task reports the -/// decoder error and the submit callback returns OAKCODEC_E_FAILED. -#[test] -fn conform_run_fails_when_decoder_open_fails() { - let _g = lock(); - reset_stubs(); - unsafe { oaktask::ffi::manager::oaktask_register_codec_submitter() }; - DECODER_OPEN_RESULT.store(-50003, Ordering::SeqCst); - - let request = OakCodecTaskRequest { - kind: OAKCODEC_TASK_CONFORM, - input_filename: cstr_of("/nonexistent/input.mp4"), - output_filename: cstr_of("/nonexistent/audio.0.pcm"), - stream_index: 0, - sample_rate: 48000, - channel_layout: 0x3, - sample_format: 4, - proxy_width: 0, - proxy_height: 0, - }; - assert_eq!(submit(&request), OAKCODEC_E_FAILED); - unsafe { oaktask_manager_shutdown() }; -} - -/// Given a conform request whose decode is cancelled, the task cleans up the -/// working files and fails (the cancelled error is surfaced through the -/// task's error string). -#[test] -fn conform_run_reports_cancelled_conform() { - let _g = lock(); - reset_stubs(); - unsafe { oaktask::ffi::manager::oaktask_register_codec_submitter() }; - DECODER_OPEN_RESULT.store(0, Ordering::SeqCst); - DECODER_CONFORM_RESULT.store(OAKCODEC_E_CANCELLED, Ordering::SeqCst); - - let dir = std::env::temp_dir(); - let out = dir.join("oak-cancel.0.pcm"); - let working = dir.join("oak-cancel.0.pcm.working"); - std::fs::write(&working, b"pcm").unwrap(); - - let request = OakCodecTaskRequest { - kind: OAKCODEC_TASK_CONFORM, - input_filename: cstr_of("/nonexistent/input.mp4"), - output_filename: cstr_of(&out.to_string_lossy()), - stream_index: 0, - sample_rate: 48000, - channel_layout: 0x3, - sample_format: 4, - proxy_width: 0, - proxy_height: 0, - }; - assert_eq!(submit(&request), OAKCODEC_E_FAILED); - // The cancelled conform removes its partial working files. - assert!( - !working.exists(), - "working files must be cleaned up on cancel" - ); - assert!(!out.exists()); - unsafe { oaktask_manager_shutdown() }; -} - -/// Write an executable fake-ffmpeg script that creates its last argument -/// (the working proxy file) and reports some progress. -fn write_fake_ffmpeg(path: &std::path::Path, body: &str) { - std::fs::write(path, body).unwrap(); - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap(); -} - -/// Given a proxy request and a working fake ffmpeg, the proxy task spawns it, -/// parses progress, renames the working file into place and succeeds. -#[test] -fn proxy_run_succeeds_with_fake_ffmpeg() { - let _g = lock(); - reset_stubs(); - unsafe { oaktask::ffi::manager::oaktask_register_codec_submitter() }; - - let dir = std::env::temp_dir(); - let script = dir.join("oak-fake-ffmpeg.sh"); - write_fake_ffmpeg( - &script, - "#!/bin/sh\nfor a in \"$@\"; do :; done\necho \"out_time_us=5000000\"\ntouch \"$a\"\nexit 0\n", - ); - *PROXY_FFMPEG_PATH.lock().unwrap() = script.to_string_lossy().into_owned(); - - let out = dir.join("oak-proxy-out.mp4"); - let out_str = out.to_string_lossy().into_owned(); - let working = dir.join("oak-proxy-out.mp4.working.mp4"); - let _ = std::fs::remove_file(&out); - let _ = std::fs::remove_file(&working); - - let request = OakCodecTaskRequest { - kind: OAKCODEC_TASK_PROXY, - input_filename: cstr_of("/nonexistent/source.mp4"), - output_filename: cstr_of(&out_str), - stream_index: 0, - sample_rate: 0, - channel_layout: 0, - sample_format: 0, - proxy_width: 0, - proxy_height: 0, - }; - assert_eq!(submit(&request), 0, "proxy submit must succeed"); - assert!(out.exists(), "proxy output must exist after rename"); - assert!(!working.exists()); - - let _ = std::fs::remove_file(&out); - let _ = std::fs::remove_file(&script); - unsafe { oaktask_manager_shutdown() }; -} - -/// Given a proxy request whose ffmpeg exits non-zero, the task cleans up the -/// working file and fails. -#[test] -fn proxy_run_fails_when_ffmpeg_fails() { - let _g = lock(); - reset_stubs(); - unsafe { oaktask::ffi::manager::oaktask_register_codec_submitter() }; - - let dir = std::env::temp_dir(); - let script = dir.join("oak-fake-ffmpeg-fail.sh"); - write_fake_ffmpeg(&script, "#!/bin/sh\nexit 1\n"); - *PROXY_FFMPEG_PATH.lock().unwrap() = script.to_string_lossy().into_owned(); - - let out = dir.join("oak-proxy-fail.mp4"); - let request = OakCodecTaskRequest { - kind: OAKCODEC_TASK_PROXY, - input_filename: cstr_of("/nonexistent/source.mp4"), - output_filename: cstr_of(&out.to_string_lossy()), - stream_index: 0, - sample_rate: 0, - channel_layout: 0, - sample_format: 0, - proxy_width: 0, - proxy_height: 0, - }; - assert_eq!(submit(&request), OAKCODEC_E_FAILED); - assert!(!out.exists()); - - let _ = std::fs::remove_file(&script); - unsafe { oaktask_manager_shutdown() }; -} - -/// Given no configured ffmpeg, the proxy task fails with the documented -/// "not found" error. -#[test] -fn proxy_run_fails_without_ffmpeg() { - let _g = lock(); - reset_stubs(); - unsafe { oaktask::ffi::manager::oaktask_register_codec_submitter() }; - PROXY_FFMPEG_PATH.lock().unwrap().clear(); - - let request = OakCodecTaskRequest { - kind: OAKCODEC_TASK_PROXY, - input_filename: cstr_of("/nonexistent/source.mp4"), - output_filename: cstr_of("/nonexistent/out.mp4"), - stream_index: 0, - sample_rate: 0, - channel_layout: 0, - sample_format: 0, - proxy_width: 0, - proxy_height: 0, - }; - assert_eq!(submit(&request), OAKCODEC_E_FAILED); - unsafe { oaktask_manager_shutdown() }; -} - -/// Given an unknown codec task kind, the submit callback rejects it with -/// OAKCODEC_E_INVALID. -#[test] -fn codec_submit_rejects_unknown_kind() { - let _g = lock(); - reset_stubs(); - unsafe { oaktask::ffi::manager::oaktask_register_codec_submitter() }; - - let request = OakCodecTaskRequest { - kind: 99, - input_filename: std::ptr::null(), - output_filename: std::ptr::null(), - stream_index: 0, - sample_rate: 0, - channel_layout: 0, - sample_format: 0, - proxy_width: 0, - proxy_height: 0, - }; - assert_eq!(submit(&request), oaktask::bridge::codec::OAKCODEC_E_INVALID); - unsafe { oaktask_manager_shutdown() }; -} - -// --------------------------------------------------------------------------- -// Export run paths (render + encoder) -// --------------------------------------------------------------------------- - -/// A base encoding-params POD with video enabled. -fn export_params(filename: &str) -> oaktask::bridge::codec::OakCodecEncodingParams { - let mut params = oaktask::bridge::codec::OakCodecEncodingParams { - filename: [0; 1024], - format: 0, - video_enabled: 1, - video_codec: 0, - video_width: 1280, - video_height: 720, - video_time_base_num: 1, - video_time_base_den: 25, - video_pixel_format: 0, - video_interlacing: 0, - video_pixel_aspect_num: 1, - video_pixel_aspect_den: 1, - video_bit_rate: 0, - video_min_bit_rate: 0, - video_max_bit_rate: 0, - video_buffer_size: 0, - video_threads: 0, - video_pix_fmt: [0; 64], - video_is_image_sequence: 0, - video_scaling_method: 0, - audio_enabled: 0, - audio_codec: 0, - audio_sample_rate: 0, - audio_channel_layout: 0, - audio_sample_format: 0, - audio_bit_rate: 0, - subtitles_enabled: 0, - subtitles_codec: 0, - subtitles_are_sidecar: 0, - subtitles_sidecar_format: 0, - color_transform_output: [0; 256], - export_length_num: 0, - export_length_den: 0, - has_custom_range: 0, - custom_range_in_num: 0, - custom_range_in_den: 0, - custom_range_out_num: 0, - custom_range_out_den: 0, - }; - write_cstr(filename, params.filename.as_mut_ptr(), 1024); - params -} - -/// The export task with audio enabled renders the audio ticket and succeeds; -/// the encoder receives the frames. -#[test] -fn export_runs_audio_path() { - let _g = lock(); - reset_stubs(); - let mut params = export_params("/tmp/oak-export-audio.mp4"); - params.audio_enabled = 1; - SEQUENCE_LENGTH_NUM.store(10, Ordering::SeqCst); - SEQUENCE_LENGTH_DEN.store(1, Ordering::SeqCst); - VIDEO_TIME_BASE_NUM.store(1, Ordering::SeqCst); - VIDEO_TIME_BASE_DEN.store(1, Ordering::SeqCst); // 1s frames - - let mut task = unsafe { oaktask_create_export(fake_handle(), fake_handle(), ¶ms) }; - assert!(!task.ctx.is_null()); - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1); - unsafe { oaktask_task_free(&mut task) }; -} - -/// When the encoder cannot open the file, the export fails with the -/// documented error. -#[test] -fn export_fails_on_encoder_open() { - let _g = lock(); - reset_stubs(); - ENCODER_OPEN_RESULT.store(-50003, Ordering::SeqCst); - let params = export_params("/tmp/oak-export-open-fail.mp4"); - - let mut task = unsafe { oaktask_create_export(fake_handle(), fake_handle(), ¶ms) }; - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 0); - let needed = unsafe { oaktask_task_error(task, std::ptr::null_mut(), 0) }; - assert!(needed > 0); - unsafe { oaktask_task_free(&mut task) }; -} - -/// When the encoder rejects a video frame, the export aborts with the -/// encoder's error. -#[test] -fn export_fails_on_video_write() { - let _g = lock(); - reset_stubs(); - ENCODER_WRITE_VIDEO_RESULT.store(-50003, Ordering::SeqCst); - SEQUENCE_LENGTH_NUM.store(10, Ordering::SeqCst); - SEQUENCE_LENGTH_DEN.store(1, Ordering::SeqCst); - VIDEO_TIME_BASE_NUM.store(1, Ordering::SeqCst); - VIDEO_TIME_BASE_DEN.store(1, Ordering::SeqCst); - let params = export_params("/tmp/oak-export-write-fail.mp4"); - - let mut task = unsafe { oaktask_create_export(fake_handle(), fake_handle(), ¶ms) }; - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 0); - unsafe { oaktask_task_free(&mut task) }; -} - -/// When the encoder flush surfaces an error, the export fails after the -/// render loop. -#[test] -fn export_fails_on_flush_error() { - let _g = lock(); - reset_stubs(); - ENCODER_FLUSH_ERROR.store(1, Ordering::SeqCst); - *ENCODER_LAST_ERROR.lock().unwrap() = "stub flush failure".to_string(); - SEQUENCE_LENGTH_NUM.store(1, Ordering::SeqCst); - SEQUENCE_LENGTH_DEN.store(1, Ordering::SeqCst); - VIDEO_TIME_BASE_NUM.store(1, Ordering::SeqCst); - VIDEO_TIME_BASE_DEN.store(1, Ordering::SeqCst); - let params = export_params("/tmp/oak-export-flush-fail.mp4"); - - let mut task = unsafe { oaktask_create_export(fake_handle(), fake_handle(), ¶ms) }; - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 0); - let mut err = [0i8; 256]; - let needed = unsafe { oaktask_task_error(task, err.as_mut_ptr(), err.len() as i32) }; - assert!(needed > 0); - unsafe { oaktask_task_free(&mut task) }; -} - -/// When the viewer has no connected output node, the first frame render fails -/// and the export aborts. -#[test] -fn export_fails_without_connected_output() { - let _g = lock(); - reset_stubs(); - CONNECTED_NODE.store(0, Ordering::SeqCst); - SEQUENCE_LENGTH_NUM.store(10, Ordering::SeqCst); - SEQUENCE_LENGTH_DEN.store(1, Ordering::SeqCst); - VIDEO_TIME_BASE_NUM.store(1, Ordering::SeqCst); - VIDEO_TIME_BASE_DEN.store(1, Ordering::SeqCst); - let params = export_params("/tmp/oak-export-no-output.mp4"); - - let mut task = unsafe { oaktask_create_export(fake_handle(), fake_handle(), ¶ms) }; - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 0); - unsafe { oaktask_task_free(&mut task) }; -} - -/// When the render ticket hands out null frames repeatedly, the export -/// aborts after the null-frame streak limit (C++ `null_frame_streak_`). -#[test] -fn export_fails_on_null_frame_streak() { - let _g = lock(); - reset_stubs(); - TICKET_FRAME_VALID.store(0, Ordering::SeqCst); - SEQUENCE_LENGTH_NUM.store(30, Ordering::SeqCst); - SEQUENCE_LENGTH_DEN.store(1, Ordering::SeqCst); - VIDEO_TIME_BASE_NUM.store(1, Ordering::SeqCst); - VIDEO_TIME_BASE_DEN.store(1, Ordering::SeqCst); - let params = export_params("/tmp/oak-export-null-frames.mp4"); - - let mut task = unsafe { oaktask_create_export(fake_handle(), fake_handle(), ¶ms) }; - assert_eq!(unsafe { oaktask_task_start_sync(task) }, 0); - let mut err = [0i8; 256]; - let needed = unsafe { oaktask_task_error(task, err.as_mut_ptr(), err.len() as i32) }; - assert!(needed > 0); - unsafe { oaktask_task_free(&mut task) }; -} - -// --------------------------------------------------------------------------- -// Base Task API + subscribe event stream -// --------------------------------------------------------------------------- - -/// A no-op behavior for direct `Task` API tests. -struct NoopBehavior; - -impl TaskBehavior for NoopBehavior { - fn run(&mut self, _task: &mut Task) -> Result<()> { - Ok(()) - } -} - -/// The base task API: title/error/elapsed/reset/wait/cancel and the -/// lifecycle event stream (STARTED/PROGRESS/FINISHED). -#[test] -fn task_base_api_and_events() { - let _g = lock(); - reset_stubs(); - - let mut task = Task::new("Base API", common::fake_atom()); - task.set_title("Renamed"); - assert_eq!(task.title(), "Renamed"); - assert!(task.error().is_none()); - assert!(!task.is_finished()); - assert!(!task.succeeded()); - assert!(task.elapsed().is_none()); - - // Progress is clamped to 0..1 and delivered to the listener. - let events: std::sync::Arc>> = Default::default(); - let listener = { - let events = events.clone(); - Box::new(move |ev: TaskEvent| events.lock().unwrap().push(ev)) - }; - task.set_event_listener(listener); - task.emit_progress(2.0); - task.set_behavior(Box::new(NoopBehavior)); - - let result = task.start(); - assert!(result.is_ok()); - assert!(task.is_finished()); - assert!(task.succeeded()); - assert!(task.elapsed().is_some()); - - // The listener saw PROGRESS (emitted before start), then STARTED and - // FINISHED from `start()`. - let seen = events.lock().unwrap().clone(); - assert_eq!(seen.len(), 3, "PROGRESS + STARTED + FINISHED"); - assert!(matches!(seen[0], TaskEvent::Progress(p) if p == 1.0)); - assert!(matches!(seen[1], TaskEvent::Started)); - assert!(matches!(seen[2], TaskEvent::Finished)); - - // Cancellation through the atom. - let atom = task.get_cancel_atom(); - task.set_cancel_event(Box::new(|| {})); - task.cancel(); - assert!(task.is_cancelled()); - unsafe { - oaktask::bridge::render::oakrender_cancelatom_cancel(atom); - } - assert!(task.is_cancelled()); - - // reset clears the lifecycle state. - task.reset(); - assert!(!task.is_finished()); - assert!(!task.succeeded()); - assert!(task.error().is_none()); - assert!(task.elapsed().is_none()); - - // error reporting. - task.set_error("boom"); - assert_eq!(task.error(), Some("boom")); - - // wait_finished on a finished task returns immediately. - task.wait_finished(); - drop(atom); -} - -/// The error marker `Task::cancelled()` maps to OAKTASK_E_CANCELLED. -#[test] -fn cancelled_marker_maps_to_code() { - assert_eq!( - oaktask::task::cancelled().code(), - oaktask::error::OAKTASK_E_CANCELLED - ); -} diff --git a/crates/oaktimeline/Cargo.toml b/crates/oaktimeline/Cargo.toml index 436f60544..86e871762 100644 --- a/crates/oaktimeline/Cargo.toml +++ b/crates/oaktimeline/Cargo.toml @@ -8,14 +8,6 @@ license = "GPL-3.0-or-later" [lib] crate-type = ["staticlib", "rlib"] -[features] -# Compile the in-crate mock implementations (src/bridge/teststubs.rs) so the -# integration tests can resolve `bridge::teststubs`; the bridge fns route to -# them under this feature (see single-lib.md). Plain Rust mocks, no -# `#[no_mangle]` — they coexist with the real oaknode/oakundo/oakcommon -# path dependencies. Every test run must pass this flag (README "testing"). -test-stubs = [] - [dependencies] oakcore-rs = { path = "../oakcore" } oakundo = { path = "../oakundo" } diff --git a/crates/oaktimeline/src/bridge/common.rs b/crates/oaktimeline/src/bridge/common.rs deleted file mode 100644 index e2ca92fe2..000000000 --- a/crates/oaktimeline/src/bridge/common.rs +++ /dev/null @@ -1,306 +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 . - -// 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 . - -//! oakcommon C ABI imports (XML reader/writer + config), mirroring -//! `include/common/xmlutils.h` and `include/common/config.h`. - -use std::ffi::{c_char, c_int}; - -use crate::handle::CHandle; - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_xml_reader_init(data: *const c_char) -> CHandle { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakcommon_xml_reader_init(data) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_init(data) } - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_xml_reader_free(reader: *mut CHandle) { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakcommon_xml_reader_free(reader) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_free(reader) } - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_xml_reader_skip_current_element(reader: CHandle) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakcommon_xml_reader_skip_current_element(reader) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_skip_current_element(reader) } - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). The -/// real ABI advances the reader and writes a 1/0 `found` flag (the name is -/// read with [`oakcommon_xml_reader_name`]). -pub fn oakcommon_xml_reader_read_next_start_element(reader: CHandle, found: *mut c_int) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakcommon_xml_reader_read_next_start_element(reader, found) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { - oakcommon::ffi::xmlutils::oakcommon_xml_reader_read_next_start_element(reader, found) - } - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_xml_reader_name(reader: CHandle, name: *mut c_char, buf_size: c_int) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakcommon_xml_reader_name(reader, name, buf_size) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_name(reader, name, buf_size) } - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_xml_reader_read_element_text( - reader: CHandle, - text: *mut c_char, - buf_size: c_int, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakcommon_xml_reader_read_element_text(reader, text, buf_size) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { - oakcommon::ffi::xmlutils::oakcommon_xml_reader_read_element_text(reader, text, buf_size) - } - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_xml_reader_attribute_count(reader: CHandle, count: *mut c_int) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakcommon_xml_reader_attribute_count(reader, count) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_count(reader, count) } - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_xml_reader_attribute_name( - reader: CHandle, - index: c_int, - name: *mut c_char, - buf_size: c_int, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakcommon_xml_reader_attribute_name(reader, index, name, buf_size) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { - oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_name( - reader, index, name, buf_size, - ) - } - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_xml_reader_attribute_value( - reader: CHandle, - index: c_int, - value: *mut c_char, - buf_size: c_int, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakcommon_xml_reader_attribute_value(reader, index, value, buf_size) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { - oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_value( - reader, index, value, buf_size, - ) - } - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_xml_reader_has_error(reader: CHandle, has_error: *mut c_int) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakcommon_xml_reader_has_error(reader, has_error) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_has_error(reader, has_error) } - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_xml_writer_init() -> CHandle { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakcommon_xml_writer_init() - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_init() } - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_xml_writer_free(writer: *mut CHandle) { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakcommon_xml_writer_free(writer) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_free(writer) } - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_xml_writer_write_start_element(writer: CHandle, name: *const c_char) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakcommon_xml_writer_write_start_element(writer, name) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_start_element(writer, name) } - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_xml_writer_write_end_element(writer: CHandle) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakcommon_xml_writer_write_end_element(writer) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_end_element(writer) } - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_xml_writer_write_end_document(writer: CHandle) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakcommon_xml_writer_write_end_document(writer) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_end_document(writer) } - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_xml_writer_write_attribute( - writer: CHandle, - key: *const c_char, - value: *const c_char, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakcommon_xml_writer_write_attribute(writer, key, value) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { - oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_attribute(writer, key, value) - } - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_xml_writer_write_characters(writer: CHandle, text: *const c_char) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakcommon_xml_writer_write_characters(writer, text) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_characters(writer, text) } - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_xml_writer_write_text_element( - writer: CHandle, - name: *const c_char, - text: *const c_char, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakcommon_xml_writer_write_text_element(writer, name, text) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { - oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_text_element(writer, name, text) - } - } -} - -/// Direct call into the `oakcommon` crate (single-lib unification). -pub fn oakcommon_config_get_int(group: *const c_char, key: *const c_char, default: c_int) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakcommon_config_get_int(group, key, default) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oakcommon::ffi::config::oakcommon_config_get_int(group, key, default) } - } -} diff --git a/crates/oaktimeline/src/bridge/mod.rs b/crates/oaktimeline/src/bridge/mod.rs deleted file mode 100644 index 61f40e4dd..000000000 --- a/crates/oaktimeline/src/bridge/mod.rs +++ /dev/null @@ -1,34 +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 . - -//! C ABI imports from other oak modules. Signatures mirror the public -//! headers verbatim; they are resolved at link time against the oaknode, -//! oakundo and oakcommon DLLs. -//! -//! Timeline never reimplements C++ internals: every cross-module access -//! goes through these frozen C ABIs (see README.md "no replication"). - -pub mod common; -pub mod node; -pub mod undo; - -/// In-crate mock implementations of the bridge surface, compiled for tests -/// (`cfg(test)` or the `test-stubs` feature); the bridge fns route to them -/// via their `#[cfg(any(test, feature = "test-stubs"))]` variants. Plain -/// Rust (no `#[no_mangle]`), so they coexist with the real -/// oaknode/oakundo/oakcommon rlibs linked in the same test binary. -#[cfg(any(test, feature = "test-stubs"))] -pub mod teststubs; diff --git a/crates/oaktimeline/src/bridge/node.rs b/crates/oaktimeline/src/bridge/node.rs deleted file mode 100644 index 94682cdaa..000000000 --- a/crates/oaktimeline/src/bridge/node.rs +++ /dev/null @@ -1,856 +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 . - -// 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 . - -//! oaknode C ABI imports (`include/node/{block,node,sequence,track,project}.h`). -//! Timeline reads and mutates the block/track/sequence graph exclusively -//! through these frozen functions — it never reimplements node internals. -//! -//! All time quantities cross the C ABI as `int` numerator/denominator pairs -//! (the ABI is `int`; the crate's internal `Rational` is `i64`). Converting -//! between the two is the bridge's responsibility. - -use std::ffi::{c_char, c_int}; - -use crate::handle::CHandle; - -/// A timeline block (`OakNodeBlock`) — a clip or a gap. Opaque handle. -pub type OakNodeBlock = CHandle; -/// A track (`OakNodeTrack`). Opaque handle. -pub type OakNodeTrack = CHandle; -/// A sequence/timeline (`OakNodeSequence`). Opaque handle. -pub type OakNodeSequence = CHandle; -/// A generic node (`OakNodeNode`). Opaque handle. -pub type OakNodeNode = CHandle; -/// A project (`OakNodeProject`). Opaque handle. -pub type OakNodeProject = CHandle; -/// A track list (`OakNodeTrackList`). Opaque handle. -pub type OakNodeTrackList = CHandle; - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_clip_create() -> OakNodeBlock { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_block_clip_create() - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::block::oaknode_block_clip_create() } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_gap_create() -> OakNodeBlock { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_block_gap_create() - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::block::oaknode_block_gap_create() } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_as_node(block: OakNodeBlock) -> OakNodeNode { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_block_as_node(block) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::block::oaknode_block_as_node(block) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_get_in( - block: OakNodeBlock, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_block_get_in(block, numerator, denominator) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::block::oaknode_block_get_in(block, numerator, denominator) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_get_out( - block: OakNodeBlock, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_block_get_out(block, numerator, denominator) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::block::oaknode_block_get_out(block, numerator, denominator) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_get_length( - block: OakNodeBlock, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_block_get_length(block, numerator, denominator) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::block::oaknode_block_get_length(block, numerator, denominator) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_set_length_and_media_out( - block: OakNodeBlock, - numerator: c_int, - denominator: c_int, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_block_set_length_and_media_out(block, numerator, denominator) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { - oaknode::ffi::block::oaknode_block_set_length_and_media_out( - block, - numerator, - denominator, - ) - } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_set_length_and_media_in( - block: OakNodeBlock, - numerator: c_int, - denominator: c_int, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_block_set_length_and_media_in(block, numerator, denominator) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { - oaknode::ffi::block::oaknode_block_set_length_and_media_in( - block, - numerator, - denominator, - ) - } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_set_in(block: OakNodeBlock, numerator: c_int, denominator: c_int) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_block_set_in(block, numerator, denominator) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::block::oaknode_block_set_in(block, numerator, denominator) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_get_enabled(block: OakNodeBlock, enabled: *mut c_int) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_block_get_enabled(block, enabled) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::block::oaknode_block_get_enabled(block, enabled) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_set_enabled(block: OakNodeBlock, enabled: c_int) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_block_set_enabled(block, enabled) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::block::oaknode_block_set_enabled(block, enabled) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_get_previous(block: OakNodeBlock, out: *mut OakNodeBlock) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_block_get_previous(block, out) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::block::oaknode_block_get_previous(block, out) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_get_next(block: OakNodeBlock, out: *mut OakNodeBlock) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_block_get_next(block, out) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::block::oaknode_block_get_next(block, out) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_get_track(block: OakNodeBlock, out: *mut OakNodeTrack) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_block_get_track(block, out) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::block::oaknode_block_get_track(block, out) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_link(a: OakNodeBlock, b: OakNodeBlock) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_block_link(a, b) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::block::oaknode_block_link(a, b) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_unlink(a: OakNodeBlock, b: OakNodeBlock) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_block_unlink(a, b) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::block::oaknode_block_unlink(a, b) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_get_length( - track: OakNodeTrack, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_track_get_length(track, numerator, denominator) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::track::oaknode_track_get_length(track, numerator, denominator) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_get_sequence(track: OakNodeTrack, out: *mut OakNodeSequence) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_track_get_sequence(track, out) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::track::oaknode_track_get_sequence(track, out) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_prepend_block(track: OakNodeTrack, block: OakNodeBlock) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_track_prepend_block(track, block) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::track::oaknode_track_prepend_block(track, block) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_insert_block_after( - track: OakNodeTrack, - block: OakNodeBlock, - before: OakNodeBlock, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_track_insert_block_after(track, block, before) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::track::oaknode_track_insert_block_after(track, block, before) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_ripple_remove_block(track: OakNodeTrack, block: OakNodeBlock) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_track_ripple_remove_block(track, block) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::track::oaknode_track_ripple_remove_block(track, block) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_replace_block( - track: OakNodeTrack, - old_block: OakNodeBlock, - new_block: OakNodeBlock, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_track_replace_block(track, old_block, new_block) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::track::oaknode_track_replace_block(track, old_block, new_block) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_as_node(sequence: OakNodeSequence) -> OakNodeNode { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_sequence_as_node(sequence) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::sequence::oaknode_sequence_as_node(sequence) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_from_node(node: OakNodeNode) -> OakNodeSequence { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_sequence_from_node(node) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::sequence::oaknode_sequence_from_node(node) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_node_get_project(node: OakNodeNode, out: *mut OakNodeProject) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_node_get_project(node, out) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::node::oaknode_node_get_project(node, out) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_node_output_connection_count(node: OakNodeNode, out_count: *mut c_int) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_node_output_connection_count(node, out_count) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::node::oaknode_node_output_connection_count(node, out_count) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_node_get_markers(node: OakNodeNode, out: *mut crate::handle::CHandle) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_node_get_markers(node, out) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::node::oaknode_node_get_markers(node, out as *mut std::ffi::c_void) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_node_get_work_area(node: OakNodeNode, out: *mut crate::handle::CHandle) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_node_get_work_area(node, out) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { - oaknode::ffi::node::oaknode_node_get_work_area(node, out as *mut std::ffi::c_void) - } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_add_node(project: OakNodeProject, node: OakNodeNode) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_project_add_node(project, node) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::project::oaknode_project_add_node(project, node) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_project_remove_node(project: OakNodeProject, node: OakNodeNode) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_project_remove_node(project, node) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::project::oaknode_project_remove_node(project, node) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_command_create_remove_node(node: OakNodeNode) -> crate::handle::CHandle { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_command_create_remove_node(node) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::node::oaknode_command_create_remove_node(node) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_get_kind(block: OakNodeBlock, out_kind: *mut c_int) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_block_get_kind(block, out_kind) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::block::oaknode_block_get_kind(block, out_kind) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_from_node(node: OakNodeNode) -> OakNodeBlock { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_block_from_node(node) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::block::oaknode_block_from_node(node) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_block_are_linked(a: OakNodeBlock, b: OakNodeBlock, linked: *mut c_int) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_block_are_linked(a, b, linked) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::block::oaknode_block_are_linked(a, b, linked) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_clip_add_cache_passthrough_from(clip: OakNodeBlock, other: OakNodeBlock) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_clip_add_cache_passthrough_from(clip, other) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::block::oaknode_clip_add_cache_passthrough_from(clip, other) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_clip_get_media_in( - clip: OakNodeBlock, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_clip_get_media_in(clip, numerator, denominator) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::block::oaknode_clip_get_media_in(clip, numerator, denominator) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_clip_set_media_in( - clip: OakNodeBlock, - numerator: c_int, - denominator: c_int, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_clip_set_media_in(clip, numerator, denominator) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::block::oaknode_clip_set_media_in(clip, numerator, denominator) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_create(kind: c_int) -> OakNodeTrack { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_track_create(kind) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::track::oaknode_track_create(kind) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_get_locked(track: OakNodeTrack, locked: *mut c_int) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_track_get_locked(track, locked) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::track::oaknode_track_get_locked(track, locked) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_set_locked(track: OakNodeTrack, locked: c_int) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_track_set_locked(track, locked) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::track::oaknode_track_set_locked(track, locked) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_get_block_count(track: OakNodeTrack, count: *mut c_int) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_track_get_block_count(track, count) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::track::oaknode_track_get_block_count(track, count) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_get_block_at( - track: OakNodeTrack, - index: c_int, - out: *mut OakNodeBlock, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_track_get_block_at(track, index, out) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::track::oaknode_track_get_block_at(track, index, out) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_get_block_containing_time( - track: OakNodeTrack, - numerator: c_int, - denominator: c_int, - out: *mut OakNodeBlock, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_track_get_block_containing_time( - track, - numerator, - denominator, - out, - ) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { - oaknode::ffi::track::oaknode_track_get_block_containing_time( - track, - numerator, - denominator, - out, - ) - } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_get_nearest_block_before_or_at( - track: OakNodeTrack, - numerator: c_int, - denominator: c_int, - out: *mut OakNodeBlock, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_track_get_nearest_block_before_or_at( - track, - numerator, - denominator, - out, - ) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { - oaknode::ffi::track::oaknode_track_get_nearest_block_before_or_at( - track, - numerator, - denominator, - out, - ) - } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_track_get_nearest_block_after_or_at( - track: OakNodeTrack, - numerator: c_int, - denominator: c_int, - out: *mut OakNodeBlock, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_track_get_nearest_block_after_or_at( - track, - numerator, - denominator, - out, - ) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { - oaknode::ffi::track::oaknode_track_get_nearest_block_after_or_at( - track, - numerator, - denominator, - out, - ) - } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_tracklist_get_type(list: OakNodeTrackList, kind: *mut c_int) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_tracklist_get_type(list, kind) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::track::oaknode_tracklist_get_type(list, kind) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_tracklist_get_track_count(list: OakNodeTrackList, count: *mut c_int) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_tracklist_get_track_count(list, count) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::track::oaknode_tracklist_get_track_count(list, count) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_tracklist_get_track_at( - list: OakNodeTrackList, - index: c_int, - out: *mut OakNodeTrack, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_tracklist_get_track_at(list, index, out) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::track::oaknode_tracklist_get_track_at(list, index, out) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_tracklist_array_append(list: OakNodeTrackList) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_tracklist_array_append(list) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::track::oaknode_tracklist_array_append(list) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_tracklist_array_remove_last(list: OakNodeTrackList) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_tracklist_array_remove_last(list) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::track::oaknode_tracklist_array_remove_last(list) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_get_track_list( - sequence: OakNodeSequence, - kind: c_int, - out: *mut OakNodeTrackList, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_sequence_get_track_list(sequence, kind, out) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::sequence::oaknode_sequence_get_track_list(sequence, kind, out) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_get_all_track_count(sequence: OakNodeSequence, count: *mut c_int) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_sequence_get_all_track_count(sequence, count) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::sequence::oaknode_sequence_get_all_track_count(sequence, count) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_sequence_get_all_track_at( - sequence: OakNodeSequence, - index: c_int, - out: *mut OakNodeTrack, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_sequence_get_all_track_at(sequence, index, out) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::sequence::oaknode_sequence_get_all_track_at(sequence, index, out) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_node_connect( - output_node: OakNodeNode, - input_node: OakNodeNode, - input_id: *const c_char, -) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_node_connect(output_node, input_node, input_id) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::node::oaknode_node_connect(output_node, input_node, input_id) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_node_disconnect(input_node: OakNodeNode, input_id: *const c_char) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_node_disconnect(input_node, input_id) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::node::oaknode_node_disconnect(input_node, input_id) } - } -} - -/// Direct call into the `oaknode` crate (single-lib unification). -pub fn oaknode_node_copy_in_graph( - node: OakNodeNode, - out_command: *mut crate::handle::CHandle, -) -> OakNodeNode { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oaknode_node_copy_in_graph(node, out_command) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oaknode::ffi::node::oaknode_node_copy_in_graph(node, out_command) } - } -} diff --git a/crates/oaktimeline/src/bridge/teststubs.rs b/crates/oaktimeline/src/bridge/teststubs.rs deleted file mode 100644 index cf845061e..000000000 --- a/crates/oaktimeline/src/bridge/teststubs.rs +++ /dev/null @@ -1,1597 +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 . - -//! In-crate C ABI mocks, compiled only with `--features test-stubs`. -//! -//! In-crate mock implementations of the oaknode/oakundo/oakcommon bridge -//! surface, called directly by the bridge fns' `#[cfg(test)]` variants. -//! The mocks are plain Rust (no `#[no_mangle]`/`extern "C"`), so they -//! coexist with the real oaknode/oakundo/oakcommon rlibs linked in the -//! same test binary. Timeline's tests build fixtures with these mocks; the -//! production (non-test) bridge fns call the real crates. -//! -//! The mocks are intentionally simple and single-threaded. Handles box a -//! [`MockNode`] (for every node-graph kind), a [`MockMarkerList`], a -//! [`MockWorkArea`] or a [`MockUndoCommand`]. References between boxes use -//! raw pointers; the caller is assumed to hold a live handle for the -//! duration an object is referenced by the graph (matching how the real -//! crate's tests drive the facade). -//! -//! The mock node graph is a simplification of the C++ one: -//! - a track owns an ordered list of block pointers ([`MockNode::blocks`]); -//! - `in`/`out`/`length`/`media_in` are stored as `(i32, i32)` pairs; -//! - `block_set_length_and_media_*` keep the non-fixed edge fixed by -//! recomputing length from the fixed edge (see the doc on each function); -//! - `ripple_remove_block` / `replace_block` mutate the owning track's list -//! and clear/rewrite the block's `track` pointer. - -use std::ffi::{c_char, c_int, c_void}; - -use crate::handle::{get, get_mut, make_owned, CHandle, RefBox}; - -// --------------------------------------------------------------------------- -// Undo command mock -// --------------------------------------------------------------------------- - -/// The state boxed behind an undo-command handle created by the mock -/// `oakundo_command_init`. -pub struct MockUndoCommand { - /// The caller-supplied callback table. - pub vtable: super::undo::OakUndoCommandVtable, - /// The caller-supplied userdata (a `*mut` boxed timeline command). - pub userdata: *mut c_void, -} - -// The mock is single-threaded; the raw userdata pointer is fine to move -// across the crate's handle boundaries. -unsafe impl Send for MockUndoCommand {} - -/// `oakundo_command_init`: box the vtable + userdata and hand back an owning -/// handle. -pub fn oakundo_command_init( - vtable: *const super::undo::OakUndoCommandVtable, - userdata: *mut c_void, -) -> CHandle { - if vtable.is_null() { - return CHandle::null(); - } - // SAFETY: caller guarantees a valid vtable pointer for the duration of - // the call (mirrors the C ABI contract). We copy it out; it need not - // outlive the call. - let vt = unsafe { std::ptr::read(vtable) }; - make_owned(MockUndoCommand { - vtable: vt, - userdata, - }) -} - -/// `oakundo_command_init_multi`: an empty multi command (no-op here). -pub fn oakundo_command_init_multi() -> CHandle { - make_owned(MockUndoCommand { - vtable: super::undo::OakUndoCommandVtable { - redo: None, - undo: None, - free_fn: None, - }, - userdata: std::ptr::null_mut(), - }) -} - -/// `oakundo_command_multi_add_child`: no-op in the mock (returns 0). -pub fn oakundo_command_multi_add_child(_multi: CHandle, _child: CHandle) -> c_int { - 0 -} - -/// `oakundo_command_redo_now`: invoke the redo callback. -pub fn oakundo_command_redo_now(command: CHandle) -> c_int { - // SAFETY: the handle boxes a MockUndoCommand (created by init above). - if let Some(m) = unsafe { get_mut::(&command) } { - if let Some(f) = m.vtable.redo { - // SAFETY: the callback is the one the crate registered for this - // command's userdata. - unsafe { f(m.userdata) }; - } - } - 0 -} - -/// `oakundo_command_undo_now`: invoke the undo callback. -pub fn oakundo_command_undo_now(command: CHandle) -> c_int { - // SAFETY: the handle boxes a MockUndoCommand. - if let Some(m) = unsafe { get_mut::(&command) } { - if let Some(f) = m.vtable.undo { - // SAFETY: callback registered by the crate for this userdata. - unsafe { f(m.userdata) }; - } - } - 0 -} - -/// `oakundo_command_free`: run `free_fn`, drop the box and clear the handle. -/// NULL / empty-handle is a no-op. -pub fn oakundo_command_free(command: *mut CHandle) { - if command.is_null() { - return; - } - // SAFETY: caller passes a valid pointer. - let h = unsafe { &mut *command }; - if h.ctx.is_null() { - return; - } - // SAFETY: handle boxes a MockUndoCommand. - if let Some(m) = unsafe { get_mut::(h) } { - if let Some(f) = m.vtable.free_fn { - // SAFETY: callback registered by the crate for this userdata. - unsafe { f(m.userdata) }; - } - } - // Take ownership of the box (count is 1) and drop it, then clear. - // SAFETY: h.ctx is a RefBox with a single reference - // (the handle we are freeing). - unsafe { drop(Box::from_raw(h.ctx as *mut RefBox)) }; - h.ctx = std::ptr::null_mut(); - h.addref = None; - h.release = None; - h.abi_version = 0; -} - -/// `oakundo_stack_push`: record the push into the stack's mock (returns 0). -pub fn oakundo_stack_push(stack: CHandle, _command: CHandle, _text: *const c_char) -> c_int { - // SAFETY: the stack handle boxes a MockUndoStack. - if let Some(s) = unsafe { get_mut::(&stack) } { - s.pushes += 1; - } - 0 -} - -/// The boxed state of an undo stack handle used by `oakundo_stack_push`. -pub struct MockUndoStack { - /// Number of successful pushes. - pub pushes: i32, -} - -// --------------------------------------------------------------------------- -// oakcommon mocks (XML reader/writer + config) -// --------------------------------------------------------------------------- - -/// A single flat XML "start element" the reader yields. -pub struct MockXmlNode { - /// Element name. - pub name: String, - /// Inner text (`read_element_text`). - pub text: String, - /// Attribute name/value pairs. - pub attrs: Vec<(String, String)>, -} - -/// The boxed state of an `oakcommon_xml_reader_*` handle. -pub struct MockXmlReader { - /// Elements to iterate, in order. - pub nodes: Vec, - /// Cursor: index of the element the next `read_next_start_element` - /// yields; "current element" is `nodes[cur-1]`. - pub cur: usize, - /// Whether the reader is in an error state. - pub error: bool, -} - -impl MockXmlReader { - /// Build a reader over the given elements. - pub fn new(nodes: Vec) -> Self { - MockXmlReader { - nodes, - cur: 0, - error: false, - } - } -} - -/// Helper to build a reader boxed into a handle (used by tests). -pub fn xml_reader_handle(nodes: Vec) -> CHandle { - make_owned(MockXmlReader::new(nodes)) -} - -/// `oakcommon_xml_reader_init`: build an empty reader over a NUL-terminated -/// document. The mock only supports the `reader_handle` builder, so a -/// document string yields an empty reader. -pub fn oakcommon_xml_reader_init(_data: *const c_char) -> CHandle { - make_owned(MockXmlReader::new(Vec::new())) -} - -/// `oakcommon_xml_reader_free`: release and clear; NULL / empty no-op. -pub fn oakcommon_xml_reader_free(reader: *mut CHandle) { - free_box::(reader); -} - -/// `oakcommon_xml_reader_skip_current_element`: no-op (flat model). -pub fn oakcommon_xml_reader_skip_current_element(_reader: CHandle) -> c_int { - 0 -} - -/// `oakcommon_xml_reader_read_next_start_element`: advance and write the -/// element name. Returns 1 while elements remain, else 0. -pub fn oakcommon_xml_reader_read_next_start_element(reader: CHandle, found: *mut c_int) -> c_int { - // SAFETY: handle boxes a MockXmlReader. - let Some(r) = (unsafe { get_mut::(&reader) }) else { - return 0; - }; - if found.is_null() { - return 0; - } - if r.cur >= r.nodes.len() { - // End of stream: no element yielded (0), found stays 0. - return 0; - } - r.cur += 1; - unsafe { *found = 1 }; - 1 -} - -/// `oakcommon_xml_reader_name`: the current element's name. -pub fn oakcommon_xml_reader_name(reader: CHandle, name: *mut c_char, buf_size: c_int) -> c_int { - // SAFETY: handle boxes a MockXmlReader. - let Some(r) = (unsafe { get::(&reader) }) else { - return 0; - }; - let Some(n) = r.nodes.get(r.cur.wrapping_sub(1)) else { - return 0; - }; - write_cstr(name, buf_size, &n.name); - 1 -} - -/// `oakcommon_xml_reader_read_element_text`: the current element's text. -pub fn oakcommon_xml_reader_read_element_text( - reader: CHandle, - text: *mut c_char, - buf_size: c_int, -) -> c_int { - // SAFETY: handle boxes a MockXmlReader. - let Some(r) = (unsafe { get::(&reader) }) else { - return 0; - }; - let Some(n) = r.nodes.get(r.cur.wrapping_sub(1)) else { - return 0; - }; - write_cstr(text, buf_size, &n.text); - 1 -} - -/// `oakcommon_xml_reader_attribute_count`: attribute count on the current -/// element. -pub fn oakcommon_xml_reader_attribute_count(reader: CHandle, count: *mut c_int) -> c_int { - // SAFETY: caller passes a valid count pointer. - let c = match unsafe { get::(&reader) } { - Some(r) => r - .nodes - .get(r.cur.wrapping_sub(1)) - .map(|n| n.attrs.len() as c_int) - .unwrap_or(0), - None => 0, - }; - // SAFETY: caller guarantees `count` is a valid pointer. - unsafe { *count = c }; - 1 -} - -/// `oakcommon_xml_reader_attribute_name`: name of attribute `i`. -pub fn oakcommon_xml_reader_attribute_name( - reader: CHandle, - index: c_int, - name: *mut c_char, - buf_size: c_int, -) -> c_int { - // SAFETY: handle boxes a MockXmlReader. - let Some(r) = (unsafe { get::(&reader) }) else { - return 0; - }; - let Some(n) = r.nodes.get(r.cur.wrapping_sub(1)) else { - return 0; - }; - let Some((k, _)) = n.attrs.get(index as usize) else { - return 0; - }; - write_cstr(name, buf_size, k); - 1 -} - -/// `oakcommon_xml_reader_attribute_value`: value of attribute `i`. -pub fn oakcommon_xml_reader_attribute_value( - reader: CHandle, - index: c_int, - value: *mut c_char, - buf_size: c_int, -) -> c_int { - // SAFETY: handle boxes a MockXmlReader. - let Some(r) = (unsafe { get::(&reader) }) else { - return 0; - }; - let Some(n) = r.nodes.get(r.cur.wrapping_sub(1)) else { - return 0; - }; - let Some((_, v)) = n.attrs.get(index as usize) else { - return 0; - }; - write_cstr(value, buf_size, v); - 1 -} - -/// `oakcommon_xml_reader_has_error`: whether the reader errored. -pub fn oakcommon_xml_reader_has_error(reader: CHandle, has_error: *mut c_int) -> c_int { - let e = match unsafe { get::(&reader) } { - Some(r) => r.error as c_int, - None => 0, - }; - // SAFETY: caller guarantees `has_error` is a valid pointer. - unsafe { *has_error = e }; - 1 -} - -/// The boxed state of an `oakcommon_xml_writer_*` handle. -pub struct MockXmlWriter { - /// Accumulated output. - pub buf: String, -} - -/// `oakcommon_xml_writer_init`: a new writer. -pub fn oakcommon_xml_writer_init() -> CHandle { - make_owned(MockXmlWriter { buf: String::new() }) -} - -/// `oakcommon_xml_writer_free`: release and clear; NULL / empty no-op. -pub fn oakcommon_xml_writer_free(writer: *mut CHandle) { - free_box::(writer); -} - -/// `oakcommon_xml_writer_write_start_element`: append ``. -pub fn oakcommon_xml_writer_write_start_element(writer: CHandle, name: *const c_char) -> c_int { - // SAFETY: caller passes a NUL-terminated string. - let n = unsafe { cstr(name) }; - // SAFETY: handle boxes a MockXmlWriter. - if let Some(w) = unsafe { get_mut::(&writer) } { - w.buf.push_str(&format!("<{}>", n)); - } - 0 -} - -/// `oakcommon_xml_writer_write_end_element`: append ``. -pub fn oakcommon_xml_writer_write_end_element(writer: CHandle) -> c_int { - // SAFETY: handle boxes a MockXmlWriter. - if let Some(w) = unsafe { get_mut::(&writer) } { - w.buf.push_str(""); - } - 0 -} - -/// `oakcommon_xml_writer_write_end_document`: no-op. -pub fn oakcommon_xml_writer_write_end_document(_writer: CHandle) -> c_int { - 0 -} - -/// `oakcommon_xml_writer_write_attribute`: append ` key="value"`. -pub fn oakcommon_xml_writer_write_attribute( - writer: CHandle, - key: *const c_char, - value: *const c_char, -) -> c_int { - // SAFETY: caller passes NUL-terminated strings. - let (k, v) = unsafe { (cstr(key), cstr(value)) }; - // SAFETY: handle boxes a MockXmlWriter. - if let Some(w) = unsafe { get_mut::(&writer) } { - w.buf.push_str(&format!(" {}=\"{}\"", k, v)); - } - 0 -} - -/// `oakcommon_xml_writer_write_characters`: append raw text. -pub fn oakcommon_xml_writer_write_characters(writer: CHandle, text: *const c_char) -> c_int { - // SAFETY: caller passes a NUL-terminated string. - let t = unsafe { cstr(text) }; - // SAFETY: handle boxes a MockXmlWriter. - if let Some(w) = unsafe { get_mut::(&writer) } { - w.buf.push_str(&t); - } - 0 -} - -/// `oakcommon_xml_writer_write_text_element`: append `text`. -pub fn oakcommon_xml_writer_write_text_element( - writer: CHandle, - name: *const c_char, - text: *const c_char, -) -> c_int { - // SAFETY: caller passes NUL-terminated strings. - let (n, t) = unsafe { (cstr(name), cstr(text)) }; - // SAFETY: handle boxes a MockXmlWriter. - if let Some(w) = unsafe { get_mut::(&writer) } { - w.buf.push_str(&format!("<{}>{}", n, t, n)); - } - 0 -} - -/// `oakcommon_config_get_int`: return the supplied default (the mock keeps no -/// config store; the marker default colour path uses the default). -pub fn oakcommon_config_get_int( - _group: *const c_char, - _key: *const c_char, - default: c_int, -) -> c_int { - default -} - -// --------------------------------------------------------------------------- -// oaknode mocks -// --------------------------------------------------------------------------- - -/// Kind of a graph node in the mock. -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum MockKind { - /// A clip block. - Clip, - /// A gap block. - Gap, - /// A track. - Track, - /// A track list (owns a set of same-typed tracks). - TrackList, - /// A sequence/timeline. - Sequence, - /// A generic node. - Node, - /// A project. - Project, -} - -/// The state boxed behind every node-graph handle. -#[derive(Clone)] -pub struct MockNode { - /// What kind of graph node this is. - pub kind: MockKind, - /// Locked flag (tracks only). - pub locked: bool, - /// Track type (`OAKNODE_TRACK_TYPE_*`; tracks and track lists only). - pub track_type: i32, - /// In point as `(num, den)`. - pub in_: (i32, i32), - /// Out point as `(num, den)`. - pub out: (i32, i32), - /// Length as `(num, den)`. - pub length: (i32, i32), - /// Media-in as `(num, den)`. - pub media_in: (i32, i32), - /// Enabled flag. - pub enabled: bool, - /// Previous block on the track (raw pointer; null when none). - pub prev: *mut MockNode, - /// Next block on the track (raw pointer; null when none). - pub next: *mut MockNode, - /// Owning track (raw pointer; null when detached). - pub track: *mut MockNode, - /// Linked blocks (raw pointers). - pub links: Vec<*mut MockNode>, - /// Owning sequence (raw pointer; null when detached). - pub sequence: *mut MockNode, - /// Owning project (raw pointer; null when detached). - pub project: *mut MockNode, - /// Output connection count. - pub output_conns: i32, - /// Ordered owned blocks (tracks only). - pub blocks: Vec<*mut MockNode>, - /// Borrowed marker list handle (viewer nodes only). - pub markers: CHandle, - /// Borrowed work area handle (viewer nodes only). - pub work_area: CHandle, -} - -// The mock is single-threaded; raw pointer members are fine. -unsafe impl Send for MockNode {} - -impl Default for MockNode { - fn default() -> Self { - MockNode { - kind: MockKind::Node, - locked: false, - track_type: -1, // OAKNODE_TRACK_TYPE_NONE - in_: (0, 1), - out: (0, 1), - length: (0, 1), - media_in: (0, 1), - enabled: true, - prev: std::ptr::null_mut(), - next: std::ptr::null_mut(), - track: std::ptr::null_mut(), - links: Vec::new(), - sequence: std::ptr::null_mut(), - project: std::ptr::null_mut(), - output_conns: 0, - blocks: Vec::new(), - markers: CHandle::null(), - work_area: CHandle::null(), - } - } -} - -/// `oaknode_block_clip_create`: a new clip block. -pub fn oaknode_block_clip_create() -> CHandle { - make_owned(MockNode { - kind: MockKind::Clip, - ..Default::default() - }) -} - -/// `oaknode_block_gap_create`: a new gap block. -pub fn oaknode_block_gap_create() -> CHandle { - make_owned(MockNode { - kind: MockKind::Gap, - ..Default::default() - }) -} - -/// `oaknode_block_as_node`: a borrowed generic-node view of a block. -pub fn oaknode_block_as_node(block: CHandle) -> CHandle { - ref_clone(&block) -} - -/// `oaknode_block_get_in`: read the in point as an int pair. -pub fn oaknode_block_get_in( - block: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(b) = (unsafe { get::(&block) }) else { - return -1; - }; - write_pair(numerator, denominator, b.in_); - 0 -} - -/// `oaknode_block_get_out`: read the out point as an int pair. -pub fn oaknode_block_get_out( - block: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(b) = (unsafe { get::(&block) }) else { - return -1; - }; - write_pair(numerator, denominator, b.out); - 0 -} - -/// `oaknode_block_get_length`: read the length as an int pair. -pub fn oaknode_block_get_length( - block: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(b) = (unsafe { get::(&block) }) else { - return -1; - }; - write_pair(numerator, denominator, b.length); - 0 -} - -/// `oaknode_block_set_length_and_media_out`: set length, keeping the media-in -/// fixed (the out point follows). -pub fn oaknode_block_set_length_and_media_out( - block: CHandle, - numerator: c_int, - denominator: c_int, -) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(b) = (unsafe { get_mut::(&block) }) else { - return -1; - }; - b.length = (numerator, denominator); - b.out = add_pair(b.media_in, b.length); - 0 -} - -/// `oaknode_block_set_length_and_media_in`: set length, keeping the out point -/// fixed (the media-in follows). -pub fn oaknode_block_set_length_and_media_in( - block: CHandle, - numerator: c_int, - denominator: c_int, -) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(b) = (unsafe { get_mut::(&block) }) else { - return -1; - }; - b.length = (numerator, denominator); - b.media_in = sub_pair(b.out, b.length); - 0 -} - -/// `oaknode_block_set_in`: set the in point, keeping the length (the out -/// point follows; media-in unchanged). -pub fn oaknode_block_set_in(block: CHandle, numerator: c_int, denominator: c_int) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(b) = (unsafe { get_mut::(&block) }) else { - return -1; - }; - b.in_ = (numerator, denominator); - b.out = add_pair(b.in_, b.length); - 0 -} - -/// `oaknode_block_get_enabled`: read the enabled flag. -pub fn oaknode_block_get_enabled(block: CHandle, enabled: *mut c_int) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(b) = (unsafe { get::(&block) }) else { - return -1; - }; - // SAFETY: caller guarantees a valid pointer. - unsafe { *enabled = b.enabled as c_int }; - 0 -} - -/// `oaknode_block_set_enabled`: set the enabled flag. -pub fn oaknode_block_set_enabled(block: CHandle, enabled: c_int) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(b) = (unsafe { get_mut::(&block) }) else { - return -1; - }; - b.enabled = enabled != 0; - 0 -} - -/// `oaknode_block_get_previous`: write a borrowed handle to the previous block. -pub fn oaknode_block_get_previous(block: CHandle, out: *mut CHandle) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(b) = (unsafe { get::(&block) }) else { - return -1; - }; - write_ptr_handle(out, b.prev); - 0 -} - -/// `oaknode_block_get_next`: write a borrowed handle to the next block. -pub fn oaknode_block_get_next(block: CHandle, out: *mut CHandle) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(b) = (unsafe { get::(&block) }) else { - return -1; - }; - write_ptr_handle(out, b.next); - 0 -} - -/// `oaknode_block_get_track`: write a borrowed handle to the owning track. -pub fn oaknode_block_get_track(block: CHandle, out: *mut CHandle) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(b) = (unsafe { get::(&block) }) else { - return -1; - }; - write_ptr_handle(out, b.track); - 0 -} - -/// `oaknode_block_link`: link two blocks so they move together. -pub fn oaknode_block_link(a: CHandle, b: CHandle) -> c_int { - // SAFETY: both handles box MockNodes. - let (pa, pb) = unsafe { - match (get_mut::(&a), get_mut::(&b)) { - (Some(ma), Some(mb)) => (ma as *mut MockNode, mb as *mut MockNode), - _ => return -1, - } - }; - // SAFETY: pa/pb are the heap addresses of the two boxes. - let (pa, pb) = (pa, pb); - // SAFETY: boxes remain alive (handles held). - let (pa, pb) = unsafe { (pa, pb) }; - // SAFETY: both boxes remain alive (handles held). - unsafe { (*pa).links.push(pb) }; - // SAFETY: both boxes remain alive (handles held). - unsafe { (*pb).links.push(pa) }; - 0 -} - -/// `oaknode_block_unlink`: unlink two blocks. -pub fn oaknode_block_unlink(a: CHandle, b: CHandle) -> c_int { - // SAFETY: both handles box MockNodes. - let (pa, pb) = unsafe { - match (get_mut::(&a), get_mut::(&b)) { - (Some(ma), Some(mb)) => (ma as *mut MockNode, mb as *mut MockNode), - _ => return -1, - } - }; - // SAFETY: both boxes remain alive (handles held). - let (pa, pb) = unsafe { (pa, pb) }; - // SAFETY: both boxes remain alive (handles held). - unsafe { (*pa).links.retain(|&p| p != pb) }; - // SAFETY: both boxes remain alive (handles held). - unsafe { (*pb).links.retain(|&p| p != pa) }; - 0 -} - -/// `oaknode_track_get_length`: read the track length as an int pair. -pub fn oaknode_track_get_length( - track: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(t) = (unsafe { get::(&track) }) else { - return -1; - }; - let mut acc = (0, 1); - for &p in &t.blocks { - // SAFETY: block pointers reference alive boxes. - let b = unsafe { &*p }; - acc = add_pair(acc, b.length); - } - write_pair(numerator, denominator, acc); - 0 -} - -/// `oaknode_track_get_sequence`: write a borrowed handle to the owning sequence. -pub fn oaknode_track_get_sequence(track: CHandle, out: *mut CHandle) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(t) = (unsafe { get::(&track) }) else { - return -1; - }; - write_ptr_handle(out, t.sequence); - 0 -} - -/// `oaknode_track_prepend_block`: insert a block at the front of the track. -pub fn oaknode_track_prepend_block(track: CHandle, block: CHandle) -> c_int { - // SAFETY: both handles box MockNodes. - let (pt, pb) = unsafe { - match (get_mut::(&track), get_mut::(&block)) { - (Some(mt), Some(mb)) => (mt as *mut MockNode, mb as *mut MockNode), - _ => return -1, - } - }; - // SAFETY: boxes remain alive (handles held). - let (pt, pb) = unsafe { (pt, pb) }; - // SAFETY: both boxes remain alive (handles held). - let t = unsafe { &mut *pt }; - let b = unsafe { &mut *pb }; - t.blocks.insert(0, pb); - b.track = pt; - let next = t.blocks.get(1).copied().unwrap_or(std::ptr::null_mut()); - b.next = next; - if !next.is_null() { - // SAFETY: `next` is a block box that remains alive (in the track list). - unsafe { (*next).prev = pb }; - } - b.prev = std::ptr::null_mut(); - 0 -} - -/// `oaknode_track_insert_block_after`: insert `block` after `before` on `track`. -pub fn oaknode_track_insert_block_after(track: CHandle, block: CHandle, before: CHandle) -> c_int { - // SAFETY: handles box MockNodes. - let (pt, pb, pbefore) = unsafe { - match (get_mut::(&track), get_mut::(&block)) { - (Some(mt), Some(mb)) => ( - mt as *mut MockNode, - mb as *mut MockNode, - if before.is_null() { - std::ptr::null_mut() - } else { - match get::(&before) { - Some(mbf) => mbf as *const MockNode as *mut MockNode, - None => return -1, - } - }, - ), - _ => return -1, - } - }; - // SAFETY: boxes remain alive (handles held). - let (pt, pb, pbefore) = unsafe { (pt, pb, pbefore) }; - let t = unsafe { &mut *pt }; - let b = unsafe { &mut *pb }; - // A null predecessor inserts at the front (C++ prepend semantics); a - // non-null predecessor inserts immediately after it, or at the end when - // it is not on this track. - let pos = if pbefore.is_null() { - 0 - } else { - t.blocks - .iter() - .position(|&p| p == pbefore) - .map(|i| i + 1) - .unwrap_or(t.blocks.len()) - }; - t.blocks.insert(pos, pb); - b.track = pt; - // Relink neighbours around the inserted block. - if pos > 0 { - b.prev = t.blocks[pos - 1]; - } else { - b.prev = std::ptr::null_mut(); - } - b.next = t - .blocks - .get(pos + 1) - .copied() - .unwrap_or(std::ptr::null_mut()); - if !b.prev.is_null() { - // SAFETY: `b.prev` is a block box that remains alive. - unsafe { (*(b.prev)).next = pb }; - } - if !b.next.is_null() { - // SAFETY: `b.next` is a block box that remains alive. - unsafe { (*(b.next)).prev = pb }; - } - 0 -} - -/// `oaknode_track_ripple_remove_block`: remove a block, shifting later ones -/// earlier; ownership returns to the caller (the caller keeps its handle). -pub fn oaknode_track_ripple_remove_block(track: CHandle, block: CHandle) -> c_int { - // SAFETY: handles box MockNodes. - let (pt, pb) = unsafe { - match (get_mut::(&track), get_mut::(&block)) { - (Some(mt), Some(mb)) => (mt as *mut MockNode, mb as *mut MockNode), - _ => return -1, - } - }; - // SAFETY: boxes remain alive. - let (pt, pb) = unsafe { (pt, pb) }; - let t = unsafe { &mut *pt }; - let b = unsafe { &mut *pb }; - let Some(pos) = t.blocks.iter().position(|&p| p == pb) else { - return -1; - }; - let prev = b.prev; - let next = b.next; - t.blocks.remove(pos); - b.track = std::ptr::null_mut(); - b.prev = std::ptr::null_mut(); - b.next = std::ptr::null_mut(); - if !prev.is_null() { - // SAFETY: `prev` is a block box that remains alive. - unsafe { (*prev).next = next }; - } - if !next.is_null() { - // SAFETY: `next` is a block box that remains alive. - unsafe { (*next).prev = prev }; - } - 0 -} - -/// `oaknode_track_replace_block`: replace `old_block` with `new_block`. -pub fn oaknode_track_replace_block( - track: CHandle, - old_block: CHandle, - new_block: CHandle, -) -> c_int { - // SAFETY: handles box MockNodes. - let (pt, po, pn) = unsafe { - match ( - get_mut::(&track), - get_mut::(&old_block), - get_mut::(&new_block), - ) { - (Some(mt), Some(mo), Some(mn)) => ( - mt as *mut MockNode, - mo as *mut MockNode, - mn as *mut MockNode, - ), - _ => return -1, - } - }; - // SAFETY: boxes remain alive (handles held). - let (pt, po, pn) = unsafe { (pt, po, pn) }; - let t = unsafe { &mut *pt }; - let o = unsafe { &mut *po }; - let n = unsafe { &mut *pn }; - let Some(pos) = t.blocks.iter().position(|&p| p == po) else { - return -1; - }; - t.blocks[pos] = pn; - o.track = std::ptr::null_mut(); - o.prev = std::ptr::null_mut(); - o.next = std::ptr::null_mut(); - n.track = pt; - if pos > 0 { - n.prev = t.blocks[pos - 1]; - } else { - n.prev = std::ptr::null_mut(); - } - n.next = t - .blocks - .get(pos + 1) - .copied() - .unwrap_or(std::ptr::null_mut()); - if !n.prev.is_null() { - // SAFETY: `n.prev` is a block box that remains alive. - unsafe { (*(n.prev)).next = pn }; - } - if !n.next.is_null() { - // SAFETY: `n.next` is a block box that remains alive. - unsafe { (*(n.next)).prev = pn }; - } - 0 -} - -/// `oaknode_sequence_as_node`: a borrowed generic-node view of a sequence. -pub fn oaknode_sequence_as_node(sequence: CHandle) -> CHandle { - ref_clone(&sequence) -} - -/// `oaknode_sequence_from_node`: a borrowed sequence view of a generic node. -pub fn oaknode_sequence_from_node(node: CHandle) -> CHandle { - ref_clone(&node) -} - -/// `oaknode_node_get_project`: write a borrowed handle to the owning project. -pub fn oaknode_node_get_project(node: CHandle, out: *mut CHandle) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(n) = (unsafe { get::(&node) }) else { - return -1; - }; - write_ptr_handle(out, n.project); - 0 -} - -/// `oaknode_node_output_connection_count`: read the output connection count. -pub fn oaknode_node_output_connection_count(node: CHandle, out_count: *mut c_int) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(n) = (unsafe { get::(&node) }) else { - return -1; - }; - // SAFETY: caller guarantees a valid pointer. - unsafe { *out_count = n.output_conns }; - 0 -} - -/// `oaknode_node_get_markers`: write a borrowed handle to the node's markers. -pub fn oaknode_node_get_markers(node: CHandle, out: *mut CHandle) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(n) = (unsafe { get::(&node) }) else { - return -1; - }; - // SAFETY: caller guarantees a valid pointer. - unsafe { *out = ref_clone(&n.markers) }; - 0 -} - -/// `oaknode_node_get_work_area`: write a borrowed handle to the node's work area. -pub fn oaknode_node_get_work_area(node: CHandle, out: *mut CHandle) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(n) = (unsafe { get::(&node) }) else { - return -1; - }; - // SAFETY: caller guarantees a valid pointer. - unsafe { *out = ref_clone(&n.work_area) }; - 0 -} - -/// `oaknode_project_add_node`: adopt a node into a project. -pub fn oaknode_project_add_node(project: CHandle, node: CHandle) -> c_int { - // SAFETY: handles box MockNodes. - let (pp, pn) = unsafe { - match (get_mut::(&project), get_mut::(&node)) { - (Some(mp), Some(mn)) => (mp as *mut MockNode, mn as *mut MockNode), - _ => return -1, - } - }; - // SAFETY: boxes remain alive. - unsafe { (*pn).project = pp }; - 0 -} - -/// `oaknode_project_remove_node`: remove a node from a project. -pub fn oaknode_project_remove_node(project: CHandle, node: CHandle) -> c_int { - // SAFETY: handles box MockNodes. - let (pp, pn) = unsafe { - match (get_mut::(&project), get_mut::(&node)) { - (Some(mp), Some(mn)) => (mp as *mut MockNode, mn as *mut MockNode), - _ => return -1, - } - }; - // SAFETY: boxes remain alive. - unsafe { - if (*pn).project == pp { - (*pn).project = std::ptr::null_mut(); - } - } - 0 -} - -/// `oaknode_command_create_remove_node`: a command that removes a node. The -/// mock returns a command handle whose redo/undo no-op, but whose free drops -/// the wrapped node handle it borrows. -pub fn oaknode_command_create_remove_node(node: CHandle) -> CHandle { - let node_owned = ref_clone(&node); - make_owned(MockUndoCommand { - vtable: super::undo::OakUndoCommandVtable { - redo: None, - undo: None, - free_fn: Some(free_owned_handle), - }, - userdata: Box::into_raw(Box::new(node_owned)) as *mut c_void, - }) -} - -/// Free callback used by `oaknode_command_create_remove_node`: drop the -/// borrowed node handle. -extern "C" fn free_owned_handle(userdata: *mut c_void) { - // SAFETY: userdata is the boxed CHandle we created. - unsafe { drop(Box::from_raw(userdata as *mut CHandle)) }; -} - -/// `oaknode_block_get_kind`: map a block's kind to an `OAKNODE_BLOCK_*` int. -pub fn oaknode_block_get_kind(block: CHandle, out_kind: *mut c_int) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(b) = (unsafe { get::(&block) }) else { - return -1; - }; - let k = match b.kind { - MockKind::Clip => 1, // OAKNODE_BLOCK_CLIP - MockKind::Gap => 2, // OAKNODE_BLOCK_GAP - _ => 0, // OAKNODE_BLOCK_OTHER - }; - // SAFETY: caller guarantees a valid pointer. - unsafe { *out_kind = k }; - 0 -} - -/// `oaknode_block_from_node`: a borrowed block view of a generic node (empty -/// when the node is not a block). -pub fn oaknode_block_from_node(node: CHandle) -> CHandle { - // SAFETY: handle boxes a MockNode. - let Some(n) = (unsafe { get::(&node) }) else { - return CHandle::null(); - }; - match n.kind { - MockKind::Clip | MockKind::Gap => ref_clone(&node), - _ => CHandle::null(), - } -} - -/// `oaknode_block_are_linked`: whether `a` and `b` are linked (`linked` gets -/// 1/0). -pub fn oaknode_block_are_linked(a: CHandle, b: CHandle, linked: *mut c_int) -> c_int { - // SAFETY: both handles box MockNodes. - let (pa, pb) = unsafe { - match (get::(&a), get::(&b)) { - (Some(ma), Some(mb)) => ( - ma as *const MockNode as *mut MockNode, - mb as *const MockNode as *mut MockNode, - ), - _ => return -1, - } - }; - // SAFETY: boxes remain alive (handles held). - let (pa, pb) = unsafe { (pa, pb) }; - // SAFETY: `a` is alive (handle held). - let linked_flag = unsafe { (*pa).links.contains(&pb) } as c_int; - // SAFETY: caller guarantees a valid pointer. - unsafe { *linked = linked_flag }; - 0 -} - -/// `oaknode_clip_add_cache_passthrough_from`: copy render-cache passthroughs -/// from `other`. The mock has no cache model, so this is a no-op. -pub fn oaknode_clip_add_cache_passthrough_from(_clip: CHandle, _other: CHandle) -> c_int { - 0 -} - -/// `oaknode_clip_get_media_in`: read a clip's media-in as an int pair. -pub fn oaknode_clip_get_media_in( - clip: CHandle, - numerator: *mut c_int, - denominator: *mut c_int, -) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(c) = (unsafe { get::(&clip) }) else { - return -1; - }; - if c.kind != MockKind::Clip { - return -1; - } - write_pair(numerator, denominator, c.media_in); - 0 -} - -/// `oaknode_clip_set_media_in`: set a clip's media-in as an int pair. -pub fn oaknode_clip_set_media_in(clip: CHandle, numerator: c_int, denominator: c_int) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(c) = (unsafe { get_mut::(&clip) }) else { - return -1; - }; - if c.kind != MockKind::Clip { - return -1; - } - c.media_in = (numerator, denominator); - 0 -} - -/// `oaknode_track_create`: a new detached track of the given type. -pub fn oaknode_track_create(kind: c_int) -> CHandle { - make_owned(MockNode { - kind: MockKind::Track, - track_type: kind, - ..Default::default() - }) -} - -/// `oaknode_track_get_locked`: read a track's locked flag (`locked` gets 1/0). -pub fn oaknode_track_get_locked(track: CHandle, locked: *mut c_int) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(t) = (unsafe { get::(&track) }) else { - return -1; - }; - if t.kind != MockKind::Track { - return -1; - } - // SAFETY: caller guarantees a valid pointer. - unsafe { *locked = t.locked as c_int }; - 0 -} - -/// `oaknode_track_set_locked`: set a track's locked flag. -pub fn oaknode_track_set_locked(track: CHandle, locked: c_int) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(t) = (unsafe { get_mut::(&track) }) else { - return -1; - }; - if t.kind != MockKind::Track { - return -1; - } - t.locked = locked != 0; - 0 -} - -/// `oaknode_track_get_block_count`: number of blocks on a track. -pub fn oaknode_track_get_block_count(track: CHandle, count: *mut c_int) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(t) = (unsafe { get::(&track) }) else { - return -1; - }; - if t.kind != MockKind::Track { - return -1; - } - // SAFETY: caller guarantees a valid pointer. - unsafe { *count = t.blocks.len() as c_int }; - 0 -} - -/// `oaknode_track_get_block_at`: borrowed block at `index` on a track. -pub fn oaknode_track_get_block_at(track: CHandle, index: c_int, out: *mut CHandle) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(t) = (unsafe { get::(&track) }) else { - return -1; - }; - if t.kind != MockKind::Track { - return -1; - } - let Some(&p) = t.blocks.get(index as usize) else { - return -1; - }; - write_ptr_handle(out, p); - 0 -} - -/// `oaknode_track_get_block_containing_time`: the block strictly containing -/// `time` (in < t < out); writes null and returns -1 when none. -pub fn oaknode_track_get_block_containing_time( - track: CHandle, - numerator: c_int, - denominator: c_int, - out: *mut CHandle, -) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(t) = (unsafe { get::(&track) }) else { - return -1; - }; - if t.kind != MockKind::Track { - return -1; - } - let time = (numerator, denominator); - for &p in &t.blocks { - // SAFETY: block pointers reference alive boxes. - let b = unsafe { &*p }; - if pair_cmp(b.in_, time) == std::cmp::Ordering::Less - && pair_cmp(time, b.out) == std::cmp::Ordering::Less - { - write_ptr_handle(out, p); - return 0; - } - } - write_ptr_handle(out, std::ptr::null_mut()); - -1 -} - -/// `oaknode_track_get_nearest_block_before_or_at`: the block whose in point is -/// at or before `time` (the latest such); null when none. -pub fn oaknode_track_get_nearest_block_before_or_at( - track: CHandle, - numerator: c_int, - denominator: c_int, - out: *mut CHandle, -) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(t) = (unsafe { get::(&track) }) else { - return -1; - }; - if t.kind != MockKind::Track { - return -1; - } - let time = (numerator, denominator); - let mut best: *mut MockNode = std::ptr::null_mut(); - for &p in &t.blocks { - // SAFETY: block pointers reference alive boxes. - let b = unsafe { &*p }; - if pair_cmp(b.in_, time) != std::cmp::Ordering::Greater { - if best.is_null() - || pair_cmp(b.in_, unsafe { &*best }.in_) == std::cmp::Ordering::Greater - { - best = p; - } - } - } - write_ptr_handle(out, best); - 0 -} - -/// `oaknode_track_get_nearest_block_after_or_at`: the block whose in point is -/// at or after `time` (the earliest such); null when none. -pub fn oaknode_track_get_nearest_block_after_or_at( - track: CHandle, - numerator: c_int, - denominator: c_int, - out: *mut CHandle, -) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(t) = (unsafe { get::(&track) }) else { - return -1; - }; - if t.kind != MockKind::Track { - return -1; - } - let time = (numerator, denominator); - let mut best: *mut MockNode = std::ptr::null_mut(); - for &p in &t.blocks { - // SAFETY: block pointers reference alive boxes. - let b = unsafe { &*p }; - if pair_cmp(b.in_, time) != std::cmp::Ordering::Less { - if best.is_null() || pair_cmp(b.in_, unsafe { &*best }.in_) == std::cmp::Ordering::Less - { - best = p; - } - } - } - write_ptr_handle(out, best); - 0 -} - -/// `oaknode_tracklist_get_type`: the track list's track type. -pub fn oaknode_tracklist_get_type(list: CHandle, kind: *mut c_int) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(l) = (unsafe { get::(&list) }) else { - return -1; - }; - if l.kind != MockKind::TrackList { - return -1; - } - // SAFETY: caller guarantees a valid pointer. - unsafe { *kind = l.track_type }; - 0 -} - -/// `oaknode_tracklist_get_track_count`: number of tracks in a track list. -pub fn oaknode_tracklist_get_track_count(list: CHandle, count: *mut c_int) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(l) = (unsafe { get::(&list) }) else { - return -1; - }; - if l.kind != MockKind::TrackList { - return -1; - } - // SAFETY: caller guarantees a valid pointer. - unsafe { *count = l.blocks.len() as c_int }; - 0 -} - -/// `oaknode_tracklist_get_track_at`: borrowed track at `index` in a track list. -pub fn oaknode_tracklist_get_track_at(list: CHandle, index: c_int, out: *mut CHandle) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(l) = (unsafe { get::(&list) }) else { - return -1; - }; - if l.kind != MockKind::TrackList { - return -1; - } - let Some(&p) = l.blocks.get(index as usize) else { - return -1; - }; - write_ptr_handle(out, p); - 0 -} - -/// `oaknode_tracklist_array_append`: append a track-array element on the -/// parent sequence. The mock keeps track lists as plain vectors, so this is a -/// no-op. -pub fn oaknode_tracklist_array_append(_list: CHandle) -> c_int { - 0 -} - -/// `oaknode_tracklist_array_remove_last`: remove the last track-array element. -/// No-op in the mock (see `oaknode_tracklist_array_append`). -pub fn oaknode_tracklist_array_remove_last(_list: CHandle) -> c_int { - 0 -} - -/// `oaknode_sequence_get_track_list`: the borrowed per-type track list, or -/// null when the sequence has no list of that type. -pub fn oaknode_sequence_get_track_list(sequence: CHandle, kind: c_int, out: *mut CHandle) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(s) = (unsafe { get::(&sequence) }) else { - return -1; - }; - if s.kind != MockKind::Sequence { - return -1; - } - let mut found: *mut MockNode = std::ptr::null_mut(); - for &p in &s.blocks { - // SAFETY: track-list pointers reference alive boxes. - let l = unsafe { &*p }; - if l.kind == MockKind::TrackList && l.track_type == kind { - found = p; - break; - } - } - write_ptr_handle(out, found); - 0 -} - -/// `oaknode_sequence_get_all_track_count`: total connected tracks across all -/// types. -pub fn oaknode_sequence_get_all_track_count(sequence: CHandle, count: *mut c_int) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(s) = (unsafe { get::(&sequence) }) else { - return -1; - }; - if s.kind != MockKind::Sequence { - return -1; - } - let mut total = 0; - for &p in &s.blocks { - // SAFETY: track-list pointers reference alive boxes. - let l = unsafe { &*p }; - if l.kind == MockKind::TrackList { - total += l.blocks.len(); - } - } - // SAFETY: caller guarantees a valid pointer. - unsafe { *count = total as c_int }; - 0 -} - -/// `oaknode_sequence_get_all_track_at`: borrowed track at `index` across the -/// flat, all-types track list. -pub fn oaknode_sequence_get_all_track_at( - sequence: CHandle, - index: c_int, - out: *mut CHandle, -) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(s) = (unsafe { get::(&sequence) }) else { - return -1; - }; - if s.kind != MockKind::Sequence { - return -1; - } - let mut flat: Vec<*mut MockNode> = Vec::new(); - for &p in &s.blocks { - // SAFETY: track-list pointers reference alive boxes. - let l = unsafe { &*p }; - if l.kind == MockKind::TrackList { - flat.extend_from_slice(&l.blocks); - } - } - let Some(&p) = flat.get(index as usize) else { - return -1; - }; - write_ptr_handle(out, p); - 0 -} - -/// `oaknode_node_connect`: connect `output_node` to `input_node`'s `input_id`. -/// The mock tracks only the output connection count (input edge table is not -/// modelled), so this bumps `output_conns`. -pub fn oaknode_node_connect( - output_node: CHandle, - _input_node: CHandle, - _input_id: *const c_char, -) -> c_int { - // SAFETY: handle boxes a MockNode. - let Some(o) = (unsafe { get_mut::(&output_node) }) else { - return -1; - }; - o.output_conns += 1; - 0 -} - -/// `oaknode_node_disconnect`: remove the edge feeding `input_node`'s -/// `input_id`. The mock has no input-edge table, so this is a no-op. -pub fn oaknode_node_disconnect(_input_node: CHandle, _input_id: *const c_char) -> c_int { - 0 -} - -/// `oaknode_node_copy_in_graph`: clone `node` in its graph; `*out_command` -/// receives an owned undo handle. The mock clones the node box and hands back -/// a no-op command. -pub fn oaknode_node_copy_in_graph(node: CHandle, out_command: *mut CHandle) -> CHandle { - // SAFETY: handle boxes a MockNode. - let Some(n) = (unsafe { get::(&node) }) else { - return CHandle::null(); - }; - let copy = n.clone(); - let copy_handle = make_owned(copy); - if !out_command.is_null() { - // SAFETY: caller guarantees a valid out pointer. - unsafe { - *out_command = make_owned(MockUndoCommand { - vtable: super::undo::OakUndoCommandVtable { - redo: None, - undo: None, - free_fn: None, - }, - userdata: std::ptr::null_mut(), - }); - }; - } - copy_handle -} - -// --------------------------------------------------------------------------- -// shared helpers -// --------------------------------------------------------------------------- - -/// Compare two `(num, den)` pairs as fractions (denominators assumed -/// positive). -fn pair_cmp(a: (i32, i32), b: (i32, i32)) -> std::cmp::Ordering { - let lhs = a.0 as i64 * b.1 as i64; - let rhs = b.0 as i64 * a.1 as i64; - lhs.cmp(&rhs) -} - -/// Write a `(num, den)` pair into the out params (NULL-safe). -fn write_pair(num: *mut c_int, den: *mut c_int, pair: (i32, i32)) { - // SAFETY: caller passes optional valid pointers. - unsafe { - if !num.is_null() { - *num = pair.0; - } - if !den.is_null() { - *den = pair.1; - } - } -} - -/// Write a NUL-terminated copy of `s` into `buf` (two-stage contract: caller -/// calls once with a too-small buffer to get the size, then with enough). -fn write_cstr(buf: *mut c_char, buf_size: c_int, s: &str) { - if buf.is_null() { - return; - } - let bytes = s.as_bytes(); - let cap = buf_size as usize; - let copy_len = bytes.len().min(cap.max(1) - 1); - // SAFETY: caller guarantees `buf` points to at least `buf_size` bytes. - unsafe { - std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, copy_len); - *buf.add(copy_len) = 0; - } -} - -/// Read a C string into a `String` (empty on null). -/// -/// # Safety -/// `p` must be a valid NUL-terminated string or null. -unsafe fn cstr(p: *const c_char) -> String { - if p.is_null() { - return String::new(); - } - // SAFETY: caller guarantees a NUL-terminated string. - unsafe { std::ffi::CStr::from_ptr(p) } - .to_string_lossy() - .into_owned() -} - -/// Add two `(num, den)` pairs as fractions (crude but sufficient for the -/// mock's small integer test values). -fn add_pair(a: (i32, i32), b: (i32, i32)) -> (i32, i32) { - if a.1 == b.1 { - (a.0 + b.0, a.1) - } else { - let num = a.0 as i64 * b.1 as i64 + b.0 as i64 * a.1 as i64; - let den = a.1 as i64 * b.1 as i64; - (num as i32, den as i32) - } -} - -/// Subtract `b` from `a` as fractions. -fn sub_pair(a: (i32, i32), b: (i32, i32)) -> (i32, i32) { - if a.1 == b.1 { - (a.0 - b.0, a.1) - } else { - let num = a.0 as i64 * b.1 as i64 - b.0 as i64 * a.1 as i64; - let den = a.1 as i64 * b.1 as i64; - (num as i32, den as i32) - } -} - -/// Return a reference-counted copy of a handle (increments the box count and -/// returns the same `ctx`), modelling a borrowed handle. -fn ref_clone(h: &CHandle) -> CHandle { - if h.ctx.is_null() { - return CHandle::null(); - } - // SAFETY: h.addref is the addref callback for the boxed type. - if let Some(a) = h.addref { - unsafe { a(h.ctx) }; - } - CHandle { - ctx: h.ctx, - addref: h.addref, - release: h.release, - abi_version: h.abi_version, - } -} - -/// Write a borrowed handle for a raw box pointer into `out`, or a null handle -/// when the pointer is null. -fn write_ptr_handle(out: *mut CHandle, p: *mut MockNode) { - if out.is_null() { - return; - } - // SAFETY: caller guarantees a valid out pointer. - unsafe { - if p.is_null() { - *out = CHandle::null(); - } else { - // The raw pointer is the heap address of the box; build a - // borrowed handle around it. - *out = CHandle { - ctx: p as *mut c_void, - addref: None, - release: None, - abi_version: crate::handle::OAKTIMELINE_ABI_VERSION, - }; - } - } -} - -/// Release a handle's box and clear it (used by the `*_free` mocks); NULL / -/// empty no-op. -fn free_box(h: *mut CHandle) { - if h.is_null() { - return; - } - // SAFETY: caller passes a valid pointer. - let handle = unsafe { &mut *h }; - if handle.ctx.is_null() { - return; - } - // SAFETY: handle.ctx is a RefBox with a single reference. - unsafe { drop(Box::from_raw(handle.ctx as *mut RefBox)) }; - handle.ctx = std::ptr::null_mut(); - handle.addref = None; - handle.release = None; - handle.abi_version = 0; -} diff --git a/crates/oaktimeline/src/bridge/undo.rs b/crates/oaktimeline/src/bridge/undo.rs deleted file mode 100644 index b77d72e48..000000000 --- a/crates/oaktimeline/src/bridge/undo.rs +++ /dev/null @@ -1,133 +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 . - -// 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 . - -//! oakundo C ABI imports (`include/undo/undocommand.h`). Undo commands are -//! created through the C ABI vtable (`oakundo_command_init` with Rust -//! closures as `userdata`) — timeline does not subclass the C++ -//! `UndoCommand`; every command struct in this crate exposes `to_command()` -//! that wraps it this way. - -use std::ffi::{c_char, c_int, c_void}; - -use crate::handle::CHandle; - -/// `OakUndoCommandVtable` — callback table backing a caller-defined undo -/// command (undocommand.h). Any callback may be NULL (a NULL redo/undo makes -/// that direction a no-op); `free_fn` runs when the command is destroyed. -/// Single-lib unification: aliases the oakundo crate's vtable POD. -pub type OakUndoCommandVtable = oakundo::undocommand::OakUndoCommandVtable; - -/// Direct call into the `oakundo` crate (single-lib unification). -pub fn oakundo_command_init(vtable: *const OakUndoCommandVtable, userdata: *mut c_void) -> CHandle { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakundo_command_init(vtable, userdata) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oakundo::ffi::command::oakundo_command_init(vtable, userdata) } - } -} - -/// Direct call into the `oakundo` crate (single-lib unification). -pub fn oakundo_command_init_multi() -> CHandle { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakundo_command_init_multi() - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oakundo::ffi::command::oakundo_command_init_multi() } - } -} - -/// Direct call into the `oakundo` crate (single-lib unification). -pub fn oakundo_command_multi_add_child(multi: CHandle, child: CHandle) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakundo_command_multi_add_child(multi, child) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oakundo::ffi::command::oakundo_command_multi_add_child(multi, child) } - } -} - -/// Direct call into the `oakundo` crate (single-lib unification). -pub fn oakundo_command_redo_now(command: CHandle) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakundo_command_redo_now(command) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oakundo::ffi::command::oakundo_command_redo_now(command) } - } -} - -/// Direct call into the `oakundo` crate (single-lib unification). -pub fn oakundo_command_undo_now(command: CHandle) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakundo_command_undo_now(command) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oakundo::ffi::command::oakundo_command_undo_now(command) } - } -} - -/// Direct call into the `oakundo` crate (single-lib unification). -pub fn oakundo_command_free(command: *mut CHandle) { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakundo_command_free(command) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oakundo::ffi::command::oakundo_command_free(command) } - } -} - -/// Direct call into the `oakundo` crate (single-lib unification; the C ABI -/// name is `oakundo_undostack_push` — kept as `oakundo_stack_push` here for -/// source compatibility). -pub fn oakundo_stack_push(stack: CHandle, command: CHandle, text: *const c_char) -> c_int { - #[cfg(any(test, feature = "test-stubs"))] - { - super::teststubs::oakundo_stack_push(stack, command, text) - } - #[cfg(not(any(test, feature = "test-stubs")))] - { - unsafe { oakundo::ffi::undostack::oakundo_undostack_push(stack, command, text) } - } -} diff --git a/crates/oaktimeline/src/common.rs b/crates/oaktimeline/src/common.rs index a4efdcacd..94a722ae2 100644 --- a/crates/oaktimeline/src/common.rs +++ b/crates/oaktimeline/src/common.rs @@ -19,8 +19,8 @@ //! //! Enum discriminants are value-compatible with both `include/timeline/edit.h` //! (`OAKTIMELINE_MOVEMENT_*`) and `include/timeline/displaymode.h` -//! (`OAK_TIMELINE_THUMBNAIL_*` / `OAK_TIMELINE_WAVEFORMS_*`); `ffi.rs` maps -//! between them mechanically. +//! (`OAK_TIMELINE_THUMBNAIL_*` / `OAK_TIMELINE_WAVEFORMS_*`); the deleted C +//! ABI export layer used to map between them mechanically. use oakcore_rs::Rational; @@ -92,13 +92,14 @@ pub enum WaveformMode { } /// `Timeline::EditToInfo` (timelinecommon.h): where the nearest block edge -/// falls for a pointer edit. Not `Copy`/`Clone`/`Debug` — it owns -/// [`crate::handle::CHandle`]s, which are opaque refcounted handles. +/// falls for a pointer edit. The node references are the single-lib +/// domain handles ([`crate::util::NodeRef`] — `None` where the C ABI +/// used a null handle). pub struct EditToInfo { /// Owning track of the nearest block. - pub track: crate::handle::CHandle, + pub track: Option, /// Nearest edit point time. pub nearest_time: Rational, /// Nearest block. - pub nearest_block: crate::handle::CHandle, + pub nearest_block: Option, } diff --git a/crates/oaktimeline/src/ffi.rs b/crates/oaktimeline/src/ffi.rs deleted file mode 100644 index f6ddd3efa..000000000 --- a/crates/oaktimeline/src/ffi.rs +++ /dev/null @@ -1,1182 +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 . - -//! C ABI export layer: implements `include/timeline/*.h` verbatim. -//! -//! One submodule per public header; the authoritative function list is the -//! header itself and each submodule carries the complete inventory comment -//! plus the export stubs. Bodies only unwrap handles, call safe Rust, and -//! map results through [`crate::handle::guard*`]. -//! -//! `error.h` defines macros only and is mirrored by [`crate::error`], not -//! here. `displaymode.h` defines enums only; their values map through -//! [`crate::common`] and no `#[no_mangle]` exports exist for them. - -use std::ffi::{c_char, c_int, CStr}; - -use oakcore_rs::{Rational, TimeRange}; - -use crate::bridge::{common as xml, node as onode, undo as oundo}; -use crate::common::MovementMode; -use crate::error::{Error, Result}; -use crate::handle::{ - get, get_mut, guard, guard_handle, guard_i32, guard_void, make_owned, CHandle, -}; -use crate::marker as marker_mod; -use crate::undogeneral::{ - TimelineAddTrackCommand, TimelineRemoveTrackCommand, TrackListInsertGaps, - TrackReplaceBlockWithGapCommand, -}; -use crate::undopointer::{ - BlockTrimCommand, TrackMoveBlockCommand, TrackPlaceBlockCommand, TrackSlideCommand, -}; -use crate::undoripple::{TimelineRippleDeleteGapsAtRegionsCommand, TrackRippleRemoveAreaCommand}; -use crate::undosplit::{BlockSplitCommand, BlockSplitPreservingLinksCommand}; -use crate::util::free_detached_handle; -use crate::workarea as workarea_mod; - -/// Shared handle used for every value/owned handle that crosses the ABI: -/// `OakTimelineMarkerList`, `OakTimelineWorkArea`, `OakUndoCommand`, and -/// the consumed `OakNode*` / `OakXmlReader` / `OakXmlWriter` arguments. -type H = CHandle; - -/// `include/timeline/displaymode.h` — enums only. No functions; the enum -/// discriminants are mapped by `crate::common::ThumbnailMode`/`WaveformMode` -/// (`to_c_int`/`from_c_int`) and by `crate::common::MovementMode` for -/// `OakTimelineMovementMode` (include/timeline/edit.h). -pub mod displaymode { - //! `OAK_TIMELINE_THUMBNAIL_OFF/IN_OUT/ON = 0/1/2` and - //! `OAK_TIMELINE_WAVEFORMS_DISABLED/ENABLED = 0/1` are value-compatible - //! with `crate::common`; see those types. No exports live here. -} - -/// `include/timeline/marker.h` exports (complete inventory): -/// oaktimeline_marker_list_create / free / of / add / count / at / -/// add_command / remove_at_command / set_time_command / -/// set_props_command / list_load / list_save. -pub mod marker { - use super::*; - - /// `oaktimeline_marker_list_create`: new owning list, count 1; empty on - /// allocation failure. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_marker_list_create() -> H { - guard_handle(|| -> Result { - Ok(make_owned(marker_mod::TimelineMarkerList::new())) - }) - } - - /// `oaktimeline_marker_list_of`: borrowed list of a viewer node; empty for - /// an empty/non-viewer node. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_marker_list_of(owner: H) -> H { - guard_handle(|| -> Result { - if owner.is_null() { - return Err(Error::Invalid); - } - let mut out = CHandle::null(); - // SAFETY: `out` is a valid out pointer; `owner` is a valid handle. - let r = unsafe { onode::oaknode_node_get_markers(owner, &mut out) }; - if r != 0 { - return Err(Error::Failed(format!("oaknode_node_get_markers: {r}"))); - } - Ok(out) - }) - } - - /// `oaktimeline_marker_list_free`: NULL/empty no-op; clears ctx after. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_marker_list_free(list: *mut H) { - guard_void(|| free_detached_handle(list)) - } - - /// `oaktimeline_marker_add`: append a marker directly (no command). - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_marker_add( - list: H, - 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 { - guard(|| { - if list.is_null() { - return Err(Error::Invalid); - } - // SAFETY: the boxed value is a `TimelineMarkerList`; the caller holds - // exclusive access for the duration of the call. - let l = unsafe { get_mut::(&list) } - .ok_or(Error::Invalid)?; - let name = cstr_to_string(name); - let range = TimeRange::new( - Rational::new(in_num as i64, in_den as i64), - Rational::new(out_num as i64, out_den as i64), - ); - l.add_marker(marker_mod::TimelineMarker::with_time(color, range, &name)); - Ok(()) - }) - } - - /// `oaktimeline_marker_count`: number of markers. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_marker_count(list: H, out_count: *mut c_int) -> c_int { - guard(|| { - if list.is_null() || out_count.is_null() { - return Err(Error::Invalid); - } - // SAFETY: the boxed value is a `TimelineMarkerList`. - let l = - unsafe { get::(&list) }.ok_or(Error::Invalid)?; - // SAFETY: `out_count` is a valid out pointer. - unsafe { - *out_count = l.size() as c_int; - } - Ok(()) - }) - } - - /// `oaktimeline_marker_at`: marker at index, two-stage string for the name. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_marker_at( - list: H, - 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 { - guard_i32(|| { - if list.is_null() { - return Err(Error::Invalid); - } - // SAFETY: the boxed value is a `TimelineMarkerList`. - let l = - unsafe { get::(&list) }.ok_or(Error::Invalid)?; - if index < 0 || (index as usize) >= l.size() { - return Err(Error::NotFound); - } - // SAFETY: `index` was bounds-checked above. - let m = l.at(index as usize).ok_or(Error::NotFound)?; - let t = m.time(); - if !in_num.is_null() { - // SAFETY: `in_num` is a valid out pointer. - unsafe { - *in_num = t.in_().numerator() as c_int; - } - } - if !in_den.is_null() { - // SAFETY: `in_den` is a valid out pointer. - unsafe { - *in_den = t.in_().denominator() as c_int; - } - } - if !out_num.is_null() { - // SAFETY: `out_num` is a valid out pointer. - unsafe { - *out_num = t.out().numerator() as c_int; - } - } - if !out_den.is_null() { - // SAFETY: `out_den` is a valid out pointer. - unsafe { - *out_den = t.out().denominator() as c_int; - } - } - if !color.is_null() { - // SAFETY: `color` is a valid out pointer. - unsafe { - *color = m.color(); - } - } - let needed = m.name().len() + 1; - if !name_buf.is_null() && buf_size >= needed as c_int { - write_cstr(name_buf, buf_size, m.name()); - } - Ok(needed as c_int) - }) - } - - /// `oaktimeline_marker_add_command`: owned `MarkerAddCommand`. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_marker_add_command( - list: H, - in_num: c_int, - in_den: c_int, - out_num: c_int, - out_den: c_int, - name: *const c_char, - color: c_int, - ) -> H { - guard_handle(|| -> Result { - if list.is_null() { - return Err(Error::Invalid); - } - let name = cstr_to_string(name); - let range = TimeRange::new( - Rational::new(in_num as i64, in_den as i64), - Rational::new(out_num as i64, out_den as i64), - ); - Ok(marker_mod::MarkerAddCommand::new(list, range, &name, color).to_command()) - }) - } - - /// `oaktimeline_marker_remove_at_command`: owned `MarkerRemoveCommand`. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_marker_remove_at_command(list: H, index: c_int) -> H { - guard_handle(|| -> Result { - let in_bounds = { - if list.is_null() { - return Err(Error::Invalid); - } - // SAFETY: the boxed value is a `TimelineMarkerList`. - let l = unsafe { get::(&list) } - .ok_or(Error::Invalid)?; - index >= 0 && (index as usize) < l.size() - }; - if !in_bounds { - return Err(Error::NotFound); - } - Ok(marker_mod::MarkerRemoveCommand::new(list, index as usize).to_command()) - }) - } - - /// `oaktimeline_marker_set_time_command`: owned `MarkerChangeTimeCommand`. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_marker_set_time_command( - list: H, - index: c_int, - in_num: c_int, - in_den: c_int, - out_num: c_int, - out_den: c_int, - ) -> H { - guard_handle(|| -> Result { - let in_bounds = { - if list.is_null() { - return Err(Error::Invalid); - } - // SAFETY: the boxed value is a `TimelineMarkerList`. - let l = unsafe { get::(&list) } - .ok_or(Error::Invalid)?; - index >= 0 && (index as usize) < l.size() - }; - if !in_bounds { - return Err(Error::NotFound); - } - let range = TimeRange::new( - Rational::new(in_num as i64, in_den as i64), - Rational::new(out_num as i64, out_den as i64), - ); - Ok(marker_mod::MarkerChangeTimeCommand::new(list, index as usize, range).to_command()) - }) - } - - /// `oaktimeline_marker_set_props_command`: owned command setting color - /// and/or name. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_marker_set_props_command( - list: H, - index: c_int, - color: c_int, - name: *const c_char, - ) -> H { - guard_handle(|| -> Result { - let in_bounds = { - if list.is_null() { - return Err(Error::Invalid); - } - // SAFETY: the boxed value is a `TimelineMarkerList`. - let l = unsafe { get::(&list) } - .ok_or(Error::Invalid)?; - index >= 0 && (index as usize) < l.size() - }; - if !in_bounds { - return Err(Error::NotFound); - } - if color < 0 && name.is_null() { - return Err(Error::Invalid); - } - let name_str = cstr_to_string(name); - if color >= 0 && !name.is_null() { - let multi = unsafe { oundo::oakundo_command_init_multi() }; - if multi.is_null() { - return Err(Error::Failed("oakundo_command_init_multi".to_string())); - } - let color_child = - marker_mod::MarkerChangeColorCommand::new(list.clone(), index as usize, color) - .to_command(); - // SAFETY: `multi` is a valid multi command handle. - let _ = - unsafe { oundo::oakundo_command_multi_add_child(multi.clone(), color_child) }; - let name_child = - marker_mod::MarkerChangeNameCommand::new(list, index as usize, &name_str) - .to_command(); - // SAFETY: `multi` is a valid multi command handle. - let _ = - unsafe { oundo::oakundo_command_multi_add_child(multi.clone(), name_child) }; - return Ok(multi); - } - if color >= 0 { - return Ok( - marker_mod::MarkerChangeColorCommand::new(list, index as usize, color) - .to_command(), - ); - } - Ok( - marker_mod::MarkerChangeNameCommand::new(list, index as usize, &name_str) - .to_command(), - ) - }) - } - - /// `oaktimeline_marker_list_load`: read the list from an oakcommon reader. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_marker_list_load(list: H, reader: H) -> c_int { - guard(|| { - if list.is_null() || reader.is_null() { - return Err(Error::Invalid); - } - // SAFETY: the boxed value is a `TimelineMarkerList`; the caller holds - // exclusive access for the duration of the call. - let l = unsafe { get_mut::(&list) } - .ok_or(Error::Invalid)?; - let group = cstr_arg("MarkerColor"); - // SAFETY: `group` is a valid NUL-terminated string for the duration of - // the call. - let default_color = - unsafe { xml::oakcommon_config_get_int(std::ptr::null(), group.as_ptr(), 0) }; - loop { - let element = xml_read_next(&reader); - if element.is_empty() { - break; - } - if element == "marker" { - let mut marker_name = String::new(); - let mut range_in = Rational::new(0, 1); - let mut range_out = Rational::new(0, 1); - let mut marker_color = default_color; - let mut count: c_int = 0; - // SAFETY: `count` is a valid out pointer; `reader` is a valid handle. - let _ = unsafe { - xml::oakcommon_xml_reader_attribute_count(reader.clone(), &mut count) - }; - for i in 0..count { - let (an, av) = xml_attribute(&reader, i); - if an == "name" { - marker_name = av; - } else if an == "in" { - range_in = Rational::from_string(&av); - } else if an == "out" { - range_out = Rational::from_string(&av); - } else if an == "color" { - // C++ `atoi`: garbage parses to 0, not the default color. - marker_color = av.parse::().unwrap_or(0); - } - } - l.add_marker(marker_mod::TimelineMarker::with_time( - marker_color, - TimeRange::new(range_in, range_out), - &marker_name, - )); - } - // SAFETY: `reader` is a valid handle; every branch of the C++ load - // ends by skipping the current element. - unsafe { xml::oakcommon_xml_reader_skip_current_element(reader.clone()) }; - } - Ok(()) - }) - } - - /// `oaktimeline_marker_list_save`: write the list to an oakcommon writer. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_marker_list_save(list: H, writer: H) -> c_int { - guard(|| { - if list.is_null() || writer.is_null() { - return Err(Error::Invalid); - } - // SAFETY: the boxed value is a `TimelineMarkerList`. - let l = - unsafe { get::(&list) }.ok_or(Error::Invalid)?; - for i in 0..l.size() { - // SAFETY: `i` is in range by the loop bound. - let m = l.at(i).ok_or(Error::NotFound)?; - let tag = cstr_arg("marker"); - // SAFETY: `writer` is a valid handle; `tag` is valid for the call. - unsafe { - xml::oakcommon_xml_writer_write_start_element(writer.clone(), tag.as_ptr()) - }; - let name_key = cstr_arg("name"); - let name_val = cstr_arg(m.name()); - // SAFETY: `writer` and both C strings are valid for the call. - unsafe { - xml::oakcommon_xml_writer_write_attribute( - writer.clone(), - name_key.as_ptr(), - name_val.as_ptr(), - ) - }; - let in_key = cstr_arg("in"); - let in_val = cstr_arg(&m.time().in_().to_display_string()); - // SAFETY: as above. - unsafe { - xml::oakcommon_xml_writer_write_attribute( - writer.clone(), - in_key.as_ptr(), - in_val.as_ptr(), - ) - }; - let out_key = cstr_arg("out"); - let out_val = cstr_arg(&m.time().out().to_display_string()); - // SAFETY: as above. - unsafe { - xml::oakcommon_xml_writer_write_attribute( - writer.clone(), - out_key.as_ptr(), - out_val.as_ptr(), - ) - }; - let color_key = cstr_arg("color"); - let color_val = cstr_arg(&m.color().to_string()); - // SAFETY: as above. - unsafe { - xml::oakcommon_xml_writer_write_attribute( - writer.clone(), - color_key.as_ptr(), - color_val.as_ptr(), - ) - }; - // SAFETY: `writer` is a valid handle. - unsafe { xml::oakcommon_xml_writer_write_end_element(writer.clone()) }; - } - Ok(()) - }) - } -} - -/// `include/timeline/workarea.h` exports (complete inventory): -/// oaktimeline_workarea_create / free / of / set_enabled / get / set_range / -/// set_range_command / set_enabled_command / reset / load / save. -pub mod workarea { - use super::*; - - /// `oaktimeline_workarea_create`: new owning work area, count 1; empty on - /// allocation failure. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_workarea_create() -> H { - guard_handle(|| -> Result { - Ok(make_owned(workarea_mod::TimelineWorkArea::new())) - }) - } - - /// `oaktimeline_workarea_of`: borrowed work area of a viewer node; empty - /// for an empty/non-viewer node. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_workarea_of(owner: H) -> H { - guard_handle(|| -> Result { - if owner.is_null() { - return Err(Error::Invalid); - } - let mut out = CHandle::null(); - // SAFETY: `out` is a valid out pointer; `owner` is a valid handle. - let r = unsafe { onode::oaknode_node_get_work_area(owner, &mut out) }; - if r != 0 { - return Err(Error::Failed(format!("oaknode_node_get_work_area: {r}"))); - } - Ok(out) - }) - } - - /// `oaktimeline_workarea_free`: NULL/empty no-op; clears ctx after. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_workarea_free(w: *mut H) { - guard_void(|| free_detached_handle(w)) - } - - /// `oaktimeline_workarea_set_enabled`: set enabled live. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_workarea_set_enabled(w: H, enabled: c_int) -> c_int { - guard(|| { - if w.is_null() { - return Err(Error::Invalid); - } - // SAFETY: the boxed value is a `TimelineWorkArea`; the caller holds - // exclusive access for the duration of the call. - let wa = - unsafe { get_mut::(&w) }.ok_or(Error::Invalid)?; - wa.set_enabled(enabled != 0); - Ok(()) - }) - } - - /// `oaktimeline_workarea_get`: read state; out params may be NULL. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_workarea_get( - w: H, - 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 { - guard(|| { - if w.is_null() { - return Err(Error::Invalid); - } - // SAFETY: the boxed value is a `TimelineWorkArea`. - let wa = unsafe { get::(&w) }.ok_or(Error::Invalid)?; - if !in_num.is_null() { - // SAFETY: `in_num` is a valid out pointer. - unsafe { - *in_num = wa.in_().numerator() as c_int; - } - } - if !in_den.is_null() { - // SAFETY: `in_den` is a valid out pointer. - unsafe { - *in_den = wa.in_().denominator() as c_int; - } - } - if !out_num.is_null() { - // SAFETY: `out_num` is a valid out pointer. - unsafe { - *out_num = wa.out().numerator() as c_int; - } - } - if !out_den.is_null() { - // SAFETY: `out_den` is a valid out pointer. - unsafe { - *out_den = wa.out().denominator() as c_int; - } - } - if !enabled.is_null() { - // SAFETY: `enabled` is a valid out pointer. - unsafe { - *enabled = wa.enabled() as c_int; - } - } - Ok(()) - }) - } - - /// `oaktimeline_workarea_set_range`: set the range live. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_workarea_set_range( - w: H, - in_num: c_int, - in_den: c_int, - out_num: c_int, - out_den: c_int, - ) -> c_int { - guard(|| { - if w.is_null() { - return Err(Error::Invalid); - } - // SAFETY: the boxed value is a `TimelineWorkArea`; the caller holds - // exclusive access for the duration of the call. - let wa = - unsafe { get_mut::(&w) }.ok_or(Error::Invalid)?; - wa.set_range(TimeRange::new( - Rational::new(in_num as i64, in_den as i64), - Rational::new(out_num as i64, out_den as i64), - )); - Ok(()) - }) - } - - /// `oaktimeline_workarea_set_range_command`: owned `WorkareaSetRangeCommand` - /// with caller-supplied old range. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_workarea_set_range_command( - w: H, - 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, - ) -> H { - guard_handle(|| -> Result { - if w.is_null() { - return Err(Error::Invalid); - } - 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), - ); - Ok( - workarea_mod::WorkareaSetRangeCommand::new_with_old(w, range, old_range) - .to_command(), - ) - }) - } - - /// `oaktimeline_workarea_set_enabled_command`: owned - /// `WorkareaSetEnabledCommand`. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_workarea_set_enabled_command(w: H, enabled: c_int) -> H { - guard_handle(|| -> Result { - if w.is_null() { - return Err(Error::Invalid); - } - Ok(workarea_mod::WorkareaSetEnabledCommand::new(w, enabled != 0).to_command()) - }) - } - - /// `oaktimeline_workarea_reset`: the reset sentinel range - /// (k_reset_in = 0/1 .. k_reset_out = -1/1). - #[no_mangle] - pub unsafe extern "C" 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 { - guard(|| { - if in_num.is_null() || in_den.is_null() || out_num.is_null() || out_den.is_null() { - return Err(Error::Invalid); - } - let rin = workarea_mod::reset_in(); - let rout = workarea_mod::reset_out(); - // SAFETY: all four pointers were checked non-NULL above. - 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; - } - Ok(()) - }) - } - - /// `oaktimeline_workarea_load`: read from an oakcommon reader. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_workarea_load(w: H, reader: H) -> c_int { - guard(|| { - if w.is_null() || reader.is_null() { - return Err(Error::Invalid); - } - // SAFETY: the boxed value is a `TimelineWorkArea`; the caller holds - // exclusive access for the duration of the call. - let wa = - unsafe { get_mut::(&w) }.ok_or(Error::Invalid)?; - let mut range_in = wa.in_(); - let mut range_out = wa.out(); - loop { - let element = xml_read_next(&reader); - if element.is_empty() { - break; - } - if element == "enabled" { - wa.set_enabled(xml_element_text(&reader) != "0"); - } else if element == "in" { - range_in = Rational::from_string(&xml_element_text(&reader)); - } else if element == "out" { - range_out = Rational::from_string(&xml_element_text(&reader)); - } else { - // SAFETY: `reader` is a valid handle. - unsafe { xml::oakcommon_xml_reader_skip_current_element(reader.clone()) }; - } - } - let loaded = TimeRange::new(range_in, range_out); - if loaded != *wa.range() { - wa.set_range(loaded); - } - Ok(()) - }) - } - - /// `oaktimeline_workarea_save`: write to an oakcommon writer. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_workarea_save(w: H, writer: H) -> c_int { - guard(|| { - if w.is_null() || writer.is_null() { - return Err(Error::Invalid); - } - // SAFETY: the boxed value is a `TimelineWorkArea`. - let wa = unsafe { get::(&w) }.ok_or(Error::Invalid)?; - let version_key = cstr_arg("version"); - let version_val = cstr_arg("1"); - // SAFETY: `writer` and both C strings are valid for the call. - unsafe { - xml::oakcommon_xml_writer_write_attribute( - writer.clone(), - version_key.as_ptr(), - version_val.as_ptr(), - ) - }; - let enabled_key = cstr_arg("enabled"); - let enabled_val = cstr_arg(if wa.enabled() { "1" } else { "0" }); - // SAFETY: `writer` and both C strings are valid for the call. - unsafe { - xml::oakcommon_xml_writer_write_text_element( - writer.clone(), - enabled_key.as_ptr(), - enabled_val.as_ptr(), - ) - }; - let in_key = cstr_arg("in"); - let in_val = cstr_arg(&wa.in_().to_display_string()); - // SAFETY: as above. - unsafe { - xml::oakcommon_xml_writer_write_text_element( - writer.clone(), - in_key.as_ptr(), - in_val.as_ptr(), - ) - }; - let out_key = cstr_arg("out"); - let out_val = cstr_arg(&wa.out().to_display_string()); - // SAFETY: as above. - unsafe { - xml::oakcommon_xml_writer_write_text_element( - writer.clone(), - out_key.as_ptr(), - out_val.as_ptr(), - ) - }; - Ok(()) - }) - } -} - -/// `include/timeline/edit.h` exports (complete inventory): -/// oaktimeline_add_track_command / remove_track_command / -/// place_block_command / replace_block_with_gap_command / move_block_command / -/// trim_command / split_command / split_preserving_links_command / -/// ripple_delete_gaps_command / slide_command / ripple_remove_area_command / -/// insert_gaps_command. `OakTimelineMovementMode` maps through -/// `crate::common::MovementMode::from_c_int`. -pub mod edit { - use super::*; - - /// `oaktimeline_add_track_command`: owned `TimelineAddTrackCommand`. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_add_track_command(list: H) -> H { - guard_handle(|| -> Result { - if list.is_null() { - return Err(Error::Invalid); - } - Ok(TimelineAddTrackCommand::new(list).to_command()) - }) - } - - /// `oaktimeline_remove_track_command`: owned `TimelineRemoveTrackCommand`. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_remove_track_command(track: H) -> H { - guard_handle(|| -> Result { - if track.is_null() { - return Err(Error::Invalid); - } - Ok(TimelineRemoveTrackCommand::new(track).to_command()) - }) - } - - /// `oaktimeline_place_block_command`: owned `TrackPlaceBlockCommand`. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_place_block_command( - list: H, - track_index: c_int, - block: H, - in_num: i64, - in_den: i64, - ) -> H { - guard_handle(|| -> Result { - if list.is_null() || block.is_null() { - return Err(Error::Invalid); - } - Ok( - TrackPlaceBlockCommand::new( - list, - track_index, - block, - Rational::new(in_num, in_den), - ) - .to_command(), - ) - }) - } - - /// `oaktimeline_replace_block_with_gap_command`: owned - /// `TrackReplaceBlockWithGapCommand`. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_replace_block_with_gap_command(track: H, block: H) -> H { - guard_handle(|| -> Result { - if track.is_null() || block.is_null() { - return Err(Error::Invalid); - } - // C++ `handle_transitions` defaults to true. - Ok(TrackReplaceBlockWithGapCommand::new(track, block, true).to_command()) - }) - } - - /// `oaktimeline_move_block_command`: owned `TrackMoveBlockCommand` — gap - /// the block's old spot and place it at `in_` on `track_index` as one - /// undoable entry (the capi's `oakengine_sequence_move_clip` assembly). - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_move_block_command( - list: H, - track_index: c_int, - block: H, - in_num: i64, - in_den: i64, - ) -> H { - guard_handle(|| -> Result { - if list.is_null() || block.is_null() { - return Err(Error::Invalid); - } - Ok( - TrackMoveBlockCommand::new(list, track_index, block, Rational::new(in_num, in_den)) - .to_command(), - ) - }) - } - - /// `oaktimeline_trim_command`: owned `BlockTrimCommand`; `mode` is an - /// `OakTimelineMovementMode` value. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_trim_command( - track: H, - block: H, - new_length_num: i64, - new_length_den: i64, - mode: c_int, - ) -> H { - guard_handle(|| -> Result { - if track.is_null() || block.is_null() { - return Err(Error::Invalid); - } - let mode = MovementMode::from_c_int(mode).ok_or(Error::Invalid)?; - // C++ rejects NONE and MOVE as well. - if !mode.is_a_trim_mode() { - return Err(Error::Invalid); - } - Ok(BlockTrimCommand::new( - track, - block, - Rational::new(new_length_num, new_length_den), - mode, - ) - .to_command()) - }) - } - - /// `oaktimeline_split_command`: owned `BlockSplitCommand` on a set of - /// blocks at one point. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_split_command( - blocks: *const H, - count: c_int, - point_num: i64, - point_den: i64, - ) -> H { - guard_handle(|| -> Result { - if blocks.is_null() || count <= 0 { - return Err(Error::Invalid); - } - let point = Rational::new(point_num, point_den); - // SAFETY: `blocks` is a valid array of `count` handles (checked - // non-NULL and positive above), readable for the duration of the call. - let slice = unsafe { std::slice::from_raw_parts(blocks, count as usize) }; - let mut children: Vec = Vec::new(); - for b in slice { - // C++ skips null block handles in the loop. - if !b.is_null() { - children.push(BlockSplitCommand::new(b.clone(), point).to_command()); - } - } - let multi = unsafe { oundo::oakundo_command_init_multi() }; - if multi.is_null() { - return Err(Error::Failed("oakundo_command_init_multi".to_string())); - } - for child in children { - // SAFETY: `multi` is a valid multi command handle. - let _ = unsafe { oundo::oakundo_command_multi_add_child(multi.clone(), child) }; - } - Ok(multi) - }) - } - - /// `oaktimeline_split_preserving_links_command`: owned - /// `BlockSplitPreservingLinksCommand`. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_split_preserving_links_command( - blocks: *const H, - count: c_int, - point_nums: *const i64, - point_dens: *const i64, - time_count: c_int, - ) -> H { - guard_handle(|| -> Result { - if blocks.is_null() - || count <= 0 - || point_nums.is_null() - || point_dens.is_null() - || time_count <= 0 - { - return Err(Error::Invalid); - } - // SAFETY: `blocks` is a valid array of `count` handles; `point_nums` - // and `point_dens` are valid arrays of `time_count` i64s; all readable - // for the duration of the call. - let block_vec: Vec = - unsafe { std::slice::from_raw_parts(blocks, count as usize) }.to_vec(); - let nums = unsafe { std::slice::from_raw_parts(point_nums, time_count as usize) }; - let dens = unsafe { std::slice::from_raw_parts(point_dens, time_count as usize) }; - let times: Vec = nums - .iter() - .zip(dens.iter()) - .map(|(n, d)| Rational::new(*n, *d)) - .collect(); - Ok(BlockSplitPreservingLinksCommand::new(block_vec, times).to_command()) - }) - } - - /// `oaktimeline_ripple_delete_gaps_command`: owned - /// `TimelineRippleDeleteGapsAtRegionsCommand`. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_ripple_delete_gaps_command( - sequence: H, - in_nums: *const i64, - in_dens: *const i64, - out_nums: *const i64, - out_dens: *const i64, - tracks: *const H, - range_count: c_int, - ) -> H { - guard_handle(|| -> Result { - if sequence.is_null() - || in_nums.is_null() - || in_dens.is_null() - || out_nums.is_null() - || out_dens.is_null() - || tracks.is_null() - || range_count <= 0 - { - return Err(Error::Invalid); - } - // SAFETY: all five arrays are valid and `range_count`-long (checked - // non-NULL and positive above), readable for the duration of the call. - let in_nums = unsafe { std::slice::from_raw_parts(in_nums, range_count as usize) }; - let in_dens = unsafe { std::slice::from_raw_parts(in_dens, range_count as usize) }; - let out_nums = unsafe { std::slice::from_raw_parts(out_nums, range_count as usize) }; - let out_dens = unsafe { std::slice::from_raw_parts(out_dens, range_count as usize) }; - let tracks = unsafe { std::slice::from_raw_parts(tracks, range_count as usize) }; - let regions: Vec<(CHandle, TimeRange)> = (0..range_count as usize) - .map(|i| { - ( - tracks[i].clone(), - TimeRange::new( - Rational::new(in_nums[i], in_dens[i]), - Rational::new(out_nums[i], out_dens[i]), - ), - ) - }) - .collect(); - Ok(TimelineRippleDeleteGapsAtRegionsCommand::new(sequence, regions).to_command()) - }) - } - - /// `oaktimeline_slide_command`: owned `TrackSlideCommand`. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_slide_command( - track: H, - blocks: *const H, - block_count: c_int, - in_adjacent: H, - out_adjacent: H, - movement_num: i64, - movement_den: i64, - ) -> H { - guard_handle(|| -> Result { - if track.is_null() || blocks.is_null() || block_count <= 0 { - return Err(Error::Invalid); - } - // SAFETY: `blocks` is a valid array of `block_count` handles (checked - // non-NULL and positive above), readable for the duration of the call. - let block_vec: Vec = - unsafe { std::slice::from_raw_parts(blocks, block_count as usize) }.to_vec(); - Ok(TrackSlideCommand::new( - track, - block_vec, - in_adjacent, - out_adjacent, - Rational::new(movement_num, movement_den), - ) - .to_command()) - }) - } - - /// `oaktimeline_ripple_remove_area_command`: owned - /// `TrackRippleRemoveAreaCommand`. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_ripple_remove_area_command( - track: H, - in_num: i64, - in_den: i64, - out_num: i64, - out_den: i64, - ) -> H { - guard_handle(|| -> Result { - if track.is_null() { - return Err(Error::Invalid); - } - let range = TimeRange::new( - Rational::new(in_num, in_den), - Rational::new(out_num, out_den), - ); - Ok(TrackRippleRemoveAreaCommand::new(track, range).to_command()) - }) - } - - /// `oaktimeline_insert_gaps_command`: owned `TrackListInsertGaps`. - #[no_mangle] - pub unsafe extern "C" fn oaktimeline_insert_gaps_command( - list: H, - point_num: i64, - point_den: i64, - length_num: i64, - length_den: i64, - ) -> H { - guard_handle(|| -> Result { - if list.is_null() { - return Err(Error::Invalid); - } - Ok(TrackListInsertGaps::new( - list, - Rational::new(point_num, point_den), - Rational::new(length_num, length_den), - ) - .to_command()) - }) - } -} - -/// Convert a NUL-terminated C string to `String`; `NULL` yields the empty -/// string (mirrors the C++ callers, which treat a null name as ""). -fn cstr_to_string(p: *const c_char) -> String { - if p.is_null() { - return String::new(); - } - // SAFETY: the caller guarantees `p` is a valid NUL-terminated string. - let s = unsafe { CStr::from_ptr(p) }; - s.to_string_lossy().into_owned() -} - -/// Build a temporary NUL-terminated C string for an extern argument. -fn cstr_arg(s: &str) -> std::ffi::CString { - std::ffi::CString::new(s).unwrap_or_default() -} - -/// Copy `s` into `dst` as a NUL-terminated C string, writing at most `size`-1 -/// bytes; NULL buffer or non-positive size is a no-op. -fn write_cstr(dst: *mut c_char, size: c_int, s: &str) { - if dst.is_null() || size <= 0 { - return; - } - let cap = (size as usize).saturating_sub(1); - let bytes = s.as_bytes(); - let n = bytes.len().min(cap); - // SAFETY: `dst` points to a writable buffer of at least `size` bytes, so - // `n <= size - 1` bytes plus the NUL terminator fit. - unsafe { - std::ptr::copy_nonoverlapping(bytes.as_ptr() as *const c_char, dst, n); - *dst.add(n) = 0; - } -} - -/// Read the next start element's name from `reader`; empty at EOF/error. -fn xml_read_next(reader: &H) -> String { - // Advance to the next start element (the real ABI writes a 1/0 flag). - let mut found = 0; - // SAFETY: `found` is a valid out pointer; `reader` is a valid handle. - let r = - unsafe { xml::oakcommon_xml_reader_read_next_start_element(reader.clone(), &mut found) }; - if r <= 0 || found == 0 { - return String::new(); - } - // Read the current element's name (two-stage). - let mut buf = [0 as c_char; 4096]; - // SAFETY: `buf` is a valid writable buffer; `reader` is a valid handle. - let n = unsafe { - xml::oakcommon_xml_reader_name(reader.clone(), buf.as_mut_ptr(), buf.len() as c_int) - }; - if n <= 0 { - return String::new(); - } - // SAFETY: the reader wrote a NUL-terminated string into `buf`. - let s = unsafe { CStr::from_ptr(buf.as_ptr()) }; - s.to_string_lossy().into_owned() -} - -/// Read the current element's text content from `reader`. -fn xml_element_text(reader: &H) -> String { - let mut buf = [0 as c_char; 4096]; - // SAFETY: `buf` is a valid writable buffer; `reader` is a valid handle. - let r = unsafe { - xml::oakcommon_xml_reader_read_element_text( - reader.clone(), - buf.as_mut_ptr(), - buf.len() as c_int, - ) - }; - if r <= 0 { - return String::new(); - } - // SAFETY: the reader wrote a NUL-terminated string into `buf`. - let s = unsafe { CStr::from_ptr(buf.as_ptr()) }; - s.to_string_lossy().into_owned() -} - -/// Read attribute `index`'s (name, value) pair from `reader`. -fn xml_attribute(reader: &H, index: c_int) -> (String, String) { - let mut name_buf = [0 as c_char; 4096]; - let mut value_buf = [0 as c_char; 4096]; - // SAFETY: both buffers are valid writable; `reader` is a valid handle. - let _ = unsafe { - xml::oakcommon_xml_reader_attribute_name( - reader.clone(), - index, - name_buf.as_mut_ptr(), - name_buf.len() as c_int, - ) - }; - let _ = unsafe { - xml::oakcommon_xml_reader_attribute_value( - reader.clone(), - index, - value_buf.as_mut_ptr(), - value_buf.len() as c_int, - ) - }; - // SAFETY: the reader wrote NUL-terminated strings into both buffers. - let name = unsafe { CStr::from_ptr(name_buf.as_ptr()) }; - let value = unsafe { CStr::from_ptr(value_buf.as_ptr()) }; - ( - name.to_string_lossy().into_owned(), - value.to_string_lossy().into_owned(), - ) -} diff --git a/crates/oaktimeline/src/handle.rs b/crates/oaktimeline/src/handle.rs index dc36b7820..282224818 100644 --- a/crates/oaktimeline/src/handle.rs +++ b/crates/oaktimeline/src/handle.rs @@ -41,8 +41,7 @@ pub struct RefBox { /// 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 -/// here so the crate's `ffi.rs` signatures and handle scaffolding stay -/// source-compatible. +/// here so the crate's handle scaffolding stays source-compatible. pub use oakcore_rs::handle::CHandle; /// Generic `addref` implementation: increments the box's reference count. diff --git a/crates/oaktimeline/src/lib.rs b/crates/oaktimeline/src/lib.rs index 94f096abf..7fd45ac8d 100644 --- a/crates/oaktimeline/src/lib.rs +++ b/crates/oaktimeline/src/lib.rs @@ -21,20 +21,20 @@ //! architectural mapping (per-header domain modules, no UndoCommand //! inheritance → vtable commands). //! -//! ## FFI discipline +//! ## Single-lib unification //! -//! Identical to the oaknode crate: every export goes through -//! [`handle::guard*`], handles are opaque refcounted boxes, and all -//! cross-module access goes through the oaknode/oakundo/oakcommon C ABIs -//! (`bridge::*`). +//! The C ABI export layer (`ffi.rs`) and the oaknode/oakundo/oakcommon +//! bridge (`bridge/`) were deleted in the single-lib unification: undo +//! commands are now `oakundo::undocommand::UndoCommand` values and every +//! node/block/track reference is a [`util::NodeRef`] — an +//! `Arc>` + `oaknode::id::NodeId` pair +//! that the commands manipulate through the oaknode Rust domain directly. #![deny(unsafe_op_in_unsafe_fn)] #![warn(missing_docs)] -pub mod bridge; pub mod common; pub mod error; -pub mod ffi; pub mod handle; pub mod marker; pub mod undocommon; diff --git a/crates/oaktimeline/src/marker.rs b/crates/oaktimeline/src/marker.rs index 328ba9b21..a461bcc59 100644 --- a/crates/oaktimeline/src/marker.rs +++ b/crates/oaktimeline/src/marker.rs @@ -26,6 +26,7 @@ //! order via `TimelineMarkerList::resort`/`resort_at`. use oakcore_rs::{Rational, TimeRange}; +use oakundo::undocommand::UndoCommand; use crate::handle::{get, get_mut}; use crate::undocommon::{box_command, Command}; @@ -232,7 +233,7 @@ fn rational_abs(r: Rational) -> Rational { // --------------------------------------------------------------------------- // Marker undo commands. Each struct exposes prepare()/redo()/undo(); the -// FFI layer wraps it through bridge::undo's vtable (to_command()). +// undo stack wraps it through undocommon's vtable (to_command()). // --------------------------------------------------------------------------- /// `MarkerAddCommand` (timelinemarker.h). @@ -302,7 +303,7 @@ impl MarkerAddCommand { } /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> crate::handle::CHandle { + pub fn to_command(self) -> UndoCommand { box_command(self) } } @@ -369,7 +370,7 @@ impl MarkerRemoveCommand { } /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> crate::handle::CHandle { + pub fn to_command(self) -> UndoCommand { box_command(self) } } @@ -437,7 +438,7 @@ impl MarkerChangeColorCommand { } /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> crate::handle::CHandle { + pub fn to_command(self) -> UndoCommand { box_command(self) } } @@ -505,7 +506,7 @@ impl MarkerChangeNameCommand { } /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> crate::handle::CHandle { + pub fn to_command(self) -> UndoCommand { box_command(self) } } @@ -576,7 +577,7 @@ impl MarkerChangeTimeCommand { } /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> crate::handle::CHandle { + pub fn to_command(self) -> UndoCommand { box_command(self) } } diff --git a/crates/oaktimeline/src/undocommon.rs b/crates/oaktimeline/src/undocommon.rs index e54494e0a..80a98fc76 100644 --- a/crates/oaktimeline/src/undocommon.rs +++ b/crates/oaktimeline/src/undocommon.rs @@ -14,72 +14,122 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//! Shared node-removal helpers and the C-command wrapper -//! (`src/timeline/src/timelineundocommon.h`). All node-graph access goes -//! through the oaknode C ABI (`bridge::node`) and undo through -//! `bridge::undo`. +//! Shared node-removal helpers and the command wrapper +//! (`src/timeline/src/timelineundocommon.h`). Since the single-lib +//! unification, commands are `oakundo::undocommand::UndoCommand` values +//! (the oakundo C ABI and the oaknode C ABI are both gone); node-graph +//! access goes directly through the oaknode Rust domain +//! ([`crate::util::NodeRef`] + the project's graph arena). //! //! `CHandleCommandWrapper` in C++ subclasses `olive::UndoCommand` to wrap a -//! raw `OakUndoCommand`; the Rust equivalent holds a command `CHandle` and -//! forwards `redo`/`undo` to `bridge::undo::oakundo_command_redo_now/undo_now`. +//! raw `OakUndoCommand`; the Rust equivalent holds an +//! [`oakundo::undocommand::UndoCommand`] and forwards `redo`/`undo` to +//! `redo_now`/`undo_now`. use std::ffi::c_void; -use crate::bridge::node::{ - oaknode_block_as_node, oaknode_command_create_remove_node, oaknode_node_output_connection_count, -}; -use crate::bridge::undo::{ - oakundo_command_free, oakundo_command_init, oakundo_command_redo_now, oakundo_command_undo_now, -}; -use crate::handle::CHandle; +use oaknode::graph::NodeEntry; +use oakundo::undocommand::{OakUndoCommandVtable, UndoCommand}; + +use crate::util::{block_add_to_graph, block_remove_from_graph, NodeRef}; + +/// An empty (all callbacks absent) command — the failure-path result of +/// the deleted oaknode remove-command factory (an empty command handle). +pub(crate) fn empty_command() -> UndoCommand { + UndoCommand::from_vtable( + OakUndoCommandVtable { + redo: None, + undo: None, + free_fn: None, + }, + std::ptr::null_mut(), + ) +} + +/// `oaknode_command_create_remove_node` — a command that detaches a node +/// from the project graph on `redo` (the detached entry is owned by this +/// command) and re-inserts it, identity-preserving, on `undo`. The C++ +/// equivalent runs `node->setParent(&memory_manager_)` / the reverse. +struct RemoveNodeCommand { + /// The node to remove. + node: NodeRef, + /// Arena entry detached on `redo`, owned here until `undo`. + entry: Option, +} + +impl RemoveNodeCommand { + /// Construct from the node to remove. + fn new(node: NodeRef) -> Self { + Self { node, entry: None } + } +} + +impl Command for RemoveNodeCommand { + /// `redo`: detach the node from the graph. + fn redo(&mut self) { + if self.entry.is_none() { + self.entry = block_remove_from_graph(&self.node); + } + } + + /// `undo`: re-insert the node into the graph at its original slot. + fn undo(&mut self) { + if let Some(entry) = self.entry.take() { + block_add_to_graph(&self.node, Some(entry)); + } + } +} /// `node_can_be_removed(Node)`: true when the node has no output /// connections (timelineundocommon.h). -pub fn node_can_be_removed(node: CHandle) -> bool { - let mut count = 0; - // SAFETY: `count` is a valid out pointer. - let _ = unsafe { oaknode_node_output_connection_count(node, &mut count) }; - count == 0 +pub fn node_can_be_removed(node: &NodeRef) -> bool { + let p = node.project.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + p.graph.output_connections(node.id).is_empty() } -/// `node_can_be_removed(Block)`: delegates via `oaknode_block_as_node`. -pub fn block_can_be_removed(block: CHandle) -> bool { - node_can_be_removed(unsafe { oaknode_block_as_node(block) }) +/// `node_can_be_removed(Block)`: the block is checked through its +/// generic-node view. +pub fn block_can_be_removed(block: &NodeRef) -> bool { + node_can_be_removed(block) } /// `create_remove_command(Node)` — `oaknode_command_create_remove_node`. -pub fn create_remove_command(node: CHandle) -> CHandle { - unsafe { oaknode_command_create_remove_node(node) } +pub fn create_remove_command(node: &NodeRef) -> UndoCommand { + box_command(RemoveNodeCommand::new(node.clone())) } -/// `create_remove_command(Block)` — via `oaknode_block_as_node`. -pub fn create_block_remove_command(block: CHandle) -> CHandle { - create_remove_command(unsafe { oaknode_block_as_node(block) }) +/// `create_remove_command(Block)`. +pub fn create_block_remove_command(block: &NodeRef) -> UndoCommand { + create_remove_command(block) } /// `create_and_run_remove_command(Node)` — create then `redo_now`. -pub fn create_and_run_remove_command(node: CHandle) -> CHandle { - let command = create_remove_command(node); - // SAFETY: `command` is a valid handle returned by the bridge. - let _ = unsafe { oakundo_command_redo_now(command.clone()) }; +pub fn create_and_run_remove_command(node: &NodeRef) -> UndoCommand { + let mut command = create_remove_command(node); + command.redo_now(); command } -/// `create_and_run_remove_command(Block)` — via `oaknode_block_as_node`. -pub fn create_and_run_block_remove_command(block: CHandle) -> CHandle { - create_and_run_remove_command(unsafe { oaknode_block_as_node(block) }) +/// `create_and_run_remove_command(Block)`. +pub fn create_and_run_block_remove_command(block: &NodeRef) -> UndoCommand { + create_and_run_remove_command(block) } -/// `free_command_handle`: release and null a command handle; NULL/empty -/// no-op. -pub fn free_command_handle(command: *mut CHandle) { - // SAFETY: passed through to the bridge, which handles NULL. - unsafe { oakundo_command_free(command) }; +/// `free_command_handle`: release and null a command; NULL no-op. With the +/// `UndoCommand` value type, "freeing" is overwriting the pointee with an +/// empty command (the old value drops, running its `free_fn`). +pub fn free_command_handle(command: *mut UndoCommand) { + if command.is_null() { + return; + } + // SAFETY: the caller passes a valid pointer; overwriting drops the old + // value and installs the no-op command. + unsafe { *command = empty_command() }; } /// Trait implemented by every timeline undo command. The crate's commands -/// are boxed into an oakundo vtable command via [`box_command`]; the bridge's -/// `redo_now`/`undo_now` dispatch to these callbacks. +/// are boxed into an oakundo vtable command via [`box_command`]; the +/// command's `redo_now`/`undo_now` dispatch to these callbacks. pub trait Command { /// Apply the change. fn redo(&mut self); @@ -96,7 +146,7 @@ unsafe extern "C" fn redo_cb(userdata: *mut c_void) { return; } // SAFETY: box_command allocated a `Box`; still alive because the - // command handle owns it. + // command owns it. (unsafe { &mut *(userdata as *mut T) }).redo(); } @@ -120,66 +170,56 @@ unsafe extern "C" fn free_cb(userdata: *mut c_void) { if userdata.is_null() { return; } - // SAFETY: the handle owns this box; destroying the handle drops it. + // SAFETY: the command owns this box; dropping it frees the command. unsafe { drop(Box::from_raw(userdata as *mut T)) }; } -/// Box a command into an oakundo vtable command handle. The handle owns the -/// boxed `T`; `redo_now`/`undo_now` dispatch to `T::redo`/`T::undo`, and -/// freeing the handle drops `T`. -pub(crate) fn box_command(cmd: T) -> CHandle { +/// Box a command into an oakundo vtable command value. The command owns +/// the boxed `T`; `redo_now`/`undo_now` dispatch to `T::redo`/`T::undo`, +/// and dropping the command drops `T`. +pub(crate) fn box_command(cmd: T) -> UndoCommand { let userdata = Box::into_raw(Box::new(cmd)) as *mut c_void; - let vtable = crate::bridge::undo::OakUndoCommandVtable { + let vtable = OakUndoCommandVtable { redo: Some(redo_cb::), undo: Some(undo_cb::), free_fn: Some(free_cb::), }; - // SAFETY: `vtable` and `userdata` remain valid for the command's lifetime - // (the bridge copies the vtable and owns `userdata`). - unsafe { oakundo_command_init(&vtable, userdata) } + // `from_vtable` copies the vtable and takes ownership of `userdata`. + UndoCommand::from_vtable(vtable, userdata) } -/// `CHandleCommandWrapper` — an oakundo vtable command exposed as a -/// timeline-level command. `redo`/`undo` forward to `bridge::undo`; -/// dropping frees the handle. +/// `CHandleCommandWrapper` — an oakundo command exposed as a timeline-level +/// command. `redo`/`undo` forward to `redo_now`/`undo_now`; dropping drops +/// the wrapped command. pub struct CHandleCommandWrapper { - /// Wrapped command handle. - command: CHandle, + /// Wrapped command; `None` mirrors the old empty (null) command handle. + command: Option, } impl CHandleCommandWrapper { - /// Construct over an owned command handle. - pub fn new(command: CHandle) -> Self { - Self { command } + /// Construct over an owned command value. + pub fn new(command: UndoCommand) -> Self { + Self { + command: Some(command), + } } - /// Whether the wrapped command is non-empty. + /// Whether the wrapper holds a command (the old non-null check). pub fn is_valid(&self) -> bool { - !self.command.is_null() + self.command.is_some() } - /// `redo`: forward to `oakundo_command_redo_now`. + /// `redo`: forward to `UndoCommand::redo_now`. pub fn redo(&mut self) { - if !self.command.is_null() { - // SAFETY: `self.command` is a valid handle while it is non-empty. - let _ = unsafe { oakundo_command_redo_now(self.command.clone()) }; + if let Some(c) = self.command.as_mut() { + c.redo_now(); } } - /// `undo`: forward to `oakundo_command_undo_now`. + /// `undo`: forward to `UndoCommand::undo_now`. pub fn undo(&mut self) { - if !self.command.is_null() { - // SAFETY: `self.command` is a valid handle while it is non-empty. - let _ = unsafe { oakundo_command_undo_now(self.command.clone()) }; - } - } -} - -impl Drop for CHandleCommandWrapper { - fn drop(&mut self) { - if !self.command.is_null() { - // SAFETY: `self.command` is a valid owned handle. - unsafe { oakundo_command_free(&mut self.command) }; + if let Some(c) = self.command.as_mut() { + c.undo_now(); } } } @@ -187,7 +227,7 @@ impl Drop for CHandleCommandWrapper { /// `MultiUndoCommand` — a command that runs several child commands in order /// on `redo` and in reverse on `undo` (timelineundocommon.h /// `MultiUndoCommand`). Children are boxed `Command`s; the whole group wraps -/// into a single oakundo vtable handle via [`box_command`]. +/// into a single oakundo vtable command via [`box_command`]. pub struct MultiUndoCommand { /// Child commands, run in order on `redo`. commands: Vec>, @@ -211,8 +251,8 @@ impl MultiUndoCommand { self.commands.is_empty() } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } diff --git a/crates/oaktimeline/src/undogeneral.rs b/crates/oaktimeline/src/undogeneral.rs index eec3cf8f3..2e6369b06 100644 --- a/crates/oaktimeline/src/undogeneral.rs +++ b/crates/oaktimeline/src/undogeneral.rs @@ -18,44 +18,31 @@ //! block resizing, media-in, track add/remove, transition removal, gap //! replacement, enable/disable, gap insertion and default transitions. //! -//! Graph mutation goes through the oaknode C ABI (`bridge::node`); command -//! wrapping through `bridge::undo`. Commands that compose child commands own -//! them as plain structs rather than pointers (see `undocommon`). +//! Graph mutation routes directly through the oaknode Rust domain (the +//! oaknode C ABI was deleted in the single-lib unification); command +//! wrapping through `oakundo::undocommand::UndoCommand` values. Commands +//! that compose child commands own them as plain structs rather than +//! pointers (see `undocommon`). Detached blocks are held as owned arena +//! entries on the command that detached them. use oakcore_rs::Rational; +use oaknode::graph::NodeEntry; +use oaknode::track::TrackType; +use oakundo::undocommand::UndoCommand; -use crate::bridge::node::{ - oaknode_block_gap_create, oaknode_block_get_enabled, oaknode_block_get_kind, - oaknode_block_set_enabled, oaknode_clip_get_media_in, oaknode_clip_set_media_in, - oaknode_track_create, oaknode_track_get_block_at, oaknode_track_get_block_count, - oaknode_track_get_locked, oaknode_track_insert_block_after, oaknode_track_replace_block, - oaknode_track_ripple_remove_block, oaknode_tracklist_array_append, - oaknode_tracklist_array_remove_last, oaknode_tracklist_get_track_at, - oaknode_tracklist_get_track_count, oaknode_tracklist_get_type, -}; -use crate::bridge::undo::{oakundo_command_redo_now, oakundo_command_undo_now}; -use crate::handle::CHandle; -use crate::undocommon::{box_command, create_block_remove_command, free_command_handle, Command}; +use crate::undocommon::{box_command, create_block_remove_command, Command}; use crate::undosplit::BlockSplitPreservingLinksCommand; use crate::util::{ - block_add_to_graph, block_in, block_length, block_next, block_out, block_previous, - block_remove_from_graph, block_set_in, block_set_length_and_media_in, - block_set_length_and_media_out, block_track, free_detached_handle, rat_nd, same_block, - track_append_block, track_insert_block_before, + block_add_to_graph, block_enabled, block_gap_create, block_in, block_kind, block_length, + block_next, block_out, block_previous, block_remove_from_graph, block_set_enabled, + block_set_in, block_set_length_and_media_in, block_set_length_and_media_out, block_track, + clip_media_in, clip_set_media_in, same_block, track_append_block, track_create, + track_insert_block_after, track_insert_block_before, track_replace_block, + track_ripple_remove_block, tracklist_append, tracklist_remove_last, tracklist_track_at, + tracklist_track_count, tracklist_type, BlockKind, NodeRef, }; -// `oaknode/block.h` block kinds. -const OAKNODE_BLOCK_OTHER: i32 = 0; -const OAKNODE_BLOCK_CLIP: i32 = 1; -const OAKNODE_BLOCK_GAP: i32 = 2; -const OAKNODE_BLOCK_TRANSITION: i32 = 3; - -// `oaknode/track.h` track types. -const OAKNODE_TRACK_TYPE_NONE: i32 = -1; -const OAKNODE_TRACK_TYPE_VIDEO: i32 = 0; -const OAKNODE_TRACK_TYPE_AUDIO: i32 = 1; - -// `oaknode/sequence.h` element input ids. +// `oaknode/sequence.h` element input ids (the C++ automerge branch). const OAKNODE_SEQUENCE_TEXTURE_INPUT: &str = "tex_in"; const OAKNODE_SEQUENCE_SAMPLES_INPUT: &str = "samples_in"; @@ -64,7 +51,7 @@ const OAKNODE_SEQUENCE_SAMPLES_INPUT: &str = "samples_in"; /// `redo` time. pub struct BlockResizeCommand { /// Block to resize. - block: CHandle, + block: NodeRef, /// New length. new_length: Rational, /// Length captured at `redo` time, restored by `undo`. @@ -73,7 +60,9 @@ pub struct BlockResizeCommand { impl BlockResizeCommand { /// Construct from block + new length. - pub fn new(block: CHandle, new_length: Rational) -> Self { + /// + /// New signature (single-lib): `pub fn new(block: NodeRef, new_length: Rational) -> BlockResizeCommand` + pub fn new(block: NodeRef, new_length: Rational) -> Self { Self { block, new_length, @@ -83,17 +72,17 @@ impl BlockResizeCommand { /// `redo`: capture `old_length`, then set the block's length. pub fn redo(&mut self) { - self.old_length = block_length(self.block.clone()); - block_set_length_and_media_out(self.block.clone(), self.new_length); + self.old_length = block_length(&self.block); + block_set_length_and_media_out(&self.block, self.new_length); } /// `undo`: restore `old_length`. pub fn undo(&mut self) { - block_set_length_and_media_out(self.block.clone(), self.old_length); + block_set_length_and_media_out(&self.block, self.old_length); } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } @@ -115,7 +104,7 @@ impl Command for BlockResizeCommand { /// (timelineundogeneral.h). The old length is captured at `redo` time. pub struct BlockResizeWithMediaInCommand { /// Block to resize. - block: CHandle, + block: NodeRef, /// New length. new_length: Rational, /// Length captured at `redo` time, restored by `undo`. @@ -124,7 +113,9 @@ pub struct BlockResizeWithMediaInCommand { impl BlockResizeWithMediaInCommand { /// Construct from block + new length. - pub fn new(block: CHandle, new_length: Rational) -> Self { + /// + /// New signature (single-lib): `pub fn new(block: NodeRef, new_length: Rational) -> BlockResizeWithMediaInCommand` + pub fn new(block: NodeRef, new_length: Rational) -> Self { Self { block, new_length, @@ -134,17 +125,17 @@ impl BlockResizeWithMediaInCommand { /// `redo`: capture `old_length`, then resize keeping the out point fixed. pub fn redo(&mut self) { - self.old_length = block_length(self.block.clone()); - block_set_length_and_media_in(self.block.clone(), self.new_length); + self.old_length = block_length(&self.block); + block_set_length_and_media_in(&self.block, self.new_length); } /// `undo`: restore `old_length`. pub fn undo(&mut self) { - block_set_length_and_media_in(self.block.clone(), self.old_length); + block_set_length_and_media_in(&self.block, self.old_length); } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } @@ -165,7 +156,7 @@ impl Command for BlockResizeWithMediaInCommand { /// (timelineundogeneral.h). The old media-in is captured at `redo` time. pub struct BlockSetMediaInCommand { /// Block whose media-in changes. - block: CHandle, + block: NodeRef, /// New media-in. new_media_in: Rational, /// Media-in captured at `redo` time, restored by `undo`. @@ -174,7 +165,9 @@ pub struct BlockSetMediaInCommand { impl BlockSetMediaInCommand { /// Construct from block + new media-in. - pub fn new(block: CHandle, new_media_in: Rational) -> Self { + /// + /// New signature (single-lib): `pub fn new(block: NodeRef, new_media_in: Rational) -> BlockSetMediaInCommand` + pub fn new(block: NodeRef, new_media_in: Rational) -> Self { Self { block, new_media_in, @@ -184,27 +177,17 @@ impl BlockSetMediaInCommand { /// `redo`: capture `old_media_in`, then set the media-in. pub fn redo(&mut self) { - let mut n = 0; - let mut d = 0; - // SAFETY: `n`/`d` are valid out pointers. - let _ = unsafe { oaknode_clip_get_media_in(self.block.clone(), &mut n, &mut d) }; - self.old_media_in = Rational::new(n as i64, d as i64); - rat_nd(self.new_media_in, &mut n, &mut d); - // SAFETY: `self.block` (copied) is a valid clip handle. - let _ = unsafe { oaknode_clip_set_media_in(self.block.clone(), n, d) }; + self.old_media_in = clip_media_in(&self.block); + clip_set_media_in(&self.block, self.new_media_in); } /// `undo`: restore `old_media_in`. pub fn undo(&mut self) { - let mut n = 0; - let mut d = 0; - rat_nd(self.old_media_in, &mut n, &mut d); - // SAFETY: `self.block` (copied) is a valid clip handle. - let _ = unsafe { oaknode_clip_set_media_in(self.block.clone(), n, d) }; + clip_set_media_in(&self.block, self.old_media_in); } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } @@ -224,136 +207,130 @@ impl Command for BlockSetMediaInCommand { /// `TimelineAddTrackCommand` — append a track (and optionally merge an /// existing one) to a track list (timelineundogeneral.h). When `automerge` is /// set, a matching neighbouring track is merged into the new one. +/// +/// The merge/input-id members below mirror the C++ class layout and are +/// reserved for the automerge step (not modelled in Rust yet). +#[allow(dead_code)] pub struct TimelineAddTrackCommand { /// Target track list. - timeline: CHandle, + timeline: NodeRef, /// Track created by this command. - track: CHandle, + track: NodeRef, /// Merge node used when `automerge_tracks` is set (unused: see `new`). - merge: CHandle, + merge: Option, /// Input id the track connects to (`tex_in`/`samples_in`). direct_input: String, /// Merge base input id (unused: see `new`). base_input: String, /// Merge blend input id (unused: see `new`). blend_input: String, - /// Undoable position command (unused: see `redo`). - position_command: CHandle, /// Whether to automerge a matching adjacent track. automerge_tracks: bool, - /// Whether `track` is detached and owned by this command. - track_orphaned: bool, - /// Whether `merge` is detached and owned by this command. - merge_orphaned: bool, + /// The detached arena entry while `track` is out of the graph + /// (between `undo` and the next `redo`). + track_entry: Option, } impl TimelineAddTrackCommand { /// Construct with the default automerge behaviour. - pub fn new(timeline: CHandle) -> Self { + /// + /// New signature (single-lib): `pub fn new(timeline: NodeRef) -> TimelineAddTrackCommand` + pub fn new(timeline: NodeRef) -> Self { // NOTE: the C++ reads the `AutoMergeTracks` config via - // `oakcommon_config_get_bool`; the bridge exposes no config getter, so the - // default is hardcoded to `false`. + // `oakcommon_config_get_bool`; the module exposes no config + // getter, so the default is hardcoded to `false`. Self::with_automerge(timeline, false) } /// Construct with an explicit automerge flag. - pub fn with_automerge(timeline: CHandle, automerge: bool) -> Self { - let mut kind = OAKNODE_TRACK_TYPE_NONE; - // SAFETY: `kind` is a valid out pointer. - let _ = unsafe { oaknode_tracklist_get_type(timeline.clone(), &mut kind) }; - // SAFETY: `kind` is a valid track type. - let track = unsafe { oaknode_track_create(kind) }; - let direct_input = if kind == OAKNODE_TRACK_TYPE_VIDEO { - OAKNODE_SEQUENCE_TEXTURE_INPUT.to_string() - } else if kind == OAKNODE_TRACK_TYPE_AUDIO { - OAKNODE_SEQUENCE_SAMPLES_INPUT.to_string() - } else { - String::new() + /// + /// New signature (single-lib): `pub fn with_automerge(timeline: NodeRef, automerge: bool) -> TimelineAddTrackCommand` + pub fn with_automerge(timeline: NodeRef, automerge: bool) -> Self { + let kind = tracklist_type(&timeline).unwrap_or(TrackType::Video); + // SAFETY-free: the track is created directly in the list's project + // graph (the C++ creates it in the scratch project then adds it on + // redo; here both steps collapse into the constructor). + let track = track_create(&timeline.project, kind); + let direct_input = match kind { + TrackType::Video => OAKNODE_SEQUENCE_TEXTURE_INPUT.to_string(), + TrackType::Audio => OAKNODE_SEQUENCE_SAMPLES_INPUT.to_string(), + TrackType::Subtitle => String::new(), }; // NOTE: the C++ merge branch creates a merge/math node via - // `oaknode_factory_create_from_id` when the sequence input is already - // connected; that needs `oaknode_tracklist_get_sequence`, - // `oaknode_node_input_is_connected` and `oaknode_factory_create_from_id`, - // which are absent from this bridge, so `merge`/`base_input`/`blend_input` - // stay empty and the automerge flag is retained but not acted on. + // `oaknode_factory_create_from_id` when the sequence input is + // already connected; the node-graph connection machinery for + // track inputs has no Rust equivalent here, so `merge`/ + // `base_input`/`blend_input` stay empty and the automerge flag is + // retained but not acted on. Self { timeline, track, - merge: CHandle::null(), + merge: None, direct_input, base_input: String::new(), blend_input: String::new(), - position_command: CHandle::null(), automerge_tracks: automerge, - track_orphaned: true, - merge_orphaned: false, + track_entry: None, } } /// `redo`: create (and merge) the track; `track()` then reports it. pub fn redo(&mut self) { - // NOTE: the C++ redo adds the track node to the project graph (via - // `oaknode_track_as_node` + `oaknode_node_get_project`), copies the last - // track's height, connects the track to the sequence element and builds an - // undoable position command; those symbols are absent from this bridge, so - // only the array append is performed. The ownership flag is still flipped, - // as the track is handed to the track list. - // SAFETY: `self.timeline` (copied) is a valid track list handle. - let _ = unsafe { oaknode_tracklist_array_append(self.timeline.clone()) }; - self.track_orphaned = false; + // NOTE: the C++ redo copies the last track's height, connects the + // track to the sequence element and builds an undoable position + // command; those steps have no Rust equivalent here (the track + // input edges are not modelled), so only the list append is + // performed. + if self.track_entry.is_some() { + block_add_to_graph(&self.track, self.track_entry.take()); + } + tracklist_append(&self.timeline, &self.track); } /// `undo`: remove the created track and restore the merged track. pub fn undo(&mut self) { - // NOTE: the C++ undo disconnects the merge/direct input and removes the - // track node from the project graph; those paths need the absent - // `oaknode_node_*` symbols above, so only the array removal is performed. - // SAFETY: `self.timeline` (copied) is a valid track list handle. - let _ = unsafe { oaknode_tracklist_array_remove_last(self.timeline.clone()) }; - self.track_orphaned = true; + // NOTE: the C++ undo disconnects the merge/direct input and removes + // the track node from the project graph; the disconnects have no + // Rust equivalent, but the graph removal is real: the track's arena + // entry is detached and owned here until the next redo. + let _ = tracklist_remove_last(&self.timeline); + if self.track_entry.is_none() { + self.track_entry = block_remove_from_graph(&self.track); + } } /// The track created by this command; only meaningful after `redo`. - pub fn track(&self) -> CHandle { + /// + /// New signature (single-lib): `pub fn track(&self) -> NodeRef` + pub fn track(&self) -> NodeRef { self.track.clone() } /// `TimelineAddTrackCommand::run_immediately` — construct, `redo` and /// return the created track in one step (static-semantics factory). - pub fn run_immediately(timeline: CHandle) -> CHandle { + /// + /// New signature (single-lib): `pub fn run_immediately(timeline: NodeRef) -> NodeRef` + pub fn run_immediately(timeline: NodeRef) -> NodeRef { let mut c = Self::new(timeline); c.redo(); c.track() } /// Overload of `run_immediately` with an explicit automerge flag. - pub fn run_immediately_with_automerge(timeline: CHandle, automerge: bool) -> CHandle { + /// + /// New signature (single-lib): `pub fn run_immediately_with_automerge(timeline: NodeRef, automerge: bool) -> NodeRef` + pub fn run_immediately_with_automerge(timeline: NodeRef, automerge: bool) -> NodeRef { let mut c = Self::with_automerge(timeline, automerge); c.redo(); c.track() } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } -impl Drop for TimelineAddTrackCommand { - /// Free the position command, and the created track/merge if still detached. - fn drop(&mut self) { - if !self.position_command.is_null() { - free_command_handle(&mut self.position_command); - } - if self.track_orphaned { - free_detached_handle(&mut self.track); - } - if self.merge_orphaned { - free_detached_handle(&mut self.merge); - } - } -} - impl Command for TimelineAddTrackCommand { /// `Command::redo` — the inherent method takes precedence. fn redo(&mut self) { @@ -370,65 +347,179 @@ impl Command for TimelineAddTrackCommand { /// on `undo` (timelineundogeneral.h). pub struct TimelineRemoveTrackCommand { /// Track to remove. - track: CHandle, - /// Owning track list, located at `prepare` time (unused: see `prepare`). - list: CHandle, - /// Index of the track in its list (unused: see `prepare`). - index: i32, - /// Graph-removal command (unused: see `prepare`). - remove_command: CHandle, + track: NodeRef, + /// Owning track list, located at `prepare` time. + list: Option, + /// Index of the track in its list (before removal). + index: Option, + /// The detached arena entry while `track` is out of the graph + /// (between `redo` and `undo`). + track_entry: Option, } impl TimelineRemoveTrackCommand { /// Construct from the track to remove. - pub fn new(track: CHandle) -> Self { + /// + /// New signature (single-lib): `pub fn new(track: NodeRef) -> TimelineRemoveTrackCommand` + pub fn new(track: NodeRef) -> Self { Self { track, - list: CHandle::null(), - index: 0, - remove_command: CHandle::null(), + list: None, + index: None, + track_entry: None, } } /// `prepare`: locate the owning list and index. pub fn prepare(&mut self) { - let _ = (self.track.clone(), self.list.clone(), self.index); - // NOTE: the C++ prepare locates the owning list and index via - // `oaknode_track_get_sequence`, `oaknode_track_get_type`, - // `oaknode_track_get_index` and - // `oaknode_tracklist_get_array_index_from_cache_index`, and builds a - // `NodeRemoveCommand` from `oaknode_track_as_node`; those symbols are - // absent from this bridge, so the command retains `track` only. + if self.list.is_some() { + return; + } + let (list_id, index) = { + let project = self.track.project.clone(); + let p = project.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + let Some(entry) = p.graph.get(self.track.id) else { + return; + }; + let Some(behavior) = entry.behavior.as_any() else { + return; + }; + let Some(track_behavior) = behavior.downcast_ref::() + else { + return; + }; + let Some(list_id) = track_behavior.track_list else { + return; + }; + let Some(list_entry) = p.graph.get(list_id) else { + return; + }; + let index = list_entry + .behavior + .as_any() + .and_then(|a| a.downcast_ref::()) + .and_then(|l| l.track_index(self.track.id)); + (list_id, index) + }; + self.list = Some(NodeRef::new(self.track.project.clone(), list_id)); + self.index = index; } - /// `redo`: remove the track from the graph. + /// `redo`: remove the track from the list and from the graph. pub fn redo(&mut self) { - // NOTE: the C++ redo runs the removal command and detaches the track's - // sequence element; that needs `remove_command` plus - // `oaknode_tracklist_get_sequence`, `oaknode_tracklist_get_track_input_id` - // and `oaknode_node_input_array_remove`, absent from this bridge, so - // nothing is executed. + self.prepare(); + let Some(list) = &self.list else { + return; + }; + let index = self.index; + // First pass: remove the track from the list and collect the ids of + // the tracks after it (their indexes shift). + let mut removed = false; + let mut later: Vec = Vec::new(); + { + let mut p = self + .track + .project + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if let Some(entry) = p.graph.get_mut(list.id) { + if let Some(a) = entry.behavior.as_any_mut() { + if let Some(l) = a.downcast_mut::() { + if let Some(i) = l.tracks.iter().position(|t| *t == self.track.id) { + l.tracks.remove(i); + later = l.tracks[i..].to_vec(); + removed = true; + } + } + } + } + } + // Second pass: shift the later tracks' indexes (C++ array semantics). + if removed { + { + let mut p = self + .track + .project + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + for t in &later { + if let Some(te) = p.graph.get_mut(*t) { + if let Some(ta) = te.behavior.as_any_mut() { + if let Some(tb) = ta.downcast_mut::() { + if tb.index > index.unwrap_or(0) as i32 { + tb.index -= 1; + } + } + } + } + } + } + // Detach the track's sequence element membership from the graph + // (the C++ also removes the sequence input connection, which the + // Rust model does not track as edges). + if self.track_entry.is_none() { + self.track_entry = block_remove_from_graph(&self.track); + } + } } /// `undo`: re-insert the track at its former index. pub fn undo(&mut self) { - // NOTE: the C++ undo re-inserts the track's sequence element before - // running the removal command's undo; that needs the same absent symbols, - // so nothing is executed. - } - - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { - box_command(self) - } -} - -impl Drop for TimelineRemoveTrackCommand { - /// Free the removal command if still owned. - fn drop(&mut self) { - if !self.remove_command.is_null() { - free_command_handle(&mut self.remove_command); + let Some(list) = &self.list else { + return; + }; + if self.track_entry.is_some() { + block_add_to_graph(&self.track, self.track_entry.take()); } + let index = self.index.unwrap_or(0); + // Re-insert the track into the list and collect the tracks after it + // (their indexes shift). + let later: Vec = 'block: { + let mut p = self + .track + .project + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if let Some(entry) = p.graph.get_mut(list.id) { + if let Some(a) = entry.behavior.as_any_mut() { + if let Some(l) = a.downcast_mut::() { + let insert_at = index.min(l.tracks.len()); + l.tracks.insert(insert_at, self.track.id); + break 'block l.tracks[insert_at + 1..].to_vec(); + } + } + } + Vec::new() + }; + let mut p = self + .track + .project + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + for t in &later { + if let Some(te) = p.graph.get_mut(*t) { + if let Some(ta) = te.behavior.as_any_mut() { + if let Some(tb) = ta.downcast_mut::() { + if tb.index >= index as i32 { + tb.index += 1; + } + } + } + } + } + if let Some(entry) = p.graph.get_mut(self.track.id) { + if let Some(a) = entry.behavior.as_any_mut() { + if let Some(tb) = a.downcast_mut::() { + tb.track_list = Some(list.id); + tb.index = index as i32; + } + } + } + } + + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { + box_command(self) } } @@ -448,80 +539,81 @@ impl Command for TimelineRemoveTrackCommand { /// optionally removing it from the whole graph (timelineundogeneral.h). pub struct TransitionRemoveCommand { /// Transition block to remove. - block: CHandle, + block: NodeRef, /// Whether to also remove the block from the graph. remove_from_graph: bool, /// Owning track, captured at `redo` time. - track: CHandle, - /// Block after the transition (unused: see `redo`). - out_block: CHandle, - /// Block before the transition (unused: see `redo`). - in_block: CHandle, + track: Option, + /// Block after the transition. + out_block: Option, + /// Block before the transition. + in_block: Option, /// Graph-removal command built at `redo` time. - remove_command: CHandle, + remove_command: Option, } impl TransitionRemoveCommand { /// Construct from the transition block + remove-from-graph flag. - pub fn new(block: CHandle, remove_from_graph: bool) -> Self { + /// + /// New signature (single-lib): `pub fn new(block: NodeRef, remove_from_graph: bool) -> TransitionRemoveCommand` + pub fn new(block: NodeRef, remove_from_graph: bool) -> Self { Self { block, remove_from_graph, - track: CHandle::null(), - out_block: CHandle::null(), - in_block: CHandle::null(), - remove_command: CHandle::null(), + track: None, + out_block: None, + in_block: None, + remove_command: None, } } /// `redo`: relink neighbours around the transition and remove it. pub fn redo(&mut self) { - self.track = block_track(self.block.clone()); - // NOTE: the C++ redo extends the neighbouring blocks by the transition - // offsets and disconnects the transition's inputs; that needs the - // `oaknode_transition_get_*` accessors and `oaknode_block_as_node` + - // `oaknode_node_disconnect`, absent from this bridge, so only the - // ripple-remove is performed. - // SAFETY: `self.track`/`self.block` (copied) are valid track/block handles. - let _ = - unsafe { oaknode_track_ripple_remove_block(self.track.clone(), self.block.clone()) }; + self.track = block_track(&self.block); + self.out_block = block_next(&self.block); + self.in_block = block_previous(&self.block); + // NOTE: the C++ extends the neighbouring blocks by the transition + // offsets and disconnects the transition's inputs; the Rust block + // model has no transition edges, so only the ripple-remove is + // performed. + if let Some(track) = &self.track { + track_ripple_remove_block(track, &self.block); + } if self.remove_from_graph { - if self.remove_command.is_null() { - self.remove_command = create_block_remove_command(self.block.clone()); + if self.remove_command.is_none() { + self.remove_command = Some(create_block_remove_command(&self.block)); + } + if let Some(c) = self.remove_command.as_mut() { + c.redo_now(); } - // SAFETY: `self.remove_command` (copied) is a valid command handle. - let _ = unsafe { oakundo_command_redo_now(self.remove_command.clone()) }; } } /// `undo`: restore the transition between its neighbours. pub fn undo(&mut self) { - if self.remove_from_graph && !self.remove_command.is_null() { - // SAFETY: `self.remove_command` (copied) is a valid command handle. - let _ = unsafe { oakundo_command_undo_now(self.remove_command.clone()) }; + if self.remove_from_graph { + if let Some(c) = self.remove_command.as_mut() { + c.undo_now(); + } + } + // NOTE: the C++ re-inserts the transition between its former + // neighbours, reconnects them and restores the offsets; the + // re-insert is real, the offset/connection restoration is not + // modelled (no transition edges in the Rust block model). + if let Some(track) = &self.track { + match &self.in_block { + Some(before) => track_insert_block_after(track, &self.block, Some(before)), + None => track_insert_block_after(track, &self.block, None), + } } - // NOTE: the C++ undo re-inserts the transition between its former - // neighbours, reconnects them and restores the offsets; that needs - // `oaknode_track_insert_block_before`, the `oaknode_transition_get_*` - // accessors and `oaknode_block_as_node` + `oaknode_node_connect`, absent - // from this bridge, so the block stays detached. } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } -impl Drop for TransitionRemoveCommand { - /// Free the removal command if still owned. - fn drop(&mut self) { - if !self.remove_command.is_null() { - free_command_handle(&mut self.remove_command); - } - } -} - impl Command for TransitionRemoveCommand { /// `Command::redo` — the inherent method takes precedence. fn redo(&mut self) { @@ -539,40 +631,50 @@ impl Command for TransitionRemoveCommand { /// attached transitions are also removed. pub struct TrackReplaceBlockWithGapCommand { /// Owning track. - track: CHandle, + track: NodeRef, /// Block to replace. - block: CHandle, + block: NodeRef, + /// The block's in point captured at construction: the gap must fill + /// the block's stored span, even when a parent command (e.g. + /// `TrackMoveBlockCommand`) re-homes the block's position before + /// this command runs. + block_in_point: Rational, /// Whether to also remove attached transitions. handle_transitions: bool, /// Pre-existing gap extended in place (found at `redo` time). - existing_gap: CHandle, + existing_gap: Option, /// Second gap merged into `existing_gap` (found at `redo` time). - existing_merged_gap: CHandle, + existing_merged_gap: Option, /// Whether `existing_gap` preceded the block (decides re-insert order). existing_gap_precedes: bool, /// Gap created by this command (owned until placed in the graph). - our_gap: CHandle, - /// Whether `our_gap` is detached and owned by this command. - our_gap_orphaned: bool, - /// Whether `existing_merged_gap` is detached and owned by this command. - merged_gap_orphaned: bool, - /// Child transition-removal commands (empty: see `create_remove_...`). + our_gap: Option, + /// Arena entry of `our_gap` while detached from the graph. + our_gap_entry: Option, + /// Arena entry of `existing_merged_gap` while detached from the graph. + merged_gap_entry: Option, + /// Child transition-removal commands (empty: see + /// `create_remove_transition_command_if_necessary`). transition_remove_commands: Vec, } impl TrackReplaceBlockWithGapCommand { /// Construct from track + block + transition handling flag. - pub fn new(track: CHandle, block: CHandle, handle_transitions: bool) -> Self { + /// + /// New signature (single-lib): `pub fn new(track: NodeRef, block: NodeRef, handle_transitions: bool) -> TrackReplaceBlockWithGapCommand` + pub fn new(track: NodeRef, block: NodeRef, handle_transitions: bool) -> Self { + let block_in_point = block_in(&block); Self { track, block, + block_in_point, handle_transitions, - existing_gap: CHandle::null(), - existing_merged_gap: CHandle::null(), + existing_gap: None, + existing_merged_gap: None, existing_gap_precedes: false, - our_gap: CHandle::null(), - our_gap_orphaned: false, - merged_gap_orphaned: false, + our_gap: None, + our_gap_entry: None, + merged_gap_entry: None, transition_remove_commands: Vec::new(), } } @@ -587,39 +689,25 @@ impl TrackReplaceBlockWithGapCommand { c.redo(); } - if !block_next(self.block.clone()).is_null() { + if block_next(&self.block).is_some() { // The block is not at the end of the track, so a gap must fill its space. - let mut new_gap_length = block_length(self.block.clone()); - let previous = block_previous(self.block.clone()); - let next = block_next(self.block.clone()); + let mut new_gap_length = block_length(&self.block); + let previous = block_previous(&self.block); + let next = block_next(&self.block); - let mut prev_kind = OAKNODE_BLOCK_OTHER; - let mut next_kind = OAKNODE_BLOCK_OTHER; - if !previous.is_null() { - // SAFETY: `prev_kind` is a valid out pointer. - let _ = unsafe { oaknode_block_get_kind(previous.clone(), &mut prev_kind) }; - } - if !next.is_null() { - // SAFETY: `next_kind` is a valid out pointer. - let _ = unsafe { oaknode_block_get_kind(next.clone(), &mut next_kind) }; - } - let previous_is_a_gap = prev_kind == OAKNODE_BLOCK_GAP; - let next_is_a_gap = next_kind == OAKNODE_BLOCK_GAP; + let previous_is_a_gap = previous.as_ref().map(|b| block_kind(b) == BlockKind::Gap).unwrap_or(false); + let next_is_a_gap = next.as_ref().map(|b| block_kind(b) == BlockKind::Gap).unwrap_or(false); if previous_is_a_gap && next_is_a_gap { // The clip is preceded and followed by a gap, so merge the two. self.existing_gap = previous.clone(); self.existing_merged_gap = next; - new_gap_length = new_gap_length + block_length(self.existing_merged_gap.clone()); - // SAFETY: `self.track`/`self.existing_merged_gap` (copied) are valid handles. - let _ = unsafe { - oaknode_track_ripple_remove_block( - self.track.clone(), - self.existing_merged_gap.clone(), - ) - }; - block_remove_from_graph(self.existing_merged_gap.clone(), self.track.clone()); - self.merged_gap_orphaned = true; + let merged = self.existing_merged_gap.clone(); + if let Some(m) = &merged { + new_gap_length = new_gap_length + block_length(m); + track_ripple_remove_block(&self.track, m); + self.merged_gap_entry = block_remove_from_graph(m); + } } else if previous_is_a_gap { // Extend this gap to fill the space left by the block. self.existing_gap = previous.clone(); @@ -628,142 +716,94 @@ impl TrackReplaceBlockWithGapCommand { self.existing_gap = next; } - if !self.existing_gap.is_null() { + if let Some(gap) = &self.existing_gap { // Extend an existing gap. In the module world the block's in/out // points are stored on the block (Olive derives them from the track // order), so the extension must keep the gap's in point anchored — // an out-anchored `set_length_and_media_out` would push the in point // negative. - new_gap_length = new_gap_length + block_length(self.existing_gap.clone()); - block_set_length_and_media_in(self.existing_gap.clone(), new_gap_length); - // SAFETY: `self.track`/`self.block` (copied) are valid handles. - let _ = unsafe { - oaknode_track_ripple_remove_block(self.track.clone(), self.block.clone()) - }; - self.existing_gap_precedes = same_block(self.existing_gap.clone(), previous); + new_gap_length = new_gap_length + block_length(gap); + block_set_length_and_media_in(gap, new_gap_length); + track_ripple_remove_block(&self.track, &self.block); + self.existing_gap_precedes = + previous.as_ref().map(|p| same_block(gap, p)).unwrap_or(false); } else { // No gap exists to fill this space, create a new one and swap it in. - if self.our_gap.is_null() { - // SAFETY: `oaknode_block_gap_create` returns a fresh gap handle. - self.our_gap = unsafe { oaknode_block_gap_create() }; - // In-anchored: the fresh gap starts at the block's own in point - // (the module stores positions on the block; Olive derives them - // from the track order) and grows to the block's length. - block_set_in(self.our_gap.clone(), block_in(self.block.clone())); - block_set_length_and_media_in(self.our_gap.clone(), new_gap_length); - self.our_gap_orphaned = true; + if self.our_gap.is_none() { + self.our_gap = Some(block_gap_create(&self.track.project)); + if let Some(gap) = &self.our_gap { + // In-anchored at the block's construction-time in point + // (the module stores positions on the block; the gap + // fills the block's stored span). + block_set_in(gap, self.block_in_point); + block_set_length_and_media_in(gap, new_gap_length); + } + } + if let Some(gap) = &self.our_gap { + block_add_to_graph(gap, self.our_gap_entry.take()); + track_replace_block(&self.track, &self.block, gap); } - block_add_to_graph(self.our_gap.clone(), self.track.clone()); - // SAFETY: `self.track`/`self.block`/`self.our_gap` (copied) are valid handles. - let _ = unsafe { - oaknode_track_replace_block( - self.track.clone(), - self.block.clone(), - self.our_gap.clone(), - ) - }; - self.our_gap_orphaned = false; } } else { // The block is at the end of the track, simply remove it. - let preceding = block_previous(self.block.clone()); - // SAFETY: `self.track`/`self.block` (copied) are valid handles. - let _ = unsafe { - oaknode_track_ripple_remove_block(self.track.clone(), self.block.clone()) - }; + let preceding = block_previous(&self.block); + track_ripple_remove_block(&self.track, &self.block); // Remove a preceding gap too, if there is one. - let mut kind = OAKNODE_BLOCK_OTHER; - if !preceding.is_null() { - // SAFETY: `kind` is a valid out pointer. - let _ = unsafe { oaknode_block_get_kind(preceding.clone(), &mut kind) }; - } - if kind == OAKNODE_BLOCK_GAP { - // SAFETY: `self.track`/`preceding` (copied) are valid handles. - let _ = unsafe { - oaknode_track_ripple_remove_block(self.track.clone(), preceding.clone()) - }; - block_remove_from_graph(preceding.clone(), self.track.clone()); - self.existing_merged_gap = preceding; - self.merged_gap_orphaned = true; + if preceding.as_ref().map(|b| block_kind(b) == BlockKind::Gap).unwrap_or(false) { + if let Some(gap) = &preceding { + track_ripple_remove_block(&self.track, gap); + self.merged_gap_entry = block_remove_from_graph(gap); + self.existing_merged_gap = Some(gap.clone()); + } } } } /// `undo`: replace the gap back with the block. pub fn undo(&mut self) { - if !self.our_gap.is_null() || !self.existing_gap.is_null() { - if !self.our_gap.is_null() { + if self.our_gap.is_some() || self.existing_gap.is_some() { + if let Some(gap) = &self.our_gap { // We made this gap, simply swap it back. - // SAFETY: `self.track`/`self.our_gap`/`self.block` (copied) are valid handles. - let _ = unsafe { - oaknode_track_replace_block( - self.track.clone(), - self.our_gap.clone(), - self.block.clone(), - ) - }; - block_remove_from_graph(self.our_gap.clone(), self.track.clone()); - self.our_gap_orphaned = true; - } else { + track_replace_block(&self.track, gap, &self.block); + if self.our_gap_entry.is_none() { + self.our_gap_entry = block_remove_from_graph(gap); + } + } else if let Some(gap) = &self.existing_gap { // We extended an existing gap; restore its original length. - let mut original_gap_length = - block_length(self.existing_gap.clone()) - block_length(self.block.clone()); + let mut original_gap_length = block_length(gap) - block_length(&self.block); // If we merged two gaps together, restore the second one now. - if !self.existing_merged_gap.is_null() { - original_gap_length = - original_gap_length - block_length(self.existing_merged_gap.clone()); - // SAFETY: the three (copied) handles are valid. - let _ = unsafe { - oaknode_track_insert_block_after( - self.track.clone(), - self.existing_merged_gap.clone(), - self.existing_gap.clone(), - ) - }; - block_add_to_graph(self.existing_merged_gap.clone(), self.track.clone()); - self.merged_gap_orphaned = false; - self.existing_merged_gap = CHandle::null(); + if let Some(merged) = &self.existing_merged_gap { + original_gap_length = original_gap_length - block_length(merged); + track_insert_block_after(&self.track, merged, Some(gap)); + block_add_to_graph(merged, self.merged_gap_entry.take()); + self.existing_merged_gap = None; } // Restore the original block. if self.existing_gap_precedes { - // SAFETY: the three (copied) handles are valid. - let _ = unsafe { - oaknode_track_insert_block_after( - self.track.clone(), - self.block.clone(), - self.existing_gap.clone(), - ) - }; + track_insert_block_after(&self.track, &self.block, Some(gap)); } else { - // The gap followed the block, so the block is restored BEFORE it - // (insert-before, synthesized as insert-after the gap's predecessor - // / prepend). - track_insert_block_before( - self.track.clone(), - self.block.clone(), - self.existing_gap.clone(), - ); + // The gap followed the block, so the block is restored BEFORE it. + track_insert_block_before(&self.track, &self.block, gap); } // Restore the gap's original length (in-anchored: its in point was // untouched by the redo extension). - block_set_length_and_media_in(self.existing_gap.clone(), original_gap_length); - self.existing_gap = CHandle::null(); + block_set_length_and_media_in(gap, original_gap_length); + self.existing_gap = None; } } else { // Both gaps null: the block was at the end of the track, so no gap // extension/replacement was needed. Re-append the merged gap (if any) // and then the block. - if !self.existing_merged_gap.is_null() { - track_append_block(self.track.clone(), self.existing_merged_gap.clone()); - block_add_to_graph(self.existing_merged_gap.clone(), self.track.clone()); - self.merged_gap_orphaned = false; - self.existing_merged_gap = CHandle::null(); + if let Some(merged) = &self.existing_merged_gap { + track_append_block(&self.track, merged); + block_add_to_graph(merged, self.merged_gap_entry.take()); + self.existing_merged_gap = None; } - track_append_block(self.track.clone(), self.block.clone()); + track_append_block(&self.track, &self.block); } for c in self.transition_remove_commands.iter_mut().rev() { @@ -773,43 +813,32 @@ impl TrackReplaceBlockWithGapCommand { fn create_remove_transition_command_if_necessary(&mut self, next: bool) { let relevant_block = if next { - block_next(self.block.clone()) + block_next(&self.block) } else { - block_previous(self.block.clone()) + block_previous(&self.block) }; - let mut kind = OAKNODE_BLOCK_OTHER; - if !relevant_block.is_null() { - // SAFETY: `kind` is a valid out pointer. - let _ = unsafe { oaknode_block_get_kind(relevant_block.clone(), &mut kind) }; - } - if kind != OAKNODE_BLOCK_TRANSITION { + let Some(relevant) = relevant_block else { + return; + }; + if block_kind(&relevant) != BlockKind::Transition { return; } // NOTE: the C++ checks whether the transition is connected only to this // block (via `oaknode_transition_get_connected_out_block`/`_in_block` + - // `same_block`) before adding a `TransitionRemoveCommand`; those accessors - // are absent from this bridge, so no child commands are produced. + // `same_block`) before adding a `TransitionRemoveCommand`; the Rust + // block model has no transition edges, so a command removing the + // transition outright is produced. + self.transition_remove_commands + .push(TransitionRemoveCommand::new(relevant, true)); } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } -impl Drop for TrackReplaceBlockWithGapCommand { - /// Free the gap/merged-gap handles still detached from the graph. - fn drop(&mut self) { - if self.our_gap_orphaned { - free_detached_handle(&mut self.our_gap); - } - if self.merged_gap_orphaned { - free_detached_handle(&mut self.existing_merged_gap); - } - } -} - impl Command for TrackReplaceBlockWithGapCommand { /// `Command::redo` — the inherent method takes precedence. fn redo(&mut self) { @@ -826,40 +855,38 @@ impl Command for TrackReplaceBlockWithGapCommand { /// (timelineundogeneral.h). The old value is captured at construction. pub struct BlockEnableDisableCommand { /// Block whose enabled flag changes. - block: CHandle, + block: NodeRef, /// New enabled value. - new_enabled: i32, + new_enabled: bool, /// Enabled value captured at construction, restored by `undo`. - old_enabled: i32, + old_enabled: bool, } impl BlockEnableDisableCommand { /// Construct from block + new enabled value (captures old at ctor). - pub fn new(block: CHandle, enabled: bool) -> Self { - let mut old_enabled = 0; - // SAFETY: `old_enabled` is a valid out pointer. - let _ = unsafe { oaknode_block_get_enabled(block.clone(), &mut old_enabled) }; + /// + /// New signature (single-lib): `pub fn new(block: NodeRef, enabled: bool) -> BlockEnableDisableCommand` + pub fn new(block: NodeRef, enabled: bool) -> Self { + let old_enabled = block_enabled(&block); Self { block, - new_enabled: enabled as i32, + new_enabled: enabled, old_enabled, } } - /// `redo`: `oaknode_block_set_enabled(block, new)`. + /// `redo`: set the new enabled value. pub fn redo(&mut self) { - // SAFETY: `self.block` (copied) is a valid block handle. - let _ = unsafe { oaknode_block_set_enabled(self.block.clone(), self.new_enabled) }; + block_set_enabled(&self.block, self.new_enabled); } - /// `undo`: `oaknode_block_set_enabled(block, old)`. + /// `undo`: restore the old enabled value. pub fn undo(&mut self) { - // SAFETY: `self.block` (copied) is a valid block handle. - let _ = unsafe { oaknode_block_set_enabled(self.block.clone(), self.old_enabled) }; + block_set_enabled(&self.block, self.old_enabled); } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } @@ -881,15 +908,15 @@ impl Command for BlockEnableDisableCommand { /// existing gaps rather than adding duplicates where possible. pub struct TrackListInsertGaps { /// Track list to operate on. - track_list: CHandle, + track_list: NodeRef, /// Insertion point. point: Rational, /// Gap length to insert. length: Rational, /// Unlocked tracks gathered at `prepare` time. - working_tracks: Vec, + working_tracks: Vec, /// Existing gaps crossed by `point`, extended by `length` on `redo`. - gaps_to_extend: Vec, + gaps_to_extend: Vec, /// Gaps inserted after their `before` blocks on their tracks. gaps_added: Vec, /// Optional per-track split command built at `prepare` time. @@ -900,18 +927,20 @@ pub struct TrackListInsertGaps { /// `undo`; the C++ `TrackListInsertGaps::AddGap` struct. struct AddGap { /// The inserted gap block. - gap: CHandle, - /// Whether `gap` is detached and owned by its parent command. - orphaned: bool, - /// Block the gap is inserted after. - before: CHandle, + gap: NodeRef, + /// Arena entry while `gap` is detached from the graph. + entry: Option, + /// Block the gap is inserted after (`None` = front of the track). + before: Option, /// Track the gap is inserted on. - track: CHandle, + track: NodeRef, } impl TrackListInsertGaps { /// Construct from track list + point + length. - pub fn new(track_list: CHandle, point: Rational, length: Rational) -> Self { + /// + /// New signature (single-lib): `pub fn new(track_list: NodeRef, point: Rational, length: Rational) -> TrackListInsertGaps` + pub fn new(track_list: NodeRef, point: Rational, length: Rational) -> Self { Self { track_list, point, @@ -925,71 +954,54 @@ impl TrackListInsertGaps { /// `prepare`: build the per-track split/gap commands. pub fn prepare(&mut self) { - let mut track_count = 0; - // SAFETY: `track_count` is a valid out pointer. - let _ = - unsafe { oaknode_tracklist_get_track_count(self.track_list.clone(), &mut track_count) }; + let track_count = tracklist_track_count(&self.track_list); for i in 0..track_count { - let mut track = CHandle::null(); - // SAFETY: `track` is a valid out pointer. - let _ = - unsafe { oaknode_tracklist_get_track_at(self.track_list.clone(), i, &mut track) }; - if track.is_null() { + let Some(track) = tracklist_track_at(&self.track_list, i) else { continue; - } - let mut locked = 0; - // SAFETY: `locked` is a valid out pointer. - let _ = unsafe { oaknode_track_get_locked(track.clone(), &mut locked) }; - if locked != 0 { + }; + if crate::util::track_locked(&track) { continue; } self.working_tracks.push(track); } - let mut blocks_to_split: Vec = Vec::new(); - let mut blocks_to_append_gap_to: Vec = Vec::new(); - let mut tracks_to_append_gap_to: Vec = Vec::new(); + let mut blocks_to_split: Vec = Vec::new(); + let mut blocks_to_append_gap_to: Vec> = Vec::new(); + let mut tracks_to_append_gap_to: Vec = Vec::new(); for track in self.working_tracks.clone() { - let mut block_count = 0; - // SAFETY: `block_count` is a valid out pointer. - let _ = unsafe { oaknode_track_get_block_count(track.clone(), &mut block_count) }; + let block_count = crate::util::track_block_count(&track); for i in 0..block_count { - let mut b = CHandle::null(); - // SAFETY: `b` is a valid out pointer. - let _ = unsafe { oaknode_track_get_block_at(track.clone(), i, &mut b) }; - if b.is_null() { + let Some(b) = crate::util::track_block_at(&track, i) else { continue; - } + }; + let kind = block_kind(&b); - let mut kind = OAKNODE_BLOCK_OTHER; - // SAFETY: `kind` is a valid out pointer. - let _ = unsafe { oaknode_block_get_kind(b.clone(), &mut kind) }; - - if kind == OAKNODE_BLOCK_GAP - && block_in(b.clone()) <= self.point - && block_out(b.clone()) >= self.point + if kind == BlockKind::Gap + && block_in(&b) <= self.point + && block_out(&b) >= self.point { // Found a gap at the location. self.gaps_to_extend.push(b); break; - } else if kind == OAKNODE_BLOCK_CLIP && block_out(b.clone()) >= self.point { + } else if kind == BlockKind::Clip && block_out(&b) >= self.point { let mut append_gap = true; + let mut before = Some(b.clone()); - if block_in(b.clone()) == self.point { + if block_in(&b) == self.point { // The block is at the start of the track; no split needs to occur. - b = CHandle::null(); - } else if block_out(b.clone()) > self.point { + before = None; + } else if block_out(&b) > self.point { // The block must be split as well as having a gap appended to it. blocks_to_split.push(b.clone()); - } else if block_next(b.clone()).is_null() { + } else if block_next(&b).is_none() { // At the end of a track, no gap needs to be added at all. append_gap = false; } if append_gap { tracks_to_append_gap_to.push(track.clone()); - blocks_to_append_gap_to.push(b); + blocks_to_append_gap_to.push(before); } break; } @@ -1004,12 +1016,10 @@ impl TrackListInsertGaps { } for i in 0..blocks_to_append_gap_to.len() { - // SAFETY: `oaknode_block_gap_create` returns a fresh gap handle. - let gap = unsafe { oaknode_block_gap_create() }; - block_set_length_and_media_out(gap.clone(), self.length); + let gap = block_gap_create(&self.track_list.project); self.gaps_added.push(AddGap { gap, - orphaned: true, + entry: None, before: blocks_to_append_gap_to[i].clone(), track: tracks_to_append_gap_to[i].clone(), }); @@ -1019,7 +1029,7 @@ impl TrackListInsertGaps { /// `redo`: apply the gap insertions. pub fn redo(&mut self) { for gap in self.gaps_to_extend.clone() { - block_set_length_and_media_out(gap.clone(), block_length(gap.clone()) + self.length); + block_set_length_and_media_out(&gap, block_length(&gap) + self.length); } if let Some(s) = self.split_command.as_mut() { @@ -1027,12 +1037,16 @@ impl TrackListInsertGaps { } for g in self.gaps_added.iter_mut() { - block_add_to_graph(g.gap.clone(), g.track.clone()); - // SAFETY: `g.track`/`g.gap`/`g.before` (copied) are valid handles. - let _ = unsafe { - oaknode_track_insert_block_after(g.track.clone(), g.gap.clone(), g.before.clone()) - }; - g.orphaned = false; + // The gap fills [point, point + length): the `before` block now + // ends at the split point (the split ran above), or the gap + // starts at `point` when it goes to the front of the track. + // Positions are stored on the block, so both ends are written + // explicitly. + let gap_in = g.before.as_ref().map(block_out).unwrap_or(self.point); + block_set_in(&g.gap, gap_in); + block_set_length_and_media_in(&g.gap, self.length); + block_add_to_graph(&g.gap, g.entry.take()); + track_insert_block_after(&g.track, &g.gap, g.before.as_ref()); } } @@ -1040,13 +1054,12 @@ impl TrackListInsertGaps { pub fn undo(&mut self) { // Remove added gaps. for g in self.gaps_added.iter_mut() { - let t = block_track(g.gap.clone()); - if !t.is_null() { - // SAFETY: `t`/`g.gap` (copied) are valid handles. - let _ = unsafe { oaknode_track_ripple_remove_block(t.clone(), g.gap.clone()) }; - block_remove_from_graph(g.gap.clone(), t); + if let Some(t) = block_track(&g.gap) { + track_ripple_remove_block(&t, &g.gap); + } + if g.entry.is_none() { + g.entry = block_remove_from_graph(&g.gap); } - g.orphaned = true; } // Un-split blocks. @@ -1056,27 +1069,16 @@ impl TrackListInsertGaps { // Restore the original length of the extended gaps. for gap in self.gaps_to_extend.clone() { - block_set_length_and_media_out(gap.clone(), block_length(gap.clone()) - self.length); + block_set_length_and_media_out(&gap, block_length(&gap) - self.length); } } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } -impl Drop for TrackListInsertGaps { - /// Free any inserted gaps still detached from the graph. - fn drop(&mut self) { - for g in self.gaps_added.iter_mut() { - if g.orphaned { - free_detached_handle(&mut g.gap); - } - } - } -} - impl Command for TrackListInsertGaps { /// `Command::redo` — the inherent method takes precedence. fn redo(&mut self) { @@ -1092,16 +1094,20 @@ impl Command for TrackListInsertGaps { /// `TimelineAddDefaultTransitionCommand` — add the default transition at the /// start/end (or both) of the given clips (timelineundogeneral.h). Composes a /// set of child commands driven by `timebase_`. +/// +/// `timebase`/`lengths` mirror the C++ members used by the length +/// adjustment once a default transition config exists. +#[allow(dead_code)] pub struct TimelineAddDefaultTransitionCommand { /// Clips to receive a transition. - clips: Vec, + clips: Vec, /// Timeline timebase. timebase: Rational, /// Child commands, executed in order on `redo` (empty: see `prepare`). commands: Vec>, /// Clip length bookkeeping used by the C++ length adjustment (unused: /// see `add_transition`). - lengths: Vec<(CHandle, Rational)>, + lengths: Vec<(NodeRef, Rational)>, } /// The side(s) of a clip a transition is added to (the C++ @@ -1118,7 +1124,9 @@ enum CreateTransitionMode { impl TimelineAddDefaultTransitionCommand { /// Construct from clips + timebase. - pub fn new(clips: Vec, timebase: Rational) -> Self { + /// + /// New signature (single-lib): `pub fn new(clips: Vec, timebase: Rational) -> TimelineAddDefaultTransitionCommand` + pub fn new(clips: Vec, timebase: Rational) -> Self { Self { clips, timebase, @@ -1131,57 +1139,46 @@ impl TimelineAddDefaultTransitionCommand { pub fn prepare(&mut self) { let selection = self.clips.clone(); for c in self.clips.clone() { - let previous = block_previous(c.clone()); - let next = block_next(c.clone()); + let previous = block_previous(&c); + let next = block_next(&c); - let is_clip_in_selection = |b: CHandle| -> bool { - if b.is_null() { - return false; + let is_clip_in_selection = |b: &Option| -> bool { + match b { + Some(b) => selection.iter().any(|clip| same_block(clip, b)), + None => false, } - selection - .iter() - .any(|clip| same_block(clip.clone(), b.clone())) }; - let mut prev_kind = OAKNODE_BLOCK_OTHER; - let mut next_kind = OAKNODE_BLOCK_OTHER; - if !previous.is_null() { - // SAFETY: `prev_kind` is a valid out pointer. - let _ = unsafe { oaknode_block_get_kind(previous.clone(), &mut prev_kind) }; - } - if !next.is_null() { - // SAFETY: `next_kind` is a valid out pointer. - let _ = unsafe { oaknode_block_get_kind(next.clone(), &mut next_kind) }; - } + let prev_kind = previous.as_ref().map(block_kind).unwrap_or(BlockKind::Other); + let next_kind = next.as_ref().map(block_kind).unwrap_or(BlockKind::Other); // Handle the in transition. - if is_clip_in_selection(previous.clone()) { + if is_clip_in_selection(&previous) { // Do nothing; assume this will be handled by a dual transition from // that clip. - } else if prev_kind == OAKNODE_BLOCK_GAP || previous.is_null() { + } else if prev_kind == BlockKind::Gap || previous.is_none() { // Create an in transition. - self.add_transition(c.clone(), CreateTransitionMode::In); + self.add_transition(&c, CreateTransitionMode::In); } // Handle the out transition. - if is_clip_in_selection(next.clone()) { - self.add_transition(c.clone(), CreateTransitionMode::OutDual); - } else if next_kind == OAKNODE_BLOCK_GAP || next.is_null() { + if is_clip_in_selection(&next) { + self.add_transition(&c, CreateTransitionMode::OutDual); + } else if next_kind == BlockKind::Gap || next.is_none() { // Create an out transition. - self.add_transition(c.clone(), CreateTransitionMode::Out); + self.add_transition(&c, CreateTransitionMode::Out); } } } - fn add_transition(&mut self, c: CHandle, mode: CreateTransitionMode) { + fn add_transition(&mut self, c: &NodeRef, mode: CreateTransitionMode) { let _ = (c, mode); // NOTE: the C++ builds one child command per transition: it looks up the // default transition id in the config (`oakcommon_config_get`), creates // the node (`oaknode_factory_create_from_id`), clips the neighbours' // lengths (`BlockResizeCommand`/`BlockResizeWithMediaInCommand`), adds the - // node (`oaknode_command_create_add_node`) and connects it - // (`oaknode_node_connect_undoable`); those symbols are absent from this - // bridge, so no child commands are produced. + // node and connects it; the default-transition config key has no Rust + // equivalent here, so no child commands are produced. } /// `redo`: redo every child command in order. @@ -1198,8 +1195,8 @@ impl TimelineAddDefaultTransitionCommand { } } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } diff --git a/crates/oaktimeline/src/undopointer.rs b/crates/oaktimeline/src/undopointer.rs index 03a627452..efc0ff45c 100644 --- a/crates/oaktimeline/src/undopointer.rs +++ b/crates/oaktimeline/src/undopointer.rs @@ -18,28 +18,25 @@ //! that compensate the adjacent block (`BlockTrimCommand`), slides //! (`TrackSlideCommand`) and destructive placement (`TrackPlaceBlockCommand`). //! -//! Graph mutation goes through the oaknode C ABI (`bridge::node`); command -//! wrapping through `bridge::undo`. +//! Graph mutation routes directly through the oaknode Rust domain (the +//! oaknode C ABI was deleted in the single-lib unification); command +//! wrapping through `oakundo::undocommand::UndoCommand` values. use oakcore_rs::{Rational, TimeRange}; +use oaknode::graph::NodeEntry; +use oakundo::undocommand::UndoCommand; -use crate::bridge::node::{ - oaknode_block_gap_create, oaknode_block_get_kind, oaknode_track_insert_block_after, - oaknode_track_ripple_remove_block, oaknode_tracklist_get_track_at, - oaknode_tracklist_get_track_count, -}; -use crate::bridge::undo::{oakundo_command_redo_now, oakundo_command_undo_now}; use crate::common::MovementMode; -use crate::handle::CHandle; use crate::undocommon::{ block_can_be_removed, box_command, create_and_run_block_remove_command, create_block_remove_command, Command, MultiUndoCommand, }; use crate::util::{ - block_add_to_graph, block_in, block_length, block_next, block_previous, - block_remove_from_graph, block_set_in, block_set_length_and_media_in, - block_set_length_and_media_out, block_track, track_append_block, track_insert_block_before, - track_length, + block_add_to_graph, block_gap_create, block_in, block_kind, block_length, block_next, + block_out, block_previous, block_remove_from_graph, block_set_in, + block_set_length_and_media_in, block_set_length_and_media_out, block_track, + track_append_block, track_insert_block_after, track_insert_block_before, track_length, + track_ripple_remove_block, tracklist_track_at, tracklist_track_count, BlockKind, NodeRef, }; use super::undogeneral::{TimelineAddTrackCommand, TrackReplaceBlockWithGapCommand}; @@ -53,9 +50,9 @@ use super::undoripple::TrackRippleRemoveAreaCommand; /// always trim into the adjacent block instead. pub struct BlockTrimCommand { /// Owning track (`track_`). - track: CHandle, + track: NodeRef, /// Block to trim (`block_`). - block: CHandle, + block: NodeRef, /// New length (`new_length_`). new_length: Rational, /// Trim direction (`mode_`). @@ -69,7 +66,7 @@ pub struct BlockTrimCommand { /// Block length before the trim (`old_length_`). old_length_: Rational, /// The block to compensate (`adjacent_`). - adjacent_: CHandle, + adjacent_: Option, /// Whether the adjacent block is required for this trim (`needs_adjacent_`). needs_adjacent_: bool, /// Whether `adjacent_` was created by this command (`we_created_adjacent_`). @@ -77,26 +74,23 @@ pub struct BlockTrimCommand { /// Whether the adjacent block is removed by this trim (`we_removed_adjacent_`). we_removed_adjacent_: bool, /// Graph-removal command for a removed adjacent (`deleted_adjacent_command_`). - /// - /// Owns its handle like the C++ `OakUndoCommand`; `CHandle` is refcounted - /// and drops without a destructor, so releasing it is implicit — no explicit - /// `free_command_handle` is needed here. - deleted_adjacent_command_: CHandle, + deleted_adjacent_command_: Option, /// Always trim into the adjacent block instead of creating a gap /// (`trim_is_a_roll_edit_`). trim_is_a_roll_edit_: bool, /// Remove a zero-length adjacent block from the whole graph (default true) /// rather than only from the track (`remove_block_from_graph_`). remove_block_from_graph_: bool, - /// Whether `adjacent_` is a created gap still detached from the graph - /// (`adjacent_orphaned_`); a detached handle is owned by this command until - /// it is added to the graph. - adjacent_orphaned_: bool, + /// Arena entry of a created adjacent gap while it is detached from the + /// graph (between `undo` and the next `redo`). + adjacent_entry_: Option, } impl BlockTrimCommand { /// Construct from track + block + new length + movement mode. - pub fn new(track: CHandle, block: CHandle, new_length: Rational, mode: MovementMode) -> Self { + /// + /// New signature (single-lib): `pub fn new(track: NodeRef, block: NodeRef, new_length: Rational, mode: MovementMode) -> BlockTrimCommand` + pub fn new(track: NodeRef, block: NodeRef, new_length: Rational, mode: MovementMode) -> Self { Self { track, block, @@ -105,14 +99,14 @@ impl BlockTrimCommand { doing_nothing_: false, trim_diff_: Rational::new(0, 1), old_length_: Rational::new(0, 1), - adjacent_: CHandle::null(), + adjacent_: None, needs_adjacent_: false, we_created_adjacent_: false, we_removed_adjacent_: false, - deleted_adjacent_command_: CHandle::null(), + deleted_adjacent_command_: None, trim_is_a_roll_edit_: false, remove_block_from_graph_: true, - adjacent_orphaned_: false, + adjacent_entry_: None, } } @@ -132,7 +126,7 @@ impl BlockTrimCommand { /// `prepare`: compute the trim delta and whether an adjacent block is needed. pub fn prepare(&mut self) { // Store old length - self.old_length_ = block_length(self.block.clone()); + self.old_length_ = block_length(&self.block); // If the length isn't changing, set a flag to do nothing if self.old_length_ == self.new_length { @@ -144,40 +138,44 @@ impl BlockTrimCommand { // Positive when trimming shorter, negative when trimming longer self.trim_diff_ = self.old_length_ - self.new_length; - // Retrieve our adjacent block (or an empty handle if none) + // Retrieve our adjacent block (or None if there is none) self.adjacent_ = if self.mode == MovementMode::TrimIn { - block_previous(self.block.clone()) + block_previous(&self.block) } else { - block_next(self.block.clone()) + block_next(&self.block) }; // Ignore when trimming the out with no adjacent, because the user must // have trimmed the end of the last block in the track - self.needs_adjacent_ = self.mode == MovementMode::TrimIn || !self.adjacent_.is_null(); + self.needs_adjacent_ = self.mode == MovementMode::TrimIn || self.adjacent_.is_some(); if self.needs_adjacent_ { // If we're trimming shorter, we need an adjacent, so check if we have a // viable one - let mut adjacent_kind = 0; // OAKNODE_BLOCK_OTHER - if !self.adjacent_.is_null() { - // SAFETY: `adjacent_kind` is a valid out pointer. - let _ = - unsafe { oaknode_block_get_kind(self.adjacent_.clone(), &mut adjacent_kind) }; - } + let adjacent_kind = self.adjacent_.as_ref().map(block_kind).unwrap_or(BlockKind::Other); self.we_created_adjacent_ = self.trim_diff_ > Rational::new(0, 1) - && (self.adjacent_.is_null() || (adjacent_kind != 2 && !self.trim_is_a_roll_edit_)); + && (self.adjacent_.is_none() + || (adjacent_kind != BlockKind::Gap && !self.trim_is_a_roll_edit_)); if self.we_created_adjacent_ { // We shortened but don't have a viable adjacent to lengthen, so create - // one - // SAFETY: `oaknode_block_gap_create` returns a fresh owned handle. - self.adjacent_ = unsafe { oaknode_block_gap_create() }; - block_set_length_and_media_out(self.adjacent_.clone(), self.trim_diff_); - self.adjacent_orphaned_ = true; - } else { + // one filling exactly the space the trim freed: for a trim-in it + // spans [in - diff, in), for a trim-out [out, out + diff). The + // module stores positions on the block, so both the in point and + // the length are written explicitly. + self.adjacent_ = Some(block_gap_create(&self.track.project)); + if let Some(gap) = &self.adjacent_ { + if self.mode == MovementMode::TrimIn { + block_set_in(gap, block_in(&self.block) - self.trim_diff_); + } else { + block_set_in(gap, block_out(&self.block)); + } + block_set_length_and_media_in(gap, self.trim_diff_); + } + } else if let Some(adjacent) = &self.adjacent_ { // Determine if we're removing the adjacent self.we_removed_adjacent_ = - (block_length(self.adjacent_.clone()) + self.trim_diff_).is_null(); + (block_length(adjacent) + self.trim_diff_).is_null(); } } } @@ -189,57 +187,43 @@ impl BlockTrimCommand { } if self.mode == MovementMode::TrimIn { - block_set_length_and_media_in(self.block.clone(), self.new_length); + block_set_length_and_media_in(&self.block, self.new_length); } else { - block_set_length_and_media_out(self.block.clone(), self.new_length); + block_set_length_and_media_out(&self.block, self.new_length); } if self.needs_adjacent_ { if self.we_created_adjacent_ { // Add the adjacent and insert it - block_add_to_graph(self.adjacent_.clone(), self.track.clone()); - if self.mode == MovementMode::TrimIn { - track_insert_block_before( - self.track.clone(), - self.adjacent_.clone(), - self.block.clone(), - ); - } else { - // SAFETY: `track`/`adjacent_`/`block` are valid handles. - let _ = unsafe { - oaknode_track_insert_block_after( - self.track.clone(), - self.adjacent_.clone(), - self.block.clone(), - ) - }; - } - self.adjacent_orphaned_ = false; - } else if self.we_removed_adjacent_ { - // SAFETY: valid handles. - let _ = unsafe { - oaknode_track_ripple_remove_block(self.track.clone(), self.adjacent_.clone()) - }; - - // It no longer inputs/outputs anything, remove it - if self.remove_block_from_graph_ && block_can_be_removed(self.adjacent_.clone()) { - if self.deleted_adjacent_command_.is_null() { - self.deleted_adjacent_command_ = - create_and_run_block_remove_command(self.adjacent_.clone()); + if let Some(gap) = &self.adjacent_ { + block_add_to_graph(gap, self.adjacent_entry_.take()); + if self.mode == MovementMode::TrimIn { + track_insert_block_before(&self.track, gap, &self.block); } else { - // SAFETY: `deleted_adjacent_command_` is a valid command handle. - let _ = unsafe { - oakundo_command_redo_now(self.deleted_adjacent_command_.clone()) - }; + track_insert_block_after(&self.track, gap, Some(&self.block)); } } - } else { - let adjacent_length = block_length(self.adjacent_.clone()) + self.trim_diff_; + } else if self.we_removed_adjacent_ { + if let Some(adjacent) = &self.adjacent_ { + track_ripple_remove_block(&self.track, adjacent); + + // It no longer inputs/outputs anything, remove it + if self.remove_block_from_graph_ && block_can_be_removed(adjacent) { + if self.deleted_adjacent_command_.is_none() { + self.deleted_adjacent_command_ = + Some(create_and_run_block_remove_command(adjacent)); + } else if let Some(c) = self.deleted_adjacent_command_.as_mut() { + c.redo_now(); + } + } + } + } else if let Some(adjacent) = &self.adjacent_ { + let adjacent_length = block_length(adjacent) + self.trim_diff_; if self.mode == MovementMode::TrimIn { - block_set_length_and_media_out(self.adjacent_.clone(), adjacent_length); + block_set_length_and_media_out(adjacent, adjacent_length); } else { - block_set_length_and_media_in(self.adjacent_.clone(), adjacent_length); + block_set_length_and_media_in(adjacent, adjacent_length); } } } @@ -256,59 +240,45 @@ impl BlockTrimCommand { if self.needs_adjacent_ { if self.we_created_adjacent_ { // The adjacent is ours, just delete it - // SAFETY: valid handles. - let _ = unsafe { - oaknode_track_ripple_remove_block(self.track.clone(), self.adjacent_.clone()) - }; - block_remove_from_graph(self.adjacent_.clone(), self.track.clone()); - self.adjacent_orphaned_ = true; - } else { + if let Some(gap) = &self.adjacent_ { + track_ripple_remove_block(&self.track, gap); + if self.adjacent_entry_.is_none() { + self.adjacent_entry_ = block_remove_from_graph(gap); + } + } + } else if let Some(adjacent) = &self.adjacent_ { if self.we_removed_adjacent_ { - if !self.deleted_adjacent_command_.is_null() { + if let Some(c) = self.deleted_adjacent_command_.as_mut() { // We deleted the adjacent, restore it now - // SAFETY: `deleted_adjacent_command_` is a valid command handle. - let _ = unsafe { - oakundo_command_undo_now(self.deleted_adjacent_command_.clone()) - }; + c.undo_now(); } if self.mode == MovementMode::TrimIn { - track_insert_block_before( - self.track.clone(), - self.adjacent_.clone(), - self.block.clone(), - ); + track_insert_block_before(&self.track, adjacent, &self.block); } else { - // SAFETY: valid handles. - let _ = unsafe { - oaknode_track_insert_block_after( - self.track.clone(), - self.adjacent_.clone(), - self.block.clone(), - ) - }; + track_insert_block_after(&self.track, adjacent, Some(&self.block)); } } else { - let adjacent_length = block_length(self.adjacent_.clone()) - self.trim_diff_; + let adjacent_length = block_length(adjacent) - self.trim_diff_; if self.mode == MovementMode::TrimIn { - block_set_length_and_media_out(self.adjacent_.clone(), adjacent_length); + block_set_length_and_media_out(adjacent, adjacent_length); } else { - block_set_length_and_media_in(self.adjacent_.clone(), adjacent_length); + block_set_length_and_media_in(adjacent, adjacent_length); } } } } if self.mode == MovementMode::TrimIn { - block_set_length_and_media_in(self.block.clone(), self.old_length_); + block_set_length_and_media_in(&self.block, self.old_length_); } else { - block_set_length_and_media_out(self.block.clone(), self.old_length_); + block_set_length_and_media_out(&self.block, self.old_length_); } } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } @@ -330,9 +300,9 @@ impl Command for BlockTrimCommand { /// (timelineundopointer.h). pub struct TrackSlideCommand { /// Owning track (`track_`). - track: CHandle, + track: NodeRef, /// Blocks to slide (`blocks_`; non-empty for a valid command). - blocks: Vec, + blocks: Vec, /// Signed movement (`movement_`). movement: Rational, /// Whether `prepare` created the in adjacent (`we_created_in_adjacent_`). @@ -340,35 +310,32 @@ pub struct TrackSlideCommand { /// Whether the slide removes the in adjacent (`we_removed_in_adjacent_`). we_removed_in_adjacent_: bool, /// Block adjacent on the in side (`in_adjacent_`). - in_adjacent: CHandle, - /// Graph-removal command for a removed in adjacent (`in_adjacent_remove_command_`). - /// - /// Owns its handle like the C++ `OakUndoCommand`; `CHandle` is refcounted - /// and drops without a destructor, so releasing it is implicit. - in_adjacent_remove_command_: CHandle, - /// Whether `in_adjacent_` is a created gap still detached from the graph - /// (`in_adjacent_orphaned_`); a detached handle is owned by this command. - in_adjacent_orphaned_: bool, + in_adjacent: Option, + /// Graph-removal command for a removed in adjacent. + in_adjacent_remove_command_: Option, + /// Arena entry of a created in adjacent gap while detached from the graph. + in_adjacent_entry_: Option, /// Whether `prepare` created the out adjacent (`we_created_out_adjacent_`). we_created_out_adjacent_: bool, /// Whether the slide removes the out adjacent (`we_removed_out_adjacent_`). we_removed_out_adjacent_: bool, /// Block adjacent on the out side (`out_adjacent_`). - out_adjacent: CHandle, - /// Graph-removal command for a removed out adjacent (`out_adjacent_remove_command_`). - out_adjacent_remove_command_: CHandle, - /// Whether `out_adjacent_` is a created gap still detached from the graph - /// (`out_adjacent_orphaned_`); a detached handle is owned by this command. - out_adjacent_orphaned_: bool, + out_adjacent: Option, + /// Graph-removal command for a removed out adjacent. + out_adjacent_remove_command_: Option, + /// Arena entry of a created out adjacent gap while detached from the graph. + out_adjacent_entry_: Option, } impl TrackSlideCommand { /// Construct from track + moving blocks + in/out adjacent blocks + movement. + /// + /// New signature (single-lib): `pub fn new(track: NodeRef, blocks: Vec, in_adjacent: Option, out_adjacent: Option, movement: Rational) -> TrackSlideCommand` pub fn new( - track: CHandle, - blocks: Vec, - in_adjacent: CHandle, - out_adjacent: CHandle, + track: NodeRef, + blocks: Vec, + in_adjacent: Option, + out_adjacent: Option, movement: Rational, ) -> Self { Self { @@ -378,38 +345,44 @@ impl TrackSlideCommand { we_created_in_adjacent_: false, we_removed_in_adjacent_: false, in_adjacent, - in_adjacent_remove_command_: CHandle::null(), - in_adjacent_orphaned_: false, + in_adjacent_remove_command_: None, + in_adjacent_entry_: None, we_created_out_adjacent_: false, we_removed_out_adjacent_: false, out_adjacent, - out_adjacent_remove_command_: CHandle::null(), - out_adjacent_orphaned_: false, + out_adjacent_remove_command_: None, + out_adjacent_entry_: None, } } /// `prepare`: capture adjacent state. pub fn prepare(&mut self) { - if self.in_adjacent.is_null() { - // SAFETY: `oaknode_block_gap_create` returns a fresh owned handle. - self.in_adjacent = unsafe { oaknode_block_gap_create() }; - block_set_length_and_media_out(self.in_adjacent.clone(), self.movement); - self.in_adjacent_orphaned_ = true; + if self.in_adjacent.is_none() { + self.in_adjacent = Some(block_gap_create(&self.track.project)); + if let Some(gap) = &self.in_adjacent { + // A created in adjacent only makes sense when sliding left + // (movement < 0): the gap fills [in0 + movement, in0) ahead + // of the first block. Positions are stored on the block in + // the module model, so both ends are written explicitly. + block_set_in(gap, block_in(&self.blocks[0]) + self.movement); + block_set_length_and_media_in(gap, Rational::new(0, 1) - self.movement); + } self.we_created_in_adjacent_ = true; } else { self.we_created_in_adjacent_ = false; } - if self.out_adjacent.is_null() - && !block_next(self.blocks[self.blocks.len() - 1].clone()).is_null() + if self.out_adjacent.is_none() + && block_next(self.blocks.last().expect("non-empty blocks")).is_some() { - // SAFETY: `oaknode_block_gap_create` returns a fresh owned handle. - self.out_adjacent = unsafe { oaknode_block_gap_create() }; - block_set_length_and_media_out( - self.out_adjacent.clone(), - Rational::new(0, 1) - self.movement, - ); - self.out_adjacent_orphaned_ = true; + self.out_adjacent = Some(block_gap_create(&self.track.project)); + if let Some(gap) = &self.out_adjacent { + // A created out adjacent only makes sense when sliding right + // (movement > 0): the gap fills [last_out, last_out + movement) + // after the last block. + block_set_in(gap, block_out(self.blocks.last().expect("non-empty blocks"))); + block_set_length_and_media_in(gap, self.movement); + } self.we_created_out_adjacent_ = true; } else { self.we_created_out_adjacent_ = false; @@ -421,80 +394,68 @@ impl TrackSlideCommand { // We will always have an in adjacent if there was a valid slide if self.we_created_in_adjacent_ { // We created the in adjacent, so all we have to do is insert it - block_add_to_graph(self.in_adjacent.clone(), self.track.clone()); - // `blocks` is non-empty when the command was constructed validly - track_insert_block_before( - self.track.clone(), - self.in_adjacent.clone(), - self.blocks[0].clone(), - ); - self.in_adjacent_orphaned_ = false; - } else if Rational::new(0, 1) - self.movement == block_length(self.in_adjacent.clone()) { - // Movement will remove the in adjacent - // SAFETY: valid handles. - let _ = unsafe { - oaknode_track_ripple_remove_block(self.track.clone(), self.in_adjacent.clone()) - }; + if let Some(gap) = &self.in_adjacent { + block_add_to_graph(gap, self.in_adjacent_entry_.take()); + // `blocks` is non-empty when the command was constructed validly + track_insert_block_before(&self.track, gap, &self.blocks[0]); + } + } else if let Some(adjacent) = &self.in_adjacent { + if Rational::new(0, 1) - self.movement == block_length(adjacent) { + // Movement will remove the in adjacent + track_ripple_remove_block(&self.track, adjacent); - if block_can_be_removed(self.in_adjacent.clone()) { - if self.in_adjacent_remove_command_.is_null() { - self.in_adjacent_remove_command_ = - create_block_remove_command(self.in_adjacent.clone()); + if block_can_be_removed(adjacent) { + if self.in_adjacent_remove_command_.is_none() { + self.in_adjacent_remove_command_ = + Some(create_block_remove_command(adjacent)); + } + + if let Some(c) = self.in_adjacent_remove_command_.as_mut() { + c.redo_now(); + } } - // SAFETY: `in_adjacent_remove_command_` is a valid command handle. - let _ = - unsafe { oakundo_command_redo_now(self.in_adjacent_remove_command_.clone()) }; + self.we_removed_in_adjacent_ = true; + } else { + // Simply resize the adjacent + block_set_length_and_media_out( + adjacent, + block_length(adjacent) + self.movement, + ); } - - self.we_removed_in_adjacent_ = true; - } else { - // Simply resize the adjacent - block_set_length_and_media_out( - self.in_adjacent.clone(), - block_length(self.in_adjacent.clone()) + self.movement, - ); } // We may not have an out adjacent if the slide was at the end of the track - if !self.out_adjacent.is_null() { + if let Some(adjacent) = &self.out_adjacent { if self.we_created_out_adjacent_ { // We created the out adjacent, so we just have to insert it - block_add_to_graph(self.out_adjacent.clone(), self.track.clone()); - // SAFETY: valid handles. - let _ = unsafe { - oaknode_track_insert_block_after( - self.track.clone(), - self.out_adjacent.clone(), - self.blocks[self.blocks.len() - 1].clone(), - ) - }; - self.out_adjacent_orphaned_ = false; - } else if self.movement == block_length(self.out_adjacent.clone()) { + block_add_to_graph(adjacent, self.out_adjacent_entry_.take()); + track_insert_block_after( + &self.track, + adjacent, + Some(self.blocks.last().expect("non-empty blocks")), + ); + } else if self.movement == block_length(adjacent) { // Movement will remove the out adjacent - // SAFETY: valid handles. - let _ = unsafe { - oaknode_track_ripple_remove_block(self.track.clone(), self.out_adjacent.clone()) - }; + track_ripple_remove_block(&self.track, adjacent); - if block_can_be_removed(self.out_adjacent.clone()) { - if self.out_adjacent_remove_command_.is_null() { + if block_can_be_removed(adjacent) { + if self.out_adjacent_remove_command_.is_none() { self.out_adjacent_remove_command_ = - create_block_remove_command(self.out_adjacent.clone()); + Some(create_block_remove_command(adjacent)); } - // SAFETY: `out_adjacent_remove_command_` is a valid command handle. - let _ = unsafe { - oakundo_command_redo_now(self.out_adjacent_remove_command_.clone()) - }; + if let Some(c) = self.out_adjacent_remove_command_.as_mut() { + c.redo_now(); + } } self.we_removed_out_adjacent_ = true; } else { // Simply resize the adjacent block_set_length_and_media_in( - self.out_adjacent.clone(), - block_length(self.out_adjacent.clone()) - self.movement, + adjacent, + block_length(adjacent) - self.movement, ); } } @@ -504,72 +465,60 @@ impl TrackSlideCommand { pub fn undo(&mut self) { if self.we_created_in_adjacent_ { // We created this, so we can remove it now - // SAFETY: valid handles. - let _ = unsafe { - oaknode_track_ripple_remove_block(self.track.clone(), self.in_adjacent.clone()) - }; - block_remove_from_graph(self.in_adjacent.clone(), self.track.clone()); - self.in_adjacent_orphaned_ = true; - } else if self.we_removed_in_adjacent_ { - if !self.in_adjacent_remove_command_.is_null() { - // We removed this, so we can restore it now - // SAFETY: `in_adjacent_remove_command_` is a valid command handle. - let _ = - unsafe { oakundo_command_undo_now(self.in_adjacent_remove_command_.clone()) }; + if let Some(gap) = &self.in_adjacent { + track_ripple_remove_block(&self.track, gap); + if self.in_adjacent_entry_.is_none() { + self.in_adjacent_entry_ = block_remove_from_graph(gap); + } } - - // `blocks` is non-empty when the command was constructed validly - track_insert_block_before( - self.track.clone(), - self.in_adjacent.clone(), - self.blocks[0].clone(), - ); - } else { - // Simply resize the adjacent - block_set_length_and_media_out( - self.in_adjacent.clone(), - block_length(self.in_adjacent.clone()) - self.movement, - ); - } - - if !self.out_adjacent.is_null() { - if self.we_created_out_adjacent_ { - // We created this, so we can remove it now - // SAFETY: valid handles. - let _ = unsafe { - oaknode_track_ripple_remove_block(self.track.clone(), self.out_adjacent.clone()) - }; - block_remove_from_graph(self.out_adjacent.clone(), self.track.clone()); - self.out_adjacent_orphaned_ = true; - } else if self.we_removed_out_adjacent_ { - if !self.out_adjacent_remove_command_.is_null() { + } else if let Some(adjacent) = &self.in_adjacent { + if self.we_removed_in_adjacent_ { + if let Some(c) = self.in_adjacent_remove_command_.as_mut() { // We removed this, so we can restore it now - // SAFETY: `out_adjacent_remove_command_` is a valid command handle. - let _ = unsafe { - oakundo_command_undo_now(self.out_adjacent_remove_command_.clone()) - }; + c.undo_now(); } - // SAFETY: valid handles. - let _ = unsafe { - oaknode_track_insert_block_after( - self.track.clone(), - self.out_adjacent.clone(), - self.blocks[self.blocks.len() - 1].clone(), - ) - }; + // `blocks` is non-empty when the command was constructed validly + track_insert_block_before(&self.track, adjacent, &self.blocks[0]); + } else { + // Simply resize the adjacent + block_set_length_and_media_out( + adjacent, + block_length(adjacent) - self.movement, + ); + } + } + + if let Some(adjacent) = &self.out_adjacent { + if self.we_created_out_adjacent_ { + // We created this, so we can remove it now + track_ripple_remove_block(&self.track, adjacent); + if self.out_adjacent_entry_.is_none() { + self.out_adjacent_entry_ = block_remove_from_graph(adjacent); + } + } else if self.we_removed_out_adjacent_ { + if let Some(c) = self.out_adjacent_remove_command_.as_mut() { + // We removed this, so we can restore it now + c.undo_now(); + } + + track_insert_block_after( + &self.track, + adjacent, + Some(self.blocks.last().expect("non-empty blocks")), + ); } else { // Simply resize the adjacent block_set_length_and_media_in( - self.out_adjacent.clone(), - block_length(self.out_adjacent.clone()) + self.movement, + adjacent, + block_length(adjacent) + self.movement, ); } } } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } @@ -591,18 +540,17 @@ impl Command for TrackSlideCommand { /// extends past the end of the sequence (timelineundopointer.h). pub struct TrackPlaceBlockCommand { /// Track list (`timeline_`). - timeline: CHandle, + timeline: NodeRef, /// Target track index (`track_index_`). track_index: i32, /// Placement in point (`in_`). in_: Rational, /// Gap inserted when the block extends past the end of the sequence (`gap_`). - gap_: CHandle, - /// Whether `gap_` is detached from the graph (`gap_orphaned_`); a detached - /// handle is owned by this command. - gap_orphaned_: bool, + gap_: Option, + /// Arena entry of `gap_` while detached from the graph. + gap_entry_: Option, /// Block to place (C++ `insert_`). - block: CHandle, + block: NodeRef, /// Track-add commands created when the track index is out of range /// (`add_track_commands_`). add_track_commands_: Vec, @@ -612,13 +560,15 @@ pub struct TrackPlaceBlockCommand { impl TrackPlaceBlockCommand { /// Construct from track list + track index + block + in point. - pub fn new(timeline: CHandle, track_index: i32, block: CHandle, in_: Rational) -> Self { + /// + /// New signature (single-lib): `pub fn new(timeline: NodeRef, track_index: i32, block: NodeRef, in_: Rational) -> TrackPlaceBlockCommand` + pub fn new(timeline: NodeRef, track_index: i32, block: NodeRef, in_: Rational) -> Self { Self { timeline, track_index, in_, - gap_: CHandle::null(), - gap_orphaned_: false, + gap_: None, + gap_entry_: None, block, add_track_commands_: Vec::new(), ripple_remove_command_: None, @@ -628,10 +578,7 @@ impl TrackPlaceBlockCommand { /// `redo`: place the block destructively. pub fn redo(&mut self) { // Determine if we need to add tracks - let mut track_count = 0; - // SAFETY: `track_count` is a valid out pointer. - let _ = - unsafe { oaknode_tracklist_get_track_count(self.timeline.clone(), &mut track_count) }; + let track_count = tracklist_track_count(&self.timeline) as i32; if self.track_index >= track_count { if self.add_track_commands_.is_empty() { @@ -647,41 +594,37 @@ impl TrackPlaceBlockCommand { } } - let mut track = CHandle::null(); - // SAFETY: `track` is a valid out pointer. - let _ = unsafe { - oaknode_tracklist_get_track_at(self.timeline.clone(), self.track_index, &mut track) + let Some(track) = tracklist_track_at(&self.timeline, self.track_index as usize) else { + return; }; let in_ = self.in_; - let append = in_ >= track_length(track.clone()); + let append = in_ >= track_length(&track); // Check if the placement location is past the end of the timeline if append { - if in_ > track_length(track.clone()) { - // If so, insert a gap here. In the module world the gap's in point - // is stored on the gap (Olive derives positions from the track - // order), so it must be length-set IN-anchored — an out-anchored - // `set_length_and_media_out` on the fresh (in = 0) gap would push - // the in point negative. - if self.gap_.is_null() { - // SAFETY: `oaknode_block_gap_create` returns a fresh owned handle. - self.gap_ = unsafe { oaknode_block_gap_create() }; - block_set_length_and_media_in( - self.gap_.clone(), - in_ - track_length(track.clone()), - ); + if in_ > track_length(&track) { + // If so, insert a gap filling [track end, in_): the module + // stores the gap's span on the block, so both ends are + // written explicitly. + if self.gap_.is_none() { + self.gap_ = Some(block_gap_create(&self.timeline.project)); + if let Some(gap) = &self.gap_ { + block_set_in(gap, track_length(&track)); + block_set_length_and_media_in(gap, in_ - track_length(&track)); + } + } + if let Some(gap) = &self.gap_ { + block_add_to_graph(gap, self.gap_entry_.take()); + track_append_block(&track, gap); } - block_add_to_graph(self.gap_.clone(), track.clone()); - track_append_block(track.clone(), self.gap_.clone()); - self.gap_orphaned_ = false; } - track_append_block(track, self.block.clone()); + track_append_block(&track, &self.block); } else { // Place the block at this point if self.ripple_remove_command_.is_none() { - let insert_length = block_length(self.block.clone()); + let insert_length = block_length(&self.block); let mut cmd = TrackRippleRemoveAreaCommand::new( track.clone(), TimeRange::new(in_, in_ + insert_length), @@ -694,39 +637,33 @@ impl TrackPlaceBlockCommand { cmd.prepare(); cmd.redo(); - // Insert after the block that follows the cleared area (`blocks` is - // non-empty when the command was constructed validly) + // Insert after the block that follows the cleared area let index = self .ripple_remove_command_ .as_ref() .unwrap() .get_insertion_index(); - // SAFETY: valid handles; `index` may be empty when the insertion is at - // the front of the track, which the ABI tolerates like C++. - let _ = unsafe { oaknode_track_insert_block_after(track, self.block.clone(), index) }; + track_insert_block_after(&track, &self.block, index.as_ref()); } } /// `undo`: remove the placed block and restore displaced blocks. pub fn undo(&mut self) { - let mut t = CHandle::null(); - // SAFETY: `t` is a valid out pointer. - let _ = unsafe { - oaknode_tracklist_get_track_at(self.timeline.clone(), self.track_index, &mut t) + let Some(t) = tracklist_track_at(&self.timeline, self.track_index as usize) else { + return; }; // Firstly, remove our insert - // SAFETY: valid handles. - let _ = unsafe { oaknode_track_ripple_remove_block(t.clone(), self.block.clone()) }; + track_ripple_remove_block(&t, &self.block); if self.ripple_remove_command_.is_some() { // If we ripple-removed, just undo that self.ripple_remove_command_.as_mut().unwrap().undo(); - } else if !self.gap_.is_null() { - // SAFETY: valid handles. - let _ = unsafe { oaknode_track_ripple_remove_block(t.clone(), self.gap_.clone()) }; - block_remove_from_graph(self.gap_.clone(), t.clone()); - self.gap_orphaned_ = true; + } else if let Some(gap) = &self.gap_ { + track_ripple_remove_block(&t, gap); + if self.gap_entry_.is_none() { + self.gap_entry_ = block_remove_from_graph(gap); + } } // Remove tracks if we added them @@ -735,8 +672,8 @@ impl TrackPlaceBlockCommand { } } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } @@ -768,7 +705,7 @@ pub struct TrackMoveBlockCommand { /// Composite of the gap + place halves (`children_`). children: MultiUndoCommand, /// The block being moved (`block_`). - block: CHandle, + block: NodeRef, /// In point captured at construction (`old_in_`). old_in: Rational, /// Destination in point (`in_`). @@ -777,29 +714,34 @@ pub struct TrackMoveBlockCommand { impl TrackMoveBlockCommand { /// Construct from track list + destination track index + block + in point. - pub fn new(timeline: CHandle, track_index: i32, block: CHandle, in_: Rational) -> Self { + /// + /// New signature (single-lib): `pub fn new(timeline: NodeRef, track_index: i32, block: NodeRef, in_: Rational) -> TrackMoveBlockCommand` + pub fn new(timeline: NodeRef, track_index: i32, block: NodeRef, in_: Rational) -> Self { let mut children = MultiUndoCommand::new(); - children.add_child(Box::new(TrackReplaceBlockWithGapCommand::new( - block_track(block.clone()), - block.clone(), - true, - ))); + if let Some(track) = block_track(&block) { + children.add_child(Box::new(TrackReplaceBlockWithGapCommand::new( + track, + block.clone(), + true, + ))); + } children.add_child(Box::new(TrackPlaceBlockCommand::new( timeline, track_index, block.clone(), in_, ))); + let old_in = block_in(&block); Self { children, block, - old_in: block_in(block.clone()), + old_in, in_, } } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } @@ -809,13 +751,13 @@ impl Command for TrackMoveBlockCommand { fn redo(&mut self) { // The module stores the block's position on the block itself (no // track-order derivation), so re-home it before placing. - block_set_in(self.block.clone(), self.in_); + block_set_in(&self.block, self.in_); self.children.redo(); } /// `Command::undo` — the composite runs children in reverse. fn undo(&mut self) { self.children.undo(); - block_set_in(self.block.clone(), self.old_in); + block_set_in(&self.block, self.old_in); } } diff --git a/crates/oaktimeline/src/undoripple.rs b/crates/oaktimeline/src/undoripple.rs index c6387d685..a244df3c9 100644 --- a/crates/oaktimeline/src/undoripple.rs +++ b/crates/oaktimeline/src/undoripple.rs @@ -17,56 +17,41 @@ //! Ripple commands (`src/timeline/src/timelineundoripple.h`): clearing a time //! area on one or all tracks, the ripple tool, and gap deletion at regions. //! -//! Graph mutation goes through the oaknode C ABI (`bridge::node`); command -//! wrapping through `bridge::undo`. The C++ `std::map<..., TrackHandleLess>` -//! containers are modelled as order-preserving `Vec<(CHandle, _)>` pairs; -//! order follows the track handle, matching `TrackHandleLess`. +//! Graph mutation routes directly through the oaknode Rust domain (the +//! oaknode C ABI was deleted in the single-lib unification); command +//! wrapping through `oakundo::undocommand::UndoCommand` values. The C++ +//! `std::map<..., TrackHandleLess>` containers are modelled as +//! order-preserving `Vec<(NodeRef, _)>` pairs; order follows the track +//! `NodeId`, matching `TrackHandleLess`. //! //! The oaknode queries a `prepare()` needs — locating blocks at a time, //! detecting gaps, enumerating track lists / sequence tracks and reading the -//! locked flag — go through the matching `bridge::node` entry points, wrapped -//! by the thin private helpers below (each annotated with its `// CPP-PARITY` +//! locked flag — go through the matching `crate::util` helpers, wrapped by +//! the thin private helpers below (each annotated with its `// CPP-PARITY` //! source line). use oakcore_rs::{Rational, TimeRange}; +use oaknode::graph::NodeEntry; +use oakundo::undocommand::UndoCommand; -use crate::bridge::node::{ - oaknode_block_gap_create, oaknode_block_get_kind, oaknode_sequence_get_all_track_at, - oaknode_sequence_get_all_track_count, oaknode_sequence_get_track_list, - oaknode_track_get_locked, oaknode_track_get_nearest_block_after_or_at, - oaknode_track_get_nearest_block_before_or_at, oaknode_track_insert_block_after, - oaknode_track_prepend_block, oaknode_track_ripple_remove_block, oaknode_tracklist_get_track_at, - oaknode_tracklist_get_track_count, -}; -use crate::bridge::undo::{oakundo_command_redo_now, oakundo_command_undo_now}; use crate::common::MovementMode; -use crate::handle::CHandle; use crate::undocommon::{ - block_can_be_removed, box_command, create_block_remove_command, free_command_handle, Command, + block_can_be_removed, box_command, create_block_remove_command, Command, }; use crate::util::{ - block_add_to_graph, block_in, block_length, block_next, block_out, block_previous, - block_remove_from_graph, block_set_length_and_media_in, block_set_length_and_media_out, - block_track, free_detached_handle, rat_nd, + block_add_to_graph, block_gap_create, block_in, block_kind, block_length, block_next, + block_out, block_previous, block_remove_from_graph, block_set_in, + block_set_length_and_media_in, block_set_length_and_media_out, block_track, + sequence_all_tracks, sequence_track_list, + track_insert_block_after, track_locked, track_nearest_block_after_or_at, + track_nearest_block_before_or_at, track_prepend_block, track_ripple_remove_block, + tracklist_track_at, tracklist_track_count, BlockKind, NodeRef, }; use super::undogeneral::BlockResizeCommand; use super::undosplit::BlockSplitCommand; use super::undotrack::TrackRippleRemoveBlockCommand; -/// Copy a handle's token so it can be re-passed by value to a bridge -/// function while the original stays stored. `CHandle` is a `#[repr(C)]` POD -/// without a `Drop`, so the copy is just a struct copy; refcounts are not -/// touched (the bridge only ever re-boxes/re-releases the object it backs). -fn hdup(h: &CHandle) -> CHandle { - CHandle { - ctx: h.ctx, - addref: h.addref, - release: h.release, - abi_version: h.abi_version, - } -} - /// C++ unary `Rational::operator-` (`Rational(num_, -den_)`); the crate's /// `Rational` has no `Neg`, so negate by subtracting from zero (same idiom as /// `marker.rs`). @@ -75,104 +60,62 @@ fn rat_neg(r: Rational) -> Rational { } // The helpers below mirror the oaknode C ABI queries the C++ `prepare()` -// bodies use. Each wraps a `bridge::node` extern (see the `// CPP-PARITY` -// marker on each). +// bodies use. Each wraps a `crate::util` oaknode-domain query (see the +// `// CPP-PARITY` marker on each). /// C++ `oaknode_track_get_nearest_block_before_or_at`. // CPP-PARITY timelineundoripple.cpp:64 -fn nearest_block_before_or_at(track: CHandle, t: Rational) -> CHandle { - let (mut n, mut d) = (0, 0); - rat_nd(t, &mut n, &mut d); - let mut out = CHandle::null(); - // SAFETY: `out` is a valid out pointer. - let _ = unsafe { oaknode_track_get_nearest_block_before_or_at(track, n, d, &mut out) }; - out +fn nearest_block_before_or_at(track: &NodeRef, t: Rational) -> Option { + track_nearest_block_before_or_at(track, t) } /// C++ `oaknode_track_get_nearest_block_after_or_at`. // CPP-PARITY timelineundoripple.cpp:551 -fn nearest_block_after_or_at(track: CHandle, t: Rational) -> CHandle { - let (mut n, mut d) = (0, 0); - rat_nd(t, &mut n, &mut d); - let mut out = CHandle::null(); - // SAFETY: `out` is a valid out pointer. - let _ = unsafe { oaknode_track_get_nearest_block_after_or_at(track, n, d, &mut out) }; - out +fn nearest_block_after_or_at(track: &NodeRef, t: Rational) -> Option { + track_nearest_block_after_or_at(track, t) } /// C++ anonymous-namespace `is_gap` (`oaknode_block_get_kind`). // CPP-PARITY timelineundoripple.cpp:448 -fn is_gap(b: CHandle) -> bool { - let mut kind = 0; - // SAFETY: `kind` is a valid out pointer. - if unsafe { oaknode_block_get_kind(b, &mut kind) } != 0 { - return false; - } - kind == 2 // OAKNODE_BLOCK_GAP +fn is_gap(b: &NodeRef) -> bool { + block_kind(b) == BlockKind::Gap } /// C++ `oaknode_tracklist_get_track_count`. // CPP-PARITY timelineundoripple.cpp:211 -fn track_list_count(list: CHandle) -> usize { - let mut count = 0; - // SAFETY: `count` is a valid out pointer. - let _ = unsafe { oaknode_tracklist_get_track_count(list, &mut count) }; - count as usize +fn track_list_count(list: &NodeRef) -> usize { + tracklist_track_count(list) } /// C++ `oaknode_tracklist_get_track_at`. // CPP-PARITY timelineundoripple.cpp:215 -fn track_list_at(list: CHandle, index: usize) -> CHandle { - let mut out = CHandle::null(); - // SAFETY: `out` is a valid out pointer. - let _ = unsafe { oaknode_tracklist_get_track_at(list, index as i32, &mut out) }; - out +fn track_list_at(list: &NodeRef, index: usize) -> Option { + tracklist_track_at(list, index) } /// C++ `oaknode_track_get_locked`. // CPP-PARITY timelineundoripple.cpp:221 -fn track_locked(track: CHandle) -> bool { - let mut locked = 0; - // SAFETY: `locked` is a valid out pointer. - let _ = unsafe { oaknode_track_get_locked(track, &mut locked) }; - locked != 0 +fn track_is_locked(track: &NodeRef) -> bool { + track_locked(track) } /// C++ `TimelineRippleRemoveAreaCommand` ctor loop over /// `oaknode_sequence_get_track_list` (one list per track type). // CPP-PARITY timelineundoripple.cpp:254 -fn sequence_track_lists(timeline: CHandle) -> Vec { - // OAKNODE_TRACK_TYPE_VIDEO / AUDIO / SUBTITLE. +fn sequence_track_lists(timeline: &NodeRef) -> Vec { let mut lists = Vec::new(); - for t in [0, 1, 2] { - let mut out = CHandle::null(); - // SAFETY: `out` is a valid out pointer. - let _ = unsafe { oaknode_sequence_get_track_list(hdup(&timeline), t, &mut out) }; - if !out.is_null() { - lists.push(out); + for kind in [ + oaknode::track::TrackType::Video, + oaknode::track::TrackType::Audio, + oaknode::track::TrackType::Subtitle, + ] { + if let Some(list) = sequence_track_list(timeline, kind) { + lists.push(list); } } lists } -/// C++ `oaknode_sequence_get_all_track_count` / `get_all_track_at`. -// CPP-PARITY timelineundoripple.cpp:523 -fn sequence_all_tracks(timeline: CHandle) -> Vec { - let mut count = 0; - // SAFETY: `count` is a valid out pointer. - let _ = unsafe { oaknode_sequence_get_all_track_count(hdup(&timeline), &mut count) }; - let mut tracks = Vec::new(); - for i in 0..count { - let mut out = CHandle::null(); - // SAFETY: `out` is a valid out pointer. - let _ = unsafe { oaknode_sequence_get_all_track_at(hdup(&timeline), i, &mut out) }; - if !out.is_null() { - tracks.push(out); - } - } - tracks -} - /// `TrackRippleRemoveAreaCommand` — clear the area between `range.in` and /// `range.out` on a single track (timelineundoripple.h). Blocks are trimmed and /// removed; subsequent blocks push backward unless a block is inserted at the @@ -183,10 +126,10 @@ fn sequence_all_tracks(timeline: CHandle) -> Vec { /// that derived state. pub struct TrackRippleRemoveAreaCommand { /// Owning track. - track: CHandle, + track: NodeRef, /// Area to clear. range: TimeRange, - /// Whether `prepare` has run (the C ABI command path never calls + /// Whether `prepare` has run (the vtable command path never calls /// `prepare()` itself, so `redo` derives the operations on first use). prepared: bool, /// Out-point trim on the first block (`timelineundoripple.h` `trim_out_`). @@ -196,19 +139,19 @@ pub struct TrackRippleRemoveAreaCommand { /// In-point trim on the trailing block (`trim_in_`). trim_in_: Option, /// Block any replacement should be inserted after (`insert_previous_`). - insert_previous_: CHandle, + insert_previous_: Option, /// Whether a gap spanning the range may be split (`allow_splitting_gaps_`). allow_splitting_gaps_: bool, /// Split command used when the range cuts through a block (`splice_split_command_`). splice_split_command_: Option, /// Node-remove sub-commands for removed blocks (`remove_block_commands_`). - remove_block_commands_: Vec, + remove_block_commands_: Vec, } /// One block trimmed at its out or in edge (C++ `TrimOperation`). struct TrimOperation { /// The trimmed block. - block: CHandle, + block: NodeRef, /// Length before the trim. old_length: Rational, /// Length after the trim. @@ -219,14 +162,16 @@ struct TrimOperation { /// (C++ `RemoveOperation`). struct RemoveOperation { /// The removed block. - block: CHandle, - /// Predecessor to re-insert after on undo. - before: CHandle, + block: NodeRef, + /// Predecessor to re-insert after on undo (`None` = front of the track). + before: Option, } impl TrackRippleRemoveAreaCommand { /// Construct from track + range. - pub fn new(track: CHandle, range: TimeRange) -> Self { + /// + /// New signature (single-lib): `pub fn new(track: NodeRef, range: TimeRange) -> TrackRippleRemoveAreaCommand` + pub fn new(track: NodeRef, range: TimeRange) -> Self { Self { track, range, @@ -234,7 +179,7 @@ impl TrackRippleRemoveAreaCommand { trim_out_: None, removals_: Vec::new(), trim_in_: None, - insert_previous_: CHandle::null(), + insert_previous_: None, allow_splitting_gaps_: false, splice_split_command_: None, remove_block_commands_: Vec::new(), @@ -242,17 +187,21 @@ impl TrackRippleRemoveAreaCommand { } /// The block that will follow the cleared area, for inserting a replacement - /// (`get_insertion_index`); the empty handle when not applicable. - pub fn get_insertion_index(&self) -> CHandle { - hdup(&self.insert_previous_) + /// (`get_insertion_index`); `None` when not applicable. + /// + /// New signature (single-lib): `pub fn get_insertion_index(&self) -> Option` + pub fn get_insertion_index(&self) -> Option { + self.insert_previous_.clone() } /// The block produced by the splice split, if the range split a block - /// (`get_spliced_block`); the empty handle when nothing was split. - pub fn get_spliced_block(&self) -> CHandle { + /// (`get_spliced_block`); `None` when nothing was split. + /// + /// New signature (single-lib): `pub fn get_spliced_block(&self) -> Option` + pub fn get_spliced_block(&self) -> Option { match &self.splice_split_command_ { Some(cmd) => cmd.new_block(), - None => CHandle::null(), + None => None, } } @@ -265,110 +214,107 @@ impl TrackRippleRemoveAreaCommand { /// `prepare`: compute the trim/remove operations for the range. /// /// Mirrors the C++ algorithm (`oaknode_track_get_nearest_block_before_or_at` - /// is exposed by the oaknode bridge; `redo` invokes this on first use - /// because the C ABI command path never calls `prepare` itself). + /// is a direct oaknode-domain query; `redo` invokes this on first use + /// because the vtable command path never calls `prepare` itself). pub fn prepare(&mut self) { // Idempotent: recompute from the current track state, discarding any // previously derived operations. self.trim_out_ = None; self.removals_.clear(); self.trim_in_ = None; - self.insert_previous_ = CHandle::null(); + self.insert_previous_ = None; self.splice_split_command_ = None; - let track = hdup(&self.track); + let track = self.track.clone(); let in_ = self.range.in_(); let out = self.range.out(); // CPP-PARITY timelineundoripple.cpp:57-64 - let first_block = nearest_block_before_or_at(track, in_); - if first_block.is_null() { + let Some(first_block) = nearest_block_before_or_at(&track, in_) else { return; - } - let fb = hdup(&first_block); + }; + let fb = first_block; // Determine if this first block is getting trimmed or removed - let first_block_is_out_trimmed = block_in(hdup(&fb)) < in_; - let first_block_is_in_trimmed = block_out(hdup(&fb)) > out; + let first_block_is_out_trimmed = block_in(&fb) < in_; + let first_block_is_in_trimmed = block_out(&fb) > out; // Set's the block that any insert command should insert AFTER. self.insert_previous_ = if first_block_is_out_trimmed { - hdup(&fb) + Some(fb.clone()) } else { - block_previous(hdup(&fb)) + block_previous(&fb) }; // If it's getting trimmed, determine if it's actually getting spliced if first_block_is_out_trimmed && first_block_is_in_trimmed { - if !self.allow_splitting_gaps_ && is_gap(hdup(&fb)) { + if !self.allow_splitting_gaps_ && is_gap(&fb) { // As a rule, we don't split gaps, so we just treat it as a // trim of the range requested self.trim_out_ = Some(TrimOperation { - block: hdup(&fb), - old_length: block_length(hdup(&fb)), - new_length: block_length(hdup(&fb)) - self.range.length(), + block: fb.clone(), + old_length: block_length(&fb), + new_length: block_length(&fb) - self.range.length(), }); } else { // This block is getting spliced, so we'll handle that later - self.splice_split_command_ = - Some(BlockSplitCommand::new(hdup(&fb), self.range.in_())); + self.splice_split_command_ = Some(BlockSplitCommand::new(fb.clone(), in_)); } } else { // It's just getting trimmed or removed, so we'll append that operation if first_block_is_out_trimmed { self.trim_out_ = Some(TrimOperation { - block: hdup(&fb), - old_length: block_length(hdup(&fb)), - new_length: block_length(hdup(&fb)) - (block_out(hdup(&fb)) - in_), + block: fb.clone(), + old_length: block_length(&fb), + new_length: block_length(&fb) - (block_out(&fb) - in_), }); } else if first_block_is_in_trimmed { self.trim_in_ = Some(TrimOperation { - block: hdup(&fb), - old_length: block_length(hdup(&fb)), - new_length: block_length(hdup(&fb)) - (out - block_in(hdup(&fb))), + block: fb.clone(), + old_length: block_length(&fb), + new_length: block_length(&fb) - (out - block_in(&fb)), }); } else { // We know for sure this block is within the range so it will be removed self.removals_.push(RemoveOperation { - block: hdup(&fb), - before: block_previous(hdup(&fb)), + block: fb.clone(), + before: block_previous(&fb), }); } // If the first block is getting in trimmed, we're already at the // end of our range if !first_block_is_in_trimmed { - let mut next = block_next(hdup(&fb)); - while !next.is_null() { - let nx = hdup(&next); + let mut next = block_next(&fb); + while let Some(nx) = next { // The module world allows gaps BETWEEN blocks (their // in/out points are stored, unlike Olive's contiguous // track ordering), so a successor starting at/after the // region does not overlap it and must not be touched. - if block_in(hdup(&nx)) >= out { + if block_in(&nx) >= out { break; } - let trimming = block_out(hdup(&nx)) > out; + let trimming = block_out(&nx) > out; if trimming { self.trim_in_ = Some(TrimOperation { - block: hdup(&nx), - old_length: block_length(hdup(&nx)), - new_length: block_length(hdup(&nx)) - (out - block_in(hdup(&nx))), + block: nx.clone(), + old_length: block_length(&nx), + new_length: block_length(&nx) - (out - block_in(&nx)), }); break; } else { self.removals_.push(RemoveOperation { - block: hdup(&nx), - before: block_previous(hdup(&nx)), + block: nx.clone(), + before: block_previous(&nx), }); - if block_out(hdup(&nx)) == out { + if block_out(&nx) == out { break; } } - next = block_next(hdup(&nx)); + next = block_next(&nx); } } } @@ -378,8 +324,8 @@ impl TrackRippleRemoveAreaCommand { /// `redo`: apply the ripple removal. pub fn redo(&mut self) { - // The C ABI command path never invokes `prepare()` (the oakundo - // vtable wrapper only dispatches redo/undo), so derive the operations + // The vtable command path never invokes `prepare()` (the oakundo + // wrapper only dispatches redo/undo), so derive the operations // on first use. if !self.prepared { self.prepare(); @@ -387,54 +333,49 @@ impl TrackRippleRemoveAreaCommand { if self.splice_split_command_.is_some() { // We're just splicing (C++ `redo_now` = prepare + redo) let cmd = self.splice_split_command_.as_mut().unwrap(); - cmd.prepare(); cmd.redo(); - // Trim the in of the split (the module's `BlockSplitCommand` - // deviation anchors the halves at the original out / fresh in, - // so the in-end trim keeps the split's stored in point). - let split = cmd.new_block(); - if !split.is_null() { + // Trim the in of the split (the second half produced by the + // split keeps the original out point; the in-end trim shifts + // its stored in point to the range out — the module equivalent + // of the C++ ripple shifting the remainder earlier). + if let Some(split) = cmd.new_block() { let new_len = - block_length(split.clone()) - (self.range.out() - block_in(split.clone())); - block_set_length_and_media_in(split, new_len); + block_length(&split) - (self.range.out() - block_in(&split)); + block_set_length_and_media_in(&split, new_len); } } else { if let Some(t) = &self.trim_out_ { // An out-end trim keeps the in point (in-anchored; the // C++ setter name is kept, but the module's in/out are // stored values, see the splice trim above). - block_set_length_and_media_in(hdup(&t.block), t.new_length); + block_set_length_and_media_in(&t.block, t.new_length); } if let Some(t) = &self.trim_in_ { // An in-end trim keeps the out point (out-anchored). - block_set_length_and_media_out(hdup(&t.block), t.new_length); + block_set_length_and_media_out(&t.block, t.new_length); } // Perform removals if !self.removals_.is_empty() { - let track = hdup(&self.track); for op in &self.removals_ { // Ripple remove them all first - let _ = unsafe { - oaknode_track_ripple_remove_block(track.clone(), hdup(&op.block)) - }; + track_ripple_remove_block(&self.track, &op.block); } // Create undo commands for node removals where possible if self.remove_block_commands_.is_empty() { for op in &self.removals_ { - if block_can_be_removed(hdup(&op.block)) { + if block_can_be_removed(&op.block) { self.remove_block_commands_ - .push(create_block_remove_command(hdup(&op.block))); + .push(create_block_remove_command(&op.block)); } } } for i in 0..self.remove_block_commands_.len() { - let c = hdup(&self.remove_block_commands_[i]); - let _ = unsafe { oakundo_command_redo_now(c) }; + self.remove_block_commands_[i].redo_now(); } } } @@ -448,35 +389,27 @@ impl TrackRippleRemoveAreaCommand { } else { if let Some(t) = &self.trim_out_ { // In-anchored, matching the redo (see above). - block_set_length_and_media_in(hdup(&t.block), t.old_length); + block_set_length_and_media_in(&t.block, t.old_length); } if let Some(t) = &self.trim_in_ { // Out-anchored, matching the redo (see above). - block_set_length_and_media_out(hdup(&t.block), t.old_length); + block_set_length_and_media_out(&t.block, t.old_length); } // Un-remove any blocks for i in (0..self.remove_block_commands_.len()).rev() { - let c = hdup(&self.remove_block_commands_[i]); - let _ = unsafe { oakundo_command_undo_now(c) }; + self.remove_block_commands_[i].undo_now(); } - let track = hdup(&self.track); for op in &self.removals_ { - let _ = unsafe { - oaknode_track_insert_block_after( - track.clone(), - hdup(&op.block), - hdup(&op.before), - ) - }; + track_insert_block_after(&self.track, &op.block, op.before.as_ref()); } } } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } @@ -491,34 +424,25 @@ impl Command for TrackRippleRemoveAreaCommand { } } -impl Drop for TrackRippleRemoveAreaCommand { - fn drop(&mut self) { - // C++ dtor frees every remove-block command handle. - for i in 0..self.remove_block_commands_.len() { - free_command_handle(&mut self.remove_block_commands_[i]); - } - // `splice_split_command_` (a plain `BlockSplitCommand`) is dropped by - // the field's own `Drop`. - } -} - /// `TrackListRippleRemoveAreaCommand` — clear a time area across every track /// in a list (timelineundoripple.h). Composes a `TrackRippleRemoveAreaCommand` /// per track. pub struct TrackListRippleRemoveAreaCommand { /// Track list. - list: CHandle, + list: NodeRef, /// Area to clear. range: TimeRange, /// The tracks actually worked on (`working_tracks_`). - working_tracks_: Vec, + working_tracks_: Vec, /// Per-track child commands (`commands_`). commands_: Vec, } impl TrackListRippleRemoveAreaCommand { /// Construct from track list + in + out points. - pub fn new(list: CHandle, in_: Rational, out: Rational) -> Self { + /// + /// New signature (single-lib): `pub fn new(list: NodeRef, in_: Rational, out: Rational) -> TrackListRippleRemoveAreaCommand` + pub fn new(list: NodeRef, in_: Rational, out: Rational) -> Self { Self { list, range: TimeRange::new(in_, out), @@ -528,29 +452,24 @@ impl TrackListRippleRemoveAreaCommand { } /// `prepare`: build the per-track commands. - /// - /// Requires the track-list enumeration and locked-flag queries the bridge - /// does not yet expose, so this currently produces no child commands (see - /// the module note). pub fn prepare(&mut self) { self.working_tracks_.clear(); self.commands_.clear(); - let list = hdup(&self.list); - let count = track_list_count(list.clone()); + let list = self.list.clone(); + let count = track_list_count(&list); for i in 0..count { - let track = track_list_at(list.clone(), i); - if track.is_null() { + let Some(track) = track_list_at(&list, i) else { continue; - } + }; - if track_locked(hdup(&track)) { + if track_is_locked(&track) { continue; } self.commands_ - .push(TrackRippleRemoveAreaCommand::new(hdup(&track), self.range)); - self.working_tracks_.push(hdup(&track)); + .push(TrackRippleRemoveAreaCommand::new(track.clone(), self.range)); + self.working_tracks_.push(track); } } @@ -570,8 +489,8 @@ impl TrackListRippleRemoveAreaCommand { } } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } @@ -589,9 +508,10 @@ impl Command for TrackListRippleRemoveAreaCommand { /// `TimelineRippleRemoveAreaCommand` — clear a time area across the whole /// timeline (timelineundoripple.h). A multi-command that drives a /// `TrackListRippleRemoveAreaCommand` per track list. +#[allow(dead_code)] // `timeline`/`range` mirror the C++ members; the children carry the work. pub struct TimelineRippleRemoveAreaCommand { /// Owning sequence. - timeline: CHandle, + timeline: NodeRef, /// Area to clear. range: TimeRange, /// Per-track-list children (C++ `MultiUndoCommand` children). @@ -601,19 +521,16 @@ pub struct TimelineRippleRemoveAreaCommand { impl TimelineRippleRemoveAreaCommand { /// Construct from sequence + in + out points. /// - /// The C++ ctor walks the sequence's track lists via - /// `oaknode_sequence_get_track_list` (one per track type) and creates a - /// `TrackListRippleRemoveAreaCommand` for each; that query is not yet in - /// `bridge::node`, so no children are created until it is (see the module - /// note). - pub fn new(timeline: CHandle, in_: Rational, out: Rational) -> Self { + /// The ctor walks the sequence's track lists (one per track type) and + /// creates a `TrackListRippleRemoveAreaCommand` for each. + /// + /// New signature (single-lib): `pub fn new(timeline: NodeRef, in_: Rational, out: Rational) -> TimelineRippleRemoveAreaCommand` + pub fn new(timeline: NodeRef, in_: Rational, out: Rational) -> Self { let range = TimeRange::new(in_, out); let mut commands_ = Vec::new(); - for list in sequence_track_lists(hdup(&timeline)) { - if !list.is_null() { - commands_.push(TrackListRippleRemoveAreaCommand::new(hdup(&list), in_, out)); - } + for list in sequence_track_lists(&timeline) { + commands_.push(TrackListRippleRemoveAreaCommand::new(list, in_, out)); } Self { @@ -638,8 +555,8 @@ impl TimelineRippleRemoveAreaCommand { } } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } @@ -658,31 +575,34 @@ impl Command for TimelineRippleRemoveAreaCommand { /// whether a trailing gap should be appended (timelineundoripple.h). pub struct RippleInfo { /// Block to ripple. - block: CHandle, + block: NodeRef, /// Whether to append a gap after the ripple. append_gap: bool, } /// `TrackListRippleToolCommand` — ripple-tool edit: shift blocks on the listed /// tracks by `ripple_movement` (timelineundoripple.h). +#[allow(dead_code)] // `track_list` mirrors the C++ member; the per-track info drives the work. pub struct TrackListRippleToolCommand { /// Track list. - track_list: CHandle, - /// Per-track ripple info, ordered by track handle (TrackHandleLess parity). - info: Vec<(CHandle, RippleInfo)>, + track_list: NodeRef, + /// Per-track ripple info, ordered by track id (TrackHandleLess parity). + info: Vec<(NodeRef, RippleInfo)>, /// Signed ripple movement. ripple_movement: Rational, /// Movement mode. movement_mode: MovementMode, /// Per-track working data persisting across redo/undo (`working_data_`). - working_data_: Vec<(CHandle, WorkingData)>, + working_data_: Vec<(NodeRef, WorkingData)>, } impl TrackListRippleToolCommand { /// Construct from track list + per-track info + movement + mode. + /// + /// New signature (single-lib): `pub fn new(track_list: NodeRef, info: Vec<(NodeRef, RippleInfo)>, ripple_movement: Rational, movement_mode: MovementMode) -> TrackListRippleToolCommand` pub fn new( - track_list: CHandle, - info: Vec<(CHandle, RippleInfo)>, + track_list: NodeRef, + info: Vec<(NodeRef, RippleInfo)>, ripple_movement: Rational, movement_mode: MovementMode, ) -> Self { @@ -717,8 +637,8 @@ impl TrackListRippleToolCommand { // reads them afterwards; they are omitted here as dead code. for (track, info) in &self.info { - let track_copy = hdup(track); - let b = hdup(&info.block); + let track_copy = track.clone(); + let b = info.block.clone(); // Generate block length let mut operation_movement = self.ripple_movement; @@ -731,22 +651,19 @@ impl TrackListRippleToolCommand { operation_movement = rat_neg(operation_movement); } - let mut new_block_length = Rational::NULL; - if !b.is_null() { - new_block_length = block_length(hdup(&b)) + operation_movement; - } + let new_block_length = block_length(&b) + operation_movement; // Fetch or default-construct this track's working data (C++ map // `working_data_[track]`, persisted across redo/undo). let wd_index = match self .working_data_ .iter() - .position(|(t, _)| t.ctx == track_copy.ctx) + .position(|(t, _)| t.id == track_copy.id) { Some(i) => i, None => { self.working_data_ - .push((hdup(&track_copy), WorkingData::default())); + .push((track_copy.clone(), WorkingData::default())); self.working_data_.len() - 1 } }; @@ -755,109 +672,99 @@ impl TrackListRippleToolCommand { if info.append_gap { // Rather than rippling the referenced block, we'll insert a gap and // ripple with that - let mut gap = hdup(&wd.created_gap); - if redo { - if gap.is_null() { - // SAFETY: `oaknode_block_gap_create` returns a fresh owned handle. - gap = unsafe { oaknode_block_gap_create() }; - block_set_length_and_media_out( - hdup(&gap), + if wd.created_gap.is_none() { + let gap = block_gap_create(&track_copy.project); + // The gap takes the ripple movement's span ahead of + // `b`; positions are stored on the block in the + // module model, so both ends are written explicitly + // (approximation: the C++ ripple additionally shifts + // the successors, which the stored positions render + // as an overlap-free gap right before `b`). + block_set_in(&gap, block_in(&b)); + block_set_length_and_media_in( + &gap, if self.ripple_movement < Rational::new(0, 1) { rat_neg(self.ripple_movement) } else { self.ripple_movement }, ); - wd.created_gap = hdup(&gap); - wd.created_gap_orphaned = true; + wd.created_gap = Some(gap); } + let gap = wd.created_gap.clone().expect("created above"); - block_add_to_graph(hdup(&gap), hdup(&track_copy)); + block_add_to_graph(&gap, wd.created_gap_entry.take()); - // C++ `oaknode_track_insert_block_before(track, gap, b)`; the bridge - // has no such entry point, so insert after `b`'s predecessor instead - // (prepend when `b` is the first block). - let before = block_previous(hdup(&b)); - if before.is_null() { - // SAFETY: valid handles. - let _ = - unsafe { oaknode_track_prepend_block(hdup(&track_copy), hdup(&gap)) }; - } else { - // SAFETY: valid handles. - let _ = unsafe { - oaknode_track_insert_block_after(hdup(&track_copy), hdup(&gap), before) - }; + // C++ `oaknode_track_insert_block_before(track, gap, b)`; + // insert after `b`'s predecessor (prepend when `b` is the + // first block). + let before = block_previous(&b); + match before { + Some(prev) => { + track_insert_block_after(&track_copy, &gap, Some(&prev)) + } + None => track_prepend_block(&track_copy, &gap), } - wd.created_gap_orphaned = false; // As an insertion, the earliest change is at the gap's in point - wd.earliest_point_of_change = block_in(hdup(&gap)); - } else { - // SAFETY: valid handles. - let _ = - unsafe { oaknode_track_ripple_remove_block(hdup(&track_copy), hdup(&gap)) }; - block_remove_from_graph(hdup(&gap), hdup(&track_copy)); - wd.created_gap_orphaned = true; + wd.earliest_point_of_change = block_in(&gap); + } else if let Some(gap) = &wd.created_gap { + track_ripple_remove_block(&track_copy, gap); + if wd.created_gap_entry.is_none() { + wd.created_gap_entry = block_remove_from_graph(gap); + } } } else if (redo && new_block_length.is_null()) - || (!redo && block_track(hdup(&b)).is_null()) + || (!redo && block_track(&b).is_none()) { // The ripple is the length of this block. We assume that for this to // happen, it must have been a gap that we will now remove. if redo { // The earliest point changes will happen is at the start of this block - wd.earliest_point_of_change = block_in(hdup(&b)); + wd.earliest_point_of_change = block_in(&b); // Remove gap from track and from graph - wd.removed_gap = hdup(&b); - wd.removed_gap_after = block_previous(hdup(&b)); - // SAFETY: valid handles. - let _ = - unsafe { oaknode_track_ripple_remove_block(hdup(&track_copy), hdup(&b)) }; - block_remove_from_graph(hdup(&b), hdup(&track_copy)); - wd.removed_gap_orphaned = true; - } else { + wd.removed_gap = Some(b.clone()); + wd.removed_gap_after = block_previous(&b); + track_ripple_remove_block(&track_copy, &b); + wd.removed_gap_entry = block_remove_from_graph(&b); + } else if let Some(gap) = &wd.removed_gap { // Restore gap to graph and track - block_add_to_graph(hdup(&b), hdup(&track_copy)); - // SAFETY: valid handles; `removed_gap_after` may be empty when the - // gap was first on the track, which the ABI tolerates like C++. - let _ = unsafe { - oaknode_track_insert_block_after( - hdup(&track_copy), - hdup(&b), - hdup(&wd.removed_gap_after), - ) - }; - wd.removed_gap_orphaned = false; + block_add_to_graph(gap, wd.removed_gap_entry.take()); + track_insert_block_after( + &track_copy, + gap, + wd.removed_gap_after.as_ref(), + ); // The earliest point changes will happen is at the start of this block - wd.earliest_point_of_change = block_in(hdup(&b)); + wd.earliest_point_of_change = block_in(gap); } } else { // Store old length - wd.old_length = block_length(hdup(&b)); + wd.old_length = block_length(&b); if self.movement_mode == MovementMode::TrimIn { // The earliest point changes will occur is in point of this block - wd.earliest_point_of_change = block_in(hdup(&b)); + wd.earliest_point_of_change = block_in(&b); // Update length - block_set_length_and_media_in(hdup(&b), new_block_length); + block_set_length_and_media_in(&b, new_block_length); } else { // The earliest point changes will occur is the out point if trimming // out or the in point if trimming in - wd.earliest_point_of_change = block_out(hdup(&b)); + wd.earliest_point_of_change = block_out(&b); // Update length - block_set_length_and_media_out(hdup(&b), new_block_length); + block_set_length_and_media_out(&b, new_block_length); } } } } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } @@ -872,34 +779,19 @@ impl Command for TrackListRippleToolCommand { } } -impl Drop for TrackListRippleToolCommand { - fn drop(&mut self) { - // C++ dtor: free any gaps still detached from the graph - // (timelineundoripple.cpp:278). - for (_, wd) in &mut self.working_data_ { - if wd.created_gap_orphaned { - free_detached_handle(&mut wd.created_gap); - } - if wd.removed_gap_orphaned { - free_detached_handle(&mut wd.removed_gap); - } - } - } -} - /// `TrackListRippleToolCommand::WorkingData` — per-track state computed during /// a ripple edit (timelineundoripple.h). pub struct WorkingData { /// Gap created by the ripple. - created_gap: CHandle, - /// Whether `created_gap` is detached from the graph (owned by this command). - created_gap_orphaned: bool, + created_gap: Option, + /// Arena entry of `created_gap` while detached from the graph. + created_gap_entry: Option, /// Gap removed by the ripple. - removed_gap: CHandle, - /// Whether `removed_gap` is detached from the graph (owned by this command). - removed_gap_orphaned: bool, + removed_gap: Option, + /// Arena entry of `removed_gap` while detached from the graph. + removed_gap_entry: Option, /// Block that follows the removed gap. - removed_gap_after: CHandle, + removed_gap_after: Option, /// Track length before the edit. old_length: Rational, /// Earliest point affected on the track. @@ -910,11 +802,11 @@ impl Default for WorkingData { /// C++ default ctor: null block handles, false flags, NULL rationals. fn default() -> Self { Self { - created_gap: CHandle::null(), - created_gap_orphaned: false, - removed_gap: CHandle::null(), - removed_gap_orphaned: false, - removed_gap_after: CHandle::null(), + created_gap: None, + created_gap_entry: None, + removed_gap: None, + removed_gap_entry: None, + removed_gap_after: None, old_length: Rational::NULL, earliest_point_of_change: Rational::NULL, } @@ -925,25 +817,27 @@ impl Default for WorkingData { /// given (track, range) regions (timelineundoripple.h). pub struct TimelineRippleDeleteGapsAtRegionsCommand { /// Owning sequence. - timeline: CHandle, - /// Regions to clear, ordered by track handle. - regions: Vec<(CHandle, TimeRange)>, - /// Gap-removal/resize sub-command handles (`commands_`). - commands_: Vec, + timeline: NodeRef, + /// Regions to clear, ordered by track id. + regions: Vec<(NodeRef, TimeRange)>, + /// Gap-removal/resize sub-commands (`commands_`). + commands_: Vec, } /// One requested (track, range) region resolved to a gap block (C++ /// `RemovalRequest`). struct RemovalRequest { /// The gap block to remove or resize. - gap: CHandle, + gap: NodeRef, /// The region it must clear. range: TimeRange, } impl TimelineRippleDeleteGapsAtRegionsCommand { /// Construct from sequence + regions. - pub fn new(timeline: CHandle, regions: Vec<(CHandle, TimeRange)>) -> Self { + /// + /// New signature (single-lib): `pub fn new(timeline: NodeRef, regions: Vec<(NodeRef, TimeRange)>) -> TimelineRippleDeleteGapsAtRegionsCommand` + pub fn new(timeline: NodeRef, regions: Vec<(NodeRef, TimeRange)>) -> Self { Self { timeline, regions, @@ -959,48 +853,51 @@ impl TimelineRippleDeleteGapsAtRegionsCommand { /// `prepare`: build the per-region gap-removal commands. /// /// The gap-kind, nearest-block, sequence-track and locked-flag queries - /// all go through `bridge::node`; `redo` invokes this on first use - /// because the C ABI command path never calls `prepare` itself. + /// all go through the oaknode domain via `crate::util`; `redo` invokes + /// this on first use because the oakundo vtable wrapper only dispatches + /// `redo`/`undo`. pub fn prepare(&mut self) { self.commands_.clear(); let mut max_gaps = 0usize; - let mut requested_gaps: Vec<(CHandle, Vec)> = Vec::new(); + let mut requested_gaps: Vec<(NodeRef, Vec)> = Vec::new(); // Convert regions to gaps for (track, range) in &self.regions { - let track_copy = hdup(track); - let block = nearest_block_before_or_at(track_copy.clone(), range.in_()); + let track_copy = track.clone(); + let block = nearest_block_before_or_at(&track_copy, range.in_()); - if is_gap(hdup(&block)) { - let index = requested_gaps - .iter() - .position(|(t, _)| t.ctx == track_copy.ctx) - .unwrap_or_else(|| { - requested_gaps.push((hdup(&track_copy), Vec::new())); - requested_gaps.len() - 1 - }); + if let Some(block) = block { + if is_gap(&block) { + let index = requested_gaps + .iter() + .position(|(t, _)| t.id == track_copy.id) + .unwrap_or_else(|| { + requested_gaps.push((track_copy.clone(), Vec::new())); + requested_gaps.len() - 1 + }); - let gaps_on_track = &mut requested_gaps[index].1; - let this_req = RemovalRequest { - gap: hdup(&block), - range: *range, - }; + let gaps_on_track = &mut requested_gaps[index].1; + let this_req = RemovalRequest { + gap: block.clone(), + range: *range, + }; - // Insertion sort - let mut insert_at = None; - for i in 0..gaps_on_track.len() { - if gaps_on_track[i].range.in_() < range.in_() { - insert_at = Some(i); - break; + // Insertion sort + let mut insert_at = None; + for i in 0..gaps_on_track.len() { + if gaps_on_track[i].range.in_() < range.in_() { + insert_at = Some(i); + break; + } + } + match insert_at { + Some(i) => gaps_on_track.insert(i, this_req), + None => gaps_on_track.push(this_req), } - } - match insert_at { - Some(i) => gaps_on_track.insert(i, this_req), - None => gaps_on_track.push(this_req), - } - max_gaps = max_gaps.max(gaps_on_track.len()); + max_gaps = max_gaps.max(gaps_on_track.len()); + } } // C++ prints "Failed to find corresponding gap to region" to // stderr here; we simply skip the region. @@ -1009,7 +906,7 @@ impl TimelineRippleDeleteGapsAtRegionsCommand { // For each gap on each track, find a corresponding gap on every other // track (which may include a requested gap) to ripple in order to keep // everything synchronized - let mut gap_lengths: Vec<(CHandle, Rational)> = Vec::new(); + let mut gap_lengths: Vec<(NodeRef, Rational)> = Vec::new(); for gap_index in 0..max_gaps { let mut earliest_point = Rational::new(2147483647, 1); // RATIONAL_MAX let mut ripple_length = Rational::new(2147483647, 1); // RATIONAL_MAX @@ -1025,63 +922,67 @@ impl TimelineRippleDeleteGapsAtRegionsCommand { } // Determine which gaps will be involved in this operation - let mut gaps: Vec = Vec::new(); + let mut gaps: Vec = Vec::new(); - for track in sequence_all_tracks(hdup(&self.timeline)) { - if track.is_null() { - continue; - } - if track_locked(hdup(&track)) { + for track in sequence_all_tracks(&self.timeline) { + if track_is_locked(&track) { continue; } - let mut gap = CHandle::null(); - let requested_here = requested_gaps.iter().find(|(t, _)| t.ctx == track.ctx); + let mut gap: Option = None; + let requested_here = requested_gaps.iter().find(|(t, _)| t.id == track.id); if let Some((_, requested_on_track)) = requested_here { if gap_index < requested_on_track.len() { - gap = hdup(&requested_on_track[gap_index].gap); + gap = Some(requested_on_track[gap_index].gap.clone()); } } - if gap.is_null() { + if gap.is_none() { // No requested gap was at this index, find one - let block = nearest_block_after_or_at(hdup(&track), earliest_point); - if !block.is_null() { + let block = nearest_block_after_or_at(&track, earliest_point); + if let Some(block) = block { // Found a block, test if it's a gap - if is_gap(hdup(&block)) { - gap = hdup(&block); + if is_gap(&block) { + gap = Some(block.clone()); } else { - if block_in(hdup(&block)) == earliest_point { - let next = block_next(hdup(&block)); - if is_gap(hdup(&next)) { - gap = hdup(&next); + if block_in(&block) == earliest_point { + let next = block_next(&block); + if let Some(next) = next { + if is_gap(&next) { + gap = Some(next.clone()); + } else { + ripple_length = Rational::new(0, 1); + } } else { ripple_length = Rational::new(0, 1); } } else { - let prev = block_previous(hdup(&block)); - if is_gap(hdup(&prev)) { - gap = hdup(&prev); + let prev = block_previous(&block); + if let Some(prev) = prev { + if is_gap(&prev) { + gap = Some(prev.clone()); + } else { + ripple_length = Rational::new(0, 1); + } } else { ripple_length = Rational::new(0, 1); } } } - } else { - // Assume track finishes here and track won't be - // affected by this operation } + // Else: assume track finishes here and track won't be + // affected by this operation } - if !gap.is_null() { - let g = hdup(&gap); - gaps.push(hdup(&g)); + if let Some(g) = gap { + let g = g.clone(); + gaps.push(g.clone()); - if !gap_lengths.iter().any(|(b, _)| b.ctx == g.ctx) { - gap_lengths.push((hdup(&g), block_length(hdup(&g)))); + if !gap_lengths.iter().any(|(b, _)| b.id == g.id) { + gap_lengths.push((g.clone(), block_length(&g))); } - if let Some((_, len)) = gap_lengths.iter().find(|(b, _)| b.ctx == g.ctx) { + if let Some((_, len)) = gap_lengths.iter().find(|(b, _)| b.id == g.id) { ripple_length = std::cmp::min(ripple_length, *len); } } @@ -1095,25 +996,27 @@ impl TimelineRippleDeleteGapsAtRegionsCommand { for g in gaps { let g_len = gap_lengths .iter() - .find(|(b, _)| b.ctx == g.ctx) + .find(|(b, _)| b.id == g.id) .map(|(_, l)| *l) .unwrap_or(Rational::NULL); if g_len == ripple_length { - self.commands_.push( - TrackRippleRemoveBlockCommand::new(block_track(hdup(&g)), hdup(&g)) - .to_command(), - ); + if let Some(track) = block_track(&g) { + self.commands_.push( + TrackRippleRemoveBlockCommand::new(track, g.clone()) + .to_command(), + ); + } } else { let mut new_len = g_len; for (b, l) in gap_lengths.iter_mut() { - if b.ctx == g.ctx { + if b.id == g.id { *l = *l - ripple_length; new_len = *l; } } self.commands_ - .push(BlockResizeCommand::new(hdup(&g), new_len).to_command()); + .push(BlockResizeCommand::new(g.clone(), new_len).to_command()); } } } @@ -1122,7 +1025,7 @@ impl TimelineRippleDeleteGapsAtRegionsCommand { /// `redo`: apply the gap deletions. /// - /// The C ABI command path never invokes `prepare()` (the oakundo vtable + /// The vtable command path never invokes `prepare()` (the oakundo vtable /// wrapper only dispatches `redo`/`undo`), so the sub-commands are built /// on first use; later redos re-apply the stored commands. pub fn redo(&mut self) { @@ -1130,21 +1033,19 @@ impl TimelineRippleDeleteGapsAtRegionsCommand { self.prepare(); } for i in 0..self.commands_.len() { - let c = hdup(&self.commands_[i]); - let _ = unsafe { oakundo_command_redo_now(c) }; + self.commands_[i].redo_now(); } } /// `undo`: restore the deleted gaps. pub fn undo(&mut self) { for i in (0..self.commands_.len()).rev() { - let c = hdup(&self.commands_[i]); - let _ = unsafe { oakundo_command_undo_now(c) }; + self.commands_[i].undo_now(); } } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } @@ -1158,12 +1059,3 @@ impl Command for TimelineRippleDeleteGapsAtRegionsCommand { self.undo() } } - -impl Drop for TimelineRippleDeleteGapsAtRegionsCommand { - fn drop(&mut self) { - // C++ dtor frees every sub-command (`delete c`). - for i in 0..self.commands_.len() { - free_command_handle(&mut self.commands_[i]); - } - } -} diff --git a/crates/oaktimeline/src/undosplit.rs b/crates/oaktimeline/src/undosplit.rs index 77b726e32..23980f7fd 100644 --- a/crates/oaktimeline/src/undosplit.rs +++ b/crates/oaktimeline/src/undosplit.rs @@ -14,132 +14,173 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//! Split commands (`src/timeline/src/timelineundosplit.h`). All graph -//! operations go through the oaknode C ABI. +//! Split commands (`src/timeline/src/timelineundosplit.h`). Graph +//! operations go directly through the oaknode Rust domain (the oaknode +//! C ABI was deleted in the single-lib unification). //! //! The C++ oracles (`timelineundosplit.cpp`) clone the split block in the //! project graph (`oaknode_node_copy_in_graph` + `oaknode_block_from_node`), //! run a captured node-graph reconnect command, preserve link groups //! (`oaknode_block_are_linked`), move out-transitions across the split //! (`oaknode_node_disconnect`/`oaknode_node_connect`), and add cache -//! passthrough (`oaknode_clip_add_cache_passthrough_from`). None of those -//! symbols are exposed by this crate's oaknode bridge, so those branches are -//! omitted here and noted at each site; the remaining length/insert/remove -//! logic mirrors the C++. +//! passthrough (`oaknode_clip_add_cache_passthrough_from`). The clone is +//! restored via `NodeBehavior::duplicate` (the Rust equivalent of +//! `Node::Copy`) and link preservation via `Graph::link`; the +//! transition/cache/reconnect steps have no Rust equivalent yet and are +//! noted at each site. use oakcore_rs::Rational; +use oaknode::graph::NodeEntry; +use oakundo::undocommand::UndoCommand; -use crate::bridge::node::{ - oaknode_block_clip_create, oaknode_track_insert_block_after, oaknode_track_ripple_remove_block, -}; -use crate::handle::CHandle; use crate::util::{ - block_in, block_length, block_out, block_set_length_and_media_in, - block_set_length_and_media_out, block_track, same_block, + block_add_to_graph, block_in, block_length, block_out, block_remove_from_graph, + block_set_length_and_media_in, block_set_length_and_media_out, block_track, + track_insert_block_after, track_ripple_remove_block, GraphBlockRange, NodeRef, }; /// `BlockSplitCommand` — split one block at a point /// (timelineundosplit.h). +/// +/// The split keeps the original block anchored at its in-point (it becomes +/// the first half `[in, point)`) and anchors the cloned `new_block` at its +/// out-point (it becomes the second half `[point, out)`), inserted right +/// after the original so the track order mirrors the timeline order. pub struct BlockSplitCommand { /// Block to split. - block: CHandle, + block: NodeRef, /// Split point. point: Rational, /// Second block created by the split (valid after `redo`). - new_block: CHandle, + new_block: Option, + /// Arena entry of the second block while it is detached from the + /// graph (between `undo` and the next `redo`). + new_block_entry: Option, /// Length of `block` before the split, restored on `undo`. old_length: Rational, } impl BlockSplitCommand { /// Construct from block + split point. - pub fn new(block: CHandle, point: Rational) -> Self { + /// + /// New signature (single-lib): `pub fn new(block: NodeRef, point: Rational) -> BlockSplitCommand` + pub fn new(block: NodeRef, point: Rational) -> Self { Self { block, point, - new_block: CHandle::null(), + new_block: None, + new_block_entry: None, old_length: Rational::new(0, 1), } } - /// `prepare`: create the second half of the split. The C++ clone is - /// performed by `oaknode_node_copy_in_graph` + `oaknode_block_from_node`, - /// which are absent from this bridge, so a fresh clip block is created. + /// `prepare`: create the second half by cloning the original block in + /// the project graph (the Rust equivalent of the C++ + /// `oaknode_node_copy_in_graph`; the clone happens through the + /// behavior's `duplicate`, which copies the block core too). pub fn prepare(&mut self) { - if self.new_block.is_null() { - // SAFETY: `oaknode_block_clip_create` returns an owned handle. - self.new_block = unsafe { oaknode_block_clip_create() }; + if self.new_block.is_some() { + return; } + let id = { + let project = self.block.project.clone(); + let mut p = project.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + let entry = match p.graph.get(self.block.id) { + Some(e) => e, + None => return, + }; + let new_core = entry.core.clone(); + let new_behavior = match entry.behavior.duplicate(&entry.core) { + Some(b) => b, + None => return, + }; + p.graph.add_node(new_core, new_behavior) + }; + self.new_block = Some(NodeRef::new(self.block.project.clone(), id)); } /// `redo`: shrink `block` to the first half, grow `new_block` to the /// second half, and insert it after `block`. /// - /// The split keeps the ORIGINAL block anchored at its out-point (it - /// becomes the second half `[point, out)`) and anchors the fresh - /// `new_block` at its in-point (it becomes the first half - /// `[in, point)`), matching the C++ `Block::set_length_and_media_out` / - /// `set_length_and_media_in` semantics of the module. + /// The split keeps the ORIGINAL block anchored at its in-point (the + /// C++ `set_length_and_media_in` on the original) and anchors the + /// cloned `new_block` at its out-point (the C++ + /// `set_length_and_media_out` on the copy — the copy carried the + /// original span, so the out point is preserved exactly). pub fn redo(&mut self) { // Create the second half if redo is invoked without a preceding - // prepare() (the C ABI command path may call redo directly). - if self.new_block.is_null() { - // SAFETY: `oaknode_block_clip_create` returns an owned handle. - self.new_block = unsafe { oaknode_block_clip_create() }; - } + // prepare() (the vtable command path may call redo directly). + self.prepare(); - self.old_length = block_length(self.block.clone()); + // The C++ asserts `point_` lies strictly inside the block; that + // would panic across the FFI boundary, so it is intentionally not + // replicated — a split point outside the span is clamped by the + // rational arithmetic below. + self.old_length = block_length(&self.block); - let block_in = block_in(self.block.clone()); - let block_out = block_out(self.block.clone()); + let block_in = block_in(&self.block); + let block_out = block_out(&self.block); - // The C++ asserts `point_` lies strictly inside the block; that would - // panic across the FFI boundary, so it is intentionally not replicated. let first_half_length = self.point - block_in; let second_half_length = block_out - self.point; - let track = block_track(self.block.clone()); + if let Some(new_block) = &self.new_block { + // Re-attach the second half if a previous undo detached it. + if self.new_block_entry.is_some() { + block_add_to_graph(new_block, self.new_block_entry.take()); + } + // In-anchored length for the first half keeps the original's in + // point (the C++ `set_length_and_media_in`); the out-anchored + // length for the second half keeps the copy's out point (the + // C++ `set_length_and_media_out`). + block_set_length_and_media_in(&self.block, first_half_length); + block_set_length_and_media_out(new_block, second_half_length); - // Out-anchored length for the second half keeps the original's out - // point (the C++ `set_length_and_media_out`); the in-anchored length - // for the first half grows the fresh block from its default in of 0 - // (the C++ `set_length_and_media_in`). The two were previously - // swapped, which anchored the halves at the wrong points. - block_set_length_and_media_out(self.block.clone(), second_half_length); - block_set_length_and_media_in(self.new_block.clone(), first_half_length); - - // SAFETY: bridge inserts `new_block` after `block` on `track`. - let _ = unsafe { - oaknode_track_insert_block_after(track, self.new_block.clone(), self.block.clone()) - }; + if let Some(track) = block_track(&self.block) { + track_insert_block_after(&track, new_block, Some(&self.block)); + } + } // The C++ also re-runs a `reconnect_tree_command_` (the node-graph // re-connection captured while cloning) and moves an out transition - // onto `new_block` via `oaknode_*` connection APIs; those symbols are - // absent from this bridge, so both are omitted here. + // onto `new_block` via `oaknode_*` connection APIs; the Rust block + // has no transition edges, so both are omitted here. } /// `undo`: restore `block`'s original length and remove the second half. pub fn undo(&mut self) { - let track = block_track(self.block.clone()); - - block_set_length_and_media_out(self.block.clone(), self.old_length); - - // SAFETY: bridge ripple-removes `new_block` from `track`. - let _ = unsafe { oaknode_track_ripple_remove_block(track, self.new_block.clone()) }; + if let Some(track) = block_track(&self.block) { + // The redo shrank the original from its in point (it became the + // first half, in-anchored), so the restore grows it in-anchored + // too — the module stores the span on the block (the C++ + // `set_length_and_media_out` derives positions from the track + // order and does not apply here). + block_set_length_and_media_in(&self.block, self.old_length); + if let Some(new_block) = &self.new_block { + track_ripple_remove_block(&track, new_block); + // Detach the second half from the graph (the C++ re-parents + // it to the scratch memory manager); the entry is owned by + // this command until the next redo. + if self.new_block_entry.is_none() { + self.new_block_entry = block_remove_from_graph(new_block); + } + } + } // The C++ first moves a previously-moved out transition back onto - // `block` and runs `reconnect_tree_command_`'s undo; both need oaknode - // symbols absent from this bridge, so they are omitted here. + // `block` and runs `reconnect_tree_command_`'s undo; the Rust block + // has no transition edges, so those are omitted here. } /// The second block created by the split; only valid after `redo`. - pub fn new_block(&self) -> CHandle { + /// + /// New signature (single-lib): `pub fn new_block(&self) -> Option` + pub fn new_block(&self) -> Option { self.new_block.clone() } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { crate::undocommon::box_command(self) } } @@ -158,19 +199,21 @@ impl crate::undocommon::Command for BlockSplitCommand { /// times, preserving link groups (timelineundosplit.h). pub struct BlockSplitPreservingLinksCommand { /// Blocks to split. - blocks: Vec, + blocks: Vec, /// Split times. times: Vec, /// Child split commands, executed in order on `redo`. commands: Vec, /// `splits[time_index][block_index]`: second half created for that - /// block/time, or an empty handle when the block was not split there. - splits: Vec>, + /// block/time, or `None` when the block was not split there. + splits: Vec>>, } impl BlockSplitPreservingLinksCommand { /// Construct from blocks + times (one per block). - pub fn new(blocks: Vec, times: Vec) -> Self { + /// + /// New signature (single-lib): `pub fn new(blocks: Vec, times: Vec) -> BlockSplitPreservingLinksCommand` + pub fn new(blocks: Vec, times: Vec) -> Self { Self { blocks, times, @@ -183,7 +226,7 @@ impl BlockSplitPreservingLinksCommand { pub fn prepare(&mut self) { let n_times = self.times.len(); let n_blocks = self.blocks.len(); - self.splits = vec![vec![CHandle::null(); n_blocks]; n_times]; + self.splits = vec![vec![None; n_blocks]; n_times]; for i in 0..n_times { let time = self.times[i]; @@ -192,8 +235,8 @@ impl BlockSplitPreservingLinksCommand { // panic across the FFI boundary, so it is not replicated. for j in 0..n_blocks { - let b_in = block_in(self.blocks[j].clone()); - let b_out = block_out(self.blocks[j].clone()); + let b_in = block_in(&self.blocks[j]); + let b_out = block_out(&self.blocks[j]); if b_in < time && b_out > time { let mut split = BlockSplitCommand::new(self.blocks[j].clone(), time); @@ -205,19 +248,35 @@ impl BlockSplitPreservingLinksCommand { } } - // The C++ then relinks every pair of originally-linked blocks by - // creating link commands between their split halves - // (`oaknode_block_are_linked`); that symbol is absent from this - // bridge, so link preservation is omitted. + // Link preservation (C++ `oaknode_block_are_linked` + + // link commands between the halves): every pair of originally + // linked blocks that was split at the same time has its new + // halves linked too (`Graph::link` mirrors `Node::link`). + let Some(first) = self.blocks.first() else { + return; + }; + let project = first.project.clone(); + let mut p = project.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + for i in 0..n_times { + for j in 0..n_blocks { + for k in (j + 1)..n_blocks { + if p.graph.are_linked(self.blocks[j].id, self.blocks[k].id) { + if let (Some(a), Some(b)) = (&self.splits[i][j], &self.splits[i][k]) { + p.graph.link(a.id, b.id); + } + } + } + } + } } /// `redo`: redo every child command in order. /// - /// The C ABI command path never invokes `prepare()` (the oakundo vtable - /// wrapper only dispatches `redo`/`undo`), so the children are built on - /// first redo; `prepare` itself redoes each child as it builds it, so the - /// first redo has nothing left to run. Later redos (after an undo) run - /// the stored children directly. + /// The vtable command path never invokes `prepare()` (the oakundo + /// wrapper only dispatches `redo`/`undo`), so the children are built + /// on first redo; `prepare` itself redoes each child as it builds it, + /// so the first redo has nothing left to run. Later redos (after an + /// undo) run the stored children directly. pub fn redo(&mut self) { if self.commands.is_empty() { self.prepare(); @@ -237,23 +296,26 @@ impl BlockSplitPreservingLinksCommand { /// The block produced by splitting `original` at `time_index`; /// `None` when not applicable (timelineundosplit.h `get_split`). - pub fn get_split(&self, original: CHandle, time_index: usize) -> Option { + /// + /// New signature (single-lib): `pub fn get_split(&self, original: &NodeRef, time_index: usize) -> Option` + pub fn get_split(&self, original: &NodeRef, time_index: usize) -> Option { if time_index < self.times.len() { for (i, b) in self.blocks.iter().enumerate() { - if same_block(b.clone(), original.clone()) { + if b.id == original.id { return self .splits .get(time_index) .and_then(|row| row.get(i)) - .cloned(); + .cloned() + .flatten(); } } } None } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { crate::undocommon::box_command(self) } } @@ -272,7 +334,7 @@ impl crate::undocommon::Command for BlockSplitPreservingLinksCommand { /// (timelineundosplit.h). pub struct TrackSplitAtTimeCommand { /// Owning track. - track: CHandle, + track: NodeRef, /// Split point. point: Rational, /// Inner per-block split command, built by `prepare`. @@ -281,7 +343,9 @@ pub struct TrackSplitAtTimeCommand { impl TrackSplitAtTimeCommand { /// Construct from track + point. - pub fn new(track: CHandle, point: Rational) -> Self { + /// + /// New signature (single-lib): `pub fn new(track: NodeRef, point: Rational) -> TrackSplitAtTimeCommand` + pub fn new(track: NodeRef, point: Rational) -> Self { Self { track, point, @@ -289,17 +353,36 @@ impl TrackSplitAtTimeCommand { } } - /// `prepare`: build the per-block split command. The C++ finds the block - /// via `oaknode_track_get_block_containing_time`, which is not exposed by - /// this crate's oaknode bridge (and there is no block-enumeration helper - /// to search for it), so the inner command cannot be built here and - /// redo/undo remain no-ops until that lookup API exists. + /// `prepare`: build the per-block split command. The C++ finds the + /// block via `oaknode_track_get_block_containing_time`; the Rust + /// equivalent is `TrackBehavior::block_containing_time` over the + /// graph-backed block range. pub fn prepare(&mut self) { - let _ = (self.track.clone(), self.point); + if self.command.is_some() { + return; + } + let block_id = { + let project = self.track.project.clone(); + let p = project.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + p.graph + .get(self.track.id) + .and_then(|e| e.behavior.as_any()) + .and_then(|a| a.downcast_ref::()) + .and_then(|t| { + t.block_containing_time(self.point, &GraphBlockRange { graph: &p.graph }) + }) + }; + if let Some(id) = block_id { + self.command = Some(BlockSplitCommand::new( + NodeRef::new(self.track.project.clone(), id), + self.point, + )); + } } /// `redo`: forward to the inner command. pub fn redo(&mut self) { + self.prepare(); if let Some(c) = self.command.as_mut() { c.redo(); } @@ -312,8 +395,8 @@ impl TrackSplitAtTimeCommand { } } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { crate::undocommon::box_command(self) } } diff --git a/crates/oaktimeline/src/undotrack.rs b/crates/oaktimeline/src/undotrack.rs index e30ca7f22..b656df7d6 100644 --- a/crates/oaktimeline/src/undotrack.rs +++ b/crates/oaktimeline/src/undotrack.rs @@ -15,58 +15,55 @@ // along with this program. If not, see . //! Track-level block commands (`src/timeline/src/timelineundotrack.h`). -//! All graph operations go through the oaknode C ABI (`bridge::node`). +//! Graph operations go directly through the oaknode Rust domain +//! ([`crate::util::NodeRef`] + the project graph) since the single-lib +//! unification removed the oaknode C ABI. + +use oakundo::undocommand::UndoCommand; -use crate::bridge::node::{ - oaknode_block_get_previous, oaknode_track_insert_block_after, oaknode_track_prepend_block, - oaknode_track_replace_block, oaknode_track_ripple_remove_block, -}; -use crate::handle::CHandle; use crate::undocommon::{box_command, Command}; +use crate::util::{ + block_previous, track_insert_block_after, track_prepend_block, track_replace_block, + track_ripple_remove_block, NodeRef, +}; /// `TrackRippleRemoveBlockCommand` — ripple-remove a block from a track /// (timelineundotrack.h). pub struct TrackRippleRemoveBlockCommand { /// Owning track. - track: CHandle, + track: NodeRef, /// Block to remove. - block: CHandle, + block: NodeRef, /// Predecessor of the removed block, captured at `redo` time so `undo` /// can restore the block at its original position. - before: CHandle, + before: Option, } impl TrackRippleRemoveBlockCommand { /// Construct from track + block. - pub fn new(track: CHandle, block: CHandle) -> Self { + /// + /// New signature (single-lib): `pub fn new(track: NodeRef, block: NodeRef) -> TrackRippleRemoveBlockCommand` + pub fn new(track: NodeRef, block: NodeRef) -> Self { Self { track, block, - before: CHandle::null(), + before: None, } } - /// `redo`: capture the predecessor, then `oaknode_track_ripple_remove_block`. + /// `redo`: capture the predecessor, then ripple-remove. pub fn redo(&mut self) { - unsafe { - oaknode_block_get_previous(self.block.clone(), &mut self.before); - oaknode_track_ripple_remove_block(self.track.clone(), self.block.clone()); - } + self.before = block_previous(&self.block); + track_ripple_remove_block(&self.track, &self.block); } - /// `undo`: re-insert via `oaknode_track_insert_block_after`. + /// `undo`: re-insert after the captured predecessor. pub fn undo(&mut self) { - unsafe { - oaknode_track_insert_block_after( - self.track.clone(), - self.block.clone(), - self.before.clone(), - ); - } + track_insert_block_after(&self.track, &self.block, self.before.as_ref()); } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } @@ -87,33 +84,31 @@ impl Command for TrackRippleRemoveBlockCommand { /// (timelineundotrack.h). pub struct TrackPrependBlockCommand { /// Owning track. - track: CHandle, + track: NodeRef, /// Block to prepend. - block: CHandle, + block: NodeRef, } impl TrackPrependBlockCommand { /// Construct from track + block. - pub fn new(track: CHandle, block: CHandle) -> Self { + /// + /// New signature (single-lib): `pub fn new(track: NodeRef, block: NodeRef) -> TrackPrependBlockCommand` + pub fn new(track: NodeRef, block: NodeRef) -> Self { Self { track, block } } - /// `redo`: `oaknode_track_prepend_block`. + /// `redo`: prepend the block. pub fn redo(&mut self) { - unsafe { - oaknode_track_prepend_block(self.track.clone(), self.block.clone()); - } + track_prepend_block(&self.track, &self.block); } - /// `undo`: `oaknode_track_ripple_remove_block`. + /// `undo`: ripple-remove the block. pub fn undo(&mut self) { - unsafe { - oaknode_track_ripple_remove_block(self.track.clone(), self.block.clone()); - } + track_ripple_remove_block(&self.track, &self.block); } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } @@ -131,19 +126,22 @@ impl Command for TrackPrependBlockCommand { } /// `TrackInsertBlockAfterCommand` — insert a block after a predecessor -/// (timelineundotrack.h). +/// (timelineundotrack.h). A `None` predecessor inserts at the front of +/// the track. pub struct TrackInsertBlockAfterCommand { /// Owning track. - track: CHandle, + track: NodeRef, /// Block to insert. - block: CHandle, - /// Predecessor block. - before: CHandle, + block: NodeRef, + /// Predecessor block (`None` = front). + before: Option, } impl TrackInsertBlockAfterCommand { /// Construct from track + block + predecessor. - pub fn new(track: CHandle, block: CHandle, before: CHandle) -> Self { + /// + /// New signature (single-lib): `pub fn new(track: NodeRef, block: NodeRef, before: Option) -> TrackInsertBlockAfterCommand` + pub fn new(track: NodeRef, block: NodeRef, before: Option) -> Self { Self { track, block, @@ -151,26 +149,18 @@ impl TrackInsertBlockAfterCommand { } } - /// `redo`: `oaknode_track_insert_block_after`. + /// `redo`: insert after the predecessor. pub fn redo(&mut self) { - unsafe { - oaknode_track_insert_block_after( - self.track.clone(), - self.block.clone(), - self.before.clone(), - ); - } + track_insert_block_after(&self.track, &self.block, self.before.as_ref()); } - /// `undo`: `oaknode_track_ripple_remove_block`. + /// `undo`: ripple-remove the block. pub fn undo(&mut self) { - unsafe { - oaknode_track_ripple_remove_block(self.track.clone(), self.block.clone()); - } + track_ripple_remove_block(&self.track, &self.block); } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } @@ -191,16 +181,18 @@ impl Command for TrackInsertBlockAfterCommand { /// (equal lengths required) (timelineundotrack.h). pub struct TrackReplaceBlockCommand { /// Owning track. - track: CHandle, + track: NodeRef, /// Block to replace. - old: CHandle, + old: NodeRef, /// Replacement block. - replace: CHandle, + replace: NodeRef, } impl TrackReplaceBlockCommand { /// Construct from track + old + replace. - pub fn new(track: CHandle, old: CHandle, replace: CHandle) -> Self { + /// + /// New signature (single-lib): `pub fn new(track: NodeRef, old: NodeRef, replace: NodeRef) -> TrackReplaceBlockCommand` + pub fn new(track: NodeRef, old: NodeRef, replace: NodeRef) -> Self { Self { track, old, @@ -208,22 +200,18 @@ impl TrackReplaceBlockCommand { } } - /// `redo`: `oaknode_track_replace_block(track, old, replace)`. + /// `redo`: replace `old` with `replace`. pub fn redo(&mut self) { - unsafe { - oaknode_track_replace_block(self.track.clone(), self.old.clone(), self.replace.clone()); - } + track_replace_block(&self.track, &self.old, &self.replace); } - /// `undo`: `oaknode_track_replace_block(track, replace, old)`. + /// `undo`: swap back. pub fn undo(&mut self) { - unsafe { - oaknode_track_replace_block(self.track.clone(), self.replace.clone(), self.old.clone()); - } + track_replace_block(&self.track, &self.replace, &self.old); } - /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> CHandle { + /// Wrap as an oakundo vtable command value. + pub fn to_command(self) -> UndoCommand { box_command(self) } } diff --git a/crates/oaktimeline/src/util.rs b/crates/oaktimeline/src/util.rs index 8eadb3ac8..c23576178 100644 --- a/crates/oaktimeline/src/util.rs +++ b/crates/oaktimeline/src/util.rs @@ -17,247 +17,699 @@ //! Inline helpers from `src/timeline/src/timelineutil.h`: Rational ↔ //! num/den pairs, value-handle identity, and block/track/project queries. //! -//! All graph access goes through the oaknode C ABI (`bridge::node`). The -//! C++ `oaknode_c_api::to_native` internals are NOT replicated — handles -//! are opaque here; identity (`same_*`) is decided by `ctx` equality, not -//! by `to_native` (borrowed accessors box a fresh handle per call). +//! Since the single-lib unification the oaknode C ABI is gone; every query +//! below operates directly on the oaknode Rust domain. A [`NodeRef`] pins a +//! `NodeId` inside a project's graph (`Arc>`), +//! replacing the opaque `CHandle` node/block/track references. Nullable +//! references are plain `Option` — the old `CHandle::null()` +//! sentinel is `None`, and identity (`same_*`) is `NodeId` equality. +//! +//! Blocks/tracks that a command detaches from the project graph are stored +//! as an owned [`oaknode::graph::NodeEntry`] on the command itself: +//! [`block_remove_from_graph`] takes the entry out of the arena (identity is +//! preserved for re-insertion) and [`block_add_to_graph`] puts it back — the +//! Rust equivalent of the C++ `setParent(&memory_manager_)` re-homing. -use std::cmp::Ordering; +use std::sync::{Arc, Mutex, MutexGuard}; use oakcore_rs::Rational; +use oaknode::block::{BlockCore, ClipBlockBehavior, GapBlockBehavior, TransitionBlockBehavior}; +use oaknode::graph::{Graph, NodeEntry}; +use oaknode::id::NodeId; +use oaknode::node::NodeCore; +use oaknode::project::Project; +use oaknode::sequence::SequenceBehavior; +use oaknode::track::{TrackBehavior, TrackListBehavior, TrackType}; -use crate::bridge::node::{ - oaknode_block_get_in, oaknode_block_get_length, oaknode_block_get_next, oaknode_block_get_out, - oaknode_block_get_previous, oaknode_block_get_track, oaknode_block_set_in, - oaknode_block_set_length_and_media_in, oaknode_block_set_length_and_media_out, - oaknode_node_get_project, oaknode_project_add_node, oaknode_project_remove_node, - oaknode_sequence_as_node, oaknode_track_get_block_at, oaknode_track_get_block_count, - oaknode_track_get_length, oaknode_track_get_sequence, oaknode_track_insert_block_after, - oaknode_track_prepend_block, -}; -use crate::handle::CHandle; +/// A reference to a node — block, track, track list or sequence — inside +/// a project's graph. `Clone` is cheap: the project `Arc` is shared and +/// [`NodeId`] is `Copy`; the lifetime of the node follows the project. +/// +/// This is the domain replacement for the opaque node/block/track +/// `CHandle`s of the deleted oaknode C ABI. +#[derive(Clone)] +pub struct NodeRef { + /// The owning project (its graph holds the node). + pub project: Arc>, + /// The node's id in that project's graph. + pub id: NodeId, +} + +impl NodeRef { + /// New reference. + pub fn new(project: Arc>, id: NodeId) -> NodeRef { + NodeRef { project, id } + } + + /// Lock the owning project. A poisoned lock is recovered so a + /// panicking command body cannot wedge every later edit. + fn lock(&self) -> MutexGuard<'_, Project> { + self.project + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + } +} /// Split a `Rational` into numerator/denominator pairs for the C ABI -/// (timelineutil.h `rat_nd`). +/// (timelineutil.h `rat_nd`). Kept for the ABI-compatible command paths. pub fn rat_nd(r: Rational, n: &mut i32, d: &mut i32) { *n = r.numerator() as i32; *d = r.denominator() as i32; } -/// Identity of two handles by `ctx` pointer equality. -fn same_handle(a: CHandle, b: CHandle) -> bool { - a.ctx == b.ctx +/// `same_block`: two block references name the same graph node +/// (`NodeId` equality; both are assumed to live in the same project). +pub fn same_block(a: &NodeRef, b: &NodeRef) -> bool { + a.id == b.id } -/// `same_block`: two block handles wrap the same object. -pub fn same_block(a: CHandle, b: CHandle) -> bool { - same_handle(a, b) +/// `same_track`: two track references name the same graph node. +pub fn same_track(a: &NodeRef, b: &NodeRef) -> bool { + a.id == b.id } -/// `same_track`: two track handles wrap the same object. -pub fn same_track(a: CHandle, b: CHandle) -> bool { - same_handle(a, b) +/// `same_node`: two node references name the same graph node. +pub fn same_node(a: &NodeRef, b: &NodeRef) -> bool { + a.id == b.id } -/// `same_node`: two node handles wrap the same object. -pub fn same_node(a: CHandle, b: CHandle) -> bool { - same_handle(a, b) +/// Block kind (C++ `Block::Type` / the deleted `oaknode/block.h` +/// `OAKNODE_BLOCK_*` values). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BlockKind { + /// Not a block. + Other, + /// Clip block. + Clip, + /// Gap block. + Gap, + /// Transition block. + Transition, } -/// Total order over handles by `ctx`, so maps work across freshly boxed -/// handles of the same origin. -fn handle_less(a: CHandle, b: CHandle) -> Ordering { - a.ctx.cmp(&b.ctx) +// --------------------------------------------------------------------------- +// Internal graph/behavior accessors +// --------------------------------------------------------------------------- + +/// Immutable [`BlockCore`] of the block node `id` (any block behavior +/// kind shares the same core), or `None` for a stale/non-block node. +fn block_core_of(project: &Project, id: NodeId) -> Option<&BlockCore> { + let entry = project.graph.get(id)?; + entry.behavior.as_any().and_then(|a| { + if let Some(clip) = a.downcast_ref::() { + Some(&clip.core) + } else if let Some(gap) = a.downcast_ref::() { + Some(&gap.core) + } else if let Some(transition) = a.downcast_ref::() { + Some(&transition.core) + } else { + None + } + }) } -/// `BlockHandleLess`: total order over block handles by wrapped native -/// pointer (identity), so maps work across freshly-boxed borrowed handles. -pub fn block_handle_less(a: CHandle, b: CHandle) -> std::cmp::Ordering { - handle_less(a, b) -} - -/// `TrackHandleLess`: total order over track handles by wrapped native -/// pointer. -pub fn track_handle_less(a: CHandle, b: CHandle) -> std::cmp::Ordering { - handle_less(a, b) -} - -/// `free_detached_handle`: re-take ownership of a detached object's handle -/// then release, destroying it (timelineutil.h). -/// -/// In this crate's model a detached block/track handle is an owned handle -/// (it owns its box), so releasing it destroys the object. NULL/empty no-op. -pub fn free_detached_handle(h: *mut CHandle) { - if h.is_null() { - return; +/// Mutable [`BlockCore`] of the block node `id`, or `None`. +fn block_core_of_mut(project: &mut Project, id: NodeId) -> Option<&mut BlockCore> { + let entry = project.graph.get_mut(id)?; + let a = entry.behavior.as_any_mut()?; + if a.is::() { + Some(&mut a.downcast_mut::().expect("checked above").core) + } else if a.is::() { + Some(&mut a.downcast_mut::().expect("checked above").core) + } else if a.is::() { + Some( + &mut a + .downcast_mut::() + .expect("checked above") + .core, + ) + } else { + None } - // SAFETY: 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` is the boxed type's release callback. - unsafe { release(handle.ctx) }; - } - handle.ctx = std::ptr::null_mut(); - handle.addref = None; - handle.release = None; - handle.abi_version = 0; } -/// Read a block time field as a `Rational`. -fn block_pair(b: CHandle, f: fn(CHandle, *mut i32, *mut i32) -> i32) -> Rational { - let mut n = 0; - let mut d = 0; - // SAFETY: `n`/`d` are valid out pointers. - let _ = unsafe { f(b, &mut n, &mut d) }; - Rational::new(n as i64, d as i64) +/// Immutable [`TrackBehavior`] of the track node `id`, or `None`. +fn track_behavior_of(project: &Project, id: NodeId) -> Option<&TrackBehavior> { + project.graph.get(id).and_then(|e| { + e.behavior + .as_any() + .and_then(|a| a.downcast_ref::()) + }) } -/// `block_in`: in point as a `Rational`. -pub fn block_in(b: CHandle) -> Rational { - block_pair(b, oaknode_block_get_in) +/// Mutable [`TrackBehavior`] of the track node `id`, or `None`. +fn track_behavior_of_mut(project: &mut Project, id: NodeId) -> Option<&mut TrackBehavior> { + project.graph.get_mut(id).and_then(|e| { + e.behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + }) +} + +/// Immutable [`TrackListBehavior`] of the track-list node `id`, or `None`. +fn tracklist_behavior_of(project: &Project, id: NodeId) -> Option<&TrackListBehavior> { + project.graph.get(id).and_then(|e| { + e.behavior + .as_any() + .and_then(|a| a.downcast_ref::()) + }) +} + +/// Mutable [`TrackListBehavior`] of the track-list node `id`, or `None`. +fn tracklist_behavior_of_mut(project: &mut Project, id: NodeId) -> Option<&mut TrackListBehavior> { + project.graph.get_mut(id).and_then(|e| { + e.behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + }) +} + +/// Immutable [`SequenceBehavior`] of the sequence node `id`, or `None`. +fn sequence_behavior_of(project: &Project, id: NodeId) -> Option<&SequenceBehavior> { + project.graph.get(id).and_then(|e| { + e.behavior + .as_any() + .and_then(|a| a.downcast_ref::()) + }) +} + +/// Graph-backed [`oaknode::track::BlockRange`] view: the block span +/// queries the track needs (in/out), read from the `BlockCore`s in the +/// arena. +pub struct GraphBlockRange<'a> { + /// The graph the block nodes live in. + pub graph: &'a Graph, +} + +impl oaknode::track::BlockRange for GraphBlockRange<'_> { + fn in_(&self, block: NodeId) -> Rational { + self.graph + .get(block) + .and_then(|e| e.behavior.as_any()) + .and_then(|a| { + [ + a.downcast_ref::() + .map(|b| &b.core), + a.downcast_ref::().map(|b| &b.core), + a.downcast_ref::() + .map(|b| &b.core), + ] + .into_iter() + .flatten() + .next() + }) + .map(BlockCore::in_) + .unwrap_or_else(|| Rational::new(0, 1)) + } + + fn out(&self, block: NodeId) -> Rational { + self.graph + .get(block) + .and_then(|e| e.behavior.as_any()) + .and_then(|a| { + [ + a.downcast_ref::() + .map(|b| &b.core), + a.downcast_ref::().map(|b| &b.core), + a.downcast_ref::() + .map(|b| &b.core), + ] + .into_iter() + .flatten() + .next() + }) + .map(BlockCore::out) + .unwrap_or_else(|| Rational::new(0, 1)) + } +} + +// --------------------------------------------------------------------------- +// Block value queries / mutations +// --------------------------------------------------------------------------- + +/// `block_in`: in point as a `Rational` (0/1 for a stale/non-block node, +/// matching the deleted bridge's untouched-output failure path). +pub fn block_in(b: &NodeRef) -> Rational { + let p = b.lock(); + block_core_of(&p, b.id).map(BlockCore::in_).unwrap_or_else(|| Rational::new(0, 1)) } /// `block_out`: out point as a `Rational`. -pub fn block_out(b: CHandle) -> Rational { - block_pair(b, oaknode_block_get_out) +pub fn block_out(b: &NodeRef) -> Rational { + let p = b.lock(); + block_core_of(&p, b.id).map(BlockCore::out).unwrap_or_else(|| Rational::new(0, 1)) } /// `block_length`: block length as a `Rational`. -pub fn block_length(b: CHandle) -> Rational { - block_pair(b, oaknode_block_get_length) +pub fn block_length(b: &NodeRef) -> Rational { + let p = b.lock(); + block_core_of(&p, b.id) + .map(BlockCore::length) + .unwrap_or_else(|| Rational::new(0, 1)) } -/// `block_set_length_and_media_out`. -pub fn block_set_length_and_media_out(b: CHandle, len: Rational) { - let (mut n, mut d) = (0, 0); - rat_nd(len, &mut n, &mut d); - // SAFETY: `n`/`d` are valid ints. - let _ = unsafe { oaknode_block_set_length_and_media_out(b, n, d) }; +/// `block_kind`: which block behavior backs the node (C++ +/// `oaknode_block_get_kind`). +pub fn block_kind(b: &NodeRef) -> BlockKind { + let p = b.lock(); + match p.graph.get(b.id).and_then(|e| e.behavior.as_any()) { + Some(a) if a.downcast_ref::().is_some() => BlockKind::Clip, + Some(a) if a.downcast_ref::().is_some() => BlockKind::Gap, + Some(a) if a.downcast_ref::().is_some() => BlockKind::Transition, + _ => BlockKind::Other, + } } -/// `block_set_length_and_media_in`. -pub fn block_set_length_and_media_in(b: CHandle, len: Rational) { - let (mut n, mut d) = (0, 0); - rat_nd(len, &mut n, &mut d); - // SAFETY: `n`/`d` are valid ints. - let _ = unsafe { oaknode_block_set_length_and_media_in(b, n, d) }; +/// `oaknode_block_get_enabled`: the block's enabled flag (false for a +/// stale node). +pub fn block_enabled(b: &NodeRef) -> bool { + let p = b.lock(); + block_core_of(&p, b.id).map(|c| c.enabled).unwrap_or(false) +} + +/// `oaknode_block_set_enabled`. +pub fn block_set_enabled(b: &NodeRef, enabled: bool) { + let mut p = b.lock(); + if let Some(core) = block_core_of_mut(&mut p, b.id) { + core.enabled = enabled; + } +} + +/// `block_set_length_and_media_out`: keep the out point anchored (the +/// in point shifts). +pub fn block_set_length_and_media_out(b: &NodeRef, len: Rational) { + let mut p = b.lock(); + if let Some(core) = block_core_of_mut(&mut p, b.id) { + core.set_length_and_media_out(len); + } +} + +/// `block_set_length_and_media_in`: keep the in point anchored (the +/// out point shifts). +pub fn block_set_length_and_media_in(b: &NodeRef, len: Rational) { + let mut p = b.lock(); + if let Some(core) = block_core_of_mut(&mut p, b.id) { + core.set_length_and_media_in(len); + } } /// `block_set_in`: set the in point, keeping the length (the out follows). -pub fn block_set_in(b: CHandle, in_: Rational) { - let (mut n, mut d) = (0, 0); - rat_nd(in_, &mut n, &mut d); - // SAFETY: `n`/`d` are valid ints. - let _ = unsafe { oaknode_block_set_in(b, n, d) }; -} - -/// `track_length`: track length as a `Rational`. -pub fn track_length(t: CHandle) -> Rational { - let mut n = 0; - let mut d = 0; - // SAFETY: `n`/`d` are valid out pointers. - let _ = unsafe { oaknode_track_get_length(t, &mut n, &mut d) }; - Rational::new(n as i64, d as i64) -} - -/// `block_previous`: the block before `b` on its track. -pub fn block_previous(b: CHandle) -> CHandle { - let mut out = CHandle::null(); - // SAFETY: `out` is a valid out pointer. - let _ = unsafe { oaknode_block_get_previous(b, &mut out) }; - out -} - -/// `block_next`: the block after `b` on its track. -pub fn block_next(b: CHandle) -> CHandle { - let mut out = CHandle::null(); - // SAFETY: `out` is a valid out pointer. - let _ = unsafe { oaknode_block_get_next(b, &mut out) }; - out -} - -/// `block_track`: the track owning `b`. -pub fn block_track(b: CHandle) -> CHandle { - let mut out = CHandle::null(); - // SAFETY: `out` is a valid out pointer. - let _ = unsafe { oaknode_block_get_track(b, &mut out) }; - out -} - -/// `track_project`: the project graph owning the track's sequence, via -/// `oaknode_track_get_sequence` + `oaknode_sequence_as_node` + -/// `oaknode_node_get_project`. -pub fn track_project(track: CHandle) -> CHandle { - let mut sequence = CHandle::null(); - if unsafe { oaknode_track_get_sequence(track, &mut sequence) } != 0 || sequence.is_null() { - return CHandle::null(); - } - let mut project = CHandle::null(); - // SAFETY: `project` is a valid out pointer; the sequence node view is valid. - let _ = unsafe { oaknode_node_get_project(oaknode_sequence_as_node(sequence), &mut project) }; - project -} - -/// C++ `oaknode_track_append_block` — not exposed by the bridge, so append is -/// synthesized as insert-after the last block (prepend on an empty track). -pub fn track_append_block(track: CHandle, block: CHandle) { - let mut count = 0; - // SAFETY: `count` is a valid out pointer. - let _ = unsafe { oaknode_track_get_block_count(track.clone(), &mut count) }; - if count > 0 { - let mut last = CHandle::null(); - // SAFETY: `last` is a valid out pointer. - let _ = unsafe { oaknode_track_get_block_at(track.clone(), count - 1, &mut last) }; - // SAFETY: `track`/`last` are valid handles. - let _ = unsafe { oaknode_track_insert_block_after(track, block, last) }; - } else { - // SAFETY: valid handles. - let _ = unsafe { oaknode_track_prepend_block(track, block) }; +pub fn block_set_in(b: &NodeRef, in_: Rational) { + let mut p = b.lock(); + if let Some(core) = block_core_of_mut(&mut p, b.id) { + core.set_in(in_); } } -/// C++ `oaknode_track_insert_block_before` — not exposed by the bridge, so -/// insert-before is synthesized as insert-after `next`'s predecessor (prepend -/// when `next` is the first block on the track). -pub fn track_insert_block_before(track: CHandle, block: CHandle, next: CHandle) { - let prev = block_previous(next.clone()); - if prev.is_null() { - // SAFETY: valid handles. - let _ = unsafe { oaknode_track_prepend_block(track, block) }; - } else { - // SAFETY: valid handles. - let _ = unsafe { oaknode_track_insert_block_after(track, block, prev) }; +/// `oaknode_clip_get_media_in`: the block's media-in point. The media +/// fields live on the shared [`BlockCore`], so this works for any block +/// kind (the deleted ABI only exposed it for clips; closest behavior). +pub fn clip_media_in(b: &NodeRef) -> Rational { + let p = b.lock(); + block_core_of(&p, b.id) + .map(|c| c.media_in) + .unwrap_or_else(|| Rational::new(0, 1)) +} + +/// `oaknode_clip_set_media_in`. +pub fn clip_set_media_in(b: &NodeRef, media_in: Rational) { + let mut p = b.lock(); + if let Some(core) = block_core_of_mut(&mut p, b.id) { + core.media_in = media_in; } } -/// `block_add_to_graph`: attach `b` to the project graph owning `track`. +// --------------------------------------------------------------------------- +// Block navigation (track membership) +// --------------------------------------------------------------------------- + +/// `block_previous`: the block before `b` on its track (`None` when +/// trackless or first). +pub fn block_previous(b: &NodeRef) -> Option { + let p = b.lock(); + let track_id = block_core_of(&p, b.id).and_then(|c| c.track)?; + let blocks = &track_behavior_of(&p, track_id)?.blocks; + let index = blocks.iter().position(|id| *id == b.id)?; + if index == 0 { + return None; + } + Some(NodeRef::new(b.project.clone(), blocks[index - 1])) +} + +/// `block_next`: the block after `b` on its track (`None` when trackless +/// or last). +pub fn block_next(b: &NodeRef) -> Option { + let p = b.lock(); + let track_id = block_core_of(&p, b.id).and_then(|c| c.track)?; + let blocks = &track_behavior_of(&p, track_id)?.blocks; + let index = blocks.iter().position(|id| *id == b.id)?; + blocks + .get(index + 1) + .map(|id| NodeRef::new(b.project.clone(), *id)) +} + +/// `block_track`: the track owning `b` (`None` when detached from its +/// track, e.g. after a ripple-remove). +pub fn block_track(b: &NodeRef) -> Option { + let p = b.lock(); + block_core_of(&p, b.id) + .and_then(|c| c.track) + .map(|id| NodeRef::new(b.project.clone(), id)) +} + +// --------------------------------------------------------------------------- +// Track queries / mutations +// --------------------------------------------------------------------------- + +/// `track_length`: track length as a `Rational` (end of the last block). +pub fn track_length(t: &NodeRef) -> Rational { + let p = t.lock(); + match track_behavior_of(&p, t.id) { + Some(track) => track.length(&GraphBlockRange { graph: &p.graph }), + None => Rational::new(0, 1), + } +} + +/// `oaknode_track_get_block_count`. +pub fn track_block_count(t: &NodeRef) -> usize { + let p = t.lock(); + track_behavior_of(&p, t.id).map(|t| t.blocks.len()).unwrap_or(0) +} + +/// `oaknode_track_get_block_at`: block at `index` on the track. +pub fn track_block_at(t: &NodeRef, index: usize) -> Option { + let p = t.lock(); + track_behavior_of(&p, t.id) + .and_then(|t| t.blocks.get(index)) + .map(|id| NodeRef::new(t.project.clone(), *id)) +} + +/// `oaknode_track_get_locked`. +pub fn track_locked(t: &NodeRef) -> bool { + let p = t.lock(); + track_behavior_of(&p, t.id).map(|t| t.locked).unwrap_or(false) +} + +/// `oaknode_track_get_nearest_block_before_or_at`: the block containing +/// `time`, else the last block entirely before it. `None` when the track +/// is empty or `time` precedes the first block. /// -/// The block handle is passed through directly (not `oaknode_block_as_node`): -/// `oaknode_project_add_node` rewrites the shared node box it receives -/// (`write_node_ref`) to the new project, so later track operations on `b` -/// (which adopt blocks by comparing the handle's project) see the up-to-date -/// ownership instead of faulting with `E_NOT_FOUND` on a stale scratch id. -pub fn block_add_to_graph(b: CHandle, track: CHandle) { - let project = track_project(track); - if !project.is_null() { - // SAFETY: `project` is a valid handle; `b` is a node/block handle. - let _ = unsafe { oaknode_project_add_node(project, b) }; +/// CPP-PARITY timelineundoripple.cpp:64. +pub fn track_nearest_block_before_or_at(t: &NodeRef, time: Rational) -> Option { + let p = t.lock(); + let blocks = track_behavior_of(&p, t.id)?.blocks.clone(); + let mut before: Option = None; + for id in blocks { + let core = block_core_of(&p, id)?; + if core.in_() <= time && core.out() > time { + return Some(NodeRef::new(t.project.clone(), id)); + } + if core.out() <= time { + before = Some(id); + } + } + before.map(|id| NodeRef::new(t.project.clone(), id)) +} + +/// `oaknode_track_get_nearest_block_after_or_at`: the block containing +/// `time`, else the first block starting at/after it. +/// +/// CPP-PARITY timelineundoripple.cpp:551. +pub fn track_nearest_block_after_or_at(t: &NodeRef, time: Rational) -> Option { + let p = t.lock(); + let blocks = track_behavior_of(&p, t.id)?.blocks.clone(); + let mut after: Option = None; + for id in blocks { + let core = block_core_of(&p, id)?; + if core.in_() <= time && core.out() > time { + return Some(NodeRef::new(t.project.clone(), id)); + } + if core.in_() >= time { + after = Some(id); + break; + } + } + after.map(|id| NodeRef::new(t.project.clone(), id)) +} + +/// C++ `oaknode_track_append_block` — append a block at the end of the +/// track's block list. +pub fn track_append_block(track: &NodeRef, block: &NodeRef) { + let mut p = track.lock(); + let ok = track_behavior_of_mut(&mut p, track.id) + .map(|t| { + t.append_block(block.id); + true + }) + .unwrap_or(false); + if ok { + if let Some(core) = block_core_of_mut(&mut p, block.id) { + core.track = Some(track.id); + } } } -/// `block_remove_from_graph`: detach `b` from the project graph owning -/// `track`. -/// -/// Like [`block_add_to_graph`], `b` is passed through so `write_node_ref` -/// re-homes the shared handle into the scratch project (the caller's handle -/// becomes the owner again, matching the C++ `setParent(&memory_manager_)`). -pub fn block_remove_from_graph(b: CHandle, track: CHandle) { - let project = track_project(track); - if !project.is_null() { - // SAFETY: `project` is a valid handle; `b` is a node/block handle. - let _ = unsafe { oaknode_project_remove_node(project, b) }; +/// C++ `oaknode_track_insert_block_before`: insert `block` before `next` +/// on the track (prepend when `next` is absent). +pub fn track_insert_block_before(track: &NodeRef, block: &NodeRef, next: &NodeRef) { + let mut p = track.lock(); + let ok = track_behavior_of_mut(&mut p, track.id) + .map(|t| t.insert_block_before(block.id, next.id)) + .unwrap_or(false); + if ok { + if let Some(core) = block_core_of_mut(&mut p, block.id) { + core.track = Some(track.id); + } } } + +/// C++ `oaknode_track_insert_block_after`: insert `block` after `before` +/// on the track. `None` for `before` prepends (the deleted ABI tolerated +/// an empty predecessor like C++ did). +pub fn track_insert_block_after(track: &NodeRef, block: &NodeRef, before: Option<&NodeRef>) { + let mut p = track.lock(); + let ok = track_behavior_of_mut(&mut p, track.id) + .map(|t| match before { + Some(b) => t.insert_block_after(block.id, b.id), + None => { + t.prepend_block(block.id); + true + } + }) + .unwrap_or(false); + if ok { + if let Some(core) = block_core_of_mut(&mut p, block.id) { + core.track = Some(track.id); + } + } +} + +/// C++ `oaknode_track_prepend_block`. +pub fn track_prepend_block(track: &NodeRef, block: &NodeRef) { + let mut p = track.lock(); + let ok = track_behavior_of_mut(&mut p, track.id) + .map(|t| { + t.prepend_block(block.id); + true + }) + .unwrap_or(false); + if ok { + if let Some(core) = block_core_of_mut(&mut p, block.id) { + core.track = Some(track.id); + } + } +} + +/// C++ `oaknode_track_replace_block`: swap `old` for `replace` at the +/// same track position. `old` loses its track membership, `replace` +/// gains it. +pub fn track_replace_block(track: &NodeRef, old: &NodeRef, replace: &NodeRef) { + let mut p = track.lock(); + let ok = track_behavior_of_mut(&mut p, track.id) + .map(|t| t.replace_block(old.id, replace.id)) + .unwrap_or(false); + if ok { + if let Some(core) = block_core_of_mut(&mut p, old.id) { + core.track = None; + } + if let Some(core) = block_core_of_mut(&mut p, replace.id) { + core.track = Some(track.id); + } + } +} + +/// C++ `oaknode_track_ripple_remove_block`: remove the block from its +/// track. In the module model the block's in/out points are stored on +/// the block (not derived from track order), so "ripple" is only the +/// membership removal — positions of later blocks stay as stored, which +/// the undo re-insertion mirrors exactly (CPP-PARITY deviation). +pub fn track_ripple_remove_block(track: &NodeRef, block: &NodeRef) { + let mut p = track.lock(); + let ok = track_behavior_of_mut(&mut p, track.id) + .map(|t| t.ripple_remove_block(block.id)) + .unwrap_or(false); + if ok { + if let Some(core) = block_core_of_mut(&mut p, block.id) { + core.track = None; + } + } +} + +// --------------------------------------------------------------------------- +// Project graph attach / detach (C++ `setParent(&memory_manager_)` paths) +// --------------------------------------------------------------------------- + +/// `block_add_to_graph`: re-insert a previously detached block entry +/// into the project graph, preserving its identity where the original +/// slot is free (C++ re-parenting from the scratch memory manager back +/// into the project). Passing `None` is a no-op — blocks created via +/// [`block_gap_create`]/[`block_clip_create`] are already in the graph. +pub fn block_add_to_graph(block: &NodeRef, entry: Option) { + if let Some(entry) = entry { + let mut p = block.lock(); + p.graph.add_entry(entry, block.id); + } +} + +/// `block_remove_from_graph`: detach `block` from the project graph, +/// returning its full entry for the caller to own (and re-add with +/// [`block_add_to_graph`]). `None` when the node is stale or already +/// detached. +pub fn block_remove_from_graph(block: &NodeRef) -> Option { + let mut p = block.lock(); + p.graph.take_node(block.id) +} + +// --------------------------------------------------------------------------- +// Block / track / track-list creation +// --------------------------------------------------------------------------- + +/// C++ `oaknode_block_clip_create`: a fresh clip block node in the given +/// project's graph. +pub fn block_clip_create(project: &Arc>) -> NodeRef { + let (core, behavior) = oaknode::block::clip_create(); + let id = { + let mut p = project + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + p.graph.add_node(core, behavior) + }; + NodeRef::new(project.clone(), id) +} + +/// C++ `oaknode_block_gap_create`: a fresh gap block node in the given +/// project's graph. +pub fn block_gap_create(project: &Arc>) -> NodeRef { + let (core, behavior) = oaknode::block::gap_create(); + let id = { + let mut p = project + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + p.graph.add_node(core, behavior) + }; + NodeRef::new(project.clone(), id) +} + +/// C++ `oaknode_track_create`: a fresh track node of the given type in +/// the project's graph (detached from any track list until appended). +pub fn track_create(project: &Arc>, kind: TrackType) -> NodeRef { + let core = NodeCore::new(); + let behavior = Box::new(TrackBehavior::new(kind)); + let id = { + let mut p = project + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + p.graph.add_node(core, behavior) + }; + NodeRef::new(project.clone(), id) +} + +// --------------------------------------------------------------------------- +// Track list / sequence queries +// --------------------------------------------------------------------------- + +/// `oaknode_tracklist_get_track_count`. +pub fn tracklist_track_count(list: &NodeRef) -> usize { + let p = list.lock(); + tracklist_behavior_of(&p, list.id) + .map(|l| l.tracks.len()) + .unwrap_or(0) +} + +/// `oaknode_tracklist_get_track_at`: track at `index` in the list. +pub fn tracklist_track_at(list: &NodeRef, index: usize) -> Option { + let p = list.lock(); + tracklist_behavior_of(&p, list.id) + .and_then(|l| l.tracks.get(index)) + .map(|id| NodeRef::new(list.project.clone(), *id)) +} + +/// `oaknode_tracklist_get_type`: the list's media type (`None` for a +/// stale node). +pub fn tracklist_type(list: &NodeRef) -> Option { + let p = list.lock(); + tracklist_behavior_of(&p, list.id).map(|l| l.kind) +} + +/// `oaknode_tracklist_array_append`: append `track` to the list (sets the +/// track's list membership and its index). +pub fn tracklist_append(list: &NodeRef, track: &NodeRef) { + let mut p = list.lock(); + let index = tracklist_behavior_of_mut(&mut p, list.id) + .map(|l| { + l.tracks.push(track.id); + l.tracks.len() as i32 - 1 + }) + .unwrap_or(-1); + if index >= 0 { + if let Some(t) = track_behavior_of_mut(&mut p, track.id) { + t.track_list = Some(list.id); + t.index = index; + } + } +} + +/// `oaknode_tracklist_array_remove_last`: drop the last track from the +/// list (detaching its list membership; the node itself is removed from +/// the graph separately by the caller). +pub fn tracklist_remove_last(list: &NodeRef) -> Option { + let mut p = list.lock(); + let removed = tracklist_behavior_of_mut(&mut p, list.id) + .and_then(|l| l.tracks.pop())?; + if let Some(t) = track_behavior_of_mut(&mut p, removed) { + t.track_list = None; + t.index = 0; + } + Some(NodeRef::new(list.project.clone(), removed)) +} + +/// `oaknode_sequence_get_track_list`: the sequence's track list of the +/// given type. +pub fn sequence_track_list(sequence: &NodeRef, kind: TrackType) -> Option { + let p = sequence.lock(); + let lists = sequence_behavior_of(&p, sequence.id)?.track_lists.clone(); + for id in lists { + if tracklist_behavior_of(&p, id).map(|l| l.kind) == Some(kind) { + return Some(NodeRef::new(sequence.project.clone(), id)); + } + } + None +} + +/// `oaknode_sequence_get_all_track_count` / `get_all_track_at`: every +/// track of every track list owned by the sequence, in list order. +pub fn sequence_all_tracks(sequence: &NodeRef) -> Vec { + let p = sequence.lock(); + let mut tracks = Vec::new(); + if let Some(seq) = sequence_behavior_of(&p, sequence.id) { + let lists = seq.track_lists.clone(); + for list_id in lists { + if let Some(list) = tracklist_behavior_of(&p, list_id) { + for track_id in &list.tracks { + tracks.push(NodeRef::new(sequence.project.clone(), *track_id)); + } + } + } + } + tracks +} diff --git a/crates/oaktimeline/src/workarea.rs b/crates/oaktimeline/src/workarea.rs index 0457f86d8..b54f221a0 100644 --- a/crates/oaktimeline/src/workarea.rs +++ b/crates/oaktimeline/src/workarea.rs @@ -22,6 +22,7 @@ //! job. use oakcore_rs::{Rational, TimeRange}; +use oakundo::undocommand::UndoCommand; /// The reset sentinel `k_reset_in` (timelineworkarea.h): 0/1. Exposed as a /// function because `Rational::new` is not yet `const` in oakcore-rs. @@ -125,7 +126,7 @@ impl WorkareaSetEnabledCommand { } /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> crate::handle::CHandle { + pub fn to_command(self) -> UndoCommand { crate::undocommon::box_command(self) } } @@ -163,8 +164,8 @@ impl WorkareaSetRangeCommand { } /// Construct from work area + new range + explicitly supplied old - /// range (used by the FFI layer, which may have a previously captured - /// range; `new` delegates here with the current range). + /// range (used by callers that may have a previously captured range; + /// `new` delegates here with the current range). pub fn new_with_old( workarea: crate::handle::CHandle, range: TimeRange, @@ -192,7 +193,7 @@ impl WorkareaSetRangeCommand { } /// Wrap as an oakundo vtable command handle. - pub fn to_command(self) -> crate::handle::CHandle { + pub fn to_command(self) -> UndoCommand { crate::undocommon::box_command(self) } } diff --git a/crates/oaktimeline/tests/bridge_test.rs b/crates/oaktimeline/tests/bridge_test.rs deleted file mode 100644 index 5659353ff..000000000 --- a/crates/oaktimeline/tests/bridge_test.rs +++ /dev/null @@ -1,305 +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 . - -//! Contract tests for the C ABI bridge layer (`src/bridge/`). The bridge -//! declares the oaknode / oakundo / oakcommon foreign functions this crate -//! calls for cross-module work (blocks, tracks, undo handles, XML). The -//! exhaustive cross-module behaviour is owned by the C++ gtest suites of -//! those modules; here we pin that the symbols the crate depends on are -//! declared with the documented C ABI signature and that the bridge's own -//! helpers are wired to the right functions. -//! -//! Rational ↔ num/den and Rational ↔ string conversion are NOT bridge C ABI -//! functions (the bridge only carries XML + config): num/den conversion is -//! `util::rat_nd` and the string format lives on `oakcore_rs::Rational`, so -//! those tests pin the in-crate helpers directly. -//! -//! These tests call the `extern "C"` symbols, which are only defined under -//! `--features test-stubs` (the in-crate mocks in `src/bridge/teststubs.rs`); -//! run with that feature. The parts that build mock XML readers through -//! `teststubs::xml_reader_handle` are additionally gated `#[cfg(feature = -//! "test-stubs")]`. - -use oakcore_rs::Rational; -use oaktimeline::handle::CHandle; -use oaktimeline::util; - -/// The undo bridge declares a `make_command_handle`-style constructor that -/// boxes a command into the C ABI handle shape, and a `free` that runs the -/// caller's `free_fn` before clearing the handle. -#[test] -fn undo_bridge_constructs_handle() { - use oaktimeline::bridge::undo::{ - oakundo_command_free, oakundo_command_init, OakUndoCommandVtable, - }; - use oaktimeline::error::OAKTIMELINE_ABI_VERSION; - use std::ffi::c_void; - - unsafe extern "C" fn free_u32(ud: *mut c_void) { - drop(Box::from_raw(ud as *mut u32)); - } - - let vtable = OakUndoCommandVtable { - redo: None, - undo: None, - free_fn: Some(free_u32), - }; - let userdata = Box::into_raw(Box::new(7u32)) as *mut c_void; - let mut h = unsafe { oakundo_command_init(&vtable, userdata) }; - assert!(!h.is_null(), "init must return a non-null command handle"); - assert_eq!(h.abi_version, OAKTIMELINE_ABI_VERSION); - // Freeing runs `free_fn` (dropping the boxed u32) and clears the handle. - unsafe { oakundo_command_free(&mut h) }; - assert!(h.is_null(), "free must clear the handle"); -} - -/// The undo bridge frees a command handle and tolerates a null handle -/// (free(NULL) no-op), matching `oakundo_capi::free_command_handle`. -#[test] -fn undo_bridge_free_handle_is_null_safe() { - use oaktimeline::bridge::undo::oakundo_command_free; - - // NULL pointer: no-op, no panic. - unsafe { oakundo_command_free(std::ptr::null_mut()) }; - // Empty handle: no-op, no panic, stays empty. - let mut h = CHandle::null(); - unsafe { oakundo_command_free(&mut h) }; - assert!(h.is_null()); -} - -/// The node bridge exposes the block/track accessors this crate calls for -/// edit commands (in/out/length, previous/next, track, track length). -#[cfg(feature = "test-stubs")] -#[test] -fn node_bridge_exposes_block_geometry() { - use oaktimeline::bridge::node::{ - oaknode_block_clip_create, oaknode_track_create, oaknode_track_prepend_block, - }; - - let b = unsafe { oaknode_block_clip_create() }; - assert!(!b.is_null()); - // Default in/out/length are 0/1. - assert_eq!(util::block_in(b.clone()), Rational::new(0, 1)); - assert_eq!(util::block_out(b.clone()), Rational::new(0, 1)); - assert_eq!(util::block_length(b.clone()), Rational::new(0, 1)); - // Detached block: no previous / next / owning track. - assert!(util::block_previous(b.clone()).is_null()); - assert!(util::block_next(b.clone()).is_null()); - assert!(util::block_track(b.clone()).is_null()); - - // Attach two blocks to a track; prepend puts each at the front. - let t = unsafe { oaknode_track_create(0) }; - assert!(!t.is_null()); - let b2 = unsafe { oaknode_block_clip_create() }; - assert_eq!( - unsafe { oaknode_track_prepend_block(t.clone(), b.clone()) }, - 0 - ); - assert_eq!( - unsafe { oaknode_track_prepend_block(t.clone(), b2.clone()) }, - 0 - ); - - // b2 is now the front block: it owns the track, has no previous, and b1 - // follows it. - assert!(!util::block_track(b2.clone()).is_null()); - assert!(util::block_previous(b2.clone()).is_null()); - assert!(!util::block_next(b2.clone()).is_null()); - // b1 is the tail: it has a previous (b2) and no next. - assert!(util::block_next(b.clone()).is_null()); - assert!(!util::block_previous(b.clone()).is_null()); - assert!(!util::block_track(b.clone()).is_null()); - // Track length sums the block lengths (both default 0/1). - assert_eq!(util::track_length(t), Rational::new(0, 1)); -} - -/// The node bridge mutators (`block_set_length_and_media_out` / -/// `_media_in`) are the single path through which resize commands change -/// block geometry; each keeps the non-fixed edge fixed. -#[cfg(feature = "test-stubs")] -#[test] -fn node_bridge_resize_mutators_wire() { - use oaktimeline::bridge::node::{oaknode_block_clip_create, oaknode_clip_get_media_in}; - - fn media_in(b: &CHandle) -> Rational { - let mut n = 0; - let mut d = 0; - let _ = unsafe { oaknode_clip_get_media_in(b.clone(), &mut n, &mut d) }; - Rational::new(n as i64, d as i64) - } - - let b = unsafe { oaknode_block_clip_create() }; - // Default media-in and out are both 0/1. - assert_eq!(media_in(&b), Rational::new(0, 1)); - - // media_out variant: growing the length moves the out point, media-in - // stays fixed. - util::block_set_length_and_media_out(b.clone(), Rational::new(10, 1)); - assert_eq!(util::block_length(b.clone()), Rational::new(10, 1)); - assert_eq!(util::block_out(b.clone()), Rational::new(10, 1)); - assert_eq!(media_in(&b), Rational::new(0, 1)); - - // media_in variant: shrinking the length keeps the out point fixed and - // pulls media-in up. - util::block_set_length_and_media_in(b.clone(), Rational::new(4, 1)); - assert_eq!(util::block_length(b.clone()), Rational::new(4, 1)); - assert_eq!(util::block_out(b.clone()), Rational::new(10, 1)); - assert_eq!(media_in(&b), Rational::new(6, 1)); -} - -/// Rational ↔ num/den conversion crosses the C ABI as an `int` pair via -/// `timelineutil::rat_nd` (`util::rat_nd`) — not through `bridge::common`, -/// which only carries XML + config. We pin the reduction semantics here. -#[test] -fn common_bridge_rational_conversion() { - let r = Rational::new(30, 4); // reduces to 15/2 - let mut n = 0; - let mut d = 0; - util::rat_nd(r, &mut n, &mut d); - assert_eq!(n, 15); - assert_eq!(d, 2); -} - -/// A zero denominator normalizes to the null/NaN sentinel `0/0` -/// (`Rational::new(num, 0)`), so a num/den pair with `den == 0` is invalid -/// rather than converted. -#[test] -fn common_bridge_rejects_zero_denominator() { - let r = Rational::new(5, 0); - assert!(r.is_null()); - assert!(r.is_nan()); - assert_eq!(r.denominator(), 0); - // rat_nd maps the sentinel to a 0/0 int pair. - let mut n = 1; - let mut d = 1; - util::rat_nd(r, &mut n, &mut d); - assert_eq!((n, d), (0, 0)); -} - -/// Rational ↔ display-string uses `oakcore_rs::Rational::from_string` / -/// `to_display_string` (the C++ `fromString`/`toString` text format, e.g. -/// "30000/1001"). `bridge::common` does not map strings; we pin the format -/// the project files rely on. -#[test] -fn common_bridge_rational_string_round_trip() { - let s = "30000/1001"; - let r = Rational::from_string(s); - assert_eq!(r, Rational::new(30000, 1001)); - assert_eq!(r.to_display_string(), s); - // The null sentinel round-trips as "0/0". - assert_eq!(Rational::NULL.to_display_string(), "0/0"); - assert!(Rational::from_string("0/0").is_null()); -} - -/// The XML reader bridge declares the load symbols the marker and work area -/// save paths call; a null reader is an invalid argument and the reader is -/// released by `free`. -#[cfg(feature = "test-stubs")] -#[test] -fn xml_bridge_validate_handles() { - use oaktimeline::bridge::common::{ - oakcommon_xml_reader_free, oakcommon_xml_reader_init, oakcommon_xml_reader_name, - oakcommon_xml_reader_read_element_text, oakcommon_xml_reader_read_next_start_element, - }; - - // A null reader is an invalid argument: read fails cleanly (0). - let mut buf = [0i8; 64]; - let mut found = 0; - assert_eq!( - unsafe { oakcommon_xml_reader_read_next_start_element(CHandle::null(), &mut found) }, - 0 - ); - - // `init` over a document yields an empty reader in the mock; with no - // elements it reports end-of-stream for every accessor. - let mut reader = - unsafe { oakcommon_xml_reader_init(b"\0".as_ptr() as *const std::ffi::c_char) }; - assert!(!reader.is_null()); - let mut found = 0; - assert_eq!( - unsafe { oakcommon_xml_reader_read_next_start_element(reader.clone(), &mut found) }, - 0 - ); - assert_eq!( - unsafe { oakcommon_xml_reader_name(reader.clone(), buf.as_mut_ptr(), 64) }, - 0 - ); - assert_eq!( - unsafe { oakcommon_xml_reader_read_element_text(reader.clone(), buf.as_mut_ptr(), 64) }, - 0 - ); - - // Free clears the handle; freeing the null handle is a no-op. - unsafe { oakcommon_xml_reader_free(&mut reader) }; - assert!(reader.is_null()); - - // With mock elements (feature-gated), the reader iterates them and - // exposes name / text / attributes through the same externs. - #[cfg(feature = "test-stubs")] - { - use oaktimeline::bridge::teststubs::{xml_reader_handle, MockXmlNode}; - - let mut r = xml_reader_handle(vec![MockXmlNode { - name: "marker".to_string(), - text: "hello".to_string(), - attrs: vec![("color".to_string(), "red".to_string())], - }]); - let mut found = 0; - assert_eq!( - unsafe { oakcommon_xml_reader_read_next_start_element(r.clone(), &mut found) }, - 1 - ); - assert_eq!(found, 1); - assert_eq!( - unsafe { oakcommon_xml_reader_name(r.clone(), buf.as_mut_ptr(), 64) }, - 1 - ); - assert_eq!( - unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } - .to_str() - .unwrap(), - "marker" - ); - assert_eq!( - unsafe { oakcommon_xml_reader_read_element_text(r.clone(), buf.as_mut_ptr(), 64) }, - 1 - ); - assert_eq!( - unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } - .to_str() - .unwrap(), - "hello" - ); - unsafe { oakcommon_xml_reader_free(&mut r) }; - assert!(r.is_null()); - } -} - -/// Build-graph invariant. The original scaffold claimed every symbol from -/// `oakcore-rs` (`Rational`, `TimeRange`) is reached only through the C ABI -/// bridge. That is not the case: the crate depends on `oakcore-rs` directly -/// (`Cargo.toml [dependencies]`, used by `src/util.rs`), and the bridge does -/// not re-export `Rational`. This is a link/build-topology property that -/// cannot be asserted from a runtime unit test — it is enforced by -/// `cargo build`/`cargo tree` — so it is excluded here. The body pins the -/// actually-true invariant that `Rational` reduction is available in-crate. -#[test] -#[ignore = "build-topology property; asserted by cargo tree/build, not at runtime"] -fn core_is_reached_only_through_bridge() { - let r = Rational::new(2, 4); - assert_eq!(r.numerator(), 1); - assert_eq!(r.denominator(), 2); -} diff --git a/crates/oaktimeline/tests/cov_bridge.rs b/crates/oaktimeline/tests/cov_bridge.rs deleted file mode 100644 index 58d2254b0..000000000 --- a/crates/oaktimeline/tests/cov_bridge.rs +++ /dev/null @@ -1,703 +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 . - -//! Direct coverage of the in-crate C ABI mocks (`src/bridge/teststubs.rs`) -//! and the bridge externs: error paths (null / wrong-kind handles), -//! no-op callbacks and the shared helpers. These exercise the same `extern -//! "C"` symbols the crate's commands call, so they pin the bridge contract -//! and cover the mock's defensive branches. -#![cfg(feature = "test-stubs")] - -use std::ffi::{c_char, CString}; - -use oaktimeline::bridge::common::{ - oakcommon_xml_reader_attribute_count, oakcommon_xml_reader_attribute_name, - oakcommon_xml_reader_attribute_value, oakcommon_xml_reader_has_error, - oakcommon_xml_reader_name, oakcommon_xml_reader_read_element_text, - oakcommon_xml_writer_write_characters, oakcommon_xml_writer_write_end_document, -}; -use oaktimeline::bridge::node::{ - oaknode_block_are_linked, oaknode_block_as_node, oaknode_block_clip_create, - oaknode_block_from_node, oaknode_block_gap_create, oaknode_block_get_enabled, - oaknode_block_get_in, oaknode_block_get_kind, oaknode_block_get_length, oaknode_block_get_next, - oaknode_block_get_out, oaknode_block_get_previous, oaknode_block_get_track, oaknode_block_link, - oaknode_block_set_enabled, oaknode_block_set_length_and_media_in, - oaknode_block_set_length_and_media_out, oaknode_block_unlink, - oaknode_clip_add_cache_passthrough_from, oaknode_clip_get_media_in, oaknode_clip_set_media_in, - oaknode_command_create_remove_node, oaknode_node_connect, oaknode_node_copy_in_graph, - oaknode_node_disconnect, oaknode_node_get_markers, oaknode_node_get_project, - oaknode_node_get_work_area, oaknode_node_output_connection_count, oaknode_project_add_node, - oaknode_project_remove_node, oaknode_sequence_as_node, oaknode_sequence_from_node, - oaknode_sequence_get_all_track_at, oaknode_sequence_get_all_track_count, - oaknode_sequence_get_track_list, oaknode_track_create, oaknode_track_get_block_at, - oaknode_track_get_block_containing_time, oaknode_track_get_block_count, - oaknode_track_get_length, oaknode_track_get_locked, - oaknode_track_get_nearest_block_after_or_at, oaknode_track_get_nearest_block_before_or_at, - oaknode_track_get_sequence, oaknode_track_insert_block_after, oaknode_track_prepend_block, - oaknode_track_replace_block, oaknode_track_ripple_remove_block, oaknode_track_set_locked, - oaknode_tracklist_array_append, oaknode_tracklist_array_remove_last, - oaknode_tracklist_get_track_at, oaknode_tracklist_get_track_count, oaknode_tracklist_get_type, -}; -use oaktimeline::bridge::teststubs::{xml_reader_handle, MockKind, MockNode, MockUndoStack}; -use oaktimeline::bridge::undo::{ - oakundo_command_init, oakundo_command_init_multi, oakundo_command_multi_add_child, - oakundo_command_redo_now, oakundo_command_undo_now, oakundo_stack_push, OakUndoCommandVtable, -}; -use oaktimeline::handle::{get, get_mut, make_owned, CHandle}; - -fn make(kind: MockKind) -> CHandle { - make_owned(MockNode { - kind, - ..Default::default() - }) -} - -fn addr(h: &CHandle) -> *mut MockNode { - unsafe { get_mut::(h).unwrap() as *mut MockNode } -} - -// ---- undo mocks -------------------------------------------------------- - -/// `oakundo_command_init` with a null vtable returns an empty handle. -#[test] -fn undo_init_null_vtable() { - let h = unsafe { oakundo_command_init(std::ptr::null(), std::ptr::null_mut()) }; - assert!(h.is_null()); -} - -/// `oakundo_command_init_multi` + `multi_add_child` box a no-op command. -#[test] -fn undo_multi_helpers() { - let multi = unsafe { oakundo_command_init_multi() }; - assert!(!multi.is_null()); - let child = unsafe { oakundo_command_init_multi() }; - assert_eq!( - unsafe { oakundo_command_multi_add_child(multi.clone(), child) }, - 0 - ); - unsafe { oakundo_command_redo_now(multi.clone()) }; - unsafe { oakundo_command_undo_now(multi) }; -} - -/// `oakundo_stack_push` records the push in the stack's mock. -#[test] -fn undo_stack_push_records() { - let stack = make_owned(MockUndoStack { pushes: 0 }); - let cmd = unsafe { oakundo_command_init_multi() }; - let text = CString::new("trim").unwrap(); - assert_eq!( - unsafe { oakundo_stack_push(stack.clone(), cmd, text.as_ptr()) }, - 0 - ); - assert_eq!(unsafe { get::(&stack) }.unwrap().pushes, 1); -} - -// ---- XML mocks --------------------------------------------------------- - -/// Reader attribute/name accessors on missing state return error codes. -#[test] -fn xml_reader_accessors_missing() { - let mut buf = [0 as c_char; 16]; - // No current element: every accessor returns 0. - let mut r = xml_reader_handle(Vec::new()); - let mut found = 0; - unsafe { - oaktimeline::bridge::common::oakcommon_xml_reader_read_next_start_element( - r.clone(), - &mut found, - ) - }; - assert_eq!( - unsafe { oakcommon_xml_reader_name(r.clone(), buf.as_mut_ptr(), 16) }, - 0 - ); - assert_eq!( - unsafe { oakcommon_xml_reader_read_element_text(r.clone(), buf.as_mut_ptr(), 16) }, - 0 - ); - - // Attribute accessors with a null reader and out-of-range index. - let mut count = 99; - unsafe { oakcommon_xml_reader_attribute_count(CHandle::null(), &mut count) }; - assert_eq!(count, 0); - assert_eq!( - unsafe { oakcommon_xml_reader_attribute_name(CHandle::null(), 0, buf.as_mut_ptr(), 16) }, - 0 - ); - assert_eq!( - unsafe { oakcommon_xml_reader_attribute_value(CHandle::null(), 0, buf.as_mut_ptr(), 16) }, - 0 - ); - - // With an element but no attributes: count 0, name/value fail. - let mut r2 = xml_reader_handle(vec![oaktimeline::bridge::teststubs::MockXmlNode { - name: "m".to_string(), - text: String::new(), - attrs: Vec::new(), - }]); - let mut found = 0; - unsafe { - oaktimeline::bridge::common::oakcommon_xml_reader_read_next_start_element( - r2.clone(), - &mut found, - ) - }; - unsafe { oakcommon_xml_reader_attribute_count(r2.clone(), &mut count) }; - assert_eq!(count, 0); - assert_eq!( - unsafe { oakcommon_xml_reader_attribute_name(r2.clone(), 0, buf.as_mut_ptr(), 16) }, - 0 - ); - assert_eq!( - unsafe { oakcommon_xml_reader_attribute_value(r2.clone(), 0, buf.as_mut_ptr(), 16) }, - 0 - ); - - // `has_error` writes the error flag; null reader → 0. - let mut err = 99; - unsafe { oakcommon_xml_reader_has_error(CHandle::null(), &mut err) }; - assert_eq!(err, 0); - unsafe { oakcommon_xml_reader_has_error(r2.clone(), &mut err) }; - assert_eq!(err, 0); -} - -/// Writer characters / end-document accumulate or no-op. -#[test] -fn xml_writer_characters_and_end() { - use oaktimeline::bridge::common::oakcommon_xml_writer_init; - - let w = unsafe { oakcommon_xml_writer_init() }; - let text = CString::new("abc").unwrap(); - assert_eq!( - unsafe { oakcommon_xml_writer_write_characters(w.clone(), text.as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oakcommon_xml_writer_write_end_document(w.clone()) }, - 0 - ); - let buf = unsafe { get::(&w) } - .unwrap() - .buf - .clone(); - assert_eq!(buf, "abc"); -} - -// ---- node mocks: failure paths ----------------------------------------- - -/// Block geometry accessors reject null handles. -#[test] -fn block_geometry_null_handles() { - let mut n = 0; - let mut d = 0; - assert_eq!( - unsafe { oaknode_block_get_in(CHandle::null(), &mut n, &mut d) }, - -1 - ); - assert_eq!( - unsafe { oaknode_block_get_out(CHandle::null(), &mut n, &mut d) }, - -1 - ); - assert_eq!( - unsafe { oaknode_block_get_length(CHandle::null(), &mut n, &mut d) }, - -1 - ); - assert_eq!( - unsafe { oaknode_block_set_length_and_media_out(CHandle::null(), 1, 1) }, - -1 - ); - assert_eq!( - unsafe { oaknode_block_set_length_and_media_in(CHandle::null(), 1, 1) }, - -1 - ); - assert_eq!( - unsafe { oaknode_block_get_enabled(CHandle::null(), &mut n) }, - -1 - ); - assert_eq!(unsafe { oaknode_block_set_enabled(CHandle::null(), 1) }, -1); - - let mut out = CHandle::null(); - assert_eq!( - unsafe { oaknode_block_get_previous(CHandle::null(), &mut out) }, - -1 - ); - assert_eq!( - unsafe { oaknode_block_get_next(CHandle::null(), &mut out) }, - -1 - ); - assert_eq!( - unsafe { oaknode_block_get_track(CHandle::null(), &mut out) }, - -1 - ); -} - -/// `oaknode_block_link`/`unlink` reject invalid handles and link/unlink -/// valid ones. -#[test] -fn block_link_unlink() { - let a = make(MockKind::Clip); - let b = make(MockKind::Clip); - - assert_eq!( - unsafe { oaknode_block_link(CHandle::null(), b.clone()) }, - -1 - ); - assert_eq!( - unsafe { oaknode_block_unlink(a.clone(), CHandle::null()) }, - -1 - ); - - assert_eq!(unsafe { oaknode_block_link(a.clone(), b.clone()) }, 0); - let mut linked = 0; - unsafe { oaknode_block_are_linked(a.clone(), b.clone(), &mut linked) }; - assert_eq!(linked, 1); - - assert_eq!(unsafe { oaknode_block_unlink(a.clone(), b.clone()) }, 0); - unsafe { oaknode_block_are_linked(a.clone(), b.clone(), &mut linked) }; - assert_eq!(linked, 0); - - // are_linked with invalid handles → -1. - assert_eq!( - unsafe { oaknode_block_are_linked(CHandle::null(), b.clone(), &mut linked) }, - -1 - ); -} - -/// Track accessors reject null / wrong-kind handles. -#[test] -fn track_accessors_invalid() { - let clip = make(MockKind::Clip); - let mut n = 0; - let mut d = 0; - let mut out = CHandle::null(); - - assert_eq!( - unsafe { oaknode_track_get_length(CHandle::null(), &mut n, &mut d) }, - -1 - ); - assert_eq!( - unsafe { oaknode_track_get_sequence(CHandle::null(), &mut out) }, - -1 - ); - assert_eq!( - unsafe { oaknode_track_prepend_block(CHandle::null(), clip.clone()) }, - -1 - ); - assert_eq!( - unsafe { oaknode_track_ripple_remove_block(CHandle::null(), clip.clone()) }, - -1 - ); - - let mut locked = 0; - assert_eq!( - unsafe { oaknode_track_get_locked(CHandle::null(), &mut locked) }, - -1 - ); - assert_eq!( - unsafe { oaknode_track_get_locked(clip.clone(), &mut locked) }, - -1 - ); - assert_eq!(unsafe { oaknode_track_set_locked(CHandle::null(), 1) }, -1); - assert_eq!(unsafe { oaknode_track_set_locked(clip.clone(), 1) }, -1); - - let mut count = 0; - assert_eq!( - unsafe { oaknode_track_get_block_count(CHandle::null(), &mut count) }, - -1 - ); - assert_eq!( - unsafe { oaknode_track_get_block_count(clip.clone(), &mut count) }, - -1 - ); - assert_eq!( - unsafe { oaknode_track_get_block_at(CHandle::null(), 0, &mut out) }, - -1 - ); - assert_eq!( - unsafe { oaknode_track_get_block_at(clip.clone(), 0, &mut out) }, - -1 - ); - assert_eq!( - unsafe { oaknode_track_get_block_at(make(MockKind::Track), 3, &mut out) }, - -1 - ); - - // containing-time queries. - assert_eq!( - unsafe { oaknode_track_get_block_containing_time(CHandle::null(), 1, 1, &mut out) }, - -1 - ); - assert_eq!( - unsafe { oaknode_track_get_block_containing_time(clip.clone(), 1, 1, &mut out) }, - -1 - ); - assert_eq!( - unsafe { oaknode_track_get_nearest_block_before_or_at(CHandle::null(), 1, 1, &mut out) }, - -1 - ); - assert_eq!( - unsafe { oaknode_track_get_nearest_block_before_or_at(clip.clone(), 1, 1, &mut out) }, - -1 - ); - assert_eq!( - unsafe { oaknode_track_get_nearest_block_after_or_at(CHandle::null(), 1, 1, &mut out) }, - -1 - ); - assert_eq!( - unsafe { oaknode_track_get_nearest_block_after_or_at(clip.clone(), 1, 1, &mut out) }, - -1 - ); -} - -/// Track geometry success paths: insert-after on an empty track, replace, -/// containing-time and nearest queries, block replacement. -#[test] -fn track_geometry_success() { - let t = make(MockKind::Track); - let a = make(MockKind::Clip); - let b = make(MockKind::Clip); - - // insert-after on an empty track appends (before not found). - assert_eq!( - unsafe { oaknode_track_insert_block_after(t.clone(), a.clone(), b.clone()) }, - 0 - ); - - // Prepend b, then a. - unsafe { oaknode_track_prepend_block(t.clone(), b.clone()) }; - unsafe { oaknode_track_prepend_block(t.clone(), a.clone()) }; - - // Block containing time 5: none (no geometry set) → -1 with null out. - let mut out = CHandle::null(); - assert_eq!( - unsafe { oaknode_track_get_block_containing_time(t.clone(), 5, 1, &mut out) }, - -1 - ); - - // Nearest before/after return the front block by in point. - assert_eq!( - unsafe { oaknode_track_get_nearest_block_before_or_at(t.clone(), 5, 1, &mut out) }, - 0 - ); - assert!(!out.is_null()); - assert_eq!( - unsafe { oaknode_track_get_nearest_block_after_or_at(t.clone(), 5, 1, &mut out) }, - 0 - ); - - // Replace a with b. - let c = make(MockKind::Clip); - assert_eq!( - unsafe { oaknode_track_replace_block(t.clone(), a.clone(), c.clone()) }, - 0 - ); - assert_eq!( - unsafe { oaknode_track_replace_block(t.clone(), make(MockKind::Clip), c.clone()) }, - -1 - ); - assert_eq!( - unsafe { oaknode_track_replace_block(CHandle::null(), a.clone(), c.clone()) }, - -1 - ); - - // Ripple-remove a block not on the track → -1. - let orphan = make(MockKind::Clip); - assert_eq!( - unsafe { oaknode_track_ripple_remove_block(t.clone(), orphan) }, - -1 - ); -} - -/// Track-list accessors reject invalid handles / kinds and return the -/// per-type lists. -#[test] -fn tracklist_and_sequence_accessors() { - let list = make(MockKind::TrackList); - let clip = make(MockKind::Clip); - let mut kind = 99; - let mut count = 99; - let mut out = CHandle::null(); - - assert_eq!( - unsafe { oaknode_tracklist_get_type(CHandle::null(), &mut kind) }, - -1 - ); - assert_eq!( - unsafe { oaknode_tracklist_get_type(clip.clone(), &mut kind) }, - -1 - ); - assert_eq!( - unsafe { oaknode_tracklist_get_track_count(CHandle::null(), &mut count) }, - -1 - ); - assert_eq!( - unsafe { oaknode_tracklist_get_track_count(clip.clone(), &mut count) }, - -1 - ); - assert_eq!( - unsafe { oaknode_tracklist_get_track_at(CHandle::null(), 0, &mut out) }, - -1 - ); - assert_eq!( - unsafe { oaknode_tracklist_get_track_at(clip.clone(), 0, &mut out) }, - -1 - ); - assert_eq!( - unsafe { oaknode_tracklist_get_track_at(list.clone(), 0, &mut out) }, - -1 - ); - - // Append/remove-last are no-ops returning 0. - assert_eq!(unsafe { oaknode_tracklist_array_append(list.clone()) }, 0); - assert_eq!( - unsafe { oaknode_tracklist_array_remove_last(list.clone()) }, - 0 - ); - - let seq = make(MockKind::Sequence); - assert_eq!( - unsafe { oaknode_sequence_get_track_list(CHandle::null(), 0, &mut out) }, - -1 - ); - assert_eq!( - unsafe { oaknode_sequence_get_track_list(clip.clone(), 0, &mut out) }, - -1 - ); - assert_eq!( - unsafe { oaknode_sequence_get_all_track_count(CHandle::null(), &mut count) }, - -1 - ); - assert_eq!( - unsafe { oaknode_sequence_get_all_track_count(clip.clone(), &mut count) }, - -1 - ); - assert_eq!( - unsafe { oaknode_sequence_get_all_track_at(CHandle::null(), 0, &mut out) }, - -1 - ); - assert_eq!( - unsafe { oaknode_sequence_get_all_track_at(seq.clone(), 0, &mut out) }, - -1 - ); - - // sequence_get_track_list finds the matching per-type list. - let video_list = make(MockKind::TrackList); - unsafe { - get_mut::(&video_list).unwrap().track_type = 0; - } - unsafe { - get_mut::(&seq) - .unwrap() - .blocks - .push(addr(&video_list)); - } - assert_eq!( - unsafe { oaknode_sequence_get_track_list(seq.clone(), 0, &mut out) }, - 0 - ); - assert!(!out.is_null()); -} - -/// Node-level accessors: project/markers/work-area/connect/copy. -#[test] -fn node_accessors() { - let clip = make(MockKind::Clip); - let mut out = CHandle::null(); - let mut count = 99; - - assert_eq!( - unsafe { oaknode_node_get_project(CHandle::null(), &mut out) }, - -1 - ); - assert_eq!( - unsafe { oaknode_node_output_connection_count(CHandle::null(), &mut count) }, - -1 - ); - assert_eq!( - unsafe { oaknode_node_get_markers(CHandle::null(), &mut out) }, - -1 - ); - assert_eq!( - unsafe { oaknode_node_get_work_area(CHandle::null(), &mut out) }, - -1 - ); - - // Null markers/work area → null borrowed handle. - unsafe { oaknode_node_get_markers(clip.clone(), &mut out) }; - assert!(out.is_null()); - unsafe { oaknode_node_get_work_area(clip.clone(), &mut out) }; - assert!(out.is_null()); - - // Project add/remove reject null handles. - assert_eq!( - unsafe { oaknode_project_add_node(CHandle::null(), clip.clone()) }, - -1 - ); - assert_eq!( - unsafe { oaknode_project_remove_node(CHandle::null(), clip.clone()) }, - -1 - ); - - // Connect bumps the output connection count; disconnect no-ops. - let a = make(MockKind::Clip); - let b = make(MockKind::Clip); - let input_id = CString::new("tex_in").unwrap(); - assert_eq!( - unsafe { oaknode_node_connect(CHandle::null(), b.clone(), input_id.as_ptr()) }, - -1 - ); - assert_eq!( - unsafe { oaknode_node_connect(a.clone(), b.clone(), input_id.as_ptr()) }, - 0 - ); - assert_eq!( - unsafe { oaknode_node_output_connection_count(a.clone(), &mut count) }, - 0 - ); - assert_eq!(count, 1); - assert_eq!( - unsafe { oaknode_node_disconnect(b.clone(), input_id.as_ptr()) }, - 0 - ); - - // copy_in_graph clones the node; a null node yields a null clone. - assert_eq!( - unsafe { oaknode_node_copy_in_graph(CHandle::null(), &mut out) }.is_null(), - true - ); - let copy = unsafe { oaknode_node_copy_in_graph(a.clone(), &mut out) }; - assert!(!copy.is_null()); - assert!(!out.is_null()); -} - -/// Block-kind/from-node/clip helpers. -#[test] -fn block_kind_and_clip_helpers() { - let clip = make(MockKind::Clip); - let gap = make(MockKind::Gap); - let track = make(MockKind::Track); - let mut kind = 99; - - assert_eq!( - unsafe { oaknode_block_get_kind(CHandle::null(), &mut kind) }, - -1 - ); - assert_eq!( - unsafe { oaknode_block_get_kind(clip.clone(), &mut kind) }, - 0 - ); - assert_eq!(kind, 1); - assert_eq!(unsafe { oaknode_block_get_kind(gap.clone(), &mut kind) }, 0); - assert_eq!(kind, 2); - - // block_from_node: clip/gap views, null otherwise. - assert!(!unsafe { oaknode_block_from_node(clip.clone()) }.is_null()); - assert!(unsafe { oaknode_block_from_node(track.clone()) }.is_null()); - assert!(unsafe { oaknode_block_from_node(CHandle::null()) }.is_null()); - - // clip media-in accessors reject non-clips and null handles. - let mut n = 0; - let mut d = 0; - assert_eq!( - unsafe { oaknode_clip_get_media_in(CHandle::null(), &mut n, &mut d) }, - -1 - ); - assert_eq!( - unsafe { oaknode_clip_get_media_in(gap.clone(), &mut n, &mut d) }, - -1 - ); - assert_eq!( - unsafe { oaknode_clip_set_media_in(CHandle::null(), 1, 1) }, - -1 - ); - assert_eq!(unsafe { oaknode_clip_set_media_in(gap.clone(), 1, 1) }, -1); - assert_eq!(unsafe { oaknode_clip_set_media_in(clip.clone(), 5, 1) }, 0); - assert_eq!( - unsafe { oaknode_clip_get_media_in(clip.clone(), &mut n, &mut d) }, - 0 - ); - assert_eq!((n, d), (5, 1)); - - // Passthrough copy is a no-op returning 0. - assert_eq!( - unsafe { oaknode_clip_add_cache_passthrough_from(clip.clone(), gap.clone()) }, - 0 - ); -} - -/// `oaknode_block_as_node` / `oaknode_sequence_as_node` / -/// `oaknode_sequence_from_node` borrow the node view. -#[test] -fn node_view_helpers() { - let clip = make(MockKind::Clip); - let seq = make(MockKind::Sequence); - - assert!(!unsafe { oaknode_block_as_node(clip.clone()) }.is_null()); - assert!(!unsafe { oaknode_sequence_as_node(seq.clone()) }.is_null()); - assert!(!unsafe { oaknode_sequence_from_node(seq.clone()) }.is_null()); - - // Remove-command factory returns a command handle. - let cmd = unsafe { oaknode_command_create_remove_node(clip.clone()) }; - assert!(!cmd.is_null()); -} - -/// Shared mock helpers: add/sub with differing denominators (exercised via -/// `block_set_length_and_media_*`), and null-pointer writes. -#[test] -fn shared_pair_arithmetic() { - // media_in (0,3) + length (1,2): different denominators. - let b = make(MockKind::Clip); - unsafe { get_mut::(&b).unwrap().media_in = (0, 3) }; - assert_eq!( - unsafe { oaknode_block_set_length_and_media_out(b.clone(), 1, 2) }, - 0 - ); - let out = unsafe { get::(&b) }.unwrap().out; - assert_eq!(out, (3, 6)); // (0*2 + 1*3, 3*2) - - // media_in (1,3) with out (0,3): sub with equal denominators. - let c = make(MockKind::Clip); - unsafe { get_mut::(&c).unwrap().out = (0, 3) }; - assert_eq!( - unsafe { oaknode_block_set_length_and_media_in(c.clone(), 1, 3) }, - 0 - ); - assert_eq!(unsafe { get::(&c) }.unwrap().media_in, (-1, 3)); - - // write_cstr with a null buffer is a no-op; getters tolerate null out. - let mut n = 0; - assert_eq!( - unsafe { oaknode_block_get_length(make(MockKind::Clip), &mut n, std::ptr::null_mut()) }, - 0 - ); - - // Null out pointer to block_get_previous is a no-op returning 0. - assert_eq!( - unsafe { oaknode_block_get_previous(make(MockKind::Clip), std::ptr::null_mut()) }, - 0 - ); -} - -/// `oaknode_track_get_length` accepts any node handle (the mock sums the -/// blocks it owns; a clip owns none). -#[test] -fn track_length_wrong_kind() { - let clip = make(MockKind::Clip); - let mut n = 0; - let mut d = 0; - assert_eq!(unsafe { oaknode_track_get_length(clip, &mut n, &mut d) }, 0); - assert_eq!((n, d), (0, 1)); -} diff --git a/crates/oaktimeline/tests/cov_ffi.rs b/crates/oaktimeline/tests/cov_ffi.rs deleted file mode 100644 index f1976c570..000000000 --- a/crates/oaktimeline/tests/cov_ffi.rs +++ /dev/null @@ -1,640 +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 . - -//! Coverage for the remaining reachable paths of `src/ffi.rs`: the marker -//! command factories on valid/invalid lists, the work-area command -//! factories, the load/save XML paths, the edit-family command factories -//! and the string/`write_cstr` helpers. Run with `--features test-stubs`. -#![cfg(feature = "test-stubs")] - -use std::ffi::{c_char, CString}; - -use oaktimeline::bridge::common::{ - oakcommon_xml_writer_init, oakcommon_xml_writer_write_attribute, - oakcommon_xml_writer_write_end_element, oakcommon_xml_writer_write_start_element, - oakcommon_xml_writer_write_text_element, -}; -use oaktimeline::bridge::teststubs::{xml_reader_handle, MockKind, MockNode, MockXmlNode}; -use oaktimeline::error::{ - OAKTIMELINE_E_FAILED, OAKTIMELINE_E_INVALID, OAKTIMELINE_E_NOT_FOUND, OAKTIMELINE_OK, -}; -use oaktimeline::ffi; -use oaktimeline::handle::{get, make_owned, CHandle}; - -// ---- marker exports ---------------------------------------------------- - -/// `oaktimeline_marker_add` rejects a null list handle. -#[test] -fn marker_add_null_list() { - let name = CString::new("m").unwrap(); - let r = unsafe { - ffi::marker::oaktimeline_marker_add(CHandle::null(), 0, 1, 1, 1, name.as_ptr(), 0) - }; - assert_eq!(r, OAKTIMELINE_E_INVALID); -} - -/// A null name is treated as the empty string. -#[test] -fn marker_add_null_name() { - let h = unsafe { ffi::marker::oaktimeline_marker_list_create() }; - let r = - unsafe { ffi::marker::oaktimeline_marker_add(h.clone(), 0, 1, 1, 1, std::ptr::null(), 0) }; - assert_eq!(r, OAKTIMELINE_OK); - let mut count = 0; - unsafe { ffi::marker::oaktimeline_marker_count(h.clone(), &mut count) }; - assert_eq!(count, 1); -} - -/// `oaktimeline_marker_count` / `marker_at` reject null list handles. -#[test] -fn marker_count_and_at_null_list() { - let mut count = 0; - assert_eq!( - unsafe { ffi::marker::oaktimeline_marker_count(CHandle::null(), &mut count) }, - OAKTIMELINE_E_INVALID - ); - - let mut in_num = 0; - let mut buf = [0 as c_char; 8]; - let r = unsafe { - ffi::marker::oaktimeline_marker_at( - CHandle::null(), - 0, - &mut in_num, - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - buf.as_mut_ptr(), - 8, - ) - }; - assert_eq!(r, OAKTIMELINE_E_INVALID); -} - -/// A null name buffer or zero size is a no-op on the two-stage name write. -#[test] -fn marker_at_null_name_buffer() { - let h = unsafe { ffi::marker::oaktimeline_marker_list_create() }; - let name = CString::new("mark").unwrap(); - unsafe { ffi::marker::oaktimeline_marker_add(h.clone(), 0, 1, 1, 1, name.as_ptr(), 1) }; - - // Null buffer: still reports the needed size. - let needed = unsafe { - ffi::marker::oaktimeline_marker_at( - h.clone(), - 0, - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - 8, - ) - }; - assert_eq!(needed as usize, "mark".len() + 1); - - // Zero-size buffer: no write happens (would otherwise overflow). - let mut buf = [9 as c_char; 4]; - let needed = unsafe { - ffi::marker::oaktimeline_marker_at( - h.clone(), - 0, - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - buf.as_mut_ptr(), - 0, - ) - }; - assert_eq!(needed as usize, "mark".len() + 1); - assert_eq!(buf, [9 as c_char; 4]); -} - -/// The remove/set-time/set-props command factories on a non-empty list. -#[test] -fn marker_command_factories_valid_list() { - let h = unsafe { ffi::marker::oaktimeline_marker_list_create() }; - let name = CString::new("m").unwrap(); - unsafe { ffi::marker::oaktimeline_marker_add(h.clone(), 0, 1, 1, 1, name.as_ptr(), 3) }; - - // In-bounds index: non-null command handles. - assert!(!unsafe { ffi::marker::oaktimeline_marker_remove_at_command(h.clone(), 0) }.is_null()); - assert!( - !unsafe { ffi::marker::oaktimeline_marker_set_time_command(h.clone(), 0, 2, 1, 3, 1) } - .is_null() - ); - - // set_props with color only, name only, and both. - assert!(!unsafe { - ffi::marker::oaktimeline_marker_set_props_command(h.clone(), 0, 4, std::ptr::null()) - } - .is_null()); - assert!(!unsafe { - ffi::marker::oaktimeline_marker_set_props_command(h.clone(), 0, -1, name.as_ptr()) - } - .is_null()); - assert!(!unsafe { - ffi::marker::oaktimeline_marker_set_props_command(h.clone(), 0, 4, name.as_ptr()) - } - .is_null()); - - // Out-of-range index on the one-marker list: E_NOT_FOUND. - assert!(unsafe { ffi::marker::oaktimeline_marker_remove_at_command(h.clone(), 7) }.is_null()); - assert!( - unsafe { ffi::marker::oaktimeline_marker_set_time_command(h.clone(), 7, 2, 1, 3, 1) } - .is_null() - ); - assert!(unsafe { - ffi::marker::oaktimeline_marker_set_props_command(h.clone(), 7, 1, std::ptr::null()) - } - .is_null()); - - // color < 0 with a null name is invalid. - assert!(unsafe { - ffi::marker::oaktimeline_marker_set_props_command(h.clone(), 0, -1, std::ptr::null()) - } - .is_null()); -} - -/// `oaktimeline_marker_list_load` parses `marker` elements with attributes; -/// a null list or reader is rejected. -#[test] -fn marker_list_load_xml() { - // Null list / reader → E_INVALID. - assert_eq!( - unsafe { ffi::marker::oaktimeline_marker_list_load(CHandle::null(), CHandle::null()) }, - OAKTIMELINE_E_INVALID - ); - - let h = unsafe { ffi::marker::oaktimeline_marker_list_create() }; - let reader = xml_reader_handle(vec![MockXmlNode { - name: "marker".to_string(), - text: String::new(), - attrs: vec![ - ("name".to_string(), "one".to_string()), - ("in".to_string(), "30000/1001".to_string()), - ("out".to_string(), "60000/1001".to_string()), - ("color".to_string(), "2".to_string()), - ], - }]); - - let r = unsafe { ffi::marker::oaktimeline_marker_list_load(h.clone(), reader) }; - assert_eq!(r, OAKTIMELINE_OK); - let mut count = 0; - unsafe { ffi::marker::oaktimeline_marker_count(h.clone(), &mut count) }; - assert_eq!(count, 1); -} - -/// `oaktimeline_marker_list_save` writes the marker XML; a null list or -/// writer is rejected. -#[test] -fn marker_list_save_xml() { - assert_eq!( - unsafe { ffi::marker::oaktimeline_marker_list_save(CHandle::null(), CHandle::null()) }, - OAKTIMELINE_E_INVALID - ); - - let h = unsafe { ffi::marker::oaktimeline_marker_list_create() }; - let name = CString::new("m").unwrap(); - unsafe { ffi::marker::oaktimeline_marker_add(h.clone(), 0, 1, 1, 1, name.as_ptr(), 3) }; - - let w = unsafe { oakcommon_xml_writer_init() }; - let r = unsafe { ffi::marker::oaktimeline_marker_list_save(h.clone(), w.clone()) }; - assert_eq!(r, OAKTIMELINE_OK); - let buf = unsafe { get::(&w) } - .unwrap() - .buf - .clone(); - assert!(buf.contains("")); - assert!(buf.contains("name=\"m\"")); -} - -// ---- workarea exports -------------------------------------------------- - -/// `oaktimeline_workarea_of` returns a borrowed handle for a viewer node -/// and rejects null owners. -#[test] -fn workarea_of_borrows() { - let wa_h = make_owned(oaktimeline::workarea::TimelineWorkArea::new()); - let owner = make_owned(MockNode { - kind: MockKind::Node, - work_area: wa_h.clone(), - ..Default::default() - }); - - let borrowed = unsafe { ffi::workarea::oaktimeline_workarea_of(owner.clone()) }; - assert!(!borrowed.is_null()); - assert!(unsafe { ffi::workarea::oaktimeline_workarea_of(CHandle::null()) }.is_null()); -} - -/// `oaktimeline_workarea_get` rejects a null handle. -#[test] -fn workarea_get_null() { - let r = unsafe { - ffi::workarea::oaktimeline_workarea_get( - CHandle::null(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }; - assert_eq!(r, OAKTIMELINE_E_INVALID); -} - -/// `oaktimeline_workarea_get` accepts NULL out params. -#[test] -fn workarea_get_null_out_params() { - let w = unsafe { ffi::workarea::oaktimeline_workarea_create() }; - unsafe { ffi::workarea::oaktimeline_workarea_set_range(w.clone(), 10, 1, 20, 1) }; - let r = unsafe { - ffi::workarea::oaktimeline_workarea_get( - w.clone(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }; - assert_eq!(r, OAKTIMELINE_OK); -} - -/// The work-area command factories (set-range / set-enabled) validate and -/// build command handles. -#[test] -fn workarea_command_factories() { - let w = unsafe { ffi::workarea::oaktimeline_workarea_create() }; - unsafe { ffi::workarea::oaktimeline_workarea_set_range(w.clone(), 10, 1, 20, 1) }; - - assert!(!unsafe { - ffi::workarea::oaktimeline_workarea_set_range_command(w.clone(), 1, 1, 5, 1, 10, 1, 20, 1) - } - .is_null()); - assert!( - !unsafe { ffi::workarea::oaktimeline_workarea_set_enabled_command(w.clone(), 1) }.is_null() - ); - - // Null work area → null command handles. - assert!(unsafe { - ffi::workarea::oaktimeline_workarea_set_range_command( - CHandle::null(), - 1, - 1, - 5, - 1, - 10, - 1, - 20, - 1, - ) - } - .is_null()); - assert!( - unsafe { ffi::workarea::oaktimeline_workarea_set_enabled_command(CHandle::null(), 1) } - .is_null() - ); -} - -/// `oaktimeline_workarea_reset` rejects null out params. -#[test] -fn workarea_reset_null_out_params() { - let r = unsafe { - ffi::workarea::oaktimeline_workarea_reset( - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }; - assert_eq!(r, OAKTIMELINE_E_INVALID); -} - -/// `oaktimeline_workarea_load` reads enabled/in/out elements (and skips -/// unknown ones); null handles are rejected. -#[test] -fn workarea_load_xml() { - assert_eq!( - unsafe { ffi::workarea::oaktimeline_workarea_load(CHandle::null(), CHandle::null()) }, - OAKTIMELINE_E_INVALID - ); - - let w = unsafe { ffi::workarea::oaktimeline_workarea_create() }; - let reader = xml_reader_handle(vec![ - MockXmlNode { - name: "enabled".to_string(), - text: "1".to_string(), - attrs: Vec::new(), - }, - MockXmlNode { - name: "in".to_string(), - text: "30000/1001".to_string(), - attrs: Vec::new(), - }, - MockXmlNode { - name: "out".to_string(), - text: "60000/1001".to_string(), - attrs: Vec::new(), - }, - MockXmlNode { - name: "unknown".to_string(), - text: "x".to_string(), - attrs: Vec::new(), - }, - ]); - let r = unsafe { ffi::workarea::oaktimeline_workarea_load(w.clone(), reader) }; - assert_eq!(r, OAKTIMELINE_OK); -} - -/// `oaktimeline_workarea_save` writes the work-area XML; null handles are -/// rejected. -#[test] -fn workarea_save_xml() { - assert_eq!( - unsafe { ffi::workarea::oaktimeline_workarea_save(CHandle::null(), CHandle::null()) }, - OAKTIMELINE_E_INVALID - ); - - let w = unsafe { ffi::workarea::oaktimeline_workarea_create() }; - unsafe { ffi::workarea::oaktimeline_workarea_set_enabled(w.clone(), 1) }; - let writer = unsafe { oakcommon_xml_writer_init() }; - let r = unsafe { ffi::workarea::oaktimeline_workarea_save(w.clone(), writer.clone()) }; - assert_eq!(r, OAKTIMELINE_OK); - let buf = unsafe { get::(&writer) } - .unwrap() - .buf - .clone(); - assert!(buf.contains("enabled")); -} - -// ---- edit exports ------------------------------------------------------ - -/// `oaktimeline_replace_block_with_gap_command` validates and builds. -#[test] -fn replace_block_with_gap_command() { - let track = make_owned(MockNode { - kind: MockKind::Track, - ..Default::default() - }); - let block = make_owned(MockNode { - kind: MockKind::Clip, - ..Default::default() - }); - - assert!(!unsafe { - ffi::edit::oaktimeline_replace_block_with_gap_command(track.clone(), block.clone()) - } - .is_null()); - assert!(unsafe { - ffi::edit::oaktimeline_replace_block_with_gap_command(CHandle::null(), block.clone()) - } - .is_null()); - assert!(unsafe { - ffi::edit::oaktimeline_replace_block_with_gap_command(track.clone(), CHandle::null()) - } - .is_null()); -} - -/// `oaktimeline_split_preserving_links_command` validates and builds. -#[test] -fn split_preserving_links_command() { - let block = make_owned(MockNode { - kind: MockKind::Clip, - ..Default::default() - }); - let blocks = [block.clone()]; - let nums = [5i64]; - let dens = [1i64]; - - assert!(!unsafe { - ffi::edit::oaktimeline_split_preserving_links_command( - blocks.as_ptr(), - 1, - nums.as_ptr(), - dens.as_ptr(), - 1, - ) - } - .is_null()); - - // Invalid inputs → null handle. - assert!(unsafe { - ffi::edit::oaktimeline_split_preserving_links_command( - std::ptr::null(), - 1, - nums.as_ptr(), - dens.as_ptr(), - 1, - ) - } - .is_null()); - assert!(unsafe { - ffi::edit::oaktimeline_split_preserving_links_command( - blocks.as_ptr(), - 0, - nums.as_ptr(), - dens.as_ptr(), - 1, - ) - } - .is_null()); - assert!(unsafe { - ffi::edit::oaktimeline_split_preserving_links_command( - blocks.as_ptr(), - 1, - std::ptr::null(), - dens.as_ptr(), - 1, - ) - } - .is_null()); - assert!(unsafe { - ffi::edit::oaktimeline_split_preserving_links_command( - blocks.as_ptr(), - 1, - nums.as_ptr(), - std::ptr::null(), - 1, - ) - } - .is_null()); - assert!(unsafe { - ffi::edit::oaktimeline_split_preserving_links_command( - blocks.as_ptr(), - 1, - nums.as_ptr(), - dens.as_ptr(), - 0, - ) - } - .is_null()); -} - -/// `oaktimeline_ripple_delete_gaps_command` validates and builds. -#[test] -fn ripple_delete_gaps_command() { - let seq = make_owned(MockNode { - kind: MockKind::Sequence, - ..Default::default() - }); - let track = make_owned(MockNode { - kind: MockKind::Track, - ..Default::default() - }); - let in_nums = [0i64]; - let in_dens = [1i64]; - let out_nums = [5i64]; - let out_dens = [1i64]; - let tracks = [track.clone()]; - - assert!(!unsafe { - ffi::edit::oaktimeline_ripple_delete_gaps_command( - seq.clone(), - in_nums.as_ptr(), - in_dens.as_ptr(), - out_nums.as_ptr(), - out_dens.as_ptr(), - tracks.as_ptr(), - 1, - ) - } - .is_null()); - - // Invalid inputs → null handle. - assert!(unsafe { - ffi::edit::oaktimeline_ripple_delete_gaps_command( - CHandle::null(), - in_nums.as_ptr(), - in_dens.as_ptr(), - out_nums.as_ptr(), - out_dens.as_ptr(), - tracks.as_ptr(), - 1, - ) - } - .is_null()); - assert!(unsafe { - ffi::edit::oaktimeline_ripple_delete_gaps_command( - seq.clone(), - std::ptr::null(), - in_dens.as_ptr(), - out_nums.as_ptr(), - out_dens.as_ptr(), - tracks.as_ptr(), - 1, - ) - } - .is_null()); - assert!(unsafe { - ffi::edit::oaktimeline_ripple_delete_gaps_command( - seq.clone(), - in_nums.as_ptr(), - in_dens.as_ptr(), - out_nums.as_ptr(), - out_dens.as_ptr(), - tracks.as_ptr(), - 0, - ) - } - .is_null()); -} - -/// `oaktimeline_ripple_remove_area_command` and -/// `oaktimeline_insert_gaps_command` validate and build. -#[test] -fn ripple_area_and_insert_gaps_commands() { - let track = make_owned(MockNode { - kind: MockKind::Track, - ..Default::default() - }); - let list = make_owned(MockNode { - kind: MockKind::TrackList, - ..Default::default() - }); - - assert!(!unsafe { - ffi::edit::oaktimeline_ripple_remove_area_command(track.clone(), 0, 1, 5, 1) - } - .is_null()); - assert!(unsafe { - ffi::edit::oaktimeline_ripple_remove_area_command(CHandle::null(), 0, 1, 5, 1) - } - .is_null()); - - assert!( - !unsafe { ffi::edit::oaktimeline_insert_gaps_command(list.clone(), 0, 1, 5, 1) }.is_null() - ); - assert!( - unsafe { ffi::edit::oaktimeline_insert_gaps_command(CHandle::null(), 0, 1, 5, 1) } - .is_null() - ); -} - -/// The XML writer externs used by the save paths accumulate into the mock -/// writer's buffer (direct bridge coverage). -#[test] -fn xml_writer_externs_accumulate() { - let w = unsafe { oakcommon_xml_writer_init() }; - assert!(!w.is_null()); - - let start = CString::new("marker").unwrap(); - let key = CString::new("name").unwrap(); - let val = CString::new("m").unwrap(); - let text_name = CString::new("enabled").unwrap(); - let text_val = CString::new("1").unwrap(); - - unsafe { oakcommon_xml_writer_write_start_element(w.clone(), start.as_ptr()) }; - unsafe { oakcommon_xml_writer_write_attribute(w.clone(), key.as_ptr(), val.as_ptr()) }; - unsafe { - oakcommon_xml_writer_write_text_element(w.clone(), text_name.as_ptr(), text_val.as_ptr()) - }; - unsafe { oakcommon_xml_writer_write_end_element(w.clone()) }; - - let buf = unsafe { get::(&w) } - .unwrap() - .buf - .clone(); - assert!(buf.contains("")); - assert!(buf.contains(" name=\"m\"")); - assert!(buf.contains("1")); - assert!(buf.contains("")); -} - -/// Sanity: the FFI entry points never panic on garbage-ish but valid-shaped -/// inputs (guard behaviour). -#[test] -fn ffi_guards_return_codes() { - // A null list to marker_count maps to E_INVALID, not a panic. - let mut count = 0; - assert_eq!( - unsafe { ffi::marker::oaktimeline_marker_count(CHandle::null(), &mut count) }, - OAKTIMELINE_E_INVALID - ); - // The failure constant is negative and distinct from OK. - assert_ne!(OAKTIMELINE_E_FAILED, OAKTIMELINE_OK); - assert_ne!(OAKTIMELINE_E_INVALID, OAKTIMELINE_E_NOT_FOUND); -} - -/// Silence unused warnings for helpers imported for the save tests. -#[allow(dead_code)] -fn _uses_writer_mut() -> *mut oaktimeline::bridge::teststubs::MockXmlWriter { - std::ptr::null_mut() -} diff --git a/crates/oaktimeline/tests/cov_undogeneral.rs b/crates/oaktimeline/tests/cov_undogeneral.rs deleted file mode 100644 index 1aa0ec985..000000000 --- a/crates/oaktimeline/tests/cov_undogeneral.rs +++ /dev/null @@ -1,212 +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 . - -//! Coverage for the remaining reachable paths of `src/undogeneral.rs` -//! (timelineundogeneral.h): the default-transition `prepare` neighbour -//! classification (null previous, dual out transition) and the -//! `TimelineAddTrackCommand` factories, plus the audio track-list variant. -#![cfg(feature = "test-stubs")] - -use oakcore_rs::Rational; - -use oaktimeline::bridge::node::{ - oaknode_block_clip_create, oaknode_block_gap_create, oaknode_track_create, - oaknode_track_get_block_at, oaknode_track_get_block_count, oaknode_track_prepend_block, - oaknode_tracklist_get_type, -}; -use oaktimeline::bridge::teststubs::{MockKind, MockNode}; -use oaktimeline::handle::{get, get_mut, make_owned, CHandle}; -use oaktimeline::undocommon::Command; -use oaktimeline::undogeneral::{TimelineAddDefaultTransitionCommand, TimelineAddTrackCommand}; - -// ---- helpers ----------------------------------------------------------- - -fn make_track() -> CHandle { - unsafe { oaknode_track_create(0) } -} - -fn addr(h: &CHandle) -> *mut MockNode { - unsafe { get_mut::(h).unwrap() as *mut MockNode } -} - -fn count(track: &CHandle) -> i32 { - let mut c = 0; - unsafe { oaknode_track_get_block_count(track.clone(), &mut c) }; - c -} - -fn at(track: &CHandle, idx: i32) -> CHandle { - let mut o = CHandle::null(); - unsafe { oaknode_track_get_block_at(track.clone(), idx, &mut o) }; - o -} - -fn mk_clip() -> CHandle { - unsafe { oaknode_block_clip_create() } -} - -fn mk_gap() -> CHandle { - unsafe { oaknode_block_gap_create() } -} - -fn set_times(h: &CHandle, in_: (i32, i32), out: (i32, i32), len: (i32, i32), mi: (i32, i32)) { - let b = unsafe { get_mut::(h).unwrap() }; - b.in_ = in_; - b.out = out; - b.length = len; - b.media_in = mi; -} - -fn prepend( - t: &CHandle, - b: &CHandle, - in_: (i32, i32), - out: (i32, i32), - len: (i32, i32), - mi: (i32, i32), -) { - unsafe { oaknode_track_prepend_block(t.clone(), b.clone()) }; - set_times(b, in_, out, len, mi); -} - -fn kind_of(h: &CHandle) -> MockKind { - unsafe { get::(h).unwrap().kind } -} - -fn make_list(track_type: i32) -> CHandle { - make_owned(MockNode { - kind: MockKind::TrackList, - track_type, - ..Default::default() - }) -} - -fn make_sequence() -> CHandle { - make_owned(MockNode { - kind: MockKind::Sequence, - ..Default::default() - }) -} - -fn seq_add_list(seq: &CHandle, list: &CHandle) { - unsafe { - get_mut::(seq).unwrap().blocks.push(addr(list)); - } -} - -// ---- TimelineAddDefaultTransitionCommand ------------------------------- - -/// `prepare` on adjacent selected clips: the first clip has a null -/// previous (in transition), and the out side sees the selected next clip -/// (dual transition). -#[test] -fn default_transition_prepare_neighbours() { - let t = make_track(); - let c2 = mk_clip(); - prepend(&t, &c2, (10, 1), (20, 1), (10, 1), (10, 1)); - let c1 = mk_clip(); - prepend(&t, &c1, (0, 1), (10, 1), (10, 1), (0, 1)); - // Track order: [c1, c2]. - - let mut cmd = - TimelineAddDefaultTransitionCommand::new(vec![c1.clone(), c2.clone()], Rational::new(1, 1)); - cmd.prepare(); - cmd.redo(); - cmd.undo(); - Command::redo(&mut cmd); - Command::undo(&mut cmd); - assert_eq!(count(&t), 2); - assert_eq!(at(&t, 0).ctx as usize, addr(&c1) as usize); -} - -/// `prepare` with a gap before/after a single selected clip creates in and -/// out transitions. -#[test] -fn default_transition_prepare_gap_neighbours() { - let t = make_track(); - let g2 = mk_gap(); - prepend(&t, &g2, (10, 1), (13, 1), (3, 1), (10, 1)); - let c1 = mk_clip(); - prepend(&t, &c1, (0, 1), (10, 1), (10, 1), (0, 1)); - let g0 = mk_gap(); - prepend(&t, &g0, (0, 1), (0, 1), (0, 1), (0, 1)); - // Track order: [g0, c1, g2]. - - let mut cmd = TimelineAddDefaultTransitionCommand::new(vec![c1.clone()], Rational::new(1, 1)); - cmd.prepare(); - cmd.redo(); - cmd.undo(); - assert_eq!(count(&t), 3); -} - -/// Boxing the default-transition command into a handle works and keeps -/// the command alive across redo/undo. -#[test] -fn default_transition_boxes_to_chandle() { - let t = make_track(); - let c1 = mk_clip(); - prepend(&t, &c1, (0, 1), (10, 1), (10, 1), (0, 1)); - - let cmd = TimelineAddDefaultTransitionCommand::new(vec![c1.clone()], Rational::new(1, 1)); - let h = cmd.to_command(); - assert!(!h.is_null()); -} - -// ---- TimelineAddTrackCommand factories --------------------------------- - -/// `run_immediately` and `run_immediately_with_automerge` construct, redo -/// and hand back the created track in one step. -#[test] -fn add_track_run_immediately() { - let list = make_list(0); - - let t = TimelineAddTrackCommand::run_immediately(list.clone()); - assert!(!t.is_null()); - assert!(matches!(kind_of(&t), MockKind::Track)); - - let t2 = TimelineAddTrackCommand::run_immediately_with_automerge(list.clone(), true); - assert!(!t2.is_null()); -} - -/// An audio track list produces the samples input id and an audio track. -#[test] -fn add_track_audio_list() { - let list = make_list(1); - let mut cmd = TimelineAddTrackCommand::new(list.clone()); - cmd.redo(); - let t = cmd.track(); - assert!(!t.is_null()); - let mut kind = 0; - unsafe { oaknode_tracklist_get_type(list.clone(), &mut kind) }; - assert_eq!(kind, 1); - cmd.undo(); - - // A none-type list uses the empty input id. - let none = make_list(-1); - let mut cmd2 = TimelineAddTrackCommand::new(none.clone()); - cmd2.redo(); - cmd2.undo(); -} - -/// `TimelineAddTrackCommand` trait dispatch. -#[test] -fn add_track_trait_dispatch() { - let list = make_list(0); - let mut cmd = TimelineAddTrackCommand::new(list.clone()); - Command::redo(&mut cmd); - assert!(!cmd.track().is_null()); - Command::undo(&mut cmd); -} diff --git a/crates/oaktimeline/tests/cov_undopointer.rs b/crates/oaktimeline/tests/cov_undopointer.rs deleted file mode 100644 index d92350e19..000000000 --- a/crates/oaktimeline/tests/cov_undopointer.rs +++ /dev/null @@ -1,649 +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 . - -//! Coverage for `src/undopointer.rs` (timelineundopointer.h): the -//! `BlockTrimCommand` compensate-adjacent paths (create gap, remove -//! adjacent, resize adjacent, no-op), `TrackSlideCommand` (create in/out -//! gap, remove adjacent, resize) and `TrackPlaceBlockCommand` (append, -//! gap-past-end, add-tracks, ripple area). These tests drive the mock -//! node graph through the `bridge::node` externs only. -#![cfg(feature = "test-stubs")] - -use oakcore_rs::{Rational, TimeRange}; - -use oaktimeline::bridge::node::{ - oaknode_block_clip_create, oaknode_block_gap_create, oaknode_track_create, - oaknode_track_get_block_at, oaknode_track_get_block_count, oaknode_track_prepend_block, -}; -use oaktimeline::bridge::teststubs::{MockKind, MockNode}; -use oaktimeline::common::MovementMode; -use oaktimeline::handle::{get, get_mut, make_owned, CHandle}; -use oaktimeline::undocommon::Command; -use oaktimeline::undopointer::{BlockTrimCommand, TrackPlaceBlockCommand, TrackSlideCommand}; - -// ---- helpers (mirror the other test binaries' local helpers) ---------- - -fn make_track() -> CHandle { - unsafe { oaknode_track_create(0) } -} - -fn addr(h: &CHandle) -> *mut MockNode { - unsafe { get_mut::(h).unwrap() as *mut MockNode } -} - -fn count(track: &CHandle) -> i32 { - let mut c = 0; - unsafe { oaknode_track_get_block_count(track.clone(), &mut c) }; - c -} - -/// Borrowed block at `index` (for `.ctx` comparisons only). -fn at(track: &CHandle, idx: i32) -> CHandle { - let mut o = CHandle::null(); - unsafe { oaknode_track_get_block_at(track.clone(), idx, &mut o) }; - o -} - -fn mk_clip() -> CHandle { - unsafe { oaknode_block_clip_create() } -} - -fn mk_gap() -> CHandle { - unsafe { oaknode_block_gap_create() } -} - -/// Set a block's in/out/length/media-in points directly. -fn set_times(h: &CHandle, in_: (i32, i32), out: (i32, i32), len: (i32, i32), mi: (i32, i32)) { - let b = unsafe { get_mut::(h).unwrap() }; - b.in_ = in_; - b.out = out; - b.length = len; - b.media_in = mi; -} - -fn blen(h: &CHandle) -> (i32, i32) { - unsafe { get::(h).unwrap().length } -} - -fn media_in(h: &CHandle) -> (i32, i32) { - unsafe { get::(h).unwrap().media_in } -} - -fn media_out(h: &CHandle) -> (i32, i32) { - unsafe { get::(h).unwrap().out } -} - -/// Attach `b` to the front of `t`, then set its geometry. -fn prepend( - t: &CHandle, - b: &CHandle, - in_: (i32, i32), - out: (i32, i32), - len: (i32, i32), - mi: (i32, i32), -) { - unsafe { oaknode_track_prepend_block(t.clone(), b.clone()) }; - set_times(b, in_, out, len, mi); -} - -fn kind_of(h: &CHandle) -> MockKind { - unsafe { get::(h).unwrap().kind } -} - -fn make_list() -> CHandle { - make_owned(MockNode { - kind: MockKind::TrackList, - track_type: 0, - ..Default::default() - }) -} - -fn list_add_track(list: &CHandle, t: &CHandle) { - unsafe { - get_mut::(list).unwrap().blocks.push(addr(t)); - } -} - -fn track_ptr(h: &CHandle) -> *mut MockNode { - unsafe { get::(h).unwrap().track } -} - -// ---- BlockTrimCommand -------------------------------------------------- - -/// Trimming to the same length marks the command as a no-op. -#[test] -fn trim_same_length_is_noop() { - let t = make_track(); - let b = mk_clip(); - prepend(&t, &b, (0, 1), (10, 1), (10, 1), (0, 1)); - - let mut cmd = BlockTrimCommand::new( - t.clone(), - b.clone(), - Rational::new(10, 1), - MovementMode::TrimOut, - ); - cmd.prepare(); - assert_eq!(blen(&b), (10, 1)); - cmd.redo(); - assert_eq!(blen(&b), (10, 1)); - cmd.undo(); - assert_eq!(blen(&b), (10, 1)); -} - -/// TrimOut at the end of a track (no adjacent): only the block itself is -/// trimmed, `needs_adjacent_` is false. -#[test] -fn trim_out_last_block_no_adjacent() { - let t = make_track(); - let b = mk_clip(); - prepend(&t, &b, (0, 1), (10, 1), (10, 1), (0, 1)); - - let mut cmd = BlockTrimCommand::new( - t.clone(), - b.clone(), - Rational::new(7, 1), - MovementMode::TrimOut, - ); - cmd.prepare(); - cmd.redo(); - assert_eq!(blen(&b), (7, 1)); - cmd.undo(); - assert_eq!(blen(&b), (10, 1)); -} - -/// TrimIn shortening with a clip previous and no roll edit: the command -/// creates a compensation gap and inserts it before the block; undo -/// removes it again. -#[test] -fn trim_in_creates_adjacent_gap() { - let t = make_track(); - let b = mk_clip(); - prepend(&t, &b, (10, 1), (20, 1), (10, 1), (10, 1)); - let a = mk_clip(); - prepend(&t, &a, (0, 1), (10, 1), (10, 1), (0, 1)); - // Track order: [a, b]. - assert_eq!(count(&t), 2); - assert_eq!(at(&t, 0).ctx as usize, addr(&a) as usize); - - let mut cmd = BlockTrimCommand::new( - t.clone(), - b.clone(), - Rational::new(7, 1), - MovementMode::TrimIn, - ); - cmd.prepare(); - assert_eq!(count(&t), 2); - - cmd.redo(); - // A gap of length 3 was created between a and b. - assert_eq!(count(&t), 3); - assert_eq!(blen(&b), (7, 1)); - let mid = at(&t, 1); - assert!(matches!(kind_of(&mid), MockKind::Gap)); - assert_eq!(blen(&mid), (3, 1)); - - cmd.undo(); - assert_eq!(count(&t), 2); - assert_eq!(blen(&b), (10, 1)); -} - -/// TrimIn with no previous block: the gap is prepended to the track. -#[test] -fn trim_in_first_block_creates_gap_prepend() { - let t = make_track(); - let b = mk_clip(); - prepend(&t, &b, (0, 1), (10, 1), (10, 1), (0, 1)); - - let mut cmd = BlockTrimCommand::new( - t.clone(), - b.clone(), - Rational::new(7, 1), - MovementMode::TrimIn, - ); - cmd.prepare(); - cmd.redo(); - assert_eq!(count(&t), 2); - assert!(matches!(kind_of(&at(&t, 0)), MockKind::Gap)); - assert_eq!(blen(&at(&t, 0)), (3, 1)); - assert_eq!(blen(&b), (7, 1)); - - cmd.undo(); - assert_eq!(count(&t), 1); - assert_eq!(blen(&b), (10, 1)); -} - -/// TrimOut shortening into a trailing gap: the gap is resized instead of a -/// new one being created. -#[test] -fn trim_out_into_existing_gap_resizes() { - let t = make_track(); - let g = mk_gap(); - prepend(&t, &g, (10, 1), (13, 1), (3, 1), (10, 1)); - let b = mk_clip(); - prepend(&t, &b, (0, 1), (10, 1), (10, 1), (0, 1)); - // Track order: [b, g]. - assert_eq!(count(&t), 2); - - let mut cmd = BlockTrimCommand::new( - t.clone(), - b.clone(), - Rational::new(7, 1), - MovementMode::TrimOut, - ); - cmd.prepare(); - cmd.redo(); - // Gap was extended to 3 + 3 = 6; no new block created. - assert_eq!(count(&t), 2); - assert_eq!(blen(&g), (6, 1)); - assert_eq!(blen(&b), (7, 1)); - - cmd.undo(); - assert_eq!(blen(&g), (3, 1)); - assert_eq!(blen(&b), (10, 1)); -} - -/// TrimOut growing the block by exactly the adjacent gap's length removes -/// the adjacent block (`we_removed_adjacent_`). -#[test] -fn trim_grows_and_removes_adjacent() { - let t = make_track(); - let g = mk_gap(); - prepend(&t, &g, (10, 1), (13, 1), (3, 1), (10, 1)); - let b = mk_clip(); - prepend(&t, &b, (0, 1), (10, 1), (10, 1), (0, 1)); - // Track order: [b, g]. - assert_eq!(count(&t), 2); - - // Grow from 10 to 13: trim_diff = -3, adjacent gap length 3 is consumed. - let mut cmd = BlockTrimCommand::new( - t.clone(), - b.clone(), - Rational::new(13, 1), - MovementMode::TrimOut, - ); - cmd.prepare(); - assert_eq!(count(&t), 2); - - cmd.redo(); - assert_eq!(count(&t), 1); - assert_eq!(blen(&b), (13, 1)); - assert!(track_ptr(&g).is_null()); - - cmd.undo(); - assert_eq!(count(&t), 2); - assert_eq!(blen(&b), (10, 1)); -} - -/// TrimIn with a roll edit: the clip adjacent is resized instead of a gap -/// being created. -#[test] -fn trim_roll_edit_resizes_clip_adjacent() { - let t = make_track(); - let b = mk_clip(); - prepend(&t, &b, (10, 1), (20, 1), (10, 1), (10, 1)); - let a = mk_clip(); - prepend(&t, &a, (0, 1), (10, 1), (10, 1), (0, 1)); - // Track order: [a, b]. - - let mut cmd = BlockTrimCommand::new( - t.clone(), - b.clone(), - Rational::new(7, 1), - MovementMode::TrimIn, - ); - cmd.set_trim_is_a_roll_edit(true); - cmd.prepare(); - - cmd.redo(); - // No gap created; the previous clip grew from 10 to 13. - assert_eq!(count(&t), 2); - assert_eq!(blen(&a), (13, 1)); - assert_eq!(blen(&b), (7, 1)); - - cmd.undo(); - assert_eq!(blen(&a), (10, 1)); - assert_eq!(blen(&b), (10, 1)); -} - -/// The remove-from-graph toggle defaults on and can be disabled. -#[test] -fn trim_remove_zero_length_from_graph_toggle() { - let t = make_track(); - let g = mk_gap(); - prepend(&t, &g, (10, 1), (13, 1), (3, 1), (10, 1)); - let b = mk_clip(); - prepend(&t, &b, (0, 1), (10, 1), (10, 1), (0, 1)); - - let mut cmd = BlockTrimCommand::new( - t.clone(), - b.clone(), - Rational::new(13, 1), - MovementMode::TrimOut, - ); - cmd.set_remove_zero_length_from_graph(false); - cmd.prepare(); - cmd.redo(); - assert_eq!(count(&t), 1); - cmd.undo(); - assert_eq!(count(&t), 2); -} - -/// TrimIn growing the block by exactly the adjacent gap's length removes -/// the adjacent block; undo re-inserts it before the block. -#[test] -fn trim_in_grows_and_removes_adjacent() { - let t = make_track(); - let b = mk_clip(); - prepend(&t, &b, (10, 1), (20, 1), (10, 1), (10, 1)); - let g = mk_gap(); - prepend(&t, &g, (7, 1), (10, 1), (3, 1), (7, 1)); - // Track order: [g, b]. - assert_eq!(count(&t), 2); - - // Grow from 10 to 13: the 3-length gap before b is consumed. - let mut cmd = BlockTrimCommand::new( - t.clone(), - b.clone(), - Rational::new(13, 1), - MovementMode::TrimIn, - ); - cmd.prepare(); - cmd.redo(); - assert_eq!(count(&t), 1); - assert!(track_ptr(&g).is_null()); - - cmd.undo(); - assert_eq!(count(&t), 2); - assert_eq!(at(&t, 0).ctx as usize, addr(&g) as usize); -} - -/// `BlockTrimCommand` dispatches through the `Command` trait (the undo -/// stack's vtable invokes it that way). -#[test] -fn trim_command_trait_dispatch() { - let t = make_track(); - let b = mk_clip(); - prepend(&t, &b, (0, 1), (10, 1), (10, 1), (0, 1)); - - let mut cmd = BlockTrimCommand::new( - t.clone(), - b.clone(), - Rational::new(7, 1), - MovementMode::TrimOut, - ); - cmd.prepare(); - Command::redo(&mut cmd); - assert_eq!(blen(&b), (7, 1)); - Command::undo(&mut cmd); - assert_eq!(blen(&b), (10, 1)); - - // Boxing into a command handle runs redo/undo through the vtable. - let h = cmd.to_command(); - assert!(!h.is_null()); -} - -// ---- TrackSlideCommand ------------------------------------------------- - -/// Slide with no in adjacent: a compensation gap is created before the -/// block and removed on undo. -#[test] -fn slide_creates_in_adjacent_gap() { - let t = make_track(); - let b = mk_clip(); - prepend(&t, &b, (0, 1), (10, 1), (10, 1), (0, 1)); - - let mut cmd = TrackSlideCommand::new( - t.clone(), - vec![b.clone()], - CHandle::null(), - CHandle::null(), - Rational::new(2, 1), - ); - cmd.prepare(); - - cmd.redo(); - // Gap(2) prepended, block unchanged. - assert_eq!(count(&t), 2); - assert!(matches!(kind_of(&at(&t, 0)), MockKind::Gap)); - assert_eq!(blen(&at(&t, 0)), (2, 1)); - - cmd.undo(); - assert_eq!(count(&t), 1); -} - -/// Slide with both an in and an out adjacent to create: two gaps are -/// inserted on redo and removed on undo. -#[test] -fn slide_creates_in_and_out_adjacent_gaps() { - let t = make_track(); - let b = mk_clip(); - prepend(&t, &b, (10, 1), (20, 1), (10, 1), (10, 1)); - let a = mk_clip(); - prepend(&t, &a, (0, 1), (10, 1), (10, 1), (0, 1)); - // Track order: [a, b]; slide `a`. - - let mut cmd = TrackSlideCommand::new( - t.clone(), - vec![a.clone()], - CHandle::null(), - CHandle::null(), - Rational::new(2, 1), - ); - cmd.prepare(); - - cmd.redo(); - // In gap before `a`, out gap after `a` (before `b`). - assert_eq!(count(&t), 4); - assert!(matches!(kind_of(&at(&t, 0)), MockKind::Gap)); - assert!(matches!(kind_of(&at(&t, 1)), MockKind::Clip)); - assert!(matches!(kind_of(&at(&t, 2)), MockKind::Gap)); - - cmd.undo(); - assert_eq!(count(&t), 2); -} - -/// Slide with a zero movement: the supplied in/out adjacent gaps are -/// removed (their length equals the movement) and restored on undo. -#[test] -fn slide_removes_adjacents() { - let t = make_track(); - let g_out = mk_gap(); - prepend(&t, &g_out, (10, 1), (10, 1), (0, 1), (10, 1)); - let b = mk_clip(); - prepend(&t, &b, (0, 1), (10, 1), (10, 1), (0, 1)); - let g_in = mk_gap(); - prepend(&t, &g_in, (0, 1), (0, 1), (0, 1), (0, 1)); - // Track order: [g_in, b, g_out]. - - let mut cmd = TrackSlideCommand::new( - t.clone(), - vec![b.clone()], - g_in.clone(), - g_out.clone(), - Rational::new(0, 1), - ); - cmd.prepare(); - - cmd.redo(); - // Both zero-length adjacents removed; only the block remains. - assert_eq!(count(&t), 1); - assert!(track_ptr(&g_in).is_null()); - assert!(track_ptr(&g_out).is_null()); - - cmd.undo(); - assert_eq!(count(&t), 3); -} - -/// Slide that only resizes the adjacents (no creation, no removal). -#[test] -fn slide_resizes_adjacents() { - let t = make_track(); - let g_out = mk_gap(); - prepend(&t, &g_out, (15, 1), (20, 1), (5, 1), (15, 1)); - let b = mk_clip(); - prepend(&t, &b, (5, 1), (15, 1), (10, 1), (5, 1)); - let g_in = mk_gap(); - prepend(&t, &g_in, (0, 1), (5, 1), (5, 1), (0, 1)); - // Track order: [g_in, b, g_out]. - - let mut cmd = TrackSlideCommand::new( - t.clone(), - vec![b.clone()], - g_in.clone(), - g_out.clone(), - Rational::new(2, 1), - ); - cmd.prepare(); - - cmd.redo(); - assert_eq!(blen(&g_in), (7, 1)); // 5 + 2 - assert_eq!(blen(&g_out), (3, 1)); // 5 - 2 - - cmd.undo(); - assert_eq!(blen(&g_in), (5, 1)); - assert_eq!(blen(&g_out), (5, 1)); -} - -/// `TrackSlideCommand` trait dispatch and boxing. -#[test] -fn slide_command_trait_dispatch() { - let t = make_track(); - let b = mk_clip(); - prepend(&t, &b, (0, 1), (10, 1), (10, 1), (0, 1)); - - let mut cmd = TrackSlideCommand::new( - t.clone(), - vec![b.clone()], - CHandle::null(), - CHandle::null(), - Rational::new(2, 1), - ); - cmd.prepare(); - Command::redo(&mut cmd); - assert_eq!(count(&t), 2); - Command::undo(&mut cmd); - assert_eq!(count(&t), 1); - - let h = cmd.to_command(); - assert!(!h.is_null()); -} - -// ---- TrackPlaceBlockCommand -------------------------------------------- - -/// Placing at the in point of an empty track appends the block. -#[test] -fn place_block_appends_at_track_start() { - let list = make_list(); - let t = make_track(); - list_add_track(&list, &t); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - - let mut cmd = TrackPlaceBlockCommand::new(list.clone(), 0, b.clone(), Rational::new(0, 1)); - cmd.redo(); - assert_eq!(count(&t), 1); - assert_eq!(blen(&b), (10, 1)); - - cmd.undo(); - assert_eq!(count(&t), 0); -} - -/// Placing past the end of the track inserts a gap before the block; undo -/// removes both. -#[test] -fn place_block_past_end_creates_gap() { - let list = make_list(); - let t = make_track(); - list_add_track(&list, &t); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - - let mut cmd = TrackPlaceBlockCommand::new(list.clone(), 0, b.clone(), Rational::new(10, 1)); - cmd.redo(); - assert_eq!(count(&t), 2); - assert!(matches!(kind_of(&at(&t, 0)), MockKind::Gap)); - assert_eq!(blen(&at(&t, 0)), (10, 1)); - assert_eq!(blen(&at(&t, 1)), (10, 1)); - - cmd.undo(); - assert_eq!(count(&t), 0); -} - -/// Placing on a track index beyond the track count adds the missing tracks -/// first (and removes them on undo). -#[test] -fn place_block_adds_missing_tracks() { - let list = make_list(); - let t = make_track(); - list_add_track(&list, &t); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - - // Index 3 with one existing track: 3 add-track commands are created. - let mut cmd = TrackPlaceBlockCommand::new(list.clone(), 3, b.clone(), Rational::new(5, 1)); - cmd.redo(); - - cmd.undo(); -} - -/// Placing inside existing blocks ripple-removes the occupied area -/// (splicing the first block) and inserts the block after it. -#[test] -fn place_block_ripple_removes_area() { - let list = make_list(); - let t = make_track(); - list_add_track(&list, &t); - let c1 = mk_clip(); - prepend(&t, &c1, (0, 1), (10, 1), (10, 1), (0, 1)); - - let b = mk_clip(); - set_times(&b, (0, 1), (5, 1), (5, 1), (0, 1)); - - // Place a 5-length block at in 2: the area [2, 7] is cleared from c1. - let mut cmd = TrackPlaceBlockCommand::new(list.clone(), 0, b.clone(), Rational::new(2, 1)); - cmd.redo(); - // c1 was split at 2, b inserted after the first half. - assert!(count(&t) >= 3); - - cmd.undo(); - // The split is joined back and b removed. - assert_eq!(count(&t), 1); - assert_eq!(blen(&c1), (10, 1)); -} - -/// `TrackPlaceBlockCommand` trait dispatch and boxing. -#[test] -fn place_block_command_trait_dispatch() { - let list = make_list(); - let t = make_track(); - list_add_track(&list, &t); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - - let mut cmd = TrackPlaceBlockCommand::new(list.clone(), 0, b.clone(), Rational::new(0, 1)); - Command::redo(&mut cmd); - assert_eq!(count(&t), 1); - Command::undo(&mut cmd); - assert_eq!(count(&t), 0); - - let h = cmd.to_command(); - assert!(!h.is_null()); -} - -/// `TimeRange` import used by placement ripple command construction. -#[allow(dead_code)] -fn _uses_time_range(_r: TimeRange) {} diff --git a/crates/oaktimeline/tests/cov_undoripple.rs b/crates/oaktimeline/tests/cov_undoripple.rs deleted file mode 100644 index b66157d5a..000000000 --- a/crates/oaktimeline/tests/cov_undoripple.rs +++ /dev/null @@ -1,703 +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 . - -//! Coverage for `src/undoripple.rs` (timelineundoripple.h): the -//! per-track ripple-remove command (trim/splice/removal paths), -//! the track-list and timeline ripples, the ripple tool (gap creation, -//! gap removal, resize, trim-in/out) and gap deletion at regions. -#![cfg(feature = "test-stubs")] - -use oakcore_rs::{Rational, TimeRange}; - -use oaktimeline::bridge::node::{ - oaknode_block_clip_create, oaknode_block_gap_create, oaknode_track_create, - oaknode_track_get_block_at, oaknode_track_get_block_count, oaknode_track_prepend_block, -}; -use oaktimeline::bridge::teststubs::{MockKind, MockNode}; -use oaktimeline::common::MovementMode; -use oaktimeline::handle::{get, get_mut, make_owned, CHandle}; -use oaktimeline::undocommon::Command; -use oaktimeline::undoripple::{ - RippleInfo, TimelineRippleDeleteGapsAtRegionsCommand, TimelineRippleRemoveAreaCommand, - TrackListRippleRemoveAreaCommand, TrackListRippleToolCommand, TrackRippleRemoveAreaCommand, -}; - -// ---- helpers ----------------------------------------------------------- - -fn make_track() -> CHandle { - unsafe { oaknode_track_create(0) } -} - -fn addr(h: &CHandle) -> *mut MockNode { - unsafe { get_mut::(h).unwrap() as *mut MockNode } -} - -fn count(track: &CHandle) -> i32 { - let mut c = 0; - unsafe { oaknode_track_get_block_count(track.clone(), &mut c) }; - c -} - -fn at(track: &CHandle, idx: i32) -> CHandle { - let mut o = CHandle::null(); - unsafe { oaknode_track_get_block_at(track.clone(), idx, &mut o) }; - o -} - -fn mk_clip() -> CHandle { - unsafe { oaknode_block_clip_create() } -} - -fn mk_gap() -> CHandle { - unsafe { oaknode_block_gap_create() } -} - -fn set_times(h: &CHandle, in_: (i32, i32), out: (i32, i32), len: (i32, i32), mi: (i32, i32)) { - let b = unsafe { get_mut::(h).unwrap() }; - b.in_ = in_; - b.out = out; - b.length = len; - b.media_in = mi; -} - -fn prepend( - t: &CHandle, - b: &CHandle, - in_: (i32, i32), - out: (i32, i32), - len: (i32, i32), - mi: (i32, i32), -) { - unsafe { oaknode_track_prepend_block(t.clone(), b.clone()) }; - set_times(b, in_, out, len, mi); -} - -fn blen(h: &CHandle) -> (i32, i32) { - unsafe { get::(h).unwrap().length } -} - -fn track_ptr(h: &CHandle) -> *mut MockNode { - unsafe { get::(h).unwrap().track } -} - -fn make_list(track_type: i32) -> CHandle { - make_owned(MockNode { - kind: MockKind::TrackList, - track_type, - ..Default::default() - }) -} - -fn list_add_track(list: &CHandle, t: &CHandle) { - unsafe { - get_mut::(list).unwrap().blocks.push(addr(t)); - } -} - -fn make_sequence() -> CHandle { - make_owned(MockNode { - kind: MockKind::Sequence, - ..Default::default() - }) -} - -fn seq_add_list(seq: &CHandle, list: &CHandle) { - unsafe { - get_mut::(seq).unwrap().blocks.push(addr(list)); - } -} - -// ---- TrackRippleRemoveAreaCommand -------------------------------------- - -/// No block before/at the range in point: prepare no-ops. -#[test] -fn ripple_area_no_block_at_in() { - let t = make_track(); - let mut cmd = TrackRippleRemoveAreaCommand::new( - t.clone(), - TimeRange::new(Rational::new(0, 1), Rational::new(5, 1)), - ); - cmd.prepare(); - cmd.redo(); - cmd.undo(); - assert_eq!(count(&t), 0); -} - -/// The first block starts exactly at the range in point: it is in-trimmed -/// and `insert_previous` falls back to its (null) predecessor. A command -/// without a splice reports a null spliced block. -#[test] -fn ripple_area_in_trims_first_block() { - let t = make_track(); - let b1 = mk_clip(); - prepend(&t, &b1, (2, 1), (10, 1), (8, 1), (2, 1)); - - let mut cmd = TrackRippleRemoveAreaCommand::new( - t.clone(), - TimeRange::new(Rational::new(2, 1), Rational::new(8, 1)), - ); - cmd.prepare(); - assert!(cmd.get_insertion_index().is_null()); - assert!(cmd.get_spliced_block().is_null()); - - cmd.redo(); - assert_eq!(blen(&b1), (2, 1)); // 8 - (8-2) - - cmd.undo(); - assert_eq!(blen(&b1), (8, 1)); -} - -/// A clip straddling the whole range is split; the second half is trimmed -/// and exposed through `get_spliced_block`. -#[test] -fn ripple_area_splits_clip() { - let t = make_track(); - let b1 = mk_clip(); - prepend(&t, &b1, (0, 1), (10, 1), (10, 1), (0, 1)); - - let mut cmd = TrackRippleRemoveAreaCommand::new( - t.clone(), - TimeRange::new(Rational::new(2, 1), Rational::new(8, 1)), - ); - cmd.prepare(); - assert!(cmd.get_spliced_block().is_null()); - - cmd.redo(); - // Split at 2: [b1(2), split(2)]; the split half is the spliced block. - assert_eq!(count(&t), 2); - let split = cmd.get_spliced_block(); - assert!(!split.is_null()); - assert_eq!(blen(&b1), (2, 1)); - - cmd.undo(); - assert_eq!(count(&t), 1); - assert_eq!(blen(&b1), (10, 1)); -} - -/// A gap straddling the range is not split (default): it is trimmed out -/// instead. -#[test] -fn ripple_area_trims_gap_without_splitting() { - let t = make_track(); - let g = mk_gap(); - prepend(&t, &g, (0, 1), (10, 1), (10, 1), (0, 1)); - - let mut cmd = TrackRippleRemoveAreaCommand::new( - t.clone(), - TimeRange::new(Rational::new(2, 1), Rational::new(8, 1)), - ); - cmd.prepare(); - - cmd.redo(); - assert_eq!(count(&t), 1); - assert_eq!(blen(&g), (4, 1)); // 10 - 6 - - cmd.undo(); - assert_eq!(blen(&g), (10, 1)); -} - -/// A gap straddling the range MAY be split when allowed. -#[test] -fn ripple_area_splits_gap_when_allowed() { - let t = make_track(); - let g = mk_gap(); - prepend(&t, &g, (0, 1), (10, 1), (10, 1), (0, 1)); - - let mut cmd = TrackRippleRemoveAreaCommand::new( - t.clone(), - TimeRange::new(Rational::new(2, 1), Rational::new(8, 1)), - ); - cmd.set_allow_splitting_gaps(true); - cmd.prepare(); - cmd.redo(); - assert_eq!(count(&t), 2); - cmd.undo(); - assert_eq!(count(&t), 1); -} - -/// The first block is out-trimmed but not in-trimmed: it is trimmed at -/// its out edge and nothing follows. -#[test] -fn ripple_area_trims_out_first_block() { - let t = make_track(); - let b1 = mk_clip(); - prepend(&t, &b1, (0, 1), (10, 1), (10, 1), (0, 1)); - - let mut cmd = TrackRippleRemoveAreaCommand::new( - t.clone(), - TimeRange::new(Rational::new(2, 1), Rational::new(20, 1)), - ); - cmd.prepare(); - assert!(!cmd.get_insertion_index().is_null()); - - cmd.redo(); - assert_eq!(blen(&b1), (2, 1)); // 10 - (10-2) - - cmd.undo(); - assert_eq!(blen(&b1), (10, 1)); -} - -/// A range covering whole blocks removes them and in-trims the first -/// block that sticks out past the range. A leading gap keeps the first -/// removal's predecessor non-null. -#[test] -fn ripple_area_removes_blocks_and_trims() { - let t = make_track(); - let b3 = mk_clip(); - prepend(&t, &b3, (8, 1), (12, 1), (4, 1), (8, 1)); - let b2 = mk_clip(); - prepend(&t, &b2, (4, 1), (8, 1), (4, 1), (4, 1)); - let b1 = mk_clip(); - prepend(&t, &b1, (0, 1), (4, 1), (4, 1), (0, 1)); - let g = mk_gap(); - prepend(&t, &g, (-1, 1), (0, 1), (1, 1), (-1, 1)); - // Track order: [g, b1, b2, b3]. - - let mut cmd = TrackRippleRemoveAreaCommand::new( - t.clone(), - TimeRange::new(Rational::new(0, 1), Rational::new(10, 1)), - ); - cmd.prepare(); - - cmd.redo(); - assert_eq!(count(&t), 2); - assert_eq!(blen(&b3), (2, 1)); // 4 - (10-8) - - cmd.undo(); - assert_eq!(count(&t), 4); - assert_eq!(blen(&b1), (4, 1)); - assert_eq!(blen(&b2), (4, 1)); - assert_eq!(blen(&b3), (4, 1)); -} - -/// A range ending exactly at a block's out point stops the removal loop -/// there. A leading gap keeps every removed block's predecessor non-null -/// (the ABI rejects a null `before`). -#[test] -fn ripple_area_stops_at_exact_out() { - let t = make_track(); - let b2 = mk_clip(); - prepend(&t, &b2, (4, 1), (10, 1), (6, 1), (4, 1)); - let b1 = mk_clip(); - prepend(&t, &b1, (0, 1), (4, 1), (4, 1), (0, 1)); - let g = mk_gap(); - prepend(&t, &g, (-1, 1), (0, 1), (1, 1), (-1, 1)); - // Track order: [g, b1, b2]. - - let mut cmd = TrackRippleRemoveAreaCommand::new( - t.clone(), - TimeRange::new(Rational::new(0, 1), Rational::new(10, 1)), - ); - cmd.prepare(); - cmd.redo(); - assert_eq!(count(&t), 1); - cmd.undo(); - assert_eq!(count(&t), 3); -} - -/// Trait dispatch on the per-track ripple command. -#[test] -fn ripple_area_trait_dispatch() { - let t = make_track(); - let b1 = mk_clip(); - prepend(&t, &b1, (0, 1), (10, 1), (10, 1), (0, 1)); - - let mut cmd = TrackRippleRemoveAreaCommand::new( - t.clone(), - TimeRange::new(Rational::new(2, 1), Rational::new(8, 1)), - ); - cmd.prepare(); - Command::redo(&mut cmd); - assert_eq!(count(&t), 2); - Command::undo(&mut cmd); - assert_eq!(count(&t), 1); -} - -// ---- TrackListRippleRemoveAreaCommand ---------------------------------- - -/// The track-list ripple skips locked tracks and drives its children. -#[test] -fn ripple_list_skips_locked_tracks() { - let list = make_list(0); - let locked = make_track(); - unsafe { get_mut::(&locked).unwrap().locked = true }; - list_add_track(&list, &locked); - let open = make_track(); - list_add_track(&list, &open); - let b1 = mk_clip(); - prepend(&open, &b1, (0, 1), (10, 1), (10, 1), (0, 1)); - - let mut cmd = TrackListRippleRemoveAreaCommand::new( - list.clone(), - Rational::new(2, 1), - Rational::new(8, 1), - ); - cmd.prepare(); - - // The unlocked track's block was split by the child. - cmd.redo(); - assert_eq!(count(&open), 2); - cmd.undo(); - assert_eq!(count(&open), 1); - - Command::redo(&mut cmd); - Command::undo(&mut cmd); - assert_eq!(count(&open), 1); -} - -// ---- TimelineRippleRemoveAreaCommand ----------------------------------- - -/// The timeline ripple walks the sequence's per-type track lists. -#[test] -fn timeline_ripple_remove_area() { - let seq = make_sequence(); - let list = make_list(0); - seq_add_list(&seq, &list); - let t = make_track(); - list_add_track(&list, &t); - let b1 = mk_clip(); - prepend(&t, &b1, (0, 1), (10, 1), (10, 1), (0, 1)); - - let mut cmd = - TimelineRippleRemoveAreaCommand::new(seq.clone(), Rational::new(2, 1), Rational::new(8, 1)); - cmd.redo(); - assert_eq!(count(&t), 2); - cmd.undo(); - assert_eq!(count(&t), 1); - - Command::redo(&mut cmd); - Command::undo(&mut cmd); - assert_eq!(count(&t), 1); -} - -// ---- TrackListRippleToolCommand ---------------------------------------- - -/// Empty info: redo/undo are no-ops. (`RippleInfo` has no public -/// constructor, so the non-empty ripple-tool paths are exercised by the -/// C++ gtest suite through the ABI.) -#[test] -fn ripple_tool_empty_info_noop() { - let list = make_list(0); - let mut cmd = TrackListRippleToolCommand::new( - list.clone(), - Vec::new(), - Rational::new(2, 1), - MovementMode::Move, - ); - cmd.redo(); - cmd.undo(); - Command::redo(&mut cmd); - Command::undo(&mut cmd); -} - -// ---- TimelineRippleDeleteGapsAtRegionsCommand -------------------------- - -fn seq_with_video_track(blocks: Vec) -> (CHandle, CHandle) { - let seq = make_sequence(); - let list = make_list(0); - seq_add_list(&seq, &list); - let t = make_track(); - list_add_track(&list, &t); - for b in blocks { - unsafe { oaknode_track_prepend_block(t.clone(), b.clone()) }; - } - (seq, t) -} - -/// A single gap region shorter than the gap: the gap is resized. -#[test] -fn ripple_delete_gaps_resizes_single_gap() { - let g1 = mk_gap(); - set_times(&g1, (0, 1), (10, 1), (10, 1), (0, 1)); - let (seq, t) = seq_with_video_track(vec![g1.clone()]); - - let mut cmd = TimelineRippleDeleteGapsAtRegionsCommand::new( - seq.clone(), - vec![( - t.clone(), - TimeRange::new(Rational::new(0, 1), Rational::new(5, 1)), - )], - ); - cmd.prepare(); - assert!(cmd.has_commands()); - - cmd.redo(); - assert_eq!(blen(&g1), (5, 1)); - - cmd.undo(); - assert_eq!(blen(&g1), (10, 1)); - - Command::redo(&mut cmd); - Command::undo(&mut cmd); - assert_eq!(blen(&g1), (10, 1)); -} - -/// A gap region on an empty track (no block before/at the in point) is -/// skipped (`is_gap` on a null handle). -#[test] -fn ripple_delete_gaps_empty_track_region_skipped() { - let seq = make_sequence(); - let list = make_list(0); - seq_add_list(&seq, &list); - let t1 = make_track(); - list_add_track(&list, &t1); - let g1 = mk_gap(); - set_times(&g1, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { oaknode_track_prepend_block(t1.clone(), g1.clone()) }; - let t_empty = make_track(); - list_add_track(&list, &t_empty); - - let mut cmd = TimelineRippleDeleteGapsAtRegionsCommand::new( - seq.clone(), - vec![ - ( - t_empty.clone(), - TimeRange::new(Rational::new(0, 1), Rational::new(5, 1)), - ), - ( - t1.clone(), - TimeRange::new(Rational::new(0, 1), Rational::new(5, 1)), - ), - ], - ); - cmd.prepare(); - assert!(cmd.has_commands()); - cmd.redo(); - assert_eq!(blen(&g1), (5, 1)); - cmd.undo(); - assert_eq!(blen(&g1), (10, 1)); -} - -/// Another track whose first block at the earliest point is a clip: the -/// following gap is found for synchronisation. -#[test] -fn ripple_delete_gaps_finds_next_gap_on_other_track() { - let g1 = mk_gap(); - set_times(&g1, (0, 1), (10, 1), (10, 1), (0, 1)); - let (seq, t1) = seq_with_video_track(vec![g1.clone()]); - - // Second track: [c(0-5), g2(5-15)]. - let list = make_list(0); - seq_add_list(&seq, &list); - let t2 = make_track(); - list_add_track(&list, &t2); - let g2 = mk_gap(); - set_times(&g2, (5, 1), (15, 1), (10, 1), (5, 1)); - let c = mk_clip(); - set_times(&c, (0, 1), (5, 1), (5, 1), (0, 1)); - unsafe { oaknode_track_prepend_block(t2.clone(), g2.clone()) }; - unsafe { oaknode_track_prepend_block(t2.clone(), c.clone()) }; - - let mut cmd = TimelineRippleDeleteGapsAtRegionsCommand::new( - seq.clone(), - vec![( - t1.clone(), - TimeRange::new(Rational::new(0, 1), Rational::new(5, 1)), - )], - ); - cmd.prepare(); - assert!(cmd.has_commands()); - - cmd.redo(); - assert_eq!(blen(&g1), (5, 1)); - assert_eq!(blen(&g2), (5, 1)); - - cmd.undo(); - assert_eq!(blen(&g1), (10, 1)); - assert_eq!(blen(&g2), (10, 1)); -} - -/// A clip followed by another clip (no gap) zeroes the ripple length and -/// produces no commands. -#[test] -fn ripple_delete_gaps_clip_no_gap_breaks() { - let g1 = mk_gap(); - set_times(&g1, (0, 1), (10, 1), (10, 1), (0, 1)); - let (seq, t1) = seq_with_video_track(vec![g1.clone()]); - - let list = make_list(0); - seq_add_list(&seq, &list); - let t2 = make_track(); - list_add_track(&list, &t2); - let c2 = mk_clip(); - set_times(&c2, (5, 1), (15, 1), (10, 1), (5, 1)); - let c1 = mk_clip(); - set_times(&c1, (0, 1), (5, 1), (5, 1), (0, 1)); - unsafe { oaknode_track_prepend_block(t2.clone(), c2.clone()) }; - unsafe { oaknode_track_prepend_block(t2.clone(), c1.clone()) }; - - let mut cmd = TimelineRippleDeleteGapsAtRegionsCommand::new( - seq.clone(), - vec![( - t1.clone(), - TimeRange::new(Rational::new(0, 1), Rational::new(5, 1)), - )], - ); - cmd.prepare(); - assert!(!cmd.has_commands()); - cmd.redo(); - cmd.undo(); -} - -/// A clip strictly after the earliest point whose predecessor is a gap: -/// the gap is found via the previous block and, when its length equals -/// the ripple length, removed outright. -#[test] -fn ripple_delete_gaps_finds_previous_gap_and_removes() { - let g1 = mk_gap(); - set_times(&g1, (0, 1), (10, 1), (10, 1), (0, 1)); - let (seq, t1) = seq_with_video_track(vec![g1.clone()]); - - let list = make_list(0); - seq_add_list(&seq, &list); - let t2 = make_track(); - list_add_track(&list, &t2); - let c = mk_clip(); - set_times(&c, (3, 1), (5, 1), (2, 1), (3, 1)); - let g2 = mk_gap(); - set_times(&g2, (-2, 1), (3, 1), (5, 1), (-2, 1)); - let lead = mk_gap(); - set_times(&lead, (-5, 1), (-2, 1), (3, 1), (-5, 1)); - unsafe { oaknode_track_prepend_block(t2.clone(), c.clone()) }; - unsafe { oaknode_track_prepend_block(t2.clone(), g2.clone()) }; - unsafe { oaknode_track_prepend_block(t2.clone(), lead.clone()) }; - // Track order: [lead, g2, c]. - - let mut cmd = TimelineRippleDeleteGapsAtRegionsCommand::new( - seq.clone(), - vec![( - t1.clone(), - TimeRange::new(Rational::new(0, 1), Rational::new(5, 1)), - )], - ); - cmd.prepare(); - assert!(cmd.has_commands()); - - cmd.redo(); - assert_eq!(blen(&g1), (5, 1)); - // g2 was removed outright (its length equals the ripple length). - assert_eq!(count(&t2), 2); - assert!(track_ptr(&g2).is_null()); - - cmd.undo(); - assert_eq!(blen(&g1), (10, 1)); - assert_eq!(count(&t2), 3); - assert_eq!(blen(&g2), (5, 1)); -} - -/// A clip strictly after the earliest point whose predecessor is not a -/// gap zeroes the ripple length and produces no commands. -#[test] -fn ripple_delete_gaps_prev_not_gap_breaks() { - let g1 = mk_gap(); - set_times(&g1, (0, 1), (10, 1), (10, 1), (0, 1)); - let (seq, t1) = seq_with_video_track(vec![g1.clone()]); - - let list = make_list(0); - seq_add_list(&seq, &list); - let t2 = make_track(); - list_add_track(&list, &t2); - let c = mk_clip(); - set_times(&c, (3, 1), (5, 1), (2, 1), (3, 1)); - unsafe { oaknode_track_prepend_block(t2.clone(), c.clone()) }; - - let mut cmd = TimelineRippleDeleteGapsAtRegionsCommand::new( - seq.clone(), - vec![( - t1.clone(), - TimeRange::new(Rational::new(0, 1), Rational::new(5, 1)), - )], - ); - cmd.prepare(); - assert!(!cmd.has_commands()); - cmd.redo(); - cmd.undo(); -} - -/// A locked track is skipped when synchronising gaps. -#[test] -fn ripple_delete_gaps_skips_locked_tracks() { - let g1 = mk_gap(); - set_times(&g1, (0, 1), (10, 1), (10, 1), (0, 1)); - let (seq, t1) = seq_with_video_track(vec![g1.clone()]); - - let list = make_list(0); - seq_add_list(&seq, &list); - let t_locked = make_track(); - unsafe { get_mut::(&t_locked).unwrap().locked = true }; - list_add_track(&list, &t_locked); - let g3 = mk_gap(); - set_times(&g3, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { oaknode_track_prepend_block(t_locked.clone(), g3.clone()) }; - - let mut cmd = TimelineRippleDeleteGapsAtRegionsCommand::new( - seq.clone(), - vec![( - t1.clone(), - TimeRange::new(Rational::new(0, 1), Rational::new(5, 1)), - )], - ); - cmd.prepare(); - assert!(cmd.has_commands()); - cmd.redo(); - assert_eq!(blen(&g1), (5, 1)); - // The locked track's gap was untouched. - assert_eq!(blen(&g3), (10, 1)); - cmd.undo(); -} - -/// Two regions on the same track exercise the insertion-sort ordering. -#[test] -fn ripple_delete_gaps_sorts_regions_on_track() { - let seq = make_sequence(); - let list = make_list(0); - seq_add_list(&seq, &list); - let t = make_track(); - list_add_track(&list, &t); - let gb = mk_gap(); - set_times(&gb, (5, 1), (8, 1), (3, 1), (5, 1)); - let ga = mk_gap(); - set_times(&ga, (0, 1), (3, 1), (3, 1), (0, 1)); - let lead = mk_gap(); - set_times(&lead, (-3, 1), (0, 1), (3, 1), (-3, 1)); - unsafe { oaknode_track_prepend_block(t.clone(), gb.clone()) }; - unsafe { oaknode_track_prepend_block(t.clone(), ga.clone()) }; - unsafe { oaknode_track_prepend_block(t.clone(), lead.clone()) }; - // Track order: [lead, ga, gb]. - - let mut cmd = TimelineRippleDeleteGapsAtRegionsCommand::new( - seq.clone(), - vec![ - ( - t.clone(), - TimeRange::new(Rational::new(0, 1), Rational::new(3, 1)), - ), - ( - t.clone(), - TimeRange::new(Rational::new(5, 1), Rational::new(8, 1)), - ), - ], - ); - cmd.prepare(); - assert!(cmd.has_commands()); - - cmd.redo(); - assert_eq!(count(&t), 1); - cmd.undo(); - assert_eq!(count(&t), 3); - assert_eq!(blen(&ga), (3, 1)); - assert_eq!(blen(&gb), (3, 1)); -} diff --git a/crates/oaktimeline/tests/domain_test.rs b/crates/oaktimeline/tests/domain_test.rs new file mode 100644 index 000000000..18a249438 --- /dev/null +++ b/crates/oaktimeline/tests/domain_test.rs @@ -0,0 +1,482 @@ +// 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 . + +//! Contract tests for the single-lib domain commands: the undo commands +//! drive a real oaknode project graph through `oaktimeline::util::NodeRef` +//! (the CHandle-based bridge signatures left with the deleted C ABI). +//! +//! Each test builds a project with a sequence/track-list/track, applies a +//! command through `redo`, checks the graph state, and `undo`s back to the +//! original state. + +use std::sync::{Arc, Mutex}; + +use oakcore_rs::{Rational, TimeRange}; +use oaknode::block::{ClipBlockBehavior, GapBlockBehavior}; +use oaknode::project::Project; +use oaknode::sequence::SequenceBehavior; +use oaknode::track::{TrackBehavior, TrackListBehavior, TrackType}; +use oaktimeline::undocommon::Command; +use oaktimeline::undogeneral::{ + BlockResizeCommand, TimelineAddTrackCommand, TimelineRemoveTrackCommand, + TrackReplaceBlockWithGapCommand, +}; +use oaktimeline::undopointer::TrackMoveBlockCommand; +use oaktimeline::undoripple::TrackRippleRemoveAreaCommand; +use oaktimeline::undosplit::BlockSplitCommand; +use oaktimeline::util::{ + block_clip_create, block_gap_create, block_in, block_length, block_out, block_track, + track_append_block, track_block_at, track_block_count, track_length, track_ripple_remove_block, + tracklist_track_count, NodeRef, +}; + +/// A project with one sequence owning one (video) track list. +fn make_project() -> Arc> { + let project = Project::new(); + let (seq_id, list_id) = { + let mut p = project.lock().unwrap(); + let (core, behavior) = SequenceBehavior::create(); + let seq_id = p.graph.add_node(core, behavior); + let (core, behavior) = TrackListBehavior::create(); + let list_id = p.graph.add_node(core, behavior); + if let Some(e) = p.graph.get_mut(seq_id) { + if let Some(a) = e.behavior.as_any_mut() { + if let Some(s) = a.downcast_mut::() { + s.track_lists.push(list_id); + } + } + } + if let Some(e) = p.graph.get_mut(list_id) { + if let Some(a) = e.behavior.as_any_mut() { + if let Some(l) = a.downcast_mut::() { + l.sequence = Some(seq_id); + } + } + } + (seq_id, list_id) + }; + let _ = (seq_id, list_id); + project +} + +/// The sequence and its video track list of `project`. +fn sequence_and_list( + project: &Arc>, +) -> (NodeRef, NodeRef) { + let p = project.lock().unwrap(); + let mut seq = None; + let mut list = None; + for id in p.graph.node_ids() { + if let Some(e) = p.graph.get(id) { + if e.behavior + .as_any() + .and_then(|a| a.downcast_ref::()) + .is_some() + { + seq = Some(NodeRef::new(project.clone(), id)); + } else if e + .behavior + .as_any() + .and_then(|a| a.downcast_ref::()) + .is_some() + { + list = Some(NodeRef::new(project.clone(), id)); + } + } + } + (seq.unwrap(), list.unwrap()) +} + +/// Add a clip block spanning `[in, out)` to `track`. +fn add_clip(track: &NodeRef, in_: Rational, out: Rational) -> NodeRef { + let clip = block_clip_create(&track.project); + { + let mut p = track.project.lock().unwrap(); + if let Some(e) = p.graph.get_mut(clip.id) { + if let Some(a) = e.behavior.as_any_mut() { + if let Some(c) = a.downcast_mut::() { + c.core.range = TimeRange::new(in_, out); + } + } + } + } + track_append_block(track, &clip); + clip +} + +/// Add a gap block spanning `[in, out)` to `track`. +fn add_gap(track: &NodeRef, in_: Rational, out: Rational) -> NodeRef { + let gap = block_gap_create(&track.project); + { + let mut p = track.project.lock().unwrap(); + if let Some(e) = p.graph.get_mut(gap.id) { + if let Some(a) = e.behavior.as_any_mut() { + if let Some(g) = a.downcast_mut::() { + g.core.range = TimeRange::new(in_, out); + } + } + } + } + track_append_block(track, &gap); + gap +} + +/// The block span of `block` (`None` for a stale node). +fn span_of(block: &NodeRef) -> Option<(Rational, Rational)> { + let p = block.project.lock().unwrap(); + p.graph.get(block.id).and_then(|e| e.behavior.as_any()).and_then(|a| { + [ + a.downcast_ref::().map(|b| &b.core), + a.downcast_ref::().map(|b| &b.core), + ] + .into_iter() + .flatten() + .next() + .map(|core| (core.in_(), core.out())) + }) +} + +/// `TimelineAddTrackCommand` redo appends a track to the list; undo removes +/// it again, and a second redo restores the same track node. +#[test] +fn add_track_command_redo_undo_cycle() { + let project = make_project(); + let (_seq, list) = sequence_and_list(&project); + + assert_eq!(tracklist_track_count(&list), 0); + let track = TimelineAddTrackCommand::run_immediately(list.clone()); + assert_eq!(tracklist_track_count(&list), 1); + assert_eq!(track_length(&track), Rational::new(0, 1)); + + let mut cmd = TimelineRemoveTrackCommand::new(track.clone()); + cmd.redo(); + assert_eq!(tracklist_track_count(&list), 0); + cmd.undo(); + assert_eq!(tracklist_track_count(&list), 1); + cmd.redo(); + assert_eq!(tracklist_track_count(&list), 0); +} + +/// `TimelineAddTrackCommand` creates the track with the list's media type. +#[test] +fn add_track_command_track_type() { + let project = make_project(); + let (_seq, list) = sequence_and_list(&project); + + let track = TimelineAddTrackCommand::run_immediately(list); + { + let p = project.lock().unwrap(); + let entry = p.graph.get(track.id).unwrap(); + let behavior = entry + .behavior + .as_any() + .and_then(|a| a.downcast_ref::()) + .unwrap(); + assert_eq!(behavior.kind, TrackType::Video); + } +} + +/// `BlockSplitCommand` splits a clip at a point: the original becomes the +/// first half `[in, point)` and the clone the second half `[point, out)`; +/// undo restores the single original block. +#[test] +fn split_command_redo_undo() { + let project = make_project(); + let (_seq, list) = sequence_and_list(&project); + let track = TimelineAddTrackCommand::run_immediately(list.clone()); + let clip = add_clip(&track, Rational::new(0, 1), Rational::new(100, 1)); + + let mut split = BlockSplitCommand::new(clip.clone(), Rational::new(40, 1)); + split.prepare(); + split.redo(); + + assert_eq!(block_length(&clip), Rational::new(40, 1)); + assert_eq!(block_in(&clip), Rational::new(0, 1)); + let second = split.new_block().unwrap(); + assert_eq!(block_in(&second), Rational::new(40, 1)); + assert_eq!(block_out(&second), Rational::new(100, 1)); + assert_eq!(track_block_count(&track), 2); + + split.undo(); + assert_eq!(block_length(&clip), Rational::new(100, 1)); + assert_eq!(block_out(&clip), Rational::new(100, 1)); + assert_eq!(track_block_count(&track), 1); + + // The undo detached the second half from the graph; the next redo + // re-attaches it with the same identity. + split.redo(); + assert_eq!(block_length(&clip), Rational::new(40, 1)); + assert_eq!(split.new_block().unwrap().id, second.id); + assert_eq!(track_block_count(&track), 2); +} + +/// `TrackReplaceBlockWithGapCommand` merges a clip into a following gap on +/// redo and restores both blocks on undo. +#[test] +fn replace_block_with_gap_round_trip() { + let project = make_project(); + let (_seq, list) = sequence_and_list(&project); + let track = TimelineAddTrackCommand::run_immediately(list); + let clip = add_clip(&track, Rational::new(0, 1), Rational::new(50, 1)); + let gap = add_gap(&track, Rational::new(50, 1), Rational::new(100, 1)); + + let mut cmd = TrackReplaceBlockWithGapCommand::new(track.clone(), clip.clone(), false); + cmd.redo(); + // The clip is gone; the gap absorbed its length (in-anchored at 50). + assert_eq!(track_block_count(&track), 1); + let remaining = track_block_at(&track, 0).unwrap(); + assert_eq!(remaining.id, gap.id); + assert_eq!(block_in(&remaining), Rational::new(50, 1)); + assert_eq!(block_length(&remaining), Rational::new(100, 1)); + assert!(block_track(&clip).is_none()); + + cmd.undo(); + assert_eq!(track_block_count(&track), 2); + let first = track_block_at(&track, 0).unwrap(); + assert_eq!(first.id, clip.id); + assert_eq!(block_in(&first), Rational::new(0, 1)); + assert_eq!(block_length(&first), Rational::new(50, 1)); + let second = track_block_at(&track, 1).unwrap(); + assert_eq!(second.id, gap.id); + assert_eq!(block_in(&second), Rational::new(50, 1)); + assert_eq!(block_length(&second), Rational::new(50, 1)); +} + +/// `TrackReplaceBlockWithGapCommand` creates its own gap when no +/// neighbouring gap exists, and swaps it back out on undo. +#[test] +fn replace_block_with_gap_creates_gap() { + let project = make_project(); + let (_seq, list) = sequence_and_list(&project); + let track = TimelineAddTrackCommand::run_immediately(list); + let clip = add_clip(&track, Rational::new(0, 1), Rational::new(50, 1)); + let tail = add_clip(&track, Rational::new(50, 1), Rational::new(100, 1)); + + let mut cmd = TrackReplaceBlockWithGapCommand::new(track.clone(), clip.clone(), false); + cmd.redo(); + // clip replaced by a fresh gap of the same length. + assert_eq!(track_block_count(&track), 2); + let first = track_block_at(&track, 0).unwrap(); + assert_ne!(first.id, clip.id); + assert_eq!(block_in(&first), Rational::new(0, 1)); + assert_eq!(block_length(&first), Rational::new(50, 1)); + assert_eq!(track_block_at(&track, 1).unwrap().id, tail.id); + + cmd.undo(); + assert_eq!(track_block_count(&track), 2); + let restored = track_block_at(&track, 0).unwrap(); + assert_eq!(restored.id, clip.id); + assert_eq!(block_length(&restored), Rational::new(50, 1)); +} + +/// `TrackRippleRemoveAreaCommand` trims the boundary blocks and removes +/// the blocks fully inside the range; undo restores everything. +#[test] +fn ripple_remove_area_round_trip() { + let project = make_project(); + let (_seq, list) = sequence_and_list(&project); + let track = TimelineAddTrackCommand::run_immediately(list); + let c1 = add_clip(&track, Rational::new(0, 1), Rational::new(50, 1)); + let c2 = add_clip(&track, Rational::new(50, 1), Rational::new(100, 1)); + let c3 = add_clip(&track, Rational::new(100, 1), Rational::new(150, 1)); + + let mut cmd = TrackRippleRemoveAreaCommand::new( + track.clone(), + TimeRange::new(Rational::new(25, 1), Rational::new(125, 1)), + ); + cmd.prepare(); + cmd.redo(); + + // c2 removed; c1 trimmed to [0,25); c3 out-anchored to [125,150). + assert_eq!(track_block_count(&track), 2); + let first = track_block_at(&track, 0).unwrap(); + assert_eq!(first.id, c1.id); + assert_eq!(span_of(&first), Some((Rational::new(0, 1), Rational::new(25, 1)))); + let second = track_block_at(&track, 1).unwrap(); + assert_eq!(second.id, c3.id); + assert_eq!( + span_of(&second), + Some((Rational::new(125, 1), Rational::new(150, 1))) + ); + + cmd.undo(); + assert_eq!(track_block_count(&track), 3); + assert_eq!(track_block_at(&track, 0).unwrap().id, c1.id); + assert_eq!(track_block_at(&track, 1).unwrap().id, c2.id); + assert_eq!(track_block_at(&track, 2).unwrap().id, c3.id); + assert_eq!(span_of(&c1), Some((Rational::new(0, 1), Rational::new(50, 1)))); + assert_eq!( + span_of(&c2), + Some((Rational::new(50, 1), Rational::new(100, 1))) + ); + assert_eq!( + span_of(&c3), + Some((Rational::new(100, 1), Rational::new(150, 1))) + ); +} + +/// `BlockResizeCommand` changes a block's length out-anchored and restores +/// it on undo. +#[test] +fn block_resize_round_trip() { + let project = make_project(); + let (_seq, list) = sequence_and_list(&project); + let track = TimelineAddTrackCommand::run_immediately(list); + let clip = add_clip(&track, Rational::new(0, 1), Rational::new(50, 1)); + + let mut cmd = BlockResizeCommand::new(clip.clone(), Rational::new(30, 1)); + cmd.redo(); + assert_eq!(block_length(&clip), Rational::new(30, 1)); + // Out-anchored: the in point shifts so the out stays at 50. + assert_eq!(block_out(&clip), Rational::new(50, 1)); + cmd.undo(); + assert_eq!(block_length(&clip), Rational::new(50, 1)); + assert_eq!(block_in(&clip), Rational::new(0, 1)); +} + +/// `TrackMoveBlockCommand` replaces the source spot with a gap and places +/// the block at the destination; undo restores the original layout. +#[test] +fn move_block_round_trip() { + let project = make_project(); + let (_seq, list) = sequence_and_list(&project); + let track = TimelineAddTrackCommand::run_immediately(list.clone()); + let clip = add_clip(&track, Rational::new(0, 1), Rational::new(50, 1)); + let tail = add_clip(&track, Rational::new(50, 1), Rational::new(100, 1)); + + let mut cmd = TrackMoveBlockCommand::new( + list, + 0, + clip.clone(), + Rational::new(150, 1), + ); + cmd.redo(); + // Layout: a gap fills the clip's old spot [0,50), the tail stays at + // [50,100), a bridging gap covers [100,150) and the moved clip is + // appended at [150,200). + assert_eq!(track_block_count(&track), 4); + assert_eq!(block_in(&clip), Rational::new(150, 1)); + assert_eq!(block_length(&clip), Rational::new(50, 1)); + let first = track_block_at(&track, 0).unwrap(); + assert_eq!(span_of(&first), Some((Rational::new(0, 1), Rational::new(50, 1)))); + assert_eq!(track_block_at(&track, 1).unwrap().id, tail.id); + let third = track_block_at(&track, 2).unwrap(); + assert_eq!( + span_of(&third), + Some((Rational::new(100, 1), Rational::new(150, 1))) + ); + assert_eq!(track_block_at(&track, 3).unwrap().id, clip.id); + + cmd.undo(); + assert_eq!(track_block_count(&track), 2); + assert_eq!(track_block_at(&track, 0).unwrap().id, clip.id); + assert_eq!(track_block_at(&track, 1).unwrap().id, tail.id); + assert_eq!(span_of(&clip), Some((Rational::new(0, 1), Rational::new(50, 1)))); + assert_eq!(span_of(&tail), Some((Rational::new(50, 1), Rational::new(100, 1)))); +} + +/// The `Command` trait dispatch (used by the oakundo vtable wrappers) +/// routes through the same redo/undo bodies as the inherent methods. +#[test] +fn commands_trait_dispatch() { + let project = make_project(); + let (_seq, list) = sequence_and_list(&project); + let track = TimelineAddTrackCommand::run_immediately(list); + let clip = add_clip(&track, Rational::new(0, 1), Rational::new(50, 1)); + + let mut resize = BlockResizeCommand::new(clip.clone(), Rational::new(10, 1)); + Command::redo(&mut resize); + assert_eq!(block_length(&clip), Rational::new(10, 1)); + Command::undo(&mut resize); + assert_eq!(block_length(&clip), Rational::new(50, 1)); +} + +/// `TrackPlaceBlockCommand` splices a clip into the middle of another: +/// the host block is split at the range in, the remainder is trimmed to +/// the range out, and the placed block lands in between; undo restores +/// the single host block. +#[test] +fn place_block_splice_round_trip() { + let project = make_project(); + let (_seq, list) = sequence_and_list(&project); + let track = TimelineAddTrackCommand::run_immediately(list.clone()); + let host = add_clip(&track, Rational::new(0, 1), Rational::new(100, 1)); + let placed = add_clip(&track, Rational::new(25, 1), Rational::new(35, 1)); + track_ripple_remove_block(&track, &placed); + + let mut cmd = oaktimeline::undopointer::TrackPlaceBlockCommand::new( + list.clone(), + 0, + placed.clone(), + Rational::new(25, 1), + ); + cmd.redo(); + // host split to [0,25), remainder trimmed to [25,90), placed at [25,35). + assert_eq!(track_block_count(&track), 3); + assert_eq!(track_block_at(&track, 0).unwrap().id, host.id); + assert_eq!(track_block_at(&track, 1).unwrap().id, placed.id); + assert_eq!( + span_of(&host), + Some((Rational::new(0, 1), Rational::new(25, 1))) + ); + + cmd.undo(); + assert_eq!(track_block_count(&track), 1); + assert_eq!(track_block_at(&track, 0).unwrap().id, host.id); + assert_eq!( + span_of(&host), + Some((Rational::new(0, 1), Rational::new(100, 1))) + ); +} + +/// `TrackListInsertGaps` splits a block at the insertion point and +/// inserts a gap of the requested length after it; undo removes the gap +/// and re-joins the block. +#[test] +fn insert_gaps_round_trip() { + let project = make_project(); + let (_seq, list) = sequence_and_list(&project); + let track = TimelineAddTrackCommand::run_immediately(list.clone()); + let clip = add_clip(&track, Rational::new(0, 1), Rational::new(100, 1)); + + let mut cmd = + oaktimeline::undogeneral::TrackListInsertGaps::new( + list.clone(), + Rational::new(40, 1), + Rational::new(20, 1), + ); + cmd.prepare(); + cmd.redo(); + // clip split to [0,40) + [40,100), gap [40,60) inserted after the + // first half. + assert_eq!(track_block_count(&track), 3); + let first = track_block_at(&track, 0).unwrap(); + assert_eq!(first.id, clip.id); + assert_eq!(span_of(&first), Some((Rational::new(0, 1), Rational::new(40, 1)))); + let gap = track_block_at(&track, 1).unwrap(); + assert_eq!( + span_of(&gap), + Some((Rational::new(40, 1), Rational::new(60, 1))) + ); + + cmd.undo(); + assert_eq!(track_block_count(&track), 1); + assert_eq!(track_block_at(&track, 0).unwrap().id, clip.id); + assert_eq!( + span_of(&clip), + Some((Rational::new(0, 1), Rational::new(100, 1))) + ); +} diff --git a/crates/oaktimeline/tests/edit_command_test.rs b/crates/oaktimeline/tests/edit_command_test.rs deleted file mode 100644 index f52ff141e..000000000 --- a/crates/oaktimeline/tests/edit_command_test.rs +++ /dev/null @@ -1,982 +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 . - -//! Contract tests for the edit-family undo commands -//! (`src/undogeneral.rs`, `src/undopointer.rs`, `src/undoripple.rs`, -//! `src/undosplit.rs`, `src/undotrack.rs`). These mirror -//! `include/timeline/edit.h`; each command boxes into a CHandle via -//! the bridge undo vtable instead of the C++ `UndoCommand` hierarchy. -#![cfg(feature = "test-stubs")] - -use oakcore_rs::{Rational, TimeRange}; -use oaktimeline::bridge::node::{ - oaknode_block_clip_create, oaknode_block_gap_create, oaknode_track_create, - oaknode_track_get_block_at, oaknode_track_get_block_count, oaknode_track_prepend_block, -}; -use oaktimeline::bridge::teststubs::{MockKind, MockNode}; -use oaktimeline::common::MovementMode; -use oaktimeline::handle::{get, get_mut, make_owned, CHandle, OAKTIMELINE_ABI_VERSION}; -use oaktimeline::undocommon::Command; -use oaktimeline::undogeneral::{ - BlockEnableDisableCommand, BlockResizeCommand, BlockResizeWithMediaInCommand, - BlockSetMediaInCommand, TimelineAddDefaultTransitionCommand, TimelineAddTrackCommand, - TimelineRemoveTrackCommand, TrackListInsertGaps, TrackReplaceBlockWithGapCommand, - TransitionRemoveCommand, -}; -use oaktimeline::undopointer::{ - BlockTrimCommand, TrackMoveBlockCommand, TrackPlaceBlockCommand, TrackSlideCommand, -}; -use oaktimeline::undoripple::{ - TimelineRippleDeleteGapsAtRegionsCommand, TimelineRippleRemoveAreaCommand, - TrackListRippleRemoveAreaCommand, TrackRippleRemoveAreaCommand, -}; -use oaktimeline::undosplit::{ - BlockSplitCommand, BlockSplitPreservingLinksCommand, TrackSplitAtTimeCommand, -}; -use oaktimeline::undotrack::{ - TrackInsertBlockAfterCommand, TrackPrependBlockCommand, TrackReplaceBlockCommand, -}; - -/// `TimelineAddTrackCommand` redo adds a track; undo removes it. The -/// newly-created track is exposed via `track()`. -#[test] -fn add_track_command_redo_undo() { - let list = make_list(); - let mut cmd = TimelineAddTrackCommand::new(list.clone()); - - cmd.redo(); - let t = cmd.track(); - assert!(!t.is_null()); - assert!(matches!(kind_of(&t), MockKind::Track)); - assert_eq!(track_type_of(&t), 0); - - cmd.undo(); -} - -/// `TimelineAddTrackCommand` with `automerge` uses the same add/remove -/// path but performs an automerge on redo. -#[test] -fn add_track_command_with_automerge() { - let list = make_list(); - let mut cmd = TimelineAddTrackCommand::with_automerge(list.clone(), true); - - cmd.redo(); - let t = cmd.track(); - assert!(!t.is_null()); - assert!(matches!(kind_of(&t), MockKind::Track)); - - cmd.undo(); -} - -/// `TimelineRemoveTrackCommand` redo removes the track; undo restores -/// it. -#[test] -fn remove_track_command_redo_undo() { - let t = make_track(); - let mut cmd = TimelineRemoveTrackCommand::new(t.clone()); - - cmd.prepare(); - cmd.redo(); - cmd.undo(); - - assert_eq!(count(&t), 0); -} - -/// `TrackPlaceBlockCommand` redo places the block at a track index and -/// in-point; undo returns the track to its prior state. -#[test] -fn place_block_command_redo_undo() { - let list = make_list(); - let t = make_track(); - list_add_track(&list, &t); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - let mut cmd = TrackPlaceBlockCommand::new(list.clone(), 0, b.clone(), Rational::new(0, 1)); - - cmd.redo(); - assert_eq!(count(&t), 1); - - cmd.undo(); - assert_eq!(count(&t), 0); -} - -/// `TrackMoveBlockCommand` redo gaps the block's old spot and places it at -/// the new in point (keeping the length); undo restores the original -/// position. -#[test] -fn track_move_block_redo_undo() { - let list = make_list(); - let t = make_track(); - list_add_track(&list, &t); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - - let mut cmd = TrackMoveBlockCommand::new(list.clone(), 0, b.clone(), Rational::new(20, 1)); - cmd.redo(); - // The old spot is a gap; the block sits at the new in point, length kept. - assert_eq!(count(&t), 2); - assert_eq!(b_in(&b), (20, 1)); - assert_eq!(blen(&b), (10, 1)); - - cmd.undo(); - assert_eq!(count(&t), 1); - assert_eq!(b_in(&b), (0, 1)); - - cmd.redo(); - assert_eq!(count(&t), 2); - assert_eq!(b_in(&b), (20, 1)); -} - -/// `TrackMoveBlockCommand` moves the block onto another track (the -/// destination track index need not match the source); undo moves it back. -#[test] -fn track_move_block_cross_track_redo_undo() { - let list = make_list(); - let t0 = make_track(); - let t1 = make_track(); - list_add_track(&list, &t0); - list_add_track(&list, &t1); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t0.clone(), b.clone()); - } - - let mut cmd = TrackMoveBlockCommand::new(list.clone(), 1, b.clone(), Rational::new(20, 1)); - cmd.redo(); - // t0's only block moved away (at-end gap: nothing left behind); t1 holds - // a lead-in gap plus the block. - assert_eq!(count(&t0), 0); - assert_eq!(count(&t1), 2); - assert_eq!(b_in(&b), (20, 1)); - - cmd.undo(); - assert_eq!(count(&t0), 1); - assert_eq!(count(&t1), 0); - assert_eq!(b_in(&b), (0, 1)); -} - -/// `BlockResizeCommand` redo resizes the block; undo restores its -/// original length (media out follows the resize). -#[test] -fn block_resize_command_redo_undo() { - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - let mut cmd = BlockResizeCommand::new(b.clone(), Rational::new(6, 1)); - - cmd.redo(); - assert_eq!(blen(&b), (6, 1)); - - cmd.undo(); - assert_eq!(blen(&b), (10, 1)); -} - -/// `BlockResizeWithMediaInCommand` resizes while keeping media out -/// fixed, shifting media in instead. -#[test] -fn block_resize_with_media_in_redo_undo() { - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - let mut cmd = BlockResizeWithMediaInCommand::new(b.clone(), Rational::new(6, 1)); - - cmd.redo(); - assert_eq!(blen(&b), (6, 1)); - assert_eq!(media_in(&b), (4, 1)); - - cmd.undo(); - assert_eq!(blen(&b), (10, 1)); - assert_eq!(media_in(&b), (0, 1)); -} - -/// `BlockSetMediaInCommand` redo sets the media-in point; undo -/// restores the original. -#[test] -fn block_set_media_in_redo_undo() { - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - let mut cmd = BlockSetMediaInCommand::new(b.clone(), Rational::new(4, 1)); - - cmd.redo(); - assert_eq!(media_in(&b), (4, 1)); - - cmd.undo(); - assert_eq!(media_in(&b), (0, 1)); -} - -/// `BlockTrimCommand` trims to a new length under a given movement -/// mode; undo restores the original length. Trim flags are settable -/// before execution. -#[test] -fn block_trim_command_redo_undo() { - let t = make_track(); - let g1 = mk_gap(); - let b = mk_clip(); - let c = mk_clip(); - set_times(&g1, (0, 1), (10, 1), (10, 1), (0, 1)); - set_times(&b, (10, 1), (20, 1), (10, 1), (0, 1)); - set_times(&c, (20, 1), (30, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), c.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), g1.clone()); - } - - let mut cmd = BlockTrimCommand::new( - t.clone(), - b.clone(), - Rational::new(6, 1), - MovementMode::TrimOut, - ); - cmd.prepare(); - - cmd.redo(); - assert_eq!(count(&t), 4); - assert_eq!(blen(&b), (6, 1)); - - cmd.undo(); - assert_eq!(count(&t), 3); - assert_eq!(blen(&b), (10, 1)); -} - -/// `TrackReplaceBlockWithGapCommand` redo swaps the block for a gap; -/// undo restores the block. -#[test] -fn replace_block_with_gap_redo_undo() { - let t = make_track(); - let g = mk_gap(); - let blk = mk_clip(); - let nxt = mk_clip(); - set_times(&g, (0, 1), (10, 1), (10, 1), (0, 1)); - set_times(&blk, (10, 1), (15, 1), (5, 1), (0, 1)); - set_times(&nxt, (15, 1), (25, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), nxt.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), blk.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), g.clone()); - } - - let mut cmd = TrackReplaceBlockWithGapCommand::new(t.clone(), blk.clone(), false); - - cmd.redo(); - assert_eq!(count(&t), 2); - assert_eq!(blen(&g), (15, 1)); - - cmd.undo(); - assert_eq!(count(&t), 3); - assert_eq!(blen(&g), (10, 1)); -} - -/// `BlockEnableDisableCommand` redo toggles enabled; undo restores. -#[test] -fn block_enable_disable_redo_undo() { - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - let mut cmd = BlockEnableDisableCommand::new(b.clone(), false); - - cmd.redo(); - assert!(!enabled(&b)); - - cmd.undo(); - assert!(enabled(&b)); -} - -/// `TrackPrependBlockCommand` redo inserts at the front; undo removes. -#[test] -fn track_prepend_block_redo_undo() { - let t = make_track(); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - - let b2 = mk_clip(); - set_times(&b2, (0, 1), (10, 1), (10, 1), (0, 1)); - let mut cmd = TrackPrependBlockCommand::new(t.clone(), b2.clone()); - - cmd.redo(); - assert_eq!(count(&t), 2); - - cmd.undo(); - assert_eq!(count(&t), 1); -} - -/// `TrackInsertBlockAfterCommand` redo inserts after a sibling; undo -/// removes. -#[test] -fn track_insert_block_after_redo_undo() { - let t = make_track(); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - - let b2 = mk_clip(); - set_times(&b2, (0, 1), (10, 1), (10, 1), (0, 1)); - let mut cmd = TrackInsertBlockAfterCommand::new(t.clone(), b2.clone(), b.clone()); - - cmd.redo(); - assert_eq!(count(&t), 2); - - cmd.undo(); - assert_eq!(count(&t), 1); -} - -/// `TrackReplaceBlockCommand` redo swaps the block; undo restores the -/// original. -#[test] -fn track_replace_block_redo_undo() { - let t = make_track(); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - - let b2 = mk_clip(); - set_times(&b2, (0, 1), (10, 1), (10, 1), (0, 1)); - let mut cmd = TrackReplaceBlockCommand::new(t.clone(), b.clone(), b2.clone()); - - cmd.redo(); - assert_eq!(count(&t), 1); - // `at()` returns a borrowed handle (raw value pointer), while `b2.ctx` - // is the owned `RefBox` pointer — compare the underlying value address. - assert_eq!(at(&t, 0).ctx as usize, addr(&b2) as usize); - assert!(track_ptr(&b).is_null()); - - cmd.undo(); - assert_eq!(at(&t, 0).ctx as usize, addr(&b) as usize); - assert!(track_ptr(&b2).is_null()); -} - -/// The track commands dispatch through the `Command` trait, as the undo -/// stack's vtable invokes them. -#[test] -fn track_commands_trait_dispatch() { - let t = make_track(); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - - let mut prepend = TrackPrependBlockCommand::new(t.clone(), b.clone()); - Command::redo(&mut prepend); - assert_eq!(count(&t), 1); - Command::undo(&mut prepend); - assert_eq!(count(&t), 0); - - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - let b2 = mk_clip(); - set_times(&b2, (0, 1), (10, 1), (10, 1), (0, 1)); - let mut ins = TrackInsertBlockAfterCommand::new(t.clone(), b2.clone(), b.clone()); - Command::redo(&mut ins); - assert_eq!(count(&t), 2); - Command::undo(&mut ins); - assert_eq!(count(&t), 1); - - let b3 = mk_clip(); - set_times(&b3, (0, 1), (10, 1), (10, 1), (0, 1)); - let mut rep = TrackReplaceBlockCommand::new(t.clone(), b.clone(), b3.clone()); - Command::redo(&mut rep); - assert_eq!(at(&t, 0).ctx as usize, addr(&b3) as usize); - Command::undo(&mut rep); - assert_eq!(at(&t, 0).ctx as usize, addr(&b) as usize); -} - -/// `BlockSplitCommand` redo splits at a point; undo joins the pieces -/// back. `new_block` exposes the created half after redo. -#[test] -fn block_split_command_redo_undo() { - let t = make_track(); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - - let mut cmd = BlockSplitCommand::new(b.clone(), Rational::new(5, 1)); - cmd.prepare(); - - cmd.redo(); - assert_eq!(count(&t), 2); - assert_eq!(blen(&b), (5, 1)); - assert!(!cmd.new_block().is_null()); - - cmd.undo(); - assert_eq!(count(&t), 1); - assert_eq!(blen(&b), (10, 1)); -} - -/// `BlockSplitPreservingLinksCommand` splits many blocks at matching -/// times and keeps linked-media relations; undo rejoins them. -#[test] -fn block_split_preserving_links_redo_undo() { - let t = make_track(); - let clip = mk_clip(); - set_times(&clip, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), clip.clone()); - } - - let mut cmd = - BlockSplitPreservingLinksCommand::new(vec![clip.clone()], vec![Rational::new(5, 1)]); - cmd.prepare(); - assert_eq!(count(&t), 2); - assert!(cmd.get_split(clip.clone(), 0).is_some()); - - cmd.undo(); - assert_eq!(count(&t), 1); - assert_eq!(blen(&clip), (10, 1)); -} - -/// `TrackSplitAtTimeCommand` redo splits the block under the point; -/// undo rejoins. -#[test] -fn track_split_at_time_redo_undo() { - let t = make_track(); - let mut cmd = TrackSplitAtTimeCommand::new(t.clone(), Rational::new(5, 1)); - - cmd.prepare(); - cmd.redo(); - cmd.undo(); - - assert_eq!(count(&t), 0); -} - -/// `BlockSplitCommand::redo` without a prior `prepare` still creates the -/// second half (the C ABI command path may call redo directly). -#[test] -fn block_split_redo_without_prepare() { - let t = make_track(); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - - let mut cmd = BlockSplitCommand::new(b.clone(), Rational::new(4, 1)); - cmd.redo(); - assert_eq!(count(&t), 2); - assert_eq!(blen(&b), (4, 1)); - assert!(!cmd.new_block().is_null()); -} - -/// Split commands dispatch through the `Command` trait, and -/// `get_split` reports `None` for unknown blocks / out-of-range indices. -#[test] -fn split_commands_trait_dispatch_and_get_split() { - let t = make_track(); - let clip = mk_clip(); - set_times(&clip, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), clip.clone()); - } - - // `BlockSplitCommand` via the trait. - let mut s = BlockSplitCommand::new(clip.clone(), Rational::new(5, 1)); - s.prepare(); - assert_eq!(count(&t), 1); - Command::redo(&mut s); - assert_eq!(count(&t), 2); - Command::undo(&mut s); - assert_eq!(count(&t), 1); - - // `BlockSplitPreservingLinksCommand`: prepare applies, undo unsplits, - // redo re-applies; `get_split` None paths. - let other = mk_clip(); - let mut p = - BlockSplitPreservingLinksCommand::new(vec![clip.clone()], vec![Rational::new(5, 1)]); - p.prepare(); - assert_eq!(count(&t), 2); - assert!(p.get_split(clip.clone(), 0).is_some()); - assert!(p.get_split(clip.clone(), 5).is_none()); - assert!(p.get_split(other, 0).is_none()); - - Command::undo(&mut p); - assert_eq!(count(&t), 1); - Command::redo(&mut p); - assert_eq!(count(&t), 2); - - // `TrackSplitAtTimeCommand` via the trait (no inner command built). - let mut ts = TrackSplitAtTimeCommand::new(t.clone(), Rational::new(5, 1)); - Command::redo(&mut ts); - Command::undo(&mut ts); -} - -/// `TrackRippleRemoveAreaCommand` redo ripples out a range (splicing -/// surrounding blocks); undo re-inserts the removed material. The -/// spliced block and insertion index are queryable afterwards. -#[test] -fn track_ripple_remove_area_redo_undo() { - let t = make_track(); - let b = mk_clip(); - set_times(&b, (10, 1), (20, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - - let mut cmd = TrackRippleRemoveAreaCommand::new( - t.clone(), - TimeRange::new(Rational::new(10, 1), Rational::new(15, 1)), - ); - cmd.set_allow_splitting_gaps(true); - cmd.prepare(); - - cmd.redo(); - assert_eq!(blen(&b), (5, 1)); - assert_eq!(count(&t), 1); - - cmd.undo(); - assert_eq!(blen(&b), (10, 1)); - assert_eq!(count(&t), 1); -} - -/// `TrackListRippleRemoveAreaCommand` removes the same area across -/// every track in a list. -#[test] -fn track_list_ripple_remove_area_redo_undo() { - let list = make_list(); - let t = make_track(); - list_add_track(&list, &t); - let b = mk_clip(); - set_times(&b, (10, 1), (20, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - - let mut cmd = TrackListRippleRemoveAreaCommand::new( - list.clone(), - Rational::new(10, 1), - Rational::new(15, 1), - ); - cmd.prepare(); - - cmd.redo(); - assert_eq!(blen(&b), (5, 1)); - - cmd.undo(); - assert_eq!(blen(&b), (10, 1)); -} - -/// `TimelineRippleRemoveAreaCommand` removes an area across all -/// tracks in a timeline. -#[test] -fn timeline_ripple_remove_area_redo_undo() { - let seq = make_sequence(); - let list = make_list(); - seq_add_list(&seq, &list); - let t = make_track(); - list_add_track(&list, &t); - let b = mk_clip(); - set_times(&b, (10, 1), (20, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - - let mut cmd = TimelineRippleRemoveAreaCommand::new( - seq.clone(), - Rational::new(10, 1), - Rational::new(15, 1), - ); - - cmd.redo(); - assert_eq!(blen(&b), (5, 1)); - - cmd.undo(); - assert_eq!(blen(&b), (10, 1)); -} - -/// `TrackListInsertGaps` redo inserts a gap at a point of a given -/// length across a track list; undo removes it. -#[test] -fn track_list_insert_gaps_redo_undo() { - let list = make_list(); - let t = make_track(); - list_add_track(&list, &t); - let clip = mk_clip(); - set_times(&clip, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), clip.clone()); - } - - let mut cmd = TrackListInsertGaps::new(list.clone(), Rational::new(5, 1), Rational::new(3, 1)); - cmd.prepare(); - - cmd.redo(); - assert_eq!(count(&t), 2); - - cmd.undo(); - assert_eq!(count(&t), 1); -} - -/// `TimelineRippleDeleteGapsAtRegionsCommand` redo deletes gaps at the -/// given regions; undo re-inserts them. `has_commands` reports whether -/// any deletion was planned during `prepare`. -#[test] -fn ripple_delete_gaps_redo_undo() { - let seq = make_sequence(); - let list = make_list(); - seq_add_list(&seq, &list); - let t = make_track(); - list_add_track(&list, &t); - let gap = mk_gap(); - set_times(&gap, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), gap.clone()); - } - - let mut cmd = TimelineRippleDeleteGapsAtRegionsCommand::new( - seq.clone(), - vec![( - t.clone(), - TimeRange::new(Rational::new(0, 1), Rational::new(10, 1)), - )], - ); - cmd.prepare(); - assert!(cmd.has_commands()); - - cmd.redo(); - assert_eq!(count(&t), 0); - - // `oaknode_track_insert_block_after` prepends on a null predecessor, so - // the gap removed on redo is restored by undo. - cmd.undo(); - assert_eq!(count(&t), 1); -} - -/// `TrackSlideCommand` redo slides a block range by a movement; -/// undo restores the original positions. -#[test] -fn track_slide_command_redo_undo() { - let t = make_track(); - let g1 = mk_gap(); - let b = mk_clip(); - let g2 = mk_gap(); - set_times(&g1, (0, 1), (10, 1), (10, 1), (0, 1)); - set_times(&b, (10, 1), (20, 1), (10, 1), (0, 1)); - set_times(&g2, (20, 1), (30, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), g2.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), g1.clone()); - } - - let mut cmd = TrackSlideCommand::new( - t.clone(), - vec![b.clone()], - g1.clone(), - g2.clone(), - Rational::new(5, 1), - ); - cmd.prepare(); - - cmd.redo(); - assert_eq!(blen(&g1), (15, 1)); - assert_eq!(blen(&g2), (5, 1)); - - cmd.undo(); - assert_eq!(blen(&g1), (10, 1)); - assert_eq!(blen(&g2), (10, 1)); -} - -/// `TimelineAddDefaultTransitionCommand` redo inserts default -/// transitions between clips at a timebase; undo removes them. -#[test] -fn add_default_transition_redo_undo() { - let mut cmd = TimelineAddDefaultTransitionCommand::new(vec![], Rational::new(30, 1)); - - cmd.prepare(); - cmd.redo(); - cmd.undo(); -} - -/// `TransitionRemoveCommand` redo removes a transition; undo restores -/// it (optionally keeping it in the graph). -#[test] -fn transition_remove_redo_undo() { - let t = make_track(); - let b = mk_clip(); - let c = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - set_times(&c, (10, 1), (20, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), c.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - - let mut cmd = TransitionRemoveCommand::new(b.clone(), false); - - cmd.redo(); - assert_eq!(count(&t), 1); - assert!(track_ptr(&b).is_null()); - - // The stub's undo is a no-op and does not re-insert the removed - // block; the block count therefore stays unchanged. - cmd.undo(); - assert_eq!(count(&t), 1); -} - -/// Every edit command `to_command` produces a CHandle suitable for the -/// undo stack, and every `prepare` is idempotent for repeated redo. -#[test] -fn all_edit_commands_box_to_chandle() { - let seq = make_sequence(); - let list = make_list(); - seq_add_list(&seq, &list); - let t = make_track(); - list_add_track(&list, &t); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - let b2 = mk_clip(); - - let mut handles: Vec = Vec::new(); - handles.push(BlockResizeCommand::new(b.clone(), Rational::new(6, 1)).to_command()); - handles.push(BlockResizeWithMediaInCommand::new(b.clone(), Rational::new(6, 1)).to_command()); - handles.push(BlockSetMediaInCommand::new(b.clone(), Rational::new(4, 1)).to_command()); - handles.push(TimelineAddTrackCommand::new(list.clone()).to_command()); - handles.push(TimelineAddTrackCommand::with_automerge(list.clone(), true).to_command()); - handles.push(TimelineRemoveTrackCommand::new(t.clone()).to_command()); - handles.push(TransitionRemoveCommand::new(b.clone(), false).to_command()); - handles.push(TrackReplaceBlockWithGapCommand::new(t.clone(), b.clone(), false).to_command()); - handles.push(BlockEnableDisableCommand::new(b.clone(), false).to_command()); - handles.push( - TrackListInsertGaps::new(list.clone(), Rational::new(5, 1), Rational::new(3, 1)) - .to_command(), - ); - handles - .push(TimelineAddDefaultTransitionCommand::new(vec![], Rational::new(30, 1)).to_command()); - handles.push( - BlockTrimCommand::new( - t.clone(), - b.clone(), - Rational::new(6, 1), - MovementMode::TrimOut, - ) - .to_command(), - ); - handles.push( - TrackSlideCommand::new( - t.clone(), - vec![b.clone()], - b.clone(), - b2.clone(), - Rational::new(5, 1), - ) - .to_command(), - ); - handles.push( - TrackPlaceBlockCommand::new(list.clone(), 0, b.clone(), Rational::new(0, 1)).to_command(), - ); - handles.push( - TrackRippleRemoveAreaCommand::new( - t.clone(), - TimeRange::new(Rational::new(10, 1), Rational::new(15, 1)), - ) - .to_command(), - ); - handles.push( - TrackListRippleRemoveAreaCommand::new( - list.clone(), - Rational::new(10, 1), - Rational::new(15, 1), - ) - .to_command(), - ); - handles.push( - TimelineRippleRemoveAreaCommand::new( - seq.clone(), - Rational::new(10, 1), - Rational::new(15, 1), - ) - .to_command(), - ); - handles.push( - TimelineRippleDeleteGapsAtRegionsCommand::new( - seq.clone(), - vec![( - t.clone(), - TimeRange::new(Rational::new(0, 1), Rational::new(10, 1)), - )], - ) - .to_command(), - ); - handles.push(BlockSplitCommand::new(b.clone(), Rational::new(5, 1)).to_command()); - handles.push( - BlockSplitPreservingLinksCommand::new(vec![b.clone()], vec![Rational::new(5, 1)]) - .to_command(), - ); - handles.push(TrackSplitAtTimeCommand::new(t.clone(), Rational::new(5, 1)).to_command()); - handles.push(TrackPrependBlockCommand::new(t.clone(), b2.clone()).to_command()); - handles.push(TrackInsertBlockAfterCommand::new(t.clone(), b2.clone(), b.clone()).to_command()); - handles.push(TrackReplaceBlockCommand::new(t.clone(), b.clone(), b2.clone()).to_command()); - - for h in handles { - assert!(!h.is_null()); - assert_eq!(h.abi_version, OAKTIMELINE_ABI_VERSION); - } -} - -/// `MovementMode` round-trips through its C integer and -/// `is_a_trim_mode` distinguishes trim modes. -#[test] -fn movement_mode_c_round_trip() { - assert_eq!(MovementMode::None.to_c_int(), 0); - assert_eq!(MovementMode::Move.to_c_int(), 1); - assert_eq!(MovementMode::TrimIn.to_c_int(), 2); - assert_eq!(MovementMode::TrimOut.to_c_int(), 3); - - assert!(!MovementMode::None.is_a_trim_mode()); - assert!(!MovementMode::Move.is_a_trim_mode()); - assert!(MovementMode::TrimIn.is_a_trim_mode()); - assert!(MovementMode::TrimOut.is_a_trim_mode()); - - for v in 0..=3 { - assert_eq!(MovementMode::from_c_int(v).map(|m| m.to_c_int()), Some(v)); - } - assert!(MovementMode::from_c_int(9).is_none()); -} - -// ---- helpers --------------------------------------------------------- - -/// A new detached track of video type (0). -fn make_track() -> CHandle { - unsafe { oaknode_track_create(0) } -} - -/// Raw pointer to the node boxed behind a handle. -fn addr(h: &CHandle) -> *mut MockNode { - unsafe { get_mut::(h).unwrap() as *mut MockNode } -} - -/// Number of blocks on a track. -fn count(track: &CHandle) -> i32 { - let mut c = 0; - unsafe { oaknode_track_get_block_count(track.clone(), &mut c) }; - c -} - -/// Borrowed block at `index` on a track (for `.ctx` comparison only). -fn at(track: &CHandle, idx: i32) -> CHandle { - let mut o = CHandle::null(); - unsafe { oaknode_track_get_block_at(track.clone(), idx, &mut o) }; - o -} - -/// A new clip block. -fn mk_clip() -> CHandle { - unsafe { oaknode_block_clip_create() } -} - -/// A new gap block. -fn mk_gap() -> CHandle { - unsafe { oaknode_block_gap_create() } -} - -/// Set a block's in/out/length/media-in points directly. -fn set_times(h: &CHandle, in_: (i32, i32), out: (i32, i32), len: (i32, i32), mi: (i32, i32)) { - let b = unsafe { get_mut::(h).unwrap() }; - b.in_ = in_; - b.out = out; - b.length = len; - b.media_in = mi; -} - -/// A block's length as `(num, den)`. -fn blen(h: &CHandle) -> (i32, i32) { - unsafe { get::(h).unwrap().length } -} - -/// A block's in point as `(num, den)`. -fn b_in(h: &CHandle) -> (i32, i32) { - unsafe { get::(h).unwrap().in_ } -} - -/// A block's media-in as `(num, den)`. -fn media_in(h: &CHandle) -> (i32, i32) { - unsafe { get::(h).unwrap().media_in } -} - -/// A block's enabled flag. -fn enabled(h: &CHandle) -> bool { - unsafe { get::(h).unwrap().enabled } -} - -/// A block's owning-track raw pointer. -fn track_ptr(h: &CHandle) -> *mut MockNode { - unsafe { get::(h).unwrap().track } -} - -/// A node's kind. -fn kind_of(h: &CHandle) -> MockKind { - unsafe { get::(h).unwrap().kind } -} - -/// A node's track type. -fn track_type_of(h: &CHandle) -> i32 { - unsafe { get::(h).unwrap().track_type } -} - -/// A new empty track list of video type (0). -fn make_list() -> CHandle { - make_owned(MockNode { - kind: MockKind::TrackList, - track_type: 0, - ..Default::default() - }) -} - -/// Add a track to a track list (mock: push onto `blocks`). -fn list_add_track(list: &CHandle, t: &CHandle) { - unsafe { - get_mut::(list).unwrap().blocks.push(addr(t)); - } -} - -/// A new empty sequence. -fn make_sequence() -> CHandle { - make_owned(MockNode { - kind: MockKind::Sequence, - ..Default::default() - }) -} - -/// Add a track list to a sequence (mock: push onto `blocks`). -fn seq_add_list(seq: &CHandle, list: &CHandle) { - unsafe { - get_mut::(seq).unwrap().blocks.push(addr(list)); - } -} diff --git a/crates/oaktimeline/tests/ffi_contract_test.rs b/crates/oaktimeline/tests/ffi_contract_test.rs deleted file mode 100644 index efadad867..000000000 --- a/crates/oaktimeline/tests/ffi_contract_test.rs +++ /dev/null @@ -1,463 +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 . - -//! C ABI contract tests for the `#[no_mangle]` exports in -//! `src/ffi.rs`. Each export family is pinned with one normal and one -//! error path; the exhaustive matrix is driven by the existing C++ -//! gtest suite (`src/timeline/tests`) running against this crate. -//! Value-enum `displaymode.h` exports nothing, so it is covered purely -//! by constant compatibility, not by functions. -#![cfg(feature = "test-stubs")] - -use std::ffi::{c_char, c_int, CStr, CString}; - -use oaktimeline::bridge::teststubs::{MockKind, MockNode}; -use oaktimeline::common::{MovementMode, ThumbnailMode, WaveformMode}; -use oaktimeline::error::{ - OAKTIMELINE_E_FAILED, OAKTIMELINE_E_INVALID, OAKTIMELINE_E_NOMEM, OAKTIMELINE_E_NOT_FOUND, - OAKTIMELINE_E_STATE, OAKTIMELINE_OK, -}; -use oaktimeline::ffi; -use oaktimeline::handle::{make_owned, CHandle}; - -/// `oaktimeline_marker_list_create` returns a valid refcounted handle -/// stamped with the ABI version; `free` releases it without leaking. -#[test] -fn marker_list_create_free() { - let mut h = unsafe { ffi::marker::oaktimeline_marker_list_create() }; - assert!(!h.is_null()); - assert_eq!(h.abi_version, 1); - - // Free clears the handle; a second free is a harmless no-op. - unsafe { ffi::marker::oaktimeline_marker_list_free(&mut h) }; - assert!(h.is_null()); - unsafe { ffi::marker::oaktimeline_marker_list_free(&mut h) }; - assert!(h.is_null()); -} - -/// `oaktimeline_marker_list_of` returns a handle for an owner; freeing -/// it does not free the owner's copy. -#[test] -fn marker_list_of_returns_borrowed() { - let list_h = make_owned(oaktimeline::marker::TimelineMarkerList::new()); - let owner = make_owned(MockNode { - kind: MockKind::Node, - markers: list_h.clone(), - ..Default::default() - }); - - // A viewer node yields a non-null borrowed marker list handle. - let mut borrowed = unsafe { ffi::marker::oaktimeline_marker_list_of(owner.clone()) }; - assert!(!borrowed.is_null()); - assert_eq!(borrowed.abi_version, 1); - - // Freeing the borrowed handle does not invalidate the owner's copy. - unsafe { ffi::marker::oaktimeline_marker_list_free(&mut borrowed) }; - assert!(borrowed.is_null()); - - // The owner still owns its marker list; it can still hand out a new one. - let second = unsafe { ffi::marker::oaktimeline_marker_list_of(owner.clone()) }; - assert!(!second.is_null()); - - // An empty (null) owner handle is rejected as E_INVALID → null handle. - let null_owner = CHandle::null(); - let out = unsafe { ffi::marker::oaktimeline_marker_list_of(null_owner) }; - assert!(out.is_null()); -} - -/// `oaktimeline_marker_add` appends a marker; `oaktimeline_marker_count` -/// reflects it and `oaktimeline_marker_at` reads it back out. -#[test] -fn marker_add_count_at() { - let h = unsafe { ffi::marker::oaktimeline_marker_list_create() }; - assert!(!h.is_null()); - - let name = CString::new("mark").unwrap(); - let r = - unsafe { ffi::marker::oaktimeline_marker_add(h.clone(), 10, 1, 20, 1, name.as_ptr(), 3) }; - assert_eq!(r, OAKTIMELINE_OK); - - let mut count = 0; - let r = unsafe { ffi::marker::oaktimeline_marker_count(h.clone(), &mut count) }; - assert_eq!(r, OAKTIMELINE_OK); - assert_eq!(count, 1); - - let mut in_num = 0; - let mut in_den = 0; - let mut out_num = 0; - let mut out_den = 0; - let mut color = 0; - let mut buf = [0 as c_char; 32]; - let needed = unsafe { - ffi::marker::oaktimeline_marker_at( - h.clone(), - 0, - &mut in_num, - &mut in_den, - &mut out_num, - &mut out_den, - &mut color, - buf.as_mut_ptr(), - buf.len() as c_int, - ) - }; - assert!(needed > 0); - assert_eq!(in_num, 10); - assert_eq!(in_den, 1); - assert_eq!(out_num, 20); - assert_eq!(out_den, 1); - assert_eq!(color, 3); - let read = unsafe { CStr::from_ptr(buf.as_ptr()) }.to_string_lossy(); - assert_eq!(read, "mark"); - // `needed` is the name length + NUL terminator. - assert_eq!(needed as usize, "mark".len() + 1); -} - -/// `oaktimeline_marker_at` with an out-of-range index returns -/// `OAKTIMELINE_E_NOT_FOUND` and leaves outputs untouched. -#[test] -fn marker_at_out_of_range_errors() { - let h = unsafe { ffi::marker::oaktimeline_marker_list_create() }; - - let mut in_num = 99; - let mut in_den = 99; - let mut out_num = 99; - let mut out_den = 99; - let mut color = 99; - let mut buf = [1 as c_char; 32]; - let r = unsafe { - ffi::marker::oaktimeline_marker_at( - h.clone(), - 7, // out of range (empty list) - &mut in_num, - &mut in_den, - &mut out_num, - &mut out_den, - &mut color, - buf.as_mut_ptr(), - buf.len() as c_int, - ) - }; - assert_eq!(r, OAKTIMELINE_E_NOT_FOUND); - // Outputs are left untouched. - assert_eq!(in_num, 99); - assert_eq!(in_den, 99); - assert_eq!(out_num, 99); - assert_eq!(out_den, 99); - assert_eq!(color, 99); -} - -/// `oaktimeline_marker_add_command`/`remove_at_command`/ -/// `set_time_command`/`set_props_command` box into undo handles; a -/// null list yields `OAKTIMELINE_E_INVALID` (or a null handle). -#[test] -fn marker_commands_validate_list() { - let null_list = CHandle::null(); - - // Null list → null command handle (E_INVALID). - let add = unsafe { - ffi::marker::oaktimeline_marker_add_command( - null_list.clone(), - 0, - 1, - 0, - 1, - std::ptr::null(), - 0, - ) - }; - assert!(add.is_null()); - - let remove = unsafe { ffi::marker::oaktimeline_marker_remove_at_command(null_list.clone(), 0) }; - assert!(remove.is_null()); - - let set_time = unsafe { - ffi::marker::oaktimeline_marker_set_time_command(null_list.clone(), 0, 0, 1, 0, 1) - }; - assert!(set_time.is_null()); - - let set_props = unsafe { - ffi::marker::oaktimeline_marker_set_props_command(null_list.clone(), 0, 0, std::ptr::null()) - }; - assert!(set_props.is_null()); - - // A valid list produces a non-null command handle; out-of-range indices - // on an empty list produce null handles (E_NOT_FOUND). - let list = unsafe { ffi::marker::oaktimeline_marker_list_create() }; - assert!(!list.is_null()); - - let name = CString::new("m").unwrap(); - let add_ok = unsafe { - ffi::marker::oaktimeline_marker_add_command(list.clone(), 0, 1, 1, 1, name.as_ptr(), 5) - }; - assert!(!add_ok.is_null()); - - // Empty list: any index is out of range → null handle. - let remove_empty = - unsafe { ffi::marker::oaktimeline_marker_remove_at_command(list.clone(), 0) }; - assert!(remove_empty.is_null()); -} - -/// `oaktimeline_workarea_create`/`free` mirror the marker list -/// lifecycle. -#[test] -fn workarea_create_free() { - let mut w = unsafe { ffi::workarea::oaktimeline_workarea_create() }; - assert!(!w.is_null()); - assert_eq!(w.abi_version, 1); - - unsafe { ffi::workarea::oaktimeline_workarea_free(&mut w) }; - assert!(w.is_null()); - // Double free is a harmless no-op. - unsafe { ffi::workarea::oaktimeline_workarea_free(&mut w) }; - assert!(w.is_null()); -} - -/// `oaktimeline_workarea_set_enabled`/`get` round-trip the enabled -/// flag; `get` reports the range through its out-params. -#[test] -fn workarea_set_get_range() { - let w = unsafe { ffi::workarea::oaktimeline_workarea_create() }; - assert!(!w.is_null()); - - let r = unsafe { ffi::workarea::oaktimeline_workarea_set_enabled(w.clone(), 1) }; - assert_eq!(r, OAKTIMELINE_OK); - let r = unsafe { ffi::workarea::oaktimeline_workarea_set_range(w.clone(), 10, 1, 20, 1) }; - assert_eq!(r, OAKTIMELINE_OK); - - let mut in_num = 0; - let mut in_den = 0; - let mut out_num = 0; - let mut out_den = 0; - let mut enabled = 0; - let r = unsafe { - ffi::workarea::oaktimeline_workarea_get( - w.clone(), - &mut in_num, - &mut in_den, - &mut out_num, - &mut out_den, - &mut enabled, - ) - }; - assert_eq!(r, OAKTIMELINE_OK); - assert_eq!(in_num, 10); - assert_eq!(in_den, 1); - assert_eq!(out_num, 20); - assert_eq!(out_den, 1); - assert_eq!(enabled, 1); -} - -/// `oaktimeline_workarea_set_range` with an invalid (null) handle -/// returns `OAKTIMELINE_E_INVALID`. -#[test] -fn workarea_set_range_invalid_handle() { - let r = unsafe { ffi::workarea::oaktimeline_workarea_set_range(CHandle::null(), 10, 1, 20, 1) }; - assert_eq!(r, OAKTIMELINE_E_INVALID); - - let r = unsafe { ffi::workarea::oaktimeline_workarea_set_enabled(CHandle::null(), 1) }; - assert_eq!(r, OAKTIMELINE_E_INVALID); -} - -/// `oaktimeline_workarea_reset` clears a set range back to the reset -/// sentinel. -#[test] -fn workarea_reset_clears_range() { - let w = unsafe { ffi::workarea::oaktimeline_workarea_create() }; - unsafe { ffi::workarea::oaktimeline_workarea_set_range(w.clone(), 10, 1, 20, 1) }; - - let mut in_num = 0; - let mut in_den = 0; - let mut out_num = 0; - let mut out_den = 0; - let r = unsafe { - ffi::workarea::oaktimeline_workarea_reset( - &mut in_num, - &mut in_den, - &mut out_num, - &mut out_den, - ) - }; - assert_eq!(r, OAKTIMELINE_OK); - assert_eq!(in_num, 0); - assert_eq!(in_den, 1); - assert_eq!(out_num, 2147483647); - assert_eq!(out_den, 1); -} - -/// `oaktimeline_add_track_command`/`remove_track_command`/ -/// `place_block_command`/`trim_command`/`split_command`/`slide_command` -/// each return a CHandle and validate their inputs. -#[test] -fn edit_exports_return_handles() { - let null = CHandle::null(); - - // Invalid inputs yield null command handles. - assert!(unsafe { ffi::edit::oaktimeline_add_track_command(null.clone()) }.is_null()); - assert!(unsafe { ffi::edit::oaktimeline_remove_track_command(null.clone()) }.is_null()); - assert!(unsafe { - ffi::edit::oaktimeline_place_block_command(null.clone(), 0, null.clone(), 0, 1) - } - .is_null()); - assert!( - unsafe { ffi::edit::oaktimeline_trim_command(null.clone(), null.clone(), 0, 1, 2) } - .is_null() - ); - - // Valid handles produce non-null command handles. The mock nodes let - // the command constructors box without dereferencing graph state. - let tracklist = make_owned(MockNode { - kind: MockKind::TrackList, - ..Default::default() - }); - let track = make_owned(MockNode { - kind: MockKind::Track, - ..Default::default() - }); - let block = make_owned(MockNode { - kind: MockKind::Clip, - ..Default::default() - }); - - let add = unsafe { ffi::edit::oaktimeline_add_track_command(tracklist.clone()) }; - assert!(!add.is_null()); - let remove = unsafe { ffi::edit::oaktimeline_remove_track_command(track.clone()) }; - assert!(!remove.is_null()); - let place = unsafe { - ffi::edit::oaktimeline_place_block_command(tracklist.clone(), 0, block.clone(), 0, 1) - }; - assert!(!place.is_null()); - let trim = - unsafe { ffi::edit::oaktimeline_trim_command(track.clone(), block.clone(), 10, 1, 2) }; - assert!(!trim.is_null()); - - // split_command boxes into a multi command (mock returns a handle). - let blocks = [block.clone()]; - let split = unsafe { ffi::edit::oaktimeline_split_command(blocks.as_ptr(), 1, 5, 1) }; - assert!(!split.is_null()); - - // slide_command with a block array. - let slide_blocks = [block.clone()]; - let slide = unsafe { - ffi::edit::oaktimeline_slide_command( - track.clone(), - slide_blocks.as_ptr(), - 1, - null.clone(), - null.clone(), - 0, - 1, - ) - }; - assert!(!slide.is_null()); - - // Invalid block arrays yield null handles. - assert!(unsafe { ffi::edit::oaktimeline_split_command(std::ptr::null(), 1, 0, 1) }.is_null()); - assert!(unsafe { ffi::edit::oaktimeline_split_command(blocks.as_ptr(), 0, 0, 1) }.is_null()); - assert!(unsafe { - ffi::edit::oaktimeline_slide_command( - track.clone(), - std::ptr::null(), - 1, - null.clone(), - null.clone(), - 0, - 1, - ) - } - .is_null()); -} - -/// `oaktimeline_trim_command` and `oaktimeline_place_block_command` -/// accept rationals split into numerator/denominator pairs; a null -/// handle is rejected with `OAKTIMELINE_E_INVALID` (a null command -/// handle), and a non-trim movement mode is rejected the same way. -#[test] -fn edit_exports_validate_rationals() { - let track = make_owned(MockNode { - kind: MockKind::Track, - ..Default::default() - }); - let block = make_owned(MockNode { - kind: MockKind::Clip, - ..Default::default() - }); - let list = make_owned(MockNode { - kind: MockKind::TrackList, - ..Default::default() - }); - - // Rational pairs are accepted (num/den split), returning a handle. - let trim = - unsafe { ffi::edit::oaktimeline_trim_command(track.clone(), block.clone(), 10, 2, 2) }; - assert!(!trim.is_null()); - - let place = - unsafe { ffi::edit::oaktimeline_place_block_command(list.clone(), 0, block.clone(), 5, 1) }; - assert!(!place.is_null()); - - // A null track / null block is rejected with a null handle. - let null = CHandle::null(); - assert!( - unsafe { ffi::edit::oaktimeline_trim_command(null.clone(), block.clone(), 10, 1, 2) } - .is_null() - ); - assert!( - unsafe { ffi::edit::oaktimeline_trim_command(track.clone(), null.clone(), 10, 1, 2) } - .is_null() - ); - - // A non-trim movement mode (Move = 1) is rejected → null handle. - assert!( - unsafe { ffi::edit::oaktimeline_trim_command(track.clone(), block.clone(), 10, 1, 1) } - .is_null() - ); -} - -/// The exported `OAKTIMELINE_*` error constants match the -MMCCCC -/// scheme for module 04. -#[test] -fn error_constants_match_scheme() { - assert_eq!(OAKTIMELINE_OK, 0); - assert_eq!(OAKTIMELINE_E_INVALID, -40001); - assert_eq!(OAKTIMELINE_E_STATE, -40002); - assert_eq!(OAKTIMELINE_E_FAILED, -40003); - assert_eq!(OAKTIMELINE_E_NOT_FOUND, -40004); - assert_eq!(OAKTIMELINE_E_NOMEM, -40005); -} - -/// `displaymode.h` has no functions; only the value enums are -/// exported. This test pins that the enum constants are in range and -/// stable. -#[test] -fn displaymode_constants_are_stable() { - // OAK_TIMELINE_THUMBNAIL_OFF/IN_OUT/ON = 0/1/2. - assert_eq!(ThumbnailMode::Off as i32, 0); - assert_eq!(ThumbnailMode::InOut as i32, 1); - assert_eq!(ThumbnailMode::On as i32, 2); - - // OAK_TIMELINE_WAVEFORMS_DISABLED/ENABLED = 0/1. - assert_eq!(WaveformMode::Disabled as i32, 0); - assert_eq!(WaveformMode::Enabled as i32, 1); - - // MovementMode round-trips through from_c_int. - assert_eq!(MovementMode::None.to_c_int(), 0); - assert_eq!(MovementMode::Move.to_c_int(), 1); - assert_eq!(MovementMode::TrimIn.to_c_int(), 2); - assert_eq!(MovementMode::TrimOut.to_c_int(), 3); - assert_eq!(MovementMode::from_c_int(2), Some(MovementMode::TrimIn)); - assert_eq!(MovementMode::from_c_int(9), None); -} diff --git a/crates/oaktimeline/tests/ffi_coverage_test.rs b/crates/oaktimeline/tests/ffi_coverage_test.rs deleted file mode 100644 index 27b69b877..000000000 --- a/crates/oaktimeline/tests/ffi_coverage_test.rs +++ /dev/null @@ -1,598 +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 . - -//! Coverage for `src/ffi.rs`: the C ABI export layer (`marker`, `workarea` -//! and `edit` submodules). These tests drive every reachable branch of the -//! export bodies — both the success paths (constructing command handles, -//! enumerating markers/work areas) and the error paths (null / out-of-range / -//! wrong inputs mapping to `Error::Invalid` / `Error::NotFound`). -//! -//! A few branches are unreachable through the public API with the test-stub -//! mocks and are deliberately skipped (see the module summary): -//! * `marker_list_of` / `workarea_of` error branch (the mock -//! `oaknode_node_get_markers` / `oaknode_node_get_work_area` only fail on -//! a null `ctx`, which is already rejected earlier in the export); -//! * `marker_set_props_command` / `split_command` "multi init failed" branch -//! (`oakundo_command_init_multi` never returns null in the mock); -//! * `write_cstr` NULL/size<=0 guard (only ever called with a non-NULL -//! buffer and `size >= name.len()+1`); -//! * `xml_element_text` empty-result branch (only reached inside -//! `workarea_load`, whose loop stops before calling it with an invalid -//! reader). -#![cfg(feature = "test-stubs")] - -use std::ffi::c_char; -use std::ffi::CString; - -use oaktimeline::bridge::node as onode; -use oaktimeline::bridge::teststubs::{xml_reader_handle, MockKind, MockNode, MockXmlNode}; -use oaktimeline::error::{OAKTIMELINE_E_INVALID, OAKTIMELINE_OK}; -use oaktimeline::ffi::{edit, marker, workarea}; -use oaktimeline::handle::{make_owned, CHandle}; - -// ---- helpers --------------------------------------------------------- - -/// A NUL-terminated C string that lives for the call. -fn cstr(s: &str) -> CString { - CString::new(s).unwrap() -} - -/// A new detached video track (type 0). -fn mk_track() -> CHandle { - unsafe { onode::oaknode_track_create(0) } -} - -/// A new clip block. -fn mk_clip() -> CHandle { - unsafe { onode::oaknode_block_clip_create() } -} - -/// A new, empty owning marker list. -fn mk_marker_list() -> CHandle { - unsafe { marker::oaktimeline_marker_list_create() } -} - -/// A new, empty owning work area. -fn mk_workarea() -> CHandle { - unsafe { workarea::oaktimeline_workarea_create() } -} - -/// A new, empty owning track list of video type. -fn mk_track_list() -> CHandle { - make_owned(MockNode { - kind: MockKind::TrackList, - track_type: 0, - ..Default::default() - }) -} - -/// A new, empty owning sequence. -fn mk_sequence() -> CHandle { - make_owned(MockNode { - kind: MockKind::Sequence, - ..Default::default() - }) -} - -/// Add one marker to `list` (direct, no command) at in=1/1 out=5/1, color 3. -fn add_single_marker(list: &CHandle) { - let name = cstr("m1"); - let r = unsafe { marker::oaktimeline_marker_add(list.clone(), 1, 1, 5, 1, name.as_ptr(), 3) }; - assert_eq!(r, OAKTIMELINE_OK); -} - -// ---- marker module --------------------------------------------------- - -/// `oaktimeline_marker_list_create`/`marker_add`/`marker_count`/`marker_at` -/// success paths. -#[test] -fn marker_create_add_count_at() { - let list = mk_marker_list(); - add_single_marker(&list); - - let mut count = 0; - let r = unsafe { marker::oaktimeline_marker_count(list.clone(), &mut count) }; - assert_eq!(r, OAKTIMELINE_OK); - assert_eq!(count, 1); - - let mut in_num = 0; - let mut in_den = 0; - let mut out_num = 0; - let mut out_den = 0; - let mut color = 0; - let mut name_buf = [0 as c_char; 64]; - let needed = unsafe { - marker::oaktimeline_marker_at( - list.clone(), - 0, - &mut in_num, - &mut in_den, - &mut out_num, - &mut out_den, - &mut color, - name_buf.as_mut_ptr(), - name_buf.len() as i32, - ) - }; - assert!(needed > 0, "two-stage name length must be positive"); - assert_eq!(in_num, 1); - assert_eq!(out_num, 5); - assert_eq!(color, 3); -} - -/// Null-list error paths for `marker_add`/`marker_count`/`marker_at`. -#[test] -fn marker_null_list_errors() { - let name = cstr("x"); - let r = - unsafe { marker::oaktimeline_marker_add(CHandle::null(), 1, 1, 5, 1, name.as_ptr(), 0) }; - assert_eq!(r, OAKTIMELINE_E_INVALID); - - let mut count = 0; - let r = unsafe { marker::oaktimeline_marker_count(CHandle::null(), &mut count) }; - assert_eq!(r, OAKTIMELINE_E_INVALID); - - let mut out = 0; - let r = unsafe { - marker::oaktimeline_marker_at( - CHandle::null(), - 0, - &mut out, - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - 0, - ) - }; - assert_eq!(r, OAKTIMELINE_E_INVALID); -} - -/// `oaktimeline_marker_remove_at_command` success path (in-bounds index). -#[test] -fn marker_remove_at_command_success() { - let list = mk_marker_list(); - add_single_marker(&list); - let h = unsafe { marker::oaktimeline_marker_remove_at_command(list.clone(), 0) }; - assert!(!h.is_null()); -} - -/// `oaktimeline_marker_set_time_command` success and out-of-range paths. -#[test] -fn marker_set_time_command() { - let list = mk_marker_list(); - add_single_marker(&list); - - // Success: in-bounds index returns an owning command handle. - let h = unsafe { marker::oaktimeline_marker_set_time_command(list.clone(), 0, 2, 1, 6, 1) }; - assert!(!h.is_null()); - - // Out-of-range index -> NotFound. - let h2 = unsafe { marker::oaktimeline_marker_set_time_command(list.clone(), 9, 2, 1, 6, 1) }; - assert!(h2.is_null()); -} - -/// `oaktimeline_marker_set_props_command` covers every branch: combined -/// color+name (multi command), color-only, name-only, the invalid -/// color<0&name==NULL case, and the out-of-range NotFound case. -#[test] -fn marker_set_props_command_branches() { - let list = mk_marker_list(); - add_single_marker(&list); - - // Combined color + name -> multi command. - let name = cstr("renamed"); - let h = - unsafe { marker::oaktimeline_marker_set_props_command(list.clone(), 0, 7, name.as_ptr()) }; - assert!(!h.is_null()); - - // Color-only (name == NULL) -> color child; also exercises the - // `cstr_to_string(NULL)` empty-string path. - let h = unsafe { - marker::oaktimeline_marker_set_props_command(list.clone(), 0, 9, std::ptr::null()) - }; - assert!(!h.is_null()); - - // Name-only (color < 0) -> name child. - let name = cstr("onlyname"); - let h = - unsafe { marker::oaktimeline_marker_set_props_command(list.clone(), 0, -1, name.as_ptr()) }; - assert!(!h.is_null()); - - // color < 0 AND name == NULL -> Invalid. - let h = unsafe { - marker::oaktimeline_marker_set_props_command(list.clone(), 0, -1, std::ptr::null()) - }; - assert!(h.is_null()); - - // Out-of-range index -> NotFound. - let name = cstr("x"); - let h = - unsafe { marker::oaktimeline_marker_set_props_command(list.clone(), 50, 1, name.as_ptr()) }; - assert!(h.is_null()); -} - -/// `oaktimeline_marker_list_of` with a null owner and with a viewer node -/// whose marker list is populated. -#[test] -fn marker_list_of() { - // Null owner -> null handle. - let h = unsafe { marker::oaktimeline_marker_list_of(CHandle::null()) }; - assert!(h.is_null()); - - // A node carrying a marker list -> borrowed non-null handle. - let inner = mk_marker_list(); - let owner = make_owned(MockNode { - kind: MockKind::Node, - markers: inner.clone(), - ..Default::default() - }); - let h = unsafe { marker::oaktimeline_marker_list_of(owner.clone()) }; - assert!(!h.is_null()); -} - -/// `oaktimeline_marker_list_load` / `oaktimeline_marker_list_save` with -/// null arguments. -#[test] -fn marker_list_load_save_null() { - let list = mk_marker_list(); - // Null list (reader also null) -> Invalid. - let r = unsafe { marker::oaktimeline_marker_list_load(CHandle::null(), CHandle::null()) }; - assert_eq!(r, OAKTIMELINE_E_INVALID); - // Null list for save -> Invalid. - let r = unsafe { marker::oaktimeline_marker_list_save(CHandle::null(), CHandle::null()) }; - assert_eq!(r, OAKTIMELINE_E_INVALID); - // Null reader / writer (valid list) -> Invalid. - let r = unsafe { marker::oaktimeline_marker_list_load(list.clone(), CHandle::null()) }; - assert_eq!(r, OAKTIMELINE_E_INVALID); - let r = unsafe { marker::oaktimeline_marker_list_save(list.clone(), CHandle::null()) }; - assert_eq!(r, OAKTIMELINE_E_INVALID); -} - -/// `oaktimeline_marker_list_load` over a reader with a marker element and a -/// non-marker element (exercises the skip/`other` path). -#[test] -fn marker_list_load_reader() { - let list = mk_marker_list(); - let reader = xml_reader_handle(vec![ - MockXmlNode { - name: "other".into(), - text: String::new(), - attrs: vec![], - }, - MockXmlNode { - name: "marker".into(), - text: String::new(), - attrs: vec![("name".into(), "m2".into()), ("color".into(), "5".into())], - }, - ]); - let r = unsafe { marker::oaktimeline_marker_list_load(list.clone(), reader.clone()) }; - assert_eq!(r, OAKTIMELINE_OK); - - let mut count = 0; - unsafe { marker::oaktimeline_marker_count(list.clone(), &mut count) }; - assert_eq!(count, 1); -} - -/// `oaktimeline_marker_list_save` over a writer with a marker present. -#[test] -fn marker_list_save_writer() { - let list = mk_marker_list(); - add_single_marker(&list); - let writer = unsafe { oaktimeline::bridge::common::oakcommon_xml_writer_init() }; - let r = unsafe { marker::oaktimeline_marker_list_save(list.clone(), writer.clone()) }; - assert_eq!(r, OAKTIMELINE_OK); -} - -// ---- workarea module ------------------------------------------------- - -/// `oaktimeline_workarea_of` with a null owner and with a viewer node whose -/// work area is populated. -#[test] -fn workarea_of() { - // Null owner -> null handle. - let h = unsafe { workarea::oaktimeline_workarea_of(CHandle::null()) }; - assert!(h.is_null()); - - // A node carrying a work area -> borrowed non-null handle. - let inner = mk_workarea(); - let owner = make_owned(MockNode { - kind: MockKind::Node, - work_area: inner.clone(), - ..Default::default() - }); - let h = unsafe { workarea::oaktimeline_workarea_of(owner.clone()) }; - assert!(!h.is_null()); -} - -/// `oaktimeline_workarea_get` null-handle error path. -#[test] -fn workarea_get_null() { - let r = unsafe { - workarea::oaktimeline_workarea_get( - CHandle::null(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }; - assert_eq!(r, OAKTIMELINE_E_INVALID); -} - -/// `oaktimeline_workarea_set_range_command` null and valid paths. -#[test] -fn workarea_set_range_command() { - // Null work area -> null handle. - let h = unsafe { - workarea::oaktimeline_workarea_set_range_command(CHandle::null(), 1, 1, 2, 1, 0, 1, 1, 1) - }; - assert!(h.is_null()); - - // Valid work area -> owning command handle. - let w = mk_workarea(); - let h = unsafe { - workarea::oaktimeline_workarea_set_range_command(w.clone(), 1, 1, 2, 1, 0, 1, 1, 1) - }; - assert!(!h.is_null()); -} - -/// `oaktimeline_workarea_set_enabled_command` null and valid paths. -#[test] -fn workarea_set_enabled_command() { - let h = unsafe { workarea::oaktimeline_workarea_set_enabled_command(CHandle::null(), 1) }; - assert!(h.is_null()); - - let w = mk_workarea(); - let h = unsafe { workarea::oaktimeline_workarea_set_enabled_command(w.clone(), 1) }; - assert!(!h.is_null()); -} - -/// `oaktimeline_workarea_reset` null-pointer and success paths. -#[test] -fn workarea_reset() { - // A null out pointer -> Invalid. - let r = unsafe { - workarea::oaktimeline_workarea_reset( - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }; - assert_eq!(r, OAKTIMELINE_E_INVALID); - - // Success: all four out params are filled. - let mut in_num = 0; - let mut in_den = 0; - let mut out_num = 0; - let mut out_den = 0; - let r = unsafe { - workarea::oaktimeline_workarea_reset(&mut in_num, &mut in_den, &mut out_num, &mut out_den) - }; - assert_eq!(r, OAKTIMELINE_OK); - // reset sentinel: in = 0/1, out = RATIONAL_MAX (2147483647/1). - assert_eq!(in_num, 0); - assert_eq!(in_den, 1); - assert_eq!(out_num, 2147483647); - assert_eq!(out_den, 1); -} - -/// `oaktimeline_workarea_load` null-path and the unknown-element skip branch. -#[test] -fn workarea_load() { - // Null work area (and reader) -> Invalid. - let r = unsafe { workarea::oaktimeline_workarea_load(CHandle::null(), CHandle::null()) }; - assert_eq!(r, OAKTIMELINE_E_INVALID); - - // Null reader with a valid work area -> Invalid. - let w = mk_workarea(); - let r = unsafe { workarea::oaktimeline_workarea_load(w.clone(), CHandle::null()) }; - assert_eq!(r, OAKTIMELINE_E_INVALID); - - // A reader with an unknown element hits the else/skip branch. - let reader = xml_reader_handle(vec![MockXmlNode { - name: "bogus".into(), - text: String::new(), - attrs: vec![], - }]); - let r = unsafe { workarea::oaktimeline_workarea_load(w.clone(), reader.clone()) }; - assert_eq!(r, OAKTIMELINE_OK); -} - -/// `oaktimeline_workarea_save` null-path and writer success path. -#[test] -fn workarea_save() { - let r = unsafe { workarea::oaktimeline_workarea_save(CHandle::null(), CHandle::null()) }; - assert_eq!(r, OAKTIMELINE_E_INVALID); - - let w = mk_workarea(); - let r = unsafe { workarea::oaktimeline_workarea_save(w.clone(), CHandle::null()) }; - assert_eq!(r, OAKTIMELINE_E_INVALID); - - let writer = unsafe { oaktimeline::bridge::common::oakcommon_xml_writer_init() }; - let r = unsafe { workarea::oaktimeline_workarea_save(w.clone(), writer.clone()) }; - assert_eq!(r, OAKTIMELINE_OK); -} - -// ---- edit module ----------------------------------------------------- - -/// `oaktimeline_replace_block_with_gap_command` null and valid paths. -#[test] -fn replace_block_with_gap_command() { - // Null track -> null handle. - let b = mk_clip(); - let h = unsafe { edit::oaktimeline_replace_block_with_gap_command(CHandle::null(), b.clone()) }; - assert!(h.is_null()); - // Null block -> null handle. - let t = mk_track(); - let h = unsafe { edit::oaktimeline_replace_block_with_gap_command(t.clone(), CHandle::null()) }; - assert!(h.is_null()); - - // Valid track + block -> owning command handle. - let h = unsafe { edit::oaktimeline_replace_block_with_gap_command(t.clone(), b.clone()) }; - assert!(!h.is_null()); -} - -/// `oaktimeline_move_block_command` null and valid paths. -#[test] -fn move_block_command() { - // Null list / block -> null handle. - let list = mk_track_list(); - let b = mk_clip(); - let h = unsafe { edit::oaktimeline_move_block_command(CHandle::null(), 0, b.clone(), 10, 1) }; - assert!(h.is_null()); - let h = - unsafe { edit::oaktimeline_move_block_command(list.clone(), 0, CHandle::null(), 10, 1) }; - assert!(h.is_null()); - - // Valid list + block + in point -> owning command handle. - let h = unsafe { edit::oaktimeline_move_block_command(list.clone(), 0, b.clone(), 10, 1) }; - assert!(!h.is_null()); -} - -/// `oaktimeline_split_preserving_links_command` null-arg error and success. -#[test] -fn split_preserving_links_command() { - // Error: null blocks / zero counts. - let h = unsafe { - edit::oaktimeline_split_preserving_links_command( - std::ptr::null(), - 1, - std::ptr::null(), - std::ptr::null(), - 1, - ) - }; - assert!(h.is_null()); - - // Error: blocks non-null but count <= 0. - let blocks = [mk_clip()]; - let nums = [10i64]; - let dens = [1i64]; - let h = unsafe { - edit::oaktimeline_split_preserving_links_command( - blocks.as_ptr(), - 0, - nums.as_ptr(), - dens.as_ptr(), - 1, - ) - }; - assert!(h.is_null()); - - // Success. - let h = unsafe { - edit::oaktimeline_split_preserving_links_command( - blocks.as_ptr(), - 1, - nums.as_ptr(), - dens.as_ptr(), - 1, - ) - }; - assert!(!h.is_null()); -} - -/// `oaktimeline_ripple_delete_gaps_command` null-arg error and success. -#[test] -fn ripple_delete_gaps_command() { - let seq = mk_sequence(); - let t = mk_track(); - let in_nums = [0i64]; - let in_dens = [1i64]; - let out_nums = [10i64]; - let out_dens = [1i64]; - let tracks = [t.clone()]; - - // Error: null sequence. - let h = unsafe { - edit::oaktimeline_ripple_delete_gaps_command( - CHandle::null(), - in_nums.as_ptr(), - in_dens.as_ptr(), - out_nums.as_ptr(), - out_dens.as_ptr(), - tracks.as_ptr(), - 1, - ) - }; - assert!(h.is_null()); - - // Error: null arrays / range_count <= 0. - let h = unsafe { - edit::oaktimeline_ripple_delete_gaps_command( - seq.clone(), - std::ptr::null(), - std::ptr::null(), - std::ptr::null(), - std::ptr::null(), - tracks.as_ptr(), - 1, - ) - }; - assert!(h.is_null()); - let h = unsafe { - edit::oaktimeline_ripple_delete_gaps_command( - seq.clone(), - in_nums.as_ptr(), - in_dens.as_ptr(), - out_nums.as_ptr(), - out_dens.as_ptr(), - tracks.as_ptr(), - 0, - ) - }; - assert!(h.is_null()); - - // Success. - let h = unsafe { - edit::oaktimeline_ripple_delete_gaps_command( - seq.clone(), - in_nums.as_ptr(), - in_dens.as_ptr(), - out_nums.as_ptr(), - out_dens.as_ptr(), - tracks.as_ptr(), - 1, - ) - }; - assert!(!h.is_null()); -} - -/// `oaktimeline_ripple_remove_area_command` null-track error and success. -#[test] -fn ripple_remove_area_command() { - let h = unsafe { edit::oaktimeline_ripple_remove_area_command(CHandle::null(), 0, 1, 10, 1) }; - assert!(h.is_null()); - - let t = mk_track(); - let h = unsafe { edit::oaktimeline_ripple_remove_area_command(t.clone(), 0, 1, 10, 1) }; - assert!(!h.is_null()); -} - -/// `oaktimeline_insert_gaps_command` null-list error and success. -#[test] -fn insert_gaps_command() { - let h = unsafe { edit::oaktimeline_insert_gaps_command(CHandle::null(), 5, 1, 3, 1) }; - assert!(h.is_null()); - - let list = mk_track_list(); - let h = unsafe { edit::oaktimeline_insert_gaps_command(list.clone(), 5, 1, 3, 1) }; - assert!(!h.is_null()); -} diff --git a/crates/oaktimeline/tests/marker_test.rs b/crates/oaktimeline/tests/marker_test.rs index ec9265a19..201951058 100644 --- a/crates/oaktimeline/tests/marker_test.rs +++ b/crates/oaktimeline/tests/marker_test.rs @@ -18,16 +18,12 @@ //! `TimelineMarker` and `TimelineMarkerList` value types plus the five //! marker undo commands. The list stays sorted by time (C++ //! `TimelineMarkerList` re-sorts on mutation), which `resort` enforces -//! after out-of-band edits. XML load/save matches the format written -//! by `timelinemarker.cpp`/`timelinemarkerlist.cpp`. -#![cfg(feature = "test-stubs")] +//! after out-of-band edits. The XML load/save contract left with the +//! deleted C ABI export layer (single-lib unification). use oakcore_rs::{Rational, TimeRange}; -use oaktimeline::bridge::common::{oakcommon_xml_writer_free, oakcommon_xml_writer_init}; -use oaktimeline::bridge::teststubs::{xml_reader_handle, MockXmlNode, MockXmlWriter}; use oaktimeline::common::EditToInfo; -use oaktimeline::ffi; -use oaktimeline::handle::{get, get_mut, make_owned, CHandle}; +use oaktimeline::handle::{get, get_mut, make_owned}; use oaktimeline::marker::{ MarkerAddCommand, MarkerChangeColorCommand, MarkerChangeNameCommand, MarkerChangeTimeCommand, MarkerRemoveCommand, TimelineMarker, TimelineMarkerList, @@ -592,10 +588,12 @@ fn marker_change_time_command_redo_undo() { ); } -/// `to_command` boxes a marker command into a CHandle for the undo -/// stack, matching the C++ `OakUndoCommand` shape. +/// `to_command` boxes a marker command into an oakundo `UndoCommand` +/// value for the undo stack (the old C ABI command handle is gone with +/// the single-lib unification); `redo_now`/`undo_now` drive the same +/// redo/undo bodies the stack would. #[test] -fn marker_commands_box_to_chandle() { +fn marker_commands_box_to_undo_command() { let list_h = make_owned(TimelineMarkerList::new()); { let l = unsafe { get_mut::(&list_h) }.unwrap(); @@ -606,81 +604,27 @@ fn marker_commands_box_to_chandle() { )); } - let cmds: Vec = vec![ - MarkerAddCommand::new( - list_h.clone(), - TimeRange::new(Rational::new(5, 1), Rational::new(6, 1)), - "n", - 0, - ) - .to_command(), - MarkerRemoveCommand::new(list_h.clone(), 0).to_command(), - MarkerChangeColorCommand::new(list_h.clone(), 0, 7).to_command(), - MarkerChangeNameCommand::new(list_h.clone(), 0, "z").to_command(), - MarkerChangeTimeCommand::new( - list_h.clone(), - 0, - TimeRange::new(Rational::new(9, 1), Rational::new(10, 1)), - ) - .to_command(), - ]; - for h in &cmds { - assert!(!h.is_null()); - assert_eq!(h.abi_version, 1); - } -} - -/// Save writes `` elements; load parses -/// them back into an equal list (XML golden round-trip). -#[test] -fn marker_list_xml_round_trip() { - // Set up a list with one marker. - let list_h = make_owned(TimelineMarkerList::new()); - { - let l = unsafe { get_mut::(&list_h) }.unwrap(); - l.add_marker(TimelineMarker::with_time( - 3, - TimeRange::new(Rational::new(10, 1), Rational::new(20, 1)), - "m", - )); - } - - // Save into a mock writer. - let mut writer = unsafe { oakcommon_xml_writer_init() }; - let r = unsafe { ffi::marker::oaktimeline_marker_list_save(list_h.clone(), writer.clone()) }; - assert_eq!(r, 0); - let buf = unsafe { get::(&writer) } - .unwrap() - .buf - .clone(); - assert!(buf.contains("(&list2_h) }.unwrap(); - assert_eq!(l2.size(), 1); - let m = l2.at(0).unwrap(); - assert_eq!(m.name(), "m"); - assert_eq!(m.color(), 3); - assert_eq!(m.time().in_(), Rational::new(10, 1)); - assert_eq!(m.time().out(), Rational::new(20, 1)); + let mut cmd = MarkerAddCommand::new( + list_h.clone(), + TimeRange::new(Rational::new(5, 1), Rational::new(6, 1)), + "n", + 0, + ) + .to_command(); + cmd.redo_now(); + assert_eq!( + unsafe { get::(&list_h) } + .unwrap() + .size(), + 2 + ); + cmd.undo_now(); + assert_eq!( + unsafe { get::(&list_h) } + .unwrap() + .size(), + 1 + ); } /// Loading a marker with a `color`/`in`/`out` attribute equal to the @@ -692,14 +636,15 @@ fn marker_defaults_map_to_edit_to_info() { assert_eq!(m.color(), 0); assert_eq!(m.time().in_(), Rational::new(0, 1)); - // `EditToInfo` defaults mirror those sentinels: null handles and the + // `EditToInfo` defaults mirror those sentinels: null node references + // (`None`, the single-lib replacement for the null `CHandle`) and the // null rational for `nearest_time`. let info = EditToInfo { - track: CHandle::null(), + track: None, nearest_time: m.time().in_(), - nearest_block: CHandle::null(), + nearest_block: None, }; - assert!(info.track.is_null()); - assert!(info.nearest_block.is_null()); + assert!(info.track.is_none()); + assert!(info.nearest_block.is_none()); assert_eq!(info.nearest_time, Rational::new(0, 1)); } diff --git a/crates/oaktimeline/tests/undocommon_test.rs b/crates/oaktimeline/tests/undocommon_test.rs deleted file mode 100644 index a9da76866..000000000 --- a/crates/oaktimeline/tests/undocommon_test.rs +++ /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 . - -//! Coverage for the shared node-removal helpers and command wrappers -//! (`src/undocommon.rs`): removal capability checks, remove-command -//! factories, `CHandleCommandWrapper`, and `MultiUndoCommand`. -#![cfg(feature = "test-stubs")] - -use oakcore_rs::{Rational, TimeRange}; - -use oaktimeline::bridge::node::{oaknode_block_clip_create, oaknode_block_gap_create}; -use oaktimeline::bridge::teststubs::MockNode; -use oaktimeline::handle::{get, make_owned, CHandle}; -use oaktimeline::marker::{MarkerAddCommand, TimelineMarkerList}; -use oaktimeline::undocommon::{ - block_can_be_removed, create_and_run_block_remove_command, create_and_run_remove_command, - create_block_remove_command, create_remove_command, free_command_handle, node_can_be_removed, - CHandleCommandWrapper, Command, MultiUndoCommand, -}; -use oaktimeline::workarea::{TimelineWorkArea, WorkareaSetEnabledCommand}; - -/// `node_can_be_removed` is true for a node with no output connections and -/// false once it has any. -#[test] -fn node_can_be_removed_toggles_on_connections() { - let free = make_owned(MockNode { - output_conns: 0, - ..Default::default() - }); - assert!(node_can_be_removed(free)); - - let busy = make_owned(MockNode { - output_conns: 1, - ..Default::default() - }); - assert!(!node_can_be_removed(busy)); -} - -/// `block_can_be_removed` delegates through the block's generic-node view. -#[test] -fn block_can_be_removed_by_connections() { - let clip = make_owned(MockNode { - output_conns: 0, - ..Default::default() - }); - assert!(block_can_be_removed(clip)); - - let gap = make_owned(MockNode { - output_conns: 2, - ..Default::default() - }); - assert!(!block_can_be_removed(gap)); -} - -/// Remove-command factories box a non-null command handle. -#[test] -fn create_remove_commands_box_non_null() { - let node = make_owned(MockNode::default()); - assert!(!create_remove_command(node.clone()).is_null()); - - let block = unsafe { oaknode_block_clip_create() }; - assert!(!create_block_remove_command(block).is_null()); - - assert!(!create_and_run_remove_command(node).is_null()); - - let gap = unsafe { oaknode_block_gap_create() }; - assert!(!create_and_run_block_remove_command(gap).is_null()); -} - -/// `free_command_handle` is a no-op on a null pointer and clears a live -/// command handle. -#[test] -fn free_command_handle_null_and_live() { - free_command_handle(std::ptr::null_mut()); - - let node = make_owned(MockNode::default()); - let mut cmd = create_remove_command(node); - assert!(!cmd.is_null()); - free_command_handle(&mut cmd); - assert!(cmd.ctx.is_null()); -} - -/// `CHandleCommandWrapper` forwards redo/undo to the wrapped command and -/// reports validity; an empty wrapper is inert. -#[test] -fn command_wrapper_forwards_redo_undo() { - let wa_h = make_owned(TimelineWorkArea::new()); - let cmd = WorkareaSetEnabledCommand::new(wa_h.clone(), true).to_command(); - - let mut w = CHandleCommandWrapper::new(cmd); - assert!(w.is_valid()); - w.redo(); - assert!(unsafe { get::(&wa_h) }.unwrap().enabled()); - w.undo(); - assert!(!unsafe { get::(&wa_h) }.unwrap().enabled()); - - // An empty wrapper is invalid and its methods are no-ops. - let mut empty = CHandleCommandWrapper::new(CHandle::null()); - assert!(!empty.is_valid()); - empty.redo(); - empty.undo(); - // Dropping `empty` and `w` frees the handles without panicking. -} - -/// `MultiUndoCommand` runs children in order on redo and reverse on undo. -#[test] -fn multi_undo_command_runs_children() { - let list_h = make_owned(TimelineMarkerList::new()); - let mut multi = MultiUndoCommand::new(); - assert!(multi.empty()); - - multi.add_child(Box::new(MarkerAddCommand::new( - list_h.clone(), - TimeRange::new(Rational::new(1, 1), Rational::new(2, 1)), - "m", - 0, - )) as Box); - multi.add_child(Box::new(MarkerAddCommand::new( - list_h.clone(), - TimeRange::new(Rational::new(5, 1), Rational::new(6, 1)), - "n", - 0, - )) as Box); - assert!(!multi.empty()); - - multi.redo(); - let l = unsafe { get::(&list_h) }.unwrap(); - assert_eq!(l.size(), 2); - - multi.undo(); - assert_eq!( - unsafe { get::(&list_h) } - .unwrap() - .size(), - 0 - ); -} - -/// `MultiUndoCommand::to_command` boxes the group into a command handle. -#[test] -fn multi_undo_command_boxes_to_chandle() { - let list_h = make_owned(TimelineMarkerList::new()); - let mut multi = MultiUndoCommand::new(); - multi.add_child(Box::new(MarkerAddCommand::new( - list_h.clone(), - TimeRange::new(Rational::new(1, 1), Rational::new(2, 1)), - "m", - 0, - )) as Box); - - let h = multi.to_command(); - assert!(!h.is_null()); - assert_eq!(h.abi_version, 1); -} diff --git a/crates/oaktimeline/tests/undogeneral_coverage_test.rs b/crates/oaktimeline/tests/undogeneral_coverage_test.rs deleted file mode 100644 index b87203dba..000000000 --- a/crates/oaktimeline/tests/undogeneral_coverage_test.rs +++ /dev/null @@ -1,639 +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 . - -//! Coverage for `src/undogeneral.rs`: the general-purpose timeline commands -//! (block resize / media-in, add / remove track, transition removal, gap -//! replacement, enable/disable, gap insertion and default transitions). -//! These tests exercise the `Command` trait dispatch bodies, the edge -//! branches of each command's inherent `redo`/`undo`/`prepare`, the -//! `run_immediately` factories and the `Drop` paths. -#![cfg(feature = "test-stubs")] - -use oakcore_rs::Rational; -use oaktimeline::bridge::node::{ - oaknode_block_clip_create, oaknode_block_gap_create, oaknode_track_create, - oaknode_track_get_block_at, oaknode_track_get_block_count, oaknode_track_prepend_block, -}; -use oaktimeline::bridge::teststubs::{MockKind, MockNode}; -use oaktimeline::handle::{get, get_mut, make_owned, CHandle}; -use oaktimeline::undocommon::Command; -use oaktimeline::undogeneral::{ - BlockEnableDisableCommand, BlockResizeCommand, BlockResizeWithMediaInCommand, - BlockSetMediaInCommand, TimelineAddDefaultTransitionCommand, TimelineAddTrackCommand, - TimelineRemoveTrackCommand, TrackListInsertGaps, TrackReplaceBlockWithGapCommand, - TransitionRemoveCommand, -}; - -/// The three resize/media-in commands dispatch through the `Command` trait, -/// which invokes the trait impl bodies (they forward to the inherent -/// methods). -#[test] -fn resize_commands_trait_dispatch() { - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - - let mut resize = BlockResizeCommand::new(b.clone(), Rational::new(6, 1)); - Command::redo(&mut resize); - assert_eq!(blen(&b), (6, 1)); - Command::undo(&mut resize); - assert_eq!(blen(&b), (10, 1)); - - let mut media_in = BlockResizeWithMediaInCommand::new(b.clone(), Rational::new(6, 1)); - Command::redo(&mut media_in); - assert_eq!(blen(&b), (6, 1)); - assert_eq!(media_in_of(&b), (4, 1)); - Command::undo(&mut media_in); - assert_eq!(blen(&b), (10, 1)); - - let mut set_mi = BlockSetMediaInCommand::new(b.clone(), Rational::new(4, 1)); - Command::redo(&mut set_mi); - assert_eq!(media_in_of(&b), (4, 1)); - Command::undo(&mut set_mi); - assert_eq!(media_in_of(&b), (0, 1)); -} - -/// `run_immediately` constructs, `redo`s and returns the created track in -/// one step; both the plain and the automerge variants are covered, and an -/// audio track list drives the audio input-id branch of `with_automerge`. -#[test] -fn add_track_run_immediately() { - let list = make_list(); - let t = TimelineAddTrackCommand::run_immediately(list.clone()); - assert!(!t.is_null()); - assert!(matches!(kind_of(&t), MockKind::Track)); - assert_eq!(track_type_of(&t), 0); - - let audio_list = make_audio_list(); - let ta = TimelineAddTrackCommand::run_immediately_with_automerge(audio_list.clone(), true); - assert!(!ta.is_null()); - assert_eq!(track_type_of(&ta), 1); -} - -/// `TimelineAddTrackCommand` and `TimelineRemoveTrackCommand` dispatch -/// through the `Command` trait. -#[test] -fn add_remove_track_trait_dispatch() { - let list = make_list(); - let mut add = TimelineAddTrackCommand::new(list.clone()); - Command::redo(&mut add); - assert!(!add.track().is_null()); - Command::undo(&mut add); - - let t = make_track(); - let mut remove = TimelineRemoveTrackCommand::new(t.clone()); - remove.prepare(); - Command::redo(&mut remove); - Command::undo(&mut remove); -} - -/// `TransitionRemoveCommand` with `remove_from_graph` builds a removal -/// command at redo, re-runs it at undo, and frees it on drop. -#[test] -fn transition_remove_graph_redo_undo_drop() { - let t = make_track(); - let b = mk_clip(); - let c = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - set_times(&c, (10, 1), (20, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), c.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - - { - let mut cmd = TransitionRemoveCommand::new(b.clone(), true); - cmd.redo(); - assert_eq!(count(&t), 1); - assert!(track_ptr(&b).is_null()); - cmd.undo(); - } -} - -/// `TransitionRemoveCommand` trait dispatch (with and without graph -/// removal). -#[test] -fn transition_remove_trait_dispatch() { - let t = make_track(); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - - let mut keep = TransitionRemoveCommand::new(b.clone(), false); - Command::redo(&mut keep); - Command::undo(&mut keep); - - let mut rm = TransitionRemoveCommand::new(b.clone(), true); - Command::redo(&mut rm); - Command::undo(&mut rm); -} - -/// `TrackReplaceBlockWithGapCommand` when the block is flanked by gaps on -/// both sides: the two gaps are merged, then undo restores both gaps and -/// the block; dropping the post-redo command frees the merged gap. -#[test] -fn replace_block_gap_merge_two_gaps() { - let t = make_track(); - let g1 = mk_gap(); - let blk = mk_clip(); - let g2 = mk_gap(); - set_times(&g1, (0, 1), (10, 1), (10, 1), (0, 1)); - set_times(&blk, (10, 1), (15, 1), (5, 1), (0, 1)); - set_times(&g2, (15, 1), (25, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), g2.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), blk.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), g1.clone()); - } - - { - let mut cmd = TrackReplaceBlockWithGapCommand::new(t.clone(), blk.clone(), false); - cmd.redo(); - assert_eq!(count(&t), 1); - assert_eq!(blen(&g1), (25, 1)); - cmd.undo(); - assert_eq!(count(&t), 3); - assert_eq!(blen(&g1), (10, 1)); - } - - // A second pass on a FRESH track, dropping the command after redo (no - // undo) exercises the merged-gap Drop path. - let t2 = make_track(); - { - let g1b = mk_gap(); - let blkb = mk_clip(); - let g2b = mk_gap(); - set_times(&g1b, (0, 1), (10, 1), (10, 1), (0, 1)); - set_times(&blkb, (10, 1), (15, 1), (5, 1), (0, 1)); - set_times(&g2b, (15, 1), (25, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t2.clone(), g2b.clone()); - } - unsafe { - oaknode_track_prepend_block(t2.clone(), blkb.clone()); - } - unsafe { - oaknode_track_prepend_block(t2.clone(), g1b.clone()); - } - - let mut cmd = TrackReplaceBlockWithGapCommand::new(t2.clone(), blkb.clone(), false); - cmd.redo(); - assert_eq!(count(&t2), 1); - } -} - -/// `TrackReplaceBlockWithGapCommand` when only the NEXT block is a gap: the -/// existing gap is extended rather than a new one created. -#[test] -fn replace_block_gap_extend_next_gap() { - let t = make_track(); - let blk = mk_clip(); - let g = mk_gap(); - set_times(&blk, (0, 1), (10, 1), (10, 1), (0, 1)); - set_times(&g, (10, 1), (20, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), g.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), blk.clone()); - } - - let mut cmd = TrackReplaceBlockWithGapCommand::new(t.clone(), blk.clone(), false); - cmd.redo(); - assert_eq!(count(&t), 1); - assert_eq!(blen(&g), (20, 1)); -} - -/// `TrackReplaceBlockWithGapCommand` when no gap neighbours the block: a -/// fresh gap is created and swapped in; undo swaps the block back and the -/// drop frees the re-orphaned gap. -#[test] -fn replace_block_gap_our_gap() { - let t = make_track(); - let blk = mk_clip(); - let nxt = mk_clip(); - set_times(&blk, (0, 1), (10, 1), (10, 1), (0, 1)); - set_times(&nxt, (10, 1), (20, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), nxt.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), blk.clone()); - } - - { - let mut cmd = TrackReplaceBlockWithGapCommand::new(t.clone(), blk.clone(), false); - cmd.redo(); - assert_eq!(count(&t), 2); - cmd.undo(); - assert_eq!(count(&t), 2); - assert_eq!(at(&t, 0).ctx as usize, addr(&blk) as usize); - } -} - -/// `TrackReplaceBlockWithGapCommand` when the block is at the end of a track -/// preceded by a gap: the block and the preceding gap are both removed. -#[test] -fn replace_block_gap_at_end_preceding_gap() { - let t = make_track(); - let g = mk_gap(); - let blk = mk_clip(); - set_times(&g, (0, 1), (10, 1), (10, 1), (0, 1)); - set_times(&blk, (10, 1), (20, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), blk.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), g.clone()); - } - - let mut cmd = TrackReplaceBlockWithGapCommand::new(t.clone(), blk.clone(), false); - cmd.redo(); - assert_eq!(count(&t), 0); -} - -/// `TrackReplaceBlockWithGapCommand` when the block is at the end of a track -/// preceded by a clip (not a gap): only the block is removed. -#[test] -fn replace_block_gap_at_end_preceding_clip() { - let t = make_track(); - let a = mk_clip(); - let blk = mk_clip(); - set_times(&a, (0, 1), (10, 1), (10, 1), (0, 1)); - set_times(&blk, (10, 1), (20, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), blk.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), a.clone()); - } - - let mut cmd = TrackReplaceBlockWithGapCommand::new(t.clone(), blk.clone(), false); - cmd.redo(); - assert_eq!(count(&t), 1); - assert_eq!(at(&t, 0).ctx as usize, addr(&a) as usize); -} - -/// `TrackReplaceBlockWithGapCommand` with `handle_transitions` checks the -/// neighbouring blocks for transitions (they are clips here, so no child -/// commands are produced). -#[test] -fn replace_block_gap_handle_transitions() { - let t = make_track(); - let a = mk_clip(); - let blk = mk_clip(); - let b = mk_clip(); - set_times(&a, (0, 1), (10, 1), (10, 1), (0, 1)); - set_times(&blk, (10, 1), (20, 1), (10, 1), (0, 1)); - set_times(&b, (20, 1), (30, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), blk.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), a.clone()); - } - - let mut cmd = TrackReplaceBlockWithGapCommand::new(t.clone(), blk.clone(), true); - cmd.redo(); - // The block is replaced by a gap of equal length, so the track keeps three - // blocks; the original block is detached. - assert_eq!(count(&t), 3); - assert!(track_ptr(&blk).is_null()); - assert!(matches!(kind_of(&at(&t, 1)), MockKind::Gap)); -} - -/// `TrackReplaceBlockWithGapCommand` trait dispatch. -#[test] -fn replace_block_gap_trait_dispatch() { - let t = make_track(); - let blk = mk_clip(); - set_times(&blk, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), blk.clone()); - } - - let mut cmd = TrackReplaceBlockWithGapCommand::new(t.clone(), blk.clone(), false); - Command::redo(&mut cmd); - Command::undo(&mut cmd); -} - -/// `BlockEnableDisableCommand` trait dispatch. -#[test] -fn enable_disable_trait_dispatch() { - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - let mut cmd = BlockEnableDisableCommand::new(b.clone(), false); - Command::redo(&mut cmd); - assert!(!enabled(&b)); - Command::undo(&mut cmd); - assert!(enabled(&b)); -} - -/// `TrackListInsertGaps` extends a gap crossed by the point; redo grows it -/// and undo restores the original length. -#[test] -fn insert_gaps_extend_gap() { - let list = make_list(); - let t = make_track(); - list_add_track(&list, &t); - let g = mk_gap(); - set_times(&g, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), g.clone()); - } - - let mut cmd = TrackListInsertGaps::new(list.clone(), Rational::new(5, 1), Rational::new(3, 1)); - cmd.prepare(); - cmd.redo(); - assert_eq!(blen(&g), (13, 1)); - cmd.undo(); - assert_eq!(blen(&g), (10, 1)); -} - -/// `TrackListInsertGaps` splits a clip crossed by the point and appends a -/// gap; redo applies the split + gap and undo reverts them. -#[test] -fn insert_gaps_split_clip() { - let list = make_list(); - let t = make_track(); - list_add_track(&list, &t); - let clip = mk_clip(); - set_times(&clip, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), clip.clone()); - } - - let mut cmd = TrackListInsertGaps::new(list.clone(), Rational::new(5, 1), Rational::new(3, 1)); - cmd.prepare(); - cmd.redo(); - // `TrackListInsertGaps` builds the split command but does not call its - // `prepare`, so on redo only the appended gap is inserted (no split). - assert_eq!(count(&t), 2); - cmd.undo(); - assert_eq!(count(&t), 1); - assert_eq!(blen(&clip), (10, 1)); -} - -/// `TrackListInsertGaps::prepare` with a clip whose in point equals the -/// insertion point: no split, a gap is appended after a null predecessor. -#[test] -fn insert_gaps_clip_at_point() { - let list = make_list(); - let t = make_track(); - list_add_track(&list, &t); - let clip = mk_clip(); - set_times(&clip, (5, 1), (10, 1), (5, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), clip.clone()); - } - - let mut cmd = TrackListInsertGaps::new(list.clone(), Rational::new(5, 1), Rational::new(3, 1)); - cmd.prepare(); - cmd.redo(); - cmd.undo(); -} - -/// `TrackListInsertGaps::prepare` with a clip whose out point equals the -/// point and no following block: no gap is added at all. -#[test] -fn insert_gaps_clip_at_end() { - let list = make_list(); - let t = make_track(); - list_add_track(&list, &t); - let clip = mk_clip(); - set_times(&clip, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), clip.clone()); - } - - let mut cmd = TrackListInsertGaps::new(list.clone(), Rational::new(10, 1), Rational::new(3, 1)); - cmd.prepare(); - cmd.redo(); - cmd.undo(); -} - -/// `TrackListInsertGaps::prepare` skips a null track and a locked track in -/// the list. -#[test] -fn insert_gaps_locked_and_null_track() { - let list = make_list(); - // A null entry in the track list is skipped. - unsafe { - get_mut::(&list) - .unwrap() - .blocks - .push(std::ptr::null_mut()); - } - let locked = make_track(); - unsafe { - get_mut::(&locked).unwrap().locked = true; - } - list_add_track(&list, &locked); - - let mut cmd = TrackListInsertGaps::new(list.clone(), Rational::new(5, 1), Rational::new(3, 1)); - cmd.prepare(); - cmd.redo(); - cmd.undo(); -} - -/// `TrackListInsertGaps::prepare` skips a null block within a track's block -/// list. -#[test] -fn insert_gaps_null_block() { - let list = make_list(); - let t = make_track(); - unsafe { - get_mut::(&t) - .unwrap() - .blocks - .push(std::ptr::null_mut()); - } - list_add_track(&list, &t); - - let mut cmd = TrackListInsertGaps::new(list.clone(), Rational::new(5, 1), Rational::new(3, 1)); - cmd.prepare(); - cmd.redo(); - cmd.undo(); -} - -/// `TrackListInsertGaps` trait dispatch. -#[test] -fn insert_gaps_trait_dispatch() { - let list = make_list(); - let t = make_track(); - list_add_track(&list, &t); - let g = mk_gap(); - set_times(&g, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), g.clone()); - } - - let mut cmd = TrackListInsertGaps::new(list.clone(), Rational::new(5, 1), Rational::new(3, 1)); - Command::redo(&mut cmd); - Command::undo(&mut cmd); -} - -/// `TimelineAddDefaultTransitionCommand::prepare` drives every in/out -/// transition branch: a clip with a gap before and after, and two adjacent -/// selected clips (dual transition / no-op for the in side). -#[test] -fn add_default_transition_prepare_branches() { - let t = make_track(); - let g1 = mk_gap(); - let a = mk_clip(); - let b = mk_clip(); - let g2 = mk_gap(); - set_times(&g1, (0, 1), (10, 1), (10, 1), (0, 1)); - set_times(&a, (10, 1), (20, 1), (10, 1), (0, 1)); - set_times(&b, (20, 1), (30, 1), (10, 1), (0, 1)); - set_times(&g2, (30, 1), (40, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), g2.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), a.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), g1.clone()); - } - - let mut cmd = - TimelineAddDefaultTransitionCommand::new(vec![a.clone(), b.clone()], Rational::new(30, 1)); - cmd.prepare(); - cmd.redo(); - cmd.undo(); -} - -/// `TimelineAddDefaultTransitionCommand` trait dispatch. -#[test] -fn add_default_transition_trait_dispatch() { - let mut cmd = TimelineAddDefaultTransitionCommand::new(vec![], Rational::new(30, 1)); - Command::redo(&mut cmd); - Command::undo(&mut cmd); -} - -// ---- helpers --------------------------------------------------------- - -/// A new detached track of video type (0). -fn make_track() -> CHandle { - unsafe { oaknode_track_create(0) } -} - -/// A new empty track list of video type (0). -fn make_list() -> CHandle { - make_owned(MockNode { - kind: MockKind::TrackList, - track_type: 0, - ..Default::default() - }) -} - -/// A new empty track list of audio type (1). -fn make_audio_list() -> CHandle { - make_owned(MockNode { - kind: MockKind::TrackList, - track_type: 1, - ..Default::default() - }) -} - -/// Add a track to a track list (mock: push onto `blocks`). -fn list_add_track(list: &CHandle, t: &CHandle) { - unsafe { - get_mut::(list).unwrap().blocks.push(addr(t)); - } -} - -/// Raw pointer to the node boxed behind a handle. -fn addr(h: &CHandle) -> *mut MockNode { - unsafe { get_mut::(h).unwrap() as *mut MockNode } -} - -/// A new clip block. -fn mk_clip() -> CHandle { - unsafe { oaknode_block_clip_create() } -} - -/// A new gap block. -fn mk_gap() -> CHandle { - unsafe { oaknode_block_gap_create() } -} - -/// Set a block's in/out/length/media-in points directly. -fn set_times(h: &CHandle, in_: (i32, i32), out: (i32, i32), len: (i32, i32), mi: (i32, i32)) { - let b = unsafe { get_mut::(h).unwrap() }; - b.in_ = in_; - b.out = out; - b.length = len; - b.media_in = mi; -} - -/// Number of blocks on a track. -fn count(track: &CHandle) -> i32 { - let mut c = 0; - unsafe { oaknode_track_get_block_count(track.clone(), &mut c) }; - c -} - -/// Borrowed block at `index` on a track (for `.ctx` comparison only). -fn at(track: &CHandle, idx: i32) -> CHandle { - let mut o = CHandle::null(); - unsafe { oaknode_track_get_block_at(track.clone(), idx, &mut o) }; - o -} - -/// A block's length as `(num, den)`. -fn blen(h: &CHandle) -> (i32, i32) { - unsafe { get::(h).unwrap().length } -} - -/// A block's media-in as `(num, den)`. -fn media_in_of(h: &CHandle) -> (i32, i32) { - unsafe { get::(h).unwrap().media_in } -} - -/// A block's enabled flag. -fn enabled(h: &CHandle) -> bool { - unsafe { get::(h).unwrap().enabled } -} - -/// A block's owning-track raw pointer. -fn track_ptr(h: &CHandle) -> *mut MockNode { - unsafe { get::(h).unwrap().track } -} - -/// A node's kind. -fn kind_of(h: &CHandle) -> MockKind { - unsafe { get::(h).unwrap().kind } -} - -/// A node's track type. -fn track_type_of(h: &CHandle) -> i32 { - unsafe { get::(h).unwrap().track_type } -} diff --git a/crates/oaktimeline/tests/undopointer_coverage_test.rs b/crates/oaktimeline/tests/undopointer_coverage_test.rs deleted file mode 100644 index 134139e97..000000000 --- a/crates/oaktimeline/tests/undopointer_coverage_test.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 . - -//! Coverage for `src/undopointer.rs`: the pointer-edit command branches that -//! the other edit-command tests do not reach. These exercise -//! `BlockTrimCommand` (do-nothing / roll-edit flags, TrimIn, removing and -//! resizing the adjacent block), `TrackSlideCommand` (creating/removing the -//! in/out adjacent blocks) and `TrackPlaceBlockCommand` (out-of-range track -//! index, gap insertion past the end of the track, ripple-removal placement), -//! plus the `Command` trait dispatch for each family. -#![cfg(feature = "test-stubs")] - -use oakcore_rs::Rational; -use oaktimeline::bridge::node::{ - oaknode_block_clip_create, oaknode_block_gap_create, oaknode_track_create, - oaknode_track_get_block_at, oaknode_track_get_block_count, oaknode_track_prepend_block, -}; -use oaktimeline::bridge::teststubs::{MockKind, MockNode}; -use oaktimeline::common::MovementMode; -use oaktimeline::handle::{get, get_mut, make_owned, CHandle}; -use oaktimeline::undocommon::Command; -use oaktimeline::undopointer::{BlockTrimCommand, TrackPlaceBlockCommand, TrackSlideCommand}; - -// ---- helpers (mirror edit_command_test.rs) -------------------------------- - -/// A new detached track of video type (0). -fn make_track() -> CHandle { - unsafe { oaknode_track_create(0) } -} - -/// Raw pointer to the node boxed behind a handle. -fn addr(h: &CHandle) -> *mut MockNode { - unsafe { get_mut::(h).unwrap() as *mut MockNode } -} - -/// Number of blocks on a track. -fn count(track: &CHandle) -> i32 { - let mut c = 0; - unsafe { oaknode_track_get_block_count(track.clone(), &mut c) }; - c -} - -/// Borrowed block at `index` on a track (for `.ctx` comparison only). -fn at(track: &CHandle, idx: i32) -> CHandle { - let mut o = CHandle::null(); - unsafe { oaknode_track_get_block_at(track.clone(), idx, &mut o) }; - o -} - -/// A new clip block. -fn mk_clip() -> CHandle { - unsafe { oaknode_block_clip_create() } -} - -/// A new gap block. -fn mk_gap() -> CHandle { - unsafe { oaknode_block_gap_create() } -} - -/// Set a block's in/out/length/media-in points directly. -fn set_times(h: &CHandle, in_: (i32, i32), out: (i32, i32), len: (i32, i32), mi: (i32, i32)) { - let b = unsafe { get_mut::(h).unwrap() }; - b.in_ = in_; - b.out = out; - b.length = len; - b.media_in = mi; -} - -/// A block's length as `(num, den)`. -fn blen(h: &CHandle) -> (i32, i32) { - unsafe { get::(h).unwrap().length } -} - -/// A block's owning-track raw pointer. -fn track_ptr(h: &CHandle) -> *mut MockNode { - unsafe { get::(h).unwrap().track } -} - -/// A new empty track list of video type (0). -fn make_list() -> CHandle { - make_owned(MockNode { - kind: MockKind::TrackList, - track_type: 0, - ..Default::default() - }) -} - -// ---- BlockTrimCommand ----------------------------------------------------- - -/// A trim whose new length equals the old length marks the command as -/// "doing nothing"; the setters are idempotent; the Command trait forwards. -#[test] -fn block_trim_doing_nothing_and_setters() { - let t = make_track(); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - - let mut cmd = BlockTrimCommand::new( - t.clone(), - b.clone(), - Rational::new(10, 1), - MovementMode::TrimOut, - ); - cmd.set_trim_is_a_roll_edit(true); - cmd.set_remove_zero_length_from_graph(false); - cmd.prepare(); - - // Trait dispatch: doing_nothing_ short-circuits redo and undo. - Command::redo(&mut cmd); - Command::undo(&mut cmd); - assert_eq!(blen(&b), (10, 1)); -} - -/// A TrimIn that shortens the block with a live (non-gap) previous block -/// creates an adjacent gap and inserts it before the block; undo removes it. -#[test] -fn block_trim_trim_in_creates_adjacent() { - let t = make_track(); - let prev = mk_clip(); - let b = mk_clip(); - set_times(&prev, (0, 1), (10, 1), (10, 1), (0, 1)); - set_times(&b, (10, 1), (20, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), prev.clone()); - } - - let mut cmd = BlockTrimCommand::new( - t.clone(), - b.clone(), - Rational::new(6, 1), - MovementMode::TrimIn, - ); - cmd.prepare(); - cmd.redo(); - // The created gap was inserted between `prev` and `b` ([prev, gap, b]). - assert_eq!(count(&t), 3); - assert_eq!(blen(&b), (6, 1)); - assert_eq!(at(&t, 0).ctx as usize, addr(&prev) as usize); - - cmd.undo(); - assert_eq!(count(&t), 2); - assert_eq!(blen(&b), (10, 1)); -} - -/// A TrimOut that lengthens the block removes a zero-length adjacent gap -/// from the graph; a second redo reuses the stored removal command, and undo -/// restores the gap. -#[test] -fn block_trim_trim_out_removes_adjacent() { - let t = make_track(); - let b = mk_clip(); - let g = mk_gap(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - set_times(&g, (10, 1), (15, 1), (5, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), g.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - - let mut cmd = BlockTrimCommand::new( - t.clone(), - b.clone(), - Rational::new(15, 1), - MovementMode::TrimOut, - ); - cmd.prepare(); - cmd.redo(); - assert_eq!(count(&t), 1); - // Reuse of `deleted_adjacent_command_` (idempotent redo). - cmd.redo(); - assert_eq!(count(&t), 1); - - cmd.undo(); - assert_eq!(count(&t), 2); - assert_eq!(blen(&b), (10, 1)); -} - -/// A TrimOut that lengthens the block against a large enough adjacent gap -/// resizes the adjacent instead of removing it (undo restores it). -#[test] -fn block_trim_trim_out_resizes_adjacent() { - let t = make_track(); - let b = mk_clip(); - let g = mk_gap(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - set_times(&g, (10, 1), (20, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), g.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - - let mut cmd = BlockTrimCommand::new( - t.clone(), - b.clone(), - Rational::new(15, 1), - MovementMode::TrimOut, - ); - cmd.prepare(); - cmd.redo(); - assert_eq!(blen(&g), (5, 1)); - - cmd.undo(); - assert_eq!(blen(&g), (10, 1)); -} - -/// A TrimIn that lengthens the block against a large enough adjacent gap -/// resizes the adjacent on the in side (undo restores it). -#[test] -fn block_trim_trim_in_resizes_adjacent() { - let t = make_track(); - let g = mk_gap(); - let b = mk_clip(); - set_times(&g, (0, 1), (10, 1), (10, 1), (0, 1)); - set_times(&b, (10, 1), (20, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), g.clone()); - } - - let mut cmd = BlockTrimCommand::new( - t.clone(), - b.clone(), - Rational::new(15, 1), - MovementMode::TrimIn, - ); - cmd.prepare(); - cmd.redo(); - assert_eq!(blen(&g), (5, 1)); - - cmd.undo(); - assert_eq!(blen(&g), (10, 1)); - assert_eq!(blen(&b), (10, 1)); -} - -// ---- TrackSlideCommand ---------------------------------------------------- - -/// A slide with no in/out adjacents creates both as gaps and inserts them on -/// redo; undo removes them. -#[test] -fn track_slide_creates_in_and_out_adjacents() { - let t = make_track(); - let b = mk_clip(); - let tail = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - set_times(&tail, (10, 1), (20, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), tail.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - - let mut cmd = TrackSlideCommand::new( - t.clone(), - vec![b.clone()], - CHandle::null(), - CHandle::null(), - Rational::new(5, 1), - ); - cmd.prepare(); - cmd.redo(); - assert_eq!(count(&t), 4); - - cmd.undo(); - assert_eq!(count(&t), 2); -} - -/// A slide that removes the in adjacent (moving back by exactly its length) -/// deletes it on redo and re-inserts it on undo. -#[test] -fn track_slide_removes_in_adjacent() { - let t = make_track(); - let g_in = mk_gap(); - let b = mk_clip(); - let g_out = mk_gap(); - set_times(&g_in, (0, 1), (5, 1), (5, 1), (0, 1)); - set_times(&b, (5, 1), (15, 1), (10, 1), (0, 1)); - set_times(&g_out, (15, 1), (25, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), g_out.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), g_in.clone()); - } - - let mut cmd = TrackSlideCommand::new( - t.clone(), - vec![b.clone()], - g_in.clone(), - g_out.clone(), - Rational::new(-5, 1), - ); - cmd.prepare(); - cmd.redo(); - assert_eq!(count(&t), 2); - - cmd.undo(); - assert_eq!(count(&t), 3); -} - -/// A slide that removes the out adjacent (moving forward by exactly its -/// length) deletes it on redo and re-inserts it on undo. -#[test] -fn track_slide_removes_out_adjacent() { - let t = make_track(); - let g_in = mk_gap(); - let b = mk_clip(); - let g_out = mk_gap(); - set_times(&g_in, (0, 1), (10, 1), (10, 1), (0, 1)); - set_times(&b, (10, 1), (20, 1), (10, 1), (0, 1)); - set_times(&g_out, (20, 1), (25, 1), (5, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), g_out.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), g_in.clone()); - } - - let mut cmd = TrackSlideCommand::new( - t.clone(), - vec![b.clone()], - g_in.clone(), - g_out.clone(), - Rational::new(5, 1), - ); - cmd.prepare(); - cmd.redo(); - assert_eq!(count(&t), 2); - - cmd.undo(); - assert_eq!(count(&t), 3); -} - -/// `TrackSlideCommand` dispatches through the `Command` trait. -#[test] -fn track_slide_trait_dispatch() { - let t = make_track(); - let b = mk_clip(); - let g = mk_gap(); - set_times(&g, (0, 1), (10, 1), (10, 1), (0, 1)); - set_times(&b, (10, 1), (20, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - unsafe { - oaknode_track_prepend_block(t.clone(), g.clone()); - } - - let mut cmd = TrackSlideCommand::new( - t.clone(), - vec![b.clone()], - g.clone(), - CHandle::null(), - Rational::new(5, 1), - ); - cmd.prepare(); - Command::redo(&mut cmd); - Command::undo(&mut cmd); - assert_eq!(blen(&g), (10, 1)); -} - -// ---- TrackPlaceBlockCommand ----------------------------------------------- - -/// Placing a block at an out-of-range track index creates the missing tracks -/// on redo and removes them on undo. -#[test] -fn track_place_creates_missing_tracks() { - let list = make_list(); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - - let mut cmd = TrackPlaceBlockCommand::new(list.clone(), 5, b.clone(), Rational::new(0, 1)); - cmd.redo(); - cmd.undo(); -} - -/// Placing a block past the end of a populated track appends a gap and then -/// the block; undo removes both. -#[test] -fn track_place_appends_gap_past_end() { - let list = make_list(); - let t = make_track(); - unsafe { - get_mut::(&list).unwrap().blocks.push(addr(&t)); - } - let existing = mk_clip(); - set_times(&existing, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), existing.clone()); - } - - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - let mut cmd = TrackPlaceBlockCommand::new(list.clone(), 0, b.clone(), Rational::new(15, 1)); - cmd.redo(); - // gap + placed block appended after the existing block. - assert_eq!(count(&t), 3); - assert_eq!(track_ptr(&b), addr(&t)); - - cmd.undo(); - assert_eq!(count(&t), 1); - assert!(track_ptr(&b).is_null()); -} - -/// Placing a block in the middle of a populated track ripple-removes the -/// occupied area then inserts the block; undo restores the area. -#[test] -fn track_place_ripples_area_in_middle() { - let list = make_list(); - let t = make_track(); - unsafe { - get_mut::(&list).unwrap().blocks.push(addr(&t)); - } - let existing = mk_clip(); - set_times(&existing, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), existing.clone()); - } - - let b = mk_clip(); - set_times(&b, (0, 1), (5, 1), (5, 1), (0, 1)); - let mut cmd = TrackPlaceBlockCommand::new(list.clone(), 0, b.clone(), Rational::new(5, 1)); - cmd.redo(); - assert_eq!(track_ptr(&b), addr(&t)); - - cmd.undo(); - assert!(track_ptr(&b).is_null()); -} - -/// `TrackPlaceBlockCommand` dispatches through the `Command` trait. -#[test] -fn track_place_trait_dispatch() { - let list = make_list(); - let t = make_track(); - unsafe { - get_mut::(&list).unwrap().blocks.push(addr(&t)); - } - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - - let mut cmd = TrackPlaceBlockCommand::new(list.clone(), 0, b.clone(), Rational::new(0, 1)); - Command::redo(&mut cmd); - assert_eq!(count(&t), 1); - Command::undo(&mut cmd); - assert_eq!(count(&t), 0); -} diff --git a/crates/oaktimeline/tests/undoripple_coverage_test.rs b/crates/oaktimeline/tests/undoripple_coverage_test.rs deleted file mode 100644 index 523c02f71..000000000 --- a/crates/oaktimeline/tests/undoripple_coverage_test.rs +++ /dev/null @@ -1,364 +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 . - -//! Coverage for the ripple commands (`src/undoripple.rs`): single-track, -//! track-list and timeline ripple-removal, and gap deletion at regions. -#![cfg(feature = "test-stubs")] - -use oakcore_rs::{Rational, TimeRange}; - -use oaktimeline::bridge::node::{ - oaknode_block_clip_create, oaknode_block_gap_create, oaknode_track_prepend_block, -}; -use oaktimeline::bridge::teststubs::{MockKind, MockNode}; -use oaktimeline::handle::{get, get_mut, make_owned, CHandle}; -use oaktimeline::undocommon::Command; -use oaktimeline::undoripple::{ - TimelineRippleDeleteGapsAtRegionsCommand, TimelineRippleRemoveAreaCommand, - TrackListRippleRemoveAreaCommand, TrackRippleRemoveAreaCommand, -}; - -/// Raw pointer to the node boxed behind a handle. -fn addr(h: &CHandle) -> *mut MockNode { - unsafe { get_mut::(h).unwrap() as *mut MockNode } -} - -/// A new detached track of video type (0). -fn make_track() -> CHandle { - make_owned(MockNode { - kind: MockKind::Track, - track_type: 0, - ..Default::default() - }) -} - -/// A new clip block. -fn mk_clip() -> CHandle { - unsafe { oaknode_block_clip_create() } -} - -/// A new gap block. -fn mk_gap() -> CHandle { - unsafe { oaknode_block_gap_create() } -} - -/// Set a block's in/out/length/media-in points directly. -fn set_times(h: &CHandle, in_: (i32, i32), out: (i32, i32), len: (i32, i32), mi: (i32, i32)) { - let b = unsafe { get_mut::(h).unwrap() }; - b.in_ = in_; - b.out = out; - b.length = len; - b.media_in = mi; -} - -/// Number of blocks on a track. -fn count(track: &CHandle) -> i32 { - let mut c = 0; - unsafe { oaktimeline::bridge::node::oaknode_track_get_block_count(track.clone(), &mut c) }; - c -} - -/// A block's length as `(num, den)`. -fn blen(h: &CHandle) -> (i32, i32) { - unsafe { get::(h).unwrap().length } -} - -/// The range cuts through a clip, so `prepare` builds a splice split; -/// redo splits into two blocks, undo rejoins. -#[test] -fn ripple_remove_area_splices_through_block() { - let t = make_track(); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - - let mut cmd = TrackRippleRemoveAreaCommand::new( - t.clone(), - TimeRange::new(Rational::new(2, 1), Rational::new(5, 1)), - ); - cmd.prepare(); - assert_eq!(count(&t), 1); - - cmd.redo(); - assert_eq!(count(&t), 2); - assert!(!cmd.get_spliced_block().is_null()); - - cmd.undo(); - assert_eq!(count(&t), 1); - assert_eq!(blen(&b), (10, 1)); -} - -/// The range starts exactly at the block's in point and cuts its out, so -/// `prepare` records an in-point trim (no splice, no removals). -#[test] -fn ripple_remove_area_trims_in_of_trailing_block() { - let t = make_track(); - let b = mk_clip(); - set_times(&b, (2, 1), (10, 1), (8, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - - let mut cmd = TrackRippleRemoveAreaCommand::new( - t.clone(), - TimeRange::new(Rational::new(2, 1), Rational::new(5, 1)), - ); - cmd.prepare(); - cmd.redo(); - // The block's out point is trimmed from 10 to 5 (length 8 -> 5). - assert_eq!(blen(&b), (5, 1)); - assert_eq!(count(&t), 1); - - cmd.undo(); - assert_eq!(blen(&b), (8, 1)); -} - -/// The range fully covers a block starting at the range's in point, so it -/// is removed; undo re-inserts it. -#[test] -fn ripple_remove_area_removes_contained_block() { - let t = make_track(); - let b = mk_clip(); - set_times(&b, (2, 1), (4, 1), (2, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - - let mut cmd = TrackRippleRemoveAreaCommand::new( - t.clone(), - TimeRange::new(Rational::new(2, 1), Rational::new(5, 1)), - ); - cmd.prepare(); - cmd.redo(); - assert_eq!(count(&t), 0); - - cmd.undo(); - assert_eq!(count(&t), 1); -} - -/// `get_insertion_index` reports the block a replacement would follow, and -/// `prepare` with a null first block is a no-op. -#[test] -fn ripple_remove_area_insertion_index() { - let t = make_track(); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - - let mut cmd = TrackRippleRemoveAreaCommand::new( - t.clone(), - TimeRange::new(Rational::new(2, 1), Rational::new(5, 1)), - ); - cmd.prepare(); - // The first block starts before the range, so inserts follow it. - assert_eq!(cmd.get_insertion_index().ctx as usize, addr(&b) as usize); - - // On an empty track, `prepare` finds no first block and does nothing. - let t2 = make_track(); - let mut empty = TrackRippleRemoveAreaCommand::new( - t2.clone(), - TimeRange::new(Rational::new(0, 1), Rational::new(5, 1)), - ); - empty.prepare(); - assert!(empty.get_insertion_index().is_null()); - assert!(empty.get_spliced_block().is_null()); -} - -/// `TrackRippleRemoveAreaCommand` dispatches through the `Command` trait. -#[test] -fn ripple_remove_area_trait_dispatch() { - let t = make_track(); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - - let mut cmd = TrackRippleRemoveAreaCommand::new( - t.clone(), - TimeRange::new(Rational::new(2, 1), Rational::new(5, 1)), - ); - cmd.prepare(); - Command::redo(&mut cmd); - assert_eq!(count(&t), 2); - Command::undo(&mut cmd); - assert_eq!(count(&t), 1); -} - -/// A locked track is skipped and an empty list produces no child commands. -#[test] -fn track_list_ripple_remove_area_prepare_redo_undo() { - let list = make_owned(MockNode { - kind: MockKind::TrackList, - track_type: 0, - ..Default::default() - }); - let t = make_track(); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - unsafe { - get_mut::(&list).unwrap().blocks.push(addr(&t)); - } - - let mut cmd = TrackListRippleRemoveAreaCommand::new( - list.clone(), - Rational::new(2, 1), - Rational::new(5, 1), - ); - cmd.prepare(); - cmd.redo(); - // The child splice splits the single block. - assert_eq!(count(&t), 2); - cmd.undo(); - assert_eq!(count(&t), 1); - - // A locked track is skipped. - let list2 = make_owned(MockNode { - kind: MockKind::TrackList, - track_type: 0, - ..Default::default() - }); - let t2 = make_track(); - unsafe { - get_mut::(&t2).unwrap().locked = true; - get_mut::(&list2).unwrap().blocks.push(addr(&t2)); - } - let mut locked = TrackListRippleRemoveAreaCommand::new( - list2.clone(), - Rational::new(2, 1), - Rational::new(5, 1), - ); - locked.prepare(); - locked.redo(); - assert_eq!(count(&t2), 0); -} - -/// `TrackListRippleRemoveAreaCommand` dispatches through the `Command` -/// trait. -#[test] -fn track_list_ripple_remove_area_trait_dispatch() { - let list = make_owned(MockNode { - kind: MockKind::TrackList, - track_type: 0, - ..Default::default() - }); - let t = make_track(); - unsafe { - get_mut::(&list).unwrap().blocks.push(addr(&t)); - } - let mut cmd = TrackListRippleRemoveAreaCommand::new( - list.clone(), - Rational::new(0, 1), - Rational::new(5, 1), - ); - Command::redo(&mut cmd); - Command::undo(&mut cmd); -} - -/// A sequence with a video track list: `TimelineRippleRemoveAreaCommand` -/// drives a child per track list. -#[test] -fn timeline_ripple_remove_area_redo_undo() { - let seq = make_owned(MockNode { - kind: MockKind::Sequence, - ..Default::default() - }); - let list = make_owned(MockNode { - kind: MockKind::TrackList, - track_type: 0, - ..Default::default() - }); - let t = make_track(); - let b = mk_clip(); - set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), b.clone()); - } - unsafe { - get_mut::(&list).unwrap().blocks.push(addr(&t)); - get_mut::(&seq).unwrap().blocks.push(addr(&list)); - } - - let mut cmd = - TimelineRippleRemoveAreaCommand::new(seq.clone(), Rational::new(2, 1), Rational::new(5, 1)); - cmd.redo(); - assert_eq!(count(&t), 2); - cmd.undo(); - assert_eq!(count(&t), 1); -} - -/// `TimelineRippleRemoveAreaCommand` dispatches through the `Command` trait. -#[test] -fn timeline_ripple_remove_area_trait_dispatch() { - let seq = make_owned(MockNode { - kind: MockKind::Sequence, - ..Default::default() - }); - let mut cmd = - TimelineRippleRemoveAreaCommand::new(seq.clone(), Rational::new(0, 1), Rational::new(5, 1)); - Command::redo(&mut cmd); - Command::undo(&mut cmd); -} - -/// `TimelineRippleDeleteGapsAtRegionsCommand` resolves a gap at a region and -/// reports it via `has_commands`; a non-gap region produces no command. -#[test] -fn ripple_delete_gaps_at_regions_prepare() { - let t = make_track(); - let g = mk_gap(); - set_times(&g, (0, 1), (5, 1), (5, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), g.clone()); - } - - let regions = vec![( - t.clone(), - TimeRange::new(Rational::new(0, 1), Rational::new(5, 1)), - )]; - let mut cmd = TimelineRippleDeleteGapsAtRegionsCommand::new(t.clone(), regions); - cmd.prepare(); - cmd.redo(); - cmd.undo(); - // `has_commands` reflects whether any gap-removal command was built. - let _ = cmd.has_commands(); -} - -/// `TimelineRippleDeleteGapsAtRegionsCommand` dispatches through the -/// `Command` trait. -#[test] -fn ripple_delete_gaps_at_regions_trait_dispatch() { - let t = make_track(); - let g = mk_gap(); - set_times(&g, (0, 1), (5, 1), (5, 1), (0, 1)); - unsafe { - oaknode_track_prepend_block(t.clone(), g.clone()); - } - - let regions = vec![( - t.clone(), - TimeRange::new(Rational::new(0, 1), Rational::new(5, 1)), - )]; - let mut cmd = TimelineRippleDeleteGapsAtRegionsCommand::new(t.clone(), regions); - Command::redo(&mut cmd); - Command::undo(&mut cmd); -} diff --git a/crates/oaktimeline/tests/util_test.rs b/crates/oaktimeline/tests/util_test.rs deleted file mode 100644 index 085b20aa5..000000000 --- a/crates/oaktimeline/tests/util_test.rs +++ /dev/null @@ -1,218 +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 . - -//! Coverage for the `util` helpers (`src/util.rs`) and the `error` mapping -//! (`src/error.rs`): identity/ordering helpers, `free_detached_handle`, -//! and the project-graph helpers (`track_project`, `block_add_to_graph`, -//! `block_remove_from_graph`). -#![cfg(feature = "test-stubs")] - -use std::cmp::Ordering; - -use oaktimeline::bridge::teststubs::{MockKind, MockNode}; -use oaktimeline::error::{ - Error, OAKTIMELINE_E_FAILED, OAKTIMELINE_E_INVALID, OAKTIMELINE_E_NOMEM, - OAKTIMELINE_E_NOT_FOUND, OAKTIMELINE_E_STATE, -}; -use oaktimeline::handle::{get, get_mut, make_owned, CHandle}; -use oaktimeline::util::{ - block_add_to_graph, block_handle_less, block_remove_from_graph, free_detached_handle, - same_block, same_node, same_track, track_handle_less, track_project, -}; - -/// Raw pointer to the node boxed behind a handle. -fn addr(h: &CHandle) -> *mut MockNode { - unsafe { get_mut::(h).unwrap() as *mut MockNode } -} - -/// A new detached track of video type (0). -fn make_track() -> CHandle { - make_owned(MockNode { - kind: MockKind::Track, - track_type: 0, - ..Default::default() - }) -} - -/// `same_track` compares by `ctx` identity. -#[test] -fn same_track_identity() { - let t = make_track(); - assert!(same_track(t.clone(), t.clone())); - assert!(same_track(t.clone(), t.clone())); - let t2 = make_track(); - assert!(!same_track(t.clone(), t2)); -} - -/// `same_node` compares by `ctx` identity. -#[test] -fn same_node_identity() { - let a = make_owned(MockNode { - kind: MockKind::Node, - ..Default::default() - }); - assert!(same_node(a.clone(), a.clone())); - let b = make_owned(MockNode { - kind: MockKind::Node, - ..Default::default() - }); - assert!(!same_node(a, b)); -} - -/// `same_block` compares by `ctx` identity. -#[test] -fn same_block_identity() { - let a = make_owned(MockNode { - kind: MockKind::Clip, - ..Default::default() - }); - assert!(same_block(a.clone(), a.clone())); - let b = make_owned(MockNode { - kind: MockKind::Clip, - ..Default::default() - }); - assert!(!same_block(a, b)); -} - -/// `block_handle_less` / `track_handle_less` give a total order by `ctx`. -#[test] -fn handle_less_total_order() { - let t1 = make_track(); - let t2 = make_track(); - - // Ordering matches the raw `ctx` comparison. - assert_eq!( - track_handle_less(t1.clone(), t2.clone()), - t1.ctx.cmp(&t2.ctx) - ); - assert_eq!( - track_handle_less(t2.clone(), t1.clone()), - t2.ctx.cmp(&t1.ctx) - ); - assert_eq!(track_handle_less(t1.clone(), t1.clone()), Ordering::Equal); - - assert_eq!( - block_handle_less(t1.clone(), t2.clone()), - t1.ctx.cmp(&t2.ctx) - ); - assert_eq!(block_handle_less(t1.clone(), t1.clone()), Ordering::Equal); -} - -/// `free_detached_handle` is a no-op for a null pointer. -#[test] -fn free_detached_handle_null_noop() { - free_detached_handle(std::ptr::null_mut()); -} - -/// `free_detached_handle` on an owned handle releases its box and clears -/// it, leaving a null/empty handle. -#[test] -fn free_detached_handle_releases_owned() { - let t = make_track(); - let mut h = t.clone(); - free_detached_handle(&mut h); - assert!(h.ctx.is_null()); - assert!(h.addref.is_none()); - assert!(h.release.is_none()); - assert_eq!(h.abi_version, 0); -} - -/// `error::Error::code` maps every variant to its frozen code. -#[test] -fn error_code_mapping() { - assert_eq!(Error::Invalid.code(), OAKTIMELINE_E_INVALID); - assert_eq!(Error::State.code(), OAKTIMELINE_E_STATE); - assert_eq!(Error::Failed("x".to_string()).code(), OAKTIMELINE_E_FAILED); - assert_eq!(Error::NotFound.code(), OAKTIMELINE_E_NOT_FOUND); - assert_eq!(Error::NoMem.code(), OAKTIMELINE_E_NOMEM); -} - -/// `track_project` returns the project owning a track's sequence, via the -/// sequence node view. -#[test] -fn track_project_follows_chain() { - let proj = make_owned(MockNode { - kind: MockKind::Project, - ..Default::default() - }); - let seq = make_owned(MockNode { - kind: MockKind::Sequence, - ..Default::default() - }); - let t = make_track(); - - // Wire the graph: track -> sequence -> project. - unsafe { - get_mut::(&t).unwrap().sequence = get_mut::(&seq).unwrap(); - get_mut::(&seq).unwrap().project = get_mut::(&proj).unwrap(); - } - - let p = track_project(t.clone()); - assert!(!p.is_null()); - assert_eq!(p.ctx as usize, addr(&proj) as usize); -} - -/// `track_project` is a null handle when the track has no sequence. -#[test] -fn track_project_empty() { - let t = make_track(); - assert!(track_project(t.clone()).is_null()); -} - -/// `block_add_to_graph` / `block_remove_from_graph` adopt and detach a -/// block from the project owning its track. -#[test] -fn block_graph_add_remove() { - let proj = make_owned(MockNode { - kind: MockKind::Project, - ..Default::default() - }); - let seq = make_owned(MockNode { - kind: MockKind::Sequence, - ..Default::default() - }); - let t = make_track(); - let b = make_owned(MockNode { - kind: MockKind::Clip, - ..Default::default() - }); - unsafe { - get_mut::(&t).unwrap().sequence = get_mut::(&seq).unwrap(); - get_mut::(&seq).unwrap().project = get_mut::(&proj).unwrap(); - } - - block_add_to_graph(b.clone(), t.clone()); - assert_eq!( - unsafe { get::(&b) }.unwrap().project as usize, - addr(&proj) as usize - ); - - block_remove_from_graph(b.clone(), t.clone()); - assert!(unsafe { get::(&b) }.unwrap().project.is_null()); -} - -/// Graph helpers are no-ops when the track owns no project. -#[test] -fn block_graph_noop_without_project() { - let t = make_track(); - let b = make_owned(MockNode { - kind: MockKind::Clip, - ..Default::default() - }); - block_add_to_graph(b.clone(), t.clone()); - assert!(unsafe { get::(&b) }.unwrap().project.is_null()); - block_remove_from_graph(b.clone(), t.clone()); -} diff --git a/crates/oaktimeline/tests/workarea_test.rs b/crates/oaktimeline/tests/workarea_test.rs index a5f5494d2..ce30de310 100644 --- a/crates/oaktimeline/tests/workarea_test.rs +++ b/crates/oaktimeline/tests/workarea_test.rs @@ -16,16 +16,11 @@ //! Contract tests for the work area domain (`src/workarea.rs`): //! `TimelineWorkArea` value type, its two undo commands, and the -//! `reset_in`/`reset_out` sentinels. XML load/save matches the format -//! written by `timelineworkarea.cpp` (`` attribute plus -//! `enabled`/`in`/`out` text elements). -#![cfg(feature = "test-stubs")] +//! `reset_in`/`reset_out` sentinels. The XML load/save contract left +//! with the deleted C ABI export layer (single-lib unification). use oakcore_rs::{Rational, TimeRange}; -use oaktimeline::bridge::common::{oakcommon_xml_writer_free, oakcommon_xml_writer_init}; -use oaktimeline::bridge::teststubs::{xml_reader_handle, MockXmlNode, MockXmlWriter}; -use oaktimeline::ffi; -use oaktimeline::handle::{get, get_mut, make_owned}; +use oaktimeline::handle::{get, make_owned}; use oaktimeline::undocommon::Command; use oaktimeline::workarea::{ reset_in, reset_out, TimelineWorkArea, WorkareaSetEnabledCommand, WorkareaSetRangeCommand, @@ -130,22 +125,33 @@ fn workarea_set_range_command_redo_undo() { ); } -/// `to_command` boxes a work area command into a CHandle for the undo -/// stack. +/// `to_command` boxes a work area command into an oakundo `UndoCommand` +/// value for the undo stack (the old C ABI command handle is gone with +/// the single-lib unification). #[test] -fn workarea_commands_box_to_chandle() { +fn workarea_commands_box_to_undo_command() { let wa_h = make_owned(TimelineWorkArea::new()); - let enabled_cmd = WorkareaSetEnabledCommand::new(wa_h.clone(), true).to_command(); - assert!(!enabled_cmd.is_null()); - assert_eq!(enabled_cmd.abi_version, 1); + let mut enabled_cmd = WorkareaSetEnabledCommand::new(wa_h.clone(), true).to_command(); + enabled_cmd.redo_now(); + assert!(unsafe { get::(&wa_h) }.unwrap().enabled()); + enabled_cmd.undo_now(); + assert!(!unsafe { get::(&wa_h) }.unwrap().enabled()); - let range_cmd = WorkareaSetRangeCommand::new( + let mut range_cmd = WorkareaSetRangeCommand::new( wa_h.clone(), TimeRange::new(Rational::new(1, 1), Rational::new(2, 1)), ) .to_command(); - assert!(!range_cmd.is_null()); - assert_eq!(range_cmd.abi_version, 1); + range_cmd.redo_now(); + assert_eq!( + unsafe { get::(&wa_h) }.unwrap().in_(), + Rational::new(1, 1) + ); + range_cmd.undo_now(); + assert_eq!( + unsafe { get::(&wa_h) }.unwrap().in_(), + reset_in() + ); } /// `Command` trait dispatch routes through the same redo/undo bodies as @@ -175,86 +181,3 @@ fn workarea_commands_trait_dispatch() { reset_in() ); } - -/// Save writes `` plus `enabled`/`in`/`out` text elements; -/// load reads them back (XML golden round-trip). -#[test] -fn workarea_xml_round_trip() { - // Set up an enabled work area with a known range. - let wa_h = make_owned(TimelineWorkArea::new()); - { - let wa = unsafe { get_mut::(&wa_h) }.unwrap(); - wa.set_enabled(true); - wa.set_range(TimeRange::new(Rational::new(10, 1), Rational::new(20, 1))); - } - - // Save into a mock writer. - let mut writer = unsafe { oakcommon_xml_writer_init() }; - let r = unsafe { ffi::workarea::oaktimeline_workarea_save(wa_h.clone(), writer.clone()) }; - assert_eq!(r, 0); - let buf = unsafe { get::(&writer) } - .unwrap() - .buf - .clone(); - assert!(buf.contains("version=\"1\""), "buf: {buf}"); - assert!(buf.contains("1"), "buf: {buf}"); - assert!(buf.contains("10/1"), "buf: {buf}"); - assert!(buf.contains("20/1"), "buf: {buf}"); - unsafe { oakcommon_xml_writer_free(&mut writer) }; - - // Load from a mock reader back into a fresh work area. - let reader = xml_reader_handle(vec![ - MockXmlNode { - name: "enabled".to_string(), - text: "1".to_string(), - attrs: Vec::new(), - }, - MockXmlNode { - name: "in".to_string(), - text: "10/1".to_string(), - attrs: Vec::new(), - }, - MockXmlNode { - name: "out".to_string(), - text: "20/1".to_string(), - attrs: Vec::new(), - }, - ]); - let wa2_h = make_owned(TimelineWorkArea::new()); - let r2 = unsafe { ffi::workarea::oaktimeline_workarea_load(wa2_h.clone(), reader.clone()) }; - assert_eq!(r2, 0); - let wa2 = unsafe { get::(&wa2_h) }.unwrap(); - assert!(wa2.enabled()); - assert_eq!(wa2.in_(), Rational::new(10, 1)); - assert_eq!(wa2.out(), Rational::new(20, 1)); -} - -/// An unset work area serializes to `enabled=0` and round-trips as a -/// disabled, null-range work area. -#[test] -fn workarea_unset_round_trips_disabled() { - let wa_h = make_owned(TimelineWorkArea::new()); - - let mut writer = unsafe { oakcommon_xml_writer_init() }; - let r = unsafe { ffi::workarea::oaktimeline_workarea_save(wa_h.clone(), writer.clone()) }; - assert_eq!(r, 0); - let buf = unsafe { get::(&writer) } - .unwrap() - .buf - .clone(); - assert!(buf.contains("0"), "buf: {buf}"); - unsafe { oakcommon_xml_writer_free(&mut writer) }; - - let reader = xml_reader_handle(vec![MockXmlNode { - name: "enabled".to_string(), - text: "0".to_string(), - attrs: Vec::new(), - }]); - let wa2_h = make_owned(TimelineWorkArea::new()); - let r2 = unsafe { ffi::workarea::oaktimeline_workarea_load(wa2_h.clone(), reader.clone()) }; - assert_eq!(r2, 0); - let wa2 = unsafe { get::(&wa2_h) }.unwrap(); - assert!(!wa2.enabled()); - assert_eq!(wa2.in_(), reset_in()); - assert_eq!(wa2.out(), reset_out()); -} diff --git a/crates/oakundo/src/ffi.rs b/crates/oakundo/src/ffi.rs deleted file mode 100644 index fcdc2f047..000000000 --- a/crates/oakundo/src/ffi.rs +++ /dev/null @@ -1,427 +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 . - -//! C ABI export layer: implements `include/undo/*.h` verbatim. -//! -//! Organization: one submodule per public header. The authoritative -//! function list is the header itself; each submodule below carries a -//! complete inventory comment plus export stubs. Bodies only unwrap -//! handles, call the safe Rust domains, and map results through -//! [`crate::handle::guard*`]. `include/undo/error.h` exports macros -//! only, so it is folded into this module's preamble instead of getting -//! its own submodule. -//! -//! Handles cross the boundary by field copy between the public -//! [`OakUndoCommand`]/[`OakUndoStack`] structs and the crate-internal -//! [`CHandle`]. Stacks live behind a `Mutex` (shared state), commands in -//! a `CommandBox`. - -use std::ffi::{c_char, c_int, c_void, CStr}; -use std::panic::{catch_unwind, AssertUnwindSafe}; -use std::sync::Mutex; - -use crate::error::{Error, Result}; -use crate::handle::{guard, guard_handle, guard_void, make_owned, CHandle}; -use crate::undocommand::{ - command_from_borrowed, command_from_owned, command_take, command_to_mut, command_to_ref, - OakUndoCommandVtable, UndoCommand, -}; -use crate::undostack::UndoStack; - -/// `include/undo/error.h` — macros only, no exported functions. -/// -/// `OAKUNDO_OK` and the `OAKUNDO_E_*` codes are mirrored as -/// [`crate::error`] constants; `OAKUNDO_ABI_VERSION` lives in -/// [`crate::handle`]. - -/// `include/undo/undocommand.h` handle type — `OakUndoCommand` -/// (`{ctx, addref, release, abi_version}`). Single-lib unification: -/// aliases the shared [`CHandle`] (identical layout; the C ABI is -/// unchanged). -pub type OakUndoCommand = CHandle; - -/// `include/undo/undostack.h` handle type — `OakUndoStack`. Single-lib -/// unification: aliases the shared [`CHandle`]. -pub type OakUndoStack = CHandle; - -/// Lock the stack behind `stack` and run `f` on it. `E_INVALID` for an -/// empty handle. A poisoned mutex is recovered (its inner value is -/// still valid). -fn with_stack(stack: OakUndoStack, f: impl FnOnce(&mut UndoStack) -> Result) -> Result { - // Keep a local handle so `get`'s returned reference lives for the - // whole call (its lifetime ties to the `&CHandle` argument). - let ch = stack; - let m = unsafe { crate::handle::get::>(&ch) }.ok_or(Error::Invalid)?; - let mut guard = m.lock().unwrap_or_else(|e| e.into_inner()); - f(&mut guard) -} - -/// Read a NUL-terminated C string; `NULL` yields an empty string -/// (mirrors the C++ `name ? name : ""`). -fn read_name(name: *const c_char) -> String { - if name.is_null() { - String::new() - } else { - // SAFETY: `name` is a valid NUL-terminated string supplied by the - // caller, or NULL (already handled). - unsafe { CStr::from_ptr(name).to_string_lossy().into_owned() } - } -} - -/// `include/undo/undocommand.h` exports (complete inventory): -/// command_init / command_init_multi / command_multi_add_child / -/// command_multi_child_count / command_multi_child / command_redo_now / -/// command_undo_now / command_free. -pub mod command { - use super::*; - - /// `oakundo_command_init`: vtable-backed command, refcount 1. - #[no_mangle] - pub unsafe extern "C" fn oakundo_command_init( - vtable: *const OakUndoCommandVtable, - userdata: *mut c_void, - ) -> OakUndoCommand { - guard_handle(|| unsafe { - if vtable.is_null() { - return Ok(CHandle::null()); - } - let table = *vtable; - Ok(command_from_owned(UndoCommand::from_vtable( - table, userdata, - ))) - }) - } - - /// `oakundo_command_init_multi`: empty multi command, refcount 1. - #[no_mangle] - pub unsafe extern "C" fn oakundo_command_init_multi() -> OakUndoCommand { - guard_handle(|| unsafe { Ok(command_from_owned(UndoCommand::multi())) }) - } - - /// `oakundo_command_multi_add_child` (stack takes one child ref). - #[no_mangle] - pub unsafe extern "C" fn oakundo_command_multi_add_child( - multi: OakUndoCommand, - child: OakUndoCommand, - ) -> c_int { - guard(|| unsafe { - let parent = command_to_mut(multi.ctx).ok_or(Error::Invalid)?; - if !parent.is_multi() { - return Err(Error::Invalid); - } - let child_cmd = command_take(child.ctx)?; - parent.multi_add_child(child_cmd); - Ok(()) - }) - } - - /// `oakundo_command_multi_child_count`. - #[no_mangle] - pub unsafe extern "C" fn oakundo_command_multi_child_count( - multi: OakUndoCommand, - out_count: *mut c_int, - ) -> c_int { - guard(|| unsafe { - if out_count.is_null() { - return Err(Error::Invalid); - } - let parent = command_to_ref(multi.ctx).ok_or(Error::Invalid)?; - if !parent.is_multi() { - return Err(Error::Invalid); - } - *out_count = parent.multi_child_count() as c_int; - Ok(()) - }) - } - - /// `oakundo_command_multi_child` (returned handle carries own ref). - #[no_mangle] - pub unsafe extern "C" fn oakundo_command_multi_child( - multi: OakUndoCommand, - index: c_int, - out_child: *mut OakUndoCommand, - ) -> c_int { - guard(|| unsafe { - if out_child.is_null() { - return Err(Error::Invalid); - } - let parent = command_to_ref(multi.ctx).ok_or(Error::Invalid)?; - if !parent.is_multi() { - return Err(Error::Invalid); - } - if index < 0 { - return Err(Error::NotFound); - } - let child = parent.multi_child(index as usize)?; - let ptr = child as *const UndoCommand as *mut UndoCommand; - *out_child = command_from_borrowed(ptr); - Ok(()) - }) - } - - /// `oakundo_command_redo_now`. - #[no_mangle] - pub unsafe extern "C" fn oakundo_command_redo_now(command: OakUndoCommand) -> c_int { - guard(|| unsafe { - let c = command_to_mut(command.ctx).ok_or(Error::Invalid)?; - c.redo_now(); - Ok(()) - }) - } - - /// `oakundo_command_undo_now`. - #[no_mangle] - pub unsafe extern "C" fn oakundo_command_undo_now(command: OakUndoCommand) -> c_int { - guard(|| unsafe { - let c = command_to_mut(command.ctx).ok_or(Error::Invalid)?; - c.undo_now(); - Ok(()) - }) - } - - /// `oakundo_command_free`: NULL/empty no-op; clears `command->ctx`. - #[no_mangle] - pub unsafe extern "C" fn oakundo_command_free(command: *mut OakUndoCommand) { - guard_void(|| unsafe { - if command.is_null() || (*command).ctx.is_null() { - return; - } - if let Some(release) = (*command).release { - release((*command).ctx); - } - (*command).ctx = std::ptr::null_mut(); - }) - } -} - -/// `include/undo/undostack.h` exports (complete inventory): -/// undostack_init / undostack_free / undostack_push / -/// undostack_push_pre_executed / undostack_undo / undostack_redo / -/// undostack_jump / undostack_clear / undostack_can_undo / -/// undostack_can_redo / undostack_count / undostack_index / -/// undostack_command_text / undostack_command_is_done. -pub mod undostack { - use super::*; - - /// `oakundo_undostack_init`: fresh stack, refcount 1. - #[no_mangle] - pub unsafe extern "C" fn oakundo_undostack_init() -> OakUndoStack { - guard_handle(|| Ok(make_owned(Mutex::new(UndoStack::new())))) - } - - /// `oakundo_undostack_free`: NULL/empty no-op; clears `stack->ctx`. - #[no_mangle] - pub unsafe extern "C" fn oakundo_undostack_free(stack: *mut OakUndoStack) { - guard_void(|| unsafe { - if stack.is_null() || (*stack).ctx.is_null() { - return; - } - if let Some(release) = (*stack).release { - release((*stack).ctx); - } - (*stack).ctx = std::ptr::null_mut(); - }) - } - - /// `oakundo_undostack_push`: redo then record; drops redoable tail. - #[no_mangle] - pub unsafe extern "C" fn oakundo_undostack_push( - stack: OakUndoStack, - command: OakUndoCommand, - name: *const c_char, - ) -> c_int { - guard(|| { - with_stack(stack, |s| unsafe { - let cmd = command_take(command.ctx)?; - let name = read_name(name); - s.push(cmd, &name); - Ok(()) - }) - }) - } - - /// `oakundo_undostack_push_pre_executed`: record without redoing. - #[no_mangle] - pub unsafe extern "C" fn oakundo_undostack_push_pre_executed( - stack: OakUndoStack, - command: OakUndoCommand, - name: *const c_char, - ) -> c_int { - guard(|| { - with_stack(stack, |s| unsafe { - let cmd = command_take(command.ctx)?; - let name = read_name(name); - s.push_pre_executed(cmd, &name); - Ok(()) - }) - }) - } - - /// `oakundo_undostack_undo`. - #[no_mangle] - pub unsafe extern "C" fn oakundo_undostack_undo(stack: OakUndoStack) -> c_int { - guard(|| with_stack(stack, |s| s.undo())) - } - - /// `oakundo_undostack_redo`. - #[no_mangle] - pub unsafe extern "C" fn oakundo_undostack_redo(stack: OakUndoStack) -> c_int { - guard(|| with_stack(stack, |s| s.redo())) - } - - /// `oakundo_undostack_jump` (clamped to 0; `index` is i64). - #[no_mangle] - pub unsafe extern "C" fn oakundo_undostack_jump(stack: OakUndoStack, index: i64) -> c_int { - guard(|| { - with_stack(stack, |s| { - s.jump(index); - Ok(()) - }) - }) - } - - /// `oakundo_undostack_clear`. - #[no_mangle] - pub unsafe extern "C" fn oakundo_undostack_clear(stack: OakUndoStack) -> c_int { - guard(|| { - with_stack(stack, |s| { - s.clear(); - Ok(()) - }) - }) - } - - /// `oakundo_undostack_can_undo`. - #[no_mangle] - pub unsafe extern "C" fn oakundo_undostack_can_undo( - stack: OakUndoStack, - out_value: *mut c_int, - ) -> c_int { - guard(|| unsafe { - if out_value.is_null() { - return Err(Error::Invalid); - } - with_stack(stack, |s| { - *out_value = if s.can_undo() { 1 } else { 0 }; - Ok(()) - }) - }) - } - - /// `oakundo_undostack_can_redo`. - #[no_mangle] - pub unsafe extern "C" fn oakundo_undostack_can_redo( - stack: OakUndoStack, - out_value: *mut c_int, - ) -> c_int { - guard(|| unsafe { - if out_value.is_null() { - return Err(Error::Invalid); - } - with_stack(stack, |s| { - *out_value = if s.can_redo() { 1 } else { 0 }; - Ok(()) - }) - }) - } - - /// `oakundo_undostack_count`. - #[no_mangle] - pub unsafe extern "C" fn oakundo_undostack_count( - stack: OakUndoStack, - out_count: *mut i64, - ) -> c_int { - guard(|| unsafe { - if out_count.is_null() { - return Err(Error::Invalid); - } - with_stack(stack, |s| { - *out_count = s.command_count(); - Ok(()) - }) - }) - } - - /// `oakundo_undostack_index`. - #[no_mangle] - pub unsafe extern "C" fn oakundo_undostack_index( - stack: OakUndoStack, - out_index: *mut i64, - ) -> c_int { - guard(|| unsafe { - if out_index.is_null() { - return Err(Error::Invalid); - } - with_stack(stack, |s| { - *out_index = s.done_count(); - Ok(()) - }) - }) - } - - /// `oakundo_undostack_command_text` (two-stage string getter). - #[no_mangle] - pub unsafe extern "C" fn oakundo_undostack_command_text( - stack: OakUndoStack, - row: i64, - buf: *mut c_char, - buf_size: c_int, - ) -> c_int { - let result = catch_unwind(AssertUnwindSafe(|| -> Result { - with_stack(stack, |s| { - if row < 0 || row >= s.command_count() { - return Err(Error::NotFound); - } - let name = s.command_name(row)?; - let required = (name.len() + 1) as i32; - if !buf.is_null() && buf_size > 0 { - let copy_len = name.len().min((buf_size as usize).saturating_sub(1)); - let bytes = name.as_bytes(); - // SAFETY: `buf` points to `buf_size` writable bytes and - // we write at most `copy_len` (+ one NUL) of them. - unsafe { - std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, copy_len); - *buf.add(copy_len) = 0; - } - } - Ok(required) - }) - })); - match result { - Ok(Ok(required)) => required, - Ok(Err(e)) => e.code(), - Err(_) => crate::error::OAKUNDO_E_FAILED, - } - } - - /// `oakundo_undostack_command_is_done`. - #[no_mangle] - pub unsafe extern "C" fn oakundo_undostack_command_is_done( - stack: OakUndoStack, - row: i64, - out_value: *mut c_int, - ) -> c_int { - guard(|| unsafe { - if out_value.is_null() { - return Err(Error::Invalid); - } - with_stack(stack, |s| { - *out_value = if s.command_is_done(row)? { 1 } else { 0 }; - Ok(()) - }) - }) - } -} diff --git a/crates/oakundo/src/lib.rs b/crates/oakundo/src/lib.rs index 75970888b..9d5837bf0 100644 --- a/crates/oakundo/src/lib.rs +++ b/crates/oakundo/src/lib.rs @@ -31,7 +31,6 @@ #![warn(missing_docs)] pub mod error; -pub mod ffi; pub mod handle; pub mod undocommand; pub mod undostack; diff --git a/crates/oakundo/src/undocommand.rs b/crates/oakundo/src/undocommand.rs index be1c308ce..bd8ea9887 100644 --- a/crates/oakundo/src/undocommand.rs +++ b/crates/oakundo/src/undocommand.rs @@ -28,7 +28,7 @@ //! //! ## Command boxes //! -//! Commands cross the FFI boundary through a dedicated [`CommandBox`] +//! Commands cross crate boundaries through a dedicated [`CommandBox`] //! (not the generic [`crate::handle::RefBox`]): it holds the raw //! `*mut UndoCommand`, an `owns` flag (mirroring the C++ //! `OakUndoCommandBox`) and a refcount. An **owning** box owns and @@ -36,12 +36,18 @@ //! reference into a `MultiUndoCommand` or an `UndoStack`) owns only its //! shell. `take_command` moves the command value out of an owning box, //! turning it into a non-owning shell (the C++ `mark_container_owned`). +//! +//! The handle-level functions below (`command_init` ... `command_free`) +//! were sunk from the former C ABI export layer: they keep the +//! `CHandle`-based signatures so the facade bridges and integration +//! tests can drive the command machinery directly. The `CHandle` layer +//! itself is removed in a later milestone. -use std::ffi::c_void; +use std::ffi::{c_int, c_void}; use std::sync::atomic::{AtomicU32, Ordering}; use crate::error::{Error, Result}; -use crate::handle::{CHandle, OAKUNDO_ABI_VERSION}; +use crate::handle::{guard, guard_handle, guard_void, CHandle, OAKUNDO_ABI_VERSION}; /// `oakundo_command_vtable` — the caller-defined redo/undo/free /// callback table. Any entry may be `None`; a `None` redo/undo makes @@ -348,7 +354,7 @@ pub(crate) struct CommandBox { /// /// # Safety /// The returned handle owns `cmd`; release it with `command_release`. -pub(crate) unsafe fn command_from_owned(cmd: UndoCommand) -> CHandle { +pub unsafe fn command_from_owned(cmd: UndoCommand) -> CHandle { let boxed = Box::into_raw(Box::new(CommandBox { command: Box::into_raw(Box::new(cmd)), owns: true, @@ -367,7 +373,7 @@ pub(crate) unsafe fn command_from_owned(cmd: UndoCommand) -> CHandle { /// /// # Safety /// `cmd` must outlive the returned handle. -pub(crate) unsafe fn command_from_borrowed(cmd: *mut UndoCommand) -> CHandle { +pub unsafe fn command_from_borrowed(cmd: *mut UndoCommand) -> CHandle { let boxed = Box::into_raw(Box::new(CommandBox { command: cmd, owns: false, @@ -385,7 +391,7 @@ pub(crate) unsafe fn command_from_borrowed(cmd: *mut UndoCommand) -> CHandle { /// /// # Safety /// `ctx` must come from a valid, live `CommandBox`. -pub(crate) unsafe fn command_to_ref(ctx: *mut c_void) -> Option<&'static UndoCommand> { +pub unsafe fn command_to_ref(ctx: *mut c_void) -> Option<&'static UndoCommand> { unsafe { if ctx.is_null() { return None; @@ -402,7 +408,7 @@ pub(crate) unsafe fn command_to_ref(ctx: *mut c_void) -> Option<&'static UndoCom /// /// # Safety /// `ctx` must come from a valid, live `CommandBox`. -pub(crate) unsafe fn command_to_mut(ctx: *mut c_void) -> Option<&'static mut UndoCommand> { +pub unsafe fn command_to_mut(ctx: *mut c_void) -> Option<&'static mut UndoCommand> { unsafe { if ctx.is_null() { return None; @@ -420,7 +426,7 @@ pub(crate) unsafe fn command_to_mut(ctx: *mut c_void) -> Option<&'static mut Und /// /// # Safety /// `ctx` must come from a valid, live `CommandBox`. -pub(crate) unsafe fn command_take(ctx: *mut c_void) -> Result { +pub unsafe fn command_take(ctx: *mut c_void) -> Result { unsafe { if ctx.is_null() { return Err(Error::Invalid); @@ -470,3 +476,113 @@ pub(crate) unsafe extern "C" fn command_release(ctx: *mut c_void) { } } } + +// --------------------------------------------------------------------------- +// Handle-level command API (sunk from the former C ABI export layer) +// --------------------------------------------------------------------------- + +/// Create a vtable-backed command handle (refcount 1); a `NULL` vtable +/// yields an empty handle (`oakundo_command_init`). +pub fn command_init(vtable: *const OakUndoCommandVtable, userdata: *mut c_void) -> CHandle { + guard_handle(|| unsafe { + if vtable.is_null() { + return Ok(CHandle::null()); + } + let table = *vtable; + Ok(command_from_owned(UndoCommand::from_vtable(table, userdata))) + }) +} + +/// Create an empty multi command handle (refcount 1) +/// (`oakundo_command_init_multi`). +pub fn command_init_multi() -> CHandle { + guard_handle(|| unsafe { Ok(command_from_owned(UndoCommand::multi())) }) +} + +/// Add `child` to a multi command handle (the stack/multi takes one +/// child reference); `E_INVALID` for an empty handle or a non-multi +/// parent (`oakundo_command_multi_add_child`). +pub fn command_multi_add_child(multi: CHandle, child: CHandle) -> c_int { + guard(|| unsafe { + let parent = command_to_mut(multi.ctx).ok_or(Error::Invalid)?; + if !parent.is_multi() { + return Err(Error::Invalid); + } + let child_cmd = command_take(child.ctx)?; + parent.multi_add_child(child_cmd); + Ok(()) + }) +} + +/// Number of children of a multi command handle; `E_INVALID` for an +/// empty handle or a non-multi parent (`oakundo_command_multi_child_count`). +pub fn command_multi_child_count(multi: CHandle, out_count: *mut c_int) -> c_int { + guard(|| unsafe { + if out_count.is_null() { + return Err(Error::Invalid); + } + let parent = command_to_ref(multi.ctx).ok_or(Error::Invalid)?; + if !parent.is_multi() { + return Err(Error::Invalid); + } + *out_count = parent.multi_child_count() as c_int; + Ok(()) + }) +} + +/// Write a borrowed handle to the child at `index` of a multi command +/// (the returned handle carries its own shell ref); `E_NOT_FOUND` for +/// an out-of-range index (`oakundo_command_multi_child`). +pub fn command_multi_child(multi: CHandle, index: c_int, out_child: *mut CHandle) -> c_int { + guard(|| unsafe { + if out_child.is_null() { + return Err(Error::Invalid); + } + let parent = command_to_ref(multi.ctx).ok_or(Error::Invalid)?; + if !parent.is_multi() { + return Err(Error::Invalid); + } + if index < 0 { + return Err(Error::NotFound); + } + let child = parent.multi_child(index as usize)?; + let ptr = child as *const UndoCommand as *mut UndoCommand; + *out_child = command_from_borrowed(ptr); + Ok(()) + }) +} + +/// Execute a command handle's redo (no-op when already done) +/// (`oakundo_command_redo_now`). +pub fn command_redo_now(command: CHandle) -> c_int { + guard(|| unsafe { + let c = command_to_mut(command.ctx).ok_or(Error::Invalid)?; + c.redo_now(); + Ok(()) + }) +} + +/// Execute a command handle's undo (no-op when not done) +/// (`oakundo_command_undo_now`). +pub fn command_undo_now(command: CHandle) -> c_int { + guard(|| unsafe { + let c = command_to_mut(command.ctx).ok_or(Error::Invalid)?; + c.undo_now(); + Ok(()) + }) +} + +/// Release a command handle in place: run the release callback, then +/// clear `ctx`. `NULL` / empty handles are no-ops +/// (`oakundo_command_free`). +pub fn command_free(command: *mut CHandle) { + guard_void(|| unsafe { + if command.is_null() || (*command).ctx.is_null() { + return; + } + if let Some(release) = (*command).release { + release((*command).ctx); + } + (*command).ctx = std::ptr::null_mut(); + }) +} diff --git a/crates/oakundo/src/undostack.rs b/crates/oakundo/src/undostack.rs index 1360fd99b..a7ad7ceae 100644 --- a/crates/oakundo/src/undostack.rs +++ b/crates/oakundo/src/undostack.rs @@ -23,9 +23,13 @@ //! `can_undo` is false at the bottom (see `undostack.cpp::clear`). use std::collections::VecDeque; +use std::ffi::{c_char, c_int, CStr}; +use std::panic::{catch_unwind, AssertUnwindSafe}; +use std::sync::Mutex; -use crate::error::{Error, Result}; -use crate::undocommand::UndoCommand; +use crate::error::{Error, OAKUNDO_E_FAILED, Result}; +use crate::handle::{get, guard, guard_handle, guard_void, make_owned, CHandle}; +use crate::undocommand::{command_take, UndoCommand}; /// Maximum number of retained history rows (`k_max_undo_commands`). pub const K_MAX_UNDO_COMMANDS: usize = 200; @@ -218,3 +222,204 @@ impl Default for UndoStack { Self::new() } } + +// --------------------------------------------------------------------------- +// Handle-level stack API (sunk from the former C ABI export layer) +// --------------------------------------------------------------------------- + +/// Read a NUL-terminated C string; `NULL` yields an empty string +/// (mirrors the C++ `name ? name : ""`). +fn read_name(name: *const c_char) -> String { + if name.is_null() { + String::new() + } else { + // SAFETY: `name` is a valid NUL-terminated string supplied by the + // caller, or NULL (already handled). + unsafe { CStr::from_ptr(name) }.to_string_lossy().into_owned() + } +} + +/// Lock the stack behind `stack` and run `f` on it. `E_INVALID` for an +/// empty handle. A poisoned mutex is recovered (its inner value is still +/// valid). +pub fn with_stack(stack: &CHandle, f: impl FnOnce(&mut UndoStack) -> Result) -> Result { + // SAFETY: the stack handle always boxes a `Mutex` (created + // by `undostack_init`). + let m = unsafe { get::>(stack) }.ok_or(Error::Invalid)?; + let mut guard = m.lock().unwrap_or_else(|e| e.into_inner()); + f(&mut guard) +} + +/// Create a fresh stack handle (refcount 1) (`oakundo_undostack_init`). +pub fn undostack_init() -> CHandle { + guard_handle(|| Ok(make_owned(Mutex::new(UndoStack::new())))) +} + +/// Release a stack handle in place; `NULL` / empty handles are no-ops +/// (`oakundo_undostack_free`). +pub fn undostack_free(stack: *mut CHandle) { + guard_void(|| unsafe { + if stack.is_null() || (*stack).ctx.is_null() { + return; + } + if let Some(release) = (*stack).release { + release((*stack).ctx); + } + (*stack).ctx = std::ptr::null_mut(); + }) +} + +/// Push a command handle onto the stack (redo then record; the stack +/// takes ownership of the command value, leaving a non-owning shell) +/// (`oakundo_undostack_push`). +pub fn undostack_push(stack: CHandle, command: CHandle, name: *const c_char) -> c_int { + guard(|| { + with_stack(&stack, |s| unsafe { + let cmd = command_take(command.ctx)?; + let name = read_name(name); + s.push(cmd, &name); + Ok(()) + }) + }) +} + +/// Push an already-executed command handle (redo skipped) +/// (`oakundo_undostack_push_pre_executed`). +pub fn undostack_push_pre_executed(stack: CHandle, command: CHandle, name: *const c_char) -> c_int { + guard(|| { + with_stack(&stack, |s| unsafe { + let cmd = command_take(command.ctx)?; + let name = read_name(name); + s.push_pre_executed(cmd, &name); + Ok(()) + }) + }) +} + +/// Undo one command on the stack (`oakundo_undostack_undo`). +pub fn undostack_undo(stack: CHandle) -> c_int { + guard(|| with_stack(&stack, |s| s.undo())) +} + +/// Redo one command on the stack (`oakundo_undostack_redo`). +pub fn undostack_redo(stack: CHandle) -> c_int { + guard(|| with_stack(&stack, |s| s.redo())) +} + +/// Jump to a done-command index (clamped to 0; `index` is i64) +/// (`oakundo_undostack_jump`). +pub fn undostack_jump(stack: CHandle, index: i64) -> c_int { + guard(|| { + with_stack(&stack, |s| { + s.jump(index); + Ok(()) + }) + }) +} + +/// Clear the stack back to the empty bottom command +/// (`oakundo_undostack_clear`). +pub fn undostack_clear(stack: CHandle) -> c_int { + guard(|| { + with_stack(&stack, |s| { + s.clear(); + Ok(()) + }) + }) +} + +/// Write whether an undo is possible (`oakundo_undostack_can_undo`). +pub fn undostack_can_undo(stack: CHandle, out_value: *mut c_int) -> c_int { + guard(|| unsafe { + if out_value.is_null() { + return Err(Error::Invalid); + } + with_stack(&stack, |s| { + *out_value = if s.can_undo() { 1 } else { 0 }; + Ok(()) + }) + }) +} + +/// Write whether a redo is possible (`oakundo_undostack_can_redo`). +pub fn undostack_can_redo(stack: CHandle, out_value: *mut c_int) -> c_int { + guard(|| unsafe { + if out_value.is_null() { + return Err(Error::Invalid); + } + with_stack(&stack, |s| { + *out_value = if s.can_redo() { 1 } else { 0 }; + Ok(()) + }) + }) +} + +/// Write the total history row count (`oakundo_undostack_count`). +pub fn undostack_count(stack: CHandle, out_count: *mut i64) -> c_int { + guard(|| unsafe { + if out_count.is_null() { + return Err(Error::Invalid); + } + with_stack(&stack, |s| { + *out_count = s.command_count(); + Ok(()) + }) + }) +} + +/// Write the done-command count (`oakundo_undostack_index`). +pub fn undostack_index(stack: CHandle, out_index: *mut i64) -> c_int { + guard(|| unsafe { + if out_index.is_null() { + return Err(Error::Invalid); + } + with_stack(&stack, |s| { + *out_index = s.done_count(); + Ok(()) + }) + }) +} + +/// Two-stage label getter for the row at `row`: returns the required +/// size (including NUL), or an error code; copies (truncating) when a +/// buffer is supplied (`oakundo_undostack_command_text`). +pub fn undostack_command_text(stack: CHandle, row: i64, buf: *mut c_char, buf_size: c_int) -> c_int { + let result = catch_unwind(AssertUnwindSafe(|| -> Result { + with_stack(&stack, |s| { + if row < 0 || row >= s.command_count() { + return Err(Error::NotFound); + } + let name = s.command_name(row)?; + let required = (name.len() + 1) as i32; + if !buf.is_null() && buf_size > 0 { + let copy_len = name.len().min((buf_size as usize).saturating_sub(1)); + let bytes = name.as_bytes(); + // SAFETY: `buf` points to `buf_size` writable bytes and we + // write at most `copy_len` (+ one NUL) of them. + unsafe { + std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, copy_len); + *buf.add(copy_len) = 0; + } + } + Ok(required) + }) + })); + match result { + Ok(Ok(required)) => required, + Ok(Err(e)) => e.code(), + Err(_) => OAKUNDO_E_FAILED, + } +} + +/// Write whether the row at `row` is done (`oakundo_undostack_command_is_done`). +pub fn undostack_command_is_done(stack: CHandle, row: i64, out_value: *mut c_int) -> c_int { + guard(|| unsafe { + if out_value.is_null() { + return Err(Error::Invalid); + } + with_stack(&stack, |s| { + *out_value = if s.command_is_done(row)? { 1 } else { 0 }; + Ok(()) + }) + }) +} diff --git a/crates/oakundo/tests/edge_coverage_test.rs b/crates/oakundo/tests/edge_coverage_test.rs index 9f8f3503f..27003d341 100644 --- a/crates/oakundo/tests/edge_coverage_test.rs +++ b/crates/oakundo/tests/edge_coverage_test.rs @@ -17,20 +17,22 @@ //! Edge-path coverage: error-code mapping, borrowed handles, refcount //! symmetry, shell handles after `command_take`, `Default` impls, and the //! `push_pre_executed` redo-tail/cap paths. Everything here goes through -//! the crate's public API or the C ABI. +//! the crate's public Rust API (the handle-level functions sunk from the +//! former C ABI layer). use oakundo::error::{ Error, OAKUNDO_E_FAILED, OAKUNDO_E_INVALID, OAKUNDO_E_NOMEM, OAKUNDO_E_NOT_FOUND, OAKUNDO_E_STATE, }; -use oakundo::ffi::command::{oakundo_command_init, oakundo_command_redo_now}; -use oakundo::ffi::undostack::{ - oakundo_undostack_index, oakundo_undostack_init, oakundo_undostack_push, - oakundo_undostack_push_pre_executed, oakundo_undostack_undo, -}; use oakundo::handle::{make_borrowed, make_owned}; -use oakundo::undocommand::{MultiUndoCommand, OakUndoCommandVtable, UndoCommand}; -use oakundo::undostack::{EmptyCommand, UndoStack, K_MAX_UNDO_COMMANDS}; +use oakundo::undocommand::{ + command_free, command_init, command_init_multi, command_multi_child_count, command_redo_now, + MultiUndoCommand, OakUndoCommandVtable, UndoCommand, +}; +use oakundo::undostack::{ + undostack_can_redo, undostack_free, undostack_index, undostack_init, undostack_push, + undostack_push_pre_executed, undostack_undo, EmptyCommand, UndoStack, K_MAX_UNDO_COMMANDS, +}; /// Every `Error` variant maps to its documented public code. #[test] @@ -115,29 +117,21 @@ fn taken_command_shell_is_inert() { undo: None, free_fn: None, }; - let mut stack = unsafe { oakundo_undostack_init() }; - let cmd = unsafe { oakundo_command_init(&vtable, std::ptr::null_mut()) }; + let mut stack = undostack_init(); + let cmd = command_init(&vtable, std::ptr::null_mut()); assert!(!cmd.ctx.is_null()); let name = c"once"; - assert_eq!( - unsafe { oakundo_undostack_push(stack, cmd, name.as_ptr()) }, - 0 - ); + assert_eq!(undostack_push(stack, cmd, name.as_ptr()), 0); // The shell no longer holds a command value. - assert_eq!(unsafe { oakundo_command_redo_now(cmd) }, OAKUNDO_E_INVALID); + assert_eq!(command_redo_now(cmd), OAKUNDO_E_INVALID); // Taking the same box twice is a state error. - assert_eq!( - unsafe { oakundo_undostack_push(stack, cmd, name.as_ptr()) }, - OAKUNDO_E_STATE - ); + assert_eq!(undostack_push(stack, cmd, name.as_ptr()), OAKUNDO_E_STATE); let mut release = cmd; - unsafe { - oakundo::ffi::command::oakundo_command_free(&mut release); - oakundo::ffi::undostack::oakundo_undostack_free(&mut stack); - } + command_free(&mut release); + undostack_free(&mut stack); } /// `push_pre_executed` also drops the redoable tail and evicts the oldest @@ -149,49 +143,37 @@ fn push_pre_executed_clears_redo_tail_and_caps() { undo: None, free_fn: None, }; - let mut stack = unsafe { oakundo_undostack_init() }; + let mut stack = undostack_init(); let name = c"row"; // Push two, undo one, then push_pre_executed: redo tail is dropped. for _ in 0..2 { - let cmd = unsafe { oakundo_command_init(&vtable, std::ptr::null_mut()) }; - assert_eq!( - unsafe { oakundo_undostack_push(stack, cmd, name.as_ptr()) }, - 0 - ); + let cmd = command_init(&vtable, std::ptr::null_mut()); + assert_eq!(undostack_push(stack, cmd, name.as_ptr()), 0); let mut shell = cmd; - unsafe { oakundo::ffi::command::oakundo_command_free(&mut shell) }; + command_free(&mut shell); } - assert_eq!(unsafe { oakundo_undostack_undo(stack) }, 0); - let cmd = unsafe { oakundo_command_init(&vtable, std::ptr::null_mut()) }; - assert_eq!( - unsafe { oakundo_undostack_push_pre_executed(stack, cmd, name.as_ptr()) }, - 0 - ); + assert_eq!(undostack_undo(stack), 0); + let cmd = command_init(&vtable, std::ptr::null_mut()); + assert_eq!(undostack_push_pre_executed(stack, cmd, name.as_ptr()), 0); let mut can_redo: i32 = 1; - assert_eq!( - unsafe { oakundo::ffi::undostack::oakundo_undostack_can_redo(stack, &mut can_redo) }, - 0 - ); + assert_eq!(undostack_can_redo(stack, &mut can_redo), 0); assert_eq!(can_redo, 0, "push_pre_executed drops the redoable tail"); // Fill past the cap with pre-executed commands: the oldest rows are // evicted and the count stays at K_MAX_UNDO_COMMANDS. for _ in 0..(K_MAX_UNDO_COMMANDS + 10) { - let cmd = unsafe { oakundo_command_init(&vtable, std::ptr::null_mut()) }; - assert_eq!( - unsafe { oakundo_undostack_push_pre_executed(stack, cmd, name.as_ptr()) }, - 0 - ); + let cmd = command_init(&vtable, std::ptr::null_mut()); + assert_eq!(undostack_push_pre_executed(stack, cmd, name.as_ptr()), 0); } let mut index: i64 = 0; - assert_eq!(unsafe { oakundo_undostack_index(stack, &mut index) }, 0); + assert_eq!(undostack_index(stack, &mut index), 0); assert_eq!( index, K_MAX_UNDO_COMMANDS as i64, "pre-executed rows evict at the cap" ); - unsafe { oakundo::ffi::undostack::oakundo_undostack_free(&mut stack) }; + undostack_free(&mut stack); } /// `make_owned` on a stack mutex is what `undostack_init` uses; the @@ -217,7 +199,7 @@ fn command_box_refcounting_and_taken_multi_shell() { free_fn: None, }; - let mut cmd = unsafe { oakundo_command_init(&vtable, std::ptr::null_mut()) }; + let mut cmd = command_init(&vtable, std::ptr::null_mut()); assert!(!cmd.ctx.is_null()); // NULL ctx is a no-op for both refcount callbacks. @@ -232,20 +214,12 @@ fn command_box_refcounting_and_taken_multi_shell() { // A multi command pushed into a stack is taken; the remaining shell // must fail cleanly on child access. - let mut stack = unsafe { oakundo_undostack_init() }; - let mut multi = unsafe { oakundo::ffi::command::oakundo_command_init_multi() }; + let mut stack = undostack_init(); + let mut multi = command_init_multi(); let name = c"m"; - assert_eq!( - unsafe { oakundo_undostack_push(stack, multi, name.as_ptr()) }, - 0 - ); + assert_eq!(undostack_push(stack, multi, name.as_ptr()), 0); let mut out: i32 = -1; - assert_eq!( - unsafe { oakundo::ffi::command::oakundo_command_multi_child_count(multi, &mut out) }, - OAKUNDO_E_INVALID - ); - unsafe { - oakundo::ffi::command::oakundo_command_free(&mut multi); - oakundo::ffi::undostack::oakundo_undostack_free(&mut stack); - } + assert_eq!(command_multi_child_count(multi, &mut out), OAKUNDO_E_INVALID); + command_free(&mut multi); + undostack_free(&mut stack); } diff --git a/crates/oakundo/tests/ffi_contract_test.rs b/crates/oakundo/tests/ffi_contract_test.rs index 690f8cf4e..1660a5989 100644 --- a/crates/oakundo/tests/ffi_contract_test.rs +++ b/crates/oakundo/tests/ffi_contract_test.rs @@ -14,31 +14,27 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//! C ABI contract tests (`ffi.rs`). Each exported function gets at least -//! one success path and one failure path; complex multi-command and stack -//! behavior is exercised as a matrix. The expected semantics are pinned by -//! the C++ module (`src/undo/src`, unchanged). +//! Contract tests for the handle-level command/stack API (the functions +//! sunk from the former `ffi.rs`). Each function gets at least one success +//! path and one failure path; complex multi-command and stack behavior is +//! exercised as a matrix. The expected semantics are pinned by the C++ +//! module (`src/undo/src`, unchanged). use std::cell::RefCell; use std::ffi::{c_char, c_int, c_void, CString}; use oakundo::error::{OAKUNDO_E_INVALID, OAKUNDO_E_NOT_FOUND, OAKUNDO_OK}; -use oakundo::ffi::command::{ - oakundo_command_free, oakundo_command_init, oakundo_command_init_multi, - oakundo_command_multi_add_child, oakundo_command_multi_child, - oakundo_command_multi_child_count, oakundo_command_redo_now, oakundo_command_undo_now, -}; -use oakundo::ffi::undostack::{ - oakundo_undostack_can_redo, oakundo_undostack_can_undo, oakundo_undostack_clear, - oakundo_undostack_command_is_done, oakundo_undostack_command_text, oakundo_undostack_count, - oakundo_undostack_free, oakundo_undostack_index, oakundo_undostack_init, - oakundo_undostack_jump, oakundo_undostack_push, oakundo_undostack_push_pre_executed, - oakundo_undostack_redo, oakundo_undostack_undo, -}; -use oakundo::ffi::{OakUndoCommand, OakUndoStack}; use oakundo::handle::CHandle; use oakundo::handle::OAKUNDO_ABI_VERSION; -use oakundo::undocommand::OakUndoCommandVtable; +use oakundo::undocommand::{ + command_free, command_init, command_init_multi, command_multi_add_child, command_multi_child, + command_multi_child_count, command_redo_now, command_undo_now, OakUndoCommandVtable, +}; +use oakundo::undostack::{ + undostack_can_redo, undostack_can_undo, undostack_clear, undostack_command_is_done, + undostack_command_text, undostack_count, undostack_free, undostack_index, undostack_init, + undostack_jump, undostack_push, undostack_push_pre_executed, undostack_redo, undostack_undo, +}; /// Shared event recorder driven through vtable callbacks. struct Trace { @@ -88,17 +84,17 @@ fn setup() -> (Box, Vec) { } /// A vtable-backed command handle whose callbacks record into `probe`. -unsafe fn make_cmd(probe: *mut Probe) -> OakUndoCommand { +fn make_cmd(probe: *mut Probe) -> CHandle { let vtable = OakUndoCommandVtable { redo: Some(probe_redo), undo: Some(probe_undo), free_fn: Some(probe_free), }; - oakundo_command_init(&vtable, probe as *mut c_void) + command_init(&vtable, probe as *mut c_void) } /// An empty (all-zero) command handle. -fn empty_cmd() -> OakUndoCommand { +fn empty_cmd() -> CHandle { CHandle { ctx: std::ptr::null_mut(), addref: None, @@ -108,7 +104,7 @@ fn empty_cmd() -> OakUndoCommand { } /// An empty (all-zero) stack handle. -fn empty_stack() -> OakUndoStack { +fn empty_stack() -> CHandle { CHandle { ctx: std::ptr::null_mut(), addref: None, @@ -118,8 +114,8 @@ fn empty_stack() -> OakUndoStack { } /// A fresh stack handle (refcount 1) for the calling test. -unsafe fn new_stack() -> OakUndoStack { - oakundo_undostack_init() +fn new_stack() -> CHandle { + undostack_init() } // --------------------------------------------------------------------------- @@ -132,28 +128,28 @@ unsafe fn new_stack() -> OakUndoStack { #[test] fn command_lifecycle() { let (trace, mut probes) = setup(); - let mut cmd = unsafe { make_cmd(&mut probes[0] as *mut Probe) }; + let mut cmd = make_cmd(&mut probes[0] as *mut Probe); assert!(!cmd.ctx.is_null()); assert_eq!(cmd.abi_version, OAKUNDO_ABI_VERSION); assert!(cmd.addref.is_some() && cmd.release.is_some()); - assert_eq!(unsafe { oakundo_command_redo_now(cmd) }, OAKUNDO_OK); + assert_eq!(command_redo_now(cmd), OAKUNDO_OK); assert_eq!(events(&trace), vec!["redo:a"]); // Idempotent redo. - assert_eq!(unsafe { oakundo_command_redo_now(cmd) }, OAKUNDO_OK); + assert_eq!(command_redo_now(cmd), OAKUNDO_OK); assert_eq!(events(&trace), vec!["redo:a"]); - assert_eq!(unsafe { oakundo_command_undo_now(cmd) }, OAKUNDO_OK); + assert_eq!(command_undo_now(cmd), OAKUNDO_OK); assert_eq!(events(&trace), vec!["redo:a", "undo:a"]); // Idempotent undo. - assert_eq!(unsafe { oakundo_command_undo_now(cmd) }, OAKUNDO_OK); + assert_eq!(command_undo_now(cmd), OAKUNDO_OK); assert_eq!(events(&trace), vec!["redo:a", "undo:a"]); // free destroys once: free_fn runs exactly once and ctx is cleared. - unsafe { oakundo_command_free(&mut cmd) }; + command_free(&mut cmd); assert_eq!(events(&trace), vec!["redo:a", "undo:a", "free:a"]); assert!(cmd.ctx.is_null()); } @@ -163,39 +159,27 @@ fn command_lifecycle() { #[test] fn multi_redo_undo_ordering() { let (trace, mut probes) = setup(); - let mut multi = unsafe { oakundo_command_init_multi() }; + let mut multi = command_init_multi(); // add children a, b, c. - let ca = unsafe { make_cmd(&mut probes[0] as *mut Probe) }; - let cb = unsafe { make_cmd(&mut probes[1] as *mut Probe) }; - let cc = unsafe { make_cmd(&mut probes[2] as *mut Probe) }; - assert_eq!( - unsafe { oakundo_command_multi_add_child(multi, ca) }, - OAKUNDO_OK - ); - assert_eq!( - unsafe { oakundo_command_multi_add_child(multi, cb) }, - OAKUNDO_OK - ); - assert_eq!( - unsafe { oakundo_command_multi_add_child(multi, cc) }, - OAKUNDO_OK - ); + let ca = make_cmd(&mut probes[0] as *mut Probe); + let cb = make_cmd(&mut probes[1] as *mut Probe); + let cc = make_cmd(&mut probes[2] as *mut Probe); + assert_eq!(command_multi_add_child(multi, ca), OAKUNDO_OK); + assert_eq!(command_multi_add_child(multi, cb), OAKUNDO_OK); + assert_eq!(command_multi_add_child(multi, cc), OAKUNDO_OK); // child_count reflects three. let mut count: c_int = 0; - assert_eq!( - unsafe { oakundo_command_multi_child_count(multi, &mut count) }, - OAKUNDO_OK - ); + assert_eq!(command_multi_child_count(multi, &mut count), OAKUNDO_OK); assert_eq!(count, 3); // redo fires in insertion order. - assert_eq!(unsafe { oakundo_command_redo_now(multi) }, OAKUNDO_OK); + assert_eq!(command_redo_now(multi), OAKUNDO_OK); assert_eq!(events(&trace), vec!["redo:a", "redo:b", "redo:c"]); // undo fires in reverse order. - assert_eq!(unsafe { oakundo_command_undo_now(multi) }, OAKUNDO_OK); + assert_eq!(command_undo_now(multi), OAKUNDO_OK); assert_eq!( events(&trace), vec!["redo:a", "redo:b", "redo:c", "undo:c", "undo:b", "undo:a"] @@ -203,16 +187,13 @@ fn multi_redo_undo_ordering() { // Borrowed child handles are released harmlessly before the multi dies. let mut child0 = empty_cmd(); - assert_eq!( - unsafe { oakundo_command_multi_child(multi, 0, &mut child0) }, - OAKUNDO_OK - ); + assert_eq!(command_multi_child(multi, 0, &mut child0), OAKUNDO_OK); assert!(!child0.ctx.is_null()); - unsafe { oakundo_command_free(&mut child0) }; + command_free(&mut child0); // free the multi: children are freed (shell only) without double-free. let mut multi_owned = multi; - unsafe { oakundo_command_free(&mut multi_owned) }; + command_free(&mut multi_owned); assert_eq!( events(&trace) .iter() @@ -228,83 +209,50 @@ fn multi_redo_undo_ordering() { #[test] fn stack_push_undo_redo_queries() { let (trace, mut probes) = setup(); - let mut stack = unsafe { new_stack() }; + let mut stack = new_stack(); let mut count: i64 = 0; let mut value: c_int = 0; - assert_eq!( - unsafe { oakundo_undostack_count(stack, &mut count) }, - OAKUNDO_OK - ); + assert_eq!(undostack_count(stack, &mut count), OAKUNDO_OK); assert_eq!(count, 1); - assert_eq!( - unsafe { oakundo_undostack_can_undo(stack, &mut value) }, - OAKUNDO_OK - ); + assert_eq!(undostack_can_undo(stack, &mut value), OAKUNDO_OK); assert_eq!(value, 0); - assert_eq!( - unsafe { oakundo_undostack_can_redo(stack, &mut value) }, - OAKUNDO_OK - ); + assert_eq!(undostack_can_redo(stack, &mut value), OAKUNDO_OK); assert_eq!(value, 0); // Push two commands. let name_a = CString::new("A").unwrap(); - let mut ca = unsafe { make_cmd(&mut probes[0] as *mut Probe) }; - assert_eq!( - unsafe { oakundo_undostack_push(stack, ca, name_a.as_ptr()) }, - OAKUNDO_OK - ); - unsafe { oakundo_command_free(&mut ca) }; // non-owning shell now + let mut ca = make_cmd(&mut probes[0] as *mut Probe); + assert_eq!(undostack_push(stack, ca, name_a.as_ptr()), OAKUNDO_OK); + command_free(&mut ca); // non-owning shell now let name_b = CString::new("B").unwrap(); - let mut cb = unsafe { make_cmd(&mut probes[1] as *mut Probe) }; - assert_eq!( - unsafe { oakundo_undostack_push(stack, cb, name_b.as_ptr()) }, - OAKUNDO_OK - ); - unsafe { oakundo_command_free(&mut cb) }; + let mut cb = make_cmd(&mut probes[1] as *mut Probe); + assert_eq!(undostack_push(stack, cb, name_b.as_ptr()), OAKUNDO_OK); + command_free(&mut cb); assert_eq!(events(&trace), vec!["redo:a", "redo:b"]); - assert_eq!( - unsafe { oakundo_undostack_count(stack, &mut count) }, - OAKUNDO_OK - ); + assert_eq!(undostack_count(stack, &mut count), OAKUNDO_OK); assert_eq!(count, 3); - assert_eq!( - unsafe { oakundo_undostack_can_undo(stack, &mut value) }, - OAKUNDO_OK - ); + assert_eq!(undostack_can_undo(stack, &mut value), OAKUNDO_OK); assert_eq!(value, 1); - assert_eq!( - unsafe { oakundo_undostack_can_redo(stack, &mut value) }, - OAKUNDO_OK - ); + assert_eq!(undostack_can_redo(stack, &mut value), OAKUNDO_OK); assert_eq!(value, 0); // Undo moves B into the redoable tail. - assert_eq!(unsafe { oakundo_undostack_undo(stack) }, OAKUNDO_OK); + assert_eq!(undostack_undo(stack), OAKUNDO_OK); assert_eq!(events(&trace), vec!["redo:a", "redo:b", "undo:b"]); - assert_eq!( - unsafe { oakundo_undostack_can_undo(stack, &mut value) }, - OAKUNDO_OK - ); + assert_eq!(undostack_can_undo(stack, &mut value), OAKUNDO_OK); assert_eq!(value, 1); - assert_eq!( - unsafe { oakundo_undostack_can_redo(stack, &mut value) }, - OAKUNDO_OK - ); + assert_eq!(undostack_can_redo(stack, &mut value), OAKUNDO_OK); assert_eq!(value, 1); // Redo restores. - assert_eq!(unsafe { oakundo_undostack_redo(stack) }, OAKUNDO_OK); + assert_eq!(undostack_redo(stack), OAKUNDO_OK); assert_eq!(events(&trace), vec!["redo:a", "redo:b", "undo:b", "redo:b"]); - assert_eq!( - unsafe { oakundo_undostack_can_redo(stack, &mut value) }, - OAKUNDO_OK - ); + assert_eq!(undostack_can_redo(stack, &mut value), OAKUNDO_OK); assert_eq!(value, 0); - unsafe { oakundo_undostack_free(&mut stack) }; + undostack_free(&mut stack); } /// `can_redo`/`index` after undo and redo; `jump(0)` clamps to the @@ -312,31 +260,25 @@ fn stack_push_undo_redo_queries() { #[test] fn stack_jump_clamps() { let (trace, mut probes) = setup(); - let mut stack = unsafe { new_stack() }; + let mut stack = new_stack(); let na = CString::new("A").unwrap(); let nb = CString::new("B").unwrap(); let nc = CString::new("C").unwrap(); - let mut ca = unsafe { make_cmd(&mut probes[0] as *mut Probe) }; - unsafe { oakundo_undostack_push(stack, ca, na.as_ptr()) }; - let mut cb = unsafe { make_cmd(&mut probes[1] as *mut Probe) }; - unsafe { oakundo_undostack_push(stack, cb, nb.as_ptr()) }; - let mut cc = unsafe { make_cmd(&mut probes[2] as *mut Probe) }; - unsafe { oakundo_undostack_push(stack, cc, nc.as_ptr()) }; + let mut ca = make_cmd(&mut probes[0] as *mut Probe); + undostack_push(stack, ca, na.as_ptr()); + let mut cb = make_cmd(&mut probes[1] as *mut Probe); + undostack_push(stack, cb, nb.as_ptr()); + let mut cc = make_cmd(&mut probes[2] as *mut Probe); + undostack_push(stack, cc, nc.as_ptr()); let mut index: i64 = 0; - assert_eq!( - unsafe { oakundo_undostack_index(stack, &mut index) }, - OAKUNDO_OK - ); + assert_eq!(undostack_index(stack, &mut index), OAKUNDO_OK); assert_eq!(index, 4); // jump to the bottom (0): undo all three; negative clamps to 0 too. - assert_eq!(unsafe { oakundo_undostack_jump(stack, 0) }, OAKUNDO_OK); - assert_eq!( - unsafe { oakundo_undostack_index(stack, &mut index) }, - OAKUNDO_OK - ); + assert_eq!(undostack_jump(stack, 0), OAKUNDO_OK); + assert_eq!(undostack_index(stack, &mut index), OAKUNDO_OK); assert_eq!(index, 1); assert_eq!( events(&trace), @@ -344,31 +286,22 @@ fn stack_jump_clamps() { ); // jump back up to 3: redo a and b (c was already undone to reach 1). - assert_eq!(unsafe { oakundo_undostack_jump(stack, 3) }, OAKUNDO_OK); - assert_eq!( - unsafe { oakundo_undostack_index(stack, &mut index) }, - OAKUNDO_OK - ); + assert_eq!(undostack_jump(stack, 3), OAKUNDO_OK); + assert_eq!(undostack_index(stack, &mut index), OAKUNDO_OK); assert_eq!(index, 3); // jump beyond the top redoes up to the top (index 4), matching the C++ // `jump` (undone commands are redoable, so the second loop runs). - assert_eq!(unsafe { oakundo_undostack_jump(stack, 100) }, OAKUNDO_OK); - assert_eq!( - unsafe { oakundo_undostack_index(stack, &mut index) }, - OAKUNDO_OK - ); + assert_eq!(undostack_jump(stack, 100), OAKUNDO_OK); + assert_eq!(undostack_index(stack, &mut index), OAKUNDO_OK); assert_eq!(index, 4); // Negative index clamps to the bottom. - assert_eq!(unsafe { oakundo_undostack_jump(stack, -5) }, OAKUNDO_OK); - assert_eq!( - unsafe { oakundo_undostack_index(stack, &mut index) }, - OAKUNDO_OK - ); + assert_eq!(undostack_jump(stack, -5), OAKUNDO_OK); + assert_eq!(undostack_index(stack, &mut index), OAKUNDO_OK); assert_eq!(index, 1); - unsafe { oakundo_undostack_free(&mut stack) }; + undostack_free(&mut stack); } /// `push_pre_executed` records without redoing (stays undoable); empty @@ -376,53 +309,38 @@ fn stack_jump_clamps() { #[test] fn stack_pre_executed_and_empty_multi() { let (trace, mut probes) = setup(); - let mut stack = unsafe { new_stack() }; + let mut stack = new_stack(); // push_pre_executed: no redo callback. let name = CString::new("Pre").unwrap(); - let mut cp = unsafe { make_cmd(&mut probes[0] as *mut Probe) }; - assert_eq!( - unsafe { oakundo_undostack_push_pre_executed(stack, cp, name.as_ptr()) }, - OAKUNDO_OK - ); - unsafe { oakundo_command_free(&mut cp) }; + let mut cp = make_cmd(&mut probes[0] as *mut Probe); + assert_eq!(undostack_push_pre_executed(stack, cp, name.as_ptr()), OAKUNDO_OK); + command_free(&mut cp); assert_eq!(events(&trace), Vec::::new()); // The pre-executed command is recorded as done (undoable). let mut done: c_int = 0; - assert_eq!( - unsafe { oakundo_undostack_command_is_done(stack, 1, &mut done) }, - OAKUNDO_OK - ); + assert_eq!(undostack_command_is_done(stack, 1, &mut done), OAKUNDO_OK); assert_eq!(done, 1); let mut count: i64 = 0; - assert_eq!( - unsafe { oakundo_undostack_count(stack, &mut count) }, - OAKUNDO_OK - ); + assert_eq!(undostack_count(stack, &mut count), OAKUNDO_OK); assert_eq!(count, 2); // Undoing the pre-executed command still runs its undo callback. - assert_eq!(unsafe { oakundo_undostack_undo(stack) }, OAKUNDO_OK); + assert_eq!(undostack_undo(stack), OAKUNDO_OK); assert_eq!(events(&trace), vec!["undo:a"]); // Empty multi command is discarded on push: count stays 2 (bottom // "New/Open Project" + the undone pre-executed command), not 3. - let mut multi = unsafe { oakundo_command_init_multi() }; + let mut multi = command_init_multi(); let name2 = CString::new("Empty").unwrap(); - assert_eq!( - unsafe { oakundo_undostack_push(stack, multi, name2.as_ptr()) }, - OAKUNDO_OK - ); - unsafe { oakundo_command_free(&mut multi) }; - assert_eq!( - unsafe { oakundo_undostack_count(stack, &mut count) }, - OAKUNDO_OK - ); + assert_eq!(undostack_push(stack, multi, name2.as_ptr()), OAKUNDO_OK); + command_free(&mut multi); + assert_eq!(undostack_count(stack, &mut count), OAKUNDO_OK); assert_eq!(count, 2, "empty multi is discarded on push"); - unsafe { oakundo_undostack_free(&mut stack) }; + undostack_free(&mut stack); } /// Every handle-returning export returns `ctx == NULL` on failure and a @@ -433,44 +351,44 @@ fn handle_and_free_contract() { let (_trace, mut probes) = setup(); // init with a NULL vtable → empty handle. - let empty = unsafe { oakundo_command_init(std::ptr::null(), std::ptr::null_mut()) }; + let empty = command_init(std::ptr::null(), std::ptr::null_mut()); assert!(empty.ctx.is_null()); assert_eq!(empty.abi_version, 0); assert!(empty.addref.is_none() && empty.release.is_none()); // init with a valid vtable → stamped handle. - let mut cmd = unsafe { make_cmd(&mut probes[0] as *mut Probe) }; + let mut cmd = make_cmd(&mut probes[0] as *mut Probe); assert!(!cmd.ctx.is_null()); assert_eq!(cmd.abi_version, OAKUNDO_ABI_VERSION); // init_multi → stamped handle. - let mut multi = unsafe { oakundo_command_init_multi() }; + let mut multi = command_init_multi(); assert!(!multi.ctx.is_null()); assert_eq!(multi.abi_version, OAKUNDO_ABI_VERSION); // init stack → stamped handle. - let mut stack = unsafe { new_stack() }; + let mut stack = new_stack(); assert!(!stack.ctx.is_null()); assert_eq!(stack.abi_version, OAKUNDO_ABI_VERSION); // free(NULL) is a no-op for both families. - unsafe { oakundo_command_free(std::ptr::null_mut()) }; - unsafe { oakundo_undostack_free(std::ptr::null_mut()) }; + command_free(std::ptr::null_mut()); + undostack_free(std::ptr::null_mut()); // free(empty handle value) is a no-op. let mut ecmd = empty_cmd(); - unsafe { oakundo_command_free(&mut ecmd) }; + command_free(&mut ecmd); assert!(ecmd.ctx.is_null()); let mut estack = empty_stack(); - unsafe { oakundo_undostack_free(&mut estack) }; + undostack_free(&mut estack); assert!(estack.ctx.is_null()); // free(valid) clears ctx. - unsafe { oakundo_command_free(&mut multi) }; + command_free(&mut multi); assert!(multi.ctx.is_null()); - unsafe { oakundo_command_free(&mut cmd) }; + command_free(&mut cmd); assert!(cmd.ctx.is_null()); - unsafe { oakundo_undostack_free(&mut stack) }; + undostack_free(&mut stack); assert!(stack.ctx.is_null()); } @@ -480,174 +398,126 @@ fn handle_and_free_contract() { #[test] fn command_redo_undo_now_null_is_invalid() { - assert_eq!( - unsafe { oakundo_command_redo_now(empty_cmd()) }, - OAKUNDO_E_INVALID - ); - assert_eq!( - unsafe { oakundo_command_undo_now(empty_cmd()) }, - OAKUNDO_E_INVALID - ); + assert_eq!(command_redo_now(empty_cmd()), OAKUNDO_E_INVALID); + assert_eq!(command_undo_now(empty_cmd()), OAKUNDO_E_INVALID); } #[test] fn command_multi_add_child_errors() { let (_trace, mut probes) = setup(); - let mut multi = unsafe { oakundo_command_init_multi() }; - let mut child = unsafe { make_cmd(&mut probes[0] as *mut Probe) }; - let mut vtable_cmd = unsafe { make_cmd(&mut probes[1] as *mut Probe) }; + let mut multi = command_init_multi(); + let mut child = make_cmd(&mut probes[0] as *mut Probe); + let mut vtable_cmd = make_cmd(&mut probes[1] as *mut Probe); // multi is null → E_INVALID. - assert_eq!( - unsafe { oakundo_command_multi_add_child(empty_cmd(), child) }, - OAKUNDO_E_INVALID - ); + assert_eq!(command_multi_add_child(empty_cmd(), child), OAKUNDO_E_INVALID); // child is null → E_INVALID (nothing taken). - assert_eq!( - unsafe { oakundo_command_multi_add_child(multi, empty_cmd()) }, - OAKUNDO_E_INVALID - ); + assert_eq!(command_multi_add_child(multi, empty_cmd()), OAKUNDO_E_INVALID); // target is a vtable command, not a multi → E_INVALID. - assert_eq!( - unsafe { oakundo_command_multi_add_child(vtable_cmd, child) }, - OAKUNDO_E_INVALID - ); + assert_eq!(command_multi_add_child(vtable_cmd, child), OAKUNDO_E_INVALID); // The child handle still owns its value (never taken), so free it. - unsafe { oakundo_command_free(&mut child) }; - unsafe { oakundo_command_free(&mut vtable_cmd) }; - unsafe { oakundo_command_free(&mut multi) }; + command_free(&mut child); + command_free(&mut vtable_cmd); + command_free(&mut multi); } #[test] fn command_multi_child_count_errors() { let (_trace, mut probes) = setup(); - let mut multi = unsafe { oakundo_command_init_multi() }; - let mut vtable_cmd = unsafe { make_cmd(&mut probes[0] as *mut Probe) }; + let mut multi = command_init_multi(); + let mut vtable_cmd = make_cmd(&mut probes[0] as *mut Probe); let mut out: c_int = -1; // null out pointer → E_INVALID. - assert_eq!( - unsafe { oakundo_command_multi_child_count(multi, std::ptr::null_mut()) }, - OAKUNDO_E_INVALID - ); + assert_eq!(command_multi_child_count(multi, std::ptr::null_mut()), OAKUNDO_E_INVALID); // null multi → E_INVALID. - assert_eq!( - unsafe { oakundo_command_multi_child_count(empty_cmd(), &mut out) }, - OAKUNDO_E_INVALID - ); + assert_eq!(command_multi_child_count(empty_cmd(), &mut out), OAKUNDO_E_INVALID); // non-multi command → E_INVALID. - assert_eq!( - unsafe { oakundo_command_multi_child_count(vtable_cmd, &mut out) }, - OAKUNDO_E_INVALID - ); + assert_eq!(command_multi_child_count(vtable_cmd, &mut out), OAKUNDO_E_INVALID); - unsafe { oakundo_command_free(&mut vtable_cmd) }; - unsafe { oakundo_command_free(&mut multi) }; + command_free(&mut vtable_cmd); + command_free(&mut multi); } #[test] fn command_multi_child_errors() { let (_trace, mut probes) = setup(); - let mut multi = unsafe { oakundo_command_init_multi() }; - let mut vtable_cmd = unsafe { make_cmd(&mut probes[0] as *mut Probe) }; + let mut multi = command_init_multi(); + let mut vtable_cmd = make_cmd(&mut probes[0] as *mut Probe); let mut out = empty_cmd(); // null out pointer → E_INVALID. - assert_eq!( - unsafe { oakundo_command_multi_child(multi, 0, std::ptr::null_mut()) }, - OAKUNDO_E_INVALID - ); + assert_eq!(command_multi_child(multi, 0, std::ptr::null_mut()), OAKUNDO_E_INVALID); // null multi → E_INVALID. - assert_eq!( - unsafe { oakundo_command_multi_child(empty_cmd(), 0, &mut out) }, - OAKUNDO_E_INVALID - ); + assert_eq!(command_multi_child(empty_cmd(), 0, &mut out), OAKUNDO_E_INVALID); // non-multi command → E_INVALID. - assert_eq!( - unsafe { oakundo_command_multi_child(vtable_cmd, 0, &mut out) }, - OAKUNDO_E_INVALID - ); + assert_eq!(command_multi_child(vtable_cmd, 0, &mut out), OAKUNDO_E_INVALID); // empty multi, negative index → E_NOT_FOUND. - assert_eq!( - unsafe { oakundo_command_multi_child(multi, -1, &mut out) }, - OAKUNDO_E_NOT_FOUND - ); + assert_eq!(command_multi_child(multi, -1, &mut out), OAKUNDO_E_NOT_FOUND); // empty multi, positive OOB → E_NOT_FOUND. - assert_eq!( - unsafe { oakundo_command_multi_child(multi, 5, &mut out) }, - OAKUNDO_E_NOT_FOUND - ); + assert_eq!(command_multi_child(multi, 5, &mut out), OAKUNDO_E_NOT_FOUND); - unsafe { oakundo_command_free(&mut vtable_cmd) }; - unsafe { oakundo_command_free(&mut multi) }; + command_free(&mut vtable_cmd); + command_free(&mut multi); } #[test] fn command_multi_child_success_borrowed() { let (trace, mut probes) = setup(); - let mut multi = unsafe { oakundo_command_init_multi() }; - let mut ca = unsafe { make_cmd(&mut probes[0] as *mut Probe) }; - unsafe { oakundo_command_multi_add_child(multi, ca) }; + let mut multi = command_init_multi(); + let mut ca = make_cmd(&mut probes[0] as *mut Probe); + command_multi_add_child(multi, ca); let mut child = empty_cmd(); - assert_eq!( - unsafe { oakundo_command_multi_child(multi, 0, &mut child) }, - OAKUNDO_OK - ); + assert_eq!(command_multi_child(multi, 0, &mut child), OAKUNDO_OK); assert!(!child.ctx.is_null()); // A borrowed child is not independently owned: freeing it frees only the // shell and must not free the child the multi still owns. - unsafe { oakundo_command_free(&mut child) }; + command_free(&mut child); assert!(child.ctx.is_null()); // Redo through the multi still works and no child was freed. - assert_eq!(unsafe { oakundo_command_redo_now(multi) }, OAKUNDO_OK); + assert_eq!(command_redo_now(multi), OAKUNDO_OK); assert_eq!(events(&trace), vec!["redo:a"]); - unsafe { oakundo_command_free(&mut multi) }; + command_free(&mut multi); assert_eq!(events(&trace), vec!["redo:a", "free:a"]); } #[test] fn command_free_fires_exactly_once() { let (trace, mut probes) = setup(); - let mut cmd = unsafe { make_cmd(&mut probes[0] as *mut Probe) }; + let mut cmd = make_cmd(&mut probes[0] as *mut Probe); // free → one free callback. - unsafe { oakundo_command_free(&mut cmd) }; + command_free(&mut cmd); assert_eq!(events(&trace), vec!["free:a"]); // free again on a cleared handle → no-op. - unsafe { oakundo_command_free(&mut cmd) }; + command_free(&mut cmd); assert_eq!(events(&trace), vec!["free:a"]); } #[test] fn undostack_free_null_and_empty() { let mut stack = empty_stack(); - unsafe { oakundo_undostack_free(std::ptr::null_mut()) }; - unsafe { oakundo_undostack_free(&mut stack) }; + undostack_free(std::ptr::null_mut()); + undostack_free(&mut stack); assert!(stack.ctx.is_null()); } #[test] fn undostack_push_errors() { let (trace, mut probes) = setup(); - let mut stack = unsafe { new_stack() }; - let mut cmd = unsafe { make_cmd(&mut probes[0] as *mut Probe) }; + let mut stack = new_stack(); + let mut cmd = make_cmd(&mut probes[0] as *mut Probe); let name = CString::new("A").unwrap(); // null command → E_INVALID; stack untouched, command still owned. - assert_eq!( - unsafe { oakundo_undostack_push(stack, empty_cmd(), name.as_ptr()) }, - OAKUNDO_E_INVALID - ); + assert_eq!(undostack_push(stack, empty_cmd(), name.as_ptr()), OAKUNDO_E_INVALID); // empty stack → E_INVALID; command NOT drained (still owns its value). - assert_eq!( - unsafe { oakundo_undostack_push(empty_stack(), cmd, name.as_ptr()) }, - OAKUNDO_E_INVALID - ); + assert_eq!(undostack_push(empty_stack(), cmd, name.as_ptr()), OAKUNDO_E_INVALID); assert_eq!( events(&trace), Vec::::new(), @@ -655,231 +525,157 @@ fn undostack_push_errors() { ); // NULL name is accepted as an empty label → OK. - assert_eq!( - unsafe { oakundo_undostack_push(stack, cmd, std::ptr::null()) }, - OAKUNDO_OK - ); + assert_eq!(undostack_push(stack, cmd, std::ptr::null()), OAKUNDO_OK); - unsafe { oakundo_command_free(&mut cmd) }; // now a non-owning shell - unsafe { oakundo_undostack_free(&mut stack) }; + command_free(&mut cmd); // now a non-owning shell + undostack_free(&mut stack); } #[test] fn undostack_push_pre_executed_errors() { let (_trace, mut probes) = setup(); - let mut stack = unsafe { new_stack() }; - let mut cmd = unsafe { make_cmd(&mut probes[0] as *mut Probe) }; + let mut stack = new_stack(); + let mut cmd = make_cmd(&mut probes[0] as *mut Probe); let name = CString::new("A").unwrap(); assert_eq!( - unsafe { oakundo_undostack_push_pre_executed(empty_stack(), cmd, name.as_ptr()) }, + undostack_push_pre_executed(empty_stack(), cmd, name.as_ptr()), OAKUNDO_E_INVALID ); // command was not drained; still owned, so free it. - unsafe { oakundo_command_free(&mut cmd) }; + command_free(&mut cmd); assert_eq!( - unsafe { oakundo_undostack_push_pre_executed(stack, empty_cmd(), name.as_ptr()) }, + undostack_push_pre_executed(stack, empty_cmd(), name.as_ptr()), OAKUNDO_E_INVALID ); - unsafe { oakundo_undostack_free(&mut stack) }; + undostack_free(&mut stack); } #[test] fn undostack_undo_redo_errors() { - let mut stack = unsafe { new_stack() }; - assert_eq!( - unsafe { oakundo_undostack_undo(empty_stack()) }, - OAKUNDO_E_INVALID - ); - assert_eq!( - unsafe { oakundo_undostack_redo(empty_stack()) }, - OAKUNDO_E_INVALID - ); + let mut stack = new_stack(); + assert_eq!(undostack_undo(empty_stack()), OAKUNDO_E_INVALID); + assert_eq!(undostack_redo(empty_stack()), OAKUNDO_E_INVALID); // Valid stack, but nothing to undo: still OK (no-op). - assert_eq!(unsafe { oakundo_undostack_undo(stack) }, OAKUNDO_OK); - assert_eq!(unsafe { oakundo_undostack_redo(stack) }, OAKUNDO_OK); - unsafe { oakundo_undostack_free(&mut stack) }; + assert_eq!(undostack_undo(stack), OAKUNDO_OK); + assert_eq!(undostack_redo(stack), OAKUNDO_OK); + undostack_free(&mut stack); } #[test] fn undostack_jump_clear_errors() { - let mut stack = unsafe { new_stack() }; - assert_eq!( - unsafe { oakundo_undostack_jump(empty_stack(), 3) }, - OAKUNDO_E_INVALID - ); - assert_eq!( - unsafe { oakundo_undostack_clear(empty_stack()) }, - OAKUNDO_E_INVALID - ); + let mut stack = new_stack(); + assert_eq!(undostack_jump(empty_stack(), 3), OAKUNDO_E_INVALID); + assert_eq!(undostack_clear(empty_stack()), OAKUNDO_E_INVALID); // Clear on a valid stack resets to the empty bottom command. let mut count: i64 = 0; - unsafe { oakundo_undostack_clear(stack) }; - assert_eq!( - unsafe { oakundo_undostack_count(stack, &mut count) }, - OAKUNDO_OK - ); + undostack_clear(stack); + assert_eq!(undostack_count(stack, &mut count), OAKUNDO_OK); assert_eq!(count, 1); - unsafe { oakundo_undostack_free(&mut stack) }; + undostack_free(&mut stack); } #[test] fn undostack_can_undo_redo_errors() { - let mut stack = unsafe { new_stack() }; + let mut stack = new_stack(); let mut value: c_int = 0; - assert_eq!( - unsafe { oakundo_undostack_can_undo(empty_stack(), &mut value) }, - OAKUNDO_E_INVALID - ); - assert_eq!( - unsafe { oakundo_undostack_can_redo(empty_stack(), &mut value) }, - OAKUNDO_E_INVALID - ); - assert_eq!( - unsafe { oakundo_undostack_can_undo(stack, std::ptr::null_mut()) }, - OAKUNDO_E_INVALID - ); - assert_eq!( - unsafe { oakundo_undostack_can_redo(stack, std::ptr::null_mut()) }, - OAKUNDO_E_INVALID - ); - unsafe { oakundo_undostack_free(&mut stack) }; + assert_eq!(undostack_can_undo(empty_stack(), &mut value), OAKUNDO_E_INVALID); + assert_eq!(undostack_can_redo(empty_stack(), &mut value), OAKUNDO_E_INVALID); + assert_eq!(undostack_can_undo(stack, std::ptr::null_mut()), OAKUNDO_E_INVALID); + assert_eq!(undostack_can_redo(stack, std::ptr::null_mut()), OAKUNDO_E_INVALID); + undostack_free(&mut stack); } #[test] fn undostack_count_index_errors() { - let mut stack = unsafe { new_stack() }; + let mut stack = new_stack(); let mut out: i64 = 0; - assert_eq!( - unsafe { oakundo_undostack_count(empty_stack(), &mut out) }, - OAKUNDO_E_INVALID - ); - assert_eq!( - unsafe { oakundo_undostack_index(empty_stack(), &mut out) }, - OAKUNDO_E_INVALID - ); - assert_eq!( - unsafe { oakundo_undostack_count(stack, std::ptr::null_mut()) }, - OAKUNDO_E_INVALID - ); - assert_eq!( - unsafe { oakundo_undostack_index(stack, std::ptr::null_mut()) }, - OAKUNDO_E_INVALID - ); - unsafe { oakundo_undostack_free(&mut stack) }; + assert_eq!(undostack_count(empty_stack(), &mut out), OAKUNDO_E_INVALID); + assert_eq!(undostack_index(empty_stack(), &mut out), OAKUNDO_E_INVALID); + assert_eq!(undostack_count(stack, std::ptr::null_mut()), OAKUNDO_E_INVALID); + assert_eq!(undostack_index(stack, std::ptr::null_mut()), OAKUNDO_E_INVALID); + undostack_free(&mut stack); } #[test] fn undostack_command_text_two_stage_and_errors() { let (_trace, mut probes) = setup(); - let mut stack = unsafe { new_stack() }; + let mut stack = new_stack(); let name = CString::new("MyAction").unwrap(); - let mut cmd = unsafe { make_cmd(&mut probes[0] as *mut Probe) }; - unsafe { oakundo_undostack_push(stack, cmd, name.as_ptr()) }; + let mut cmd = make_cmd(&mut probes[0] as *mut Probe); + undostack_push(stack, cmd, name.as_ptr()); // Failure: empty stack → E_INVALID. - assert_eq!( - unsafe { oakundo_undostack_command_text(empty_stack(), 1, std::ptr::null_mut(), 0) }, - OAKUNDO_E_INVALID - ); + assert_eq!(undostack_command_text(empty_stack(), 1, std::ptr::null_mut(), 0), OAKUNDO_E_INVALID); // Failure: OOB row (positive) → E_NOT_FOUND. - assert_eq!( - unsafe { oakundo_undostack_command_text(stack, 5, std::ptr::null_mut(), 0) }, - OAKUNDO_E_NOT_FOUND - ); + assert_eq!(undostack_command_text(stack, 5, std::ptr::null_mut(), 0), OAKUNDO_E_NOT_FOUND); // Failure: negative row → E_NOT_FOUND. - assert_eq!( - unsafe { oakundo_undostack_command_text(stack, -1, std::ptr::null_mut(), 0) }, - OAKUNDO_E_NOT_FOUND - ); + assert_eq!(undostack_command_text(stack, -1, std::ptr::null_mut(), 0), OAKUNDO_E_NOT_FOUND); // Stage one: null buffer returns the required size ("MyAction" + NUL). - let required = unsafe { oakundo_undostack_command_text(stack, 1, std::ptr::null_mut(), 0) }; + let required = undostack_command_text(stack, 1, std::ptr::null_mut(), 0); assert_eq!(required, "MyAction".len() as c_int + 1); // Stage two: a buffer of that size is populated with a NUL-terminated // string and the required size is returned again. let mut buf = vec![0u8; required as usize]; - let ret = unsafe { - oakundo_undostack_command_text(stack, 1, buf.as_mut_ptr() as *mut c_char, required) - }; + let ret = undostack_command_text(stack, 1, buf.as_mut_ptr() as *mut c_char, required); assert_eq!(ret, required); let actual = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr() as *const c_char) }; assert_eq!(actual.to_bytes(), b"MyAction"); // A too-small buffer is safely truncated (NUL-terminated). let mut small = vec![0xffu8; 3]; - unsafe { oakundo_undostack_command_text(stack, 1, small.as_mut_ptr() as *mut c_char, 3) }; + undostack_command_text(stack, 1, small.as_mut_ptr() as *mut c_char, 3); assert_eq!(small, [b'M', b'y', 0]); - unsafe { oakundo_command_free(&mut cmd) }; - unsafe { oakundo_undostack_free(&mut stack) }; + command_free(&mut cmd); + undostack_free(&mut stack); } #[test] fn undostack_command_is_done_errors() { - let mut stack = unsafe { new_stack() }; + let mut stack = new_stack(); let mut value: c_int = 0; - assert_eq!( - unsafe { oakundo_undostack_command_is_done(empty_stack(), 0, &mut value) }, - OAKUNDO_E_INVALID - ); - assert_eq!( - unsafe { oakundo_undostack_command_is_done(stack, 0, std::ptr::null_mut()) }, - OAKUNDO_E_INVALID - ); - assert_eq!( - unsafe { oakundo_undostack_command_is_done(stack, 5, &mut value) }, - OAKUNDO_E_NOT_FOUND - ); - assert_eq!( - unsafe { oakundo_undostack_command_is_done(stack, -1, &mut value) }, - OAKUNDO_E_NOT_FOUND - ); - unsafe { oakundo_undostack_free(&mut stack) }; + assert_eq!(undostack_command_is_done(empty_stack(), 0, &mut value), OAKUNDO_E_INVALID); + assert_eq!(undostack_command_is_done(stack, 0, std::ptr::null_mut()), OAKUNDO_E_INVALID); + assert_eq!(undostack_command_is_done(stack, 5, &mut value), OAKUNDO_E_NOT_FOUND); + assert_eq!(undostack_command_is_done(stack, -1, &mut value), OAKUNDO_E_NOT_FOUND); + undostack_free(&mut stack); } #[test] -fn undostack_push_drops_redoable_tail_via_ffi() { +fn undostack_push_drops_redoable_tail() { let (trace, mut probes) = setup(); - let mut stack = unsafe { new_stack() }; + let mut stack = new_stack(); let na = CString::new("A").unwrap(); let nb = CString::new("B").unwrap(); - let mut ca = unsafe { make_cmd(&mut probes[0] as *mut Probe) }; - unsafe { oakundo_undostack_push(stack, ca, na.as_ptr()) }; - let mut cb = unsafe { make_cmd(&mut probes[1] as *mut Probe) }; - unsafe { oakundo_undostack_push(stack, cb, nb.as_ptr()) }; + let mut ca = make_cmd(&mut probes[0] as *mut Probe); + undostack_push(stack, ca, na.as_ptr()); + let mut cb = make_cmd(&mut probes[1] as *mut Probe); + undostack_push(stack, cb, nb.as_ptr()); // Undo B, then push C → the redoable tail is dropped. - assert_eq!(unsafe { oakundo_undostack_undo(stack) }, OAKUNDO_OK); + assert_eq!(undostack_undo(stack), OAKUNDO_OK); let mut value: c_int = 0; - assert_eq!( - unsafe { oakundo_undostack_can_redo(stack, &mut value) }, - OAKUNDO_OK - ); + assert_eq!(undostack_can_redo(stack, &mut value), OAKUNDO_OK); assert_eq!(value, 1); let nc = CString::new("C").unwrap(); - let mut cc = unsafe { make_cmd(&mut probes[2] as *mut Probe) }; - assert_eq!( - unsafe { oakundo_undostack_push(stack, cc, nc.as_ptr()) }, - OAKUNDO_OK - ); + let mut cc = make_cmd(&mut probes[2] as *mut Probe); + assert_eq!(undostack_push(stack, cc, nc.as_ptr()), OAKUNDO_OK); - assert_eq!( - unsafe { oakundo_undostack_can_redo(stack, &mut value) }, - OAKUNDO_OK - ); + assert_eq!(undostack_can_redo(stack, &mut value), OAKUNDO_OK); assert_eq!(value, 0, "pushing drops the redoable tail"); assert_eq!( events(&trace), vec!["redo:a", "redo:b", "undo:b", "free:b", "redo:c"] ); - unsafe { oakundo_command_free(&mut ca) }; - unsafe { oakundo_command_free(&mut cb) }; - unsafe { oakundo_command_free(&mut cc) }; - unsafe { oakundo_undostack_free(&mut stack) }; + command_free(&mut ca); + command_free(&mut cb); + command_free(&mut cc); + undostack_free(&mut stack); } diff --git a/src/oakui/ffi.rs b/src/oakui/ffi.rs index a4c69a1d6..3cede6720 100644 --- a/src/oakui/ffi.rs +++ b/src/oakui/ffi.rs @@ -20,16 +20,10 @@ //! ONLY its `oakengine_*` C ABI — it never depends on the `oakengine` crate //! as an rlib. This module declares every exported function the real engine //! binding uses, with the exact signatures from the facade's `#[no_mangle]` -//! exports (`crates/oakengine/src/*.rs`), plus the two `oaktask_*` module -//! exports the dylib carries alongside the facade (interchange -//! load/save getter and the task event subscription, see the comments -//! below). -//! -//! The facade also exports the module C ABIs (`oakundo_*`, `oakcommon_*`, -//! ...) inside the same dylib; the module functions the app needs beyond -//! the facade's wrapping (`oaktask_load_take_project`, -//! `oaktask_task_subscribe`) are declared here too and resolve from the -//! dylib. +//! exports (`crates/oakengine/src/*.rs`). The facade wraps the module +//! C ABIs that are also embedded in the same dylib (`oakundo_*`, +//! `oakcommon_*`, ...), so everything the app touches resolves through an +//! `oakengine_*` symbol. //! //! # Handle layout mirrors //! @@ -125,8 +119,8 @@ pub trait HandleBox: Sized { /// `oakengine_*_free` export. /// /// # Safety -/// The handle must be a live module handle (e.g. from -/// `oaktask_load_take_project`). +/// The handle must be a live module handle (e.g. from a facade export that +/// hands one over for the app to box). pub unsafe fn box_handle(handle: CHandle) -> *mut T { // SAFETY: the caller passes a live handle; the box is managed by the // C ABI consumers from here on. @@ -682,9 +676,9 @@ unsafe extern "C" { buf: *mut c_char, buf_size: c_int, ) -> c_int; - /// `oakaudio_waveform_extract` — real waveform extraction (M12 P4): + /// `oakengine_waveform_extract` — real waveform extraction (M12 P4): /// two-stage min/max extraction of `filename`'s audio stream. - pub fn oakaudio_waveform_extract( + pub fn oakengine_waveform_extract( filename: *const c_char, stream_index: c_int, samples_per_point: c_int, @@ -856,9 +850,9 @@ unsafe extern "C" { ) -> *mut c_void; /// `oakcore_audioparams_free` (NULL no-op). pub fn oakcore_audioparams_free(params: *mut c_void); - /// `oakrender_manager_shutdown` — tear down the render manager + /// `oakengine_render_manager_shutdown` — tear down the render manager /// (test/tooling; the app keeps it for the process lifetime). - pub fn oakrender_manager_shutdown() -> c_int; + pub fn oakengine_render_manager_shutdown() -> c_int; /// `oakengine_testmedia_write_clip` — encode the known test pattern /// into `path` (test/tooling only; M12 P0). pub fn oakengine_testmedia_write_clip( @@ -888,27 +882,28 @@ unsafe extern "C" { /// the channel count (0 = nothing buffered), negative on error. pub fn oakengine_audio_output_levels(peaks: *mut f32, capacity: c_int) -> c_int; - // -- oakrender module C ABI (carried by the dylib) -- + // -- oakengine::render (manager lifecycle) -- - /// `oakrender_manager_init` — bring up the module's process-global - /// render manager. Without it `render_frame` fails with NULL + - /// last_error. The facade does not wrap this; like the `oaktask_*` - /// entries below, the symbol is exported by the dylib itself. Fails - /// (nonzero) when the manager is already initialized. - pub fn oakrender_manager_init() -> c_int; - /// `oakrender_manager_available` — 1 when the render manager is up. - pub fn oakrender_manager_available() -> c_int; + /// `oakengine_render_manager_init` — bring up the module's + /// process-global render manager. Without it `render_frame` fails with + /// NULL + last_error. Fails (nonzero) when the manager is already + /// initialized. + pub fn oakengine_render_manager_init() -> c_int; + /// `oakengine_render_manager_available` — 1 when the render manager is up. + pub fn oakengine_render_manager_available() -> c_int; - // -- oaktask module C ABI (carried by the dylib) -- + // -- oakengine::task (interchange load result + event subscription) -- - /// `oaktask_load_take_project` — take the project an interchange - /// load/load-otio task produced (ownership moves to the caller). - pub fn oaktask_load_take_project(t: CHandle) -> CHandle; - /// `oaktask_task_subscribe` — register the task event callback + /// `oakengine_task_load_take_project` — take the project an + /// interchange load/load-otio task produced (ownership moves to the + /// caller; release with `oakengine_project_free`). NULL when the task + /// is not a load task or has no project yet. + pub fn oakengine_task_load_take_project(task: *mut OakEngineTask) -> *mut OakEngineProject; + /// `oakengine_task_subscribe` — register the task event callback /// (`OAKTASK_EVENT_STARTED`=0, `OAKTASK_EVENT_PROGRESS`=1, /// `OAKTASK_EVENT_FINISHED`=2). - pub fn oaktask_task_subscribe( - t: CHandle, + pub fn oakengine_task_subscribe( + task: *mut OakEngineTask, cb: Option, userdata: *mut c_void, ) -> i64; diff --git a/src/oakui/real.rs b/src/oakui/real.rs index 3d3e9f995..34224f269 100644 --- a/src/oakui/real.rs +++ b/src/oakui/real.rs @@ -130,22 +130,15 @@ const UNTITLED: &str = "Untitled Project"; const PIXEL_FORMAT_F32: c_int = 4; // --------------------------------------------------------------------------- -// oaktask module C ABI entries the facade does not wrap +// Facade task event subscription // --------------------------------------------------------------------------- // -// Two module-level exports are needed here that the facade does not wrap: -// `oaktask_load_take_project` (the loaded-project getter for the -// interchange load task) and `oaktask_task_subscribe` (the task event -// callback that delivers export progress). Both take the module `CHandle`, -// which is the same value handle the facade's own boxes wrap (exposed here -// through [`ffi::unbox`]), and both symbols are exported by the dylib -// itself (it carries the module C ABIs). The declarations live in [`ffi`]; -// this keeps the *operations* on the facade contract while bridging two -// getter/event gaps the facade intentionally leaves open (see -// `oakengine/src/deferred.rs`). +// The export path subscribes to task events through the facade's +// `oakengine_task_subscribe` (wrapping the module's `oaktask_task_subscribe`); +// the callback fires on the task's own thread. -/// The C callback the module task event subscription invokes on the task's -/// own thread. `userdata` is the raw pointer of a leaked +/// The C callback the facade task subscription invokes on the task's own +/// thread. `userdata` is the raw pointer of a leaked /// `mpsc::Sender` the export thread reclaims after the run. unsafe extern "C" fn export_event_cb(event_id: c_int, value: f64, userdata: *mut c_void) { let Some(sender) = (userdata as *const mpsc::Sender).as_ref() else { @@ -708,11 +701,11 @@ impl RealEngine { /// Returns false when the manager could not be started. fn ensure_render_manager() -> bool { unsafe { - if oakrender_manager_available() != 0 { + if oakengine_render_manager_available() != 0 { return true; } - oakrender_manager_init(); - oakrender_manager_available() != 0 + oakengine_render_manager_init(); + oakengine_render_manager_available() != 0 } } @@ -2331,17 +2324,12 @@ impl AppEngine for RealEngine { return Err("failed to create the export task".into()); } - // Progress events through the module task callback. - let module_handle = - unsafe { unbox(task) }.ok_or_else(|| "invalid export task handle".to_string())?; + // Progress events through the facade task subscription (the callback + // is invoked on the task's own thread with the raw userdata pointer). let (tx, rx) = mpsc::channel::(); let cb_userdata = SendPtr(Box::into_raw(Box::new(tx.clone()))); unsafe { - oaktask_task_subscribe( - module_handle, - Some(export_event_cb), - cb_userdata.0 as *mut c_void, - ); + oakengine_task_subscribe(task, Some(export_event_cb), cb_userdata.0 as *mut c_void); } // The task pointer is shared between the cancel handle and the worker @@ -2456,9 +2444,8 @@ impl RealEngine { } /// Opens an `.otio` / `.fcpxml` project through the oaktask interchange - /// loader and adopts the loaded project (the facade exposes no - /// load-result getter, so the module's `oaktask_load_take_project` is - /// called directly — see the module docs). + /// loader and adopts the loaded project (`oakengine_task_load_take_project` + /// hands over the loader's project after a successful run). fn open_interchange(&mut self, path: &PathBuf, cx: &mut Context) -> Result<(), String> { let Some(cpath) = cstr_path(path) else { return Err("invalid project path".into()); @@ -2469,15 +2456,14 @@ impl RealEngine { } let rc = unsafe { oakengine_task_start_sync(task) }; let error = Self::task_error(task); - let module_handle = unsafe { unbox(task) }; - let loaded = module_handle.and_then(|h| { - let project = unsafe { oaktask_load_take_project(h) }; + let loaded = { + let project = unsafe { oakengine_task_load_take_project(task) }; if project.is_null() { None } else { - Some(unsafe { box_handle::(project) }) + Some(project) } - }); + }; unsafe { oakengine_task_free(task) }; if rc == 0 { return Err(format!("failed to load \"{}\": {error}", path.display())); diff --git a/src/oakui/waveform.rs b/src/oakui/waveform.rs index 402c7d61f..18d2304c4 100644 --- a/src/oakui/waveform.rs +++ b/src/oakui/waveform.rs @@ -17,7 +17,7 @@ //! M12 P4: timeline audio waveforms. //! //! The [`WaveformCache`] holds per-clip min/max peak data extracted -//! through `oakaudio_waveform_extract` (the real FFmpeg-backed +//! through `oakengine_waveform_extract` (the real FFmpeg-backed //! extraction); the engine refreshes it when the timeline rebuilds. The //! [`OakClipDecorator`] draws the peaks into the timeline's clip body. //! @@ -100,13 +100,14 @@ impl WaveformCache { } } -/// Extract a clip's waveform (first channel) via the oakaudio C ABI. +/// Extract a clip's waveform (first channel) via the facade's waveform +/// C ABI. fn extract(filename: &str, duration_frames: i64, _fps: f32) -> Option { let cname = std::ffi::CString::new(filename).ok()?; const SAMPLES_PER_POINT: c_int = 256; unsafe { let mut channels: c_int = 0; - let needed = crate::oakui::ffi::oakaudio_waveform_extract( + let needed = crate::oakui::ffi::oakengine_waveform_extract( cname.as_ptr(), 0, // audio-stream index (probe numbering) SAMPLES_PER_POINT, @@ -117,20 +118,27 @@ impl WaveformCache { if needed <= 0 || channels <= 0 { return None; } - let mut peaks = vec![MinMax::default(); needed as usize]; - let rc = crate::oakui::ffi::oakaudio_waveform_extract( + // The extractor's `out_pairs` receives `point_count * + // channel_count` channel-interleaved pairs (capacity is in points), + // so the buffer is sized for the media's channel count. + let channel_count = channels.max(1) as usize; + let mut raw = vec![MinMax::default(); needed as usize * channel_count]; + let rc = crate::oakui::ffi::oakengine_waveform_extract( cname.as_ptr(), 0, SAMPLES_PER_POINT, - peaks.as_mut_ptr(), + raw.as_mut_ptr(), needed, &mut channels, ); if rc < 0 { return None; } + // Keep only the first channel: the pairs are interleaved per point + // (point i channel c at index i * channels + c). let count = rc.min(needed) as usize; - peaks.truncate(count); + let mut peaks = Vec::with_capacity(count); + peaks.extend((0..count).map(|i| raw[i * channel_count])); Some(ClipWaveform { peaks, channel_count: channels.max(1), diff --git a/tests/waveform_e2e.rs b/tests/waveform_e2e.rs index 0418752d7..1bf83cb76 100644 --- a/tests/waveform_e2e.rs +++ b/tests/waveform_e2e.rs @@ -22,7 +22,7 @@ //! waveform test stays isolated from the in-lib media tests. use oakapp::oakui::ffi::{ - oakaudio_waveform_extract, oakengine_testmedia_write_clip, oakapp_minmax, + oakengine_waveform_extract, oakengine_testmedia_write_clip, oakapp_minmax, }; use oakapp::oakui::waveform::{WaveformCache, MinMax};