Files
oak-editor/crates/oak-task/src/task.rs
T
Mike-Solar 4babbf5de8
CI / Build & test (Linux) (push) Successful in 24m6s
CI / Build & test (Windows) (push) Successful in 31m14s
core: merge oak-common into oak-core
oak-common is gone; its modules (configstore, xmlutils, ocioutils,
oiioutils, colormath, colortransform, videoparams, ffmpegutils, ...)
now live in oak-core alongside the value types. The render value/GPU
types moved too: backend (wgpu context + DisplayRenderer), color
(ColorProcessor over ocio-rs), texture, frame, and the commonutil
config helpers.

Fix-ups to make the merged tree build and pass tests:

- oak-core Cargo.toml: wgpu back to 25 (the moved backend code is
  written against that API generation); add the toml/quick-xml/image
  deps oak-common carried.
- lib.rs: drop the duplicate 'pub mod error;'.
- error.rs: unified OAKCORE_* codes; restore Error::new() and
  From<OcioError> from oak-common's error type.
- backend.rs/color.rs: oak_core::/oak_render:: self-references
  rewritten to crate::; the shaderfx-dependent GPU effect test moved
  to oak-render's shaderfx tests (shaderfx depends on oak-node and
  cannot live in oak-core).
- oak-render's error module re-exports oak_core::error::{Error,
  Result}; the OAKRENDER_* codes stay as the public-code contract.
- oak-node jobs.rs: ColorProcessor imported from oak_core::color.
- Integration tests repointed at oak_core::{texture, frame, backend,
  color, colormath}.
- the display-ICC regression test treats an empty OAK_DISPLAY_ICC as
  unset, matching displayicc::env_override_icc.
2026-09-03 17:42:20 +08:00

