diff --git a/crates/oak-node/src/nodes/despill.rs b/crates/oak-node/src/nodes/despill.rs index 2f5b4e2c9..c35d96077 100644 --- a/crates/oak-node/src/nodes/despill.rs +++ b/crates/oak-node/src/nodes/despill.rs @@ -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; } } diff --git a/crates/oak-node/src/nodes/merge.rs b/crates/oak-node/src/nodes/merge.rs index 467822327..7aad846f5 100644 --- a/crates/oak-node/src/nodes/merge.rs +++ b/crates/oak-node/src/nodes/merge.rs @@ -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` diff --git a/crates/oak-node/src/nodes/shapenode.rs b/crates/oak-node/src/nodes/shapenode.rs index 05b399a53..04d6ba705 100644 --- a/crates/oak-node/src/nodes/shapenode.rs +++ b/crates/oak-node/src/nodes/shapenode.rs @@ -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(), diff --git a/crates/oak-render/src/eval.rs b/crates/oak-render/src/eval.rs index 17a9e07de..4af770bef 100644 --- a/crates/oak-render/src/eval.rs +++ b/crates/oak-render/src/eval.rs @@ -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] + ); + } + } + }