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