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::{HashMap, VecDeque};
9use std::num::NonZeroUsize;
10use std::sync::Arc;
11
12pub use crate::subsystems::RendererConfig;
14
15pub(crate) mod context_helpers;
16pub(crate) mod draw;
17pub(crate) mod init;
18pub(crate) mod pipelines;
19pub(crate) mod svg;
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, Default, PartialEq, Eq)]
55pub enum QualityLevel {
56 #[default]
57 High,
58 Medium,
59 Low,
60}
61
62impl QualityLevel {
63 pub fn msaa_sample_count(self) -> u32 {
65 match self {
66 QualityLevel::High => 4,
67 QualityLevel::Medium => 2,
68 QualityLevel::Low => 1,
69 }
70 }
71}
72
73pub struct GpuRenderer {
75 pub(crate) instance: Arc<wgpu::Instance>,
76 pub(crate) adapter: Arc<wgpu::Adapter>,
77 pub(crate) device: Arc<wgpu::Device>,
78 pub(crate) queue: Arc<wgpu::Queue>,
79
80 pub(crate) registry: crate::kvasir::registry::ResourceRegistry,
82
83 pub(crate) active_offscreens: Vec<crate::types::OffscreenEffectConfig>,
84 pub(crate) world_space_panels: Vec<(u64, cvkg_vdom::WorldSpacePanel)>,
85 pub(crate) panel_vdoms: HashMap<u64, cvkg_vdom::VDom>,
88 pub(crate) effect_pipelines: std::collections::HashMap<String, wgpu::RenderPipeline>,
89 pub(crate) effect_params_buffer: wgpu::Buffer,
90 pub(crate) effect_params_bind_group: wgpu::BindGroup,
91 pub(crate) linear_sampler: wgpu::Sampler,
92 pub ai_material_rx: Option<
94 std::sync::mpsc::Receiver<
95 Result<crate::material::CompiledMaterial, crate::ai::GeneratorError>,
96 >,
97 >,
98
99 pub(crate) surfaces: std::collections::HashMap<winit::window::WindowId, SurfaceContext>,
101 pub(crate) current_window: Option<winit::window::WindowId>,
102 pub headless_context: Option<HeadlessContext>,
103
104 pub(crate) text: crate::types::TextSubsystem,
106 pub(crate) mega_heim_tex: wgpu::Texture,
107 pub(crate) mega_heim_bind_group: wgpu::BindGroup,
108 pub(crate) heim_packer: SkylinePacker,
109 pub(crate) image_uv_registry: LruCache<String, Rect>,
110 pub(crate) texture_registry: LruCache<String, u32>,
111 pub(crate) texture_views: Vec<wgpu::TextureView>,
112 pub(crate) dummy_sampler: wgpu::Sampler,
113 pub(crate) dummy_depth_view: wgpu::TextureView,
120 pub(crate) dummy_depth_view_msaa: wgpu::TextureView,
127 pub(crate) shadow_map_texture: Option<wgpu::Texture>,
130 pub(crate) shadow_map_view: Option<wgpu::TextureView>,
132 pub(crate) shadow_sampler: Option<wgpu::Sampler>,
134 pub(crate) shadow_light_vp: glam::Mat4,
136 pub(crate) shadow_map_size: u32,
138 pub(crate) shadow_bias: f32,
140 pub(crate) svg: crate::types::SvgSubsystem,
141
142 pub(crate) dummy_texture_bind_group: wgpu::BindGroup,
144 pub(crate) dummy_env_bind_group: wgpu::BindGroup,
145 pub(crate) texture_bind_group_layout: wgpu::BindGroupLayout,
146 pub(crate) texture_bind_groups: Vec<wgpu::BindGroup>,
147 pub(crate) shared_elements: LruCache<String, cvkg_core::Rect>,
148
149 pub(crate) geometry_buffers: crate::types::GeometryBuffers,
151 pub(crate) vertices: Vec<Vertex>,
152 pub(crate) indices: Vec<u32>,
153 pub(crate) instance_data: Vec<InstanceData>,
154 pub(crate) instance_data_3d: Vec<InstanceData3D>,
158 pub(crate) staging_belt: wgpu::util::StagingBelt,
159 pub(crate) staging_command_buffers: Vec<wgpu::CommandBuffer>,
160 pub(crate) draw_calls: Vec<DrawCall>,
161 pub(crate) current_texture_id: Option<u32>,
162 pub(crate) current_panel_id: Option<u64>,
165 pub(crate) panel_stack: Vec<u64>,
166
167 pub(crate) opacity_stack: Vec<f32>,
169 pub(crate) clip_stack: Vec<Rect>,
170 pub(crate) slice_stack: Vec<(f32, f32)>,
171 pub(crate) shadow_stack: Vec<ShadowState>,
172
173 pub blur_pipeline: Option<wgpu::RenderPipeline>,
177 pub blur_uniform: Option<wgpu::Buffer>,
180 pub blur_bind_group_layout: Option<wgpu::BindGroupLayout>,
183 pub blend_pipeline: Option<wgpu::RenderPipeline>,
186 pub blend_bind_group_layout: Option<wgpu::BindGroupLayout>,
189 pub flood_pipeline: Option<wgpu::RenderPipeline>,
192 pub copy_bind_group_layout: Option<wgpu::BindGroupLayout>,
195
196 pub(crate) theme_buffer: wgpu::Buffer,
198 pub(crate) scene_buffer: wgpu::Buffer,
199 pub(crate) theme_stack: Vec<ColorTheme>,
201 pub(crate) portal_theme_stack: Vec<ColorTheme>,
203 pub(crate) berserker_bind_group: wgpu::BindGroup,
204 pub(crate) berserker_bind_group_layout: wgpu::BindGroupLayout,
205 pub(crate) start_time: std::time::Instant,
206 pub(crate) current_theme: ColorTheme,
207 pub(crate) current_scene: SceneUniforms,
208 pub(crate) current_z: f32,
209
210 pub(crate) default_background_color: [f32; 4],
214
215 pub(crate) app_drew_background: bool,
218
219 pub(crate) frame_rendered: bool,
222
223 pub(crate) current_draw_order: i32,
226
227 pub(crate) pipeline: wgpu::RenderPipeline,
229 pub(crate) opaque_pipeline: wgpu::RenderPipeline,
231 pub(crate) ui_pipeline: wgpu::RenderPipeline,
234 pub(crate) glass_pipeline: wgpu::RenderPipeline,
236 pub(crate) background_pipeline: wgpu::RenderPipeline,
237 pub(crate) bloom_extract_pipeline: wgpu::RenderPipeline,
238 pub(crate) copy_pipeline: wgpu::RenderPipeline,
240 pub(crate) composite_pipeline: wgpu::RenderPipeline,
241 pub(crate) color_blind_pipeline: wgpu::RenderPipeline,
243 pub(crate) volumetric_pipeline: wgpu::RenderPipeline,
245 pub(crate) volumetric_bind_group_layout: wgpu::BindGroupLayout,
247 pub(crate) volumetric_uniform_buffer: wgpu::Buffer,
249 pub(crate) volumetric_depth_sampler: wgpu::Sampler,
251 pub(crate) hologram_instances: Vec<HologramInstance>,
254 pub(crate) kawase_down_pipeline: wgpu::RenderPipeline,
256 pub(crate) kawase_up_pipeline: wgpu::RenderPipeline,
258 pub(crate) kawase_bind_group_layout: wgpu::BindGroupLayout,
260 pub(crate) kawase_uniform: wgpu::Buffer,
262 pub(crate) kawase_uniform_buffers: Vec<wgpu::Buffer>,
264 pub(crate) env_bind_group_layout: wgpu::BindGroupLayout,
266
267 pub telemetry: cvkg_core::TelemetryData,
269
270 pub(crate) pipeline_cache: Option<wgpu::PipelineCache>,
273
274 pub frame_budget: cvkg_core::FrameBudget,
276 pub(crate) capture_staging_buffer: Option<wgpu::Buffer>,
278 pub last_redraw_start: std::time::Instant,
280 pub last_frame_start: std::time::Instant,
282
283 pub(crate) vram_buffers_bytes: u64,
285 pub(crate) vram_textures_bytes: u64,
286
287 pub(crate) _debug_layout: bool,
289
290 pub(crate) transform_stack: Vec<glam::Mat3>,
292 pub(crate) transform_stack_3d: Vec<glam::Mat4>,
294 pub redraw_requested: bool,
296 pub(crate) compositor_index_cursor: u32,
298
299 pub bloom_enabled: bool,
301 pub volumetric_enabled: bool,
303
304 pub(crate) path_geometry_cache: lru::LruCache<u64, (Vec<Vertex>, Vec<u32>)>,
306 pub(crate) color_blind_bind_group_layout: wgpu::BindGroupLayout,
308 pub(crate) color_blind_uniform_buffer: wgpu::Buffer,
310 pub color_blind_mode: crate::color_blindness::ColorBlindMode,
312 pub color_blind_intensity: f32,
314 pub(crate) sampler: wgpu::Sampler,
316
317 pub(crate) skuld_queries: Option<wgpu::QuerySet>,
319 pub(crate) skuld_buffer: Option<wgpu::Buffer>,
320 pub(crate) skuld_read_buffer: Option<wgpu::Buffer>,
321 pub(crate) skuld_period: f32,
322 pub last_gpu_time_ns: u64,
323
324 pub(crate) particle_compute_pipeline: wgpu::ComputePipeline,
326 pub(crate) particle_compute_bgl: wgpu::BindGroupLayout,
327 pub(crate) particle_buffer: wgpu::Buffer,
328 pub(crate) particle_uniform_buffer: wgpu::Buffer,
329 pub(crate) particles: crate::types::ParticleSubsystem,
330 pub(crate) particle_render_pipeline: wgpu::RenderPipeline,
331 pub(crate) particle_render_bgl: wgpu::BindGroupLayout,
332 pub(crate) particle_render_bind_group: Option<wgpu::BindGroup>,
333 pub(crate) particle_compute_bind_group: Option<wgpu::BindGroup>,
334
335 pub(crate) vnode_stack: Vec<(Rect, &'static str)>,
337
338 pub(crate) event_handlers: std::collections::HashMap<
340 String,
341 Vec<std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>>,
342 >,
343
344 pub(crate) render_error_count: u64,
346 pub(crate) has_fatal_error: bool,
347
348 pub(crate) glass_output_bind_group_layout: wgpu::BindGroupLayout,
350 pub(crate) current_draw_material: cvkg_core::DrawMaterial,
352
353 pub(crate) portal_regions: std::collections::VecDeque<cvkg_core::Rect>,
355
356 pub(crate) gradient_stop_texture: wgpu::Texture,
359 pub(crate) gradient_stop_texture_view: wgpu::TextureView,
360 pub(crate) gradient_bind_group: wgpu::BindGroup,
361 pub(crate) gradient_texture_cache:
363 std::collections::HashMap<u64, (wgpu::Texture, wgpu::TextureView, wgpu::BindGroup)>,
364 pub(crate) gradient_stops_hash: u64,
366 pub(crate) gradient_bind_group_layout: wgpu::BindGroupLayout,
368
369 pub(crate) cached_graph_plan: Option<crate::kvasir::graph_cache::CachedGraphPlan>,
371 pub(crate) material_compilation_hash: u64,
373 pub(crate) memo_cache: std::collections::HashMap<u64, crate::types::MemoEntry>,
375 pub(crate) frame_generation: u64,
377 pub(crate) config: crate::subsystems::RendererConfig,
379 pub(crate) quality_level: QualityLevel,
381 pub(crate) bind_group_cache: std::sync::Mutex<
383 std::collections::HashMap<
384 (
385 Option<winit::window::WindowId>,
386 crate::kvasir::resource::ResourceId,
387 u32,
388 bool,
389 ),
390 wgpu::BindGroup,
391 >,
392 >,
393 pub(crate) texture_view_cache: std::sync::Mutex<
395 std::collections::HashMap<
396 (
397 Option<winit::window::WindowId>,
398 crate::kvasir::resource::ResourceId,
399 u32,
400 ),
401 wgpu::TextureView,
402 >,
403 >,
404}
405
406#[cfg(target_arch = "wasm32")]
407unsafe impl Send for GpuRenderer {}
408#[cfg(target_arch = "wasm32")]
409unsafe impl Sync for GpuRenderer {}
410
411#[derive(Debug, Clone)]
413pub struct HologramInstance {
414 pub rect: cvkg_core::Rect,
416 pub id_hash: u32,
418 pub time: f32,
420}
421
422pub trait ClearInto {
425 fn clear_into(&mut self);
426}
427
428impl<K, V, S> ClearInto for std::collections::HashMap<K, V, S>
429where
430 S: std::hash::BuildHasher,
431{
432 fn clear_into(&mut self) {
433 self.clear();
434 }
435}
436
437impl<T> ClearInto for Vec<T> {
438 fn clear_into(&mut self) {
439 self.clear();
440 }
441}
442
443fn load_pipeline_cache_with_integrity_check(
449 cache_path: &std::path::Path,
450) -> Result<Option<Vec<u8>>, String> {
451 let cache_data = match std::fs::read(cache_path) {
452 Ok(d) => d,
453 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
454 Err(e) => return Err(format!("read failed: {e}")),
455 };
456
457 let hash_path = cache_path.with_extension("bin.sha256");
458 let expected_hash = match std::fs::read_to_string(&hash_path) {
459 Ok(s) => s.trim().to_lowercase(),
460 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
461 return Err(format!(
462 "sidecar hash file missing at {}",
463 hash_path.display()
464 ));
465 }
466 Err(e) => return Err(format!("sidecar read failed: {e}")),
467 };
468
469 let actual = compute_sha256(&cache_data);
470 let actual_hex: String = actual.iter().map(|b| format!("{:02x}", b)).collect();
471 if actual_hex != expected_hash {
472 return Err(format!(
473 "hash mismatch: expected {expected_hash}, got {actual_hex}"
474 ));
475 }
476
477 Ok(Some(cache_data))
478}
479
480fn compute_sha256(data: &[u8]) -> [u8; 32] {
482 let mut hasher = Sha256::new();
483 hasher.update(data);
484 hasher.finalize()
485}
486
487#[derive(Clone)]
490struct Sha256 {
491 state: [u32; 8],
492 buffer: [u8; 64],
493 buffer_len: usize,
494 total_len: u64,
495}
496
497impl Sha256 {
498 const K: [u32; 64] = [
499 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
500 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
501 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
502 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
503 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
504 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
505 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
506 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
507 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
508 0xc67178f2,
509 ];
510
511 fn new() -> Self {
512 Self {
513 state: [
514 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
515 0x5be0cd19,
516 ],
517 buffer: [0; 64],
518 buffer_len: 0,
519 total_len: 0,
520 }
521 }
522
523 fn update(&mut self, data: &[u8]) {
524 self.total_len = self.total_len.wrapping_add(data.len() as u64);
525 for &b in data {
526 self.buffer[self.buffer_len] = b;
527 self.buffer_len += 1;
528 if self.buffer_len == 64 {
529 let block = self.buffer;
530 self.compress(&block);
531 self.buffer_len = 0;
532 }
533 }
534 }
535
536 fn finalize(mut self) -> [u8; 32] {
537 self.buffer[self.buffer_len] = 0x80;
538 self.buffer_len += 1;
539 if self.buffer_len > 56 {
540 for b in &mut self.buffer[self.buffer_len..] {
541 *b = 0;
542 }
543 let block = self.buffer;
544 self.compress(&block);
545 self.buffer_len = 0;
546 }
547 for b in &mut self.buffer[self.buffer_len..56] {
548 *b = 0;
549 }
550 let bit_len = self.total_len.wrapping_mul(8);
551 self.buffer[56..64].copy_from_slice(&bit_len.to_be_bytes());
552 let block = self.buffer;
553 self.compress(&block);
554
555 let mut out = [0u8; 32];
556 for (i, &s) in self.state.iter().enumerate() {
557 out[i * 4..(i + 1) * 4].copy_from_slice(&s.to_be_bytes());
558 }
559 out
560 }
561
562 fn compress(&mut self, block: &[u8]) {
563 let mut w = [0u32; 64];
564 for i in 0..16 {
565 w[i] = u32::from_be_bytes([
566 block[i * 4],
567 block[i * 4 + 1],
568 block[i * 4 + 2],
569 block[i * 4 + 3],
570 ]);
571 }
572 for i in 16..64 {
573 let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
574 let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
575 w[i] = w[i - 16]
576 .wrapping_add(s0)
577 .wrapping_add(w[i - 7])
578 .wrapping_add(s1);
579 }
580 let mut a = self.state[0];
581 let mut b = self.state[1];
582 let mut c = self.state[2];
583 let mut d = self.state[3];
584 let mut e = self.state[4];
585 let mut f = self.state[5];
586 let mut g = self.state[6];
587 let mut h = self.state[7];
588 for (i, wi) in w.iter().enumerate() {
589 let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
590 let ch = (e & f) ^ ((!e) & g);
591 let t1 = h
592 .wrapping_add(s1)
593 .wrapping_add(ch)
594 .wrapping_add(Self::K[i])
595 .wrapping_add(*wi);
596 let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
597 let mj = (a & b) ^ (a & c) ^ (b & c);
598 let t2 = s0.wrapping_add(mj);
599 h = g;
600 g = f;
601 f = e;
602 e = d.wrapping_add(t1);
603 d = c;
604 c = b;
605 b = a;
606 a = t1.wrapping_add(t2);
607 }
608 self.state[0] = self.state[0].wrapping_add(a);
609 self.state[1] = self.state[1].wrapping_add(b);
610 self.state[2] = self.state[2].wrapping_add(c);
611 self.state[3] = self.state[3].wrapping_add(d);
612 self.state[4] = self.state[4].wrapping_add(e);
613 self.state[5] = self.state[5].wrapping_add(f);
614 self.state[6] = self.state[6].wrapping_add(g);
615 self.state[7] = self.state[7].wrapping_add(h);
616 }
617}
618
619fn compute_mip_levels(width: u32, height: u32) -> u32 {
620 let max_dim = width.max(height);
621 if max_dim <= 1 {
622 return 1;
623 }
624 (32 - max_dim.leading_zeros()).clamp(2, 8)
625}
626
627impl GpuRenderer {
628 pub fn hologram_instances(&self) -> &[HologramInstance] {
630 &self.hologram_instances
631 }
632
633 pub fn set_quality_level(&mut self, level: QualityLevel) {
634 self.quality_level = level;
635 }
636
637 pub fn set_config(&mut self, config: crate::subsystems::RendererConfig) {
638 self.config = config;
639 }
640
641 pub fn config(&self) -> &crate::subsystems::RendererConfig {
642 &self.config
643 }
644
645 pub fn quality_level(&self) -> QualityLevel {
646 self.quality_level
647 }
648
649 pub(crate) fn lock_or_clear_cache<'a, T: ClearInto>(
650 lock: &'a std::sync::Mutex<T>,
651 ) -> std::sync::MutexGuard<'a, T> {
652 match lock.lock() {
653 Ok(guard) => guard,
654 Err(poisoned) => {
655 tracing::warn!("[GPU] lock_or_clear_cache: mutex poisoned, clearing cache...");
656 let mut guard = poisoned.into_inner();
657 guard.clear_into();
658 guard
659 }
660 }
661 }
662
663 pub fn update_mouse(&mut self, mouse: [f32; 2], velocity: [f32; 2]) {
664 self.current_scene.mouse = mouse;
665 self.current_scene.mouse_velocity = velocity;
666 self.queue.write_buffer(
667 &self.scene_buffer,
668 0,
669 bytemuck::bytes_of(&self.current_scene),
670 );
671 }
672
673 pub fn invalidate_material_cache(&mut self) {
674 self.cached_graph_plan = None;
675 }
676
677 pub fn invalidate_all_caches(&mut self) -> usize {
678 let mut cleared = 0;
679 {
680 let mut bg_cache = Self::lock_or_clear_cache(&self.bind_group_cache);
681 cleared += bg_cache.len();
682 bg_cache.clear();
683 }
684 {
685 let mut view_cache = Self::lock_or_clear_cache(&self.texture_view_cache);
686 cleared += view_cache.len();
687 view_cache.clear();
688 }
689 cleared += self.text.shaped_cache.len();
690 self.text.shaped_cache.clear();
691 cleared += self.svg.model_cache.len();
692 self.svg.model_cache.clear();
693 cleared += self.svg.tree_cache.len();
694 self.svg.tree_cache.clear();
695 self.svg.clear_filter_batches();
696 cleared
697 }
698
699 pub fn prewarm_text_cache(&mut self, labels: &[(&str, f32)]) {
700 let mut count = 0;
701 for (text, size) in labels {
702 let cache_key = (text.to_string(), (size * 100.0) as u32);
703 if self.text.shaped_cache.contains(&cache_key) {
704 continue;
705 }
706 let style = cvkg_runic_text::TextStyle::new("Inter", *size);
707 let spans = [cvkg_runic_text::TextSpan::new(text, style)];
708 if let Ok(shaped) = self.text.engine.shape_layout(
709 &spans,
710 None,
711 cvkg_runic_text::TextAlign::Start,
712 cvkg_runic_text::TextOverflow::Visible,
713 ) {
714 self.text
715 .shaped_cache
716 .put(cache_key, std::sync::Arc::new(shaped));
717 count += 1;
718 }
719 }
720 if count > 0 {
721 tracing::info!("[Surtr] prewarm_text_cache: pre-shaped {} labels", count);
722 }
723 }
724
725 pub(crate) fn select_best_surface_format(
726 formats: &[wgpu::TextureFormat],
727 ) -> wgpu::TextureFormat {
728 if formats.is_empty() {
729 return wgpu::TextureFormat::Rgba8Unorm;
730 }
731 let preferred_formats = [
732 wgpu::TextureFormat::Rgba16Float,
733 wgpu::TextureFormat::Rgba8Unorm,
734 wgpu::TextureFormat::Bgra8UnormSrgb,
735 wgpu::TextureFormat::Rgba8UnormSrgb,
736 wgpu::TextureFormat::Bgra8Unorm,
737 wgpu::TextureFormat::Rgba8Unorm,
738 wgpu::TextureFormat::Rgba8Unorm,
739 ];
740 for preferred in &preferred_formats {
741 if formats.contains(preferred) {
742 return *preferred;
743 }
744 }
745 if formats.contains(&wgpu::TextureFormat::Rgba8Unorm) {
746 return wgpu::TextureFormat::Rgba8Unorm;
747 }
748 formats[0]
749 }
750
751 pub(crate) fn rebuild_texture_array_bind_group(&mut self) {
752 let views: Vec<&wgpu::TextureView> = self.texture_views.iter().collect();
753 self.mega_heim_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
754 layout: &self.texture_bind_group_layout,
755 entries: &[
756 wgpu::BindGroupEntry {
757 binding: 0,
758 resource: wgpu::BindingResource::TextureViewArray(&views),
759 },
760 wgpu::BindGroupEntry {
761 binding: 1,
762 resource: wgpu::BindingResource::Sampler(&self.dummy_sampler),
763 },
764 ],
765 label: Some("Mega-Heim Rebuilt Bind Group"),
766 });
767 }
768
769 pub(crate) fn update_vram_telemetry(&mut self) {
770 let buffers = self.geometry_buffers.vertex_buffer.size()
771 + self.geometry_buffers.index_buffer.size()
772 + self.geometry_buffers.instance_buffer.size()
773 + self.scene_buffer.size()
774 + self.theme_buffer.size()
775 + self.particle_buffer.size()
776 + self.particle_uniform_buffer.size();
777
778 let mut textures = self.config.mega_heim_vram_bytes();
779 textures += 4; for surface in self.surfaces.values() {
782 let width = surface.config.width;
783 let height = surface.config.height;
784 let format_bytes = 8; textures += (width * height * format_bytes) as u64; textures +=
787 (width * height * format_bytes * self.quality_level.msaa_sample_count()) as u64; textures += (width * height * 4) as u64; let blur_width = (width / 2).max(1);
791 let blur_height = (height / 2).max(1);
792 let blur_bytes = (blur_width * blur_height * 4) as u64;
793 textures += blur_bytes * 4; }
795
796 if let Some(ref ctx) = self.headless_context {
797 let format_bytes = 8; textures += (ctx.width * ctx.height * format_bytes) as u64; textures +=
800 (ctx.width * ctx.height * format_bytes * self.quality_level.msaa_sample_count())
801 as u64; textures += (ctx.width * ctx.height * 4) as u64; textures += (ctx.width * ctx.height * 4) as u64; }
805
806 self.vram_buffers_bytes = buffers;
807 self.vram_textures_bytes = textures;
808 self.telemetry.vram_usage_mb = (buffers + textures) as f32 / (1024.0 * 1024.0);
809 }
810
811 pub fn get_telemetry(&self) -> cvkg_core::TelemetryData {
812 self.telemetry.clone()
813 }
814
815 pub fn resize(
816 &mut self,
817 window_id: winit::window::WindowId,
818 width: u32,
819 height: u32,
820 scale_factor: f32,
821 ) {
822 if width > 0
823 && height > 0
824 && let Some(ctx) = self.surfaces.get_mut(&window_id)
825 {
826 if ctx.config.width == width && ctx.config.height == height {
827 return;
828 }
829
830 tracing::info!("[GPU] Reconfiguring surface: {}x{}", width, height);
831 GpuRenderer::lock_or_clear_cache(&self.bind_group_cache).clear();
832 GpuRenderer::lock_or_clear_cache(&self.texture_view_cache).clear();
833 self.text.shaped_cache.clear();
834 ctx.config.width = width;
835 ctx.config.height = height;
836 ctx.scale_factor = scale_factor;
837 ctx.surface.configure(&self.device, &ctx.config);
838
839 let texture_desc = wgpu::TextureDescriptor {
840 label: Some("Surtr Scene Texture"),
841 size: wgpu::Extent3d {
842 width,
843 height,
844 depth_or_array_layers: 1,
845 },
846 mip_level_count: 1,
847 sample_count: 1,
848 dimension: wgpu::TextureDimension::D2,
849 format: wgpu::TextureFormat::Rgba16Float,
850 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
851 | wgpu::TextureUsages::TEXTURE_BINDING,
852 view_formats: &[],
853 };
854
855 let scene_tex = self.device.create_texture(&texture_desc);
856
857 let msaa_desc = wgpu::TextureDescriptor {
858 label: Some("Scene MSAA"),
859 size: texture_desc.size,
860 mip_level_count: 1,
861 sample_count: self.quality_level.msaa_sample_count(),
862 dimension: wgpu::TextureDimension::D2,
863 format: wgpu::TextureFormat::Rgba16Float,
864 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
865 view_formats: &[],
866 };
867 let scene_msaa_tex = self.device.create_texture(&msaa_desc);
868 ctx.scene_texture = scene_tex.create_view(&wgpu::TextureViewDescriptor::default());
869 ctx.scene_msaa_texture =
870 scene_msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
871
872 self.registry.remove_image(ctx.blur_tex_a);
873 self.registry.remove_image(ctx.blur_tex_b);
874 self.registry.remove_image(ctx.bloom_tex_a);
875 self.registry.remove_image(ctx.bloom_tex_b);
876
877 let blur_width = (width / 2).max(1);
878 let blur_height = (height / 2).max(1);
879
880 let blur_desc_a = crate::kvasir::resource::ResourceDescriptor {
881 label: Some("Surtr Blur Texture A".into()),
882 kind: crate::kvasir::resource::ResourceKind::Image {
883 format: ctx.config.format,
884 width: blur_width,
885 height: blur_height,
886 mip_level_count: compute_mip_levels(blur_width, blur_height),
887 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
888 | wgpu::TextureUsages::TEXTURE_BINDING
889 | wgpu::TextureUsages::COPY_SRC,
890 },
891 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
892 };
893 ctx.blur_tex_a = self.registry.allocate_image(&self.device, &blur_desc_a);
894
895 let blur_desc_b = crate::kvasir::resource::ResourceDescriptor {
896 label: Some("Surtr Blur Texture B".into()),
897 kind: crate::kvasir::resource::ResourceKind::Image {
898 format: ctx.config.format,
899 width: blur_width,
900 height: blur_height,
901 mip_level_count: compute_mip_levels(blur_width, blur_height),
902 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
903 | wgpu::TextureUsages::TEXTURE_BINDING
904 | wgpu::TextureUsages::COPY_SRC,
905 },
906 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
907 };
908 ctx.blur_tex_b = self.registry.allocate_image(&self.device, &blur_desc_b);
909
910 let bloom_desc_a = crate::kvasir::resource::ResourceDescriptor {
911 label: Some("Surtr Bloom Texture A".into()),
912 kind: crate::kvasir::resource::ResourceKind::Image {
913 format: ctx.config.format,
914 width: blur_width,
915 height: blur_height,
916 mip_level_count: compute_mip_levels(blur_width, blur_height),
917 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
918 | wgpu::TextureUsages::TEXTURE_BINDING
919 | wgpu::TextureUsages::COPY_SRC,
920 },
921 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
922 };
923 ctx.bloom_tex_a = self.registry.allocate_image(&self.device, &bloom_desc_a);
924
925 let bloom_desc_b = crate::kvasir::resource::ResourceDescriptor {
926 label: Some("Surtr Bloom Texture B".into()),
927 kind: crate::kvasir::resource::ResourceKind::Image {
928 format: ctx.config.format,
929 width: blur_width,
930 height: blur_height,
931 mip_level_count: compute_mip_levels(blur_width, blur_height),
932 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
933 | wgpu::TextureUsages::TEXTURE_BINDING
934 | wgpu::TextureUsages::COPY_SRC,
935 },
936 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
937 };
938 ctx.bloom_tex_b = self.registry.allocate_image(&self.device, &bloom_desc_b);
939
940 ctx.scene_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
941 layout: &self.env_bind_group_layout,
942 entries: &[
943 wgpu::BindGroupEntry {
944 binding: 0,
945 resource: wgpu::BindingResource::TextureView(&ctx.scene_texture),
946 },
947 wgpu::BindGroupEntry {
948 binding: 1,
949 resource: wgpu::BindingResource::Sampler(&ctx.sampler),
950 },
951 ],
952 label: Some("Scene Bind Group Resize"),
953 });
954
955 let scene_views: Vec<&wgpu::TextureView> =
956 (0..32).map(|_| &ctx.scene_texture).collect();
957 ctx.scene_texture_bind_group =
958 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
959 layout: &self.texture_bind_group_layout,
960 entries: &[
961 wgpu::BindGroupEntry {
962 binding: 0,
963 resource: wgpu::BindingResource::TextureViewArray(&scene_views),
964 },
965 wgpu::BindGroupEntry {
966 binding: 1,
967 resource: wgpu::BindingResource::Sampler(&ctx.sampler),
968 },
969 ],
970 label: Some("Scene Texture Bind Group Resize"),
971 });
972
973 let depth_texture = self.device.create_texture(&wgpu::TextureDescriptor {
974 label: Some("Surtr Depth Texture"),
975 size: wgpu::Extent3d {
976 width,
977 height,
978 depth_or_array_layers: 1,
979 },
980 mip_level_count: 1,
981 sample_count: self.quality_level.msaa_sample_count(),
982 dimension: wgpu::TextureDimension::D2,
983 format: wgpu::TextureFormat::Depth32Float,
984 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
985 | wgpu::TextureUsages::TEXTURE_BINDING,
986 view_formats: &[],
987 });
988 ctx.depth_texture_view =
989 depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
990 }
991 }
992
993 pub fn reset_time(&mut self) {
994 self.start_time = std::time::Instant::now();
995 }
996
997 pub fn reclaim_vram(&mut self) {
998 tracing::warn!("[GPU] Sundr Compaction: Compacting Mega-Heim...");
999
1000 let new_mega_heim_tex = self.device.create_texture(&wgpu::TextureDescriptor {
1001 label: Some("Sundr Mega-Heim (Compacted)"),
1002 size: wgpu::Extent3d {
1003 width: 4096,
1004 height: 4096,
1005 depth_or_array_layers: 1,
1006 },
1007 mip_level_count: 1,
1008 sample_count: 1,
1009 dimension: wgpu::TextureDimension::D2,
1010 format: wgpu::TextureFormat::Rgba8UnormSrgb,
1011 usage: wgpu::TextureUsages::TEXTURE_BINDING
1012 | wgpu::TextureUsages::COPY_DST
1013 | wgpu::TextureUsages::COPY_SRC,
1014 view_formats: &[],
1015 });
1016
1017 let mut new_packer = SkylinePacker::new(4096, 4096);
1018 let mut encoder = self
1019 .device
1020 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1021 label: Some("Heim Compaction Encoder"),
1022 });
1023
1024 let image_entries: Vec<(String, Rect)> = self
1025 .image_uv_registry
1026 .iter()
1027 .map(|(k, v)| (k.clone(), *v))
1028 .collect();
1029 for (name, old_uv) in image_entries {
1030 if let Some(&tex_idx) = self.texture_registry.get(&name)
1031 && tex_idx == 0
1032 {
1033 let w_px = (old_uv.width * 4096.0).round() as u32;
1034 let h_px = (old_uv.height * 4096.0).round() as u32;
1035 let old_x_px = (old_uv.x * 4096.0).round() as u32;
1036 let old_y_px = (old_uv.y * 4096.0).round() as u32;
1037
1038 if let Some((new_x, new_y)) = new_packer.pack(w_px, h_px) {
1039 encoder.copy_texture_to_texture(
1040 wgpu::TexelCopyTextureInfo {
1041 texture: &self.mega_heim_tex,
1042 mip_level: 0,
1043 origin: wgpu::Origin3d {
1044 x: old_x_px,
1045 y: old_y_px,
1046 z: 0,
1047 },
1048 aspect: wgpu::TextureAspect::All,
1049 },
1050 wgpu::TexelCopyTextureInfo {
1051 texture: &new_mega_heim_tex,
1052 mip_level: 0,
1053 origin: wgpu::Origin3d {
1054 x: new_x,
1055 y: new_y,
1056 z: 0,
1057 },
1058 aspect: wgpu::TextureAspect::All,
1059 },
1060 wgpu::Extent3d {
1061 width: w_px,
1062 height: h_px,
1063 depth_or_array_layers: 1,
1064 },
1065 );
1066
1067 let new_uv = Rect {
1068 x: new_x as f32 / 4096.0,
1069 y: new_y as f32 / 4096.0,
1070 width: old_uv.width,
1071 height: old_uv.height,
1072 };
1073 self.image_uv_registry.put(name.clone(), new_uv);
1074 }
1075 }
1076 }
1077
1078 let text_entries: Vec<(u64, (Rect, f32, f32, f32, f32))> = self
1079 .text
1080 .glyph_cache
1081 .iter()
1082 .map(|(k, v)| (*k, *v))
1083 .collect();
1084 for (hash, (old_uv, w_f, h_f, x_off, y_off)) in text_entries {
1085 let w_px = (old_uv.width * 4096.0).round() as u32;
1086 let h_px = (old_uv.height * 4096.0).round() as u32;
1087 let old_x_px = (old_uv.x * 4096.0).round() as u32;
1088 let old_y_px = (old_uv.y * 4096.0).round() as u32;
1089
1090 if let Some((new_x, new_y)) = new_packer.pack(w_px, h_px) {
1091 encoder.copy_texture_to_texture(
1092 wgpu::TexelCopyTextureInfo {
1093 texture: &self.mega_heim_tex,
1094 mip_level: 0,
1095 origin: wgpu::Origin3d {
1096 x: old_x_px,
1097 y: old_y_px,
1098 z: 0,
1099 },
1100 aspect: wgpu::TextureAspect::All,
1101 },
1102 wgpu::TexelCopyTextureInfo {
1103 texture: &new_mega_heim_tex,
1104 mip_level: 0,
1105 origin: wgpu::Origin3d {
1106 x: new_x,
1107 y: new_y,
1108 z: 0,
1109 },
1110 aspect: wgpu::TextureAspect::All,
1111 },
1112 wgpu::Extent3d {
1113 width: w_px,
1114 height: h_px,
1115 depth_or_array_layers: 1,
1116 },
1117 );
1118
1119 let new_uv = Rect {
1120 x: new_x as f32 / 4096.0,
1121 y: new_y as f32 / 4096.0,
1122 width: old_uv.width,
1123 height: old_uv.height,
1124 };
1125 self.text
1126 .glyph_cache
1127 .put(hash, (new_uv, w_f, h_f, x_off, y_off));
1128 }
1129 }
1130
1131 self.queue.submit(std::iter::once(encoder.finish()));
1132
1133 self.mega_heim_tex = new_mega_heim_tex;
1134 let mega_heim_view_obj = self
1135 .mega_heim_tex
1136 .create_view(&wgpu::TextureViewDescriptor::default());
1137 self.texture_views[0] = mega_heim_view_obj.clone();
1138
1139 self.rebuild_texture_array_bind_group();
1140
1141 if !self.texture_bind_groups.is_empty() {
1142 self.texture_bind_groups[0] = self.mega_heim_bind_group.clone();
1143 }
1144
1145 self.heim_packer = new_packer;
1146 self.telemetry.vram_exhausted = false;
1147 }
1148}
1149
1150impl Drop for GpuRenderer {
1151 fn drop(&mut self) {
1152 let cache_dir = std::env::current_exe()
1153 .ok()
1154 .and_then(|p| p.parent().map(|d| d.join("pipeline_cache")))
1155 .unwrap_or_else(|| std::env::temp_dir().join("cvkg_pipeline_cache"));
1156 let _ = std::fs::create_dir_all(&cache_dir);
1157 let cache_path = cache_dir.join("cvkg_render_gpu.bin");
1158 if let Some(cache) = &self.pipeline_cache
1159 && let Some(data) = cache.get_data()
1160 && let Err(e) = std::fs::write(&cache_path, data)
1161 {
1162 tracing::warn!("Failed to persist pipeline cache: {}", e);
1163 }
1164
1165 let _ = self.device.poll(wgpu::PollType::Wait {
1166 submission_index: None,
1167 timeout: None,
1168 });
1169 }
1170}
1171
1172impl GpuRenderer {
1173 pub(crate) fn current_width(&self) -> u32 {
1174 if let Some(id) = self.current_window {
1175 self.surfaces.get(&id).map(|s| s.config.width).unwrap_or(1)
1176 } else {
1177 self.headless_context.as_ref().map(|h| h.width).unwrap_or(1)
1178 }
1179 }
1180
1181 pub(crate) fn current_height(&self) -> u32 {
1182 if let Some(id) = self.current_window {
1183 self.surfaces.get(&id).map(|s| s.config.height).unwrap_or(1)
1184 } else {
1185 self.headless_context
1186 .as_ref()
1187 .map(|h| h.height)
1188 .unwrap_or(1)
1189 }
1190 }
1191
1192 pub(crate) fn current_scale_factor(&self) -> f32 {
1193 if let Some(id) = self.current_window {
1194 self.surfaces
1195 .get(&id)
1196 .map(|s| s.scale_factor)
1197 .unwrap_or(1.0)
1198 } else {
1199 self.headless_context
1200 .as_ref()
1201 .map(|h| h.scale_factor)
1202 .unwrap_or(1.0)
1203 }
1204 }
1205
1206 pub(crate) fn current_time(&self) -> f32 {
1207 self.start_time.elapsed().as_secs_f32()
1208 }
1209
1210 pub async fn forge_headless(width: u32, height: u32) -> Self {
1212 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
1213 backends: wgpu::Backends::all(),
1214 flags: wgpu::InstanceFlags::default(),
1215 backend_options: wgpu::BackendOptions::default(),
1216 display: None,
1217 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
1218 });
1219
1220 tracing::info!("[GPU] Requesting HighPerformance adapter (headless)...");
1222 let mut adapter = instance
1223 .request_adapter(&wgpu::RequestAdapterOptions {
1224 power_preference: wgpu::PowerPreference::HighPerformance,
1225 compatible_surface: None,
1226 force_fallback_adapter: false,
1227 })
1228 .await
1229 .ok();
1230
1231 if adapter.is_none() {
1232 tracing::warn!(
1233 "[GPU] HighPerformance adapter failed (possible Bumblebee/Optimus), trying LowPower..."
1234 );
1235 adapter = instance
1236 .request_adapter(&wgpu::RequestAdapterOptions {
1237 power_preference: wgpu::PowerPreference::LowPower,
1238 compatible_surface: None,
1239 force_fallback_adapter: false,
1240 })
1241 .await
1242 .ok();
1243 }
1244
1245 if adapter.is_none() {
1246 tracing::warn!("[GPU] Hardware adapters failed, trying Software fallback...");
1247 adapter = instance
1248 .request_adapter(&wgpu::RequestAdapterOptions {
1249 power_preference: wgpu::PowerPreference::LowPower,
1250 compatible_surface: None,
1251 force_fallback_adapter: true,
1252 })
1253 .await
1254 .ok();
1255 }
1256
1257 let adapter = adapter.expect("Failed to find a suitable GPU for Surtr");
1258 let info = adapter.get_info();
1259 let caps =
1260 crate::subsystems::GpuCapabilities::detect(&info.name, format!("{:?}", info.backend));
1261 tracing::info!(
1262 "[GPU] Selected adapter: {} ({:?}) on backend: {:?} -- detected as {}",
1263 info.name,
1264 info.device_type,
1265 info.backend,
1266 caps.vendor
1267 );
1268 tracing::info!("[GPU] Driver info: {} - {}", info.driver, info.driver_info);
1269 let required_features = adapter.features()
1270 & (wgpu::Features::TIMESTAMP_QUERY
1271 | wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING
1272 | wgpu::Features::TEXTURE_BINDING_ARRAY);
1273
1274 let (device, queue) = adapter
1275 .request_device(&wgpu::DeviceDescriptor {
1276 label: Some("Surtr Headless Forge"),
1277 required_features,
1278 required_limits: wgpu::Limits {
1279 max_bindings_per_bind_group: adapter
1280 .limits()
1281 .max_bindings_per_bind_group
1282 .min(256),
1283 max_binding_array_elements_per_shader_stage: adapter
1284 .limits()
1285 .max_binding_array_elements_per_shader_stage
1286 .min(256),
1287 ..wgpu::Limits::default()
1288 },
1289 memory_hints: wgpu::MemoryHints::default(),
1290 experimental_features: wgpu::ExperimentalFeatures::disabled(),
1291 trace: wgpu::Trace::Off,
1292 })
1293 .await
1294 .expect("Failed to create Surtr device");
1295
1296 let instance = Arc::new(instance);
1297 let adapter = Arc::new(adapter);
1298
1299 device.on_uncaptured_error(Arc::new(|error| {
1300 tracing::error!(
1301 "[GPU] Uncaptured device error (Device Lost or Panic): {:?}",
1302 error
1303 );
1304 }));
1305
1306 let device = Arc::new(device);
1307 let queue = Arc::new(queue);
1308
1309 Self::forge_internal(
1310 instance,
1311 adapter,
1312 device,
1313 queue,
1314 None,
1315 Some((width, height, wgpu::TextureFormat::Rgba8UnormSrgb)),
1316 )
1317 .await
1318 }
1319
1320 pub fn readback_headless_rgba8(&self) -> Vec<u8> {
1323 let ctx = self
1324 .headless_context
1325 .as_ref()
1326 .expect("readback_headless_rgba8 requires a headless renderer");
1327
1328 let width = ctx.width;
1329 let height = ctx.height;
1330 let row_bytes = width * 4;
1331 let padded_row_bytes = ((row_bytes + 255) / 256) * 256;
1332 let buffer_size = (padded_row_bytes * height) as u64;
1333
1334 let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
1335 label: Some("headless-readback-buffer"),
1336 size: buffer_size,
1337 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1338 mapped_at_creation: false,
1339 });
1340
1341 let mut encoder = self
1342 .device
1343 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1344 label: Some("headless-readback-encoder"),
1345 });
1346
1347 encoder.copy_texture_to_buffer(
1348 wgpu::TexelCopyTextureInfo {
1349 texture: &ctx.output_texture,
1350 mip_level: 0,
1351 origin: wgpu::Origin3d::ZERO,
1352 aspect: wgpu::TextureAspect::All,
1353 },
1354 wgpu::TexelCopyBufferInfo {
1355 buffer: &output_buffer,
1356 layout: wgpu::TexelCopyBufferLayout {
1357 offset: 0,
1358 bytes_per_row: Some(padded_row_bytes),
1359 rows_per_image: Some(height),
1360 },
1361 },
1362 wgpu::Extent3d {
1363 width,
1364 height,
1365 depth_or_array_layers: 1,
1366 },
1367 );
1368
1369 self.queue.submit(std::iter::once(encoder.finish()));
1370 let buffer_slice = output_buffer.slice(..);
1371 buffer_slice.map_async(wgpu::MapMode::Read, |_| {});
1372 let _ = self.device.poll(wgpu::PollType::Wait {
1373 submission_index: None,
1374 timeout: None,
1375 });
1376
1377 let data = buffer_slice.get_mapped_range();
1378 let mut result = Vec::with_capacity((width * height * 4) as usize);
1379 for row in 0..height {
1380 let start = (row * padded_row_bytes) as usize;
1381 let end = start + row_bytes as usize;
1382 result.extend_from_slice(&data[start..end]);
1383 }
1384 drop(data);
1385 output_buffer.unmap();
1386 output_buffer.destroy();
1387 result
1388 }
1389
1390 pub fn render_headless_frame<F>(&mut self, draw: F) -> Vec<u8>
1392 where
1393 F: FnOnce(&mut Self),
1394 {
1395 let encoder = self.begin_frame_headless();
1396 draw(self);
1397 self.end_frame(encoder);
1398 self.readback_headless_rgba8()
1399 }
1400
1401 pub async fn from_external(
1408 device: Arc<wgpu::Device>,
1409 queue: Arc<wgpu::Queue>,
1410 surface: wgpu::Surface<'static>,
1411 width: u32,
1412 height: u32,
1413 ) -> Self {
1414 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
1415 backends: wgpu::Backends::all(),
1416 flags: wgpu::InstanceFlags::default(),
1417 backend_options: wgpu::BackendOptions::default(),
1418 display: None,
1419 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
1420 });
1421
1422 let adapter = instance
1423 .request_adapter(&wgpu::RequestAdapterOptions {
1424 power_preference: wgpu::PowerPreference::default(),
1425 compatible_surface: Some(&surface),
1426 force_fallback_adapter: false,
1427 })
1428 .await
1429 .expect("No compatible adapter found");
1430
1431 Self::forge_internal(
1432 Arc::new(instance),
1433 Arc::new(adapter),
1434 device,
1435 queue,
1436 None,
1437 Some((width, height, wgpu::TextureFormat::Rgba8UnormSrgb)),
1438 )
1439 .await
1440 }
1441
1442 pub fn set_world_space_panels(&mut self, panels: Vec<(u64, cvkg_vdom::WorldSpacePanel)>) {
1443 self.world_space_panels = panels;
1444 }
1445
1446 pub fn begin_world_space_panel(
1447 &mut self,
1448 node_id: u64,
1449 transform: &cvkg_core::Transform3D,
1450 glass: Option<cvkg_materials::GlassMaterial>,
1451 pixels_per_unit: f32,
1452 world_size: (f32, f32),
1453 ) {
1454 if let Some(prev) = self.current_panel_id {
1455 self.panel_stack.push(prev);
1456 }
1457 self.current_panel_id = Some(node_id);
1458
1459 let panel = cvkg_vdom::WorldSpacePanel {
1460 transform: transform.clone(),
1461 glass,
1462 pixels_per_unit,
1463 world_size,
1464 };
1465 if !self.world_space_panels.iter().any(|(id, _)| *id == node_id) {
1467 self.world_space_panels.push((node_id, panel));
1468 }
1469 }
1470
1471 pub fn end_world_space_panel(&mut self, _node_id: u64) {
1472 self.current_panel_id = self.panel_stack.pop();
1473 }
1474}