Skip to main content

pfx_game/
headless.rs

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}