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