Skip to main content

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}