// 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 tickets: async render requests with completion delivery
//! (C++ `RenderTicket`/`RenderTicketWatcher`, Qt signals replaced by
//! boxed callbacks on a delivery thread).
//!
//! Exactly-once contract: a ticket's completion fires exactly once — on
//! success with the result, on cancel (or pool shutdown) with
//! `Error::State`. Cancellation never races the delivery: `cancel` only
//! sets a flag that `finish` honors, and `finish` is the single delivery
//! point.
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Condvar, Mutex, MutexGuard};
use oak_core::{Rational, TimeRange};
use crate::error::{Error, Result};
use crate::eval;
use crate::texture::Texture;
use crate::worker::{JobDispatch, JobSchedule};
/// One effect of a montage clip's effect stack (M14 R3 effect-chain
/// wiring into the montage render path). The stack is ordered
/// source-first (the chain walk's signal order: the first element feeds
/// the media side, the last feeds the clip's effect input); the renderer
/// applies them in sequence after decode, before compositing.
#[derive(Clone, Debug)]
pub struct MontageEffect {
/// Node type id: the built-in factory type id (e.g.
/// `org.olivevideoeditor.Olive.opacity`) or the OFX plugin identifier.
pub type_id: String,
/// The effect's enabled flag. Disabled effects are bypassed (the C++
/// traverser's bypass pushes the effect input through unchanged), so
/// the renderer skips them; they are carried anyway so the render side
/// can log what it skipped.
pub enabled: bool,
/// The clip/input name the source texture arrives on (the node's
/// effect input id — "tex_in" for built-ins, "Source" for typical OFX
/// filters; `None` when the node has no effect input).
pub effect_input_id: Option,
/// Parameter values (input id -> value): the node's non-hidden,
/// non-connection data inputs at their standard (non-keyframed)
/// values — the same parameter set the inspector exposes.
pub params: Vec<(String, oak_node::value::NodeValue)>,
}
/// One clip of a sequence montage (M12 P0): the facade resolves the
/// timeline into an ordered list of clips; the producer decodes each and
/// composites them topmost-last.
#[derive(Clone, Debug)]
pub struct MontageClip {
/// Footage filename.
pub filename: String,
/// Media stream index.
pub stream_index: i32,
/// Clip in point (sequence time).
pub in_time: Rational,
/// Clip out point (sequence time).
pub out_time: Rational,
/// Media in point.
pub media_in: Rational,
/// Playback gain (1.0 = unity).
pub gain: f32,
/// The clip's effect stack (source-first; empty for audio clips and
/// for montage builders that do not resolve effect chains).
pub effects: Vec,
}
/// Audio ticket parameters (M12 P1): the output format plus the audio
/// montage to mix over the requested range.
#[derive(Clone, Debug)]
pub struct AudioTicketParams {
/// Node graph context (copied project identity).
pub viewer: u64,
/// The range to render (sequence time).
pub range: TimeRange,
/// Output sample rate (Hz).
pub sample_rate: i32,
/// Output channel layout mask.
pub channel_layout: u64,
/// Clips to mix (ordered arbitrarily; gains applied, silence
/// elsewhere).
pub montage: Vec,
}
/// Ticket parameters (Rust view of `oakrender_video_ticket_params`).
#[derive(Clone, Debug)]
pub struct VideoTicketParams {
/// Node graph context (copied project identity).
pub viewer: u64,
/// The owning project's uuid (M16 S1): the worker renders the viewer's
/// graph frame from the loaded snapshot ONLY when this matches the
/// snapshot's project — empty means "no graph mode" (montage/footage/
/// generated frames never match a loaded graph).
pub project: String,
/// Frame time.
pub time: Rational,
/// Forced size override (None = sequence size).
pub force_size: Option<(i32, i32)>,
/// Forced pixel format (None = pipeline default F32).
pub force_format: Option,
/// Frame cache to record into (cache identity).
pub cache: Option,
/// Cache directory (marshalled from the cache handle; frame-cache
/// write path).
pub cache_dir: Option,
/// Cache uuid.
pub cache_id: Option,
/// Cache timebase.
pub cache_timebase: Option,
/// Single-footage decode (footage node render; M12 P0).
pub footage: Option<(String, i32)>,
/// Sequence montage (ordered topmost-last; M12 P0). When set, the
/// footage field is ignored.
pub montage: Vec,
}
impl VideoTicketParams {
/// The render size: force_size when set, else the pipeline default.
pub fn render_size(&self) -> (i32, i32) {
self.force_size.unwrap_or((
crate::frame::VideoParamsPod::DEFAULT_WIDTH,
crate::frame::VideoParamsPod::DEFAULT_HEIGHT,
))
}
}
/// Audio samples produced by an audio ticket (M12 P1).
#[derive(Clone, Debug)]
pub struct AudioSamples {
/// Interleaved f32 samples.
pub samples: Vec,
/// Sample rate (Hz).
pub sample_rate: i32,
/// Channel layout mask.
pub channel_layout: u64,
/// Channel count.
pub channel_count: i32,
}
/// The ticket completion payload: video frames or audio samples.
#[derive(Clone, Debug)]
pub enum TicketPayload {
/// A rendered video texture.
Video(Texture),
/// Rendered interleaved audio.
Audio(AudioSamples),
/// A rendered frame living in a worker's shared-memory slot (M15
/// process backend): zero copy — the consumer reads the pixels from
/// the mapping and releases the slot through the dispatcher.
ShmFrame(crate::procpool::ShmFrameRef),
/// Rendered audio living in a worker's shared-memory slot (M15 S3):
/// interleaved f32 in `SLOT_FORMAT_AUDIO_F32` slots, consumed with
/// [`crate::procpool::ShmAudioRef::samples`] and released through the
/// dispatcher — the audio counterpart of `ShmFrame`.
ShmAudio(crate::procpool::ShmAudioRef),
}
/// Completion payload: the rendered texture/samples or the failure
/// reason.
pub type TicketResult = Result;
/// Completion callback (exactly-once delivery).
pub type Completion = Box;
/// Frame producer: renders the frame for a ticket. Installed by the
/// manager (eval-based CPU generation for this pass); tests install
/// custom producers.
pub type Producer = Arc TicketResult + Send + Sync>;
/// Ticket metadata: the closed set of C++ `set_property` keys
/// (no Variant property bag).
#[derive(Clone, Debug, Default)]
pub struct TicketMeta {
/// Ticket kind (video/audio).
pub kind: Option,
/// Frame time.
pub time: Option,
/// Cache directory/uuid/timebase (video tickets writing the frame
/// cache).
pub cache_dir: Option,
/// Cache uuid.
pub cache_id: Option,
/// Cache timebase.
pub cache_timebase: Option,
}
/// Ticket kinds (C++ `RenderManager::TicketType`).
pub mod ticket_kind {
/// Video ticket.
pub const VIDEO: i32 = 0;
/// Audio ticket.
pub const AUDIO: i32 = 1;
}
/// A submitted ticket (arena id).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TicketId(pub u64);
enum SlotState {
Running,
Finished,
}
/// A single in-flight ticket (shared between the arena, the worker's job
/// closure and the FFI ticket handle).
struct TicketSlot {
id: TicketId,
kind: i32,
time: Rational,
range: TimeRange,
meta: Mutex,
state: Mutex,
cv: Condvar,
cancel: AtomicBool,
delivered: AtomicBool,
completion: Mutex