From f651f94b609bbd39e364919b0e8c9e70c99c7deb Mon Sep 17 00:00:00 2001 From: Taj Pereira Date: Fri, 13 Feb 2026 04:01:19 +1100 Subject: [PATCH 1/7] Use render context --- Cargo.lock | 5 +- crates/anyrender/Cargo.toml | 1 + crates/anyrender/src/lib.rs | 60 +++--- crates/anyrender/src/null_backend.rs | 50 ++++- crates/anyrender/src/recording.rs | 57 +++++- crates/anyrender/src/types.rs | 75 ++++---- crates/anyrender_serialize/Cargo.toml | 2 + crates/anyrender_serialize/src/font_writer.rs | 18 +- crates/anyrender_serialize/src/lib.rs | 113 +++++++----- crates/anyrender_serialize/tests/serialize.rs | 116 ++++++++---- crates/anyrender_skia/Cargo.toml | 1 + crates/anyrender_skia/src/image_renderer.rs | 13 +- crates/anyrender_skia/src/lib.rs | 2 +- crates/anyrender_skia/src/scene.rs | 91 +++++++--- crates/anyrender_skia/src/window_renderer.rs | 12 +- crates/anyrender_svg/src/lib.rs | 53 +++++- crates/anyrender_svg/src/render.rs | 30 ++- crates/anyrender_svg/src/util.rs | 10 +- crates/anyrender_vello/src/image_renderer.rs | 8 +- crates/anyrender_vello/src/lib.rs | 2 +- crates/anyrender_vello/src/scene.rs | 139 +++++++++++--- crates/anyrender_vello/src/window_renderer.rs | 13 +- crates/anyrender_vello_cpu/Cargo.toml | 5 - .../anyrender_vello_cpu/src/image_renderer.rs | 42 +++-- crates/anyrender_vello_cpu/src/lib.rs | 2 +- crates/anyrender_vello_cpu/src/scene.rs | 146 +++++++++------ crates/anyrender_vello_hybrid/src/lib.rs | 5 +- crates/anyrender_vello_hybrid/src/scene.rs | 165 ++++++++++------- .../anyrender_vello_hybrid/src/webgl_scene.rs | 102 +++++++---- .../src/window_renderer.rs | 73 ++------ crates/pixels_window_renderer/src/lib.rs | 9 +- crates/softbuffer_window_renderer/src/lib.rs | 9 +- examples/bunnymark/Cargo.toml | 2 +- examples/bunnymark/src/bunny.rs | 50 +++-- examples/bunnymark/src/main.rs | 128 +++++++------ examples/serialize/src/main.rs | 66 ++++--- examples/winit/src/main.rs | 171 +++++++++--------- 37 files changed, 1184 insertions(+), 662 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a3c1d17..0432958 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,6 +160,7 @@ dependencies = [ "kurbo", "peniko", "raw-window-handle", + "rustc-hash 2.1.1", "serde", ] @@ -171,7 +172,8 @@ dependencies = [ "image", "kurbo", "peniko", - "read-fonts 0.37.0", + "read-fonts 0.37.0 (git+https://github.com/googlefonts/fontations?rev=41ec2bdd6dd8589b65eaaaf182b0a2e701d0d826)", + "rustc-hash 2.1.1", "serde", "serde_json", "sha2", @@ -203,6 +205,7 @@ dependencies = [ "peniko", "pixels_window_renderer", "raw-window-handle", + "rustc-hash 2.1.1", "skia-safe", "softbuffer_window_renderer", ] diff --git a/crates/anyrender/Cargo.toml b/crates/anyrender/Cargo.toml index f65a8ca..836919f 100644 --- a/crates/anyrender/Cargo.toml +++ b/crates/anyrender/Cargo.toml @@ -20,6 +20,7 @@ serde = [ kurbo = { workspace = true } peniko = { workspace = true } raw-window-handle = { workspace = true } +rustc-hash = { workspace = true } # Serde serde = { workspace = true, features = ["derive"], optional = true } diff --git a/crates/anyrender/src/lib.rs b/crates/anyrender/src/lib.rs index 1048cb0..3b193b5 100644 --- a/crates/anyrender/src/lib.rs +++ b/crates/anyrender/src/lib.rs @@ -28,7 +28,7 @@ //! - [anyrender_vello_cpu](https://docs.rs/anyrender_vello_cpu) use kurbo::{Affine, Rect, Shape, Stroke}; -use peniko::{BlendMode, Brush, Color, Fill, FontData, ImageBrushRef, StyleRef}; +use peniko::{BlendMode, Brush, Color, Fill, FontData, ImageBrush, StyleRef}; use recording::RenderCommand; use std::sync::Arc; @@ -39,18 +39,23 @@ pub use types::*; mod null_backend; pub use null_backend::*; pub mod recording; -pub use recording::Scene; +pub use recording::{RecordingRenderContext, Scene}; /// Abstraction for rendering a scene to a window pub trait WindowRenderer { type ScenePainter<'a>: PaintScene where Self: 'a; + type Context: RenderContext; fn resume(&mut self, window: Arc, width: u32, height: u32); fn suspend(&mut self); fn is_active(&self) -> bool; fn set_size(&mut self, width: u32, height: u32); - fn render)>(&mut self, draw_fn: F); + fn render)>( + &mut self, + ctx: &mut Self::Context, + draw_fn: F, + ); } /// Abstraction for rendering a scene to an image buffer @@ -58,26 +63,34 @@ pub trait ImageRenderer { type ScenePainter<'a>: PaintScene where Self: 'a; + type Context: RenderContext; fn new(width: u32, height: u32) -> Self; fn resize(&mut self, width: u32, height: u32); fn reset(&mut self); fn render_to_vec)>( &mut self, + ctx: &mut Self::Context, draw_fn: F, vec: &mut Vec, ); - fn render)>(&mut self, draw_fn: F, buffer: &mut [u8]); + fn render)>( + &mut self, + ctx: &mut Self::Context, + draw_fn: F, + buffer: &mut [u8], + ); } /// Draw a scene to a buffer using an `ImageRenderer` pub fn render_to_buffer)>( + ctx: &mut R::Context, draw_fn: F, width: u32, height: u32, ) -> Vec { let mut buf = Vec::with_capacity((width * height * 4) as usize); let mut renderer = R::new(width, height); - renderer.render_to_vec(draw_fn, &mut buf); + renderer.render_to_vec(ctx, draw_fn, &mut buf); buf } @@ -171,10 +184,10 @@ pub trait PaintScene { RenderCommand::Stroke(cmd) => self.stroke( &cmd.style, scene_transform * cmd.transform, - match cmd.brush { - Brush::Solid(alpha_color) => Brush::Solid(alpha_color), - Brush::Gradient(ref gradient) => Brush::Gradient(gradient), - Brush::Image(ref image) => Brush::Image(image.as_ref()), + match &cmd.brush { + Brush::Solid(alpha_color) => Paint::Solid(*alpha_color), + Brush::Gradient(gradient) => Paint::Gradient(gradient), + Brush::Image(image) => Paint::Image(image.clone()), }, cmd.brush_transform, &cmd.shape, @@ -182,10 +195,10 @@ pub trait PaintScene { RenderCommand::Fill(cmd) => self.fill( cmd.fill, scene_transform * cmd.transform, - match cmd.brush { - Brush::Solid(alpha_color) => Brush::Solid(alpha_color), - Brush::Gradient(ref gradient) => Brush::Gradient(gradient), - Brush::Image(ref image) => Brush::Image(image.as_ref()), + match &cmd.brush { + Brush::Solid(alpha_color) => Paint::Solid(*alpha_color), + Brush::Gradient(gradient) => Paint::Gradient(gradient), + Brush::Image(image) => Paint::Image(image.clone()), }, cmd.brush_transform, &cmd.shape, @@ -196,10 +209,10 @@ pub trait PaintScene { cmd.hint, &cmd.normalized_coords, &cmd.style, - match cmd.brush { - Brush::Solid(alpha_color) => Brush::Solid(alpha_color), - Brush::Gradient(ref gradient) => Brush::Gradient(gradient), - Brush::Image(ref image) => Brush::Image(image.as_ref()), + match &cmd.brush { + Brush::Solid(alpha_color) => Paint::Solid(*alpha_color), + Brush::Gradient(gradient) => Paint::Gradient(gradient), + Brush::Image(image) => Paint::Image(image.clone()), }, cmd.brush_alpha, scene_transform * cmd.transform, @@ -217,19 +230,16 @@ pub trait PaintScene { } } - /// Utility method to draw an image at it's natural size. For more advanced image drawing use the `fill` method - fn draw_image(&mut self, image: ImageBrushRef, transform: Affine) { + /// Utility method to draw an image at its natural size. For more advanced image drawing use the `fill` method + fn draw_image(&mut self, image: ImageBrush, transform: Affine) { + let width = image.image.width as f64; + let height = image.image.height as f64; self.fill( Fill::NonZero, transform, image, None, - &Rect::new( - 0.0, - 0.0, - image.image.width as f64, - image.image.height as f64, - ), + &Rect::new(0.0, 0.0, width, height), ); } } diff --git a/crates/anyrender/src/null_backend.rs b/crates/anyrender/src/null_backend.rs index 3af5dcf..a836dcf 100644 --- a/crates/anyrender/src/null_backend.rs +++ b/crates/anyrender/src/null_backend.rs @@ -1,8 +1,32 @@ //! A dummy implementation of the AnyRender traits while simply ignores all commands -use crate::{ImageRenderer, PaintScene, WindowHandle, WindowRenderer}; +use crate::{ + ImageRenderer, ImageResource, PaintScene, RenderContext, ResourceId, WindowHandle, + WindowRenderer, +}; use std::sync::Arc; +#[derive(Default)] +pub struct NullRenderContext {} + +impl NullRenderContext { + pub fn new() -> Self { + Self {} + } +} + +impl RenderContext for NullRenderContext { + fn register_image(&mut self, image: peniko::ImageData) -> ImageResource { + ImageResource { + id: ResourceId(0), + width: image.width, + height: image.height, + } + } + + fn unregister_resource(&mut self, _id: ResourceId) {} +} + #[derive(Copy, Clone, Default)] pub struct NullWindowRenderer { is_active: bool, @@ -19,6 +43,7 @@ impl WindowRenderer for NullWindowRenderer { = NullScenePainter where Self: 'a; + type Context = NullRenderContext; fn resume(&mut self, _window: Arc, _width: u32, _height: u32) { self.is_active = true; @@ -34,15 +59,20 @@ impl WindowRenderer for NullWindowRenderer { fn set_size(&mut self, _width: u32, _height: u32) {} - fn render)>(&mut self, _draw_fn: F) {} + fn render)>( + &mut self, + _ctx: &mut Self::Context, + _draw_fn: F, + ) { + } } -#[derive(Copy, Clone, Default)] +#[derive(Clone, Default)] pub struct NullImageRenderer; impl NullImageRenderer { pub fn new() -> Self { - Self + Self::default() } } @@ -51,9 +81,10 @@ impl ImageRenderer for NullImageRenderer { = NullScenePainter where Self: 'a; + type Context = NullRenderContext; fn new(_width: u32, _height: u32) -> Self { - Self + Self::default() } fn resize(&mut self, _width: u32, _height: u32) {} @@ -62,12 +93,19 @@ impl ImageRenderer for NullImageRenderer { fn render_to_vec)>( &mut self, + _ctx: &mut Self::Context, _draw_fn: F, _vec: &mut Vec, ) { } - fn render)>(&mut self, _draw_fn: F, _buffer: &mut [u8]) {} + fn render)>( + &mut self, + _ctx: &mut Self::Context, + _draw_fn: F, + _buffer: &mut [u8], + ) { + } } #[derive(Copy, Clone, Default)] diff --git a/crates/anyrender/src/recording.rs b/crates/anyrender/src/recording.rs index ff6fc73..908e9d8 100644 --- a/crates/anyrender/src/recording.rs +++ b/crates/anyrender/src/recording.rs @@ -1,6 +1,9 @@ -use crate::{Glyph, NormalizedCoord, Paint, PaintRef, PaintScene}; +use crate::{ + Glyph, ImageResource, NormalizedCoord, Paint, PaintRef, PaintScene, RenderContext, ResourceId, +}; use kurbo::{Affine, BezPath, Rect, Shape, Stroke}; use peniko::{BlendMode, Brush, Color, Fill, FontData, ImageBrush, ImageData, Style, StyleRef}; +use rustc_hash::FxHashMap; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -9,7 +12,7 @@ const DEFAULT_TOLERANCE: f64 = 0.1; #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum RenderCommand { +pub enum RenderCommand { /// Pushes a new layer clipped by the specified shape and composed with previous layers using the specified blend mode. /// Every drawing command after this call will be clipped by the shape until the layer is popped. /// However, the transforms are not saved or modified by the layer stack. @@ -98,7 +101,7 @@ pub struct FillCommand { /// Draws a run of glyphs #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct GlyphRunCommand { +pub struct GlyphRunCommand { pub font_data: Font, pub font_size: f32, pub hint: bool, @@ -152,11 +155,11 @@ impl Scene { } } - fn convert_paintref(&mut self, paint_ref: PaintRef<'_>) -> Brush { + fn convert_paintref(&mut self, paint_ref: PaintRef<'_>) -> Brush> { match paint_ref { Paint::Solid(color) => Brush::Solid(color), Paint::Gradient(gradient) => Brush::Gradient(gradient.clone()), - Paint::Image(image) => Brush::Image(image.to_owned()), + Paint::Image(image) => Brush::Image(image.clone()), // TODO: handle this somehow Paint::Custom(_) => Brush::Solid(Color::TRANSPARENT), } @@ -293,6 +296,50 @@ impl PaintScene for Scene { } } +/// A simple [`RenderContext`] for use with recording scenes. +/// +/// This context assigns [`ResourceId`]s and stores the associated [`ImageData`] in a +/// `HashMap` so that recorded scenes can later be serialized or replayed through a +/// backend-specific context. +pub struct RecordingRenderContext { + image_data: FxHashMap, + next_resource_id: u64, +} + +impl RecordingRenderContext { + pub fn new() -> Self { + Self { + image_data: FxHashMap::default(), + next_resource_id: 0, + } + } + + pub fn image_data(&self) -> &FxHashMap { + &self.image_data + } +} + +impl Default for RecordingRenderContext { + fn default() -> Self { + Self::new() + } +} + +impl RenderContext for RecordingRenderContext { + fn register_image(&mut self, image: ImageData) -> ImageResource { + let id = ResourceId(self.next_resource_id); + self.next_resource_id += 1; + let width = image.width; + let height = image.height; + self.image_data.insert(id, image); + ImageResource { id, width, height } + } + + fn unregister_resource(&mut self, id: ResourceId) { + self.image_data.remove(&id); + } +} + /// Serde helper for serializing `BezPath` as an SVG path string. #[cfg(feature = "serde")] mod svg_path { diff --git a/crates/anyrender/src/types.rs b/crates/anyrender/src/types.rs index ba3f02b..8855f47 100644 --- a/crates/anyrender/src/types.rs +++ b/crates/anyrender/src/types.rs @@ -1,6 +1,6 @@ //! Types that are used within the Anyrender traits -use peniko::{Brush, BrushRef, Color, Gradient, ImageBrush, ImageBrushRef}; +use peniko::{Color, Gradient, ImageBrush, ImageData}; use std::{any::Any, sync::Arc}; #[cfg(feature = "serde")] @@ -8,6 +8,31 @@ use serde::{Deserialize, Serialize}; pub type NormalizedCoord = i16; +/// Opaque handle to a registered resource managed by a [`RenderContext`]. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct ResourceId(pub u64); + +/// A registered image resource that combines a [`ResourceId`] with the image dimensions. +#[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct ImageResource { + pub id: ResourceId, + pub width: u32, + pub height: u32, +} + +/// Renderers implement this trait to handle resource allocation/deallocation separately +/// from scene construction. Resources are registered once and then referenced by +/// [`ResourceId`] during painting. +pub trait RenderContext { + /// Register an image and upload/convert it into a backend-specific backing resource. + fn register_image(&mut self, image: ImageData) -> ImageResource; + + /// Unregister a previously registered resource, freeing any backing storage. + fn unregister_resource(&mut self, id: ResourceId); +} + /// A positioned glyph. #[derive(Copy, Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -28,7 +53,7 @@ pub struct CustomPaint { #[derive(Clone, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum Paint> { +pub enum Paint, G = Gradient, C = Arc> { /// Solid color brush. Solid(Color), /// Gradient brush. @@ -39,14 +64,18 @@ pub enum Paint> { Custom(C), } -pub type PaintRef<'a> = Paint, &'a Gradient, &'a (dyn Any + Send + Sync)>; +/// A reference-friendly version of [`Paint`] used in [`PaintScene`](crate::PaintScene) method signatures. +/// +/// Images are referenced by [`ImageResource`] (containing a [`ResourceId`]) rather than +/// by raw pixel data. +pub type PaintRef<'a> = Paint, &'a Gradient, &'a (dyn Any + Send + Sync)>; impl Paint { pub fn as_ref(&self) -> PaintRef<'_> { match self { Paint::Solid(color) => Paint::Solid(*color), Paint::Gradient(gradient) => Paint::Gradient(gradient), - Paint::Image(image) => Paint::Image(image.as_ref()), + Paint::Image(image) => Paint::Image(image.clone()), // Custom paints are translated into "invisible" where they are not supported Paint::Custom(custom) => Paint::Custom(custom.as_ref()), @@ -60,31 +89,6 @@ impl<'a> From<&'a Paint> for PaintRef<'a> { } } -impl<'a> From> for BrushRef<'a> { - fn from(value: PaintRef<'a>) -> Self { - match value { - Paint::Solid(color) => Brush::Solid(color), - Paint::Gradient(gradient) => Brush::Gradient(gradient), - Paint::Image(image) => Brush::Image(image), - - // Custom paints are translated into "invisible" where they are not supported - Paint::Custom(_) => Brush::Solid(Color::TRANSPARENT), - } - } -} - -// #[derive(Clone, Debug)] -// pub enum PaintRef<'a> { -// /// Solid color brush. -// Solid(Color), -// /// Gradient brush. -// Gradient(&'a Gradient), -// /// Image brush. -// Image(ImageBrushRef<'a>), -// /// Custom paint (type erased as each backend will have their own) -// Custom(Arc), -// } - impl From for Paint { fn from(value: Color) -> Self { Paint::Solid(value) @@ -95,8 +99,8 @@ impl<'a, I, C> From<&'a Gradient> for Paint { Paint::Gradient(value) } } -impl<'a, G, C> From> for Paint, G, C> { - fn from(value: ImageBrushRef<'a>) -> Self { +impl From> for Paint, G, C> { + fn from(value: ImageBrush) -> Self { Paint::Image(value) } } @@ -105,12 +109,3 @@ impl From> for Paint From> for PaintRef<'a> { - fn from(value: BrushRef<'a>) -> Self { - match value { - BrushRef::Solid(color) => PaintRef::Solid(color), - BrushRef::Gradient(gradient) => PaintRef::Gradient(gradient), - BrushRef::Image(image) => PaintRef::Image(image), - } - } -} diff --git a/crates/anyrender_serialize/Cargo.toml b/crates/anyrender_serialize/Cargo.toml index 6c4977a..6c3655e 100644 --- a/crates/anyrender_serialize/Cargo.toml +++ b/crates/anyrender_serialize/Cargo.toml @@ -12,6 +12,7 @@ edition.workspace = true anyrender = { workspace = true, features = ["serde"] } peniko = { workspace = true } +rustc-hash = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } zip = { workspace = true } @@ -25,6 +26,7 @@ wuff = { workspace = true } [dev-dependencies] kurbo = { workspace = true } peniko = { workspace = true } +rustc-hash = { workspace = true } serde_json = { workspace = true } wuff = { workspace = true } zip = { workspace = true } diff --git a/crates/anyrender_serialize/src/font_writer.rs b/crates/anyrender_serialize/src/font_writer.rs index 0c11eb7..e25ddac 100644 --- a/crates/anyrender_serialize/src/font_writer.rs +++ b/crates/anyrender_serialize/src/font_writer.rs @@ -1,6 +1,6 @@ //! Write-side font processing: collection, deduplication, subsetting, and encoding. -use std::collections::{HashMap, HashSet}; +use rustc_hash::{FxHashMap, FxHashSet}; use peniko::FontData; use read_fonts::FontRef; @@ -8,7 +8,7 @@ use read_fonts::collections::int_set::IntSet; use read_fonts::types::GlyphId; use skera::{Plan, SubsetFlags}; -use crate::{ArchiveError, ResourceId, SerializeConfig, sha256_hex}; +use crate::{ArchiveError, SerializeConfig, SerializedResourceId, sha256_hex}; /// A font that has been processed (optionally subsetted and/or WOFF2-encoded) and is /// ready to be written into the archive. @@ -35,23 +35,23 @@ pub(crate) struct FontWriter { /// Map `(Blob ID, face index)` to [`ResourceId`]. /// When subsetting is disabled, the face in the `(Blob ID, face index)` tuple is always 0. /// This is because multiple faces sharing the same TTC should be keyed together. - id_map: HashMap<(u64, u32), ResourceId>, + id_map: FxHashMap<(u64, u32), SerializedResourceId>, fonts: Vec, - glyph_ids: Vec>, + glyph_ids: Vec>, } impl FontWriter { pub fn new(config: SerializeConfig) -> Self { Self { config, - id_map: HashMap::new(), + id_map: FxHashMap::default(), fonts: Vec::new(), glyph_ids: Vec::new(), } } /// Register a font and return its [`ResourceId`]. - pub fn register(&mut self, font: &FontData) -> ResourceId { + pub fn register(&mut self, font: &FontData) -> SerializedResourceId { let key = if self.config.subset_fonts { (font.data.id(), font.index) } else { @@ -64,15 +64,15 @@ impl FontWriter { return id; } - let id = ResourceId(self.fonts.len()); + let id = SerializedResourceId(self.fonts.len()); self.id_map.insert(key, id); self.fonts.push(font.clone()); - self.glyph_ids.push(HashSet::new()); + self.glyph_ids.push(FxHashSet::default()); id } /// Record glyph IDs used for a font resource (used for subsetting). - pub fn record_glyphs(&mut self, id: ResourceId, glyphs: &[anyrender::Glyph]) { + pub fn record_glyphs(&mut self, id: SerializedResourceId, glyphs: &[anyrender::Glyph]) { if self.config.subset_fonts { let glyph_set = &mut self.glyph_ids[id.0]; for glyph in glyphs { diff --git a/crates/anyrender_serialize/src/lib.rs b/crates/anyrender_serialize/src/lib.rs index e573d73..8f47bfd 100644 --- a/crates/anyrender_serialize/src/lib.rs +++ b/crates/anyrender_serialize/src/lib.rs @@ -9,17 +9,18 @@ //! - `images/.png` - Image files (PNG format) //! - `fonts/.{woff2,ttf}` - Font data files (optionally WOFF2-compressed and subsetted) -use std::collections::HashMap; use std::io::{Read, Seek, Write}; use image::{ImageBuffer, ImageEncoder, RgbaImage}; use peniko::{Blob, Brush, FontData, ImageAlphaType, ImageBrush, ImageData, ImageFormat}; +use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use zip::write::SimpleFileOptions; use zip::{ZipArchive, ZipWriter}; use anyrender::recording::{FillCommand, GlyphRunCommand, RenderCommand, Scene, StrokeCommand}; +use anyrender::{ImageResource, RecordingRenderContext, RenderContext, ResourceId}; mod font_writer; mod json_formatter; @@ -27,23 +28,23 @@ mod json_formatter; use font_writer::FontWriter; /// A render command with resources replaced by IDs. -pub type SerializableRenderCommand = RenderCommand; +pub type SerializableRenderCommand = RenderCommand; /// A brush with images replaced by IDs. -pub type SerializableBrush = Brush>; +pub type SerializableBrush = Brush>; /// A unique identifier for a serialized resource. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(transparent)] -pub struct ResourceId(pub usize); +pub struct SerializedResourceId(pub usize); /// A reference to a font in a serialized scene. /// /// Pairs a [`ResourceId`] (which identifies the font file) with a collection index /// (which identifies a specific face). #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct FontResourceId { - pub resource_id: ResourceId, +pub struct SerializedFontResourceId { + pub resource_id: SerializedResourceId, pub index: u32, } @@ -109,7 +110,7 @@ pub struct FontMetadata { /// Metadata for a resource in the archive. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ResourceEntry { - pub id: ResourceId, + pub id: SerializedResourceId, pub kind: ResourceKind, /// The size of the raw decompressed resource data in bytes. pub size: usize, @@ -130,8 +131,8 @@ pub enum ResourceKind { /// Collects and deduplicates resources from a scene. struct ResourceCollector { fonts: FontWriter, - /// Maps Blob ID to ResourceId for images - image_id_map: HashMap, + /// Maps ResourceId to SerializedResourceId for images + image_id_map: FxHashMap, /// Collected images images: Vec, } @@ -140,31 +141,39 @@ impl ResourceCollector { fn new(config: SerializeConfig) -> Self { Self { fonts: FontWriter::new(config), - image_id_map: HashMap::new(), + image_id_map: FxHashMap::default(), images: Vec::new(), } } - /// Register an image and return its [`ResourceId`]. - fn register_image(&mut self, image: &ImageData) -> ResourceId { - let blob_id = image.data.id(); - if let Some(&id) = self.image_id_map.get(&blob_id) { + /// Register an image and return its [`SerializedResourceId`]. + fn register_image( + &mut self, + ctx: &RecordingRenderContext, + resource: &ImageResource, + ) -> SerializedResourceId { + if let Some(&id) = self.image_id_map.get(&resource.id) { return id; } - let id = ResourceId(self.images.len()); - self.image_id_map.insert(blob_id, id); - self.images.push(image.clone()); + let image_data = ctx.image_data().get(&resource.id).unwrap(); + let id = SerializedResourceId(self.images.len()); + self.image_id_map.insert(resource.id, id); + self.images.push(image_data.clone()); id } - /// Convert a [`Brush`] to a [`SerializableBrush`] by registering images. - fn convert_brush(&mut self, brush: &Brush) -> SerializableBrush { + /// Convert a [`Brush>`] to a [`SerializableBrush`] by registering images. + fn convert_brush( + &mut self, + ctx: &RecordingRenderContext, + brush: &Brush>, + ) -> SerializableBrush { match brush { Brush::Solid(color) => Brush::Solid(*color), Brush::Gradient(gradient) => Brush::Gradient(gradient.clone()), Brush::Image(image_brush) => { - let id = self.register_image(&image_brush.image); + let id = self.register_image(ctx, &image_brush.image); Brush::Image(ImageBrush { image: id, sampler: image_brush.sampler, @@ -174,7 +183,11 @@ impl ResourceCollector { } /// Convert a [`RenderCommand`] to a [`SerializableRenderCommand`]. - fn convert_command(&mut self, cmd: &RenderCommand) -> SerializableRenderCommand { + fn convert_command( + &mut self, + ctx: &RecordingRenderContext, + cmd: &RenderCommand, + ) -> SerializableRenderCommand { match cmd { RenderCommand::PushLayer(layer) => SerializableRenderCommand::PushLayer(layer.clone()), RenderCommand::PushClipLayer(clip) => { @@ -184,23 +197,23 @@ impl ResourceCollector { RenderCommand::Stroke(stroke) => SerializableRenderCommand::Stroke(StrokeCommand { style: stroke.style.clone(), transform: stroke.transform, - brush: self.convert_brush(&stroke.brush), + brush: self.convert_brush(ctx, &stroke.brush), brush_transform: stroke.brush_transform, shape: stroke.shape.clone(), }), RenderCommand::Fill(fill) => SerializableRenderCommand::Fill(FillCommand { fill: fill.fill, transform: fill.transform, - brush: self.convert_brush(&fill.brush), + brush: self.convert_brush(ctx, &fill.brush), brush_transform: fill.brush_transform, shape: fill.shape.clone(), }), RenderCommand::GlyphRun(glyph_run) => { let resource_id = self.fonts.register(&glyph_run.font_data); self.fonts.record_glyphs(resource_id, &glyph_run.glyphs); - let brush = self.convert_brush(&glyph_run.brush); + let brush = self.convert_brush(ctx, &glyph_run.brush); SerializableRenderCommand::GlyphRun(GlyphRunCommand { - font_data: FontResourceId { + font_data: SerializedFontResourceId { resource_id, index: self.fonts.face_index(&glyph_run.font_data), }, @@ -225,35 +238,47 @@ impl ResourceCollector { /// Reconstructs resources from deserialized data. struct ResourceReconstructor { fonts: Vec, - images: Vec, + image_resources: Vec, } impl ResourceReconstructor { - fn new(fonts: Vec, images: Vec) -> Self { - Self { fonts, images } + fn new(ctx: &mut impl RenderContext, fonts: Vec, images: Vec) -> Self { + let image_resources: Vec = images + .into_iter() + .map(|image| ctx.register_image(image)) + .collect(); + + Self { + fonts, + image_resources, + } } - fn get_font(&self, id: ResourceId) -> Result<&FontData, ArchiveError> { + fn get_font(&self, id: SerializedResourceId) -> Result<&FontData, ArchiveError> { self.fonts .get(id.0) .ok_or(ArchiveError::ResourceNotFound(id)) } - fn get_image(&self, id: ResourceId) -> Result<&ImageData, ArchiveError> { - self.images + fn get_image_resource(&self, id: SerializedResourceId) -> Result { + self.image_resources .get(id.0) + .copied() .ok_or(ArchiveError::ResourceNotFound(id)) } - /// Convert a [`SerializableBrush`] back to a [`Brush`]. - fn convert_brush(&self, brush: &SerializableBrush) -> Result { + /// Convert a [`SerializableBrush`] back to a [`Brush>`]. + fn convert_brush( + &self, + brush: &SerializableBrush, + ) -> Result>, ArchiveError> { Ok(match brush { Brush::Solid(color) => Brush::Solid(*color), Brush::Gradient(gradient) => Brush::Gradient(gradient.clone()), Brush::Image(image_brush) => { - let image = self.get_image(image_brush.image)?; + let resource = self.get_image_resource(image_brush.image)?; Brush::Image(ImageBrush { - image: image.clone(), + image: resource, sampler: image_brush.sampler, }) } @@ -381,14 +406,18 @@ fn convert_to_rgba(image: &ImageData) -> Result, ArchiveError> { impl SceneArchive { /// Create a new SceneArchive from a recorded Scene. - pub fn from_scene(scene: &Scene, config: &SerializeConfig) -> Result { + pub fn from_scene( + ctx: &RecordingRenderContext, + scene: &Scene, + config: &SerializeConfig, + ) -> Result { let mut manifest = ResourceManifest::new(scene.tolerance); let mut collector = ResourceCollector::new(config.clone()); let commands: Vec<_> = scene .commands .iter() - .map(|cmd| collector.convert_command(cmd)) + .map(|cmd| collector.convert_command(ctx, cmd)) .collect(); // Normalize all images to RGBA8 @@ -416,7 +445,7 @@ impl SceneArchive { manifest.images.push(ImageMetadata { entry: ResourceEntry { - id: ResourceId(idx), + id: SerializedResourceId(idx), kind: ResourceKind::Image, size: data.len(), sha256_hash: hash, @@ -435,7 +464,7 @@ impl SceneArchive { let font = result?; manifest.fonts.push(FontMetadata { entry: ResourceEntry { - id: ResourceId(idx), + id: SerializedResourceId(idx), kind: ResourceKind::Font, size: font.raw_size, sha256_hash: font.hash, @@ -454,7 +483,7 @@ impl SceneArchive { } /// Convert this archive back to a Scene. - pub fn to_scene(&self) -> Result { + pub fn to_scene(&self, ctx: &mut impl RenderContext) -> Result { // Convert images back to their original format let images: Vec = self .images @@ -489,7 +518,7 @@ impl SceneArchive { }) .collect::, ArchiveError>>()?; - let reconstructor = ResourceReconstructor::new(fonts_ttf, images); + let reconstructor = ResourceReconstructor::new(ctx, fonts_ttf, images); let commands: Result, _> = self .commands @@ -651,7 +680,7 @@ pub enum ArchiveError { Image(image::ImageError), FontProcessing(String), InvalidFormat(String), - ResourceNotFound(ResourceId), + ResourceNotFound(SerializedResourceId), UnsupportedVersion(u32), } diff --git a/crates/anyrender_serialize/tests/serialize.rs b/crates/anyrender_serialize/tests/serialize.rs index 8716fab..b0668c4 100644 --- a/crates/anyrender_serialize/tests/serialize.rs +++ b/crates/anyrender_serialize/tests/serialize.rs @@ -2,8 +2,8 @@ use std::io::{Cursor, Read}; -use anyrender::recording::{RenderCommand, Scene}; -use anyrender::{Glyph, PaintScene}; +use anyrender::recording::{RecordingRenderContext, RenderCommand, Scene}; +use anyrender::{Glyph, ImageResource, PaintScene, RenderContext}; use anyrender_serialize::{ ArchiveError, ResourceManifest, SceneArchive, SerializableRenderCommand, SerializeConfig, }; @@ -17,7 +17,7 @@ use zip::ZipArchive; #[test] fn test_empty_scene_roundtrip() { - assert_scene_roundtrip(&Scene::new()); + assert_scene_roundtrip(&RecordingRenderContext::new(), &Scene::new()); } /// Tests that all non-image and non-font command types survive a roundtrip. @@ -98,7 +98,7 @@ fn test_all_command_types_roundtrip() { scene.pop_layer(); - assert_scene_roundtrip(&scene); + assert_scene_roundtrip(&RecordingRenderContext::new(), &scene); } #[test] @@ -117,16 +117,23 @@ fn test_image_data_roundtrip() { height: 2, }; + let mut ctx = RecordingRenderContext::new(); + let resource = ctx.register_image(image_data); + let image_brush = ImageBrush { + image: resource, + sampler: Default::default(), + }; + let mut scene = Scene::new(); scene.fill( Fill::NonZero, Affine::IDENTITY, - ImageBrush::new(image_data).as_ref(), + image_brush, None, &Rect::new(0.0, 0.0, 100.0, 100.0), ); - let data = serialize_to_vec(&scene, &default_config()).unwrap(); + let data = serialize_to_vec(&ctx, &scene, &default_config()).unwrap(); let archive = archive_deserialize_from_slice(&data).unwrap(); // Verify manifest metadata @@ -136,32 +143,38 @@ fn test_image_data_roundtrip() { assert_eq!(archive.manifest.images[0].entry.size, 16); // Verify pixel data survives the roundtrip - let restored = archive.to_scene().unwrap(); - assert_eq!(extract_image_pixels(&restored, 0), pixels); + let mut restore_ctx = RecordingRenderContext::new(); + let restored = archive.to_scene(&mut restore_ctx).unwrap(); + assert_eq!(extract_image_pixels(&restored, &restore_ctx, 0), pixels); } #[test] fn test_image_deduplication() { - let image_brush = ImageBrush::new(make_1x1_image(255, 0, 0, 255)); + let mut ctx = RecordingRenderContext::new(); + let resource = ctx.register_image(make_1x1_image(255, 0, 0, 255)); + let image_brush: ImageBrush = ImageBrush { + image: resource, + sampler: Default::default(), + }; let mut scene = Scene::new(); // Same image drawn twice scene.fill( Fill::NonZero, Affine::IDENTITY, - image_brush.as_ref(), + image_brush.clone(), None, &Rect::new(0.0, 0.0, 50.0, 50.0), ); scene.fill( Fill::NonZero, Affine::translate((50.0, 0.0)), - image_brush.as_ref(), + image_brush, None, &Rect::new(0.0, 0.0, 50.0, 50.0), ); - let archive = SceneArchive::from_scene(&scene, &default_config()).unwrap(); + let archive = SceneArchive::from_scene(&ctx, &scene, &default_config()).unwrap(); assert_eq!(archive.commands.len(), 2); assert_eq!(archive.images.len(), 1); // deduplicated } @@ -171,34 +184,48 @@ fn test_multiple_different_images() { let red_pixels = vec![255, 0, 0, 255]; let blue_pixels = vec![0, 0, 255, 255]; + let mut ctx = RecordingRenderContext::new(); + let red_resource = ctx.register_image(make_1x1_image(255, 0, 0, 255)); + let blue_resource = ctx.register_image(make_1x1_image(0, 0, 255, 255)); + let mut scene = Scene::new(); scene.fill( Fill::NonZero, Affine::IDENTITY, - ImageBrush::new(make_1x1_image(255, 0, 0, 255)).as_ref(), + ImageBrush:: { + image: red_resource, + sampler: Default::default(), + }, None, &Rect::new(0.0, 0.0, 50.0, 50.0), ); scene.fill( Fill::NonZero, Affine::IDENTITY, - ImageBrush::new(make_1x1_image(0, 0, 255, 255)).as_ref(), + ImageBrush:: { + image: blue_resource, + sampler: Default::default(), + }, None, &Rect::new(0.0, 0.0, 50.0, 50.0), ); - let archive = SceneArchive::from_scene(&scene, &default_config()).unwrap(); + let archive = SceneArchive::from_scene(&ctx, &scene, &default_config()).unwrap(); assert_eq!(archive.commands.len(), 2); assert_eq!(archive.images.len(), 2); // Verify pixel data survives roundtrip let data = archive_serialize_to_vec(&archive).unwrap(); + let mut restore_ctx = RecordingRenderContext::new(); let restored = archive_deserialize_from_slice(&data) .unwrap() - .to_scene() + .to_scene(&mut restore_ctx) .unwrap(); - assert_eq!(extract_image_pixels(&restored, 0), red_pixels); - assert_eq!(extract_image_pixels(&restored, 1), blue_pixels); + assert_eq!(extract_image_pixels(&restored, &restore_ctx, 0), red_pixels); + assert_eq!( + extract_image_pixels(&restored, &restore_ctx, 1), + blue_pixels + ); } #[test] @@ -206,7 +233,7 @@ fn test_glyph_run_roundtrip() { let font = roboto_font(); let scene = build_glyph_scene(&font); - let data = serialize_to_vec(&scene, &default_config()).unwrap(); + let data = serialize_to_vec(&RecordingRenderContext::new(), &scene, &default_config()).unwrap(); let archive = archive_deserialize_from_slice(&data).unwrap(); // Verify font metadata @@ -214,7 +241,9 @@ fn test_glyph_run_roundtrip() { assert!(archive.manifest.fonts[0].entry.path.ends_with(".ttf")); assert_eq!(archive.manifest.fonts[0].entry.size, font.data.data().len(),); - let restored = archive.to_scene().unwrap(); + let restored = archive + .to_scene(&mut RecordingRenderContext::new()) + .unwrap(); assert_glyph_run_preserved(&restored); } @@ -225,7 +254,7 @@ fn test_glyph_run_roundtrip_with_subsetting_and_woff2() { let scene = build_glyph_scene(&font); let config = subset_and_woff2_config(); - let data = serialize_to_vec(&scene, &config).unwrap(); + let data = serialize_to_vec(&RecordingRenderContext::new(), &scene, &config).unwrap(); let archive = archive_deserialize_from_slice(&data).unwrap(); assert_eq!(archive.manifest.fonts.len(), 1); @@ -267,7 +296,9 @@ fn test_glyph_run_roundtrip_with_subsetting_and_woff2() { } // Verify the scene roundtrip - let restored = archive.to_scene().unwrap(); + let restored = archive + .to_scene(&mut RecordingRenderContext::new()) + .unwrap(); assert_glyph_run_preserved(&restored); } @@ -297,7 +328,9 @@ fn test_font_deduplication() { ); } - let archive = SceneArchive::from_scene(&scene, &default_config()).unwrap(); + let archive = + SceneArchive::from_scene(&RecordingRenderContext::new(), &scene, &default_config()) + .unwrap(); assert_eq!(archive.commands.len(), 2); assert_eq!(archive.fonts.len(), 1); // deduplicated } @@ -318,7 +351,7 @@ fn test_archive_contains_expected_files() { &Rect::new(0.0, 0.0, 100.0, 100.0), ); - let data = serialize_to_vec(&scene, &default_config()).unwrap(); + let data = serialize_to_vec(&RecordingRenderContext::new(), &scene, &default_config()).unwrap(); let mut zip = ZipArchive::new(Cursor::new(&data)).unwrap(); // Verify resources.json @@ -354,16 +387,16 @@ fn subset_and_woff2_config() -> SerializeConfig { .with_woff2_fonts(true) } -fn serialize_to_vec(scene: &Scene, config: &SerializeConfig) -> Result, ArchiveError> { +fn serialize_to_vec( + ctx: &RecordingRenderContext, + scene: &Scene, + config: &SerializeConfig, +) -> Result, ArchiveError> { let mut buf = Cursor::new(Vec::new()); - SceneArchive::from_scene(scene, config)?.serialize(&mut buf)?; + SceneArchive::from_scene(ctx, scene, config)?.serialize(&mut buf)?; Ok(buf.into_inner()) } -fn deserialize_from_slice(data: &[u8]) -> Result { - SceneArchive::deserialize(Cursor::new(data))?.to_scene() -} - fn archive_serialize_to_vec(archive: &SceneArchive) -> Result, ArchiveError> { let mut buf = Cursor::new(Vec::new()); archive.serialize(&mut buf)?; @@ -374,9 +407,12 @@ fn archive_deserialize_from_slice(data: &[u8]) -> Result ImageData { } } -fn extract_image_pixels(scene: &Scene, command_index: usize) -> Vec { +fn extract_image_pixels( + scene: &Scene, + render_ctx: &RecordingRenderContext, + command_index: usize, +) -> Vec { match &scene.commands[command_index] { RenderCommand::Fill(f) => match &f.brush { - Brush::Image(img) => img.image.data.data().to_vec(), + Brush::Image(img) => { + let data = render_ctx + .image_data() + .get(&img.image.id) + .expect("Image data not found for resource"); + data.data.data().to_vec() + } other => panic!("Expected image brush, got {other:?}"), }, other => panic!("Expected Fill command, got {other:?}"), diff --git a/crates/anyrender_skia/Cargo.toml b/crates/anyrender_skia/Cargo.toml index 04d5232..638b50d 100644 --- a/crates/anyrender_skia/Cargo.toml +++ b/crates/anyrender_skia/Cargo.toml @@ -22,6 +22,7 @@ peniko = { workspace = true } raw-window-handle = { workspace = true } softbuffer_window_renderer = { workspace = true, optional = true } pixels_window_renderer = { workspace = true, optional = true } +rustc-hash = { workspace = true } skia-safe = { version = "0.91.0", features = ["gl", "pdf", "textlayout"]} oaty = "0.1" hashbrown = "0.16.0" diff --git a/crates/anyrender_skia/src/image_renderer.rs b/crates/anyrender_skia/src/image_renderer.rs index 10f8de9..0a0d0a6 100644 --- a/crates/anyrender_skia/src/image_renderer.rs +++ b/crates/anyrender_skia/src/image_renderer.rs @@ -2,7 +2,7 @@ use anyrender::ImageRenderer; use debug_timer::debug_timer; use skia_safe::{AlphaType, Color, ColorType, ImageInfo, SurfaceProps, graphics, surfaces}; -use crate::{SkiaScenePainter, scene::SkiaSceneCache}; +use crate::{SkiaRenderContext, SkiaScenePainter, scene::SkiaSceneCache}; pub struct SkiaImageRenderer { image_info: ImageInfo, @@ -15,6 +15,7 @@ impl ImageRenderer for SkiaImageRenderer { = SkiaScenePainter<'a> where Self: 'a; + type Context = SkiaRenderContext; fn new(width: u32, height: u32) -> Self { graphics::set_font_cache_count_limit(100); @@ -46,6 +47,7 @@ impl ImageRenderer for SkiaImageRenderer { fn render_to_vec)>( &mut self, + ctx: &mut Self::Context, draw_fn: F, buffer: &mut Vec, ) { @@ -62,6 +64,7 @@ impl ImageRenderer for SkiaImageRenderer { surface.canvas().clear(Color::WHITE); draw_fn(&mut SkiaScenePainter { + ctx, inner: surface.canvas(), cache: &mut self.scene_cache, }); @@ -73,7 +76,12 @@ impl ImageRenderer for SkiaImageRenderer { timer.print_times("skia_raster: "); } - fn render)>(&mut self, draw_fn: F, buffer: &mut [u8]) { + fn render)>( + &mut self, + ctx: &mut Self::Context, + draw_fn: F, + buffer: &mut [u8], + ) { debug_timer!(timer, feature = "log_frame_times"); let mut surface = surfaces::wrap_pixels( @@ -87,6 +95,7 @@ impl ImageRenderer for SkiaImageRenderer { surface.canvas().clear(Color::WHITE); draw_fn(&mut SkiaScenePainter { + ctx, inner: surface.canvas(), cache: &mut self.scene_cache, }); diff --git a/crates/anyrender_skia/src/lib.rs b/crates/anyrender_skia/src/lib.rs index 53b53e4..a3c7a04 100644 --- a/crates/anyrender_skia/src/lib.rs +++ b/crates/anyrender_skia/src/lib.rs @@ -12,5 +12,5 @@ mod opengl; mod vulkan; pub use image_renderer::SkiaImageRenderer; -pub use scene::SkiaScenePainter; +pub use scene::{SkiaRenderContext, SkiaScenePainter}; pub use window_renderer::*; diff --git a/crates/anyrender_skia/src/scene.rs b/crates/anyrender_skia/src/scene.rs index 6df7c2d..cc71632 100644 --- a/crates/anyrender_skia/src/scene.rs +++ b/crates/anyrender_skia/src/scene.rs @@ -1,4 +1,6 @@ -use anyrender::PaintScene; +use anyrender::{ImageResource, PaintScene, RenderContext, ResourceId}; +use peniko::ImageData; +use rustc_hash::FxHashMap; use skia_safe::{ BlurStyle, Canvas, Color, ColorSpace, Font, FontArguments, FontHinting, FontMgr, GlyphId, MaskFilter, Paint, PaintCap, PaintJoin, PaintStyle, Point, RRect, Rect, Shader, Typeface, @@ -12,13 +14,53 @@ use crate::cache::{ NormalizedTypefaceCacheKeyBorrowed, }; +pub struct SkiaRenderContext { + pub(crate) resource_map: FxHashMap, + next_resource_id: u64, +} + +impl SkiaRenderContext { + pub fn new() -> Self { + Self { + resource_map: FxHashMap::default(), + next_resource_id: 0, + } + } +} + +impl Default for SkiaRenderContext { + fn default() -> Self { + Self::new() + } +} + +impl RenderContext for SkiaRenderContext { + fn register_image(&mut self, image: ImageData) -> ImageResource { + let resource_id = ResourceId(self.next_resource_id); + self.next_resource_id += 1; + let width = image.width; + let height = image.height; + let sk_image = sk_peniko::skia_image_from_peniko(&image); + self.resource_map.insert(resource_id, sk_image); + ImageResource { + id: resource_id, + width, + height, + } + } + + fn unregister_resource(&mut self, id: ResourceId) { + self.resource_map.remove(&id); + } +} + pub(crate) struct SkiaSceneCache { paint: Paint, #[cfg(any(target_os = "macos", target_os = "ios"))] extracted_font_data: GenerationalCache<(u64, u32), peniko::FontData>, typeface: GenerationalCache<(u64, u32), Typeface>, normalized_typeface: GenerationalCache, - image_shader: GenerationalCache, + image_shader: GenerationalCache, font: GenerationalCache, font_mgr: FontMgr, glyph_id_buf: Vec, @@ -52,6 +94,7 @@ impl Default for SkiaSceneCache { } pub struct SkiaScenePainter<'a> { + pub(crate) ctx: &'a SkiaRenderContext, pub(crate) inner: &'a Canvas, pub(crate) cache: &'a mut SkiaSceneCache, } @@ -111,17 +154,22 @@ impl SkiaScenePainter<'_> { .set_shader(sk_peniko::shader_from_gradient(gradient, brush_transform)); } anyrender::Paint::Image(image_brush) => { - if let Some(shader) = self.cache.image_shader.hit(&image_brush.image.data.id()) { + if let Some(shader) = self.cache.image_shader.hit(&image_brush.image.id) { self.cache.paint.set_shader(shader.clone()); return; } - let image_shader = sk_peniko::shader_from_image_brush(image_brush, brush_transform); + let sk_image = &self.ctx.resource_map[&image_brush.image.id]; + let image_shader = sk_peniko::shader_from_skia_image( + sk_image, + image_brush.sampler, + brush_transform, + ); if let Some(shader) = &image_shader { self.cache .image_shader - .insert(image_brush.image.data.id(), shader.clone()); + .insert(image_brush.image.id, shader.clone()); } self.cache.paint.set_shader(image_shader); @@ -524,11 +572,11 @@ fn lerp_f32(a: f32, b: f32, t: f32) -> f32 { a + (b - a) * t } -mod sk_peniko { +pub(crate) mod sk_peniko { use peniko::color::{AlphaColor, ColorSpaceTag, HueDirection, Srgb}; use peniko::{ - BlendMode, Compose, Extend, Gradient, GradientKind, ImageAlphaType, ImageBrush, ImageData, - ImageFormat, Mix, + BlendMode, Compose, Extend, Gradient, GradientKind, ImageAlphaType, ImageData, ImageFormat, + Mix, }; use peniko::{Fill, color::DynamicColor}; use skia_safe::AlphaType as SkAlphaType; @@ -544,12 +592,7 @@ mod sk_peniko { use skia_safe::gradient_shader::interpolation::ColorSpace as SkGradientShaderColorSpace; use skia_safe::gradient_shader::interpolation::HueMethod as SkGradientShaderHueMethod; - pub(super) fn shader_from_image_brush( - image_brush: ImageBrush<&ImageData>, - brush_transform: Option, - ) -> Option { - let image_data = image_brush.image; - + pub(crate) fn skia_image_from_peniko(image_data: &ImageData) -> skia_safe::Image { let image_info = SkImageInfo::new( (image_data.width as i32, image_data.height as i32), match image_data.format { @@ -566,11 +609,17 @@ mod sk_peniko { let pixels = unsafe { SkData::new_bytes(image_data.data.data()) // We have to ensure the src image data lives long enough }; - let image = - skia_safe::images::raster_from_data(&image_info, pixels, image_info.min_row_bytes()) - .unwrap(); + skia_safe::images::raster_from_data(&image_info, pixels, image_info.min_row_bytes()) + .unwrap() + } - let sampling = match image_brush.sampler.quality { + /// Create a shader from a pre-created `skia_safe::Image` with the given sampler and transform. + pub(super) fn shader_from_skia_image( + image: &skia_safe::Image, + sampler: peniko::ImageSampler, + brush_transform: Option, + ) -> Option { + let sampling = match sampler.quality { peniko::ImageQuality::Low => { SkSamplingOptions::new(skia_safe::FilterMode::Nearest, skia_safe::MipmapMode::None) } @@ -584,10 +633,10 @@ mod sk_peniko { }; skia_safe::shaders::image( - image, + image.clone(), ( - tile_mode_from_extend(image_brush.sampler.x_extend), - tile_mode_from_extend(image_brush.sampler.y_extend), + tile_mode_from_extend(sampler.x_extend), + tile_mode_from_extend(sampler.y_extend), ), &sampling, &brush_transform.map(super::sk_kurbo::matrix_from_affine), diff --git a/crates/anyrender_skia/src/window_renderer.rs b/crates/anyrender_skia/src/window_renderer.rs index 076cd72..5eb3d2e 100644 --- a/crates/anyrender_skia/src/window_renderer.rs +++ b/crates/anyrender_skia/src/window_renderer.rs @@ -3,7 +3,7 @@ use debug_timer::debug_timer; use skia_safe::{Color, Surface, graphics}; use std::sync::Arc; -use crate::{SkiaScenePainter, scene::SkiaSceneCache}; +use crate::{SkiaRenderContext, SkiaScenePainter, scene::SkiaSceneCache}; pub(crate) trait SkiaBackend { fn set_size(&mut self, width: u32, height: u32); @@ -41,13 +41,12 @@ impl SkiaWindowRenderer { } } -impl SkiaWindowRenderer {} - impl WindowRenderer for SkiaWindowRenderer { type ScenePainter<'a> = SkiaScenePainter<'a> where Self: 'a; + type Context = SkiaRenderContext; fn resume(&mut self, window: Arc, width: u32, height: u32) { graphics::set_font_cache_count_limit(100); @@ -79,7 +78,11 @@ impl WindowRenderer for SkiaWindowRenderer { } } - fn render)>(&mut self, draw_fn: F) { + fn render)>( + &mut self, + ctx: &mut Self::Context, + draw_fn: F, + ) { let RenderState::Active(state) = &mut self.render_state else { return; }; @@ -95,6 +98,7 @@ impl WindowRenderer for SkiaWindowRenderer { surface.canvas().clear(Color::WHITE); draw_fn(&mut SkiaScenePainter { + ctx, inner: surface.canvas(), cache: &mut state.scene_cache, }); diff --git a/crates/anyrender_svg/src/lib.rs b/crates/anyrender_svg/src/lib.rs index 2cdab8f..660c42b 100644 --- a/crates/anyrender_svg/src/lib.rs +++ b/crates/anyrender_svg/src/lib.rs @@ -27,56 +27,93 @@ mod util; pub use error::Error; pub use usvg; -use anyrender::PaintScene; +use anyrender::{ImageResource, PaintScene, RenderContext}; use kurbo::Affine; /// Append an SVG to an [`anyrender::PaintScene`]. /// /// This will draw a red box over (some) unsupported elements. -pub fn render_svg_str( +/// +/// Any raster images embedded in the SVG are registered with `ctx` and their +/// [`ImageResource`] handles are appended to `images`, so the caller can +/// later unregister them via [`RenderContext::unregister_resource`]. +pub fn render_svg_str( + ctx: &mut RC, scene: &mut S, + images: &mut Vec, svg: &str, transform: Affine, ) -> Result<(), Error> { let opt = usvg::Options::default(); let tree = usvg::Tree::from_str(svg, &opt)?; - render_svg_tree(scene, &tree, transform); + render_svg_tree(ctx, scene, images, &tree, transform); Ok(()) } /// Append an SVG to an [`anyrender::PaintScene`] (with custom error handling). /// /// See the [module level documentation](crate#unsupported-features) for a list of some unsupported svg features -pub fn render_svg_str_with( +/// +/// Any raster images embedded in the SVG are registered with `ctx` and their +/// [`ImageResource`] handles are appended to `images`, so that you can +/// later unregister them via [`RenderContext::unregister_resource`]. +pub fn render_svg_str_with( + ctx: &mut RC, scene: &mut S, + images: &mut Vec, svg: &str, transform: Affine, error_handler: &mut F, ) -> Result<(), Error> { let opt = usvg::Options::default(); let tree = usvg::Tree::from_str(svg, &opt)?; - render_svg_tree_with(scene, &tree, transform, error_handler); + render_svg_tree_with(ctx, scene, images, &tree, transform, error_handler); Ok(()) } /// Append a [`usvg::Tree`] to an [`anyrender::PaintScene`]. /// /// This will draw a red box over (some) unsupported elements. -pub fn render_svg_tree(scene: &mut S, svg: &usvg::Tree, transform: Affine) { - render_svg_tree_with(scene, svg, transform, &mut util::default_error_handler); +/// +/// Any raster images embedded in the SVG are registered with `ctx` and their +/// [`ImageResource`] handles are appended to `images`, so that you can +/// later unregister them via [`RenderContext::unregister_resource`]. +pub fn render_svg_tree( + ctx: &mut RC, + scene: &mut S, + images: &mut Vec, + svg: &usvg::Tree, + transform: Affine, +) { + render_svg_tree_with( + ctx, + scene, + images, + svg, + transform, + &mut util::default_error_handler, + ); } /// Append a [`usvg::Tree`] to an [`anyrender::PaintScene`] (with custom error handling). /// /// See the [module level documentation](crate#unsupported-features) for a list of some unsupported svg features -pub fn render_svg_tree_with( +/// +/// Any raster images embedded in the SVG are registered with `ctx` and their +/// [`ImageResource`] handles are appended to `images`, so that you can +/// later unregister them via [`RenderContext::unregister_resource`]. +pub fn render_svg_tree_with( + ctx: &mut RC, scene: &mut S, + images: &mut Vec, svg: &usvg::Tree, transform: Affine, error_handler: &mut F, ) { render::render_group( + ctx, scene, + images, svg.root(), Affine::IDENTITY, transform, diff --git a/crates/anyrender_svg/src/render.rs b/crates/anyrender_svg/src/render.rs index 902ef7f..d5bc740 100644 --- a/crates/anyrender_svg/src/render.rs +++ b/crates/anyrender_svg/src/render.rs @@ -2,13 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT use crate::util; -use anyrender::PaintScene; +use anyrender::{ImageResource, PaintScene, RenderContext}; use kurbo::{Affine, BezPath}; use peniko::{BlendMode, Fill}; use usvg::{Node, Path}; -pub(crate) fn render_group( +pub(crate) fn render_group( + ctx: &mut RC, scene: &mut S, + images: &mut Vec, group: &usvg::Group, transform: Affine, global_transform: Affine, @@ -68,7 +70,15 @@ pub(crate) fn render_group( _ => false, }; - render_group(scene, g, Affine::IDENTITY, global_transform, error_handler); + render_group( + ctx, + scene, + images, + g, + Affine::IDENTITY, + global_transform, + error_handler, + ); if did_push_layer { scene.pop_layer(); @@ -108,9 +118,15 @@ pub(crate) fn render_group( error_handler(scene, node); continue; }; - let image = util::into_image(decoded_image); + let image_data = util::into_image(decoded_image); + let resource = ctx.register_image(image_data); + images.push(resource); let image_ts = global_transform * util::to_affine(&img.abs_transform()); - scene.draw_image(image.as_ref(), image_ts); + let brush = peniko::ImageBrush { + image: resource, + sampler: Default::default(), + }; + scene.draw_image(brush, image_ts); } #[cfg(not(feature = "image"))] @@ -121,7 +137,9 @@ pub(crate) fn render_group( } usvg::ImageKind::SVG(svg) => { render_group( + ctx, scene, + images, svg.root(), transform, global_transform, @@ -132,7 +150,9 @@ pub(crate) fn render_group( } usvg::Node::Text(text) => { render_group( + ctx, scene, + images, text.flattened(), transform, global_transform, diff --git a/crates/anyrender_svg/src/util.rs b/crates/anyrender_svg/src/util.rs index 4a0cb97..8ba2241 100644 --- a/crates/anyrender_svg/src/util.rs +++ b/crates/anyrender_svg/src/util.rs @@ -7,7 +7,7 @@ use peniko::color::{self, DynamicColor}; use peniko::{Color, Fill, Mix}; #[cfg(feature = "image")] -use peniko::{Blob, ImageBrush}; +use peniko::Blob; pub(crate) fn to_affine(ts: &usvg::Transform) -> Affine { let usvg::Transform { @@ -115,18 +115,16 @@ pub(crate) fn to_bez_path(path: &usvg::Path) -> BezPath { } #[cfg(feature = "image")] -pub(crate) fn into_image(image: image::ImageBuffer, Vec>) -> ImageBrush { - use peniko::ImageData; - +pub(crate) fn into_image(image: image::ImageBuffer, Vec>) -> peniko::ImageData { let (width, height) = (image.width(), image.height()); let image_data: Vec = image.into_vec(); - ImageBrush::new(ImageData { + peniko::ImageData { data: Blob::new(std::sync::Arc::new(image_data)), format: peniko::ImageFormat::Rgba8, alpha_type: peniko::ImageAlphaType::Alpha, width, height, - }) + } } pub(crate) fn to_brush(paint: &usvg::Paint, opacity: usvg::Opacity) -> Option<(Paint, Affine)> { diff --git a/crates/anyrender_vello/src/image_renderer.rs b/crates/anyrender_vello/src/image_renderer.rs index 4b79be1..b406285 100644 --- a/crates/anyrender_vello/src/image_renderer.rs +++ b/crates/anyrender_vello/src/image_renderer.rs @@ -4,7 +4,7 @@ use vello::{Renderer as VelloRenderer, RendererOptions, Scene as VelloScene}; use wgpu::TextureUsages; use wgpu_context::{BufferRenderer, BufferRendererConfig, WGPUContext}; -use crate::{DEFAULT_THREADS, VelloScenePainter}; +use crate::{DEFAULT_THREADS, VelloRenderContext, VelloScenePainter}; pub struct VelloImageRenderer { buffer_renderer: BufferRenderer, @@ -17,6 +17,7 @@ impl ImageRenderer for VelloImageRenderer { = VelloScenePainter<'a, 'a> where Self: 'a; + type Context = VelloRenderContext; fn new(width: u32, height: u32) -> Self { // Create WGPUContext @@ -60,20 +61,23 @@ impl ImageRenderer for VelloImageRenderer { fn render_to_vec)>( &mut self, + ctx: &mut Self::Context, draw_fn: F, cpu_buffer: &mut Vec, ) { let size = self.buffer_renderer.size(); cpu_buffer.resize((size.width * size.height * 4) as usize, 0); - self.render(draw_fn, cpu_buffer); + self.render(ctx, draw_fn, cpu_buffer); } fn render)>( &mut self, + ctx: &mut Self::Context, draw_fn: F, cpu_buffer: &mut [u8], ) { draw_fn(&mut VelloScenePainter { + ctx, inner: &mut self.scene, renderer: Some(&mut self.vello_renderer), custom_paint_sources: Some(&mut FxHashMap::default()), diff --git a/crates/anyrender_vello/src/lib.rs b/crates/anyrender_vello/src/lib.rs index 9550442..daede46 100644 --- a/crates/anyrender_vello/src/lib.rs +++ b/crates/anyrender_vello/src/lib.rs @@ -7,7 +7,7 @@ pub mod custom_paint_source; pub use custom_paint_source::*; pub use image_renderer::VelloImageRenderer; -pub use scene::VelloScenePainter; +pub use scene::{VelloRenderContext, VelloScenePainter}; pub use window_renderer::{VelloRendererOptions, VelloWindowRenderer}; pub use wgpu; diff --git a/crates/anyrender_vello/src/scene.rs b/crates/anyrender_vello/src/scene.rs index 732db1c..3eef26d 100644 --- a/crates/anyrender_vello/src/scene.rs +++ b/crates/anyrender_vello/src/scene.rs @@ -1,20 +1,67 @@ -use anyrender::{CustomPaint, NormalizedCoord, Paint, PaintRef, PaintScene}; +use anyrender::{ + CustomPaint, ImageResource, NormalizedCoord, Paint, PaintRef, PaintScene, RenderContext, + ResourceId, +}; use kurbo::{Affine, Rect, Shape, Stroke}; -use peniko::{BlendMode, BrushRef, Color, Fill, FontData, ImageBrush, StyleRef}; +use peniko::{BlendMode, Color, Fill, FontData, ImageBrush, ImageData, StyleRef}; use rustc_hash::FxHashMap; use vello::Renderer as VelloRenderer; use crate::{CustomPaintSource, custom_paint_source::CustomPaintCtx}; +pub struct VelloRenderContext { + pub(crate) resource_map: FxHashMap, + next_resource_id: u64, +} + +impl VelloRenderContext { + pub fn new() -> Self { + Self { + resource_map: FxHashMap::default(), + next_resource_id: 0, + } + } +} + +impl Default for VelloRenderContext { + fn default() -> Self { + Self::new() + } +} + +impl RenderContext for VelloRenderContext { + fn register_image(&mut self, image: ImageData) -> ImageResource { + let resource_id = ResourceId(self.next_resource_id); + self.next_resource_id += 1; + let width = image.width; + let height = image.height; + self.resource_map.insert(resource_id, image); + ImageResource { + id: resource_id, + width, + height, + } + } + + fn unregister_resource(&mut self, id: ResourceId) { + self.resource_map.remove(&id); + } +} + pub struct VelloScenePainter<'r, 's> { + pub(crate) ctx: &'r VelloRenderContext, pub(crate) renderer: Option<&'r mut VelloRenderer>, pub(crate) custom_paint_sources: Option<&'r mut FxHashMap>>, pub(crate) inner: &'s mut vello::Scene, } impl VelloScenePainter<'_, '_> { - pub fn new<'s>(scene: &'s mut vello::Scene) -> VelloScenePainter<'static, 's> { + pub fn new<'r, 's>( + ctx: &'r VelloRenderContext, + scene: &'s mut vello::Scene, + ) -> VelloScenePainter<'r, 's> { VelloScenePainter { + ctx, renderer: None, custom_paint_sources: None, inner: scene, @@ -43,6 +90,22 @@ impl VelloScenePainter<'_, '_> { // Return dummy image Some(ImageBrush::new(texture_handle.0)) } + + /// Convert a PaintRef to an owned peniko::Brush, looking up image resources from the context. + fn paint_to_brush(&self, paint: PaintRef<'_>) -> Option { + Some(match paint { + Paint::Solid(color) => peniko::Brush::Solid(color), + Paint::Gradient(gradient) => peniko::Brush::Gradient(gradient.clone()), + Paint::Image(image_brush) => { + let image_data = self.ctx.resource_map[&image_brush.image.id].clone(); + peniko::Brush::Image(ImageBrush { + image: image_data, + sampler: image_brush.sampler, + }) + } + Paint::Custom(_) => return None, + }) + } } impl PaintScene for VelloScenePainter<'_, '_> { @@ -77,10 +140,11 @@ impl PaintScene for VelloScenePainter<'_, '_> { brush_transform: Option, shape: &impl Shape, ) { - let paint_ref: PaintRef<'_> = paint_ref.into(); - let brush_ref: BrushRef<'_> = paint_ref.into(); + let Some(brush) = self.paint_to_brush(paint_ref.into()) else { + return; + }; self.inner - .stroke(style, transform, brush_ref, brush_transform, shape); + .stroke(style, transform, &brush, brush_transform, shape); } fn fill<'a>( @@ -93,11 +157,16 @@ impl PaintScene for VelloScenePainter<'_, '_> { ) { let paint: PaintRef<'_> = paint.into(); - let dummy_image: peniko::ImageBrush; - let brush_ref: BrushRef<'_> = match paint { - Paint::Solid(color) => BrushRef::Solid(color), - Paint::Gradient(gradient) => BrushRef::Gradient(gradient), - Paint::Image(image) => BrushRef::Image(image), + let brush: peniko::Brush = match paint { + Paint::Solid(color) => peniko::Brush::Solid(color), + Paint::Gradient(gradient) => peniko::Brush::Gradient(gradient.clone()), + Paint::Image(image_brush) => { + let image_data = self.ctx.resource_map[&image_brush.image.id].clone(); + peniko::Brush::Image(ImageBrush { + image: image_data, + sampler: image_brush.sampler, + }) + } Paint::Custom(custom_paint) => { let Some(custom_paint) = custom_paint.downcast_ref::() else { return; @@ -105,13 +174,12 @@ impl PaintScene for VelloScenePainter<'_, '_> { let Some(image) = self.render_custom_source(*custom_paint) else { return; }; - dummy_image = image; - BrushRef::Image(dummy_image.as_ref()) + peniko::Brush::Image(image) } }; self.inner - .fill(style, transform, brush_ref, brush_transform, shape); + .fill(style, transform, &brush, brush_transform, shape); } fn draw_glyphs<'a, 's: 'a>( @@ -127,23 +195,44 @@ impl PaintScene for VelloScenePainter<'_, '_> { glyph_transform: Option, glyphs: impl Iterator, ) { - self.inner + let paint: PaintRef<'_> = paint.into(); + let resource_map = &self.ctx.resource_map; + + let glyph_iter = glyphs.map(|g: anyrender::Glyph| vello::Glyph { + id: g.id, + x: g.x, + y: g.y, + }); + + let mut glyph_renderer = self + .inner .draw_glyphs(font) .font_size(font_size) .hint(hint) .normalized_coords(normalized_coords) - .brush(paint.into()) .brush_alpha(brush_alpha) .transform(transform) - .glyph_transform(glyph_transform) - .draw( - style, - glyphs.map(|g: anyrender::Glyph| vello::Glyph { - id: g.id, - x: g.x, - y: g.y, - }), - ); + .glyph_transform(glyph_transform); + + match paint { + Paint::Solid(color) => { + glyph_renderer = glyph_renderer.brush(peniko::Brush::Solid(color)) + } + Paint::Gradient(gradient) => { + glyph_renderer = glyph_renderer.brush(peniko::Brush::Gradient(gradient)) + } + Paint::Image(image_brush) => { + let image_data = &resource_map[&image_brush.image.id]; + let brush = ImageBrush { + image: image_data, + sampler: image_brush.sampler, + }; + glyph_renderer = glyph_renderer.brush(brush); + } + Paint::Custom(_) => {} + } + + glyph_renderer.draw(style, glyph_iter); } fn draw_box_shadow( diff --git a/crates/anyrender_vello/src/window_renderer.rs b/crates/anyrender_vello/src/window_renderer.rs index f1331ab..e5717e1 100644 --- a/crates/anyrender_vello/src/window_renderer.rs +++ b/crates/anyrender_vello/src/window_renderer.rs @@ -15,7 +15,7 @@ use wgpu_context::{ DeviceHandle, SurfaceRenderer, SurfaceRendererConfiguration, TextureConfiguration, WGPUContext, }; -use crate::{CustomPaintSource, DEFAULT_THREADS, VelloScenePainter}; +use crate::{CustomPaintSource, DEFAULT_THREADS, VelloRenderContext, VelloScenePainter}; static PAINT_SOURCE_ID: AtomicU64 = AtomicU64::new(0); @@ -122,6 +122,7 @@ impl WindowRenderer for VelloWindowRenderer { = VelloScenePainter<'a, 'a> where Self: 'a; + type Context = VelloRenderContext; fn is_active(&self) -> bool { matches!(self.render_state, RenderState::Active(_)) @@ -190,7 +191,11 @@ impl WindowRenderer for VelloWindowRenderer { }; } - fn render)>(&mut self, draw_fn: F) { + fn render)>( + &mut self, + ctx: &mut Self::Context, + draw_fn: F, + ) { let RenderState::Active(state) = &mut self.render_state else { return; }; @@ -201,6 +206,7 @@ impl WindowRenderer for VelloWindowRenderer { // Regenerate the vello scene draw_fn(&mut VelloScenePainter { + ctx, inner: &mut self.scene, renderer: Some(&mut state.renderer), custom_paint_sources: Some(&mut self.custom_paint_sources), @@ -238,9 +244,6 @@ impl WindowRenderer for VelloWindowRenderer { timer.record_time("wait"); timer.print_times("vello: "); - // static COUNTER: AtomicU64 = AtomicU64::new(0); - // println!("FRAME {}", COUNTER.fetch_add(1, atomic::Ordering::Relaxed)); - // Empty the Vello scene (memory optimisation) self.scene.reset(); } diff --git a/crates/anyrender_vello_cpu/Cargo.toml b/crates/anyrender_vello_cpu/Cargo.toml index 0903645..182479b 100644 --- a/crates/anyrender_vello_cpu/Cargo.toml +++ b/crates/anyrender_vello_cpu/Cargo.toml @@ -18,11 +18,6 @@ log_frame_times = [ "pixels_window_renderer?/log_frame_times", ] -# Caches image premultiplication, but the cache is never cleared -# Useful for benchmarking (as we expect to eventually eliminate this cost) -# but not recommended for use in production -experimental_image_cache = [] - [dependencies] anyrender = { workspace = true } debug_timer = { workspace = true } diff --git a/crates/anyrender_vello_cpu/src/image_renderer.rs b/crates/anyrender_vello_cpu/src/image_renderer.rs index 1581453..a5d1d24 100644 --- a/crates/anyrender_vello_cpu/src/image_renderer.rs +++ b/crates/anyrender_vello_cpu/src/image_renderer.rs @@ -1,42 +1,51 @@ -use crate::VelloCpuScenePainter; +use crate::{VelloCpuRenderContext, VelloCpuScenePainter}; use anyrender::ImageRenderer; use debug_timer::debug_timer; -use vello_cpu::{RenderContext, RenderMode}; +use vello_cpu::{RenderContext as VelloCpuRenderCtx, RenderMode}; pub struct VelloCpuImageRenderer { - scene: VelloCpuScenePainter, + render_ctx: VelloCpuRenderCtx, } impl ImageRenderer for VelloCpuImageRenderer { - type ScenePainter<'a> = VelloCpuScenePainter; + type ScenePainter<'a> = VelloCpuScenePainter<'a>; + type Context = VelloCpuRenderContext; fn new(width: u32, height: u32) -> Self { Self { - scene: VelloCpuScenePainter(RenderContext::new(width as u16, height as u16)), + render_ctx: VelloCpuRenderCtx::new(width as u16, height as u16), } } fn resize(&mut self, width: u32, height: u32) { - self.scene.0 = RenderContext::new(width as u16, height as u16); + self.render_ctx = VelloCpuRenderCtx::new(width as u16, height as u16); } fn reset(&mut self) { - self.scene.0.reset(); + self.render_ctx.reset(); } - fn render)>(&mut self, draw_fn: F, buffer: &mut [u8]) { + fn render)>( + &mut self, + ctx: &mut Self::Context, + draw_fn: F, + buffer: &mut [u8], + ) { debug_timer!(timer, feature = "log_frame_times"); - draw_fn(&mut self.scene); + { + let mut scene = VelloCpuScenePainter::new(ctx, &mut self.render_ctx); + draw_fn(&mut scene); + } timer.record_time("cmds"); - self.scene.0.flush(); + self.render_ctx.flush(); timer.record_time("flush"); - self.scene.0.render_to_buffer( + self.render_ctx.render_to_buffer( buffer, - self.scene.0.width(), - self.scene.0.height(), + self.render_ctx.width(), + self.render_ctx.height(), RenderMode::OptimizeSpeed, ); timer.record_time("render"); @@ -46,12 +55,13 @@ impl ImageRenderer for VelloCpuImageRenderer { fn render_to_vec)>( &mut self, + ctx: &mut Self::Context, draw_fn: F, buffer: &mut Vec, ) { - let width = self.scene.0.width(); - let height = self.scene.0.height(); + let width = self.render_ctx.width(); + let height = self.render_ctx.height(); buffer.resize(width as usize * height as usize * 4, 0); - self.render(draw_fn, buffer); + self.render(ctx, draw_fn, buffer); } } diff --git a/crates/anyrender_vello_cpu/src/lib.rs b/crates/anyrender_vello_cpu/src/lib.rs index d72c750..32ba298 100644 --- a/crates/anyrender_vello_cpu/src/lib.rs +++ b/crates/anyrender_vello_cpu/src/lib.rs @@ -6,7 +6,7 @@ mod scene; mod window_renderer; pub use image_renderer::VelloCpuImageRenderer; -pub use scene::VelloCpuScenePainter; +pub use scene::{VelloCpuRenderContext, VelloCpuScenePainter}; #[cfg(any( feature = "pixels_window_renderer", diff --git a/crates/anyrender_vello_cpu/src/scene.rs b/crates/anyrender_vello_cpu/src/scene.rs index 4132013..ede86ae 100644 --- a/crates/anyrender_vello_cpu/src/scene.rs +++ b/crates/anyrender_vello_cpu/src/scene.rs @@ -1,19 +1,62 @@ -use anyrender::{NormalizedCoord, Paint, PaintRef, PaintScene}; +use anyrender::{ + ImageResource, NormalizedCoord, Paint, PaintRef, PaintScene, RenderContext, ResourceId, +}; use kurbo::{Affine, Rect, Shape, Stroke}; -use peniko::{BlendMode, Color, Fill, FontData, ImageBrush, StyleRef}; +use peniko::{BlendMode, Color, Fill, FontData, ImageBrush, ImageData, StyleRef}; +use std::collections::HashMap; use vello_cpu::{ImageSource, PaintType, Pixmap}; const DEFAULT_TOLERANCE: f64 = 0.1; -fn anyrender_paint_to_vello_cpu_paint<'a>(paint: PaintRef<'a>) -> PaintType { +pub struct VelloCpuRenderContext { + pub(crate) resource_map: HashMap, + next_resource_id: u64, +} + +impl VelloCpuRenderContext { + pub fn new() -> Self { + Self { + resource_map: HashMap::new(), + next_resource_id: 0, + } + } +} + +impl Default for VelloCpuRenderContext { + fn default() -> Self { + Self::new() + } +} + +impl RenderContext for VelloCpuRenderContext { + fn register_image(&mut self, image: ImageData) -> ImageResource { + let resource_id = ResourceId(self.next_resource_id); + self.next_resource_id += 1; + + let image_source = ImageSource::from_peniko_image_data(&image); + self.resource_map.insert(resource_id, image_source); + + ImageResource { + id: resource_id, + width: image.width, + height: image.height, + } + } + + fn unregister_resource(&mut self, id: ResourceId) { + self.resource_map.remove(&id); + } +} + +fn anyrender_paint_to_vello_cpu_paint( + paint: PaintRef<'_>, + ctx: &VelloCpuRenderContext, +) -> PaintType { match paint { Paint::Solid(alpha_color) => PaintType::Solid(alpha_color), Paint::Gradient(gradient) => PaintType::Gradient(gradient.clone()), Paint::Image(image) => PaintType::Image(ImageBrush { - #[cfg(not(feature = "experimental_image_cache"))] - image: ImageSource::from_peniko_image_data(image.image), - #[cfg(feature = "experimental_image_cache")] - image: convert_image_cached(image.image), + image: ctx.resource_map[&image.image.id].clone(), sampler: image.sampler, }), // TODO: custom paint @@ -21,33 +64,29 @@ fn anyrender_paint_to_vello_cpu_paint<'a>(paint: PaintRef<'a>) -> PaintType { } } -#[cfg(feature = "experimental_image_cache")] -fn convert_image_cached(image: &peniko::ImageData) -> ImageSource { - use std::collections::HashMap; - use std::sync::{LazyLock, Mutex}; - static CACHE: LazyLock>> = - LazyLock::new(|| Mutex::new(HashMap::new())); - - let mut map = CACHE.lock().unwrap(); - let id = image.data.id(); - map.entry(id) - .or_insert_with(|| ImageSource::from_peniko_image_data(image)) - .clone() +pub struct VelloCpuScenePainter<'a> { + pub(crate) ctx: &'a VelloCpuRenderContext, + pub render_ctx: &'a mut vello_cpu::RenderContext, } -pub struct VelloCpuScenePainter(pub vello_cpu::RenderContext); +impl<'a> VelloCpuScenePainter<'a> { + pub fn new( + ctx: &'a VelloCpuRenderContext, + render_ctx: &'a mut vello_cpu::RenderContext, + ) -> Self { + Self { ctx, render_ctx } + } -impl VelloCpuScenePainter { pub fn finish(self) -> Pixmap { - let mut pixmap = Pixmap::new(self.0.width(), self.0.height()); - self.0.render_to_pixmap(&mut pixmap); + let mut pixmap = Pixmap::new(self.render_ctx.width(), self.render_ctx.height()); + self.render_ctx.render_to_pixmap(&mut pixmap); pixmap } } -impl PaintScene for VelloCpuScenePainter { +impl PaintScene for VelloCpuScenePainter<'_> { fn reset(&mut self) { - self.0.reset(); + self.render_ctx.reset(); } fn push_layer( @@ -57,8 +96,8 @@ impl PaintScene for VelloCpuScenePainter { transform: Affine, clip: &impl Shape, ) { - self.0.set_transform(transform); - self.0.push_layer( + self.render_ctx.set_transform(transform); + self.render_ctx.push_layer( Some(&clip.into_path(DEFAULT_TOLERANCE)), Some(blend.into()), Some(alpha), @@ -68,12 +107,13 @@ impl PaintScene for VelloCpuScenePainter { } fn push_clip_layer(&mut self, transform: Affine, clip: &impl Shape) { - self.0.set_transform(transform); - self.0.push_clip_layer(&clip.into_path(DEFAULT_TOLERANCE)); + self.render_ctx.set_transform(transform); + self.render_ctx + .push_clip_layer(&clip.into_path(DEFAULT_TOLERANCE)); } fn pop_layer(&mut self) { - self.0.pop_layer(); + self.render_ctx.pop_layer(); } fn stroke<'a>( @@ -84,13 +124,14 @@ impl PaintScene for VelloCpuScenePainter { brush_transform: Option, shape: &impl Shape, ) { - self.0.set_transform(transform); - self.0.set_stroke(style.clone()); - self.0 - .set_paint(anyrender_paint_to_vello_cpu_paint(paint.into())); - self.0 + self.render_ctx.set_transform(transform); + self.render_ctx.set_stroke(style.clone()); + self.render_ctx + .set_paint(anyrender_paint_to_vello_cpu_paint(paint.into(), self.ctx)); + self.render_ctx .set_paint_transform(brush_transform.unwrap_or(Affine::IDENTITY)); - self.0.stroke_path(&shape.into_path(DEFAULT_TOLERANCE)); + self.render_ctx + .stroke_path(&shape.into_path(DEFAULT_TOLERANCE)); } fn fill<'a>( @@ -101,13 +142,14 @@ impl PaintScene for VelloCpuScenePainter { brush_transform: Option, shape: &impl Shape, ) { - self.0.set_transform(transform); - self.0.set_fill_rule(style); - self.0 - .set_paint(anyrender_paint_to_vello_cpu_paint(paint.into())); - self.0 + self.render_ctx.set_transform(transform); + self.render_ctx.set_fill_rule(style); + self.render_ctx + .set_paint(anyrender_paint_to_vello_cpu_paint(paint.into(), self.ctx)); + self.render_ctx .set_paint_transform(brush_transform.unwrap_or(Affine::IDENTITY)); - self.0.fill_path(&shape.into_path(DEFAULT_TOLERANCE)); + self.render_ctx + .fill_path(&shape.into_path(DEFAULT_TOLERANCE)); } fn draw_glyphs<'a, 's: 'a>( @@ -123,9 +165,9 @@ impl PaintScene for VelloCpuScenePainter { glyph_transform: Option, glyphs: impl Iterator, ) { - self.0.set_transform(transform); - self.0 - .set_paint(anyrender_paint_to_vello_cpu_paint(paint.into())); + self.render_ctx.set_transform(transform); + self.render_ctx + .set_paint(anyrender_paint_to_vello_cpu_paint(paint.into(), self.ctx)); fn into_vello_cpu_glyph(g: anyrender::Glyph) -> vello_cpu::Glyph { vello_cpu::Glyph { @@ -138,8 +180,8 @@ impl PaintScene for VelloCpuScenePainter { let style: StyleRef<'a> = style.into(); match style { StyleRef::Fill(fill) => { - self.0.set_fill_rule(fill); - self.0 + self.render_ctx.set_fill_rule(fill); + self.render_ctx .glyph_run(font) .font_size(font_size) .hint(hint) @@ -148,8 +190,8 @@ impl PaintScene for VelloCpuScenePainter { .fill_glyphs(glyphs.map(into_vello_cpu_glyph)); } StyleRef::Stroke(stroke) => { - self.0.set_stroke(stroke.clone()); - self.0 + self.render_ctx.set_stroke(stroke.clone()); + self.render_ctx .glyph_run(font) .font_size(font_size) .hint(hint) @@ -167,9 +209,9 @@ impl PaintScene for VelloCpuScenePainter { radius: f64, std_dev: f64, ) { - self.0.set_transform(transform); - self.0.set_paint(PaintType::Solid(color)); - self.0 + self.render_ctx.set_transform(transform); + self.render_ctx.set_paint(PaintType::Solid(color)); + self.render_ctx .fill_blurred_rounded_rect(&rect, radius as f32, std_dev as f32); } } diff --git a/crates/anyrender_vello_hybrid/src/lib.rs b/crates/anyrender_vello_hybrid/src/lib.rs index 632e0ae..9a28e45 100644 --- a/crates/anyrender_vello_hybrid/src/lib.rs +++ b/crates/anyrender_vello_hybrid/src/lib.rs @@ -6,8 +6,7 @@ mod scene; mod webgl_scene; mod window_renderer; -pub use scene::ImageManager; -pub use scene::VelloHybridScenePainter; +pub use scene::{VelloHybridRenderContext, VelloHybridScenePainter}; #[cfg(all(target_arch = "wasm32", feature = "webgl"))] pub use webgl_scene::*; -pub use window_renderer::*; +pub use window_renderer::{VelloHybridRendererOptions, VelloHybridWindowRenderer}; diff --git a/crates/anyrender_vello_hybrid/src/scene.rs b/crates/anyrender_vello_hybrid/src/scene.rs index 2a23462..277065d 100644 --- a/crates/anyrender_vello_hybrid/src/scene.rs +++ b/crates/anyrender_vello_hybrid/src/scene.rs @@ -1,82 +1,119 @@ -use anyrender::{NormalizedCoord, Paint, PaintRef, PaintScene}; +use anyrender::{ + ImageResource, NormalizedCoord, Paint, PaintRef, PaintScene, RenderContext, ResourceId, +}; use kurbo::{Affine, Rect, Shape, Stroke}; use peniko::{BlendMode, Color, Fill, FontData, ImageBrush, ImageData, StyleRef}; use rustc_hash::FxHashMap; use vello_common::paint::{ImageId, ImageSource, PaintType}; -use vello_hybrid::Renderer; -use wgpu::{CommandEncoder, Device, Queue}; +use vello_hybrid::Renderer as VelloHybridRenderer; +use wgpu::CommandEncoderDescriptor; +use wgpu_context::SurfaceRenderer; const DEFAULT_TOLERANCE: f64 = 0.1; -fn anyrender_paint_to_vello_hybrid_paint<'a>( - paint: PaintRef<'a>, - image_manager: &mut ImageManager<'_>, -) -> PaintType { - match paint { - Paint::Solid(alpha_color) => PaintType::Solid(alpha_color), - Paint::Gradient(gradient) => PaintType::Gradient(gradient.clone()), +/// A standalone [`RenderContext`] for the Vello Hybrid (WGPU) backend. +/// +/// Image registration is deferred: calling [`register_image`](RenderContext::register_image) +/// stores the raw [`ImageData`] in a pending queue and returns an [`ImageResource`] +/// immediately. The actual GPU upload happens transparently when the renderer's +/// [`render`](WindowRenderer::render) method is called. +pub struct VelloHybridRenderContext { + pub(crate) resource_map: FxHashMap, + next_resource_id: u64, + pending_uploads: Vec<(ResourceId, ImageData)>, +} - Paint::Image(image_brush) => { - let image_id = image_manager.upload_image(image_brush.image); - PaintType::Image(ImageBrush { - image: ImageSource::OpaqueId(image_id), - sampler: image_brush.sampler, - }) +impl VelloHybridRenderContext { + pub fn new() -> Self { + Self { + resource_map: FxHashMap::default(), + next_resource_id: 0, + pending_uploads: Vec::new(), } + } - // TODO: custom paint - Paint::Custom(_) => PaintType::Solid(peniko::color::palette::css::TRANSPARENT), + /// Flush any pending image uploads to the GPU. + /// + /// This must be called before rendering the scene. + pub fn flush_pending_uploads( + &mut self, + renderer: &mut VelloHybridRenderer, + render_surface: &mut SurfaceRenderer<'static>, + ) { + if self.pending_uploads.is_empty() { + return; + } + + let mut encoder = + render_surface + .device() + .create_command_encoder(&CommandEncoderDescriptor { + label: Some("Image upload"), + }); + + for (resource_id, image_data) in self.pending_uploads.drain(..) { + let ImageSource::Pixmap(pixmap) = ImageSource::from_peniko_image_data(&image_data) + else { + unreachable!(); // ImageSource::from_peniko_image_data always returns a Pixmap + }; + + let image_id = renderer.upload_image( + render_surface.device(), + render_surface.queue(), + &mut encoder, + &pixmap, + ); + + self.resource_map.insert(resource_id, image_id); + } + + render_surface.queue().submit([encoder.finish()]); } } -pub struct ImageManager<'a> { - pub(crate) renderer: &'a mut Renderer, - pub(crate) device: &'a Device, - pub(crate) queue: &'a Queue, - pub(crate) encoder: &'a mut CommandEncoder, - pub(crate) cache: &'a mut FxHashMap, +impl Default for VelloHybridRenderContext { + fn default() -> Self { + Self::new() + } } -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, +impl RenderContext for VelloHybridRenderContext { + fn register_image(&mut self, image: ImageData) -> ImageResource { + let resource_id = ResourceId(self.next_resource_id); + self.next_resource_id += 1; + let width = image.width; + let height = image.height; + self.pending_uploads.push((resource_id, image)); + ImageResource { + id: resource_id, + width, + height, } } - pub(crate) fn upload_image(&mut self, image: &ImageData) -> ImageId { - let peniko_id = image.data.id(); - - // Try to get ImageId from cache first - if let Some(atlas_id) = self.cache.get(&peniko_id) { - return *atlas_id; - }; - - // Convert ImageData to Pixmap - let ImageSource::Pixmap(pixmap) = ImageSource::from_peniko_image_data(image) else { - unreachable!(); // ImageSource::from_peniko_image_data always return a Pixmap - }; + fn unregister_resource(&mut self, id: ResourceId) { + self.resource_map.remove(&id); + } +} - // Upload Pixamp - let atlas_id = self - .renderer - .upload_image(self.device, self.queue, self.encoder, &pixmap); +fn anyrender_paint_to_vello_hybrid_paint( + paint: PaintRef<'_>, + ctx: &VelloHybridRenderContext, +) -> PaintType { + match paint { + Paint::Solid(alpha_color) => PaintType::Solid(alpha_color), + Paint::Gradient(gradient) => PaintType::Gradient(gradient.clone()), - // Store ImageId in cache - self.cache.insert(peniko_id, atlas_id); + Paint::Image(image_brush) => { + let image_id = ctx.resource_map[&image_brush.image.id]; + PaintType::Image(ImageBrush { + image: ImageSource::OpaqueId(image_id), + sampler: image_brush.sampler, + }) + } - // Return ImageId - atlas_id + // TODO: custom paint + Paint::Custom(_) => PaintType::Solid(peniko::color::palette::css::TRANSPARENT), } } @@ -86,20 +123,20 @@ pub(crate) enum LayerKind { } pub struct VelloHybridScenePainter<'s> { + pub(crate) ctx: &'s VelloHybridRenderContext, pub(crate) scene: &'s mut vello_hybrid::Scene, pub(crate) layer_stack: Vec, - pub(crate) image_manager: ImageManager<'s>, } impl VelloHybridScenePainter<'_> { pub fn new<'s>( + ctx: &'s VelloHybridRenderContext, scene: &'s mut vello_hybrid::Scene, - image_manager: ImageManager<'s>, ) -> VelloHybridScenePainter<'s> { VelloHybridScenePainter { + ctx, scene, layer_stack: Vec::with_capacity(16), - image_manager, } } } @@ -153,7 +190,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); + let paint = anyrender_paint_to_vello_hybrid_paint(paint.into(), self.ctx); self.scene.set_paint(paint); self.scene .set_paint_transform(brush_transform.unwrap_or(Affine::IDENTITY)); @@ -170,7 +207,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); + let paint = anyrender_paint_to_vello_hybrid_paint(paint.into(), self.ctx); self.scene.set_paint(paint); self.scene .set_paint_transform(brush_transform.unwrap_or(Affine::IDENTITY)); @@ -190,7 +227,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); + let paint = anyrender_paint_to_vello_hybrid_paint(paint.into(), self.ctx); self.scene.set_paint(paint); self.scene.set_transform(transform); diff --git a/crates/anyrender_vello_hybrid/src/webgl_scene.rs b/crates/anyrender_vello_hybrid/src/webgl_scene.rs index db4b709..53967b9 100644 --- a/crates/anyrender_vello_hybrid/src/webgl_scene.rs +++ b/crates/anyrender_vello_hybrid/src/webgl_scene.rs @@ -1,43 +1,69 @@ //! WebGL-compatible [`PaintScene`] implementation for [`vello_hybrid::Scene`]. -use anyrender::{Glyph, NormalizedCoord, Paint, PaintRef, PaintScene}; +use anyrender::{ + Glyph, ImageResource, NormalizedCoord, Paint, PaintRef, PaintScene, RenderContext, ResourceId, +}; use kurbo::{Affine, Rect, Shape, Stroke}; -use peniko::{BlendMode, Color, Fill, FontData, StyleRef}; -use vello_common::paint::PaintType; - -use peniko::ImageBrush; +use peniko::{BlendMode, Color, Fill, FontData, ImageBrush, ImageData, StyleRef}; use rustc_hash::FxHashMap; -use vello_common::paint::{ImageId, ImageSource}; +use vello_common::paint::{ImageId, ImageSource, PaintType}; const DEFAULT_TOLERANCE: f64 = 0.1; -pub struct WebGlImageManager<'a> { - pub(crate) renderer: &'a mut vello_hybrid::WebGlRenderer, - pub(crate) cache: &'a mut FxHashMap, +pub struct WebGlRenderContext { + resource_map: FxHashMap, + next_id: u64, + pending_uploads: Vec<(ResourceId, ImageData)>, } -impl<'a> WebGlImageManager<'a> { - pub fn new( - renderer: &'a mut vello_hybrid::WebGlRenderer, - cache: &'a mut FxHashMap, - ) -> Self { - Self { renderer, cache } +impl WebGlRenderContext { + pub fn new() -> Self { + Self { + resource_map: FxHashMap::default(), + next_id: 0, + pending_uploads: Vec::new(), + } } - 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; + /// Flush any pending image uploads to the WebGL renderer. + /// + /// Must be called before creating a [`WebGlScenePainter`] if images have been + /// registered since the last flush. + pub fn flush_pending_uploads(&mut self, renderer: &mut vello_hybrid::WebGlRenderer) { + for (resource_id, image_data) in self.pending_uploads.drain(..) { + let ImageSource::Pixmap(pixmap) = ImageSource::from_peniko_image_data(&image_data) + else { + unreachable!(); + }; + + let image_id = renderer.upload_image(&pixmap); + self.resource_map.insert(resource_id, image_id); } + } +} + +impl Default for WebGlRenderContext { + fn default() -> Self { + Self::new() + } +} - let ImageSource::Pixmap(pixmap) = ImageSource::from_peniko_image_data(image) else { - unreachable!(); - }; +impl RenderContext for WebGlRenderContext { + fn register_image(&mut self, image: ImageData) -> ImageResource { + let resource_id = ResourceId(self.next_id); + self.next_id += 1; + let width = image.width; + let height = image.height; + self.pending_uploads.push((resource_id, image)); + ImageResource { + id: resource_id, + width, + height, + } + } - let atlas_id = self.renderer.upload_image(&pixmap); - self.cache.insert(peniko_id, atlas_id); - atlas_id + fn unregister_resource(&mut self, id: ResourceId) { + self.resource_map.remove(&id); } } @@ -47,38 +73,36 @@ enum LayerKind { } pub struct WebGlScenePainter<'s> { + ctx: &'s WebGlRenderContext, 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 { + pub fn new(ctx: &'s WebGlRenderContext, scene: &'s mut vello_hybrid::Scene) -> Self { Self { + ctx, scene, layer_stack: Vec::with_capacity(16), - image_manager, } } } impl WebGlScenePainter<'_> { - fn convert_paint(&mut self, paint: PaintRef<'_>) -> PaintType { + fn convert_paint(&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::Image(image_brush) => { + let image_id = self.ctx.resource_map[&image_brush.image.id]; + PaintType::Image(ImageBrush { + image: ImageSource::OpaqueId(image_id), + sampler: image_brush.sampler, + }) + } 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<'_> { diff --git a/crates/anyrender_vello_hybrid/src/window_renderer.rs b/crates/anyrender_vello_hybrid/src/window_renderer.rs index e6e216d..aff68c2 100644 --- a/crates/anyrender_vello_hybrid/src/window_renderer.rs +++ b/crates/anyrender_vello_hybrid/src/window_renderer.rs @@ -1,11 +1,9 @@ -use anyrender::{WindowHandle, WindowRenderer}; +use anyrender::{ImageResource, RenderContext, ResourceId, WindowHandle, WindowRenderer}; use debug_timer::debug_timer; +use peniko::ImageData; use rustc_hash::FxHashMap; -use std::sync::{ - Arc, - // atomic::{AtomicU64}, -}; -use vello_common::paint::ImageId; +use std::sync::Arc; +use vello_common::paint::{ImageId, ImageSource}; use vello_hybrid::{ RenderSettings, RenderSize, RenderTargetConfig, Renderer as VelloHybridRenderer, Scene as VelloHybridScene, @@ -13,10 +11,7 @@ use vello_hybrid::{ use wgpu::{CommandEncoderDescriptor, Features, Limits, PresentMode, TextureFormat}; use wgpu_context::{DeviceHandle, SurfaceRenderer, SurfaceRendererConfiguration, WGPUContext}; -use crate::{VelloHybridScenePainter, scene::ImageManager}; -// use crate::CustomPaintSource; - -// static PAINT_SOURCE_ID: AtomicU64 = AtomicU64::new(0); +use crate::{VelloHybridScenePainter, scene::VelloHybridRenderContext}; // Simple struct to hold the state of the renderer struct ActiveRenderState { @@ -56,8 +51,6 @@ pub struct VelloHybridWindowRenderer { wgpu_context: WGPUContext, scene: VelloHybridScene, config: VelloHybridRendererOptions, - // custom_paint_sources: FxHashMap>, - cached_images: FxHashMap, } impl VelloHybridWindowRenderer { #[allow(clippy::new_without_default)] @@ -79,31 +72,12 @@ impl VelloHybridWindowRenderer { render_state: RenderState::Suspended, window_handle: None, scene: VelloHybridScene::new_with(0, 0, render_settings), - // custom_paint_sources: FxHashMap::default(), - cached_images: FxHashMap::default(), } } pub fn current_device_handle(&self) -> Option<&DeviceHandle> { self.render_state.current_device_handle() } - - // pub fn register_custom_paint_source(&mut self, mut source: Box) -> u64 { - // if let Some(device_handle) = self.render_state.current_device_handle() { - // source.resume(device_handle); - // } - // let id = PAINT_SOURCE_ID.fetch_add(1, atomic::Ordering::SeqCst); - // self.custom_paint_sources.insert(id, source); - - // id - // } - - // pub fn unregister_custom_paint_source(&mut self, id: u64) { - // if let Some(mut source) = self.custom_paint_sources.remove(&id) { - // source.suspend(); - // drop(source); - // } - // } } // TODO: Make configurable? @@ -117,6 +91,7 @@ impl WindowRenderer for VelloHybridWindowRenderer { = VelloHybridScenePainter<'a> where Self: 'a; + type Context = VelloHybridRenderContext; fn is_active(&self) -> bool { matches!(self.render_state, RenderState::Active(_)) @@ -137,9 +112,6 @@ impl WindowRenderer for VelloHybridWindowRenderer { view_formats: vec![], }, None, - // Some(TextureConfiguration { - // usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, - // }), )) .expect("Error creating surface"); @@ -153,12 +125,6 @@ impl WindowRenderer for VelloHybridWindowRenderer { }, ); - // Resume custom paint sources - // let device_handle = &render_surface.device_handle; - // for source in self.custom_paint_sources.values_mut() { - // source.resume(device_handle) - // } - // Create a Scene with the correct dimensions self.scene = VelloHybridScene::new_with(width as u16, height as u16, self.config.render_settings); @@ -172,11 +138,6 @@ impl WindowRenderer for VelloHybridWindowRenderer { } fn suspend(&mut self) { - // Suspend custom paint sources - // for source in self.custom_paint_sources.values_mut() { - // source.suspend() - // } - // Set state to Suspended self.render_state = RenderState::Suspended; } @@ -194,11 +155,18 @@ impl WindowRenderer for VelloHybridWindowRenderer { } } - fn render)>(&mut self, draw_fn: F) { + fn render)>( + &mut self, + ctx: &mut Self::Context, + draw_fn: F, + ) { let RenderState::Active(state) = &mut self.render_state else { return; }; + // Flush any pending image uploads before drawing + ctx.flush_pending_uploads(&mut state.renderer, &mut state.render_surface); + let render_surface = &mut state.render_surface; debug_timer!(timer, feature = "log_frame_times"); @@ -210,19 +178,11 @@ impl WindowRenderer for VelloHybridWindowRenderer { label: Some("Render scene"), }); - let image_manager = ImageManager { - renderer: &mut state.renderer, - device: render_surface.device(), - queue: render_surface.queue(), - encoder: &mut encoder, - cache: &mut self.cached_images, - }; - // Regenerate the vello scene draw_fn(&mut VelloHybridScenePainter { + ctx, scene: &mut self.scene, layer_stack: Vec::new(), - image_manager, }); timer.record_time("cmd"); @@ -258,9 +218,6 @@ impl WindowRenderer for VelloHybridWindowRenderer { timer.record_time("wait"); timer.print_times("vello_hybrid: "); - // static COUNTER: AtomicU64 = AtomicU64::new(0); - // println!("FRAME {}", COUNTER.fetch_add(1, atomic::Ordering::Relaxed)); - // Empty the Vello scene (memory optimisation) self.scene.reset(); } diff --git a/crates/pixels_window_renderer/src/lib.rs b/crates/pixels_window_renderer/src/lib.rs index e36986f..4941b5a 100644 --- a/crates/pixels_window_renderer/src/lib.rs +++ b/crates/pixels_window_renderer/src/lib.rs @@ -47,6 +47,7 @@ impl WindowRenderer for PixelsWindowRenderer = ::ScenePainter<'a> where Renderer: 'a; + type Context = Renderer::Context; fn is_active(&self) -> bool { matches!(self.render_state, RenderState::Active(_)) @@ -86,7 +87,11 @@ impl WindowRenderer for PixelsWindowRenderer }; } - fn render)>(&mut self, draw_fn: F) { + fn render)>( + &mut self, + ctx: &mut Self::Context, + draw_fn: F, + ) { let RenderState::Active(state) = &mut self.render_state else { return; }; @@ -94,7 +99,7 @@ impl WindowRenderer for PixelsWindowRenderer debug_timer!(timer, feature = "log_frame_times"); // Paint - self.renderer.render(draw_fn, state.pixels.frame_mut()); + self.renderer.render(ctx, draw_fn, state.pixels.frame_mut()); timer.record_time("render"); state.pixels.render().unwrap(); diff --git a/crates/softbuffer_window_renderer/src/lib.rs b/crates/softbuffer_window_renderer/src/lib.rs index 30ee68d..7ef44ba 100644 --- a/crates/softbuffer_window_renderer/src/lib.rs +++ b/crates/softbuffer_window_renderer/src/lib.rs @@ -49,6 +49,7 @@ impl WindowRenderer for SoftbufferWindowRenderer where Self: 'a; + type Context = Renderer::Context; fn is_active(&self) -> bool { matches!(self.render_state, RenderState::Active(_)) @@ -83,7 +84,11 @@ impl WindowRenderer for SoftbufferWindowRenderer)>(&mut self, draw_fn: F) { + fn render)>( + &mut self, + ctx: &mut Self::Context, + draw_fn: F, + ) { let RenderState::Active(state) = &mut self.render_state else { return; }; @@ -96,7 +101,7 @@ impl WindowRenderer for SoftbufferWindowRenderer>, bunnies: Vec, } @@ -81,11 +82,27 @@ impl BunnyManager { width: canvas_width, height: canvas_height, }, - bunny_image: create_bunny_image(), + bunny_image_data: create_bunny_image_data(), + bunny_image: None, bunnies: Vec::new(), } } + /// Register the bunny image with the given render context. + /// Must be called whenever the backend renderer changes. + pub fn register_image(&mut self, ctx: &mut impl RenderContext) { + let resource = ctx.register_image(self.bunny_image_data.clone()); + self.bunny_image = Some(ImageBrush { + image: resource, + sampler: ImageSampler { + x_extend: peniko::Extend::Pad, + y_extend: peniko::Extend::Pad, + quality: peniko::ImageQuality::Medium, + alpha: 1.0, + }, + }); + } + pub fn add_bunnies(&mut self, count: usize) { self.bunnies .resize_with(self.bunnies.len() + count, || Bunny::new(self.canvas_size)); @@ -108,17 +125,20 @@ impl BunnyManager { } pub fn draw(&self, scene: &mut S, scale_factor: f64) { + let Some(bunny_image) = &self.bunny_image else { + return; + }; for bunny in &self.bunnies { let pos = bunny.position(); scene.draw_image( - self.bunny_image.as_ref(), + bunny_image.clone(), Affine::translate(pos).then_scale(scale_factor), ); } } } -fn create_bunny_image() -> ImageBrush { +fn create_bunny_image_data() -> ImageData { static BUNNY_IMAGE_DATA: &[u8] = include_bytes!("./bunny.png"); let raw_bunny_image = ImageReader::with_format(Cursor::new(BUNNY_IMAGE_DATA), image::ImageFormat::Png) @@ -127,19 +147,11 @@ fn create_bunny_image() -> ImageBrush { .into_rgba8(); let width = raw_bunny_image.width(); let height = raw_bunny_image.height(); - ImageBrush { - image: ImageData { - data: Blob::new(Arc::new(raw_bunny_image.into_vec())), - format: peniko::ImageFormat::Rgba8, - alpha_type: peniko::ImageAlphaType::Alpha, - width, - height, - }, - sampler: ImageSampler { - x_extend: peniko::Extend::Pad, - y_extend: peniko::Extend::Pad, - quality: peniko::ImageQuality::Medium, - alpha: 1.0, - }, + ImageData { + data: Blob::new(Arc::new(raw_bunny_image.into_vec())), + format: peniko::ImageFormat::Rgba8, + alpha_type: peniko::ImageAlphaType::Alpha, + width, + height, } } diff --git a/examples/bunnymark/src/main.rs b/examples/bunnymark/src/main.rs index 9e94b4a..73b80d4 100644 --- a/examples/bunnymark/src/main.rs +++ b/examples/bunnymark/src/main.rs @@ -1,8 +1,8 @@ use anyrender::{PaintScene, WindowRenderer}; -use anyrender_skia::{SkiaImageRenderer, SkiaWindowRenderer}; -use anyrender_vello::VelloWindowRenderer; -use anyrender_vello_cpu::VelloCpuWindowRenderer; -use anyrender_vello_hybrid::VelloHybridWindowRenderer; +use anyrender_skia::{SkiaImageRenderer, SkiaRenderContext, SkiaWindowRenderer}; +use anyrender_vello::{VelloRenderContext, VelloWindowRenderer}; +use anyrender_vello_cpu::{VelloCpuRenderContext, VelloCpuWindowRenderer}; +use anyrender_vello_hybrid::{VelloHybridRenderContext, VelloHybridWindowRenderer}; use bunny::BunnyManager; use kurbo::{Affine, Circle, Point, Rect, Stroke}; use peniko::{Color, Fill}; @@ -31,31 +31,31 @@ struct App { } enum Renderer { - Skia(Box), - SkiaRaster(Box), - Gpu(Box), - Hybrid(Box), - Cpu(Box), + Skia(Box, SkiaRenderContext), + SkiaRaster(Box, SkiaRenderContext), + Gpu(Box, VelloRenderContext), + Hybrid(Box, VelloHybridRenderContext), + Cpu(Box, VelloCpuRenderContext), } impl Renderer { fn is_active(&self) -> bool { match self { - Renderer::Skia(r) => r.is_active(), - Renderer::SkiaRaster(r) => r.is_active(), - Renderer::Gpu(r) => r.is_active(), - Renderer::Hybrid(r) => r.is_active(), - Renderer::Cpu(r) => r.is_active(), + Renderer::Skia(r, _) => r.is_active(), + Renderer::SkiaRaster(r, _) => r.is_active(), + Renderer::Gpu(r, _) => r.is_active(), + Renderer::Hybrid(r, _) => r.is_active(), + Renderer::Cpu(r, _) => r.is_active(), } } fn set_size(&mut self, w: u32, h: u32) { match self { - Renderer::Skia(r) => r.set_size(w, h), - Renderer::SkiaRaster(r) => r.set_size(w, h), - Renderer::Gpu(r) => r.set_size(w, h), - Renderer::Hybrid(r) => r.set_size(w, h), - Renderer::Cpu(r) => r.set_size(w, h), + Renderer::Skia(r, _) => r.set_size(w, h), + Renderer::SkiaRaster(r, _) => r.set_size(w, h), + Renderer::Gpu(r, _) => r.set_size(w, h), + Renderer::Hybrid(r, _) => r.set_size(w, h), + Renderer::Cpu(r, _) => r.set_size(w, h), } } } @@ -126,8 +126,9 @@ impl App { fn set_backend( &mut self, mut renderer: R, + mut ctx: R::Context, event_loop: &ActiveEventLoop, - f: impl FnOnce(R) -> Renderer, + f: impl FnOnce(R, R::Context) -> Renderer, ) { let mut window = match &self.render_state { RenderState::Active { window, .. } => Some(window.clone()), @@ -149,9 +150,10 @@ impl App { let physical_size = window.inner_size(); renderer.resume(window.clone(), physical_size.width, physical_size.height); + self.bunny_manager.register_image(&mut ctx); self.render_state = RenderState::Active { window, - renderer: f(renderer), + renderer: f(renderer, ctx), }; self.request_redraw(); } @@ -165,9 +167,12 @@ impl ApplicationHandler for App { } fn resumed(&mut self, event_loop: &ActiveEventLoop) { - self.set_backend(SkiaWindowRenderer::new(), event_loop, |r| { - Renderer::Skia(Box::new(r)) - }); + self.set_backend( + SkiaWindowRenderer::new(), + SkiaRenderContext::new(), + event_loop, + |r, ctx| Renderer::Skia(Box::new(r), ctx), + ); } fn window_event( @@ -207,11 +212,11 @@ impl ApplicationHandler for App { self.bunny_manager .update(self.logical_width as f64, self.logical_height as f64); let renderer_name = match renderer { - Renderer::Skia(_) => "skia", - Renderer::SkiaRaster(_) => "skia(raster)", - Renderer::Gpu(_) => "vello", - Renderer::Hybrid(_) => "vello_hybrid", - Renderer::Cpu(_) => "vello_cpu", + Renderer::Skia(..) => "skia", + Renderer::SkiaRaster(..) => "skia(raster)", + Renderer::Gpu(..) => "vello", + Renderer::Hybrid(..) => "vello_hybrid", + Renderer::Cpu(..) => "vello_cpu", }; print!( "[{}] [{} bunnies] ", @@ -219,7 +224,7 @@ impl ApplicationHandler for App { self.bunny_manager.count(), ); match renderer { - Renderer::Skia(r) => r.render(|scene_painter| { + Renderer::Skia(r, ctx) => r.render(ctx, |scene_painter| { App::draw_scene( scene_painter, self.logical_width, @@ -229,7 +234,7 @@ impl ApplicationHandler for App { Color::from_rgb8(255, 0, 0), ); }), - Renderer::SkiaRaster(r) => r.render(|scene_painter| { + Renderer::SkiaRaster(r, ctx) => r.render(ctx, |scene_painter| { App::draw_scene( scene_painter, self.logical_width, @@ -239,7 +244,7 @@ impl ApplicationHandler for App { Color::from_rgb8(255, 0, 0), ); }), - Renderer::Gpu(r) => r.render(|scene_painter| { + Renderer::Gpu(r, ctx) => r.render(ctx, |scene_painter| { App::draw_scene( scene_painter, self.logical_width, @@ -249,7 +254,7 @@ impl ApplicationHandler for App { Color::from_rgb8(255, 0, 0), ); }), - Renderer::Hybrid(r) => r.render(|scene_painter| { + Renderer::Hybrid(r, ctx) => r.render(ctx, |scene_painter| { App::draw_scene( scene_painter, self.logical_width, @@ -259,7 +264,7 @@ impl ApplicationHandler for App { Color::from_rgb8(255, 0, 0), ); }), - Renderer::Cpu(r) => r.render(|scene_painter| { + Renderer::Cpu(r, ctx) => r.render(ctx, |scene_painter| { App::draw_scene( scene_painter, self.logical_width, @@ -288,30 +293,45 @@ impl ApplicationHandler for App { } => { if logical_key == Key::Named(NamedKey::Space) { match renderer { - Renderer::SkiaRaster(_) => { - self.set_backend(VelloCpuWindowRenderer::new(), event_loop, |r| { - Renderer::Cpu(Box::new(r)) - }); + Renderer::SkiaRaster(..) => { + self.set_backend( + VelloCpuWindowRenderer::new(), + VelloCpuRenderContext::new(), + event_loop, + |r, ctx| Renderer::Cpu(Box::new(r), ctx), + ); } - Renderer::Cpu(_) => { - self.set_backend(VelloHybridWindowRenderer::new(), event_loop, |r| { - Renderer::Hybrid(Box::new(r)) - }); + Renderer::Cpu(..) => { + self.set_backend( + VelloHybridWindowRenderer::new(), + VelloHybridRenderContext::new(), + event_loop, + |r, ctx| Renderer::Hybrid(Box::new(r), ctx), + ); } - Renderer::Hybrid(_) => { - self.set_backend(VelloWindowRenderer::new(), event_loop, |r| { - Renderer::Gpu(Box::new(r)) - }); + Renderer::Hybrid(..) => { + self.set_backend( + VelloWindowRenderer::new(), + VelloRenderContext::new(), + event_loop, + |r, ctx| Renderer::Gpu(Box::new(r), ctx), + ); } - Renderer::Gpu(_) => { - self.set_backend(SkiaWindowRenderer::new(), event_loop, |r| { - Renderer::Skia(Box::new(r)) - }); + Renderer::Gpu(..) => { + self.set_backend( + SkiaWindowRenderer::new(), + SkiaRenderContext::new(), + event_loop, + |r, ctx| Renderer::Skia(Box::new(r), ctx), + ); } - Renderer::Skia(_) => { - self.set_backend(SkiaRasterWindowRenderer::new(), event_loop, |r| { - Renderer::SkiaRaster(Box::new(r)) - }); + Renderer::Skia(..) => { + self.set_backend( + SkiaRasterWindowRenderer::new(), + SkiaRenderContext::new(), + event_loop, + |r, ctx| Renderer::SkiaRaster(Box::new(r), ctx), + ); } } } else if logical_key == Key::Character(SmolStr::new("r")) { diff --git a/examples/serialize/src/main.rs b/examples/serialize/src/main.rs index 1fba673..5b18e0f 100644 --- a/examples/serialize/src/main.rs +++ b/examples/serialize/src/main.rs @@ -5,9 +5,9 @@ use std::io::BufWriter; use std::path::Path; use anyrender::recording::Scene; -use anyrender::{Glyph, PaintScene, render_to_buffer}; +use anyrender::{Glyph, ImageRenderer, PaintScene, RecordingRenderContext, RenderContext}; use anyrender_serialize::{SceneArchive, SerializeConfig}; -use anyrender_vello_cpu::VelloCpuImageRenderer; +use anyrender_vello_cpu::{VelloCpuImageRenderer, VelloCpuRenderContext}; use image::{ImageBuffer, RgbaImage}; use kurbo::{Affine, Circle, Point, Rect, RoundedRect, Stroke}; use parley::style::{FontFamily, FontStack}; @@ -22,13 +22,23 @@ const HEIGHT: u32 = 300; const OUTPUT_DIR: &str = "examples/serialize/_output"; fn main() { - let original_scene = create_demo_scene(); + let mut rec_ctx = RecordingRenderContext::new(); + let original_scene = create_demo_scene(&mut rec_ctx); - // Render original prior to serialization/deserialization roundtrip - let pixels = render_scene_to_buffer(&original_scene); - let img: RgbaImage = ImageBuffer::from_raw(WIDTH, HEIGHT, pixels.to_vec()).unwrap(); - img.save(Path::new(OUTPUT_DIR).join("original.png")) - .unwrap(); + // Render original prior to serialization/deserialization roundtrip. + { + let mut ctx = VelloCpuRenderContext::new(); + let mut renderer = VelloCpuImageRenderer::new(WIDTH, HEIGHT); + let mut entries: Vec<_> = rec_ctx.image_data().iter().collect(); + entries.sort_by_key(|(id, _)| id.0); + for (_id, data) in entries { + ctx.register_image(data.clone()); + } + let pixels = render_to_vec(&mut renderer, &mut ctx, &original_scene); + let img: RgbaImage = ImageBuffer::from_raw(WIDTH, HEIGHT, pixels.to_vec()).unwrap(); + img.save(Path::new(OUTPUT_DIR).join("original.png")) + .unwrap(); + } // Serialize let archive_path = Path::new(OUTPUT_DIR).join("demo_scene.anyrender.zip"); @@ -37,17 +47,21 @@ fn main() { let config = SerializeConfig::new() .with_subset_fonts(true) .with_woff2_fonts(true); - SceneArchive::from_scene(&original_scene, &config) + SceneArchive::from_scene(&rec_ctx, &original_scene, &config) .unwrap() .serialize(writer) .unwrap(); // Deserialize + let mut renderer = VelloCpuImageRenderer::new(WIDTH, HEIGHT); + let mut ctx = VelloCpuRenderContext::new(); let file = File::open(&archive_path).unwrap(); - let deserialized_scene = SceneArchive::deserialize(file).unwrap().to_scene().unwrap(); + let deserialized_scene = SceneArchive::deserialize(file) + .unwrap() + .to_scene(&mut ctx) + .unwrap(); - // Render deserialized scene to verify against original - let pixels = render_scene_to_buffer(&deserialized_scene); + let pixels = render_to_vec(&mut renderer, &mut ctx, &deserialized_scene); let img: RgbaImage = ImageBuffer::from_raw(WIDTH, HEIGHT, pixels.to_vec()).unwrap(); img.save(Path::new(OUTPUT_DIR).join("roundtrip.png")) .unwrap(); @@ -58,7 +72,7 @@ fn main() { assert_eq!(original_img.to_rgba8(), roundtrip_img.to_rgba8()); } -fn create_demo_scene() -> Scene { +fn create_demo_scene(render_ctx: &mut impl RenderContext) -> Scene { let mut scene = Scene::new(); scene.fill( @@ -169,12 +183,16 @@ fn create_demo_scene() -> Scene { // Draw with an image brush (checkerboard pattern) let checkerboard = create_checkerboard_image(8, 8); - let image_brush = ImageBrush::new(checkerboard); + let resource = render_ctx.register_image(checkerboard); + let image_brush: ImageBrush = ImageBrush { + image: resource, + sampler: Default::default(), + }; scene.fill( Fill::NonZero, Affine::translate((220.0, 180.0)) * Affine::scale(8.0), - image_brush.as_ref(), + image_brush, None, &Rect::new(0.0, 0.0, 16.0, 12.0), ); @@ -302,13 +320,19 @@ fn create_checkerboard_image(width: u32, height: u32) -> ImageData { } } -/// Render a scene to an RGBA buffer using Vello CPU. -fn render_scene_to_buffer(scene: &Scene) -> Vec { - render_to_buffer::( +/// Render a scene to an RGBA buffer using an already-configured renderer and context. +fn render_to_vec( + renderer: &mut VelloCpuImageRenderer, + ctx: &mut VelloCpuRenderContext, + scene: &Scene, +) -> Vec { + let mut buf = Vec::with_capacity((WIDTH * HEIGHT * 4) as usize); + renderer.render_to_vec( + ctx, |painter| { painter.append_scene(scene.clone(), Affine::IDENTITY); }, - WIDTH, - HEIGHT, - ) + &mut buf, + ); + buf } diff --git a/examples/winit/src/main.rs b/examples/winit/src/main.rs index b651707..2369cbc 100644 --- a/examples/winit/src/main.rs +++ b/examples/winit/src/main.rs @@ -1,8 +1,10 @@ -use anyrender::{NullWindowRenderer, PaintScene, WindowRenderer}; -use anyrender_skia::SkiaWindowRenderer; -use anyrender_vello::VelloWindowRenderer; -use anyrender_vello_cpu::{PixelsWindowRenderer, SoftbufferWindowRenderer, VelloCpuImageRenderer}; -use anyrender_vello_hybrid::VelloHybridWindowRenderer; +use anyrender::{NullRenderContext, NullWindowRenderer, PaintScene, WindowRenderer}; +use anyrender_skia::{SkiaRenderContext, SkiaWindowRenderer}; +use anyrender_vello::{VelloRenderContext, VelloWindowRenderer}; +use anyrender_vello_cpu::{ + PixelsWindowRenderer, SoftbufferWindowRenderer, VelloCpuImageRenderer, VelloCpuRenderContext, +}; +use anyrender_vello_hybrid::{VelloHybridRenderContext, VelloHybridWindowRenderer}; use kurbo::{Affine, Circle, Point, Rect, Stroke}; use peniko::{Color, Fill}; use std::sync::Arc; @@ -23,72 +25,35 @@ struct App { type VelloCpuSBWindowRenderer = SoftbufferWindowRenderer; type VelloCpuWindowRenderer = PixelsWindowRenderer; -type InitialBackend = SkiaWindowRenderer; -// type InitialBackend = VelloWindowRenderer; -// type InitialBackend = VelloHybridWindowRenderer; -// type InitialBackend = VelloCpuWindowRenderer; -// type InitialBackend = VelloCpuSBWindowRenderer; -// type InitialBackend = NullWindowRenderer; - enum Renderer { - Gpu(Box), - Hybrid(Box), - Cpu(Box), - CpuSoftbuffer(Box), - Skia(Box), - Null(NullWindowRenderer), -} -impl From for Renderer { - fn from(renderer: VelloWindowRenderer) -> Self { - Self::Gpu(Box::new(renderer)) - } -} -impl From for Renderer { - fn from(renderer: VelloHybridWindowRenderer) -> Self { - Self::Hybrid(Box::new(renderer)) - } -} -impl From for Renderer { - fn from(renderer: VelloCpuWindowRenderer) -> Self { - Self::Cpu(Box::new(renderer)) - } -} -impl From for Renderer { - fn from(renderer: VelloCpuSBWindowRenderer) -> Self { - Self::CpuSoftbuffer(Box::new(renderer)) - } -} -impl From for Renderer { - fn from(renderer: SkiaWindowRenderer) -> Self { - Self::Skia(Box::new(renderer)) - } -} -impl From for Renderer { - fn from(renderer: NullWindowRenderer) -> Self { - Self::Null(renderer) - } + Gpu(Box, VelloRenderContext), + Hybrid(Box, VelloHybridRenderContext), + Cpu(Box, VelloCpuRenderContext), + CpuSoftbuffer(Box, VelloCpuRenderContext), + Skia(Box, SkiaRenderContext), + Null(NullWindowRenderer, NullRenderContext), } impl Renderer { fn is_active(&self) -> bool { match self { - Renderer::Gpu(r) => r.is_active(), - Renderer::Hybrid(r) => r.is_active(), - Renderer::Cpu(r) => r.is_active(), - Renderer::CpuSoftbuffer(r) => r.is_active(), - Renderer::Null(r) => r.is_active(), - Renderer::Skia(r) => r.is_active(), + Renderer::Gpu(r, _) => r.is_active(), + Renderer::Hybrid(r, _) => r.is_active(), + Renderer::Cpu(r, _) => r.is_active(), + Renderer::CpuSoftbuffer(r, _) => r.is_active(), + Renderer::Null(r, _) => r.is_active(), + Renderer::Skia(r, _) => r.is_active(), } } fn set_size(&mut self, w: u32, h: u32) { match self { - Renderer::Gpu(r) => r.set_size(w, h), - Renderer::Hybrid(r) => r.set_size(w, h), - Renderer::Cpu(r) => r.set_size(w, h), - Renderer::CpuSoftbuffer(r) => r.set_size(w, h), - Renderer::Null(r) => r.set_size(w, h), - Renderer::Skia(r) => r.set_size(w, h), + Renderer::Gpu(r, _) => r.set_size(w, h), + Renderer::Hybrid(r, _) => r.set_size(w, h), + Renderer::Cpu(r, _) => r.set_size(w, h), + Renderer::CpuSoftbuffer(r, _) => r.set_size(w, h), + Renderer::Null(r, _) => r.set_size(w, h), + Renderer::Skia(r, _) => r.set_size(w, h), } } } @@ -143,10 +108,12 @@ impl App { ); } - fn set_backend>( + fn set_backend( &mut self, mut renderer: R, + ctx: R::Context, event_loop: &ActiveEventLoop, + f: impl FnOnce(R, R::Context) -> Renderer, ) { let mut window = match &self.render_state { RenderState::Active { window, .. } => Some(window.clone()), @@ -163,8 +130,10 @@ impl App { }); renderer.resume(window.clone(), self.width, self.height); - let renderer = renderer.into(); - self.render_state = RenderState::Active { window, renderer }; + self.render_state = RenderState::Active { + window, + renderer: f(renderer, ctx), + }; self.request_redraw(); } } @@ -177,7 +146,12 @@ impl ApplicationHandler for App { } fn resumed(&mut self, event_loop: &ActiveEventLoop) { - self.set_backend(InitialBackend::new(), event_loop); + self.set_backend( + SkiaWindowRenderer::new(), + SkiaRenderContext::new(), + event_loop, + |r, ctx| Renderer::Skia(Box::new(r), ctx), + ); } fn window_event( @@ -203,16 +177,24 @@ impl ApplicationHandler for App { self.request_redraw(); } WindowEvent::RedrawRequested => match renderer { - Renderer::Skia(r) => { - r.render(|p| App::draw_scene(p, Color::from_rgb8(128, 128, 128))) + Renderer::Skia(r, ctx) => { + r.render(ctx, |p| App::draw_scene(p, Color::from_rgb8(128, 128, 128))) + } + Renderer::Gpu(r, ctx) => { + r.render(ctx, |p| App::draw_scene(p, Color::from_rgb8(255, 0, 0))) + } + Renderer::Hybrid(r, ctx) => { + r.render(ctx, |p| App::draw_scene(p, Color::from_rgb8(0, 0, 0))) + } + Renderer::Cpu(r, ctx) => { + r.render(ctx, |p| App::draw_scene(p, Color::from_rgb8(0, 255, 0))) + } + Renderer::CpuSoftbuffer(r, ctx) => { + r.render(ctx, |p| App::draw_scene(p, Color::from_rgb8(0, 0, 255))) } - Renderer::Gpu(r) => r.render(|p| App::draw_scene(p, Color::from_rgb8(255, 0, 0))), - Renderer::Hybrid(r) => r.render(|p| App::draw_scene(p, Color::from_rgb8(0, 0, 0))), - Renderer::Cpu(r) => r.render(|p| App::draw_scene(p, Color::from_rgb8(0, 255, 0))), - Renderer::CpuSoftbuffer(r) => { - r.render(|p| App::draw_scene(p, Color::from_rgb8(0, 0, 255))) + Renderer::Null(r, ctx) => { + r.render(ctx, |p| App::draw_scene(p, Color::from_rgb8(0, 0, 0))) } - Renderer::Null(r) => r.render(|p| App::draw_scene(p, Color::from_rgb8(0, 0, 0))), }, WindowEvent::KeyboardInput { event: @@ -223,20 +205,45 @@ impl ApplicationHandler for App { }, .. } => match renderer { - Renderer::Cpu(_) | Renderer::CpuSoftbuffer(_) => { - self.set_backend(VelloHybridWindowRenderer::new(), event_loop); + Renderer::Cpu(..) | Renderer::CpuSoftbuffer(..) => { + self.set_backend( + VelloHybridWindowRenderer::new(), + VelloHybridRenderContext::new(), + event_loop, + |r, ctx| Renderer::Hybrid(Box::new(r), ctx), + ); } - Renderer::Hybrid(_) => { - self.set_backend(VelloWindowRenderer::new(), event_loop); + Renderer::Hybrid(..) => { + self.set_backend( + VelloWindowRenderer::new(), + VelloRenderContext::new(), + event_loop, + |r, ctx| Renderer::Gpu(Box::new(r), ctx), + ); } - Renderer::Gpu(_) => { - self.set_backend(SkiaWindowRenderer::new(), event_loop); + Renderer::Gpu(..) => { + self.set_backend( + SkiaWindowRenderer::new(), + SkiaRenderContext::new(), + event_loop, + |r, ctx| Renderer::Skia(Box::new(r), ctx), + ); } - Renderer::Skia(_) => { - self.set_backend(NullWindowRenderer::new(), event_loop); + Renderer::Skia(..) => { + self.set_backend( + NullWindowRenderer::new(), + NullRenderContext::new(), + event_loop, + |r, ctx| Renderer::Null(r, ctx), + ); } - Renderer::Null(_) => { - self.set_backend(VelloCpuWindowRenderer::new(), event_loop); + Renderer::Null(..) => { + self.set_backend( + VelloCpuWindowRenderer::new(), + VelloCpuRenderContext::new(), + event_loop, + |r, ctx| Renderer::Cpu(Box::new(r), ctx), + ); } }, _ => {} From cabbd4e26781a04d11d977656f7f68b787355b0a Mon Sep 17 00:00:00 2001 From: Taj Pereira Date: Fri, 13 Feb 2026 04:21:41 +1100 Subject: [PATCH 2/7] . --- Cargo.lock | 2 +- crates/anyrender/src/recording.rs | 4 ++-- crates/anyrender_svg/src/lib.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0432958..713a8f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,7 +172,7 @@ dependencies = [ "image", "kurbo", "peniko", - "read-fonts 0.37.0 (git+https://github.com/googlefonts/fontations?rev=41ec2bdd6dd8589b65eaaaf182b0a2e701d0d826)", + "read-fonts 0.37.0", "rustc-hash 2.1.1", "serde", "serde_json", diff --git a/crates/anyrender/src/recording.rs b/crates/anyrender/src/recording.rs index 908e9d8..815978f 100644 --- a/crates/anyrender/src/recording.rs +++ b/crates/anyrender/src/recording.rs @@ -296,10 +296,10 @@ impl PaintScene for Scene { } } -/// A simple [`RenderContext`] for use with recording scenes. +/// A [`RenderContext`] for use with recording scenes. /// /// This context assigns [`ResourceId`]s and stores the associated [`ImageData`] in a -/// `HashMap` so that recorded scenes can later be serialized or replayed through a +/// [`FxHashMap`] so that recorded scenes can later be serialized or replayed through a /// backend-specific context. pub struct RecordingRenderContext { image_data: FxHashMap, diff --git a/crates/anyrender_svg/src/lib.rs b/crates/anyrender_svg/src/lib.rs index 660c42b..439e8a9 100644 --- a/crates/anyrender_svg/src/lib.rs +++ b/crates/anyrender_svg/src/lib.rs @@ -76,7 +76,7 @@ pub fn render_svg_str_with( ctx: &mut RC, @@ -100,7 +100,7 @@ pub fn render_svg_tree( /// See the [module level documentation](crate#unsupported-features) for a list of some unsupported svg features /// /// Any raster images embedded in the SVG are registered with `ctx` and their -/// [`ImageResource`] handles are appended to `images`, so that you can +/// [`ImageResource`] handles are appended to `images`, so that the caller can /// later unregister them via [`RenderContext::unregister_resource`]. pub fn render_svg_tree_with( ctx: &mut RC, From f64c0761653fe16849676ffb9967443c3b788d4d Mon Sep 17 00:00:00 2001 From: Taj Pereira Date: Fri, 13 Feb 2026 04:32:24 +1100 Subject: [PATCH 3/7] . --- crates/anyrender_vello_hybrid/src/scene.rs | 133 +++++++++--------- .../anyrender_vello_hybrid/src/webgl_scene.rs | 47 ++++--- .../src/window_renderer.rs | 33 ++--- 3 files changed, 110 insertions(+), 103 deletions(-) diff --git a/crates/anyrender_vello_hybrid/src/scene.rs b/crates/anyrender_vello_hybrid/src/scene.rs index 277065d..1861903 100644 --- a/crates/anyrender_vello_hybrid/src/scene.rs +++ b/crates/anyrender_vello_hybrid/src/scene.rs @@ -7,7 +7,6 @@ use rustc_hash::FxHashMap; use vello_common::paint::{ImageId, ImageSource, PaintType}; use vello_hybrid::Renderer as VelloHybridRenderer; use wgpu::CommandEncoderDescriptor; -use wgpu_context::SurfaceRenderer; const DEFAULT_TOLERANCE: f64 = 0.1; @@ -15,12 +14,11 @@ const DEFAULT_TOLERANCE: f64 = 0.1; /// /// Image registration is deferred: calling [`register_image`](RenderContext::register_image) /// stores the raw [`ImageData`] in a pending queue and returns an [`ImageResource`] -/// immediately. The actual GPU upload happens transparently when the renderer's -/// [`render`](WindowRenderer::render) method is called. +/// immediately. pub struct VelloHybridRenderContext { pub(crate) resource_map: FxHashMap, next_resource_id: u64, - pending_uploads: Vec<(ResourceId, ImageData)>, + pub(crate) pending_uploads: Vec<(ResourceId, ImageData)>, } impl VelloHybridRenderContext { @@ -31,44 +29,6 @@ impl VelloHybridRenderContext { pending_uploads: Vec::new(), } } - - /// Flush any pending image uploads to the GPU. - /// - /// This must be called before rendering the scene. - pub fn flush_pending_uploads( - &mut self, - renderer: &mut VelloHybridRenderer, - render_surface: &mut SurfaceRenderer<'static>, - ) { - if self.pending_uploads.is_empty() { - return; - } - - let mut encoder = - render_surface - .device() - .create_command_encoder(&CommandEncoderDescriptor { - label: Some("Image upload"), - }); - - for (resource_id, image_data) in self.pending_uploads.drain(..) { - let ImageSource::Pixmap(pixmap) = ImageSource::from_peniko_image_data(&image_data) - else { - unreachable!(); // ImageSource::from_peniko_image_data always returns a Pixmap - }; - - let image_id = renderer.upload_image( - render_surface.device(), - render_surface.queue(), - &mut encoder, - &pixmap, - ); - - self.resource_map.insert(resource_id, image_id); - } - - render_surface.queue().submit([encoder.finish()]); - } } impl Default for VelloHybridRenderContext { @@ -96,49 +56,86 @@ impl RenderContext for VelloHybridRenderContext { } } -fn anyrender_paint_to_vello_hybrid_paint( - paint: PaintRef<'_>, - ctx: &VelloHybridRenderContext, -) -> PaintType { - match paint { - Paint::Solid(alpha_color) => PaintType::Solid(alpha_color), - Paint::Gradient(gradient) => PaintType::Gradient(gradient.clone()), - - Paint::Image(image_brush) => { - let image_id = ctx.resource_map[&image_brush.image.id]; - PaintType::Image(ImageBrush { - image: ImageSource::OpaqueId(image_id), - sampler: image_brush.sampler, - }) - } - - // TODO: custom paint - Paint::Custom(_) => PaintType::Solid(peniko::color::palette::css::TRANSPARENT), - } -} - pub(crate) enum LayerKind { Layer, Clip, } pub struct VelloHybridScenePainter<'s> { - pub(crate) ctx: &'s VelloHybridRenderContext, + pub(crate) ctx: &'s mut VelloHybridRenderContext, + pub(crate) renderer: &'s mut VelloHybridRenderer, + pub(crate) device: &'s wgpu::Device, + pub(crate) queue: &'s wgpu::Queue, pub(crate) scene: &'s mut vello_hybrid::Scene, pub(crate) layer_stack: Vec, } impl VelloHybridScenePainter<'_> { pub fn new<'s>( - ctx: &'s VelloHybridRenderContext, + ctx: &'s mut VelloHybridRenderContext, + renderer: &'s mut VelloHybridRenderer, + device: &'s wgpu::Device, + queue: &'s wgpu::Queue, scene: &'s mut vello_hybrid::Scene, ) -> VelloHybridScenePainter<'s> { VelloHybridScenePainter { ctx, + renderer, + device, + queue, scene, layer_stack: Vec::with_capacity(16), } } + + /// Upload any images that have been registered with the context but not yet + /// sent to the GPU. This is called automatically before resolving image paints. + fn flush_pending_uploads(&mut self) { + if self.ctx.pending_uploads.is_empty() { + return; + } + + let mut encoder = self + .device + .create_command_encoder(&CommandEncoderDescriptor { + label: Some("Image upload"), + }); + + for (resource_id, image_data) in self.ctx.pending_uploads.drain(..) { + let ImageSource::Pixmap(pixmap) = ImageSource::from_peniko_image_data(&image_data) + else { + unreachable!(); // ImageSource::from_peniko_image_data always returns a Pixmap + }; + + let image_id = + self.renderer + .upload_image(self.device, self.queue, &mut encoder, &pixmap); + + self.ctx.resource_map.insert(resource_id, image_id); + } + + self.queue.submit([encoder.finish()]); + } + + /// Convert a [`PaintRef`] to a [`PaintType`], flushing pending image uploads if needed. + 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.flush_pending_uploads(); + let image_id = self.ctx.resource_map[&image_brush.image.id]; + PaintType::Image(ImageBrush { + image: ImageSource::OpaqueId(image_id), + sampler: image_brush.sampler, + }) + } + + // TODO: custom paint + Paint::Custom(_) => PaintType::Solid(peniko::color::palette::css::TRANSPARENT), + } + } } impl PaintScene for VelloHybridScenePainter<'_> { @@ -190,7 +187,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(), self.ctx); + let paint = self.convert_paint(paint.into()); self.scene.set_paint(paint); self.scene .set_paint_transform(brush_transform.unwrap_or(Affine::IDENTITY)); @@ -207,7 +204,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(), self.ctx); + let paint = self.convert_paint(paint.into()); self.scene.set_paint(paint); self.scene .set_paint_transform(brush_transform.unwrap_or(Affine::IDENTITY)); @@ -227,7 +224,7 @@ impl PaintScene for VelloHybridScenePainter<'_> { glyph_transform: Option, glyphs: impl Iterator, ) { - let paint = anyrender_paint_to_vello_hybrid_paint(paint.into(), self.ctx); + let paint = self.convert_paint(paint.into()); self.scene.set_paint(paint); self.scene.set_transform(transform); diff --git a/crates/anyrender_vello_hybrid/src/webgl_scene.rs b/crates/anyrender_vello_hybrid/src/webgl_scene.rs index 53967b9..e969818 100644 --- a/crates/anyrender_vello_hybrid/src/webgl_scene.rs +++ b/crates/anyrender_vello_hybrid/src/webgl_scene.rs @@ -24,22 +24,6 @@ impl WebGlRenderContext { pending_uploads: Vec::new(), } } - - /// Flush any pending image uploads to the WebGL renderer. - /// - /// Must be called before creating a [`WebGlScenePainter`] if images have been - /// registered since the last flush. - pub fn flush_pending_uploads(&mut self, renderer: &mut vello_hybrid::WebGlRenderer) { - for (resource_id, image_data) in self.pending_uploads.drain(..) { - let ImageSource::Pixmap(pixmap) = ImageSource::from_peniko_image_data(&image_data) - else { - unreachable!(); - }; - - let image_id = renderer.upload_image(&pixmap); - self.resource_map.insert(resource_id, image_id); - } - } } impl Default for WebGlRenderContext { @@ -73,15 +57,21 @@ enum LayerKind { } pub struct WebGlScenePainter<'s> { - ctx: &'s WebGlRenderContext, + ctx: &'s mut WebGlRenderContext, + renderer: &'s mut vello_hybrid::WebGlRenderer, scene: &'s mut vello_hybrid::Scene, layer_stack: Vec, } impl<'s> WebGlScenePainter<'s> { - pub fn new(ctx: &'s WebGlRenderContext, scene: &'s mut vello_hybrid::Scene) -> Self { + pub fn new( + ctx: &'s mut WebGlRenderContext, + renderer: &'s mut vello_hybrid::WebGlRenderer, + scene: &'s mut vello_hybrid::Scene, + ) -> Self { Self { ctx, + renderer, scene, layer_stack: Vec::with_capacity(16), } @@ -89,11 +79,30 @@ impl<'s> WebGlScenePainter<'s> { } impl WebGlScenePainter<'_> { - fn convert_paint(&self, paint: PaintRef<'_>) -> PaintType { + /// Upload any images that have been registered with the context but not yet + /// sent to the WebGL renderer. Called automatically before resolving image paints. + fn flush_pending_uploads(&mut self) { + if self.ctx.pending_uploads.is_empty() { + return; + } + + for (resource_id, image_data) in self.ctx.pending_uploads.drain(..) { + let ImageSource::Pixmap(pixmap) = ImageSource::from_peniko_image_data(&image_data) + else { + unreachable!(); + }; + + let image_id = self.renderer.upload_image(&pixmap); + self.ctx.resource_map.insert(resource_id, image_id); + } + } + + 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.flush_pending_uploads(); let image_id = self.ctx.resource_map[&image_brush.image.id]; PaintType::Image(ImageBrush { image: ImageSource::OpaqueId(image_id), diff --git a/crates/anyrender_vello_hybrid/src/window_renderer.rs b/crates/anyrender_vello_hybrid/src/window_renderer.rs index aff68c2..ead0824 100644 --- a/crates/anyrender_vello_hybrid/src/window_renderer.rs +++ b/crates/anyrender_vello_hybrid/src/window_renderer.rs @@ -1,9 +1,6 @@ -use anyrender::{ImageResource, RenderContext, ResourceId, WindowHandle, WindowRenderer}; +use anyrender::{WindowHandle, WindowRenderer}; use debug_timer::debug_timer; -use peniko::ImageData; -use rustc_hash::FxHashMap; use std::sync::Arc; -use vello_common::paint::{ImageId, ImageSource}; use vello_hybrid::{ RenderSettings, RenderSize, RenderTargetConfig, Renderer as VelloHybridRenderer, Scene as VelloHybridScene, @@ -164,12 +161,24 @@ impl WindowRenderer for VelloHybridWindowRenderer { return; }; - // Flush any pending image uploads before drawing - ctx.flush_pending_uploads(&mut state.renderer, &mut state.render_surface); + debug_timer!(timer, feature = "log_frame_times"); - let render_surface = &mut state.render_surface; + // Regenerate the vello scene. + { + let device = state.render_surface.device(); + let queue = state.render_surface.queue(); + draw_fn(&mut VelloHybridScenePainter { + ctx, + renderer: &mut state.renderer, + device, + queue, + scene: &mut self.scene, + layer_stack: Vec::new(), + }); + } + timer.record_time("cmd"); - debug_timer!(timer, feature = "log_frame_times"); + let render_surface = &mut state.render_surface; let mut encoder = render_surface @@ -178,14 +187,6 @@ impl WindowRenderer for VelloHybridWindowRenderer { label: Some("Render scene"), }); - // Regenerate the vello scene - draw_fn(&mut VelloHybridScenePainter { - ctx, - scene: &mut self.scene, - layer_stack: Vec::new(), - }); - timer.record_time("cmd"); - let texture_view = render_surface.target_texture_view(); state From bc1a3f34534426f086d27a6b03c6888893880985 Mon Sep 17 00:00:00 2001 From: Taj Pereira Date: Fri, 13 Feb 2026 04:44:59 +1100 Subject: [PATCH 4/7] . --- crates/anyrender/src/lib.rs | 6 +++--- crates/anyrender/src/null_backend.rs | 4 ++-- crates/anyrender/src/recording.rs | 2 +- crates/anyrender/src/types.rs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/anyrender/src/lib.rs b/crates/anyrender/src/lib.rs index 3b193b5..af52960 100644 --- a/crates/anyrender/src/lib.rs +++ b/crates/anyrender/src/lib.rs @@ -187,7 +187,7 @@ pub trait PaintScene { match &cmd.brush { Brush::Solid(alpha_color) => Paint::Solid(*alpha_color), Brush::Gradient(gradient) => Paint::Gradient(gradient), - Brush::Image(image) => Paint::Image(image.clone()), + Brush::Image(image) => Paint::Image(*image), }, cmd.brush_transform, &cmd.shape, @@ -198,7 +198,7 @@ pub trait PaintScene { match &cmd.brush { Brush::Solid(alpha_color) => Paint::Solid(*alpha_color), Brush::Gradient(gradient) => Paint::Gradient(gradient), - Brush::Image(image) => Paint::Image(image.clone()), + Brush::Image(image) => Paint::Image(*image), }, cmd.brush_transform, &cmd.shape, @@ -212,7 +212,7 @@ pub trait PaintScene { match &cmd.brush { Brush::Solid(alpha_color) => Paint::Solid(*alpha_color), Brush::Gradient(gradient) => Paint::Gradient(gradient), - Brush::Image(image) => Paint::Image(image.clone()), + Brush::Image(image) => Paint::Image(*image), }, cmd.brush_alpha, scene_transform * cmd.transform, diff --git a/crates/anyrender/src/null_backend.rs b/crates/anyrender/src/null_backend.rs index a836dcf..b73dc78 100644 --- a/crates/anyrender/src/null_backend.rs +++ b/crates/anyrender/src/null_backend.rs @@ -72,7 +72,7 @@ pub struct NullImageRenderer; impl NullImageRenderer { pub fn new() -> Self { - Self::default() + Self } } @@ -84,7 +84,7 @@ impl ImageRenderer for NullImageRenderer { type Context = NullRenderContext; fn new(_width: u32, _height: u32) -> Self { - Self::default() + Self } fn resize(&mut self, _width: u32, _height: u32) {} diff --git a/crates/anyrender/src/recording.rs b/crates/anyrender/src/recording.rs index 815978f..dc6261e 100644 --- a/crates/anyrender/src/recording.rs +++ b/crates/anyrender/src/recording.rs @@ -159,7 +159,7 @@ impl Scene { match paint_ref { Paint::Solid(color) => Brush::Solid(color), Paint::Gradient(gradient) => Brush::Gradient(gradient.clone()), - Paint::Image(image) => Brush::Image(image.clone()), + Paint::Image(image) => Brush::Image(image), // TODO: handle this somehow Paint::Custom(_) => Brush::Solid(Color::TRANSPARENT), } diff --git a/crates/anyrender/src/types.rs b/crates/anyrender/src/types.rs index 8855f47..1f0108c 100644 --- a/crates/anyrender/src/types.rs +++ b/crates/anyrender/src/types.rs @@ -75,7 +75,7 @@ impl Paint { match self { Paint::Solid(color) => Paint::Solid(*color), Paint::Gradient(gradient) => Paint::Gradient(gradient), - Paint::Image(image) => Paint::Image(image.clone()), + Paint::Image(image) => Paint::Image(*image), // Custom paints are translated into "invisible" where they are not supported Paint::Custom(custom) => Paint::Custom(custom.as_ref()), From 2658c7f28062694770876853b0f64651a0fbb714 Mon Sep 17 00:00:00 2001 From: Taj Pereira Date: Fri, 13 Feb 2026 04:51:12 +1100 Subject: [PATCH 5/7] . --- examples/bunnymark/src/bunny.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/bunnymark/src/bunny.rs b/examples/bunnymark/src/bunny.rs index fb4c2c0..385971d 100644 --- a/examples/bunnymark/src/bunny.rs +++ b/examples/bunnymark/src/bunny.rs @@ -131,7 +131,7 @@ impl BunnyManager { for bunny in &self.bunnies { let pos = bunny.position(); scene.draw_image( - bunny_image.clone(), + *bunny_image, Affine::translate(pos).then_scale(scale_factor), ); } From 80338d11ad6ae5f1a42b3e188b310cc52783f418 Mon Sep 17 00:00:00 2001 From: Taj Pereira Date: Fri, 13 Feb 2026 04:58:36 +1100 Subject: [PATCH 6/7] . --- examples/winit/src/main.rs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/examples/winit/src/main.rs b/examples/winit/src/main.rs index 2369cbc..bfc2ec8 100644 --- a/examples/winit/src/main.rs +++ b/examples/winit/src/main.rs @@ -1,9 +1,7 @@ use anyrender::{NullRenderContext, NullWindowRenderer, PaintScene, WindowRenderer}; use anyrender_skia::{SkiaRenderContext, SkiaWindowRenderer}; use anyrender_vello::{VelloRenderContext, VelloWindowRenderer}; -use anyrender_vello_cpu::{ - PixelsWindowRenderer, SoftbufferWindowRenderer, VelloCpuImageRenderer, VelloCpuRenderContext, -}; +use anyrender_vello_cpu::{PixelsWindowRenderer, VelloCpuImageRenderer, VelloCpuRenderContext}; use anyrender_vello_hybrid::{VelloHybridRenderContext, VelloHybridWindowRenderer}; use kurbo::{Affine, Circle, Point, Rect, Stroke}; use peniko::{Color, Fill}; @@ -22,14 +20,12 @@ struct App { height: u32, } -type VelloCpuSBWindowRenderer = SoftbufferWindowRenderer; type VelloCpuWindowRenderer = PixelsWindowRenderer; enum Renderer { Gpu(Box, VelloRenderContext), Hybrid(Box, VelloHybridRenderContext), Cpu(Box, VelloCpuRenderContext), - CpuSoftbuffer(Box, VelloCpuRenderContext), Skia(Box, SkiaRenderContext), Null(NullWindowRenderer, NullRenderContext), } @@ -40,7 +36,6 @@ impl Renderer { Renderer::Gpu(r, _) => r.is_active(), Renderer::Hybrid(r, _) => r.is_active(), Renderer::Cpu(r, _) => r.is_active(), - Renderer::CpuSoftbuffer(r, _) => r.is_active(), Renderer::Null(r, _) => r.is_active(), Renderer::Skia(r, _) => r.is_active(), } @@ -51,7 +46,6 @@ impl Renderer { Renderer::Gpu(r, _) => r.set_size(w, h), Renderer::Hybrid(r, _) => r.set_size(w, h), Renderer::Cpu(r, _) => r.set_size(w, h), - Renderer::CpuSoftbuffer(r, _) => r.set_size(w, h), Renderer::Null(r, _) => r.set_size(w, h), Renderer::Skia(r, _) => r.set_size(w, h), } @@ -189,9 +183,6 @@ impl ApplicationHandler for App { Renderer::Cpu(r, ctx) => { r.render(ctx, |p| App::draw_scene(p, Color::from_rgb8(0, 255, 0))) } - Renderer::CpuSoftbuffer(r, ctx) => { - r.render(ctx, |p| App::draw_scene(p, Color::from_rgb8(0, 0, 255))) - } Renderer::Null(r, ctx) => { r.render(ctx, |p| App::draw_scene(p, Color::from_rgb8(0, 0, 0))) } @@ -205,7 +196,7 @@ impl ApplicationHandler for App { }, .. } => match renderer { - Renderer::Cpu(..) | Renderer::CpuSoftbuffer(..) => { + Renderer::Cpu(..) => { self.set_backend( VelloHybridWindowRenderer::new(), VelloHybridRenderContext::new(), @@ -234,7 +225,7 @@ impl ApplicationHandler for App { NullWindowRenderer::new(), NullRenderContext::new(), event_loop, - |r, ctx| Renderer::Null(r, ctx), + Renderer::Null, ); } Renderer::Null(..) => { From bbfa9e1e42d99f6d21be8e0d8da69f646cad46d8 Mon Sep 17 00:00:00 2001 From: Taj Pereira Date: Sat, 21 Feb 2026 07:56:15 +1030 Subject: [PATCH 7/7] Comments --- crates/anyrender/src/lib.rs | 17 +--- crates/anyrender/src/null_backend.rs | 48 +++++------ crates/anyrender/src/types.rs | 3 +- crates/anyrender_skia/src/image_renderer.rs | 28 ++++--- crates/anyrender_skia/src/window_renderer.rs | 24 ++++-- crates/anyrender_vello/src/image_renderer.rs | 22 +++-- crates/anyrender_vello/src/window_renderer.rs | 28 ++++--- .../anyrender_vello_cpu/src/image_renderer.rs | 28 ++++--- .../src/window_renderer.rs | 24 ++++-- crates/pixels_window_renderer/src/lib.rs | 21 +++-- crates/softbuffer_window_renderer/src/lib.rs | 21 +++-- examples/bunnymark/src/main.rs | 73 ++++++++--------- examples/serialize/src/main.rs | 21 ++--- examples/winit/src/main.rs | 81 +++++++++---------- 14 files changed, 229 insertions(+), 210 deletions(-) diff --git a/crates/anyrender/src/lib.rs b/crates/anyrender/src/lib.rs index af52960..43ea4d5 100644 --- a/crates/anyrender/src/lib.rs +++ b/crates/anyrender/src/lib.rs @@ -42,40 +42,32 @@ pub mod recording; pub use recording::{RecordingRenderContext, Scene}; /// Abstraction for rendering a scene to a window -pub trait WindowRenderer { +pub trait WindowRenderer: RenderContext { type ScenePainter<'a>: PaintScene where Self: 'a; - type Context: RenderContext; fn resume(&mut self, window: Arc, width: u32, height: u32); fn suspend(&mut self); fn is_active(&self) -> bool; fn set_size(&mut self, width: u32, height: u32); - fn render)>( - &mut self, - ctx: &mut Self::Context, - draw_fn: F, - ); + fn render)>(&mut self, draw_fn: F); } /// Abstraction for rendering a scene to an image buffer -pub trait ImageRenderer { +pub trait ImageRenderer: RenderContext { type ScenePainter<'a>: PaintScene where Self: 'a; - type Context: RenderContext; fn new(width: u32, height: u32) -> Self; fn resize(&mut self, width: u32, height: u32); fn reset(&mut self); fn render_to_vec)>( &mut self, - ctx: &mut Self::Context, draw_fn: F, vec: &mut Vec, ); fn render)>( &mut self, - ctx: &mut Self::Context, draw_fn: F, buffer: &mut [u8], ); @@ -83,14 +75,13 @@ pub trait ImageRenderer { /// Draw a scene to a buffer using an `ImageRenderer` pub fn render_to_buffer)>( - ctx: &mut R::Context, draw_fn: F, width: u32, height: u32, ) -> Vec { let mut buf = Vec::with_capacity((width * height * 4) as usize); let mut renderer = R::new(width, height); - renderer.render_to_vec(ctx, draw_fn, &mut buf); + renderer.render_to_vec(draw_fn, &mut buf); buf } diff --git a/crates/anyrender/src/null_backend.rs b/crates/anyrender/src/null_backend.rs index b73dc78..3953ad8 100644 --- a/crates/anyrender/src/null_backend.rs +++ b/crates/anyrender/src/null_backend.rs @@ -1,4 +1,4 @@ -//! A dummy implementation of the AnyRender traits while simply ignores all commands +//! A dummy implementation of the AnyRender traits that simply ignores all commands use crate::{ ImageRenderer, ImageResource, PaintScene, RenderContext, ResourceId, WindowHandle, @@ -6,16 +6,18 @@ use crate::{ }; use std::sync::Arc; -#[derive(Default)] -pub struct NullRenderContext {} +#[derive(Copy, Clone, Default)] +pub struct NullWindowRenderer { + is_active: bool, +} -impl NullRenderContext { +impl NullWindowRenderer { pub fn new() -> Self { - Self {} + Self::default() } } -impl RenderContext for NullRenderContext { +impl RenderContext for NullWindowRenderer { fn register_image(&mut self, image: peniko::ImageData) -> ImageResource { ImageResource { id: ResourceId(0), @@ -27,23 +29,11 @@ impl RenderContext for NullRenderContext { fn unregister_resource(&mut self, _id: ResourceId) {} } -#[derive(Copy, Clone, Default)] -pub struct NullWindowRenderer { - is_active: bool, -} - -impl NullWindowRenderer { - pub fn new() -> Self { - Self::default() - } -} - impl WindowRenderer for NullWindowRenderer { type ScenePainter<'a> = NullScenePainter where Self: 'a; - type Context = NullRenderContext; fn resume(&mut self, _window: Arc, _width: u32, _height: u32) { self.is_active = true; @@ -59,12 +49,7 @@ impl WindowRenderer for NullWindowRenderer { fn set_size(&mut self, _width: u32, _height: u32) {} - fn render)>( - &mut self, - _ctx: &mut Self::Context, - _draw_fn: F, - ) { - } + fn render)>(&mut self, _draw_fn: F) {} } #[derive(Clone, Default)] @@ -76,12 +61,23 @@ impl NullImageRenderer { } } +impl RenderContext for NullImageRenderer { + fn register_image(&mut self, image: peniko::ImageData) -> ImageResource { + ImageResource { + id: ResourceId(0), + width: image.width, + height: image.height, + } + } + + fn unregister_resource(&mut self, _id: ResourceId) {} +} + impl ImageRenderer for NullImageRenderer { type ScenePainter<'a> = NullScenePainter where Self: 'a; - type Context = NullRenderContext; fn new(_width: u32, _height: u32) -> Self { Self @@ -93,7 +89,6 @@ impl ImageRenderer for NullImageRenderer { fn render_to_vec)>( &mut self, - _ctx: &mut Self::Context, _draw_fn: F, _vec: &mut Vec, ) { @@ -101,7 +96,6 @@ impl ImageRenderer for NullImageRenderer { fn render)>( &mut self, - _ctx: &mut Self::Context, _draw_fn: F, _buffer: &mut [u8], ) { diff --git a/crates/anyrender/src/types.rs b/crates/anyrender/src/types.rs index 1f0108c..d76dd27 100644 --- a/crates/anyrender/src/types.rs +++ b/crates/anyrender/src/types.rs @@ -1,6 +1,7 @@ //! Types that are used within the Anyrender traits -use peniko::{Color, Gradient, ImageBrush, ImageData}; +pub use peniko::ImageData; +use peniko::{Color, Gradient, ImageBrush}; use std::{any::Any, sync::Arc}; #[cfg(feature = "serde")] diff --git a/crates/anyrender_skia/src/image_renderer.rs b/crates/anyrender_skia/src/image_renderer.rs index 0a0d0a6..7aa136d 100644 --- a/crates/anyrender_skia/src/image_renderer.rs +++ b/crates/anyrender_skia/src/image_renderer.rs @@ -1,21 +1,32 @@ -use anyrender::ImageRenderer; +use anyrender::{ImageRenderer, ImageResource, RenderContext, ResourceId}; use debug_timer::debug_timer; +use peniko::ImageData; use skia_safe::{AlphaType, Color, ColorType, ImageInfo, SurfaceProps, graphics, surfaces}; use crate::{SkiaRenderContext, SkiaScenePainter, scene::SkiaSceneCache}; pub struct SkiaImageRenderer { + ctx: SkiaRenderContext, image_info: ImageInfo, surface_props: SurfaceProps, scene_cache: SkiaSceneCache, } +impl RenderContext for SkiaImageRenderer { + fn register_image(&mut self, image: ImageData) -> ImageResource { + self.ctx.register_image(image) + } + + fn unregister_resource(&mut self, id: ResourceId) { + self.ctx.unregister_resource(id) + } +} + impl ImageRenderer for SkiaImageRenderer { type ScenePainter<'a> = SkiaScenePainter<'a> where Self: 'a; - type Context = SkiaRenderContext; fn new(width: u32, height: u32) -> Self { graphics::set_font_cache_count_limit(100); @@ -23,6 +34,7 @@ impl ImageRenderer for SkiaImageRenderer { graphics::set_resource_cache_total_bytes_limit(10485760); Self { + ctx: SkiaRenderContext::new(), image_info: ImageInfo::new( (width as i32, height as i32), ColorType::RGBA8888, @@ -47,7 +59,6 @@ impl ImageRenderer for SkiaImageRenderer { fn render_to_vec)>( &mut self, - ctx: &mut Self::Context, draw_fn: F, buffer: &mut Vec, ) { @@ -64,7 +75,7 @@ impl ImageRenderer for SkiaImageRenderer { surface.canvas().clear(Color::WHITE); draw_fn(&mut SkiaScenePainter { - ctx, + ctx: &self.ctx, inner: surface.canvas(), cache: &mut self.scene_cache, }); @@ -76,12 +87,7 @@ impl ImageRenderer for SkiaImageRenderer { timer.print_times("skia_raster: "); } - fn render)>( - &mut self, - ctx: &mut Self::Context, - draw_fn: F, - buffer: &mut [u8], - ) { + fn render)>(&mut self, draw_fn: F, buffer: &mut [u8]) { debug_timer!(timer, feature = "log_frame_times"); let mut surface = surfaces::wrap_pixels( @@ -95,7 +101,7 @@ impl ImageRenderer for SkiaImageRenderer { surface.canvas().clear(Color::WHITE); draw_fn(&mut SkiaScenePainter { - ctx, + ctx: &self.ctx, inner: surface.canvas(), cache: &mut self.scene_cache, }); diff --git a/crates/anyrender_skia/src/window_renderer.rs b/crates/anyrender_skia/src/window_renderer.rs index 5eb3d2e..a7e5bb0 100644 --- a/crates/anyrender_skia/src/window_renderer.rs +++ b/crates/anyrender_skia/src/window_renderer.rs @@ -1,5 +1,6 @@ -use anyrender::WindowRenderer; +use anyrender::{ImageResource, RenderContext, ResourceId, WindowRenderer}; use debug_timer::debug_timer; +use peniko::ImageData; use skia_safe::{Color, Surface, graphics}; use std::sync::Arc; @@ -24,6 +25,7 @@ struct ActiveRenderState { } pub struct SkiaWindowRenderer { + ctx: SkiaRenderContext, render_state: RenderState, } @@ -36,17 +38,27 @@ impl Default for SkiaWindowRenderer { impl SkiaWindowRenderer { pub fn new() -> Self { Self { + ctx: SkiaRenderContext::new(), render_state: RenderState::Suspended, } } } +impl RenderContext for SkiaWindowRenderer { + fn register_image(&mut self, image: ImageData) -> ImageResource { + self.ctx.register_image(image) + } + + fn unregister_resource(&mut self, id: ResourceId) { + self.ctx.unregister_resource(id) + } +} + impl WindowRenderer for SkiaWindowRenderer { type ScenePainter<'a> = SkiaScenePainter<'a> where Self: 'a; - type Context = SkiaRenderContext; fn resume(&mut self, window: Arc, width: u32, height: u32) { graphics::set_font_cache_count_limit(100); @@ -78,11 +90,7 @@ impl WindowRenderer for SkiaWindowRenderer { } } - fn render)>( - &mut self, - ctx: &mut Self::Context, - draw_fn: F, - ) { + fn render)>(&mut self, draw_fn: F) { let RenderState::Active(state) = &mut self.render_state else { return; }; @@ -98,7 +106,7 @@ impl WindowRenderer for SkiaWindowRenderer { surface.canvas().clear(Color::WHITE); draw_fn(&mut SkiaScenePainter { - ctx, + ctx: &self.ctx, inner: surface.canvas(), cache: &mut state.scene_cache, }); diff --git a/crates/anyrender_vello/src/image_renderer.rs b/crates/anyrender_vello/src/image_renderer.rs index b406285..0b80d80 100644 --- a/crates/anyrender_vello/src/image_renderer.rs +++ b/crates/anyrender_vello/src/image_renderer.rs @@ -1,4 +1,5 @@ -use anyrender::ImageRenderer; +use anyrender::{ImageRenderer, ImageResource, RenderContext, ResourceId}; +use peniko::ImageData; use rustc_hash::FxHashMap; use vello::{Renderer as VelloRenderer, RendererOptions, Scene as VelloScene}; use wgpu::TextureUsages; @@ -7,17 +8,27 @@ use wgpu_context::{BufferRenderer, BufferRendererConfig, WGPUContext}; use crate::{DEFAULT_THREADS, VelloRenderContext, VelloScenePainter}; pub struct VelloImageRenderer { + ctx: VelloRenderContext, buffer_renderer: BufferRenderer, vello_renderer: VelloRenderer, scene: VelloScene, } +impl RenderContext for VelloImageRenderer { + fn register_image(&mut self, image: ImageData) -> ImageResource { + self.ctx.register_image(image) + } + + fn unregister_resource(&mut self, id: ResourceId) { + self.ctx.unregister_resource(id) + } +} + impl ImageRenderer for VelloImageRenderer { type ScenePainter<'a> = VelloScenePainter<'a, 'a> where Self: 'a; - type Context = VelloRenderContext; fn new(width: u32, height: u32) -> Self { // Create WGPUContext @@ -45,6 +56,7 @@ impl ImageRenderer for VelloImageRenderer { .expect("Got non-Send/Sync error from creating renderer"); Self { + ctx: VelloRenderContext::new(), buffer_renderer, vello_renderer, scene: VelloScene::new(), @@ -61,23 +73,21 @@ impl ImageRenderer for VelloImageRenderer { fn render_to_vec)>( &mut self, - ctx: &mut Self::Context, draw_fn: F, cpu_buffer: &mut Vec, ) { let size = self.buffer_renderer.size(); cpu_buffer.resize((size.width * size.height * 4) as usize, 0); - self.render(ctx, draw_fn, cpu_buffer); + self.render(draw_fn, cpu_buffer); } fn render)>( &mut self, - ctx: &mut Self::Context, draw_fn: F, cpu_buffer: &mut [u8], ) { draw_fn(&mut VelloScenePainter { - ctx, + ctx: &self.ctx, inner: &mut self.scene, renderer: Some(&mut self.vello_renderer), custom_paint_sources: Some(&mut FxHashMap::default()), diff --git a/crates/anyrender_vello/src/window_renderer.rs b/crates/anyrender_vello/src/window_renderer.rs index e5717e1..34e935c 100644 --- a/crates/anyrender_vello/src/window_renderer.rs +++ b/crates/anyrender_vello/src/window_renderer.rs @@ -1,6 +1,6 @@ -use anyrender::{WindowHandle, WindowRenderer}; +use anyrender::{ImageResource, RenderContext, ResourceId, WindowHandle, WindowRenderer}; use debug_timer::debug_timer; -use peniko::Color; +use peniko::{Color, ImageData}; use rustc_hash::FxHashMap; use std::sync::{ Arc, @@ -60,6 +60,7 @@ impl Default for VelloRendererOptions { } pub struct VelloWindowRenderer { + ctx: VelloRenderContext, // The fields MUST be in this order, so that the surface is dropped before the window // Window is cached even when suspended so that it can be reused when the app is resumed after being suspended render_state: RenderState, @@ -83,6 +84,7 @@ impl VelloWindowRenderer { | Features::CLEAR_TEXTURE | Features::PIPELINE_CACHE; Self { + ctx: VelloRenderContext::new(), wgpu_context: WGPUContext::with_features_and_limits( Some(features), config.limits.clone(), @@ -117,12 +119,21 @@ impl VelloWindowRenderer { } } +impl RenderContext for VelloWindowRenderer { + fn register_image(&mut self, image: ImageData) -> ImageResource { + self.ctx.register_image(image) + } + + fn unregister_resource(&mut self, id: ResourceId) { + self.ctx.unregister_resource(id) + } +} + impl WindowRenderer for VelloWindowRenderer { type ScenePainter<'a> = VelloScenePainter<'a, 'a> where Self: 'a; - type Context = VelloRenderContext; fn is_active(&self) -> bool { matches!(self.render_state, RenderState::Active(_)) @@ -191,28 +202,23 @@ impl WindowRenderer for VelloWindowRenderer { }; } - fn render)>( - &mut self, - ctx: &mut Self::Context, - draw_fn: F, - ) { + fn render)>(&mut self, draw_fn: F) { let RenderState::Active(state) = &mut self.render_state else { return; }; - let render_surface = &mut state.render_surface; - debug_timer!(timer, feature = "log_frame_times"); // Regenerate the vello scene draw_fn(&mut VelloScenePainter { - ctx, + ctx: &self.ctx, inner: &mut self.scene, renderer: Some(&mut state.renderer), custom_paint_sources: Some(&mut self.custom_paint_sources), }); timer.record_time("cmd"); + let render_surface = &mut state.render_surface; let texture_view = render_surface.target_texture_view(); state .renderer diff --git a/crates/anyrender_vello_cpu/src/image_renderer.rs b/crates/anyrender_vello_cpu/src/image_renderer.rs index a5d1d24..23c5326 100644 --- a/crates/anyrender_vello_cpu/src/image_renderer.rs +++ b/crates/anyrender_vello_cpu/src/image_renderer.rs @@ -1,18 +1,30 @@ use crate::{VelloCpuRenderContext, VelloCpuScenePainter}; -use anyrender::ImageRenderer; +use anyrender::{ImageRenderer, ImageResource, RenderContext, ResourceId}; use debug_timer::debug_timer; +use peniko::ImageData; use vello_cpu::{RenderContext as VelloCpuRenderCtx, RenderMode}; pub struct VelloCpuImageRenderer { + ctx: VelloCpuRenderContext, render_ctx: VelloCpuRenderCtx, } +impl RenderContext for VelloCpuImageRenderer { + fn register_image(&mut self, image: ImageData) -> ImageResource { + self.ctx.register_image(image) + } + + fn unregister_resource(&mut self, id: ResourceId) { + self.ctx.unregister_resource(id) + } +} + impl ImageRenderer for VelloCpuImageRenderer { type ScenePainter<'a> = VelloCpuScenePainter<'a>; - type Context = VelloCpuRenderContext; fn new(width: u32, height: u32) -> Self { Self { + ctx: VelloCpuRenderContext::new(), render_ctx: VelloCpuRenderCtx::new(width as u16, height as u16), } } @@ -25,16 +37,11 @@ impl ImageRenderer for VelloCpuImageRenderer { self.render_ctx.reset(); } - fn render)>( - &mut self, - ctx: &mut Self::Context, - draw_fn: F, - buffer: &mut [u8], - ) { + fn render)>(&mut self, draw_fn: F, buffer: &mut [u8]) { debug_timer!(timer, feature = "log_frame_times"); { - let mut scene = VelloCpuScenePainter::new(ctx, &mut self.render_ctx); + let mut scene = VelloCpuScenePainter::new(&self.ctx, &mut self.render_ctx); draw_fn(&mut scene); } timer.record_time("cmds"); @@ -55,13 +62,12 @@ impl ImageRenderer for VelloCpuImageRenderer { fn render_to_vec)>( &mut self, - ctx: &mut Self::Context, draw_fn: F, buffer: &mut Vec, ) { let width = self.render_ctx.width(); let height = self.render_ctx.height(); buffer.resize(width as usize * height as usize * 4, 0); - self.render(ctx, draw_fn, buffer); + self.render(draw_fn, buffer); } } diff --git a/crates/anyrender_vello_hybrid/src/window_renderer.rs b/crates/anyrender_vello_hybrid/src/window_renderer.rs index ead0824..4a284e6 100644 --- a/crates/anyrender_vello_hybrid/src/window_renderer.rs +++ b/crates/anyrender_vello_hybrid/src/window_renderer.rs @@ -1,5 +1,6 @@ -use anyrender::{WindowHandle, WindowRenderer}; +use anyrender::{ImageResource, RenderContext, ResourceId, WindowHandle, WindowRenderer}; use debug_timer::debug_timer; +use peniko::ImageData; use std::sync::Arc; use vello_hybrid::{ RenderSettings, RenderSize, RenderTargetConfig, Renderer as VelloHybridRenderer, @@ -39,6 +40,7 @@ pub struct VelloHybridRendererOptions { } pub struct VelloHybridWindowRenderer { + ctx: VelloHybridRenderContext, // The fields MUST be in this order, so that the surface is dropped before the window // Window is cached even when suspended so that it can be reused when the app is resumed after being suspended render_state: RenderState, @@ -61,6 +63,7 @@ impl VelloHybridWindowRenderer { | Features::PIPELINE_CACHE; let render_settings = config.render_settings; Self { + ctx: VelloHybridRenderContext::new(), wgpu_context: WGPUContext::with_features_and_limits( Some(features), config.limits.clone(), @@ -83,12 +86,21 @@ const DEFAULT_TEXTURE_FORMAT: TextureFormat = TextureFormat::Rgba8Unorm; #[cfg(not(target_os = "android"))] const DEFAULT_TEXTURE_FORMAT: TextureFormat = TextureFormat::Bgra8Unorm; +impl RenderContext for VelloHybridWindowRenderer { + fn register_image(&mut self, image: ImageData) -> ImageResource { + self.ctx.register_image(image) + } + + fn unregister_resource(&mut self, id: ResourceId) { + self.ctx.unregister_resource(id) + } +} + impl WindowRenderer for VelloHybridWindowRenderer { type ScenePainter<'a> = VelloHybridScenePainter<'a> where Self: 'a; - type Context = VelloHybridRenderContext; fn is_active(&self) -> bool { matches!(self.render_state, RenderState::Active(_)) @@ -152,11 +164,7 @@ impl WindowRenderer for VelloHybridWindowRenderer { } } - fn render)>( - &mut self, - ctx: &mut Self::Context, - draw_fn: F, - ) { + fn render)>(&mut self, draw_fn: F) { let RenderState::Active(state) = &mut self.render_state else { return; }; @@ -168,7 +176,7 @@ impl WindowRenderer for VelloHybridWindowRenderer { let device = state.render_surface.device(); let queue = state.render_surface.queue(); draw_fn(&mut VelloHybridScenePainter { - ctx, + ctx: &mut self.ctx, renderer: &mut state.renderer, device, queue, diff --git a/crates/pixels_window_renderer/src/lib.rs b/crates/pixels_window_renderer/src/lib.rs index 4941b5a..5c71450 100644 --- a/crates/pixels_window_renderer/src/lib.rs +++ b/crates/pixels_window_renderer/src/lib.rs @@ -2,7 +2,7 @@ #![cfg_attr(docsrs, feature(doc_cfg))] -use anyrender::{ImageRenderer, WindowHandle, WindowRenderer}; +use anyrender::{ImageData, ImageRenderer, ImageResource, RenderContext, ResourceId, WindowHandle, WindowRenderer}; use debug_timer::debug_timer; use pixels::{Pixels, SurfaceTexture, wgpu::Color}; use std::sync::Arc; @@ -42,12 +42,21 @@ impl PixelsWindowRenderer { } } +impl RenderContext for PixelsWindowRenderer { + fn register_image(&mut self, image: ImageData) -> ImageResource { + self.renderer.register_image(image) + } + + fn unregister_resource(&mut self, id: ResourceId) { + self.renderer.unregister_resource(id) + } +} + impl WindowRenderer for PixelsWindowRenderer { type ScenePainter<'a> = ::ScenePainter<'a> where Renderer: 'a; - type Context = Renderer::Context; fn is_active(&self) -> bool { matches!(self.render_state, RenderState::Active(_)) @@ -87,11 +96,7 @@ impl WindowRenderer for PixelsWindowRenderer }; } - fn render)>( - &mut self, - ctx: &mut Self::Context, - draw_fn: F, - ) { + fn render)>(&mut self, draw_fn: F) { let RenderState::Active(state) = &mut self.render_state else { return; }; @@ -99,7 +104,7 @@ impl WindowRenderer for PixelsWindowRenderer debug_timer!(timer, feature = "log_frame_times"); // Paint - self.renderer.render(ctx, draw_fn, state.pixels.frame_mut()); + self.renderer.render(draw_fn, state.pixels.frame_mut()); timer.record_time("render"); state.pixels.render().unwrap(); diff --git a/crates/softbuffer_window_renderer/src/lib.rs b/crates/softbuffer_window_renderer/src/lib.rs index 7ef44ba..8e57976 100644 --- a/crates/softbuffer_window_renderer/src/lib.rs +++ b/crates/softbuffer_window_renderer/src/lib.rs @@ -2,7 +2,7 @@ #![cfg_attr(docsrs, feature(doc_cfg))] -use anyrender::{ImageRenderer, WindowHandle, WindowRenderer}; +use anyrender::{ImageData, ImageRenderer, ImageResource, RenderContext, ResourceId, WindowHandle, WindowRenderer}; use debug_timer::debug_timer; use softbuffer::{Context, Surface}; use std::{num::NonZero, sync::Arc}; @@ -44,12 +44,21 @@ impl SoftbufferWindowRenderer { } } +impl RenderContext for SoftbufferWindowRenderer { + fn register_image(&mut self, image: ImageData) -> ImageResource { + self.renderer.register_image(image) + } + + fn unregister_resource(&mut self, id: ResourceId) { + self.renderer.unregister_resource(id) + } +} + impl WindowRenderer for SoftbufferWindowRenderer { type ScenePainter<'a> = Renderer::ScenePainter<'a> where Self: 'a; - type Context = Renderer::Context; fn is_active(&self) -> bool { matches!(self.render_state, RenderState::Active(_)) @@ -84,11 +93,7 @@ impl WindowRenderer for SoftbufferWindowRenderer)>( - &mut self, - ctx: &mut Self::Context, - draw_fn: F, - ) { + fn render)>(&mut self, draw_fn: F) { let RenderState::Active(state) = &mut self.render_state else { return; }; @@ -101,7 +106,7 @@ impl WindowRenderer for SoftbufferWindowRenderer, SkiaRenderContext), - SkiaRaster(Box, SkiaRenderContext), - Gpu(Box, VelloRenderContext), - Hybrid(Box, VelloHybridRenderContext), - Cpu(Box, VelloCpuRenderContext), + Skia(Box), + SkiaRaster(Box), + Gpu(Box), + Hybrid(Box), + Cpu(Box), } impl Renderer { fn is_active(&self) -> bool { match self { - Renderer::Skia(r, _) => r.is_active(), - Renderer::SkiaRaster(r, _) => r.is_active(), - Renderer::Gpu(r, _) => r.is_active(), - Renderer::Hybrid(r, _) => r.is_active(), - Renderer::Cpu(r, _) => r.is_active(), + Renderer::Skia(r) => r.is_active(), + Renderer::SkiaRaster(r) => r.is_active(), + Renderer::Gpu(r) => r.is_active(), + Renderer::Hybrid(r) => r.is_active(), + Renderer::Cpu(r) => r.is_active(), } } fn set_size(&mut self, w: u32, h: u32) { match self { - Renderer::Skia(r, _) => r.set_size(w, h), - Renderer::SkiaRaster(r, _) => r.set_size(w, h), - Renderer::Gpu(r, _) => r.set_size(w, h), - Renderer::Hybrid(r, _) => r.set_size(w, h), - Renderer::Cpu(r, _) => r.set_size(w, h), + Renderer::Skia(r) => r.set_size(w, h), + Renderer::SkiaRaster(r) => r.set_size(w, h), + Renderer::Gpu(r) => r.set_size(w, h), + Renderer::Hybrid(r) => r.set_size(w, h), + Renderer::Cpu(r) => r.set_size(w, h), } } } @@ -126,9 +126,8 @@ impl App { fn set_backend( &mut self, mut renderer: R, - mut ctx: R::Context, event_loop: &ActiveEventLoop, - f: impl FnOnce(R, R::Context) -> Renderer, + f: impl FnOnce(R) -> Renderer, ) { let mut window = match &self.render_state { RenderState::Active { window, .. } => Some(window.clone()), @@ -150,10 +149,10 @@ impl App { let physical_size = window.inner_size(); renderer.resume(window.clone(), physical_size.width, physical_size.height); - self.bunny_manager.register_image(&mut ctx); + self.bunny_manager.register_image(&mut renderer); self.render_state = RenderState::Active { window, - renderer: f(renderer, ctx), + renderer: f(renderer), }; self.request_redraw(); } @@ -169,9 +168,8 @@ impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &ActiveEventLoop) { self.set_backend( SkiaWindowRenderer::new(), - SkiaRenderContext::new(), event_loop, - |r, ctx| Renderer::Skia(Box::new(r), ctx), + |r| Renderer::Skia(Box::new(r)), ); } @@ -224,7 +222,7 @@ impl ApplicationHandler for App { self.bunny_manager.count(), ); match renderer { - Renderer::Skia(r, ctx) => r.render(ctx, |scene_painter| { + Renderer::Skia(r) => r.render(|scene_painter| { App::draw_scene( scene_painter, self.logical_width, @@ -234,7 +232,7 @@ impl ApplicationHandler for App { Color::from_rgb8(255, 0, 0), ); }), - Renderer::SkiaRaster(r, ctx) => r.render(ctx, |scene_painter| { + Renderer::SkiaRaster(r) => r.render(|scene_painter| { App::draw_scene( scene_painter, self.logical_width, @@ -244,7 +242,7 @@ impl ApplicationHandler for App { Color::from_rgb8(255, 0, 0), ); }), - Renderer::Gpu(r, ctx) => r.render(ctx, |scene_painter| { + Renderer::Gpu(r) => r.render(|scene_painter| { App::draw_scene( scene_painter, self.logical_width, @@ -254,7 +252,7 @@ impl ApplicationHandler for App { Color::from_rgb8(255, 0, 0), ); }), - Renderer::Hybrid(r, ctx) => r.render(ctx, |scene_painter| { + Renderer::Hybrid(r) => r.render(|scene_painter| { App::draw_scene( scene_painter, self.logical_width, @@ -264,7 +262,7 @@ impl ApplicationHandler for App { Color::from_rgb8(255, 0, 0), ); }), - Renderer::Cpu(r, ctx) => r.render(ctx, |scene_painter| { + Renderer::Cpu(r) => r.render(|scene_painter| { App::draw_scene( scene_painter, self.logical_width, @@ -296,41 +294,36 @@ impl ApplicationHandler for App { Renderer::SkiaRaster(..) => { self.set_backend( VelloCpuWindowRenderer::new(), - VelloCpuRenderContext::new(), event_loop, - |r, ctx| Renderer::Cpu(Box::new(r), ctx), + |r| Renderer::Cpu(Box::new(r)), ); } Renderer::Cpu(..) => { self.set_backend( VelloHybridWindowRenderer::new(), - VelloHybridRenderContext::new(), event_loop, - |r, ctx| Renderer::Hybrid(Box::new(r), ctx), + |r| Renderer::Hybrid(Box::new(r)), ); } Renderer::Hybrid(..) => { self.set_backend( VelloWindowRenderer::new(), - VelloRenderContext::new(), event_loop, - |r, ctx| Renderer::Gpu(Box::new(r), ctx), + |r| Renderer::Gpu(Box::new(r)), ); } Renderer::Gpu(..) => { self.set_backend( SkiaWindowRenderer::new(), - SkiaRenderContext::new(), event_loop, - |r, ctx| Renderer::Skia(Box::new(r), ctx), + |r| Renderer::Skia(Box::new(r)), ); } Renderer::Skia(..) => { self.set_backend( SkiaRasterWindowRenderer::new(), - SkiaRenderContext::new(), event_loop, - |r, ctx| Renderer::SkiaRaster(Box::new(r), ctx), + |r| Renderer::SkiaRaster(Box::new(r)), ); } } diff --git a/examples/serialize/src/main.rs b/examples/serialize/src/main.rs index 5b18e0f..a65be2b 100644 --- a/examples/serialize/src/main.rs +++ b/examples/serialize/src/main.rs @@ -7,7 +7,7 @@ use std::path::Path; use anyrender::recording::Scene; use anyrender::{Glyph, ImageRenderer, PaintScene, RecordingRenderContext, RenderContext}; use anyrender_serialize::{SceneArchive, SerializeConfig}; -use anyrender_vello_cpu::{VelloCpuImageRenderer, VelloCpuRenderContext}; +use anyrender_vello_cpu::VelloCpuImageRenderer; use image::{ImageBuffer, RgbaImage}; use kurbo::{Affine, Circle, Point, Rect, RoundedRect, Stroke}; use parley::style::{FontFamily, FontStack}; @@ -27,14 +27,13 @@ fn main() { // Render original prior to serialization/deserialization roundtrip. { - let mut ctx = VelloCpuRenderContext::new(); let mut renderer = VelloCpuImageRenderer::new(WIDTH, HEIGHT); let mut entries: Vec<_> = rec_ctx.image_data().iter().collect(); entries.sort_by_key(|(id, _)| id.0); for (_id, data) in entries { - ctx.register_image(data.clone()); + renderer.register_image(data.clone()); } - let pixels = render_to_vec(&mut renderer, &mut ctx, &original_scene); + let pixels = render_to_vec(&mut renderer, &original_scene); let img: RgbaImage = ImageBuffer::from_raw(WIDTH, HEIGHT, pixels.to_vec()).unwrap(); img.save(Path::new(OUTPUT_DIR).join("original.png")) .unwrap(); @@ -54,14 +53,13 @@ fn main() { // Deserialize let mut renderer = VelloCpuImageRenderer::new(WIDTH, HEIGHT); - let mut ctx = VelloCpuRenderContext::new(); let file = File::open(&archive_path).unwrap(); let deserialized_scene = SceneArchive::deserialize(file) .unwrap() - .to_scene(&mut ctx) + .to_scene(&mut renderer) .unwrap(); - let pixels = render_to_vec(&mut renderer, &mut ctx, &deserialized_scene); + let pixels = render_to_vec(&mut renderer, &deserialized_scene); let img: RgbaImage = ImageBuffer::from_raw(WIDTH, HEIGHT, pixels.to_vec()).unwrap(); img.save(Path::new(OUTPUT_DIR).join("roundtrip.png")) .unwrap(); @@ -320,15 +318,10 @@ fn create_checkerboard_image(width: u32, height: u32) -> ImageData { } } -/// Render a scene to an RGBA buffer using an already-configured renderer and context. -fn render_to_vec( - renderer: &mut VelloCpuImageRenderer, - ctx: &mut VelloCpuRenderContext, - scene: &Scene, -) -> Vec { +/// Render a scene to an RGBA buffer using an already-configured renderer. +fn render_to_vec(renderer: &mut VelloCpuImageRenderer, scene: &Scene) -> Vec { let mut buf = Vec::with_capacity((WIDTH * HEIGHT * 4) as usize); renderer.render_to_vec( - ctx, |painter| { painter.append_scene(scene.clone(), Affine::IDENTITY); }, diff --git a/examples/winit/src/main.rs b/examples/winit/src/main.rs index bfc2ec8..5cd7850 100644 --- a/examples/winit/src/main.rs +++ b/examples/winit/src/main.rs @@ -1,8 +1,8 @@ -use anyrender::{NullRenderContext, NullWindowRenderer, PaintScene, WindowRenderer}; -use anyrender_skia::{SkiaRenderContext, SkiaWindowRenderer}; -use anyrender_vello::{VelloRenderContext, VelloWindowRenderer}; -use anyrender_vello_cpu::{PixelsWindowRenderer, VelloCpuImageRenderer, VelloCpuRenderContext}; -use anyrender_vello_hybrid::{VelloHybridRenderContext, VelloHybridWindowRenderer}; +use anyrender::{NullWindowRenderer, PaintScene, WindowRenderer}; +use anyrender_skia::SkiaWindowRenderer; +use anyrender_vello::VelloWindowRenderer; +use anyrender_vello_cpu::{PixelsWindowRenderer, VelloCpuImageRenderer}; +use anyrender_vello_hybrid::VelloHybridWindowRenderer; use kurbo::{Affine, Circle, Point, Rect, Stroke}; use peniko::{Color, Fill}; use std::sync::Arc; @@ -23,31 +23,31 @@ struct App { type VelloCpuWindowRenderer = PixelsWindowRenderer; enum Renderer { - Gpu(Box, VelloRenderContext), - Hybrid(Box, VelloHybridRenderContext), - Cpu(Box, VelloCpuRenderContext), - Skia(Box, SkiaRenderContext), - Null(NullWindowRenderer, NullRenderContext), + Gpu(Box), + Hybrid(Box), + Cpu(Box), + Skia(Box), + Null(NullWindowRenderer), } impl Renderer { fn is_active(&self) -> bool { match self { - Renderer::Gpu(r, _) => r.is_active(), - Renderer::Hybrid(r, _) => r.is_active(), - Renderer::Cpu(r, _) => r.is_active(), - Renderer::Null(r, _) => r.is_active(), - Renderer::Skia(r, _) => r.is_active(), + Renderer::Gpu(r) => r.is_active(), + Renderer::Hybrid(r) => r.is_active(), + Renderer::Cpu(r) => r.is_active(), + Renderer::Null(r) => r.is_active(), + Renderer::Skia(r) => r.is_active(), } } fn set_size(&mut self, w: u32, h: u32) { match self { - Renderer::Gpu(r, _) => r.set_size(w, h), - Renderer::Hybrid(r, _) => r.set_size(w, h), - Renderer::Cpu(r, _) => r.set_size(w, h), - Renderer::Null(r, _) => r.set_size(w, h), - Renderer::Skia(r, _) => r.set_size(w, h), + Renderer::Gpu(r) => r.set_size(w, h), + Renderer::Hybrid(r) => r.set_size(w, h), + Renderer::Cpu(r) => r.set_size(w, h), + Renderer::Null(r) => r.set_size(w, h), + Renderer::Skia(r) => r.set_size(w, h), } } } @@ -105,9 +105,8 @@ impl App { fn set_backend( &mut self, mut renderer: R, - ctx: R::Context, event_loop: &ActiveEventLoop, - f: impl FnOnce(R, R::Context) -> Renderer, + f: impl FnOnce(R) -> Renderer, ) { let mut window = match &self.render_state { RenderState::Active { window, .. } => Some(window.clone()), @@ -126,7 +125,7 @@ impl App { renderer.resume(window.clone(), self.width, self.height); self.render_state = RenderState::Active { window, - renderer: f(renderer, ctx), + renderer: f(renderer), }; self.request_redraw(); } @@ -142,9 +141,8 @@ impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &ActiveEventLoop) { self.set_backend( SkiaWindowRenderer::new(), - SkiaRenderContext::new(), event_loop, - |r, ctx| Renderer::Skia(Box::new(r), ctx), + |r| Renderer::Skia(Box::new(r)), ); } @@ -171,20 +169,20 @@ impl ApplicationHandler for App { self.request_redraw(); } WindowEvent::RedrawRequested => match renderer { - Renderer::Skia(r, ctx) => { - r.render(ctx, |p| App::draw_scene(p, Color::from_rgb8(128, 128, 128))) + Renderer::Skia(r) => { + r.render(|p| App::draw_scene(p, Color::from_rgb8(128, 128, 128))) } - Renderer::Gpu(r, ctx) => { - r.render(ctx, |p| App::draw_scene(p, Color::from_rgb8(255, 0, 0))) + Renderer::Gpu(r) => { + r.render(|p| App::draw_scene(p, Color::from_rgb8(255, 0, 0))) } - Renderer::Hybrid(r, ctx) => { - r.render(ctx, |p| App::draw_scene(p, Color::from_rgb8(0, 0, 0))) + Renderer::Hybrid(r) => { + r.render(|p| App::draw_scene(p, Color::from_rgb8(0, 0, 0))) } - Renderer::Cpu(r, ctx) => { - r.render(ctx, |p| App::draw_scene(p, Color::from_rgb8(0, 255, 0))) + Renderer::Cpu(r) => { + r.render(|p| App::draw_scene(p, Color::from_rgb8(0, 255, 0))) } - Renderer::Null(r, ctx) => { - r.render(ctx, |p| App::draw_scene(p, Color::from_rgb8(0, 0, 0))) + Renderer::Null(r) => { + r.render(|p| App::draw_scene(p, Color::from_rgb8(0, 0, 0))) } }, WindowEvent::KeyboardInput { @@ -199,31 +197,27 @@ impl ApplicationHandler for App { Renderer::Cpu(..) => { self.set_backend( VelloHybridWindowRenderer::new(), - VelloHybridRenderContext::new(), event_loop, - |r, ctx| Renderer::Hybrid(Box::new(r), ctx), + |r| Renderer::Hybrid(Box::new(r)), ); } Renderer::Hybrid(..) => { self.set_backend( VelloWindowRenderer::new(), - VelloRenderContext::new(), event_loop, - |r, ctx| Renderer::Gpu(Box::new(r), ctx), + |r| Renderer::Gpu(Box::new(r)), ); } Renderer::Gpu(..) => { self.set_backend( SkiaWindowRenderer::new(), - SkiaRenderContext::new(), event_loop, - |r, ctx| Renderer::Skia(Box::new(r), ctx), + |r| Renderer::Skia(Box::new(r)), ); } Renderer::Skia(..) => { self.set_backend( NullWindowRenderer::new(), - NullRenderContext::new(), event_loop, Renderer::Null, ); @@ -231,9 +225,8 @@ impl ApplicationHandler for App { Renderer::Null(..) => { self.set_backend( VelloCpuWindowRenderer::new(), - VelloCpuRenderContext::new(), event_loop, - |r, ctx| Renderer::Cpu(Box::new(r), ctx), + |r| Renderer::Cpu(Box::new(r)), ); } },