nodes: spell shape/despill shader dispatch as if/else chains
naga's WGSL emitter rejects fall-through-capable GLSL switch blocks, so every shape and despill job failed to compile and silently fell back to the effect input - both effects were no-ops. Rewrite the type/method dispatch as if/else chains (same semantics as the C++ shaders) and cover all three shape types plus green-screen despill with GPU pixel tests. Also drop the now-stale nested-payload/merge-binding TODO notes.
This commit is contained in:
@@ -52,9 +52,12 @@ pub const LUMA_COEFFS_INPUT: &str = "luma_coeffs";
|
||||
pub struct DespillNode;
|
||||
|
||||
/// Fragment shader (C++ loads the `:/shaders/despill.frag` resource in
|
||||
/// `get_shader_code`). Text copied verbatim from
|
||||
/// `engine/shaders/despill.frag`. The `luma_coeffs` uniform is not a
|
||||
/// node input — it is injected into the shader job by `value()`.
|
||||
/// `get_shader_code`). Text adapted from
|
||||
/// `engine/shaders/despill.frag`: the C++ `switch (method_in)`
|
||||
/// dispatch is spelled as if/else chains because naga's WGSL emitter
|
||||
/// rejects fall-through-capable GLSL switch blocks. The `luma_coeffs`
|
||||
/// uniform is not a node input — it is injected into the shader job by
|
||||
/// `value()`.
|
||||
const SHADER_FRAG: &str = r#"uniform sampler2D tex_in;
|
||||
uniform int color_in;
|
||||
uniform int method_in;
|
||||
@@ -75,40 +78,30 @@ void main(void) {
|
||||
float color_average = 0.0;
|
||||
|
||||
if(color_in == 0) { // Green screen
|
||||
switch (method_in) {
|
||||
case AVERAGE:
|
||||
if (method_in == AVERAGE) {
|
||||
color_average = dot(tex_col.rb, vec2(0.5)); // (tex_col.r + tex_col.b) / 2.0
|
||||
tex_col.g = tex_col.g > color_average ? color_average: tex_col.g;
|
||||
break;
|
||||
case DOUBLE_RED_AVERAGE:
|
||||
} else if (method_in == DOUBLE_RED_AVERAGE) {
|
||||
color_average = dot(tex_col.rb, vec2(2.0, 1.0) / 3.0); // (2.0 * tex_col.r + tex_col.b) / 3.0
|
||||
tex_col.g = tex_col.g > color_average ? color_average : tex_col.g;
|
||||
break;
|
||||
case DOUBLE_AVERAGE:
|
||||
} else if (method_in == DOUBLE_AVERAGE) {
|
||||
color_average = dot(tex_col.br, vec2(2.0, 1.0) / 3.0); // (2.0 * tex_col.b + tex_col.r) / 3.0
|
||||
tex_col.g = tex_col.g > color_average ? color_average : tex_col.g;
|
||||
break;
|
||||
case BLUE_LIMIT:
|
||||
} else if (method_in == BLUE_LIMIT) {
|
||||
tex_col.g = tex_col.g > tex_col.b ? tex_col.b : tex_col.g;
|
||||
break;
|
||||
}
|
||||
} else { // Blue screen
|
||||
switch (method_in) {
|
||||
case AVERAGE:
|
||||
if (method_in == AVERAGE) {
|
||||
color_average = dot(tex_col.rg, vec2(0.5)); // (tex_col.r + tex_col.g) / 2.0
|
||||
tex_col.b = tex_col.b > color_average ? color_average : tex_col.b;
|
||||
break;
|
||||
case DOUBLE_RED_AVERAGE:
|
||||
} else if (method_in == DOUBLE_RED_AVERAGE) {
|
||||
color_average = dot(tex_col.rg, vec2(2.0, 1.0) / 3.0); // (2.0 * tex_col.r + tex_col.g) / 3.0
|
||||
tex_col.b = tex_col.b > color_average ? color_average : tex_col.b;
|
||||
break;
|
||||
case DOUBLE_AVERAGE:
|
||||
} else if (method_in == DOUBLE_AVERAGE) {
|
||||
color_average = dot(tex_col.gr, vec2(2.0, 1.0) / 3.0); // (2.0 * tex_col.g+ tex_col.r) / 3.0
|
||||
tex_col.b = tex_col.b > color_average ? color_average : tex_col.b;
|
||||
break;
|
||||
case BLUE_LIMIT:
|
||||
} else if (method_in == BLUE_LIMIT) {
|
||||
tex_col.b = tex_col.b > tex_col.g ? tex_col.g : tex_col.b;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -122,9 +122,10 @@ impl NodeBehavior for MergeNode {
|
||||
/// renderer's resolve hook executes and replaces with the result
|
||||
/// texture; the params row carries both input textures, keyed by
|
||||
/// their input ids. The C++ `MergeNode` constructor never sets an
|
||||
/// effect input, so `effect_input` is empty and the runner has no
|
||||
/// main texture to bind — binding `base_in`/`blend_in` explicitly is
|
||||
/// a renderer TODO. The "blend has fewer than 4 channels" check
|
||||
/// effect input, so `effect_input` is empty and the runner binds
|
||||
/// `base_in`/`blend_in` explicitly by name (the pass size follows the
|
||||
/// first bound texture — the base). The "blend has fewer than 4
|
||||
/// channels" check
|
||||
/// needs the texture's channel count, which the Rust texture handle
|
||||
/// does not carry, so the alpha-less blend case is only
|
||||
/// distinguishable by presence here (`// CPP-PARITY: merge.cpp`
|
||||
|
||||
@@ -43,8 +43,10 @@ pub const RADIUS_INPUT: &str = "radius_in";
|
||||
pub struct ShapeNode;
|
||||
|
||||
/// Fragment shader for the `"shape"` shader id (C++ loads
|
||||
/// `:/shaders/shape.frag` in `get_shader_code`). Text copied verbatim
|
||||
/// from `engine/shaders/shape.frag`.
|
||||
/// `:/shaders/shape.frag` in `get_shader_code`). Text adapted from
|
||||
/// `engine/shaders/shape.frag`: the C++ `switch (type_in)` dispatch is
|
||||
/// spelled as if/else chains because naga's WGSL emitter rejects
|
||||
/// fall-through-capable GLSL switch blocks.
|
||||
const SHADER_FRAG: &str = r#"// Input texture coordinate
|
||||
in vec2 ove_texcoord;
|
||||
out vec4 frag_color;
|
||||
@@ -88,22 +90,14 @@ void main() {
|
||||
|
||||
vec4 col = vec4(0.0);
|
||||
|
||||
switch (type_in) {
|
||||
case SHAPE_RECTANGLE:
|
||||
{
|
||||
if (type_in == SHAPE_RECTANGLE) {
|
||||
col = draw_rect(real_position, real_size);
|
||||
break;
|
||||
}
|
||||
case SHAPE_ELLIPSE:
|
||||
{
|
||||
} else if (type_in == SHAPE_ELLIPSE) {
|
||||
vec2 center = p+size_in*0.5;
|
||||
float radius = size_in.y*0.5;
|
||||
float aspect_ratio = size_in.x/size_in.y;
|
||||
col = draw_ellipse(center, radius, aspect_ratio);
|
||||
break;
|
||||
}
|
||||
case SHAPE_ROUNDEDRECT:
|
||||
{
|
||||
} else if (type_in == SHAPE_ROUNDEDRECT) {
|
||||
// Limit radius so it is never larger than half the shortest size
|
||||
float r = min(radius_in, min(size_in.y*0.5, size_in.x*0.5));
|
||||
vec2 real_rad = vec2(r / resolution_in.x, r / resolution_in.y);
|
||||
@@ -122,8 +116,6 @@ void main() {
|
||||
} else {
|
||||
col = draw_rect(real_position, real_size);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
frag_color = col;
|
||||
@@ -199,9 +191,8 @@ impl NodeBehavior for ShapeNode {
|
||||
/// the params here. With a `base_in` texture connected, the C++
|
||||
/// `push_mergable_job` instead pushes a `"mrg"` alpha-over job whose
|
||||
/// `blend_in` is the shape job nested as a texture value — mirrored
|
||||
/// here as a nested payload, which the renderer cannot yet
|
||||
/// recursively resolve (TODO; `// CPP-PARITY: shapenode.cpp`
|
||||
/// `value()`, `generatorwithmerge.cpp` `push_mergable_job`).
|
||||
/// here as a nested payload, which the renderer resolves recursively
|
||||
/// (see `process_shader_job_depth`).
|
||||
fn value(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
@@ -231,10 +222,9 @@ impl NodeBehavior for ShapeNode {
|
||||
// texture and `blend_in` = the shape job nested as a texture
|
||||
// value; the merge's params are a fresh row holding exactly
|
||||
// those two keys, and the default `ShaderJob` has
|
||||
// `iterations = 1` and no iterative input. Recursively
|
||||
// resolving the nested payload is a renderer TODO
|
||||
// (`// CPP-PARITY: generatorwithmerge.cpp`
|
||||
// `push_mergable_job`).
|
||||
// `iterations = 1` and no iterative input. The renderer
|
||||
// resolves the nested payload recursively
|
||||
// (`process_shader_job_depth`).
|
||||
let mut params = crate::value::NodeValueRow::new();
|
||||
params.insert(
|
||||
super::generatorwithmerge::BASE_INPUT.to_string(),
|
||||
|
||||
@@ -2889,5 +2889,103 @@ mod tests {
|
||||
assert_eq!(pixel_at(&out, 0, 0), [0.0, 0.0, 0.0, 0.0]);
|
||||
}
|
||||
|
||||
/// Shape generator over the real GPU path: a centered 8x8 rectangle
|
||||
/// on a 16x16 frame fills exactly the middle block (pixel centers
|
||||
/// with texcoord in [0.25, 0.75)), everything outside stays
|
||||
/// transparent.
|
||||
#[test]
|
||||
fn gpu_shape_rectangle_draws_centered_block() {
|
||||
if oak_core::backend::GpuContext::shared().is_none() {
|
||||
eprintln!("no adapter; skipping");
|
||||
return;
|
||||
}
|
||||
let mut inputs = NodeValueRow::new();
|
||||
inputs.insert("pos_in".into(), NodeValue::Vec2([0.0, 0.0]));
|
||||
inputs.insert("size_in".into(), NodeValue::Vec2([8.0, 8.0]));
|
||||
inputs.insert("color_in".into(), NodeValue::Color([1.0, 0.0, 0.0, 1.0]));
|
||||
inputs.insert("type_in".into(), NodeValue::Combo(0));
|
||||
inputs.insert("radius_in".into(), NodeValue::Float(20.0));
|
||||
|
||||
let frame = eval_node_row("org.olivevideoeditor.Olive.shape", inputs, Some((16, 16)));
|
||||
assert_eq!((frame.width, frame.height), (16, 16));
|
||||
for (x, y, inside) in [(8, 8, true), (4, 4, true), (11, 11, true), (0, 0, false), (3, 8, false), (12, 8, false), (15, 15, false)] {
|
||||
let px = pixel_at(&frame, x, y);
|
||||
if inside {
|
||||
assert_eq!(px, [1.0, 0.0, 0.0, 1.0], "({x},{y}) inside the rect");
|
||||
} else {
|
||||
assert_eq!(px, [0.0, 0.0, 0.0, 0.0], "({x},{y}) outside the rect");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shape generator, the ellipse and rounded-rectangle dispatches:
|
||||
/// the ellipse fills the center and fades out before the corners;
|
||||
/// the rounded rect fills the middle but cuts the corner at (4,4)
|
||||
/// (radius 20 clamps to half the 8px size).
|
||||
#[test]
|
||||
fn gpu_shape_ellipse_and_rounded_rect() {
|
||||
if oak_core::backend::GpuContext::shared().is_none() {
|
||||
eprintln!("no adapter; skipping");
|
||||
return;
|
||||
}
|
||||
let base_inputs = || {
|
||||
let mut inputs = NodeValueRow::new();
|
||||
inputs.insert("pos_in".into(), NodeValue::Vec2([0.0, 0.0]));
|
||||
inputs.insert("size_in".into(), NodeValue::Vec2([8.0, 8.0]));
|
||||
inputs.insert("color_in".into(), NodeValue::Color([1.0, 0.0, 0.0, 1.0]));
|
||||
inputs.insert("radius_in".into(), NodeValue::Float(20.0));
|
||||
inputs
|
||||
};
|
||||
|
||||
let mut ellipse = base_inputs();
|
||||
ellipse.insert("type_in".into(), NodeValue::Combo(1));
|
||||
let frame = eval_node_row("org.olivevideoeditor.Olive.shape", ellipse, Some((16, 16)));
|
||||
assert_eq!(pixel_at(&frame, 8, 8), [1.0, 0.0, 0.0, 1.0], "ellipse center");
|
||||
assert_eq!(pixel_at(&frame, 0, 0), [0.0, 0.0, 0.0, 0.0], "ellipse corner faded out");
|
||||
|
||||
let mut rounded = base_inputs();
|
||||
rounded.insert("type_in".into(), NodeValue::Combo(2));
|
||||
let frame = eval_node_row("org.olivevideoeditor.Olive.shape", rounded, Some((16, 16)));
|
||||
assert_eq!(pixel_at(&frame, 8, 8), [1.0, 0.0, 0.0, 1.0], "rounded rect middle");
|
||||
assert_eq!(pixel_at(&frame, 6, 6), [1.0, 0.0, 0.0, 1.0], "rounded rect inside the corner arc");
|
||||
assert_eq!(pixel_at(&frame, 0, 0), [0.0, 0.0, 0.0, 0.0], "rounded rect far corner");
|
||||
assert!(
|
||||
pixel_at(&frame, 4, 4)[3] < 0.1,
|
||||
"rounded rect corner (4,4) is cut by the arc: {:?}",
|
||||
pixel_at(&frame, 4, 4)
|
||||
);
|
||||
}
|
||||
|
||||
/// Despill over the real GPU path: green-screen AVERAGE caps the
|
||||
/// green channel at the red/blue average (the shader's method
|
||||
/// dispatch must survive translation).
|
||||
#[test]
|
||||
fn gpu_despill_average_caps_green() {
|
||||
if oak_core::backend::GpuContext::shared().is_none() {
|
||||
eprintln!("no adapter; skipping");
|
||||
return;
|
||||
}
|
||||
let mut inputs = NodeValueRow::new();
|
||||
inputs.insert(
|
||||
"tex_in".into(),
|
||||
texture_value(filled_frame((4, 4), [0.2, 0.9, 0.3, 1.0])),
|
||||
);
|
||||
inputs.insert("color_in".into(), NodeValue::Combo(0));
|
||||
inputs.insert("method_in".into(), NodeValue::Combo(0));
|
||||
inputs.insert("preserve_luminance_input".into(), NodeValue::Boolean(false));
|
||||
|
||||
let frame = eval_node_row("org.olivevideoeditor.Olive.despill", inputs, None);
|
||||
assert_eq!((frame.width, frame.height), (4, 4));
|
||||
let px = pixel_at(&frame, 2, 2);
|
||||
let want = [0.2f32, 0.25, 0.3, 1.0];
|
||||
for (c, w) in want.iter().enumerate() {
|
||||
assert!(
|
||||
(px[c] - w).abs() < 1e-4,
|
||||
"despill ch{c}: got {}, want {w}",
|
||||
px[c]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user