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//!
17//! This is the **only** crate that should be used by game developers.
18//! All internal crates (khora-agents, khora-control, etc.) are implementation details.
19
20#![warn(missing_docs)]
21
22mod game_world;
23mod vessel;
24
25pub use game_world::GameWorld;
26pub use vessel::{spawn_cube_at, spawn_plane, spawn_sphere, Vessel};
27
28use anyhow::Result;
29use khora_control::{DccConfig, DccService};
30use khora_core::platform::KhoraWindow;
31use khora_core::renderer::api::core::RenderSettings;
32use khora_core::renderer::api::scene::RenderObject;
33use khora_core::renderer::traits::RenderSystem;
34use khora_core::telemetry::MonitoredResourceType;
35use khora_core::ServiceRegistry;
36use khora_infra::platform::input::translate_winit_input;
37use khora_infra::platform::window::{WinitWindow, WinitWindowBuilder};
38use khora_infra::telemetry::memory_monitor::MemoryMonitor;
39use khora_infra::{GpuMonitor, WgpuRenderSystem};
40use khora_telemetry::TelemetryService;
41use std::collections::VecDeque;
42use std::sync::atomic::{AtomicBool, Ordering};
43use std::sync::{Arc, Mutex};
44use std::time::Duration;
45use winit::application::ApplicationHandler;
46use winit::event::WindowEvent;
47use winit::event_loop::{ActiveEventLoop, EventLoop};
48use winit::window::WindowId;
49
50pub mod prelude {
51    //! Common imports for convenience.
52    pub use khora_core::asset::{AssetHandle, AssetMetadata, AssetSource, AssetUUID};
53    pub use khora_core::renderer::api::{
54        core::{ShaderModuleDescriptor, ShaderModuleId, ShaderSourceData},
55        pipeline::state::{DepthBiasState, StencilFaceState},
56        pipeline::{
57            ColorTargetStateDescriptor, ColorWrites, CompareFunction, DepthStencilStateDescriptor,
58            MultisampleStateDescriptor, PipelineLayoutDescriptor, RenderPipelineDescriptor,
59            RenderPipelineId, VertexAttributeDescriptor, VertexBufferLayoutDescriptor,
60            VertexFormat, VertexStepMode,
61        },
62        resource::{BufferDescriptor, BufferId, BufferUsage},
63        scene::RenderObject,
64        util::{IndexFormat, SampleCount, ShaderStageFlags as ShaderStage, TextureFormat},
65    };
66    pub use khora_core::EngineContext;
67    pub use khora_data::allocators::SaaTrackingAllocator;
68    pub use khora_data::ecs::HandleComponent;
69    pub use khora_infra::platform::input::MouseButton;
70
71    pub mod ecs {
72        //! ECS types exposed through the SDK.
73        pub use khora_core::ecs::entity::EntityId;
74        pub use khora_core::renderer::light::{DirectionalLight, LightType, PointLight, SpotLight};
75        pub use khora_data::ecs::{
76            Camera, Component, ComponentBundle, GlobalTransform, Light, MaterialComponent,
77            Transform,
78        };
79    }
80
81    pub mod materials {
82        //! Built-in material types.
83        pub use khora_core::asset::{
84            EmissiveMaterial, StandardMaterial, UnlitMaterial, WireframeMaterial,
85        };
86    }
87
88    pub mod shaders {
89        //! Built-in engine shaders.
90        pub use khora_lanes::render_lane::shaders::*;
91    }
92
93    pub mod math {
94        //! Math types and utilities.
95        pub use khora_core::math::*;
96    }
97}
98
99pub use khora_core::EngineContext;
100pub use khora_infra::platform::input::InputEvent;
101
102/// Application trait for user-defined game logic.
103///
104/// The engine manages the internal state. Users interact through
105/// `&mut GameWorld` - no direct access to internal engine types.
106pub trait Application: Sized + 'static {
107    /// Called once at initialization with the graphics context.
108    fn new(context: EngineContext) -> Self;
109
110    /// Called once after construction for scene setup.
111    fn setup(&mut self, _world: &mut GameWorld) {}
112
113    /// Called every frame for game logic.
114    fn update(&mut self, _world: &mut GameWorld, _inputs: &[InputEvent]) {}
115
116    /// Called every frame to produce render objects.
117    fn render(&mut self) -> Vec<RenderObject> {
118        Vec::new()
119    }
120}
121
122/// Internal engine state.
123struct EngineState<A: Application> {
124    app: Option<A>,
125    game_world: Option<GameWorld>,
126    window: Option<WinitWindow>,
127    renderer: Option<Arc<Mutex<Box<dyn RenderSystem>>>>,
128    /// Cached graphics device (extracted from renderer at init).
129    graphics_device: Option<Arc<dyn khora_core::renderer::GraphicsDevice>>,
130    telemetry: Option<TelemetryService>,
131    dcc: Option<DccService>,
132    render_settings: RenderSettings,
133    simulation_started: bool,
134    running: Arc<AtomicBool>,
135    /// Accumulated input events for the current frame.
136    input_events: VecDeque<InputEvent>,
137}
138
139impl<A: Application> EngineState<A> {
140    fn log_telemetry_summary(&self) {
141        if let Some(telemetry) = &self.telemetry {
142            log::info!("--- Telemetry Summary ---");
143            for monitor in telemetry.monitor_registry().get_all_monitors() {
144                let report = monitor.get_usage_report();
145                match monitor.resource_type() {
146                    MonitoredResourceType::SystemRam => {
147                        let current_mb = report.current_bytes as f64 / (1024.0 * 1024.0);
148                        let peak_mb = report.peak_bytes.unwrap_or(0) as f64 / (1024.0 * 1024.0);
149                        log::info!("  RAM: {:.2} MB (Peak: {:.2} MB)", current_mb, peak_mb);
150                    }
151                    MonitoredResourceType::Vram => {
152                        let current_mb = report.current_bytes as f64 / (1024.0 * 1024.0);
153                        let peak_mb = report.peak_bytes.unwrap_or(0) as f64 / (1024.0 * 1024.0);
154                        log::info!("  VRAM: {:.2} MB (Peak: {:.2} MB)", current_mb, peak_mb);
155                    }
156                    MonitoredResourceType::Gpu => {
157                        if let Some(gpu_monitor) = monitor.as_any().downcast_ref::<GpuMonitor>() {
158                            if let Some(gpu_report) = gpu_monitor.get_gpu_report() {
159                                log::info!(
160                                    "  GPU: {:.3} ms (Frame: {})",
161                                    gpu_report.frame_total_duration_us().unwrap_or(0) as f32
162                                        / 1000.0,
163                                    gpu_report.frame_number
164                                );
165                            }
166                        }
167                    }
168                    MonitoredResourceType::Hardware => {
169                        log::info!("  Hardware: Active");
170                    }
171                }
172            }
173            log::info!("-------------------------");
174        }
175    }
176}
177
178impl<A: Application> Drop for EngineState<A> {
179    fn drop(&mut self) {
180        log::info!("EngineState: Shutting down...");
181        self.running.store(false, Ordering::SeqCst);
182        if let Some(renderer) = self.renderer.take() {
183            if let Ok(mut r) = renderer.lock() {
184                r.shutdown();
185            }
186        }
187        log::info!("Engine shutdown complete.");
188    }
189}
190
191impl<A: Application> ApplicationHandler for EngineState<A> {
192    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
193        if self.window.is_some() {
194            return;
195        }
196
197        log::info!("Engine: Initializing...");
198
199        let window = WinitWindowBuilder::new().build(event_loop).unwrap();
200        let mut renderer: Box<dyn RenderSystem> = Box::new(WgpuRenderSystem::new());
201        let renderer_monitors = renderer.init(&window).unwrap();
202
203        let (mut dcc, dcc_rx) = DccService::new(DccConfig::default());
204        let telemetry =
205            TelemetryService::new(Duration::from_secs(1)).with_dcc_sender(dcc.event_sender());
206
207        dcc.start(dcc_rx);
208
209        for monitor in renderer_monitors {
210            telemetry.monitor_registry().register(monitor);
211        }
212
213        let memory_monitor = Arc::new(MemoryMonitor::new("System_RAM".to_string()));
214        telemetry.monitor_registry().register(memory_monitor);
215
216        let graphics_device = renderer.graphics_device();
217
218        // Build a minimal EngineContext for Application::new().
219        let mut services = ServiceRegistry::new();
220        services.insert(graphics_device.clone());
221        let context = EngineContext {
222            world: None,
223            services,
224        };
225
226        let mut app = A::new(context);
227        let mut game_world = GameWorld::new();
228        app.setup(&mut game_world);
229
230        // Register default agents with their execution priorities
231        // Higher priority = executed first in the update loop
232        Self::register_default_agents(&dcc, graphics_device.clone());
233
234        let _ = dcc
235            .event_sender()
236            .send(khora_core::telemetry::TelemetryEvent::PhaseChange(
237                "boot".to_string(),
238            ));
239
240        // Wrap renderer in Arc<Mutex<...>> for ServiceRegistry sharing.
241        let renderer = Arc::new(Mutex::new(renderer));
242
243        self.window = Some(window);
244        self.renderer = Some(renderer);
245        self.graphics_device = Some(graphics_device);
246        self.telemetry = Some(telemetry);
247        self.dcc = Some(dcc);
248        self.game_world = Some(game_world);
249        self.render_settings = RenderSettings::default();
250        self.simulation_started = false;
251        self.app = Some(app);
252    }
253
254    fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
255        match event {
256            WindowEvent::CloseRequested => {
257                log::info!("Shutdown requested");
258                event_loop.exit();
259            }
260            WindowEvent::Resized(size) => {
261                if let Some(renderer) = self.renderer.as_ref() {
262                    log::info!("Window resized: {}x{}", size.width, size.height);
263                    if let Ok(mut r) = renderer.lock() {
264                        r.resize(size.width, size.height);
265                    }
266                }
267            }
268            WindowEvent::RedrawRequested => {
269                self.handle_frame(event_loop);
270            }
271            _ => {
272                // Log raw events to debug
273                if !matches!(event, WindowEvent::CursorMoved { .. }) {
274                    log::info!("Raw event: {:?}", event);
275                }
276                if let Some(input_event) = translate_winit_input(&event) {
277                    log::info!("Input: {:?}", input_event);
278                    self.input_events.push_back(input_event);
279                }
280            }
281        }
282    }
283
284    fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
285        if let Some(window) = &self.window {
286            window.request_redraw();
287        }
288    }
289}
290
291impl<A: Application> EngineState<A> {
292    fn register_default_agents(
293        dcc: &DccService,
294        _graphics_device: Arc<dyn khora_core::renderer::GraphicsDevice>,
295    ) {
296        // Priorities: higher = executed first
297        // Renderer: 1.0 (critical for visual feedback)
298        // Physics: 0.9 (critical for gameplay)
299        // Ecs: 0.8 (garbage collection, less critical)
300        // Asset: 0.5 (background loading)
301
302        let render_agent = khora_agents::render_agent::RenderAgent::new()
303            .with_telemetry_sender(dcc.event_sender());
304        dcc.register_agent(Arc::new(Mutex::new(render_agent)), 1.0);
305
306        let gc_agent = khora_agents::ecs_agent::GarbageCollectorAgent::new()
307            .with_dcc_sender(dcc.event_sender());
308        dcc.register_agent(Arc::new(Mutex::new(gc_agent)), 0.8);
309
310        log::info!("Engine: Registered {} default agents", dcc.agent_count());
311    }
312
313    fn handle_frame(&mut self, _event_loop: &ActiveEventLoop) {
314        let Some(renderer) = self.renderer.as_ref() else {
315            return;
316        };
317        let Some(telemetry) = self.telemetry.as_mut() else {
318            return;
319        };
320
321        if !self.simulation_started {
322            if let Some(dcc) = &self.dcc {
323                let _ =
324                    dcc.event_sender()
325                        .send(khora_core::telemetry::TelemetryEvent::PhaseChange(
326                            "simulation".to_string(),
327                        ));
328            }
329            self.simulation_started = true;
330        }
331
332        let should_log_summary = telemetry.tick();
333
334        let Some(app) = self.app.as_mut() else { return };
335
336        // Collect inputs for this frame
337        let inputs: Vec<InputEvent> = self.input_events.drain(..).collect();
338
339        // User update with inputs (logic first - move camera, etc.)
340        if let Some(gw) = self.game_world.as_mut() {
341            app.update(gw, &inputs);
342        }
343
344        // Build the ServiceRegistry for this frame.
345        let mut services = ServiceRegistry::new();
346        if let Some(device) = &self.graphics_device {
347            services.insert(device.clone());
348        }
349        services.insert(Arc::clone(renderer));
350
351        // Update all agents in priority order (handled by DCC).
352        // The RenderAgent will extract scene data, prepare the frame,
353        // and render — all within its update() method.
354        if let (Some(dcc), Some(gw)) = (&self.dcc, self.game_world.as_mut()) {
355            let mut context = gw.as_engine_context(services);
356            dcc.update_agents(&mut context);
357        }
358
359        if should_log_summary {
360            self.log_telemetry_summary();
361        }
362    }
363}
364
365/// The main entry point for the Khora Engine.
366pub struct Engine;
367
368impl Engine {
369    /// Runs the engine with the specified application.
370    ///
371    /// This is the primary entry point for game developers.
372    /// All engine systems are initialized and managed internally.
373    pub fn run<A: Application>() -> Result<()> {
374        log::info!("Khora Engine SDK: Starting...");
375        let event_loop = EventLoop::new()?;
376
377        let mut app_state = EngineState::<A> {
378            app: None,
379            game_world: None,
380            window: None,
381            renderer: None,
382            graphics_device: None,
383            telemetry: None,
384            dcc: None,
385            render_settings: RenderSettings::default(),
386            simulation_started: false,
387            running: Arc::new(AtomicBool::new(true)),
388            input_events: VecDeque::new(),
389        };
390
391        event_loop.run_app(&mut app_state)?;
392        Ok(())
393    }
394}