.
diff --git a/sparse_strips/vello_hybrid/src/render/webgl.rs b/sparse_strips/vello_hybrid/src/render/webgl.rs index 14e5bfd..23e945b 100644 --- a/sparse_strips/vello_hybrid/src/render/webgl.rs +++ b/sparse_strips/vello_hybrid/src/render/webgl.rs
@@ -150,6 +150,15 @@ .dyn_into::<WebGl2RenderingContext>() .expect("Context to be a WebGL2 context"); + let cloned_gl = gl.clone(); + let _state_guard = WebGlStateGuard::with_config( + &cloned_gl, + WebGlStateConfig { + framebuffer: true, + ..Default::default() + }, + ); + // Note: It is not entirely clear whether we really _have_ to ensure anti-aliasing is disabled. // This code is inherited from a similar snippet in wgpu // (https://github.com/gfx-rs/wgpu/blob/56e4a389ddd02403e232beef3d3ff305625e6485/wgpu-hal/src/gles/web.rs#L101-L106),
diff --git a/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl b/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl index 6c39f54..e5f8f1d 100644 --- a/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl +++ b/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
@@ -283,8 +283,8 @@ let ndc_x = pix_x * 2.0 / f32(config.width) - 1.0; let ndc_y = 1.0 - pix_y * 2.0 / f32(config.height); - let color_source = (instance.paint_and_rect_flag >> 29u) & 0x3u; - if color_source == COLOR_SOURCE_PAYLOAD { + let color_source = i32((instance.paint_and_rect_flag >> 29u) & 0x3u); + if color_source == i32(COLOR_SOURCE_PAYLOAD) { let paint_type = (instance.paint_and_rect_flag >> 26u) & 0x7u; // Unpack view coordinates for image sampling and gradient calculations let scene_strip_x = instance.payload & 0xffffu; @@ -374,10 +374,10 @@ alpha = f32((alphas_u32 >> (y * 8u)) & 0xffu) * (1.0 / 255.0); } // Apply the alpha value to the unpacked RGBA color or slot index - let color_source = (in.paint_and_rect_flag >> 29u) & 0x3u; + let color_source = i32((in.paint_and_rect_flag >> 29u) & 0x3u); var final_color: vec4<f32>; - if color_source == COLOR_SOURCE_PAYLOAD { + if color_source == i32(COLOR_SOURCE_PAYLOAD) { let paint_type = (in.paint_and_rect_flag >> 26u) & 0x7u; // in.payload encodes a color for PAINT_TYPE_SOLID or sample_xy for PAINT_TYPE_IMAGE @@ -510,7 +510,7 @@ ); final_color = alpha * gradient_color; } - } else if color_source == COLOR_SOURCE_SLOT { + } else if color_source == i32(COLOR_SOURCE_SLOT) { // Depending on the value of `ndc_y_negate`, the y position will have a value that either // assumes a `y-up` or `y-down` coordinate system. However, for slot textures, we need the original // coordinate in the `y-down` system. Therefore, we invert the y-position _again_ in case we are @@ -532,7 +532,7 @@ let opacity = f32(in.paint_and_rect_flag & 0xFFu) * (1.0 / 255.0); final_color = alpha * opacity * clip_in_color; - } else if color_source == COLOR_SOURCE_BLEND { + } else if color_source == i32(COLOR_SOURCE_BLEND) { // See the comment above. let sample_y = select(in.position.y, f32(config.height) - in.position.y, config.ndc_y_negate != 0u); let opacity = f32((in.paint_and_rect_flag >> 16u) & 0xFFu) * (1.0 / 255.0);
diff --git a/webgl_demo/README.md b/webgl_demo/README.md new file mode 100644 index 0000000..a44f4df --- /dev/null +++ b/webgl_demo/README.md
@@ -0,0 +1,29 @@ +# WebGL Demo + +This folder contains a minimal WebGL2 repro for the bug where a linked program +reports `_group_0_binding_2_fs` as missing. + +The four fragment variants are: + +- `Minimal Pass` +- `Minimal Fail` +- `Render-Like Fail` +- `Render-Like Fixed` + +The only preserved Vello detail is the sampler name. Everything else is reduced +to the minimum needed for the working vs failing comparisons. + +## Run + +Serve from inside this folder: + +```sh +cd webgl_demo +python3 -m http.server +``` + +Then open: + +```text +http://localhost:8000/ +```
diff --git a/webgl_demo/app.js b/webgl_demo/app.js new file mode 100644 index 0000000..3219a97 --- /dev/null +++ b/webgl_demo/app.js
@@ -0,0 +1,116 @@ +const canvas = document.getElementById("gl"); + +const gl = canvas.getContext("webgl2", { + alpha: false, + antialias: false, + depth: false, + stencil: false, +}); + +const VERTEX_SHADER_SOURCE = `#version 300 es +precision highp float; +precision highp int; + +layout(location = 0) in vec2 a_position; + +void main() { + gl_Position = vec4(a_position, 0.0, 1.0); +} +`; + +async function main() { + if (!gl) { + document.body.style.background = "#000"; + return; + } + + const fragmentSource = await fetchText("./render_strips_min_fail.frag.glsl"); + const program = createProgram(VERTEX_SHADER_SOURCE, fragmentSource); + if (!program) { + document.body.style.background = "#000"; + return; + } + + const vao = gl.createVertexArray(); + gl.bindVertexArray(vao); + + const vertexBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); + gl.bufferData( + gl.ARRAY_BUFFER, + new Float32Array([ + -1.0, -1.0, + 3.0, -1.0, + -1.0, 3.0, + ]), + gl.STATIC_DRAW, + ); + gl.enableVertexAttribArray(0); + gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); + + gl.disable(gl.BLEND); + gl.disable(gl.DEPTH_TEST); + gl.disable(gl.CULL_FACE); + gl.useProgram(program); + resize(); + window.addEventListener("resize", resize); + draw(); + + function resize() { + const width = Math.max(1, window.innerWidth); + const height = Math.max(1, window.innerHeight); + canvas.width = width; + canvas.height = height; + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + gl.viewport(0, 0, width, height); + draw(); + } + + function draw() { + gl.drawArrays(gl.TRIANGLES, 0, 3); + } +} + +async function fetchText(path) { + const response = await fetch(path, { cache: "no-store" }); + if (!response.ok) { + throw new Error(`Failed to fetch ${path}: HTTP ${response.status}`); + } + return response.text(); +} + +function createProgram(vertexSource, fragmentSource) { + const vertexShader = compileShader(gl.VERTEX_SHADER, vertexSource); + const fragmentShader = compileShader(gl.FRAGMENT_SHADER, fragmentSource); + if (!vertexShader || !fragmentShader) { + return null; + } + + const program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + console.error(gl.getProgramInfoLog(program) || "Program link failed."); + return null; + } + + return program; +} + +function compileShader(type, source) { + const shader = gl.createShader(type); + gl.shaderSource(shader, source); + gl.compileShader(shader); + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + console.error(gl.getShaderInfoLog(shader) || "Shader compile failed."); + return null; + } + + return shader; +} + +void main();
diff --git a/webgl_demo/index.html b/webgl_demo/index.html new file mode 100644 index 0000000..2bf0248 --- /dev/null +++ b/webgl_demo/index.html
@@ -0,0 +1,49 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>WebGL Branch Repro</title> + <style> + /* + Original UI intentionally left disabled while focusing on fullscreen + branch visualization. + + :root { + color-scheme: light; + --bg: #f4f0e8; + --panel: #fffaf2; + --ink: #1d1d1b; + } + */ + + html, + body { + margin: 0; + width: 100%; + height: 100%; + overflow: hidden; + background: #000; + } + + canvas { + display: block; + width: 100vw; + height: 100vh; + } + </style> + </head> + <body> + <!-- + <main> + <section class="controls"> + <button id="run-all">Refresh</button> + </section> + <section class="panel meta" id="meta"></section> + <section class="results" id="results"></section> + </main> + --> + <canvas id="gl"></canvas> + <script type="module" src="./app.js?v=fullscreen2"></script> + </body> +</html>
diff --git a/webgl_demo/render_strips_current.frag.glsl b/webgl_demo/render_strips_current.frag.glsl new file mode 120000 index 0000000..b734ee3 --- /dev/null +++ b/webgl_demo/render_strips_current.frag.glsl
@@ -0,0 +1 @@ +../sparse_strips/vello_sparse_shaders/generated_glsl/render_strips.frag.glsl \ No newline at end of file
diff --git a/webgl_demo/render_strips_current.vert.glsl b/webgl_demo/render_strips_current.vert.glsl new file mode 120000 index 0000000..2d6a9e5 --- /dev/null +++ b/webgl_demo/render_strips_current.vert.glsl
@@ -0,0 +1 @@ +../sparse_strips/vello_sparse_shaders/generated_glsl/render_strips.vert.glsl \ No newline at end of file
diff --git a/webgl_demo/render_strips_generated.frag.glsl b/webgl_demo/render_strips_generated.frag.glsl new file mode 100644 index 0000000..26673db --- /dev/null +++ b/webgl_demo/render_strips_generated.frag.glsl
@@ -0,0 +1,43 @@ +#version 300 es + +precision highp float; +precision highp int; + +struct Data { + uint a; + vec2 b; + vec2 c; + uint d; + uint e; + uint f; + vec4 g; +}; + +uniform highp sampler2D _group_0_binding_2_fs; + +flat in uint v0; +in vec2 v1; +in vec2 v2; +flat in uint v3; +flat in uint v4; +flat in uint v5; +layout(location = 0) out vec4 out_color; + +void main() { + Data x = Data(v0, v1, v2, v3, v4, v5, gl_FragCoord); + uint mode = (x.a >> 29u) & 3u; + float sink = 0.0; + sink += x.b.x * 0.0; + sink += x.c.y * 0.0; + sink += float(x.d & 1u) * 0.0; + sink += float(x.f & 1u) * 0.0; + + if (mode == 1u || mode == 2u) { + ivec2 p = ivec2(int(x.g.x), int(x.g.y) + int(x.e & 1u)); + out_color = texelFetch(_group_0_binding_2_fs, p, 0); + out_color.x += sink; + } else { + out_color = vec4(1.0, 0.0, 0.0, 1.0); + out_color.x += sink; + } +}
diff --git a/webgl_demo/render_strips_generated_patched.frag.glsl b/webgl_demo/render_strips_generated_patched.frag.glsl new file mode 100644 index 0000000..164afa4 --- /dev/null +++ b/webgl_demo/render_strips_generated_patched.frag.glsl
@@ -0,0 +1,39 @@ +#version 300 es + +precision highp float; +precision highp int; + +uniform highp sampler2D _group_0_binding_2_fs; + +flat in uint v0; +in vec2 v1; +in vec2 v2; +flat in uint v3; +flat in uint v4; +flat in uint v5; +layout(location = 0) out vec4 out_color; + +void main() { + uint a = v0; + vec2 b = v1; + vec2 c = v2; + uint d = v3; + uint e = v4; + uint f = v5; + vec4 g = gl_FragCoord; + uint mode = (a >> 29u) & 3u; + float sink = 0.0; + sink += b.x * 0.0; + sink += c.y * 0.0; + sink += float(d & 1u) * 0.0; + sink += float(f & 1u) * 0.0; + + if (mode == 1u || mode == 2u) { + ivec2 p = ivec2(int(g.x), int(g.y) + int(e & 1u)); + out_color = texelFetch(_group_0_binding_2_fs, p, 0); + out_color.x += sink; + } else { + out_color = vec4(1.0, 0.0, 0.0, 1.0); + out_color.x += sink; + } +}
diff --git a/webgl_demo/render_strips_min_fail.frag.glsl b/webgl_demo/render_strips_min_fail.frag.glsl new file mode 100644 index 0000000..a9a8268 --- /dev/null +++ b/webgl_demo/render_strips_min_fail.frag.glsl
@@ -0,0 +1,18 @@ +#version 300 es + +precision highp float; +precision highp int; + +struct Pair { + uint f0; +}; + +layout(location = 0) out vec4 out_color; + +void main() { + Pair p = Pair(0x00010000u); + uint mode = p.f0 >> 16; + float green = (mode == 1u) ? 1.0 : 0.0; + float red = 1.0 - green; + out_color = vec4(red, green, 0.0, 1.0); +}
diff --git a/webgl_demo/render_strips_min_pass.frag.glsl b/webgl_demo/render_strips_min_pass.frag.glsl new file mode 100644 index 0000000..97e725d --- /dev/null +++ b/webgl_demo/render_strips_min_pass.frag.glsl
@@ -0,0 +1,17 @@ +#version 300 es + +precision highp float; +precision highp int; + +flat in uint v0; +layout(location = 0) out vec4 out_color; + +void main() { + uint mode = v0 >> 29; + + if (mode == 1u) { + out_color = vec4(0.0, 1.0, 0.0, 1.0); + } else { + out_color = vec4(1.0, 0.0, 0.0, 1.0); + } +}