khora_sdk/winit_adapters.rs
1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Winit-specific integration for the Khora Engine.
16//!
17//! This module provides the `run_winit` function that bridges winit's
18//! `ApplicationHandler` with the winit-agnostic `EngineCore`.
19
20use std::any::Any;
21use std::sync::{Arc, Mutex};
22
23use anyhow::Result;
24use khora_core::platform::KhoraWindow;
25use khora_core::renderer::api::core::FrameContext;
26use khora_core::renderer::traits::RenderSystem;
27use khora_infra::platform::window::WinitWindow;
28use winit::application::ApplicationHandler;
29use winit::event::WindowEvent;
30use winit::event_loop::{ActiveEventLoop, EventLoop};
31use winit::window::WindowId;
32
33use crate::engine::{EngineCore, PRIMARY_VIEWPORT};
34use crate::traits::{EngineApp, WindowProvider};
35use crate::{InputEvent, WindowConfig};
36
37// ─────────────────────────────────────────────────────────────────────
38// WinitWindowProvider — concrete window provider
39// ─────────────────────────────────────────────────────────────────────
40
41/// A window provider backed by winit.
42pub struct WinitWindowProvider {
43 window: WinitWindow,
44}
45
46impl WindowProvider for WinitWindowProvider {
47 fn create(native_loop: &dyn Any, config: &WindowConfig) -> Self
48 where
49 Self: Sized,
50 {
51 let event_loop = native_loop
52 .downcast_ref::<ActiveEventLoop>()
53 .expect("WindowProvider::create called with wrong native_loop type");
54
55 let mut builder = khora_infra::platform::window::WinitWindowBuilder::new()
56 .with_title(&config.title)
57 .with_dimensions(config.width, config.height);
58
59 if let Some(icon) = &config.icon {
60 builder = builder.with_icon_rgba(icon.rgba.clone(), icon.width, icon.height);
61 }
62
63 let window = builder.build(event_loop).expect("Failed to create window");
64 Self { window }
65 }
66
67 fn request_redraw(&self) {
68 self.window.request_redraw();
69 }
70
71 fn inner_size(&self) -> (u32, u32) {
72 self.window.inner_size()
73 }
74
75 fn scale_factor(&self) -> f64 {
76 self.window.scale_factor()
77 }
78
79 fn as_khora_window(&self) -> &dyn KhoraWindow {
80 &self.window
81 }
82
83 fn clone_raw_window_arc(&self) -> Arc<dyn Any + Send + Sync> {
84 self.window.clone_winit_arc()
85 }
86
87 fn translate_event(&self, raw_event: &dyn Any) -> Option<InputEvent> {
88 if let Some(winit_event) = raw_event.downcast_ref::<WindowEvent>() {
89 khora_infra::platform::input::translate_winit_input(winit_event)
90 } else {
91 None
92 }
93 }
94}
95
96// ─────────────────────────────────────────────────────────────────────
97// WinitAppRunner — bridges winit ApplicationHandler with EngineCore
98// ─────────────────────────────────────────────────────────────────────
99
100/// One-shot bootstrap closure used to wire engine backends, services, and
101/// resources into the [`khora_core::Runtime`] at window-creation time.
102type BootstrapFn = Box<dyn FnOnce(&dyn KhoraWindow, &mut khora_core::Runtime, &dyn Any) + Send>;
103
104/// Winit-specific application runner.
105pub struct WinitAppRunner<W: WindowProvider, A: EngineApp> {
106 window: Option<W>,
107 engine: EngineCore<A>,
108 renderer: Option<Arc<Mutex<Box<dyn RenderSystem>>>>,
109 bootstrap: Option<BootstrapFn>,
110 tokio_runtime: Option<tokio::runtime::Runtime>,
111 /// Per-frame context, recreated each frame.
112 frame_context: Option<Arc<FrameContext>>,
113}
114
115impl<W: WindowProvider, A: EngineApp> WinitAppRunner<W, A> {
116 /// Creates a new winit app runner with the given bootstrap closure.
117 pub fn new(
118 bootstrap: impl FnOnce(&dyn KhoraWindow, &mut khora_core::Runtime, &dyn Any) + Send + 'static,
119 ) -> Self {
120 Self {
121 window: None,
122 engine: EngineCore::new(),
123 renderer: None,
124 bootstrap: Some(Box::new(bootstrap)),
125 tokio_runtime: None,
126 frame_context: None,
127 }
128 }
129
130 /// Runs one frame, interleaving the staged engine methods with the
131 /// `EngineApp` lifecycle hooks. Sandbox-style apps (no overrides) get
132 /// the same behavior as the legacy monolithic `tick`.
133 fn run_frame(&mut self) {
134 // The engine-level Runtime is immutable per-frame. The
135 // `Arc<FrameContext>` registered at engine init has interior
136 // mutability (its blackboard is a `Mutex<AnyMap>`), so each
137 // frame's writes (`ColorTarget`, `DepthTarget`, …) overwrite the
138 // previous frame's by replacing entries of the same type.
139 let runtime_arc = Arc::clone(self.engine.runtime());
140 if self.frame_context.is_none() {
141 self.frame_context = runtime_arc.resources.get::<Arc<FrameContext>>().cloned();
142 }
143
144 // Stage 1: drain inputs (also marks simulation started + ticks telemetry).
145 let inputs = self.engine.drain_inputs();
146
147 // Hook: before_frame — overlay.begin_frame + shell.show_frame.
148 if let Some(window) = self.window.as_ref() {
149 let kwin = window.as_khora_window();
150 self.engine.with_app_and_world(|app, world| {
151 app.before_frame(world, &runtime_arc, kwin);
152 });
153 }
154
155 // Stage 2: app.update + maintenance + extractions.
156 self.engine.run_app_update(&inputs);
157
158 // Stage 3: begin_frame on the renderer.
159 let presents = self.engine.begin_render_frame(&runtime_arc);
160
161 // Hook: before_agents — render offscreen viewport, set_render_to_viewport(true).
162 self.engine.with_app_and_world(|app, world| {
163 app.before_agents(world, &runtime_arc);
164 });
165
166 // Stage 4: scheduler dispatch.
167 self.engine.run_scheduler(&runtime_arc);
168
169 // Stage 5a: submit recorded agent passes. Done BEFORE `after_agents`
170 // so editor overlay rendering (in `after_agents`) paints on top of
171 // the 3D scene rather than getting overwritten by the agent submit.
172 self.engine.submit_passes(presents);
173
174 // Hook: after_agents — gizmos, set false, render_overlay.
175 self.engine.with_app_and_world(|app, world| {
176 app.after_agents(world, &runtime_arc);
177 });
178
179 // Stage 5b: present.
180 self.engine.present_frame(presents);
181
182 // Stage 6: end-of-tick Maintenance (writebacks, ECS compaction,
183 // deferred cleanup). Without this, Maintenance-phase DataSystems
184 // (`audio_playback_writeback`, `physics_world_writeback`,
185 // `ecs_maintenance`) never run in the production winit flow.
186 self.engine.run_maintenance();
187
188 // Wait for hot-path tasks before returning so the next frame sees a
189 // settled GPU state.
190 if let Some(rt) = &self.tokio_runtime {
191 if let Some(fctx) = &self.frame_context {
192 rt.block_on(fctx.wait_for_all());
193 }
194 }
195 }
196}
197
198impl<W: WindowProvider, A: EngineApp> ApplicationHandler for WinitAppRunner<W, A> {
199 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
200 if self.window.is_some() {
201 return;
202 }
203
204 log::info!("Khora Engine: Initializing...");
205
206 let window_config = A::window_config();
207 let window = W::create(event_loop as &dyn Any, &window_config);
208
209 // Create the tokio runtime for hot-path async tasks BEFORE
210 // bootstrap so we can register an `Arc<FrameContext>` resource
211 // into the runtime alongside the rest of the engine state.
212 match tokio::runtime::Builder::new_multi_thread()
213 .enable_all()
214 .thread_name("khora-hotpath")
215 .build()
216 {
217 Ok(rt) => {
218 log::info!("WinitAppRunner: Tokio runtime created for hot-path tasks");
219 self.tokio_runtime = Some(rt);
220 }
221 Err(e) => {
222 log::warn!("WinitAppRunner: Failed to create tokio runtime: {}", e);
223 }
224 }
225
226 // Build the runtime bundle and pre-populate it with windowing-tier
227 // entries before handing off to the bootstrap closure.
228 let mut runtime = khora_core::Runtime::new();
229
230 // PRIMARY_VIEWPORT — engine-wide handle for the primary 3D viewport.
231 runtime.resources.insert(PRIMARY_VIEWPORT);
232
233 // Per-engine FrameContext (interior mutability: blackboard reused
234 // across frames).
235 if let Some(rt) = &self.tokio_runtime {
236 let fctx = Arc::new(FrameContext::new(rt.handle().clone()));
237 runtime.resources.insert(Arc::clone(&fctx));
238 self.frame_context = Some(fctx);
239 }
240
241 // Insert a long-lived clone of the raw window handle so editor hooks
242 // (e.g., overlay `begin_frame`) can retrieve `Arc<winit::window::Window>`
243 // from resources and pass it to the overlay each frame.
244 let raw_window_arc = window.clone_raw_window_arc();
245 if let Ok(winit_arc) = raw_window_arc.downcast::<winit::window::Window>() {
246 runtime.resources.insert(winit_arc);
247 }
248
249 // Call bootstrap closure — pass the active event loop opaquely so the
250 // app may construct windowing-aware resources (e.g., an egui overlay
251 // backed by `egui_winit::State`) without the SDK needing to know.
252 let bootstrap = self.bootstrap.take().expect("bootstrap not set");
253 bootstrap(
254 window.as_khora_window(),
255 &mut runtime,
256 event_loop as &dyn Any,
257 );
258
259 // Bootstrap the engine, transferring ownership of the runtime.
260 // bootstrap() inserts built-in entries (GpuCache, etc.), wraps in
261 // Arc, and stores the final runtime in self.engine.runtime.
262 let app = A::new();
263 self.engine.bootstrap(app, runtime);
264
265 // Cache renderer for resize handling.
266 if let Some(rs) = self
267 .engine
268 .runtime()
269 .backends
270 .get::<Arc<Mutex<Box<dyn RenderSystem>>>>()
271 {
272 self.renderer = Some(rs.clone());
273 }
274
275 self.window = Some(window);
276 }
277
278 fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
279 // Give the app a chance to intercept raw events (e.g., forward to an
280 // egui overlay). If consumed, do not forward to game input nor act on
281 // the event further (close / resize / redraw still proceed below).
282 let consumed_by_app =
283 if let (Some(app), Some(window)) = (self.engine.app_mut(), &self.window) {
284 app.intercept_window_event(&event as &dyn Any, window.as_khora_window())
285 } else {
286 false
287 };
288
289 match event {
290 WindowEvent::CloseRequested => {
291 log::info!("Shutdown requested");
292 event_loop.exit();
293 }
294 WindowEvent::Resized(size) => {
295 if let Some(renderer) = self.renderer.as_ref() {
296 log::info!("Window resized: {}x{}", size.width, size.height);
297 if let Ok(mut r) = renderer.lock() {
298 r.resize(size.width, size.height);
299 }
300 }
301 }
302 WindowEvent::RedrawRequested => {
303 self.run_frame();
304 }
305 _ => {
306 if consumed_by_app {
307 return;
308 }
309 if let Some(window) = &self.window {
310 if let Some(input_event) = window.translate_event(&event) {
311 self.engine.feed_input(input_event);
312 }
313 }
314 }
315 }
316 }
317
318 fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
319 if let Some(window) = &self.window {
320 window.request_redraw();
321 }
322 }
323}
324
325impl<W: WindowProvider, A: EngineApp> Drop for WinitAppRunner<W, A> {
326 fn drop(&mut self) {
327 self.engine.shutdown();
328 }
329}
330
331// ─────────────────────────────────────────────────────────────────────
332// run_winit — entry point for winit-based applications
333// ─────────────────────────────────────────────────────────────────────
334
335/// Runs the Khora Engine with a winit event loop.
336///
337/// # Arguments
338///
339/// * `bootstrap` — A closure called once during initialization. Receives:
340/// - `&dyn KhoraWindow` — the created window for renderer initialization
341/// - `&mut ServiceRegistry` — the service registry to populate with
342/// renderer, text renderer, layout system, monitors, etc.
343/// - `&dyn Any` — the active winit event loop, opaque so the SDK does not
344/// leak the winit type. Apps that need it (e.g., egui) downcast to
345/// `&winit::event_loop::ActiveEventLoop`.
346///
347/// This call blocks until the window is closed, so it is the last thing `main`
348/// does.
349///
350/// # Errors
351///
352/// Returns an error if the winit event loop cannot be created or fails while
353/// running (e.g. the platform refuses to open a window).
354///
355/// # Examples
356///
357/// ```rust,no_run
358/// use khora_sdk::prelude::*;
359/// use khora_sdk::{
360/// run_winit, AgentProvider, DccService, EngineApp, GameWorld, PhaseProvider,
361/// Runtime, WindowConfig,
362/// };
363/// use khora_sdk::winit_adapters::WinitWindowProvider;
364///
365/// struct MyGame;
366///
367/// impl EngineApp for MyGame {
368/// fn window_config() -> WindowConfig { WindowConfig::default() }
369/// fn new() -> Self { MyGame }
370/// fn setup(&mut self, _world: &mut GameWorld, _runtime: &Runtime) {}
371/// fn update(&mut self, _world: &mut GameWorld, _inputs: &[InputEvent]) {}
372/// }
373/// impl AgentProvider for MyGame {
374/// fn register_agents(&self, _dcc: &DccService, _runtime: &mut Runtime) {}
375/// }
376/// impl PhaseProvider for MyGame {}
377///
378/// fn main() -> anyhow::Result<()> {
379/// run_winit::<WinitWindowProvider, MyGame>(|_window, runtime, _event_loop| {
380/// // Insert your renderer, physics, audio, and UI backends into
381/// // `runtime.backends` / `runtime.resources` here. See the `sandbox`
382/// // example for a full backend wiring.
383/// let _ = runtime;
384/// })
385/// }
386/// ```
387pub fn run_winit<W: WindowProvider, A: EngineApp>(
388 bootstrap: impl FnOnce(&dyn KhoraWindow, &mut khora_core::Runtime, &dyn Any) + Send + 'static,
389) -> Result<()> {
390 log::info!("Khora Engine: Starting...");
391
392 let event_loop = EventLoop::new()?;
393 let mut runner = WinitAppRunner::<W, A>::new(bootstrap);
394 event_loop.run_app(&mut runner)?;
395 Ok(())
396}