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.
This commit is contained in:
Emil Ernerfeldt 2024-02-01 16:27:59 +01:00 committed by GitHub
parent 8860930ec8
commit 1db291721f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 290 additions and 111 deletions

46
Cargo.lock generated
View File

@ -1011,14 +1011,30 @@ dependencies = [
] ]
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-deque"
version = "0.8.16" version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
dependencies = [ 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]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.6" version = "0.1.6"
@ -1460,6 +1476,8 @@ dependencies = [
"log", "log",
"nohash-hasher", "nohash-hasher",
"parking_lot", "parking_lot",
"puffin",
"rayon",
"serde", "serde",
] ]
@ -3000,6 +3018,26 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544" 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]] [[package]]
name = "rctree" name = "rctree"
version = "0.5.0" version = "0.5.0"

View File

@ -66,7 +66,12 @@ persistence = ["serde", "epaint/serde", "ron"]
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. ## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
## ##
## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. ## 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). ## Allow serialization using [`serde`](https://docs.rs/serde).
serde = ["dep:serde", "epaint/serde", "accesskit?/serde"] serde = ["dep:serde", "epaint/serde", "accesskit?/serde"]

View File

@ -1977,13 +1977,13 @@ impl Context {
let paint_stats = PaintStats::from_shapes(&shapes); let paint_stats = PaintStats::from_shapes(&shapes);
let clipped_primitives = { let clipped_primitives = {
crate::profile_scope!("tessellator::tessellate_shapes"); crate::profile_scope!("tessellator::tessellate_shapes");
tessellator::tessellate_shapes( tessellator::Tessellator::new(
pixels_per_point, pixels_per_point,
tessellation_options, tessellation_options,
font_tex_size, font_tex_size,
prepared_discs, prepared_discs,
shapes,
) )
.tessellate_shapes(shapes)
}; };
ctx.paint_stats = paint_stats.with_clipped_primitives(&clipped_primitives); ctx.paint_stats = paint_stats.with_clipped_primitives(&clipped_primitives);
clipped_primitives clipped_primitives

View File

@ -146,6 +146,8 @@ impl Widget for &mut epaint::TessellationOptions {
debug_ignore_clip_rects, debug_ignore_clip_rects,
bezier_tolerance, bezier_tolerance,
epsilon: _, epsilon: _,
parallel_tessellation,
validate_meshes,
} = self; } = self;
ui.checkbox(feathering, "Feathering (antialias)") 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_clip_rects, "Paint clip rectangles");
ui.checkbox(debug_paint_text_rects, "Paint text bounds"); 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 .response
} }

View File

