Skip to main content

pfx_editor/
view.rs

1use std::path::{Path, PathBuf};
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use pfx_game::{Config, Driver, Event, Exit, Painter, Warmup};
5use pfx_gpu::pace::Pacer;
6use pfx_gpu::screens::{SystemSource, detect};
7use pfx_gpu::wgpu;
8use pfx_gpu::window::Size;
9use pfx_gpu::{Gpu, OffscreenTarget};
10use pfx_input::{KeyLabels, LayoutSource, Recording};
11use pfx_live::renderer::Renderer;
12use pfx_load::scene::{Camera, Scene};
13use pfx_play::{Game, Options, PlayHooks, PlaySession, Rigs, SceneGame, Tunables};
14use pfx_trace::Trace;
15
16use crate::audio::Audio;
17use crate::editor::{Editor, Play};
18use crate::input::Layout;
19use crate::inspect::Change;
20
21pub const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
22pub const SHOWN: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
23pub const TRACE_SAMPLES: u32 = 256;
24
25pub fn stamp_of(seconds: u64) -> String {
26    let days = (seconds / 86_400) as i64;
27    let rest = seconds % 86_400;
28    let shifted = days + 719_468;
29    let era = shifted.div_euclid(146_097);
30    let day_of_era = shifted.rem_euclid(146_097);
31    let year_of_era =
32        (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
33    let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
34    let month_part = (5 * day_of_year + 2) / 153;
35    let day = day_of_year - (153 * month_part + 2) / 5 + 1;
36    let month = if month_part < 10 {
37        month_part + 3
38    } else {
39        month_part - 9
40    };
41    let year = year_of_era + era * 400 + i64::from(month <= 2);
42    format!(
43        "{year:04}{month:02}{day:02}-{:02}{:02}{:02}",
44        rest / 3_600,
45        rest % 3_600 / 60,
46        rest % 60
47    )
48}
49
50fn stamp() -> String {
51    let seconds = SystemTime::now()
52        .duration_since(UNIX_EPOCH)
53        .map(|elapsed| elapsed.as_secs())
54        .unwrap_or_default();
55    stamp_of(seconds)
56}
57
58pub fn write_recording(
59    folder: &Path,
60    scene: &str,
61    stamp: &str,
62    recording: &Recording,
63) -> Result<PathBuf, String> {
64    let text = serde_json::to_string(recording).map_err(|error| error.to_string())?;
65    std::fs::create_dir_all(folder).map_err(|error| format!("{}: {error}", folder.display()))?;
66    let mut path = folder.join(format!("{scene}-{stamp}.json"));
67    let mut again = 1;
68    while path.exists() {
69        again += 1;
70        path = folder.join(format!("{scene}-{stamp}-{again}.json"));
71    }
72    std::fs::write(&path, text).map_err(|error| format!("{}: {error}", path.display()))?;
73    Ok(path)
74}
75
76pub struct Surface {
77    pub image: OffscreenTarget,
78    pub shown: wgpu::TextureView,
79}
80
81pub fn surface(gpu: &Gpu, size: [u32; 2]) -> Surface {
82    let [width, height] = [size[0].max(1), size[1].max(1)];
83    let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
84        label: Some("pfx edit play and trace"),
85        size: wgpu::Extent3d {
86            width,
87            height,
88            depth_or_array_layers: 1,
89        },
90        mip_level_count: 1,
91        sample_count: 1,
92        dimension: wgpu::TextureDimension::D2,
93        format: FORMAT,
94        usage: wgpu::TextureUsages::RENDER_ATTACHMENT
95            | wgpu::TextureUsages::TEXTURE_BINDING
96            | wgpu::TextureUsages::COPY_SRC
97            | wgpu::TextureUsages::COPY_DST,
98        view_formats: &[SHOWN],
99    });
100    let view = texture.create_view(&Default::default());
101    let shown = texture.create_view(&wgpu::TextureViewDescriptor {
102        format: Some(SHOWN),
103        ..Default::default()
104    });
105    Surface {
106        image: OffscreenTarget {
107            texture,
108            view,
109            format: FORMAT,
110            width,
111            height,
112        },
113        shown,
114    }
115}
116
117pub fn display(linear: f32) -> u8 {
118    let mapped = 1.0 - (-linear.max(0.0)).exp();
119    let encoded = pfx_materials::encode_channel(mapped);
120    (encoded.clamp(0.0, 1.0) * 255.0).round() as u8
121}
122
123pub fn tonemap(color: &[u8]) -> Vec<u8> {
124    color
125        .chunks_exact(16)
126        .flat_map(|pixel| {
127            let channel =
128                |at: usize| f32::from_le_bytes(pixel[at..at + 4].try_into().unwrap_or([0; 4]));
129            [
130                display(channel(0)),
131                display(channel(4)),
132                display(channel(8)),
133                255,
134            ]
135        })
136        .collect()
137}
138
139struct Preview {
140    trace: Trace,
141    pacer: Pacer,
142    samples: u32,
143    of: (Camera, u64),
144}
145
146struct Playing {
147    driver: Driver,
148    painter: Painter,
149    warmup: Option<Warmup>,
150    clock_ns: u64,
151    drawn: bool,
152}
153
154pub struct Stage<'a> {
155    pub config: &'a Config,
156    pub flat: bool,
157    pub layout: Option<Layout>,
158    pub labels: KeyLabels,
159}
160
161pub struct View {
162    pub gpu: Gpu,
163    pub target: Surface,
164    texture: Option<egui::TextureId>,
165    play: Option<Playing>,
166    hooks: Option<Box<dyn PlayHooks>>,
167    trace: Option<Preview>,
168    stepped: bool,
169    audio: Audio,
170}
171
172impl View {
173    pub fn open(gpu: Gpu, size: [u32; 2], audio: Audio) -> View {
174        View {
175            target: surface(&gpu, size),
176            gpu,
177            texture: None,
178            play: None,
179            hooks: None,
180            trace: None,
181            stepped: false,
182            audio,
183        }
184    }
185
186    pub fn size(&self) -> [u32; 2] {
187        [self.target.image.width, self.target.image.height]
188    }
189
190    pub fn aspect(&self) -> f32 {
191        self.target.image.width as f32 / self.target.image.height.max(1) as f32
192    }
193
194    pub fn playing(&self) -> bool {
195        self.play.is_some()
196    }
197
198    pub fn running(&self) -> bool {
199        self.play
200            .as_ref()
201            .is_some_and(|play| !play.driver.session().paused())
202    }
203
204    pub fn shown(&self) -> bool {
205        self.play.as_ref().is_some_and(|play| play.drawn)
206    }
207
208    pub fn warming(&self) -> bool {
209        self.play.as_ref().is_some_and(|play| play.warmup.is_some())
210    }
211
212    pub fn renderer(&self) -> Option<&Renderer> {
213        self.play.as_ref().map(|play| play.painter.renderer())
214    }
215
216    pub fn driver(&mut self) -> Option<&mut Driver> {
217        self.play.as_mut().map(|play| &mut play.driver)
218    }
219
220    pub fn texture(&mut self, egui: &mut egui_wgpu::Renderer) -> egui::TextureId {
221        let device = &self.gpu.device;
222        let shown = &self.target.shown;
223        *self.texture.get_or_insert_with(|| {
224            egui.register_native_texture(device, shown, wgpu::FilterMode::Linear)
225        })
226    }
227
228    pub fn resize(
229        &mut self,
230        egui: &mut egui_wgpu::Renderer,
231        size: [u32; 2],
232    ) -> Result<bool, String> {
233        let size = [size[0].max(1), size[1].max(1)];
234        if size == self.size() {
235            return Ok(false);
236        }
237        self.target = surface(&self.gpu, size);
238        if let Some(id) = self.texture {
239            egui.update_egui_texture_from_wgpu_texture(
240                &self.gpu.device,
241                &self.target.shown,
242                wgpu::FilterMode::Linear,
243                id,
244            );
245        }
246        if let Some(play) = &mut self.play {
247            play.driver.feed(Event::Resized(Size {
248                width: size[0],
249                height: size[1],
250            }));
251            self.stepped = true;
252        }
253        self.trace = None;
254        Ok(true)
255    }
256
257    pub fn forget_trace(&mut self) {
258        self.trace = None;
259    }
260
261    pub fn trace_samples(&self) -> u32 {
262        self.trace.as_ref().map_or(0, |preview| preview.samples)
263    }
264
265    pub fn tracing(&self) -> bool {
266        self.trace
267            .as_ref()
268            .is_none_or(|preview| preview.samples < TRACE_SAMPLES)
269    }
270
271    pub fn sync(&mut self, editor: &mut Editor) {
272        if editor.muted != self.audio.muted() {
273            self.audio.set_muted(editor.muted);
274        }
275        editor.sound = self.audio.line();
276        editor.sound_available = self.audio.available();
277        if !editor.trace {
278            self.trace = None;
279        }
280        editor.trace_samples = self.trace_samples();
281    }
282
283    fn play_frame(&mut self, seconds: f32) -> Result<bool, String> {
284        let running = self.running();
285        let stepped = std::mem::take(&mut self.stepped);
286        let Some(play) = &mut self.play else {
287            return Ok(false);
288        };
289        if running {
290            let start = play.clock_ns;
291            play.clock_ns =
292                start.saturating_add((f64::from(seconds.max(0.0)) * 1e9).round() as u64);
293            if let Some(warmup) = &mut play.warmup {
294                if !warmup.pass(&mut play.painter, &mut play.driver, FORMAT, play.clock_ns)? {
295                    return Ok(false);
296                }
297                play.warmup = None;
298                play.driver.start_clock(start);
299            }
300            play.driver.update(play.clock_ns);
301            self.audio.advance(play.driver.session_mut());
302        } else if !stepped {
303            return Ok(false);
304        }
305        if play.warmup.is_some() || play.driver.exit().is_some() {
306            return Ok(false);
307        }
308        play.painter
309            .draw(&mut play.driver, &self.target.image.view, FORMAT)?;
310        play.drawn = true;
311        Ok(true)
312    }
313
314    pub fn play(&mut self, editor: &mut Editor, seconds: f32) -> bool {
315        let drew = match self.play_frame(seconds) {
316            Ok(drew) => drew,
317            Err(error) => {
318                editor.log.error(format!("play: {error}; play stopped"));
319                self.stop_play(editor, false);
320                return true;
321            }
322        };
323        let exit = self
324            .play
325            .as_ref()
326            .and_then(|play| play.driver.exit().cloned());
327        match exit {
328            Some(Exit::Failed(error)) => {
329                editor
330                    .log
331                    .error(format!("play: the game failed: {error}; play stopped"));
332                self.stop_play(editor, false);
333                true
334            }
335            Some(Exit::Quit(code)) => {
336                editor
337                    .log
338                    .info(format!("play: the game quit with code {code}"));
339                self.stop_play(editor, false);
340                true
341            }
342            None => drew,
343        }
344    }
345
346    pub fn trace_frame(&mut self, scene: &Scene, of: (Camera, u64)) -> Result<bool, String> {
347        if self.trace.as_ref().is_some_and(|preview| preview.of != of) {
348            self.trace = None;
349        }
350        if self.trace.is_none() {
351            let mut scene = scene.clone();
352            scene.camera = Some(of.0);
353            let [width, height] = self.size();
354            let staged = pfx_trace::stage::scene(&scene, self.aspect())?;
355            let trace = staged
356                .trace(&self.gpu, width, height)
357                .map_err(|error| format!("{error:?}"))?;
358            self.trace = Some(Preview {
359                trace,
360                pacer: Pacer::default(),
361                samples: 0,
362                of,
363            });
364        }
365        let Some(preview) = &mut self.trace else {
366            return Ok(false);
367        };
368        if preview.samples >= TRACE_SAMPLES {
369            return Ok(false);
370        }
371        preview
372            .trace
373            .sample_paced(&self.gpu, 1, preview.samples, &mut preview.pacer, |_| ())?;
374        preview.samples += 1;
375        let color = preview.trace.readback(&self.gpu)?.color;
376        self.gpu
377            .upload_rgba8(&self.target.image, &tonemap(&color))?;
378        Ok(true)
379    }
380
381    pub fn start_play(
382        &mut self,
383        editor: &mut Editor,
384        game: Option<Box<dyn Game>>,
385        stage: Stage<'_>,
386    ) -> Result<(), String> {
387        if self.play.is_some() {
388            return Ok(());
389        }
390        let edited = editor.scene().clone();
391        let empty = Scene::empty();
392        let scene = if stage.flat { &empty } else { &edited };
393        let [width, height] = self.size();
394        let window = Size { width, height };
395        let mut painter = Painter::new(
396            self.gpu.clone(),
397            (!stage.flat).then_some(&edited),
398            stage.config,
399            window,
400        )?;
401        painter.wait_for_frames(true);
402        if !stage.flat {
403            painter.renderer_mut().wait_sky()?;
404        }
405        let hooks: Box<dyn PlayHooks> = match self.hooks.take() {
406            Some(hooks) => hooks,
407            None => Box::new(pfx_play::NoHooks),
408        };
409        let (audio_rate, audio_channels) = self.audio.begin(&mut editor.log);
410        let own = game.is_some();
411        let game = game.unwrap_or_else(|| Box::new(SceneGame));
412        let first = game
413            .settings()
414            .cloned()
415            .unwrap_or_else(|| stage.config.settings.clone());
416        let session = PlaySession::play_hooked(
417            scene,
418            painter.staged(),
419            game,
420            Options {
421                seed: stage.config.seed,
422                audio_rate,
423                audio_channels,
424                record: true,
425                tunables: stage.flat.then(Tunables::default),
426                rigs: stage.flat.then(Rigs::default),
427                ..Options::default()
428            },
429            hooks,
430        )
431        .map_err(|error| error.to_string());
432        let mut session = match session {
433            Ok(session) => session,
434            Err(error) => {
435                self.audio.abandon();
436                return Err(error);
437            }
438        };
439        session.set_frame_stats(painter.renderer().frame_stats());
440        let settings = session.game().settings().cloned().unwrap_or(first);
441        let device = stage
442            .config
443            .device
444            .unwrap_or_else(|| detect(&SystemSource).device);
445        let mut driver = Driver::new(
446            session,
447            settings,
448            stage.config.screen.clone(),
449            device,
450            window,
451            None,
452        );
453        *driver.labels_mut() = stage.labels;
454        driver.set_layout(
455            stage
456                .layout
457                .map(|layout| Box::new(layout) as Box<dyn LayoutSource>),
458        );
459        self.play = Some(Playing {
460            driver,
461            painter,
462            warmup: Some(Warmup::new(stage.config.warm_budget)),
463            clock_ns: 0,
464            drawn: false,
465        });
466        self.stepped = false;
467        editor.play = Play::Playing;
468        editor.log.info(match (own, stage.flat) {
469            (true, true) => "play: the game's own logic runs on an empty world; no file changes",
470            (true, false) => {
471                "play: the game's own logic runs on the scene; its files stay as they are"
472            }
473            (false, true) => "play: the empty world runs as a game; no file changes",
474            (false, false) => "play: the scene runs as a game; its files stay as they are",
475        });
476        Ok(())
477    }
478
479    pub fn session(&mut self) -> Option<&mut PlaySession> {
480        self.play.as_mut().map(|play| play.driver.session_mut())
481    }
482
483    pub fn pause(&mut self, editor: &mut Editor) {
484        if let Some(play) = &mut self.play {
485            let session = play.driver.session_mut();
486            session.pause();
487            self.audio.pause(session);
488            editor.play = Play::Paused;
489        }
490    }
491
492    pub fn resume(&mut self, editor: &mut Editor) {
493        if let Some(play) = &mut self.play {
494            play.driver.session_mut().resume();
495            self.audio.resume();
496            editor.play = Play::Playing;
497        }
498    }
499
500    pub fn step(&mut self, editor: &mut Editor) {
501        if let Some(play) = &mut self.play
502            && play.warmup.is_none()
503            && play.driver.step()
504        {
505            editor
506                .log
507                .info(format!("step: tick {}", play.driver.ticks()));
508            self.audio.step(play.driver.session_mut());
509            self.stepped = true;
510        }
511    }
512
513    pub fn save_recording(&mut self, editor: &mut Editor) {
514        let Some(play) = &self.play else {
515            return;
516        };
517        let Some(recording) = play.driver.session().recording() else {
518            editor
519                .log
520                .warn("record: this play session has no input recording");
521            return;
522        };
523        let scene = editor
524            .name()
525            .split('.')
526            .next()
527            .unwrap_or_default()
528            .to_string();
529        let folder = editor.project_root().join("tmp/pfx/recordings");
530        match write_recording(&folder, &scene, &stamp(), recording) {
531            Ok(path) => editor.log.info(format!(
532                "record: {} ticks of input saved to {}",
533                recording.len(),
534                path.display()
535            )),
536            Err(error) => editor.log.error(format!("record: {error}")),
537        }
538    }
539
540    pub fn stop_play(&mut self, editor: &mut Editor, keep: bool) {
541        let Some(mut play) = self.play.take() else {
542            return;
543        };
544        self.audio.stop(play.driver.session_mut());
545        let stopped = play.driver.into_session().stop();
546        for error in &stopped.errors {
547            editor.log.scene_error(error);
548        }
549        self.hooks = Some(stopped.hooks);
550        let held = stopped.pending.len();
551        let note = match (keep, held) {
552            (_, 0) => String::new(),
553            (true, _) => format!("; keeping {held} play edits"),
554            (false, _) => format!("; {held} play edits discarded"),
555        };
556        editor.log.info(format!(
557            "stop after {} ticks: back to the edited scene{note}",
558            stopped.ticks
559        ));
560        editor.stopped(keep.then_some(&stopped.pending));
561    }
562
563    pub fn world_edit(&mut self, editor: &mut Editor, change: &Change) {
564        let Some(play) = &mut self.play else {
565            return;
566        };
567        if editor.play_edit(play.driver.session_mut(), change) {
568            self.stepped = true;
569        }
570    }
571
572    pub fn pixels(&self) -> Result<Vec<u8>, String> {
573        self.gpu.readback_bytes(&self.target.image)
574    }
575}
576
577#[cfg(test)]
578mod tests {
579    use super::*;
580
581    #[test]
582    fn a_stamp_is_the_utc_date_and_time_of_the_second() {
583        assert_eq!(stamp_of(0), "19700101-000000");
584        assert_eq!(stamp_of(951_782_400), "20000229-000000");
585        assert_eq!(stamp_of(1_800_000_000), "20270115-080000");
586        assert_eq!(
587            stamp_of(1_800_000_000 + 86_399 - 8 * 3_600),
588            "20270115-235959"
589        );
590    }
591
592    #[test]
593    fn a_recording_is_written_as_json_pfx_bench_reads_and_never_over_another() {
594        use pfx_input::{InputEvent, Key};
595        let folder = Path::new(env!("CARGO_MANIFEST_DIR"))
596            .join("../../tmp/editor-tests")
597            .join("recordings");
598        let _ = std::fs::remove_dir_all(&folder);
599        let mut input = pfx_input::Input::new(pfx_input::Actions::new(Vec::new()).unwrap(), 16_667);
600        input.start_recording();
601        input.feed(InputEvent::Key {
602            key: Key::D,
603            pressed: true,
604        });
605        input.step();
606        input.step();
607        let recording = input.stop_recording().unwrap();
608        assert_eq!(recording.len(), 2);
609        let first = write_recording(&folder, "room", "20270115-080000", &recording).unwrap();
610        let second = write_recording(&folder, "room", "20270115-080000", &recording).unwrap();
611        assert_eq!(first, folder.join("room-20270115-080000.json"));
612        assert_eq!(second, folder.join("room-20270115-080000-2.json"));
613        let read: Recording =
614            serde_json::from_str(&std::fs::read_to_string(&first).unwrap()).unwrap();
615        assert_eq!(read, recording);
616        let _ = std::fs::remove_dir_all(&folder);
617    }
618
619    #[test]
620    fn the_preview_tonemap_maps_black_to_black_and_never_clips_a_bright_pixel_to_zero() {
621        assert_eq!(display(0.0), 0);
622        assert_eq!(display(-1.0), 0);
623        assert!(display(0.18) > 100 && display(0.18) < 140);
624        assert_eq!(display(1e9), 255);
625        let pixel: Vec<u8> = [0.0f32, 0.18, 4.0, 1.0]
626            .iter()
627            .flat_map(|value| value.to_le_bytes())
628            .collect();
629        let shown = tonemap(&pixel);
630        assert_eq!(shown.len(), 4);
631        assert_eq!(shown[0], 0);
632        assert_eq!(shown[3], 255);
633        assert!(shown[2] > shown[1]);
634    }
635}