1use 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
16pub const BUDGET_120: f64 = 1.0 / 120.0;
18pub const BUDGET_60: f64 = 1.0 / 60.0;
20const WARM: usize = 8;
22const FRAMES: usize = 24;
24const ENOUGH: f64 = 0.05;
26const MOST: usize = 4_000_000;
28const DISTINCT_IMAGES: usize = 512;
30
31#[derive(Clone, Copy, Debug)]
33pub struct Load {
34 pub name: &'static str,
36 pub unit: &'static str,
38 pub start: usize,
40 pub make: fn(usize, &mut Assets) -> Scene,
42}
43
44#[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
65fn 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
80fn 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
138fn 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
166fn 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
189fn 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
200pub 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#[derive(Clone, Copy, Debug)]
215pub struct Point2 {
216 pub amount: usize,
218 pub cpu: f64,
220 pub total: f64,
222 pub draw_calls: usize,
224 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 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
269pub 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
278pub fn drops_from(curve: &[Point2]) -> Option<usize> {
280 curve.iter().find(|point| point.paths > 0 && point.paths < point.amount).map(|point| point.paths)
281}