1use egui::{
2 Align, Area, Color32, Context, Frame, FullOutput, Id, Layout, Margin, Order, Pos2, RawInput,
3 Rect, Sense, Stroke, TexturesDelta, ViewportId, ViewportInfo, epaint::ClippedPrimitive, vec2,
4};
5use egui_wgpu::{RendererOptions, ScreenDescriptor};
6use pfx_gpu::Gpu;
7use pfx_input::device::Key;
8use pfx_input::event::InputEvent;
9use pfx_theme::style::Theme;
10use pfx_theme::style::theme::apply_theme;
11
12use crate::renderer::Renderer;
13use crate::stats::{FrameKind, FrameStats, Sample};
14
15pub const TOGGLE: Key = Key::F3;
16pub const BUDGET_MS: f64 = 1000.0 / 60.0;
17pub const WINDOW_MS: f64 = 4000.0;
18pub const KEPT: usize = 512;
19pub const WIDTH: f32 = 216.0;
20pub const MARGIN: f32 = 12.0;
21pub const GRAPH_HEIGHT: f32 = 48.0;
22const CEILING_MS: f64 = 2.0 * BUDGET_MS;
23const CANVAS: f32 = 512.0;
24const CANVAS_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
25
26#[derive(Clone, Debug, Default, PartialEq)]
27pub struct Readout {
28 pub sim_ms: Option<f64>,
29 pub submit_ms: Option<f64>,
30 pub gpu: Option<(u64, f64)>,
31 pub kind: Option<FrameKind>,
32 pub hud_ms: Option<f64>,
33 pub frames: Vec<(f64, f64)>,
34 pub over: usize,
35}
36
37impl Readout {
38 pub fn from_samples(samples: &[Sample]) -> Self {
39 let Some(latest) = samples.last() else {
40 return Self::default();
41 };
42 let start = latest.t_ms - WINDOW_MS;
43 let frames: Vec<(f64, f64)> = samples
44 .iter()
45 .filter(|sample| sample.t_ms >= start)
46 .filter_map(|sample| {
47 sample
48 .interval_ms
49 .map(|interval| (sample.t_ms - start, interval))
50 })
51 .collect();
52 Self {
53 sim_ms: latest.sim_ms,
54 submit_ms: Some(latest.submit_ms),
55 gpu: samples
56 .iter()
57 .rev()
58 .find_map(|sample| sample.gpu_ms.map(|ms| (sample.tick, ms))),
59 kind: Some(latest.kind),
60 hud_ms: samples.iter().rev().find_map(|sample| sample.hud_ms),
61 over: frames
62 .iter()
63 .filter(|(_, interval)| *interval > BUDGET_MS)
64 .count(),
65 frames,
66 }
67 }
68}
69
70fn millis(value: Option<f64>) -> String {
71 value.map_or_else(|| "-".into(), |ms| format!("{ms:.2} ms"))
72}
73
74fn kind_colour(theme: &Theme, kind: FrameKind) -> Color32 {
75 match kind {
76 FrameKind::Full => theme.text,
77 FrameKind::Partial => theme.secondary,
78 FrameKind::Reproject => theme.secondary_soft,
79 FrameKind::Idle => theme.muted,
80 }
81}
82
83fn row(ui: &mut egui::Ui, theme: &Theme, name: &str, value: String, colour: Color32) {
84 ui.horizontal(|ui| {
85 ui.label(pfx_theme::label(theme, name, theme.text2));
86 ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
87 ui.label(
88 egui::RichText::new(value)
89 .font(theme.font(theme.size_body))
90 .color(colour),
91 );
92 });
93 });
94}
95
96fn graph(ui: &mut egui::Ui, theme: &Theme, readout: &Readout) {
97 let (rect, _) =
98 ui.allocate_exact_size(vec2(ui.available_width(), GRAPH_HEIGHT), Sense::hover());
99 let painter = ui.painter_at(rect);
100 painter.rect_filled(rect, 0.0, theme.well);
101 let y = |ms: f64| rect.bottom() - (ms.min(CEILING_MS) / CEILING_MS) as f32 * rect.height();
102 let x = |ms: f64| rect.left() + (ms / WINDOW_MS).clamp(0.0, 1.0) as f32 * rect.width();
103 for &(end, interval) in &readout.frames {
104 let colour = if interval > CEILING_MS {
105 theme.error
106 } else if interval > BUDGET_MS {
107 theme.warning
108 } else {
109 theme.secondary
110 };
111 let left = x(end - interval);
112 let right = x(end).max(left + 1.0);
113 painter.rect_filled(
114 Rect::from_min_max(
115 Pos2::new(left, y(interval)),
116 Pos2::new(right, rect.bottom()),
117 ),
118 0.0,
119 colour,
120 );
121 }
122 let budget = y(BUDGET_MS).round() + 0.5;
123 painter.hline(rect.x_range(), budget, Stroke::new(theme.stroke, theme.hot));
124}
125
126pub fn ui(ctx: &Context, readout: &Readout) -> Rect {
127 let theme = Theme::of(ctx);
128 let theme = theme.as_ref();
129 Area::new(Id::new("pfx dev hud"))
130 .fixed_pos(Pos2::ZERO)
131 .order(Order::Foreground)
132 .show(ctx, |ui| {
133 Frame::new()
134 .fill(theme.bg)
135 .stroke(Stroke::new(theme.stroke, theme.line))
136 .inner_margin(Margin::same(theme.panel_padding))
137 .corner_radius(theme.radius)
138 .show(ui, |ui| {
139 ui.set_width(WIDTH);
140 ui.label(pfx_theme::title(
141 theme,
142 "frame",
143 theme.size_body,
144 theme.text,
145 ));
146 row(ui, theme, "sim", millis(readout.sim_ms), theme.text);
147 row(ui, theme, "submit", millis(readout.submit_ms), theme.text);
148 row(
149 ui,
150 theme,
151 "gpu",
152 millis(readout.gpu.map(|(_, ms)| ms)),
153 theme.text,
154 );
155 let (kind, colour) = match readout.kind {
156 Some(kind) => (kind.name().to_uppercase(), kind_colour(theme, kind)),
157 None => ("-".into(), theme.muted),
158 };
159 row(ui, theme, "kind", kind, colour);
160 row(ui, theme, "hud", millis(readout.hud_ms), theme.text2);
161 graph(ui, theme, readout);
162 let over = if readout.over > 0 {
163 theme.warning
164 } else {
165 theme.muted
166 };
167 row(ui, theme, "over 16.67", readout.over.to_string(), over);
168 });
169 })
170 .response
171 .rect
172}
173
174pub fn layout(
175 ctx: &Context,
176 pixels_per_point: f32,
177 readout: &Readout,
178 passes: usize,
179) -> (Rect, FullOutput) {
180 let mut rect = Rect::NOTHING;
181 let mut textures = TexturesDelta::default();
182 let mut full = FullOutput::default();
183 for _ in 0..passes.max(1) {
184 full = ctx.run_ui(input(pixels_per_point), |root| {
185 rect = ui(root.ctx(), readout)
186 });
187 textures.append(std::mem::take(&mut full.textures_delta));
188 }
189 full.textures_delta = textures;
190 (rect, full)
191}
192
193pub fn context() -> Context {
194 let ctx = Context::default();
195 apply_theme(&ctx, &pfx_theme::pfx());
196 ctx
197}
198
199pub fn input(pixels_per_point: f32) -> RawInput {
200 let mut input = RawInput {
201 screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(CANVAS, CANVAS))),
202 max_texture_side: Some(2048),
203 ..RawInput::default()
204 };
205 input.viewports.insert(
206 ViewportId::ROOT,
207 ViewportInfo {
208 native_pixels_per_point: Some(pixels_per_point),
209 ..ViewportInfo::default()
210 },
211 );
212 input
213}
214
215pub fn default_scale(height: u32) -> f32 {
216 (height as f32 / 1080.0).max(1.0)
217}
218
219pub fn placement(size: [u32; 2], panel: [u32; 2], pixels_per_point: f32) -> Option<[u32; 4]> {
220 let margin = (MARGIN * pixels_per_point).round() as u32;
221 let width = panel[0].min(size[0].saturating_sub(2 * margin));
222 let height = panel[1].min(size[1].saturating_sub(2 * margin));
223 if width == 0 || height == 0 {
224 return None;
225 }
226 Some([size[0] - margin - width, margin, width, height])
227}
228
229fn encodes(format: wgpu::TextureFormat) -> bool {
230 !format.is_srgb()
231 && !matches!(
232 format,
233 wgpu::TextureFormat::Rgba16Float
234 | wgpu::TextureFormat::Rgba32Float
235 | wgpu::TextureFormat::Rg11b10Ufloat
236 )
237}
238
239const COMPOSITE: &str = r#"
240struct Place {
241 origin: vec2<i32>,
242 encode: u32,
243 pad: u32,
244}
245
246@group(0) @binding(0) var canvas: texture_2d<f32>;
247@group(0) @binding(1) var<uniform> place: Place;
248
249@vertex
250fn vs(@builtin(vertex_index) index: u32) -> @builtin(position) vec4<f32> {
251 let corner = vec2<f32>(f32((index << 1u) & 2u), f32(index & 2u));
252 return vec4<f32>(corner * 2.0 - 1.0, 0.0, 1.0);
253}
254
255fn gamma(linear: vec3<f32>) -> vec3<f32> {
256 let low = linear * 12.92;
257 let high = 1.055 * pow(linear, vec3<f32>(1.0 / 2.4)) - 0.055;
258 return select(high, low, linear <= vec3<f32>(0.0031308));
259}
260
261@fragment
262fn fs(@builtin(position) at: vec4<f32>) -> @location(0) vec4<f32> {
263 let colour = textureLoad(canvas, vec2<i32>(at.xy) - place.origin, 0);
264 if place.encode == 1u && colour.a > 0.0 {
265 return vec4<f32>(gamma(colour.rgb / colour.a) * colour.a, colour.a);
266 }
267 return colour;
268}
269"#;
270
271struct Canvas {
272 size: [u32; 2],
273 view: wgpu::TextureView,
274 bind: wgpu::BindGroup,
275}
276
277struct Paint {
278 format: wgpu::TextureFormat,
279 egui: egui_wgpu::Renderer,
280 pipeline: wgpu::RenderPipeline,
281 layout: wgpu::BindGroupLayout,
282 place: wgpu::Buffer,
283 canvas: Option<Canvas>,
284}
285
286impl Paint {
287 fn new(gpu: &Gpu, format: wgpu::TextureFormat) -> Self {
288 let device = &gpu.device;
289 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
290 label: Some("pfx dev hud composite"),
291 source: wgpu::ShaderSource::Wgsl(COMPOSITE.into()),
292 });
293 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
294 label: Some("pfx dev hud composite"),
295 entries: &[
296 wgpu::BindGroupLayoutEntry {
297 binding: 0,
298 visibility: wgpu::ShaderStages::FRAGMENT,
299 ty: wgpu::BindingType::Texture {
300 sample_type: wgpu::TextureSampleType::Float { filterable: false },
301 view_dimension: wgpu::TextureViewDimension::D2,
302 multisampled: false,
303 },
304 count: None,
305 },
306 wgpu::BindGroupLayoutEntry {
307 binding: 1,
308 visibility: wgpu::ShaderStages::FRAGMENT,
309 ty: wgpu::BindingType::Buffer {
310 ty: wgpu::BufferBindingType::Uniform,
311 has_dynamic_offset: false,
312 min_binding_size: None,
313 },
314 count: None,
315 },
316 ],
317 });
318 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
319 label: Some("pfx dev hud composite"),
320 bind_group_layouts: &[Some(&layout)],
321 immediate_size: 0,
322 });
323 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
324 label: Some("pfx dev hud composite"),
325 layout: Some(&pipeline_layout),
326 vertex: wgpu::VertexState {
327 module: &module,
328 entry_point: Some("vs"),
329 compilation_options: Default::default(),
330 buffers: &[],
331 },
332 fragment: Some(wgpu::FragmentState {
333 module: &module,
334 entry_point: Some("fs"),
335 compilation_options: Default::default(),
336 targets: &[Some(wgpu::ColorTargetState {
337 format,
338 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
339 write_mask: wgpu::ColorWrites::ALL,
340 })],
341 }),
342 primitive: wgpu::PrimitiveState::default(),
343 depth_stencil: None,
344 multisample: wgpu::MultisampleState::default(),
345 multiview_mask: None,
346 cache: None,
347 });
348 let place = device.create_buffer(&wgpu::BufferDescriptor {
349 label: Some("pfx dev hud place"),
350 size: 16,
351 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
352 mapped_at_creation: false,
353 });
354 Self {
355 format,
356 egui: egui_wgpu::Renderer::new(device, CANVAS_FORMAT, RendererOptions::PREDICTABLE),
357 pipeline,
358 layout,
359 place,
360 canvas: None,
361 }
362 }
363
364 fn canvas(&mut self, gpu: &Gpu, size: [u32; 2]) -> &Canvas {
365 if self
366 .canvas
367 .as_ref()
368 .is_none_or(|canvas| canvas.size != size)
369 {
370 let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
371 label: Some("pfx dev hud canvas"),
372 size: wgpu::Extent3d {
373 width: size[0],
374 height: size[1],
375 depth_or_array_layers: 1,
376 },
377 mip_level_count: 1,
378 sample_count: 1,
379 dimension: wgpu::TextureDimension::D2,
380 format: CANVAS_FORMAT,
381 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
382 | wgpu::TextureUsages::TEXTURE_BINDING,
383 view_formats: &[],
384 });
385 let view = texture.create_view(&Default::default());
386 let bind = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
387 label: Some("pfx dev hud composite"),
388 layout: &self.layout,
389 entries: &[
390 wgpu::BindGroupEntry {
391 binding: 0,
392 resource: wgpu::BindingResource::TextureView(&view),
393 },
394 wgpu::BindGroupEntry {
395 binding: 1,
396 resource: self.place.as_entire_binding(),
397 },
398 ],
399 });
400 self.canvas = Some(Canvas { size, view, bind });
401 }
402 self.canvas.as_ref().unwrap()
403 }
404
405 fn upload(&mut self, gpu: &Gpu, textures: &TexturesDelta) {
406 for (id, deltas) in &textures.set {
407 for delta in deltas {
408 self.egui
409 .update_texture(&gpu.device, &gpu.queue, *id, delta);
410 }
411 }
412 }
413
414 #[allow(clippy::too_many_arguments)]
415 fn draw(
416 &mut self,
417 gpu: &Gpu,
418 output: &wgpu::TextureView,
419 textures: &TexturesDelta,
420 primitives: &[ClippedPrimitive],
421 pixels_per_point: f32,
422 panel: [u32; 2],
423 at: [u32; 4],
424 ) {
425 self.upload(gpu, textures);
426 let mut place = [0u8; 16];
427 place[0..4].copy_from_slice(&(at[0] as i32).to_le_bytes());
428 place[4..8].copy_from_slice(&(at[1] as i32).to_le_bytes());
429 place[8..12].copy_from_slice(&u32::from(encodes(self.format)).to_le_bytes());
430 gpu.queue.write_buffer(&self.place, 0, &place);
431 let screen = ScreenDescriptor {
432 size_in_pixels: panel,
433 pixels_per_point,
434 };
435 let mut encoder = gpu
436 .device
437 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
438 label: Some("pfx dev hud"),
439 });
440 let mut commands =
441 self.egui
442 .update_buffers(&gpu.device, &gpu.queue, &mut encoder, primitives, &screen);
443 let canvas = self.canvas(gpu, panel);
444 let (view, bind) = (canvas.view.clone(), canvas.bind.clone());
445 {
446 let pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
447 multiview_mask: None,
448 label: Some("pfx dev hud ui"),
449 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
450 view: &view,
451 depth_slice: None,
452 resolve_target: None,
453 ops: wgpu::Operations {
454 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
455 store: wgpu::StoreOp::Store,
456 },
457 })],
458 depth_stencil_attachment: None,
459 timestamp_writes: None,
460 occlusion_query_set: None,
461 });
462 let mut pass = pass.forget_lifetime();
463 self.egui.render(&mut pass, primitives, &screen);
464 }
465 {
466 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
467 multiview_mask: None,
468 label: Some("pfx dev hud composite"),
469 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
470 view: output,
471 depth_slice: None,
472 resolve_target: None,
473 ops: wgpu::Operations {
474 load: wgpu::LoadOp::Load,
475 store: wgpu::StoreOp::Store,
476 },
477 })],
478 depth_stencil_attachment: None,
479 timestamp_writes: None,
480 occlusion_query_set: None,
481 });
482 pass.set_pipeline(&self.pipeline);
483 pass.set_bind_group(0, &bind, &[]);
484 pass.set_viewport(
485 at[0] as f32,
486 at[1] as f32,
487 at[2] as f32,
488 at[3] as f32,
489 0.0,
490 1.0,
491 );
492 pass.set_scissor_rect(at[0], at[1], at[2], at[3]);
493 pass.draw(0..3, 0..1);
494 }
495 commands.push(encoder.finish());
496 gpu.queue.submit(commands);
497 for id in &textures.free {
498 self.egui.free_texture(id);
499 }
500 }
501}
502
503#[derive(Default)]
504pub struct Hud {
505 visible: bool,
506 held: bool,
507 ctx: Option<Context>,
508 paint: Option<Paint>,
509 rect: Option<[u32; 4]>,
510}
511
512impl Hud {
513 pub fn new() -> Self {
514 Self::default()
515 }
516
517 pub fn visible(&self) -> bool {
518 self.visible
519 }
520
521 pub fn set_visible(&mut self, visible: bool) {
522 self.visible = visible;
523 }
524
525 pub fn toggle(&mut self) {
526 self.visible = !self.visible;
527 }
528
529 pub fn rect(&self) -> Option<[u32; 4]> {
530 self.rect
531 }
532
533 pub fn input(&mut self, event: &InputEvent) -> bool {
534 match event {
535 InputEvent::Key { key, pressed } if *key == TOGGLE => {
536 if *pressed && !self.held {
537 self.toggle();
538 }
539 self.held = *pressed;
540 true
541 }
542 InputEvent::FocusLost => {
543 self.held = false;
544 false
545 }
546 _ => false,
547 }
548 }
549
550 pub fn attach(renderer: &mut Renderer) -> FrameStats {
551 let mut stats = renderer.frame_stats();
552 if !stats.enabled() {
553 stats = FrameStats::measure();
554 renderer.set_frame_stats(stats.clone());
555 }
556 stats.keep_recent(KEPT);
557 stats
558 }
559
560 pub fn draw(&mut self, renderer: &mut Renderer, output: &wgpu::TextureView) {
561 let format = renderer.output_format();
562 let (width, height) = renderer.size();
563 self.draw_on(renderer, output, format, [width, height]);
564 }
565
566 pub fn draw_on(
567 &mut self,
568 renderer: &mut Renderer,
569 output: &wgpu::TextureView,
570 format: wgpu::TextureFormat,
571 size: [u32; 2],
572 ) {
573 let stats = Self::attach(renderer);
574 if !self.visible {
575 self.rect = None;
576 return;
577 }
578 stats.hud_begin();
579 self.paint(renderer, output, format, size, &stats);
580 stats.hud_end();
581 }
582
583 fn paint(
584 &mut self,
585 renderer: &Renderer,
586 output: &wgpu::TextureView,
587 format: wgpu::TextureFormat,
588 size: [u32; 2],
589 stats: &FrameStats,
590 ) {
591 let [width, height] = size;
592 let scale = default_scale(height);
593 let readout = stats.with_recent(Readout::from_samples);
594 let passes = if self.ctx.is_none() { 2 } else { 1 };
595 let ctx = self.ctx.get_or_insert_with(context);
596 let (rect, mut full) = layout(ctx, scale, &readout, passes);
597 let primitives = ctx.tessellate(full.shapes, full.pixels_per_point);
598 let panel = [
599 ((rect.max.x * scale).ceil() as u32).clamp(1, (CANVAS * scale) as u32),
600 ((rect.max.y * scale).ceil() as u32).clamp(1, (CANVAS * scale) as u32),
601 ];
602 let gpu = renderer.gpu();
603 if self
604 .paint
605 .as_ref()
606 .is_none_or(|paint| paint.format != format)
607 {
608 self.paint = Some(Paint::new(gpu, format));
609 }
610 let Some(paint) = self.paint.as_mut() else {
611 full.textures_delta.clear();
612 return;
613 };
614 let Some(at) = placement([width, height], panel, scale) else {
615 paint.upload(gpu, &full.textures_delta);
616 full.textures_delta.clear();
617 self.rect = None;
618 return;
619 };
620 paint.draw(
621 gpu,
622 output,
623 &full.textures_delta,
624 &primitives,
625 scale,
626 panel,
627 at,
628 );
629 full.textures_delta.clear();
630 self.rect = Some(at);
631 }
632}
633
634#[cfg(test)]
635mod tests {
636 use super::*;
637
638 fn sample(tick: u64, t_ms: f64, interval_ms: Option<f64>) -> Sample {
639 Sample {
640 tick,
641 t_ms,
642 sim_ms: Some(0.5),
643 submit_ms: 2.0,
644 gpu_ms: None,
645 interval_ms,
646 kind: FrameKind::Full,
647 size: [320, 180],
648 hud_ms: None,
649 }
650 }
651
652 #[test]
653 fn f3_toggles_once_per_press_and_other_input_passes_through() {
654 let mut hud = Hud::new();
655 assert!(!hud.visible());
656 let press = InputEvent::Key {
657 key: Key::F3,
658 pressed: true,
659 };
660 let release = InputEvent::Key {
661 key: Key::F3,
662 pressed: false,
663 };
664 assert!(hud.input(&press));
665 assert!(hud.visible());
666 assert!(hud.input(&press));
667 assert!(hud.visible());
668 assert!(hud.input(&release));
669 assert!(hud.input(&press));
670 assert!(!hud.visible());
671 assert!(!hud.input(&InputEvent::FocusLost));
672 assert!(hud.input(&press));
673 assert!(hud.visible());
674 let other = InputEvent::Key {
675 key: Key::F4,
676 pressed: true,
677 };
678 assert!(!hud.input(&other));
679 assert!(hud.visible());
680 }
681
682 #[test]
683 fn the_readout_takes_the_newest_numbers_and_the_last_four_seconds() {
684 assert_eq!(Readout::from_samples(&[]), Readout::default());
685 let mut samples: Vec<Sample> = (0..300)
686 .map(|tick| {
687 let interval = if tick % 100 == 0 { 40.0 } else { 16.0 };
688 sample(tick, tick as f64 * 16.0, (tick > 0).then_some(interval))
689 })
690 .collect();
691 samples[297].gpu_ms = Some(3.25);
692 samples[296].hud_ms = Some(0.75);
693 samples[299].kind = FrameKind::Partial;
694 samples[299].sim_ms = None;
695 let readout = Readout::from_samples(&samples);
696 assert_eq!(readout.gpu, Some((297, 3.25)));
697 assert_eq!(readout.hud_ms, Some(0.75));
698 assert_eq!(readout.kind, Some(FrameKind::Partial));
699 assert_eq!(readout.sim_ms, None);
700 assert_eq!(readout.submit_ms, Some(2.0));
701 assert_eq!(readout.frames.len(), 251);
702 assert!(
703 readout
704 .frames
705 .iter()
706 .all(|(end, _)| (0.0..=WINDOW_MS).contains(end))
707 );
708 assert_eq!(readout.over, 2);
709 }
710
711 #[test]
712 fn the_panel_lays_out_the_same_in_pfx_theme_and_sits_top_right() {
713 let readout = Readout::from_samples(&[sample(0, 0.0, None), sample(1, 16.0, Some(16.0))]);
714 let ctx = context();
715 let mut rects = Vec::new();
716 let mut texts = Vec::new();
717 for passes in [2, 1] {
718 let (rect, mut full) = layout(&ctx, 1.0, &readout, passes);
719 full.textures_delta.clear();
720 rects.push(rect);
721 texts = full
722 .shapes
723 .iter()
724 .filter_map(|clipped| match &clipped.shape {
725 egui::Shape::Text(text) => Some(text.galley.text().to_string()),
726 _ => None,
727 })
728 .collect();
729 }
730 assert_eq!(rects[0], rects[1]);
731 assert_eq!(rects[0].min, Pos2::ZERO);
732 assert!(rects[0].width() > WIDTH && rects[0].width() < WIDTH + 40.0);
733 for want in [
734 "FRAME", "SIM", "SUBMIT", "GPU", "KIND", "HUD", "FULL", "2.00 ms", "0.50 ms",
735 ] {
736 assert!(texts.iter().any(|text| text == want), "{want} in {texts:?}");
737 }
738 assert_eq!(Theme::of(&ctx).bg, pfx_theme::BG);
739 assert_eq!(
740 placement([1920, 1080], [240, 200], 1.0),
741 Some([1920 - 12 - 240, 12, 240, 200])
742 );
743 assert_eq!(
744 placement([3840, 2160], [480, 400], 2.0),
745 Some([3840 - 24 - 480, 24, 480, 400])
746 );
747 assert_eq!(
748 placement([100, 50], [240, 200], 1.0),
749 Some([12, 12, 76, 26])
750 );
751 assert_eq!(placement([20, 20], [240, 200], 1.0), None);
752 assert_eq!(default_scale(800), 1.0);
753 assert_eq!(default_scale(2160), 2.0);
754 assert!(encodes(wgpu::TextureFormat::Rgba8Unorm));
755 assert!(!encodes(wgpu::TextureFormat::Rgba8UnormSrgb));
756 assert!(!encodes(wgpu::TextureFormat::Rgba16Float));
757 }
758}