Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the vello hybrid webgl tests and LFS. Without LFS the build system can't find any images to compile into the WASM test binary.

Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,27 @@ jobs:

test-stable-wasm:
name: cargo test (wasm32)
needs: prime-lfs-cache
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# We intentionally do not use lfs: true here, instead using the caching method to save LFS bandwidth.

- name: Restore lfs cache
id: lfs-cache
uses: actions/cache/restore@v4
with:
path: .git/lfs
# The files targeted with git lfs
key: vello-lfs-${{ needs.prime-lfs-cache.outputs.lfs-hash }}
enableCrossOsArchive: true

- name: Checkout LFS files
# `git lfs checkout` requires that each individual glob is a separate command line argument.
# The string `''' '''` is how you write `' '` in GitHub's expression context (i.e. two quotes separated by a space)
# The quotes are to avoid the shell from evaluating the globs itself.
run: git lfs checkout '${{ join(fromJson(env.LFS_FILES), ''' ''') }}'
continue-on-error: true

- name: install stable toolchain
uses: dtolnay/rust-toolchain@master
Expand Down Expand Up @@ -337,6 +355,10 @@ jobs:
run: wasm-pack test --headless --chrome
working-directory: sparse_strips/vello_hybrid/examples/native_webgl

- name: check vello_hybrid webgl backend passes vello_sparse_tests suite
run: wasm-pack test --headless --chrome --features webgl
working-directory: sparse_strips/vello_sparse_tests


check-stable-android:
name: cargo check (aarch64-android)
Expand Down
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions sparse_strips/vello_dev_macros/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ pub(crate) fn vello_test_inner(attr: TokenStream, item: TokenStream) -> TokenStr
let u8_fn_name = Ident::new(&format!("{}_cpu_u8", input_fn_name), input_fn_name.span());
let f32_fn_name = Ident::new(&format!("{}_cpu_f32", input_fn_name), input_fn_name.span());
let hybrid_fn_name = Ident::new(&format!("{}_hybrid", input_fn_name), input_fn_name.span());
let webgl_fn_name = Ident::new(
&format!("{}_hybrid_webgl", input_fn_name),
input_fn_name.span(),
);

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

let Arguments {
width,
Expand Down Expand Up @@ -127,6 +132,7 @@ pub(crate) fn vello_test_inner(attr: TokenStream, item: TokenStream) -> TokenStr
render_mode: proc_macro2::TokenStream| {
quote! {
#ignore_cpu
#[cfg(not(all(target_arch = "wasm32", feature = "webgl")))]
#[test]
fn #fn_name() {
use crate::util::{
Expand Down Expand Up @@ -166,6 +172,7 @@ pub(crate) fn vello_test_inner(attr: TokenStream, item: TokenStream) -> TokenStr
#f32_snippet

#ignore_hybrid
#[cfg(not(all(target_arch = "wasm32", feature = "webgl")))]
#[test]
fn #hybrid_fn_name() {
use crate::util::{
Expand All @@ -180,6 +187,23 @@ pub(crate) fn vello_test_inner(attr: TokenStream, item: TokenStream) -> TokenStr
check_ref(&ctx, #input_fn_name_str, #hybrid_fn_name_str, #hybrid_tolerance, false, RenderMode::OptimizeSpeed);
}
}

#ignore_hybrid
#[cfg(all(target_arch = "wasm32", feature = "webgl"))]
#[wasm_bindgen_test::wasm_bindgen_test]
async fn #webgl_fn_name() {
use crate::util::{
check_ref, get_ctx
};
use vello_hybrid::Scene;
use vello_cpu::RenderMode;

let mut ctx = get_ctx::<Scene>(#width, #height, #transparent);
#input_fn_name(&mut ctx);
if !#no_ref {
check_ref(&ctx, #input_fn_name_str, #webgl_fn_name_str, #hybrid_tolerance, false, RenderMode::OptimizeSpeed);
}
}
};

expanded.into()
Expand Down
22 changes: 22 additions & 0 deletions sparse_strips/vello_sparse_tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,27 @@ image = { workspace = true, features = ["png"] }
skrifa = { workspace = true }
smallvec = { workspace = true }

[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen-test = "0.3.50"
web-sys = { version = "0.3.77", features = [
"HtmlCanvasElement",
"WebGl2RenderingContext",
"Document",
"Window",
"Element",
"HtmlElement",
"HtmlImageElement",
"Blob",
"BlobPropertyBag",
"Url",
] }
wasm-bindgen = "0.2.100"

[build-dependencies]
quote = { workspace = true }

[features]
webgl = ["vello_hybrid/webgl"]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ensures that we're testing the vello_hybrid WebGL2 rendering backend by activating the webgl feature.


[lints]
workspace = true
27 changes: 27 additions & 0 deletions sparse_strips/vello_sparse_tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<div align="center">

# Vello Sparse Tests

[![Apache 2.0 or MIT license.](https://img.shields.io/badge/license-Apache--2.0_OR_MIT-blue.svg)](#license)
\
[![Linebender Zulip chat.](https://img.shields.io/badge/Linebender-%23vello-blue?logo=Zulip)](https://xi.zulipchat.com/#narrow/channel/197075-vello)
[![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)

</div>

This is a development only crate for testing the sparse_strip renderers across a corpus of reference
images:
- cpu
- wgpu
- wasm32 WebGL

## Testing WebGL on the Browser

To run the `vello_sparse_tests` suite on WebGL headless:

```sh
wasm-pack test --headless --chrome --features webgl
```

To debug the output images in webgl, run the same command without `--headless`. Any tests that fail
will have their diff image appended to the bottom of the page.
57 changes: 57 additions & 0 deletions sparse_strips/vello_sparse_tests/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! Build step for `vello_sparse_tests`.
//!
//! This build step exists primarily for the wasm32 target testing the browser exclusive "webgl"
//! feature which cannot read snapshot images from the file system. Instead, the reference images
//! are inlined into the binary.
fn main() {
println!("cargo:rerun-if-changed=snapshots");

// Target architecture for compilation must be queried via environment variables.
// Exit early if not building for `wasm32`.
if std::env::var("CARGO_CFG_TARGET_ARCH").as_deref() != Ok("wasm32") {
return;
}

use quote::quote;
use std::fs;
use std::path::Path;

let out_dir = std::env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("reference_images_wasm.rs");

let snapshot_dir = Path::new("snapshots");
let mut match_arms = Vec::new();

if snapshot_dir.exists() {
for entry in fs::read_dir(snapshot_dir).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("png") {
let name = path.file_stem().unwrap().to_str().unwrap();
let name_str = name.to_string();
let path_str = format!("/snapshots/{}.png", name);

match_arms.push(quote! {
#name_str => Some(include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), #path_str))),
});
}
}
}

let generated = quote! {
/// Generated by build.rs: Inlined reference images for WASM tests.
pub(crate) mod reference_images_wasm {
pub(crate) fn get_reference_image(name: &str) -> Option<&'static [u8]> {
match name {
#(#match_arms)*
_ => None,
}
}
}
};

fs::write(&dest_path, generated.to_string()).unwrap();
}
3 changes: 3 additions & 0 deletions sparse_strips/vello_sparse_tests/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
#![allow(missing_docs, reason = "we don't need docs for testing")]
#![allow(clippy::cast_possible_truncation, reason = "not critical for testing")]

#[cfg(all(target_arch = "wasm32", feature = "webgl"))]
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);

mod basic;
mod blurred_rounded_rect;
mod clip;
Expand Down
52 changes: 52 additions & 0 deletions sparse_strips/vello_sparse_tests/tests/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ impl Renderer for Scene {
// This method creates device resources every time it is called. This does not matter much for
// testing, but should not be used as a basis for implementing something real. This would be a
// very bad example for that.
#[cfg(not(all(target_arch = "wasm32", feature = "webgl")))]
fn render_to_pixmap(&self, pixmap: &mut Pixmap, _: RenderMode) {
// On some platforms using `cargo test` triggers segmentation faults in wgpu when the GPU
// tests are run in parallel (likely related to the number of device resources being
Expand Down Expand Up @@ -362,6 +363,57 @@ impl Renderer for Scene {
texture_copy_buffer.unmap();
}

#[cfg(all(target_arch = "wasm32", feature = "webgl"))]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we given much thought to the pattern we use for target specific implementations? We're currently using the below approach:

#[cfg(TARGET_1)]
pub fn check_ref(...) { ... }

#[cfg(TARGET_2)]
pub fn check_ref(...) { ... }

I suspect that, for better organisation and discovering all target implementations, something like the below may be better when we have separate function implementations. It's more code, but I think it might ease the addition of later targets (e.g. cpu webgl) and improves discoverability.

pub(crate) fn check_ref(
    ...
) {
    #[cfg(TARGET_1)]
    check_ref_wgpu(...)
    #[cfg(TARGET_2]
    check_ref_webgl(...)
}

#[cfg(TARGET_1)]
fn check_ref_wgpu(...) { ... }

#[cfg(TARGET_2)]
fn check_ref_webgl(...) { ... }

I think when we have sufficiently complex scenarios, it might be worth using a module splitting approach. But perhaps for small functions, the above is better?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracking this great refactor as followup work that I'll include in the PR resolving this issue: #1025 (comment)

fn render_to_pixmap(&self, pixmap: &mut Pixmap, _: RenderMode) {
use wasm_bindgen::JsCast;
use web_sys::{HtmlCanvasElement, WebGl2RenderingContext};

let width = self.width();
let height = self.height();

// Create an offscreen HTMLCanvasElement, render the test image to it, and finally read off
// the pixmap for diff checking.
let document = web_sys::window().unwrap().document().unwrap();

let canvas = document
.create_element("canvas")
.unwrap()
.dyn_into::<HtmlCanvasElement>()
.unwrap();

canvas.set_width(width.into());
canvas.set_height(height.into());

let mut renderer = vello_hybrid::WebGlRenderer::new(&canvas);
let render_size = vello_hybrid::RenderSize {
width: width.into(),
height: height.into(),
};

renderer.render(self, &render_size).unwrap();

let gl = canvas
.get_context("webgl2")
.unwrap()
.unwrap()
.dyn_into::<WebGl2RenderingContext>()
.unwrap();
let mut pixels = vec![0_u8; (width as usize) * (height as usize) * 4];
gl.read_pixels_with_opt_u8_array(
0,
0,
width.into(),
height.into(),
WebGl2RenderingContext::RGBA,
WebGl2RenderingContext::UNSIGNED_BYTE,
Some(&mut pixels),
)
.unwrap();

let pixmap_data = pixmap.data_as_u8_slice_mut();
pixmap_data.copy_from_slice(&pixels);
}

fn width(&self) -> u16 {
Self::width(self)
}
Expand Down
Loading
Loading