|
| 1 | +// Copyright 2025 the Parley Authors |
| 2 | +// SPDX-License-Identifier: Apache-2.0 OR MIT |
| 3 | + |
| 4 | +//! # Draw Benchmarks |
| 5 | +//! |
| 6 | +//! Benchmarks for text rendering using `parley_draw` with `vello_cpu`. |
| 7 | +
|
| 8 | +use crate::{ColorBrush, FONT_FAMILY_LIST, with_contexts}; |
| 9 | +use parley::{ |
| 10 | + Alignment, AlignmentOptions, FontFamily, Layout, PositionedLayoutItem, StyleProperty, |
| 11 | +}; |
| 12 | +use parley_draw::{Glyph, GlyphCaches, GlyphRunBuilder}; |
| 13 | +use std::hint::black_box; |
| 14 | +use tango_bench::{Benchmark, benchmark_fn}; |
| 15 | +use vello_cpu::{RenderContext, kurbo}; |
| 16 | + |
| 17 | +const DISPLAY_SCALE: f32 = 1.0; |
| 18 | +const QUANTIZE: bool = true; |
| 19 | +const MAX_ADVANCE: f32 = 400.0 * DISPLAY_SCALE; |
| 20 | +const PADDING: u16 = 20; |
| 21 | + |
| 22 | +/// Long sample text for benchmarking. |
| 23 | +const SAMPLE_TEXT: &str = "Call me Ishmael. Some years ago—never mind how long precisely—having\ |
| 24 | +little or no money in my purse, and nothing particular to interest me\ |
| 25 | +on shore, I thought I would sail about a little and see the watery part\ |
| 26 | +of the world. It is a way I have of driving off the spleen and\ |
| 27 | +regulating the circulation. Whenever I find myself growing grim about\ |
| 28 | +the mouth; whenever it is a damp, drizzly November in my soul; whenever\ |
| 29 | +I find myself involuntarily pausing before coffin warehouses, and\ |
| 30 | +bringing up the rear of every funeral I meet; and especially whenever\ |
| 31 | +my hypos get such an upper hand of me, that it requires a strong moral\ |
| 32 | +principle to prevent me from deliberately stepping into the street, and\ |
| 33 | +methodically knocking people’s hats off—then, I account it high time to\ |
| 34 | +get to sea as soon as I can. This is my substitute for pistol and ball.\ |
| 35 | +With a philosophical flourish Cato throws himself upon his sword; I\ |
| 36 | +quietly take to the ship. There is nothing surprising in this. If they\ |
| 37 | +but knew it, almost all men in their degree, some time or other,\ |
| 38 | +cherish very nearly the same feelings towards the ocean with me. |
| 39 | +
|
| 40 | +There now is your insular city of the Manhattoes, belted round by\ |
| 41 | +wharves as Indian isles by coral reefs—commerce surrounds it with her\ |
| 42 | +surf. Right and left, the streets take you waterward. Its extreme\ |
| 43 | +downtown is the battery, where that noble mole is washed by waves, and\ |
| 44 | +cooled by breezes, which a few hours previous were out of sight of\ |
| 45 | +land. Look at the crowds of water-gazers there."; |
| 46 | + |
| 47 | +/// Builds a layout with or without underlines. |
| 48 | +fn build_layout(text: &str, underline: bool) -> Layout<ColorBrush> { |
| 49 | + with_contexts(|font_cx, layout_cx| { |
| 50 | + let mut builder = layout_cx.ranged_builder(font_cx, text, DISPLAY_SCALE, QUANTIZE); |
| 51 | + builder.push_default(FontFamily::from(FONT_FAMILY_LIST)); |
| 52 | + builder.push_default(StyleProperty::FontSize(16.0)); |
| 53 | + |
| 54 | + if underline { |
| 55 | + builder.push(StyleProperty::Underline(true), 0..text.len()); |
| 56 | + } |
| 57 | + |
| 58 | + let mut layout: Layout<ColorBrush> = builder.build(text); |
| 59 | + layout.break_all_lines(Some(MAX_ADVANCE)); |
| 60 | + layout.align( |
| 61 | + Some(MAX_ADVANCE), |
| 62 | + Alignment::Start, |
| 63 | + AlignmentOptions::default(), |
| 64 | + ); |
| 65 | + layout |
| 66 | + }) |
| 67 | +} |
| 68 | + |
| 69 | +/// Renders a layout to a renderer, optionally with underlines. |
| 70 | +fn render_layout( |
| 71 | + layout: &Layout<ColorBrush>, |
| 72 | + renderer: &mut RenderContext, |
| 73 | + glyph_caches: &mut GlyphCaches, |
| 74 | + with_underline: bool, |
| 75 | +) { |
| 76 | + for line in layout.lines() { |
| 77 | + for item in line.items() { |
| 78 | + match item { |
| 79 | + PositionedLayoutItem::GlyphRun(glyph_run) => { |
| 80 | + let run = glyph_run.run(); |
| 81 | + GlyphRunBuilder::new(run.font().clone(), *renderer.transform(), renderer) |
| 82 | + .font_size(run.font_size()) |
| 83 | + .hint(true) |
| 84 | + .normalized_coords(run.normalized_coords()) |
| 85 | + .fill_glyphs( |
| 86 | + glyph_run.positioned_glyphs().map(|glyph| Glyph { |
| 87 | + id: glyph.id, |
| 88 | + x: glyph.x, |
| 89 | + y: glyph.y, |
| 90 | + }), |
| 91 | + glyph_caches, |
| 92 | + ); |
| 93 | + |
| 94 | + if with_underline { |
| 95 | + if let Some(decoration) = &glyph_run.style().underline { |
| 96 | + let offset = |
| 97 | + decoration.offset.unwrap_or(run.metrics().underline_offset); |
| 98 | + let size = decoration.size.unwrap_or(run.metrics().underline_size); |
| 99 | + |
| 100 | + let x = glyph_run.offset(); |
| 101 | + let x1 = x + glyph_run.advance(); |
| 102 | + let baseline = glyph_run.baseline(); |
| 103 | + |
| 104 | + GlyphRunBuilder::new( |
| 105 | + run.font().clone(), |
| 106 | + *renderer.transform(), |
| 107 | + renderer, |
| 108 | + ) |
| 109 | + .font_size(run.font_size()) |
| 110 | + .normalized_coords(run.normalized_coords()) |
| 111 | + .render_decoration( |
| 112 | + glyph_run.positioned_glyphs().map(|glyph| Glyph { |
| 113 | + id: glyph.id, |
| 114 | + x: glyph.x, |
| 115 | + y: glyph.y, |
| 116 | + }), |
| 117 | + x..=x1, |
| 118 | + baseline, |
| 119 | + offset, |
| 120 | + size, |
| 121 | + 1.0, // buffer around exclusions |
| 122 | + glyph_caches, |
| 123 | + ); |
| 124 | + } |
| 125 | + } |
| 126 | + } |
| 127 | + PositionedLayoutItem::InlineBox(_) => {} |
| 128 | + } |
| 129 | + } |
| 130 | + } |
| 131 | +} |
| 132 | + |
| 133 | +/// Creates the render context for drawing. |
| 134 | +fn create_renderer(layout: &Layout<ColorBrush>) -> RenderContext { |
| 135 | + #[expect( |
| 136 | + clippy::cast_possible_truncation, |
| 137 | + reason = "the layout's not *that* big" |
| 138 | + )] |
| 139 | + let width = layout.width().ceil() as u16 + PADDING * 2; |
| 140 | + #[expect( |
| 141 | + clippy::cast_possible_truncation, |
| 142 | + reason = "the layout's not *that* big" |
| 143 | + )] |
| 144 | + let height = layout.height().ceil() as u16 + PADDING * 2; |
| 145 | + |
| 146 | + let mut renderer = RenderContext::new(width, height); |
| 147 | + renderer.set_transform(kurbo::Affine::translate(kurbo::Vec2::new( |
| 148 | + PADDING as f64, |
| 149 | + PADDING as f64, |
| 150 | + ))); |
| 151 | + renderer |
| 152 | +} |
| 153 | + |
| 154 | +/// Benchmark for drawing text without underlines, with a fresh cache each iteration. |
| 155 | +pub fn draw_no_underline_cold_cache() -> Vec<Benchmark> { |
| 156 | + vec![benchmark_fn("Draw - No underline (cold cache)", |b| { |
| 157 | + let layout = build_layout(SAMPLE_TEXT, false); |
| 158 | + |
| 159 | + b.iter(move || { |
| 160 | + let layout = layout.clone(); |
| 161 | + let mut renderer = create_renderer(&layout); |
| 162 | + let mut glyph_caches = GlyphCaches::new(); |
| 163 | + render_layout(&layout, &mut renderer, &mut glyph_caches, false); |
| 164 | + black_box(&renderer); |
| 165 | + }) |
| 166 | + })] |
| 167 | +} |
| 168 | + |
| 169 | +/// Benchmark for drawing text without underlines, reusing the cache. |
| 170 | +pub fn draw_no_underline_warm_cache() -> Vec<Benchmark> { |
| 171 | + vec![benchmark_fn("Draw - No underline (warm cache)", |b| { |
| 172 | + let layout = build_layout(SAMPLE_TEXT, false); |
| 173 | + let mut glyph_caches = GlyphCaches::new(); |
| 174 | + |
| 175 | + b.iter(move || { |
| 176 | + let mut renderer = create_renderer(&layout); |
| 177 | + render_layout(&layout, &mut renderer, &mut glyph_caches, false); |
| 178 | + glyph_caches.maintain(); |
| 179 | + black_box(&renderer); |
| 180 | + }) |
| 181 | + })] |
| 182 | +} |
| 183 | + |
| 184 | +/// Benchmark for drawing text with underlines, with a fresh cache each iteration. |
| 185 | +pub fn draw_with_underline_cold_cache() -> Vec<Benchmark> { |
| 186 | + vec![benchmark_fn("Draw - With underline (cold cache)", |b| { |
| 187 | + let layout = build_layout(SAMPLE_TEXT, true); |
| 188 | + |
| 189 | + b.iter(move || { |
| 190 | + let layout = layout.clone(); |
| 191 | + let mut renderer = create_renderer(&layout); |
| 192 | + let mut glyph_caches = GlyphCaches::new(); |
| 193 | + render_layout(&layout, &mut renderer, &mut glyph_caches, true); |
| 194 | + black_box(&renderer); |
| 195 | + }) |
| 196 | + })] |
| 197 | +} |
| 198 | + |
| 199 | +/// Benchmark for drawing text with underlines, reusing the cache. |
| 200 | +pub fn draw_with_underline_warm_cache() -> Vec<Benchmark> { |
| 201 | + vec![benchmark_fn("Draw - With underline (warm cache)", |b| { |
| 202 | + let layout = build_layout(SAMPLE_TEXT, true); |
| 203 | + let mut glyph_caches = GlyphCaches::new(); |
| 204 | + |
| 205 | + b.iter(move || { |
| 206 | + let mut renderer = create_renderer(&layout); |
| 207 | + render_layout(&layout, &mut renderer, &mut glyph_caches, true); |
| 208 | + glyph_caches.maintain(); |
| 209 | + black_box(&renderer); |
| 210 | + }) |
| 211 | + })] |
| 212 | +} |
0 commit comments