1use crate::heim::SkylinePacker;
3use crate::types::*;
4use crate::vertex::*;
5use cvkg_core::Rect;
6use cvkg_core::{ColorTheme, SceneUniforms};
7use lru::LruCache;
8use std::collections::VecDeque;
9use std::num::NonZeroUsize;
10use std::sync::Arc;
11
12pub use crate::subsystems::RendererConfig;
14
15pub(crate) mod pipelines;
16pub(crate) mod init;
17pub(crate) mod draw;
18pub(crate) mod svg;
19pub(crate) mod context_helpers;
20#[cfg(test)]
21pub(crate) mod tests;
22
23pub(crate) mod material_id {
26 pub const OPAQUE: u32 = 0;
28 pub const ELLIPSE: u32 = 4;
30 pub const TOP_UI: u32 = 6;
32 pub const GLASS: u32 = 7;
34 pub const BLEND_START: u32 = 8;
36 pub const BLEND_END: u32 = 22;
37 pub const RADIAL_GRADIENT: u32 = 16;
39 pub const SQUIRCLE_STROKE: u32 = 17;
41 pub const DROP_SHADOW: u32 = 18;
43 pub const DASHED_STROKE: u32 = 19;
45 pub const MESH_3D: u32 = 21;
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum QualityLevel {
56 High,
57 Medium,
58 Low,
59}
60
61impl QualityLevel {
62 pub fn msaa_sample_count(self) -> u32 {
64 match self {
65 QualityLevel::High => 4,
66 QualityLevel::Medium => 2,
67 QualityLevel::Low => 1,
68 }
69 }
70}
71
72impl Default for QualityLevel {
73 fn default() -> Self {
74 QualityLevel::High
75 }
76}
77
78pub struct GpuRenderer {
80 pub(crate) instance: Arc<wgpu::Instance>,
81 pub(crate) adapter: Arc<wgpu::Adapter>,
82 pub(crate) device: Arc<wgpu::Device>,
83 pub(crate) queue: Arc<wgpu::Queue>,
84
85 pub(crate) registry: crate::kvasir::registry::ResourceRegistry,
87
88 pub(crate) active_offscreens: Vec<crate::types::OffscreenEffectConfig>,
89 pub(crate) effect_pipelines: std::collections::HashMap<String, wgpu::RenderPipeline>,
90 pub(crate) effect_params_buffer: wgpu::Buffer,
91 pub(crate) effect_params_bind_group: wgpu::BindGroup,
92 pub(crate) linear_sampler: wgpu::Sampler,
93 pub ai_material_rx: Option<
95 std::sync::mpsc::Receiver<
96 Result<crate::material::CompiledMaterial, crate::ai::GeneratorError>,
97 >,
98 >,
99
100 pub(crate) surfaces: std::collections::HashMap<winit::window::WindowId, SurfaceContext>,
102 pub(crate) current_window: Option<winit::window::WindowId>,
103 pub headless_context: Option<HeadlessContext>,
104
105 pub(crate) text: crate::types::TextSubsystem,
107 pub(crate) mega_heim_tex: wgpu::Texture,
108 pub(crate) mega_heim_bind_group: wgpu::BindGroup,
109 pub(crate) heim_packer: SkylinePacker,
110 pub(crate) image_uv_registry: LruCache<String, Rect>,
111 pub(crate) texture_registry: LruCache<String, u32>,
112 pub(crate) texture_views: Vec<wgpu::TextureView>,
113 pub(crate) dummy_sampler: wgpu::Sampler,
114 pub(crate) svg: crate::types::SvgSubsystem,
115
116 pub(crate) dummy_texture_bind_group: wgpu::BindGroup,
118 pub(crate) dummy_env_bind_group: wgpu::BindGroup,
119 pub(crate) texture_bind_group_layout: wgpu::BindGroupLayout,
120 pub(crate) texture_bind_groups: Vec<wgpu::BindGroup>,
121 pub(crate) shared_elements: LruCache<String, cvkg_core::Rect>,
122
123 pub(crate) geometry_buffers: crate::types::GeometryBuffers,
125 pub(crate) vertices: Vec<Vertex>,
126 pub(crate) indices: Vec<u32>,
127 pub(crate) instance_data: Vec<InstanceData>,
128 pub(crate) staging_belt: wgpu::util::StagingBelt,
129 pub(crate) staging_command_buffers: Vec<wgpu::CommandBuffer>,
130 pub(crate) draw_calls: Vec<DrawCall>,
131 pub(crate) current_texture_id: Option<u32>,
132
133 pub(crate) opacity_stack: Vec<f32>,
135 pub(crate) clip_stack: Vec<Rect>,
136 pub(crate) slice_stack: Vec<(f32, f32)>,
137 pub(crate) shadow_stack: Vec<ShadowState>,
138
139 pub blur_pipeline: Option<wgpu::RenderPipeline>,
143 pub blur_uniform: Option<wgpu::Buffer>,
146 pub blur_bind_group_layout: Option<wgpu::BindGroupLayout>,
149 pub blend_pipeline: Option<wgpu::RenderPipeline>,
152 pub blend_bind_group_layout: Option<wgpu::BindGroupLayout>,
155 pub flood_pipeline: Option<wgpu::RenderPipeline>,
158 pub copy_bind_group_layout: Option<wgpu::BindGroupLayout>,
161
162 pub(crate) theme_buffer: wgpu::Buffer,
164 pub(crate) scene_buffer: wgpu::Buffer,
165 pub(crate) berserker_bind_group: wgpu::BindGroup,
166 pub(crate) berserker_bind_group_layout: wgpu::BindGroupLayout,
167 pub(crate) start_time: std::time::Instant,
168 pub(crate) current_theme: ColorTheme,
169 pub(crate) current_scene: SceneUniforms,
170 pub(crate) current_z: f32,
171
172 pub(crate) default_background_color: [f32; 4],
176
177 pub(crate) app_drew_background: bool,
180
181 pub(crate) frame_rendered: bool,
184
185 pub(crate) current_draw_order: i32,
188
189 pub(crate) pipeline: wgpu::RenderPipeline,
191 pub(crate) opaque_pipeline: wgpu::RenderPipeline,
193 pub(crate) ui_pipeline: wgpu::RenderPipeline,
196 pub(crate) glass_pipeline: wgpu::RenderPipeline,
198 pub(crate) background_pipeline: wgpu::RenderPipeline,
199 pub(crate) bloom_extract_pipeline: wgpu::RenderPipeline,
200 pub(crate) copy_pipeline: wgpu::RenderPipeline,
202 pub(crate) composite_pipeline: wgpu::RenderPipeline,
203 pub(crate) color_blind_pipeline: wgpu::RenderPipeline,
205 pub(crate) volumetric_pipeline: wgpu::RenderPipeline,
207 pub(crate) volumetric_bind_group_layout: wgpu::BindGroupLayout,
209 pub(crate) volumetric_uniform_buffer: wgpu::Buffer,
211 pub(crate) volumetric_depth_sampler: wgpu::Sampler,
213 pub(crate) hologram_instances: Vec<HologramInstance>,
216 pub(crate) kawase_down_pipeline: wgpu::RenderPipeline,
218 pub(crate) kawase_up_pipeline: wgpu::RenderPipeline,
220 pub(crate) kawase_bind_group_layout: wgpu::BindGroupLayout,
222 pub(crate) kawase_uniform: wgpu::Buffer,
224 pub(crate) kawase_uniform_buffers: Vec<wgpu::Buffer>,
226 pub(crate) env_bind_group_layout: wgpu::BindGroupLayout,
228
229 pub telemetry: cvkg_core::TelemetryData,
231
232 pub(crate) pipeline_cache: Option<wgpu::PipelineCache>,
235
236 pub frame_budget: cvkg_core::FrameBudget,
238 pub(crate) capture_staging_buffer: Option<wgpu::Buffer>,
240 pub last_redraw_start: std::time::Instant,
242 pub last_frame_start: std::time::Instant,
244
245 pub(crate) vram_buffers_bytes: u64,
247 pub(crate) vram_textures_bytes: u64,
248
249 pub(crate) _debug_layout: bool,
251
252 pub(crate) transform_stack: Vec<glam::Mat3>,
254 pub redraw_requested: bool,
256 pub(crate) compositor_index_cursor: u32,
258
259 pub bloom_enabled: bool,
261 pub volumetric_enabled: bool,
263
264 pub(crate) path_geometry_cache: lru::LruCache<u64, (Vec<Vertex>, Vec<u32>)>,
266 pub(crate) color_blind_bind_group_layout: wgpu::BindGroupLayout,
268 pub(crate) color_blind_uniform_buffer: wgpu::Buffer,
270 pub color_blind_mode: crate::color_blindness::ColorBlindMode,
272 pub color_blind_intensity: f32,
274 pub(crate) sampler: wgpu::Sampler,
276
277 pub(crate) skuld_queries: Option<wgpu::QuerySet>,
279 pub(crate) skuld_buffer: Option<wgpu::Buffer>,
280 pub(crate) skuld_read_buffer: Option<wgpu::Buffer>,
281 pub(crate) skuld_period: f32,
282 pub last_gpu_time_ns: u64,
283
284 pub(crate) particle_compute_pipeline: wgpu::ComputePipeline,
286 pub(crate) particle_compute_bgl: wgpu::BindGroupLayout,
287 pub(crate) particle_buffer: wgpu::Buffer,
288 pub(crate) particle_uniform_buffer: wgpu::Buffer,
289 pub(crate) particles: crate::types::ParticleSubsystem,
290 pub(crate) particle_render_pipeline: wgpu::RenderPipeline,
291 pub(crate) particle_render_bgl: wgpu::BindGroupLayout,
292 pub(crate) particle_render_bind_group: Option<wgpu::BindGroup>,
293 pub(crate) particle_compute_bind_group: Option<wgpu::BindGroup>,
294
295 pub(crate) vnode_stack: Vec<(Rect, &'static str)>,
297
298 pub(crate) event_handlers: std::collections::HashMap<
300 String,
301 Vec<std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>>,
302 >,
303
304 pub(crate) glass_output_bind_group_layout: wgpu::BindGroupLayout,
306 pub(crate) current_draw_material: cvkg_core::DrawMaterial,
308
309 pub(crate) portal_regions: std::collections::VecDeque<cvkg_core::Rect>,
311
312 pub(crate) cached_graph_plan: Option<crate::kvasir::graph_cache::CachedGraphPlan>,
314 pub(crate) material_compilation_hash: u64,
316 pub(crate) memo_cache: std::collections::HashMap<u64, crate::types::MemoEntry>,
318 pub(crate) frame_generation: u64,
320 pub(crate) config: crate::subsystems::RendererConfig,
322 pub(crate) quality_level: QualityLevel,
324 pub(crate) bind_group_cache: std::sync::Mutex<
326 std::collections::HashMap<
327 (crate::kvasir::resource::ResourceId, u32, bool),
328 wgpu::BindGroup,
329 >,
330 >,
331 pub(crate) texture_view_cache: std::sync::Mutex<
333 std::collections::HashMap<(crate::kvasir::resource::ResourceId, u32), wgpu::TextureView>,
334 >,
335}
336
337#[cfg(target_arch = "wasm32")]
338unsafe impl Send for GpuRenderer {}
339#[cfg(target_arch = "wasm32")]
340unsafe impl Sync for GpuRenderer {}
341
342#[derive(Debug, Clone)]
344pub struct HologramInstance {
345 pub rect: cvkg_core::Rect,
347 pub id_hash: u32,
349 pub time: f32,
351}
352
353pub trait ClearInto {
356 fn clear_into(&mut self);
357}
358
359impl<K, V, S> ClearInto for std::collections::HashMap<K, V, S>
360where
361 S: std::hash::BuildHasher,
362{
363 fn clear_into(&mut self) {
364 self.clear();
365 }
366}
367
368impl<T> ClearInto for Vec<T> {
369 fn clear_into(&mut self) {
370 self.clear();
371 }
372}
373
374fn load_pipeline_cache_with_integrity_check(
380 cache_path: &std::path::Path,
381) -> Result<Option<Vec<u8>>, String> {
382 let cache_data = match std::fs::read(cache_path) {
383 Ok(d) => d,
384 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
385 Err(e) => return Err(format!("read failed: {e}")),
386 };
387
388 let hash_path = cache_path.with_extension("bin.sha256");
389 let expected_hash = match std::fs::read_to_string(&hash_path) {
390 Ok(s) => s.trim().to_lowercase(),
391 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
392 return Err(format!(
393 "sidecar hash file missing at {}",
394 hash_path.display()
395 ))
396 }
397 Err(e) => return Err(format!("sidecar read failed: {e}")),
398 };
399
400 let actual = compute_sha256(&cache_data);
401 let actual_hex: String = actual.iter().map(|b| format!("{:02x}", b)).collect();
402 if actual_hex != expected_hash {
403 return Err(format!(
404 "hash mismatch: expected {expected_hash}, got {actual_hex}"
405 ));
406 }
407
408 Ok(Some(cache_data))
409}
410
411fn compute_sha256(data: &[u8]) -> [u8; 32] {
413 let mut hasher = Sha256::new();
414 hasher.update(data);
415 hasher.finalize()
416}
417
418#[derive(Clone)]
421struct Sha256 {
422 state: [u32; 8],
423 buffer: [u8; 64],
424 buffer_len: usize,
425 total_len: u64,
426}
427
428impl Sha256 {
429 const K: [u32; 64] = [
430 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
431 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
432 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
433 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
434 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
435 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
436 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
437 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
438 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
439 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
440 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
441 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
442 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
443 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
444 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
445 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
446 ];
447
448 fn new() -> Self {
449 Self {
450 state: [
451 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
452 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
453 ],
454 buffer: [0; 64],
455 buffer_len: 0,
456 total_len: 0,
457 }
458 }
459
460 fn update(&mut self, data: &[u8]) {
461 self.total_len = self.total_len.wrapping_add(data.len() as u64);
462 for &b in data {
463 self.buffer[self.buffer_len] = b;
464 self.buffer_len += 1;
465 if self.buffer_len == 64 {
466 let block = self.buffer;
467 self.compress(&block);
468 self.buffer_len = 0;
469 }
470 }
471 }
472
473 fn finalize(mut self) -> [u8; 32] {
474 self.buffer[self.buffer_len] = 0x80;
475 self.buffer_len += 1;
476 if self.buffer_len > 56 {
477 for b in &mut self.buffer[self.buffer_len..] { *b = 0; }
478 let block = self.buffer;
479 self.compress(&block);
480 self.buffer_len = 0;
481 }
482 for b in &mut self.buffer[self.buffer_len..56] { *b = 0; }
483 let bit_len = self.total_len.wrapping_mul(8);
484 self.buffer[56..64].copy_from_slice(&bit_len.to_be_bytes());
485 let block = self.buffer;
486 self.compress(&block);
487
488 let mut out = [0u8; 32];
489 for (i, &s) in self.state.iter().enumerate() {
490 out[i*4..(i+1)*4].copy_from_slice(&s.to_be_bytes());
491 }
492 out
493 }
494
495 fn compress(&mut self, block: &[u8]) {
496 let mut w = [0u32; 64];
497 for i in 0..16 {
498 w[i] = u32::from_be_bytes([
499 block[i*4], block[i*4+1], block[i*4+2], block[i*4+3]
500 ]);
501 }
502 for i in 16..64 {
503 let s0 = w[i-15].rotate_right(7) ^ w[i-15].rotate_right(18) ^ (w[i-15] >> 3);
504 let s1 = w[i-2].rotate_right(17) ^ w[i-2].rotate_right(19) ^ (w[i-2] >> 10);
505 w[i] = w[i-16].wrapping_add(s0).wrapping_add(w[i-7]).wrapping_add(s1);
506 }
507 let mut a = self.state[0];
508 let mut b = self.state[1];
509 let mut c = self.state[2];
510 let mut d = self.state[3];
511 let mut e = self.state[4];
512 let mut f = self.state[5];
513 let mut g = self.state[6];
514 let mut h = self.state[7];
515 for i in 0..64 {
516 let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
517 let ch = (e & f) ^ ((!e) & g);
518 let t1 = h.wrapping_add(s1).wrapping_add(ch).wrapping_add(Self::K[i]).wrapping_add(w[i]);
519 let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
520 let mj = (a & b) ^ (a & c) ^ (b & c);
521 let t2 = s0.wrapping_add(mj);
522 h = g; g = f; f = e;
523 e = d.wrapping_add(t1);
524 d = c; c = b; b = a;
525 a = t1.wrapping_add(t2);
526 }
527 self.state[0] = self.state[0].wrapping_add(a);
528 self.state[1] = self.state[1].wrapping_add(b);
529 self.state[2] = self.state[2].wrapping_add(c);
530 self.state[3] = self.state[3].wrapping_add(d);
531 self.state[4] = self.state[4].wrapping_add(e);
532 self.state[5] = self.state[5].wrapping_add(f);
533 self.state[6] = self.state[6].wrapping_add(g);
534 self.state[7] = self.state[7].wrapping_add(h);
535 }
536}
537
538fn compute_mip_levels(width: u32, height: u32) -> u32 {
539 let max_dim = width.max(height);
540 if max_dim <= 1 {
541 return 1;
542 }
543 let mips = (32 - max_dim.leading_zeros()).clamp(2, 8);
544 mips
545}
546
547impl GpuRenderer {
548 pub fn hologram_instances(&self) -> &[HologramInstance] {
550 &self.hologram_instances
551 }
552
553 pub fn set_quality_level(&mut self, level: QualityLevel) {
554 self.quality_level = level;
555 }
556
557 pub fn set_config(&mut self, config: crate::subsystems::RendererConfig) {
558 self.config = config;
559 }
560
561 pub fn config(&self) -> &crate::subsystems::RendererConfig {
562 &self.config
563 }
564
565 pub fn quality_level(&self) -> QualityLevel {
566 self.quality_level
567 }
568
569 pub(crate) fn lock_or_clear_cache<'a, T: ClearInto>(
570 lock: &'a std::sync::Mutex<T>,
571 ) -> std::sync::MutexGuard<'a, T> {
572 match lock.lock() {
573 Ok(guard) => guard,
574 Err(poisoned) => {
575 log::warn!("[GPU] lock_or_clear_cache: mutex poisoned, clearing cache...");
576 let mut guard = poisoned.into_inner();
577 guard.clear_into();
578 guard
579 }
580 }
581 }
582
583 pub fn update_mouse(&mut self, mouse: [f32; 2], velocity: [f32; 2]) {
584 self.current_scene.mouse = mouse;
585 self.current_scene.mouse_velocity = velocity;
586 self.queue.write_buffer(
587 &self.scene_buffer,
588 0,
589 bytemuck::bytes_of(&self.current_scene),
590 );
591 }
592
593 pub fn invalidate_material_cache(&mut self) {
594 self.cached_graph_plan = None;
595 }
596
597 pub fn invalidate_all_caches(&mut self) -> usize {
598 let mut cleared = 0;
599 {
600 let mut bg_cache = Self::lock_or_clear_cache(&self.bind_group_cache);
601 cleared += bg_cache.len();
602 bg_cache.clear();
603 }
604 {
605 let mut view_cache = Self::lock_or_clear_cache(&self.texture_view_cache);
606 cleared += view_cache.len();
607 view_cache.clear();
608 }
609 cleared += self.text.shaped_cache.len();
610 self.text.shaped_cache.clear();
611 cleared += self.svg.model_cache.len();
612 self.svg.model_cache.clear();
613 cleared += self.svg.tree_cache.len();
614 self.svg.tree_cache.clear();
615 self.svg.clear_filter_batches();
616 cleared
617 }
618
619 pub fn prewarm_text_cache(&mut self, labels: &[(&str, f32)]) {
620 let mut count = 0;
621 for (text, size) in labels {
622 let cache_key = (text.to_string(), (size * 100.0) as u32);
623 if self.text.shaped_cache.contains(&cache_key) {
624 continue;
625 }
626 let style = cvkg_runic_text::TextStyle::new("Inter", *size);
627 let spans = [cvkg_runic_text::TextSpan::new(text, style)];
628 if let Some(shaped) = self.text.engine.shape_layout(
629 &spans,
630 None,
631 cvkg_runic_text::TextAlign::Start,
632 cvkg_runic_text::TextOverflow::Visible,
633 ).ok() {
634 self.text.shaped_cache.put(cache_key, std::sync::Arc::new(shaped));
635 count += 1;
636 }
637 }
638 if count > 0 {
639 log::info!("[Surtr] prewarm_text_cache: pre-shaped {} labels", count);
640 }
641 }
642
643 pub(crate) fn select_best_surface_format(
644 formats: &[wgpu::TextureFormat],
645 ) -> wgpu::TextureFormat {
646 if formats.is_empty() {
647 return wgpu::TextureFormat::Rgba8Unorm;
648 }
649 let preferred_formats = [
650 wgpu::TextureFormat::Rgba16Float,
651 wgpu::TextureFormat::Rgba8Unorm,
652 wgpu::TextureFormat::Bgra8UnormSrgb,
653 wgpu::TextureFormat::Rgba8UnormSrgb,
654 wgpu::TextureFormat::Bgra8Unorm,
655 wgpu::TextureFormat::Rgba8Unorm,
656 wgpu::TextureFormat::Rgba8Unorm,
657 ];
658 for preferred in &preferred_formats {
659 if formats.contains(preferred) {
660 return *preferred;
661 }
662 }
663 if formats.contains(&wgpu::TextureFormat::Rgba8Unorm) {
664 return wgpu::TextureFormat::Rgba8Unorm;
665 }
666 formats[0]
667 }
668
669 pub(crate) fn rebuild_texture_array_bind_group(&mut self) {
670 let views: Vec<&wgpu::TextureView> = self.texture_views.iter().collect();
671 self.mega_heim_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
672 layout: &self.texture_bind_group_layout,
673 entries: &[
674 wgpu::BindGroupEntry {
675 binding: 0,
676 resource: wgpu::BindingResource::TextureViewArray(&views),
677 },
678 wgpu::BindGroupEntry {
679 binding: 1,
680 resource: wgpu::BindingResource::Sampler(&self.dummy_sampler),
681 },
682 ],
683 label: Some("Mega-Heim Rebuilt Bind Group"),
684 });
685 }
686
687 pub(crate) fn update_vram_telemetry(&mut self) {
688 let buffers = self.geometry_buffers.vertex_buffer.size()
689 + self.geometry_buffers.index_buffer.size()
690 + self.geometry_buffers.instance_buffer.size()
691 + self.scene_buffer.size()
692 + self.theme_buffer.size()
693 + self.particle_buffer.size()
694 + self.particle_uniform_buffer.size();
695
696 let mut textures = self.config.mega_heim_vram_bytes();
697 textures += 1 * 1 * 4; for surface in self.surfaces.values() {
700 let width = surface.config.width;
701 let height = surface.config.height;
702 let format_bytes = 8; textures += (width * height * format_bytes) as u64; textures += (width * height * format_bytes * self.quality_level.msaa_sample_count() as u32) as u64; textures += (width * height * 4) as u64; let blur_width = (width / 2).max(1);
708 let blur_height = (height / 2).max(1);
709 let blur_bytes = (blur_width * blur_height * 4) as u64;
710 textures += blur_bytes * 4; }
712
713 if let Some(ref ctx) = self.headless_context {
714 let format_bytes = 8; textures += (ctx.width * ctx.height * format_bytes) as u64; textures += (ctx.width * ctx.height * format_bytes * self.quality_level.msaa_sample_count() as u32) as u64; textures += (ctx.width * ctx.height * 4) as u64; textures += (ctx.width * ctx.height * 4) as u64; }
720
721 self.vram_buffers_bytes = buffers;
722 self.vram_textures_bytes = textures;
723 self.telemetry.vram_usage_mb = (buffers + textures) as f32 / (1024.0 * 1024.0);
724 }
725
726 pub fn get_telemetry(&self) -> cvkg_core::TelemetryData {
727 self.telemetry.clone()
728 }
729
730 pub fn resize(
731 &mut self,
732 window_id: winit::window::WindowId,
733 width: u32,
734 height: u32,
735 scale_factor: f32,
736 ) {
737 if width > 0
738 && height > 0
739 && let Some(ctx) = self.surfaces.get_mut(&window_id)
740 {
741 if ctx.config.width == width && ctx.config.height == height {
742 return;
743 }
744
745 log::info!("[GPU] Reconfiguring surface: {}x{}", width, height);
746 GpuRenderer::lock_or_clear_cache(&self.bind_group_cache).clear();
747 GpuRenderer::lock_or_clear_cache(&self.texture_view_cache).clear();
748 self.text.shaped_cache.clear();
749 ctx.config.width = width;
750 ctx.config.height = height;
751 ctx.scale_factor = scale_factor;
752 ctx.surface.configure(&self.device, &ctx.config);
753
754 let texture_desc = wgpu::TextureDescriptor {
755 label: Some("Surtr Scene Texture"),
756 size: wgpu::Extent3d {
757 width,
758 height,
759 depth_or_array_layers: 1,
760 },
761 mip_level_count: 1,
762 sample_count: 1,
763 dimension: wgpu::TextureDimension::D2,
764 format: wgpu::TextureFormat::Rgba16Float,
765 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
766 | wgpu::TextureUsages::TEXTURE_BINDING,
767 view_formats: &[],
768 };
769
770 let scene_tex = self.device.create_texture(&texture_desc);
771
772 let msaa_desc = wgpu::TextureDescriptor {
773 label: Some("Scene MSAA"),
774 size: texture_desc.size,
775 mip_level_count: 1,
776 sample_count: self.quality_level.msaa_sample_count(),
777 dimension: wgpu::TextureDimension::D2,
778 format: wgpu::TextureFormat::Rgba16Float,
779 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
780 view_formats: &[],
781 };
782 let scene_msaa_tex = self.device.create_texture(&msaa_desc);
783 ctx.scene_texture = scene_tex.create_view(&wgpu::TextureViewDescriptor::default());
784 ctx.scene_msaa_texture =
785 scene_msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
786
787 self.registry.remove_image(ctx.blur_tex_a);
788 self.registry.remove_image(ctx.blur_tex_b);
789 self.registry.remove_image(ctx.bloom_tex_a);
790 self.registry.remove_image(ctx.bloom_tex_b);
791
792 let blur_width = (width / 2).max(1);
793 let blur_height = (height / 2).max(1);
794
795 let blur_desc_a = crate::kvasir::resource::ResourceDescriptor {
796 label: Some("Surtr Blur Texture A".into()),
797 kind: crate::kvasir::resource::ResourceKind::Image {
798 format: ctx.config.format,
799 width: blur_width,
800 height: blur_height,
801 mip_level_count: compute_mip_levels(blur_width, blur_height),
802 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
803 | wgpu::TextureUsages::TEXTURE_BINDING
804 | wgpu::TextureUsages::COPY_SRC,
805 },
806 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
807 };
808 ctx.blur_tex_a = self.registry.allocate_image(&self.device, &blur_desc_a);
809
810 let blur_desc_b = crate::kvasir::resource::ResourceDescriptor {
811 label: Some("Surtr Blur Texture B".into()),
812 kind: crate::kvasir::resource::ResourceKind::Image {
813 format: ctx.config.format,
814 width: blur_width,
815 height: blur_height,
816 mip_level_count: compute_mip_levels(blur_width, blur_height),
817 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
818 | wgpu::TextureUsages::TEXTURE_BINDING
819 | wgpu::TextureUsages::COPY_SRC,
820 },
821 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
822 };
823 ctx.blur_tex_b = self.registry.allocate_image(&self.device, &blur_desc_b);
824
825 let bloom_desc_a = crate::kvasir::resource::ResourceDescriptor {
826 label: Some("Surtr Bloom Texture A".into()),
827 kind: crate::kvasir::resource::ResourceKind::Image {
828 format: ctx.config.format,
829 width: blur_width,
830 height: blur_height,
831 mip_level_count: compute_mip_levels(blur_width, blur_height),
832 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
833 | wgpu::TextureUsages::TEXTURE_BINDING
834 | wgpu::TextureUsages::COPY_SRC,
835 },
836 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
837 };
838 ctx.bloom_tex_a = self.registry.allocate_image(&self.device, &bloom_desc_a);
839
840 let bloom_desc_b = crate::kvasir::resource::ResourceDescriptor {
841 label: Some("Surtr Bloom Texture B".into()),
842 kind: crate::kvasir::resource::ResourceKind::Image {
843 format: ctx.config.format,
844 width: blur_width,
845 height: blur_height,
846 mip_level_count: compute_mip_levels(blur_width, blur_height),
847 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
848 | wgpu::TextureUsages::TEXTURE_BINDING
849 | wgpu::TextureUsages::COPY_SRC,
850 },
851 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
852 };
853 ctx.bloom_tex_b = self.registry.allocate_image(&self.device, &bloom_desc_b);
854
855 ctx.scene_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
856 layout: &self.env_bind_group_layout,
857 entries: &[
858 wgpu::BindGroupEntry {
859 binding: 0,
860 resource: wgpu::BindingResource::TextureView(&ctx.scene_texture),
861 },
862 wgpu::BindGroupEntry {
863 binding: 1,
864 resource: wgpu::BindingResource::Sampler(&ctx.sampler),
865 },
866 ],
867 label: Some("Scene Bind Group Resize"),
868 });
869
870 let scene_views: Vec<&wgpu::TextureView> =
871 (0..32).map(|_| &ctx.scene_texture).collect();
872 ctx.scene_texture_bind_group =
873 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
874 layout: &self.texture_bind_group_layout,
875 entries: &[
876 wgpu::BindGroupEntry {
877 binding: 0,
878 resource: wgpu::BindingResource::TextureViewArray(&scene_views),
879 },
880 wgpu::BindGroupEntry {
881 binding: 1,
882 resource: wgpu::BindingResource::Sampler(&ctx.sampler),
883 },
884 ],
885 label: Some("Scene Texture Bind Group Resize"),
886 });
887
888 let depth_texture = self.device.create_texture(&wgpu::TextureDescriptor {
889 label: Some("Surtr Depth Texture"),
890 size: wgpu::Extent3d {
891 width,
892 height,
893 depth_or_array_layers: 1,
894 },
895 mip_level_count: 1,
896 sample_count: self.quality_level.msaa_sample_count(),
897 dimension: wgpu::TextureDimension::D2,
898 format: wgpu::TextureFormat::Depth32Float,
899 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
900 view_formats: &[],
901 });
902 ctx.depth_texture_view =
903 depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
904 }
905 }
906
907 pub fn reset_time(&mut self) {
908 self.start_time = std::time::Instant::now();
909 }
910
911 pub fn reclaim_vram(&mut self) {
912 log::warn!("[GPU] Sundr Compaction: Compacting Mega-Heim...");
913
914 let new_mega_heim_tex = self.device.create_texture(&wgpu::TextureDescriptor {
915 label: Some("Sundr Mega-Heim (Compacted)"),
916 size: wgpu::Extent3d {
917 width: 4096,
918 height: 4096,
919 depth_or_array_layers: 1,
920 },
921 mip_level_count: 1,
922 sample_count: 1,
923 dimension: wgpu::TextureDimension::D2,
924 format: wgpu::TextureFormat::Rgba8UnormSrgb,
925 usage: wgpu::TextureUsages::TEXTURE_BINDING
926 | wgpu::TextureUsages::COPY_DST
927 | wgpu::TextureUsages::COPY_SRC,
928 view_formats: &[],
929 });
930
931 let mut new_packer = SkylinePacker::new(4096, 4096);
932 let mut encoder = self
933 .device
934 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
935 label: Some("Heim Compaction Encoder"),
936 });
937
938 let image_entries: Vec<(String, Rect)> = self
939 .image_uv_registry
940 .iter()
941 .map(|(k, v)| (k.clone(), *v))
942 .collect();
943 for (name, old_uv) in image_entries {
944 if let Some(&tex_idx) = self.texture_registry.get(&name)
945 && tex_idx == 0
946 {
947 let w_px = (old_uv.width * 4096.0).round() as u32;
948 let h_px = (old_uv.height * 4096.0).round() as u32;
949 let old_x_px = (old_uv.x * 4096.0).round() as u32;
950 let old_y_px = (old_uv.y * 4096.0).round() as u32;
951
952 if let Some((new_x, new_y)) = new_packer.pack(w_px, h_px) {
953 encoder.copy_texture_to_texture(
954 wgpu::TexelCopyTextureInfo {
955 texture: &self.mega_heim_tex,
956 mip_level: 0,
957 origin: wgpu::Origin3d {
958 x: old_x_px,
959 y: old_y_px,
960 z: 0,
961 },
962 aspect: wgpu::TextureAspect::All,
963 },
964 wgpu::TexelCopyTextureInfo {
965 texture: &new_mega_heim_tex,
966 mip_level: 0,
967 origin: wgpu::Origin3d {
968 x: new_x,
969 y: new_y,
970 z: 0,
971 },
972 aspect: wgpu::TextureAspect::All,
973 },
974 wgpu::Extent3d {
975 width: w_px,
976 height: h_px,
977 depth_or_array_layers: 1,
978 },
979 );
980
981 let new_uv = Rect {
982 x: new_x as f32 / 4096.0,
983 y: new_y as f32 / 4096.0,
984 width: old_uv.width,
985 height: old_uv.height,
986 };
987 self.image_uv_registry.put(name.clone(), new_uv);
988 }
989 }
990 }
991
992 let text_entries: Vec<(u64, (Rect, f32, f32, f32, f32))> =
993 self.text.glyph_cache.iter().map(|(k, v)| (*k, *v)).collect();
994 for (hash, (old_uv, w_f, h_f, x_off, y_off)) in text_entries {
995 let w_px = (old_uv.width * 4096.0).round() as u32;
996 let h_px = (old_uv.height * 4096.0).round() as u32;
997 let old_x_px = (old_uv.x * 4096.0).round() as u32;
998 let old_y_px = (old_uv.y * 4096.0).round() as u32;
999
1000 if let Some((new_x, new_y)) = new_packer.pack(w_px, h_px) {
1001 encoder.copy_texture_to_texture(
1002 wgpu::TexelCopyTextureInfo {
1003 texture: &self.mega_heim_tex,
1004 mip_level: 0,
1005 origin: wgpu::Origin3d {
1006 x: old_x_px,
1007 y: old_y_px,
1008 z: 0,
1009 },
1010 aspect: wgpu::TextureAspect::All,
1011 },
1012 wgpu::TexelCopyTextureInfo {
1013 texture: &new_mega_heim_tex,
1014 mip_level: 0,
1015 origin: wgpu::Origin3d {
1016 x: new_x,
1017 y: new_y,
1018 z: 0,
1019 },
1020 aspect: wgpu::TextureAspect::All,
1021 },
1022 wgpu::Extent3d {
1023 width: w_px,
1024 height: h_px,
1025 depth_or_array_layers: 1,
1026 },
1027 );
1028
1029 let new_uv = Rect {
1030 x: new_x as f32 / 4096.0,
1031 y: new_y as f32 / 4096.0,
1032 width: old_uv.width,
1033 height: old_uv.height,
1034 };
1035 self.text.glyph_cache.put(hash, (new_uv, w_f, h_f, x_off, y_off));
1036 }
1037 }
1038
1039 self.queue.submit(std::iter::once(encoder.finish()));
1040
1041 self.mega_heim_tex = new_mega_heim_tex;
1042 let mega_heim_view_obj = self
1043 .mega_heim_tex
1044 .create_view(&wgpu::TextureViewDescriptor::default());
1045 self.texture_views[0] = mega_heim_view_obj.clone();
1046
1047 self.rebuild_texture_array_bind_group();
1048
1049 if !self.texture_bind_groups.is_empty() {
1050 self.texture_bind_groups[0] = self.mega_heim_bind_group.clone();
1051 }
1052
1053 self.heim_packer = new_packer;
1054 self.telemetry.vram_exhausted = false;
1055 }
1056}
1057
1058impl Drop for GpuRenderer {
1059 fn drop(&mut self) {
1060 let cache_dir = std::env::current_exe()
1061 .ok()
1062 .and_then(|p| p.parent().map(|d| d.join("pipeline_cache")))
1063 .unwrap_or_else(|| std::env::temp_dir().join("cvkg_pipeline_cache"));
1064 let _ = std::fs::create_dir_all(&cache_dir);
1065 let cache_path = cache_dir.join("cvkg_render_gpu.bin");
1066 if let Some(cache) = &self.pipeline_cache {
1067 if let Some(data) = cache.get_data() {
1068 if let Err(e) = std::fs::write(&cache_path, data) {
1069 log::warn!("Failed to persist pipeline cache: {}", e);
1070 }
1071 }
1072 }
1073
1074 let _ = self.device.poll(wgpu::PollType::Wait {
1075 submission_index: None,
1076 timeout: None,
1077 });
1078 }
1079}
1080
1081impl GpuRenderer {
1082 pub(crate) fn current_width(&self) -> u32 {
1083 if let Some(id) = self.current_window {
1084 self.surfaces.get(&id).map(|s| s.config.width).unwrap_or(1)
1085 } else {
1086 self.headless_context.as_ref().map(|h| h.width).unwrap_or(1)
1087 }
1088 }
1089
1090 pub(crate) fn current_height(&self) -> u32 {
1091 if let Some(id) = self.current_window {
1092 self.surfaces.get(&id).map(|s| s.config.height).unwrap_or(1)
1093 } else {
1094 self.headless_context
1095 .as_ref()
1096 .map(|h| h.height)
1097 .unwrap_or(1)
1098 }
1099 }
1100
1101 pub(crate) fn current_scale_factor(&self) -> f32 {
1102 if let Some(id) = self.current_window {
1103 self.surfaces
1104 .get(&id)
1105 .map(|s| s.scale_factor)
1106 .unwrap_or(1.0)
1107 } else {
1108 self.headless_context
1109 .as_ref()
1110 .map(|h| h.scale_factor)
1111 .unwrap_or(1.0)
1112 }
1113 }
1114
1115 pub(crate) fn current_time(&self) -> f32 {
1116 self.start_time.elapsed().as_secs_f32()
1117 }
1118
1119 pub async fn forge_headless(width: u32, height: u32) -> Self {
1121 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
1122 backends: wgpu::Backends::all(),
1123 flags: wgpu::InstanceFlags::default(),
1124 backend_options: wgpu::BackendOptions::default(),
1125 display: None,
1126 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
1127 });
1128
1129 log::info!("[GPU] Requesting HighPerformance adapter (headless)...");
1131 let mut adapter = instance
1132 .request_adapter(&wgpu::RequestAdapterOptions {
1133 power_preference: wgpu::PowerPreference::HighPerformance,
1134 compatible_surface: None,
1135 force_fallback_adapter: false,
1136 })
1137 .await
1138 .ok();
1139
1140 if adapter.is_none() {
1141 log::warn!(
1142 "[GPU] HighPerformance adapter failed (possible Bumblebee/Optimus), trying LowPower..."
1143 );
1144 adapter = instance
1145 .request_adapter(&wgpu::RequestAdapterOptions {
1146 power_preference: wgpu::PowerPreference::LowPower,
1147 compatible_surface: None,
1148 force_fallback_adapter: false,
1149 })
1150 .await
1151 .ok();
1152 }
1153
1154 if adapter.is_none() {
1155 log::warn!("[GPU] Hardware adapters failed, trying Software fallback...");
1156 adapter = instance
1157 .request_adapter(&wgpu::RequestAdapterOptions {
1158 power_preference: wgpu::PowerPreference::LowPower,
1159 compatible_surface: None,
1160 force_fallback_adapter: true,
1161 })
1162 .await
1163 .ok();
1164 }
1165
1166 let adapter = adapter.expect("Failed to find a suitable GPU for Surtr");
1167 let info = adapter.get_info();
1168 let caps = crate::subsystems::GpuCapabilities::detect(
1169 &info.name,
1170 format!("{:?}", info.backend),
1171 );
1172 log::info!(
1173 "[GPU] Selected adapter: {} ({:?}) on backend: {:?} -- detected as {}",
1174 info.name,
1175 info.device_type,
1176 info.backend,
1177 caps.vendor
1178 );
1179 log::info!("[GPU] Driver info: {} - {}", info.driver, info.driver_info);
1180 let required_features = adapter.features()
1181 & (wgpu::Features::TIMESTAMP_QUERY
1182 | wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING
1183 | wgpu::Features::TEXTURE_BINDING_ARRAY);
1184
1185 let (device, queue) = adapter
1186 .request_device(&wgpu::DeviceDescriptor {
1187 label: Some("Surtr Headless Forge"),
1188 required_features,
1189 required_limits: wgpu::Limits {
1190 max_bindings_per_bind_group: adapter
1191 .limits()
1192 .max_bindings_per_bind_group
1193 .min(256),
1194 max_binding_array_elements_per_shader_stage: adapter
1195 .limits()
1196 .max_binding_array_elements_per_shader_stage
1197 .min(256),
1198 ..wgpu::Limits::default()
1199 },
1200 memory_hints: wgpu::MemoryHints::default(),
1201 experimental_features: wgpu::ExperimentalFeatures::disabled(),
1202 trace: wgpu::Trace::Off,
1203 })
1204 .await
1205 .expect("Failed to create Surtr device");
1206
1207 let instance = Arc::new(instance);
1208 let adapter = Arc::new(adapter);
1209
1210 device.on_uncaptured_error(Arc::new(|error| {
1211 log::error!(
1212 "[GPU] Uncaptured device error (Device Lost or Panic): {:?}",
1213 error
1214 );
1215 }));
1216
1217 let device = Arc::new(device);
1218 let queue = Arc::new(queue);
1219
1220 Self::forge_internal(
1221 instance,
1222 adapter,
1223 device,
1224 queue,
1225 None,
1226 Some((width, height, wgpu::TextureFormat::Rgba8UnormSrgb)),
1227 )
1228 .await
1229 }
1230}