// 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 .
//! Textures and CPU frames.
use std::sync::Arc;
use oakcore_rs::{PixelFormat, Rational};
use crate::backend::{BackendKind, GpuContextLike};
use crate::error::Result;
use crate::frame::VideoParamsPod;
/// A CPU frame (the payload oakcodec frames bridge into, and the value
/// `OakCodecFrame` handles box).
#[derive(Clone, Debug, PartialEq)]
pub struct Frame {
/// Width of the pixel buffer (effective resolution).
pub width: i32,
/// Height of the pixel buffer (effective resolution).
pub height: i32,
/// Pixel format (F32 on the main pipeline).
pub format: PixelFormat,
/// Channel count (4 on the main pipeline).
pub channels: i32,
/// Timestamp in the sequence timebase.
pub timestamp: Rational,
/// Pixel payload (row-major, tightly packed).
pub data: Vec,
/// Full video metadata (divider/aspect/interlacing etc.).
pub params: VideoParamsPod,
}
impl Default for Frame {
fn default() -> Self {
Self {
width: 0,
height: 0,
format: PixelFormat::Invalid,
channels: 0,
timestamp: Rational::NULL,
data: Vec::new(),
params: VideoParamsPod::default(),
}
}
}
impl Frame {
/// An empty frame (C++ `Frame::create()` before allocation).
pub fn new() -> Self {
Self::default()
}
/// The dummy frame: 0×0, transparent black, never uploaded
/// (`Texture::dummy` semantics).
pub fn dummy() -> Self {
Self {
width: 0,
height: 0,
format: PixelFormat::F32,
channels: VideoParamsPod::INTERNAL_CHANNEL_COUNT,
timestamp: Rational::new(0, 1),
data: Vec::new(),
params: VideoParamsPod::default(),
}
}
/// Bytes per channel for the frame's format.
pub fn bytes_per_channel(&self) -> usize {
self.format.bytes_per_channel()
}
/// Line stride in bytes (tightly packed rows: `width * channels * bpc`).
pub fn linesize_bytes(&self) -> usize {
(self.width as usize)
.saturating_mul(self.channels as usize)
.saturating_mul(self.bytes_per_channel())
}
/// Total pixel payload size.
pub fn allocated_size(&self) -> usize {
(self.height as usize).saturating_mul(self.linesize_bytes())
}
/// True when the pixel buffer is allocated.
pub fn is_allocated(&self) -> bool {
!self.data.is_empty()
}
/// Set the frame's video metadata (dims, format, divider, aspect…).
/// The channel count stays at the pipeline constant (4).
pub fn set_video_params(&mut self, pod: VideoParamsPod) {
self.params = pod;
self.width = pod.effective_width();
self.height = pod.effective_height();
self.format = match pod.format {
f if f == PixelFormat::U8 as i32 => PixelFormat::U8,
f if f == PixelFormat::U10 as i32 => PixelFormat::U10,
f if f == PixelFormat::U16 as i32 => PixelFormat::U16,
f if f == PixelFormat::F16 as i32 => PixelFormat::F16,
f if f == PixelFormat::F32 as i32 => PixelFormat::F32,
_ => PixelFormat::Invalid,
};
self.channels = VideoParamsPod::INTERNAL_CHANNEL_COUNT;
}
/// The frame's video metadata as the public POD.
pub fn video_params(&self) -> VideoParamsPod {
let mut p = self.params;
p.width = self.width;
p.height = self.height;
p.format = self.format as i32;
p
}
/// Allocate (or re-allocate) the pixel buffer per the current metadata,
/// zeroed. Returns false when the metadata is invalid
/// (C++ `Frame::allocate`).
pub fn allocate(&mut self) -> bool {
if self.width <= 0 || self.height <= 0 || self.channels <= 0 {
return false;
}
let size = self.allocated_size();
if size == 0 {
return false;
}
if self.data.len() != size {
self.data = vec![0u8; size];
} else {
self.data.fill(0);
}
true
}
/// Borrowed pixel data pointer (empty when not allocated).
pub fn data(&self) -> *const u8 {
self.data.as_ptr()
}
/// Mutable pixel data pointer (empty when not allocated).
pub fn data_mut(&mut self) -> *mut u8 {
self.data.as_mut_ptr()
}
/// True for the dummy frame.
pub fn is_dummy(&self) -> bool {
self.width == 0 && self.height == 0 && self.data.is_empty()
}
/// True when the pixel format is a float type.
pub fn is_float(&self) -> bool {
matches!(self.format, PixelFormat::F16 | PixelFormat::F32)
}
/// Number of pixels.
pub fn pixel_count(&self) -> usize {
(self.width as usize).saturating_mul(self.height as usize)
}
}
/// A texture: either backend-resident (GPU) or a CPU-frame wrapper.
/// `Clone` is safe: the GPU token destroy is idempotent (registry
/// lookup), so two clones both release safely at their own drop.
///
/// GPU textures carry an `Arc` to their [`GpuContext`] (the C++ `TexturePtr`
/// keeps its renderer alive the same way), so a texture value can upload/
/// download/blit without a separate renderer handle. `Drop` releases the
/// backend token; destroying a token twice is harmless (registry lookup).
#[derive(Clone)]
pub enum Texture {
/// Backend GPU texture.
Gpu {
/// Backend token (wgpu texture registry key).
token: u64,
/// Owning backend.
backend: BackendKind,
/// Width.
width: i32,
/// Height.
height: i32,
/// Pixel format.
format: PixelFormat,
/// The context owning the texture (trait object so tests can fake
/// the GPU side; `GpuContext` is the only production implementor).
ctx: Arc,
},
/// CPU-frame wrapper (uploaded lazily by the backend).
Cpu(Frame),
}
impl std::fmt::Debug for Texture {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Texture::Gpu {
token,
backend,
width,
height,
format,
..
} => f
.debug_struct("Texture::Gpu")
.field("token", token)
.field("backend", backend)
.field("width", width)
.field("height", height)
.field("format", format)
.finish(),
Texture::Cpu(frame) => f.debug_tuple("Texture::Cpu").field(frame).finish(),
}
}
}
impl Drop for Texture {
fn drop(&mut self) {
if let Texture::Gpu { token, ctx, .. } = self {
ctx.destroy_texture(*token);
}
}
}
impl Texture {
/// A dummy/empty texture (C++ `Texture::dummy` semantics): reads as
/// transparent black, never uploaded.
pub fn dummy() -> Self {
Texture::Cpu(Frame::dummy())
}
/// True for dummy textures.
pub fn is_dummy(&self) -> bool {
match self {
Texture::Gpu { .. } => false,
Texture::Cpu(f) => f.is_dummy(),
}
}
/// Wrap a CPU frame (no copy).
pub fn wrap_frame(frame: Frame) -> Self {
Texture::Cpu(frame)
}
/// Read back into a CPU frame (downloads for GPU textures).
pub fn to_frame(&self) -> Result {
match self {
Texture::Cpu(f) => Ok(f.clone()),
Texture::Gpu { token, ctx, .. } => ctx.download(*token),
}
}
/// Dimensions (0x0 for dummy).
pub fn size(&self) -> (i32, i32) {
match self {
Texture::Gpu { width, height, .. } => (*width, *height),
Texture::Cpu(f) => (f.width, f.height),
}
}
/// The texture's pixel format.
pub fn format(&self) -> PixelFormat {
match self {
Texture::Gpu { format, .. } => *format,
Texture::Cpu(f) => f.format,
}
}
/// The backend kind hosting the texture.
pub fn backend(&self) -> BackendKind {
match self {
Texture::Gpu { backend, .. } => *backend,
Texture::Cpu(_) => BackendKind::Cpu,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frame_allocate_and_linesize() {
let mut f = Frame::new();
let mut p = VideoParamsPod::default();
p.width = 4;
p.height = 3;
f.set_video_params(p);
assert_eq!(f.width, 4);
assert_eq!(f.height, 3);
assert_eq!(f.channels, 4);
assert_eq!(f.linesize_bytes(), 4 * 4 * 4);
assert!(f.allocate());
assert_eq!(f.data.len(), 4 * 3 * 4 * 4);
assert!(f.data.iter().all(|&b| b == 0));
}
#[test]
fn allocate_rejects_invalid() {
let mut f = Frame::new();
assert!(!f.allocate());
f.width = 0;
f.height = 10;
f.channels = 4;
f.format = PixelFormat::F32;
assert!(!f.allocate());
}
#[test]
fn dummy_frame_semantics() {
let d = Frame::dummy();
assert!(d.is_dummy());
assert_eq!(d.width, 0);
let t = Texture::dummy();
assert!(t.is_dummy());
assert_eq!(t.size(), (0, 0));
assert_eq!(t.backend(), BackendKind::Cpu);
}
#[test]
fn video_params_roundtrip_and_pointers() {
let mut f = Frame::new();
let mut p = VideoParamsPod::default();
p.width = 6;
p.height = 4;
p.divider = 2;
p.pixel_aspect_num = 2;
p.pixel_aspect_den = 1;
f.set_video_params(p);
// Divider shrinks the buffer dims (effective resolution).
assert_eq!(f.width, 3);
assert_eq!(f.height, 2);
let pod = f.video_params();
assert_eq!(pod.width, 3);
assert_eq!(pod.pixel_aspect_num, 2);
assert_eq!(f.is_float(), true);
assert_eq!(f.pixel_count(), 6);
f.allocate();
assert!(!f.data().is_null());
assert!(!f.data_mut().is_null());
// timestamp default null.
assert!(f.timestamp.is_null());
}
#[test]
fn wrap_and_to_frame_roundtrip() {
let mut f = Frame::new();
let mut p = VideoParamsPod::default();
p.width = 2;
p.height = 2;
f.set_video_params(p);
f.allocate();
f.data[0] = 0xAB;
let t = Texture::wrap_frame(f.clone());
assert_eq!(t.to_frame().unwrap(), f);
}
}