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