core: merge oak-common into oak-core
CI / Build & test (Linux) (push) Successful in 24m6s
CI / Build & test (Windows) (push) Successful in 31m14s

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.
This commit is contained in:
2026-09-03 17:42:20 +08:00
parent 49fed365a4
commit 4babbf5de8
150 changed files with 3052 additions and 4030 deletions
+2 -2
View File
@@ -297,9 +297,9 @@ mod tests {
use super::*;
use std::sync::atomic::{AtomicU32, Ordering};
use crate::frame::VideoParamsPod;
use crate::texture::{Frame, Texture};
use crate::worker::{InlineDispatcher, JobDispatch};
use oak_core::frame::VideoParamsPod;
use oak_core::texture::{Frame, Texture};
fn frame_producer() -> crate::ticket::Producer {
Arc::new(|_, _| {
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -261,7 +261,7 @@ pub struct PlaybackCache {
impl PlaybackCache {
/// New cache for `owner` (C++ `PlaybackCache(parent)`).
pub fn new(kind: CacheKind, owner: OwnerIdentity) -> Self {
let disk_dir = crate::commonutil::default_disk_cache_path();
let disk_dir = oak_core::commonutil::default_disk_cache_path();
Self {
kind,
owner,
+3 -3
View File
@@ -18,9 +18,9 @@
//! flag shared between a render/encode caller and its worker.
//!
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): the
//! implementation moved to oakcommon; this module re-exports it so the
//! implementation moved to oak_core; this module re-exports it so the
//! render ffi's `oakrender_cancelatom_*` exports (and their C-ABI
//! consumers, e.g. oakcodec) keep working unchanged.
/// Shared cancellation atom (oakcommon).
pub use oak_common::cancelatom::CancelAtom;
/// Shared cancellation atom (oak_core).
pub use oak_core::cancelatom::CancelAtom;
File diff suppressed because it is too large Load Diff
-87
View File
@@ -1,87 +0,0 @@
// 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/>.
//! oakcommon helpers (config, file functions) — direct Rust calls into
//! the oakcommon crate (single-lib unification; the former
//! `bridge/common.rs`). The configuration-location and disk-cache
//! helpers delegate to oakcommon's own implementation.
use std::sync::Mutex;
/// Read a config string via the domain store
/// (`ConfigStore::get(group, key)`); `None` when missing or empty.
pub fn config_get_string(group: Option<&str>, key: &str) -> Option<String> {
oak_common::configstore::ConfigStore::instance()
.get(group, key)
.ok()
.filter(|s| !s.is_empty())
}
/// `oakcommon_config_get_int(group, key, default)`.
pub fn config_get_int(group: Option<&str>, key: &str, default: i32) -> i32 {
oak_common::configstore::ConfigStore::instance().get_int(group, key, default)
}
/// The configuration directory — oakcommon's implementation
/// (`FileFunctions::get_configuration_location`, honoring `OAK_CONFIG_DIR`
/// and the platform fallbacks).
pub fn configuration_location() -> String {
oak_common::filefunctions::FileFunctions::new()
.get_configuration_location()
.unwrap_or_default()
}
/// Serializes tests that mutate `OAK_CONFIG_DIR` / `OAK_RENDER_BACKEND`
/// (env is process-global; the manager tests share this lock too).
pub static ENV_TEST_LOCK: Mutex<()> = Mutex::new(());
/// The default disk cache directory (C++ `DiskManager::
/// get_default_disk_cache_path`): `<configuration_location>/mediacache`.
pub fn default_disk_cache_path() -> String {
oak_common::filefunctions::default_disk_cache_path()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_missing_key_falls_back() {
// The real config store is empty under cargo test: defaults apply.
assert_eq!(config_get_int(None, "GraphicsBackend", 7), 7);
assert_eq!(config_get_string(None, "missing"), None);
}
#[test]
fn configuration_location_uses_env_override() {
let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let dir = std::env::temp_dir().join("oakrender-config-test");
std::env::set_var("OAK_CONFIG_DIR", &dir);
let loc = configuration_location();
assert_eq!(loc, dir.to_string_lossy());
std::env::remove_var("OAK_CONFIG_DIR");
}
#[test]
fn default_disk_cache_path_is_under_config() {
let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let dir = std::env::temp_dir().join("oakrender-cache-test");
std::env::set_var("OAK_CONFIG_DIR", &dir);
let p = default_disk_cache_path();
assert!(p.ends_with("/mediacache") || p.ends_with("\\mediacache"));
std::env::remove_var("OAK_CONFIG_DIR");
}
}
+8 -45
View File
@@ -16,6 +16,13 @@
//! Error codes, mirroring `include/render/error.h` verbatim; project-wide
//! -MMCCCC scheme (module registry in include/common/error.h), pass-through untranslated.
//!
//! With the oak-common/oak-core merge the render value/GPU types moved into
//! `oak-core`, so this crate no longer carries its own error enum:
//! [`Error`]/[`Result`] are re-exported from [`oak_core::error`] (identical
//! variant shape and messages). The `OAKRENDER_*` codes below remain as the
//! module's public-code contract; `Error::code()` reports the unified
//! `OAKCORE_*` values.
/// Success.
pub const OAKRENDER_OK: i32 = 0;
@@ -30,41 +37,7 @@ pub const OAKRENDER_E_NOT_FOUND: i32 = -70004;
/// Allocation failed.
pub const OAKRENDER_E_NOMEM: i32 = -70005;
/// Crate-internal result type.
pub type Result<T> = std::result::Result<T, Error>;
/// Crate-internal error.
#[derive(Clone, Debug, thiserror::Error)]
pub enum Error {
/// Null handle or invalid argument.
#[error("render: invalid argument")]
Invalid,
/// Wrong state.
#[error("render: invalid state")]
State,
/// Operation failed (context string is log-only).
#[error("render: operation failed: {0}")]
Failed(String),
/// Not found.
#[error("render: not found")]
NotFound,
/// Out of memory.
#[error("render: out of memory")]
NoMem,
}
impl Error {
/// Map to the public error code.
pub fn code(&self) -> i32 {
match self {
Error::Invalid => OAKRENDER_E_INVALID,
Error::State => OAKRENDER_E_STATE,
Error::Failed(_) => OAKRENDER_E_FAILED,
Error::NotFound => OAKRENDER_E_NOT_FOUND,
Error::NoMem => OAKRENDER_E_NOMEM,
}
}
}
pub use oak_core::error::{Error, Result};
#[cfg(test)]
mod tests {
@@ -108,14 +81,4 @@ mod tests {
fn source_is_none() {
assert!(std::error::Error::source(&Error::Failed("context".into())).is_none());
}
/// The `code()` mapping must be unchanged by the `Error` trait impl.
#[test]
fn code_mapping_unchanged() {
assert_eq!(Error::Invalid.code(), OAKRENDER_E_INVALID);
assert_eq!(Error::State.code(), OAKRENDER_E_STATE);
assert_eq!(Error::Failed("context".into()).code(), OAKRENDER_E_FAILED);
assert_eq!(Error::NotFound.code(), OAKRENDER_E_NOT_FOUND);
assert_eq!(Error::NoMem.code(), OAKRENDER_E_NOMEM);
}
}
+1930 -2005
View File
File diff suppressed because it is too large Load Diff
-146
View File
@@ -1,146 +0,0 @@
// 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/>.
//! CPU frame payloads and the [`VideoParamsPod`] value (the Rust mirror of
//! the `oakrender_video_params` POD in `include/render/renderer.h`).
//!
//! The C ABI frame functions (`oakrender_codec_frame_*`) marshal this
//! type; the FFI layer stores [`Frame`] values in `OakCodecFrame` handles.
use oak_core::{PixelFormat, Rational};
/// Mirror of the `oakrender_video_params` POD (include/render/renderer.h,
/// field order and semantics verbatim). Stored inside [`crate::texture::Frame`]
/// so the ffi `*_get_params` exports can report the full metadata.
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct VideoParamsPod {
/// Frame width (full resolution).
pub width: i32,
/// Frame height (full resolution).
pub height: i32,
/// Frame duration numerator (e.g. 1001/30000 s).
pub time_base_num: i32,
/// Frame duration denominator.
pub time_base_den: i32,
/// `olive::PixelFormat::Format` as int.
pub format: i32,
/// Pixel aspect numerator.
pub pixel_aspect_num: i32,
/// Pixel aspect denominator.
pub pixel_aspect_den: i32,
/// `olive::VideoParams::Interlacing` as int.
pub interlacing: i32,
/// `olive::VideoParams::ColorRange` as int.
pub color_range: i32,
/// Preview resolution divider (1 = full).
pub divider: i32,
/// `olive::VideoParams::Type` (0 = video).
pub video_type: i32,
/// 0/1.
pub premultiplied_alpha: i32,
}
impl Default for VideoParamsPod {
fn default() -> Self {
Self {
width: 0,
height: 0,
time_base_num: 1,
time_base_den: 1,
format: PixelFormat::F32 as i32,
pixel_aspect_num: 1,
pixel_aspect_den: 1,
interlacing: 0,
color_range: 0,
divider: 1,
video_type: 0,
premultiplied_alpha: 0,
}
}
}
impl VideoParamsPod {
/// The default render size used when a ticket carries no force size and
/// the output node's video params cannot be queried (oakcommon bridge
/// pending).
pub const DEFAULT_WIDTH: i32 = 1920;
/// See [`VideoParamsPod::DEFAULT_WIDTH`].
pub const DEFAULT_HEIGHT: i32 = 1080;
/// Frame rate as a rational (time base flipped; null for a null time
/// base).
pub fn frame_rate(&self) -> Rational {
if self.time_base_num <= 0 || self.time_base_den <= 0 {
return Rational::NULL;
}
Rational::new(self.time_base_den as i64, self.time_base_num as i64)
}
/// The time base (frame duration).
pub fn time_base(&self) -> Rational {
if self.time_base_num <= 0 || self.time_base_den <= 0 {
return Rational::NULL;
}
Rational::new(self.time_base_num as i64, self.time_base_den as i64)
}
/// The internal (fixed) channel count — the C++ engine uses 4 for the
/// video pipeline; not exposed in the POD.
pub const INTERNAL_CHANNEL_COUNT: i32 = 4;
/// Pixel dimensions of the data buffer honoring the preview divider
/// (C++ `VideoParams::effective_width/height` with a divider).
pub fn effective_width(&self) -> i32 {
(self.width / self.divider.max(1)).max(0)
}
/// See [`VideoParamsPod::effective_width`].
pub fn effective_height(&self) -> i32 {
(self.height / self.divider.max(1)).max(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pod_defaults() {
let p = VideoParamsPod::default();
assert_eq!(p.format, PixelFormat::F32 as i32);
assert_eq!(p.time_base(), Rational::new(1, 1));
}
#[test]
fn effective_size_honors_divider() {
let mut p = VideoParamsPod::default();
p.width = 3840;
p.height = 2160;
p.divider = 2;
assert_eq!(p.effective_width(), 1920);
assert_eq!(p.effective_height(), 1080);
}
#[test]
fn frame_rate_is_flipped_time_base() {
let mut p = VideoParamsPod::default();
p.time_base_num = 1001;
p.time_base_den = 30000;
assert_eq!(p.frame_rate(), Rational::new(30000, 1001));
assert_eq!(p.time_base(), Rational::new(1001, 30000));
}
}
+3 -10
View File
@@ -21,43 +21,36 @@
//!
//! Module map (mirrors `COVERAGE.md`):
//! - `handle` — refcounted C-handle scaffolding (C++ internalhandles.h)
//! - `texture`/`frame` — Texture + CPU Frame values (C++ texture.h/frame)
//! - `cache` — PlaybackCache/FrameHashCache family + disk state
//! - `color` — ColorProcessor over `ocio-rs` + LUT library
//! - `ticket` — ticket arena with exactly-once completion
//! - `worker` — the JobDispatch seam + thread-free inline dispatcher +
//! graph snapshot store (the in-process thread pool was deleted in M15 S2)
//! - `manager` — RenderManager singleton + disk cache
//! - `autocacher` — PreviewAutoCacher
//! - `eval` — the evaluation seam (RenderHooks)
//! - `backend` — wgpu GPU context + display renderer
//! - `shaderfx` — effect GLSL→WGSL translation + uniform packing
//! - `copier` — render-side project-copy client (oaknode C ABI)
//! - `cancelatom` — the cancellation primitive
//! - `ipc` — render-worker NDJSON protocol + shm frame-slot transport
//! - `scheduler` — preview frame scheduler (interleaved batch claims)
//! - `procpool` — process-isolated render backend (M15)
//! - `bridge` — direct-call C ABI bridges (oakcommon/oaknode/oakcodec)
//! - `ffi` — the `include/render/*.h` export layer
//! - `error` — re-exports `oak_core::error` (the backend/color/texture/
//! frame value types moved to `oak-core` in the oak-common merge)
#![deny(unsafe_op_in_unsafe_fn)]
#![warn(missing_docs)]
pub mod autocacher;
pub mod backend;
pub mod cache;
pub mod cancelatom;
pub mod color;
pub mod commonutil;
pub mod copier;
pub mod error;
pub mod eval;
pub mod frame;
pub mod handle;
pub mod ipc;
pub mod manager;
pub mod procpool;
pub mod scheduler;
pub mod shaderfx;
pub mod texture;
pub mod ticket;
pub mod worker;
+8 -8
View File
@@ -28,12 +28,12 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, MutexGuard};
use crate::autocacher::PreviewAutoCacher;
use crate::backend::BackendKind;
use crate::error::{Error, Result};
use crate::eval;
use crate::procpool::{DispatcherConfig, ProcessDispatcher, ShmAudioRef, ShmFrameRef};
use crate::ticket::{TicketArena, TicketId};
use crate::worker::{GraphSnapshotStore, InlineDispatcher, JobDispatch};
use oak_core::backend::BackendKind;
static MANAGER: Mutex<Option<Arc<RenderManager>>> = Mutex::new(None);
@@ -421,10 +421,10 @@ impl RenderManager {
}
/// The default disk cache directory (C++ `DiskManager::
/// get_default_disk_cache_path`); shared with oaknode via oakcommon
/// get_default_disk_cache_path`); shared with oaknode via oak_core
/// (single-lib unification).
pub fn disk_cache_path() -> String {
oak_common::filefunctions::default_disk_cache_path()
oak_core::filefunctions::default_disk_cache_path()
}
/// Bytes consumed by the default disk cache folder (direct filesystem
@@ -474,10 +474,10 @@ fn walk(dir: &std::path::Path) -> Vec<std::path::PathBuf> {
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Mutex, MutexGuard};
use super::*;
use std::sync::{Mutex, MutexGuard};
/// Serializes the manager-singleton tests (the singleton is global).
/// Serializes the manager-singleton tests (the singleton is global).
static MANAGER_TEST_LOCK: Mutex<()> = Mutex::new(());
fn manager_lock() -> MutexGuard<'static, ()> {
@@ -533,7 +533,7 @@ mod tests {
#[test]
fn disk_cache_size_and_clear() {
let _guard = crate::commonutil::ENV_TEST_LOCK
let _guard = oak_core::commonutil::ENV_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let dir = std::env::temp_dir().join("oakrender-diskcache-test");
@@ -550,7 +550,7 @@ mod tests {
#[test]
fn disk_cache_size_missing_dir_is_zero() {
let _guard = crate::commonutil::ENV_TEST_LOCK
let _guard = oak_core::commonutil::ENV_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let dir = std::env::temp_dir().join("oakrender-diskcache-missing");
+86 -9
View File
@@ -155,13 +155,13 @@ pub struct CompiledEffect {
/// The translation result (uniform layout + texture bindings).
pub translated: TranslatedShader,
/// The compiled pipeline in the context cache.
pub program: std::sync::Arc<crate::backend::ShaderProgram>,
pub program: std::sync::Arc<oak_core::backend::ShaderProgram>,
}
/// Translate `glsl` and compile the pipeline on `ctx`. `key` is the
/// pipeline cache key (the effect type id plus any shader-variant id).
pub fn compile_effect(
ctx: &crate::backend::GpuContext,
ctx: &oak_core::backend::GpuContext,
key: &str,
glsl: &str,
filtering: bool,
@@ -193,7 +193,7 @@ pub fn compile_effect(
/// `params`: `resolution_in` (the frame size), `ove_iteration`,
/// `ove_mvpmat` (identity).
pub fn run_effect(
ctx: &crate::backend::GpuContext,
ctx: &oak_core::backend::GpuContext,
effect: &CompiledEffect,
params: &oak_node::value::NodeValueRow,
inputs: &[(String, u64)],
@@ -780,14 +780,14 @@ void main() { frag_color = texture(tex_in, ove_texcoord); }
// ---- GPU runner tests (skipped without an adapter) -------------------
fn gpu() -> Option<std::sync::Arc<crate::backend::GpuContext>> {
crate::backend::GpuContext::create(crate::backend::BackendKind::Auto)
fn gpu() -> Option<std::sync::Arc<oak_core::backend::GpuContext>> {
oak_core::backend::GpuContext::create(oak_core::backend::BackendKind::Auto)
}
fn f32_frame(w: i32, h: i32, fill: impl Fn(usize) -> [f32; 4]) -> crate::texture::Frame {
use crate::texture::Frame;
fn f32_frame(w: i32, h: i32, fill: impl Fn(usize) -> [f32; 4]) -> oak_core::texture::Frame {
use oak_core::texture::Frame;
let mut frame = Frame::new();
let mut pod = crate::frame::VideoParamsPod::default();
let mut pod = oak_core::frame::VideoParamsPod::default();
pod.width = w;
pod.height = h;
pod.format = oak_core::PixelFormat::F32 as i32;
@@ -803,7 +803,7 @@ void main() { frag_color = texture(tex_in, ove_texcoord); }
frame
}
fn pixel(out: &crate::texture::Frame, x: usize) -> [f32; 4] {
fn pixel(out: &oak_core::texture::Frame, x: usize) -> [f32; 4] {
let mut rgba = [0.0f32; 4];
for (c, v) in rgba.iter_mut().enumerate() {
*v = f32::from_le_bytes(out.data[(x * 4 + c) * 4..(x * 4 + c) * 4 + 4].try_into().unwrap());
@@ -811,6 +811,83 @@ void main() { frag_color = texture(tex_in, ove_texcoord); }
rgba
}
/// End-to-end effect pass: a translated node shader (gain multiply)
/// runs through `compile_shader_pass`/`run_shader_pass` and the
/// readback matches the expected pixels exactly. (Moved from
/// oak-core's backend tests — the GLSL→WGSL translation and uniform
/// packing live in this module.)
#[test]
fn gpu_effect_pass_runs_translated_shader() {
use oak_core::frame::VideoParamsPod;
use oak_core::texture::Frame;
let Some(ctx) = gpu() else {
eprintln!("no adapter; skipping effect pass");
return;
};
let glsl = r#"
uniform sampler2D tex_in;
uniform float gain_in;
in vec2 ove_texcoord;
out vec4 frag_color;
void main() {
frag_color = texture(tex_in, ove_texcoord) * gain_in;
}
"#;
let translated = translate(glsl).unwrap();
let program = ctx
.compile_shader_pass(
"test-gain",
&translated.wgsl,
translated.textures.len() as u32,
!translated.uniforms.is_empty(),
false,
)
.unwrap();
let mut row = oak_node::value::NodeValueRow::new();
row.insert("gain_in".into(), oak_node::value::NodeValue::Float(0.5));
let uniforms = pack_uniforms(&translated, &row);
let w = 4;
let h = 2;
let src = ctx.create_texture(w, h).unwrap();
let dst = ctx.create_texture(w, h).unwrap();
let mut frame = Frame::new();
let mut pod = VideoParamsPod::default();
pod.width = w;
pod.height = h;
frame.set_video_params(pod);
frame.allocate();
// Distinct values per pixel (F32 RGBA): 0.2/0.4/0.6/1.0 shifted
// per pixel, so a UV mixup would be visible.
for px in 0..(w * h) as usize {
for c in 0..4 {
let v = 0.2 + 0.1 * (px + c) as f32;
frame.data[(px * 4 + c) * 4..(px * 4 + c) * 4 + 4]
.copy_from_slice(&v.to_le_bytes());
}
}
ctx.upload(src, &frame).unwrap();
ctx.run_shader_pass(&program, &uniforms, &[src], dst).unwrap();
let out = ctx.download(dst).unwrap();
for px in 0..(w * h) as usize {
for c in 0..4 {
let at = (px * 4 + c) * 4;
let got = f32::from_le_bytes(out.data[at..at + 4].try_into().unwrap());
let want = (0.2 + 0.1 * (px + c) as f32) * 0.5;
assert!(
(got - want).abs() < 1e-6,
"px {px} ch {c}: got {got}, want {want}"
);
}
}
ctx.destroy_texture(src);
ctx.destroy_texture(dst);
}
/// The real opacity shader through the full runner: pixels are
/// multiplied by the factor (and the UV convention is identity —
/// a flip would move the non-uniform pixels around).
-367
View File
@@ -1,367 +0,0 @@
// 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/>.
//! Textures and CPU frames.
use std::sync::Arc;
use oak_core::{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<u8>,
/// 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<dyn GpuContextLike>,
},
/// 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<Frame> {
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);
}
}
+5 -5
View File
@@ -32,8 +32,8 @@ use oak_core::{Rational, TimeRange};
use crate::error::{Error, Result};
use crate::eval;
use crate::texture::Texture;
use crate::worker::{JobDispatch, JobSchedule};
use oak_core::texture::Texture;
/// One effect of a montage clip's effect stack (M14 R3 effect-chain
/// wiring into the montage render path). The stack is ordered
@@ -135,8 +135,8 @@ 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,
oak_core::frame::VideoParamsPod::DEFAULT_WIDTH,
oak_core::frame::VideoParamsPod::DEFAULT_HEIGHT,
))
}
}
@@ -698,9 +698,9 @@ mod tests {
use std::sync::mpsc;
use std::time::Duration;
use crate::frame::VideoParamsPod;
use crate::texture::Frame;
use crate::worker::{InlineDispatcher, JobDispatch};
use oak_core::frame::VideoParamsPod;
use oak_core::texture::Frame;
fn small_frame() -> Frame {
let mut f = Frame::new();
+8 -8
View File
@@ -299,8 +299,8 @@ impl GraphSnapshotStore {
"oakrender-snapshots-{}-{:x}",
std::process::id(),
{
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
SEQ.fetch_add(1, Ordering::Relaxed)
}
));
@@ -463,13 +463,13 @@ impl Default for GraphSnapshotStore {
#[cfg(test)]
mod tests {
use super::*;
use std::sync::mpsc;
use std::time::Duration;
use super::*;
use std::sync::mpsc;
use std::time::Duration;
use crate::texture::Texture;
use oak_core::texture::Texture;
fn job(tag: u64, tx: mpsc::Sender<u64>, gate: Option<Arc<AtomicBool>>) -> Job {
fn job(tag: u64, tx: mpsc::Sender<u64>, gate: Option<Arc<AtomicBool>>) -> Job {
let produce: Producer = Arc::new(move |_, _| {
if let Some(g) = &gate {
if g.load(Ordering::Acquire) {
@@ -685,7 +685,7 @@ mod tests {
// rewrite the file (the dedup the resync exists to bypass)...
{
let mut guard = project.lock().unwrap_or_else(|e| e.into_inner());
guard.set_working_color_space(oak_common::colormath::WorkingColorSpace::SrgbLegacy);
guard.set_working_color_space(oak_core::colormath::WorkingColorSpace::SrgbLegacy);
}
let p2 = store.acquire(&project, 1).unwrap();
assert_eq!(p1, p2, "same (uuid, revision) key reuses the file");