Skip to content

Commit 2871242

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

File tree

8 files changed

+292
-0
lines changed

8 files changed

+292
-0
lines changed

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: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,13 @@ 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 = ["HtmlCanvasElement", "WebGl2RenderingContext", "Document", "Window", "Element", "HtmlElement", "HtmlImageElement", "Blob", "BlobPropertyBag", "Url"] }
34+
wasm-bindgen = "0.2.100"
35+
36+
[features]
37+
webgl = ["vello_hybrid/webgl"]
38+
3139
[lints]
3240
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: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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!(f, "// Auto-generated reference images for WASM tests").unwrap();
23+
writeln!(f).unwrap();
24+
writeln!(
25+
f,
26+
"pub fn get_reference_image(name: &str) -> Option<&'static [u8]> {{"
27+
)
28+
.unwrap();
29+
writeln!(f, " match name {{").unwrap();
30+
31+
// Read all PNG files from the snapshots directory
32+
let snapshot_dir = Path::new("../vello_sparse_tests/snapshots");
33+
if snapshot_dir.exists() {
34+
for entry in fs::read_dir(snapshot_dir).unwrap() {
35+
let entry = entry.unwrap();
36+
let path = entry.path();
37+
if path.extension().and_then(|s| s.to_str()) == Some("png") {
38+
let name = path.file_stem().unwrap().to_str().unwrap();
39+
writeln!(
40+
f,
41+
r#" "{}" => Some(include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/../vello_sparse_tests/snapshots/{}.png"))),"#,
42+
name, name
43+
).unwrap();
44+
}
45+
}
46+
}
47+
48+
writeln!(f, " _ => None,").unwrap();
49+
writeln!(f, " }}").unwrap();
50+
writeln!(f, "}}").unwrap();
51+
}

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"))]
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: 63 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,68 @@ 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 canvas for rendering
375+
let document = web_sys::window()
376+
.expect("Failed to get window")
377+
.document()
378+
.expect("Failed to get document");
379+
380+
let canvas = document
381+
.create_element("canvas")
382+
.expect("Failed to create canvas")
383+
.dyn_into::<HtmlCanvasElement>()
384+
.expect("Failed to cast to HtmlCanvasElement");
385+
386+
// Set canvas dimensions
387+
canvas.set_width(width.into());
388+
canvas.set_height(height.into());
389+
390+
// Create WebGL renderer
391+
let mut renderer = vello_hybrid::WebGlRenderer::new(&canvas);
392+
393+
// Create render size
394+
let render_size = vello_hybrid::RenderSize {
395+
width: width.into(),
396+
height: height.into(),
397+
};
398+
399+
// Render the scene
400+
renderer.render(self, &render_size).unwrap();
401+
402+
// Get the WebGL context to read pixels
403+
let gl = canvas
404+
.get_context("webgl2")
405+
.unwrap()
406+
.unwrap()
407+
.dyn_into::<WebGl2RenderingContext>()
408+
.unwrap();
409+
410+
// Create a buffer to read pixels into
411+
let mut pixels = vec![0u8; (width as usize) * (height as usize) * 4];
412+
413+
gl.read_pixels_with_opt_u8_array(
414+
0,
415+
0,
416+
width.into(),
417+
height.into(),
418+
WebGl2RenderingContext::RGBA,
419+
WebGl2RenderingContext::UNSIGNED_BYTE,
420+
Some(&mut pixels),
421+
)
422+
.expect("Failed to read pixels");
423+
424+
let pixmap_data = pixmap.data_as_u8_slice_mut();
425+
pixmap_data.copy_from_slice(&pixels);
426+
}
427+
365428
fn width(&self) -> u16 {
366429
Self::width(self)
367430
}

sparse_strips/vello_sparse_tests/tests/util.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ 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+
2427
static REFS_PATH: LazyLock<PathBuf> = LazyLock::new(|| {
2528
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../vello_sparse_tests/snapshots")
2629
});
@@ -225,6 +228,7 @@ pub(crate) fn pixmap_to_png(pixmap: Pixmap, width: u32, height: u32) -> Vec<u8>
225228
png_data
226229
}
227230

231+
#[cfg(not(target_arch = "wasm32"))]
228232
pub(crate) fn check_ref(
229233
ctx: &impl Renderer,
230234
// The name of the test.
@@ -286,6 +290,116 @@ pub(crate) fn check_ref(
286290
}
287291
}
288292

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

0 commit comments

Comments
 (0)