@ -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`](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"] 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). ## Allow serialization using [`serde`](https://docs.rs/serde).
serde = ["dep:serde", "ahash/serde", "emath/serde", "ecolor/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 } document-features = { version = "0.2", optional = true }
log = { version = "0.4", optional = true, features = ["std"] } 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) . ## Allow serialization using [`serde`](https://docs.rs/serde) .
serde = { version = "1", optional = true, features = ["derive", "rc"] } serde = { version = "1", optional = true, features = ["derive", "rc"] }

View File

@ -60,14 +60,14 @@ fn tessellate_circles(c: &mut Criterion) {
let prepared_discs = atlas.prepared_discs(); let prepared_discs = atlas.prepared_discs();
b.iter(|| { b.iter(|| {
let clipped_primitive = tessellate_shapes( let mut tessellator = Tessellator::new(
pixels_per_point, pixels_per_point,
options, options,
font_tex_size, font_tex_size,
prepared_discs.clone(), prepared_discs.clone(),
clipped_shapes.clone(),
); );
black_box(clipped_primitive); let clipped_primitives = tessellator.tessellate_shapes(clipped_shapes.clone());
black_box(clipped_primitives);
}); });
}); });
} }

View File

@ -52,13 +52,16 @@ pub use {
}, },
stats::PaintStats, stats::PaintStats,
stroke::Stroke, stroke::Stroke,
tessellator::{tessellate_shapes, TessellationOptions, Tessellator}, tessellator::{TessellationOptions, Tessellator},
text::{FontFamily, FontId, Fonts, Galley}, text::{FontFamily, FontId, Fonts, Galley},
texture_atlas::TextureAtlas, texture_atlas::TextureAtlas,
texture_handle::TextureHandle, texture_handle::TextureHandle,
textures::TextureManager, textures::TextureManager,
}; };
#[allow(deprecated)]
pub use tessellator::tessellate_shapes;
pub use ecolor::{Color32, Hsva, HsvaGamma, Rgba}; pub use ecolor::{Color32, Hsva, HsvaGamma, Rgba};
pub use emath::{pos2, vec2, Pos2, Rect, Vec2}; 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); 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::*;

View File

@ -84,6 +84,8 @@ impl Mesh {
/// Are all indices within the bounds of the contained vertices? /// Are all indices within the bounds of the contained vertices?
pub fn is_valid(&self) -> bool { pub fn is_valid(&self) -> bool {
crate::profile_function!();
if let Ok(n) = u32::try_from(self.vertices.len()) { if let Ok(n) = u32::try_from(self.vertices.len()) {
self.indices.iter().all(|&i| i < n) self.indices.iter().all(|&i| i < n)
} else { } else {
@ -106,6 +108,7 @@ impl Mesh {
/// Append all the indices and vertices of `other` to `self`. /// Append all the indices and vertices of `other` to `self`.
pub fn append(&mut self, other: Self) { pub fn append(&mut self, other: Self) {
crate::profile_function!();
crate::epaint_assert!(other.is_valid()); crate::epaint_assert!(other.is_valid());
if self.is_empty() { if self.is_empty() {

View File

@ -654,6 +654,15 @@ pub struct TessellationOptions {
/// The default value will be 1.0e-5, it will be used during float compare. /// The default value will be 1.0e-5, it will be used during float compare.
pub epsilon: f32, 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 { impl Default for TessellationOptions {
@ -669,6 +678,8 @@ impl Default for TessellationOptions {
debug_ignore_clip_rects: false, debug_ignore_clip_rects: false,
bezier_tolerance: 0.1, bezier_tolerance: 0.1,
epsilon: 1.0e-5, 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`]. /// For performance reasons it is smart to reuse the same [`Tessellator`].
/// ///
/// See also [`tessellate_shapes`], a convenient wrapper around [`Tessellator`]. /// See also [`tessellate_shapes`], a convenient wrapper around [`Tessellator`].
#[derive(Clone)]
pub struct Tessellator { pub struct Tessellator {
pixels_per_point: f32, pixels_per_point: f32,
options: TessellationOptions, options: TessellationOptions,
@ -1086,6 +1098,9 @@ pub struct Tessellator {
impl Tessellator { impl Tessellator {
/// Create a new [`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. /// * `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. /// * `prepared_discs`: What [`TextureAtlas::prepared_discs`] returns. Can safely be set to an empty vec.
pub fn new( pub fn new(
@ -1132,31 +1147,22 @@ impl Tessellator {
clipped_shape: ClippedShape, clipped_shape: ClippedShape,
out_primitives: &mut Vec<ClippedPrimitive>, out_primitives: &mut Vec<ClippedPrimitive>,
) { ) {
let ClippedShape { let ClippedShape { clip_rect, shape } = clipped_shape;
clip_rect: new_clip_rect,
shape: new_shape,
} = clipped_shape;
if !new_clip_rect.is_positive() { if !clip_rect.is_positive() {
return; // skip empty clip rectangles return; // skip empty clip rectangles
} }
if let Shape::Vec(shapes) = new_shape { if let Shape::Vec(shapes) = shape {
for shape in shapes { for shape in shapes {
self.tessellate_clipped_shape( self.tessellate_clipped_shape(ClippedShape { clip_rect, shape }, out_primitives);
ClippedShape {
clip_rect: new_clip_rect,
shape,
},
out_primitives,
);
} }
return; return;
} }
if let Shape::Callback(callback) = new_shape { if let Shape::Callback(callback) = shape {
out_primitives.push(ClippedPrimitive { out_primitives.push(ClippedPrimitive {
clip_rect: new_clip_rect, clip_rect,
primitive: Primitive::Callback(callback), primitive: Primitive::Callback(callback),
}); });
return; return;
@ -1165,10 +1171,10 @@ impl Tessellator {
let start_new_mesh = match out_primitives.last() { let start_new_mesh = match out_primitives.last() {
None => true, None => true,
Some(output_clipped_primitive) => { Some(output_clipped_primitive) => {
output_clipped_primitive.clip_rect != new_clip_rect output_clipped_primitive.clip_rect != clip_rect
|| match &output_clipped_primitive.primitive { || match &output_clipped_primitive.primitive {
Primitive::Mesh(output_mesh) => { Primitive::Mesh(output_mesh) => {
output_mesh.texture_id != new_shape.texture_id() output_mesh.texture_id != shape.texture_id()
} }
Primitive::Callback(_) => true, Primitive::Callback(_) => true,
} }
@ -1177,7 +1183,7 @@ impl Tessellator {
if start_new_mesh { if start_new_mesh {
out_primitives.push(ClippedPrimitive { out_primitives.push(ClippedPrimitive {
clip_rect: new_clip_rect, clip_rect,
primitive: Primitive::Mesh(Mesh::default()), primitive: Primitive::Mesh(Mesh::default()),
}); });
} }
@ -1185,8 +1191,8 @@ impl Tessellator {
let out = out_primitives.last_mut().unwrap(); let out = out_primitives.last_mut().unwrap();
if let Primitive::Mesh(out_mesh) = &mut out.primitive { if let Primitive::Mesh(out_mesh) = &mut out.primitive {
self.clip_rect = new_clip_rect; self.clip_rect = clip_rect;
self.tessellate_shape(new_shape, out_mesh); self.tessellate_shape(shape, out_mesh);
} else { } else {
unreachable!(); unreachable!();
} }
@ -1199,6 +1205,8 @@ impl Tessellator {
/// * `shape`: the shape to tessellate. /// * `shape`: the shape to tessellate.
/// * `out`: triangles are appended to this. /// * `out`: triangles are appended to this.
pub fn tessellate_shape(&mut self, shape: Shape, out: &mut Mesh) { pub fn tessellate_shape(&mut self, shape: Shape, out: &mut Mesh) {
crate::profile_function!();
match shape { match shape {
Shape::Noop => {} Shape::Noop => {}
Shape::Vec(vec) => { Shape::Vec(vec) => {
@ -1210,16 +1218,20 @@ impl Tessellator {
self.tessellate_circle(circle, out); self.tessellate_circle(circle, out);
} }
Shape::Mesh(mesh) => { 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"); crate::epaint_assert!(false, "Invalid Mesh in Shape::Mesh");
return; return;
} }
// note: `append` still checks if the mesh is valid if extra asserts are enabled.
if self.options.coarse_tessellation_culling if self.options.coarse_tessellation_culling
&& !self.clip_rect.intersects(mesh.calc_bounds()) && !self.clip_rect.intersects(mesh.calc_bounds())
{ {
return; return;
} }
out.append(mesh); out.append(mesh);
} }
Shape::LineSegment { points, stroke } => self.tessellate_line(points, stroke, out), Shape::LineSegment { points, stroke } => self.tessellate_line(points, stroke, out),
@ -1362,6 +1374,8 @@ impl Tessellator {
return; return;
} }
crate::profile_function!();
let PathShape { let PathShape {
points, points,
closed, closed,
@ -1674,21 +1688,7 @@ impl Tessellator {
} }
} }
/// Turns [`Shape`]:s into sets of triangles. #[deprecated = "Use `Tessellator::new(…).tessellate_shapes(…)` instead"]
///
/// 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`].
pub fn tessellate_shapes( pub fn tessellate_shapes(
pixels_per_point: f32, pixels_per_point: f32,
options: TessellationOptions, options: TessellationOptions,
@ -1696,20 +1696,49 @@ pub fn tessellate_shapes(
prepared_discs: Vec<PreparedDisc>, prepared_discs: Vec<PreparedDisc>,
shapes: Vec<ClippedShape>, shapes: Vec<ClippedShape>,
) -> Vec<ClippedPrimitive> { ) -> 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)
}
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);
}
let mut clipped_primitives: Vec<ClippedPrimitive> = Vec::default(); let mut clipped_primitives: Vec<ClippedPrimitive> = Vec::default();
{
crate::profile_scope!("tessellate");
for clipped_shape in shapes { for clipped_shape in shapes {
tessellator.tessellate_clipped_shape(clipped_shape, &mut clipped_primitives); self.tessellate_clipped_shape(clipped_shape, &mut clipped_primitives);
}
} }
if options.debug_paint_clip_rects { if self.options.debug_paint_clip_rects {
clipped_primitives = add_clip_rects(&mut tessellator, clipped_primitives); clipped_primitives = self.add_clip_rects(clipped_primitives);
} }
if options.debug_ignore_clip_rects { if self.options.debug_ignore_clip_rects {
for clipped_primitive in &mut clipped_primitives { for clipped_primitive in &mut clipped_primitives {
clipped_primitive.clip_rect = Rect::EVERYTHING; clipped_primitive.clip_rect = Rect::EVERYTHING;
} }
@ -1730,20 +1759,69 @@ pub fn tessellate_shapes(
} }
clipped_primitives clipped_primitives
} }
fn add_clip_rects( /// Find large shapes and throw them on the rayon thread pool,
tessellator: &mut Tessellator, /// 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!();
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,
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>, clipped_primitives: Vec<ClippedPrimitive>,
) -> Vec<ClippedPrimitive> { ) -> Vec<ClippedPrimitive> {
tessellator.clip_rect = Rect::EVERYTHING; self.clip_rect = Rect::EVERYTHING;
let stroke = Stroke::new(2.0, Color32::from_rgb(150, 255, 150)); let stroke = Stroke::new(2.0, Color32::from_rgb(150, 255, 150));
clipped_primitives clipped_primitives
.into_iter() .into_iter()
.flat_map(|clipped_primitive| { .flat_map(|clipped_primitive| {
let mut clip_rect_mesh = Mesh::default(); let mut clip_rect_mesh = Mesh::default();
tessellator.tessellate_shape( self.tessellate_shape(
Shape::rect_stroke(clipped_primitive.clip_rect, 0.0, stroke), Shape::rect_stroke(clipped_primitive.clip_rect, 0.0, stroke),
&mut clip_rect_mesh, &mut clip_rect_mesh,
); );
@ -1757,6 +1835,7 @@ fn add_clip_rects(
] ]
}) })
.collect() .collect()
}
} }
#[test] #[test]
@ -1785,12 +1864,8 @@ fn test_tessellator() {
let font_tex_size = [1024, 1024]; // unused let font_tex_size = [1024, 1024]; // unused
let prepared_discs = vec![]; // unused let prepared_discs = vec![]; // unused
let primitives = tessellate_shapes( let primitives = Tessellator::new(1.0, Default::default(), font_tex_size, prepared_discs)
1.0, .tessellate_shapes(clipped_shapes);
Default::default(),
font_tex_size,
prepared_discs,
clipped_shapes,
);
assert_eq!(primitives.len(), 2); assert_eq!(primitives.len(), 2);
} }