1use crate::telemetry::gpu_monitor::GpuMonitor;
18
19use super::backend::WgpuBackendSelector;
20use super::context::WgpuGraphicsContext;
21use super::device::WgpuDevice;
22use super::profiler::WgpuTimestampProfiler;
23use super::resilience::{
24 classify_acquire, surface_is_renderable, AcquireAction, SurfaceAcquireStatus,
25};
26use khora_core::math::LinearRgba;
27use khora_core::platform::window::{KhoraWindow, KhoraWindowHandle};
28use khora_core::renderer::api::command::{
29 BindGroupId, BindGroupLayoutId, LoadOp, Operations, RenderPassColorAttachment,
30 RenderPassDescriptor, StoreOp,
31};
32use khora_core::renderer::api::core::{
33 BackendSelectionConfig, GraphicsAdapterInfo, RenderSettings, RenderStats,
34};
35use khora_core::renderer::api::resource::{
36 BufferId, ImageAspect, TextureDescriptor, TextureDimension, TextureId, TextureUsage,
37 TextureViewDescriptor, TextureViewId, ViewInfo,
38};
39use khora_core::renderer::api::scene::RenderObject;
40use khora_core::renderer::api::util::ShaderStageFlags;
41use khora_core::renderer::api::util::{IndexFormat, SampleCount, TextureFormat};
42use khora_core::renderer::traits::{
43 FrameTargets, GpuProfiler, GraphicsBackendSelector, RenderSystem,
44};
45use khora_core::renderer::{GraphicsDevice, RenderError};
46use khora_core::telemetry::ResourceMonitor;
47use khora_core::Stopwatch;
48use std::fmt;
49use std::sync::{Arc, Mutex};
50use std::time::Instant;
51use winit::dpi::PhysicalSize;
52
53pub struct WgpuRenderSystem {
61 graphics_context_shared: Option<Arc<Mutex<WgpuGraphicsContext>>>,
62 wgpu_device: Option<Arc<WgpuDevice>>,
63 gpu_monitor: Option<Arc<GpuMonitor>>,
64 current_width: u32,
65 current_height: u32,
66 frame_count: u64,
67 last_frame_stats: RenderStats,
68 gpu_profiler: Option<Box<dyn GpuProfiler>>,
69 current_frame_view_id: Option<TextureViewId>,
70
71 camera_uniform_buffer: Option<BufferId>,
73 camera_bind_group: Option<BindGroupId>,
74 camera_bind_group_layout: Option<BindGroupLayoutId>,
75
76 depth_texture: Option<TextureId>,
78 depth_texture_view: Option<TextureViewId>,
79
80 active_frame_texture: Option<wgpu::SurfaceTexture>,
83
84 last_resize_event: Option<Instant>,
86 pending_resize: bool,
87 last_surface_config: Option<Instant>,
88 pending_resize_frames: u32,
89 last_pending_size: Option<(u32, u32)>,
90 stable_size_frame_count: u32,
91
92 viewport_texture: Option<wgpu::Texture>,
94 viewport_view: Option<wgpu::TextureView>,
95 viewport_depth_texture: Option<wgpu::Texture>,
96 viewport_depth_view: Option<wgpu::TextureView>,
97 viewport_width: u32,
98 viewport_height: u32,
99 viewport_color_view_id: Option<khora_core::renderer::api::resource::TextureViewId>,
102 viewport_depth_view_id: Option<khora_core::renderer::api::resource::TextureViewId>,
103 render_to_viewport: bool,
106 }
110
111impl fmt::Debug for WgpuRenderSystem {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 f.debug_struct("WgpuRenderSystem")
114 .field("graphics_context_shared", &self.graphics_context_shared)
115 .field("wgpu_device", &self.wgpu_device)
116 .field("gpu_monitor", &self.gpu_monitor)
117 .field("current_width", &self.current_width)
118 .field("current_height", &self.current_height)
119 .field("frame_count", &self.frame_count)
120 .field("last_frame_stats", &self.last_frame_stats)
121 .field(
122 "gpu_profiler",
123 &self.gpu_profiler.as_ref().map(|_| "GpuProfiler(...)"),
124 )
125 .field("current_frame_view_id", &self.current_frame_view_id)
126 .field(
127 "camera_uniform_buffer",
128 &self.camera_uniform_buffer.as_ref().map(|_| "Buffer(...)"),
129 )
130 .field(
131 "camera_bind_group",
132 &self.camera_bind_group.as_ref().map(|_| "BindGroup(...)"),
133 )
134 .field(
135 "camera_bind_group_layout",
136 &self
137 .camera_bind_group_layout
138 .as_ref()
139 .map(|_| "BindGroupLayout(...)"),
140 )
141 .finish()
142 }
143}
144
145impl Default for WgpuRenderSystem {
146 fn default() -> Self {
147 Self::new()
148 }
149}
150
151impl WgpuRenderSystem {
152 pub fn new() -> Self {
156 log::info!("WgpuRenderSystem created (uninitialized).");
157 Self {
158 graphics_context_shared: None,
159 wgpu_device: None,
160 gpu_monitor: None,
161 current_width: 0,
162 current_height: 0,
163 frame_count: 0,
164 last_frame_stats: RenderStats::default(),
165 gpu_profiler: None,
166 current_frame_view_id: None,
167 camera_uniform_buffer: None,
168 camera_bind_group: None,
169 camera_bind_group_layout: None,
170 depth_texture: None,
171 depth_texture_view: None,
172 active_frame_texture: None,
173 last_resize_event: None,
174 pending_resize: false,
175 last_surface_config: None,
176 pending_resize_frames: 0,
177 last_pending_size: None,
178 stable_size_frame_count: 0,
179 viewport_texture: None,
180 viewport_view: None,
181 viewport_depth_texture: None,
182 viewport_depth_view: None,
183 viewport_width: 0,
184 viewport_height: 0,
185 viewport_color_view_id: None,
186 viewport_depth_view_id: None,
187 render_to_viewport: false,
188 }
189 }
190
191 async fn initialize(
192 &mut self,
193 window_handle: KhoraWindowHandle,
194 window_size: PhysicalSize<u32>,
195 ) -> Result<Vec<Arc<dyn ResourceMonitor>>, RenderError> {
196 if self.graphics_context_shared.is_some() {
197 return Err(RenderError::InitializationFailed(
198 "WgpuRenderSystem is already initialized.".to_string(),
199 ));
200 }
201 log::info!("WgpuRenderSystem: Initializing...");
202
203 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_with_display_handle(
208 Box::new(window_handle.clone()),
209 ));
210 let backend_selector = WgpuBackendSelector::new(instance.clone());
211 let selection_config = BackendSelectionConfig::default();
212
213 let selection_result = backend_selector
214 .select_backend(&selection_config)
215 .await
216 .map_err(|e| RenderError::InitializationFailed(e.to_string()))?;
217 let adapter = selection_result.adapter;
218
219 let context = WgpuGraphicsContext::new(&instance, window_handle, adapter, window_size)
220 .await
221 .map_err(|e| RenderError::InitializationFailed(e.to_string()))?;
222
223 self.current_width = context.get_size().0;
224 self.current_height = context.get_size().1;
225 let context_arc = Arc::new(Mutex::new(context));
226 self.graphics_context_shared = Some(context_arc.clone());
227
228 log::info!(
229 "WgpuRenderSystem: GraphicsContext created with size: {}x{}",
230 self.current_width,
231 self.current_height
232 );
233
234 let graphics_device = WgpuDevice::new(context_arc.clone());
235 let device_arc = Arc::new(graphics_device);
236 self.wgpu_device = Some(device_arc.clone());
237
238 if let Ok(gc_guard) = context_arc.lock() {
239 if WgpuTimestampProfiler::feature_available(gc_guard.active_device_features) {
240 if let Some(mut profiler) = WgpuTimestampProfiler::new(&gc_guard.device) {
241 let period = gc_guard.queue.get_timestamp_period();
242 profiler.set_timestamp_period(period);
243 self.gpu_profiler = Some(Box::new(profiler));
244 }
245 } else {
246 log::info!("GPU timestamp feature not available; instrumentation disabled.");
247 }
248 }
249
250 let mut created_monitors: Vec<Arc<dyn ResourceMonitor>> = Vec::new();
251 let gpu_monitor = Arc::new(GpuMonitor::new("WGPU".to_string()));
252 created_monitors.push(gpu_monitor.clone());
253 self.gpu_monitor = Some(gpu_monitor);
254
255 let vram_monitor = device_arc as Arc<dyn ResourceMonitor>;
256 created_monitors.push(vram_monitor);
257
258 self.initialize_camera_uniforms()?;
260
261 self.create_depth_texture()?;
263
264 Ok(created_monitors)
265 }
266
267 fn initialize_camera_uniforms(&mut self) -> Result<(), RenderError> {
274 use khora_core::renderer::api::command::{
275 BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry,
276 BindingResource, BindingType, BufferBinding, BufferBindingType,
277 };
278 use khora_core::renderer::api::resource::{
279 BufferDescriptor, BufferUsage, CameraUniformData,
280 };
281
282 let device = self.wgpu_device.as_ref().ok_or_else(|| {
283 RenderError::InitializationFailed("WGPU device not initialized".to_string())
284 })?;
285
286 let buffer_size = std::mem::size_of::<CameraUniformData>() as u64;
287
288 let buffer_descriptor = BufferDescriptor {
290 label: Some(std::borrow::Cow::Borrowed("Camera Uniform Buffer")),
291 size: buffer_size,
292 usage: BufferUsage::UNIFORM | BufferUsage::COPY_DST,
293 mapped_at_creation: false,
294 };
295
296 let uniform_buffer = device.create_buffer(&buffer_descriptor).map_err(|e| {
297 RenderError::InitializationFailed(format!(
298 "Failed to create camera uniform buffer: {:?}",
299 e
300 ))
301 })?;
302
303 let layout_entry = BindGroupLayoutEntry {
305 binding: 0,
306 visibility: ShaderStageFlags::VERTEX | ShaderStageFlags::FRAGMENT,
307 ty: BindingType::Buffer {
308 ty: BufferBindingType::Uniform,
309 has_dynamic_offset: false,
310 min_binding_size: None,
311 },
312 };
313
314 let layout_descriptor = BindGroupLayoutDescriptor {
315 label: Some("Camera Bind Group Layout"),
316 entries: &[layout_entry],
317 };
318
319 let bind_group_layout = device
320 .create_bind_group_layout(&layout_descriptor)
321 .map_err(|e| {
322 RenderError::InitializationFailed(format!(
323 "Failed to create camera bind group layout: {:?}",
324 e
325 ))
326 })?;
327
328 let bind_group_entry = BindGroupEntry {
330 binding: 0,
331 resource: BindingResource::Buffer(BufferBinding {
332 buffer: uniform_buffer,
333 offset: 0,
334 size: None,
335 }),
336 _phantom: std::marker::PhantomData,
337 };
338
339 let bind_group_descriptor = BindGroupDescriptor {
340 label: Some("Camera Bind Group"),
341 layout: bind_group_layout,
342 entries: &[bind_group_entry],
343 };
344
345 let bind_group = device
346 .create_bind_group(&bind_group_descriptor)
347 .map_err(|e| {
348 RenderError::InitializationFailed(format!(
349 "Failed to create camera bind group: {:?}",
350 e
351 ))
352 })?;
353
354 self.camera_uniform_buffer = Some(uniform_buffer);
355 self.camera_bind_group_layout = Some(bind_group_layout);
356 self.camera_bind_group = Some(bind_group);
357
358 log::info!("Camera uniform resources initialized with abstract API");
359
360 Ok(())
361 }
362
363 fn update_camera_uniforms(&mut self, view_info: &ViewInfo) {
368 use khora_core::renderer::api::resource::CameraUniformData;
369
370 let uniform_data = CameraUniformData::from_view_info(view_info);
371
372 if let (Some(device), Some(buffer_id)) = (&self.wgpu_device, &self.camera_uniform_buffer) {
373 if let Err(e) =
375 device.write_buffer(*buffer_id, 0, bytemuck::cast_slice(&[uniform_data]))
376 {
377 log::warn!("Failed to write camera uniform data: {:?}", e);
378 }
379 }
380 }
381
382 fn create_depth_texture(&mut self) -> Result<(), RenderError> {
387 use khora_core::math::Extent3D;
388 use std::borrow::Cow;
389
390 let device = self.wgpu_device.as_ref().ok_or_else(|| {
391 RenderError::InitializationFailed("WGPU device not initialized".to_string())
392 })?;
393
394 if self.current_width == 0 || self.current_height == 0 {
396 return Ok(());
397 }
398
399 if let Some(old_view) = self.depth_texture_view.take() {
401 let _ = device.destroy_texture_view(old_view);
402 }
403 if let Some(old_tex) = self.depth_texture.take() {
404 let _ = device.destroy_texture(old_tex);
405 }
406
407 let texture_desc = TextureDescriptor {
409 label: Some(Cow::Borrowed("Depth Texture")),
410 size: Extent3D {
411 width: self.current_width,
412 height: self.current_height,
413 depth_or_array_layers: 1,
414 },
415 mip_level_count: 1,
416 sample_count: SampleCount::X1,
417 dimension: TextureDimension::D2,
418 format: TextureFormat::Depth32Float,
419 usage: TextureUsage::RENDER_ATTACHMENT,
420 view_formats: Cow::Borrowed(&[]),
421 };
422
423 let texture_id = device.create_texture(&texture_desc).map_err(|e| {
424 RenderError::InitializationFailed(format!("Failed to create depth texture: {:?}", e))
425 })?;
426
427 let view_desc = TextureViewDescriptor {
429 label: Some(Cow::Borrowed("Depth Texture View")),
430 format: Some(TextureFormat::Depth32Float),
431 dimension: None,
432 aspect: ImageAspect::DepthOnly,
433 base_mip_level: 0,
434 mip_level_count: None,
435 base_array_layer: 0,
436 array_layer_count: None,
437 };
438
439 let view_id = device
440 .create_texture_view(texture_id, &view_desc)
441 .map_err(|e| {
442 RenderError::InitializationFailed(format!(
443 "Failed to create depth texture view: {:?}",
444 e
445 ))
446 })?;
447
448 self.depth_texture = Some(texture_id);
449 self.depth_texture_view = Some(view_id);
450
451 log::info!(
452 "Depth texture created: {}x{} (Depth32Float)",
453 self.current_width,
454 self.current_height
455 );
456
457 Ok(())
458 }
459
460 pub fn create_viewport_target(
466 &mut self,
467 width: u32,
468 height: u32,
469 overlay: &mut crate::ui::egui::overlay::EguiOverlay,
470 ) -> Result<egui::TextureId, RenderError> {
471 let gc = self
472 .graphics_context_shared
473 .as_ref()
474 .ok_or(RenderError::NotInitialized)?
475 .lock()
476 .map_err(|_| RenderError::Internal("Context lock poisoned".into()))?;
477
478 let format = wgpu::TextureFormat::Rgba8UnormSrgb;
480
481 let color_tex = gc.device.create_texture(&wgpu::TextureDescriptor {
483 label: Some("viewport_color"),
484 size: wgpu::Extent3d {
485 width,
486 height,
487 depth_or_array_layers: 1,
488 },
489 mip_level_count: 1,
490 sample_count: 1,
491 dimension: wgpu::TextureDimension::D2,
492 format,
493 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
494 view_formats: &[],
495 });
496 let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
497
498 let depth_tex = gc.device.create_texture(&wgpu::TextureDescriptor {
500 label: Some("viewport_depth"),
501 size: wgpu::Extent3d {
502 width,
503 height,
504 depth_or_array_layers: 1,
505 },
506 mip_level_count: 1,
507 sample_count: 1,
508 dimension: wgpu::TextureDimension::D2,
509 format: wgpu::TextureFormat::Depth32Float,
510 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
511 view_formats: &[],
512 });
513 let depth_view = depth_tex.create_view(&wgpu::TextureViewDescriptor::default());
514
515 let egui_id = overlay.register_viewport_texture(&gc.device, &color_view);
517
518 let device = self
519 .wgpu_device
520 .clone()
521 .ok_or(RenderError::NotInitialized)?;
522 let viewport_color_vid = device
523 .register_texture_view(&color_tex, Some("viewport_color_view"))
524 .map_err(|e| RenderError::Internal(format!("register viewport color view: {e}")))?;
525 let viewport_depth_vid = device
526 .register_texture_view(&depth_tex, Some("viewport_depth_view"))
527 .map_err(|e| RenderError::Internal(format!("register viewport depth view: {e}")))?;
528
529 self.viewport_texture = Some(color_tex);
530 self.viewport_view = Some(color_view);
531 self.viewport_depth_texture = Some(depth_tex);
532 self.viewport_depth_view = Some(depth_view);
533 self.viewport_width = width;
534 self.viewport_height = height;
535 self.viewport_color_view_id = Some(viewport_color_vid);
536 self.viewport_depth_view_id = Some(viewport_depth_vid);
537
538 log::info!("Viewport target created: {width}x{height} ({format:?})");
539
540 Ok(egui_id)
541 }
542
543 pub fn render_viewport_clear(&mut self, clear_color: LinearRgba) -> Result<(), RenderError> {
549 let color_view = self
550 .viewport_view
551 .as_ref()
552 .ok_or(RenderError::NotInitialized)?;
553 let depth_view = self
554 .viewport_depth_view
555 .as_ref()
556 .ok_or(RenderError::NotInitialized)?;
557
558 let gc = self
559 .graphics_context_shared
560 .as_ref()
561 .ok_or(RenderError::NotInitialized)?
562 .lock()
563 .map_err(|_| RenderError::Internal("Context lock poisoned".into()))?;
564
565 let mut encoder = gc
566 .device
567 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
568 label: Some("viewport_clear_encoder"),
569 });
570
571 {
572 let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
573 label: Some("viewport_clear_pass"),
574 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
575 view: color_view,
576 resolve_target: None,
577 ops: wgpu::Operations {
578 load: wgpu::LoadOp::Clear(wgpu::Color {
579 r: clear_color.r as f64,
580 g: clear_color.g as f64,
581 b: clear_color.b as f64,
582 a: clear_color.a as f64,
583 }),
584 store: wgpu::StoreOp::Store,
585 },
586 depth_slice: None,
587 })],
588 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
589 view: depth_view,
590 depth_ops: Some(wgpu::Operations {
591 load: wgpu::LoadOp::Clear(1.0),
592 store: wgpu::StoreOp::Store,
593 }),
594 stencil_ops: None,
595 }),
596 timestamp_writes: None,
597 occlusion_query_set: None,
598 multiview_mask: None,
599 });
600 }
602
603 gc.queue.submit(std::iter::once(encoder.finish()));
604 Ok(())
605 }
606
607 pub fn viewport_size(&self) -> (u32, u32) {
609 (self.viewport_width, self.viewport_height)
610 }
611
612 pub fn create_editor_overlay(
616 &self,
617 event_loop: &winit::event_loop::ActiveEventLoop,
618 shader_source: &str,
619 ) -> Result<crate::ui::egui::overlay::EguiOverlay, RenderError> {
620 let gc = self
621 .graphics_context_shared
622 .as_ref()
623 .ok_or(RenderError::NotInitialized)?
624 .lock()
625 .map_err(|_| RenderError::Internal("Context lock poisoned".into()))?;
626
627 Ok(crate::ui::egui::overlay::EguiOverlay::new(
628 event_loop,
629 gc.surface_config.format,
630 &gc.device,
631 shader_source,
632 ))
633 }
634
635 pub fn create_editor_overlay_and_shell(
643 &mut self,
644 event_loop: &winit::event_loop::ActiveEventLoop,
645 shader_source: &str,
646 theme: khora_core::ui::UiTheme,
647 viewport_handle: khora_core::ui::editor::viewport_texture::ViewportTextureHandle,
648 ) -> Result<
649 (
650 crate::ui::egui::overlay::EguiOverlay,
651 crate::ui::egui::shell::EguiEditorShell,
652 ),
653 RenderError,
654 > {
655 let mut overlay = self.create_editor_overlay(event_loop, shader_source)?;
656
657 let egui_id = self.create_viewport_target(800, 600, &mut overlay)?;
659
660 let mut shell = crate::ui::egui::shell::EguiEditorShell::new(overlay.context(), theme);
665 shell.register_viewport_texture(viewport_handle, egui_id);
666
667 Ok((overlay, shell))
668 }
669
670 fn check_device_health(&self) -> Result<(), RenderError> {
677 let Some(device) = self.wgpu_device.as_ref() else {
678 return Ok(());
679 };
680 if device.is_device_out_of_memory() {
681 return Err(RenderError::DeviceOutOfMemory(
682 "device reported out-of-memory via wgpu error callback".to_string(),
683 ));
684 }
685 if device.is_device_lost() {
686 return Err(RenderError::DeviceLost);
687 }
688 Ok(())
689 }
690
691 fn acquire_surface_texture(
704 &mut self,
705 gc: &Arc<Mutex<WgpuGraphicsContext>>,
706 ) -> Result<Option<wgpu::SurfaceTexture>, RenderError> {
707 if !surface_is_renderable(self.current_width, self.current_height) {
710 log::debug!(
711 "WgpuRenderSystem: surface not renderable ({}x{}); skipping frame.",
712 self.current_width,
713 self.current_height
714 );
715 return Ok(None);
716 }
717
718 let max_attempts = 2;
721 for attempt in 0..max_attempts {
722 let mut gc_guard = gc
723 .lock()
724 .map_err(|_| RenderError::Internal("graphics context lock poisoned".into()))?;
725
726 let (status, texture) = match gc_guard.get_current_texture() {
727 wgpu::CurrentSurfaceTexture::Success(t)
728 | wgpu::CurrentSurfaceTexture::Suboptimal(t) => {
729 (SurfaceAcquireStatus::Usable, Some(t))
730 }
731 wgpu::CurrentSurfaceTexture::Lost | wgpu::CurrentSurfaceTexture::Outdated => {
732 (SurfaceAcquireStatus::LostOrOutdated, None)
733 }
734 wgpu::CurrentSurfaceTexture::Timeout => (SurfaceAcquireStatus::Timeout, None),
735 wgpu::CurrentSurfaceTexture::Occluded => (SurfaceAcquireStatus::Occluded, None),
736 wgpu::CurrentSurfaceTexture::Validation => (SurfaceAcquireStatus::Validation, None),
737 #[allow(unreachable_patterns)]
742 _ => (SurfaceAcquireStatus::Unknown, None),
743 };
744
745 let has_valid_size = surface_is_renderable(self.current_width, self.current_height);
746 match classify_acquire(status, has_valid_size) {
747 AcquireAction::Proceed => {
748 return Ok(texture);
750 }
751 AcquireAction::ReconfigureAndRetry => {
752 log::warn!(
753 "WgpuRenderSystem: surface lost/outdated; reconfiguring to {}x{} (attempt {}).",
754 self.current_width,
755 self.current_height,
756 attempt + 1
757 );
758 gc_guard.resize(self.current_width, self.current_height);
759 drop(gc_guard);
760 self.last_surface_config = Some(Instant::now());
761 self.pending_resize = false;
762 continue;
764 }
765 AcquireAction::SkipFrame => {
766 log::debug!(
767 "WgpuRenderSystem: acquire status {:?} — skipping frame.",
768 status
769 );
770 return Ok(None);
771 }
772 AcquireAction::NonFatalError => {
773 log::error!(
774 "WgpuRenderSystem: non-fatal surface acquire failure ({:?}).",
775 status
776 );
777 return Err(RenderError::SurfaceAcquisitionFailed(format!("{status:?}")));
778 }
779 }
780 }
781
782 log::warn!(
786 "WgpuRenderSystem: surface still unavailable after {} acquire attempts; skipping frame.",
787 max_attempts
788 );
789 Ok(None)
790 }
791}
792
793impl RenderSystem for WgpuRenderSystem {
794 fn init(
795 &mut self,
796 window: &dyn KhoraWindow,
797 ) -> Result<Vec<Arc<dyn ResourceMonitor>>, RenderError> {
798 let (width, height) = window.inner_size();
799 let window_size = PhysicalSize::new(width, height);
800 let window_handle_arc = window.clone_handle_arc();
801 pollster::block_on(self.initialize(window_handle_arc, window_size))
802 }
803
804 fn resize(&mut self, new_width: u32, new_height: u32) {
805 if new_width > 0 && new_height > 0 {
806 log::debug!(
807 "WgpuRenderSystem: resize_surface called with W:{new_width}, H:{new_height}"
808 );
809 self.current_width = new_width;
810 self.current_height = new_height;
811 let now = Instant::now();
812 if let Some((lw, lh)) = self.last_pending_size {
813 if lw == new_width && lh == new_height {
814 self.stable_size_frame_count = self.stable_size_frame_count.saturating_add(1);
815 } else {
816 self.stable_size_frame_count = 0;
817 }
818 }
819 self.last_pending_size = Some((new_width, new_height));
820
821 let immediate_threshold_ms: u128 = 80;
822 let can_immediate = self
823 .last_surface_config
824 .map(|t| t.elapsed().as_millis() >= immediate_threshold_ms)
825 .unwrap_or(true);
826 let early_stable = self.stable_size_frame_count >= 2
827 && self
828 .last_surface_config
829 .map(|t| t.elapsed().as_millis() >= 20)
830 .unwrap_or(true);
831 if can_immediate || early_stable {
832 let mut did_resize = false;
833 if let Some(gc_arc_mutex) = &self.graphics_context_shared {
834 if let Ok(mut gc_guard) = gc_arc_mutex.lock() {
835 gc_guard.resize(self.current_width, self.current_height);
836 self.last_surface_config = Some(now);
837 self.pending_resize = false;
838 self.pending_resize_frames = 0;
839 did_resize = true;
840 }
841 }
842 if did_resize {
843 if let Err(e) = self.create_depth_texture() {
845 log::warn!("Failed to recreate depth texture during resize: {:?}", e);
846 }
847 log::info!(
848 "WGPUGraphicsContext: Immediate/Early surface configuration to {}x{}",
849 self.current_width,
850 self.current_height
851 );
852 return;
853 }
854 }
855 self.last_resize_event = Some(now);
856 self.pending_resize = true;
857 self.pending_resize_frames = 0;
858 } else {
859 log::warn!(
860 "WgpuRenderSystem::resize_surface called with zero size ({new_width}, {new_height}). Ignoring."
861 );
862 }
863 }
864
865 fn prepare_frame(&mut self, view_info: &ViewInfo) {
866 if self.graphics_context_shared.is_none() {
867 return;
868 }
869 let stopwatch = Stopwatch::new();
870
871 self.update_camera_uniforms(view_info);
873
874 self.last_frame_stats.cpu_preparation_time_ms = stopwatch.elapsed_ms().unwrap_or(0) as f32;
875 }
876
877 fn render(
878 &mut self,
879 renderables: &[RenderObject],
880 _view_info: &ViewInfo,
881 settings: &RenderSettings,
882 ) -> Result<RenderStats, RenderError> {
883 let full_frame_timer = Stopwatch::new();
884
885 let device = self
886 .wgpu_device
887 .clone()
888 .ok_or(RenderError::NotInitialized)?;
889
890 self.check_device_health()?;
892
893 device.poll_device_non_blocking();
896
897 let gc = self
898 .graphics_context_shared
899 .clone()
900 .ok_or(RenderError::NotInitialized)?;
901
902 if let Some(p) = self.gpu_profiler.as_mut() {
903 p.try_read_previous_frame();
904 }
905
906 let mut resized_this_frame = false;
908 if self.pending_resize {
909 self.pending_resize_frames = self.pending_resize_frames.saturating_add(1);
910 if let Some(t) = self.last_resize_event {
911 let quiet_elapsed = t.elapsed().as_millis();
912 let debounce_quiet_ms = settings.resize_debounce_ms as u128;
913 let max_pending_frames = settings.resize_max_pending_frames;
914 let early_stable = self.stable_size_frame_count >= 3;
915
916 if quiet_elapsed >= debounce_quiet_ms
917 || self.pending_resize_frames >= max_pending_frames
918 || early_stable
919 {
920 if let Ok(mut gc_guard) = gc.lock() {
921 gc_guard.resize(self.current_width, self.current_height);
922 self.pending_resize = false;
923 self.last_surface_config = Some(Instant::now());
924 self.stable_size_frame_count = 0;
925 resized_this_frame = true;
926 log::info!(
927 "Deferred surface configuration to {}x{}",
928 self.current_width,
929 self.current_height
930 );
931 }
932 }
933 }
934 if self.pending_resize && !resized_this_frame {
935 return Ok(self.last_frame_stats.clone());
936 }
937 }
938
939 if resized_this_frame {
941 if let Err(e) = self.create_depth_texture() {
942 log::warn!(
943 "Failed to recreate depth texture during deferred resize: {:?}",
944 e
945 );
946 }
947 }
948
949 device.wait_for_last_submission();
951 let output_surface_texture = match self.acquire_surface_texture(&gc)? {
952 Some(texture) => texture,
953 None => {
954 return Ok(self.last_frame_stats.clone());
957 }
958 };
959
960 let command_recording_timer = Stopwatch::new();
961
962 if let Some(old_id) = self.current_frame_view_id.take() {
964 device.destroy_texture_view(old_id)?;
965 }
966 let target_view_id = device.register_texture_view(
967 &output_surface_texture.texture,
968 Some("Primary Swap Chain View"),
969 )?;
970 self.current_frame_view_id = Some(target_view_id);
971
972 let mut command_encoder = device.create_command_encoder(Some("Khora Main Command Encoder"));
974
975 if settings.enable_gpu_timestamps {
977 if let Some(profiler) = self.gpu_profiler.as_ref() {
978 let _pass_a = command_encoder.begin_profiler_compute_pass(
979 Some("Timestamp Pass A"),
980 profiler.as_ref(),
981 0,
982 );
983 }
984 }
985
986 {
988 let gc_guard = gc
989 .lock()
990 .map_err(|_| RenderError::Internal("graphics context lock poisoned".into()))?;
991 let wgpu_color = gc_guard.get_clear_color();
992 let clear_color = LinearRgba::new(
993 wgpu_color.r as f32,
994 wgpu_color.g as f32,
995 wgpu_color.b as f32,
996 wgpu_color.a as f32,
997 );
998
999 let color_attachment = RenderPassColorAttachment {
1000 view: &target_view_id,
1001 resolve_target: None,
1002 ops: Operations {
1003 load: LoadOp::Clear(clear_color),
1004 store: StoreOp::Store,
1005 },
1006 base_array_layer: 0,
1007 base_mip_level: 0,
1008 };
1009
1010 use khora_core::renderer::api::command::RenderPassDepthStencilAttachment;
1012 let depth_attachment = self.depth_texture_view.as_ref().map(|depth_view| {
1013 RenderPassDepthStencilAttachment {
1014 view: depth_view,
1015 depth_ops: Some(Operations {
1016 load: LoadOp::Clear(1.0), store: StoreOp::Store,
1018 }),
1019 stencil_ops: None, base_array_layer: 0,
1021 }
1022 });
1023
1024 let pass_descriptor = RenderPassDescriptor {
1025 label: Some("Khora Main Abstract Render Pass"),
1026 color_attachments: &[color_attachment],
1027 depth_stencil_attachment: depth_attachment,
1028 };
1029
1030 let mut render_pass = command_encoder.begin_render_pass(&pass_descriptor);
1031
1032 if let Some(camera_bind_group) = &self.camera_bind_group {
1034 render_pass.set_bind_group(0, camera_bind_group, &[]);
1035 }
1036 let (draw_calls, triangles) = renderables.iter().fold((0, 0), |(dc, tris), obj| {
1037 render_pass.set_pipeline(&obj.pipeline);
1038 render_pass.set_vertex_buffer(0, &obj.vertex_buffer, 0);
1039 render_pass.set_index_buffer(&obj.index_buffer, 0, IndexFormat::Uint16);
1040 render_pass.draw_indexed(0..obj.index_count, 0, 0..1);
1041 (dc + 1, tris + obj.index_count / 3)
1042 });
1043 self.last_frame_stats.draw_calls = draw_calls;
1044 self.last_frame_stats.triangles_rendered = triangles;
1045 }
1046
1047 if settings.enable_gpu_timestamps {
1049 if let Some(profiler) = self.gpu_profiler.as_ref() {
1050 {
1053 let _pass_b = command_encoder.begin_profiler_compute_pass(
1054 Some("Timestamp Pass B"),
1055 profiler.as_ref(),
1056 1,
1057 );
1058 }
1059 profiler.resolve_and_copy(command_encoder.as_mut());
1060 profiler.copy_to_staging(command_encoder.as_mut(), self.frame_count);
1061 }
1062 }
1063
1064 let submission_timer = Stopwatch::new();
1066 let submission_ms = match command_encoder.finish() {
1067 Some(command_buffer) => {
1068 device.submit_command_buffer(command_buffer);
1069 submission_timer.elapsed_ms().unwrap_or(0)
1070 }
1071 None => {
1072 log::error!(
1073 "WgpuRenderSystem: command_encoder.finish() returned None — skipping submit"
1074 );
1075 0
1076 }
1077 };
1078
1079 if settings.enable_gpu_timestamps {
1080 if let Some(p) = self.gpu_profiler.as_mut() {
1081 p.schedule_map_after_submit(self.frame_count);
1082 }
1083 }
1084
1085 output_surface_texture.present();
1087
1088 self.frame_count += 1;
1090 if let Some(p) = self.gpu_profiler.as_ref() {
1091 self.last_frame_stats.gpu_main_pass_time_ms = p.last_main_pass_ms();
1092 self.last_frame_stats.gpu_frame_total_time_ms = p.last_frame_total_ms();
1093 }
1094 let full_frame_ms = full_frame_timer.elapsed_ms().unwrap_or(0);
1095 self.last_frame_stats.frame_number = self.frame_count;
1096 self.last_frame_stats.cpu_preparation_time_ms =
1097 (full_frame_ms - command_recording_timer.elapsed_ms().unwrap_or(0)) as f32;
1098 self.last_frame_stats.cpu_render_submission_time_ms = submission_ms as f32;
1099
1100 if let Some(monitor) = &self.gpu_monitor {
1101 monitor.update_from_frame_stats(&self.last_frame_stats);
1102 }
1103
1104 Ok(self.last_frame_stats.clone())
1105 }
1106
1107 fn begin_frame(&mut self) -> Result<FrameTargets, RenderError> {
1108 let device = self
1109 .wgpu_device
1110 .clone()
1111 .ok_or(RenderError::NotInitialized)?;
1112
1113 self.check_device_health()?;
1116
1117 device.poll_device_non_blocking();
1119 device.wait_for_last_submission();
1122
1123 let gc = self
1124 .graphics_context_shared
1125 .clone()
1126 .ok_or(RenderError::NotInitialized)?;
1127
1128 if let Some(p) = self.gpu_profiler.as_mut() {
1129 p.try_read_previous_frame();
1130 }
1131
1132 let mut resized_this_frame = false;
1134 if self.pending_resize {
1135 self.pending_resize_frames = self.pending_resize_frames.saturating_add(1);
1136 if let Some(t) = self.last_resize_event {
1137 let quiet_elapsed = t.elapsed().as_millis();
1138 let debounce_quiet_ms = 120u128;
1139 let max_pending_frames = 10u32;
1140 let early_stable = self.stable_size_frame_count >= 3;
1141
1142 if quiet_elapsed >= debounce_quiet_ms
1143 || self.pending_resize_frames >= max_pending_frames
1144 || early_stable
1145 {
1146 if let Ok(mut gc_guard) = gc.lock() {
1147 gc_guard.resize(self.current_width, self.current_height);
1148 self.pending_resize = false;
1149 self.last_surface_config = Some(Instant::now());
1150 self.stable_size_frame_count = 0;
1151 resized_this_frame = true;
1152 }
1153 }
1154 }
1155 }
1156
1157 if resized_this_frame {
1158 if let Err(e) = self.create_depth_texture() {
1159 log::warn!("Failed to recreate depth texture: {:?}", e);
1160 }
1161 }
1162
1163 let output_surface_texture = match self.acquire_surface_texture(&gc)? {
1165 Some(texture) => texture,
1166 None => {
1167 return Err(RenderError::SurfaceAcquisitionFailed(
1171 "frame skipped (surface not ready)".to_string(),
1172 ));
1173 }
1174 };
1175
1176 if let Some(old_id) = self.current_frame_view_id.take() {
1178 device.destroy_texture_view(old_id)?;
1179 }
1180 let target_view_id = device.register_texture_view(
1181 &output_surface_texture.texture,
1182 Some("Primary Swap Chain View"),
1183 )?;
1184 self.current_frame_view_id = Some(target_view_id);
1185
1186 self.active_frame_texture = Some(output_surface_texture);
1187
1188 let (color, depth) = if self.render_to_viewport {
1190 let c = self
1191 .viewport_color_view_id
1192 .ok_or_else(|| RenderError::Internal("viewport color view not created".into()))?;
1193 (c, self.viewport_depth_view_id)
1194 } else {
1195 (target_view_id, self.depth_texture_view)
1196 };
1197
1198 Ok(FrameTargets { color, depth })
1199 }
1200
1201 fn end_frame(&mut self) -> Result<RenderStats, RenderError> {
1202 self.check_device_health()?;
1205
1206 if let Some(texture) = self.active_frame_texture.take() {
1211 texture.present();
1212 }
1213
1214 self.frame_count += 1;
1215 self.last_frame_stats.frame_number = self.frame_count;
1216
1217 if let Some(monitor) = &self.gpu_monitor {
1218 monitor.update_from_frame_stats(&self.last_frame_stats);
1219 }
1220
1221 Ok(self.last_frame_stats.clone())
1222 }
1223
1224 fn render_overlay(
1225 &mut self,
1226 overlay: &mut dyn khora_core::ui::EditorOverlay,
1227 screen: khora_core::ui::OverlayScreenDescriptor,
1228 ) -> Result<(), RenderError> {
1229 let gc_arc = self
1230 .graphics_context_shared
1231 .as_ref()
1232 .ok_or(RenderError::NotInitialized)?
1233 .clone();
1234
1235 let (encoder, target_view) = {
1237 let gc = gc_arc
1238 .lock()
1239 .map_err(|_| RenderError::Internal("Context lock poisoned".into()))?;
1240
1241 let surface_tex = self
1242 .active_frame_texture
1243 .as_ref()
1244 .ok_or(RenderError::NotInitialized)?;
1245
1246 let target_view = surface_tex
1247 .texture
1248 .create_view(&wgpu::TextureViewDescriptor::default());
1249
1250 let encoder = gc
1251 .device
1252 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1253 label: Some("egui_overlay_encoder"),
1254 });
1255
1256 (encoder, target_view)
1257 }; let mut render_state = crate::ui::egui::overlay::EguiFrameRenderState {
1260 graphics_context: gc_arc.clone(),
1261 encoder: Some(encoder),
1262 target_view,
1263 width_px: screen.width_px,
1264 height_px: screen.height_px,
1265 };
1266
1267 overlay
1268 .end_frame_and_render(&mut render_state as &mut dyn std::any::Any)
1269 .map_err(|e| RenderError::RenderingFailed(e.to_string()))?;
1270
1271 let encoder = render_state.encoder.take().ok_or_else(|| {
1273 RenderError::RenderingFailed("Encoder consumed during overlay render".into())
1274 })?;
1275
1276 let gc = gc_arc
1277 .lock()
1278 .map_err(|_| RenderError::Internal("Context lock poisoned".into()))?;
1279 gc.queue.submit(std::iter::once(encoder.finish()));
1280
1281 Ok(())
1282 }
1283
1284 fn get_last_frame_stats(&self) -> &RenderStats {
1285 &self.last_frame_stats
1286 }
1287
1288 fn supports_feature(&self, feature_name: &str) -> bool {
1289 self.wgpu_device
1290 .as_ref()
1291 .is_some_and(|d| d.supports_feature(feature_name))
1292 }
1293
1294 fn shutdown(&mut self) {
1295 log::info!("WgpuRenderSystem shutting down...");
1296 if let Some(mut profiler) = self.gpu_profiler.take() {
1297 if let Some(device) = self.wgpu_device.as_ref() {
1298 if let Some(wgpu_profiler) = profiler
1299 .as_any_mut()
1300 .downcast_mut::<WgpuTimestampProfiler>()
1301 {
1302 wgpu_profiler.shutdown(device);
1303 }
1304 }
1305 }
1306 if let Some(old_id) = self.current_frame_view_id.take() {
1307 if let Some(device) = self.wgpu_device.as_ref() {
1308 let _ = device.destroy_texture_view(old_id);
1309 }
1310 }
1311 self.wgpu_device = None;
1312 self.graphics_context_shared = None;
1313 self.gpu_monitor = None;
1314 }
1315
1316 fn as_any(&self) -> &dyn std::any::Any {
1317 self
1318 }
1319
1320 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
1321 self
1322 }
1323
1324 fn render_to_viewport(&self) -> bool {
1325 self.render_to_viewport
1326 }
1327
1328 fn set_render_to_viewport(&mut self, enabled: bool) {
1329 self.render_to_viewport = enabled;
1330 }
1331
1332 fn get_adapter_info(&self) -> Option<GraphicsAdapterInfo> {
1333 self.wgpu_device.as_ref().map(|d| d.get_adapter_info())
1334 }
1335
1336 fn graphics_device(&self) -> Arc<dyn GraphicsDevice> {
1337 self.wgpu_device.clone().expect(
1346 "WgpuRenderSystem::graphics_device called before initialization or after shutdown",
1347 )
1348 }
1349}
1350
1351unsafe impl Send for WgpuRenderSystem {}
1352unsafe impl Sync for WgpuRenderSystem {}