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    BindGroupId, BindGroupLayoutId, LoadOp, Operations, RenderPassColorAttachment,
27    RenderPassDescriptor, StoreOp,
28};
29use khora_core::renderer::api::core::{
30    BackendSelectionConfig, GraphicsAdapterInfo, RenderSettings, RenderStats,
31};
32use khora_core::renderer::api::resource::{
33    BufferId, ImageAspect, TextureDescriptor, TextureDimension, TextureId, TextureUsage,
34    TextureViewDescriptor, TextureViewId, ViewInfo,
35};
36use khora_core::renderer::api::scene::RenderObject;
37use khora_core::renderer::api::util::ShaderStageFlags;
38use khora_core::renderer::api::util::{IndexFormat, SampleCount, TextureFormat};
39use khora_core::renderer::traits::{GpuProfiler, GraphicsBackendSelector, RenderSystem};
40use khora_core::renderer::{GraphicsDevice, RenderError};
41use khora_core::telemetry::ResourceMonitor;
42use khora_core::Stopwatch;
43use std::fmt;
44use std::sync::{Arc, Mutex};
45use std::time::Instant;
46use winit::dpi::PhysicalSize;
47
48/// The concrete, WGPU-based implementation of the [`RenderSystem`] trait.
49///
50/// This struct encapsulates all the state necessary to drive rendering with WGPU,
51/// including the graphics context, the logical device, GPU profiler, and complex
52/// state for handling window resizing gracefully.
53///
54/// It acts as the primary rendering backend for the engine when the WGPU feature is enabled.
55pub struct WgpuRenderSystem {
56    graphics_context_shared: Option<Arc<Mutex<WgpuGraphicsContext>>>,
57    wgpu_device: Option<Arc<WgpuDevice>>,
58    gpu_monitor: Option<Arc<GpuMonitor>>,
59    current_width: u32,
60    current_height: u32,
61    frame_count: u64,
62    last_frame_stats: RenderStats,
63    gpu_profiler: Option<Box<dyn GpuProfiler>>,
64    current_frame_view_id: Option<TextureViewId>,
65
66    // --- Camera Uniform Resources ---
67    camera_uniform_buffer: Option<BufferId>,
68    camera_bind_group: Option<BindGroupId>,
69    camera_bind_group_layout: Option<BindGroupLayoutId>,
70
71    // --- Depth Buffer Resources ---
72    depth_texture: Option<TextureId>,
73    depth_texture_view: Option<TextureViewId>,
74
75    // --- Resize Heuristics State ---
76    last_resize_event: Option<Instant>,
77    pending_resize: bool,
78    last_surface_config: Option<Instant>,
79    pending_resize_frames: u32,
80    last_pending_size: Option<(u32, u32)>,
81    stable_size_frame_count: u32,
82}
83
84impl fmt::Debug for WgpuRenderSystem {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        f.debug_struct("WgpuRenderSystem")
87            .field("graphics_context_shared", &self.graphics_context_shared)
88            .field("wgpu_device", &self.wgpu_device)
89            .field("gpu_monitor", &self.gpu_monitor)
90            .field("current_width", &self.current_width)
91            .field("current_height", &self.current_height)
92            .field("frame_count", &self.frame_count)
93            .field("last_frame_stats", &self.last_frame_stats)
94            .field(
95                "gpu_profiler",
96                &self.gpu_profiler.as_ref().map(|_| "GpuProfiler(...)"),
97            )
98            .field("current_frame_view_id", &self.current_frame_view_id)
99            .field(
100                "camera_uniform_buffer",
101                &self.camera_uniform_buffer.as_ref().map(|_| "Buffer(...)"),
102            )
103            .field(
104                "camera_bind_group",
105                &self.camera_bind_group.as_ref().map(|_| "BindGroup(...)"),
106            )
107            .field(
108                "camera_bind_group_layout",
109                &self
110                    .camera_bind_group_layout
111                    .as_ref()
112                    .map(|_| "BindGroupLayout(...)"),
113            )
114            .finish()
115    }
116}
117
118impl Default for WgpuRenderSystem {
119    fn default() -> Self {
120        Self::new()
121    }
122}
123
124impl WgpuRenderSystem {
125    /// Creates a new, uninitialized `WgpuRenderSystem`.
126    ///
127    /// The system is not usable until [`RenderSystem::init`] is called.
128    pub fn new() -> Self {
129        log::info!("WgpuRenderSystem created (uninitialized).");
130        Self {
131            graphics_context_shared: None,
132            wgpu_device: None,
133            gpu_monitor: None,
134            current_width: 0,
135            current_height: 0,
136            frame_count: 0,
137            last_frame_stats: RenderStats::default(),
138            gpu_profiler: None,
139            current_frame_view_id: None,
140            camera_uniform_buffer: None,
141            camera_bind_group: None,
142            camera_bind_group_layout: None,
143            depth_texture: None,
144            depth_texture_view: None,
145            last_resize_event: None,
146            pending_resize: false,
147            last_surface_config: None,
148            pending_resize_frames: 0,
149            last_pending_size: None,
150            stable_size_frame_count: 0,
151        }
152    }
153
154    async fn initialize(
155        &mut self,
156        window_handle: KhoraWindowHandle,
157        window_size: PhysicalSize<u32>,
158    ) -> Result<Vec<Arc<dyn ResourceMonitor>>, RenderError> {
159        if self.graphics_context_shared.is_some() {
160            return Err(RenderError::InitializationFailed(
161                "WgpuRenderSystem is already initialized.".to_string(),
162            ));
163        }
164        log::info!("WgpuRenderSystem: Initializing...");
165
166        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
167        let backend_selector = WgpuBackendSelector::new(instance.clone());
168        let selection_config = BackendSelectionConfig::default();
169
170        let selection_result = backend_selector
171            .select_backend(&selection_config)
172            .await
173            .map_err(|e| RenderError::InitializationFailed(e.to_string()))?;
174        let adapter = selection_result.adapter;
175
176        let context = WgpuGraphicsContext::new(&instance, window_handle, adapter, window_size)
177            .await
178            .map_err(|e| RenderError::InitializationFailed(e.to_string()))?;
179
180        self.current_width = context.get_size().0;
181        self.current_height = context.get_size().1;
182        let context_arc = Arc::new(Mutex::new(context));
183        self.graphics_context_shared = Some(context_arc.clone());
184
185        log::info!(
186            "WgpuRenderSystem: GraphicsContext created with size: {}x{}",
187            self.current_width,
188            self.current_height
189        );
190
191        let graphics_device = WgpuDevice::new(context_arc.clone());
192        let device_arc = Arc::new(graphics_device);
193        self.wgpu_device = Some(device_arc.clone());
194
195        if let Ok(gc_guard) = context_arc.lock() {
196            if WgpuTimestampProfiler::feature_available(gc_guard.active_device_features) {
197                if let Some(mut profiler) = WgpuTimestampProfiler::new(&gc_guard.device) {
198                    let period = gc_guard.queue.get_timestamp_period();
199                    profiler.set_timestamp_period(period);
200                    self.gpu_profiler = Some(Box::new(profiler));
201                }
202            } else {
203                log::info!("GPU timestamp feature not available; instrumentation disabled.");
204            }
205        }
206
207        let mut created_monitors: Vec<Arc<dyn ResourceMonitor>> = Vec::new();
208        let gpu_monitor = Arc::new(GpuMonitor::new("WGPU".to_string()));
209        created_monitors.push(gpu_monitor.clone());
210        self.gpu_monitor = Some(gpu_monitor);
211
212        let vram_monitor = device_arc as Arc<dyn ResourceMonitor>;
213        created_monitors.push(vram_monitor);
214
215        // Initialize camera uniform resources
216        self.initialize_camera_uniforms()?;
217
218        // Initialize depth texture for depth buffering
219        self.create_depth_texture()?;
220
221        Ok(created_monitors)
222    }
223
224    /// Initializes the camera uniform buffer and bind group.
225    ///
226    /// This creates:
227    /// - A uniform buffer to hold camera data (view-projection matrix and camera position)
228    /// - A bind group layout describing the shader resource binding
229    /// - A bind group that binds the buffer to group 0, binding 0
230    fn initialize_camera_uniforms(&mut self) -> Result<(), RenderError> {
231        use khora_core::renderer::api::command::{
232            BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry,
233            BindingResource, BindingType, BufferBinding, BufferBindingType,
234        };
235        use khora_core::renderer::api::resource::{
236            BufferDescriptor, BufferUsage, CameraUniformData,
237        };
238
239        let device = self.wgpu_device.as_ref().ok_or_else(|| {
240            RenderError::InitializationFailed("WGPU device not initialized".to_string())
241        })?;
242
243        let buffer_size = std::mem::size_of::<CameraUniformData>() as u64;
244
245        // Create the uniform buffer using the abstract API
246        let buffer_descriptor = BufferDescriptor {
247            label: Some(std::borrow::Cow::Borrowed("Camera Uniform Buffer")),
248            size: buffer_size,
249            usage: BufferUsage::UNIFORM | BufferUsage::COPY_DST,
250            mapped_at_creation: false,
251        };
252
253        let uniform_buffer = device.create_buffer(&buffer_descriptor).map_err(|e| {
254            RenderError::InitializationFailed(format!(
255                "Failed to create camera uniform buffer: {:?}",
256                e
257            ))
258        })?;
259
260        // Create the bind group layout using the abstract API
261        let layout_entry = BindGroupLayoutEntry {
262            binding: 0,
263            visibility: ShaderStageFlags::VERTEX | ShaderStageFlags::FRAGMENT,
264            ty: BindingType::Buffer {
265                ty: BufferBindingType::Uniform,
266                has_dynamic_offset: false,
267                min_binding_size: None,
268            },
269        };
270
271        let layout_descriptor = BindGroupLayoutDescriptor {
272            label: Some("Camera Bind Group Layout"),
273            entries: &[layout_entry],
274        };
275
276        let bind_group_layout = device
277            .create_bind_group_layout(&layout_descriptor)
278            .map_err(|e| {
279                RenderError::InitializationFailed(format!(
280                    "Failed to create camera bind group layout: {:?}",
281                    e
282                ))
283            })?;
284
285        // Create the bind group using the abstract API
286        let bind_group_entry = BindGroupEntry {
287            binding: 0,
288            resource: BindingResource::Buffer(BufferBinding {
289                buffer: uniform_buffer,
290                offset: 0,
291                size: None,
292            }),
293            _phantom: std::marker::PhantomData,
294        };
295
296        let bind_group_descriptor = BindGroupDescriptor {
297            label: Some("Camera Bind Group"),
298            layout: bind_group_layout,
299            entries: &[bind_group_entry],
300        };
301
302        let bind_group = device
303            .create_bind_group(&bind_group_descriptor)
304            .map_err(|e| {
305                RenderError::InitializationFailed(format!(
306                    "Failed to create camera bind group: {:?}",
307                    e
308                ))
309            })?;
310
311        self.camera_uniform_buffer = Some(uniform_buffer);
312        self.camera_bind_group_layout = Some(bind_group_layout);
313        self.camera_bind_group = Some(bind_group);
314
315        log::info!("Camera uniform resources initialized with abstract API");
316
317        Ok(())
318    }
319
320    /// Updates the camera uniform buffer with the current ViewInfo data.
321    ///
322    /// This method is called every frame to upload the latest camera matrices
323    /// to the GPU uniform buffer.
324    fn update_camera_uniforms(&mut self, view_info: &ViewInfo) {
325        use khora_core::renderer::api::resource::CameraUniformData;
326
327        let uniform_data = CameraUniformData::from_view_info(view_info);
328
329        if let (Some(device), Some(buffer_id)) = (&self.wgpu_device, &self.camera_uniform_buffer) {
330            // Write the uniform data to the buffer using the abstract API
331            if let Err(e) =
332                device.write_buffer(*buffer_id, 0, bytemuck::cast_slice(&[uniform_data]))
333            {
334                log::warn!("Failed to write camera uniform data: {:?}", e);
335            }
336        }
337    }
338
339    /// Creates or recreates the depth texture for depth buffering.
340    ///
341    /// This method should be called during initialization and whenever the window is resized.
342    /// It destroys any existing depth texture resources before creating new ones.
343    fn create_depth_texture(&mut self) -> Result<(), RenderError> {
344        use khora_core::math::Extent3D;
345        use std::borrow::Cow;
346
347        let device = self.wgpu_device.as_ref().ok_or_else(|| {
348            RenderError::InitializationFailed("WGPU device not initialized".to_string())
349        })?;
350
351        // Skip if dimensions are zero
352        if self.current_width == 0 || self.current_height == 0 {
353            return Ok(());
354        }
355
356        // Destroy old depth texture resources if they exist
357        if let Some(old_view) = self.depth_texture_view.take() {
358            let _ = device.destroy_texture_view(old_view);
359        }
360        if let Some(old_tex) = self.depth_texture.take() {
361            let _ = device.destroy_texture(old_tex);
362        }
363
364        // Create new depth texture
365        let texture_desc = TextureDescriptor {
366            label: Some(Cow::Borrowed("Depth Texture")),
367            size: Extent3D {
368                width: self.current_width,
369                height: self.current_height,
370                depth_or_array_layers: 1,
371            },
372            mip_level_count: 1,
373            sample_count: SampleCount::X1,
374            dimension: TextureDimension::D2,
375            format: TextureFormat::Depth32Float,
376            usage: TextureUsage::RENDER_ATTACHMENT,
377            view_formats: Cow::Borrowed(&[]),
378        };
379
380        let texture_id = device.create_texture(&texture_desc).map_err(|e| {
381            RenderError::InitializationFailed(format!("Failed to create depth texture: {:?}", e))
382        })?;
383
384        // Create depth texture view
385        let view_desc = TextureViewDescriptor {
386            label: Some(Cow::Borrowed("Depth Texture View")),
387            format: Some(TextureFormat::Depth32Float),
388            dimension: None,
389            aspect: ImageAspect::DepthOnly,
390            base_mip_level: 0,
391            mip_level_count: None,
392            base_array_layer: 0,
393            array_layer_count: None,
394        };
395
396        let view_id = device
397            .create_texture_view(texture_id, &view_desc)
398            .map_err(|e| {
399                RenderError::InitializationFailed(format!(
400                    "Failed to create depth texture view: {:?}",
401                    e
402                ))
403            })?;
404
405        self.depth_texture = Some(texture_id);
406        self.depth_texture_view = Some(view_id);
407
408        log::info!(
409            "Depth texture created: {}x{} (Depth32Float)",
410            self.current_width,
411            self.current_height
412        );
413
414        Ok(())
415    }
416}
417
418impl RenderSystem for WgpuRenderSystem {
419    fn init(
420        &mut self,
421        window: &dyn KhoraWindow,
422    ) -> Result<Vec<Arc<dyn ResourceMonitor>>, RenderError> {
423        let (width, height) = window.inner_size();
424        let window_size = PhysicalSize::new(width, height);
425        let window_handle_arc = window.clone_handle_arc();
426        pollster::block_on(self.initialize(window_handle_arc, window_size))
427    }
428
429    fn resize(&mut self, new_width: u32, new_height: u32) {
430        if new_width > 0 && new_height > 0 {
431            log::debug!(
432                "WgpuRenderSystem: resize_surface called with W:{new_width}, H:{new_height}"
433            );
434            self.current_width = new_width;
435            self.current_height = new_height;
436            let now = Instant::now();
437            if let Some((lw, lh)) = self.last_pending_size {
438                if lw == new_width && lh == new_height {
439                    self.stable_size_frame_count = self.stable_size_frame_count.saturating_add(1);
440                } else {
441                    self.stable_size_frame_count = 0;
442                }
443            }
444            self.last_pending_size = Some((new_width, new_height));
445
446            let immediate_threshold_ms: u128 = 80;
447            let can_immediate = self
448                .last_surface_config
449                .map(|t| t.elapsed().as_millis() >= immediate_threshold_ms)
450                .unwrap_or(true);
451            let early_stable = self.stable_size_frame_count >= 2
452                && self
453                    .last_surface_config
454                    .map(|t| t.elapsed().as_millis() >= 20)
455                    .unwrap_or(true);
456            if can_immediate || early_stable {
457                let mut did_resize = false;
458                if let Some(gc_arc_mutex) = &self.graphics_context_shared {
459                    if let Ok(mut gc_guard) = gc_arc_mutex.lock() {
460                        gc_guard.resize(self.current_width, self.current_height);
461                        self.last_surface_config = Some(now);
462                        self.pending_resize = false;
463                        self.pending_resize_frames = 0;
464                        did_resize = true;
465                    }
466                }
467                if did_resize {
468                    // Recreate depth texture to match new size (after lock is released)
469                    if let Err(e) = self.create_depth_texture() {
470                        log::warn!("Failed to recreate depth texture during resize: {:?}", e);
471                    }
472                    log::info!(
473                        "WGPUGraphicsContext: Immediate/Early surface configuration to {}x{}",
474                        self.current_width,
475                        self.current_height
476                    );
477                    return;
478                }
479            }
480            self.last_resize_event = Some(now);
481            self.pending_resize = true;
482            self.pending_resize_frames = 0;
483        } else {
484            log::warn!(
485                "WgpuRenderSystem::resize_surface called with zero size ({new_width}, {new_height}). Ignoring."
486            );
487        }
488    }
489
490    fn prepare_frame(&mut self, view_info: &ViewInfo) {
491        if self.graphics_context_shared.is_none() {
492            return;
493        }
494        let stopwatch = Stopwatch::new();
495
496        // Update camera uniform buffer with the current ViewInfo
497        self.update_camera_uniforms(view_info);
498
499        self.last_frame_stats.cpu_preparation_time_ms = stopwatch.elapsed_ms().unwrap_or(0) as f32;
500    }
501
502    fn render(
503        &mut self,
504        renderables: &[RenderObject],
505        _view_info: &ViewInfo,
506        settings: &RenderSettings,
507    ) -> Result<RenderStats, RenderError> {
508        let full_frame_timer = Stopwatch::new();
509
510        let device = self
511            .wgpu_device
512            .clone()
513            .ok_or(RenderError::NotInitialized)?;
514
515        // Poll the device to process any pending GPU-to-CPU callbacks, such as
516        // those from the profiler's `map_async` calls. This is crucial.
517        device.poll_device_non_blocking();
518
519        let gc = self
520            .graphics_context_shared
521            .clone()
522            .ok_or(RenderError::NotInitialized)?;
523
524        if let Some(p) = self.gpu_profiler.as_mut() {
525            p.try_read_previous_frame();
526        }
527
528        // --- Handle Pending Resizes ---
529        let mut resized_this_frame = false;
530        if self.pending_resize {
531            self.pending_resize_frames = self.pending_resize_frames.saturating_add(1);
532            if let Some(t) = self.last_resize_event {
533                let quiet_elapsed = t.elapsed().as_millis();
534                let debounce_quiet_ms = settings.resize_debounce_ms as u128;
535                let max_pending_frames = settings.resize_max_pending_frames;
536                let early_stable = self.stable_size_frame_count >= 3;
537
538                if quiet_elapsed >= debounce_quiet_ms
539                    || self.pending_resize_frames >= max_pending_frames
540                    || early_stable
541                {
542                    if let Ok(mut gc_guard) = gc.lock() {
543                        gc_guard.resize(self.current_width, self.current_height);
544                        self.pending_resize = false;
545                        self.last_surface_config = Some(Instant::now());
546                        self.stable_size_frame_count = 0;
547                        resized_this_frame = true;
548                        log::info!(
549                            "Deferred surface configuration to {}x{}",
550                            self.current_width,
551                            self.current_height
552                        );
553                    }
554                }
555            }
556            if self.pending_resize && !resized_this_frame {
557                return Ok(self.last_frame_stats.clone());
558            }
559        }
560
561        // Recreate depth texture if we just resized
562        if resized_this_frame {
563            if let Err(e) = self.create_depth_texture() {
564                log::warn!(
565                    "Failed to recreate depth texture during deferred resize: {:?}",
566                    e
567                );
568            }
569        }
570
571        // --- 1. Acquire Frame from Swap Chain ---
572        let output_surface_texture = loop {
573            let mut gc_guard = gc.lock().unwrap();
574            match gc_guard.get_current_texture() {
575                Ok(texture) => break texture,
576                Err(e @ wgpu::SurfaceError::Lost) | Err(e @ wgpu::SurfaceError::Outdated) => {
577                    if self.current_width > 0 && self.current_height > 0 {
578                        log::warn!(
579                            "WgpuRenderSystem: Swapchain surface lost or outdated ({:?}). Reconfiguring with current dimensions: W={}, H={}",
580                            e,
581                            self.current_width,
582                            self.current_height
583                        );
584                        gc_guard.resize(self.current_width, self.current_height);
585                        self.last_surface_config = Some(Instant::now());
586                        self.pending_resize = false; // reset pending state after forced reconfigure
587                    } else {
588                        log::error!(
589                            "WgpuRenderSystem: Swapchain lost/outdated ({:?}), but current stored size is zero ({},{}). Cannot reconfigure. Waiting for valid resize event.",
590                            e,
591                            self.current_width,
592                            self.current_height
593                        );
594                        return Err(RenderError::SurfaceAcquisitionFailed(format!(
595                            "Surface Lost/Outdated ({e:?}) and current size is zero",
596                        )));
597                    }
598                }
599                Err(e @ wgpu::SurfaceError::OutOfMemory) => {
600                    log::error!("WgpuRenderSystem: Swapchain OutOfMemory! ({e:?})");
601                    return Err(RenderError::SurfaceAcquisitionFailed(format!(
602                        "OutOfMemory: {e:?}"
603                    )));
604                }
605                Err(e @ wgpu::SurfaceError::Timeout) => {
606                    log::warn!("WgpuRenderSystem: Swapchain Timeout acquiring frame. ({e:?})");
607                    return Err(RenderError::SurfaceAcquisitionFailed(format!(
608                        "Timeout: {e:?}"
609                    )));
610                }
611                Err(e) => {
612                    log::error!("WgpuRenderSystem: Unexpected SurfaceError: {e:?}");
613                    return Err(RenderError::SurfaceAcquisitionFailed(format!(
614                        "Unexpected SurfaceError: {e:?}"
615                    )));
616                }
617            }
618        };
619
620        let command_recording_timer = Stopwatch::new();
621
622        // --- 2. Create a managed, abstract view for the swap chain texture ---
623        if let Some(old_id) = self.current_frame_view_id.take() {
624            device.destroy_texture_view(old_id)?;
625        }
626        let target_view_id = device.register_texture_view(
627            &output_surface_texture.texture,
628            Some("Primary Swap Chain View"),
629        )?;
630        self.current_frame_view_id = Some(target_view_id);
631
632        // --- 3. Create an abstract Command Encoder ---
633        let mut command_encoder = device.create_command_encoder(Some("Khora Main Command Encoder"));
634
635        // --- 4. Profiler Pass A (records start timestamps) ---
636        if settings.enable_gpu_timestamps {
637            if let Some(profiler) = self.gpu_profiler.as_ref() {
638                let _pass_a = command_encoder.begin_profiler_compute_pass(
639                    Some("Timestamp Pass A"),
640                    profiler.as_ref(),
641                    0,
642                );
643            }
644        }
645
646        // --- 5. Main Render Pass (drawing all objects) ---
647        {
648            let gc_guard = gc.lock().unwrap();
649            let wgpu_color = gc_guard.get_clear_color();
650            let clear_color = LinearRgba::new(
651                wgpu_color.r as f32,
652                wgpu_color.g as f32,
653                wgpu_color.b as f32,
654                wgpu_color.a as f32,
655            );
656
657            let color_attachment = RenderPassColorAttachment {
658                view: &target_view_id,
659                resolve_target: None,
660                ops: Operations {
661                    load: LoadOp::Clear(clear_color),
662                    store: StoreOp::Store,
663                },
664                base_array_layer: 0,
665            };
666
667            // Create depth/stencil attachment if depth texture is available
668            use khora_core::renderer::api::command::RenderPassDepthStencilAttachment;
669            let depth_attachment = self.depth_texture_view.as_ref().map(|depth_view| {
670                RenderPassDepthStencilAttachment {
671                    view: depth_view,
672                    depth_ops: Some(Operations {
673                        load: LoadOp::Clear(1.0), // Clear to far plane (1.0)
674                        store: StoreOp::Store,
675                    }),
676                    stencil_ops: None, // No stencil operations
677                    base_array_layer: 0,
678                }
679            });
680
681            let pass_descriptor = RenderPassDescriptor {
682                label: Some("Khora Main Abstract Render Pass"),
683                color_attachments: &[color_attachment],
684                depth_stencil_attachment: depth_attachment,
685            };
686
687            let mut render_pass = command_encoder.begin_render_pass(&pass_descriptor);
688
689            // Apply the same bind group and pipeline to all chunks to limit state changes
690            if let Some(camera_bind_group) = &self.camera_bind_group {
691                render_pass.set_bind_group(0, camera_bind_group, &[]);
692            }
693            let (draw_calls, triangles) = renderables.iter().fold((0, 0), |(dc, tris), obj| {
694                render_pass.set_pipeline(&obj.pipeline);
695                render_pass.set_vertex_buffer(0, &obj.vertex_buffer, 0);
696                render_pass.set_index_buffer(&obj.index_buffer, 0, IndexFormat::Uint16);
697                render_pass.draw_indexed(0..obj.index_count, 0, 0..1);
698                (dc + 1, tris + obj.index_count / 3)
699            });
700            self.last_frame_stats.draw_calls = draw_calls;
701            self.last_frame_stats.triangles_rendered = triangles;
702        }
703
704        // --- 6. Profiler Pass B and Timestamp Resolution ---
705        if settings.enable_gpu_timestamps {
706            if let Some(profiler) = self.gpu_profiler.as_ref() {
707                // This scope ensures the compute pass ends, releasing its mutable borrow on the encoder,
708                // before we try to mutably borrow the encoder again for resolve/copy.
709                {
710                    let _pass_b = command_encoder.begin_profiler_compute_pass(
711                        Some("Timestamp Pass B"),
712                        profiler.as_ref(),
713                        1,
714                    );
715                }
716                profiler.resolve_and_copy(command_encoder.as_mut());
717                profiler.copy_to_staging(command_encoder.as_mut(), self.frame_count);
718            }
719        }
720
721        // --- 7. Finalize and Submit Commands ---
722        let submission_timer = Stopwatch::new();
723        let command_buffer = command_encoder.finish();
724        device.submit_command_buffer(command_buffer);
725        let submission_ms = submission_timer.elapsed_ms().unwrap_or(0);
726
727        if settings.enable_gpu_timestamps {
728            if let Some(p) = self.gpu_profiler.as_mut() {
729                p.schedule_map_after_submit(self.frame_count);
730            }
731        }
732
733        // --- 8. Present the final image to the screen ---
734        output_surface_texture.present();
735
736        // --- 9. Update final frame statistics ---
737        self.frame_count += 1;
738        if let Some(p) = self.gpu_profiler.as_ref() {
739            self.last_frame_stats.gpu_main_pass_time_ms = p.last_main_pass_ms();
740            self.last_frame_stats.gpu_frame_total_time_ms = p.last_frame_total_ms();
741        }
742        let full_frame_ms = full_frame_timer.elapsed_ms().unwrap_or(0);
743        self.last_frame_stats.frame_number = self.frame_count;
744        self.last_frame_stats.cpu_preparation_time_ms =
745            (full_frame_ms - command_recording_timer.elapsed_ms().unwrap_or(0)) as f32;
746        self.last_frame_stats.cpu_render_submission_time_ms = submission_ms as f32;
747
748        if let Some(monitor) = &self.gpu_monitor {
749            monitor.update_from_frame_stats(&self.last_frame_stats);
750        }
751
752        Ok(self.last_frame_stats.clone())
753    }
754
755    fn render_with_encoder(
756        &mut self,
757        clear_color: khora_core::math::LinearRgba,
758        encoder_fn: Box<
759            dyn FnOnce(
760                    &mut dyn khora_core::renderer::traits::CommandEncoder,
761                    &khora_core::renderer::api::core::RenderContext,
762                ) + Send
763                + '_,
764        >,
765    ) -> Result<RenderStats, RenderError> {
766        use khora_core::renderer::api::core::RenderContext;
767
768        let full_frame_timer = Stopwatch::new();
769
770        let device = self
771            .wgpu_device
772            .clone()
773            .ok_or(RenderError::NotInitialized)?;
774
775        device.poll_device_non_blocking();
776
777        let gc = self
778            .graphics_context_shared
779            .clone()
780            .ok_or(RenderError::NotInitialized)?;
781
782        if let Some(p) = self.gpu_profiler.as_mut() {
783            p.try_read_previous_frame();
784        }
785
786        // --- Handle Pending Resizes ---
787        let mut resized_this_frame = false;
788        if self.pending_resize {
789            self.pending_resize_frames = self.pending_resize_frames.saturating_add(1);
790            if let Some(t) = self.last_resize_event {
791                let quiet_elapsed = t.elapsed().as_millis();
792                let debounce_quiet_ms = 120u128;
793                let max_pending_frames = 10u32;
794                let early_stable = self.stable_size_frame_count >= 3;
795
796                if quiet_elapsed >= debounce_quiet_ms
797                    || self.pending_resize_frames >= max_pending_frames
798                    || early_stable
799                {
800                    if let Ok(mut gc_guard) = gc.lock() {
801                        gc_guard.resize(self.current_width, self.current_height);
802                        self.pending_resize = false;
803                        self.last_surface_config = Some(Instant::now());
804                        self.stable_size_frame_count = 0;
805                        resized_this_frame = true;
806                    }
807                }
808            }
809            if self.pending_resize && !resized_this_frame {
810                return Ok(self.last_frame_stats.clone());
811            }
812        }
813
814        if resized_this_frame {
815            if let Err(e) = self.create_depth_texture() {
816                log::warn!("Failed to recreate depth texture: {:?}", e);
817            }
818        }
819
820        // --- Acquire Frame ---
821        let output_surface_texture = loop {
822            let mut gc_guard = gc.lock().unwrap();
823            match gc_guard.get_current_texture() {
824                Ok(texture) => break texture,
825                Err(e) => {
826                    if self.current_width > 0 && self.current_height > 0 {
827                        gc_guard.resize(self.current_width, self.current_height);
828                        continue;
829                    }
830                    return Err(RenderError::SurfaceAcquisitionFailed(format!("{:?}", e)));
831                }
832            }
833        };
834
835        let command_recording_timer = Stopwatch::new();
836
837        // --- Create texture view ---
838        if let Some(old_id) = self.current_frame_view_id.take() {
839            device.destroy_texture_view(old_id)?;
840        }
841        let target_view_id = device.register_texture_view(
842            &output_surface_texture.texture,
843            Some("Primary Swap Chain View"),
844        )?;
845        self.current_frame_view_id = Some(target_view_id);
846
847        // --- Create encoder ---
848        let mut command_encoder = device.create_command_encoder(Some("Khora Main Command Encoder"));
849
850        // --- Build RenderContext with actual target ---
851        let render_ctx = RenderContext {
852            color_target: &target_view_id,
853            depth_target: self.depth_texture_view.as_ref(),
854            clear_color,
855            shadow_atlas: None,
856            shadow_sampler: None,
857        };
858
859        // --- Call the encoder function (agents do their rendering here) ---
860        encoder_fn(command_encoder.as_mut(), &render_ctx);
861
862        // --- Submit ---
863        let submission_timer = Stopwatch::new();
864        let command_buffer = command_encoder.finish();
865        device.submit_command_buffer(command_buffer);
866        let submission_ms = submission_timer.elapsed_ms().unwrap_or(0);
867
868        // --- Present ---
869        output_surface_texture.present();
870
871        // --- Update stats ---
872        self.frame_count += 1;
873        let full_frame_ms = full_frame_timer.elapsed_ms().unwrap_or(0);
874        self.last_frame_stats.frame_number = self.frame_count;
875        self.last_frame_stats.cpu_preparation_time_ms =
876            (full_frame_ms - command_recording_timer.elapsed_ms().unwrap_or(0)) as f32;
877        self.last_frame_stats.cpu_render_submission_time_ms = submission_ms as f32;
878
879        if let Some(monitor) = &self.gpu_monitor {
880            monitor.update_from_frame_stats(&self.last_frame_stats);
881        }
882
883        Ok(self.last_frame_stats.clone())
884    }
885
886    fn get_last_frame_stats(&self) -> &RenderStats {
887        &self.last_frame_stats
888    }
889
890    fn supports_feature(&self, feature_name: &str) -> bool {
891        self.wgpu_device
892            .as_ref()
893            .is_some_and(|d| d.supports_feature(feature_name))
894    }
895
896    fn shutdown(&mut self) {
897        log::info!("WgpuRenderSystem shutting down...");
898        if let Some(mut profiler) = self.gpu_profiler.take() {
899            if let Some(device) = self.wgpu_device.as_ref() {
900                if let Some(wgpu_profiler) = profiler
901                    .as_any_mut()
902                    .downcast_mut::<WgpuTimestampProfiler>()
903                {
904                    wgpu_profiler.shutdown(device);
905                }
906            }
907        }
908        if let Some(old_id) = self.current_frame_view_id.take() {
909            if let Some(device) = self.wgpu_device.as_ref() {
910                let _ = device.destroy_texture_view(old_id);
911            }
912        }
913        self.wgpu_device = None;
914        self.graphics_context_shared = None;
915        self.gpu_monitor = None;
916    }
917
918    fn as_any(&self) -> &dyn std::any::Any {
919        self
920    }
921
922    fn get_adapter_info(&self) -> Option<GraphicsAdapterInfo> {
923        self.wgpu_device.as_ref().map(|d| d.get_adapter_info())
924    }
925
926    fn graphics_device(&self) -> Arc<dyn GraphicsDevice> {
927        self.wgpu_device
928            .clone()
929            .expect("WgpuRenderSystem: No WgpuDevice available.")
930    }
931}
932
933unsafe impl Send for WgpuRenderSystem {}
934unsafe impl Sync for WgpuRenderSystem {}