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(fb_width as f64, fb_height as f64))
73                        .with_resizable(config.resizable),
74                )
75                .map_err(|e| WindowError::WindowCreation(e.to_string()))?,
76        )
77    };
78
79    let mut pixels = {
80        let window_size = window.inner_size();
81        let surface_texture =
82            SurfaceTexture::new(window_size.width, window_size.height, Arc::clone(&window));
83        PixelsBuilder::new(fb_width, fb_height, surface_texture).build()?
84    };
85
86    let mut frame_buffer = G::Fb::new_screen(fb_width, fb_height);
87    let mut input = InputState::new();
88    let mut next_frame_time = Instant::now();
89
90    #[allow(deprecated)]
91    let res = event_loop.run(move |event, elwt| match event {
92        Event::WindowEvent { event, .. } => match event {
93            WindowEvent::CloseRequested => {
94                elwt.exit();
95            }
96            WindowEvent::RedrawRequested => {
97                game.draw(&mut frame_buffer);
98                frame_buffer.present_into(pixels.frame_mut());
99                if let Err(err) = pixels.render() {
100                    log_error("pixels.render", err);
101                    elwt.exit();
102                }
103            }
104            WindowEvent::Resized(size) => {
105                if size.width > 0 && size.height > 0 {
106                    if let Err(err) = pixels.resize_surface(size.width, size.height) {
107                        log_error("pixels.resize_surface", err);
108                        elwt.exit();
109                    }
110                }
111            }
112            WindowEvent::KeyboardInput {
113                event: key_event, ..
114            } => {
115                if let PhysicalKey::Code(keycode) = key_event.physical_key {
116                    let pressed = key_event.state == ElementState::Pressed;
117                    if pressed && keycode == KeyCode::Escape {
118                        elwt.exit();
119                        return;
120                    }
121                    input.set_from_keycode(keycode, pressed);
122                }
123            }
124            _ => {}
125        },
126        Event::AboutToWait => {
127            let now = Instant::now();
128            if now >= next_frame_time {
129                game.update(&input);
130                input.begin_frame();
131                if game.should_exit() {
132                    elwt.exit();
133                    return;
134                }
135                window.request_redraw();
136                next_frame_time += FRAME_DURATION;
137                if next_frame_time < now {
138                    next_frame_time = now + FRAME_DURATION;
139                }
140            }
141            let sleep_duration = next_frame_time.saturating_duration_since(Instant::now());
142            if !sleep_duration.is_zero() {
143                std::thread::sleep(sleep_duration);
144            }
145        }
146        _ => {}
147    });
148
149    res.map_err(|e| WindowError::EventLoop(e.to_string()))
150}
151
152fn log_error<E: core::error::Error + 'static>(method_name: &str, err: E) {
153    error!("{method_name}() failed: {err}");
154    for source in err.sources().skip(1) {
155        error!("  Caused by: {source}");
156    }
157}