diff --git a/Cargo.toml b/Cargo.toml index f94c2a1b3..39df11309 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,6 +125,9 @@ image = "0.25" # `icons/icon.png` is NOT committed: it is generated from `Oak_Icon.svg` # (rsvg-convert) in CD right before packaging, because cargo-packager needs a # bitmap (it converts a square PNG into .icns / .ico / hicolor PNGs itself). +# It must be 512x512: cargo-packager's tauri-icns 0.1.0 maps only 512x512@1x +# (and 1024x1024@2x) to an ICNS type — a plain 1024x1024 PNG aborts with +# "No matching IconType". # Keep the path here in sync with the CD workflow. # --------------------------------------------------------------------------- [package.metadata.packager] diff --git a/build.rs b/build.rs index 4370ec662..d95f4b192 100644 --- a/build.rs +++ b/build.rs @@ -36,15 +36,18 @@ //! `target//deps/`, so dyld finds it by that absolute path at //! load time; the `-rpath` flag covers `@rpath`-relative configurations. //! -//! Linux: undefined symbols in a `.so` need no link-time flag; the app's -//! own link only needs the search path plus `-Wl,--export-dynamic` (the -//! ELF equivalent of `-export_dynamic`) so the host symbols resolve from -//! the binary at runtime. An `$ORIGIN`-relative rpath lets a packaged -//! binary find a sibling `liboakengine.so`. +//! Linux: the app's own link needs the search path plus +//! `-Wl,--export-dynamic` (the ELF equivalent of `-export_dynamic`) so +//! process-global symbol lookups resolve from the binary at runtime. An +//! `$ORIGIN`-relative rpath lets a packaged binary find a sibling +//! `liboakengine.so`. //! -//! Windows: NOT SUPPORTED YET — a DLL cannot carry the undefined -//! `oakcore_*` imports (they need a stub import library or delay-load -//! plumbing that does not exist yet), so the app does not link there. +//! Windows: the engine dylib now links there — the `oakcore_*` runtime +//! imports were folded into the cdylib in M12 P5 (crates/oakengine/src/ +//! stubs.rs, module `audio`), so `liboakengine.dll` carries no undefined +//! symbols. The app binary itself still has no Windows link +//! configuration here and the early return stays; that is a separate +//! effort (gpui win32 support). use std::path::PathBuf; diff --git a/crates/oakengine/Cargo.toml b/crates/oakengine/Cargo.toml index ed3842c1a..29baaa38f 100644 --- a/crates/oakengine/Cargo.toml +++ b/crates/oakengine/Cargo.toml @@ -29,9 +29,10 @@ libc = "0.2" # oakaudio/oakplugin, and clearly marked STUBs for the handle-based # oaknode/oaktimeline paths). # -# 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). +# The `oakcore_audioparams_*` accessors the audio paths read through are +# implemented inside the dylib too (src/stubs.rs, module `audio`, M12 P5): +# they used to be C++ liboakcore host-provided symbols left as runtime +# lookups, which blocked Windows DLL linking. # # src/linkage.rs anchors every crate (and oakcore-rs) so the linker pulls # their object files into the cdylib. diff --git a/crates/oakengine/README.md b/crates/oakengine/README.md index c6d25d371..64fc68ad8 100644 --- a/crates/oakengine/README.md +++ b/crates/oakengine/README.md @@ -44,10 +44,11 @@ anchors them so their `#[no_mangle]` exports are linked into the `liboakengine` cdylib — the dylib carries the module C ABIs (oakundo_*, oakcommon_*, oaktimeline_*, oakcodec_*, oakaudio_*, oakrender_*, oaktask_*, oakplugin_*, oaknode_*) next to the facade's oakengine_* -exports. The only remaining imports are the C++ host symbols -(`oakcore_*` from liboakcore, `fb_*` from ffmpeg_bridge), which -`build.rs` leaves as runtime lookups (macOS `-undefined dynamic_lookup`) -resolved from the host Oak process. +exports. The `oakcore_audioparams_*` accessors the audio paths read +through are implemented in the dylib too (src/stubs.rs, module `audio`, +M12 P5) — they used to be C++ host symbols left as runtime lookups +(macOS `-undefined dynamic_lookup`); the cdylib now carries no undefined +imports. ### Handle mapping @@ -103,8 +104,8 @@ binaries keep the in-crate mocks (ffmpeg_bridge stub / render mocks): oaknode/oakundo/oakcommon C ABI symbols as link-time externs, which the sibling crate rlibs provide; oaknode itself resolves cross-module symbols at runtime with `dlsym(RTLD_DEFAULT)`. -- `tests/common/mod.rs` defines the `oakcore_*` (liboakcore) and `fb_*` - (libffmpeg_bridge) symbols the oakcodec/oakaudio rlibs reference, and +- `tests/common/mod.rs` re-exports the facade's in-dylib + `oakcore_audioparams_*` accessors (M12 P5 folded them in) and force-links the oakcommon XML writer/reader + the oakundo command factory so the oaknode serializer's dlsym lookups resolve in every test binary. @@ -120,8 +121,8 @@ the module crates' real implementations. ``` 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) +cargo build # cdylib embeds the module C ABIs + the folded-in + # oakcore_audioparams_* accessors (no undefined imports) ``` ## FFI discipline diff --git a/crates/oakengine/build.rs b/crates/oakengine/build.rs index 195e0dd49..9294715dd 100644 --- a/crates/oakengine/build.rs +++ b/crates/oakengine/build.rs @@ -16,39 +16,41 @@ //! Build-time link configuration for the `liboakengine` cdylib. //! -//! The dylib now carries the module C ABIs itself (oakundo_*, oakcommon_*, -//! ... — see Cargo.toml), so the only remaining undefined imports are the -//! C++ host-provided symbols the modules call directly: `oakcore_*` -//! (liboakcore's `oakcore_audioparams_*` / `oakcore_rational_*`, called by -//! oakcodec) and `fb_find_best_pix_fmt_of_list` (ffmpeg_bridge, called by -//! oakcommon's pixel-format helper). Those live in the host Oak process, -//! which loads this dylib, so macOS `ld` must accept them as runtime -//! lookups instead of link-time errors. Only the cdylib gets this flag — -//! the rlib/staticlib (and the worker/cli consumers) are unaffected. +//! The dylib carries the module C ABIs itself (oakundo_*, oakcommon_*, ... +//! — see Cargo.toml). The `oakcore_audioparams_*` accessors the audio +//! paths read through used to be host-provided C++ liboakcore symbols, +//! left as runtime lookups via `-undefined,dynamic_lookup`; M12 P5 +//! implemented them inside the dylib (src/stubs.rs, module `audio`), so +//! no undefined imports remain except system frameworks/libc++, and the +//! cdylib links on every platform (Windows DLLs reject undefined symbols, +//! which was the blocker). fn main() { - if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") { - println!("cargo:rustc-cdylib-link-arg=-Wl,-undefined,dynamic_lookup"); + let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + if os == "macos" { // The static FFmpeg's transitive system deps (libz etc.) are // recorded as `@rpath/libz.1.dylib`; the dylib itself carries the // rpath so standalone binaries (and the packaged app) resolve // them without extra host rpaths. println!("cargo:rustc-cdylib-link-arg=-Wl,-rpath,/usr/lib"); } - // The dlsym codec bridge (M12 P0) resolves `oakcodec_*` from the - // process-global scope; the engine's unit-test binary (the former - // integration tests live in src/test_support/) statically links the - // module crates, so their symbols must be exported from the test - // executable. `cargo:rustc-link-arg-tests` is NOT usable: the facade - // is cdylib-only, so cargo reports "does not have a test target" for - // that directive — use the generic `rustc-link-arg` (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. + if os == "macos" || os == "linux" { + // The dlsym codec bridge (M12 P0) resolves `oakcodec_*` from the + // process-global scope; the engine's unit-test binary (the former + // integration tests live in src/test_support/) statically links the + // module crates, so their symbols must be exported from the test + // executable. `cargo:rustc-link-arg-tests` is NOT usable: the facade + // is cdylib-only, so cargo reports "does not have a test target" for + // that directive — use the generic `rustc-link-arg` (a no-op for the + // cdylib link itself, and not emitted on Windows where the flag is + // meaningless and would break the DLL link). + println!("cargo:rustc-link-arg=-Wl,-export_dynamic"); + } + if os == "macos" { + // The bundled OpenColorIO's macOS system monitor references + // IOKit / ColorSync / CoreGraphics display APIs; link them for + // the cdylib link and the unit-test binary (which statically + // pulls the same OCIO rlib). for fw in ["IOKit", "ColorSync", "CoreGraphics"] { println!("cargo:rustc-link-arg=-framework"); println!("cargo:rustc-link-arg={fw}"); diff --git a/crates/oakengine/src/audio.rs b/crates/oakengine/src/audio.rs index 037881834..fb1c9e9ed 100644 --- a/crates/oakengine/src/audio.rs +++ b/crates/oakengine/src/audio.rs @@ -21,7 +21,9 @@ //! through [`manager()`] (a borrowed handle; empty when no instance //! exists — engine semantics then report `paNoDevice`/error as //! documented). The borrowed `OakAudioParams*` handles are read through -//! the liboakcore `oakcore_audioparams_*` accessors. +//! the `oakcore_audioparams_*` accessors the facade provides itself +//! (crate::stubs::audio, folded in M12 P5 — they used to be host-provided +//! liboakcore symbols). use std::ffi::{c_char, c_double, c_int, c_void}; @@ -126,6 +128,58 @@ pub extern "C" fn oakengine_audio_set_input_device(device: i64) -> c_int { }) } +/// `oakengine_audio_output_device_count` — the number of host output +/// devices; the list index is the device index +/// `oakengine_audio_set_output_device` takes. Needs no AudioManager +/// instance. +#[no_mangle] +pub extern "C" fn oakengine_audio_output_device_count() -> c_int { + guard_int(|| Ok(unsafe { a::oakaudio_output_device_count() })) +} + +/// `oakengine_audio_input_device_count` — the input side of +/// [`oakengine_audio_output_device_count`]. +#[no_mangle] +pub extern "C" fn oakengine_audio_input_device_count() -> c_int { + guard_int(|| Ok(unsafe { a::oakaudio_input_device_count() })) +} + +/// `oakengine_audio_output_device_name` — the name of output device +/// `index` (buf/size; the length excludes the NUL). +#[no_mangle] +pub unsafe extern "C" fn oakengine_audio_output_device_name( + index: c_int, + buf: *mut c_char, + buf_size: c_int, +) -> c_int { + guard_int(|| unsafe { + let rc = a::oakaudio_output_device_name(index, buf, buf_size); + if rc < 0 { + Err(Error::Module(rc)) + } else { + Ok(crate::handle::string_result(rc)) + } + }) +} + +/// `oakengine_audio_input_device_name` — the input side of +/// [`oakengine_audio_output_device_name`]. +#[no_mangle] +pub unsafe extern "C" fn oakengine_audio_input_device_name( + index: c_int, + buf: *mut c_char, + buf_size: c_int, +) -> c_int { + guard_int(|| unsafe { + let rc = a::oakaudio_input_device_name(index, buf, buf_size); + if rc < 0 { + Err(Error::Module(rc)) + } else { + Ok(crate::handle::string_result(rc)) + } + }) +} + /// `oakengine_audio_hard_reset` — re-initialize PortAudio and refresh the /// device lists. #[no_mangle] diff --git a/crates/oakengine/src/stubs.rs b/crates/oakengine/src/stubs.rs index 79436f6d1..41d3cafdd 100644 --- a/crates/oakengine/src/stubs.rs +++ b/crates/oakengine/src/stubs.rs @@ -3944,7 +3944,7 @@ pub mod node { if params.is_null() { return OAKNODE_E_INVALID; } - // SAFETY: the host liboakcore audioparams contract. + // SAFETY: the oakcore audioparams contract. let (sample_rate, channel_layout, format) = unsafe { ( crate::stubs::audio::oakcore_audioparams_sample_rate(params), @@ -6064,7 +6064,7 @@ pub mod node { Some(p) => *p, None => return OAKNODE_E_NOT_FOUND, }; - // SAFETY: the host liboakcore audioparams contract. + // SAFETY: the oakcore audioparams contract. let ptr = unsafe { crate::stubs::audio::oakcore_audioparams_create( params.sample_rate, @@ -6089,7 +6089,7 @@ pub mod node { if params.is_null() || index < 0 { return OAKNODE_E_INVALID; } - // SAFETY: the host liboakcore audioparams contract. + // SAFETY: the oakcore audioparams contract. let converted = unsafe { oaknode::value::AudioParams { sample_rate: crate::stubs::audio::oakcore_audioparams_sample_rate(params), @@ -11291,9 +11291,11 @@ pub mod timeline { /// 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. +/// callback. The `oakcore_audioparams_*` C ABI (opaque `AudioParams` +/// handles in the frozen engine contract) is implemented here, inside the +/// dylib — the accessors were host-provided (C++ liboakcore, later Rust +/// mock shims in the app/cli/test binaries) until M12 P5 folded them in +/// so the cdylib carries no undefined imports. pub mod audio { use std::ffi::{c_char, c_double, c_int, c_void, CStr}; @@ -11315,23 +11317,116 @@ pub mod audio { 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; + /// The liboakcore `AudioParams` object layout + /// (`oakcore_audioparams.h`): sample rate, ffmpeg channel-layout mask, + /// sample format, stream index, duration and time base. Mirrors + /// `olive::core::AudioParams` (and the app/cli mock shims it replaces), + /// so the engine contract's borrowed/owned `AudioParams*` handles + /// round-trip unchanged. + #[repr(C)] + struct HostAudioParams { + sample_rate: c_int, + channel_layout: u64, + format: c_int, + stream_index: c_int, + duration: i64, + time_base_num: c_int, + time_base_den: c_int, + } + + /// `oakcore_audioparams_create` — new owned audio params (release with + /// `oakcore_audioparams_free`). The time base defaults to 1/sample_rate, + /// exactly like the liboakcore constructor. + #[no_mangle] + pub extern "C" fn oakcore_audioparams_create( + sample_rate: c_int, + channel_layout: u64, + format: c_int, + ) -> *mut c_void { + let den = if sample_rate > 0 { sample_rate } else { 1 }; + Box::into_raw(Box::new(HostAudioParams { + sample_rate, + channel_layout, + format, + stream_index: 0, + duration: 0, + time_base_num: 1, + time_base_den: den, + })) as *mut c_void + } + + /// `oakcore_audioparams_free` — NULL no-op. + /// + /// # Safety + /// `params` must come from [`oakcore_audioparams_create`] or be NULL. + #[no_mangle] + pub extern "C" fn oakcore_audioparams_free(params: *mut c_void) { + if params.is_null() { + return; + } + // SAFETY: produced by `oakcore_audioparams_create`; we hold the only + // reference after the box is dropped. + unsafe { drop(Box::from_raw(params as *mut HostAudioParams)) }; + } + + /// `oakcore_audioparams_sample_rate` — 0 for NULL (liboakcore contract). + /// + /// # Safety + /// `params` must come from [`oakcore_audioparams_create`] or be NULL. + #[no_mangle] + pub extern "C" fn oakcore_audioparams_sample_rate(params: *const c_void) -> c_int { + if params.is_null() { + return 0; + } + // SAFETY: contract above. + unsafe { (*(params as *const HostAudioParams)).sample_rate } + } + + /// `oakcore_audioparams_channel_layout` — 0 for NULL. + /// + /// # Safety + /// `params` must come from [`oakcore_audioparams_create`] or be NULL. + #[no_mangle] + pub extern "C" fn oakcore_audioparams_channel_layout(params: *const c_void) -> u64 { + if params.is_null() { + return 0; + } + // SAFETY: contract above. + unsafe { (*(params as *const HostAudioParams)).channel_layout } + } + + /// `oakcore_audioparams_format` — 0 for NULL. + /// + /// # Safety + /// `params` must come from [`oakcore_audioparams_create`] or be NULL. + #[no_mangle] + pub extern "C" fn oakcore_audioparams_format(params: *const c_void) -> c_int { + if params.is_null() { + return 0; + } + // SAFETY: contract above. + unsafe { (*(params as *const HostAudioParams)).format } + } + + /// `oakcore_audioparams_set_time_base` — NULL no-op. + /// + /// # Safety + /// `params` must come from [`oakcore_audioparams_create`] or be NULL. + #[no_mangle] + pub extern "C" fn oakcore_audioparams_set_time_base( + params: *mut c_void, + num: c_int, + den: c_int, + ) { + if params.is_null() { + return; + } + // SAFETY: contract above. + unsafe { + let p = &mut *(params as *mut HostAudioParams); + p.time_base_num = num; + p.time_base_den = den; + } } /// Map an oakaudio `Box` result to its public module code @@ -11546,6 +11641,67 @@ pub mod audio { } } + /// The number of host output devices (enumeration order == the device + /// index `set_output_device` takes). Needs no manager instance. + pub fn oakaudio_output_device_count() -> c_int { + oakaudio::manager::output_device_names().len() as c_int + } + + /// The number of host input devices (see [`oakaudio_output_device_count`]). + pub fn oakaudio_input_device_count() -> c_int { + oakaudio::manager::input_device_names().len() as c_int + } + + /// The name of output device `index` (two-stage buf/size; reports the + /// required size INCLUDING the NUL, the module convention the facade + /// converts with `string_result`). + /// + /// # Safety + /// `buf` must point to `buf_size` writable bytes when non-NULL. + pub unsafe fn oakaudio_output_device_name( + index: c_int, + buf: *mut c_char, + buf_size: c_int, + ) -> c_int { + unsafe { device_name_result(oakaudio::manager::output_device_names(), index, buf, buf_size) } + } + + /// The name of input device `index` (see [`oakaudio_output_device_name`]). + /// + /// # Safety + /// `buf` must point to `buf_size` writable bytes when non-NULL. + pub unsafe fn oakaudio_input_device_name( + index: c_int, + buf: *mut c_char, + buf_size: c_int, + ) -> c_int { + unsafe { device_name_result(oakaudio::manager::input_device_names(), index, buf, buf_size) } + } + + /// Shared two-stage string write for the device-name getters: reports + /// `len + 1` (the required buffer size including the NUL), writes the + /// NUL-terminated name when it fits. `OAKAUDIO_E_NOT_FOUND` for an + /// out-of-range index. + unsafe fn device_name_result( + names: Vec, + index: c_int, + buf: *mut c_char, + buf_size: c_int, + ) -> c_int { + let Some(name) = names.get(index.max(0) as usize).filter(|_| index >= 0) else { + return OAKAUDIO_E_NOT_FOUND; + }; + // SAFETY: `buf` points to `buf_size` writable bytes when non-NULL. + unsafe { + if !buf.is_null() && buf_size > 0 { + let copy = name.len().min((buf_size as usize).saturating_sub(1)); + std::ptr::copy_nonoverlapping(name.as_ptr(), buf as *mut u8, copy); + *buf.add(copy) = 0; + } + } + name.len() as c_int + 1 + } + /// Close the output stream and reset the playback state. pub fn oakaudio_manager_hard_reset(_self: CHandle) -> c_int { match with_manager(_self).and_then(|mut m| m.hard_reset().map_err(|e| audio_code(&*e))) { @@ -12413,13 +12569,13 @@ pub mod render { let sample_rate = if params.is_null() { 48000 } else { - // SAFETY: the host liboakcore audioparams contract. + // SAFETY: the oakcore audioparams contract. unsafe { crate::stubs::audio::oakcore_audioparams_sample_rate(params) } }; let channel_layout = if params.is_null() { 0x3 } else { - // SAFETY: the host liboakcore audioparams contract. + // SAFETY: the oakcore audioparams contract. unsafe { crate::stubs::audio::oakcore_audioparams_channel_layout(params) } }; let audio_params = oakrender::ticket::AudioTicketParams { diff --git a/crates/oakengine/src/test_support/common/mod.rs b/crates/oakengine/src/test_support/common/mod.rs index b0d7ad602..272899081 100644 --- a/crates/oakengine/src/test_support/common/mod.rs +++ b/crates/oakengine/src/test_support/common/mod.rs @@ -25,19 +25,16 @@ //! would otherwise drop the dev-dependency rlibs from the link and //! leave the imports undefined. //! -//! 2. **Provide the `oakcore_*` symbols** that the -//! oakcodec rlib references: `oakcore_audioparams_*` / -//! `oakcore_rational_*` live in the C++ liboakcore (only linked in the -//! real build), so cargo tests define minimal in-memory mocks — the -//! same mock the oakcodec crate itself compiles under `#[cfg(test)]` -//! (src/bridge/test_stubs.rs). The real dylib behavior is required -//! for actual media decode; those facade tests are `#[ignore]`. +//! 2. **Re-export the folded-in `oakcore_audioparams_*` accessors** for +//! the former mock call sites (`common::oakcore_audioparams_*`). The +//! facade used to leave those symbols as runtime lookups for a C++ +//! liboakcore host, and the tests defined in-memory mocks; M12 P5 +//! implemented them inside the dylib (crates/oakengine/src/stubs.rs, +//! module `audio`), so the tests just use those implementations. #![allow(dead_code, unused_variables)] -use std::collections::HashMap; -use std::ffi::{c_int, c_void}; -use std::sync::{Mutex, OnceLock}; +use std::sync::Mutex; /// One public direct-Rust symbol per module crate (the module C ABIs are /// deleted; this mirrors the anchors in `crates/oakengine/src/linkage.rs`). @@ -106,201 +103,14 @@ pub fn storage_off_guard() -> std::sync::MutexGuard<'static, ()> { } // --------------------------------------------------------------------------- -// oakcore_* stubs (see module docs) +// oakcore_audioparams_* (see module docs) // --------------------------------------------------------------------------- -/// Opaque `OakAudioParams` handle type (the real one lives in liboakcore). -#[repr(C)] -pub struct OakAudioParams { - _opaque: [u8; 0], -} - -/// Per-`OakAudioParams` backing state. -#[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() -} - -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); - } -} - -/// 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 { - 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: produced by `oakcore_audioparams_create`; 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 -} - -#[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: 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))) }; -} +/// The facade's in-dylib `oakcore_audioparams_*` C ABI (see +/// `crate::stubs::audio`): the accessors were host-provided mocks until +/// M12 P5 folded them into the engine, so the tests now share the real +/// implementations instead of defining per-binary duplicates. Only the +/// two entry points the test files call (`create`/`free`) are re-exported; +/// the read accessors are reached through `crate::stubs::audio` where the +/// tests need them. +pub use crate::stubs::audio::{oakcore_audioparams_create, oakcore_audioparams_free}; diff --git a/crates/oakengine/src/test_support/it_audio.rs b/crates/oakengine/src/test_support/it_audio.rs index d7b30eebb..4d9d70f77 100644 --- a/crates/oakengine/src/test_support/it_audio.rs +++ b/crates/oakengine/src/test_support/it_audio.rs @@ -108,9 +108,10 @@ fn alive() -> c_int { crate::stubs::audio::oakaudio_debug_alive_count() } -/// A borrowed `OakAudioParams*` mock handle (tests/common/mod.rs provides -/// the `oakcore_audioparams_*` accessors the facade reads through). -fn audio_params(rate: c_int, layout: u64, format: c_int) -> *mut common::OakAudioParams { +/// A borrowed `OakAudioParams*` handle created through the facade's +/// in-dylib `oakcore_audioparams_*` accessors (tests/common/mod.rs +/// re-exports them; see `crate::stubs::audio`). +fn audio_params(rate: c_int, layout: u64, format: c_int) -> *mut c_void { common::oakcore_audioparams_create(rate, layout, format) } diff --git a/crates/oakengine/src/test_support/it_export.rs b/crates/oakengine/src/test_support/it_export.rs index 243fb3307..525ac878a 100644 --- a/crates/oakengine/src/test_support/it_export.rs +++ b/crates/oakengine/src/test_support/it_export.rs @@ -20,8 +20,9 @@ //! //! 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 +//! module crates (the `oakcore_audioparams_*` accessors the facade +//! reads through are its own in-dylib implementations, re-exported by +//! `tests/common`); the output file is a REAL mp4 written //! by the statically linked FFmpeg (oakcodec encoder), asserted by //! its `ftyp` box; //! 2. every exporter-family export is exercised on a legal path with the diff --git a/crates/oakengine/src/test_support/it_task.rs b/crates/oakengine/src/test_support/it_task.rs index bd63ef104..205a7cb59 100644 --- a/crates/oakengine/src/test_support/it_task.rs +++ b/crates/oakengine/src/test_support/it_task.rs @@ -19,9 +19,10 @@ //! //! Coverage rules (see the family test charter): //! 1. no mocks — every call goes through the real facade into the real -//! oaktask/oaknode/oakundo/oakcodec module crates (the only stubs are -//! the host-provided `oakcore_*` symbols in `tests/common`, -//! the same mechanism the other family tests use); +//! oaktask/oaknode/oakundo/oakcodec module crates (the +//! `oakcore_audioparams_*` accessors the facade reads through are its +//! own in-dylib implementations, re-exported by `tests/common`, the +//! same mechanism the other family tests use); //! 2. every one of the 27 `oakengine_task_*` / `oakengine_cli_task_*` //! exports is exercised on a legal path with the result asserted; //! 3. legal-input matrix (compression flags, url counts, indices, buffer diff --git a/crates/oakengine/src/test_support/it_timeline.rs b/crates/oakengine/src/test_support/it_timeline.rs index 3119b9a30..af8e6bdbf 100644 --- a/crates/oakengine/src/test_support/it_timeline.rs +++ b/crates/oakengine/src/test_support/it_timeline.rs @@ -21,8 +21,9 @@ //! undoable). 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` (no media is decoded, so no FFmpeg); +//! module crates (the `oakcore_audioparams_*` accessors the facade +//! reads through are its own in-dylib implementations, re-exported by +//! `tests/common`; no media is decoded, so no FFmpeg); //! 2. every export under test is exercised on a legal path with the //! result asserted; //! 3. illegal inputs (NULL seq, bad track types, out-of-range indices, diff --git a/crates/oakengine/src/test_support/mod.rs b/crates/oakengine/src/test_support/mod.rs index 7bd216244..731705aea 100644 --- a/crates/oakengine/src/test_support/mod.rs +++ b/crates/oakengine/src/test_support/mod.rs @@ -21,8 +21,8 @@ //! 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. +//! is replaced by the single [`common`] declaration below — its +//! `oakcore_audioparams_*` re-exports may exist only once per binary. //! //! The node/timeline/render-graph families (and the graph-op tests that //! built fixtures through the deleted handle-based module C ABIs) are