From 1db291721fe6c3196fb3a11e59ab6928096edd62 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 1 Feb 2024 16:27:59 +0100 Subject: [PATCH] Parallell tessellation (#3934) * Part of https://github.com/emilk/egui/issues/1485 This adds a `rayon` feature to `epaint` and `egui` to parallelize tessellation of large shapes, such as high-resolution plot lines. --- Cargo.lock | 46 +++++- crates/egui/Cargo.toml | 7 +- crates/egui/src/context.rs | 4 +- crates/egui/src/introspection.rs | 8 + crates/epaint/Cargo.toml | 12 ++ crates/epaint/benches/benchmark.rs | 6 +- crates/epaint/src/lib.rs | 40 ++++- crates/epaint/src/mesh.rs | 3 + crates/epaint/src/tessellator.rs | 257 +++++++++++++++++++---------- 9 files changed, 281 insertions(+), 102 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae896877b..af293f56a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1011,14 +1011,30 @@ dependencies = [ ] [[package]] -name = "crossbeam-utils" -version = "0.8.16" +name = "crossbeam-deque" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ - "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1460,6 +1476,8 @@ dependencies = [ "log", "nohash-hasher", "parking_lot", + "puffin", + "rayon", "serde", ] @@ -3000,6 +3018,26 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544" +[[package]] +name = "rayon" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "rctree" version = "0.5.0" diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index 993a61533..c6d5e9e5a 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -66,7 +66,12 @@ persistence = ["serde", "epaint/serde", "ron"] ## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. ## ## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. -puffin = ["dep:puffin"] +puffin = ["dep:puffin", "epaint/puffin"] + +## Enable parallel tessellation using [`rayon`](https://docs.rs/rayon). +## +## This can help performance for graphics-intense applications. +rayon = ["epaint/rayon"] ## Allow serialization using [`serde`](https://docs.rs/serde). serde = ["dep:serde", "epaint/serde", "accesskit?/serde"] diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 8cbf00a63..0f63521f1 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1977,13 +1977,13 @@ impl Context { let paint_stats = PaintStats::from_shapes(&shapes); let clipped_primitives = { crate::profile_scope!("tessellator::tessellate_shapes"); - tessellator::tessellate_shapes( + tessellator::Tessellator::new( pixels_per_point, tessellation_options, font_tex_size, prepared_discs, - shapes, ) + .tessellate_shapes(shapes) }; ctx.paint_stats = paint_stats.with_clipped_primitives(&clipped_primitives); clipped_primitives diff --git a/crates/egui/src/introspection.rs b/crates/egui/src/introspection.rs index 6f0cada78..43b3a88b8 100644 --- a/crates/egui/src/introspection.rs +++ b/crates/egui/src/introspection.rs @@ -146,6 +146,8 @@ impl Widget for &mut epaint::TessellationOptions { debug_ignore_clip_rects, bezier_tolerance, epsilon: _, + parallel_tessellation, + validate_meshes, } = self; ui.checkbox(feathering, "Feathering (antialias)") @@ -176,6 +178,12 @@ impl Widget for &mut epaint::TessellationOptions { ui.checkbox(debug_paint_clip_rects, "Paint clip rectangles"); ui.checkbox(debug_paint_text_rects, "Paint text bounds"); }); + + ui.add_enabled(epaint::HAS_RAYON, crate::Checkbox::new(parallel_tessellation, "Parallelize tessellation") + ).on_hover_text("Only available if epaint was compiled with the rayon feature") + .on_disabled_hover_text("epaint was not compiled with the rayon feature"); + + ui.checkbox(validate_meshes, "Validate meshes").on_hover_text("Check that incoming meshes are valid, i.e. that all indices are in range, etc."); }) .response } diff --git a/crates/epaint/Cargo.toml b/crates/epaint/Cargo.toml index df9ee4077..12824f9fb 100644 --- a/crates/epaint/Cargo.toml +++ b/crates/epaint/Cargo.toml @@ -63,6 +63,16 @@ log = ["dep:log"] ## [`mint`](https://docs.rs/mint) enables interoperability with other math libraries such as [`glam`](https://docs.rs/glam) and [`nalgebra`](https://docs.rs/nalgebra). mint = ["emath/mint"] +## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. +## +## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. +puffin = ["dep:puffin"] + +## Enable parallel tessellation using [`rayon`](https://docs.rs/rayon). +## +## This can help performance for graphics-intense applications. +rayon = ["dep:rayon"] + ## Allow serialization using [`serde`](https://docs.rs/serde). serde = ["dep:serde", "ahash/serde", "emath/serde", "ecolor/serde"] @@ -88,6 +98,8 @@ bytemuck = { version = "1.7.2", optional = true, features = ["derive"] } document-features = { version = "0.2", optional = true } log = { version = "0.4", optional = true, features = ["std"] } +puffin = { workspace = true, optional = true } +rayon = { version = "1.7", optional = true } ## Allow serialization using [`serde`](https://docs.rs/serde) . serde = { version = "1", optional = true, features = ["derive", "rc"] } diff --git a/crates/epaint/benches/benchmark.rs b/crates/epaint/benches/benchmark.rs index 7f7c9f1b1..709adbfae 100644 --- a/crates/epaint/benches/benchmark.rs +++ b/crates/epaint/benches/benchmark.rs @@ -60,14 +60,14 @@ fn tessellate_circles(c: &mut Criterion) { let prepared_discs = atlas.prepared_discs(); b.iter(|| { - let clipped_primitive = tessellate_shapes( + let mut tessellator = Tessellator::new( pixels_per_point, options, font_tex_size, prepared_discs.clone(), - clipped_shapes.clone(), ); - black_box(clipped_primitive); + let clipped_primitives = tessellator.tessellate_shapes(clipped_shapes.clone()); + black_box(clipped_primitives); }); }); } diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index 18193dba1..c83be860a 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -52,13 +52,16 @@ pub use { }, stats::PaintStats, stroke::Stroke, - tessellator::{tessellate_shapes, TessellationOptions, Tessellator}, + tessellator::{TessellationOptions, Tessellator}, text::{FontFamily, FontId, Fonts, Galley}, texture_atlas::TextureAtlas, texture_handle::TextureHandle, textures::TextureManager, }; +#[allow(deprecated)] +pub use tessellator::tessellate_shapes; + pub use ecolor::{Color32, Hsva, HsvaGamma, Rgba}; pub use emath::{pos2, vec2, Pos2, Rect, Vec2}; @@ -172,3 +175,38 @@ pub(crate) fn f64_hash(state: &mut H, f: f64) { f.to_bits().hash(state); } } + +// --------------------------------------------------------------------------- + +/// Was epaint compiled with the `rayon` feature? +pub const HAS_RAYON: bool = cfg!(feature = "rayon"); + +// --------------------------------------------------------------------------- + +mod profiling_scopes { + #![allow(unused_macros)] + #![allow(unused_imports)] + + /// Profiling macro for feature "puffin" + macro_rules! profile_function { + ($($arg: tt)*) => { + #[cfg(feature = "puffin")] + #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. + puffin::profile_function!($($arg)*); + }; + } + pub(crate) use profile_function; + + /// Profiling macro for feature "puffin" + macro_rules! profile_scope { + ($($arg: tt)*) => { + #[cfg(feature = "puffin")] + #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. + puffin::profile_scope!($($arg)*); + }; + } + pub(crate) use profile_scope; +} + +#[allow(unused_imports)] +pub(crate) use profiling_scopes::*; diff --git a/crates/epaint/src/mesh.rs b/crates/epaint/src/mesh.rs index e33d950ec..2f85214e1 100644 --- a/crates/epaint/src/mesh.rs +++ b/crates/epaint/src/mesh.rs @@ -84,6 +84,8 @@ impl Mesh { /// Are all indices within the bounds of the contained vertices? pub fn is_valid(&self) -> bool { + crate::profile_function!(); + if let Ok(n) = u32::try_from(self.vertices.len()) { self.indices.iter().all(|&i| i < n) } else { @@ -106,6 +108,7 @@ impl Mesh { /// Append all the indices and vertices of `other` to `self`. pub fn append(&mut self, other: Self) { + crate::profile_function!(); crate::epaint_assert!(other.is_valid()); if self.is_empty() { diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index bc729045d..925524f2f 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -654,6 +654,15 @@ pub struct TessellationOptions { /// The default value will be 1.0e-5, it will be used during float compare. pub epsilon: f32, + + /// If `rayon` feature is activated, should we parallelize tessellation? + pub parallel_tessellation: bool, + + /// If `true`, invalid meshes will be silently ignored. + /// If `false`, invalid meshes will cause a panic. + /// + /// The default is `false` to save performance. + pub validate_meshes: bool, } impl Default for TessellationOptions { @@ -669,6 +678,8 @@ impl Default for TessellationOptions { debug_ignore_clip_rects: false, bezier_tolerance: 0.1, epsilon: 1.0e-5, + parallel_tessellation: true, + validate_meshes: false, } } } @@ -1065,6 +1076,7 @@ fn mul_color(color: Color32, factor: f32) -> Color32 { /// For performance reasons it is smart to reuse the same [`Tessellator`]. /// /// See also [`tessellate_shapes`], a convenient wrapper around [`Tessellator`]. +#[derive(Clone)] pub struct Tessellator { pixels_per_point: f32, options: TessellationOptions, @@ -1086,6 +1098,9 @@ pub struct Tessellator { impl Tessellator { /// Create a new [`Tessellator`]. /// + /// * `pixels_per_point`: number of physical pixels to each logical point + /// * `options`: tessellation quality + /// * `shapes`: what to tessellate /// * `font_tex_size`: size of the font texture. Required to normalize glyph uv rectangles when tessellating text. /// * `prepared_discs`: What [`TextureAtlas::prepared_discs`] returns. Can safely be set to an empty vec. pub fn new( @@ -1132,31 +1147,22 @@ impl Tessellator { clipped_shape: ClippedShape, out_primitives: &mut Vec, ) { - let ClippedShape { - clip_rect: new_clip_rect, - shape: new_shape, - } = clipped_shape; + let ClippedShape { clip_rect, shape } = clipped_shape; - if !new_clip_rect.is_positive() { + if !clip_rect.is_positive() { return; // skip empty clip rectangles } - if let Shape::Vec(shapes) = new_shape { + if let Shape::Vec(shapes) = shape { for shape in shapes { - self.tessellate_clipped_shape( - ClippedShape { - clip_rect: new_clip_rect, - shape, - }, - out_primitives, - ); + self.tessellate_clipped_shape(ClippedShape { clip_rect, shape }, out_primitives); } return; } - if let Shape::Callback(callback) = new_shape { + if let Shape::Callback(callback) = shape { out_primitives.push(ClippedPrimitive { - clip_rect: new_clip_rect, + clip_rect, primitive: Primitive::Callback(callback), }); return; @@ -1165,10 +1171,10 @@ impl Tessellator { let start_new_mesh = match out_primitives.last() { None => true, Some(output_clipped_primitive) => { - output_clipped_primitive.clip_rect != new_clip_rect + output_clipped_primitive.clip_rect != clip_rect || match &output_clipped_primitive.primitive { Primitive::Mesh(output_mesh) => { - output_mesh.texture_id != new_shape.texture_id() + output_mesh.texture_id != shape.texture_id() } Primitive::Callback(_) => true, } @@ -1177,7 +1183,7 @@ impl Tessellator { if start_new_mesh { out_primitives.push(ClippedPrimitive { - clip_rect: new_clip_rect, + clip_rect, primitive: Primitive::Mesh(Mesh::default()), }); } @@ -1185,8 +1191,8 @@ impl Tessellator { let out = out_primitives.last_mut().unwrap(); if let Primitive::Mesh(out_mesh) = &mut out.primitive { - self.clip_rect = new_clip_rect; - self.tessellate_shape(new_shape, out_mesh); + self.clip_rect = clip_rect; + self.tessellate_shape(shape, out_mesh); } else { unreachable!(); } @@ -1199,6 +1205,8 @@ impl Tessellator { /// * `shape`: the shape to tessellate. /// * `out`: triangles are appended to this. pub fn tessellate_shape(&mut self, shape: Shape, out: &mut Mesh) { + crate::profile_function!(); + match shape { Shape::Noop => {} Shape::Vec(vec) => { @@ -1210,16 +1218,20 @@ impl Tessellator { self.tessellate_circle(circle, out); } Shape::Mesh(mesh) => { - if !mesh.is_valid() { + crate::profile_scope!("mesh"); + + if self.options.validate_meshes && !mesh.is_valid() { crate::epaint_assert!(false, "Invalid Mesh in Shape::Mesh"); return; } + // note: `append` still checks if the mesh is valid if extra asserts are enabled. if self.options.coarse_tessellation_culling && !self.clip_rect.intersects(mesh.calc_bounds()) { return; } + out.append(mesh); } Shape::LineSegment { points, stroke } => self.tessellate_line(points, stroke, out), @@ -1362,6 +1374,8 @@ impl Tessellator { return; } + crate::profile_function!(); + let PathShape { points, closed, @@ -1674,21 +1688,7 @@ impl Tessellator { } } -/// Turns [`Shape`]:s into sets of triangles. -/// -/// The given shapes will tessellated in the same order as they are given. -/// They will be batched together by clip rectangle. -/// -/// * `pixels_per_point`: number of physical pixels to each logical point -/// * `options`: tessellation quality -/// * `shapes`: what to tessellate -/// * `font_tex_size`: size of the font texture. Required to normalize glyph uv rectangles when tessellating text. -/// * `prepared_discs`: What [`TextureAtlas::prepared_discs`] returns. Can safely be set to an empty vec. -/// -/// The implementation uses a [`Tessellator`]. -/// -/// ## Returns -/// A list of clip rectangles with matching [`Mesh`]. +#[deprecated = "Use `Tessellator::new(…).tessellate_shapes(…)` instead"] pub fn tessellate_shapes( pixels_per_point: f32, options: TessellationOptions, @@ -1696,67 +1696,146 @@ pub fn tessellate_shapes( prepared_discs: Vec, shapes: Vec, ) -> Vec { - let mut tessellator = - Tessellator::new(pixels_per_point, options, font_tex_size, prepared_discs); + Tessellator::new(pixels_per_point, options, font_tex_size, prepared_discs) + .tessellate_shapes(shapes) +} - let mut clipped_primitives: Vec = Vec::default(); +impl Tessellator { + /// Turns [`Shape`]:s into sets of triangles. + /// + /// The given shapes will tessellated in the same order as they are given. + /// They will be batched together by clip rectangle. + /// + /// * `pixels_per_point`: number of physical pixels to each logical point + /// * `options`: tessellation quality + /// * `shapes`: what to tessellate + /// * `font_tex_size`: size of the font texture. Required to normalize glyph uv rectangles when tessellating text. + /// * `prepared_discs`: What [`TextureAtlas::prepared_discs`] returns. Can safely be set to an empty vec. + /// + /// The implementation uses a [`Tessellator`]. + /// + /// ## Returns + /// A list of clip rectangles with matching [`Mesh`]. + #[allow(unused_mut)] + pub fn tessellate_shapes(&mut self, mut shapes: Vec) -> Vec { + crate::profile_function!(); + + #[cfg(feature = "rayon")] + if self.options.parallel_tessellation { + self.parallel_tessellation_of_large_shapes(&mut shapes); + } - for clipped_shape in shapes { - tessellator.tessellate_clipped_shape(clipped_shape, &mut clipped_primitives); - } + let mut clipped_primitives: Vec = Vec::default(); - if options.debug_paint_clip_rects { - clipped_primitives = add_clip_rects(&mut tessellator, clipped_primitives); - } + { + crate::profile_scope!("tessellate"); + for clipped_shape in shapes { + self.tessellate_clipped_shape(clipped_shape, &mut clipped_primitives); + } + } - if options.debug_ignore_clip_rects { - for clipped_primitive in &mut clipped_primitives { - clipped_primitive.clip_rect = Rect::EVERYTHING; + if self.options.debug_paint_clip_rects { + clipped_primitives = self.add_clip_rects(clipped_primitives); } - } - clipped_primitives.retain(|p| { - p.clip_rect.is_positive() - && match &p.primitive { - Primitive::Mesh(mesh) => !mesh.is_empty(), - Primitive::Callback(_) => true, + if self.options.debug_ignore_clip_rects { + for clipped_primitive in &mut clipped_primitives { + clipped_primitive.clip_rect = Rect::EVERYTHING; } - }); + } - for clipped_primitive in &clipped_primitives { - if let Primitive::Mesh(mesh) = &clipped_primitive.primitive { - crate::epaint_assert!(mesh.is_valid(), "Tessellator generated invalid Mesh"); + clipped_primitives.retain(|p| { + p.clip_rect.is_positive() + && match &p.primitive { + Primitive::Mesh(mesh) => !mesh.is_empty(), + Primitive::Callback(_) => true, + } + }); + + for clipped_primitive in &clipped_primitives { + if let Primitive::Mesh(mesh) = &clipped_primitive.primitive { + crate::epaint_assert!(mesh.is_valid(), "Tessellator generated invalid Mesh"); + } } + + clipped_primitives } - clipped_primitives -} + /// Find large shapes and throw them on the rayon thread pool, + /// then replace the original shape with their tessellated meshes. + #[cfg(feature = "rayon")] + fn parallel_tessellation_of_large_shapes(&self, shapes: &mut [ClippedShape]) { + crate::profile_function!(); -fn add_clip_rects( - tessellator: &mut Tessellator, - clipped_primitives: Vec, -) -> Vec { - tessellator.clip_rect = Rect::EVERYTHING; - let stroke = Stroke::new(2.0, Color32::from_rgb(150, 255, 150)); - - clipped_primitives - .into_iter() - .flat_map(|clipped_primitive| { - let mut clip_rect_mesh = Mesh::default(); - tessellator.tessellate_shape( - Shape::rect_stroke(clipped_primitive.clip_rect, 0.0, stroke), - &mut clip_rect_mesh, - ); + use rayon::prelude::*; + + // We only parallelize large/slow stuff, because each tessellation job + // will allocate a new Mesh, and so it creates a lot of extra memory framentation + // and callocations that is only worth it for large shapes. + fn should_parallelize(shape: &Shape) -> bool { + match shape { + Shape::Vec(shapes) => 4 < shapes.len() || shapes.iter().any(should_parallelize), + + Shape::Path(path_shape) => 32 < path_shape.points.len(), + + Shape::QuadraticBezier(_) | Shape::CubicBezier(_) => true, - [ - clipped_primitive, - ClippedPrimitive { - clip_rect: Rect::EVERYTHING, // whatever - primitive: Primitive::Mesh(clip_rect_mesh), - }, - ] - }) - .collect() + Shape::Noop + | Shape::Text(_) + | Shape::Circle(_) + | Shape::Mesh(_) + | Shape::LineSegment { .. } + | Shape::Rect(_) + | Shape::Callback(_) => false, + } + } + + let tessellated: Vec<(usize, Mesh)> = shapes + .par_iter() + .enumerate() + .filter(|(_, clipped_shape)| should_parallelize(&clipped_shape.shape)) + .map(|(index, clipped_shape)| { + crate::profile_scope!("tessellate_big_shape"); + // TODO: reuse tessellator in a thread local + let mut tessellator = (*self).clone(); + let mut mesh = Mesh::default(); + tessellator.tessellate_shape(clipped_shape.shape.clone(), &mut mesh); + (index, mesh) + }) + .collect(); + + crate::profile_scope!("distribute results", tessellated.len().to_string()); + for (index, mesh) in tessellated { + shapes[index].shape = Shape::Mesh(mesh); + } + } + + fn add_clip_rects( + &mut self, + clipped_primitives: Vec, + ) -> Vec { + self.clip_rect = Rect::EVERYTHING; + let stroke = Stroke::new(2.0, Color32::from_rgb(150, 255, 150)); + + clipped_primitives + .into_iter() + .flat_map(|clipped_primitive| { + let mut clip_rect_mesh = Mesh::default(); + self.tessellate_shape( + Shape::rect_stroke(clipped_primitive.clip_rect, 0.0, stroke), + &mut clip_rect_mesh, + ); + + [ + clipped_primitive, + ClippedPrimitive { + clip_rect: Rect::EVERYTHING, // whatever + primitive: Primitive::Mesh(clip_rect_mesh), + }, + ] + }) + .collect() + } } #[test] @@ -1785,12 +1864,8 @@ fn test_tessellator() { let font_tex_size = [1024, 1024]; // unused let prepared_discs = vec![]; // unused - let primitives = tessellate_shapes( - 1.0, - Default::default(), - font_tex_size, - prepared_discs, - clipped_shapes, - ); + let primitives = Tessellator::new(1.0, Default::default(), font_tex_size, prepared_discs) + .tessellate_shapes(clipped_shapes); + assert_eq!(primitives.len(), 2); }