Files
oak-editor/crates/oaktask/tests/render_real_integration_test.rs
T
Mike-Solar 18ff60f147 feat(engine): clip move, clip effect_input, mandatory static FFmpeg
- oakengine_sequence_move_clip implemented for real (oaktimeline
  TrackMoveBlockCommand; fixes the graph-ownership/gap-anchor/ripple
  trim bugs the stub was hiding); same-track via the frozen C ABI,
  cross-track supported by the module command
- oaknode clip blocks now declare a tex_in texture input and set
  effect_input to it, so timeline clips can host effect chains; facade
  test covers effect insert/remove on a real clip
- oakffmpeg-link: FFMPEG_DIR is now mandatory with a clear panic (a
  Homebrew upgrade left the system ffmpeg .pc pointing at a deleted
  dav1d Cellar path, breaking links); reads a git-ignored workspace
  .env for IDEs that cannot inject env vars (RustRover); links the C++
  stdlib for C++ codec libs (svt-av1)
- oakengine re-exports oaknode so tests share one crate instance;
  it_node uses the direct instance's value type where it calls the
  module FFI (the --workspace dev-dependency feature split builds
  oaknode twice)
2026-08-11 23:04:48 +08:00

168 lines
6.0 KiB
Rust

// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General 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/>.
//! 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<Rational>,
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::<oakrender::texture::Frame>(&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"
);
}