refactor: workspace layout — crates/, app at root, legacy C++ removed
Single mechanical restructure commit: - root Cargo.toml = oakapp bin + workspace; one cargo build produces oakapp, oak-cli, oak-worker, liboakengine.dylib - app/rust/src -> src/ (app at repo root, no rust/ nesting) - src/<mod>/rust -> crates/oak<mod>; src/oakcore-rs -> crates/oakcore; src/bindings/oakotio -> crates/oakotio; src/engine/rust -> crates/oakengine (keeps cdylib+staticlib+rlib) - public C headers include/<mod>/ -> crates/oakengine/include/<mod>/ - OFX SDK headers vendored into crates/oakplugin/ofx/ (HostSupport gone) - legacy deleted: old src/ C++ modules, engine/, core/, ffmpeg_bridge/, app/ (Qt), cli/worker C++, root CMakeLists, third_party/KDDockWidgets submodule, otio-install, all build-* output (~40GB) - oakstorage kept but excluded from the workspace (skeleton w/ todos); gpui excluded (own workspace) - verified: cargo build green, cargo test --workspace 1845/0 (with the documented OCIO_RS_* env override for the homebrew OCIO)
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! `olive::ConformManager` — pcm waveform cache files for fast scrubbing.
|
||||
//!
|
||||
//! Mirrors `src/codec/src/conformmanager.h`. Stateless (NOTES.md): actual
|
||||
//! conform work is delegated to the global task submit callback
|
||||
//! ([`crate::task`]); with no registrar the state queries report
|
||||
//! `Unavailable`. Deterministic per-channel filenames derive from the
|
||||
//! source + target audio params.
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::path::Path;
|
||||
|
||||
/// Conform state of one audio stream.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum ConformState {
|
||||
/// Conform files exist.
|
||||
Exists = 0,
|
||||
/// Conform is being generated.
|
||||
Generating = 1,
|
||||
/// No task registrar; conform unavailable.
|
||||
Unavailable = 2,
|
||||
}
|
||||
|
||||
/// `olive::ConformManager` — stateless conform query/produce manager.
|
||||
pub struct ConformManager;
|
||||
|
||||
impl ConformManager {
|
||||
/// The process-wide ConformManager singleton.
|
||||
pub fn instance() -> &'static ConformManager {
|
||||
static INSTANCE: ConformManager = ConformManager;
|
||||
&INSTANCE
|
||||
}
|
||||
|
||||
/// Query (and when possible start) the conform of one audio stream.
|
||||
///
|
||||
/// `wait != 0` treats a post-submit miss as `Unavailable`; `wait == 0`
|
||||
/// reports it as `Generating`. Without a task registrar the result is
|
||||
/// always `Unavailable`.
|
||||
pub fn get_conform_state(
|
||||
&self,
|
||||
cache_path: &str,
|
||||
source_filename: &str,
|
||||
stream_index: i32,
|
||||
sample_rate: i32,
|
||||
channel_layout: u64,
|
||||
sample_format: i32,
|
||||
wait: bool,
|
||||
) -> crate::error::Result<ConformState> {
|
||||
let filenames = conform_filenames(
|
||||
cache_path,
|
||||
source_filename,
|
||||
stream_index,
|
||||
sample_rate,
|
||||
sample_format,
|
||||
channel_layout,
|
||||
);
|
||||
|
||||
// Return existing conform if it exists.
|
||||
if all_conforms_exist(&filenames) {
|
||||
return Ok(ConformState::Exists);
|
||||
}
|
||||
|
||||
// Interim state (pre-M8): no task system, conform cannot be generated.
|
||||
if !crate::task::task_submit_is_registered() {
|
||||
return Ok(ConformState::Unavailable);
|
||||
}
|
||||
|
||||
// The task owns the ".working" temporary names and the rename to the
|
||||
// final per-channel filenames on success; output_filename carries the
|
||||
// first channel's final path and the task derives the siblings.
|
||||
let req = crate::task::TaskRequest {
|
||||
kind: crate::task::TaskKind::Conform,
|
||||
input_filename: source_filename,
|
||||
output_filename: filenames.first().map(String::as_str).unwrap_or(""),
|
||||
stream_index,
|
||||
sample_rate,
|
||||
channel_layout,
|
||||
sample_format,
|
||||
proxy_width: 0,
|
||||
proxy_height: 0,
|
||||
};
|
||||
|
||||
// Interim simplification: submission is synchronous — we always wait
|
||||
// for the submit to return, regardless of `wait`.
|
||||
if crate::task::submit_task(&req).is_err() {
|
||||
return Ok(ConformState::Unavailable);
|
||||
}
|
||||
|
||||
if all_conforms_exist(&filenames) {
|
||||
return Ok(ConformState::Exists);
|
||||
}
|
||||
|
||||
if wait {
|
||||
// Synchronous wait already happened and the conform still does not
|
||||
// exist: report the wait as failed.
|
||||
return Ok(ConformState::Unavailable);
|
||||
}
|
||||
|
||||
Ok(ConformState::Generating)
|
||||
}
|
||||
|
||||
/// Number of conform (pcm) files for the given stream/params — one per
|
||||
/// channel; 0 on invalid arguments.
|
||||
pub fn get_conform_filename_count(
|
||||
&self,
|
||||
_cache_path: &str,
|
||||
_source_filename: &str,
|
||||
_stream_index: i32,
|
||||
_sample_rate: i32,
|
||||
channel_layout: u64,
|
||||
_sample_format: i32,
|
||||
) -> usize {
|
||||
channel_layout.count_ones() as usize
|
||||
}
|
||||
|
||||
/// The `index`-th conform filename.
|
||||
pub fn get_conform_filename(
|
||||
&self,
|
||||
cache_path: &str,
|
||||
source_filename: &str,
|
||||
stream_index: i32,
|
||||
sample_rate: i32,
|
||||
channel_layout: u64,
|
||||
sample_format: i32,
|
||||
index: usize,
|
||||
) -> crate::error::Result<String> {
|
||||
let filenames = conform_filenames(
|
||||
cache_path,
|
||||
source_filename,
|
||||
stream_index,
|
||||
sample_rate,
|
||||
sample_format,
|
||||
channel_layout,
|
||||
);
|
||||
filenames
|
||||
.get(index)
|
||||
.cloned()
|
||||
.ok_or(crate::error::Error::NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
/// Deterministic conform base name plus per-channel pcm filenames, mirroring
|
||||
/// `ConformManager::get_conformed_filename`: one file per channel under
|
||||
/// `cache_path`, named `<identifier>-<stream>.<rate>.<format>.<layout>.<i>.pcm`.
|
||||
fn conform_filenames(
|
||||
cache_path: &str,
|
||||
source_filename: &str,
|
||||
stream_index: i32,
|
||||
sample_rate: i32,
|
||||
sample_format: i32,
|
||||
channel_layout: u64,
|
||||
) -> Vec<String> {
|
||||
let count = channel_layout.count_ones() as usize;
|
||||
let base = format!(
|
||||
"{}-{}.{}.{}.{}",
|
||||
unique_file_identifier(source_filename),
|
||||
stream_index,
|
||||
sample_rate,
|
||||
sample_format,
|
||||
channel_layout,
|
||||
);
|
||||
|
||||
let mut out = Vec::with_capacity(count);
|
||||
for i in 0..count {
|
||||
let p = Path::new(cache_path).join(format!("{}.{}.pcm", base, i));
|
||||
out.push(p.to_string_lossy().into_owned());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper (the bridge
|
||||
/// returns a 64-bit id directly).
|
||||
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)
|
||||
}
|
||||
|
||||
/// True when every conform filename already exists on disk.
|
||||
fn all_conforms_exist(filenames: &[String]) -> bool {
|
||||
filenames.iter().all(|f| Path::new(f).exists())
|
||||
}
|
||||
|
||||
/// Shared test support: serializes access to the global task-submit registry
|
||||
/// (unit tests run in parallel and would otherwise clear each other's
|
||||
/// registration) and provides a callback that accepts any task.
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test_util {
|
||||
use crate::error::OAKCODEC_OK;
|
||||
use crate::task::OakCodecTaskRequest;
|
||||
|
||||
/// Serializes every test that mutates the task-submit registry.
|
||||
pub static REG_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
/// A task-submit callback that accepts every request (no-op).
|
||||
pub unsafe extern "C" fn accept_cb(
|
||||
_req: *const OakCodecTaskRequest,
|
||||
_ud: *mut std::ffi::c_void,
|
||||
) -> i32 {
|
||||
OAKCODEC_OK
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn temp_subdir(name: &str) -> String {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"oakcodec_conform_{}_{}",
|
||||
name,
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
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 test stub computes an FNV-1a-64 of the path bytes.
|
||||
let expected = format!("{}", fnv1a64(b"media.mp4") as i64);
|
||||
assert_eq!(unique_file_identifier("media.mp4"), expected);
|
||||
// Deterministic: same input, same id.
|
||||
assert_eq!(
|
||||
unique_file_identifier("media.mp4"),
|
||||
unique_file_identifier("media.mp4")
|
||||
);
|
||||
// Different input, different id.
|
||||
assert_ne!(
|
||||
unique_file_identifier("media.mp4"),
|
||||
unique_file_identifier("other.mp4")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filename_count_from_channel_layout() {
|
||||
let m = ConformManager::instance();
|
||||
assert_eq!(m.get_conform_filename_count("c", "s", 0, 48000, 0x3, 0), 2); // stereo
|
||||
assert_eq!(m.get_conform_filename_count("c", "s", 0, 48000, 0x4, 0), 1); // mono
|
||||
assert_eq!(m.get_conform_filename_count("c", "s", 0, 48000, 0, 0), 0); // invalid
|
||||
assert_eq!(
|
||||
m.get_conform_filename_count("c", "s", 0, 48000, 0x60F, 0),
|
||||
6
|
||||
); // 5.1
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conform_filename_derivation_and_range() {
|
||||
let m = ConformManager::instance();
|
||||
let cache = temp_subdir("names");
|
||||
let id = fnv1a64(b"media.mp4") as i64;
|
||||
let base = format!("{}-0.48000.0.3", id);
|
||||
let f0 = m
|
||||
.get_conform_filename(&cache, "media.mp4", 0, 48000, 0x3, 0, 0)
|
||||
.unwrap();
|
||||
let f1 = m
|
||||
.get_conform_filename(&cache, "media.mp4", 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),
|
||||
Err(crate::error::Error::NotFound)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_conform_state_unavailable_without_registrar() {
|
||||
let _g = super::test_util::REG_LOCK.lock().unwrap();
|
||||
// Ensure no registrar is left over.
|
||||
crate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());
|
||||
let cache = temp_subdir("unavail");
|
||||
let s = ConformManager::instance()
|
||||
.get_conform_state(&cache, "missing.mp4", 0, 48000, 0x3, 0, false)
|
||||
.unwrap();
|
||||
assert_eq!(s, ConformState::Unavailable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_conform_state_exists_when_files_present() {
|
||||
let cache = temp_subdir("exists");
|
||||
let m = ConformManager::instance();
|
||||
for i in 0..2 {
|
||||
let f = m
|
||||
.get_conform_filename(&cache, "media.mp4", 0, 48000, 0x3, 0, i)
|
||||
.unwrap();
|
||||
std::fs::write(&f, b"pcm").unwrap();
|
||||
}
|
||||
let s = m
|
||||
.get_conform_state(&cache, "media.mp4", 0, 48000, 0x3, 0, false)
|
||||
.unwrap();
|
||||
assert_eq!(s, ConformState::Exists);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_conform_state_generating_when_registered() {
|
||||
let _g = super::test_util::REG_LOCK.lock().unwrap();
|
||||
crate::task::set_task_submit_cb_extern(
|
||||
Some(super::test_util::accept_cb),
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
let cache = temp_subdir("generating");
|
||||
let s = ConformManager::instance()
|
||||
.get_conform_state(&cache, "missing.mp4", 0, 48000, 0x3, 0, false)
|
||||
.unwrap();
|
||||
crate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());
|
||||
assert_eq!(s, ConformState::Generating);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user