321 lines
10 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/>.
//! The `Task` base class, mirroring `src/task/src/task.h` (`olive::Task`).
//!
//! The C++ class is abstract with a protected virtual `run()`. In Rust each
//! 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 shared
//! `oak_core::cancelatom::CancelAtom` (single-lib unification: the old
//! oakrender cancelatom C ABI is gone).
//!
//! CPP-PARITY: src/task/src/task.h
//!
//! ## Concurrency contract
//!
//! Like the C++ original, the task's `title`/`error` strings are mutated on
//! the task's own thread (`run()`). The C ABI layer must therefore only read
//! them while the task is not concurrently running — i.e. before
//! [`Task::start`] or after the task finished (the `finished` flag is
//! synchronized through the `done` condvar, so `is_finished()`/
//! `succeeded()` are always race-free). This mirrors the C++ semantics
//! exactly.
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use oak_core::cancelatom::CancelAtom;
use crate::error::{Error, Result};
/// Event type emitted through a task's [`EventListener`], mirroring the
/// C++ `EventType` enum (`k_event_started`/`k_event_progress`/`k_event_finished`).
///
/// CPP-PARITY: src/task/src/task.h (EventType)
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TaskEvent {
/// Task began executing.
Started,
/// Progress updated; the payload is in 0.0..=1.0.
Progress(f64),
/// Task finished (success or failure).
Finished,
}
/// A listener notified of [`TaskEvent`]s, mirroring the C++
/// `std::function<void(EventType,double)>` listener. Delivered under the
/// task's lock; kept as the single async return channel (decision #3).
///
/// CPP-PARITY: src/task/src/task.h (EventListener)
pub type EventListener = Box<dyn FnMut(TaskEvent) + Send>;
/// Trait supplying the per-task work, standing in for the C++ protected
/// virtual `run()`. Concrete tasks implement this; [`Task::start`] drives it.
///
/// CPP-PARITY: src/task/src/task.h (run)
pub trait TaskBehavior {
/// Perform the task's work. Returning `Err(Error::Cancelled)` is treated
/// as a cancellation; any other `Err` marks the task failed.
fn run(&mut self, task: &mut Task) -> Result<()>;
}
/// Values the subscribe wrapper needs to re-encode [`TaskEvent`]s into the
/// 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).
#[derive(Debug, Default)]
pub struct SubscriberState {
/// Start timestamp (ms since epoch) published before `Started` is emitted.
pub start_ms: AtomicI64,
/// 1.0/0.0 published before `Finished` is emitted.
pub finished_value: AtomicI64,
}
/// Finish state, protected by the `done` condvar pair so `is_finished`/
/// `succeeded` are readable from any thread while the task thread mutates
/// them.
#[derive(Default)]
struct TaskDone {
/// Whether the task has finished (success or failure).
finished: bool,
/// Whether the task finished successfully.
succeeded: bool,
}
/// 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<String>,
start_time: Option<std::time::Instant>,
cancel_atom: Arc<CancelAtom>,
event_listener: Option<EventListener>,
cancel_event: Option<Box<dyn FnMut() + Send>>,
started: bool,
finished: bool,
succeeded: bool,
behavior: Option<Box<dyn TaskBehavior + Send>>,
/// Finish/success flag pair + wakeup condvar (race-free readers).
done: Arc<(Mutex<TaskDone>, Condvar)>,
/// Values published for the legacy subscribe wrapper.
subscriber: Option<Arc<SubscriberState>>,
}
impl Task {
/// 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<Arc<CancelAtom>>) -> Task {
Task {
title: title.to_string(),
error: None,
start_time: None,
cancel_atom: cancel_atom.unwrap_or_else(|| Arc::new(CancelAtom::new())),
event_listener: None,
cancel_event: None,
started: false,
finished: false,
succeeded: false,
behavior: None,
done: Arc::new((Mutex::new(TaskDone::default()), Condvar::new())),
subscriber: None,
}
}
/// Attach the concrete behavior (defaults to a no-op).
pub fn set_behavior(&mut self, behavior: Box<dyn TaskBehavior + Send>) {
self.behavior = Some(behavior);
}
/// Start the task: set `started`, emit [`TaskEvent::Started`], run the
/// behavior, then mark finished and emit [`TaskEvent::Finished`].
///
/// The returned `Result` mirrors the C++ `start()` bool: `Ok(())` when
/// the behavior succeeded, `Err(..)` otherwise.
pub fn start(&mut self) -> Result<()> {
let start_ms = system_time_ms();
self.start_time = Some(std::time::Instant::now());
self.started = true;
if let Some(s) = &self.subscriber {
s.start_ms.store(start_ms, Ordering::SeqCst);
}
self.emit_event(TaskEvent::Started);
// Take the behavior out so it can receive `self` (avoids a
// self-referential borrow); put it back afterwards.
let behavior = self.behavior.take();
let ret = if let Some(mut b) = behavior {
let r = b.run(self);
self.behavior = Some(b);
r
} else {
Ok(())
};
let succeeded = ret.is_ok();
{
let mut done = self.done.0.lock().unwrap();
done.finished = true;
done.succeeded = succeeded;
self.done.1.notify_all();
}
if let Some(s) = &self.subscriber {
s.finished_value
.store(if succeeded { 1 } else { 0 }, Ordering::SeqCst);
}
self.emit_event(TaskEvent::Finished);
// One-shot subscription: drop the listener after the final event.
self.event_listener = None;
ret
}
/// Request cancellation through the shared cancel atom, then invoke the
/// cancel event callback if one is registered.
pub fn cancel(&mut self) {
self.cancel_atom.cancel();
if let Some(cb) = self.cancel_event.as_mut() {
cb();
}
}
/// Whether cancellation was requested (queries the shared atom).
pub fn is_cancelled(&self) -> bool {
self.cancel_atom.is_cancelled()
}
/// The shared cancel atom (a clone of the task's `Arc`).
pub fn get_cancel_atom(&self) -> Arc<CancelAtom> {
self.cancel_atom.clone()
}
/// 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<CancelAtom>) {
self.cancel_atom = atom;
}
/// Register the event listener. Replaces any previous listener.
pub fn set_event_listener(&mut self, listener: EventListener) {
self.event_listener = Some(listener);
}
/// Register a hook invoked when [`Task::cancel`] is called.
pub fn set_cancel_event(&mut self, cb: Box<dyn FnMut() + Send>) {
self.cancel_event = Some(cb);
}
/// Publish the shared values used by the legacy subscribe wrapper.
pub fn set_subscriber(&mut self, state: Arc<SubscriberState>) {
self.subscriber = Some(state);
}
/// Emit a progress update, clamping to 0.0..=1.0 and delivering it to the
/// listener as [`TaskEvent::Progress`].
pub fn emit_progress(&mut self, progress: f64) {
self.emit_event(TaskEvent::Progress(progress.clamp(0.0, 1.0)));
}
/// Reset lifecycle state so the task can be run again.
pub fn reset(&mut self) {
self.started = false;
self.finished = false;
self.succeeded = false;
self.error = None;
self.start_time = None;
{
let mut done = self.done.0.lock().unwrap();
done.finished = false;
done.succeeded = false;
}
}
/// Record a failure message and mark the task failed.
pub fn set_error(&mut self, message: &str) {
self.error = Some(message.to_string());
}
/// Change the task title.
pub fn set_title(&mut self, title: &str) {
self.title = title.to_string();
}
/// The task title.
pub fn title(&self) -> &str {
&self.title
}
/// The failure message, if the task failed.
pub fn error(&self) -> Option<&str> {
self.error.as_deref()
}
/// Whether the task has finished (success or failure).
pub fn is_finished(&self) -> bool {
self.done.0.lock().unwrap().finished
}
/// Whether the task finished successfully.
pub fn succeeded(&self) -> bool {
self.done.0.lock().unwrap().succeeded
}
/// Block until the task finishes. Mirrors the C++ finished-wait condvar;
/// the manager joins worker threads instead, but this is useful for
/// direct (non-manager) runs. Returns immediately when the task has not
/// been started yet (nothing to wait for) or is already finished.
pub fn wait_finished(&self) {
if !self.started {
return;
}
let mut done = self.done.0.lock().unwrap();
while !done.finished {
done = self.done.1.wait(done).unwrap();
}
}
/// The elapsed time since [`Task::start`], for duration reporting.
pub fn elapsed(&self) -> Option<std::time::Duration> {
self.start_time.map(|t| t.elapsed())
}
fn emit_event(&mut self, ev: TaskEvent) {
if let Some(listener) = self.event_listener.as_mut() {
listener(ev);
}
}
}
/// Current wall-clock time in milliseconds since the Unix epoch, matching
/// the C++ `start_time_` convention (std::chrono::system_clock ms).
pub fn system_time_ms() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}
/// 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
}