nodes: spell shape/despill shader dispatch as if/else chains
CI / Build & test (Linux) (push) Successful in 24m7s
CI / Build & test (Windows) (push) Successful in 30m31s

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:
2026-09-09 16:51:13 +08:00
parent 4f404f8cc6
commit 37df1d3d34
4 changed files with 128 additions and 46 deletions
+14 -21
View File
@@ -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;
}
}
+4 -3
View File
@@ -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`
+12 -22
View File
@@ -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(),