1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
use super::*;
/// Implements the top-level static engine entry points on the `Engine` namespace.
///
/// Mirrors the role of `euv::App` - every public engine operation begins
/// with an `Engine::xxx` call, and `Engine` itself holds no state.
impl Engine {
/// Creates a new engine handle bound to the given configuration.
///
/// The handle is uninitialized; call `init_canvas` / `init_webgpu` and
/// `start` on the returned handle to begin the game loop, or use
/// `Engine::run` for the one-line equivalent.
///
/// # Arguments
///
/// - `EngineConfig` - The engine configuration.
///
/// # Returns
///
/// - `EngineHandle` - The new uninitialized engine handle.
pub fn new_handle(config: EngineConfig) -> EngineHandle {
let mut handle: EngineHandle = EngineHandle::new(config, None, None, None, None);
handle.set_tasks(Rc::new(EngineCell::new(TaskRegistry::default())));
handle
}
/// Runs the engine through its complete lifecycle in a single async call.
///
/// Equivalent to calling `Engine::new_handle(config)` followed by
/// `init_canvas` / `init_webgpu` (matching the chosen backend) and
/// `start(handler)`. The returned handle can still be used to stop
/// the loop later via `handle.stop()`.
///
/// # Arguments
///
/// - `EngineConfig` - The engine configuration.
/// - `TickHandlerRc` - The tick handler receiving update and render callbacks.
///
/// # Returns
///
/// - `EngineHandle` - A handle to the running engine. If renderer
/// initialization failed the handle's renderer field will be `None`
/// and `is_running` will be `false`. Initialization errors are not
/// logged inside the engine; callers should call `init_webgpu`
/// directly if they need access to the typed `WebGpuInitError`.
pub async fn run(config: EngineConfig, handler: TickHandlerRc) -> EngineHandle {
let mut handle: EngineHandle = Engine::new_handle(config);
match handle.get_config().get_render().get_backend() {
RenderBackendType::Canvas2D => {
handle.init_canvas();
}
RenderBackendType::WebGpu => {
let _: Result<WebGpuRenderer, WebGpuInitError> = handle.init_webgpu().await;
}
RenderBackendType::WebGl => {
let _: Result<WebGlRenderer, WebGlInitError> = handle.init_webgl();
}
}
handle.start(handler);
handle
}
/// Returns the default engine configuration.
///
/// Uses `RenderConfig::default()` (Canvas 2D backend) and the default
/// scheduler configuration (60 Hz fixed timestep).
///
/// # Returns
///
/// - `EngineConfig` - The default engine configuration.
pub fn default_config() -> EngineConfig {
EngineConfig::default()
}
/// Creates a Canvas 2D renderer directly from a render configuration.
///
/// Use this when you want a `CanvasRenderer` without going through
/// the full `EngineHandle` lifecycle (for example, in a custom render
/// loop that does not use the fixed-timestep scheduler).
///
/// # Arguments
///
/// - `&RenderConfig` - The rendering configuration.
///
/// # Returns
///
/// - `Option<CanvasRenderer>` - The renderer, or `None` if the canvas element was not found.
pub fn canvas_renderer(config: &RenderConfig) -> Option<CanvasRenderer> {
CanvasRenderer::from_selector(
config.get_canvas_selector(),
config.get_width(),
config.get_height(),
)
}
/// Creates a WebGPU renderer directly from a render configuration.
///
/// Async because GPU adapter and device acquisition returns JavaScript
/// Promises that must be awaited. Mirrors `Engine::canvas_renderer` for
/// callers that want the renderer without the full engine handle.
///
/// Failures are surfaced as `WebGpuInitError` so the caller can decide
/// how to react (typically by logging via `Console::error` or by
/// falling back to the Canvas 2D backend).
///
/// # Arguments
///
/// - `&RenderConfig` - The rendering configuration.
///
/// # Returns
///
/// - `Result<WebGpuRenderer, WebGpuInitError>` - The initialized renderer,
/// or a typed error describing the specific failure.
pub async fn webgpu_renderer(config: &RenderConfig) -> Result<WebGpuRenderer, WebGpuInitError> {
WebGpuRenderer::init(config).await
}
/// Creates a WebGL 2 renderer directly from a render configuration.
///
/// Unlike [`Engine::webgpu_renderer`] this is synchronous because WebGL
/// context acquisition is a plain DOM call with no Promises involved.
///
/// # Arguments
///
/// - `&RenderConfig` - The rendering configuration.
///
/// # Returns
///
/// - `Result<WebGlRenderer, WebGlInitError>` - The initialized renderer,
/// or a typed error describing the specific failure.
pub fn webgl_renderer(config: &RenderConfig) -> Result<WebGlRenderer, WebGlInitError> {
WebGlRenderer::init(config)
}
}
/// Implements lifecycle management for `EngineHandle`.
///
/// All getter methods (`get_config`, `get_canvas_renderer`, `get_webgpu_renderer`)
/// are generated by the `Data` derive on the struct definition; this impl
/// only contributes lifecycle and accessor behavior that is not expressible
/// as a plain getter.
impl EngineHandle {
/// Initializes the Canvas 2D rendering backend.
///
/// On success, populates `canvas_renderer` and clears `webgpu_renderer`.
/// On failure, both renderer fields remain `None`.
///
/// # Returns
///
/// - `bool` - `true` if the renderer was created successfully.
pub fn init_canvas(&mut self) -> bool {
let render_config: &RenderConfig = &self.get_config().get_render();
let renderer: Option<CanvasRenderer> = CanvasRenderer::from_selector(
render_config.get_canvas_selector(),
render_config.get_width(),
render_config.get_height(),
);
match renderer {
Some(r) => {
self.set_canvas_renderer(Some(r));
self.set_webgpu_renderer(None);
self.set_webgl_renderer(None);
true
}
None => {
self.set_canvas_renderer(None);
self.set_webgpu_renderer(None);
self.set_webgl_renderer(None);
false
}
}
}
/// Initializes the WebGPU rendering backend.
///
/// On success, populates `webgpu_renderer` and clears `canvas_renderer`.
/// On failure, both renderer fields remain `None` and the typed error is
/// returned so the caller can decide how to surface it (typically via
/// `Console::error`).
///
/// # Returns
///
/// - `Result<WebGpuRenderer, WebGpuInitError>` - The initialized renderer,
/// or a typed error describing the specific failure.
pub async fn init_webgpu(&mut self) -> Result<WebGpuRenderer, WebGpuInitError> {
let render_config: &RenderConfig = &self.get_config().get_render();
let renderer: WebGpuRenderer = WebGpuRenderer::init(render_config).await?;
self.set_webgpu_renderer(Some(renderer.clone()));
self.set_canvas_renderer(None);
self.set_webgl_renderer(None);
Ok(renderer)
}
/// Initializes the WebGL 2 rendering backend.
///
/// On success, populates `webgl_renderer` and clears the other renderer
/// fields. On failure, all renderer fields remain `None` and the typed
/// error is returned so the caller can decide how to surface it.
///
/// # Returns
///
/// - `Result<WebGlRenderer, WebGlInitError>` - The initialized renderer,
/// or a typed error describing the specific failure.
pub fn init_webgl(&mut self) -> Result<WebGlRenderer, WebGlInitError> {
let render_config: &RenderConfig = &self.get_config().get_render();
let renderer: WebGlRenderer = WebGlRenderer::init(render_config)?;
self.set_webgl_renderer(Some(renderer.clone()));
self.set_canvas_renderer(None);
self.set_webgpu_renderer(None);
Ok(renderer)
}
/// Attaches DOM input listeners and stores the shared input state.
///
/// Resolves the canvas element from the render configuration's
/// `canvas_selector` and routes DOM events into a shared [`InputState`]:
/// keyboard events bind to `window`, mouse and touch events bind to the
/// canvas. This works for both the Canvas 2D and WebGPU backends because
/// either way the same `<canvas>` element receives the pointer events.
///
/// Returns `None` (and stores nothing) if the canvas selector does not
/// resolve at call time - for example when called before the canvas
/// element is mounted into the DOM.
///
/// The registered listeners stay alive for the lifetime of the document
/// (mount-only convention; there is no matching `unregister_input`).
///
/// # Returns
///
/// - `Option<InputStateCell>` - The shared input state cell, or `None`
/// if the canvas element was not found.
pub fn register_input(&mut self) -> Option<InputStateCell> {
if let Some(existing) = self.try_get_input_cell() {
return Some(existing.clone());
}
let window_value: Window = window()?;
let document_value: Document = window_value.document()?;
let render_config: &RenderConfig = &self.get_config().get_render();
let canvas_selector: String = render_config.get_canvas_selector();
let element: Element = document_value
.query_selector(canvas_selector.as_ref())
.ok()
.flatten()?;
let target: EventTarget = element.into();
let cell: InputStateCell = Input::attach(
Rc::new(EngineCell::new(InputState::default())),
&window_value,
&target,
);
self.set_input_cell(Some(cell.clone()));
Some(cell)
}
/// Starts the game loop with the given tick handler.
///
/// The scheduler configuration from `EngineConfig` controls the fixed
/// timestep and maximum frame time. The scheduler handle is stored
/// internally and can be stopped via `stop`. If `register_input` has
/// attached DOM listeners, the shared input state is closed out at the
/// end of every frame so edge-triggered queries (`keys_pressed`,
/// `keys_released`, `mouse_buttons_pressed`, `touch_started`) only
/// report the frame they occurred in. Tasks registered through
/// [`EngineHandle::register_task`] are advanced once per fixed step,
/// immediately after the handler's `on_update` callback.
///
/// # Arguments
///
/// - `TickHandlerRc` - The tick handler receiving update and render callbacks.
pub fn start(&mut self, handler: TickHandlerRc) {
let scheduler_config: SchedulerConfig = self.get_config().get_scheduler();
let input_cell: Option<&InputStateCell> = self.try_get_input_cell().as_ref();
let tasks: &TaskRegistryRc = self.get_tasks();
self.set_scheduler_handle(Some(SchedulerHandle::start(
scheduler_config,
handler,
Some(tasks),
input_cell,
)));
}
/// Returns the shared task registry, for direct inspection or
/// registration outside [`EngineHandle::register_task`].
///
/// # Returns
///
/// - `&TaskRegistryRc` - The engine's task registry.
pub fn tasks(&self) -> &TaskRegistryRc {
self.get_tasks()
}
/// Registers a task to be advanced on every fixed step.
///
/// The task is driven by [`SchedulerState::tick`] immediately after the
/// handler's `on_update` callback, with the configured
/// [`SchedulerConfig::get_fixed_timestep`] delta. This is the entry
/// point that gives `Timer`, `Tween`, `ParticleEmitter`, `Entity`,
/// `Animator`, `SceneManager`, and the physics worlds a heartbeat — all
/// of them implement [`Updatable`] but none were driven before the
/// registry existed.
///
/// Registration order is preserved: tasks are updated in the order they
/// were registered.
///
/// # Arguments
///
/// - `T` - The updater to register. Must implement [`Updatable`].
///
/// # Returns
///
/// - `TaskHandle` - A handle used to unregister the task.
pub fn register_task<T>(&self, task: T) -> TaskHandle
where
T: Updatable + 'static,
{
self.get_tasks().get_mut().register(task)
}
/// Removes a previously registered task.
///
/// # Arguments
///
/// - `&TaskHandle` - The handle returned by [`EngineHandle::register_task`].
///
/// # Returns
///
/// - `bool` - `true` if a task was removed, `false` if the handle did
/// not match any registered task.
pub fn unregister_task(&self, handle: &TaskHandle) -> bool {
self.get_tasks().get_mut().unregister(handle)
}
/// Creates the engine's asset loader and drives it from the game loop.
///
/// The loader implements [`Updatable`], so it is registered with the
/// scheduler's [`TaskRegistry`] rather than polled by hand. On every
/// fixed step it releases the `onload` / `onerror` callbacks of loads
/// that have already settled — a `wasm_bindgen::Closure` cannot be
/// dropped from inside the callback that is running, so this engine
/// update is the only safe drop point. Without this the loader would
/// pin two closures per asset forever and `is_all_loaded` would never
/// observe the decrement of the pending count.
///
/// Calling this more than once returns the already-created loader rather
/// than replacing it, so pending loads are not orphaned.
///
/// # Returns
///
/// - `AssetLoader` - The engine's asset loader.
pub fn register_assets(&mut self) -> AssetLoader {
if let Some(existing) = self.try_get_asset_loader() {
return existing.clone();
}
let loader: AssetLoader = AssetLoader::default();
self.set_asset_loader(Some(loader.clone()));
let _ = self.register_task(loader.clone());
loader
}
/// Returns the engine's asset loader, if one has been created.
///
/// # Returns
///
/// - `Option<AssetLoader>` - The loader, or `None` when
/// [`EngineHandle::register_assets`] has not been called.
pub fn asset_loader(&self) -> Option<AssetLoader> {
self.try_get_asset_loader().as_ref().cloned()
}
/// Stops the game loop and cancels any pending animation frame request.
pub fn stop(&self) {
if let Some(handle) = self.try_get_scheduler_handle().as_ref() {
handle.stop();
}
}
/// Returns whether the game loop is currently running.
///
/// # Returns
///
/// - `bool` - `true` if the scheduler is running.
pub fn is_running(&self) -> bool {
self.try_get_scheduler_handle()
.as_ref()
.is_some_and(|handle: &SchedulerHandle| handle.is_running())
}
}