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 LoadOp, RenderPassColorAttachment, RenderPassDescriptor, StoreOp,
27};
28use khora_core::renderer::api::texture::TextureViewId;
29use khora_core::renderer::traits::{GpuProfiler, GraphicsBackendSelector};
30use khora_core::renderer::{
31 BackendSelectionConfig, BindGroupId, BindGroupLayoutId, BufferId, GraphicsDevice, IndexFormat,
32 Operations, RenderError, RenderObject, RenderSettings, RenderStats, RenderSystem,
33 RendererAdapterInfo, ViewInfo,
34};
35use khora_core::telemetry::ResourceMonitor;
36use khora_core::Stopwatch;
37use std::fmt;
38use std::sync::{Arc, Mutex};
39use std::time::Instant;
40use winit::dpi::PhysicalSize;
41
42pub struct WgpuRenderSystem {
50 graphics_context_shared: Option<Arc<Mutex<WgpuGraphicsContext>>>,
51 wgpu_device: Option<Arc<WgpuDevice>>,
52 gpu_monitor: Option<Arc<GpuMonitor>>,
53 current_width: u32,
54 current_height: u32,
55 frame_count: u64,
56 last_frame_stats: RenderStats,
57 gpu_profiler: Option<Box<dyn GpuProfiler>>,
58 current_frame_view_id: Option<TextureViewId>,
59
60 camera_uniform_buffer: Option<BufferId>,
62 camera_bind_group: Option<BindGroupId>,
63 camera_bind_group_layout: Option<BindGroupLayoutId>,
64
65 last_resize_event: Option<Instant>,
67 pending_resize: bool,
68 last_surface_config: Option<Instant>,
69 pending_resize_frames: u32,
70 last_pending_size: Option<(u32, u32)>,
71 stable_size_frame_count: u32,
72}
73
74impl fmt::Debug for WgpuRenderSystem {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 f.debug_struct("WgpuRenderSystem")
77 .field("graphics_context_shared", &self.graphics_context_shared)
78 .field("wgpu_device", &self.wgpu_device)
79 .field("gpu_monitor", &self.gpu_monitor)
80 .field("current_width", &self.current_width)
81 .field("current_height", &self.current_height)
82 .field("frame_count", &self.frame_count)
83 .field("last_frame_stats", &self.last_frame_stats)
84 .field(
85 "gpu_profiler",
86 &self.gpu_profiler.as_ref().map(|_| "GpuProfiler(...)"),
87 )
88 .field("current_frame_view_id", &self.current_frame_view_id)
89 .field(
90 "camera_uniform_buffer",
91 &self.camera_uniform_buffer.as_ref().map(|_| "Buffer(...)"),
92 )
93 .field(
94 "camera_bind_group",
95 &self.camera_bind_group.as_ref().map(|_| "BindGroup(...)"),
96 )
97 .field(
98 "camera_bind_group_layout",
99 &self
100 .camera_bind_group_layout
101 .as_ref()
102 .map(|_| "BindGroupLayout(...)"),
103 )
104 .finish()
105 }
106}
107
108impl Default for WgpuRenderSystem {
109 fn default() -> Self {
110 Self::new()
111 }
112}
113
114impl WgpuRenderSystem {
115 pub fn new() -> Self {
119 log::info!("WgpuRenderSystem created (uninitialized).");
120 Self {
121 graphics_context_shared: None,
122 wgpu_device: None,
123 gpu_monitor: None,
124 current_width: 0,
125 current_height: 0,
126 frame_count: 0,
127 last_frame_stats: RenderStats::default(),
128 gpu_profiler: None,
129 current_frame_view_id: None,
130 camera_uniform_buffer: None,
131 camera_bind_group: None,
132 camera_bind_group_layout: None,
133 last_resize_event: None,
134 pending_resize: false,
135 last_surface_config: None,
136 pending_resize_frames: 0,
137 last_pending_size: None,
138 stable_size_frame_count: 0,
139 }
140 }
141
142 async fn initialize(
143 &mut self,
144 window_handle: KhoraWindowHandle,
145 window_size: PhysicalSize<u32>,
146 ) -> Result<Vec<Arc<dyn ResourceMonitor>>, RenderError> {
147 if self.graphics_context_shared.is_some() {
148 return Err(RenderError::InitializationFailed(
149 "WgpuRenderSystem is already initialized.".to_string(),
150 ));
151 }
152 log::info!("WgpuRenderSystem: Initializing...");
153
154 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
155 let backend_selector = WgpuBackendSelector::new(instance.clone());
156 let selection_config = BackendSelectionConfig::default();
157
158 let selection_result = backend_selector
159 .select_backend(&selection_config)
160 .await
161 .map_err(|e| RenderError::InitializationFailed(e.to_string()))?;
162 let adapter = selection_result.adapter;
163
164 let context = WgpuGraphicsContext::new(&instance, window_handle, adapter, window_size)
165 .await
166 .map_err(|e| RenderError::InitializationFailed(e.to_string()))?;
167
168 self.current_width = context.get_size().0;
169 self.current_height = context.get_size().1;
170 let context_arc = Arc::new(Mutex::new(context));
171 self.graphics_context_shared = Some(context_arc.clone());
172
173 log::info!(
174 "WgpuRenderSystem: GraphicsContext created with size: {}x{}",
175 self.current_width,
176 self.current_height
177 );
178
179 let graphics_device = WgpuDevice::new(context_arc.clone());
180 let device_arc = Arc::new(graphics_device);
181 self.wgpu_device = Some(device_arc.clone());
182
183 if let Ok(gc_guard) = context_arc.lock() {
184 if WgpuTimestampProfiler::feature_available(gc_guard.active_device_features) {
185 if let Some(mut profiler) = WgpuTimestampProfiler::new(&gc_guard.device) {
186 let period = gc_guard.queue.get_timestamp_period();
187 profiler.set_timestamp_period(period);
188 self.gpu_profiler = Some(Box::new(profiler));
189 }
190 } else {
191 log::info!("GPU timestamp feature not available; instrumentation disabled.");
192 }
193 }
194
195 let mut created_monitors: Vec<Arc<dyn ResourceMonitor>> = Vec::new();
196 let gpu_monitor = Arc::new(GpuMonitor::new("WGPU".to_string()));
197 created_monitors.push(gpu_monitor.clone());
198 self.gpu_monitor = Some(gpu_monitor);
199
200 let vram_monitor = device_arc as Arc<dyn ResourceMonitor>;
201 created_monitors.push(vram_monitor);
202
203 self.initialize_camera_uniforms()?;
205
206 Ok(created_monitors)
207 }
208
209 fn initialize_camera_uniforms(&mut self) -> Result<(), RenderError> {
216 use khora_core::renderer::{
217 BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry,
218 BindingResource, BindingType, BufferBinding, BufferBindingType, BufferDescriptor,
219 BufferUsage, CameraUniformData, ShaderStageFlags,
220 };
221
222 let device = self.wgpu_device.as_ref().ok_or_else(|| {
223 RenderError::InitializationFailed("WGPU device not initialized".to_string())
224 })?;
225
226 let buffer_size = std::mem::size_of::<CameraUniformData>() as u64;
227
228 let buffer_descriptor = BufferDescriptor {
230 label: Some(std::borrow::Cow::Borrowed("Camera Uniform Buffer")),
231 size: buffer_size,
232 usage: BufferUsage::UNIFORM | BufferUsage::COPY_DST,
233 mapped_at_creation: false,
234 };
235
236 let uniform_buffer = device.create_buffer(&buffer_descriptor).map_err(|e| {
237 RenderError::InitializationFailed(format!(
238 "Failed to create camera uniform buffer: {:?}",
239 e
240 ))
241 })?;
242
243 let layout_entry = BindGroupLayoutEntry {
245 binding: 0,
246 visibility: ShaderStageFlags::VERTEX | ShaderStageFlags::FRAGMENT,
247 ty: BindingType::Buffer {
248 ty: BufferBindingType::Uniform,
249 has_dynamic_offset: false,
250 min_binding_size: None,
251 },
252 };
253
254 let layout_descriptor = BindGroupLayoutDescriptor {
255 label: Some("Camera Bind Group Layout"),
256 entries: &[layout_entry],
257 };
258
259 let bind_group_layout = device
260 .create_bind_group_layout(&layout_descriptor)
261 .map_err(|e| {
262 RenderError::InitializationFailed(format!(
263 "Failed to create camera bind group layout: {:?}",
264 e
265 ))
266 })?;
267
268 let bind_group_entry = BindGroupEntry {
270 binding: 0,
271 resource: BindingResource::Buffer(BufferBinding {
272 buffer: uniform_buffer,
273 offset: 0,
274 size: None,
275 }),
276 _phantom: std::marker::PhantomData,
277 };
278
279 let bind_group_descriptor = BindGroupDescriptor {
280 label: Some("Camera Bind Group"),
281 layout: bind_group_layout,
282 entries: &[bind_group_entry],
283 };
284
285 let bind_group = device
286 .create_bind_group(&bind_group_descriptor)
287 .map_err(|e| {
288 RenderError::InitializationFailed(format!(
289 "Failed to create camera bind group: {:?}",
290 e
291 ))
292 })?;
293
294 self.camera_uniform_buffer = Some(uniform_buffer);
295 self.camera_bind_group_layout = Some(bind_group_layout);
296 self.camera_bind_group = Some(bind_group);
297
298 log::info!("Camera uniform resources initialized with abstract API");
299
300 Ok(())
301 }
302
303 fn update_camera_uniforms(&mut self, view_info: &ViewInfo) {
308 use khora_core::renderer::CameraUniformData;
309
310 let uniform_data = CameraUniformData::from_view_info(view_info);
311
312 if let (Some(device), Some(buffer_id)) = (&self.wgpu_device, &self.camera_uniform_buffer) {
313 if let Err(e) =
315 device.write_buffer(*buffer_id, 0, bytemuck::cast_slice(&[uniform_data]))
316 {
317 log::warn!("Failed to write camera uniform data: {:?}", e);
318 }
319 }
320 }
321}
322
323impl RenderSystem for WgpuRenderSystem {
324 fn init(
325 &mut self,
326 window: &dyn KhoraWindow,
327 ) -> Result<Vec<Arc<dyn ResourceMonitor>>, RenderError> {
328 let (width, height) = window.inner_size();
329 let window_size = PhysicalSize::new(width, height);
330 let window_handle_arc = window.clone_handle_arc();
331 pollster::block_on(self.initialize(window_handle_arc, window_size))
332 }
333
334 fn resize(&mut self, new_width: u32, new_height: u32) {
335 if new_width > 0 && new_height > 0 {
336 log::debug!(
337 "WgpuRenderSystem: resize_surface called with W:{new_width}, H:{new_height}"
338 );
339 self.current_width = new_width;
340 self.current_height = new_height;
341 let now = Instant::now();
342 if let Some((lw, lh)) = self.last_pending_size {
343 if lw == new_width && lh == new_height {
344 self.stable_size_frame_count = self.stable_size_frame_count.saturating_add(1);
345 } else {
346 self.stable_size_frame_count = 0;
347 }
348 }
349 self.last_pending_size = Some((new_width, new_height));
350
351 let immediate_threshold_ms: u128 = 80;
352 let can_immediate = self
353 .last_surface_config
354 .map(|t| t.elapsed().as_millis() >= immediate_threshold_ms)
355 .unwrap_or(true);
356 let early_stable = self.stable_size_frame_count >= 2
357 && self
358 .last_surface_config
359 .map(|t| t.elapsed().as_millis() >= 20)
360 .unwrap_or(true);
361 if can_immediate || early_stable {
362 if let Some(gc_arc_mutex) = &self.graphics_context_shared {
363 if let Ok(mut gc_guard) = gc_arc_mutex.lock() {
364 gc_guard.resize(self.current_width, self.current_height);
365 self.last_surface_config = Some(now);
366 self.pending_resize = false;
367 self.pending_resize_frames = 0;
368 log::info!(
369 "WGPUGraphicsContext: Immediate/Early surface configuration to {}x{}",
370 self.current_width,
371 self.current_height
372 );
373 return;
374 }
375 }
376 }
377 self.last_resize_event = Some(now);
378 self.pending_resize = true;
379 self.pending_resize_frames = 0;
380 } else {
381 log::warn!(
382 "WgpuRenderSystem::resize_surface called with zero size ({new_width}, {new_height}). Ignoring."
383 );
384 }
385 }
386
387 fn prepare_frame(&mut self, view_info: &ViewInfo) {
388 if self.graphics_context_shared.is_none() {
389 return;
390 }
391 let stopwatch = Stopwatch::new();
392
393 self.update_camera_uniforms(view_info);
395
396 self.last_frame_stats.cpu_preparation_time_ms = stopwatch.elapsed_ms().unwrap_or(0) as f32;
397 }
398
399 fn render(
400 &mut self,
401 renderables: &[RenderObject],
402 _view_info: &ViewInfo,
403 settings: &RenderSettings,
404 ) -> Result<RenderStats, RenderError> {
405 let full_frame_timer = Stopwatch::new();
406
407 let device = self
408 .wgpu_device
409 .as_ref()
410 .ok_or(RenderError::NotInitialized)?;
411
412 device.poll_device_non_blocking();
415
416 let gc = self
417 .graphics_context_shared
418 .as_ref()
419 .ok_or(RenderError::NotInitialized)?;
420
421 if let Some(p) = self.gpu_profiler.as_mut() {
422 p.try_read_previous_frame();
423 }
424
425 if self.pending_resize {
427 self.pending_resize_frames = self.pending_resize_frames.saturating_add(1);
428 let mut resized_this_frame = false;
429 if let Some(t) = self.last_resize_event {
430 let quiet_elapsed = t.elapsed().as_millis();
431 let debounce_quiet_ms = settings.resize_debounce_ms as u128;
432 let max_pending_frames = settings.resize_max_pending_frames;
433 let early_stable = self.stable_size_frame_count >= 3;
434
435 if quiet_elapsed >= debounce_quiet_ms
436 || self.pending_resize_frames >= max_pending_frames
437 || early_stable
438 {
439 if let Ok(mut gc_guard) = gc.lock() {
440 gc_guard.resize(self.current_width, self.current_height);
441 self.pending_resize = false;
442 self.last_surface_config = Some(Instant::now());
443 self.stable_size_frame_count = 0;
444 resized_this_frame = true;
445 log::info!(
446 "Deferred surface configuration to {}x{}",
447 self.current_width,
448 self.current_height
449 );
450 }
451 }
452 }
453 if self.pending_resize && !resized_this_frame {
454 return Ok(self.last_frame_stats.clone());
455 }
456 }
457
458 let output_surface_texture = loop {
460 let mut gc_guard = gc.lock().unwrap();
461 match gc_guard.get_current_texture() {
462 Ok(texture) => break texture,
463 Err(e @ wgpu::SurfaceError::Lost) | Err(e @ wgpu::SurfaceError::Outdated) => {
464 if self.current_width > 0 && self.current_height > 0 {
465 log::warn!(
466 "WgpuRenderSystem: Swapchain surface lost or outdated ({:?}). Reconfiguring with current dimensions: W={}, H={}",
467 e,
468 self.current_width,
469 self.current_height
470 );
471 gc_guard.resize(self.current_width, self.current_height);
472 self.last_surface_config = Some(Instant::now());
473 self.pending_resize = false; } else {
475 log::error!(
476 "WgpuRenderSystem: Swapchain lost/outdated ({:?}), but current stored size is zero ({},{}). Cannot reconfigure. Waiting for valid resize event.",
477 e,
478 self.current_width,
479 self.current_height
480 );
481 return Err(RenderError::SurfaceAcquisitionFailed(format!(
482 "Surface Lost/Outdated ({e:?}) and current size is zero",
483 )));
484 }
485 }
486 Err(e @ wgpu::SurfaceError::OutOfMemory) => {
487 log::error!("WgpuRenderSystem: Swapchain OutOfMemory! ({e:?})");
488 return Err(RenderError::SurfaceAcquisitionFailed(format!(
489 "OutOfMemory: {e:?}"
490 )));
491 }
492 Err(e @ wgpu::SurfaceError::Timeout) => {
493 log::warn!("WgpuRenderSystem: Swapchain Timeout acquiring frame. ({e:?})");
494 return Err(RenderError::SurfaceAcquisitionFailed(format!(
495 "Timeout: {e:?}"
496 )));
497 }
498 Err(e) => {
499 log::error!("WgpuRenderSystem: Unexpected SurfaceError: {e:?}");
500 return Err(RenderError::SurfaceAcquisitionFailed(format!(
501 "Unexpected SurfaceError: {e:?}"
502 )));
503 }
504 }
505 };
506
507 let command_recording_timer = Stopwatch::new();
508
509 if let Some(old_id) = self.current_frame_view_id.take() {
511 device.destroy_texture_view(old_id)?;
512 }
513 let target_view_id = device.create_texture_view_for_surface(
514 &output_surface_texture.texture,
515 Some("Primary Swap Chain View"),
516 )?;
517 self.current_frame_view_id = Some(target_view_id);
518
519 let mut command_encoder = device.create_command_encoder(Some("Khora Main Command Encoder"));
521
522 if settings.enable_gpu_timestamps {
524 if let Some(profiler) = self.gpu_profiler.as_ref() {
525 let _pass_a = command_encoder.begin_profiler_compute_pass(
526 Some("Timestamp Pass A"),
527 profiler.as_ref(),
528 0,
529 );
530 }
531 }
532
533 {
535 let gc_guard = gc.lock().unwrap();
536 let wgpu_color = gc_guard.get_clear_color();
537 let clear_color = LinearRgba::new(
538 wgpu_color.r as f32,
539 wgpu_color.g as f32,
540 wgpu_color.b as f32,
541 wgpu_color.a as f32,
542 );
543
544 let color_attachment = RenderPassColorAttachment {
545 view: &target_view_id,
546 resolve_target: None,
547 ops: Operations {
548 load: LoadOp::Clear(clear_color),
549 store: StoreOp::Store,
550 },
551 };
552 let pass_descriptor = RenderPassDescriptor {
553 label: Some("Khora Main Abstract Render Pass"),
554 color_attachments: &[color_attachment],
555 };
556
557 let mut render_pass = command_encoder.begin_render_pass(&pass_descriptor);
558
559 if let Some(camera_bind_group) = &self.camera_bind_group {
561 render_pass.set_bind_group(0, camera_bind_group);
562 }
563
564 let (draw_calls, triangles) = renderables.iter().fold((0, 0), |(dc, tris), obj| {
565 render_pass.set_pipeline(&obj.pipeline);
566 render_pass.set_vertex_buffer(0, &obj.vertex_buffer, 0);
567 render_pass.set_index_buffer(&obj.index_buffer, 0, IndexFormat::Uint16);
568 render_pass.draw_indexed(0..obj.index_count, 0, 0..1);
569 (dc + 1, tris + obj.index_count / 3)
570 });
571 self.last_frame_stats.draw_calls = draw_calls;
572 self.last_frame_stats.triangles_rendered = triangles;
573 }
574
575 if settings.enable_gpu_timestamps {
577 if let Some(profiler) = self.gpu_profiler.as_ref() {
578 {
581 let _pass_b = command_encoder.begin_profiler_compute_pass(
582 Some("Timestamp Pass B"),
583 profiler.as_ref(),
584 1,
585 );
586 }
587 profiler.resolve_and_copy(command_encoder.as_mut());
588 profiler.copy_to_staging(command_encoder.as_mut(), self.frame_count);
589 }
590 }
591
592 let submission_timer = Stopwatch::new();
594 let command_buffer = command_encoder.finish();
595 device.submit_command_buffer(command_buffer);
596 let submission_ms = submission_timer.elapsed_ms().unwrap_or(0);
597
598 if settings.enable_gpu_timestamps {
599 if let Some(p) = self.gpu_profiler.as_mut() {
600 p.schedule_map_after_submit(self.frame_count);
601 }
602 }
603
604 output_surface_texture.present();
606
607 self.frame_count += 1;
609 if let Some(p) = self.gpu_profiler.as_ref() {
610 self.last_frame_stats.gpu_main_pass_time_ms = p.last_main_pass_ms();
611 self.last_frame_stats.gpu_frame_total_time_ms = p.last_frame_total_ms();
612 }
613 let full_frame_ms = full_frame_timer.elapsed_ms().unwrap_or(0);
614 self.last_frame_stats.frame_number = self.frame_count;
615 self.last_frame_stats.cpu_preparation_time_ms =
616 (full_frame_ms - command_recording_timer.elapsed_ms().unwrap_or(0)) as f32;
617 self.last_frame_stats.cpu_render_submission_time_ms = submission_ms as f32;
618
619 if let Some(monitor) = &self.gpu_monitor {
620 monitor.update_from_frame_stats(&self.last_frame_stats);
621 }
622
623 Ok(self.last_frame_stats.clone())
624 }
625
626 fn get_last_frame_stats(&self) -> &RenderStats {
627 &self.last_frame_stats
628 }
629
630 fn supports_feature(&self, feature_name: &str) -> bool {
631 self.wgpu_device
632 .as_ref()
633 .is_some_and(|d| d.supports_feature(feature_name))
634 }
635
636 fn shutdown(&mut self) {
637 log::info!("WgpuRenderSystem shutting down...");
638 if let Some(mut profiler) = self.gpu_profiler.take() {
639 if let Some(device) = self.wgpu_device.as_ref() {
640 if let Some(wgpu_profiler) = profiler
641 .as_any_mut()
642 .downcast_mut::<WgpuTimestampProfiler>()
643 {
644 wgpu_profiler.shutdown(device);
645 }
646 }
647 }
648 if let Some(old_id) = self.current_frame_view_id.take() {
649 if let Some(device) = self.wgpu_device.as_ref() {
650 let _ = device.destroy_texture_view(old_id);
651 }
652 }
653 self.wgpu_device = None;
654 self.graphics_context_shared = None;
655 self.gpu_monitor = None;
656 }
657
658 fn as_any(&self) -> &dyn std::any::Any {
659 self
660 }
661
662 fn get_adapter_info(&self) -> Option<RendererAdapterInfo> {
663 self.wgpu_device.as_ref().map(|d| d.get_adapter_info())
664 }
665
666 fn graphics_device(&self) -> Arc<dyn GraphicsDevice> {
667 self.wgpu_device
668 .clone()
669 .expect("WgpuRenderSystem: No WgpuDevice available.")
670 }
671}
672
673unsafe impl Send for WgpuRenderSystem {}
674unsafe impl Sync for WgpuRenderSystem {}