From ef010cdd006ab1a6ffeb7c16a785eaff61b45f08 Mon Sep 17 00:00:00 2001 From: Taj Pereira Date: Tue, 10 Feb 2026 04:07:19 +1100 Subject: [PATCH] Vello Hybrid WebGL --- .github/workflows/ci.yml | 16 ++ Cargo.lock | 59 +++-- crates/anyrender_vello_hybrid/Cargo.toml | 1 + crates/anyrender_vello_hybrid/src/lib.rs | 4 + .../anyrender_vello_hybrid/src/webgl_scene.rs | 217 ++++++++++++++++++ 5 files changed, 283 insertions(+), 14 deletions(-) create mode 100644 crates/anyrender_vello_hybrid/src/webgl_scene.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67bc4e0..1ea7b77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,6 +74,22 @@ jobs: - uses: dtolnay/rust-toolchain@stable - run: cargo doc + check-wasm: + name: "Check [wasm32 + webgl]" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + components: clippy + - uses: Swatinem/rust-cache@v2 + with: + key: "wasm32-unknown-unknown" + cache-all-crates: "true" + save-if: ${{ github.ref == 'refs/heads/main' }} + - run: cargo clippy -p anyrender_vello_hybrid --target wasm32-unknown-unknown --features webgl -- -D warnings + # just cargo check for now matrix_test: runs-on: ${{ matrix.platform.os }} diff --git a/Cargo.lock b/Cargo.lock index c98bb8d..b58cc7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1283,6 +1283,25 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1734,9 +1753,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -2544,6 +2563,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pixels" version = "0.15.0" @@ -3726,10 +3751,12 @@ dependencies = [ "bytemuck", "guillotiere", "hashbrown 0.16.1", + "js-sys", "log", "thiserror 2.0.17", "vello_common", "vello_sparse_shaders", + "web-sys", "wgpu 27.0.1", ] @@ -3751,6 +3778,9 @@ name = "vello_sparse_shaders" version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d84ceb248d455e90199f37bf3e3dab63bbe10ad75c0b0556611a9ad094ef1b6b" +dependencies = [ + "naga 27.0.3", +] [[package]] name = "version_check" @@ -3779,9 +3809,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -3792,11 +3822,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.55" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -3805,9 +3836,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3815,9 +3846,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", @@ -3828,9 +3859,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] @@ -3946,9 +3977,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/crates/anyrender_vello_hybrid/Cargo.toml b/crates/anyrender_vello_hybrid/Cargo.toml index 86f6a89..bfb1155 100644 --- a/crates/anyrender_vello_hybrid/Cargo.toml +++ b/crates/anyrender_vello_hybrid/Cargo.toml @@ -10,6 +10,7 @@ edition.workspace = true [features] log_frame_times = ["debug_timer/enable"] +webgl = ["vello_hybrid/webgl"] [dependencies] anyrender = { workspace = true } diff --git a/crates/anyrender_vello_hybrid/src/lib.rs b/crates/anyrender_vello_hybrid/src/lib.rs index d201136..ae11636 100644 --- a/crates/anyrender_vello_hybrid/src/lib.rs +++ b/crates/anyrender_vello_hybrid/src/lib.rs @@ -2,7 +2,11 @@ #![cfg_attr(docsrs, feature(doc_cfg))] mod scene; +#[cfg(all(target_arch = "wasm32", feature = "webgl"))] +mod webgl_scene; mod window_renderer; pub use scene::VelloHybridScenePainter; +#[cfg(all(target_arch = "wasm32", feature = "webgl"))] +pub use webgl_scene::*; pub use window_renderer::*; diff --git a/crates/anyrender_vello_hybrid/src/webgl_scene.rs b/crates/anyrender_vello_hybrid/src/webgl_scene.rs new file mode 100644 index 0000000..db4b709 --- /dev/null +++ b/crates/anyrender_vello_hybrid/src/webgl_scene.rs @@ -0,0 +1,217 @@ +//! WebGL-compatible [`PaintScene`] implementation for [`vello_hybrid::Scene`]. + +use anyrender::{Glyph, NormalizedCoord, Paint, PaintRef, PaintScene}; +use kurbo::{Affine, Rect, Shape, Stroke}; +use peniko::{BlendMode, Color, Fill, FontData, StyleRef}; +use vello_common::paint::PaintType; + +use peniko::ImageBrush; +use rustc_hash::FxHashMap; +use vello_common::paint::{ImageId, ImageSource}; + +const DEFAULT_TOLERANCE: f64 = 0.1; + +pub struct WebGlImageManager<'a> { + pub(crate) renderer: &'a mut vello_hybrid::WebGlRenderer, + pub(crate) cache: &'a mut FxHashMap, +} + +impl<'a> WebGlImageManager<'a> { + pub fn new( + renderer: &'a mut vello_hybrid::WebGlRenderer, + cache: &'a mut FxHashMap, + ) -> Self { + Self { renderer, cache } + } + + pub(crate) fn upload_image(&mut self, image: &peniko::ImageData) -> ImageId { + let peniko_id = image.data.id(); + + if let Some(atlas_id) = self.cache.get(&peniko_id) { + return *atlas_id; + } + + let ImageSource::Pixmap(pixmap) = ImageSource::from_peniko_image_data(image) else { + unreachable!(); + }; + + let atlas_id = self.renderer.upload_image(&pixmap); + self.cache.insert(peniko_id, atlas_id); + atlas_id + } +} + +enum LayerKind { + Layer, + Clip, +} + +pub struct WebGlScenePainter<'s> { + scene: &'s mut vello_hybrid::Scene, + layer_stack: Vec, + image_manager: WebGlImageManager<'s>, +} + +impl<'s> WebGlScenePainter<'s> { + pub fn new(scene: &'s mut vello_hybrid::Scene, image_manager: WebGlImageManager<'s>) -> Self { + Self { + scene, + layer_stack: Vec::with_capacity(16), + image_manager, + } + } +} + +impl WebGlScenePainter<'_> { + fn convert_paint(&mut self, paint: PaintRef<'_>) -> PaintType { + match paint { + Paint::Solid(alpha_color) => PaintType::Solid(alpha_color), + Paint::Gradient(gradient) => PaintType::Gradient(gradient.clone()), + Paint::Image(image_brush) => self.convert_image_paint(image_brush), + Paint::Custom(_) => PaintType::Solid(peniko::color::palette::css::TRANSPARENT), + } + } + + fn convert_image_paint(&mut self, image_brush: peniko::ImageBrushRef<'_>) -> PaintType { + let image_id = self.image_manager.upload_image(image_brush.image); + PaintType::Image(ImageBrush { + image: ImageSource::OpaqueId(image_id), + sampler: image_brush.sampler, + }) + } +} + +impl PaintScene for WebGlScenePainter<'_> { + fn reset(&mut self) { + self.scene.reset(); + } + + fn push_layer( + &mut self, + blend: impl Into, + alpha: f32, + transform: Affine, + clip: &impl Shape, + ) { + self.scene.set_transform(transform); + self.layer_stack.push(LayerKind::Layer); + self.scene.push_layer( + Some(&clip.into_path(DEFAULT_TOLERANCE)), + Some(blend.into()), + Some(alpha), + None, + None, + ); + } + + fn push_clip_layer(&mut self, transform: Affine, clip: &impl Shape) { + self.scene.set_transform(transform); + self.layer_stack.push(LayerKind::Clip); + self.scene + .push_clip_path(&clip.into_path(DEFAULT_TOLERANCE)); + } + + fn pop_layer(&mut self) { + if let Some(kind) = self.layer_stack.pop() { + match kind { + LayerKind::Layer => self.scene.pop_layer(), + LayerKind::Clip => self.scene.pop_clip_path(), + } + } + } + + fn stroke<'a>( + &mut self, + style: &Stroke, + transform: Affine, + paint: impl Into>, + brush_transform: Option, + shape: &impl Shape, + ) { + self.scene.set_transform(transform); + self.scene.set_stroke(style.clone()); + let paint = self.convert_paint(paint.into()); + self.scene.set_paint(paint); + self.scene + .set_paint_transform(brush_transform.unwrap_or(Affine::IDENTITY)); + self.scene.stroke_path(&shape.into_path(DEFAULT_TOLERANCE)); + } + + fn fill<'a>( + &mut self, + style: Fill, + transform: Affine, + paint: impl Into>, + brush_transform: Option, + shape: &impl Shape, + ) { + self.scene.set_transform(transform); + self.scene.set_fill_rule(style); + let paint = self.convert_paint(paint.into()); + self.scene.set_paint(paint); + self.scene + .set_paint_transform(brush_transform.unwrap_or(Affine::IDENTITY)); + self.scene.fill_path(&shape.into_path(DEFAULT_TOLERANCE)); + } + + fn draw_glyphs<'a, 's2: 'a>( + &'a mut self, + font: &'a FontData, + font_size: f32, + hint: bool, + normalized_coords: &'a [NormalizedCoord], + style: impl Into>, + paint: impl Into>, + _brush_alpha: f32, + transform: Affine, + glyph_transform: Option, + glyphs: impl Iterator, + ) { + let paint = self.convert_paint(paint.into()); + self.scene.set_paint(paint); + self.scene.set_transform(transform); + + fn into_vello_glyph(g: Glyph) -> vello_common::glyph::Glyph { + vello_common::glyph::Glyph { + id: g.id, + x: g.x, + y: g.y, + } + } + + let style: StyleRef<'a> = style.into(); + match style { + StyleRef::Fill(fill) => { + self.scene.set_fill_rule(fill); + self.scene + .glyph_run(font) + .font_size(font_size) + .hint(hint) + .normalized_coords(normalized_coords) + .glyph_transform(glyph_transform.unwrap_or_default()) + .fill_glyphs(glyphs.map(into_vello_glyph)); + } + StyleRef::Stroke(stroke) => { + self.scene.set_stroke(stroke.clone()); + self.scene + .glyph_run(font) + .font_size(font_size) + .hint(hint) + .normalized_coords(normalized_coords) + .glyph_transform(glyph_transform.unwrap_or_default()) + .stroke_glyphs(glyphs.map(into_vello_glyph)); + } + } + } + + fn draw_box_shadow( + &mut self, + _transform: Affine, + _rect: Rect, + _color: Color, + _radius: f64, + _std_dev: f64, + ) { + // Not yet supported in vello_hybrid WebGL. + } +}