Skip to main content

ravenrs_perf/
capacity.rs

1//! How much the renderer can draw: each kind of work grows until a frame stops fitting.
2//!
3//! The screens in `scenes` ask whether real screens are cheap; this asks where the renderer
4//! itself becomes the bottleneck for whoever uses it. The time counted runs from handing the
5//! scene over to the GPU having finished, because the GPU's own clock is too coarse on short
6//! passes, and because this is what a frame really has to fit in.
7
8use std::time::{Duration, Instant};
9
10use ravenrs_core::{Color, ImageData, ImageQuad, PathElement, PathFill, Point, Quad, Rect, Scene, Shadow};
11use ravenrs_render::Renderer;
12use ravenrs_text::{TextAttrs, TextSystem};
13
14use crate::{Bench, FORMAT, SCALE, TARGET, VIEWPORT};
15
16/// A frame on the 120 Hz screen, in seconds.
17pub const BUDGET_120: f64 = 1.0 / 120.0;
18/// A frame on a 60 Hz screen, in seconds.
19pub const BUDGET_60: f64 = 1.0 / 60.0;
20/// Frames before timing, for atlases and caches to fill.
21const WARM: usize = 8;
22/// Frames timed at each size.
23const FRAMES: usize = 24;
24/// The growth stops once a frame takes this long, in seconds.
25const ENOUGH: f64 = 0.05;
26/// Nothing asks for more than this.
27const MOST: usize = 4_000_000;
28/// Images in the scene with many different images.
29const DISTINCT_IMAGES: usize = 512;
30
31/// One kind of work.
32#[derive(Clone, Copy, Debug)]
33pub struct Load {
34    /// Its name in the table.
35    pub name: &'static str,
36    /// What is being counted.
37    pub unit: &'static str,
38    /// Where the growth starts.
39    pub start: usize,
40    /// Makes a scene with this many.
41    pub make: fn(usize, &mut Assets) -> Scene,
42}
43
44/// What the scenes draw with.
45#[derive(Debug)]
46pub struct Assets {
47    text: TextSystem,
48    images: Vec<ImageData>,
49    circle: std::sync::Arc<[PathElement]>,
50}
51
52impl Default for Assets {
53    fn default() -> Self {
54        let images = (0..DISTINCT_IMAGES as u32)
55            .filter_map(|i| {
56                let pixels: Vec<u8> =
57                    (0..32u32 * 32).flat_map(|p| [(i % 256) as u8, p as u8, 90, 255]).collect();
58                ImageData::from_rgba8(32, 32, pixels)
59            })
60            .collect();
61        Self { text: TextSystem::new(), images, circle: circle().into() }
62    }
63}
64
65/// A circle of 12 logical pixels of radius, in curves, at the origin.
66fn circle() -> Vec<PathElement> {
67    const R: f32 = 12.0;
68    const K: f32 = R * 0.5523;
69    let p = Point::new;
70    vec![
71        PathElement::MoveTo(p(R, 0.0)),
72        PathElement::CubicTo(p(R, K), p(K, R), p(0.0, R)),
73        PathElement::CubicTo(p(-K, R), p(-R, K), p(-R, 0.0)),
74        PathElement::CubicTo(p(-R, -K), p(-K, -R), p(0.0, -R)),
75        PathElement::CubicTo(p(K, -R), p(R, -K), p(R, 0.0)),
76        PathElement::Close,
77    ]
78}
79
80/// Where the `i`-th of `n` things goes, spread over the whole window.
81fn place(i: usize, n: usize, width: f32, height: f32) -> Point {
82    let columns = ((n as f32 * VIEWPORT.width / VIEWPORT.height).sqrt().ceil() as usize).max(1);
83    let rows = n.div_ceil(columns).max(1);
84    let (column, row) = (i % columns, i / columns);
85    Point::new(
86        column as f32 * (VIEWPORT.width - width) / columns.max(2).saturating_sub(1) as f32,
87        row as f32 * (VIEWPORT.height - height) / rows.max(2).saturating_sub(1) as f32,
88    )
89}
90
91fn color(i: usize) -> Color {
92    Color::rgba(0.2 + (i % 7) as f32 * 0.1, 0.4, 0.9 - (i % 5) as f32 * 0.1, 1.0)
93}
94
95fn card(i: usize, n: usize) -> Rect {
96    let at = place(i, n, 40.0, 24.0);
97    Rect::new(at.x, at.y, 40.0, 24.0)
98}
99
100fn quads(n: usize, _: &mut Assets) -> Scene {
101    let mut scene = Scene::new();
102    for i in 0..n {
103        scene.push_quad(Quad::new(card(i, n), color(i)).radius(6.0));
104    }
105    scene
106}
107
108fn shadows(n: usize, _: &mut Assets) -> Scene {
109    let mut scene = Scene::new();
110    for i in 0..n {
111        let mut quad = Quad::new(card(i, n), color(i)).radius(6.0);
112        quad.shadow = Some(Shadow::soft());
113        scene.push_quad(quad);
114    }
115    scene
116}
117
118const SENTENCE: &str = "The quick brown fox jumps over the lazy dog";
119
120fn sentences(n: usize, attrs: &TextAttrs, assets: &mut Assets, between: bool) -> Scene {
121    let mut layout = assets.text.layout(SENTENCE, attrs);
122    layout.measure(None);
123    let mut scene = Scene::new();
124    for i in 0..n {
125        let at = place(i, n, 300.0, 20.0);
126        if between {
127            scene.push_quad(Quad::new(Rect::new(at.x, at.y, 300.0, 20.0), color(i)).radius(4.0));
128        }
129        layout.paint(at, Color::WHITE, &mut scene);
130    }
131    scene
132}
133
134fn glyphs(n: usize, assets: &mut Assets) -> Scene {
135    sentences(n / SENTENCE.len(), &TextAttrs::default(), assets, false)
136}
137
138/// What a real screen looks like: a shape, then text on it, and so on, which splits the draws.
139fn ui_mix(n: usize, assets: &mut Assets) -> Scene {
140    sentences(n, &TextAttrs::default(), assets, true)
141}
142
143fn clips(n: usize, _: &mut Assets) -> Scene {
144    let mut scene = Scene::new();
145    for i in 0..n {
146        let rect = card(i, n);
147        scene.push_clip(rect);
148        scene.push_quad(Quad::new(rect, color(i)).radius(6.0));
149        scene.pop_clip();
150    }
151    scene
152}
153
154fn paths(n: usize, assets: &mut Assets) -> Scene {
155    let mut scene = Scene::new();
156    for i in 0..n {
157        let at = place(i, n, 24.0, 24.0);
158        scene.push_path(PathFill {
159            transform: [1.0, 0.0, 0.0, 1.0, at.x + 12.0, at.y + 12.0],
160            ..PathFill::solid(assets.circle.clone(), color(i))
161        });
162    }
163    scene
164}
165
166/// Shapes as big as the window, as a large animation has.
167fn big_paths(n: usize, assets: &mut Assets) -> Scene {
168    let mut scene = Scene::new();
169    let scale = VIEWPORT.height / 24.0;
170    for i in 0..n {
171        scene.push_path(PathFill {
172            transform: [scale, 0.0, 0.0, scale, VIEWPORT.width / 2.0, VIEWPORT.height / 2.0],
173            opacity: 0.05,
174            ..PathFill::solid(assets.circle.clone(), color(i))
175        });
176    }
177    scene
178}
179
180fn images(n: usize, assets: &mut Assets) -> Scene {
181    let mut scene = Scene::new();
182    for i in 0..n {
183        let image = assets.images[i % assets.images.len()].clone();
184        scene.push_image(ImageQuad::new(card(i, n), image));
185    }
186    scene
187}
188
189/// Layers covering the whole window, each seen through the last: what the GPU's fill costs.
190fn overdraw(n: usize, _: &mut Assets) -> Scene {
191    let mut scene = Scene::new();
192    let window = Rect::from_origin_size(Point::ZERO, VIEWPORT);
193    for i in 0..n {
194        let tint = Color::rgba(0.3, 0.5, (i % 3) as f32 * 0.3, 0.05);
195        scene.push_quad(Quad::new(window, tint).radius(16.0));
196    }
197    scene
198}
199
200/// Every kind of work measured, in the order reported.
201pub const LOADS: [Load; 9] = [
202    Load { name: "quads", unit: "quads", start: 1_000, make: quads },
203    Load { name: "soft shadows", unit: "quads", start: 500, make: shadows },
204    Load { name: "text", unit: "glyphs", start: 2_000, make: glyphs },
205    Load { name: "ui mix", unit: "quad+text", start: 100, make: ui_mix },
206    Load { name: "clips", unit: "clipped quads", start: 500, make: clips },
207    Load { name: "curves", unit: "shapes", start: 100, make: paths },
208    Load { name: "big curves", unit: "shapes", start: 2, make: big_paths },
209    Load { name: "images", unit: "images", start: 100, make: images },
210    Load { name: "overdraw", unit: "full layers", start: 2, make: overdraw },
211];
212
213/// What a frame took with this much work.
214#[derive(Clone, Copy, Debug)]
215pub struct Point2 {
216    /// How much was drawn.
217    pub amount: usize,
218    /// Preparing and submitting on the CPU, in seconds.
219    pub cpu: f64,
220    /// Until the GPU finished, in seconds.
221    pub total: f64,
222    /// Draw calls.
223    pub draw_calls: usize,
224    /// Shapes that really got drawn, for telling when some are being left out.
225    pub paths: usize,
226}
227
228fn median(values: &mut [Duration]) -> f64 {
229    values.sort();
230    values.get(values.len() / 2).map_or(0.0, Duration::as_secs_f64)
231}
232
233impl Bench {
234    /// Grows `load` until a frame takes too long, and says what each size cost.
235    pub fn capacity(&self, load: &Load) -> Vec<Point2> {
236        let mut assets = Assets::default();
237        let mut renderer = Renderer::new(&self.gpu.device, &self.gpu.queue, FORMAT);
238        let mut curve = Vec::new();
239        let mut amount = load.start;
240        while amount <= MOST {
241            let scene = (load.make)(amount, &mut assets);
242            let (mut draw_calls, mut paths) = (0, 0);
243            for _ in 0..WARM {
244                renderer.render(&scene, &self.target, TARGET, SCALE);
245                self.wait();
246            }
247            let (mut cpu, mut total) = (Vec::new(), Vec::new());
248            for _ in 0..FRAMES {
249                let started = Instant::now();
250                let stats = renderer.render(&scene, &self.target, TARGET, SCALE);
251                cpu.push(started.elapsed());
252                self.wait();
253                total.push(started.elapsed());
254                draw_calls = stats.draw_calls;
255                paths = stats.paths;
256            }
257            let point =
258                Point2 { amount, cpu: median(&mut cpu), total: median(&mut total), draw_calls, paths };
259            curve.push(point);
260            if point.total > ENOUGH {
261                break;
262            }
263            amount *= 2;
264        }
265        curve
266    }
267}
268
269/// How much fits in `budget` seconds, between the sizes measured; `None` when all of it did.
270pub fn fits(curve: &[Point2], budget: f64) -> Option<usize> {
271    let after = curve.iter().position(|point| point.total > budget)?;
272    let Some(before) = after.checked_sub(1).map(|i| curve[i]) else { return Some(curve[0].amount) };
273    let next = curve[after];
274    let part = (budget - before.total) / (next.total - before.total);
275    Some(before.amount + (part * (next.amount - before.amount) as f64) as usize)
276}
277
278/// The size at which shapes started being left out, for a load made of shapes.
279pub fn drops_from(curve: &[Point2]) -> Option<usize> {
280    curve.iter().find(|point| point.paths > 0 && point.paths < point.amount).map(|point| point.paths)
281}