diff --git a/sparse_strips/vello_common/src/coarse.rs b/sparse_strips/vello_common/src/coarse.rs index 91a931425c..de9372fd88 100644 --- a/sparse_strips/vello_common/src/coarse.rs +++ b/sparse_strips/vello_common/src/coarse.rs @@ -404,6 +404,11 @@ impl Wide { !self.layer_stack.is_empty() } + /// Whether any coarse batch boundaries have been recorded. + pub fn has_coarse_batches(&self) -> bool { + self.batch_count != 0 + } + /// Reset all tiles in the container. pub fn reset(&mut self) { if self.tiles_dirty { diff --git a/sparse_strips/vello_hybrid/src/render/webgl.rs b/sparse_strips/vello_hybrid/src/render/webgl.rs index 8958d0fec8..cb22932e7b 100644 --- a/sparse_strips/vello_hybrid/src/render/webgl.rs +++ b/sparse_strips/vello_hybrid/src/render/webgl.rs @@ -61,8 +61,8 @@ use vello_common::{ EncodedBlurredRoundedRectangle, EncodedGradient, EncodedKind, EncodedPaint, MAX_GRADIENT_LUT_SIZE, RadialKind, }, - paint::{ImageId, ImageSource}, - peniko::{self}, + paint::{ImageId, ImageSource, PremulColor}, + peniko::{self, color::palette::css::TRANSPARENT}, pixmap::Pixmap, tile::Tile, }; @@ -270,7 +270,9 @@ impl WebGlRenderer { scene, &mut resources.image_cache, render_size, - true, + scene + .background + .or(Some(PremulColor::from_alpha_color(TRANSPARENT))), RootRenderTarget::UserSurface, )?; @@ -353,7 +355,13 @@ impl WebGlRenderer { scene, &mut dummy_image_cache, &atlas_render_size, - false, + // Note: By default, we don't want to clear the atlas, since the existing glyphs + // should be preserved. The scene background is only set if we have an opaque colored + // rect which covers the whole viewport; meaning that this is only `Some` if for + // some reason the glyph atlas was filled with such a rectangle (which should never happen + // but in case it does happen it means that all existing glyphs are being covered by this + // rectangle anyway), so it's fine to just set the reset color to the scene background. + scene.background, RootRenderTarget::AtlasLayer, ); self.dummy_image_cache = Some(dummy_image_cache); @@ -465,7 +473,9 @@ impl WebGlRenderer { &scene, &mut probe_image_cache, &render_size, - true, + scene + .background + .or(Some(PremulColor::from_alpha_color(TRANSPARENT))), RootRenderTarget::AtlasLayer, ); self.programs.resources.view_framebuffer_override = previous_view_framebuffer; @@ -494,16 +504,14 @@ impl WebGlRenderer { /// Shared render pipeline: prepares GPU resources, runs the scheduler, and /// maintains caches. /// - /// When `clear` is true the view framebuffer is cleared to transparent black - /// before drawing. This must happen *after* `prepare` (which may create/resize - /// the framebuffer attachment). Atlas renders skip the clear so previously - /// rendered atlas content is preserved. + /// When `clear_color` is `Some`, the view framebuffer is cleared to that color before drawing. + /// This must happen *after* `prepare` (which may create/resize the framebuffer attachment). fn render_scene( &mut self, scene: &Scene, image_cache: &mut ImageCache, render_size: &RenderSize, - clear: bool, + clear_color: Option, root_output_target: RootRenderTarget, ) -> Result<(), RenderError> { if !self.filter_context.filter_textures.is_empty() { @@ -541,8 +549,8 @@ impl WebGlRenderer { &self.filter_context, ); - if clear { - self.programs.clear_view_framebuffer(&self.gl); + if let Some(clear_color) = clear_color { + self.programs.clear_view_framebuffer(&self.gl, clear_color); } self.programs.resources.depth_cleared_this_frame = false; let mut ctx = WebGlRendererContext { @@ -1568,12 +1576,14 @@ impl WebGlPrograms { /// Clear the view framebuffer. // TODO: Investigate adding tests for the clear_view behavior. - fn clear_view_framebuffer(&mut self, gl: &WebGl2RenderingContext) { + fn clear_view_framebuffer(&mut self, gl: &WebGl2RenderingContext, color: PremulColor) { + let [r, g, b, a] = color.as_premul_f32().components; + gl.bind_framebuffer( WebGl2RenderingContext::FRAMEBUFFER, self.resources.view_framebuffer_override.as_ref(), ); - gl.clear_color(0.0, 0.0, 0.0, 0.0); + gl.clear_color(r, g, b, a); gl.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT); } diff --git a/sparse_strips/vello_hybrid/src/render/wgpu.rs b/sparse_strips/vello_hybrid/src/render/wgpu.rs index 112f16243a..d3c654c175 100644 --- a/sparse_strips/vello_hybrid/src/render/wgpu.rs +++ b/sparse_strips/vello_hybrid/src/render/wgpu.rs @@ -54,8 +54,8 @@ use vello_common::{ EncodedBlurredRoundedRectangle, EncodedGradient, EncodedKind, EncodedPaint, MAX_GRADIENT_LUT_SIZE, RadialKind, }, - paint::ImageSource, - peniko, + paint::{ImageSource, PremulColor}, + peniko::{self, color::palette::css::TRANSPARENT}, pixmap::Pixmap, tile::Tile, }; @@ -291,7 +291,9 @@ impl Renderer { view, &resources.image_cache, &encoded_paints, - true, + scene + .background + .or(Some(PremulColor::from_alpha_color(TRANSPARENT))), RootRenderTarget::UserSurface, ); @@ -383,7 +385,8 @@ impl Renderer { &layer_view, &dummy_image_cache, &encoded_paints, - false, + // See the note in the WebGL backend. + scene.background, RootRenderTarget::AtlasLayer, ); self.dummy_image_cache = Some(dummy_image_cache); @@ -404,8 +407,7 @@ impl Renderer { /// Shared render pipeline: prepares GPU resources, runs the scheduler against /// the provided `view` at `render_size`, and maintains caches. /// - /// When `clear` is true the render target is cleared to transparent black - /// before drawing (normal frame rendering). + /// When `clear_color` is `Some`, the render target is cleared to that color before drawing. fn render_scene( &mut self, scene: &Scene, @@ -416,7 +418,7 @@ impl Renderer { view: &TextureView, image_cache: &ImageCache, encoded_paints: &[EncodedPaint], - clear: bool, + clear_color: Option, root_output_target: RootRenderTarget, ) -> Result<(), RenderError> { self.programs.depth_cleared_this_frame = false; @@ -435,8 +437,8 @@ impl Renderer { &self.filter_context, ); - if clear { - Self::clear_view(encoder, view); + if let Some(clear_color) = clear_color { + Self::clear_view(encoder, view, clear_color); } let mut ctx = RendererContext { programs: &mut self.programs, @@ -462,16 +464,22 @@ impl Renderer { Ok(()) } - /// Clear the view to transparent black. + /// Clear the view to the scene background. // TODO: Investigate adding tests for the clear_view behavior. - fn clear_view(encoder: &mut CommandEncoder, view: &TextureView) { + fn clear_view(encoder: &mut CommandEncoder, view: &TextureView, color: PremulColor) { + let [r, g, b, a] = color.as_premul_f32().components; encoder.begin_render_pass(&RenderPassDescriptor { label: Some("Clear View"), color_attachments: &[Some(RenderPassColorAttachment { view, resolve_target: None, ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + load: wgpu::LoadOp::Clear(wgpu::Color { + r: r.into(), + g: g.into(), + b: b.into(), + a: a.into(), + }), store: wgpu::StoreOp::Store, }, depth_slice: None, diff --git a/sparse_strips/vello_hybrid/src/scene.rs b/sparse_strips/vello_hybrid/src/scene.rs index 8207bb3745..ecc9d710b1 100644 --- a/sparse_strips/vello_hybrid/src/scene.rs +++ b/sparse_strips/vello_hybrid/src/scene.rs @@ -20,7 +20,7 @@ use vello_common::filter_effects::Filter; use vello_common::kurbo::{Affine, BezPath, Rect, Shape, Stroke}; use vello_common::mask::Mask; use vello_common::multi_atlas::AtlasConfig; -use vello_common::paint::{Paint, PaintType, Tint}; +use vello_common::paint::{Paint, PaintType, PremulColor, Tint}; #[cfg(feature = "text")] use vello_common::peniko::FontData; use vello_common::peniko::color::palette::css::BLACK; @@ -225,6 +225,8 @@ pub struct Scene { pub(crate) render_graph: RenderGraph, /// Current filter effect applied to individual draw operations. filter: Option, + /// The background color of the scene. + pub(crate) background: Option, /// A buffer that stores the strips of path drawing calls that are rendered directly /// to the surface, bypassing coarse rasterization. pub(crate) fast_strips_buffer: FastStripsBuffer, @@ -316,6 +318,7 @@ impl Scene { layer_id_next: 0, render_graph, filter: None, + background: None, fast_strips_buffer: FastStripsBuffer::default(), strip_path_mode: StripPathMode::FastOnly, coarse_batch_splits: Vec::new(), @@ -481,6 +484,10 @@ impl Scene { return; } + if self.try_set_background_from_rect(rect) { + return; + } + if self.try_fast_rect(rect) { return; } @@ -515,6 +522,37 @@ impl Scene { true } + #[inline] + fn try_set_background_from_rect(&mut self, rect: &Rect) -> bool { + let PaintType::Solid(color) = &self.render_state.paint else { + return false; + }; + + if !self.fast_strips_buffer.commands.is_empty() + || !self.constraints.use_default_blending_only() + || color.components[3] != 1.0 + || self.filter.is_some() + || self.wide.has_layers() + || self.wide.has_coarse_batches() + || self.clip_context.get().is_some() + || !is_axis_aligned(&self.render_state.transform) + { + return false; + } + + let transformed_rect = self.render_state.transform.transform_rect_bbox(*rect); + if transformed_rect.x0 <= 0.0 + && transformed_rect.y0 <= 0.0 + && transformed_rect.x1 >= f64::from(self.width) + && transformed_rect.y1 >= f64::from(self.height) + { + self.background = Some(PremulColor::from_alpha_color(*color)); + return true; + } + + false + } + #[expect( clippy::cast_possible_truncation, reason = "f64→f32 truncation is acceptable for pixel coordinates" @@ -929,6 +967,7 @@ impl Scene { self.encoded_paints.borrow_mut().clear(); self.render_state.reset(); + self.background = None; self.fast_strips_buffer.clear(); self.strip_path_mode = StripPathMode::FastOnly; @@ -1162,6 +1201,124 @@ mod tests { assert!(is_rect(&scene.fast_strips_buffer.commands[0])); } + #[test] + fn background_optimized_for_first_full_viewport_rect() { + let mut scene = default_blending_only(); + let color = Color::from_rgba8(255, 0, 0, 255); + scene.set_paint(color); + scene.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0)); + + assert_eq!(scene.background, Some(PremulColor::from_alpha_color(color))); + assert!(scene.fast_strips_buffer.commands.is_empty()); + } + + #[test] + fn background_optimized_for_scaled_up_full_viewport_rect() { + let mut scene = default_blending_only(); + let color = Color::from_rgba8(255, 0, 0, 255); + scene.set_paint(color); + scene.set_transform(Affine::scale(2.0)); + scene.fill_rect(&Rect::new(0.0, 0.0, 100.0, 100.0)); + + assert_eq!(scene.background, Some(PremulColor::from_alpha_color(color))); + assert!(scene.fast_strips_buffer.commands.is_empty()); + } + + #[test] + fn background_optimization_ignores_rect_with_transparency() { + let mut scene = default_blending_only(); + scene.set_paint(Color::from_rgba8(255, 0, 0, 128)); + scene.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0)); + + assert_eq!(scene.background, None); + assert_eq!(scene.fast_strips_buffer.commands.len(), 1); + } + + #[test] + fn background_optimization_requires_default_blending_constraint() { + let mut scene = unconstrained(); + scene.set_paint(Color::from_rgba8(255, 0, 0, 255)); + scene.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0)); + + assert_eq!(scene.background, None); + } + + #[test] + fn background_optimization_uses_newest_opaque_consecutive_rect() { + let mut scene = default_blending_only(); + let first = Color::from_rgba8(255, 0, 0, 255); + let second = Color::from_rgba8(0, 0, 255, 255); + scene.set_paint(first); + scene.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0)); + + scene.set_paint(second); + scene.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0)); + + assert_eq!( + scene.background, + Some(PremulColor::from_alpha_color(second)) + ); + assert!(scene.fast_strips_buffer.commands.is_empty()); + } + + #[test] + fn background_optimization_stops_after_normal_shape() { + let mut scene = default_blending_only(); + let first = Color::from_rgba8(255, 0, 0, 255); + scene.set_paint(first); + scene.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0)); + + scene.set_paint(Color::from_rgba8(0, 255, 0, 255)); + scene.fill_path(&triangle_path()); + scene.set_paint(Color::from_rgba8(0, 0, 255, 255)); + scene.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0)); + + assert_eq!(scene.background, Some(PremulColor::from_alpha_color(first))); + assert_eq!(scene.fast_strips_buffer.commands.len(), 2); + } + + #[test] + fn background_optimization_rejects_after_layer_rendering() { + let mut scene = default_blending_only(); + scene.set_paint(Color::from_rgba8(255, 0, 0, 255)); + scene.push_layer(None, None, Some(0.5), None, None); + scene.fill_rect(&small_rect()); + scene.pop_layer(); + + scene.set_paint(Color::from_rgba8(0, 0, 255, 255)); + scene.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0)); + + assert_eq!(scene.background, None); + assert_eq!(scene.fast_strips_buffer.commands.len(), 1); + assert!(is_rect(&scene.fast_strips_buffer.commands[0])); + } + + #[test] + fn background_optimization_rejects_after_empty_layer() { + let mut scene = default_blending_only(); + scene.push_layer(None, None, Some(0.5), None, None); + scene.pop_layer(); + + scene.set_paint(Color::from_rgba8(0, 0, 255, 255)); + scene.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0)); + + assert_eq!(scene.background, None); + assert_eq!(scene.fast_strips_buffer.commands.len(), 1); + assert!(is_rect(&scene.fast_strips_buffer.commands[0])); + } + + #[test] + fn background_optimization_rejects_inside_active_layer() { + let mut scene = default_blending_only(); + scene.push_layer(None, None, Some(0.5), None, None); + + scene.set_paint(Color::from_rgba8(0, 0, 255, 255)); + scene.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0)); + + assert_eq!(scene.background, None); + assert!(scene.fast_strips_buffer.commands.is_empty()); + } + #[test] fn rect_rejected_by_clip_path() { let mut scene = unconstrained(); diff --git a/sparse_strips/vello_sparse_tests/tests/util.rs b/sparse_strips/vello_sparse_tests/tests/util.rs index 7260750208..d67dca7ea6 100644 --- a/sparse_strips/vello_sparse_tests/tests/util.rs +++ b/sparse_strips/vello_sparse_tests/tests/util.rs @@ -14,7 +14,7 @@ use std::cmp::max; use std::sync::Arc; use vello_common::color::DynamicColor; use vello_common::color::palette::css::{BLUE, GREEN, RED, WHITE, YELLOW}; -use vello_common::kurbo::{BezPath, Join, Point, Rect, Shape, Stroke, Vec2}; +use vello_common::kurbo::{BezPath, Join, Point, Rect, Stroke, Vec2}; use vello_common::peniko::{Blob, ColorStop, ColorStops, FontData}; use vello_common::pixmap::Pixmap; use vello_cpu::{Level, RenderMode}; @@ -156,10 +156,8 @@ pub(crate) fn get_ctx( ); if !transparent { - let path = Rect::new(0.0, 0.0, width as f64, height as f64).to_path(0.1); - ctx.set_paint(WHITE); - ctx.fill_path(&path); + ctx.fill_rect(&Rect::new(0.0, 0.0, width as f64, height as f64)); } ctx