mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-25 10:36:56 -06:00
Cause: the config reader didn't know ColorSpaceTransform or ExponentWithLinearTransform and passed colours through them silently. Blender's Linear Rec.709 -> sRGB therefore stayed in its E-white XYZ reference space (the blue cast) without the sRGB curve. Display colorspaces, view transforms, v2 views, looks, AllocationTransform, BuiltinTransform and .spi1d / .spimtx files were missing too, and nothing told the user. Fix (from the OCIO v2 documentation and the CLF "moncurve", not OCIO's source): ExponentWithLinear, ColorSpace, Look, DisplayView, Allocation and Builtin transforms (identity, ACES AP0/AP1 matrices, ACEScct / ACEScc, the sRGB / P3 / Rec.1886 / gamma / PQ displays). RangeTransform now clamps and takes one-sided ranges. Display colorspaces meet the scene reference through the default view transform. Added v2 views, view looks, the looks section and .spi1d / .spi3d / .spimtx files. A conversion is built once as a flat list of steps (Config::path / display_path) for both the CPU effect and the GPU colour program (new moncurve and clamp ops and the ACEScc curve; a program over the kernel's 255 ops renders on the CPU). Named-transform resolution is limited to 16 levels and 4096 steps, and YAML nesting to 64 levels, so hostile or looping configs stop. Transforms that are still unsupported (Grading Primary / Tone, ACES output, HLG…), unreadable configs and unknown spaces are now reported: a warning under the effect in Effect Controls, and the new effect.warning query. Tests: ocio_config::blender_like_config_converts_linear_rec709_to_srgb (CST, display colorspaces, v1 / v2 views, allocation, looks, builtin, range, loops), moncurve_matches_srgb_and_inverts, builtin_styles, yaml_nesting_is_bounded, ocio::spi_files_parse, tests_effects::ocio_effect_warns_about_unsupported_config_transforms, gpu tests_fx_lut::ocio_custom_config_v2 (GPU = CPU). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
184 lines
7.2 KiB
Rust
184 lines
7.2 KiB
Rust
//! GPU effects, colour management (kernels in `shaders/fx_lut.wgsl`, every entry point
|
||
//! prefixed `fxl_`): Apply Color LUT, OCIO CDL / Color Space / Display / File / Look Transform
|
||
//! and Color Profile Converter; Lumetri Color's Input LUT and Look also use the LUT helpers.
|
||
//!
|
||
//! Each effect compiles to a colour program ([`effectcraft_effects::color_program`]: transfer
|
||
//! functions, matrices, ASC CDLs, tone mapping, gamut compression, LUTs) built next to the CPU
|
||
//! effect, which `fxl_point` interprets per pixel. LUTs (1D tables and 3D lattices, with a
|
||
//! cineSpace shaper) travel in the kernel's storage buffer and are interpolated in the kernel
|
||
//! exactly as the CPU does (nearest, trilinear or tetrahedral; inverse 1D tables and the
|
||
//! iterative 3D inverse): hardware 3D-texture filtering would round the interpolation weights
|
||
//! and cannot do tetrahedral interpolation. The CPU evaluates transfer functions, CDLs and
|
||
//! Color Profile Converter's matrix in f64; the kernel in f32 (within the 1e-3 tolerance).
|
||
//! Custom `.ocio` configurations compile their conversions (matrix and offset, exponent,
|
||
//! exponent with linear segment, log / log-affine, allocation, range with its clamp, CDL, file
|
||
//! LUTs and matrices, built-in curves, with colour space / look / display-view transforms
|
||
//! resolved) to the same program; one longer than the kernel's 255 ops renders on the CPU.
|
||
|
||
use effectcraft_effects::{ColorOp, EffectCtx, Lut, Straight, Tf};
|
||
|
||
use crate::context::{Enc, Params};
|
||
use crate::effects::GBuf;
|
||
|
||
/// Compute entry points in `fx_lut.wgsl`.
|
||
pub(crate) const KERNELS: &[&str] = &["fxl_point"];
|
||
|
||
/// Effect ids implemented here.
|
||
pub(crate) const IDS: &[&str] = &[
|
||
"ec.utility.applylut",
|
||
"ec.color.ociocdl",
|
||
"ec.color.ociocolorspace",
|
||
"ec.color.ociodisplay",
|
||
"ec.color.ociofile",
|
||
"ec.color.ociolook",
|
||
"ec.utility.colorprofileconverter",
|
||
];
|
||
|
||
/// Largest LUT the kernels take (floats in the storage buffer); bigger ones render on the CPU.
|
||
const MAX_FLOATS: usize = 1 << 24;
|
||
|
||
/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU.
|
||
pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||
let (ops, straight) = effectcraft_effects::color_program(id, ctx)?;
|
||
if ops.is_empty() {
|
||
return Some(b);
|
||
}
|
||
let data = encode(&ops)?;
|
||
let buf = e.data(&data);
|
||
let mut p = Params::default();
|
||
p.u[0][0] = (straight == Straight::Divide) as u32;
|
||
let out = e.scratch(b.img.width, b.img.height);
|
||
e.pixels("fxl_point", &p, &b.img, None, &out, Some(&buf));
|
||
Some(GBuf { img: out, ..b })
|
||
}
|
||
|
||
fn tf_code(t: Tf) -> [f32; 2] {
|
||
match t {
|
||
Tf::Linear => [0.0, 0.0],
|
||
Tf::Srgb => [1.0, 0.0],
|
||
Tf::Gamma(g) => [2.0, g as f32],
|
||
Tf::AcesCct => [3.0, 0.0],
|
||
Tf::Pq => [4.0, 0.0],
|
||
Tf::AcesCc => [5.0, 0.0],
|
||
}
|
||
}
|
||
|
||
/// Most ops `fxl_run` interprets (its loop guard); longer programs render on the CPU.
|
||
const MAX_OPS: usize = 255;
|
||
|
||
/// A LUT in the layout `fxl_lut_*` read at the returned offset: n1, n3, domain min (3), domain
|
||
/// max (3), the 1D table (n1 × RGB), then the lattice (n3³ × RGB, red fastest).
|
||
pub(crate) fn push_lut(d: &mut Vec<f32>, lut: &Lut) -> f32 {
|
||
let at = d.len() as f32;
|
||
d.extend([lut.n1 as f32, lut.n3 as f32]);
|
||
d.extend(lut.dmin);
|
||
d.extend(lut.dmax);
|
||
for v in lut.d1.iter().chain(&lut.d3) {
|
||
d.extend(v);
|
||
}
|
||
at
|
||
}
|
||
|
||
/// Program layout: a stream of (opcode, arguments…) ending in 0, followed by LUT data.
|
||
fn encode(ops: &[ColorOp]) -> Option<Vec<f32>> {
|
||
if ops.len() > MAX_OPS {
|
||
return None;
|
||
}
|
||
let mut prog: Vec<f32> = vec![];
|
||
let mut tables: Vec<Vec<f32>> = vec![];
|
||
// LUT offsets are patched once the program's length is known.
|
||
let mut patches: Vec<(usize, usize)> = vec![];
|
||
for op in ops {
|
||
match op {
|
||
ColorOp::Decode(Tf::Linear) | ColorOp::Encode(Tf::Linear) => {}
|
||
ColorOp::Decode(t) => {
|
||
prog.push(1.0);
|
||
prog.extend(tf_code(*t));
|
||
}
|
||
ColorOp::Encode(t) => {
|
||
prog.push(2.0);
|
||
prog.extend(tf_code(*t));
|
||
}
|
||
ColorOp::Matrix(m) => {
|
||
prog.push(3.0);
|
||
for r in m {
|
||
prog.extend(r);
|
||
}
|
||
}
|
||
ColorOp::Cdl { cdl, clamp, inverse } => {
|
||
prog.push(4.0);
|
||
for v in cdl.slope.iter().chain(&cdl.offset).chain(&cdl.power) {
|
||
prog.push(*v as f32);
|
||
}
|
||
prog.extend([cdl.sat as f32, *clamp as u32 as f32, *inverse as u32 as f32]);
|
||
}
|
||
ColorOp::Tonemap { inverse } => prog.extend([5.0, *inverse as u32 as f32]),
|
||
ColorOp::Gamut(l) => prog.extend([6.0, l[0] as f32, l[1] as f32, l[2] as f32]),
|
||
ColorOp::Max0 => prog.push(7.0),
|
||
ColorOp::Affine { m, pre, post } => {
|
||
prog.push(9.0);
|
||
for r in m {
|
||
prog.extend(r);
|
||
}
|
||
prog.extend(pre);
|
||
prog.extend(post);
|
||
}
|
||
ColorOp::Pow(e) => {
|
||
prog.push(10.0);
|
||
prog.extend(e);
|
||
}
|
||
ColorOp::LogAffine { base, log_slope, log_offset, lin_slope, lin_offset, inverse } => {
|
||
prog.extend([11.0, *base]);
|
||
prog.extend(log_slope.iter().chain(log_offset).chain(lin_slope).chain(lin_offset));
|
||
prog.extend([*inverse as u32 as f32, f64::MIN_POSITIVE.ln() as f32]);
|
||
}
|
||
ColorOp::MonCurve { gamma, offset, inverse, mirror } => {
|
||
prog.push(12.0);
|
||
prog.extend(gamma.iter().chain(offset));
|
||
prog.extend([*inverse as u32 as f32, *mirror as u32 as f32]);
|
||
}
|
||
ColorOp::Clamp { lo, hi } => {
|
||
prog.push(13.0);
|
||
prog.extend(lo.iter().chain(hi));
|
||
}
|
||
ColorOp::Lut { lut, shaper, interp, inverse } => {
|
||
prog.extend([8.0, 0.0, *interp as f32, *inverse as u32 as f32, -1.0]);
|
||
let mut t = vec![];
|
||
push_lut(&mut t, lut);
|
||
let lut_at = prog.len() - 4;
|
||
patches.push((lut_at, tables.len()));
|
||
tables.push(t);
|
||
if let Some(s) = shaper {
|
||
// Shaper: three point counts, then each channel's (x, y) points.
|
||
let mut t: Vec<f32> = s.iter().map(|c| c.len() as f32).collect();
|
||
for c in s.iter() {
|
||
for (x, y) in c {
|
||
t.extend([*x, *y]);
|
||
}
|
||
}
|
||
patches.push((prog.len() - 1, tables.len()));
|
||
tables.push(t);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
prog.push(0.0);
|
||
let mut starts = vec![];
|
||
let mut at = prog.len();
|
||
for t in &tables {
|
||
starts.push(at);
|
||
at += t.len();
|
||
}
|
||
if at > MAX_FLOATS {
|
||
return None;
|
||
}
|
||
// Offsets travel as f32: exact below 2^24.
|
||
for (slot, table) in patches {
|
||
prog[slot] = starts[table] as f32;
|
||
}
|
||
for t in tables {
|
||
prog.extend(t);
|
||
}
|
||
Some(prog)
|
||
}
|