1use std::time::{Duration, Instant};
6
7use crate::gfx::{Gfx, HEADLESS_SIZE};
8use crate::host::{Stop, VirtualClock};
9use crate::present::Present;
10use crate::script::{Script, ScriptState};
11use crate::session::Session;
12
13pub struct Options {
14 pub frames: u64,
15 pub screenshot: Option<String>,
17 pub screenshot_filtered: Option<String>,
19 pub present: Present,
20 pub window: (u32, u32),
21 pub script: Script,
22 pub realtime: bool,
24 pub hash: bool,
25}
26
27pub struct Report {
29 pub frames: u64,
30 pub presented: u64,
31 pub size: (usize, usize),
32 pub video_hash: u32,
33 pub audio_hash: u32,
34 pub audio_frames: u64,
35 pub frame_rate: f64,
36 pub seconds: f64,
37 pub exit: Option<i32>,
39}
40
41impl Report {
42 pub fn print(&self) {
44 println!("frames={} presented={} size={}x{}", self.frames, self.presented, self.size.0, self.size.1);
45 println!("video_fnv32={:08x} audio_fnv32={:08x} audio_frames={}", self.video_hash, self.audio_hash, self.audio_frames);
46 }
47}
48
49pub fn run(session: Session, opts: &Options) -> Result<Report, String> {
50 let gpu = opts.screenshot.is_some() || opts.screenshot_filtered.is_some();
52 let gfx = match gpu {
53 true => Gfx::offscreen(HEADLESS_SIZE.0, HEADLESS_SIZE.1).unwrap_or_else(|e| {
54 eprintln!("[gasm] no GPU for screenshots ({e}); gfx output will be blank");
55 Gfx::null()
56 }),
57 false => Gfx::null(),
58 };
59 if let Some(t) = crate::host::static_title(&session.wasm) {
60 eprintln!("[gasm] title: {t} (gasm.title)");
61 }
62 let mut game = match session.start(None, gfx, true, opts.hash) {
63 Ok(g) => g,
64 Err(Stop::Exit(code)) => {
65 return Ok(Report { frames: 0, presented: 0, size: (0, 0), video_hash: 0x811c_9dc5, audio_hash: 0x811c_9dc5, audio_frames: 0, frame_rate: 60.0, seconds: 0.0, exit: Some(code) });
66 }
67 Err(Stop::Trap(e)) => return Err(e),
68 };
69 let t0 = Instant::now();
70 let mut exit = None;
71 let mut ran = 0;
72 let mut clock = VirtualClock::default();
73 let mut script_state = ScriptState::default();
74 for i in 0..opts.frames {
75 let host = game.host_mut();
76 let rate = host.frame_rate;
77 host.virtual_time_ms = Some(clock.at(i, rate));
78 host.pads = [opts.script.pad(i), 0, 0, 0];
79 host.text = Some(opts.script.text(i));
80 let (w, h) = host.gfx.size();
81 let mode = host.input_mode;
82 host.input = opts.script.raw(i, &mut script_state, (w as f32, h as f32), mode);
83 host.show_frame = gpu && i + 1 == opts.frames;
84 ran = i + 1;
85 let result = game.frame();
86 let host = game.host_mut();
87 if std::mem::take(&mut host.title_changed) {
88 eprintln!("[gasm] title: {}", host.title.as_deref().unwrap_or("(default)"));
89 }
90 match result {
91 Ok(()) => {}
92 Err(Stop::Exit(code)) => {
93 exit = Some(code);
94 break;
95 }
96 Err(Stop::Trap(e)) => return Err(format!("frame {i}: {e}")),
97 }
98 if opts.realtime {
99 let due = t0 + Duration::from_secs_f64((i + 1) as f64 / game.host().frame_rate);
100 if let Some(wait) = due.checked_duration_since(Instant::now()) {
101 std::thread::sleep(wait);
102 }
103 }
104 }
105 if exit.is_none() {
106 game.exit();
107 }
108 if let Some(path) = &opts.screenshot_filtered {
109 let h = game.host_mut();
110 let img = match h.gfx.used {
111 true => h.gfx.read_offscreen(), false if h.width > 0 => {
113 let rgba = std::mem::take(&mut h.rgba);
114 h.gfx.present = opts.present;
115 let img = h.gfx.render_video(&rgba, h.width as u32, h.height as u32, h.aspect, opts.window);
116 h.rgba = rgba;
117 img
118 }
119 false => None,
120 };
121 if let Some((w, hgt, rgba)) = img {
122 write_png(path, w, hgt, &rgba)?;
123 eprintln!("[gasm] wrote {path}");
124 }
125 }
126 let h = game.host();
127 if let Some(path) = &opts.screenshot {
128 let (w, hgt, rgba) = match h.gfx.read_offscreen() {
129 Some(img) if h.gfx.used => img,
130 _ => (h.width as u32, h.height as u32, h.rgba.clone()),
131 };
132 if w > 0 {
133 write_png(path, w, hgt, &rgba)?;
134 eprintln!("[gasm] wrote {path}");
135 }
136 }
137 Ok(Report {
138 frames: ran,
139 presented: h.frames_presented,
140 size: (h.width, h.height),
141 video_hash: h.video_hash.0,
142 audio_hash: h.audio_hash.0,
143 audio_frames: h.audio_frames,
144 frame_rate: h.frame_rate,
145 seconds: t0.elapsed().as_secs_f64(),
146 exit,
147 })
148}
149
150pub fn write_png(path: &str, w: u32, h: u32, rgba: &[u8]) -> Result<(), String> {
151 let file = std::fs::File::create(path).map_err(|e| format!("{path}: {e}"))?;
152 let mut enc = png::Encoder::new(std::io::BufWriter::new(file), w, h);
153 enc.set_color(png::ColorType::Rgba);
154 enc.set_depth(png::BitDepth::Eight);
155 let mut writer = enc.write_header().map_err(|e| e.to_string())?;
156 writer.write_image_data(rgba).map_err(|e| e.to_string())
157}