.
diff --git a/Cargo.lock b/Cargo.lock index 7915a4e..e295a7a 100644 --- a/Cargo.lock +++ b/Cargo.lock
@@ -4018,6 +4018,7 @@ name = "vello_cpu_winit" version = "0.0.0" dependencies = [ + "parley_draw", "softbuffer", "vello_common", "vello_cpu", @@ -4089,6 +4090,7 @@ name = "vello_hybrid_winit" version = "0.0.0" dependencies = [ + "parley_draw", "pollster", "vello_common", "vello_example_scenes",
diff --git a/Cargo.toml b/Cargo.toml index 9e5d170..5f3559f 100644 --- a/Cargo.toml +++ b/Cargo.toml
@@ -167,3 +167,7 @@ inherits = "release" debug = true strip = "none" + +[profile.profiling] +inherits = "release" +debug = true
diff --git a/sparse_strips/vello_cpu/Cargo.toml b/sparse_strips/vello_cpu/Cargo.toml index 3f94d66..912ac19 100644 --- a/sparse_strips/vello_cpu/Cargo.toml +++ b/sparse_strips/vello_cpu/Cargo.toml
@@ -48,6 +48,7 @@ u8_pipeline = [] # Quality focussed rendering using f32 math f32_pipeline = [] +text = [] [lints] workspace = true
diff --git a/sparse_strips/vello_cpu/examples/winit/Cargo.toml b/sparse_strips/vello_cpu/examples/winit/Cargo.toml index 73bc454..85ad9dd 100644 --- a/sparse_strips/vello_cpu/examples/winit/Cargo.toml +++ b/sparse_strips/vello_cpu/examples/winit/Cargo.toml
@@ -11,6 +11,7 @@ [dependencies] winit = { workspace = true } +parley_draw = { workspace = true, features = ["std", "vello_cpu"] } vello_common = { workspace = true } vello_cpu = { workspace = true, features = ["multithreading"] } vello_example_scenes = { workspace = true, features = ["cpu"] }
diff --git a/sparse_strips/vello_cpu/examples/winit/src/main.rs b/sparse_strips/vello_cpu/examples/winit/src/main.rs index de1eda0..a6ac2b9 100644 --- a/sparse_strips/vello_cpu/examples/winit/src/main.rs +++ b/sparse_strips/vello_cpu/examples/winit/src/main.rs
@@ -8,6 +8,10 @@ reason = "truncation has no appreciable impact in this demo" )] +use parley_draw::renderers::vello_renderer::replay_atlas_commands; +use parley_draw::{ + AtlasConfig, CpuGlyphCaches, GlyphCache, GlyphCacheConfig, ImageCache, PendingClearRect, +}; #[cfg(not(target_arch = "wasm32"))] use std::env; use std::num::NonZeroU32; @@ -18,7 +22,7 @@ use vello_common::pixmap::Pixmap; use vello_cpu::{RenderContext, RenderSettings}; use vello_example_scenes::image::ImageScene; -use vello_example_scenes::{AnyScene, get_example_scenes}; +use vello_example_scenes::{AnyScene, TextConfig, get_example_scenes}; use winit::{ application::ApplicationHandler, event::{ElementState, KeyEvent, Modifiers, MouseButton, MouseScrollDelta, WindowEvent}, @@ -36,6 +40,10 @@ current_scene: usize, render_state: RenderState, renderer: RenderContext, + glyph_renderer: RenderContext, + glyph_caches: CpuGlyphCaches, + image_cache: ImageCache, + text_config: TextConfig, pixmap: Pixmap, transform: Affine, mouse_down: bool, @@ -44,6 +52,7 @@ frame_count: u32, fps_update_time: Instant, accumulated_frame_time: f64, + accumulated_render_time: f64, rotating: bool, rotation_speed: f64, shearing: bool, @@ -62,11 +71,9 @@ let mut svg_paths: Vec<&str> = Vec::new(); if args.len() > 1 { - // Check if the first argument is a number (scene index) if let Ok(index) = args[1].parse::<usize>() { start_scene_index = index; } else { - // Collect all arguments as SVG paths for arg in args.iter().skip(1) { svg_paths.push(arg); } @@ -112,10 +119,31 @@ width, height, RenderSettings { - num_threads: 0, // 0 means use default (number of CPU cores) + num_threads: 0, ..Default::default() }, ), + glyph_renderer: RenderContext::new_with( + 512, + 512, + RenderSettings { + num_threads: 0, + ..Default::default() + }, + ), + glyph_caches: CpuGlyphCaches::with_config( + 512, + 512, + GlyphCacheConfig { + max_entry_age: u32::MAX, + eviction_frequency: u32::MAX, + }, + ), + image_cache: ImageCache::new_with_config(AtlasConfig { + atlas_size: (512, 512), + ..AtlasConfig::default() + }), + text_config: TextConfig::default(), pixmap: Pixmap::new(width, height), transform: Affine::IDENTITY, mouse_down: false, @@ -124,15 +152,13 @@ frame_count: 0, fps_update_time: now, accumulated_frame_time: 0.0, + accumulated_render_time: 0.0, rotating: false, rotation_speed: 1.0, shearing: false, - // shear rate (units of tan(angle)) per second shear_speed: 0.8, - // maximum |shear| to oscillate between shear_amplitude: 0.35, current_shear: 0.0, - // 1 for increasing toward +amplitude, -1 toward -amplitude shear_direction: 1.0, modifiers: Modifiers::default(), }; @@ -247,31 +273,39 @@ window.request_redraw(); } Key::Named(NamedKey::Space) => { - // Reset transform on spacebar self.transform = Affine::IDENTITY; window.request_redraw(); } Key::Named(NamedKey::Escape) => { event_loop.exit(); } - Key::Character(ch) => { - if ch.as_str() == "r" { - if is_cmd { - // Cmd+r: Toggle continuous rotation around the window center - self.rotating = !self.rotating; - window.request_redraw(); - } else { - // r: Single-step rotation around the window center - let center = Point { - x: 0.5 * self.pixmap.width() as f64, - y: 0.5 * self.pixmap.height() as f64, - }; - self.transform = - self.transform.then_rotate_about(ROTATION_STEP, center); - window.request_redraw(); - } - } else if ch.as_str() == "R" { - // R: Counter-rotation step (opposite direction of r) + Key::Character(ch) => match ch.as_str() { + "a" | "A" => { + self.text_config.use_atlas_cache = !self.text_config.use_atlas_cache; + println!( + "Atlas cache: {}", + if self.text_config.use_atlas_cache { + "ON" + } else { + "OFF" + } + ); + window.request_redraw(); + } + "r" if is_cmd => { + self.rotating = !self.rotating; + window.request_redraw(); + } + "r" => { + let center = Point { + x: 0.5 * self.pixmap.width() as f64, + y: 0.5 * self.pixmap.height() as f64, + }; + self.transform = + self.transform.then_rotate_about(ROTATION_STEP, center); + window.request_redraw(); + } + "R" => { let center = Point { x: 0.5 * self.pixmap.width() as f64, y: 0.5 * self.pixmap.height() as f64, @@ -279,25 +313,23 @@ self.transform = self.transform.then_rotate_about(-ROTATION_STEP, center); window.request_redraw(); - } else if ch.as_str() == "s" { - if is_cmd { - // Cmd+s: Toggle shear oscillation around the window center - self.shearing = !self.shearing; - window.request_redraw(); - } else { - // s: Single-step shear about the window center in X - let center = Point { - x: 0.5 * self.pixmap.width() as f64, - y: 0.5 * self.pixmap.height() as f64, - }; - let about_center = Affine::translate((-center.x, -center.y)) - * Affine::skew(SHEAR_STEP, 0.0) - * Affine::translate((center.x, center.y)); - self.transform *= about_center; - window.request_redraw(); - } - } else if ch.as_str() == "S" { - // S: Counter-shear step (opposite direction of s) + } + "s" if is_cmd => { + self.shearing = !self.shearing; + window.request_redraw(); + } + "s" => { + let center = Point { + x: 0.5 * self.pixmap.width() as f64, + y: 0.5 * self.pixmap.height() as f64, + }; + let about_center = Affine::translate((-center.x, -center.y)) + * Affine::skew(SHEAR_STEP, 0.0) + * Affine::translate((center.x, center.y)); + self.transform *= about_center; + window.request_redraw(); + } + "S" => { let center = Point { x: 0.5 * self.pixmap.width() as f64, y: 0.5 * self.pixmap.height() as f64, @@ -307,12 +339,15 @@ * Affine::translate((center.x, center.y)); self.transform *= about_center; window.request_redraw(); - } else if let Some(scene) = self.scenes.get_mut(self.current_scene) - && scene.handle_key(ch.as_str()) - { - window.request_redraw(); } - } + _ => { + if let Some(scene) = self.scenes.get_mut(self.current_scene) + && scene.handle_key(ch.as_str()) + { + window.request_redraw(); + } + } + }, _ => {} } } @@ -320,7 +355,6 @@ if button == MouseButton::Left { self.mouse_down = state == ElementState::Pressed; if !self.mouse_down { - // Mouse button released self.last_cursor_position = None; } } @@ -332,7 +366,6 @@ }; if self.mouse_down { - // Pan the scene if mouse is down if let Some(last_pos) = self.last_cursor_position { self.transform = self.transform.then_translate(current_pos - last_pos); window.request_redraw(); @@ -342,7 +375,6 @@ self.last_cursor_position = Some(current_pos); } WindowEvent::MouseWheel { delta, .. } => { - // Handle zoom with mouse wheel let delta_y = match delta { MouseScrollDelta::LineDelta(_, y) => y as f64, MouseScrollDelta::PixelDelta(pos) => pos.y / 100.0, @@ -350,18 +382,12 @@ if let Some(cursor_pos) = self.last_cursor_position { let zoom_factor = (1.0 + delta_y * ZOOM_STEP).max(0.1); - - // Zoom centered at cursor position self.transform = self.transform.then_scale_about(zoom_factor, cursor_pos); - window.request_redraw(); } } WindowEvent::PinchGesture { delta, .. } => { - // Handle pinch-to-zoom on touchpad. let zoom_factor = 1.0 + delta * ZOOM_STEP * 5.0; - - // Zoom centered at cursor position, or the center if no position is set. self.transform = self.transform.then_scale_about( zoom_factor, self.last_cursor_position.unwrap_or(Point { @@ -369,34 +395,39 @@ y: 0.5 * self.pixmap.height() as f64, }), ); - window.request_redraw(); } WindowEvent::RedrawRequested => { - // Measure frame time let now = Instant::now(); let delta_s = self .last_frame_time .map(|t| now.duration_since(t).as_secs_f64()) .unwrap_or(0.0); if let Some(last_time) = self.last_frame_time { - let frame_time = now.duration_since(last_time).as_secs_f64() * 1000.0; // Convert to milliseconds + let frame_time = now.duration_since(last_time).as_secs_f64() * 1000.0; self.accumulated_frame_time += frame_time; self.frame_count += 1; - // Update window title every second with average FPS if now.duration_since(self.fps_update_time).as_secs_f64() >= 1.0 { let avg_frame_time = self.accumulated_frame_time / self.frame_count as f64; let avg_fps = 1000.0 / avg_frame_time; - println!("Average FPS: {avg_fps:.1}"); + let avg_render_time = + self.accumulated_render_time / self.frame_count as f64; + let status = self.scenes[self.current_scene] + .status() + .map(|s| format!(" - {s}")) + .unwrap_or_default(); + println!( + "FPS: {avg_fps:.1} | render: {avg_render_time:.2}ms | frame: {avg_frame_time:.2}ms{status}" + ); window.set_title(&format!( - "Vello CPU - Scene {} - {:.1} FPS ({:.2}ms avg)", - self.current_scene, avg_fps, avg_frame_time + "Vello CPU - Scene {} - {:.1} FPS (render {:.2}ms){status}", + self.current_scene, avg_fps, avg_render_time )); - // Reset counters self.frame_count = 0; self.accumulated_frame_time = 0.0; + self.accumulated_render_time = 0.0; self.fps_update_time = now; } } @@ -412,7 +443,7 @@ self.transform = self.transform.then_rotate_about(angle, center); } - // Apply shear oscillation if enabled (bounded back-and-forth) + // Apply shear oscillation if enabled if self.shearing && delta_s > 0.0 { let old = self.current_shear; let mut new = old + self.shear_speed * delta_s * self.shear_direction; @@ -433,7 +464,6 @@ x: 0.5 * self.pixmap.width() as f64, y: 0.5 * self.pixmap.height() as f64, }; - // Shear about window center in X; Y shear remains 0.0 let about_center = Affine::translate((-center.x, -center.y)) * Affine::skew(delta_shear, 0.0) * Affine::translate((center.x, center.y)); @@ -442,32 +472,135 @@ } } + let render_start = Instant::now(); + // Render the scene self.renderer.reset(); + self.renderer.set_transform(self.transform); + self.scenes[self.current_scene].render( + &mut self.renderer, + self.transform, + &mut self.glyph_caches, + &mut self.image_cache, + &self.text_config, + ); - self.scenes[self.current_scene].render(&mut self.renderer, self.transform); + // Replay outline/COLR draw commands into each atlas page's pixmap. + for mut recorder in self.glyph_caches.glyph_atlas.take_pending_atlas_commands() { + self.glyph_renderer.reset(); + replay_atlas_commands(&mut recorder.commands, &mut self.glyph_renderer); + self.glyph_renderer.flush(); + if let Some(atlas_pixmap) = self + .glyph_caches + .glyph_atlas + .page_pixmap_mut(recorder.page_index as usize) + { + self.glyph_renderer + .composite_to_pixmap_at_offset(atlas_pixmap, 0, 0); + } + } + + // Upload bitmap glyphs to atlas pages. + for upload in self.glyph_caches.glyph_atlas.take_pending_uploads() { + let page_index = upload.atlas_slot.page_index as usize; + if let Some(atlas_pixmap) = + self.glyph_caches.glyph_atlas.page_pixmap_mut(page_index) + { + copy_pixmap_to_atlas( + &upload.pixmap, + atlas_pixmap, + upload.atlas_slot.x, + upload.atlas_slot.y, + upload.atlas_slot.width, + upload.atlas_slot.height, + ); + } + } + + // Share atlas page pixmaps with the renderer. + let page_count = self.glyph_caches.glyph_atlas.page_count(); + for page_index in 0..page_count { + if let Some(page_pixmap) = self.glyph_caches.glyph_atlas.page_pixmap(page_index) + { + self.renderer.register_image(page_pixmap.clone()); + } + } + self.renderer.flush(); self.renderer.render_to_pixmap(&mut self.pixmap); + self.renderer.clear_images(); + + // Maintain caches (eviction, etc.) + self.glyph_caches.maintain(&mut self.image_cache); + + // Clear stale atlas regions after eviction. + for rect in self.glyph_caches.glyph_atlas.take_pending_clear_rects() { + if let Some(atlas_pixmap) = self + .glyph_caches + .glyph_atlas + .page_pixmap_mut(rect.page_index as usize) + { + clear_pixmap_region(atlas_pixmap, &rect); + } + } + + self.accumulated_render_time += render_start.elapsed().as_secs_f64() * 1000.0; // Copy pixmap to window surface let mut buffer = surface.buffer_mut().unwrap(); let pixmap_data = self.pixmap.data(); - // Convert RGBA to BGRA/XRGB format expected by softbuffer for (buffer_pixel, pixel) in buffer.iter_mut().zip(pixmap_data.iter()) { - // softbuffer expects 0RGB format (little-endian: B, G, R, 0) - // Our pixmap is premultiplied RGBA *buffer_pixel = u32::from_le_bytes([pixel.b, pixel.g, pixel.r, 0]); } buffer.present().unwrap(); // Request continuous redraw for FPS measurement - if self.rotating || self.shearing { - window.request_redraw(); - } + window.request_redraw(); } _ => {} } } } + +/// Zero out a rectangular region in the atlas pixmap. +fn clear_pixmap_region(dst: &mut Pixmap, rect: &PendingClearRect) { + let dst_stride = dst.width() as usize; + let dst_data = dst.data_as_u8_slice_mut(); + let clear_width = rect.width as usize; + let clear_height = rect.height as usize; + + for y in 0..clear_height { + let row_start = ((rect.y as usize + y) * dst_stride + rect.x as usize) * 4; + let row_end = row_start + clear_width * 4; + dst_data[row_start..row_end].fill(0); + } +} + +/// Copy bitmap glyph pixels into a rectangular region of an atlas page. +fn copy_pixmap_to_atlas( + src: &Pixmap, + dst: &mut Pixmap, + dst_x: u16, + dst_y: u16, + width: u16, + height: u16, +) { + let copy_width = width as usize; + let copy_height = height as usize; + let src_stride = src.width() as usize; + let dst_stride = dst.width() as usize; + + let src_data = src.data_as_u8_slice(); + let dst_data = dst.data_as_u8_slice_mut(); + + for y in 0..copy_height { + let src_row_start = y * src_stride * 4; + let src_row_end = src_row_start + copy_width * 4; + let dst_row_start = ((dst_y as usize + y) * dst_stride + dst_x as usize) * 4; + let dst_row_end = dst_row_start + copy_width * 4; + + dst_data[dst_row_start..dst_row_end].copy_from_slice(&src_data[src_row_start..src_row_end]); + } +}
diff --git a/sparse_strips/vello_cpu/src/render.rs b/sparse_strips/vello_cpu/src/render.rs index 9d657df..870fca1 100644 --- a/sparse_strips/vello_cpu/src/render.rs +++ b/sparse_strips/vello_cpu/src/render.rs
@@ -244,22 +244,39 @@ &ctx.encoded_paints, ); } else { - // Fall back to path-based rendering for rotated/skewed transforms. - ctx.rect_to_temp_path(rect); - ctx.dispatcher.fill_path( - &ctx.temp_path, - ctx.fill_rule, - ctx.transform, - paint, - ctx.blend_mode, - ctx.aliasing_threshold, - ctx.mask.clone(), - &ctx.encoded_paints, - ); + ctx.fill_rect_slow(rect, paint); } }); } + /// Fill a rectangle, bypassing the pixel-alignment fast-path check. + /// + /// Use this when the caller already knows the transform/rect combination is + /// *not* pixel-aligned (e.g. glyph atlas rendering with fractional bearing + /// offsets or non-identity paint transforms). This avoids the per-call cost + /// of `is_integer_translation` and `is_integer_rect`. + #[inline] + pub fn fill_rect_pixel_aligned(&mut self, rect: &Rect) { + self.with_optional_filter(|ctx| { + let paint = ctx.encode_current_paint(); + ctx.fill_rect_slow(rect, paint); + }); + } + + fn fill_rect_slow(&mut self, rect: &Rect, paint: Paint) { + self.rect_to_temp_path(rect); + self.dispatcher.fill_path( + &self.temp_path, + self.fill_rule, + self.transform, + paint, + self.blend_mode, + self.aliasing_threshold, + self.mask.clone(), + &self.encoded_paints, + ); + } + /// Stroke a rectangle. pub fn stroke_rect(&mut self, rect: &Rect) { self.with_optional_filter(|ctx| {
diff --git a/sparse_strips/vello_example_scenes/src/blend.rs b/sparse_strips/vello_example_scenes/src/blend.rs index ea3cabe..ea66b97 100644 --- a/sparse_strips/vello_example_scenes/src/blend.rs +++ b/sparse_strips/vello_example_scenes/src/blend.rs
@@ -3,7 +3,10 @@ //! Example compositing an image using blend layers. -use crate::{ExampleScene, RenderingContext}; +use core::any::Any; +use parley_draw::ImageCache; + +use crate::{ExampleScene, RenderingContext, TextConfig}; use vello_common::color::palette::css::{BLUE, GREEN, PURPLE, RED, YELLOW}; use vello_common::kurbo::{Affine, Circle, Point, Rect, Shape}; use vello_common::peniko::{BlendMode, Color, Compose, Mix}; @@ -13,7 +16,14 @@ pub struct BlendScene {} impl ExampleScene for BlendScene { - fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) { + fn render( + &mut self, + ctx: &mut impl RenderingContext, + root_transform: Affine, + _glyph_caches: &mut dyn Any, + _image_cache: &mut ImageCache, + _text_config: &TextConfig, + ) { render(ctx, root_transform); } }
diff --git a/sparse_strips/vello_example_scenes/src/clip.rs b/sparse_strips/vello_example_scenes/src/clip.rs index 95a600c..1df7df7 100644 --- a/sparse_strips/vello_example_scenes/src/clip.rs +++ b/sparse_strips/vello_example_scenes/src/clip.rs
@@ -9,7 +9,10 @@ only break in edge cases, and some of them are also only related to conversions from f64 to f32." )] -use crate::{ExampleScene, RenderingContext}; +use core::any::Any; +use parley_draw::ImageCache; + +use crate::{ExampleScene, RenderingContext, TextConfig}; use vello_common::color::palette::css::{ BLACK, BLUE, DARK_BLUE, DARK_GREEN, GREEN, REBECCA_PURPLE, RED, }; @@ -24,7 +27,14 @@ } impl ExampleScene for ClipScene { - fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) { + fn render( + &mut self, + ctx: &mut impl RenderingContext, + root_transform: Affine, + _glyph_caches: &mut dyn Any, + _image_cache: &mut ImageCache, + _text_config: &TextConfig, + ) { render(ctx, root_transform, self.use_clip_path, self.num_circles); }
diff --git a/sparse_strips/vello_example_scenes/src/filter.rs b/sparse_strips/vello_example_scenes/src/filter.rs index fd598d5..1b29624 100644 --- a/sparse_strips/vello_example_scenes/src/filter.rs +++ b/sparse_strips/vello_example_scenes/src/filter.rs
@@ -6,7 +6,10 @@ //! This scene is based on the `filter_varying_depths_clips_and_compositions` test. //! See: `sparse_strips/vello_sparse_tests/tests/filter.rs` -use crate::{ExampleScene, RenderingContext}; +use core::any::Any; +use parley_draw::ImageCache; + +use crate::{ExampleScene, RenderingContext, TextConfig}; use vello_common::color::AlphaColor; use vello_common::color::palette::css::{PURPLE, ROYAL_BLUE, SEA_GREEN, TOMATO, VIOLET}; use vello_common::filter_effects::{EdgeMode, Filter, FilterPrimitive}; @@ -18,7 +21,14 @@ pub struct FilterScene {} impl ExampleScene for FilterScene { - fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) { + fn render( + &mut self, + ctx: &mut impl RenderingContext, + root_transform: Affine, + _glyph_caches: &mut dyn Any, + _image_cache: &mut ImageCache, + _text_config: &TextConfig, + ) { ctx.set_transform(root_transform); let filter_drop_shadow = Filter::from_primitive(FilterPrimitive::DropShadow {
diff --git a/sparse_strips/vello_example_scenes/src/gradient.rs b/sparse_strips/vello_example_scenes/src/gradient.rs index e97d413..241b18a 100644 --- a/sparse_strips/vello_example_scenes/src/gradient.rs +++ b/sparse_strips/vello_example_scenes/src/gradient.rs
@@ -9,7 +9,10 @@ //! - `RadialScene`: //! - `two_point_radial` method from `https://github.com/linebender/vello/blob/0f3ef03a823eb10b0d7a60164e286cde77ffa222/examples/scenes/src/test_scenes.rs#L882` -use crate::{ExampleScene, RenderingContext}; +use core::any::Any; +use parley_draw::ImageCache; + +use crate::{ExampleScene, RenderingContext, TextConfig}; use smallvec::smallvec; use vello_common::color::palette::css::{BLACK, BLUE, LIME, RED, WHITE, YELLOW}; use vello_common::kurbo::{Affine, Ellipse, Point, Rect, Shape, Stroke}; @@ -28,7 +31,14 @@ } impl ExampleScene for GradientExtendScene { - fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) { + fn render( + &mut self, + ctx: &mut impl RenderingContext, + root_transform: Affine, + _glyph_caches: &mut dyn Any, + _image_cache: &mut ImageCache, + _text_config: &TextConfig, + ) { enum Kind { Linear, Radial, @@ -148,7 +158,14 @@ } impl ExampleScene for RadialScene { - fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) { + fn render( + &mut self, + ctx: &mut impl RenderingContext, + root_transform: Affine, + _glyph_caches: &mut dyn Any, + _image_cache: &mut ImageCache, + _text_config: &TextConfig, + ) { /// Helper function to create color stops fn create_color_stops(colors: &[Color]) -> ColorStops { ColorStops(smallvec![
diff --git a/sparse_strips/vello_example_scenes/src/image.rs b/sparse_strips/vello_example_scenes/src/image.rs index 4f5a56c..ba2c0e6 100644 --- a/sparse_strips/vello_example_scenes/src/image.rs +++ b/sparse_strips/vello_example_scenes/src/image.rs
@@ -4,6 +4,8 @@ //! Image rendering example scene. use std::f64::consts::PI; +use core::any::Any; +use parley_draw::ImageCache; use vello_common::color::PremulRgba8; use vello_common::kurbo::{BezPath, Point, Shape, Vec2}; use vello_common::peniko::ImageFormat; @@ -15,7 +17,7 @@ peniko::{Extend, ImageQuality}, }; -use crate::{ExampleScene, RenderingContext}; +use crate::{ExampleScene, RenderingContext, TextConfig}; /// Image scene state #[derive(Debug, Default)] @@ -31,7 +33,14 @@ } impl ExampleScene for ImageScene { - fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) { + fn render( + &mut self, + ctx: &mut impl RenderingContext, + root_transform: Affine, + _glyph_caches: &mut dyn Any, + _image_cache: &mut ImageCache, + _text_config: &TextConfig, + ) { let splash_flower_id = self.img_sources[0].clone(); let cowboy_id = self.img_sources[1].clone();
diff --git a/sparse_strips/vello_example_scenes/src/lib.rs b/sparse_strips/vello_example_scenes/src/lib.rs index 11c9cdc..e37e4b3 100644 --- a/sparse_strips/vello_example_scenes/src/lib.rs +++ b/sparse_strips/vello_example_scenes/src/lib.rs
@@ -31,6 +31,24 @@ use vello_cpu::RenderContext; use vello_hybrid::Scene; +/// Configuration for text rendering passed through to example scenes. +#[derive(Clone, Copy, Debug)] +pub struct TextConfig { + /// Whether font hinting is enabled. + pub hint: bool, + /// Whether the glyph atlas cache is enabled. + pub use_atlas_cache: bool, +} + +impl Default for TextConfig { + fn default() -> Self { + Self { + hint: true, + use_atlas_cache: true, + } + } +} + /// A generic rendering context. pub trait RenderingContext: Sized { /// Width of the render target in pixels. @@ -87,22 +105,21 @@ /// Get the current transform. fn transform(&self) -> Affine; - /// Create a new set of glyph caches for this renderer backend. - /// - /// Returns a type-erased cache that must be passed to [`Self::fill_glyphs`]. - fn create_glyph_caches(&self) -> Box<dyn Any>; - /// Fill glyphs using the renderer's glyph pipeline. /// - /// `glyph_caches` must be the value returned by [`Self::create_glyph_caches`]. + /// `glyph_caches` is a type-erased backend-specific glyph cache + /// (e.g. `GpuGlyphCaches` or `CpuGlyphCaches`). + /// `image_cache` is the shared atlas allocator that must be the same + /// instance used by the GPU renderer for atlas lookups. fn fill_glyphs( &mut self, font: &FontData, font_size: f32, - hint: bool, normalized_coords: &[i16], glyphs: impl Iterator<Item = Glyph>, glyph_caches: &mut dyn Any, + image_cache: &mut ImageCache, + text_config: &TextConfig, ); } @@ -203,41 +220,29 @@ *self.transform() } - fn create_glyph_caches(&self) -> Box<dyn Any> { - Box::new(CpuGlyphState { - glyph_caches: parley_draw::CpuGlyphCaches::new(512, 512), - image_cache: ImageCache::new_with_config(parley_draw::AtlasConfig::default()), - }) - } - fn fill_glyphs( &mut self, font: &FontData, font_size: f32, - hint: bool, normalized_coords: &[i16], glyphs: impl Iterator<Item = Glyph>, glyph_caches: &mut dyn Any, + image_cache: &mut ImageCache, + text_config: &TextConfig, ) { - let state = glyph_caches - .downcast_mut::<CpuGlyphState>() + let caches = glyph_caches + .downcast_mut::<parley_draw::CpuGlyphCaches>() .expect("wrong glyph cache type for CPU renderer"); let transform = *self.transform(); GlyphRunBuilder::new(font.clone(), transform, self) .font_size(font_size) - .hint(hint) + .hint(text_config.hint) .normalized_coords(bytemuck::cast_slice(normalized_coords)) - .fill_glyphs(glyphs, &mut state.glyph_caches, &mut state.image_cache); + .atlas_cache(text_config.use_atlas_cache) + .fill_glyphs(glyphs, caches, image_cache); } } -/// Glyph caches for the CPU renderer backend. -#[cfg(feature = "cpu")] -struct CpuGlyphState { - glyph_caches: parley_draw::CpuGlyphCaches, - image_cache: ImageCache, -} - impl RenderingContext for Scene { fn width(&self) -> u16 { self.width() @@ -334,46 +339,43 @@ *self.transform() } - fn create_glyph_caches(&self) -> Box<dyn Any> { - Box::new(GpuGlyphState { - glyph_caches: parley_draw::GpuGlyphCaches::with_config( - parley_draw::GlyphCacheConfig::default(), - ), - image_cache: ImageCache::new_with_config(parley_draw::AtlasConfig::default()), - }) - } - fn fill_glyphs( &mut self, font: &FontData, font_size: f32, - hint: bool, normalized_coords: &[i16], glyphs: impl Iterator<Item = Glyph>, glyph_caches: &mut dyn Any, + image_cache: &mut ImageCache, + text_config: &TextConfig, ) { - let state = glyph_caches - .downcast_mut::<GpuGlyphState>() + let caches = glyph_caches + .downcast_mut::<parley_draw::GpuGlyphCaches>() .expect("wrong glyph cache type for hybrid renderer"); let transform = *self.transform(); GlyphRunBuilder::new(font.clone(), transform, self) .font_size(font_size) - .hint(hint) + .hint(text_config.hint) .normalized_coords(bytemuck::cast_slice(normalized_coords)) - .fill_glyphs(glyphs, &mut state.glyph_caches, &mut state.image_cache); + .atlas_cache(text_config.use_atlas_cache) + .fill_glyphs(glyphs, caches, image_cache); } } -/// Glyph caches for the hybrid (GPU) renderer backend. -struct GpuGlyphState { - glyph_caches: parley_draw::GpuGlyphCaches, - image_cache: ImageCache, -} - /// Example scene that can maintain state between renders. pub trait ExampleScene { /// Render the scene using the current state. - fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine); + /// + /// `glyph_caches` and `image_cache` are provided by the application so that + /// text-rendering scenes can share atlas state with the GPU renderer. + fn render( + &mut self, + ctx: &mut impl RenderingContext, + root_transform: Affine, + glyph_caches: &mut dyn Any, + image_cache: &mut ImageCache, + text_config: &TextConfig, + ); /// Handle key press events (optional). /// Returns true if the key was handled, false otherwise. @@ -400,7 +402,7 @@ } /// A type-erased render function. -type RenderFn<T> = Box<dyn FnMut(&mut T, Affine)>; +type RenderFn<T> = Box<dyn FnMut(&mut T, Affine, &mut dyn Any, &mut ImageCache, &TextConfig)>; /// A type-erased key handler function. type KeyHandlerFn = Box<dyn FnMut(&str) -> bool>; @@ -424,7 +426,9 @@ let scene_status = scene.clone(); Self { - render_fn: Box::new(move |s, transform| scene.borrow_mut().render(s, transform)), + render_fn: Box::new(move |s, transform, gc, ic, tc| { + scene.borrow_mut().render(s, transform, gc, ic, tc); + }), key_handler_fn: Box::new(move |key| scene_clone.borrow_mut().handle_key(key)), status_fn: Box::new(move || scene_status.borrow().status()), show_widetile_columns: false, @@ -432,11 +436,16 @@ } /// Render the scene. - pub fn render(&mut self, ctx: &mut T, root_transform: Affine) { - // Render the actual scene content - (self.render_fn)(ctx, root_transform); + pub fn render( + &mut self, + ctx: &mut T, + root_transform: Affine, + glyph_caches: &mut dyn Any, + image_cache: &mut ImageCache, + text_config: &TextConfig, + ) { + (self.render_fn)(ctx, root_transform, glyph_caches, image_cache, text_config); - // Draw tile grid overlay if enabled if self.show_widetile_columns { self.draw_widetile_columns(ctx); }
diff --git a/sparse_strips/vello_example_scenes/src/multi_image.rs b/sparse_strips/vello_example_scenes/src/multi_image.rs index 7c58ea2..77bcb86 100644 --- a/sparse_strips/vello_example_scenes/src/multi_image.rs +++ b/sparse_strips/vello_example_scenes/src/multi_image.rs
@@ -4,7 +4,10 @@ //! Scene that renders multiple axis-aligned images at randomized positions across the viewport. //! Press "a"/"A" to add 50/1 images, "d"/"D" to remove 50/1 images. -use crate::{ExampleScene, RenderingContext}; +use core::any::Any; +use parley_draw::ImageCache; + +use crate::{ExampleScene, RenderingContext, TextConfig}; use std::fmt::{Debug, Formatter, Result}; use vello_common::color::palette::css::WHITE; use vello_common::kurbo::{Affine, Rect, Vec2}; @@ -71,7 +74,14 @@ } impl ExampleScene for MultiImageScene { - fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) { + fn render( + &mut self, + ctx: &mut impl RenderingContext, + root_transform: Affine, + _glyph_caches: &mut dyn Any, + _image_cache: &mut ImageCache, + _text_config: &TextConfig, + ) { let vw = ctx.width() as f64; let vh = ctx.height() as f64; let t = root_transform.translation();
diff --git a/sparse_strips/vello_example_scenes/src/path.rs b/sparse_strips/vello_example_scenes/src/path.rs index 54e124d..d4f87c7 100644 --- a/sparse_strips/vello_example_scenes/src/path.rs +++ b/sparse_strips/vello_example_scenes/src/path.rs
@@ -10,7 +10,10 @@ //! - `fill_types` method //! - `robust_paths` method -use crate::{ExampleScene, RenderingContext}; +use core::any::Any; +use parley_draw::ImageCache; + +use crate::{ExampleScene, RenderingContext, TextConfig}; use vello_common::color::palette::css::{AQUA, BLUE, GRAY, LIME, YELLOW}; use vello_common::kurbo::{Affine, BezPath, Cap, Join, Point, Rect, Shape, Stroke}; use vello_common::peniko::{Color, Fill}; @@ -48,7 +51,14 @@ const X_OFFSET: f64 = 450.0; impl ExampleScene for StrokeStylesScene { - fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) { + fn render( + &mut self, + ctx: &mut impl RenderingContext, + root_transform: Affine, + _glyph_caches: &mut dyn Any, + _image_cache: &mut ImageCache, + _text_config: &TextConfig, + ) { let colors = [ Color::from_rgb8(140, 181, 236), Color::from_rgb8(246, 236, 202), @@ -193,7 +203,14 @@ // TODO: fix issue https://github.com/linebender/vello/issues/1240 impl ExampleScene for FunkyPathsScene { - fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) { + fn render( + &mut self, + ctx: &mut impl RenderingContext, + root_transform: Affine, + _glyph_caches: &mut dyn Any, + _image_cache: &mut ImageCache, + _text_config: &TextConfig, + ) { // Missing movetos path let mut missing_movetos = BezPath::new(); missing_movetos.move_to((0.0, 0.0)); @@ -242,7 +259,14 @@ } impl ExampleScene for TrickyStrokesScene { - fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) { + fn render( + &mut self, + ctx: &mut impl RenderingContext, + root_transform: Affine, + _glyph_caches: &mut dyn Any, + _image_cache: &mut ImageCache, + _text_config: &TextConfig, + ) { let colors = [ Color::from_rgb8(140, 181, 236), Color::from_rgb8(246, 236, 202), @@ -372,7 +396,14 @@ } impl ExampleScene for FillTypesScene { - fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) { + fn render( + &mut self, + ctx: &mut impl RenderingContext, + root_transform: Affine, + _glyph_caches: &mut dyn Any, + _image_cache: &mut ImageCache, + _text_config: &TextConfig, + ) { let rect = Rect::from_origin_size(Point::new(0.0, 0.0), (500.0, 500.0)); // Create star path @@ -469,7 +500,14 @@ } impl ExampleScene for RobustPathsScene { - fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) { + fn render( + &mut self, + ctx: &mut impl RenderingContext, + root_transform: Affine, + _glyph_caches: &mut dyn Any, + _image_cache: &mut ImageCache, + _text_config: &TextConfig, + ) { let mut path = BezPath::new(); path.move_to((16.0, 16.0)); path.line_to((32.0, 16.0));
diff --git a/sparse_strips/vello_example_scenes/src/simple.rs b/sparse_strips/vello_example_scenes/src/simple.rs index 23539fc..3e9357e 100644 --- a/sparse_strips/vello_example_scenes/src/simple.rs +++ b/sparse_strips/vello_example_scenes/src/simple.rs
@@ -3,17 +3,26 @@ //! Simple example scene with basic shapes. +use core::any::Any; +use parley_draw::ImageCache; use vello_common::kurbo::{Affine, BezPath, Stroke}; use vello_common::peniko::color::palette; -use crate::{ExampleScene, RenderingContext}; +use crate::{ExampleScene, RenderingContext, TextConfig}; /// Simple scene state #[derive(Debug)] pub struct SimpleScene {} impl ExampleScene for SimpleScene { - fn render(&mut self, target: &mut impl RenderingContext, root_transform: Affine) { + fn render( + &mut self, + target: &mut impl RenderingContext, + root_transform: Affine, + _glyph_caches: &mut dyn Any, + _image_cache: &mut ImageCache, + _text_config: &TextConfig, + ) { render(target, root_transform); } }
diff --git a/sparse_strips/vello_example_scenes/src/svg.rs b/sparse_strips/vello_example_scenes/src/svg.rs index 9165ebb..afb34c3 100644 --- a/sparse_strips/vello_example_scenes/src/svg.rs +++ b/sparse_strips/vello_example_scenes/src/svg.rs
@@ -3,6 +3,8 @@ //! SVG rendering example scene. +use core::any::Any; +use parley_draw::ImageCache; use std::fmt; use vello_common::kurbo::{Affine, Stroke}; use vello_common::pico_svg::{Item, PicoSvg}; @@ -11,7 +13,7 @@ #[cfg(not(target_arch = "wasm32"))] use std::path::{Path, PathBuf}; -use crate::{ExampleScene, RenderingContext}; +use crate::{ExampleScene, RenderingContext, TextConfig}; /// SVG scene that renders an SVG file pub struct SvgScene { @@ -30,7 +32,14 @@ } impl ExampleScene for SvgScene { - fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) { + fn render( + &mut self, + ctx: &mut impl RenderingContext, + root_transform: Affine, + _glyph_caches: &mut dyn Any, + _image_cache: &mut ImageCache, + _text_config: &TextConfig, + ) { let current_transform = root_transform * self.transform; if self.recording_enabled {
diff --git a/sparse_strips/vello_example_scenes/src/text.rs b/sparse_strips/vello_example_scenes/src/text.rs index de70a53..9c45eca 100644 --- a/sparse_strips/vello_example_scenes/src/text.rs +++ b/sparse_strips/vello_example_scenes/src/text.rs
@@ -13,19 +13,55 @@ Alignment, AlignmentOptions, FontContext, FontFamily, FontWeight, GenericFamily, GlyphRun, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, }; -use parley_draw::Glyph; +use parley_draw::{Glyph, ImageCache}; use vello_common::kurbo::{Affine, Rect, Vec2}; use vello_common::peniko::Color; -use crate::{ExampleScene, RenderingContext}; +use crate::{ExampleScene, RenderingContext, TextConfig}; -const PADDING: u32 = 20; -const MAX_ADVANCE: f32 = 200.0; -const FONT_SIZE: f32 = 16.0; +const PADDING: u32 = 100; +const MAX_ADVANCE: f32 = 1780.0; +const FONT_SIZE: f32 = 32.0; const SIMPLE_TEXT: &str = "Some text here. Let's make it a bit longer so that \ line wrapping kicks in easily. This demonstrates basic glyph caching with \ - plain Latin text and common punctuation???"; + plain Latin text and common punctuation??? The quick brown fox jumps over \ + the lazy dog. Pack my box with five dozen liquor jugs. How vexingly quick \ + daft zebras jump! The five boxing wizards jump quickly. Sphinx of black \ + quartz, judge my vow. Two driven jocks help fax my big quiz. The jay, pig, \ + fox, zebra and my wolves quack! Crazy Frederick bought many very exquisite \ + opal jewels. We promptly judged antique ivory buckles for the next prize. \ + A mad boxer shot a quick, gloved jab to the jaw of his dizzy opponent. \ + Jived fox nymph grabs quick waltz. Glib jocks quiz nymph to vex dwarf. \ + How quickly daft jumping zebras vex! Jackdaws love my big sphinx of quartz. \ + The quick brown fox jumps over the lazy dog again and again and again. \ + Amazingly few discotheques provide jukeboxes. My girl wove six dozen plaid \ + jackets before she quit. Six big devils from Japan quickly forgot how to \ + waltz. Big July earthquakes confound zany experimental vow. Foxy parsons \ + quiz and cajole the lovably dim wiki-Loss. Have a pick: twenty-six letters, \ + no more, no less. Each sentence is a pangram, using every letter at least \ + once. This block of text is designed to stress test glyph caching by \ + exercising the full Latin alphabet repeatedly across many lines of wrapped \ + text at a small font size, ensuring the atlas must handle hundreds of glyph \ + instances with varying subpixel positions. Some text here. Let's make it a bit longer so that \ + line wrapping kicks in easily. This demonstrates basic glyph caching with \ + plain Latin text and common punctuation??? The quick brown fox jumps over \ + the lazy dog. Pack my box with five dozen liquor jugs. How vexingly quick \ + daft zebras jump! The five boxing wizards jump quickly. Sphinx of black \ + quartz, judge my vow. Two driven jocks help fax my big quiz. The jay, pig, \ + fox, zebra and my wolves quack! Crazy Frederick bought many very exquisite \ + opal jewels. We promptly judged antique ivory buckles for the next prize. \ + A mad boxer shot a quick, gloved jab to the jaw of his dizzy opponent. \ + Jived fox nymph grabs quick waltz. Glib jocks quiz nymph to vex dwarf. \ + How quickly daft jumping zebras vex! Jackdaws love my big sphinx of quartz. \ + The quick brown fox jumps over the lazy dog again and again and again. \ + Amazingly few discotheques provide jukeboxes. My girl wove six dozen plaid \ + jackets before she quit. Six big devils from Japan quickly forgot how to \ + waltz. Big July earthquakes confound zany experimental vow."; + +// const SIMPLE_TEXT: &str = "Some text here. Let's make it a bit longer so that \ +// line wrapping kicks in easily. This demonstrates basic glyph caching with \ +// plain Latin text and common punctuation???"; const RICH_TEXT: &str = "Some text here. Let's make it a bit longer so that \ line wrapping kicks in. Bitmap emoji 😊 and COLR emoji 🎉.\n\ @@ -53,8 +89,7 @@ /// State for the text example. pub struct TextScene { layout: Layout<ColorBrush>, - /// Type-erased glyph caches, lazily initialized per backend. - glyph_caches: Option<Box<dyn Any>>, + repeat_count: u32, } impl fmt::Debug for TextScene { @@ -64,28 +99,47 @@ } impl ExampleScene for TextScene { - fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) { - if self.glyph_caches.is_none() { - self.glyph_caches = Some(ctx.create_glyph_caches()); - } + fn render( + &mut self, + ctx: &mut impl RenderingContext, + root_transform: Affine, + glyph_caches: &mut dyn Any, + image_cache: &mut ImageCache, + text_config: &TextConfig, + ) { + for i in 0..self.repeat_count { + let offset = i as f64 * 10.0; + let content_transform = root_transform + * Affine::translate(Vec2::new(PADDING as f64 + offset, PADDING as f64 + offset)); + ctx.set_transform(content_transform); - let content_transform = - root_transform * Affine::translate(Vec2::new(PADDING as f64, PADDING as f64)); - ctx.set_transform(content_transform); - - let glyph_caches = self - .glyph_caches - .as_mut() - .expect("glyph caches not initialized"); - - for line in self.layout.lines() { - for item in line.items() { - if let PositionedLayoutItem::GlyphRun(glyph_run) = item { - render_glyph_run(ctx, &glyph_run, glyph_caches.as_mut()); + for line in self.layout.lines() { + for item in line.items() { + if let PositionedLayoutItem::GlyphRun(glyph_run) = item { + render_glyph_run(ctx, &glyph_run, glyph_caches, image_cache, text_config); + } } } } } + + fn handle_key(&mut self, key: &str) -> bool { + match key { + "+" | "=" => { + self.repeat_count += 1; + true + } + "-" | "_" => { + self.repeat_count = self.repeat_count.saturating_sub(1).max(1); + true + } + _ => false, + } + } + + fn status(&self) -> Option<String> { + Some(format!("repeats: {}", self.repeat_count)) + } } impl TextScene { @@ -103,7 +157,7 @@ Self { layout, - glyph_caches: None, + repeat_count: 1, } } @@ -121,7 +175,7 @@ Self { layout, - glyph_caches: None, + repeat_count: 1, } } } @@ -218,6 +272,8 @@ ctx: &mut impl RenderingContext, glyph_run: &GlyphRun<'_, ColorBrush>, glyph_caches: &mut dyn Any, + image_cache: &mut ImageCache, + text_config: &TextConfig, ) { let style = glyph_run.style(); ctx.set_paint(style.brush.color); @@ -227,22 +283,18 @@ let font_size = run.font_size(); let normalized_coords = run.normalized_coords(); - let glyphs: Vec<Glyph> = glyph_run - .positioned_glyphs() - .map(|g| Glyph { - id: u32::from(g.id), - x: g.x, - y: g.y, - }) - .collect(); - ctx.fill_glyphs( font, font_size, - true, normalized_coords, - glyphs.into_iter(), + glyph_run.positioned_glyphs().map(|g| Glyph { + id: u32::from(g.id), + x: g.x, + y: g.y, + }), glyph_caches, + image_cache, + text_config, ); if let Some(decoration) = &style.underline {
diff --git a/sparse_strips/vello_hybrid/Cargo.toml b/sparse_strips/vello_hybrid/Cargo.toml index c3eb0c7..ecfd779 100644 --- a/sparse_strips/vello_hybrid/Cargo.toml +++ b/sparse_strips/vello_hybrid/Cargo.toml
@@ -54,6 +54,7 @@ # with the features which you need enabled. wgpu_default = ["wgpu", "wgpu/default"] webgl = ["dep:js-sys", "dep:web-sys", "dep:vello_sparse_shaders", "vello_sparse_shaders/glsl"] +text = [] [lints] workspace = true
diff --git a/sparse_strips/vello_hybrid/examples/winit/Cargo.toml b/sparse_strips/vello_hybrid/examples/winit/Cargo.toml index 23e8426..f233dc3 100644 --- a/sparse_strips/vello_hybrid/examples/winit/Cargo.toml +++ b/sparse_strips/vello_hybrid/examples/winit/Cargo.toml
@@ -15,4 +15,5 @@ vello_common = { workspace = true } vello_hybrid = { workspace = true } vello_example_scenes = { workspace = true } +parley_draw = { workspace = true, features = ["std", "vello_hybrid"] } pollster = { workspace = true }
diff --git a/sparse_strips/vello_hybrid/examples/winit/src/main.rs b/sparse_strips/vello_hybrid/examples/winit/src/main.rs index 113967f..f81641f 100644 --- a/sparse_strips/vello_hybrid/examples/winit/src/main.rs +++ b/sparse_strips/vello_hybrid/examples/winit/src/main.rs
@@ -4,6 +4,8 @@ //! Renders our example scenes with Vello Hybrid. mod render_context; +use parley_draw::renderers::vello_renderer::replay_atlas_commands; +use parley_draw::{GLYPH_PADDING, GlyphCache, GlyphCacheConfig, GpuGlyphCaches, PendingClearRect}; use render_context::{RenderContext, RenderSurface, create_vello_renderer, create_winit_window}; #[cfg(not(target_arch = "wasm32"))] use std::env; @@ -13,8 +15,8 @@ use vello_common::paint::ImageId; use vello_common::paint::ImageSource; use vello_example_scenes::image::ImageScene; -use vello_example_scenes::{AnyScene, get_example_scenes}; -use vello_hybrid::{Pixmap, RenderSize, Renderer, Scene}; +use vello_example_scenes::{AnyScene, TextConfig, get_example_scenes}; +use vello_hybrid::{AtlasId, Pixmap, RenderSize, Renderer, Scene}; use winit::{ application::ApplicationHandler, event::{ElementState, KeyEvent, MouseButton, MouseScrollDelta, WindowEvent}, @@ -32,6 +34,9 @@ renderers: Vec<Option<Renderer>>, render_state: RenderState<'s>, scene: Scene, + glyph_renderer: Scene, + glyph_caches: GpuGlyphCaches, + text_config: TextConfig, transform: Affine, mouse_down: bool, last_cursor_position: Option<Point>, @@ -40,6 +45,9 @@ fps_update_time: Instant, accumulated_frame_time: f64, accumulated_render_time: f64, + accumulated_acquire_time: f64, + accumulated_scene_time: f64, + accumulated_gpu_time: f64, } fn main() { @@ -90,6 +98,12 @@ current_scene: start_scene_index, render_state: RenderState::Suspended(None), scene: Scene::new(1800, 1200), + glyph_renderer: Scene::new(4096, 4096), + glyph_caches: GpuGlyphCaches::with_config(GlyphCacheConfig { + max_entry_age: u32::MAX, + eviction_frequency: u32::MAX, + }), + text_config: TextConfig::default(), transform: Affine::IDENTITY, mouse_down: false, last_cursor_position: None, @@ -98,6 +112,9 @@ fps_update_time: now, accumulated_frame_time: 0.0, accumulated_render_time: 0.0, + accumulated_acquire_time: 0.0, + accumulated_scene_time: 0.0, + accumulated_gpu_time: 0.0, }; let event_loop = EventLoop::new().unwrap(); @@ -213,13 +230,27 @@ Key::Named(NamedKey::Escape) => { event_loop.exit(); } - Key::Character(ch) => { - if let Some(scene) = self.scenes.get_mut(self.current_scene) - && scene.handle_key(ch.as_str()) - { + Key::Character(ch) => match ch.as_str() { + "a" | "A" => { + self.text_config.use_atlas_cache = !self.text_config.use_atlas_cache; + println!( + "Atlas cache: {}", + if self.text_config.use_atlas_cache { + "ON" + } else { + "OFF" + } + ); window.request_redraw(); } - } + _ => { + if let Some(scene) = self.scenes.get_mut(self.current_scene) + && scene.handle_key(ch.as_str()) + { + window.request_redraw(); + } + } + }, _ => {} }, WindowEvent::MouseInput { state, button, .. } => { @@ -292,58 +323,114 @@ let avg_fps = 1000.0 / avg_frame_time; let avg_render_time = self.accumulated_render_time / self.frame_count as f64; + let avg_acquire_time = + self.accumulated_acquire_time / self.frame_count as f64; + let avg_scene_time = self.accumulated_scene_time / self.frame_count as f64; + let avg_gpu_time = self.accumulated_gpu_time / self.frame_count as f64; let status = self.scenes[self.current_scene] .status() .map(|s| format!(" - {s}")) .unwrap_or_default(); println!( - "FPS: {avg_fps:.1} | render: {avg_render_time:.2}ms | frame: {avg_frame_time:.2}ms{status}" + "FPS: {avg_fps:.1} | scene: {avg_scene_time:.2}ms | gpu: {avg_gpu_time:.2}ms | render: {avg_render_time:.2}ms | acquire: {avg_acquire_time:.2}ms | frame: {avg_frame_time:.2}ms{status}" ); + let atlas_label = if self.text_config.use_atlas_cache { + "atlas ON" + } else { + "atlas OFF" + }; window.set_title(&format!( - "Vello Hybrid - Scene {} - {:.1} FPS (render {:.2}ms){status}", - self.current_scene, avg_fps, avg_render_time + "Vello Hybrid - Scene {} - {:.1} FPS (scene {:.2}ms, gpu {:.2}ms, acquire {:.2}ms) [{atlas_label}]{status}", + self.current_scene, avg_fps, avg_scene_time, avg_gpu_time, avg_acquire_time )); // Reset counters self.frame_count = 0; self.accumulated_frame_time = 0.0; self.accumulated_render_time = 0.0; + self.accumulated_acquire_time = 0.0; + self.accumulated_scene_time = 0.0; + self.accumulated_gpu_time = 0.0; self.fps_update_time = now; } } self.last_frame_time = Some(now); - self.scene.reset(); - - let render_start = Instant::now(); - - self.scene.set_transform(self.transform); - self.scenes[self.current_scene].render(&mut self.scene, self.transform); - - let device_handle = &self.context.devices[surface.dev_id]; let render_size = RenderSize { width: surface.config.width, height: surface.config.height, }; + let acquire_start = Instant::now(); let surface_texture = surface .surface .get_current_texture() .expect("failed to get surface texture"); - let texture_view = surface_texture .texture .create_view(&wgpu::TextureViewDescriptor::default()); + self.accumulated_acquire_time += acquire_start.elapsed().as_secs_f64() * 1000.0; + let render_start = Instant::now(); + + let renderer = self.renderers[surface.dev_id].as_mut().unwrap(); + + let scene_start = Instant::now(); + self.scene.reset(); + self.scene.set_transform(self.transform); + self.scenes[self.current_scene].render( + &mut self.scene, + self.transform, + &mut self.glyph_caches, + &mut renderer.image_cache, + &self.text_config, + ); + self.accumulated_scene_time += scene_start.elapsed().as_secs_f64() * 1000.0; + + let device_handle = &self.context.devices[surface.dev_id]; + + let gpu_start = Instant::now(); + // Replay outline/COLR draw commands into each atlas page. + let pending_cmds = self.glyph_caches.glyph_atlas.take_pending_atlas_commands(); + for mut recorder in pending_cmds { + self.glyph_renderer.reset(); + replay_atlas_commands(&mut recorder.commands, &mut self.glyph_renderer); + renderer + .render_to_atlas( + &self.glyph_renderer, + &device_handle.device, + &device_handle.queue, + AtlasId::new(recorder.page_index), + ) + .expect("Failed to render glyphs to atlas"); + } + + // Upload bitmap glyphs to the GPU atlas. + let padding = u32::from(GLYPH_PADDING); let mut encoder = device_handle .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Vello Render to Surface pass"), + label: Some("Glyph bitmap upload"), }); - self.renderers[surface.dev_id] - .as_mut() - .unwrap() + for upload in self.glyph_caches.glyph_atlas.take_pending_uploads() { + let resource = renderer + .image_cache + .get(upload.image_id) + .expect("Bitmap image not found in cache"); + let dst_x = resource.offset[0] as u32 + padding; + let dst_y = resource.offset[1] as u32 + padding; + renderer.write_to_atlas( + &device_handle.device, + &device_handle.queue, + &mut encoder, + upload.image_id, + &upload.pixmap, + Some([dst_x, dst_y]), + ); + } + + renderer .render( &self.scene, &device_handle.device, @@ -357,7 +444,17 @@ device_handle.queue.submit([encoder.finish()]); surface_texture.present(); + // Maintain caches (eviction, etc.) + self.glyph_caches.maintain(&mut renderer.image_cache); + + clear_atlas_regions( + &device_handle.queue, + renderer, + &self.glyph_caches.glyph_atlas.take_pending_clear_rects(), + ); + device_handle.device.poll(wgpu::PollType::Poll).unwrap(); + self.accumulated_gpu_time += gpu_start.elapsed().as_secs_f64() * 1000.0; self.accumulated_render_time += render_start.elapsed().as_secs_f64() * 1000.0; @@ -369,6 +466,40 @@ } } +/// Zero out atlas regions on the GPU after eviction. +fn clear_atlas_regions(queue: &wgpu::Queue, renderer: &Renderer, rects: &[PendingClearRect]) { + if rects.is_empty() { + return; + } + let atlas_texture = renderer.atlas_texture(); + for rect in rects { + let zeroed = vec![0_u8; rect.width as usize * rect.height as usize * 4]; + queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: atlas_texture, + mip_level: 0, + origin: wgpu::Origin3d { + x: rect.x as u32, + y: rect.y as u32, + z: rect.page_index, + }, + aspect: wgpu::TextureAspect::All, + }, + &zeroed, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(rect.width as u32 * 4), + rows_per_image: None, + }, + wgpu::Extent3d { + width: rect.width as u32, + height: rect.height as u32, + depth_or_array_layers: 1, + }, + ); + } +} + impl App<'_> { fn upload_images_to_atlas(&mut self, device_id: usize) { let device_handle = &self.context.devices[device_id];
diff --git a/sparse_strips/vello_hybrid/src/render/wgpu.rs b/sparse_strips/vello_hybrid/src/render/wgpu.rs index 9d1ec2f..4865261 100644 --- a/sparse_strips/vello_hybrid/src/render/wgpu.rs +++ b/sparse_strips/vello_hybrid/src/render/wgpu.rs
@@ -145,7 +145,16 @@ render_size: &RenderSize, view: &TextureView, ) -> Result<(), RenderError> { - self.render_scene(scene, device, queue, encoder, render_size, view, true) + self.render_scene( + scene, + device, + queue, + encoder, + render_size, + view, + true, + false, + ) } /// Render a `scene` directly into an atlas layer. @@ -215,10 +224,6 @@ &mut self.programs.resources.stub_atlas_bind_group, ); - // TODO: The atlas is always RGBA8; when the surface uses a different format (e.g. BGRA on - // macOS), we may need a dedicated RGBA8 render pipeline for atlas rendering. Adopt the - // fix from the filters/native-format pipeline work when available. - let result = self.render_scene( scene, device, @@ -227,6 +232,7 @@ &atlas_render_size, &layer_view, false, + true, ); // Restore the real atlas bind group. @@ -256,6 +262,7 @@ render_size: &RenderSize, view: &TextureView, clear: bool, + use_atlas_pipeline: bool, ) -> Result<(), RenderError> { self.prepare_gpu_encoded_paints(&scene.encoded_paints); // TODO: For the time being, we upload the entire alpha buffer as one big chunk. As a future @@ -286,6 +293,7 @@ queue, encoder, view, + use_atlas_pipeline, }; let load_op = if clear { LoadOp::Clear } else { LoadOp::Load }; ctx.render_strips(&self.fast_path_gpu_strips, 2, load_op); @@ -298,6 +306,7 @@ queue, encoder, view, + use_atlas_pipeline, }; self.scheduler .do_scene(&mut self.scheduler_state, &mut ctx, scene, &self.paint_idxs) @@ -613,6 +622,8 @@ struct Programs { /// Pipeline for rendering wide tile commands. strip_pipeline: RenderPipeline, + /// Pipeline for rendering strips into the atlas (always Rgba8Unorm). + atlas_strip_pipeline: RenderPipeline, /// Bind group layout for strip draws strip_bind_group_layout: BindGroupLayout, /// Bind group layout for encoded paints @@ -871,6 +882,39 @@ cache: None, }); + let atlas_strip_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Atlas Strip Pipeline"), + layout: Some(&strip_pipeline_layout), + vertex: wgpu::VertexState { + module: &strip_shader, + entry_point: Some("vs_main"), + buffers: &[wgpu::VertexBufferLayout { + array_stride: size_of::<GpuStrip>() as u64, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &GpuStrip::vertex_attributes(), + }], + compilation_options: PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &strip_shader, + entry_point: Some("fs_main"), + targets: &[Some(ColorTargetState { + format: wgpu::TextureFormat::Rgba8Unorm, + blend: Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: ColorWrites::ALL, + })], + compilation_options: PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + cache: None, + }); + let clear_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("Clear Slots Pipeline"), layout: Some(&clear_pipeline_layout), @@ -1099,6 +1143,7 @@ Self { strip_pipeline, + atlas_strip_pipeline, strip_bind_group_layout, encoded_paints_bind_group_layout, gradient_bind_group_layout, @@ -1737,6 +1782,9 @@ queue: &'a Queue, encoder: &'a mut CommandEncoder, view: &'a TextureView, + /// When true, use the Rgba8Unorm atlas strip pipeline instead of the + /// surface-format strip pipeline. + use_atlas_pipeline: bool, } impl RendererContext<'_> { @@ -1774,7 +1822,12 @@ occlusion_query_set: None, timestamp_writes: None, }); - render_pass.set_pipeline(&self.programs.strip_pipeline); + let pipeline = if self.use_atlas_pipeline { + &self.programs.atlas_strip_pipeline + } else { + &self.programs.strip_pipeline + }; + render_pass.set_pipeline(pipeline); render_pass.set_bind_group(0, &self.programs.resources.slot_bind_groups[ix], &[]); render_pass.set_bind_group(1, &self.programs.resources.atlas_bind_group, &[]); render_pass.set_bind_group(2, &self.programs.resources.encoded_paints_bind_group, &[]);
diff --git a/sparse_strips/vello_hybrid/src/scene.rs b/sparse_strips/vello_hybrid/src/scene.rs index f103cbc..24e3e27 100644 --- a/sparse_strips/vello_hybrid/src/scene.rs +++ b/sparse_strips/vello_hybrid/src/scene.rs
@@ -454,6 +454,20 @@ } } + /// Fill a rectangle, bypassing the pixel-alignment fast-path check. + /// + /// Use this when the caller already knows the transform/rect combination is + /// *not* pixel-aligned (e.g. glyph atlas rendering with fractional bearing + /// offsets or non-identity paint transforms). This avoids the per-call cost + /// of `is_integer_translation` and `is_integer_rect`. + #[inline] + pub fn fill_rect_pixel_aligned(&mut self, rect: &Rect) { + if !self.paint_visible { + return; + } + self.fill_rect_fast(rect); + } + /// Fast path for filling a pixel-aligned rectangle. /// /// Bypasses path processing by generating strips directly for the rectangle.