khora_sdk/
lib.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//! The public-facing Software Development Kit (SDK) for the Khora Engine.
16//! This crate provides a simple and stable API for game developers to create
17//! and run applications using Khora.
18
19#![warn(missing_docs)]
20
21use anyhow::Result;
22use khora_core::platform::window::KhoraWindow;
23use khora_core::renderer::{RenderObject, RenderSettings, RenderSystem};
24use khora_core::telemetry::MonitoredResourceType;
25use khora_infra::platform::input::translate_winit_input;
26use khora_infra::platform::window::{WinitWindow, WinitWindowBuilder};
27use khora_infra::telemetry::memory_monitor::MemoryMonitor;
28use khora_infra::{GpuMonitor, WgpuRenderSystem};
29use khora_telemetry::TelemetryService;
30use std::sync::Arc;
31use std::time::Duration;
32use winit::application::ApplicationHandler;
33use winit::event::WindowEvent;
34use winit::event_loop::{ActiveEventLoop, EventLoop};
35use winit::window::WindowId;
36
37/// Publicly re-exported types and traits for ease of use.
38pub mod prelude {
39    pub use khora_core::asset::{AssetMetadata, AssetSource, AssetUUID};
40    pub use khora_core::renderer::{
41        BufferDescriptor, BufferId, BufferUsage, ColorTargetStateDescriptor, ColorWrites,
42        IndexFormat, MultisampleStateDescriptor, PipelineLayoutDescriptor, RenderObject,
43        RenderPipelineDescriptor, RenderPipelineId, SampleCount, ShaderModuleDescriptor,
44        ShaderModuleId, ShaderSourceData, ShaderStage, VertexAttributeDescriptor,
45        VertexBufferLayoutDescriptor, VertexFormat, VertexStepMode,
46    };
47    pub use khora_data::allocators::SaaTrackingAllocator;
48
49    /// Built-in engine shaders.
50    pub mod shaders {
51        pub use khora_lanes::render_lane::shaders::*;
52    }
53}
54
55/// Engine context providing access to various subsystems.
56pub struct EngineContext {
57    /// The graphics device used for rendering.
58    pub graphics_device: Arc<dyn khora_core::renderer::GraphicsDevice>,
59}
60
61/// Application trait for user-defined applications.
62pub trait Application: Sized + 'static {
63    /// Called once at the beginning of the application to create the initial state.
64    fn new(context: EngineContext) -> Self;
65
66    /// Called every frame for game logic updates.
67    fn update(&mut self);
68
69    /// Called every frame to handle rendering.
70    fn render(&mut self) -> Vec<RenderObject>;
71}
72
73/// The internal state of the running engine, managed by the winit event loop.
74/// It now holds the user's application state (`app: A`).
75struct EngineState<A: Application> {
76    app: Option<A>, // The user's application logic and data.
77    window: Option<WinitWindow>,
78    renderer: Option<Box<dyn RenderSystem>>,
79    telemetry: Option<TelemetryService>,
80    render_settings: RenderSettings,
81}
82
83impl<A: Application> EngineState<A> {
84    /// Logs a summary of all registered telemetry monitors to the console.
85    fn log_telemetry_summary(&self) {
86        if let Some(telemetry) = &self.telemetry {
87            log::info!("--- Telemetry Summary ---");
88            let monitors = telemetry.monitor_registry().get_all_monitors();
89
90            if monitors.is_empty() {
91                log::info!("  No monitors registered.");
92            }
93
94            for monitor in monitors {
95                let report = monitor.get_usage_report();
96                match monitor.resource_type() {
97                    MonitoredResourceType::SystemRam => {
98                        let current_mb = report.current_bytes as f64 / (1024.0 * 1024.0);
99                        let peak_mb = report.peak_bytes.unwrap_or(0) as f64 / (1024.0 * 1024.0);
100                        log::info!(
101                            "  RAM Usage: {:.2} MB (Peak: {:.2} MB)",
102                            current_mb,
103                            peak_mb
104                        );
105                    }
106                    MonitoredResourceType::Vram => {
107                        let current_mb = report.current_bytes as f64 / (1024.0 * 1024.0);
108                        let peak_mb = report.peak_bytes.unwrap_or(0) as f64 / (1024.0 * 1024.0);
109                        log::info!(
110                            "  VRAM Usage: {:.2} MB (Peak: {:.2} MB)",
111                            current_mb,
112                            peak_mb
113                        );
114                    }
115                    MonitoredResourceType::Gpu => {
116                        // Downcast to the concrete GpuMonitor type to access detailed reports.
117                        if let Some(gpu_monitor) = monitor.as_any().downcast_ref::<GpuMonitor>() {
118                            if let Some(gpu_report) = gpu_monitor.get_gpu_report() {
119                                log::info!(
120                                    "  GPU Time: {:.3} ms (Main Pass: {:.3} ms)",
121                                    gpu_report.frame_total_duration_us().unwrap_or(0) as f32
122                                        / 1000.0,
123                                    gpu_report.main_pass_duration_us().unwrap_or(0) as f32 / 1000.0
124                                );
125                            }
126                        }
127                    }
128                }
129            }
130            log::info!("-------------------------");
131        }
132    }
133}
134
135/// Implementing `Drop` is the idiomatic Rust way to handle cleanup.
136/// When `EngineState` goes out of scope (after the event loop exits), this `drop`
137/// function will be called automatically, ensuring a controlled shutdown.
138impl<A: Application> Drop for EngineState<A> {
139    fn drop(&mut self) {
140        log::info!("EngineState is being dropped. Performing controlled shutdown...");
141
142        if let Some(mut renderer) = self.renderer.take() {
143            renderer.shutdown();
144        }
145
146        log::info!("Engine systems shutdown complete.");
147    }
148}
149
150impl<A: Application> ApplicationHandler for EngineState<A> {
151    /// Called when the event loop is ready to start processing events.
152    /// This is the ideal place to initialize systems that require a window.
153    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
154        if self.window.is_some() {
155            return; // Avoid re-initializing if the app is resumed multiple times.
156        }
157
158        log::info!("Application resumed. Initializing window and engine systems...");
159
160        // 1. Create the window using the builder from khora-infra.
161        let window = WinitWindowBuilder::new().build(event_loop).unwrap();
162
163        // 2. Create the renderer and get its associated resource monitors.
164        let mut renderer: Box<dyn RenderSystem> = Box::new(WgpuRenderSystem::new());
165        let renderer_monitors = renderer.init(&window).unwrap();
166
167        // 3. Create the telemetry service.
168        let telemetry = TelemetryService::new(Duration::from_secs(1));
169
170        // 4. Register all available default monitors with the telemetry service.
171        log::info!("Registering default resource monitors...");
172
173        // Register the monitors that were created and returned by the renderer.
174        for monitor in renderer_monitors {
175            telemetry.monitor_registry().register(monitor);
176        }
177
178        // Register other independent monitors.
179        let memory_monitor = Arc::new(MemoryMonitor::new("System_RAM".to_string()));
180        telemetry.monitor_registry().register(memory_monitor);
181
182        // 5. Create the application instance.
183        let context = EngineContext {
184            graphics_device: renderer.graphics_device(),
185        };
186        self.app = Some(A::new(context));
187
188        // 6. Store the initialized systems in our application state.
189        self.window = Some(window);
190        self.renderer = Some(renderer);
191        self.telemetry = Some(telemetry);
192        self.render_settings = RenderSettings::default();
193    }
194
195    fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
196        if let Some(app_window) = self.window.as_ref() {
197            use std::collections::hash_map::DefaultHasher;
198            use std::hash::{Hash, Hasher};
199
200            let mut hasher = DefaultHasher::new();
201            id.hash(&mut hasher);
202            let event_window_hash = hasher.finish();
203
204            if app_window.id() == event_window_hash {
205                match event {
206                    WindowEvent::CloseRequested => {
207                        log::info!("Shutdown requested, exiting event loop...");
208                        event_loop.exit();
209                    }
210                    WindowEvent::Resized(size) => {
211                        if let Some(renderer) = self.renderer.as_mut() {
212                            log::info!("Window resized to: {}x{}", size.width, size.height);
213                            renderer.resize(size.width, size.height);
214                        }
215                    }
216                    WindowEvent::RedrawRequested => {
217                        if let (Some(renderer), Some(telemetry)) =
218                            (self.renderer.as_mut(), self.telemetry.as_mut())
219                        {
220                            // Update "active" monitors like the memory monitor.
221                            let should_log_summary = telemetry.tick();
222
223                            // Call the user's application update and render methods.
224                            let app = self.app.as_mut().unwrap();
225
226                            app.update();
227
228                            let render_objects = app.render();
229
230                            // The renderer will update its own internal monitors (like GpuMonitor) during this call.
231                            match renderer.render(
232                                &render_objects,
233                                &Default::default(),
234                                &self.render_settings,
235                            ) {
236                                Ok(stats) => {
237                                    log::trace!("Frame {} rendered.", stats.frame_number);
238                                }
239                                Err(e) => log::error!("Rendering error: {}", e),
240                            }
241
242                            if should_log_summary {
243                                self.log_telemetry_summary();
244                            }
245                        }
246                    }
247                    _ => {
248                        // Translate winit events into our engine's event type for game logic to consume.
249                        if let Some(input_event) = translate_winit_input(&event) {
250                            log::debug!("Input event: {:?}", input_event);
251                        }
252                    }
253                }
254            }
255        }
256    }
257
258    /// Called when the event loop has processed all pending events and is about to wait.
259    /// This is the ideal place to request a redraw for continuous rendering (i.e., a game loop).
260    fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
261        if let Some(window) = &self.window {
262            window.request_redraw();
263        }
264    }
265}
266
267/// The public entry point for the Khora Engine.
268pub struct Engine;
269
270impl Engine {
271    /// Creates a new engine instance and runs it.
272    ///
273    /// This is the primary function for a game developer to call. It will create a window,
274    /// initialize the rendering and other core systems, and start the main event loop,
275    /// blocking the current thread until the application is closed.
276    pub fn run<A: Application>() -> Result<()> {
277        log::info!("Khora Engine SDK: Starting...");
278        let event_loop = EventLoop::new()?;
279
280        // The initial state is empty; it will be populated in the `resumed` event.
281        let mut app_state = EngineState::<A> {
282            app: None,
283            window: None,
284            renderer: None,
285            telemetry: None,
286            render_settings: RenderSettings::default(),
287        };
288
289        event_loop.run_app(&mut app_state)?;
290
291        Ok(())
292    }
293}