nodes: real OCIO color grading (linear + log) on the GPU
The OCIO grading nodes previously pushed null texture handles; they now push real ShaderJobPayloads whose GLSL is the OCIO-generated dynamic grading-primary GPU shader — the exact code the C++ path applies, so no approximation: - color: grading_primary_function_shader(style) builds a dynamic GradingPrimaryTransform (LIN/LOG) on the default config, extracts the GLSL via GpuShaderDesc (function 'ove_grading_primary', resource prefix ocio_, no LUT textures) and caches it per style + config id. - eval: OCIO_GRADING_STUBS maps the two node type ids to the grading style; process_shader_job resolves the stub and splices it into the node's %1 marker (same wiring as the chromakey OCIO stub); the pipeline cache key folds the stub text so a config change recompiles. - nodes: value() pushes a ShaderJobPayload with the C++ value() rewrite applied to the row — vec4 (RGBM x=master) grading inputs to the vec3 GPU uniform form (lin: contrast RGB=c*m, offset RGB=c+m, exposure RGB=2^(c+m); log: lift RGB=c+m, gain c*m, gamma c*m), plus pivot/saturation floats, the log pivotBlack/pivotWhite normalization range (0/1), clamp sentinels (NoClampBlack -1 / NoClampWhite 2), white>black enforcement per frame, and localBypass=false. Generated uniform names bind by name (the log node's OCIO_NAMESPACE_ id text normalizes to the ocio_ resource prefix). - Tests: grading stub generation (analytic GLSL, cache) in color, end-to-end GPU exposure doubling for lin (+1 stop on 0.2 gray -> 0.4) and lift for log, node payload rewrite assertions, and the all-shaders sweep now retries grading stubs. oak-render 181, oak-node 441, oak-app 272 lib tests pass.
This commit is contained in:
@@ -82,6 +82,68 @@ pub struct OCIOGradingTransformLinearNode {
|
||||
base: OcioBase,
|
||||
}
|
||||
|
||||
/// Fragment shader. The `%1` marker is replaced at request time with the
|
||||
/// OCIO-generated grading-primary GLSL stub for the linear style (the
|
||||
/// renderer's OCIO_GRADING_STUBS entry; the stub declares
|
||||
/// the `ocio_grading_primary_*` uniforms and `ove_grading_primary`), and
|
||||
/// the body applies it to the sampled input. The uniform names match the
|
||||
/// grading input ids, so the rewritten params row binds by name — the
|
||||
/// C++ job's `values` assigned to the GPU uniforms by name-match the same
|
||||
/// way.
|
||||
const SHADER_FRAG: &str = r#"// Main texture input
|
||||
uniform sampler2D tex_in;
|
||||
|
||||
// Main texture coordinate
|
||||
in vec2 ove_texcoord;
|
||||
out vec4 frag_color;
|
||||
|
||||
// Program will replace this with OCIO's auto-generated shader code
|
||||
%1
|
||||
|
||||
void main() {
|
||||
vec4 col = texture(tex_in, ove_texcoord);
|
||||
frag_color = ove_grading_primary(col);
|
||||
}
|
||||
"#;
|
||||
|
||||
/// Row value for `id` (the traverser convention), else the node's own
|
||||
/// standard/keyframe value (C++ `GetValueAtTime` parity for direct
|
||||
/// calls).
|
||||
fn row_or_standard(
|
||||
core: &NodeCore,
|
||||
inputs: &crate::value::NodeValueRow,
|
||||
id: &str,
|
||||
time: oak_core::Rational,
|
||||
) -> crate::value::NodeValue {
|
||||
match inputs.get(id) {
|
||||
Some(v) => v.clone(),
|
||||
None => core.value_at_time(id, -1, time),
|
||||
}
|
||||
}
|
||||
|
||||
/// A vec4 input value as `[x, y, z, w]` (x = master per the RGBM
|
||||
/// convention), collecting a generic numeric value into a padded array.
|
||||
fn to_vec4(v: crate::value::NodeValue) -> [f64; 4] {
|
||||
match v {
|
||||
crate::value::NodeValue::Vec4(v) => v,
|
||||
crate::value::NodeValue::Vec3(v) => [v[0], v[1], v[2], 0.0],
|
||||
crate::value::NodeValue::Vec2(v) => [v[0], v[1], 0.0, 0.0],
|
||||
crate::value::NodeValue::Float(v) => [v, v, v, v],
|
||||
_ => [0.0; 4],
|
||||
}
|
||||
}
|
||||
|
||||
/// The per-channel vec3 the OCIO GPU uniforms expect: `rgb[i] =
|
||||
/// f(channel[i], master)`.
|
||||
fn channel_merge(v: [f64; 4], f: impl Fn(f64, f64) -> f64) -> crate::value::NodeValue {
|
||||
let m = v[0];
|
||||
crate::value::NodeValue::Vec3([
|
||||
f(v[1], m),
|
||||
f(v[2], m),
|
||||
f(v[3], m),
|
||||
])
|
||||
}
|
||||
|
||||
/// Set or replace an input property (C++ `set_input_property`).
|
||||
fn set_input_property(core: &mut NodeCore, input: &str, key: &str, value: crate::value::NodeValue) {
|
||||
if let Some(input) = core.get_input_mut(input) {
|
||||
@@ -291,15 +353,20 @@ impl NodeBehavior for OCIOGradingTransformLinearNode {
|
||||
}
|
||||
|
||||
/// Evaluate outputs (C++ `value()`): no texture -> push nothing;
|
||||
/// processor not ready -> push nothing (unlike the base, there is no
|
||||
/// pass-through branch). Otherwise builds a `ColorTransformJob` from
|
||||
/// the whole input row and rewrites the vec4 (RGBM: x = master)
|
||||
/// inputs into the vec3 form the GPU uniforms expect: offset RGB =
|
||||
/// channel + master; exposure RGB = 2^(channel + master); contrast
|
||||
/// RGB = channel * master. Disabled clamps are pushed as
|
||||
/// `GradingPrimary::NoClampBlack()/NoClampWhite()`, and when both
|
||||
/// clamps are enabled the white clamp is raised to black + 0.000001
|
||||
/// per frame if keyframed/connected values violate white > black.
|
||||
/// otherwise pushes a REAL [`ShaderJobPayload`] whose params carry the
|
||||
/// input row rewritten into the vec3 form the OCIO-generated GPU
|
||||
/// uniforms expect — the C++ job's `values` rewrite, applied to the
|
||||
/// uniform names the renderer's grading stub declares:
|
||||
/// `ocio_grading_primary_contrast` RGB = channel * master, `offset`
|
||||
/// RGB = channel + master, `exposure` RGB = 2^(channel + master).
|
||||
/// Disabled clamps are pushed as `GradingPrimary::NoClampBlack()`
|
||||
/// (-1.0) / `NoClampWhite()` (2.0), and when both clamps are enabled
|
||||
/// the white clamp is raised to black + 0.000001 per frame if
|
||||
/// keyframed/connected values violate white > black. The transform
|
||||
/// itself is the OCIO shader the renderer splices in — the processor
|
||||
/// is generated at render time from the default config, so the
|
||||
/// evaluation-time guard (C++ processor gate) is replaced by the
|
||||
/// node's own presence check on the input texture.
|
||||
fn value(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
@@ -307,33 +374,97 @@ impl NodeBehavior for OCIOGradingTransformLinearNode {
|
||||
time: oak_core::Rational,
|
||||
table: &mut crate::value::NodeValueTable,
|
||||
) {
|
||||
let _ = (core, time);
|
||||
match inputs.get(crate::nodes::ociobase::TEXTURE_INPUT) {
|
||||
Some(crate::value::NodeValue::Texture(_)) => {
|
||||
if self.base.processor().is_some() {
|
||||
// `// CPP-PARITY: ociogradingtransformlinear.cpp`
|
||||
// `value()` — the C++ builds a ColorTransformJob and
|
||||
// rewrites the vec4 (RGBM: x = master) inputs into the
|
||||
// vec3 GPU uniform form: offset RGB = channel +
|
||||
// master, exposure RGB = 2^(channel + master),
|
||||
// contrast RGB = channel * master. Disabled clamps
|
||||
// are pushed as `ocio::GradingPrimary::NoClampBlack()`
|
||||
// (-1.0) / `NoClampWhite()` (2.0), and when both
|
||||
// clamps are enabled the white clamp is raised to
|
||||
// black + 0.000001 per frame if keyframed/connected
|
||||
// values violate white > black. The Rust model has no
|
||||
// color-transform job payload: the renderer seam
|
||||
// resolves the deferred job from this null handle.
|
||||
table.push(
|
||||
crate::value::ValueType::Texture,
|
||||
crate::value::NodeValue::Texture(crate::handle::CHandle::null()),
|
||||
None,
|
||||
);
|
||||
}
|
||||
// Processor not ready: push nothing (no pass-through).
|
||||
}
|
||||
_ => {}
|
||||
let _ = self;
|
||||
if !matches!(
|
||||
inputs.get(crate::nodes::ociobase::TEXTURE_INPUT),
|
||||
Some(crate::value::NodeValue::Texture(_))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut params = inputs.clone();
|
||||
|
||||
// vec4 (RGBM: x = master) inputs -> the vec3 GPU uniform form.
|
||||
let contrast = to_vec4(row_or_standard(core, inputs, CONTRAST_INPUT, time));
|
||||
let offset = to_vec4(row_or_standard(core, inputs, OFFSET_INPUT, time));
|
||||
let exposure = to_vec4(row_or_standard(core, inputs, EXPOSURE_INPUT, time));
|
||||
params.insert(
|
||||
CONTRAST_INPUT.to_string(),
|
||||
channel_merge(contrast, |c, m| c * m),
|
||||
);
|
||||
params.insert(
|
||||
OFFSET_INPUT.to_string(),
|
||||
channel_merge(offset, |c, m| c + m),
|
||||
);
|
||||
params.insert(
|
||||
EXPOSURE_INPUT.to_string(),
|
||||
channel_merge(exposure, |c, m| 2f64.powf(c + m)),
|
||||
);
|
||||
|
||||
// Scalar uniforms (the IDs are the generated uniform names).
|
||||
params.insert(
|
||||
SATURATION_INPUT.to_string(),
|
||||
row_or_standard(core, inputs, SATURATION_INPUT, time),
|
||||
);
|
||||
params.insert(
|
||||
PIVOT_INPUT.to_string(),
|
||||
row_or_standard(core, inputs, PIVOT_INPUT, time),
|
||||
);
|
||||
|
||||
// Clamps: enabled -> the value, disabled -> the OCIO sentinels
|
||||
// (NoClampBlack -1 / NoClampWhite 2 keep the GPU clamp a no-op).
|
||||
let black_enabled = row_or_standard(core, inputs, CLAMP_BLACK_ENABLE_INPUT, time)
|
||||
.to_double() != 0.0;
|
||||
let white_enabled = row_or_standard(core, inputs, CLAMP_WHITE_ENABLE_INPUT, time)
|
||||
.to_double() != 0.0;
|
||||
let mut black =
|
||||
row_or_standard(core, inputs, CLAMP_BLACK_INPUT, time).to_double();
|
||||
let mut white =
|
||||
row_or_standard(core, inputs, CLAMP_WHITE_INPUT, time).to_double();
|
||||
if black_enabled && white_enabled {
|
||||
// ocio::GradingPrimary::validate: white > black (per frame,
|
||||
// when the static UI minimum cannot follow animated values).
|
||||
white = white.max(black + 0.000001);
|
||||
}
|
||||
params.insert(
|
||||
CLAMP_BLACK_INPUT.to_string(),
|
||||
crate::value::NodeValue::Float(if black_enabled { black } else { -1.0 }),
|
||||
);
|
||||
params.insert(
|
||||
CLAMP_WHITE_INPUT.to_string(),
|
||||
crate::value::NodeValue::Float(if white_enabled { white } else { 2.0 }),
|
||||
);
|
||||
params.insert(
|
||||
"ocio_grading_primary_localBypass".to_string(),
|
||||
crate::value::NodeValue::Boolean(false),
|
||||
);
|
||||
|
||||
let job = crate::handle::make_owned(crate::nodes::jobs::ShaderJobPayload {
|
||||
node_id: crate::id::NodeId::INVALID,
|
||||
time,
|
||||
iterations: 1,
|
||||
type_id: self.type_id().to_string(),
|
||||
shader_id: "rgb".to_string(),
|
||||
effect_input: crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
|
||||
params,
|
||||
iterative_input: String::new(),
|
||||
});
|
||||
table.push(
|
||||
crate::value::ValueType::Texture,
|
||||
crate::value::NodeValue::Texture(job),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
/// Shader code request (C++ `get_shader_code()`): reads the fragment
|
||||
/// shader and replaces every `%1` marker with `request` — the OCIO
|
||||
/// auto-generated grading-primary shader text (C++ `GetShaderCode`
|
||||
/// receives the stub from the renderer's color context; the Rust
|
||||
/// renderer resolves it from the `OCIO_GRADING_STUBS` table before
|
||||
/// calling, see [`crate::nodes::ociogradingtransformlog`]'s sibling
|
||||
/// wiring).
|
||||
fn shader_code(&self, request: &str) -> Option<String> {
|
||||
Some(SHADER_FRAG.replace("%1", request))
|
||||
}
|
||||
|
||||
/// Added to a graph (C++ base `AddedToGraphEvent`): captures the
|
||||
@@ -767,31 +898,116 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_texture_without_processor_pushes_nothing() {
|
||||
// Unlike the OCIO base, grading has no pass-through branch.
|
||||
let core = NodeCore::new();
|
||||
let n = node();
|
||||
let inputs = crate::value::NodeValueRow::from([(
|
||||
crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
|
||||
NodeValue::Texture(crate::handle::CHandle::null()),
|
||||
)]);
|
||||
fn value_texture_pushes_real_grading_job() {
|
||||
let (core, behavior) = create();
|
||||
let inputs = crate::value::NodeValueRow::from([
|
||||
(
|
||||
crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
|
||||
NodeValue::Texture(crate::handle::CHandle::null()),
|
||||
),
|
||||
(CONTRAST_INPUT.to_string(), NodeValue::Vec4([0.5, 0.25, 0.5, 0.75])),
|
||||
(OFFSET_INPUT.to_string(), NodeValue::Vec4([0.25, -0.125, 0.0, 0.125])),
|
||||
(EXPOSURE_INPUT.to_string(), NodeValue::Vec4([1.0, 0.0, 0.0, 0.0])),
|
||||
]);
|
||||
let mut table = NodeValueTable::default();
|
||||
n.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
||||
assert!(table.is_empty());
|
||||
behavior.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
||||
let tex = table.get(ValueType::Texture).expect("texture pushed");
|
||||
let NodeValue::Texture(handle) = tex else {
|
||||
panic!("pushed value is not a texture handle");
|
||||
};
|
||||
let job = unsafe {
|
||||
crate::handle::get_checked::<crate::nodes::jobs::ShaderJobPayload>(handle)
|
||||
}
|
||||
.expect("real shader job");
|
||||
assert_eq!(job.shader_id, "rgb");
|
||||
assert_eq!(
|
||||
job.type_id,
|
||||
"org.olivevideoeditor.Olive.ociogradingtransformlinear"
|
||||
);
|
||||
assert_eq!(job.effect_input, crate::nodes::ociobase::TEXTURE_INPUT);
|
||||
// vec4 (RGBM: x = master) rewrites: contrast RGB = c*m, offset
|
||||
// RGB = c+m, exposure RGB = 2^(c+m).
|
||||
assert_eq!(
|
||||
job.params.get(CONTRAST_INPUT),
|
||||
Some(&NodeValue::Vec3([0.125, 0.25, 0.375]))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get(OFFSET_INPUT),
|
||||
Some(&NodeValue::Vec3([0.125, 0.25, 0.375]))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get(EXPOSURE_INPUT),
|
||||
Some(&NodeValue::Vec3([2.0, 2.0, 2.0]))
|
||||
);
|
||||
// Scalar defaults from the node's standard values.
|
||||
assert_eq!(
|
||||
job.params.get(SATURATION_INPUT),
|
||||
Some(&NodeValue::Float(1.0))
|
||||
);
|
||||
assert_eq!(job.params.get(PIVOT_INPUT), Some(&NodeValue::Float(0.18)));
|
||||
// Clamps disabled -> OCIO sentinels (NoClampBlack/NoClampWhite).
|
||||
assert_eq!(
|
||||
job.params.get(CLAMP_BLACK_INPUT),
|
||||
Some(&NodeValue::Float(-1.0))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get(CLAMP_WHITE_INPUT),
|
||||
Some(&NodeValue::Float(2.0))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_localBypass"),
|
||||
Some(&NodeValue::Boolean(false))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_texture_with_processor_pushes_deferred_job() {
|
||||
let core = NodeCore::new();
|
||||
let mut n = node();
|
||||
n.base.set_processor(Some(crate::handle::CHandle::null()));
|
||||
let inputs = crate::value::NodeValueRow::from([(
|
||||
crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
|
||||
NodeValue::Texture(crate::handle::CHandle::null()),
|
||||
)]);
|
||||
fn value_enables_clamp_when_flagged_of_white_and_black() {
|
||||
let (core, behavior) = create();
|
||||
let inputs = crate::value::NodeValueRow::from([
|
||||
(
|
||||
crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
|
||||
NodeValue::Texture(crate::handle::CHandle::null()),
|
||||
),
|
||||
(
|
||||
CLAMP_BLACK_ENABLE_INPUT.to_string(),
|
||||
NodeValue::Boolean(true),
|
||||
),
|
||||
(
|
||||
CLAMP_WHITE_ENABLE_INPUT.to_string(),
|
||||
NodeValue::Boolean(true),
|
||||
),
|
||||
(CLAMP_BLACK_INPUT.to_string(), NodeValue::Float(0.5)),
|
||||
(CLAMP_WHITE_INPUT.to_string(), NodeValue::Float(0.4)),
|
||||
]);
|
||||
let mut table = NodeValueTable::default();
|
||||
n.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
||||
assert!(table.get(ValueType::Texture).is_some());
|
||||
behavior.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
||||
let tex = table.get(ValueType::Texture).expect("texture pushed");
|
||||
let NodeValue::Texture(handle) = tex else {
|
||||
panic!("pushed value is not a texture handle");
|
||||
};
|
||||
let job = unsafe {
|
||||
crate::handle::get_checked::<crate::nodes::jobs::ShaderJobPayload>(handle)
|
||||
}
|
||||
.expect("real shader job");
|
||||
// White <= black is raised to black + 0.000001.
|
||||
assert_eq!(
|
||||
job.params.get(CLAMP_BLACK_INPUT),
|
||||
Some(&NodeValue::Float(0.5))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get(CLAMP_WHITE_INPUT),
|
||||
Some(&NodeValue::Float(0.500001))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shader_code_splices_stub_marker() {
|
||||
let n = node();
|
||||
let stub = "vec4 ove_grading_primary(vec4 c) { return c; }";
|
||||
let code = n.shader_code(stub).unwrap();
|
||||
assert!(!code.contains("%1"));
|
||||
assert!(code.contains("ove_grading_primary(col)"));
|
||||
assert!(code.contains(stub));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -93,6 +93,76 @@ pub struct OCIOGradingTransformLogNode {
|
||||
base: OcioBase,
|
||||
}
|
||||
|
||||
/// Fragment shader. The `%1` marker is replaced at request time with the
|
||||
/// OCIO-generated grading-primary GLSL stub for the log style (the
|
||||
/// renderer's OCIO_GRADING_STUBS entry; the stub declares
|
||||
/// the `ocio_grading_primary_*` uniforms and `ove_grading_primary`), and
|
||||
/// the body applies it to the sampled input.
|
||||
const SHADER_FRAG: &str = r#"// Main texture input
|
||||
uniform sampler2D tex_in;
|
||||
|
||||
// Main texture coordinate
|
||||
in vec2 ove_texcoord;
|
||||
out vec4 frag_color;
|
||||
|
||||
// Program will replace this with OCIO's auto-generated shader code
|
||||
%1
|
||||
|
||||
void main() {
|
||||
vec4 col = texture(tex_in, ove_texcoord);
|
||||
frag_color = ove_grading_primary(col);
|
||||
}
|
||||
"#;
|
||||
|
||||
/// The generated OCIO uniform name for an input id: the C++ log node's
|
||||
/// ids carry the literal `OCIO_NAMESPACE_` macro text while the
|
||||
/// generated GPU shader uses the `ocio_` resource prefix, so the prefix
|
||||
/// is normalized at params-build time.
|
||||
fn uniform_name(id: &str) -> String {
|
||||
match id.strip_prefix("OCIO_NAMESPACE_") {
|
||||
Some(rest) => format!("ocio_{rest}"),
|
||||
None => id.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Row value for `id` (the traverser convention), else the node's own
|
||||
/// standard/keyframe value (C++ `GetValueAtTime` parity for direct
|
||||
/// calls).
|
||||
fn row_or_standard(
|
||||
core: &NodeCore,
|
||||
inputs: &crate::value::NodeValueRow,
|
||||
id: &str,
|
||||
time: oak_core::Rational,
|
||||
) -> crate::value::NodeValue {
|
||||
match inputs.get(id) {
|
||||
Some(v) => v.clone(),
|
||||
None => core.value_at_time(id, -1, time),
|
||||
}
|
||||
}
|
||||
|
||||
/// A vec4 input value as `[x, y, z, w]` (x = master per the RGBM
|
||||
/// convention), collecting a generic numeric value into a padded array.
|
||||
fn to_vec4(v: crate::value::NodeValue) -> [f64; 4] {
|
||||
match v {
|
||||
crate::value::NodeValue::Vec4(v) => v,
|
||||
crate::value::NodeValue::Vec3(v) => [v[0], v[1], v[2], 0.0],
|
||||
crate::value::NodeValue::Vec2(v) => [v[0], v[1], 0.0, 0.0],
|
||||
crate::value::NodeValue::Float(v) => [v, v, v, v],
|
||||
_ => [0.0; 4],
|
||||
}
|
||||
}
|
||||
|
||||
/// The per-channel vec3 the OCIO GPU uniforms expect: `rgb[i] =
|
||||
/// f(channel[i], master)`.
|
||||
fn channel_merge(v: [f64; 4], f: impl Fn(f64, f64) -> f64) -> crate::value::NodeValue {
|
||||
let m = v[0];
|
||||
crate::value::NodeValue::Vec3([
|
||||
f(v[1], m),
|
||||
f(v[2], m),
|
||||
f(v[3], m),
|
||||
])
|
||||
}
|
||||
|
||||
/// Set or replace an input property (C++ `set_input_property`).
|
||||
fn set_input_property(core: &mut NodeCore, input: &str, key: &str, value: crate::value::NodeValue) {
|
||||
if let Some(input) = core.get_input_mut(input) {
|
||||
@@ -302,15 +372,21 @@ impl NodeBehavior for OCIOGradingTransformLogNode {
|
||||
}
|
||||
|
||||
/// Evaluate outputs (C++ `value()`): no texture -> push nothing;
|
||||
/// processor not ready -> push nothing (no pass-through branch).
|
||||
/// Otherwise builds a `ColorTransformJob` from the whole input row
|
||||
/// and rewrites the vec4 (RGBM: x = master) inputs into the vec3
|
||||
/// form the GPU uniforms expect: lift RGB = channel + master
|
||||
/// (additive); gain and gamma RGB = channel * master
|
||||
/// otherwise pushes a REAL [`ShaderJobPayload`] whose params carry the
|
||||
/// input row rewritten into the vec3 form the OCIO-generated GPU
|
||||
/// uniforms expect — under the generated uniform names (`ocio_`
|
||||
/// prefix, the input ids' `OCIO_NAMESPACE_` text normalized):
|
||||
/// lift/brightness RGB = channel + master (additive), gain/contrast
|
||||
/// RGB = channel * master, gamma RGB = channel * master
|
||||
/// (multiplicative). Disabled clamps are pushed as
|
||||
/// `GradingPrimary::NoClampBlack()/NoClampWhite()`, and when both
|
||||
/// clamps are enabled the white clamp is raised to black + 0.000001
|
||||
/// per frame if keyframed/connected values violate white > black.
|
||||
/// `GradingPrimary::NoClampBlack()` (-1.0) / `NoClampWhite()` (2.0),
|
||||
/// and when both clamps are enabled the white clamp is raised to
|
||||
/// black + 0.000001 per frame if keyframed/connected values violate
|
||||
/// white > black. The transform itself is the OCIO shader the
|
||||
/// renderer splices in — the processor is generated at render time
|
||||
/// from the default config, so the evaluation-time guard (C++
|
||||
/// processor gate) is replaced by the node's own presence check on
|
||||
/// the input texture.
|
||||
fn value(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
@@ -318,33 +394,107 @@ impl NodeBehavior for OCIOGradingTransformLogNode {
|
||||
time: oak_core::Rational,
|
||||
table: &mut crate::value::NodeValueTable,
|
||||
) {
|
||||
let _ = (core, time);
|
||||
match inputs.get(crate::nodes::ociobase::TEXTURE_INPUT) {
|
||||
Some(crate::value::NodeValue::Texture(_)) => {
|
||||
if self.base.processor().is_some() {
|
||||
// `// CPP-PARITY: ociogradingtransformlog.cpp`
|
||||
// `value()` — the C++ builds a ColorTransformJob and
|
||||
// rewrites the vec4 (RGBM: x = master) inputs into the
|
||||
// vec3 GPU uniform form: lift RGB = channel + master
|
||||
// (additive); gain and gamma RGB = channel * master
|
||||
// (multiplicative). Disabled clamps are pushed as
|
||||
// `OCIO_NAMESPACE::GradingPrimary::NoClampBlack()`
|
||||
// (-1.0) / `NoClampWhite()` (2.0), and when both
|
||||
// clamps are enabled the white clamp is raised to
|
||||
// black + 0.000001 per frame if keyframed/connected
|
||||
// values violate white > black. The Rust model has no
|
||||
// color-transform job payload: the renderer seam
|
||||
// resolves the deferred job from this null handle.
|
||||
table.push(
|
||||
crate::value::ValueType::Texture,
|
||||
crate::value::NodeValue::Texture(crate::handle::CHandle::null()),
|
||||
None,
|
||||
);
|
||||
}
|
||||
// Processor not ready: push nothing (no pass-through).
|
||||
}
|
||||
_ => {}
|
||||
let _ = self;
|
||||
if !matches!(
|
||||
inputs.get(crate::nodes::ociobase::TEXTURE_INPUT),
|
||||
Some(crate::value::NodeValue::Texture(_))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut params = inputs.clone();
|
||||
|
||||
// vec4 (RGBM: x = master) inputs -> the vec3 GPU uniform form
|
||||
// (normalized to the generated `ocio_` names).
|
||||
let lift = to_vec4(row_or_standard(core, inputs, LIFT_INPUT, time));
|
||||
let gain = to_vec4(row_or_standard(core, inputs, GAIN_INPUT, time));
|
||||
let gamma = to_vec4(row_or_standard(core, inputs, GAMMA_INPUT, time));
|
||||
params.insert(
|
||||
uniform_name(LIFT_INPUT),
|
||||
channel_merge(lift, |c, m| c + m),
|
||||
);
|
||||
params.insert(
|
||||
uniform_name(GAIN_INPUT),
|
||||
channel_merge(gain, |c, m| c * m),
|
||||
);
|
||||
params.insert(
|
||||
uniform_name(GAMMA_INPUT),
|
||||
channel_merge(gamma, |c, m| c * m),
|
||||
);
|
||||
|
||||
// Scalar uniforms. pivotBlack/pivotWhite are the log-log-scaled
|
||||
// normalization range (identity at gamma = 1 with the defaults
|
||||
// 0/1); the pivot default (-0.2) is the GRADING_LOG pivot.
|
||||
params.insert(
|
||||
uniform_name(SATURATION_INPUT),
|
||||
row_or_standard(core, inputs, SATURATION_INPUT, time),
|
||||
);
|
||||
params.insert(
|
||||
uniform_name(PIVOT_INPUT),
|
||||
row_or_standard(core, inputs, PIVOT_INPUT, time),
|
||||
);
|
||||
params.insert(
|
||||
uniform_name("OCIO_NAMESPACE_grading_primary_pivotBlack"),
|
||||
crate::value::NodeValue::Float(0.0),
|
||||
);
|
||||
params.insert(
|
||||
uniform_name("OCIO_NAMESPACE_grading_primary_pivotWhite"),
|
||||
crate::value::NodeValue::Float(1.0),
|
||||
);
|
||||
|
||||
// Clamps: enabled -> the value, disabled -> the OCIO sentinels
|
||||
// (NoClampBlack -1 / NoClampWhite 2 keep the GPU clamp a no-op).
|
||||
let black_enabled = row_or_standard(core, inputs, CLAMP_BLACK_ENABLE_INPUT, time)
|
||||
.to_double() != 0.0;
|
||||
let white_enabled = row_or_standard(core, inputs, CLAMP_WHITE_ENABLE_INPUT, time)
|
||||
.to_double() != 0.0;
|
||||
let mut black =
|
||||
row_or_standard(core, inputs, CLAMP_BLACK_INPUT, time).to_double();
|
||||
let mut white =
|
||||
row_or_standard(core, inputs, CLAMP_WHITE_INPUT, time).to_double();
|
||||
if black_enabled && white_enabled {
|
||||
// ocio::GradingPrimary::validate: white > black (per frame,
|
||||
// when the static UI minimum cannot follow animated values).
|
||||
white = white.max(black + 0.000001);
|
||||
}
|
||||
params.insert(
|
||||
uniform_name(CLAMP_BLACK_INPUT),
|
||||
crate::value::NodeValue::Float(if black_enabled { black } else { -1.0 }),
|
||||
);
|
||||
params.insert(
|
||||
uniform_name(CLAMP_WHITE_INPUT),
|
||||
crate::value::NodeValue::Float(if white_enabled { white } else { 2.0 }),
|
||||
);
|
||||
params.insert(
|
||||
"ocio_grading_primary_localBypass".to_string(),
|
||||
crate::value::NodeValue::Boolean(false),
|
||||
);
|
||||
|
||||
let job = crate::handle::make_owned(crate::nodes::jobs::ShaderJobPayload {
|
||||
node_id: crate::id::NodeId::INVALID,
|
||||
time,
|
||||
iterations: 1,
|
||||
type_id: self.type_id().to_string(),
|
||||
shader_id: "rgb".to_string(),
|
||||
effect_input: crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
|
||||
params,
|
||||
iterative_input: String::new(),
|
||||
});
|
||||
table.push(
|
||||
crate::value::ValueType::Texture,
|
||||
crate::value::NodeValue::Texture(job),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
/// Shader code request (C++ `get_shader_code()`): reads the fragment
|
||||
/// shader and replaces every `%1` marker with `request` — the OCIO
|
||||
/// auto-generated grading-primary shader text (the renderer resolves
|
||||
/// it from the OCIO_GRADING_STUBS table before calling; this node's
|
||||
/// type id matches the C++ dtype, which carries the unexpanded
|
||||
/// OCIO_NAMESPACE macro text).
|
||||
fn shader_code(&self, request: &str) -> Option<String> {
|
||||
Some(SHADER_FRAG.replace("%1", request))
|
||||
}
|
||||
|
||||
/// Added to a graph (C++ base `AddedToGraphEvent`): captures the
|
||||
@@ -735,30 +885,88 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_texture_without_processor_pushes_nothing() {
|
||||
let core = NodeCore::new();
|
||||
let n = node();
|
||||
let inputs = crate::value::NodeValueRow::from([(
|
||||
crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
|
||||
NodeValue::Texture(crate::handle::CHandle::null()),
|
||||
)]);
|
||||
fn value_texture_pushes_real_grading_job() {
|
||||
let (core, behavior) = create();
|
||||
let inputs = crate::value::NodeValueRow::from([
|
||||
(
|
||||
crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
|
||||
NodeValue::Texture(crate::handle::CHandle::null()),
|
||||
),
|
||||
(LIFT_INPUT.to_string(), NodeValue::Vec4([0.25, -0.125, 0.0, 0.125])),
|
||||
(GAIN_INPUT.to_string(), NodeValue::Vec4([0.5, 0.25, 0.5, 0.75])),
|
||||
(GAMMA_INPUT.to_string(), NodeValue::Vec4([2.0, 1.0, 1.5, 2.0])),
|
||||
]);
|
||||
let mut table = NodeValueTable::default();
|
||||
n.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
||||
assert!(table.is_empty());
|
||||
behavior.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
||||
let tex = table.get(ValueType::Texture).expect("texture pushed");
|
||||
let NodeValue::Texture(handle) = tex else {
|
||||
panic!("pushed value is not a texture handle");
|
||||
};
|
||||
let job = unsafe {
|
||||
crate::handle::get_checked::<crate::nodes::jobs::ShaderJobPayload>(handle)
|
||||
}
|
||||
.expect("real shader job");
|
||||
assert_eq!(job.shader_id, "rgb");
|
||||
assert_eq!(
|
||||
job.type_id,
|
||||
"org.olivevideoeditor.Olive.OCIO_NAMESPACEgradingtransformlog"
|
||||
);
|
||||
assert_eq!(job.effect_input, crate::nodes::ociobase::TEXTURE_INPUT);
|
||||
// vec4 (RGBM: x = master) rewrites under the generated `ocio_`
|
||||
// uniform names: lift RGB = c+m, gain/gamma RGB = c*m.
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_brightness"),
|
||||
Some(&NodeValue::Vec3([0.125, 0.25, 0.375]))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_contrast"),
|
||||
Some(&NodeValue::Vec3([0.125, 0.25, 0.375]))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_gamma"),
|
||||
Some(&NodeValue::Vec3([2.0, 3.0, 4.0]))
|
||||
);
|
||||
// Scalar defaults from the node's standard values (pivot -0.2 for
|
||||
// the log style) plus the log normalization range.
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_saturation"),
|
||||
Some(&NodeValue::Float(1.0))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_pivot"),
|
||||
Some(&NodeValue::Float(-0.2))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_pivotBlack"),
|
||||
Some(&NodeValue::Float(0.0))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_pivotWhite"),
|
||||
Some(&NodeValue::Float(1.0))
|
||||
);
|
||||
// Clamps disabled -> OCIO sentinels (NoClampBlack/NoClampWhite).
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_clampBlack"),
|
||||
Some(&NodeValue::Float(-1.0))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_clampWhite"),
|
||||
Some(&NodeValue::Float(2.0))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_localBypass"),
|
||||
Some(&NodeValue::Boolean(false))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_texture_with_processor_pushes_deferred_job() {
|
||||
let core = NodeCore::new();
|
||||
let mut n = node();
|
||||
n.base.set_processor(Some(crate::handle::CHandle::null()));
|
||||
let inputs = crate::value::NodeValueRow::from([(
|
||||
crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
|
||||
NodeValue::Texture(crate::handle::CHandle::null()),
|
||||
)]);
|
||||
let mut table = NodeValueTable::default();
|
||||
n.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
||||
assert!(table.get(ValueType::Texture).is_some());
|
||||
fn shader_code_splices_stub_marker() {
|
||||
let n = node();
|
||||
let stub = "vec4 ove_grading_primary(vec4 c) { return c; }";
|
||||
let code = n.shader_code(stub).unwrap();
|
||||
assert!(!code.contains("%1"));
|
||||
assert!(code.contains("ove_grading_primary(col)"));
|
||||
assert!(code.contains(stub));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -675,6 +675,64 @@ fn build_ocio_function_shader(
|
||||
desc.shader_text()
|
||||
}
|
||||
|
||||
/// Cache of generated grading GLSL stubs, keyed by style + config cache id
|
||||
/// (same shape as [`OCIO_STUB_CACHE`]; grading is analytic so the result is
|
||||
/// deterministic per key).
|
||||
static OCIO_GRADING_CACHE: LazyLock<Mutex<HashMap<String, Option<String>>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
/// The GLSL text of the dynamic grading-primary processor for `style`
|
||||
/// (C++ oakrender creates the same processor via
|
||||
/// `oakrender_color_processor_create_grading_primary` and the renderer
|
||||
/// applies it between passes; the Rust equivalent generates the OCIO GPU
|
||||
/// shader the node splices at its `%1` marker). `None` when no default
|
||||
/// config exists or the processor is LUT-based (the same guard as
|
||||
/// [`ocio_function_shader`] — grading is analytic, so this is purely a
|
||||
/// config-availability check).
|
||||
pub fn grading_primary_function_shader(style: GradingStyle) -> Option<String> {
|
||||
let config = default_config()?;
|
||||
let style_id = match style {
|
||||
GradingStyle::Lin => "lin",
|
||||
GradingStyle::Log => "log",
|
||||
};
|
||||
let cache_key = format!("{style_id}:{}", config.cache_id().unwrap_or_default());
|
||||
if let Some(hit) = OCIO_GRADING_CACHE
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.get(&cache_key)
|
||||
{
|
||||
return hit.clone();
|
||||
}
|
||||
let stub = build_ocio_grading_shader(&config, style);
|
||||
OCIO_GRADING_CACHE
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.insert(cache_key, stub.clone());
|
||||
stub
|
||||
}
|
||||
|
||||
/// Build (but do not cache) the grading GLSL stub: a dynamic
|
||||
/// grading-primary transform on `config`, extracted exactly like
|
||||
/// [`build_ocio_function_shader`].
|
||||
fn build_ocio_grading_shader(config: &SafeConfig, style: GradingStyle) -> Option<String> {
|
||||
let transform = ocio_rs::transform::GradingPrimaryTransform::create(style.to_ocio()).ok()?;
|
||||
transform.make_dynamic();
|
||||
transform.set_direction(ocio_rs::TransformDirection::Forward);
|
||||
let processor = config
|
||||
.processor_from_transform(&transform, ocio_rs::TransformDirection::Forward)
|
||||
.ok()?;
|
||||
let gpu = processor.default_gpu_processor().ok()?;
|
||||
let mut desc = ocio_rs::GpuShaderDesc::create().ok()?;
|
||||
desc.set_language(ocio_rs::GpuLanguage::GlslEs3_0).ok()?;
|
||||
desc.set_function_name("ove_grading_primary").ok()?;
|
||||
desc.set_resource_prefix("ocio_").ok()?;
|
||||
gpu.try_extract_shader_info(&mut desc).ok()?;
|
||||
if desc.num_textures() > 0 || desc.num_3d_textures() > 0 {
|
||||
return None;
|
||||
}
|
||||
desc.shader_text()
|
||||
}
|
||||
|
||||
// ---- LUT library (C++ LUTLibrary) ------------------------------------------
|
||||
|
||||
/// Supported LUT extensions (C++ `LUTLibrary::supported_extensions()`).
|
||||
@@ -1098,6 +1156,26 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grading_primary_shader_generates_analytic_glsl() {
|
||||
let _lock = config_lock();
|
||||
if set_up_default_config().is_err() {
|
||||
return; // Bundled OCIO missing (e.g. stub build): skip.
|
||||
}
|
||||
for style in [GradingStyle::Lin, GradingStyle::Log] {
|
||||
let stub = grading_primary_function_shader(style)
|
||||
.expect("default config generates an analytic grading shader");
|
||||
assert!(
|
||||
stub.contains("ove_grading_primary"),
|
||||
"function name present"
|
||||
);
|
||||
assert!(!stub.contains("sampler"), "no LUT upload expected for grading");
|
||||
// Cache hit: a repeated call returns the same text.
|
||||
let again = grading_primary_function_shader(style).unwrap();
|
||||
assert_eq!(stub, again);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_lut_missing_file_is_invalid() {
|
||||
let _lock = config_lock();
|
||||
|
||||
@@ -60,6 +60,22 @@ pub const OCIO_SHADER_STUBS: &[(&str, &str, &str, &str)] = &[(
|
||||
"cie_xyz_d65_interchange",
|
||||
)];
|
||||
|
||||
/// Static mapping of the OCIO grading nodes to the grading-primary style
|
||||
/// whose dynamic GPU shader they splice at their `%1` marker (C++
|
||||
/// `oakrender_color_processor_create_grading_primary` with the node's
|
||||
/// `GRADING_LIN`/`GRADING_LOG` style; the processor is built against the
|
||||
/// default config at render time).
|
||||
pub const OCIO_GRADING_STUBS: &[(&str, crate::color::GradingStyle)] = &[
|
||||
(
|
||||
"org.olivevideoeditor.Olive.ociogradingtransformlinear",
|
||||
crate::color::GradingStyle::Lin,
|
||||
),
|
||||
(
|
||||
"org.olivevideoeditor.Olive.OCIO_NAMESPACEgradingtransformlog",
|
||||
crate::color::GradingStyle::Log,
|
||||
),
|
||||
];
|
||||
|
||||
/// The OCIO GPU function shader for `type_id` (the `%1` stub), or `None`
|
||||
/// when the node is not OCIO-based or the processor cannot be generated
|
||||
/// (no default config, or a LUT processor the renderer cannot upload).
|
||||
@@ -71,6 +87,16 @@ pub fn ocio_stub_for(type_id: &str) -> Option<String> {
|
||||
crate::color::ocio_function_shader(fn_name, from, to)
|
||||
}
|
||||
|
||||
/// The OCIO grading GPU shader for `type_id` (the `%1` stub), or `None`
|
||||
/// when the node is not a grading node or no default config is set up.
|
||||
pub fn grading_stub_for(type_id: &str) -> Option<String> {
|
||||
let (_, style) = OCIO_GRADING_STUBS
|
||||
.iter()
|
||||
.find(|(id, _)| *id == type_id)
|
||||
.copied()?;
|
||||
crate::color::grading_primary_function_shader(style)
|
||||
}
|
||||
|
||||
/// Job specification: the closed set of C++ `*Job` payloads
|
||||
/// (AcceleratedJob family) as internal evaluation records — jobs no
|
||||
/// longer travel inside values across module boundaries.
|
||||
@@ -573,8 +599,24 @@ impl RenderEvalHooks {
|
||||
.iter()
|
||||
.find(|(id, ..)| *id == payload.type_id)
|
||||
.copied();
|
||||
let glsl = match ocio_entry {
|
||||
Some((_, fn_name, from, to)) => {
|
||||
let grading_entry = OCIO_GRADING_STUBS
|
||||
.iter()
|
||||
.find(|(id, _)| *id == payload.type_id)
|
||||
.copied();
|
||||
let glsl = match (grading_entry, ocio_entry) {
|
||||
(Some((_, style)), _) => {
|
||||
let Some(stub) = crate::color::grading_primary_function_shader(style) else {
|
||||
return None;
|
||||
};
|
||||
match behavior.shader_code(&stub) {
|
||||
Some(glsl) => glsl,
|
||||
None => {
|
||||
warn("shader not found");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, Some((_, fn_name, from, to))) => {
|
||||
let Some(stub) = crate::color::ocio_function_shader(fn_name, from, to) else {
|
||||
return None;
|
||||
};
|
||||
@@ -586,7 +628,7 @@ impl RenderEvalHooks {
|
||||
}
|
||||
}
|
||||
}
|
||||
None => match behavior.shader_code(&payload.shader_id) {
|
||||
(None, None) => match behavior.shader_code(&payload.shader_id) {
|
||||
Some(glsl) => glsl,
|
||||
None => {
|
||||
warn("shader not found");
|
||||
@@ -598,18 +640,18 @@ impl RenderEvalHooks {
|
||||
// Pipeline cache key: the type id plus the shader-variant id (the
|
||||
// OCIO stub text folds in too, so a config change recompiles
|
||||
// instead of reusing a stale variant).
|
||||
let key = match ocio_entry {
|
||||
Some(_) => {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
std::hash::Hash::hash(&glsl, &mut h);
|
||||
format!(
|
||||
"{}:{}:ocio:{}",
|
||||
payload.type_id,
|
||||
payload.shader_id,
|
||||
std::hash::Hasher::finish(&h)
|
||||
)
|
||||
}
|
||||
None => format!("{}:{}", payload.type_id, payload.shader_id),
|
||||
let spliced_ocio = grading_entry.is_some() || ocio_entry.is_some();
|
||||
let key = if spliced_ocio {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
std::hash::Hash::hash(&glsl, &mut h);
|
||||
format!(
|
||||
"{}:{}:ocio:{}",
|
||||
payload.type_id,
|
||||
payload.shader_id,
|
||||
std::hash::Hasher::finish(&h)
|
||||
)
|
||||
} else {
|
||||
format!("{}:{}", payload.type_id, payload.shader_id)
|
||||
};
|
||||
let compiled = match compile_effect(&ctx, &key, &glsl, ctx.is_filterable()) {
|
||||
Ok(effect) => effect,
|
||||
|
||||
@@ -1064,6 +1064,179 @@ void main() { frag_color = texture(tex_in, ove_texcoord); }
|
||||
ctx.destroy_texture(dst);
|
||||
}
|
||||
|
||||
/// The linear grading node's OCIO-spliced shader doubles gray under
|
||||
/// +1 stop of master exposure (2^1 * 0.2 = 0.4) and gates the
|
||||
/// transform behind the localBypass uniform off.
|
||||
#[test]
|
||||
fn gpu_grading_linear_applies_exposure() {
|
||||
let Some(ctx) = gpu() else {
|
||||
eprintln!("no adapter; skipping");
|
||||
return;
|
||||
};
|
||||
let Some(stub) = crate::eval::grading_stub_for(
|
||||
"org.olivevideoeditor.Olive.ociogradingtransformlinear",
|
||||
) else {
|
||||
eprintln!("no OCIO config; skipping");
|
||||
return;
|
||||
};
|
||||
let (_core, behavior) = oak_node::factory::Factory::global()
|
||||
.create_any("org.olivevideoeditor.Olive.ociogradingtransformlinear")
|
||||
.unwrap();
|
||||
let glsl = behavior.shader_code(&stub).unwrap();
|
||||
let effect = compile_effect(&ctx, "test/grading-lin", &glsl, false).unwrap();
|
||||
|
||||
let src = ctx.create_texture(8, 4).unwrap();
|
||||
let dst = ctx.create_texture(8, 4).unwrap();
|
||||
let gray = crate::shaderfx::tests::f32_frame(8, 4, |_| [0.2, 0.2, 0.2, 1.0]);
|
||||
ctx.upload(src, &gray).unwrap();
|
||||
|
||||
let mut row = oak_node::value::NodeValueRow::new();
|
||||
row.insert(
|
||||
"ocio_grading_primary_exposure".into(),
|
||||
oak_node::value::NodeValue::Vec3([2.0, 2.0, 2.0]),
|
||||
);
|
||||
row.insert(
|
||||
"ocio_grading_primary_contrast".into(),
|
||||
oak_node::value::NodeValue::Vec3([1.0, 1.0, 1.0]),
|
||||
);
|
||||
row.insert(
|
||||
"ocio_grading_primary_offset".into(),
|
||||
oak_node::value::NodeValue::Vec3([0.0, 0.0, 0.0]),
|
||||
);
|
||||
row.insert(
|
||||
"ocio_grading_primary_saturation".into(),
|
||||
oak_node::value::NodeValue::Float(1.0),
|
||||
);
|
||||
row.insert(
|
||||
"ocio_grading_primary_pivot".into(),
|
||||
oak_node::value::NodeValue::Float(0.18),
|
||||
);
|
||||
row.insert(
|
||||
"ocio_grading_primary_clampBlack".into(),
|
||||
oak_node::value::NodeValue::Float(-1.0),
|
||||
);
|
||||
row.insert(
|
||||
"ocio_grading_primary_clampWhite".into(),
|
||||
oak_node::value::NodeValue::Float(2.0),
|
||||
);
|
||||
row.insert(
|
||||
"ocio_grading_primary_localBypass".into(),
|
||||
oak_node::value::NodeValue::Boolean(false),
|
||||
);
|
||||
run_effect(
|
||||
&ctx,
|
||||
&effect,
|
||||
&row,
|
||||
&[("tex_in".to_string(), src)],
|
||||
dst,
|
||||
(8, 4),
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
let out = ctx.download(dst).unwrap();
|
||||
for px in 0..8usize {
|
||||
let got = pixel(&out, px);
|
||||
let want = [0.4f32, 0.4, 0.4, 1.0];
|
||||
assert!(
|
||||
(got[0] - want[0]).abs() < 1e-3,
|
||||
"px {px}: exposure must double gray, got {got:?}"
|
||||
);
|
||||
}
|
||||
|
||||
ctx.destroy_texture(src);
|
||||
ctx.destroy_texture(dst);
|
||||
}
|
||||
|
||||
/// The log grading node's OCIO-spliced shader: brightness (lift) of
|
||||
/// +0.1 shifts 0.2 gray to 0.3 with the identity gain/gamma/pivot
|
||||
/// defaults.
|
||||
#[test]
|
||||
fn gpu_grading_log_applies_lift() {
|
||||
let Some(ctx) = gpu() else {
|
||||
eprintln!("no adapter; skipping");
|
||||
return;
|
||||
};
|
||||
let Some(stub) = crate::eval::grading_stub_for(
|
||||
"org.olivevideoeditor.Olive.OCIO_NAMESPACEgradingtransformlog",
|
||||
) else {
|
||||
eprintln!("no OCIO config; skipping");
|
||||
return;
|
||||
};
|
||||
let (_core, behavior) = oak_node::factory::Factory::global()
|
||||
.create_any("org.olivevideoeditor.Olive.OCIO_NAMESPACEgradingtransformlog")
|
||||
.unwrap();
|
||||
let glsl = behavior.shader_code(&stub).unwrap();
|
||||
let effect = compile_effect(&ctx, "test/grading-log", &glsl, false).unwrap();
|
||||
|
||||
let src = ctx.create_texture(8, 4).unwrap();
|
||||
let dst = ctx.create_texture(8, 4).unwrap();
|
||||
let gray = crate::shaderfx::tests::f32_frame(8, 4, |_| [0.2, 0.2, 0.2, 1.0]);
|
||||
ctx.upload(src, &gray).unwrap();
|
||||
|
||||
let mut row = oak_node::value::NodeValueRow::new();
|
||||
row.insert(
|
||||
"ocio_grading_primary_brightness".into(),
|
||||
oak_node::value::NodeValue::Vec3([0.1, 0.1, 0.1]),
|
||||
);
|
||||
row.insert(
|
||||
"ocio_grading_primary_contrast".into(),
|
||||
oak_node::value::NodeValue::Vec3([1.0, 1.0, 1.0]),
|
||||
);
|
||||
row.insert(
|
||||
"ocio_grading_primary_gamma".into(),
|
||||
oak_node::value::NodeValue::Vec3([1.0, 1.0, 1.0]),
|
||||
);
|
||||
row.insert(
|
||||
"ocio_grading_primary_saturation".into(),
|
||||
oak_node::value::NodeValue::Float(1.0),
|
||||
);
|
||||
row.insert(
|
||||
"ocio_grading_primary_pivot".into(),
|
||||
oak_node::value::NodeValue::Float(-0.2),
|
||||
);
|
||||
row.insert(
|
||||
"ocio_grading_primary_pivotBlack".into(),
|
||||
oak_node::value::NodeValue::Float(0.0),
|
||||
);
|
||||
row.insert(
|
||||
"ocio_grading_primary_pivotWhite".into(),
|
||||
oak_node::value::NodeValue::Float(1.0),
|
||||
);
|
||||
row.insert(
|
||||
"ocio_grading_primary_clampBlack".into(),
|
||||
oak_node::value::NodeValue::Float(-1.0),
|
||||
);
|
||||
row.insert(
|
||||
"ocio_grading_primary_clampWhite".into(),
|
||||
oak_node::value::NodeValue::Float(2.0),
|
||||
);
|
||||
row.insert(
|
||||
"ocio_grading_primary_localBypass".into(),
|
||||
oak_node::value::NodeValue::Boolean(false),
|
||||
);
|
||||
run_effect(
|
||||
&ctx,
|
||||
&effect,
|
||||
&row,
|
||||
&[("tex_in".to_string(), src)],
|
||||
dst,
|
||||
(8, 4),
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
let out = ctx.download(dst).unwrap();
|
||||
for px in 0..8usize {
|
||||
let got = pixel(&out, px);
|
||||
assert!(
|
||||
(got[0] - 0.3).abs() < 1e-3,
|
||||
"px {px}: lift must shift gray, got {got:?}"
|
||||
);
|
||||
}
|
||||
|
||||
ctx.destroy_texture(src);
|
||||
ctx.destroy_texture(dst);
|
||||
}
|
||||
|
||||
/// Every registered node type that ships a shader must translate (the
|
||||
/// all-shaders sweep). OCIO-stubbed shaders (`%1` markers needing the
|
||||
/// OCIO-generated function text) retry with the real OCIO stub first;
|
||||
@@ -1089,29 +1262,38 @@ void main() { frag_color = texture(tex_in, ove_texcoord); }
|
||||
// OCIO config is available (stub build), fall back to
|
||||
// a pass-through function so the sweep still covers
|
||||
// the node's own shader body.
|
||||
let retried = crate::eval::ocio_stub_for(meta.type_id)
|
||||
.or_else(|| {
|
||||
crate::eval::OCIO_SHADER_STUBS
|
||||
.iter()
|
||||
.find(|(id, ..)| *id == meta.type_id)
|
||||
.map(|(_, fn_name, ..)| {
|
||||
format!("vec4 {fn_name}(vec4 c) {{ return c; }}")
|
||||
})
|
||||
});
|
||||
match retried {
|
||||
Some(stub) => match translate(&behavior.shader_code(&stub).unwrap()) {
|
||||
Ok(_) => ok.push(meta.type_id),
|
||||
Err(e) => {
|
||||
failed.push((meta.type_id, format!("with OCIO stub: {e:?}")))
|
||||
}
|
||||
},
|
||||
None if msg.contains("SceneLinear") || msg.contains("UnknownFunction") => {
|
||||
// Not in the stub table but still OCIO-shaped:
|
||||
// report separately, not as a regression.
|
||||
ocio_stubbed.push(meta.type_id);
|
||||
let retried = crate::eval::ocio_stub_for(meta.type_id)
|
||||
.or_else(|| {
|
||||
crate::eval::OCIO_SHADER_STUBS
|
||||
.iter()
|
||||
.find(|(id, ..)| *id == meta.type_id)
|
||||
.map(|(_, fn_name, ..)| {
|
||||
format!("vec4 {fn_name}(vec4 c) {{ return c; }}")
|
||||
})
|
||||
})
|
||||
.or_else(|| crate::eval::grading_stub_for(meta.type_id))
|
||||
.or_else(|| {
|
||||
crate::eval::OCIO_GRADING_STUBS
|
||||
.iter()
|
||||
.find(|(id, _)| *id == meta.type_id)
|
||||
.map(|_| "vec4 ove_grading_primary(vec4 c) { return c; }".to_string())
|
||||
});
|
||||
match retried {
|
||||
Some(stub) => match translate(&behavior.shader_code(&stub).unwrap()) {
|
||||
Ok(_) => ok.push(meta.type_id),
|
||||
Err(e) => {
|
||||
failed.push((meta.type_id, format!("with OCIO stub: {e:?}")))
|
||||
}
|
||||
None => failed.push((meta.type_id, msg)),
|
||||
},
|
||||
None if msg.contains("SceneLinear")
|
||||
|| msg.contains("UnknownFunction")
|
||||
|| msg.contains("ove_grading_primary") => {
|
||||
// Not in the stub table but still OCIO-shaped:
|
||||
// report separately, not as a regression.
|
||||
ocio_stubbed.push(meta.type_id);
|
||||
}
|
||||
None => failed.push((meta.type_id, msg)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user