ocio 0.3.1

A pure Rust port of OpenColorIO, the color management framework for visual effects and animation.
Documentation
// SPDX-License-Identifier: BSD-3-Clause
// Copyright Contributors to the OpenColorIO Project.
// Copyright (c) 2026 Philippe Llerena.

//! Ops: the internal, CPU-evaluable building blocks a processor is made of
//! (port of `Op.h` / `OpData` / the `*OpCPU` renderers).
//!
//! Each op module owns:
//! * a struct implementing [`Op`] (data + CPU evaluation),
//! * the `BuildOps` implementation of the transform(s) it serves, which
//!   converts a transform into ops (see `crate::transforms::build`).
//!
//! Pixels are always processed as interleaved RGBA `f32`.

pub mod allocation;
pub mod cdl;
pub mod exponent;
pub mod exposure_contrast;
pub mod fixed_function;
pub mod gamma;
pub mod grading_hue_curve;
pub mod grading_primary;
pub mod grading_rgb_curve;
pub mod grading_tone;
pub mod log;
pub mod lut1d;
pub mod lut3d;
pub mod matrix;
pub mod noop;
pub mod range;
pub(crate) mod sse;

// GPU renderers of the ops (port of the `*OpGPU.cpp` files).
#[path = "cdl/gpu.rs"]
pub(crate) mod cdl_gpu;
pub(crate) mod exponent_gpu;
#[path = "exposure_contrast/gpu.rs"]
pub(crate) mod exposure_contrast_gpu;
#[path = "fixed_function/gpu.rs"]
pub(crate) mod fixed_function_gpu;
#[path = "gamma/gpu.rs"]
pub(crate) mod gamma_gpu;
pub(crate) mod grading_hue_curve_gpu;
pub(crate) mod grading_primary_gpu;
pub(crate) mod grading_rgb_curve_gpu;
pub(crate) mod grading_tone_gpu;
#[path = "log/gpu.rs"]
pub(crate) mod log_gpu;
#[path = "lut1d/gpu.rs"]
pub(crate) mod lut1d_gpu;
#[path = "lut3d/gpu.rs"]
pub(crate) mod lut3d_gpu;
#[path = "matrix/gpu.rs"]
pub(crate) mod matrix_gpu;
pub(crate) mod range_gpu;

use crate::dynamic_property::DynamicProperty;
use crate::transforms::Transform;
use crate::types::{DynamicPropertyType, OptimizationFlags};
use std::any::Any;
use std::fmt::Debug;
use std::sync::Arc;

/// Shared pointer to an op.
pub type OpRc = Arc<dyn Op>;
/// Ordered list of ops.
pub type OpVec = Vec<OpRc>;

/// An RGBA pixel.
pub type Pixel = [f32; 4];

/// A color operation.
///
/// Implementations must be cheap to share (`Arc`) and thread safe: `apply`
/// may be called concurrently from several threads.
pub trait Op: Debug + Send + Sync + Any {
    /// Short type name, e.g. `"Matrix"`, `"Lut1D"`. Used in debugging and
    /// cache ids.
    fn name(&self) -> &'static str;

    /// Process pixels in place.
    fn apply(&self, pixels: &mut [Pixel]);

    /// True if the op does nothing at all (e.g. file/look markers, or an
    /// op whose parameters make it an exact no-op). No-ops are always
    /// removed by the optimizer.
    fn is_no_op(&self) -> bool {
        false
    }

    /// True for pure marker ops (file / look / metadata markers, allocation
    /// no-ops), OCIO's `NoOpType` ops. They are removed when a processor is
    /// created, whatever the optimization flags.
    fn is_no_op_type(&self) -> bool {
        false
    }

    /// True if the op is an identity (may still clamp is **not** an
    /// identity). Removed by the optimizer under `OptimizationFlags::IDENTITY`.
    fn is_identity(&self) -> bool {
        self.is_no_op()
    }

    /// True if an output channel depends on other input channels.
    fn has_channel_crosstalk(&self) -> bool {
        true
    }

    /// Unique id describing the op parameters (used for processor caching
    /// and equality checks).
    fn cache_id(&self) -> String;

    /// `Any` access, used to downcast in `combine_with` / `is_inverse_of`.
    fn as_any(&self) -> &dyn Any;

    /// If `self` followed by `next` can be replaced by fewer ops, return the
    /// replacement:
    /// * `Some(vec![])` when the pair cancels out (an op followed by its
    ///   exact inverse, gated by the matching `PAIR_IDENTITY_*` flag),
    /// * `Some(vec![combined])` when both compose into one op (e.g. two
    ///   matrices, gated by `COMP_*` flags),
    /// * `None` otherwise (default).
    ///
    /// Implementations must check `flags` for the relevant bits and use
    /// `next.downcast_ref::<Self>()` to identify the other op.
    fn combine_with(&self, _next: &dyn Op, _flags: OptimizationFlags) -> Option<OpVec> {
        None
    }

