1use 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
48pub 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_buffer: Option<BufferId>,
68 camera_bind_group: Option<BindGroupId>,
69 camera_bind_group_layout: Option<BindGroupLayoutId>,
70
71 depth_texture: Option<TextureId>,
73 depth_texture_view: Option<TextureViewId>,
74
75 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 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 self.initialize_camera_uniforms()?;
217
218 self.create_depth_texture()?;
220
221 Ok(created_monitors)
222 }
223
224 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 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 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 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 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 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 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 if self.current_width == 0 || self.current_height == 0 {
353 return Ok(());
354 }
355
356 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 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 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 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 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 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 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 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 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; } 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 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 let mut command_encoder = device.create_command_encoder(Some("Khora Main Command Encoder"));
634
635 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 {
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 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), store: StoreOp::Store,
675 }),
676 stencil_ops: None, 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 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 if settings.enable_gpu_timestamps {
706 if let Some(profiler) = self.gpu_profiler.as_ref() {
707 {
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 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 output_surface_texture.present();
735
736 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 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 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 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 let mut command_encoder = device.create_command_encoder(Some("Khora Main Command Encoder"));
849
850 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 encoder_fn(command_encoder.as_mut(), &render_ctx);
861
862 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 output_surface_texture.present();
870
871 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 {}