Skip to content

Commit 377cc3c

Browse files
Add WebGL browser tests to vello_sparse_tests (#1020)
Note: This PR currently branches off #1021 as there is a lint error on `main` and I wanted to ensure CI fully passed. > [!TIP] > This is ready to review and passes all tests in CI. ### Context This follows #1011 and is the testing portion. In order to ensure renderer parity and correctness, I've forked the `vello_hybrid` tests to also include a `_hybrid_webgl` variant that use: `#[wasm_bindgen_test::wasm_bindgen_test]` and run via the browser. <details> <summary> As a demo of this PR, I locally introduced a "fake error" that reflects the image. After running `wasm-pack test --chrome --features webgl` from the `vello_sparse_tests` directory, I got the following very long webpage. (unfurl details section by clicking this text, and scroll to bottom of test results) </summary> ![127 0 0 1_8000_](https://github.com/user-attachments/assets/d9033ede-ecac-4602-b329-07ee5b54f465) </details> ### Features - Add headless and headed browser testing of the webgl backend. - Because the browser can't read from the file system easily, reference images are inlined into the binary via the proc macro and provided to the `check_refs` function. - Existing `_hybrid` tests are unchanged. I've added a `_hybrid_webgl` test. - For ease of debugging the diff'd images are appended to the document after tests have run. This makes it much easier to understand the failure. - Add `check vello_hybrid webgl backend passes vello_sparse_tests suite` CI step that uses headless browser. These tests could be fancier, but they do the job right now. Debugging a failure isn't easy, however the diff can be manually retrieved and compared by a human by spinning up a browser. The goal of having webgl tests automatically match the vello_hybrid tests is to keep the `wgpu` and `webgl` renderer backends in sync. ### Test plan This PR changes no business logic and is the test. Note: Browser WebGL backend tests currently pass all of the same tests as the vello_hybrid wgpu renderer. Can be seen in CI here: https://github.com/linebender/vello/actions/runs/15211363959/job/42786078261?pr=1020 I tested the functionality of this change by introducing intentional breakages and ensuring that headless tests failed, and diff images could be found on the browser when run without headless. ### Risks I'm not sure of any risks as this isn't user facing. ### Manually testing this change Pull branch down locally. Then navigate the `vello_sparse_tests` and run: `wasm-pack test --chrome --features webgl`. Navigate to the browser URL and see 65 tests succeed. ### Followup work #1026 #1025 --------- Co-authored-by: Daniel McNab <[email protected]>
1 parent 8491dca commit 377cc3c

File tree

8 files changed

+313
-7
lines changed

8 files changed

+313
-7
lines changed

.github/workflows/ci.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,9 +306,27 @@ jobs:
306306

307307
test-stable-wasm:
308308
name: cargo test (wasm32)
309+
needs: prime-lfs-cache
309310
runs-on: ubuntu-latest
310311
steps:
311312
- uses: actions/checkout@v4
313+
# We intentionally do not use lfs: true here, instead using the caching method to save LFS bandwidth.
314+
315+
- name: Restore lfs cache
316+
id: lfs-cache
317+
uses: actions/cache/restore@v4
318+
with:
319+
path: .git/lfs
320+
# The files targeted with git lfs
321+
key: vello-lfs-${{ needs.prime-lfs-cache.outputs.lfs-hash }}
322+
enableCrossOsArchive: true
323+
324+
- name: Checkout LFS files
325+
# `git lfs checkout` requires that each individual glob is a separate command line argument.
326+
# The string `''' '''` is how you write `' '` in GitHub's expression context (i.e. two quotes separated by a space)
327+
# The quotes are to avoid the shell from evaluating the globs itself.
328+
run: git lfs checkout '${{ join(fromJson(env.LFS_FILES), ''' ''') }}'
329+
continue-on-error: true
312330

313331
- name: install stable toolchain
314332
uses: dtolnay/rust-toolchain@master
@@ -337,6 +355,10 @@ jobs:
337355
run: wasm-pack test --headless --chrome
338356
working-directory: sparse_strips/vello_hybrid/examples/native_webgl
339357

358+
- name: Run vello_sparse_tests on Chrome
359+
run: wasm-pack test --headless --chrome --features webgl
360+
working-directory: sparse_strips/vello_sparse_tests
361+
340362

341363
check-stable-android:
342364
name: cargo check (aarch64-android)

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sparse_strips/vello_dev_macros/src/test.rs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ pub(crate) fn vello_test_inner(attr: TokenStream, item: TokenStream) -> TokenStr
6161
let u8_fn_name = Ident::new(&format!("{}_cpu_u8", input_fn_name), input_fn_name.span());
6262
let f32_fn_name = Ident::new(&format!("{}_cpu_f32", input_fn_name), input_fn_name.span());
6363
let hybrid_fn_name = Ident::new(&format!("{}_hybrid", input_fn_name), input_fn_name.span());
64+
let webgl_fn_name = Ident::new(
65+
&format!("{}_hybrid_webgl", input_fn_name),
66+
input_fn_name.span(),
67+
);
6468

6569
// TODO: Tests with the same names in different modules can clash, see
6670
// https://github.com/linebender/vello/pull/925#discussion_r2070710362.
@@ -70,6 +74,7 @@ pub(crate) fn vello_test_inner(attr: TokenStream, item: TokenStream) -> TokenStr
7074
let u8_fn_name_str = u8_fn_name.to_string();
7175
let f32_fn_name_str = f32_fn_name.to_string();
7276
let hybrid_fn_name_str = hybrid_fn_name.to_string();
77+
let webgl_fn_name_str = webgl_fn_name.to_string();
7378

7479
let Arguments {
7580
width,
@@ -83,6 +88,30 @@ pub(crate) fn vello_test_inner(attr: TokenStream, item: TokenStream) -> TokenStr
8388
no_ref,
8489
} = parse_args(&attrs);
8590

91+
// Wasm doesn't have access to the filesystem. For wasm, inline the snapshot bytes into the
92+
// binary.
93+
let reference_image_name = Ident::new(
94+
&format!(
95+
"{}_REFERENCE_IMAGE",
96+
input_fn_name.to_string().to_uppercase()
97+
),
98+
input_fn_name.span(),
99+
);
100+
let reference_image_const = if !no_ref {
101+
quote! {
102+
#[cfg(target_arch = "wasm32")]
103+
const #reference_image_name: &[u8] = include_bytes!(
104+
concat!(env!("CARGO_MANIFEST_DIR"), "/snapshots/", #input_fn_name_str, ".png")
105+
);
106+
#[cfg(not(target_arch = "wasm32"))]
107+
const #reference_image_name: &[u8] = &[];
108+
}
109+
} else {
110+
quote! {
111+
const #reference_image_name: &[u8] = &[];
112+
}
113+
};
114+
86115
let cpu_u8_tolerance = cpu_u8_tolerance + DEFAULT_CPU_U8_TOLERANCE;
87116
// Since f32 is our gold standard, we always require exact matches for this one.
88117
let cpu_f32_tolerance = DEFAULT_CPU_F32_TOLERANCE;
@@ -137,7 +166,7 @@ pub(crate) fn vello_test_inner(attr: TokenStream, item: TokenStream) -> TokenStr
137166
let mut ctx = get_ctx::<RenderContext>(#width, #height, #transparent);
138167
#input_fn_name(&mut ctx);
139168
if !#no_ref {
140-
check_ref(&ctx, #input_fn_name_str, #fn_name_str, #tolerance, #is_reference, #render_mode);
169+
check_ref(&ctx, #input_fn_name_str, #fn_name_str, #tolerance, #is_reference, #render_mode, #reference_image_name);
141170
}
142171
}
143172
}
@@ -161,6 +190,8 @@ pub(crate) fn vello_test_inner(attr: TokenStream, item: TokenStream) -> TokenStr
161190
let expanded = quote! {
162191
#input_fn
163192

193+
#reference_image_const
194+
164195
#u8_snippet
165196

166197
#f32_snippet
@@ -177,7 +208,24 @@ pub(crate) fn vello_test_inner(attr: TokenStream, item: TokenStream) -> TokenStr
177208
let mut ctx = get_ctx::<Scene>(#width, #height, #transparent);
178209
#input_fn_name(&mut ctx);
179210
if !#no_ref {
180-
check_ref(&ctx, #input_fn_name_str, #hybrid_fn_name_str, #hybrid_tolerance, false, RenderMode::OptimizeSpeed);
211+
check_ref(&ctx, #input_fn_name_str, #hybrid_fn_name_str, #hybrid_tolerance, false, RenderMode::OptimizeSpeed, #reference_image_name);
212+
}
213+
}
214+
215+
#ignore_hybrid
216+
#[cfg(all(target_arch = "wasm32", feature = "webgl"))]
217+
#[wasm_bindgen_test::wasm_bindgen_test]
218+
async fn #webgl_fn_name() {
219+
use crate::util::{
220+
check_ref, get_ctx
221+
};
222+
use vello_hybrid::Scene;
223+
use vello_cpu::RenderMode;
224+
225+
let mut ctx = get_ctx::<Scene>(#width, #height, #transparent);
226+
#input_fn_name(&mut ctx);
227+
if !#no_ref {
228+
check_ref(&ctx, #input_fn_name_str, #webgl_fn_name_str, #hybrid_tolerance, false, RenderMode::OptimizeSpeed, #reference_image_name);
181229
}
182230
}
183231
};

sparse_strips/vello_sparse_tests/Cargo.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,24 @@ image = { workspace = true, features = ["png"] }
2828
skrifa = { workspace = true }
2929
smallvec = { workspace = true }
3030

31+
[target.'cfg(target_arch = "wasm32")'.dependencies]
32+
wasm-bindgen-test = "0.3.50"
33+
web-sys = { version = "0.3.77", features = [
34+
"HtmlCanvasElement",
35+
"WebGl2RenderingContext",
36+
"Document",
37+
"Window",
38+
"Element",
39+
"HtmlElement",
40+
"HtmlImageElement",
41+
"Blob",
42+
"BlobPropertyBag",
43+
"Url",
44+
] }
45+
wasm-bindgen = "0.2.100"
46+
47+
[features]
48+
webgl = ["vello_hybrid/webgl"]
49+
3150
[lints]
3251
workspace = true
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<div align="center">
2+
3+
# Vello Sparse Tests
4+
5+
[![Apache 2.0 or MIT license.](https://img.shields.io/badge/license-Apache--2.0_OR_MIT-blue.svg)](#license)
6+
\
7+
[![Linebender Zulip chat.](https://img.shields.io/badge/Linebender-%23vello-blue?logo=Zulip)](https://xi.zulipchat.com/#narrow/channel/197075-vello)
8+
[![GitHub Actions CI status.](https://img.shields.io/github/actions/workflow/status/linebender/vello/ci.yml?logo=github&label=CI)](https://github.com/linebender/vello/actions)
9+
10+
</div>
11+
12+
This is a development-only crate for testing the sparse_strip renderers across a corpus of reference
13+
images:
14+
- CPU
15+
- WGPU
16+
- WASM32 WebGL
17+
18+
The `vello_test` proc macro will create a snapshot test for each supported renderer target. See the
19+
below example usage.
20+
21+
```rs
22+
// Draws a filled triangle into a 125x125 scene.
23+
#[vello_test(width = 125, height = 125)]
24+
fn filled_triangle(ctx: &mut impl Renderer) {
25+
let path = {
26+
let mut path = BezPath::new();
27+
path.move_to((5.0, 5.0));
28+
path.line_to((95.0, 50.0));
29+
path.line_to((5.0, 95.0));
30+
path.close_path();
31+
32+
path
33+
};
34+
35+
ctx.set_paint(LIME);
36+
ctx.fill_path(&path);
37+
}
38+
```
39+
40+
See all the attributes that can be passed to `vello_test` in `vello_dev_macros/test.rs`.
41+
42+
## Testing WebGL on the Browser
43+
44+
Requirements:
45+
- on MacOS, a minimum Clang major version of 20 is required.
46+
47+
To run the `vello_sparse_tests` suite on WebGL headless:
48+
49+
```sh
50+
wasm-pack test --headless --chrome --features webgl
51+
```
52+
53+
To debug the output images in webgl, run the same command without `--headless`. Any tests that fail
54+
will have their diff image appended to the bottom of the page.

sparse_strips/vello_sparse_tests/tests/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
#![allow(missing_docs, reason = "we don't need docs for testing")]
2222
#![allow(clippy::cast_possible_truncation, reason = "not critical for testing")]
2323

24+
#[cfg(target_arch = "wasm32")]
25+
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
26+
2427
mod basic;
2528
mod blurred_rounded_rect;
2629
mod clip;

sparse_strips/vello_sparse_tests/tests/renderer.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ impl Renderer for Scene {
219219
// This method creates device resources every time it is called. This does not matter much for
220220
// testing, but should not be used as a basis for implementing something real. This would be a
221221
// very bad example for that.
222+
#[cfg(not(all(target_arch = "wasm32", feature = "webgl")))]
222223
fn render_to_pixmap(&self, pixmap: &mut Pixmap, _: RenderMode) {
223224
// On some platforms using `cargo test` triggers segmentation faults in wgpu when the GPU
224225
// tests are run in parallel (likely related to the number of device resources being
@@ -362,6 +363,58 @@ impl Renderer for Scene {
362363
texture_copy_buffer.unmap();
363364
}
364365

366+
// vello_hybrid WebGL renderer backend.
367+
#[cfg(all(target_arch = "wasm32", feature = "webgl"))]
368+
fn render_to_pixmap(&self, pixmap: &mut Pixmap, _: RenderMode) {
369+
use wasm_bindgen::JsCast;
370+
use web_sys::{HtmlCanvasElement, WebGl2RenderingContext};
371+
372+
let width = self.width();
373+
let height = self.height();
374+
375+
// Create an offscreen HTMLCanvasElement, render the test image to it, and finally read off
376+
// the pixmap for diff checking.
377+
let document = web_sys::window().unwrap().document().unwrap();
378+
379+
let canvas = document
380+
.create_element("canvas")
381+
.unwrap()
382+
.dyn_into::<HtmlCanvasElement>()
383+
.unwrap();
384+
385+
canvas.set_width(width.into());
386+
canvas.set_height(height.into());
387+
388+
let mut renderer = vello_hybrid::WebGlRenderer::new(&canvas);
389+
let render_size = vello_hybrid::RenderSize {
390+
width: width.into(),
391+
height: height.into(),
392+
};
393+
394+
renderer.render(self, &render_size).unwrap();
395+
396+
let gl = canvas
397+
.get_context("webgl2")
398+
.unwrap()
399+
.unwrap()
400+
.dyn_into::<WebGl2RenderingContext>()
401+
.unwrap();
402+
let mut pixels = vec![0_u8; (width as usize) * (height as usize) * 4];
403+
gl.read_pixels_with_opt_u8_array(
404+
0,
405+
0,
406+
width.into(),
407+
height.into(),
408+
WebGl2RenderingContext::RGBA,
409+
WebGl2RenderingContext::UNSIGNED_BYTE,
410+
Some(&mut pixels),
411+
)
412+
.unwrap();
413+
414+
let pixmap_data = pixmap.data_as_u8_slice_mut();
415+
pixmap_data.copy_from_slice(&pixels);
416+
}
417+
365418
fn width(&self) -> u16 {
366419
Self::width(self)
367420
}

0 commit comments

Comments
 (0)