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