Skip to content

Commit 40c53e5

Browse files
draft of testing in the browser
1 parent 8491dca commit 40c53e5

File tree

9 files changed

+288
-0
lines changed

9 files changed

+288
-0
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,10 @@ jobs:
337337
run: wasm-pack test --headless --chrome
338338
working-directory: sparse_strips/vello_hybrid/examples/native_webgl
339339

340+
- name: check vello_hybrid webgl backend passes vello_sparse_tests suite
341+
run: wasm-pack test --headless --chrome --features webgl
342+
working-directory: sparse_strips/vello_sparse_tests
343+
340344

341345
check-stable-android:
342346
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: 24 additions & 0 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,
@@ -127,6 +132,7 @@ pub(crate) fn vello_test_inner(attr: TokenStream, item: TokenStream) -> TokenStr
127132
render_mode: proc_macro2::TokenStream| {
128133
quote! {
129134
#ignore_cpu
135+
#[cfg(not(all(target_arch = "wasm32", feature = "webgl")))]
130136
#[test]
131137
fn #fn_name() {
132138
use crate::util::{
@@ -166,6 +172,7 @@ pub(crate) fn vello_test_inner(attr: TokenStream, item: TokenStream) -> TokenStr
166172
#f32_snippet
167173

168174
#ignore_hybrid
175+
#[cfg(not(all(target_arch = "wasm32", feature = "webgl")))]
169176
#[test]
170177
fn #hybrid_fn_name() {
171178
use crate::util::{
@@ -180,6 +187,23 @@ pub(crate) fn vello_test_inner(attr: TokenStream, item: TokenStream) -> TokenStr
180187
check_ref(&ctx, #input_fn_name_str, #hybrid_fn_name_str, #hybrid_tolerance, false, RenderMode::OptimizeSpeed);
181188
}
182189
}
190+
191+
#ignore_hybrid
192+
#[cfg(all(target_arch = "wasm32", feature = "webgl"))]
193+
#[wasm_bindgen_test::wasm_bindgen_test]
194+
async fn #webgl_fn_name() {
195+
use crate::util::{
196+
check_ref, get_ctx
197+
};
198+
use vello_hybrid::Scene;
199+
use vello_cpu::RenderMode;
200+
201+
let mut ctx = get_ctx::<Scene>(#width, #height, #transparent);
202+
#input_fn_name(&mut ctx);
203+
if !#no_ref {
204+
check_ref(&ctx, #input_fn_name_str, #webgl_fn_name_str, #hybrid_tolerance, false, RenderMode::OptimizeSpeed);
205+
}
206+
}
183207
};
184208

185209
expanded.into()

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: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
## Testing WebGL on the Browser
19+
20+
To run the `vello_sparse_tests` suite on WebGL headless:
21+
22+
```
23+
wasm-pack test --headless --chrome --features webgl
24+
```
25+
26+
To debug the output images in webgl, run the same command without `--headless`. Any tests that fail
27+
will have their diff image appended to the bottom of the page.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright 2025 the Vello Authors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
//! Build step for `vello_sparse_tests`.
5+
//!
6+
//! This build step exists primarily for the wasm32 target testing the browser exclusive "webgl"
7+
//! feature which cannot read snapshot images from the file system. Instead, the reference images
8+
//! are inlined into the binary.
9+
#[cfg(not(feature = "webgl"))]
10+
fn main() {}
11+
#[cfg(feature = "webgl")]
12+
fn main() {
13+
use std::fs;
14+
use std::io::Write;
15+
use std::path::Path;
16+
println!("cargo:rerun-if-changed=snapshots");
17+
18+
let out_dir = std::env::var("OUT_DIR").unwrap();
19+
let dest_path = Path::new(&out_dir).join("reference_images.rs");
20+
let mut f = fs::File::create(&dest_path).unwrap();
21+
22+
writeln!(
23+
f,
24+
"// Generated by build.rs: Inlined reference images for WASM tests"
25+
)
26+
.unwrap();
27+
writeln!(f).unwrap();
28+
writeln!(
29+
f,
30+
"fn get_reference_image(name: &str) -> Option<&'static [u8]> {{"
31+
)
32+
.unwrap();
33+
writeln!(f, " match name {{").unwrap();
34+
35+
// Read all PNG files from the snapshots directory
36+
let snapshot_dir = Path::new("../vello_sparse_tests/snapshots");
37+
if snapshot_dir.exists() {
38+
for entry in fs::read_dir(snapshot_dir).unwrap() {
39+
let entry = entry.unwrap();
40+
let path = entry.path();
41+
if path.extension().and_then(|s| s.to_str()) == Some("png") {
42+
let name = path.file_stem().unwrap().to_str().unwrap();
43+
writeln!(
44+
f,
45+
r#" "{}" => Some(include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/../vello_sparse_tests/snapshots/{}.png"))),"#,
46+
name, name
47+
).unwrap();
48+
}
49+
}
50+
}
51+
52+
writeln!(f, " _ => None,").unwrap();
53+
writeln!(f, " }}").unwrap();
54+
writeln!(f, "}}").unwrap();
55+
}

sparse_strips/vello_sparse_tests/tests/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
2121
#![allow(missing_docs, reason = "we don't need docs for testing")]
2222
#![allow(clippy::cast_possible_truncation, reason = "not critical for testing")]
23+
#![cfg(all(target_arch = "wasm32", feature = "webgl"))]
24+
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
2325

2426
mod basic;
2527
mod blurred_rounded_rect;

sparse_strips/vello_sparse_tests/tests/renderer.rs

Lines changed: 52 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,57 @@ impl Renderer for Scene {
362363
texture_copy_buffer.unmap();
363364
}
364365

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

sparse_strips/vello_sparse_tests/tests/util.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,14 @@ use vello_common::peniko::{Blob, ColorStop, ColorStops, Font};
2121
use vello_common::pixmap::Pixmap;
2222
use vello_cpu::RenderMode;
2323

24+
#[cfg(target_arch = "wasm32")]
25+
include!(concat!(env!("OUT_DIR"), "/reference_images.rs"));
26+
27+
#[cfg(not(target_arch = "wasm32"))]
2428
static REFS_PATH: LazyLock<PathBuf> = LazyLock::new(|| {
2529
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../vello_sparse_tests/snapshots")
2630
});
31+
#[cfg(not(target_arch = "wasm32"))]
2732
static DIFFS_PATH: LazyLock<PathBuf> =
2833
LazyLock::new(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../vello_sparse_tests/diffs"));
2934

@@ -225,6 +230,7 @@ pub(crate) fn pixmap_to_png(pixmap: Pixmap, width: u32, height: u32) -> Vec<u8>
225230
png_data
226231
}
227232

233+
#[cfg(not(target_arch = "wasm32"))]
228234
pub(crate) fn check_ref(
229235
ctx: &impl Renderer,
230236
// The name of the test.
@@ -286,6 +292,102 @@ pub(crate) fn check_ref(
286292
}
287293
}
288294

295+
#[cfg(target_arch = "wasm32")]
296+
pub(crate) fn check_ref(
297+
ctx: &impl Renderer,
298+
// The name of the test.
299+
test_name: &str,
300+
_: &str,
301+
// Tolerance for pixel differences.
302+
threshold: u8,
303+
// Must be `false` on `wasm32` as reference image cannot be written to filesystem.
304+
is_reference: bool,
305+
render_mode: RenderMode,
306+
) {
307+
assert!(!is_reference, "WASM cannot create new reference images");
308+
309+
let pixmap = render_pixmap(ctx, render_mode);
310+
let encoded_image = pixmap_to_png(pixmap, ctx.width() as u32, ctx.height() as u32);
311+
let actual = load_from_memory(&encoded_image).unwrap().into_rgba8();
312+
313+
let ref_data = get_reference_image(test_name).expect("no reference image exists");
314+
let ref_image = load_from_memory(ref_data).unwrap().into_rgba8();
315+
316+
let diff_image = get_diff(&ref_image, &actual, threshold);
317+
if let Some(ref img) = diff_image {
318+
append_diff_image_to_browser_document(test_name, img);
319+
panic!("test didn't match reference image. Scroll to bottom of browser to view diff.");
320+
}
321+
}
322+
323+
#[cfg(target_arch = "wasm32")]
324+
fn append_diff_image_to_browser_document(test_name: &str, diff_image: &RgbaImage) {
325+
use wasm_bindgen::JsCast;
326+
use web_sys::js_sys::{Array, Uint8Array};
327+
use web_sys::{Blob, BlobPropertyBag, HtmlImageElement, Url, window};
328+
329+
let window = window().unwrap();
330+
let document = window.document().unwrap();
331+
let body = document.body().unwrap();
332+
333+
let container = document.create_element("div").unwrap();
334+
container
335+
.set_attribute(
336+
"style",
337+
"border: 2px solid red; \
338+
margin: 20px; \
339+
padding: 20px; \
340+
background: #f0f0f0; \
341+
display: inline-block;",
342+
)
343+
.unwrap();
344+
345+
let title = document.create_element("h3").unwrap();
346+
title.set_text_content(Some(&format!("Test Failed: {}", test_name)));
347+
title
348+
.set_attribute("style", "color: red; margin-top: 0;")
349+
.unwrap();
350+
container.append_child(&title).unwrap();
351+
352+
let diff_png = {
353+
let mut png_data = Vec::new();
354+
let cursor = std::io::Cursor::new(&mut png_data);
355+
let encoder = image::codecs::png::PngEncoder::new(cursor);
356+
encoder
357+
.write_image(
358+
diff_image.as_raw(),
359+
diff_image.width(),
360+
diff_image.height(),
361+
image::ExtendedColorType::Rgba8,
362+
)
363+
.unwrap();
364+
png_data
365+
};
366+
367+
let uint8_array = Uint8Array::new_with_length(diff_png.len() as u32);
368+
uint8_array.copy_from(&diff_png);
369+
let array = Array::new();
370+
array.push(&uint8_array.buffer());
371+
let mut blob_property_bag = BlobPropertyBag::new();
372+
blob_property_bag.set_type("image/png");
373+
let blob = Blob::new_with_u8_array_sequence_and_options(&array, &blob_property_bag).unwrap();
374+
let url = Url::create_object_url_with_blob(&blob).unwrap();
375+
376+
let img = document
377+
.create_element("img")
378+
.unwrap()
379+
.dyn_into::<HtmlImageElement>()
380+
.unwrap();
381+
img.set_src(&url);
382+
img.set_attribute("style", "border: 1px solid #ccc; max-width: 100%;")
383+
.unwrap();
384+
img.set_attribute("title", "Expected | Diff | Actual")
385+
.unwrap();
386+
387+
container.append_child(&img).unwrap();
388+
body.append_child(&container).unwrap();
389+
}
390+
289391
fn get_diff(
290392
expected_image: &RgbaImage,
291393
actual_image: &RgbaImage,

0 commit comments

Comments
 (0)