khora_infra/graphics/wgpu/
system.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 concrete, WGPU-based implementation of the `RenderSystem` trait.
16
17use crate::telemetry::gpu_monitor::GpuMonitor;
18
19use super::backend::WgpuBackendSelector;
20use super::context::WgpuGraphicsContext;
21use super::device::WgpuDevice;
22use super::profiler::WgpuTimestampProfiler;
23use khora_core::math::LinearRgba;
24use khora_core::platform::window::{KhoraWindow, KhoraWindowHandle};
25use khora_core::renderer::api::command::{
26    LoadOp, RenderPassColorAttachment, RenderPassDescriptor, StoreOp,
27};
28use khora_core::renderer::api::texture::TextureViewId;
29use khora_core::renderer::traits::{GpuProfiler, GraphicsBackendSelector};
30use khora_core::renderer::{
31    BackendSelectionConfig, GraphicsDevice, IndexFormat, Operations, RenderError, RenderObject,
32    RenderSettings, RenderStats, RenderSystem, RendererAdapterInfo, ViewInfo,
33};
34use khora_core::telemetry::ResourceMonitor;
35use khora_core::Stopwatch;
36use std::fmt;
37use std::sync::{Arc, Mutex};
38use std::time::Instant;
39use winit::dpi::PhysicalSize;
40
41/// The concrete, WGPU-based implementation of the [`RenderSystem`] trait.
42///
43/// This struct encapsulates all the state necessary to drive rendering with WGPU,
44/// including the graphics context, the logical device, GPU profiler, and complex
45/// state for handling window resizing gracefully.
46///
47/// It acts as the primary rendering backend for the engine when the WGPU feature is enabled.
48pub struct WgpuRenderSystem {
49    graphics_context_shared: Option<Arc<Mutex<WgpuGraphicsContext>>>,
50    wgpu_device: Option<Arc<WgpuDevice>>,
51    gpu_monitor: Option<Arc<GpuMonitor>>,
52    current_width: u32,
53    current_height: u32,
54    frame_count: u64,
55    last_frame_stats: RenderStats,
56    gpu_profiler: Option<Box<dyn GpuProfiler>>,
57    current_frame_view_id: Option<TextureViewId>,
58
59    // --- Resize Heuristics State ---
60    last_resize_event: Option<Instant>,
61    pending_resize: bool,
62    last_surface_config: Option<Instant>,
63    pending_resize_frames: u32,
64    last_pending_size: Option<(u32, u32)>,
65    stable_size_frame_count: u32,
66}
67
68impl fmt::Debug for WgpuRenderSystem {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        f.debug_struct("WgpuRenderSystem")
71            .field("graphics_context_shared", &self.graphics_context_shared)
72            .field("wgpu_device", &self.wgpu_device)
73            .field("gpu_monitor", &self.gpu_monitor)
74            .field("current_width", &self.current_width)
75            .field("current_height", &self.current_height)
76            .field("frame_count", &self.frame_count)
77            .field("last_frame_stats", &self.last_frame_stats)
78            .field(
79                "gpu_profiler",
80                &self.gpu_profiler.as_ref().map(|_| "GpuProfiler(...)"),
81            )
82            .field("current_frame_view_id", &self.current_frame_view_id)
83            .finish()
84    }
85}
86
87impl Default for WgpuRenderSystem {
88    fn default() -> Self {
89        Self::new()
90    }
91}
92
93impl WgpuRenderSystem {
94    /// Creates a new, uninitialized `WgpuRenderSystem`.
95    ///
96    /// The system is not usable until [`RenderSystem::init`] is called.
97    pub fn new() -> Self {
98        log::info!("WgpuRenderSystem created (uninitialized).");
99        Self {
100            graphics_context_shared: None,
101            wgpu_device: None,
102            gpu_monitor: None,
103            current_width: 0,
104            current_height: 0,
105            frame_count: 0,
106            last_frame_stats: RenderStats::default(),
107            gpu_profiler: None,
108            current_frame_view_id: None,
109            last_resize_event: None,
110            pending_resize: false,
111            last_surface_config: None,
112            pending_resize_frames: 0,
113            last_pending_size: None,
114            stable_size_frame_count: 0,
115        }
116    }
117
118    async fn initialize(
119        &mut self,
120        window_handle: KhoraWindowHandle,
121        window_size: PhysicalSize<u32>,
122    ) -> Result<Vec<Arc<dyn ResourceMonitor>>, RenderError> {
123        if self.graphics_context_shared.is_some() {
124            return Err(RenderError::InitializationFailed(
125                "WgpuRenderSystem is already initialized.".to_string(),
126            ));
127        }
128        log::info!("WgpuRenderSystem: Initializing...");
129
130        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
131        let backend_selector = WgpuBackendSelector::new(instance.clone());
132        let selection_config = BackendSelectionConfig::default();
133
134        let selection_result = backend_selector
135            .select_backend(&selection_config)
136            .await
137            .map_err(|e| RenderError::InitializationFailed(e.to_string()))?;
138        let adapter = selection_result.adapter;
139
140        let context = WgpuGraphicsContext::new(&instance, window_handle, adapter, window_size)
141            .await
142            .map_err(|e| RenderError::InitializationFailed(e.to_string()))?;
143
144        self.current_width = context.get_size().0;
145        self.current_height = context.get_size().1;
146        let context_arc = Arc::new(Mutex::new(context));
147        self.graphics_context_shared = Some(context_arc.clone());
148
149        log::info!(
150            "WgpuRenderSystem: GraphicsContext created with size: {}x{}",
151            self.current_width,
152            self.current_height
153        );
154
155        let graphics_device = WgpuDevice::new(context_arc.clone());
156        let device_arc = Arc::new(graphics_device);
157        self.wgpu_device = Some(device_arc.clone());
158
159        if let Ok(gc_guard) = context_arc.lock() {
160            if WgpuTimestampProfiler::feature_available(gc_guard.active_device_features) {
161                if let Some(mut profiler) = WgpuTimestampProfiler::new(&gc_guard.device) {
162                    let period = gc_guard.queue.get_timestamp_period();
163                    profiler.set_timestamp_period(period);
164                    self.gpu_profiler = Some(Box::new(profiler));
165                }
166            } else {
167                log::info!("GPU timestamp feature not available; instrumentation disabled.");
168            }
169        }
170
171        let mut created_monitors: Vec<Arc<dyn ResourceMonitor>> = Vec::new();
172        let gpu_monitor = Arc::new(GpuMonitor::new("WGPU".to_string()));
173        created_monitors.push(gpu_monitor.clone());
174        self.gpu_monitor = Some(gpu_monitor);
175
176        let vram_monitor = device_arc as Arc<dyn ResourceMonitor>;
177        created_monitors.push(vram_monitor);
178
179        Ok(created_monitors)
180    }
181}
182
183impl RenderSystem for WgpuRenderSystem {
184    fn init(
185        &mut self,
186        window: &dyn KhoraWindow,
187    ) -> Result<Vec<Arc<dyn ResourceMonitor>>, RenderError> {
188        let (width, height) = window.inner_size();
189        let window_size = PhysicalSize::new(width, height);
190        let window_handle_arc = window.clone_handle_arc();
191        pollster::block_on(self.initialize(window_handle_arc, window_size))
192    }
193
194    fn resize(&mut self, new_width: u32, new_height: u32) {
195        if new_width > 0 && new_height > 0 {
196            log::debug!(
197                "WgpuRenderSystem: resize_surface called with W:{new_width}, H:{new_height}"
198            );
199            self.current_width = new_width;
200            self.current_height = new_height;
201            let now = Instant::now();
202            if let Some((lw, lh)) = self.last_pending_size {
203                if lw == new_width && lh == new_height {
204                    self.stable_size_frame_count = self.stable_size_frame_count.saturating_add(1);
205                } else {
206                    self.stable_size_frame_count = 0;
207                }
208            }
209            self.last_pending_size = Some((new_width, new_height));
210
211            let immediate_threshold_ms: u128 = 80;
212            let can_immediate = self
213                .last_surface_config
214                .map(|t| t.elapsed().as_millis() >= immediate_threshold_ms)
215                .unwrap_or(true);
216            let early_stable = self.stable_size_frame_count >= 2
217                && self
218                    .last_surface_config
219                    .map(|t| t.elapsed().as_millis() >= 20)
220                    .unwrap_or(true);
221            if can_immediate || early_stable {
222                if let Some(gc_arc_mutex) = &self.graphics_context_shared {
223                    if let Ok(mut gc_guard) = gc_arc_mutex.lock() {
224                        gc_guard.resize(self.current_width, self.current_height);
225                        self.last_surface_config = Some(now);
226                        self.pending_resize = false;
227                        self.pending_resize_frames = 0;
228                        log::info!(
229                            "WGPUGraphicsContext: Immediate/Early surface configuration to {}x{}",
230                            self.current_width,
231                            self.current_height
232                        );
233                        return;
234                    }
235                }
236            }
237            self.last_resize_event = Some(now);
238            self.pending_resize = true;
239            self.pending_resize_frames = 0;
240        } else {
241            log::warn!(
242                "WgpuRenderSystem::resize_surface called with zero size ({new_width}, {new_height}). Ignoring."
243            );
244        }
245    }
246
247    fn prepare_frame(&mut self, _view_info: &ViewInfo) {
248        if self.graphics_context_shared.is_none() {
249            return;
250        }
251        let stopwatch = Stopwatch::new();
252        self.last_frame_stats.cpu_preparation_time_ms = stopwatch.elapsed_ms().unwrap_or(0) as f32;
253    }
254
255    fn render(
256        &mut self,
257        renderables: &[RenderObject],
258        _view_info: &ViewInfo,
259        settings: &RenderSettings,
260    ) -> Result<RenderStats, RenderError> {
261        let full_frame_timer = Stopwatch::new();
262
263        let device = self
264            .wgpu_device
265            .as_ref()
266            .ok_or(RenderError::NotInitialized)?;
267
268        // Poll the device to process any pending GPU-to-CPU callbacks, such as
269        // those from the profiler's `map_async` calls. This is crucial.
270        device.poll_device_non_blocking();
271
272        let gc = self
273            .graphics_context_shared
274            .as_ref()
275            .ok_or(RenderError::NotInitialized)?;
276
277        if let Some(p) = self.gpu_profiler.as_mut() {
278            p.try_read_previous_frame();
279        }
280
281        // --- Handle Pending Resizes ---
282        if self.pending_resize {
283            self.pending_resize_frames = self.pending_resize_frames.saturating_add(1);
284            let mut resized_this_frame = false;
285            if let Some(t) = self.last_resize_event {
286                let quiet_elapsed = t.elapsed().as_millis();
287                let debounce_quiet_ms = settings.resize_debounce_ms as u128;
288                let max_pending_frames = settings.resize_max_pending_frames;
289                let early_stable = self.stable_size_frame_count >= 3;
290
291                if quiet_elapsed >= debounce_quiet_ms
292                    || self.pending_resize_frames >= max_pending_frames
293                    || early_stable
294                {
295                    if let Ok(mut gc_guard) = gc.lock() {
296                        gc_guard.resize(self.current_width, self.current_height);
297                        self.pending_resize = false;
298                        self.last_surface_config = Some(Instant::now());
299                        self.stable_size_frame_count = 0;
300                        resized_this_frame = true;
301                        log::info!(
302                            "Deferred surface configuration to {}x{}",
303                            self.current_width,
304                            self.current_height
305                        );
306                    }
307                }
308            }
309            if self.pending_resize && !resized_this_frame {
310                return Ok(self.last_frame_stats.clone());
311            }
312        }
313
314        // --- 1. Acquire Frame from Swap Chain ---
315        let output_surface_texture = loop {
316            let mut gc_guard = gc.lock().unwrap();
317            match gc_guard.get_current_texture() {
318                Ok(texture) => break texture,
319                Err(e @ wgpu::SurfaceError::Lost) | Err(e @ wgpu::SurfaceError::Outdated) => {
320                    if self.current_width > 0 && self.current_height > 0 {
321                        log::warn!(
322                            "WgpuRenderSystem: Swapchain surface lost or outdated ({:?}). Reconfiguring with current dimensions: W={}, H={}",
323                            e,
324                            self.current_width,
325                            self.current_height
326                        );
327                        gc_guard.resize(self.current_width, self.current_height);
328                        self.last_surface_config = Some(Instant::now());
329                        self.pending_resize = false; // reset pending state after forced reconfigure
330                    } else {
331                        log::error!(
332                            "WgpuRenderSystem: Swapchain lost/outdated ({:?}), but current stored size is zero ({},{}). Cannot reconfigure. Waiting for valid resize event.",
333                            e,
334                            self.current_width,
335                            self.current_height
336                        );
337                        return Err(RenderError::SurfaceAcquisitionFailed(format!(
338                            "Surface Lost/Outdated ({e:?}) and current size is zero",
339                        )));
340                    }
341                }
342                Err(e @ wgpu::SurfaceError::OutOfMemory) => {
343                    log::error!("WgpuRenderSystem: Swapchain OutOfMemory! ({e:?})");
344                    return Err(RenderError::SurfaceAcquisitionFailed(format!(
345                        "OutOfMemory: {e:?}"
346                    )));
347                }
348                Err(e @ wgpu::SurfaceError::Timeout) => {
349                    log::warn!("WgpuRenderSystem: Swapchain Timeout acquiring frame. ({e:?})");
350                    return Err(RenderError::SurfaceAcquisitionFailed(format!(
351                        "Timeout: {e:?}"
352                    )));
353                }
354                Err(e) => {
355                    log::error!("WgpuRenderSystem: Unexpected SurfaceError: {e:?}");
356                    return Err(RenderError::SurfaceAcquisitionFailed(format!(
357                        "Unexpected SurfaceError: {e:?}"
358                    )));
359                }
360            }
361        };
362
363        let command_recording_timer = Stopwatch::new();
364
365        // --- 2. Create a managed, abstract view for the swap chain texture ---
366        if let Some(old_id) = self.current_frame_view_id.take() {
367            device.destroy_texture_view(old_id)?;
368        }
369        let target_view_id = device.create_texture_view_for_surface(
370            &output_surface_texture.texture,
371            Some("Primary Swap Chain View"),
372        )?;
373        self.current_frame_view_id = Some(target_view_id);
374
375        // --- 3. Create an abstract Command Encoder ---
376        let mut command_encoder = device.create_command_encoder(Some("Khora Main Command Encoder"));
377
378        // --- 4. Profiler Pass A (records start timestamps) ---
379        if settings.enable_gpu_timestamps {
380            if let Some(profiler) = self.gpu_profiler.as_ref() {
381                let _pass_a = command_encoder.begin_profiler_compute_pass(
382                    Some("Timestamp Pass A"),
383                    profiler.as_ref(),
384                    0,
385                );
386            }
387        }
388
389        // --- 5. Main Render Pass (drawing all objects) ---
390        {
391            let gc_guard = gc.lock().unwrap();
392            let wgpu_color = gc_guard.get_clear_color();
393            let clear_color = LinearRgba::new(
394                wgpu_color.r as f32,
395                wgpu_color.g as f32,
396                wgpu_color.b as f32,
397                wgpu_color.a as f32,
398            );
399
400            let color_attachment = RenderPassColorAttachment {
401                view: &target_view_id,
402                resolve_target: None,
403                ops: Operations {
404                    load: LoadOp::Clear(clear_color),
405                    store: StoreOp::Store,
406                },
407            };
408            let pass_descriptor = RenderPassDescriptor {
409                label: Some("Khora Main Abstract Render Pass"),
410                color_attachments: &[color_attachment],
411            };
412
413            let mut render_pass = command_encoder.begin_render_pass(&pass_descriptor);
414
415            let (draw_calls, triangles) = renderables.iter().fold((0, 0), |(dc, tris), obj| {
416                render_pass.set_pipeline(&obj.pipeline);
417                render_pass.set_vertex_buffer(0, &obj.vertex_buffer, 0);
418                render_pass.set_index_buffer(&obj.index_buffer, 0, IndexFormat::Uint16);
419                render_pass.draw_indexed(0..obj.index_count, 0, 0..1);
420                (dc + 1, tris + obj.index_count / 3)
421            });
422            self.last_frame_stats.draw_calls = draw_calls;
423            self.last_frame_stats.triangles_rendered = triangles;
424        }
425
426        // --- 6. Profiler Pass B and Timestamp Resolution ---
427        if settings.enable_gpu_timestamps {
428            if let Some(profiler) = self.gpu_profiler.as_ref() {
429                // This scope ensures the compute pass ends, releasing its mutable borrow on the encoder,
430                // before we try to mutably borrow the encoder again for resolve/copy.
431                {
432                    let _pass_b = command_encoder.begin_profiler_compute_pass(
433                        Some("Timestamp Pass B"),
434                        profiler.as_ref(),
435                        1,
436                    );
437                }
438                profiler.resolve_and_copy(command_encoder.as_mut());
439                profiler.copy_to_staging(command_encoder.as_mut(), self.frame_count);
440            }
441        }
442
443        // --- 7. Finalize and Submit Commands ---
444        let submission_timer = Stopwatch::new();
445        let command_buffer = command_encoder.finish();
446        device.submit_command_buffer(command_buffer);
447        let submission_ms = submission_timer.elapsed_ms().unwrap_or(0);
448
449        if settings.enable_gpu_timestamps {
450            if let Some(p) = self.gpu_profiler.as_mut() {
451                p.schedule_map_after_submit(self.frame_count);
452            }
453        }
454
455        // --- 8. Present the final image to the screen ---
456        output_surface_texture.present();
457
458        // --- 9. Update final frame statistics ---
459        self.frame_count += 1;
460        if let Some(p) = self.gpu_profiler.as_ref() {
461            self.last_frame_stats.gpu_main_pass_time_ms = p.last_main_pass_ms();
462            self.last_frame_stats.gpu_frame_total_time_ms = p.last_frame_total_ms();
463        }
464        let full_frame_ms = full_frame_timer.elapsed_ms().unwrap_or(0);
465        self.last_frame_stats.frame_number = self.frame_count;
466        self.last_frame_stats.cpu_preparation_time_ms =
467            (full_frame_ms - command_recording_timer.elapsed_ms().unwrap_or(0)) as f32;
468        self.last_frame_stats.cpu_render_submission_time_ms = submission_ms as f32;
469
470        if let Some(monitor) = &self.gpu_monitor {
471            monitor.update_from_frame_stats(&self.last_frame_stats);
472        }
473
474        Ok(self.last_frame_stats.clone())
475    }
476
477    fn get_last_frame_stats(&self) -> &RenderStats {
478        &self.last_frame_stats
479    }
480
481    fn supports_feature(&self, feature_name: &str) -> bool {
482        self.wgpu_device
483            .as_ref()
484            .is_some_and(|d| d.supports_feature(feature_name))
485    }
486
487    fn shutdown(&mut self) {
488        log::info!("WgpuRenderSystem shutting down...");
489        if let Some(mut profiler) = self.gpu_profiler.take() {
490            if let Some(device) = self.wgpu_device.as_ref() {
491                if let Some(wgpu_profiler) = profiler
492                    .as_any_mut()
493                    .downcast_mut::<WgpuTimestampProfiler>()
494                {
495                    wgpu_profiler.shutdown(device);
496                }
497            }
498        }
499        if let Some(old_id) = self.current_frame_view_id.take() {
500            if let Some(device) = self.wgpu_device.as_ref() {
501                let _ = device.destroy_texture_view(old_id);
502            }
503        }
504        self.wgpu_device = None;
505        self.graphics_context_shared = None;
506        self.gpu_monitor = None;
507    }
508
509    fn as_any(&self) -> &dyn std::any::Any {
510        self
511    }
512
513    fn get_adapter_info(&self) -> Option<RendererAdapterInfo> {
514        self.wgpu_device.as_ref().map(|d| d.get_adapter_info())
515    }
516
517    fn graphics_device(&self) -> Arc<dyn GraphicsDevice> {
518        self.wgpu_device
519            .clone()
520            .expect("WgpuRenderSystem: No WgpuDevice available.")
521    }
522}
523
524unsafe impl Send for WgpuRenderSystem {}
525unsafe impl Sync for WgpuRenderSystem {}