Skip to main content

dotzuki_renderer/
window.rs

1use std::sync::Arc;
2use std::time::{Duration, Instant};
3
4use error_iter::ErrorIter as _;
5use log::error;
6use pixels::{PixelsBuilder, SurfaceTexture};
7use winit::dpi::LogicalSize;
8use winit::event::{ElementState, Event, WindowEvent};
9use winit::event_loop::EventLoop;
10use winit::keyboard::{KeyCode, PhysicalKey};
11use winit::window::Window;
12
13use crate::input::InputState;
14use crate::FbSurface;
15
16/// Original Game Boy VBlank frequency: 4194304 Hz / 70224 cycles ≈ 59.7275 Hz
17const FRAME_DURATION: Duration = Duration::from_nanos(16_742_706); // 1e9 / 59.7275
18
19pub struct GameWindowConfig {
20    pub title: String,
21    pub scale: u32,
22    pub resizable: bool,
23    /// Logical framebuffer width in pixels.
24    /// Set to 240 for GBA-resolution games (e.g. the FireRed example).
25    pub width: u32,
26    /// Logical framebuffer height in pixels.
27    /// Set to 160 for GBA-resolution games.
28    pub height: u32,
29}
30
31pub trait GameLoop {
32    /// The framebuffer type the game draws into: either the engine's RGBA
33    /// [`FrameBuffer`] (true-color games) or the indexed
34    /// [`crate::RgbaIndexedFrameBuffer`] (fixed-palette games).
35    type Fb: FbSurface;
36
37    fn update(&mut self, input: &InputState);
38    fn draw(&mut self, frame_buffer: &mut Self::Fb);
39    fn should_exit(&self) -> bool {
40        false
41    }
42}
43
44#[derive(Debug, thiserror::Error)]
45pub enum WindowError {
46    #[error("Failed to create event loop: {0}")]
47    EventLoop(String),
48    #[error("Failed to create window: {0}")]
49    WindowCreation(String),
50    #[error("Failed to create pixel buffer: {0}")]
51    PixelBuffer(#[from] pixels::Error),
52}
53
54pub fn run<G: GameLoop + 'static>(
55    config: GameWindowConfig,
56    mut game: G,
57) -> Result<(), WindowError> {
58    let event_loop = EventLoop::new().map_err(|e| WindowError::EventLoop(e.to_string()))?;
59    let (fb_width, fb_height) = (config.width, config.height);
60    let window = {
61        let size = LogicalSize::new(
62            (fb_width * config.scale) as f64,
63            (fb_height * config.scale) as f64,
64        );
65        #[allow(deprecated)]
66        Arc::new(
67            event_loop
68                .create_window(
69                    Window::default_attributes()
70                        .with_title(&config.title)
71                        .with_inner_size(size)
72                        .with_min_inner_size(LogicalSize::new(
73                            fb_width as f64,
74                            fb_height as f64,
75                        ))
76                        .with_resizable(config.resizable),
77                )
78                .map_err(|e| WindowError::WindowCreation(e.to_string()))?,
79        )
80    };
81
82    let mut pixels = {
83        let window_size = window.inner_size();
84        let surface_texture =
85            SurfaceTexture::new(window_size.width, window_size.height, Arc::clone(&window));
86        PixelsBuilder::new(fb_width, fb_height, surface_texture).build()?
87    };
88
89    let mut frame_buffer = G::Fb::new_screen(fb_width, fb_height);
90    let mut input = InputState::new();
91    let mut next_frame_time = Instant::now();
92
93    #[allow(deprecated)]
94    let res = event_loop.run(move |event, elwt| match event {
95        Event::WindowEvent { event, .. } => match event {
96            WindowEvent::CloseRequested => {
97                elwt.exit();
98            }
99            WindowEvent::RedrawRequested => {
100                game.draw(&mut frame_buffer);
101                frame_buffer.present_into(pixels.frame_mut());
102                if let Err(err) = pixels.render() {
103                    log_error("pixels.render", err);
104                    elwt.exit();
105                }
106            }
107            WindowEvent::Resized(size) => {
108                if size.width > 0 && size.height > 0 {
109                    if let Err(err) = pixels.resize_surface(size.width, size.height) {
110                        log_error("pixels.resize_surface", err);
111                        elwt.exit();
112                    }
113                }
114            }
115            WindowEvent::KeyboardInput {
116                event: key_event, ..
117            } => {
118                if let PhysicalKey::Code(keycode) = key_event.physical_key {
119                    let pressed = key_event.state == ElementState::Pressed;
120                    if pressed && keycode == KeyCode::Escape {
121                        elwt.exit();
122                        return;
123                    }
124                    input.set_from_keycode(keycode, pressed);
125                }
126            }
127            _ => {}
128        },
129        Event::AboutToWait => {
130            let now = Instant::now();
131            if now >= next_frame_time {
132                game.update(&input);
133                input.begin_frame();
134                if game.should_exit() {
135                    elwt.exit();
136                    return;
137                }
138                window.request_redraw();
139                next_frame_time += FRAME_DURATION;
140                if next_frame_time < now {
141                    next_frame_time = now + FRAME_DURATION;
142                }
143            }
144            let sleep_duration = next_frame_time.saturating_duration_since(Instant::now());
145            if !sleep_duration.is_zero() {
146                std::thread::sleep(sleep_duration);
147            }
148        }
149        _ => {}
150    });
151
152    res.map_err(|e| WindowError::EventLoop(e.to_string()))
153}
154
155fn log_error<E: std::error::Error + 'static>(method_name: &str, err: E) {
156    error!("{method_name}() failed: {err}");
157    for source in err.sources().skip(1) {
158        error!("  Caused by: {source}");
159    }
160}