1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::time::{Duration, Instant, SystemTime};
5
6use egui::{Context, ViewportId};
7use egui_wgpu::{RendererOptions, ScreenDescriptor};
8use egui_winit::winit::application::ApplicationHandler;
9use egui_winit::winit::dpi::LogicalSize;
10use egui_winit::winit::event::WindowEvent;
11use egui_winit::winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy};
12use egui_winit::winit::window::{Window, WindowId};
13use pfx_gpu::{Gpu, WindowTarget, wgpu};
14
15use crate::paint::Painter;
16use crate::pgpu::{self, Pgpu};
17use crate::{App, Error, Theme, Watch, block_on, context};
18
19const SLOW: Duration = Duration::from_millis(250);
20const TALLY: u32 = 240;
21const SIZE: [f64; 2] = [1586.0, 992.0];
22
23pub trait Pulse {
24 fn frame(&self, work: Duration);
25 fn idle(&self);
26 fn label(&self, _label: &str) {}
27}
28
29#[derive(Clone, Debug, PartialEq, Eq)]
30enum Signal {
31 Files(u64, Vec<PathBuf>),
32 App,
33 Pgpu(Pgpu),
34}
35
36struct Live {
37 window: Arc<Window>,
38 gpu: Gpu,
39 target: WindowTarget,
40 state: egui_winit::State,
41 painter: Painter,
42}
43
44#[derive(Default)]
45struct Tally {
46 frames: u32,
47 work: Duration,
48 longest: Duration,
49 gpu_ms: f64,
50 gpu_frames: u32,
51 noted: u64,
52}
53
54impl Tally {
55 fn add(&mut self, name: &str, work: Duration, gpu_ms: Option<f64>, drops: u64) -> Vec<String> {
56 let mut lines = Vec::new();
57 if drops > self.noted {
58 lines.push(format!(
59 "{name}: {drops} frame timings dropped so far; the GPU ran more than two frames behind"
60 ));
61 self.noted = drops.saturating_mul(2);
62 }
63 self.frames += 1;
64 self.work += work;
65 self.longest = self.longest.max(work);
66 if let Some(ms) = gpu_ms {
67 self.gpu_ms += ms;
68 self.gpu_frames += 1;
69 }
70 if self.frames == TALLY {
71 let gpu = if self.gpu_frames > 0 {
72 format!("{:.2} ms", self.gpu_ms / f64::from(self.gpu_frames))
73 } else {
74 "unmeasured".to_string()
75 };
76 lines.push(format!(
77 "{name}: {} frames, the UI thread {:.2} ms a frame (longest {:.2} ms), the viewport on the GPU {gpu}",
78 self.frames,
79 self.work.as_secs_f64() * 1000.0 / f64::from(self.frames),
80 self.longest.as_secs_f64() * 1000.0,
81 ));
82 *self = Tally {
83 noted: self.noted,
84 ..Tally::default()
85 };
86 }
87 lines
88 }
89}
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
92struct Stamp {
93 modified: Option<SystemTime>,
94 len: u64,
95}
96
97fn stamp(path: &Path) -> Option<Stamp> {
98 let meta = std::fs::metadata(path).ok()?;
99 Some(Stamp {
100 modified: meta.modified().ok(),
101 len: meta.len(),
102 })
103}
104
105fn moved(files: &[PathBuf], last: &[Option<Stamp>], now: &[Option<Stamp>]) -> Vec<PathBuf> {
106 files
107 .iter()
108 .zip(last.iter().zip(now))
109 .filter(|(_, (then, now))| then != now)
110 .map(|(file, _)| file.clone())
111 .collect()
112}
113
114fn watch_files(
115 watch: Watch,
116 generation: u64,
117 stop: Arc<AtomicBool>,
118 proxy: EventLoopProxy<Signal>,
119) {
120 std::thread::spawn(move || {
121 let stamps = || {
122 watch
123 .files
124 .iter()
125 .map(|file| stamp(file))
126 .collect::<Vec<_>>()
127 };
128 let mut last = stamps();
129 while !stop.load(Ordering::Relaxed) {
130 std::thread::sleep(watch.every);
131 if stop.load(Ordering::Relaxed) {
132 return;
133 }
134 let now = stamps();
135 if now != last {
136 let changed = moved(&watch.files, &last, &now);
137 last = now;
138 if proxy
139 .send_event(Signal::Files(generation, changed))
140 .is_err()
141 {
142 return;
143 }
144 }
145 }
146 });
147}
148
149fn title(name: &str, label: Option<&str>) -> String {
150 match label {
151 Some(label) => format!("{name} · {label}"),
152 None => name.to_string(),
153 }
154}
155
156fn retitle(window: &Window, pulse: Option<&dyn Pulse>, app: &impl App) {
157 let label = app.label();
158 window.set_title(&title(app.name(), label.as_deref()));
159 if let (Some(pulse), Some(label)) = (pulse, label) {
160 pulse.label(&label);
161 }
162}
163
164struct Watcher {
165 stop: Option<Arc<AtomicBool>>,
166 watch: Watch,
167 generation: u64,
168}
169
170impl Watcher {
171 fn stop(&mut self) {
172 if let Some(stop) = self.stop.take() {
173 stop.store(true, Ordering::Relaxed);
174 }
175 }
176
177 fn follow(&mut self, watch: Watch, proxy: &EventLoopProxy<Signal>) {
178 if self.stop.is_some() && watch == self.watch {
179 return;
180 }
181 self.stop();
182 self.generation += 1;
183 self.watch = watch.clone();
184 if watch.files.is_empty() {
185 return;
186 }
187 let flag = Arc::new(AtomicBool::new(false));
188 self.stop = Some(Arc::clone(&flag));
189 watch_files(watch, self.generation, flag, proxy.clone());
190 }
191}
192
193struct Host<A: App> {
194 app: A,
195 ctx: Context,
196 live: Option<Live>,
197 failure: Option<String>,
198 pulse: Option<Box<dyn Pulse>>,
199 proxy: EventLoopProxy<Signal>,
200 watching: bool,
201 watcher: Watcher,
202 pgpu: Pgpu,
203 drew: bool,
204 started: Instant,
205 ui_shown: bool,
206 view_shown: bool,
207 tally: Tally,
208}
209
210impl<A: App> Host<A> {
211 fn fail(&mut self, event_loop: &ActiveEventLoop, failure: String) {
212 self.failure = Some(failure);
213 event_loop.exit();
214 }
215
216 fn open(&mut self, event_loop: &ActiveEventLoop) -> Result<Live, String> {
217 let attributes = Window::default_attributes()
218 .with_title(title(self.app.name(), self.app.label().as_deref()))
219 .with_inner_size(LogicalSize::new(SIZE[0], SIZE[1]));
220 let window = Arc::new(
221 event_loop
222 .create_window(attributes)
223 .map_err(|error| error.to_string())?,
224 );
225 retitle(&window, self.pulse.as_deref(), &self.app);
226 let (gpu, target) = block_on(Gpu::window(window.clone()))?;
227 let side = gpu.device.limits().max_texture_dimension_2d as usize;
228 let state = egui_winit::State::new(
229 self.ctx.clone(),
230 ViewportId::ROOT,
231 event_loop,
232 Some(window.scale_factor() as f32),
233 window.theme(),
234 Some(side),
235 );
236 let painter = Painter::new(&gpu, target.config.format, RendererOptions::default());
237 window.request_redraw();
238 if !self.watching {
239 self.watching = true;
240 let proxy = self.proxy.clone();
241 pgpu::watch(self.app.name().to_string(), move |state| {
242 proxy.send_event(Signal::Pgpu(state)).is_ok()
243 });
244 }
245 if self.watcher.stop.is_none() {
246 self.watcher.follow(self.app.watched(), &self.proxy);
247 }
248 Ok(Live {
249 window,
250 gpu,
251 target,
252 state,
253 painter,
254 })
255 }
256
257 fn redraw(&mut self) -> Result<(), String> {
258 let Host {
259 app,
260 ctx,
261 live,
262 pulse,
263 proxy,
264 watcher,
265 drew,
266 started: opened,
267 ui_shown,
268 view_shown,
269 tally,
270 ..
271 } = self;
272 let Some(live) = live.as_mut() else {
273 return Ok(());
274 };
275 let started = Instant::now();
276 let native = live.window.scale_factor() as f32;
277 ctx.set_zoom_factor(native.round().max(1.0) / native);
278 let input = live.state.take_egui_input(&live.window);
279 let mut output = ctx.run_ui(input, |root| app.ui(root));
280 if app.opened() {
281 watcher.follow(app.watched(), proxy);
282 retitle(&live.window, pulse.as_deref(), app);
283 }
284 let mut more = false;
285 if app.viewport() {
286 more = app.frame(&live.gpu, live.painter.renderer(), output.pixels_per_point);
287 }
288 live.state
289 .handle_platform_output(&live.window, output.platform_output);
290 let primitives = ctx.tessellate(output.shapes, output.pixels_per_point);
291 let frame = match live.target.surface.get_current_texture() {
292 wgpu::CurrentSurfaceTexture::Success(frame)
293 | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
294 wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
295 let size = live.window.inner_size();
296 live.target.resize(&live.gpu, size.width, size.height);
297 live.window.request_redraw();
298 return Ok(());
299 }
300 wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
301 live.window.request_redraw();
302 return Ok(());
303 }
304 wgpu::CurrentSurfaceTexture::Validation => {
305 return Err("surface validation failed".to_string());
306 }
307 };
308 let view = frame
309 .texture
310 .create_view(&wgpu::TextureViewDescriptor::default());
311 let screen = ScreenDescriptor {
312 size_in_pixels: [live.target.config.width, live.target.config.height],
313 pixels_per_point: output.pixels_per_point,
314 };
315 live.painter.paint(
316 &live.gpu,
317 &view,
318 &screen,
319 &output.textures_delta,
320 &primitives,
321 Theme::of(ctx).bg,
322 );
323 output.textures_delta.clear();
324 live.window.pre_present_notify();
325 live.gpu.queue.present(frame);
326 let work = started.elapsed();
327 let name = app.name().to_string();
328 if work >= SLOW {
329 eprintln!("{name}: a frame took {} ms", work.as_millis());
330 }
331 let timing = app.timing();
332 for line in tally.add(&name, work, timing.gpu_ms, timing.drops) {
333 eprintln!("{line}");
334 }
335 if let Some(pulse) = pulse.as_ref() {
336 pulse.frame(work);
337 }
338 *drew = true;
339 if !*ui_shown {
340 *ui_shown = true;
341 eprintln!(
342 "{name}: the window's first frame at {} ms",
343 opened.elapsed().as_millis()
344 );
345 }
346 if !*view_shown && timing.drawn {
347 *view_shown = true;
348 eprintln!(
349 "{name}: the viewport's first frame at {} ms",
350 opened.elapsed().as_millis()
351 );
352 }
353 let again = output
354 .viewport_output
355 .get(&ViewportId::ROOT)
356 .is_some_and(|viewport| viewport.repaint_delay.is_zero());
357 if again || more {
358 live.window.request_redraw();
359 }
360 Ok(())
361 }
362}
363
364impl<A: App> ApplicationHandler<Signal> for Host<A> {
365 fn user_event(&mut self, _: &ActiveEventLoop, signal: Signal) {
366 let mut redraw = false;
367 match signal {
368 Signal::Files(generation, files) => {
369 if generation == self.watcher.generation {
370 self.app.changed(&files);
371 self.watcher.follow(self.app.watched(), &self.proxy);
372 redraw = true;
373 }
374 }
375 Signal::App => redraw = true,
376 Signal::Pgpu(state) => {
377 if state != self.pgpu {
378 self.pgpu = state;
379 self.app.pgpu(state);
380 redraw = true;
381 }
382 if !self.drew
383 && let Some(pulse) = self.pulse.as_ref()
384 {
385 pulse.idle();
386 }
387 self.drew = false;
388 }
389 }
390 if redraw && let Some(live) = self.live.as_ref() {
391 live.window.request_redraw();
392 }
393 }
394
395 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
396 if self.live.is_some() {
397 return;
398 }
399 match self.open(event_loop) {
400 Ok(live) => self.live = Some(live),
401 Err(failure) => self.fail(event_loop, failure),
402 }
403 }
404
405 fn window_event(&mut self, event_loop: &ActiveEventLoop, _: WindowId, event: WindowEvent) {
406 let Some(live) = self.live.as_mut() else {
407 return;
408 };
409 match &event {
410 WindowEvent::CloseRequested => {
411 event_loop.exit();
412 return;
413 }
414 WindowEvent::RedrawRequested => {
415 if let Err(failure) = self.redraw() {
416 self.fail(event_loop, failure);
417 }
418 return;
419 }
420 WindowEvent::Resized(size) => {
421 live.target.resize(&live.gpu, size.width, size.height);
422 live.window.request_redraw();
423 }
424 _ => {}
425 }
426 let response = live.state.on_window_event(&live.window, &event);
427 if response.repaint {
428 live.window.request_redraw();
429 }
430 }
431
432 fn exiting(&mut self, _: &ActiveEventLoop) {
433 self.watcher.stop();
434 self.live = None;
435 }
436}
437
438pub fn window(mut app: impl App, pulse: Option<Box<dyn Pulse>>) -> Result<(), Error> {
439 pfx_gpu::refuse_window("the editor window").map_err(Error)?;
440 let event_loop = EventLoop::<Signal>::with_user_event()
441 .build()
442 .map_err(|error| Error(error.to_string()))?;
443 event_loop.set_control_flow(ControlFlow::Wait);
444 let ctx = context(&app.theme());
445 let proxy = event_loop.create_proxy();
446 let started = Instant::now();
447 let wake = proxy.clone();
448 app.waker(Arc::new(move || {
449 let _ = wake.send_event(Signal::App);
450 }));
451 let mut host = Host {
452 app,
453 ctx,
454 live: None,
455 failure: None,
456 pulse,
457 proxy,
458 watching: false,
459 watcher: Watcher {
460 stop: None,
461 watch: Watch::default(),
462 generation: 0,
463 },
464 pgpu: Pgpu::default(),
465 drew: false,
466 started,
467 ui_shown: false,
468 view_shown: false,
469 tally: Tally::default(),
470 };
471 event_loop
472 .run_app(&mut host)
473 .map_err(|error| Error(error.to_string()))?;
474 match host.failure {
475 Some(failure) => Err(Error(failure)),
476 None => Ok(()),
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483
484 struct Quiet;
485
486 impl Pulse for Quiet {
487 fn frame(&self, _: Duration) {}
488 fn idle(&self) {}
489 }
490
491 #[test]
492 fn the_title_names_the_label_and_a_pulse_may_ignore_it() {
493 assert_eq!(title("editor", None), "editor");
494 assert_eq!(title("editor", Some("desk.toml")), "editor · desk.toml");
495 Quiet.label("desk.toml");
496 }
497
498 #[test]
499 fn dropped_timings_are_noted_as_they_climb_and_not_every_frame() {
500 let mut tally = Tally::default();
501 let ms = Duration::from_millis(2);
502 assert!(tally.add("editor", ms, Some(4.0), 0).is_empty());
503 let noted: Vec<usize> = [1, 1, 2, 3, 3, 9, 10, 30]
504 .into_iter()
505 .map(|drops| tally.add("editor", ms, None, drops).len())
506 .collect();
507 assert_eq!(noted, [1, 0, 0, 1, 0, 1, 0, 1]);
508 assert!(
509 tally.add("editor", ms, None, 61)[0]
510 .starts_with("editor: 61 frame timings dropped so far")
511 );
512 }
513
514 #[test]
515 fn every_240_frames_log_the_ui_threads_time_and_the_gpus() {
516 let mut tally = Tally::default();
517 let mut lines = Vec::new();
518 for frame in 0..TALLY * 2 {
519 let work = Duration::from_micros(if frame == 7 { 9_000 } else { 1_000 });
520 lines.extend(tally.add("editor", work, (frame % 2 == 0).then_some(5.0), 0));
521 }
522 assert_eq!(
523 lines,
524 [
525 "editor: 240 frames, the UI thread 1.03 ms a frame (longest 9.00 ms), the viewport on the GPU 5.00 ms",
526 "editor: 240 frames, the UI thread 1.00 ms a frame (longest 1.00 ms), the viewport on the GPU 5.00 ms",
527 ]
528 );
529 }
530
531 #[test]
532 fn only_the_files_whose_stamps_moved_are_named() {
533 let files = [
534 PathBuf::from("a.toml"),
535 PathBuf::from("b.toml"),
536 PathBuf::from("c.toml"),
537 ];
538 let at = |len| {
539 Some(Stamp {
540 modified: None,
541 len,
542 })
543 };
544 let last = [at(1), None, at(3)];
545 let now = [at(1), at(2), at(4)];
546 assert_eq!(moved(&files, &last, &now), files[1..].to_vec());
547 assert!(moved(&files, &last, &last).is_empty());
548 }
549
550 #[test]
551 fn a_stamp_reads_a_file_and_none_for_a_missing_one() {
552 let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tmp/shell-tests/stamp");
553 std::fs::create_dir_all(&dir).unwrap();
554 let file = dir.join("watched.toml");
555 std::fs::write(&file, "a = 1\n").unwrap();
556 assert_eq!(stamp(&file).map(|stamp| stamp.len), Some(6));
557 assert_eq!(stamp(&dir.join("missing.toml")), None);
558 }
559}