Browse Source

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.
pull/3937/head
Emil Ernerfeldt 9 months ago
committed by GitHub
parent
commit
1db291721f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 46
      Cargo.lock
  2. 7
      crates/egui/Cargo.toml
  3. 4
      crates/egui/src/context.rs
  4. 8
      crates/egui/src/introspection.rs
  5. 12
      crates/epaint/Cargo.toml
  6. 6
      crates/epaint/benches/benchmark.rs
  7. 40
      crates/epaint/src/lib.rs
  8. 3
      crates/epaint/src/mesh.rs
  9. 257
      crates/epaint/src/tessellator.rs

46
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"

7
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"]

4
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

8
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
}

12
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"] }

6
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);
});
});
}

40
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<H: std::hash::Hasher>(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::*;

3
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() {

257
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<ClippedPrimitive>,
) {
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<PreparedDisc>,
shapes: Vec<ClippedShape>,
) -> Vec<ClippedPrimitive> {
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<ClippedPrimitive> = 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<ClippedShape>) -> Vec<ClippedPrimitive> {
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<ClippedPrimitive> = 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<ClippedPrimitive>,
) -> Vec<ClippedPrimitive> {
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<ClippedPrimitive>,
) -> Vec<ClippedPrimitive> {
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);
}

Loading…
Cancel
Save