Skip to main content

optic_loop/
lib.rs

1//! The game loop and runtime — drives the winit event loop and owns the GPU,
2//! camera, windows, and timing.
3//!
4//! This crate provides two ergonomic levels for running an Optic application:
5//!
6//! # High-level API: [`Game`] + [`Runtime`]
7//!
8//! The [`Game`] struct owns all engine subsystems (GPU, camera, window, events,
9//! time, gamepad). You implement the [`Runtime`] trait and pass it to
10//! [`Game::run`]:
11//!
12//! ```ignore
13//! use optic_loop::{Game, Runtime};
14//!
15//! struct App;
16//! impl Runtime for App {
17//!     fn start(&mut self, _game: &mut Game) {}
18//!     fn update(&mut self, game: &mut Game) {
19//!         game.renderer.clear();
20//!         // render things...
21//!     }
22//!     fn end(&mut self, _game: &mut Game) {}
23//! }
24//!
25//! Game::run(App);
26//! ```
27//!
28//! # Low-level API: [`GameLoop`] + closure
29//!
30//! [`GameLoop`] takes a `FnMut(&mut FrameState)` closure and gives you more
31//! control over setup. Use [`run`] for a quick single-window start:
32//!
33//! ```ignore
34//! use optic_loop::run;
35//!
36//! run("My Window", (800, 600).into(), |frame| {
37//!     frame.gpu.clear();
38//!     // render things...
39//! });
40//! ```
41//!
42//! # Frame timing
43//!
44//! Both APIs update [`Time`] automatically each frame. Access delta time and
45//! FPS through the [`FrameState`] (low-level) or `game.time` (high-level).
46
47mod game;
48mod runtime;
49mod time;
50
51pub use game::*;
52pub use runtime::*;
53pub use time::*;
54
55use gilrs::Gilrs;
56use optic_core::{log_error, CamProj, Coord2D, OpticResult, Size2D};
57use optic_render::{Camera, GPU};
58use optic_window::{Events, Window};
59use winit::application::ApplicationHandler;
60use winit::event::WindowEvent;
61use winit::event_loop::{ActiveEventLoop, EventLoop};
62use winit::window::WindowId;
63
64/// A single window and its associated event sink and GPU surface index.
65///
66/// Used by [`GameLoop`] to manage multiple windows. Each `WindowState`
67/// owns a [`Window`], an [`Events`] collector, and the index of its
68/// surface within the GPU's context.
69///
70/// # Example
71///
72/// ```ignore
73/// let ws = WindowState::new(&event_loop, "My Window", (800, 600).into());
74/// ```
75pub struct WindowState {
76    pub window: Window,
77    pub events: Events,
78    pub surface_index: usize,
79}
80
81impl WindowState {
82    /// Creates a new window and registers it with the event loop.
83    pub fn new(el: &EventLoop<()>, title: &str, size: Size2D) -> Self {
84        Self {
85            window: Window::new(el, title, size),
86            events: Events::new(),
87            surface_index: 0,
88        }
89    }
90
91    /// Closes the underlying window.
92    pub fn close(&mut self) {
93        self.window.close();
94    }
95
96    /// Returns `true` if the window has been closed.
97    pub fn is_closed(&self) -> bool {
98        self.window.is_closed()
99    }
100
101    /// Returns the GPU surface index for this window.
102    pub fn surface_index(&self) -> usize {
103        self.surface_index
104    }
105}
106
107/// A snapshot of per-frame mutable state, passed to the user's closure.
108///
109/// Contains borrows of the engine subsystems that the user may access
110/// during a frame callback in the [`GameLoop`] API.
111pub struct FrameState<'a> {
112    pub time: &'a Time,
113    pub windows: &'a mut [WindowState],
114    pub gpu: &'a mut GPU,
115    pub camera: &'a mut Camera,
116}
117
118/// A low-level game loop that drives a closure once per frame.
119///
120/// Owns the event loop, one or more windows, the GPU, a camera, timing,
121/// and gamepad state. The user provides a `FnMut(&mut FrameState)` closure
122/// that is invoked every frame.
123///
124/// This is the lower-level alternative to [`Game`] + [`Runtime`]. Use it
125/// when you want more control over the setup process or need multiple
126/// windows.
127///
128/// # Example
129///
130/// ```ignore
131/// use optic_loop::{GameLoop, WindowState};
132///
133/// let el = EventLoop::new().unwrap();
134/// let ws = WindowState::new(&el, "App", (800, 600).into());
135/// let gpu = GPU::new_headless()?;
136/// let camera = Camera::new((800, 600).into(), CamProj::Persp);
137///
138/// let game = GameLoop::new(el, gpu, camera, vec![ws], |frame| {
139///     frame.gpu.clear();
140/// })?;
141/// game.run();
142/// ```
143pub struct GameLoop<F: FnMut(&mut FrameState)> {
144    event_loop: Option<EventLoop<()>>,
145    windows: Vec<WindowState>,
146    gpu: Option<GPU>,
147    camera: Camera,
148    time: Time,
149    gilrs: Gilrs,
150    frame_fn: F,
151}
152
153impl<F: FnMut(&mut FrameState)> GameLoop<F> {
154    /// Constructs a new game loop.
155    ///
156    /// Attaches each window's raw handle to the GPU context and initialises
157    /// gamepad support via `gilrs`.
158    ///
159    /// # Errors
160    ///
161    /// Returns an error if window attachment to the GPU surface or gamepad
162    /// initialisation fails.
163    pub fn new(
164        el: EventLoop<()>,
165        mut gpu: GPU,
166        camera: Camera,
167        mut windows: Vec<WindowState>,
168        frame_fn: F,
169    ) -> OpticResult<Self> {
170        for ws in windows.iter_mut() {
171            if let Some(handle) = ws.window.raw_handle() {
172                let size = ws.window.size();
173                let idx = gpu.ctx.attach_window(handle, size)
174                    .map_err(|e| optic_core::OpticError::custom(&format!("attach window failed: {e}")))?;
175                ws.surface_index = idx;
176            }
177        }
178
179        let gilrs = Gilrs::new()
180            .map_err(|e| optic_core::OpticError::custom(&format!("gilrs init failed: {e}")))?;
181
182        Ok(Self {
183            event_loop: Some(el),
184            windows,
185            gpu: Some(gpu),
186            camera,
187            time: Time::new(),
188            gilrs,
189            frame_fn,
190        })
191    }
192
193    /// Starts the event loop, consuming `self`.
194    ///
195    /// This call blocks until all windows are closed or the application
196    /// exits.
197    pub fn run(mut self) {
198        let el = self.event_loop.take().unwrap();
199        let _ = el.run_app(&mut self);
200    }
201}
202
203impl<F: FnMut(&mut FrameState)> ApplicationHandler for GameLoop<F> {
204    fn resumed(&mut self, _el: &ActiveEventLoop) {
205        self.time.start_time = std::time::Instant::now();
206        self.time.prev_time = std::time::Instant::now();
207        self.time.prev_sec = std::time::Instant::now();
208    }
209
210    fn window_event(
211        &mut self,
212        _el: &ActiveEventLoop,
213        id: WindowId,
214        event: WindowEvent,
215    ) {
216        for ws in &mut self.windows {
217            if !ws.window.is_running() { continue; }
218            if ws.window.id().map_or(true, |wid| wid != id) { continue; }
219
220            match &event {
221                WindowEvent::Resized(_size) => {
222                    if let Some(gpu) = &mut self.gpu {
223                        gpu.ctx.resize_window(ws.surface_index, ws.window.size());
224                        let _ = gpu.ctx.make_current(ws.surface_index);
225                        self.camera.set_size(ws.window.size());
226                    }
227                }
228                WindowEvent::CursorMoved { position, .. } => {
229                    ws.window.notify_cursor_moved(Coord2D::from(position.x, position.y));
230                }
231                WindowEvent::CursorEntered { .. } => {
232                    ws.window.notify_cursor_inside(true);
233                }
234                WindowEvent::CursorLeft { .. } => {
235                    ws.window.notify_cursor_inside(false);
236                }
237                WindowEvent::CloseRequested => {
238                    ws.events.close_requested = true;
239                }
240                _ => {}
241            }
242            ws.events.process_window_event(&event, &ws.window);
243            break;
244        }
245    }
246
247    fn about_to_wait(&mut self, _el: &ActiveEventLoop) {
248        let gpu = match &mut self.gpu {
249            Some(g) => g,
250            None => return,
251        };
252
253        self.windows.retain(|ws| !ws.window.is_closed());
254
255        if self.windows.is_empty() {
256            return;
257        }
258
259        while let Some(gilrs_event) = self.gilrs.next_event() {
260            for ws in &mut self.windows {
261                ws.events.process_gilrs_event(&gilrs_event);
262            }
263        }
264
265        self.time.update();
266
267        {
268            let mut frame = FrameState {
269                time: &self.time,
270                windows: &mut self.windows,
271                gpu,
272                camera: &mut self.camera,
273            };
274            (self.frame_fn)(&mut frame);
275        }
276
277        for ws in &mut self.windows {
278            ws.events.end_frame();
279        }
280
281        for ws in &mut self.windows {
282            ws.window.request_redraw();
283        }
284    }
285}
286
287/// Runs a single-window application with a per-frame closure.
288///
289/// This is the simplest way to get a window on screen. On error, the
290/// error is logged and the process exits with `ERROR`.
291///
292/// # Example
293///
294/// ```ignore
295/// use optic_loop::run;
296///
297/// run("Hello", (800, 600).into(), |frame| {
298///     frame.gpu.clear();
299/// });
300/// ```
301pub fn run<F>(title: &str, size: Size2D, frame_fn: F)
302where
303    F: FnMut(&mut FrameState) + 'static,
304{
305    let result = try_run(title, size, frame_fn);
306    if let Err(e) = result {
307        log_error!("{}", e);
308        optic_core::end(optic_core::ERROR);
309    }
310}
311
312fn try_run<F>(title: &str, size: Size2D, frame_fn: F) -> OpticResult<()>
313where
314    F: FnMut(&mut FrameState) + 'static,
315{
316    let el = EventLoop::new()
317        .map_err(|e| optic_core::OpticError::custom(&format!("event loop creation failed: {e}")))?;
318    let ws = WindowState::new(&el, title, size);
319    let handle = ws.window.raw_handle()
320        .ok_or_else(|| optic_core::OpticError::custom("failed to get raw window handle"))?;
321    let display_handle = ws.window.raw_display_handle()
322        .ok_or_else(|| optic_core::OpticError::custom("failed to get raw display handle"))?;
323    let gpu = GPU::new_windowed(handle, display_handle, ws.window.size())
324        .map_err(|e| optic_core::OpticError::custom(&format!("gpu init failed: {e}")))?;
325    let camera = Camera::new(ws.window.size(), CamProj::Persp);
326    let game = GameLoop::new(el, gpu, camera, vec![ws], frame_fn)?;
327    game.run();
328    Ok(())
329}