From 4ede40dbecd142d5d9fb9647b4449541b3f97a05 Mon Sep 17 00:00:00 2001 From: Adoo Date: Sun, 26 May 2024 15:29:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(painter):=20=F0=9F=8E=B8Add=20support=20fo?= =?UTF-8?q?r=20drawing=20bundled=20commands=20as=20a=20single=20command=20?= =?UTF-8?q?in=20Painter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 10 +- core/src/test_helper.rs | 5 +- core/src/window.rs | 6 +- dev-helper/src/image_test.rs | 7 +- dev-helper/src/widget_test.rs | 4 +- geom/src/lib.rs | 9 + gpu/src/gpu_backend.rs | 309 +++++++++++++----- gpu/src/gpu_backend/atlas.rs | 4 +- gpu/src/gpu_backend/textures_mgr.rs | 89 +++-- gpu/src/lib.rs | 4 +- painter/Cargo.toml | 2 +- painter/src/painter.rs | 111 +++++-- painter/src/svg.rs | 32 +- ribir/src/backends/wgpu_backend.rs | 15 +- ribir/src/winit_shell_wnd.rs | 16 +- .../tests/draw_bundle_svg_wgpu.png | Bin 0 -> 84255 bytes .../tests/checked_with_default_by_wgpu.png | Bin 764 -> 798 bytes .../tests/checked_with_material_by_wgpu.png | Bin 764 -> 798 bytes .../indeterminate_with_default_by_wgpu.png | Bin 578 -> 600 bytes .../indeterminate_with_material_by_wgpu.png | Bin 578 -> 600 bytes .../tests/unchecked_with_default_by_wgpu.png | Bin 537 -> 558 bytes .../tests/unchecked_with_material_by_wgpu.png | Bin 537 -> 558 bytes tests/include_svg_test.rs | 2 +- text/src/font_db.rs | 2 +- 24 files changed, 430 insertions(+), 197 deletions(-) create mode 100644 test_cases/ribir_gpu/gpu_backend/tests/draw_bundle_svg_wgpu.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eb376327..9bf6784aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,10 +41,13 @@ Check out our Wordle game demo, now running smoothly in your browser! - **gpu**: Updated the `wgpu` implementation of the GPU backend to support WebGL. (#578, @M-Adoo) - **ci**: add wasm test (#583 @wjian23) - **ci**: wasm server watch file change (#586 @wjian23) +- **painter**: Introduced support for `Resource` for drawing. This indicates that the `Path` may be shared with others, allowing the backend to cache it. (#589 @M-Adoo) +- **painter**: Introduced support for bundled commands, enabling the backend to process these commands as a single entity and cache the resulting output. (#589 @M-Adoo) ### Fixed - **gpu**: Retrieve the texture limit size from the GPU instead of using a hardcoded value. (#578, @M-Adoo) +- **ribir**: fixed the crash issue when the shell window is zero-sized at startup. (#582, @M-Adoo) ### Changed @@ -56,11 +59,8 @@ Check out our Wordle game demo, now running smoothly in your browser! - **painter**: Removed the AntiAliasing feature from the `painter` package, This responsibility now lies with the painter backend. (#584 @M-Adoo) - **gpu**: The GPU backend no longer relies on MSAA, which is dependent on the graphics API. Instead, it uses the alpha atlas to provide a solution similar to SSAA.(#584, @M-Adoo) - **example**: run example in web wasm (#571 @wjian23) - - -### Fixed - -- **ribir**: fixed the crash issue when the shell window is zero-sized at startup. (#582, @M-Adoo) +- **gpu**: The GPU backend now only caches the path command if it is a `Resource`. This change reduces GPU memory usage and accelerates cache detection. (#589 @M-Adoo) +- **text**: Implemented caching of the glyph path as a `Resource` to improve performance. (#589 @M-Adoo) ### Documented diff --git a/core/src/test_helper.rs b/core/src/test_helper.rs index ec4469814..1d86baef5 100644 --- a/core/src/test_helper.rs +++ b/core/src/test_helper.rs @@ -161,8 +161,9 @@ impl ShellWindow for TestShellWindow { fn begin_frame(&mut self, surface: Color) { self.surface_color = surface; } - fn draw_commands(&mut self, viewport: Rect, commands: Vec) { - self.last_frame = Some(Frame { commands, viewport, surface: self.surface_color }); + fn draw_commands(&mut self, viewport: Rect, commands: &[PaintCommand]) { + self.last_frame = + Some(Frame { commands: commands.to_owned(), viewport, surface: self.surface_color }); } fn end_frame(&mut self) {} diff --git a/core/src/window.rs b/core/src/window.rs index 63a2aa015..a876ff0a4 100644 --- a/core/src/window.rs +++ b/core/src/window.rs @@ -85,7 +85,7 @@ pub trait ShellWindow { /// device. fn device_pixel_ratio(&self) -> f32; fn begin_frame(&mut self, surface_color: Color); - fn draw_commands(&mut self, viewport: Rect, commands: Vec); + fn draw_commands(&mut self, viewport: Rect, commands: &[PaintCommand]); fn end_frame(&mut self); } @@ -202,8 +202,8 @@ impl Window { let mut shell = self.shell_wnd.borrow_mut(); let inner_size = shell.inner_size(); - let paint_cmds = self.painter.borrow_mut().finish(); - shell.draw_commands(Rect::from_size(inner_size), paint_cmds); + let mut painter = self.painter.borrow_mut(); + shell.draw_commands(Rect::from_size(inner_size), &painter.finish()); shell.end_frame(); } diff --git a/dev-helper/src/image_test.rs b/dev-helper/src/image_test.rs index d616bb008..517148c7c 100644 --- a/dev-helper/src/image_test.rs +++ b/dev-helper/src/image_test.rs @@ -1,3 +1,4 @@ +use ribir_geom::Transform; use ribir_painter::{image::ColorFormat, PixelImage}; /// This macro generates image tests for the painter with every backend. Accept @@ -26,7 +27,7 @@ macro_rules! painter_backend_eq_image_test { fn []() { let mut painter = $painter_fn(); let viewport = painter.viewport().to_i32().cast_unit(); - let img = wgpu_render_commands(painter.finish(), viewport, Color::TRANSPARENT); + let img = wgpu_render_commands(&painter.finish(), viewport, Color::TRANSPARENT); let name = format!("{}_wgpu", std::stringify!($painter_fn)); let file_path = test_case_name!(name, "png"); ImageTest::new(img, &file_path) @@ -174,7 +175,7 @@ pub fn assert_texture_eq_png(test_img: PixelImage, ref_path: &std::path::Path) { /// Render painter by wgpu backend, and return the image. pub fn wgpu_render_commands( - commands: Vec, viewport: ribir_geom::DeviceRect, + commands: &[ribir_painter::PaintCommand], viewport: ribir_geom::DeviceRect, surface: ribir_painter::Color, ) -> PixelImage { use futures::executor::block_on; @@ -189,7 +190,7 @@ pub fn wgpu_render_commands( let mut backend = GPUBackend::new(gpu_impl); backend.begin_frame(surface); - backend.draw_commands(rect, commands, &mut texture); + backend.draw_commands(rect, commands, &Transform::identity(), &mut texture); let img = texture.copy_as_image(&rect, backend.get_impl_mut()); backend.end_frame(); block_on(img).unwrap() diff --git a/dev-helper/src/widget_test.rs b/dev-helper/src/widget_test.rs index 2db43abe7..46c621d79 100644 --- a/dev-helper/src/widget_test.rs +++ b/dev-helper/src/widget_test.rs @@ -224,7 +224,7 @@ macro_rules! widget_image_test { wnd.draw_frame(); let Frame { commands, viewport, surface} = wnd.take_last_frame().unwrap(); let viewport = viewport.to_i32().cast_unit(); - let img = wgpu_render_commands(commands, viewport, surface); + let img = wgpu_render_commands(&commands, viewport, surface); let name = format!("{}_with_default_by_wgpu", std::stringify!($widget_fn)); let file_path = test_case_name!(name, "png"); ImageTest::new(img, &file_path) @@ -241,7 +241,7 @@ macro_rules! widget_image_test { wnd.draw_frame(); let Frame { commands, viewport, surface} = wnd.take_last_frame().unwrap(); let viewport = viewport.to_i32().cast_unit(); - let img = wgpu_render_commands(commands, viewport, surface); + let img = wgpu_render_commands(&commands, viewport, surface); let name = format!("{}_with_material_by_wgpu", std::stringify!($widget_fn)); let file_path = test_case_name!(name, "png"); ImageTest::new(img, &file_path) diff --git a/geom/src/lib.rs b/geom/src/lib.rs index 304d9840f..9de6dac7b 100644 --- a/geom/src/lib.rs +++ b/geom/src/lib.rs @@ -42,3 +42,12 @@ where Point2D::new(rect.min_x(), rect.max_y()), ] } + +/// Apply the transform to the rect and return the new rect as device unit. +pub fn transform_to_device_rect(rect: &Rect, matrix: &Transform) -> DeviceRect { + matrix + .outer_transformed_rect(rect) + .round_out() + .to_i32() + .cast_unit() +} diff --git a/gpu/src/gpu_backend.rs b/gpu/src/gpu_backend.rs index 8296783e4..ecea5cde7 100644 --- a/gpu/src/gpu_backend.rs +++ b/gpu/src/gpu_backend.rs @@ -1,8 +1,11 @@ use std::error::Error; -use ribir_geom::{rect_corners, DeviceRect, DeviceSize, Point}; +use guillotiere::euclid::Vector2D; +use ribir_geom::{ + rect_corners, transform_to_device_rect, DeviceRect, DeviceSize, Point, Transform, +}; use ribir_painter::{ - image::ColorFormat, Color, PaintCommand, PaintPathAction, PainterBackend, PathCommand, + image::ColorFormat, Color, PaintCommand, PaintPath, PaintPathAction, PainterBackend, PathCommand, PixelImage, Vertex, VertexBuffers, }; @@ -93,36 +96,20 @@ where } fn draw_commands( - &mut self, viewport: DeviceRect, commands: Vec, output: &mut Self::Texture, + &mut self, viewport: DeviceRect, commands: &[PaintCommand], global_matrix: &Transform, + output: &mut Self::Texture, ) { + let clips = self.clip_layer_stack.len(); self.viewport = viewport; self.begin_draw_phase(); let output_size = output.size(); - for cmd in commands.into_iter() { - let max_tex_per_draw = self.gpu_impl.limits().max_tex_load; - let maybe_used = match cmd { - PaintCommand::Path(PathCommand { action: PaintPathAction::Image { .. }, .. }) => 2, - PaintCommand::PopClip => 0, - _ => 1, - }; - if !self.can_batch(&cmd) { - // if the next command may hit the texture limit, submit the current draw phase. - // And start a new draw phase. - self.draw_triangles(output); - self.end_draw_phase(); - self.begin_draw_phase(); - - assert!( - self.tex_ids_map.all_textures().len() + maybe_used < max_tex_per_draw, - "The GPUBackend implementation does not provide a sufficient texture limit per draw." - ) - } - self.draw_command(cmd, output_size); + for cmd in commands { + self.draw_command(cmd, global_matrix, output_size, output); } self.draw_triangles(output); self.end_draw_phase(); - assert!(self.clip_layer_stack.is_empty()); + assert_eq!(self.clip_layer_stack.len(), clips); } fn end_frame(&mut self) { @@ -166,27 +153,37 @@ where #[inline] pub fn get_impl_mut(&mut self) -> &mut Impl { &mut self.gpu_impl } - fn draw_command(&mut self, cmd: PaintCommand, output_tex_size: DeviceSize) { + fn draw_command( + &mut self, cmd: &PaintCommand, global_matrix: &Transform, output_tex_size: DeviceSize, + output: &mut Impl::Texture, + ) { match cmd { - PaintCommand::Path(path) => { + PaintCommand::Path(cmd @ PathCommand { path, paint_bounds, transform, action }) => { if self.skip_clip_cnt > 0 { - if matches!(path.action, PaintPathAction::Clip) { + if matches!(action, PaintPathAction::Clip) { self.skip_clip_cnt += 1; } // Skip the commands if the clip layer is not visible. return; } + let bounds = transform_to_device_rect(paint_bounds, global_matrix); - let bounds = path.paint_bounds.round_out().to_i32().cast_unit(); - let Some((rect, mask_head)) = self.new_mask_layer(&path) else { - if matches!(path.action, PaintPathAction::Clip) { + let Some(viewport) = self.viewport().intersection(&bounds) else { + if matches!(action, PaintPathAction::Clip) { self.skip_clip_cnt += 1; } - // Skip the command if its mask is not visible. + // Skip the command if it is not visible. return; }; - match path.action { + if !self.can_batch_path_command(cmd) { + self.new_draw_phase(output); + } + + let matrix = transform.then(global_matrix); + let (rect, mask_head) = self.new_mask_layer(&viewport, &matrix, path); + + match &action { PaintPathAction::Color(color) => { let color = color.into_components(); let color_attr = ColorAttr { color, mask_head }; @@ -195,27 +192,13 @@ where self.current_phase = CurrentPhase::Color; } PaintPathAction::Image { img, opacity } => { - let img_slice = self.tex_mgr.store_image(&img, &mut self.gpu_impl); - let img_start = img_slice.rect.origin.to_f32().to_array(); - let img_size = img_slice.rect.size.to_f32().to_array(); - let mask_head_and_tex_idx = - mask_head << 16 | self.tex_ids_map.tex_idx(img_slice.tex_id) as i32; - let prim_idx = self.img_prims.len() as u32; - let prim = ImgPrimitive { - transform: path.transform.inverse().unwrap().to_array(), - img_start, - img_size, - mask_head_and_tex_idx, - opacity, - }; - self.img_prims.push(prim); - let buffer = &mut self.img_vertices_buffer; - add_rect_vertices(rect, output_tex_size, ImagePrimIndex(prim_idx), buffer); - self.current_phase = CurrentPhase::Img; + let slice = self.tex_mgr.store_image(img, &mut self.gpu_impl); + let ts = matrix.inverse().unwrap(); + self.draw_img_slice(slice, &ts, mask_head, *opacity, output_tex_size, rect); } PaintPathAction::Radial(radial) => { let prim: RadialGradientPrimitive = RadialGradientPrimitive { - transform: path.transform.inverse().unwrap().to_array(), + transform: matrix.inverse().unwrap().to_array(), stop_start: self.radial_gradient_stops.len() as u32, stop_cnt: radial.stops.len() as u32, start_center: radial.start_center.to_array(), @@ -227,8 +210,8 @@ where }; let stops = radial .stops - .into_iter() - .map(Into::::into); + .iter() + .map(GradientStopPrimitive::new); self.radial_gradient_stops.extend(stops); let prim_idx = self.radial_gradient_prims.len() as u32; self.radial_gradient_prims.push(prim); @@ -241,7 +224,7 @@ where let stop = (self.linear_gradient_stops.len() << 16 | linear.stops.len()) as u32; let mask_head_and_spread = mask_head << 16 | linear.spread_method as i32; let prim: LinearGradientPrimitive = LinearGradientPrimitive { - transform: path.transform.inverse().unwrap().to_array(), + transform: matrix.inverse().unwrap().to_array(), stop, start_position: linear.start.to_array(), end_position: linear.end.to_array(), @@ -249,8 +232,8 @@ where }; let stops = linear .stops - .into_iter() - .map(Into::::into); + .iter() + .map(GradientStopPrimitive::new); self.linear_gradient_stops.extend(stops); let prim_idx = self.linear_gradient_prims.len() as u32; self.linear_gradient_prims.push(prim); @@ -258,13 +241,9 @@ where add_rect_vertices(rect, output_tex_size, LinearGradientPrimIndex(prim_idx), buffer); self.current_phase = CurrentPhase::LinearGradient; } - PaintPathAction::Clip => { - if let Some(viewport) = bounds.intersection(self.viewport()) { - self - .clip_layer_stack - .push(ClipLayer { viewport, mask_head }); - } - } + PaintPathAction::Clip => self + .clip_layer_stack + .push(ClipLayer { viewport, mask_head }), } } PaintCommand::PopClip => { @@ -274,21 +253,109 @@ where self.clip_layer_stack.pop(); } } + PaintCommand::Bundle { transform, opacity, bounds, cmds } => { + let matrix = transform.then(global_matrix); + let scale = self.tex_mgr.cache_scale(&bounds.size, &matrix); + let cache_size = bounds.size * scale; + + let this = self as *mut Self; + let (cache_scale, slice) = self.tex_mgr.store_commands( + cache_size.to_i32().cast_unit(), + cmds.clone().into_any(), + scale, + &mut self.gpu_impl, + |slice, tex, _| { + // SAFETY: We already hold a mut reference to the texture in the texture + // manager, so we cant use `self` here, but this texture should always exist + // within the frame, and no modifications will be made to the slice + // that has already been allocated. + let this = unsafe { &mut *this }; + + // Initiate a new drawing phase to ensure a clean state for rendering in a new + // texture. + this.new_draw_phase(output); + + // store the viewport + let viewport = self.viewport; + // Overwrite the viewport to the slice bounds. + self + .clip_layer_stack + .push(ClipLayer { viewport, mask_head: -1 }); + + let matrix = Transform::translation(bounds.origin.x, bounds.origin.y) + .then_scale(scale, scale) + .then_translate(slice.origin.to_f32().cast_unit().to_vector()); + this.draw_commands(*slice, cmds, &matrix, tex); + + // restore the clip layer and viewport + self.clip_layer_stack.pop(); + this.viewport = viewport; + this.begin_draw_phase(); + }, + ); + + let mut points: [_; 4] = rect_corners(&bounds.to_f32().cast_unit()); + for p in points.iter_mut() { + *p = matrix.transform_point(*p); + } + + let view_to_slice = matrix + // point back to the bundle commands axis. + .inverse() + .unwrap() + // align to the zero point, draw image slice is start from zero. + .then_translate(Vector2D::new(-bounds.origin.x, -bounds.origin.y)) + // scale to the cache size. + .then_scale(cache_scale, cache_scale); + + if !self.can_batch_img_path() { + self.new_draw_phase(output); + } + let mask_head = self + .clip_layer_stack + .last() + .map_or(-1, |l| l.mask_head); + self.draw_img_slice(slice, &view_to_slice, mask_head, *opacity, output_tex_size, points); + } } } + fn can_batch_img_path(&self) -> bool { + let limits = self.gpu_impl.limits(); + self.current_phase == CurrentPhase::None + || (self.current_phase == CurrentPhase::Img + && self.tex_ids_map.len() < limits.max_tex_load - 1 + && self.img_prims.len() < limits.max_image_primitives) + } + + // end current draw phase and start a new draw phase. + fn new_draw_phase(&mut self, output: &mut Impl::Texture) { + self.draw_triangles(output); + self.end_draw_phase(); + self.begin_draw_phase(); + } + fn begin_draw_phase(&mut self) { - self.current_phase = CurrentPhase::None; if !self.clip_layer_stack.is_empty() { // clear unused mask layers and update mask index. let mut retain_masks = Vec::with_capacity(self.clip_layer_stack.len()); + let mut mask_new_idx = vec![-1; self.mask_layers.len()]; for s in self.clip_layer_stack.iter_mut() { - retain_masks.push(s.mask_head); - s.mask_head = retain_masks.len() as i32 - 1; + if s.mask_head != -1 { + retain_masks.push(s.mask_head); + mask_new_idx[s.mask_head as usize] = retain_masks.len() as i32 - 1; + s.mask_head = retain_masks.len() as i32 - 1; + } } self.mask_layers = retain_masks .iter() - .map(|&idx| self.mask_layers[idx as usize].clone()) + .map(|&idx| { + let mut mask = self.mask_layers[idx as usize].clone(); + if mask.prev_mask_idx != -1 { + mask.prev_mask_idx = mask_new_idx[mask.prev_mask_idx as usize]; + } + mask + }) .collect(); // update the texture index of mask layers in new draw phase. @@ -305,6 +372,7 @@ where } fn end_draw_phase(&mut self) { + self.current_phase = CurrentPhase::None; self.color_vertices_buffer.vertices.clear(); self.color_vertices_buffer.indices.clear(); self.img_vertices_buffer.vertices.clear(); @@ -328,12 +396,30 @@ where self.linear_gradient_stops.clear(); } - fn can_batch(&self, cmd: &PaintCommand) -> bool { - let limits = self.gpu_impl.limits(); - let tex_used = self.tex_ids_map.all_textures().len(); - // Pop commands can always be batched. - let PaintCommand::Path(cmd) = cmd else { return true }; + fn draw_img_slice( + &mut self, img_slice: TextureSlice, transform: &Transform, mask_head: i32, opacity: f32, + output_tex_size: DeviceSize, rect: [Point; 4], + ) { + let img_start = img_slice.rect.origin.to_f32().to_array(); + let img_size = img_slice.rect.size.to_f32().to_array(); + let mask_head_and_tex_idx = mask_head << 16 | self.tex_ids_map.tex_idx(img_slice.tex_id) as i32; + let prim_idx = self.img_prims.len() as u32; + let prim = ImgPrimitive { + transform: transform.to_array(), + img_start, + img_size, + mask_head_and_tex_idx, + opacity, + }; + self.img_prims.push(prim); + let buffer = &mut self.img_vertices_buffer; + add_rect_vertices(rect, output_tex_size, ImagePrimIndex(prim_idx), buffer); + self.current_phase = CurrentPhase::Img; + } + fn can_batch_path_command(&self, cmd: &PathCommand) -> bool { + let limits = self.gpu_impl.limits(); + let tex_used = self.tex_ids_map.len(); match (self.current_phase, &cmd.action) { (CurrentPhase::None, _) => true, (_, PaintPathAction::Clip) | (CurrentPhase::Color, PaintPathAction::Color(_)) => { @@ -370,13 +456,13 @@ where .map_or(&self.viewport, |l| &l.viewport) } - fn new_mask_layer(&mut self, path: &PathCommand) -> Option<([Point; 4], i32)> { - let paint_bounds = path.paint_bounds.round_out().to_i32().cast_unit(); - let view = paint_bounds.intersection(self.viewport())?; - - let (mask, mask_to_view) = self - .tex_mgr - .store_alpha_path(path, view, &mut self.gpu_impl); + fn new_mask_layer( + &mut self, view: &DeviceRect, matrix: &Transform, path: &PaintPath, + ) -> ([Point; 4], i32) { + let (mask, mask_to_view) = + self + .tex_mgr + .store_alpha_path(path, matrix, view, &mut self.gpu_impl); let mut points = rect_corners(&mask.rect.to_f32().cast_unit()); for p in points.iter_mut() { @@ -393,7 +479,7 @@ where mask_tex_idx: self.tex_ids_map.tex_idx(mask.tex_id), prev_mask_idx: self.current_clip_mask_index(), }); - Some((points, index as i32)) + (points, index as i32) } fn draw_triangles(&mut self, output: &mut Impl::Texture) { @@ -401,44 +487,64 @@ where let gpu_impl = &mut self.gpu_impl; self.tex_mgr.draw_alpha_textures(gpu_impl); - gpu_impl.load_mask_layers(&self.mask_layers); + if !self.mask_layers.is_empty() { + gpu_impl.load_mask_layers(&self.mask_layers); + } let textures = self.tex_ids_map.all_textures(); let max_textures = gpu_impl.limits().max_tex_load; let mut tex_buffer = Vec::with_capacity(max_textures); - textures.iter().take(max_textures).for_each(|id| { - tex_buffer.push(self.tex_mgr.texture(*id)); - }); - + if textures.is_empty() { + tex_buffer.push(self.tex_mgr.texture(TextureID::Alpha(0))); + } else { + textures.iter().take(max_textures).for_each(|id| { + tex_buffer.push(self.tex_mgr.texture(*id)); + }); + } gpu_impl.load_textures(&tex_buffer); match self.current_phase { - CurrentPhase::None => gpu_impl.draw_color_triangles(output, 0..0, color.take()), - CurrentPhase::Color => { + CurrentPhase::None => { + if color.is_some() { + gpu_impl.draw_color_triangles(output, 0..0, color.take()) + } + } + CurrentPhase::Color if !self.color_vertices_buffer.indices.is_empty() => { gpu_impl.load_color_vertices(&self.color_vertices_buffer); let rg = 0..self.color_vertices_buffer.indices.len() as u32; gpu_impl.draw_color_triangles(output, rg, color.take()) } - CurrentPhase::Img => { + CurrentPhase::Img if !self.img_vertices_buffer.indices.is_empty() => { gpu_impl.load_img_primitives(&self.img_prims); gpu_impl.load_img_vertices(&self.img_vertices_buffer); let rg = 0..self.img_vertices_buffer.indices.len() as u32; gpu_impl.draw_img_triangles(output, rg, color.take()) } - CurrentPhase::RadialGradient => { + CurrentPhase::RadialGradient + if !self + .radial_gradient_vertices_buffer + .indices + .is_empty() => + { gpu_impl.load_radial_gradient_primitives(&self.radial_gradient_prims); gpu_impl.load_radial_gradient_stops(&self.radial_gradient_stops); gpu_impl.load_radial_gradient_vertices(&self.radial_gradient_vertices_buffer); let rg = 0..self.radial_gradient_vertices_buffer.indices.len() as u32; gpu_impl.draw_radial_gradient_triangles(output, rg, color.take()) } - CurrentPhase::LinearGradient => { + CurrentPhase::LinearGradient + if !self + .linear_gradient_vertices_buffer + .indices + .is_empty() => + { gpu_impl.load_linear_gradient_primitives(&self.linear_gradient_prims); gpu_impl.load_linear_gradient_stops(&self.linear_gradient_stops); gpu_impl.load_linear_gradient_vertices(&self.linear_gradient_vertices_buffer); let rg = 0..self.linear_gradient_vertices_buffer.indices.len() as u32; gpu_impl.draw_linear_gradient_triangles(output, rg, color.take()) } + _ => {} } } } @@ -458,6 +564,8 @@ impl TextureIdxMap { self.texture_map.clear(); self.textures.clear(); } + + fn len(&self) -> usize { self.textures.len() } } pub fn vertices_coord(pos: Point, tex_size: DeviceSize) -> [f32; 2] { @@ -580,7 +688,7 @@ mod tests { painter } - painter_backend_eq_image_test!(stroke_include_border, comparison = 0.00035); + painter_backend_eq_image_test!(stroke_include_border, comparison = 0.0004); fn stroke_include_border() -> Painter { let mut painter = painter(Size::new(100., 100.)); painter @@ -664,4 +772,25 @@ mod tests { } #[cfg(not(target_os = "windows"))] painter_backend_eq_image_test!(multi_draw_phase, comparison = 0.001); + + fn draw_bundle_svg() -> Painter { + let mut painter = painter(Size::new(512., 512.)); + let circle = Resource::new(Path::circle(Point::new(4., 4.), 100.)); + let commands = (0..64) + .map(|i| { + let color = if i % 2 == 0 { Color::GREEN } else { Color::RED }; + PaintCommand::Path(PathCommand { + paint_bounds: *circle.bounds(), + path: circle.clone().into(), + transform: Transform::translation(i as f32 * 8., i as f32 * 8.), + action: PaintPathAction::Color(color), + }) + }) + .collect(); + + let svg = Svg { size: Size::new(512., 512.), commands: Resource::new(commands) }; + painter.draw_svg(&svg); + painter + } + painter_backend_eq_image_test!(draw_bundle_svg, comparison = 0.001); } diff --git a/gpu/src/gpu_backend/atlas.rs b/gpu/src/gpu_backend/atlas.rs index 25e3777ab..e6a50d1a0 100644 --- a/gpu/src/gpu_backend/atlas.rs +++ b/gpu/src/gpu_backend/atlas.rs @@ -32,7 +32,9 @@ pub(crate) struct Atlas { atlas_allocator: AtlasAllocator, texture: T, cache: FrameCache, AtlasHandle>, + /// Extra textures which store only single allocation. extras: Slab, + /// All allocations in the current frame and not cached. islands: ahash::HashSet, } @@ -184,7 +186,7 @@ where impl AtlasConfig { pub fn new(label: &'static str, max_size: DeviceSize) -> Self { - Self { label, min_size: max_size / 4, max_size } + Self { label, min_size: max_size / 8, max_size } } } diff --git a/gpu/src/gpu_backend/textures_mgr.rs b/gpu/src/gpu_backend/textures_mgr.rs index 7113ed1da..37e824655 100644 --- a/gpu/src/gpu_backend/textures_mgr.rs +++ b/gpu/src/gpu_backend/textures_mgr.rs @@ -1,12 +1,10 @@ -use std::{cmp::Ordering, hash::Hash, ops::Range}; +use std::{any::Any, cmp::Ordering, hash::Hash, ops::Range}; use guillotiere::euclid::SideOffsets2D; use rayon::{prelude::ParallelIterator, slice::ParallelSlice}; use ribir_algo::Resource; -use ribir_geom::{DeviceRect, DeviceSize, Transform}; -use ribir_painter::{ - image::ColorFormat, PaintPath, Path, PathCommand, PixelImage, Vertex, VertexBuffers, -}; +use ribir_geom::{transform_to_device_rect, DeviceRect, DeviceSize, Size, Transform}; +use ribir_painter::{image::ColorFormat, PaintPath, Path, PixelImage, Vertex, VertexBuffers}; use super::{ atlas::{Atlas, AtlasConfig, AtlasDist}, @@ -20,11 +18,19 @@ const PAR_CHUNKS_SIZE: usize = 64; pub(super) enum TextureID { Alpha(usize), Rgba(usize), + Bundle(usize), } pub(super) struct TexturesMgr { alpha_atlas: Atlas, rgba_atlas: Atlas, + /// Similar to the `rgba_atlas`, this is used to allocate the target texture + /// for drawing commands. + /// + /// We keep it separate from `rgba_atlas` because the backend may not permit a + /// texture to be used both as a target and as a sampled resource in the same + /// draw call. + target_atlas: Atlas, fill_task: Vec, fill_task_buffers: VertexBuffers<()>, need_clear_areas: Vec, @@ -49,6 +55,7 @@ macro_rules! id_to_texture_mut { match $id { TextureID::Alpha(id) => $mgr.alpha_atlas.get_texture_mut(id), TextureID::Rgba(id) => $mgr.rgba_atlas.get_texture_mut(id), + TextureID::Bundle(id) => $mgr.target_atlas.get_texture_mut(id), } }; } @@ -58,6 +65,7 @@ macro_rules! id_to_texture { match $id { TextureID::Alpha(id) => $mgr.alpha_atlas.get_texture(id), TextureID::Rgba(id) => $mgr.rgba_atlas.get_texture(id), + TextureID::Bundle(id) => $mgr.target_atlas.get_texture(id), } }; } @@ -81,6 +89,11 @@ where ColorFormat::Rgba8, gpu_impl, ), + target_atlas: Atlas::new( + AtlasConfig::new("Bundle atlas", max_size), + ColorFormat::Rgba8, + gpu_impl, + ), fill_task: <_>::default(), fill_task_buffers: <_>::default(), need_clear_areas: vec![], @@ -90,28 +103,25 @@ where /// Store an alpha path in texture and return the texture and a transform that /// can transform the mask to viewport pub(super) fn store_alpha_path( - &mut self, path: &PathCommand, viewport: DeviceRect, gpu: &mut T::Host, + &mut self, path: &PaintPath, matrix: &Transform, viewport: &DeviceRect, gpu: &mut T::Host, ) -> (TextureSlice, Transform) { - match &path.path { + match path { PaintPath::Share(p) => { - let cache_scale: f32 = self.alpha_cache_path_scale(path); + let cache_scale: f32 = self.cache_scale(&path.bounds().size, matrix); let key = p.clone().into_any(); let (slice, scale) = if let Some(h) = self.alpha_atlas.get(&key, cache_scale).copied() { let mask_slice = self.alpha_atlas_dist_to_tex_silice(&h.dist); (mask_slice, h.scale) } else { - let scale_bounds = p - .bounds() - .scale(cache_scale, cache_scale) - .round_out(); - let (dist, slice) = self.alpha_allocate(scale_bounds.size.to_i32().cast_unit(), gpu); + let scale_bounds = p.bounds().scale(cache_scale, cache_scale); + let (dist, slice) = + self.alpha_allocate(scale_bounds.round_out().size.to_i32().cast_unit(), gpu); let _ = self.alpha_atlas.cache(key, cache_scale, dist); let offset = slice.rect.origin.to_f32().cast_unit() - scale_bounds.origin; let transform = Transform::scale(cache_scale, cache_scale).then_translate(offset); - let path = path.path.clone(); self .fill_task - .push(FillTask { slice, path, transform, clip_rect: None }); + .push(FillTask { slice, path: path.clone(), transform, clip_rect: None }); (slice, cache_scale) }; @@ -119,17 +129,17 @@ where let slice_origin = slice.rect.origin.to_vector().to_f32(); // back to slice origin let matrix = Transform::translation(-slice_origin.x, -slice_origin.y) - // move to cachedc path axis. + // move to cached path axis. .then_translate(path_origin.to_vector().cast_unit()) // scale back to path axis. .then_scale(1. / scale, 1. / scale) // apply path transform matrix to view. - .then(&path.transform); + .then(matrix); (slice.expand_for_paste(), matrix) } PaintPath::Own(_) => { - let paint_bounds = path.paint_bounds.round_out().to_i32().cast_unit(); + let paint_bounds = transform_to_device_rect(path.bounds(), matrix); let alloc_size = size_expand_blank(paint_bounds.size); let (visual_rect, clip_rect) = if self.alpha_atlas.is_good_size_to_alloc(alloc_size) { @@ -137,7 +147,7 @@ where } else { // We intersect the path bounds with the viewport to reduce the number of pixels // drawn for large paths. - let visual_rect = paint_bounds.intersection(&viewport).unwrap(); + let visual_rect = paint_bounds.intersection(viewport).unwrap(); (visual_rect, Some(visual_rect)) }; @@ -145,8 +155,8 @@ where let offset = (slice.rect.origin - visual_rect.origin) .to_f32() .cast_unit(); - let ts = path.transform.then_translate(offset); - let task = FillTask { slice, transform: ts, path: path.path.clone(), clip_rect }; + let ts = matrix.then_translate(offset); + let task = FillTask { slice, transform: ts, path: path.clone(), clip_rect }; self.fill_task.push(task); let offset = (visual_rect.origin - slice.rect.origin).to_f32(); @@ -171,6 +181,22 @@ where TextureSlice { tex_id: TextureID::Rgba(h.tex_id()), rect: h.tex_rect(atlas) } } + pub(super) fn store_commands( + &mut self, size: DeviceSize, target: Resource, scale: f32, gpu: &mut T::Host, + init: impl FnOnce(&DeviceRect, &mut T, &mut T::Host), + ) -> (f32, TextureSlice) { + let dist = self + .target_atlas + .get_or_cache(target, scale, size, gpu, init); + ( + dist.scale, + TextureSlice { + tex_id: TextureID::Bundle(dist.tex_id()), + rect: dist.tex_rect(&self.target_atlas), + }, + ) + } + pub(super) fn texture(&self, tex_id: TextureID) -> &T { id_to_texture!(self, tex_id) } fn alpha_allocate( @@ -191,18 +217,17 @@ where TextureSlice { tex_id: TextureID::Alpha(dist.tex_id()), rect: rect.inner_rect(blank_side) } } - fn alpha_cache_path_scale(&self, path: &PathCommand) -> f32 { - let Transform { m11, m12, m21, m22, .. } = path.transform; + pub(super) fn cache_scale(&self, size: &Size, matrix: &Transform) -> f32 { + let Transform { m11, m12, m21, m22, .. } = matrix; let scale = (m11.abs() + m12.abs()).max(m21.abs() + m22.abs()); - let path_size = path.path.bounds().size; - let dis = path_size.width.max(path_size.height); + let dis = size.width.max(size.height); if dis * scale < 32. { // If the path is too small, set a minimum tessellation size of 32 pixels. 32. / dis } else { // 2 * BLANK_EDGE is the blank edge for each side. let max_size = size_shrink_blank(self.alpha_atlas.max_size()).to_f32(); - let max_scale = (max_size.width / path_size.width).min(max_size.width / path_size.height); + let max_scale = (max_size.width / size.width).min(max_size.width / size.height); scale.min(max_scale) } } @@ -338,6 +363,7 @@ where self.need_clear_areas.push(rect); }); self.rgba_atlas.end_frame(); + self.target_atlas.end_frame(); } } @@ -384,7 +410,7 @@ pub mod tests { use futures::executor::block_on; use ribir_geom::*; - use ribir_painter::{Color, PaintPathAction}; + use ribir_painter::Color; use super::*; use crate::{WgpuImpl, WgpuTexture}; @@ -452,15 +478,12 @@ pub mod tests { let p = Resource::new(Path::rect(&rect(0., 0., 300., 300.))); let p = PaintPath::Share(p.clone()); - let path1 = - PathCommand::new(p.clone(), PaintPathAction::Color(Color::RED), Transform::scale(2., 2.)); - let path2 = - PathCommand::new(p, PaintPathAction::Color(Color::BLUE), Transform::translation(100., 100.)); let viewport = rect(0, 0, 1024, 1024); - let (slice1, ts1) = mgr.store_alpha_path(&path1, viewport, &mut wgpu); + let (slice1, ts1) = mgr.store_alpha_path(&p, &Transform::scale(2., 2.), &viewport, &mut wgpu); - let (slice2, ts2) = mgr.store_alpha_path(&path2, viewport, &mut wgpu); + let (slice2, ts2) = + mgr.store_alpha_path(&p, &Transform::translation(100., 100.), &viewport, &mut wgpu); assert_eq!(slice1, slice2); assert_eq!(ts1, Transform::new(1., 0., 0., 1., -2., -2.)); diff --git a/gpu/src/lib.rs b/gpu/src/lib.rs index 0c84efcd6..440290054 100644 --- a/gpu/src/lib.rs +++ b/gpu/src/lib.rs @@ -211,8 +211,8 @@ pub struct GradientStopPrimitive { pub offset: f32, } -impl From for GradientStopPrimitive { - fn from(stop: GradientStop) -> Self { +impl GradientStopPrimitive { + fn new(stop: &GradientStop) -> Self { GradientStopPrimitive { color: stop.color.into_u32(), offset: stop.offset } } } diff --git a/painter/Cargo.toml b/painter/Cargo.toml index b64077579..6accacf9d 100644 --- a/painter/Cargo.toml +++ b/painter/Cargo.toml @@ -22,7 +22,7 @@ material-color-utilities-rs = {workspace = true} rctree.workspace = true ribir_algo = {path = "../algo", version = "0.3.0-alpha.5" } ribir_geom = {path = "../geom", version = "0.3.0-alpha.5" } -serde = {version = "1.0", features = ["rc", "derive"]} +serde = {version = "1.0", features = ["derive"]} serde_json.workspace = true tiny-skia-path = {workspace = true} usvg.workspace = true diff --git a/painter/src/painter.rs b/painter/src/painter.rs index 667311b27..e60a9b85e 100644 --- a/painter/src/painter.rs +++ b/painter/src/painter.rs @@ -22,6 +22,8 @@ pub struct Painter { path_builder: PathBuilder, } +pub struct PainterResult<'a>(&'a mut Vec); + /// `PainterBackend` use to draw textures for every frame, All `draw_commands` /// will called between `begin_frame` and `end_frame` /// @@ -49,9 +51,9 @@ pub trait PainterBackend { /// You should guarantee the output be same one in the same frame, otherwise /// it may cause undefined behavior. fn draw_commands( - &mut self, viewport: DeviceRect, commands: Vec, output: &mut Self::Texture, + &mut self, viewport: DeviceRect, commands: &[PaintCommand], global_matrix: &Transform, + output: &mut Self::Texture, ); - /// A frame end. fn end_frame(&mut self); } @@ -111,14 +113,16 @@ pub struct PathCommand { pub enum PaintCommand { Path(PathCommand), PopClip, - // /// A Bundle of paint commands that can be assumed as a single command, that - // /// means the backend can cache it. - // Bundle { - // transform: Transform, - // opacity: f32, - // paint_bounds: Rect, - // cmds: Resource>, - // }, + /// A Bundle of paint commands that can be assumed as a single command, that + /// means the backend can cache it. + Bundle { + transform: Transform, + opacity: f32, + /// the bounds of the bundle commands. This is the union of all paint + /// command + bounds: Rect, + cmds: Resource>, + }, } #[derive(Clone)] @@ -180,11 +184,9 @@ impl Painter { } #[inline] - pub fn finish(&mut self) -> Vec { + pub fn finish(&mut self) -> PainterResult { self.fill_all_pop_clips(); - let commands = self.commands.clone(); - self.commands.clear(); - commands + PainterResult(&mut self.commands) } /// Saves the entire state and return a guard to auto restore the state when @@ -494,22 +496,58 @@ impl Painter { self } + /// Draws a bundle of paint commands that can be treated as a single command. + /// This allows the backend to cache it. + /// + /// - **bounds** - The bounds of the bundle commands. This is the union of all + /// paint command bounds. It does not configure where the bundle is placed. + /// If you want to change the position of the bundle, you should call + /// `Painter::translate` before calling this method. + /// - **cmds** - The list of paint commands to draw. + pub fn draw_bundle_commands( + &mut self, bounds: Rect, cmds: Resource>, + ) -> &mut Self { + invisible_return!(self); + let transform = *self.get_transform(); + let opacity = self.alpha(); + let cmd = PaintCommand::Bundle { transform, opacity, bounds, cmds }; + self.commands.push(cmd); + self + } + pub fn draw_svg(&mut self, svg: &Svg) -> &mut Self { invisible_return!(self); - let transform = *self.get_transform(); - let alpha = self.alpha(); - - for cmd in svg.paint_commands.iter() { - let cmd = match cmd.clone() { - PaintCommand::Path(mut path) => { - path.transform(&transform); - path.action.apply_alpha(alpha); - PaintCommand::Path(path) - } - PaintCommand::PopClip => PaintCommand::PopClip, - }; - self.commands.push(cmd); + // For a large number of path commands (more than 16), bundle them + // together as a single resource. This allows the backend to cache + // them collectively. + // For a small number of path commands (less than 16), store them + // individually as multiple resources. This means the backend doesn't + // need to perform a single draw operation for an SVG. + if svg.commands.len() <= 16 { + let transform = *self.get_transform(); + let alpha = self.alpha(); + + for cmd in svg.commands.iter() { + let cmd = match cmd.clone() { + PaintCommand::Path(mut path) => { + path.transform(&transform); + path.action.apply_alpha(alpha); + PaintCommand::Path(path) + } + PaintCommand::PopClip => PaintCommand::PopClip, + PaintCommand::Bundle { transform: b_ts, opacity, bounds, cmds } => PaintCommand::Bundle { + transform: transform.then(&b_ts), + opacity: alpha * opacity, + bounds, + cmds, + }, + }; + self.commands.push(cmd); + } + } else { + let rect = Rect::from_size(svg.size); + self.draw_bundle_commands(rect, svg.commands.clone()); } self @@ -613,6 +651,19 @@ impl Painter { } } +impl Drop for PainterResult<'_> { + fn drop(&mut self) { self.0.clear() } +} + +impl<'a> Deref for PainterResult<'a> { + type Target = [PaintCommand]; + fn deref(&self) -> &Self::Target { self.0 } +} + +impl<'a> DerefMut for PainterResult<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { self.0 } +} + /// An RAII implementation of a "scoped state" of the render layer. When this /// structure is dropped (falls out of scope), changed state will auto restore. /// The data can be accessed through this guard via its Deref and DerefMut @@ -751,10 +802,12 @@ mod test { .fill() .finish(); - assert_eq!(painter.current_state().clip_cnt, 0); - assert!(matches!(commands[commands.len() - 1], PaintCommand::PopClip)); assert!(matches!(commands[commands.len() - 2], PaintCommand::PopClip)); + + std::mem::drop(commands); + + assert_eq!(painter.current_state().clip_cnt, 0); } #[test] diff --git a/painter/src/svg.rs b/painter/src/svg.rs index 09fad61e2..a4df48796 100644 --- a/painter/src/svg.rs +++ b/painter/src/svg.rs @@ -1,5 +1,6 @@ use std::{error::Error, io::Read}; +use ribir_algo::Resource; use ribir_geom::{Point, Rect, Size, Transform}; use serde::{Deserialize, Serialize}; use usvg::{Options, Stop, Tree, TreeParsing}; @@ -12,7 +13,7 @@ use crate::{ #[derive(Serialize, Deserialize, Clone)] pub struct Svg { pub size: Size, - pub paint_commands: Vec, + pub commands: Resource>, } /// Fits size into a viewbox. copy from resvg @@ -54,14 +55,13 @@ impl Svg { if let Some(ref fill) = p.fill { let (brush, transform) = brush_from_usvg_paint(&fill.paint, fill.opacity, &size); let mut painter = painter.save_guard(); + + let inverse_ts = transform.inverse().unwrap(); + let path = Resource::new(path.clone().transform(&inverse_ts)); painter .set_brush(brush.clone()) .apply_transform(&transform) - .fill_path( - path - .clone() - .transform(&transform.inverse().unwrap()), - ); + .fill_path(path); //&o_ts.then(&n_ts.inverse().unwrap()))); } @@ -86,11 +86,18 @@ impl Svg { let (brush, transform) = brush_from_usvg_paint(&stroke.paint, stroke.opacity, &size); let mut painter = painter.save_guard(); + painter .set_brush(brush.clone()) - .apply_transform(&transform) - .set_strokes(options) - .stroke_path(path.transform(&transform.inverse().unwrap())); + .apply_transform(&transform); + + let path = path + .transform(&transform.inverse().unwrap()) + .stroke(&options, Some(painter.get_transform())); + + if let Some(p) = path { + painter.fill_path(Resource::new(p)); + } }; } NodeKind::Image(_) => { @@ -123,7 +130,12 @@ impl Svg { } }); - Ok(Svg { size: Size::new(size.width(), size.height()), paint_commands: painter.finish() }) + let paint_commands = painter.finish().to_owned().into_boxed_slice(); + + Ok(Svg { + size: Size::new(size.width(), size.height()), + commands: Resource::new(paint_commands), + }) } pub fn open>(path: P) -> Result> { diff --git a/ribir/src/backends/wgpu_backend.rs b/ribir/src/backends/wgpu_backend.rs index e44e06a6a..25fbae302 100644 --- a/ribir/src/backends/wgpu_backend.rs +++ b/ribir/src/backends/wgpu_backend.rs @@ -1,4 +1,4 @@ -use ribir_core::prelude::{Color, DeviceRect, DeviceSize, PaintCommand, PainterBackend}; +use ribir_core::prelude::{Color, DeviceRect, DeviceSize, PaintCommand, PainterBackend, Transform}; use ribir_gpu::Surface; use crate::winit_shell_wnd::WinitBackend; @@ -28,10 +28,15 @@ impl<'a> WinitBackend<'a> for WgpuBackend<'a> { fn begin_frame(&mut self, surface_color: Color) { self.backend.begin_frame(surface_color); } - fn draw_commands(&mut self, viewport: DeviceRect, commands: Vec) { - self - .backend - .draw_commands(viewport, commands, self.surface.get_current_texture()); + fn draw_commands( + &mut self, viewport: DeviceRect, global_matrix: &Transform, commands: &[PaintCommand], + ) { + self.backend.draw_commands( + viewport, + commands, + global_matrix, + self.surface.get_current_texture(), + ); } fn end_frame(&mut self) { diff --git a/ribir/src/winit_shell_wnd.rs b/ribir/src/winit_shell_wnd.rs index 116bcf75c..a976236ef 100644 --- a/ribir/src/winit_shell_wnd.rs +++ b/ribir/src/winit_shell_wnd.rs @@ -20,7 +20,9 @@ pub trait WinitBackend<'a>: Sized { fn begin_frame(&mut self, surface_color: Color); - fn draw_commands(&mut self, viewport: DeviceRect, commands: Vec); + fn draw_commands( + &mut self, viewport: DeviceRect, global_matrix: &Transform, commands: &[PaintCommand], + ); fn end_frame(&mut self); } @@ -147,13 +149,7 @@ impl ShellWindow for WinitShellWnd { fn begin_frame(&mut self, surface: Color) { self.backend.begin_frame(surface) } #[inline] - fn draw_commands(&mut self, viewport: Rect, mut commands: Vec) { - for c in &mut commands { - if let PaintCommand::Path(path) = c { - path.scale(self.winit_wnd.scale_factor() as f32); - } - } - + fn draw_commands(&mut self, viewport: Rect, commands: &[PaintCommand]) { let scale = self.winit_wnd.scale_factor() as f32; let viewport: DeviceRect = viewport .scale(scale, scale) @@ -162,7 +158,9 @@ impl ShellWindow for WinitShellWnd { .cast_unit(); self.winit_wnd.pre_present_notify(); - self.backend.draw_commands(viewport, commands); + self + .backend + .draw_commands(viewport, &Transform::scale(scale, scale), commands); } #[inline] diff --git a/test_cases/ribir_gpu/gpu_backend/tests/draw_bundle_svg_wgpu.png b/test_cases/ribir_gpu/gpu_backend/tests/draw_bundle_svg_wgpu.png new file mode 100644 index 0000000000000000000000000000000000000000..42edbb318321a0672c61a8bdfd20641f8be92e3d GIT binary patch literal 84255 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 literal 0 HcmV?d00001 diff --git a/test_cases/ribir_widgets/checkbox/tests/checked_with_default_by_wgpu.png b/test_cases/ribir_widgets/checkbox/tests/checked_with_default_by_wgpu.png index e704ec9bc5ba15099783b84e933f384b82716750..6ea1dd801fa0dbc59bd563c3b6f64266276edf97 100644 GIT binary patch delta 480 zcmV<60U!SS1)c_wBmw1-C9VbU`kwD*k>Mth?*@P8rU%{3r+?3b$xnF-uk>oK%%A+}pYT9`@P~iEV_)L2 zyu?eq1i$bzKhMt24*vrXybxXOc)NFad;a!s{}zBh_`^Tob>HCic=?xmd4B)5e~*(V zPx3zm!3*$+hd-R}`L6Hbh8u6-)n4n>`GbEy`~zP14PK9zf4P_E4}SOe>HD7lK?q)m zhduP6{KSv`I1hf%gLvIHcs*X~rCy5P`t{%Be;|T?!Ob__%rE}@FY-R`_1^sQ&;JsA z-}65Zl^4OyH{HxPf8#gvKNyw&8>sx>fTq=Gp6R9s-OM8&`AA;=;PcP?hGkbX=4EcPeE#;{x3?8% z7Rw*rf7Jia75@)Ej@!@s`mpPNQpmj1$#Q=s>>gjXn_Iv7eEpy2pYJ!E`p?Vq-><*= z&*Pu+4G-==RImG_UNLiey`o5Y1{wr^ZrNm3{Bz>z-GJznj!c7$MMe&5Ot`^IDpE4rs*)e z|0VbS)ML5-6%44>)c<{6fAqfsi~fOR`Ogw{AAZ*fG=SXj6y)2=>)$KPAN@ZyP+5?q%Lo06H_38``RnfUe=5uWUH0ee{Qsi= zodYWc;hKKP_W(m8vZ>`+_mgArAx13_$bNBWhS^JN{_6MKW zKYac<|Le>7zh3?^cetFzV)FlY;N*==;gjDov5WoZh`X85A-D1PGX@~=boFyt=akR{ E02|@_sQ>@~ diff --git a/test_cases/ribir_widgets/checkbox/tests/checked_with_material_by_wgpu.png b/test_cases/ribir_widgets/checkbox/tests/checked_with_material_by_wgpu.png index e704ec9bc5ba15099783b84e933f384b82716750..6ea1dd801fa0dbc59bd563c3b6f64266276edf97 100644 GIT binary patch delta 480 zcmV<60U!SS1)c_wBmw1-C9VbU`kwD*k>Mth?*@P8rU%{3r+?3b$xnF-uk>oK%%A+}pYT9`@P~iEV_)L2 zyu?eq1i$bzKhMt24*vrXybxXOc)NFad;a!s{}zBh_`^Tob>HCic=?xmd4B)5e~*(V zPx3zm!3*$+hd-R}`L6Hbh8u6-)n4n>`GbEy`~zP14PK9zf4P_E4}SOe>HD7lK?q)m zhduP6{KSv`I1hf%gLvIHcs*X~rCy5P`t{%Be;|T?!Ob__%rE}@FY-R`_1^sQ&;JsA z-}65Zl^4OyH{HxPf8#gvKNyw&8>sx>fTq=Gp6R9s-OM8&`AA;=;PcP?hGkbX=4EcPeE#;{x3?8% z7Rw*rf7Jia75@)Ej@!@s`mpPNQpmj1$#Q=s>>gjXn_Iv7eEpy2pYJ!E`p?Vq-><*= z&*Pu+4G-==RImG_UNLiey`o5Y1{wr^ZrNm3{Bz>z-GJznj!c7$MMe&5Ot`^IDpE4rs*)e z|0VbS)ML5-6%44>)c<{6fAqfsi~fOR`Ogw{AAZ*fG=SXj6y)2=>)$KPAN@ZyP+5?q%Lo06H_38``RnfUe=5uWUH0ee{Qsi= zodYWc;hKKP_W(m8vZ>`+_mgArAx13_$bNBWhS^JN{_6MKW zKYac<|Le>7zh3?^cetFzV)FlY;N*==;gjDov5WoZh`X85A-D1PGX@~=boFyt=akR{ E02|@_sQ>@~ diff --git a/test_cases/ribir_widgets/checkbox/tests/indeterminate_with_default_by_wgpu.png b/test_cases/ribir_widgets/checkbox/tests/indeterminate_with_default_by_wgpu.png index cf6d79e171c96d86727f029f1702aa11c3bfc3c7..a41e6a137e073825d1a7476d9cd4eb206aa31bd7 100644 GIT binary patch delta 280 zcmV+z0q6e01lRMth?*@P8rU%{3r+?BeCj`EN$$1^DtW{c`>nq4IwNmH!*4{NF(3{{|}mH=t=XnrFJ{K{xZrM?R95f4P_A z-{Pl#=BIhaZMU<=8Fw$^*RPJG=uw`eQ%FAN=7T@Ne~%U;dT+H$c;BH2?cilK}!N eliL9^7XJuULqG{hH1GNV0000Y)&p+>b@cCzc!?LRx^D?(tK7V`f+uMpW zi{%gRKkEPIivNcn$L;5Rec1IsDP-R1WVyc*c8@RH&8=U3zW&ei&-WWn{pSt&m%rds zJ@eH6(kuSlcX-PBV~_rinz~2J>zc%mCd+@0sQd7{uHnC1NLMth?*@P8rU%{3r+?BeCj`EN$$1^DtW{c`>nq4IwNmH!*4{NF(3{{|}mH=t=XnrFJ{K{xZrM?R95f4P_A z-{Pl#=BIhaZMU<=8Fw$^*RPJG=uw`eQ%FAN=7T@Ne~%U;dT+H$c;BH2?cilK}!N eliL9^7XJuULqG{hH1GNV0000Y)&p+>b@cCzc!?LRx^D?(tK7V`f+uMpW zi{%gRKkEPIivNcn$L;5Rec1IsDP-R1WVyc*c8@RH&8=U3zW&ei&-WWn{pSt&m%rds zJ@eH6(kuSlcX-PBV~_rinz~2J>zc%mCd+@0sQd7{uHnC1NL-Bmwo2C9VbU`kwD*k>Mth?*>QbrU%{3r+?7V&&o^ji4tPfZG7o+k3@AwYyz>oge zkMRe8_y_!3edU*bCI1c3v>MI-ev`ZbN0ZF~7Z<=k0<7jV;Wi@~07*qoM6N<$ Eg7GhJNB{r; delta 181 zcmV;m080O^1epYoBmv-&C9VaZ_{pDOk>Mke?+iZY^SD6{98T#36JN$36%$UySI5ee&VNog3tfL&*$Ij(?9jo`EP)x)oA|rqmy6(7nAP+ jD3kmF8yZ06{|5dM7sV--0 diff --git a/test_cases/ribir_widgets/checkbox/tests/unchecked_with_material_by_wgpu.png b/test_cases/ribir_widgets/checkbox/tests/unchecked_with_material_by_wgpu.png index 6f5d16db6f43308cfa8184e4a8fb756aed8ad3f4..03d3ff4ff1ebfb9739167b92f937f956acc2e987 100644 GIT binary patch delta 202 zcmV;*05$)a1g->-Bmwo2C9VbU`kwD*k>Mth?*>QbrU%{3r+?7V&&o^ji4tPfZG7o+k3@AwYyz>oge zkMRe8_y_!3edU*bCI1c3v>MI-ev`ZbN0ZF~7Z<=k0<7jV;Wi@~07*qoM6N<$ Eg7GhJNB{r; delta 181 zcmV;m080O^1epYoBmv-&C9VaZ_{pDOk>Mke?+iZY^SD6{98T#36JN$36%$UySI5ee&VNog3tfL&*$Ij(?9jo`EP)x)oA|rqmy6(7nAP+ jD3kmF8yZ06{|5dM7sV--0 diff --git a/tests/include_svg_test.rs b/tests/include_svg_test.rs index 5c84871b2..cfcb31c43 100644 --- a/tests/include_svg_test.rs +++ b/tests/include_svg_test.rs @@ -4,7 +4,7 @@ use ribir_dev_helper::*; #[test] fn include_svg() { let svg: Svg = include_crate_svg!("./assets/test1.svg"); - assert_eq!(svg.paint_commands.len(), 2); + assert_eq!(svg.commands.len(), 2); } fn fix_draw_svg_not_apply_alpha() -> Painter { diff --git a/text/src/font_db.rs b/text/src/font_db.rs index 686b51530..09508da55 100644 --- a/text/src/font_db.rs +++ b/text/src/font_db.rs @@ -299,7 +299,7 @@ impl Face { bounds.and_then(move |b| { let mut path = builder.build(rect(b.x_min, b.y_min, b.width(), b.height()).to_f32()); if let PathStyle::Stroke(options) = style { - path = path.stroke(&options, None)?; + path = path.stroke(options, None)?; } Some(Resource::new(path)) })