From ef010cdd006ab1a6ffeb7c16a785eaff61b45f08 Mon Sep 17 00:00:00 2001 From: Taj Pereira Date: Tue, 10 Feb 2026 04:07:19 +1100 Subject: [PATCH 1/2] 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. + } +} From 764d27c670344a1838a2026c30ebf0527385f98b Mon Sep 17 00:00:00 2001 From: Taj Pereira Date: Tue, 10 Feb 2026 05:12:26 +1100 Subject: [PATCH 2/2] Expose image manager --- crates/anyrender_vello_hybrid/src/lib.rs | 1 + crates/anyrender_vello_hybrid/src/scene.rs | 54 +++++++++++-------- .../src/window_renderer.rs | 2 +- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/crates/anyrender_vello_hybrid/src/lib.rs b/crates/anyrender_vello_hybrid/src/lib.rs index ae11636..632e0ae 100644 --- a/crates/anyrender_vello_hybrid/src/lib.rs +++ b/crates/anyrender_vello_hybrid/src/lib.rs @@ -6,6 +6,7 @@ mod scene; mod webgl_scene; mod window_renderer; +pub use scene::ImageManager; pub use scene::VelloHybridScenePainter; #[cfg(all(target_arch = "wasm32", feature = "webgl"))] pub use webgl_scene::*; diff --git a/crates/anyrender_vello_hybrid/src/scene.rs b/crates/anyrender_vello_hybrid/src/scene.rs index c658138..2a23462 100644 --- a/crates/anyrender_vello_hybrid/src/scene.rs +++ b/crates/anyrender_vello_hybrid/src/scene.rs @@ -10,22 +10,18 @@ const DEFAULT_TOLERANCE: f64 = 0.1; fn anyrender_paint_to_vello_hybrid_paint<'a>( paint: PaintRef<'a>, - mut image_manager: &mut Option<&mut ImageManager<'_>>, + image_manager: &mut ImageManager<'_>, ) -> PaintType { match paint { Paint::Solid(alpha_color) => PaintType::Solid(alpha_color), Paint::Gradient(gradient) => PaintType::Gradient(gradient.clone()), Paint::Image(image_brush) => { - if let Some(image_manager) = &mut image_manager { - let image_id = image_manager.upload_image(image_brush.image); - PaintType::Image(ImageBrush { - image: ImageSource::OpaqueId(image_id), - sampler: image_brush.sampler, - }) - } else { - PaintType::Solid(peniko::color::palette::css::TRANSPARENT) - } + let image_id = image_manager.upload_image(image_brush.image); + PaintType::Image(ImageBrush { + image: ImageSource::OpaqueId(image_id), + sampler: image_brush.sampler, + }) } // TODO: custom paint @@ -33,7 +29,7 @@ fn anyrender_paint_to_vello_hybrid_paint<'a>( } } -pub(crate) struct ImageManager<'a> { +pub struct ImageManager<'a> { pub(crate) renderer: &'a mut Renderer, pub(crate) device: &'a Device, pub(crate) queue: &'a Queue, @@ -41,7 +37,23 @@ pub(crate) struct ImageManager<'a> { pub(crate) cache: &'a mut FxHashMap, } -impl ImageManager<'_> { +impl<'a> ImageManager<'a> { + pub fn new( + renderer: &'a mut Renderer, + device: &'a Device, + queue: &'a Queue, + encoder: &'a mut CommandEncoder, + cache: &'a mut FxHashMap, + ) -> Self { + Self { + renderer, + device, + queue, + encoder, + cache, + } + } + pub(crate) fn upload_image(&mut self, image: &ImageData) -> ImageId { let peniko_id = image.data.id(); @@ -76,15 +88,18 @@ pub(crate) enum LayerKind { pub struct VelloHybridScenePainter<'s> { pub(crate) scene: &'s mut vello_hybrid::Scene, pub(crate) layer_stack: Vec, - pub(crate) image_manager: Option>, + pub(crate) image_manager: ImageManager<'s>, } impl VelloHybridScenePainter<'_> { - pub fn new<'s>(scene: &'s mut vello_hybrid::Scene) -> VelloHybridScenePainter<'s> { + pub fn new<'s>( + scene: &'s mut vello_hybrid::Scene, + image_manager: ImageManager<'s>, + ) -> VelloHybridScenePainter<'s> { VelloHybridScenePainter { scene, layer_stack: Vec::with_capacity(16), - image_manager: None, + image_manager, } } } @@ -138,8 +153,7 @@ impl PaintScene for VelloHybridScenePainter<'_> { ) { self.scene.set_transform(transform); self.scene.set_stroke(style.clone()); - let paint = - anyrender_paint_to_vello_hybrid_paint(paint.into(), &mut self.image_manager.as_mut()); + let paint = anyrender_paint_to_vello_hybrid_paint(paint.into(), &mut self.image_manager); self.scene.set_paint(paint); self.scene .set_paint_transform(brush_transform.unwrap_or(Affine::IDENTITY)); @@ -156,8 +170,7 @@ impl PaintScene for VelloHybridScenePainter<'_> { ) { self.scene.set_transform(transform); self.scene.set_fill_rule(style); - let paint = - anyrender_paint_to_vello_hybrid_paint(paint.into(), &mut self.image_manager.as_mut()); + let paint = anyrender_paint_to_vello_hybrid_paint(paint.into(), &mut self.image_manager); self.scene.set_paint(paint); self.scene .set_paint_transform(brush_transform.unwrap_or(Affine::IDENTITY)); @@ -177,8 +190,7 @@ impl PaintScene for VelloHybridScenePainter<'_> { glyph_transform: Option, glyphs: impl Iterator, ) { - let paint = - anyrender_paint_to_vello_hybrid_paint(paint.into(), &mut self.image_manager.as_mut()); + let paint = anyrender_paint_to_vello_hybrid_paint(paint.into(), &mut self.image_manager); self.scene.set_paint(paint); self.scene.set_transform(transform); diff --git a/crates/anyrender_vello_hybrid/src/window_renderer.rs b/crates/anyrender_vello_hybrid/src/window_renderer.rs index 43b9b38..e6e216d 100644 --- a/crates/anyrender_vello_hybrid/src/window_renderer.rs +++ b/crates/anyrender_vello_hybrid/src/window_renderer.rs @@ -222,7 +222,7 @@ impl WindowRenderer for VelloHybridWindowRenderer { draw_fn(&mut VelloHybridScenePainter { scene: &mut self.scene, layer_stack: Vec::new(), - image_manager: Some(image_manager), + image_manager, }); timer.record_time("cmd");