    /// If `self` alone can be replaced by simpler ops, return them (port of
    /// OCIO's `getSimplerReplacement`, e.g. a CDL without power becoming
    /// matrices, gated by `SIMPLIFY_OPS`, and of `getIdentityReplacement` for
    /// identities that still clamp, e.g. a basic gamma of 1 becoming a clamp
    /// range, gated by `IDENTITY` / `IDENTITY_GAMMA`). Implementations must
    /// check `flags`. `None` (default) keeps the op.
    fn simplify(&self, _flags: OptimizationFlags) -> Option<OpVec> {
        None
    }

    /// True if the op holds dynamic properties.
    fn is_dynamic(&self) -> bool {
        false
    }

    /// Return the dynamic property of the given type, if held.
    fn dynamic_property(&self, _ty: DynamicPropertyType) -> Option<DynamicProperty> {
        None
    }

    /// Replace the dynamic property of the op by `prop` (so several ops can
    /// share one property). Default: no-op.
    fn replace_dynamic_property(&mut self, _prop: &DynamicProperty) {}

    /// Copy of the op with dynamic properties converted to static values
    /// (used by `OptimizationFlags::NO_DYNAMIC_PROPERTIES`). Default: none.
    fn make_non_dynamic(&self) -> Option<OpRc> {
        None
    }

    /// Convert back to an equivalent transform (used by
    /// `Processor::create_group_transform` and CLF export). `None` if not
    /// representable (e.g. no-op markers).
    fn to_transform(&self) -> Option<Transform> {
        None
    }

    /// Finalized version of the op, if it differs (port of `Op::finalize`:
    /// e.g. an inverse matrix becomes the equivalent forward matrix). Called
    /// when a processor is created. Default: `None` (already final).
    fn finalize(&self) -> Option<OpRc> {
        None
    }

    /// The op evaluated by a CPU processor (port of `Op::getCPUOp`).
    ///
    /// `fast_log_exp_pow` is set when the processor is created with
    /// `OptimizationFlags::FAST_LOG_EXP_POW`: the ops having a fast renderer
    /// in OCIO (gamma, log, CDL and the PQ fixed functions) then return a
    /// copy of themselves using it (a port of OCIO's `*SSE` renderer).
    /// `None` (default) keeps the op.
    fn cpu_op(&self, _fast_log_exp_pow: bool) -> Option<OpRc> {
        None
    }

    /// Clone into a new boxed op (so it can be mutated before sharing).
    fn clone_box(&self) -> Box<dyn Op>;

    /// Add the GPU shader program implementing the op (plus its textures and
    /// uniforms) to `shader_creator` (port of `Op::extractGpuShaderInfo`).
    ///
    /// The default implementation handles every op of this crate (see
    /// [`crate::gpu::default_extract_gpu_shader_info`]) and returns an error
    /// for other op types.
    fn extract_gpu_shader_info(
        &self,
        shader_creator: &mut dyn crate::gpu::GpuShaderCreator,
    ) -> crate::error::Result<()> {
        crate::gpu::default_extract_gpu_shader_info(self.as_any(), shader_creator)
    }
}

impl dyn Op {
    /// Downcast helper.
    pub fn downcast_ref<T: Op>(&self) -> Option<&T> {
        self.as_any().downcast_ref::<T>()
    }
}

/// Combined cache id of an op list.
pub fn ops_cache_id(ops: &[OpRc]) -> String {
    let mut s = String::new();
    for op in ops {
        s.push_str(&op.cache_id());
        s.push(' ');
    }
    s
}

/// True if every op is a no-op.
pub fn ops_are_no_op(ops: &[OpRc]) -> bool {
    ops.iter().all(|o| o.is_no_op())
}

/// Evaluate an op list on pixels.
pub fn apply_ops(ops: &[OpRc], pixels: &mut [Pixel]) {
    for op in ops {
        op.apply(pixels);
    }
}

/// Helper for cache ids: hash of float data.
pub fn hash_f32(values: &[f32]) -> String {
    let mut bytes = Vec::with_capacity(values.len() * 4);
    for v in values {
        bytes.extend_from_slice(&v.to_le_bytes());
    }
    format!("{:x}", md5::compute(&bytes))
}

/// Helper for cache ids: hash of double data.
pub fn hash_f64(values: &[f64]) -> String {
    let mut bytes = Vec::with_capacity(values.len() * 8);
    for v in values {
        bytes.extend_from_slice(&v.to_le_bytes());
    }
    format!("{:x}", md5::compute(&bytes))
}