Skip to main content

pfx_live/
hud.rs

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}