1use pfx_core::clock::Tick;
2use pfx_gpu::pace::{Pace, WallClock};
3use pfx_gpu::screens::{Device, Display};
4use pfx_gpu::window::{
5 FakeClock, FullscreenPlan, Host, Monitor, Monitors, Pace as Due, PlatformCaps, Point,
6 PresentPreference, Size, WindowOps,
7};
8use pfx_gpu::{Gpu, OffscreenTarget, wgpu};
9use pfx_play::{Game, NoWarmer, Warming};
10
11use crate::config::{Config, Crash, Pads, report};
12use crate::driver::{Driver, Event, present_preference};
13use crate::painter::{Drawn, Painter};
14use crate::start;
15
16#[derive(Clone, Debug, PartialEq)]
17pub enum WindowCall {
18 Fullscreen(FullscreenPlan),
19 Decorations(bool),
20 Size(Size),
21 Position(Point),
22 Resizable(bool),
23 Present(PresentPreference),
24}
25
26#[derive(Clone, Debug, Default, PartialEq)]
27pub struct StandIn {
28 pub calls: Vec<WindowCall>,
29}
30
31impl WindowOps for StandIn {
32 fn set_fullscreen(&mut self, fullscreen: &FullscreenPlan) {
33 self.calls.push(WindowCall::Fullscreen(*fullscreen));
34 }
35
36 fn set_decorations(&mut self, decorations: bool) {
37 self.calls.push(WindowCall::Decorations(decorations));
38 }
39
40 fn request_inner_size(&mut self, size: Size) {
41 self.calls.push(WindowCall::Size(size));
42 }
43
44 fn set_outer_position(&mut self, position: Point) {
45 self.calls.push(WindowCall::Position(position));
46 }
47
48 fn set_resizable(&mut self, resizable: bool) {
49 self.calls.push(WindowCall::Resizable(resizable));
50 }
51}
52
53pub fn stand_in_monitors(size: Size) -> Monitors {
54 Monitors {
55 list: vec![Monitor {
56 name: Some("stand-in".into()),
57 position: Point { x: 0, y: 0 },
58 size,
59 refresh_millihertz: Some(60_000),
60 video_modes: Vec::new(),
61 }],
62 primary: Some(0),
63 current: Some(0),
64 }
65}
66
67#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
68pub struct Count {
69 pub frames: u64,
70 pub ticks: u64,
71}
72
73struct Gpued {
74 painter: Painter,
75 target: OffscreenTarget,
76 pace: Pace<WallClock>,
77}
78
79pub struct Headless {
80 pub driver: Driver,
81 pub clock: FakeClock,
82 pub window: StandIn,
83 pub monitors: Monitors,
84 pub host: Host,
85 pub audio: Vec<f32>,
86 pub frame_cost_ns: u64,
87 pub drawn: Option<Drawn>,
88 pub warm_passes: u32,
89 pub warm_budget: f64,
90 warmed: bool,
91 gpu: Option<Gpued>,
92 name: String,
93 crash: Option<Crash>,
94}
95
96pub const TARGET_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
97
98fn backend(config: &mut Config) -> Option<Box<dyn pfx_input::Backend>> {
99 match std::mem::replace(&mut config.pads, Pads::None) {
100 Pads::Backend(backend) => Some(backend),
101 Pads::None | Pads::Gilrs => None,
102 }
103}
104
105impl Headless {
106 pub fn new<G: Game + 'static>(game: G, config: Config, window: Size) -> Result<Self, String> {
107 Self::build(Box::new(game), config, None, window)
108 }
109
110 pub fn with_gpu<G: Game + 'static>(
111 game: G,
112 config: Config,
113 gpu: Gpu,
114 window: Size,
115 ) -> Result<Self, String> {
116 Self::build(Box::new(game), config, Some(gpu), window)
117 }
118
119 fn build(
120 game: Box<dyn Game>,
121 mut config: Config,
122 gpu: Option<Gpu>,
123 window: Size,
124 ) -> Result<Self, String> {
125 let scene = start::scene(&config)?;
126 let device = config.device.unwrap_or(Device::Desktop);
127 let gpu = gpu
128 .map(|gpu| -> Result<Gpued, String> {
129 let target = gpu.offscreen(window.width, window.height, TARGET_FORMAT)?;
130 let painter = Painter::new(gpu, scene.as_ref(), &config, window)?;
131 Ok(Gpued {
132 painter,
133 target,
134 pace: Pace::new(),
135 })
136 })
137 .transpose()?;
138 let pads = backend(&mut config);
139 let mut driver = start::driver(
140 scene.as_ref(),
141 gpu.as_ref().map(|gpu| &gpu.painter),
142 game,
143 &config,
144 device,
145 window,
146 (48_000, 2),
147 pads,
148 )?;
149 driver.set_layout(config.layout.take());
150 let mut headless = Self {
151 driver,
152 clock: FakeClock::default(),
153 window: StandIn::default(),
154 monitors: stand_in_monitors(window),
155 host: Host::Wayland,
156 audio: Vec::new(),
157 frame_cost_ns: 1_000_000,
158 drawn: None,
159 warm_passes: 0,
160 warm_budget: config.warm_budget,
161 warmed: false,
162 gpu,
163 crash: config.crash.take(),
164 name: config.name,
165 };
166 let display = headless.driver.display().clone();
167 apply(&display, &mut headless.window, &headless.monitors);
168 Ok(headless)
169 }
170
171 pub fn exited(&self) -> bool {
172 self.driver.exit().is_some()
173 }
174
175 pub fn finish(self) -> Result<u8, String> {
176 let outcome = self.driver.finish();
177 if let Err(error) = &outcome {
178 report(&self.name, self.crash.as_ref(), error);
179 }
180 outcome
181 }
182
183 pub fn feed(&mut self, event: Event) {
184 self.driver.feed(event);
185 }
186
187 pub fn frame(&mut self) -> Result<Option<Tick>, String> {
188 self.frame_before(u64::MAX)
189 }
190
191 pub fn warm(&mut self) -> Result<u32, String> {
192 while !self.warmed {
193 let elapsed = self.warm_passes as f64 * self.frame_cost_ns as f64 / 1e9;
194 if self.warm_passes > 0 && elapsed >= self.warm_budget {
195 break;
196 }
197 let pass = self.warm_passes;
198 let budget = self.warm_budget;
199 let warming = match &mut self.gpu {
200 Some(gpu) => {
201 let driver = &mut self.driver;
202 let painter = &mut gpu.painter;
203 let mut result = Ok(Warming::Done);
204 gpu.pace.frame(|| {
205 result = painter.warm(driver, TARGET_FORMAT, pass, elapsed, budget);
206 });
207 result?
208 }
209 None => self.driver.warm(&mut NoWarmer, pass, elapsed, budget)?,
210 };
211 self.warm_passes += 1;
212 if warming == Warming::Done {
213 break;
214 }
215 }
216 self.warmed = true;
217 Ok(self.warm_passes)
218 }
219
220 fn frame_before(&mut self, end_ns: u64) -> Result<Option<Tick>, String> {
221 if self.exited() {
222 return Ok(None);
223 }
224 self.warm()?;
225 loop {
226 match self.driver.poll(&self.clock) {
227 Due::Start { .. } => break,
228 Due::Wait { until_ns } if until_ns < end_ns => self.clock.now_ns = until_ns,
229 Due::Wait { .. } | Due::Paused => return Ok(None),
230 }
231 }
232 let tick = self.driver.update(self.clock.now_ns);
233 if let Some(display) = self.driver.take_display().cloned() {
234 apply(&display, &mut self.window, &self.monitors);
235 }
236 if let Some(vrr) = self.driver.take_present() {
237 self.window
238 .calls
239 .push(WindowCall::Present(present_preference(vrr, self.host)));
240 }
241 if self.exited() {
242 self.audio.extend(self.driver.take_audio());
243 return Ok(Some(tick));
244 }
245 if let Some(gpu) = &mut self.gpu {
246 let driver = &mut self.driver;
247 let painter = &mut gpu.painter;
248 let view = &gpu.target.view;
249 let mut result = Ok(None);
250 gpu.pace.frame(|| {
251 result = painter.draw(driver, view, TARGET_FORMAT);
252 });
253 self.drawn = result?;
254 }
255 self.audio.extend(self.driver.take_audio());
256 self.clock.now_ns = self.clock.now_ns.saturating_add(self.frame_cost_ns);
257 Ok(Some(tick))
258 }
259
260 pub fn run_for(&mut self, seconds: f64) -> Result<Count, String> {
261 let end = self
262 .clock
263 .now_ns
264 .saturating_add((seconds * 1e9).round() as u64);
265 let frames = self.driver.frames();
266 let ticks = self.driver.ticks();
267 while self.clock.now_ns < end {
268 if self.frame_before(end)?.is_none() {
269 break;
270 }
271 }
272 self.clock.now_ns = self.clock.now_ns.max(end);
273 Ok(Count {
274 frames: self.driver.frames() - frames,
275 ticks: self.driver.ticks() - ticks,
276 })
277 }
278
279 pub fn target(&self) -> Option<&OffscreenTarget> {
280 self.gpu.as_ref().map(|gpu| &gpu.target)
281 }
282
283 pub fn painter(&self) -> Option<&Painter> {
284 self.gpu.as_ref().map(|gpu| &gpu.painter)
285 }
286
287 pub fn painter_mut(&mut self) -> Option<&mut Painter> {
288 self.gpu.as_mut().map(|gpu| &mut gpu.painter)
289 }
290
291 pub fn readback(&self) -> Result<Vec<u8>, String> {
292 let gpu = self.gpu.as_ref().ok_or("this run has no GPU")?;
293 gpu.painter.renderer().gpu().readback_rgba8(&gpu.target)
294 }
295}
296
297fn apply(display: &Display, window: &mut StandIn, monitors: &Monitors) {
298 let _ = display.apply(PlatformCaps { exclusive: true }, monitors, window);
299}